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); +});