Skip to content

Commit 2d6af56

Browse files
committed
refactor(logger): remove redundant #constructorArgs private field
Replaced the private #constructorArgs field with exclusive use of the privateConstructorArgs WeakMap. This eliminates redundant storage while maintaining compatibility with both regular private methods and dynamically created console methods. Changes: - Removed #constructorArgs private field declaration - Updated constructor to only store args in WeakMap - Modified #getConsole() to read from WeakMap - Updated stderr/stdout getters to read from WeakMap - Fixed writeRaw() to read from WeakMap - Added comprehensive JSDoc explaining why WeakMap is required The WeakMap is necessary because ES private fields cannot be accessed from dynamically created functions (Logger adds console methods to prototype at runtime). Both storage mechanisms were functionally equivalent, but the WeakMap serves both use cases.
1 parent c7cfa90 commit 2d6af56

1 file changed

Lines changed: 38 additions & 15 deletions

File tree

src/logger.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,27 @@ const consolePropAttributes = {
214214
configurable: true,
215215
}
216216
const maxIndentation = 1000
217+
218+
/**
219+
* WeakMap storing the Console instance for each Logger.
220+
*
221+
* Console creation is lazy - deferred until first logging method call.
222+
* This allows logger to be imported during early Node.js bootstrap before
223+
* stdout is ready, avoiding ERR_CONSOLE_WRITABLE_STREAM errors.
224+
*/
217225
const privateConsole = new WeakMap()
226+
227+
/**
228+
* WeakMap storing constructor arguments for lazy Console initialization.
229+
*
230+
* WeakMap is required instead of a private field (#constructorArgs) because:
231+
* 1. Private fields can't be accessed from dynamically created functions
232+
* 2. Logger adds console methods dynamically to its prototype (lines 1560+)
233+
* 3. These dynamic methods need constructor args for lazy initialization
234+
* 4. WeakMap allows both regular methods and dynamic functions to access args
235+
*
236+
* The args are deleted from the WeakMap after Console is created (memory cleanup).
237+
*/
218238
const privateConstructorArgs = new WeakMap()
219239

220240
const consoleSymbols = Object.getOwnPropertySymbols(globalConsole)
@@ -319,7 +339,6 @@ export class Logger {
319339
#stderrLastWasBlank = false
320340
#stdoutLastWasBlank = false
321341
#logCallCount = 0
322-
#constructorArgs: unknown[]
323342
#options: Record<string, unknown>
324343
#originalStdout?: any
325344

@@ -345,8 +364,7 @@ export class Logger {
345364
* ```
346365
*/
347366
constructor(...args: unknown[]) {
348-
// Store constructor args for child loggers
349-
this.#constructorArgs = args
367+
// Store constructor args for lazy Console initialization.
350368
privateConstructorArgs.set(this, args)
351369

352370
// Store options if provided (for future extensibility)
@@ -377,8 +395,9 @@ export class Logger {
377395
let con = privateConsole.get(this)
378396
if (!con) {
379397
// Lazy initialization - create Console on first use.
380-
if (this.#constructorArgs.length) {
381-
con = constructConsole(...this.#constructorArgs)
398+
const ctorArgs = privateConstructorArgs.get(this) ?? []
399+
if (ctorArgs.length) {
400+
con = constructConsole(...ctorArgs)
382401
} else {
383402
// Create a new console that acts like the builtin one so that it will
384403
// work with Node's --frozen-intrinsics flag.
@@ -420,8 +439,9 @@ export class Logger {
420439
*/
421440
get stderr(): Logger {
422441
if (!this.#stderrLogger) {
423-
// Pass parent's constructor args to maintain config
424-
const instance = new Logger(...this.#constructorArgs)
442+
// Pass parent's constructor args to maintain config.
443+
const ctorArgs = privateConstructorArgs.get(this) ?? []
444+
const instance = new Logger(...ctorArgs)
425445
instance.#parent = this
426446
instance.#boundStream = 'stderr'
427447
instance.#options = { __proto__: null, ...this.#options }
@@ -453,8 +473,9 @@ export class Logger {
453473
*/
454474
get stdout(): Logger {
455475
if (!this.#stdoutLogger) {
456-
// Pass parent's constructor args to maintain config
457-
const instance = new Logger(...this.#constructorArgs)
476+
// Pass parent's constructor args to maintain config.
477+
const ctorArgs = privateConstructorArgs.get(this) ?? []
478+
const instance = new Logger(...ctorArgs)
458479
instance.#parent = this
459480
instance.#boundStream = 'stdout'
460481
instance.#options = { __proto__: null, ...this.#options }
@@ -1447,9 +1468,10 @@ export class Logger {
14471468
// 1. Use stored reference from constructor options
14481469
// 2. Try to get from constructor args
14491470
// 3. Fall back to con._stdout (which applies formatting)
1471+
const ctorArgs = privateConstructorArgs.get(this) ?? []
14501472
const stdout =
14511473
this.#originalStdout ||
1452-
(this.#constructorArgs[0] as any)?.stdout ||
1474+
(ctorArgs[0] as any)?.stdout ||
14531475
con._stdout
14541476
stdout.write(text)
14551477
this[lastWasBlankSymbol](false)
@@ -1564,9 +1586,11 @@ Object.defineProperties(
15641586
if (con === undefined) {
15651587
// Lazy initialization - this will only happen if someone calls a
15661588
// dynamically added console method before any core logger method.
1567-
const constructorArgs = privateConstructorArgs.get(this) || []
1568-
if (constructorArgs.length) {
1569-
con = constructConsole(...constructorArgs)
1589+
const ctorArgs = privateConstructorArgs.get(this) ?? []
1590+
// Clean up constructor args - no longer needed after Console creation.
1591+
privateConstructorArgs.delete(this)
1592+
if (ctorArgs.length) {
1593+
con = constructConsole(...ctorArgs)
15701594
} else {
15711595
con = constructConsole({
15721596
stdout: process.stdout,
@@ -1577,8 +1601,7 @@ Object.defineProperties(
15771601
}
15781602
}
15791603
privateConsole.set(this, con)
1580-
// Clean up constructor args - no longer needed after Console creation.
1581-
privateConstructorArgs.delete(this)
1604+
15821605
}
15831606
const result = (con as any)[key](...args)
15841607
return result === undefined || result === con ? this : result

0 commit comments

Comments
 (0)