Skip to content

Commit 11d6d1d

Browse files
committed
fix(logger): defer all console access for early bootstrap compatibility
Fixes issue where accessing global console properties/symbols at module load time causes ERR_CONSOLE_WRITABLE_STREAM errors in Node.js internal bootstrap contexts before stdout is initialized. Changes: - Defer Object.getOwnPropertySymbols(console) until first logger use - Defer kGroupIndentationWidth symbol lookup - Defer Object.entries(console) and prototype initialization - Call ensurePrototypeInitialized() in #getConsole() method This ensures the logger can be safely imported during early Node.js bootstrap (e.g., lib/internal/bootstrap/*.js) without triggering premature Console initialization.
1 parent e4cf4f9 commit 11d6d1d

1 file changed

Lines changed: 102 additions & 68 deletions

File tree

src/logger.ts

Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,20 @@ const privateConsole = new WeakMap()
237237
*/
238238
const privateConstructorArgs = new WeakMap()
239239

240-
const consoleSymbols = Object.getOwnPropertySymbols(globalConsole)
240+
/**
241+
* Lazily get console symbols on first access.
242+
*
243+
* Deferred to avoid accessing global console during early Node.js bootstrap
244+
* before stdout is ready.
245+
* @private
246+
*/
247+
let _consoleSymbols: symbol[] | undefined
248+
function getConsoleSymbols(): symbol[] {
249+
if (_consoleSymbols === undefined) {
250+
_consoleSymbols = Object.getOwnPropertySymbols(globalConsole)
251+
}
252+
return _consoleSymbols
253+
}
241254

242255
/**
243256
* Symbol for incrementing the internal log call counter.
@@ -247,9 +260,19 @@ const consoleSymbols = Object.getOwnPropertySymbols(globalConsole)
247260
*/
248261
export const incLogCallCountSymbol = Symbol.for('logger.logCallCount++')
249262

250-
const kGroupIndentationWidthSymbol =
251-
consoleSymbols.find(s => (s as any).label === 'kGroupIndentWidth') ??
252-
Symbol('kGroupIndentWidth')
263+
/**
264+
* Lazily get kGroupIndentationWidth symbol on first access.
265+
* @private
266+
*/
267+
let _kGroupIndentationWidthSymbol: symbol | undefined
268+
function getKGroupIndentationWidthSymbol(): symbol {
269+
if (_kGroupIndentationWidthSymbol === undefined) {
270+
_kGroupIndentationWidthSymbol =
271+
getConsoleSymbols().find(s => (s as any).label === 'kGroupIndentWidth') ??
272+
Symbol('kGroupIndentWidth')
273+
}
274+
return _kGroupIndentationWidthSymbol
275+
}
253276

254277
/**
255278
* Symbol for tracking whether the last logged line was blank.
@@ -392,6 +415,9 @@ export class Logger {
392415
* @private
393416
*/
394417
#getConsole(): typeof console & Record<string, unknown> {
418+
// Ensure prototype is initialized before creating Console.
419+
ensurePrototypeInitialized()
420+
395421
let con = privateConsole.get(this)
396422
if (!con) {
397423
// Lazy initialization - create Console on first use.
@@ -971,7 +997,7 @@ export class Logger {
971997
if (length) {
972998
ReflectApply(this.log, this, label)
973999
}
974-
this.indent((this as any)[kGroupIndentationWidthSymbol])
1000+
this.indent((this as any)[getKGroupIndentationWidthSymbol()])
9751001
if (length) {
9761002
;(this as any)[lastWasBlankSymbol](false)
9771003
;(this as any)[incLogCallCountSymbol]()
@@ -1017,7 +1043,7 @@ export class Logger {
10171043
* ```
10181044
*/
10191045
groupEnd() {
1020-
this.dedent((this as any)[kGroupIndentationWidthSymbol])
1046+
this.dedent((this as any)[getKGroupIndentationWidthSymbol()])
10211047
return this
10221048
}
10231049

@@ -1560,71 +1586,79 @@ export class Logger {
15601586
}
15611587
}
15621588

1563-
Object.defineProperties(
1564-
Logger.prototype,
1565-
Object.fromEntries(
1566-
(() => {
1567-
const entries: Array<[string | symbol, PropertyDescriptor]> = [
1568-
[
1569-
kGroupIndentationWidthSymbol,
1570-
{
1571-
...consolePropAttributes,
1572-
value: 2,
1573-
},
1574-
],
1575-
[
1576-
Symbol.toStringTag,
1577-
{
1578-
__proto__: null,
1579-
configurable: true,
1580-
value: 'logger',
1581-
} as PropertyDescriptor,
1582-
],
1583-
]
1584-
for (const { 0: key, 1: value } of Object.entries(globalConsole)) {
1585-
if (!(Logger.prototype as any)[key] && typeof value === 'function') {
1586-
// Dynamically name the log method without using Object.defineProperty.
1587-
const { [key]: func } = {
1588-
[key](this: Logger, ...args: unknown[]) {
1589-
// Access Console via WeakMap directly since private methods can't be
1590-
// called from dynamically created functions.
1591-
let con = privateConsole.get(this)
1592-
if (con === undefined) {
1593-
// Lazy initialization - this will only happen if someone calls a
1594-
// dynamically added console method before any core logger method.
1595-
const ctorArgs = privateConstructorArgs.get(this) ?? []
1596-
// Clean up constructor args - no longer needed after Console creation.
1597-
privateConstructorArgs.delete(this)
1598-
if (ctorArgs.length) {
1599-
con = constructConsole(...ctorArgs)
1600-
} else {
1601-
con = constructConsole({
1602-
stdout: process.stdout,
1603-
stderr: process.stderr,
1604-
}) as typeof console & Record<string, unknown>
1605-
for (const { 0: k, 1: method } of boundConsoleEntries) {
1606-
con[k] = method
1607-
}
1608-
}
1609-
privateConsole.set(this, con)
1589+
/**
1590+
* Lazily add dynamic console methods to Logger prototype.
1591+
*
1592+
* This is deferred until first access to avoid calling Object.entries(globalConsole)
1593+
* during early Node.js bootstrap before stdout is ready.
1594+
* @private
1595+
*/
1596+
let _prototypeInitialized = false
1597+
function ensurePrototypeInitialized() {
1598+
if (_prototypeInitialized) {
1599+
return
1600+
}
1601+
_prototypeInitialized = true
1602+
1603+
const entries: Array<[string | symbol, PropertyDescriptor]> = [
1604+
[
1605+
getKGroupIndentationWidthSymbol(),
1606+
{
1607+
...consolePropAttributes,
1608+
value: 2,
1609+
},
1610+
],
1611+
[
1612+
Symbol.toStringTag,
1613+
{
1614+
__proto__: null,
1615+
configurable: true,
1616+
value: 'logger',
1617+
} as PropertyDescriptor,
1618+
],
1619+
]
1620+
for (const { 0: key, 1: value } of Object.entries(globalConsole)) {
1621+
if (!(Logger.prototype as any)[key] && typeof value === 'function') {
1622+
// Dynamically name the log method without using Object.defineProperty.
1623+
const { [key]: func } = {
1624+
[key](this: Logger, ...args: unknown[]) {
1625+
// Access Console via WeakMap directly since private methods can't be
1626+
// called from dynamically created functions.
1627+
let con = privateConsole.get(this)
1628+
if (con === undefined) {
1629+
// Lazy initialization - this will only happen if someone calls a
1630+
// dynamically added console method before any core logger method.
1631+
const ctorArgs = privateConstructorArgs.get(this) ?? []
1632+
// Clean up constructor args - no longer needed after Console creation.
1633+
privateConstructorArgs.delete(this)
1634+
if (ctorArgs.length) {
1635+
con = constructConsole(...ctorArgs)
1636+
} else {
1637+
con = constructConsole({
1638+
stdout: process.stdout,
1639+
stderr: process.stderr,
1640+
}) as typeof console & Record<string, unknown>
1641+
for (const { 0: k, 1: method } of boundConsoleEntries) {
1642+
con[k] = method
16101643
}
1611-
const result = (con as any)[key](...args)
1612-
return result === undefined || result === con ? this : result
1613-
},
1644+
}
1645+
privateConsole.set(this, con)
16141646
}
1615-
entries.push([
1616-
key,
1617-
{
1618-
...consolePropAttributes,
1619-
value: func,
1620-
},
1621-
])
1622-
}
1647+
const result = (con as any)[key](...args)
1648+
return result === undefined || result === con ? this : result
1649+
},
16231650
}
1624-
return entries
1625-
})(),
1626-
),
1627-
)
1651+
entries.push([
1652+
key,
1653+
{
1654+
...consolePropAttributes,
1655+
value: func,
1656+
},
1657+
])
1658+
}
1659+
}
1660+
Object.defineProperties(Logger.prototype, Object.fromEntries(entries))
1661+
}
16281662

16291663
/**
16301664
* Default logger instance for the application.

0 commit comments

Comments
 (0)