diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 895848fcd305..6c95b66594d4 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -425,7 +425,10 @@ fn message_with_type_and_level_( // SAFETY: no other borrow of the console is live in this // early-return arm (the deferred `_indent_guard` only holds the raw // pointer, not a reference). - let ew = unsafe { vm_console_mut(global) }.error_writer(); + let this = unsafe { vm_console_mut(global) }; + let default_indent = this.default_indent; + let ew = this.error_writer(); + let _ = formatter::write_indent_n(u32::from(default_indent), ew); let _ = ew.write_all(text.as_bytes()); let _ = ew.flush(); return Ok(()); @@ -445,10 +448,7 @@ fn message_with_type_and_level_( let default_indent = unsafe { vm_console_mut(global) }.default_indent; // SAFETY: see [`vm_console`] — `console` points at the live boxed - // `ConsoleObject` for this VM; JS-thread-only. Kept as a raw deref (not - // `vm_console_mut`) so the resulting `writer` borrow does not pin a - // long-lived `&mut ConsoleObject` across the re-derive in the empty-`Log` - // arm below. + // `ConsoleObject` for this VM; JS-thread-only. let raw_writer: &mut bun_core::io::Writer = unsafe { if matches!(level, MessageLevel::Warning | MessageLevel::Error) { (*console).error_writer() @@ -541,13 +541,11 @@ fn message_with_type_and_level_( print_options, )?; } else if message_type == MessageType::Log { - // SAFETY: see [`vm_console`]. `writer` (above) is dead in this arm — - // the only later uses are in the mutually-exclusive `Trace` block, and - // `message_type == Log` here. - let w = unsafe { (*console).writer() }; - let _ = w.write_all(b"\n"); - let _ = w.flush(); + let _ = formatter::write_indent_n(u32::from(default_indent), writer); + let _ = writer.write_all(b"\n"); + let _ = writer.flush(); } else if message_type != MessageType::Trace { + let _ = formatter::write_indent_n(u32::from(default_indent), writer); let _ = writer.write_all(b"undefined\n"); } @@ -2800,7 +2798,10 @@ pub mod formatter { /// conflict with the `&self` borrow `Formatter::write_indent` takes. /// `self.indent` is a disjoint field read, so passing it by value here /// keeps the borrow checker happy. - fn write_indent_n(indent: u32, writer: &mut dyn bun_io::Write) -> bun_io::Result<()> { + pub(super) fn write_indent_n( + indent: u32, + writer: &mut dyn bun_io::Write, + ) -> bun_io::Result<()> { let mut total_remain: u32 = indent; while total_remain > 0 { let written: u8 = total_remain.min(32) as u8; @@ -5785,7 +5786,9 @@ pub(crate) extern "C" fn Bun__ConsoleObject__count( } + 1; *counter.value_ptr = current; + let default_indent = this.default_indent; let writer = this.writer(); + let _ = formatter::write_indent_n(u32::from(default_indent), writer); if Output::enable_ansi_colors_stdout() { let _ = writeln!( writer, @@ -5856,7 +5859,7 @@ pub(crate) extern "C" fn Bun__ConsoleObject__time( #[crate::host_call] pub(crate) extern "C" fn Bun__ConsoleObject__timeEnd( _console: *mut ConsoleObject, - _global: &JSGlobalObject, + global_this: &JSGlobalObject, chars: *const u8, len: usize, ) { @@ -5873,6 +5876,10 @@ pub(crate) extern "C" fn Bun__ConsoleObject__timeEnd( return; }; let Some(value) = prev else { return }; + // SAFETY: top-level JS-thread host call ⇒ exclusive access to the + // set-once `VirtualMachine.console` box. + let default_indent = unsafe { vm_console_mut(global_this) }.default_indent; + let _ = formatter::write_indent_n(u32::from(default_indent), Output::error_writer()); // get the duration in microseconds, then display it in milliseconds Output::print_elapsed( (value.read() / bun_core::time::NS_PER_US) as f64 / bun_core::time::US_PER_MS as f64, @@ -5905,6 +5912,10 @@ pub(crate) extern "C" fn Bun__ConsoleObject__timeLog( let Some(Some(value)) = PENDING_TIME_LOGS.with_borrow(|m| m.get(&id).copied()) else { return; }; + // SAFETY: top-level JS-thread host call ⇒ exclusive access to the + // set-once `VirtualMachine.console` box. + let default_indent = unsafe { vm_console_mut(global) }.default_indent; + let _ = formatter::write_indent_n(u32::from(default_indent), Output::error_writer()); // get the duration in microseconds, then display it in milliseconds Output::print_elapsed( (value.read() / bun_core::time::NS_PER_US) as f64 / bun_core::time::US_PER_MS as f64, diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index 4356ae993262..b4d7004adc77 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -143,6 +143,63 @@ NamedError: console.error a named error `); }); +it.concurrent("console.group indents count/time/assert and the no-args log paths", async () => { + const src = ` + console.group("G1"); + console.count("cnt"); + console.count("cnt"); + console.log(); + console.info(); + console.warn(); + console.error(); + console.group("G2"); + console.count("cnt"); + console.assert(false); + console.time("t"); + console.timeLog("t"); + console.timeEnd("t"); + console.groupEnd(); + console.count("cnt"); + console.groupEnd(); + console.count("cnt"); + console.log(); + console.assert(false); + console.time("u"); + console.timeEnd("u"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout.replaceAll("\r\n", "\n")).toBe( + "G1\n" + // + " cnt: 1\n" + + " cnt: 2\n" + + " \n" + + " \n" + + " G2\n" + + " cnt: 3\n" + + " cnt: 4\n" + + "cnt: 5\n" + + "\n", + ); + + const stderrLines = stderr.replaceAll("\r\n", "\n").replace(/\n$/, "").split("\n"); + expect(stderrLines.length).toBe(7); + expect(stderrLines[0]).toBe(" "); + expect(stderrLines[1]).toBe(" "); + expect(stderrLines[2]).toBe(" Assertion failed"); + expect(stderrLines[3]).toMatch(/^ \[[\d.]+[mnµ]?s\] t$/); + expect(stderrLines[4]).toMatch(/^ \[[\d.]+[mnµ]?s\] t$/); + expect(stderrLines[5]).toBe("Assertion failed"); + expect(stderrLines[6]).toMatch(/^\[[\d.]+[mnµ]?s\] u$/); + expect(exitCode).toBe(0); +}); + it("console.log with SharedArrayBuffer", () => { // console.log(x) === Bun.inspect(x) + "\n" written to stdout. expect(Bun.inspect(new ArrayBuffer(0))).toBe("ArrayBuffer(0) []");