@@ -215,6 +215,7 @@ const consolePropAttributes = {
215215}
216216const maxIndentation = 1000
217217const privateConsole = new WeakMap ( )
218+ const privateConstructorArgs = new WeakMap ( )
218219
219220const consoleSymbols = Object . getOwnPropertySymbols ( globalConsole )
220221
@@ -346,6 +347,7 @@ export class Logger {
346347 constructor ( ...args : unknown [ ] ) {
347348 // Store constructor args for child loggers
348349 this . #constructorArgs = args
350+ privateConstructorArgs . set ( this , args )
349351
350352 // Store options if provided (for future extensibility)
351353 const options = args [ '0' ]
@@ -357,20 +359,40 @@ export class Logger {
357359 this . #options = { __proto__ : null }
358360 }
359361
360- if ( args . length ) {
361- privateConsole . set ( this , constructConsole ( ...args ) )
362- } else {
363- // Create a new console that acts like the builtin one so that it will
364- // work with Node's --frozen-intrinsics flag.
365- const con = constructConsole ( {
366- stdout : process . stdout ,
367- stderr : process . stderr ,
368- } ) as typeof console & Record < string , unknown >
369- for ( const { 0 : key , 1 : method } of boundConsoleEntries ) {
370- con [ key ] = method
362+ // Note: Console initialization is now lazy (happens on first use).
363+ // This allows logger to be imported during early bootstrap before
364+ // stdout is ready, avoiding ERR_CONSOLE_WRITABLE_STREAM errors.
365+ }
366+
367+ /**
368+ * Get the Console instance for this logger, creating it lazily on first access.
369+ *
370+ * This lazy initialization allows the logger to be imported during early
371+ * Node.js bootstrap before stdout is ready, avoiding Console initialization
372+ * errors (ERR_CONSOLE_WRITABLE_STREAM).
373+ *
374+ * @private
375+ */
376+ #getConsole( ) : typeof console & Record < string , unknown > {
377+ let con = privateConsole . get ( this )
378+ if ( ! con ) {
379+ // Lazy initialization - create Console on first use.
380+ if ( this . #constructorArgs. length ) {
381+ con = constructConsole ( ...this . #constructorArgs)
382+ } else {
383+ // Create a new console that acts like the builtin one so that it will
384+ // work with Node's --frozen-intrinsics flag.
385+ con = constructConsole ( {
386+ stdout : process . stdout ,
387+ stderr : process . stderr ,
388+ } ) as typeof console & Record < string , unknown >
389+ for ( const { 0 : key , 1 : method } of boundConsoleEntries ) {
390+ con [ key ] = method
391+ }
371392 }
372393 privateConsole . set ( this , con )
373394 }
395+ return con
374396 }
375397
376398 /**
@@ -510,8 +532,7 @@ export class Logger {
510532 args : unknown [ ] ,
511533 stream ?: 'stderr' | 'stdout' ,
512534 ) : this {
513- const con = privateConsole . get ( this ) as typeof console &
514- Record < string , unknown >
535+ const con = this . #getConsole( )
515536 const text = args . at ( 0 )
516537 const hasText = typeof text === 'string'
517538 // Determine which stream this method writes to
@@ -547,7 +568,7 @@ export class Logger {
547568 * @private
548569 */
549570 #symbolApply( symbolType : string , args : unknown [ ] ) : this {
550- const con = privateConsole . get ( this )
571+ const con = this . #getConsole ( )
551572 let text = args . at ( 0 )
552573 // biome-ignore lint/suspicious/noImplicitAnyLet: Flexible argument handling.
553574 let extras
@@ -649,7 +670,7 @@ export class Logger {
649670 * ```
650671 */
651672 assert ( value : unknown , ...message : unknown [ ] ) : this {
652- const con = privateConsole . get ( this )
673+ const con = this . #getConsole ( )
653674 con . assert ( value , ...message )
654675 this [ lastWasBlankSymbol ] ( false )
655676 return value ? this : this [ incLogCallCountSymbol ] ( )
@@ -680,7 +701,7 @@ export class Logger {
680701 'clearVisible() is only available on the main logger instance, not on stream-bound instances' ,
681702 )
682703 }
683- const con = privateConsole . get ( this )
704+ const con = this . #getConsole ( )
684705 con . clear ( )
685706 if ( ( con as any ) . _stdout . isTTY ) {
686707 ; ( this as any ) [ lastWasBlankSymbol ] ( true )
@@ -707,7 +728,7 @@ export class Logger {
707728 * ```
708729 */
709730 count ( label ?: string | undefined ) : this {
710- const con = privateConsole . get ( this )
731+ const con = this . #getConsole ( )
711732 con . count ( label )
712733 this [ lastWasBlankSymbol ] ( false )
713734 return this [ incLogCallCountSymbol ] ( )
@@ -809,7 +830,7 @@ export class Logger {
809830 * ```
810831 */
811832 dir ( obj : unknown , options ?: unknown | undefined ) : this {
812- const con = privateConsole . get ( this )
833+ const con = this . #getConsole ( )
813834 con . dir ( obj , options )
814835 this [ lastWasBlankSymbol ] ( false )
815836 return this [ incLogCallCountSymbol ] ( )
@@ -830,7 +851,7 @@ export class Logger {
830851 * ```
831852 */
832853 dirxml ( ...data : unknown [ ] ) : this {
833- const con = privateConsole . get ( this )
854+ const con = this . #getConsole ( )
834855 con . dirxml ( data )
835856 this [ lastWasBlankSymbol ] ( false )
836857 return this [ incLogCallCountSymbol ] ( )
@@ -1159,7 +1180,7 @@ export class Logger {
11591180 const text = this . #stripSymbols( msg )
11601181 // Note: Step messages always go to stdout (unlike info/fail/etc which go to stderr).
11611182 const indent = this . #getIndent( 'stdout' )
1162- const con = privateConsole . get ( this ) as typeof console &
1183+ const con = this . #getConsole ( ) as typeof console &
11631184 Record < string , unknown >
11641185 con . log (
11651186 applyLinePrefix ( `${ LOG_SYMBOLS . step } ${ text } ` , {
@@ -1281,7 +1302,7 @@ export class Logger {
12811302 tabularData : unknown ,
12821303 properties ?: readonly string [ ] | undefined ,
12831304 ) : this {
1284- const con = privateConsole . get ( this )
1305+ const con = this . #getConsole ( )
12851306 con . table ( tabularData , properties )
12861307 this [ lastWasBlankSymbol ] ( false )
12871308 return this [ incLogCallCountSymbol ] ( )
@@ -1311,7 +1332,7 @@ export class Logger {
13111332 * ```
13121333 */
13131334 timeEnd ( label ?: string | undefined ) : this {
1314- const con = privateConsole . get ( this )
1335+ const con = this . #getConsole ( )
13151336 con . timeEnd ( label )
13161337 this [ lastWasBlankSymbol ] ( false )
13171338 return this [ incLogCallCountSymbol ] ( )
@@ -1342,7 +1363,7 @@ export class Logger {
13421363 * ```
13431364 */
13441365 timeLog ( label ?: string | undefined , ...data : unknown [ ] ) : this {
1345- const con = privateConsole . get ( this )
1366+ const con = this . #getConsole ( )
13461367 con . timeLog ( label , ...data )
13471368 this [ lastWasBlankSymbol ] ( false )
13481369 return this [ incLogCallCountSymbol ] ( )
@@ -1369,7 +1390,7 @@ export class Logger {
13691390 * ```
13701391 */
13711392 trace ( message ?: unknown | undefined , ...args : unknown [ ] ) : this {
1372- const con = privateConsole . get ( this )
1393+ const con = this . #getConsole ( )
13731394 con . trace ( message , ...args )
13741395 this [ lastWasBlankSymbol ] ( false )
13751396 return this [ incLogCallCountSymbol ] ( )
@@ -1418,7 +1439,7 @@ export class Logger {
14181439 * ```
14191440 */
14201441 write ( text : string ) : this {
1421- const con = privateConsole . get ( this )
1442+ const con = this . #getConsole ( )
14221443 // Write directly to the original stdout stream to bypass Console formatting
14231444 // (e.g., group indentation). Try multiple approaches to get the raw stream:
14241445 // 1. Use stored reference from constructor options
@@ -1459,7 +1480,7 @@ export class Logger {
14591480 * ```
14601481 */
14611482 progress ( text : string ) : this {
1462- const con = privateConsole . get ( this )
1483+ const con = this . #getConsole ( )
14631484 const stream = this . #getTargetStream( )
14641485 const streamObj = stream === 'stderr' ? con . _stderr : con . _stdout
14651486 streamObj . write ( `∴ ${ text } ` )
@@ -1496,7 +1517,7 @@ export class Logger {
14961517 * ```
14971518 */
14981519 clearLine ( ) : this {
1499- const con = privateConsole . get ( this )
1520+ const con = this . #getConsole ( )
15001521 const stream = this . #getTargetStream( )
15011522 const streamObj = stream === 'stderr' ? con . _stderr : con . _stdout
15021523 if ( streamObj . isTTY ) {
@@ -1534,8 +1555,27 @@ Object.defineProperties(
15341555 if ( ! ( Logger . prototype as any ) [ key ] && typeof value === 'function' ) {
15351556 // Dynamically name the log method without using Object.defineProperty.
15361557 const { [ key ] : func } = {
1537- [ key ] ( ...args : unknown [ ] ) {
1538- const con = privateConsole . get ( this )
1558+ [ key ] ( this : Logger , ...args : unknown [ ] ) {
1559+ // Access Console via WeakMap directly since private methods can't be
1560+ // called from dynamically created functions.
1561+ let con = privateConsole . get ( this )
1562+ if ( ! con ) {
1563+ // Lazy initialization - this will only happen if someone calls a
1564+ // dynamically added console method before any core logger method.
1565+ const constructorArgs = privateConstructorArgs . get ( this ) || [ ]
1566+ if ( constructorArgs . length ) {
1567+ con = constructConsole ( ...constructorArgs )
1568+ } else {
1569+ con = constructConsole ( {
1570+ stdout : process . stdout ,
1571+ stderr : process . stderr ,
1572+ } ) as typeof console & Record < string , unknown >
1573+ for ( const { 0 : k , 1 : method } of boundConsoleEntries ) {
1574+ con [ k ] = method
1575+ }
1576+ }
1577+ privateConsole . set ( this , con )
1578+ }
15391579 const result = ( con as any ) [ key ] ( ...args )
15401580 return result === undefined || result === con ? this : result
15411581 } ,
0 commit comments