Skip to content

Commit 0f74945

Browse files
robobunClaude Botclaude
authored
fix(console): implement %j format specifier for JSON output (#25195)
## 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 <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 9fd6b54 commit 0f74945

File tree

3 files changed

+85
-1
lines changed

3 files changed

+85
-1
lines changed

src/bun.js/ConsoleObject.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,7 @@ pub const Formatter = struct {
14251425
o, // o
14261426
O, // O
14271427
c, // c
1428+
j, // j
14281429
};
14291430

14301431
fn writeWithFormatting(
@@ -1466,6 +1467,7 @@ pub const Formatter = struct {
14661467
'O' => .O,
14671468
'd', 'i' => .i,
14681469
'c' => .c,
1470+
'j' => .j,
14691471
'%' => {
14701472
// print up to and including the first %
14711473
const end = slice[0..i];
@@ -1625,6 +1627,16 @@ pub const Formatter = struct {
16251627
.c => {
16261628
// TODO: Implement %c
16271629
},
1630+
1631+
.j => {
1632+
// JSON.stringify the value
1633+
var str = bun.String.empty;
1634+
defer str.deref();
1635+
1636+
try next_value.jsonStringify(global, 0, &str);
1637+
this.addForNewLine(str.length());
1638+
writer.print("{f}", .{str});
1639+
},
16281640
}
16291641
if (this.remaining_values.len == 0) break;
16301642
},

test/js/web/console/console-log.expected.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ Hello World 123
236236
Hello %vWorld 123
237237
Hello NaN %i
238238
Hello NaN % 1
239-
Hello NaN %j 1
239+
Hello NaN 1
240240
Hello \5 6,
241241
Hello %i 5 6
242242
%d 1
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from "bun:test";
2+
import { bunEnv, bunExe } from "harness";
3+
4+
test("console.log with %j should format as JSON", async () => {
5+
await using proc = Bun.spawn({
6+
cmd: [bunExe(), "-e", "console.log('%j', {foo: 'bar'})"],
7+
env: bunEnv,
8+
stderr: "pipe",
9+
});
10+
11+
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
12+
13+
expect(stderr).toBe("");
14+
expect(stdout).toBe('{"foo":"bar"}\n');
15+
expect(exitCode).toBe(0);
16+
});
17+
18+
test("console.log with %j should handle arrays", async () => {
19+
await using proc = Bun.spawn({
20+
cmd: [bunExe(), "-e", "console.log('%j', [1, 2, 3])"],
21+
env: bunEnv,
22+
stderr: "pipe",
23+
});
24+
25+
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
26+
27+
expect(stderr).toBe("");
28+
expect(stdout).toBe("[1,2,3]\n");
29+
expect(exitCode).toBe(0);
30+
});
31+
32+
test("console.log with %j should handle nested objects", async () => {
33+
await using proc = Bun.spawn({
34+
cmd: [bunExe(), "-e", "console.log('%j', {a: {b: {c: 123}}})"],
35+
env: bunEnv,
36+
stderr: "pipe",
37+
});
38+
39+
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
40+
41+
expect(stderr).toBe("");
42+
expect(stdout).toBe('{"a":{"b":{"c":123}}}\n');
43+
expect(exitCode).toBe(0);
44+
});
45+
46+
test("console.log with %j should handle primitives", async () => {
47+
await using proc = Bun.spawn({
48+
cmd: [bunExe(), "-e", "console.log('%j %j %j %j', 'string', 123, true, null)"],
49+
env: bunEnv,
50+
stderr: "pipe",
51+
});
52+
53+
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
54+
55+
expect(stderr).toBe("");
56+
expect(stdout).toBe('"string" 123 true null\n');
57+
expect(exitCode).toBe(0);
58+
});
59+
60+
test("console.log with %j and additional text", async () => {
61+
await using proc = Bun.spawn({
62+
cmd: [bunExe(), "-e", "console.log('Result: %j', {status: 'ok'})"],
63+
env: bunEnv,
64+
stderr: "pipe",
65+
});
66+
67+
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68+
69+
expect(stderr).toBe("");
70+
expect(stdout).toBe('Result: {"status":"ok"}\n');
71+
expect(exitCode).toBe(0);
72+
});

0 commit comments

Comments
 (0)