From 0f7494569e81a411ba64d11fcc503e3939e28fad Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 28 Nov 2025 22:57:55 -0800 Subject: [PATCH] fix(console): implement %j format specifier for JSON output (#25195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Implements the `%j` format specifier for `console.log` and related console methods - `%j` outputs the JSON stringified representation of the value - Previously, `%j` was not recognized and was left as literal text in the output ## Test plan - [x] Run `bun bd test test/regression/issue/24234.test.ts` - all 5 tests pass - [x] Verify tests fail with system Bun (`USE_SYSTEM_BUN=1`) to confirm fix validity - [x] Manual verification: `console.log('%j', {foo: 'bar'})` outputs `{"foo":"bar"}` ## Example Before (bug): ``` $ bun -e "console.log('%j %s', {foo: 'bar'}, 'hello')" %j [object Object] hello ``` After (fixed): ``` $ bun -e "console.log('%j %s', {foo: 'bar'}, 'hello')" {"foo":"bar"} hello ``` Closes #24234 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude --- src/bun.js/ConsoleObject.zig | 12 ++++ test/js/web/console/console-log.expected.txt | 2 +- test/regression/issue/24234.test.ts | 72 ++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/24234.test.ts diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 20e1de9898..b9a24edf18 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1425,6 +1425,7 @@ pub const Formatter = struct { o, // o O, // O c, // c + j, // j }; fn writeWithFormatting( @@ -1466,6 +1467,7 @@ pub const Formatter = struct { 'O' => .O, 'd', 'i' => .i, 'c' => .c, + 'j' => .j, '%' => { // print up to and including the first % const end = slice[0..i]; @@ -1625,6 +1627,16 @@ pub const Formatter = struct { .c => { // TODO: Implement %c }, + + .j => { + // JSON.stringify the value + var str = bun.String.empty; + defer str.deref(); + + try next_value.jsonStringify(global, 0, &str); + this.addForNewLine(str.length()); + writer.print("{f}", .{str}); + }, } if (this.remaining_values.len == 0) break; }, diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 882b76de63..9a6cdf569c 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -236,7 +236,7 @@ Hello World 123 Hello %vWorld 123 Hello NaN %i Hello NaN % 1 -Hello NaN %j 1 +Hello NaN 1 Hello \5 6, Hello %i 5 6 %d 1 diff --git a/test/regression/issue/24234.test.ts b/test/regression/issue/24234.test.ts new file mode 100644 index 0000000000..5bf23a5d21 --- /dev/null +++ b/test/regression/issue/24234.test.ts @@ -0,0 +1,72 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +test("console.log with %j should format as JSON", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log('%j', {foo: 'bar'})"], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toBe('{"foo":"bar"}\n'); + expect(exitCode).toBe(0); +}); + +test("console.log with %j should handle arrays", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log('%j', [1, 2, 3])"], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toBe("[1,2,3]\n"); + expect(exitCode).toBe(0); +}); + +test("console.log with %j should handle nested objects", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log('%j', {a: {b: {c: 123}}})"], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toBe('{"a":{"b":{"c":123}}}\n'); + expect(exitCode).toBe(0); +}); + +test("console.log with %j should handle primitives", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log('%j %j %j %j', 'string', 123, true, null)"], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toBe('"string" 123 true null\n'); + expect(exitCode).toBe(0); +}); + +test("console.log with %j and additional text", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "console.log('Result: %j', {status: 'ok'})"], + env: bunEnv, + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toBe('Result: {"status":"ok"}\n'); + expect(exitCode).toBe(0); +});