console-log-json normally writes its final formatted output directly to process.stdout.write(...) when that API exists. That is the correct behavior in real Node.js because it bypasses patched console.log(...) wrappers and writes the final JSON string straight to stdout.
In the code.esm.sh playground, the runtime is browser-like but also exposes a compatibility process.stdout.write(...) shim. That shim is not real Node stdout. It feeds output back into the playground's console plumbing.
On top of that, browser-style hosts can also expose a saved console.log(...) implementation that is not a hard boundary. Even after console-log-json captures the "original" console method, calling it can still feed back into the patched console path.
When LoggerAdaptToConsole() patches console.log(...), the following loop can happen:
- Application calls
console.log(...) console-log-jsonformats the logwriteOutput(...)calls eitherprocess.stdout.write(...)or the saved console implementation- The host routes that write back through console handling
- The patched
console.log(...)is hit again - The cycle repeats until
Maximum call stack size exceeded
The direct stdout path must remain intact in real Node.js. Changing that globally would risk breaking the existing contract for:
- direct JSON writes to stdout
- environments that depend on stdout semantics instead of console semantics
- tests and integrations that intentionally intercept
process.stdout.write(...)
Because of that, the fix is intentionally limited to browser-like DOM hosts.
writeOutput(...) now avoids process.stdout.write(...) when a real DOM-style document is present. In that case it falls back to the saved native console method instead.
That by itself was not enough for code.esm.sh, because the saved console method can still feed back into patched console handling.
The current fix therefore adds a second narrow safeguard:
- while
console-log-jsonis emitting its own final output, any immediate re-entrant call back into the patched console methods is ignored - captured console methods are invoked with the
consolereceiver, instead of as detached bare functions
This preserves the existing behavior in real Node.js while preventing browser/playground feedback loops from recursively re-logging the logger's own output.
This change does not try to redefine runtime detection for every hybrid environment.
It deliberately does not:
- remove stdout writes in real Node.js
- change the public API
- rely on fragile heuristics such as function source inspection
- guess about every possible shim implementation
The fix is covered by regression tests for:
- a Node-side simulation of a browser-like host with a fake
process.stdout.write(...) - a real browser bundle test that injects a fake
process.stdout.write(...)and verifies it is ignored - a Node-side simulation where the saved console implementation immediately feeds back into patched
console.log(...) - a real browser bundle test that simulates the same saved-console feedback loop
Those tests also verify the opposite case: plain Node-like execution still uses stdout.