From abc5a163f2b8fbc9e071d05d7553c4129d65121d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:18:27 +0000 Subject: [PATCH] console: route console.trace() output to stderr console.trace() was writing to stdout because the stream selection in message_with_type_and_level only checked MessageLevel::Warning/Error and JSC dispatches Trace with MessageLevel::Log. Node.js writes console.trace() to stderr. Add MessageType::Trace to the use_stderr condition and reuse that flag for the color and writer selection so the three stay in sync. Fixes #19952 Co-authored-by: MOHAMMED HANAN M T P <91409429+hanu-14@users.noreply.github.com> --- src/jsc/ConsoleObject.rs | 7 ++-- test/js/web/console/console-trace.test.ts | 43 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 test/js/web/console/console-trace.test.ts diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 895848fcd305..e542a3f47e42 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -408,7 +408,8 @@ fn message_with_type_and_level_( // Lock/unlock a mutex incase two JS threads are console.log'ing at the same // time. We do this the slightly annoying way to avoid assigning a pointer. let use_stderr = matches!(level, MessageLevel::Warning | MessageLevel::Error) - || message_type == MessageType::Assert; + || message_type == MessageType::Assert + || message_type == MessageType::Trace; let _stream_lock = ConsoleStreamLock::acquire(use_stderr); if message_type == MessageType::Clear { @@ -431,7 +432,7 @@ fn message_with_type_and_level_( return Ok(()); } - let enable_colors = if matches!(level, MessageLevel::Warning | MessageLevel::Error) { + let enable_colors = if use_stderr { Output::enable_ansi_colors_stderr() } else { Output::enable_ansi_colors_stdout() @@ -450,7 +451,7 @@ fn message_with_type_and_level_( // long-lived `&mut ConsoleObject` across the re-derive in the empty-`Log` // arm below. let raw_writer: &mut bun_core::io::Writer = unsafe { - if matches!(level, MessageLevel::Warning | MessageLevel::Error) { + if use_stderr { (*console).error_writer() } else { (*console).writer() diff --git a/test/js/web/console/console-trace.test.ts b/test/js/web/console/console-trace.test.ts new file mode 100644 index 000000000000..12aab47694c4 --- /dev/null +++ b/test/js/web/console/console-trace.test.ts @@ -0,0 +1,43 @@ +import { expect, it } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// https://github.com/oven-sh/bun/issues/19952 +it.concurrent("console.trace() writes to stderr, not stdout", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `console.trace("marker");`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe(""); + expect(stderr).toContain("marker"); + expect(stderr).toContain("at "); + expect(exitCode).toBe(0); +}); + +it.concurrent("console.trace() with no arguments writes the stack to stderr", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `console.trace();`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe(""); + expect(stderr).toContain("at "); + expect(exitCode).toBe(0); +}); + +it.concurrent("console.trace() does not interleave with console.log() on stdout", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `console.log("before"); console.trace("traced"); console.log("after");`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe("before\nafter\n"); + expect(stderr).toContain("traced"); + expect(exitCode).toBe(0); +});