Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
edf99ab33d Add %j (JSON stringify) support to console formatter
Implements the %j format specifier in ConsoleObject.zig to match Node.js
behavior. The %j formatter calls JSON.stringify on the argument.

Behavior:
- Strings, numbers, objects, arrays: JSON stringified normally
- undefined: prints "undefined" (matching Node.js)
- Circular references: prints "[Circular]" (matching Node.js)
- Other JSON stringify errors: prints "[Circular]"

Fixes #19992

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 01:12:55 +00:00
2 changed files with 169 additions and 0 deletions

View File

@@ -1409,6 +1409,7 @@ pub const Formatter = struct {
o, // o
O, // O
c, // c
j, // j (JSON.stringify)
};
fn writeWithFormatting(
@@ -1450,6 +1451,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];
@@ -1609,6 +1611,35 @@ pub const Formatter = struct {
.c => {
// TODO: Implement %c
},
.j => {
// JSON.stringify the value
// If circular or other error, print [Circular] like Node.js does
var str = bun.String.empty;
defer str.deref();
next_value.jsonStringify(global, 0, &str) catch |err| {
// Clear any exception that occurred
if (global.hasException()) {
_ = global.takeException(err);
}
// For circular references and other JSON stringify errors,
// print [Circular] to match Node.js behavior
this.addForNewLine("[Circular]".len);
writer.writeAll("[Circular]");
continue;
};
// JSON.stringify(undefined) produces empty string, but Node prints "undefined"
if (str.isEmpty()) {
this.addForNewLine("undefined".len);
writer.writeAll("undefined");
} else {
this.addForNewLine(str.length());
writer.print("{}", .{str});
}
},
}
if (this.remaining_values.len == 0) break;
},

View File

@@ -0,0 +1,138 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe } from "harness";
describe("console.log %j formatter", () => {
it("should format strings with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", "abc")`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe('"abc"\n');
});
it("should format numbers with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", 123)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("123\n");
});
it("should format objects with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", { a: 1, b: 2 })`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe('{"a":1,"b":2}\n');
});
it("should format arrays with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", [1, 2, 3])`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("[1,2,3]\n");
});
it("should format null with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", null)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("null\n");
});
it("should format undefined with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", undefined)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
// JSON.stringify(undefined) returns undefined
// Node.js prints "undefined" for undefined values
expect(proc.stdout.toString("utf8")).toBe("undefined\n");
});
it("should handle circular references with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `const a = {}; a.self = a; console.log("%j", a)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("[Circular]\n");
});
it("should format multiple values with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j %j", "abc", 123)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe('"abc" 123\n');
});
it("should append remaining args after format string", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", "abc", "extra")`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe('"abc" extra\n');
});
it("should handle %% escape sequence", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%%j", "abc")`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("%j abc\n");
});
it("should format booleans with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j %j", true, false)`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe("true false\n");
});
it("should format nested objects with %j", () => {
const proc = Bun.spawnSync({
cmd: [bunExe(), "-e", `console.log("%j", { a: { b: { c: 1 } } })`],
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
expect(proc.stderr.toString("utf8")).toBeEmpty();
expect(proc.stdout.toString("utf8")).toBe('{"a":{"b":{"c":1}}}\n');
});
});