Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,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 {
Expand All @@ -477,7 +478,9 @@ fn message_with_type_and_level_(
return Ok(());
}

let enable_colors = if matches!(level, MessageLevel::Warning | MessageLevel::Error) {
let enable_colors = if matches!(level, MessageLevel::Warning | MessageLevel::Error)
|| message_type == MessageType::Trace
{
Output::enable_ansi_colors_stderr()
} else {
Output::enable_ansi_colors_stdout()
Expand All @@ -496,7 +499,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 matches!(level, MessageLevel::Warning | MessageLevel::Error) || message_type == MessageType::Trace {
(*console).error_writer()
} else {
(*console).writer()
Expand Down Expand Up @@ -578,6 +581,19 @@ fn message_with_type_and_level_(
}
}

// console.trace prints "Trace: <message>" (or just "Trace" with no args),
// matching Node, with the stack trace appended below. The label goes through
// FormatOptions so it lands after the console.group indent rather than
// before it.
if message_type == MessageType::Trace {
if print_length > 0 {
print_options.prefix = b"Trace: ";
} else {
let _ = formatter::write_indent_n(u32::from(default_indent), writer);
let _ = writer.write_all(b"Trace\n");
}
}

if print_length > 0 {
format2(
level,
Expand Down Expand Up @@ -1298,6 +1314,9 @@ pub struct FormatOptions {
pub single_line: bool,
pub default_indent: u16,
pub error_display_level: ErrorDisplayLevel,
/// Label emitted immediately after the indent, before the formatted values
/// (used by `console.trace` for its "Trace: " prefix).
pub prefix: &'static [u8],
}

impl Default for FormatOptions {
Expand All @@ -1312,6 +1331,7 @@ impl Default for FormatOptions {
single_line: false,
default_indent: 0,
error_display_level: ErrorDisplayLevel::Full,
prefix: b"",
}
}
}
Expand Down Expand Up @@ -1484,6 +1504,7 @@ pub fn format2(
if fmt.write_indent(writer).is_err() {
return Ok(());
}
let _ = writer.write_all(options.prefix);

if matches!(tag.tag, TagPayload::String) {
if options.enable_colors {
Expand Down Expand Up @@ -1548,6 +1569,7 @@ pub fn format2(
if fmt.write_indent(writer).is_err() {
return Ok(());
}
let _ = writer.write_all(options.prefix);

let mut any = false;
if options.enable_colors {
Expand Down
50 changes: 50 additions & 0 deletions test/js/node/console/console.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { Console } from "node:console";
import { bunEnv, bunExe } from "harness";

import { Writable } from "node:stream";

Expand Down Expand Up @@ -88,3 +89,52 @@ test("console._stderr", () => {
configurable: true,
});
});

// console.trace writes to stderr with a "Trace:" prefix, matching Node.
// https://github.com/oven-sh/bun/issues/19952
describe("console.trace", () => {
async function run(code: string) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", code],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}

test("goes to stderr, not stdout", async () => {
const { stdout, stderr, exitCode } = await run(`console.trace("hi")`);
expect(stdout).toBe("");
expect(stderr).toStartWith("Trace: hi\n");
expect(stderr).toContain("at ");
expect(exitCode).toBe(0);
});

test("no arguments prints bare 'Trace'", async () => {
const { stdout, stderr, exitCode } = await run(`console.trace()`);
expect(stdout).toBe("");
expect(stderr).toStartWith("Trace\n");
expect(stderr).toContain("at ");
expect(exitCode).toBe(0);
});

test("applies format specifiers", async () => {
const { stdout, stderr, exitCode } = await run(`console.trace("x=%d", 5)`);
expect(stdout).toBe("");
expect(stderr).toStartWith("Trace: x=5\n");
expect(exitCode).toBe(0);
});

test("label goes after the console.group indent", async () => {
const { stdout, stderr, exitCode } = await run(
`console.group("G"); console.trace("x"); console.trace(); console.groupEnd(); console.trace("top");`,
);
// The group indent precedes the label, and the bare header is indented too.
const headers = stderr.match(/^(?: Trace: x| Trace|Trace: top)$/gm);
expect(headers).toEqual([" Trace: x", " Trace", "Trace: top"]);
expect(stdout).toBe("G\n");
expect(exitCode).toBe(0);
});
});