From 3bba4e144646300a2a5614e98d5f1a5fea5a8fd0 Mon Sep 17 00:00:00 2001 From: jarred-sumner-bot Date: Mon, 14 Jul 2025 04:58:41 -0700 Subject: [PATCH] Add --console-depth CLI flag and console.depth bunfig option (#21016) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: jarred-sumner-bot <220441119+jarred-sumner-bot@users.noreply.github.com> Co-authored-by: Jarred Sumner --- docs/api/console.md | 19 ++ docs/cli/run.md | 17 ++ docs/runtime/bunfig.md | 15 ++ src/bun.js/ConsoleObject.zig | 15 ++ src/bunfig.zig | 11 + src/cli.zig | 1 + src/cli/Arguments.zig | 9 + test/cli/console-depth.test.ts | 309 ++++++++++++++++++++++++ test/js/web/console/console-log.test.ts | 25 +- 9 files changed, 410 insertions(+), 11 deletions(-) create mode 100644 test/cli/console-depth.test.ts diff --git a/docs/api/console.md b/docs/api/console.md index 49b8a1b797..9f65d489f9 100644 --- a/docs/api/console.md +++ b/docs/api/console.md @@ -2,6 +2,25 @@ **Note** — Bun provides a browser- and Node.js-compatible [console](https://developer.mozilla.org/en-US/docs/Web/API/console) global. This page only documents Bun-native APIs. {% /callout %} +## Object inspection depth + +Bun allows you to configure how deeply nested objects are displayed in `console.log()` output: + +- **CLI flag**: Use `--console-depth ` to set the depth for a single run +- **Configuration**: Set `console.depth` in your `bunfig.toml` for persistent configuration +- **Default**: Objects are inspected to a depth of `2` levels + +```js +const nested = { a: { b: { c: { d: "deep" } } } }; +console.log(nested); +// Default (depth 2): { a: { b: [Object] } } +// With depth 4: { a: { b: { c: { d: 'deep' } } } } +``` + +The CLI flag takes precedence over the configuration file setting. + +## Reading from stdin + In Bun, the `console` object can be used as an `AsyncIterable` to sequentially read lines from `process.stdin`. ```ts diff --git a/docs/cli/run.md b/docs/cli/run.md index 3899bac555..80a00b2f93 100644 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -185,6 +185,23 @@ This is TypeScript! For convenience, all code is treated as TypeScript with JSX support when using `bun run -`. +## `bun run --console-depth` + +Control the depth of object inspection in console output with the `--console-depth` flag. + +```bash +$ bun --console-depth 5 run index.tsx +``` + +This sets how deeply nested objects are displayed in `console.log()` output. The default depth is `2`. Higher values show more nested properties but may produce verbose output for complex objects. + +```js +const nested = { a: { b: { c: { d: "deep" } } } }; +console.log(nested); +// With --console-depth 2 (default): { a: { b: [Object] } } +// With --console-depth 4: { a: { b: { c: { d: 'deep' } } } } +``` + ## `bun run --smol` In memory-constrained environments, use the `--smol` flag to reduce memory usage at a cost to performance. diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 3324c2c183..96a5252550 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -108,6 +108,21 @@ The `telemetry` field permit to enable/disable the analytics records. Bun record telemetry = false ``` +### `console` + +Configure console output behavior. + +#### `console.depth` + +Set the default depth for `console.log()` object inspection. Default `2`. + +```toml +[console] +depth = 3 +``` + +This controls how deeply nested objects are displayed in console output. Higher values show more nested properties but may produce verbose output for complex objects. This setting can be overridden by the `--console-depth` CLI flag. + ## Test runner The test runner is configured under the `[test]` section of your bunfig.toml. diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index a7a5357463..0db081579a 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -18,8 +18,13 @@ const Environment = bun.Environment; const default_allocator = bun.default_allocator; const JestPrettyFormat = @import("./test/pretty_format.zig").JestPrettyFormat; const JSPromise = JSC.JSPromise; +const CLI = @import("../cli.zig").Command; const EventType = JSC.EventType; +/// Default depth for console.log object inspection +/// Only --console-depth CLI flag and console.depth bunfig option should modify this +const DEFAULT_CONSOLE_LOG_DEPTH: u16 = 2; + const Counter = std.AutoHashMapUnmanaged(u64, u32); const BufferedWriter = std.io.BufferedWriter(4096, Output.WriterType); @@ -168,11 +173,16 @@ fn messageWithTypeAndLevel_( const Writer = @TypeOf(writer); var print_length = len; + // Get console depth from CLI options or bunfig, fallback to default + const cli_context = CLI.get(); + const console_depth = cli_context.runtime_options.console_depth orelse DEFAULT_CONSOLE_LOG_DEPTH; + var print_options: FormatOptions = .{ .enable_colors = enable_colors, .add_newline = true, .flush = true, .default_indent = console.default_indent, + .max_depth = console_depth, .error_display_level = switch (level) { .Error => .full, else => .normal, @@ -912,6 +922,7 @@ pub fn format2( .globalThis = global, .ordered_properties = options.ordered_properties, .quote_strings = options.quote_strings, + .max_depth = options.max_depth, .single_line = options.single_line, .indent = options.default_indent, .stack_check = bun.StackCheck.init(), @@ -3632,6 +3643,10 @@ pub fn timeLog( .globalThis = global, .ordered_properties = false, .quote_strings = false, + .max_depth = blk: { + const cli_context = CLI.get(); + break :blk cli_context.runtime_options.console_depth orelse DEFAULT_CONSOLE_LOG_DEPTH; + }, .stack_check = bun.StackCheck.init(), .can_throw_stack_overflow = true, }; diff --git a/src/bunfig.zig b/src/bunfig.zig index 40cc8ec7a0..2bf6f27765 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -629,6 +629,17 @@ pub const Bunfig = struct { } } } + + if (json.get("console")) |console_expr| { + if (console_expr.get("depth")) |depth| { + if (depth.data == .e_number) { + const depth_value = @as(u16, @intFromFloat(depth.data.e_number.value)); + this.ctx.runtime_options.console_depth = depth_value; + } else { + try this.addError(depth.loc, "Expected number"); + } + } + } } if (json.getObject("serve")) |serve_obj2| { diff --git a/src/cli.zig b/src/cli.zig index 96c3f2d4b1..85c5199069 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -388,6 +388,7 @@ pub const Command = struct { /// compatibility. expose_gc: bool = false, preserve_symlinks_main: bool = false, + console_depth: ?u16 = null, }; var global_cli_ctx: Context = undefined; diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 55eb512c7f..03ec522e31 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -108,6 +108,7 @@ pub const runtime_params_ = [_]ParamType{ clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable, clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable, clap.parseParam("--unhandled-rejections One of \"strict\", \"throw\", \"warn\", \"none\", or \"warn-with-error-code\"") catch unreachable, + clap.parseParam("--console-depth Set the default depth for console.log object inspection (default: 2)") catch unreachable, }; pub const auto_or_run_params = [_]ParamType{ @@ -668,6 +669,14 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C ctx.runtime_options.preconnect = args.options("--fetch-preconnect"); ctx.runtime_options.expose_gc = args.flag("--expose-gc"); + if (args.option("--console-depth")) |depth_str| { + const depth = std.fmt.parseInt(u16, depth_str, 10) catch { + Output.errGeneric("Invalid value for --console-depth: \"{s}\". Must be a positive integer\n", .{depth_str}); + Global.exit(1); + }; + ctx.runtime_options.console_depth = depth; + } + if (args.option("--dns-result-order")) |order| { ctx.runtime_options.dns_result_order = order; } diff --git a/test/cli/console-depth.test.ts b/test/cli/console-depth.test.ts new file mode 100644 index 0000000000..1f21ef9beb --- /dev/null +++ b/test/cli/console-depth.test.ts @@ -0,0 +1,309 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +describe("console depth", () => { + const deepObject = { + level1: { + level2: { + level3: { + level4: { + level5: { + level6: { + level7: { + level8: { + level9: { + level10: "deep value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const testScript = `console.log(${JSON.stringify(deepObject)});`; + + function normalizeOutput(output: string): string { + // Normalize line endings and trim whitespace + return output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim(); + } + + test("default console depth should be 2", async () => { + const dir = tempDirWithFiles("console-depth-default", { + "test.js": testScript, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: { + level2: { + level3: [Object ...], + }, + }, +}" +`); + }); + + test("--console-depth flag sets custom depth", async () => { + const dir = tempDirWithFiles("console-depth-cli", { + "test.js": testScript, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "3", "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: { + level2: { + level3: { + level4: [Object ...], + }, + }, + }, +}" +`); + }); + + test("--console-depth with higher value shows deeper nesting", async () => { + const dir = tempDirWithFiles("console-depth-high", { + "test.js": testScript, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "10", "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: { + level2: { + level3: { + level4: { + level5: { + level6: { + level7: { + level8: { + level9: { + level10: \"deep value\", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}" +`); + }); + + test("bunfig.toml console.depth configuration", async () => { + const dir = tempDirWithFiles("console-depth-bunfig", { + "test.js": testScript, + "bunfig.toml": `[console]\ndepth = 4`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: { + level2: { + level3: { + level4: { + level5: [Object ...], + }, + }, + }, + }, +}" +`); + }); + + test("CLI flag overrides bunfig.toml", async () => { + const dir = tempDirWithFiles("console-depth-override", { + "test.js": testScript, + "bunfig.toml": `[console]\ndepth = 6`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "2", "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: { + level2: { + level3: [Object ...], + }, + }, +}" +`); + }); + + test("invalid --console-depth value shows error", async () => { + const dir = tempDirWithFiles("console-depth-invalid", { + "test.js": testScript, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "invalid", "test.js"], + env: bunEnv, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(1); + const allOutput = normalizeOutput(stdout + stderr); + expect(allOutput).toMatchInlineSnapshot( + `"error: Invalid value for --console-depth: \"invalid\". Must be a positive integer"`, + ); + }); + + test("edge case: depth 0 should show only top level structure", async () => { + const dir = tempDirWithFiles("console-depth-zero", { + "test.js": testScript, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "0", "test.js"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + expect(normalizeOutput(stdout)).toMatchInlineSnapshot(` +"{ + level1: [Object ...], +}" +`); + }); + + test("console depth affects console.log, console.error, and console.warn", async () => { + const testScriptMultiple = ` + const obj = ${JSON.stringify(deepObject)}; + console.log("LOG:", obj); + console.error("ERROR:", obj); + console.warn("WARN:", obj); + `; + + const dir = tempDirWithFiles("console-depth-multiple", { + "test.js": testScriptMultiple, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--console-depth", "2", "test.js"], + env: bunEnv, + cwd: dir, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(normalizeOutput(stdout + stderr)).toMatchInlineSnapshot(` +"LOG: { + level1: { + level2: { + level3: [Object ...], + }, + }, +} +ERROR: { + level1: { + level2: { + level3: [Object ...], + }, + }, +} +WARN: { + level1: { + level2: { + level3: [Object ...], + }, + }, +}" +`); + }); +}); diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index bc6ae0c6ac..4b0d92824b 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -76,7 +76,10 @@ it("console.group", async () => { .replaceAll("\r\n", "\n") .replaceAll("\\", "/") .trim() - .replaceAll(filepath, ""); + .replaceAll(filepath, "") + // Normalize line numbers for consistency between debug and release builds + .replace(/\(\d+:\d+\)/g, "(N:NN)") + .replace(/:\d+:\d+/g, ":NN:NN"); expect(stdout).toMatchInlineSnapshot(` "Basic group Inside basic group @@ -118,8 +121,8 @@ Quote"Backslash expect(stderr).toMatchInlineSnapshot(` "Warning log warn: console.warn an error - at :56:14 - at loadAndEvaluateModule (2:1) + at :NN:NN + at loadAndEvaluateModule (N:NN) 52 | console.group("Different logs"); 53 | console.log("Regular log"); @@ -129,8 +132,8 @@ Quote"Backslash 57 | console.error(new Error("console.error an error")); ^ error: console.error an error - at :57:15 - at loadAndEvaluateModule (2:1) + at :NN:NN + at loadAndEvaluateModule (N:NN) 41 | console.groupEnd(); // Extra 42 | console.groupEnd(); // Extra @@ -140,14 +143,14 @@ error: console.error an error 46 | super(message); ^ NamedError: console.error a named error - at new NamedError (:46:5) - at :58:15 - at loadAndEvaluateModule (2:1) + at new NamedError (:NN:NN) + at :NN:NN + at loadAndEvaluateModule (N:NN) NamedError: console.warn a named error - at new NamedError (:46:5) - at :59:14 - at loadAndEvaluateModule (2:1) + at new NamedError (:NN:NN) + at :NN:NN + at loadAndEvaluateModule (N:NN) Error log" `);