diff --git a/docs/runtime/environment-variables.mdx b/docs/runtime/environment-variables.mdx index b2d6c40219b9..9903cb72601b 100644 --- a/docs/runtime/environment-variables.mdx +++ b/docs/runtime/environment-variables.mdx @@ -202,6 +202,7 @@ These environment variables are read by Bun and configure aspects of its behavio | `FORCE_COLOR` | If `FORCE_COLOR=1`, then ANSI color output is force enabled, even if `NO_COLOR` is set. | | `BUN_CONFIG_MAX_HTTP_REQUESTS` | Control the maximum number of concurrent HTTP requests sent by fetch and `bun install`. Defaults to `256`. If you are running into rate limits or connection issues, you can reduce this number. | | `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD` | If `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=true`, then `bun --watch` will not clear the console on reload | +| `BUN_CONFIG_ELIDE_LINES` | Maximum number of lines of script output shown when using `bun run --filter`. Defaults to `10`. The `--elide-lines ` argument takes precedence over this environment variable. Set to `0` to show all lines. | | `DO_NOT_TRACK` | Disable uploading crash reports to `bun.report` on crash. On macOS & Windows, crash report uploads are enabled by default. Otherwise, telemetry is not sent yet as of May 21st, 2024, but we are planning to add telemetry in the coming weeks. If `DO_NOT_TRACK=1`, then auto-uploading crash reports and telemetry are both [disabled](https://do-not-track.dev/). | | `BUN_OPTIONS` | Prepends command-line arguments to any Bun execution. For example, `BUN_OPTIONS="--hot"` makes `bun run dev` behave like `bun --hot run dev` | diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index 9ddfaf7b9a42..ab1d964b28d3 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -55,6 +55,7 @@ new!(pub BUN_CONFIG_DISABLE_ioctl_ficlonerange: boolean, "BUN_CONFIG_DISABLE_ioc // // It's unclear why this was done. new!(pub BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS: unsigned, "BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS", { default: 30 }); +new!(pub BUN_CONFIG_ELIDE_LINES: unsigned, "BUN_CONFIG_ELIDE_LINES", {}); // Idle timeout for HTTP client sockets (fetch / `bun install`), in seconds. // The timer is armed when the socket opens and re-armed on every read/write; // if it fires the request fails with `error.Timeout`. Covers the TLS diff --git a/src/bunfig/bunfig.rs b/src/bunfig/bunfig.rs index 8ed18c0faf5e..cc937b4e6466 100644 --- a/src/bunfig/bunfig.rs +++ b/src/bunfig/bunfig.rs @@ -788,8 +788,17 @@ impl<'a> Parser<'a> { if let Some(elide_lines) = run_expr.get(b"elide-lines") { if let Some(n) = elide_lines.as_number() { - // Note: Rust `as` saturates on overflow/NaN where Zig @intFromFloat is UB - self.ctx.bundler_options.elide_lines = Some(n as usize); + // Reject negative, non-finite, fractional, and + // out-of-range values. A bare `n as usize` would + // silently truncate `0.7` to `0` (disabling elision — + // the opposite of the error-message intent) and + // saturate negatives to `0`. Mirror the CLI flag, + // which rejects these via integer parsing. + if n < 0.0 || !n.is_finite() || n.fract() != 0.0 || n > usize::MAX as f64 { + self.add_error(elide_lines.loc, b"Expected a non-negative integer")?; + } else { + self.ctx.bundler_options.elide_lines = Some(n as usize); + } } else { self.add_error(elide_lines.loc, b"Expected number")?; } diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 0fb898d14539..0b8b445dffd5 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -78,6 +78,31 @@ pub(crate) fn resolve_jsx_runtime(s: &[u8]) -> Result, ctx: Context<'_>) { + let cli_value = args.option(b"--elide-lines").filter(|v| !v.is_empty()); + + if let Some(elide_lines) = cli_value { + ctx.bundler_options.elide_lines = match strings::parse_int::(elide_lines, 10) { + Ok(v) => Some(v), + Err(_) => { + Output::pretty_errorln(format_args!( + "error: Invalid elide-lines: \"{}\"", + BStr::new(elide_lines) + )); + Global::exit(1); + } + }; + } else if let Some(value) = env_var::BUN_CONFIG_ELIDE_LINES.get() { + // `BUN_CONFIG_ELIDE_LINES` is parsed as `u64`; `elide_lines` is a + // `usize`. Clamp rather than truncate, so 32-bit targets don't lose + // the high bits for values above `usize::MAX`. + ctx.bundler_options.elide_lines = Some(usize::try_from(value).unwrap_or(usize::MAX)); + } +} + pub(crate) type ParamType = clap::Param; // ─── param tables ──────────────────────────────────────────────────────────── @@ -810,22 +835,6 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> Result(elide_lines, 10) - { - Ok(v) => Some(v), - Err(_) => { - Output::pretty_errorln(format_args!( - "error: Invalid elide-lines: \"{}\"", - BStr::new(elide_lines) - )); - Global::exit(1); - } - }; - } - } } if cmd == CommandTag::TestCommand { @@ -1377,21 +1386,10 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> Result(elide_lines, 10) - { - Ok(v) => Some(v), - Err(_) => { - Output::pretty_errorln(format_args!( - "error: Invalid elide-lines: \"{}\"", - BStr::new(elide_lines) - )); - Global::exit(1); - } - }; - } - } + // Runs after `load_config_with_cmd_args` so the `--elide-lines` flag + // and `BUN_CONFIG_ELIDE_LINES` env var take precedence over any value + // set by `run.elide-lines` in bunfig.toml. + parse_elide_lines_option(&args, &mut *ctx); if let Some(define) = &opts.define { if !define.keys.is_empty() { diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 038d5e6a19dd..a59afab93ed4 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -474,10 +474,12 @@ describe("bun", () => { elideLines, target_pattern, antipattern, + env = {}, }: { - elideLines: number; + elideLines?: number; target_pattern: RegExp[]; antipattern?: RegExp[]; + env?: Record; }) { const dir = tempDirWithFiles("testworkspace", { packages: { @@ -509,8 +511,15 @@ describe("bun", () => { // code path. const { exitCode, stderr, stdout } = spawnSync({ cwd: dir, - cmd: [bunExe(), "run", "--filter", "./packages/dep0", "--elide-lines", String(elideLines), "script"], - env: { ...bunEnv, FORCE_COLOR: "1", NO_COLOR: "0" }, + cmd: [ + bunExe(), + "run", + "--filter", + "./packages/dep0", + ...(elideLines !== undefined ? ["--elide-lines", String(elideLines)] : []), + "script", + ], + env: { ...bunEnv, ...env, FORCE_COLOR: "1", NO_COLOR: "0" }, stdout: "pipe", stderr: "pipe", }); @@ -530,7 +539,7 @@ describe("bun", () => { runInCwdSuccess({ cwd: dir, pattern: "./packages/dep0", - env: { FORCE_COLOR: "1", NO_COLOR: "0" }, + env: { ...env, FORCE_COLOR: "1", NO_COLOR: "0" }, target_pattern, antipattern, command: ["script"], @@ -590,4 +599,87 @@ describe("bun", () => { expect(stdoutval).toMatch(/(?:log_line[\s\S]*?){20}/); expect(exitCode).toBe(0); }); + + test("respects BUN_CONFIG_ELIDE_LINES environment variable", () => { + runElideLinesTest({ + target_pattern: [/\[3 lines elided\]/, /(?:log_line[\s\S]*?){20}/], + antipattern: [/\[10 lines elided\]/], + env: { BUN_CONFIG_ELIDE_LINES: "17" }, + }); + }); + + test("command-line flag takes precedence over environment variable", () => { + runElideLinesTest({ + elideLines: 12, + target_pattern: [/\[8 lines elided\]/, /(?:log_line[\s\S]*?){20}/], + antipattern: [/\[15 lines elided\]/], + env: { BUN_CONFIG_ELIDE_LINES: "5" }, + }); + }); + + test("empty --elide-lines value falls back to BUN_CONFIG_ELIDE_LINES", () => { + // Mimics `--elide-lines "$MAYBE_UNSET"` expanding to `--elide-lines ""`: + // the empty flag value must not shadow the env var, since neither was + // an explicit user choice. + const dir = tempDirWithFiles("testworkspace", { + packages: { + dep0: { + "index.js": Array(20).fill("console.log('log_line');").join("\n"), + "package.json": JSON.stringify({ + name: "dep0", + scripts: { script: `${bunExe()} run index.js` }, + }), + }, + }, + "package.json": JSON.stringify({ name: "ws", workspaces: ["packages/*"] }), + }); + + const { exitCode, stdout } = spawnSync({ + cwd: dir, + cmd: [bunExe(), "run", "--filter", "./packages/dep0", "--elide-lines", "", "script"], + env: { ...bunEnv, BUN_CONFIG_ELIDE_LINES: "17", FORCE_COLOR: "1", NO_COLOR: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + + if (process.platform !== "win32") { + expect(stdout.toString()).toMatch(/\[3 lines elided\]/); + expect(stdout.toString()).not.toMatch(/\[10 lines elided\]/); + } + expect(exitCode).toBe(0); + }); + + test("run.elide-lines in bunfig.toml rejects fractional values", () => { + const dir = tempDirWithFiles("testworkspace", { + packages: { + dep0: { + "index.js": `console.log('hi');`, + "package.json": JSON.stringify({ + name: "dep0", + scripts: { script: `${bunExe()} run index.js` }, + }), + }, + }, + "package.json": JSON.stringify({ name: "ws", workspaces: ["packages/*"] }), + // `0.7` would otherwise silently `@intFromFloat` to `0`, which + // disables elision entirely — the opposite of what the error + // message suggests. `--config=./bunfig.toml` uses the `=` form + // because `-c` / `--config` is declared as `.one_optional` in + // clap and does not consume a following argv element; the split + // `-c ./bunfig.toml` form would leave the path as a positional + // and dispatch as `AutoCommand`. + "bunfig.toml": `[run]\nelide-lines = 0.7\n`, + }); + + const { exitCode, stderr } = spawnSync({ + cwd: dir, + cmd: [bunExe(), "--config=./bunfig.toml", "run", "--filter", "./packages/dep0", "script"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + expect(stderr.toString()).toMatch(/Expected a non-negative integer/); + expect(exitCode).not.toBe(0); + }); });