From 4a5d48611ee5a9ccaf4377a9879054f3705f8946 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:45:04 +0000 Subject: [PATCH 1/3] cli: make several unhelpful error messages actionable - 'bun install --linker=' now echoes the rejected value - 'bun patch' / 'bun patch --commit' / 'bun patch-commit' with no positional now show a one-line usage example and point at --help - 'bun build --format=' now echoes the rejected value - 'bun build --loader ' and '--define ' (no ':' / '=') now name which flag rejected the token, echo the token, and show an example of the expected shape - 'bun run --filter' now names which workspace package.json it failed to read instead of a bare 'Failed to read package.json' --- .../PackageManager/CommandLineArguments.rs | 23 ++++++-- src/runtime/cli/Arguments.rs | 5 +- src/runtime/cli/colon_list_type.rs | 22 +++++--- src/runtime/cli/filter_run.rs | 5 +- test/bundler/cli.test.ts | 52 +++++++++++++++++ test/cli/install/bun-patch.test.ts | 56 +++++++++++++++++++ test/cli/install/isolated-install.test.ts | 17 ++++++ test/cli/run/filter-workspace.test.ts | 28 +++++++++- 8 files changed, 192 insertions(+), 16 deletions(-) diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index f0fa1bef6f5..95d8ea8fea9 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -1054,8 +1054,8 @@ Full documentation is available at https://bun.com/docs/cli/pm#scan. Some(l) => l, None => { Output::err_generic( - "Expected --linker to be one of 'isolated' or 'hoisted'", - (), + "Invalid value for --linker: {}. Must be 'isolated' or 'hoisted'.", + (bun_core::fmt::quote(linker),), ); Global::exit(1); } @@ -1382,12 +1382,27 @@ Full documentation is available at https://bun.com/docs/cli/pm#scan. } if subcommand == Subcommand::Patch && cli.positionals.len() < 2 { - Output::err_generic("Missing pkg to patch\n", ()); + if let PatchOpts::Commit { .. } = cli.patch { + Output::err_generic( + "Missing path to the package directory containing your changes.\n Usage: bun patch --commit node_modules/\\", + (), + ); + } else { + Output::err_generic( + "Missing package name to patch.\n Usage: bun patch \\[@\\]", + (), + ); + } + bun_core::note!("Run 'bun patch --help' for more information"); Global::crash(); } if subcommand == Subcommand::PatchCommit && cli.positionals.len() < 2 { - Output::err_generic("Missing pkg folder to patch\n", ()); + Output::err_generic( + "Missing path to the package directory containing your changes.\n Usage: bun patch-commit node_modules/\\", + (), + ); + bun_core::note!("Run 'bun patch-commit --help' for more information"); Global::crash(); } diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 7e8e01ffc65..4bd85ae1582 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -2282,7 +2282,10 @@ fn parse_build_command_options( if let Some(format_str) = args.option(b"--format") { let Some(format) = options::Format::from_string(format_str) else { - Output::err_generic("Invalid format - must be esm, cjs, or iife", ()); + Output::err_generic( + "Invalid value for --format: {}. Must be 'esm', 'cjs', or 'iife'.", + (bun_core::fmt::quote(format_str),), + ); Global::crash(); }; diff --git a/src/runtime/cli/colon_list_type.rs b/src/runtime/cli/colon_list_type.rs index cd52f300ed8..2dc5bfacbbb 100644 --- a/src/runtime/cli/colon_list_type.rs +++ b/src/runtime/cli/colon_list_type.rs @@ -38,7 +38,18 @@ impl ColonListType { .unwrap_or(u32::MAX) .min(strings::index_of_char(str, b'=').unwrap_or(u32::MAX)); if midpoint == u32::MAX { - return Err(err!("InvalidSeparator")); + if T::IS_LOADER { + pretty_errorln!( + "error: --loader {} is missing a \":\" separator. Expected --loader .ext:loader, for example --loader .md:text", + bun_fmt::quote(str), + ); + } else { + pretty_errorln!( + "error: --define {} is missing a \":\" or \"=\" separator. Expected --define key=value, for example --define process.env.NODE_ENV='\"production\"'", + bun_fmt::quote(str), + ); + } + Global::exit(1); } let midpoint = midpoint as usize; @@ -72,14 +83,7 @@ impl ColonListType { pub(crate) fn resolve(input: &[&'static [u8]]) -> Result { let mut list = Self::init(input.len()); - match list.load(input) { - Ok(()) => {} - Err(e) if e == err!("InvalidSeparator") => { - pretty_errorln!("error: expected \":\" separator"); - Global::exit(1); - } - Err(e) => return Err(e), - } + list.load(input)?; Ok(list) } } diff --git a/src/runtime/cli/filter_run.rs b/src/runtime/cli/filter_run.rs index 97e554a7ae9..18e817cd05f 100644 --- a/src/runtime/cli/filter_run.rs +++ b/src/runtime/cli/filter_run.rs @@ -765,7 +765,10 @@ pub(crate) fn run_scripts_with_filter( None, IncludeScripts::IncludeScripts, ) else { - bun_core::warn!("Failed to read package.json\n"); + bun_core::warn!( + "Failed to read {}, skipping this workspace package\n", + bun_core::fmt::quote(&*package_json_path), + ); continue; }; let Some(pkgscripts) = &pkgjson.scripts else { diff --git a/test/bundler/cli.test.ts b/test/bundler/cli.test.ts index 342c31ff437..1f7954efc2a 100644 --- a/test/bundler/cli.test.ts +++ b/test/bundler/cli.test.ts @@ -466,3 +466,55 @@ test("multi-entry build writes each entry point into the output directory", asyn expect(a).toContain('"A"'); expect(b).toContain('"B"'); }); + +describe("CLI argument error messages", () => { + test("--format with an unrecognized value echoes the value back", async () => { + using dir = tempDir("build-format-err", { "in.js": "console.log(1)" }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "--format=commonjs", "in.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr }).toEqual({ + stdout: "", + stderr: expect.stringContaining("--format: \"commonjs\""), + }); + expect(stderr).toContain("'esm', 'cjs', or 'iife'"); + expect(exitCode).toBe(1); + }); + + test("--loader without a ':' separator names the flag and the bad token", async () => { + using dir = tempDir("build-loader-err", { "in.js": "console.log(1)" }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "--loader", "text", "in.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("--loader"); + expect(stderr).toContain("\"text\""); + expect(stderr).toContain(".ext:loader"); + expect(exitCode).toBe(1); + }); + + test("--define without a separator names the flag and shows an example", async () => { + using dir = tempDir("build-define-err", { "in.js": "console.log(FOO)" }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "--define", "FOO", "in.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("--define"); + expect(stderr).toContain("\"FOO\""); + expect(stderr).toContain("key=value"); + expect(exitCode).toBe(1); + }); +}); diff --git a/test/cli/install/bun-patch.test.ts b/test/cli/install/bun-patch.test.ts index 79fa09c71d8..21ac94d86ca 100644 --- a/test/cli/install/bun-patch.test.ts +++ b/test/cli/install/bun-patch.test.ts @@ -9,6 +9,62 @@ const platformPath = (path: string) => path; setDefaultTimeout(1000 * 60 * 5); +describe("error messages", () => { + test("'bun patch' with no package name shows a usage example", async () => { + const dir = tempDirWithFiles("bun-patch-noarg", { + "package.json": JSON.stringify({ name: "t" }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "patch"], + env: bunEnv, + cwd: dir, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("Missing package name to patch"); + expect(stderr).toContain("bun patch "); + expect(stderr).toContain("bun patch --help"); + expect(exitCode).toBe(1); + }); + + test("'bun patch --commit' with no directory shows a usage example", async () => { + const dir = tempDirWithFiles("bun-patch-commit-noarg", { + "package.json": JSON.stringify({ name: "t" }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "patch", "--commit"], + env: bunEnv, + cwd: dir, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("Missing path to the package directory"); + expect(stderr).toContain("bun patch --commit node_modules/"); + expect(stderr).toContain("bun patch --help"); + expect(exitCode).toBe(1); + }); + + test("'bun patch-commit' with no directory shows a usage example", async () => { + const dir = tempDirWithFiles("bun-patchcommit-noarg", { + "package.json": JSON.stringify({ name: "t" }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "patch-commit"], + env: bunEnv, + cwd: dir, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("Missing path to the package directory"); + expect(stderr).toContain("bun patch-commit node_modules/"); + expect(stderr).toContain("bun patch-commit --help"); + expect(exitCode).toBe(1); + }); +}); + describe("bun patch ", async () => { describe("workspace interactions", async () => { /** diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index d2565a0451a..cbc609a5153 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -2026,3 +2026,20 @@ test("rejects dependency aliases that traverse outside node_modules", async () = expect(() => lstatSync(join(packageDir, "pwned-by-alias"))).toThrow(); expect(exitCode).not.toBe(0); }); + +test("invalid --linker value is echoed back in the error", async () => { + using dir = tempDir("install-linker-err", { + "package.json": JSON.stringify({ name: "t" }), + }); + await using proc = spawn({ + cmd: [bunExe(), "install", "--linker=isoalted"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("--linker: \"isoalted\""); + expect(stderr).toContain("'isolated' or 'hoisted'"); + expect(exitCode).toBe(1); +}); diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 038d5e6a19d..15a0c01935d 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -436,7 +436,7 @@ describe("bun", () => { runInCwdFailure(cwd_root, "*", "notpresent", /No packages matched/); }); test("should warn about malformed package.json", () => { - runInCwdFailure(cwd_root, "*", "x", /Failed to read package.json/); + runInCwdFailure(cwd_root, "*", "x", /Failed to read .*malformed2.*package\.json/); }); test("nonzero exit code on failure", () => { const dir = tempDirWithFiles("testworkspace", { @@ -590,4 +590,30 @@ describe("bun", () => { expect(stdoutval).toMatch(/(?:log_line[\s\S]*?){20}/); expect(exitCode).toBe(0); }); + + test("warning names which package.json failed to parse", async () => { + const dir = tempDirWithFiles("filter-bad-pkgjson", { + packages: { + good: { + "package.json": JSON.stringify({ name: "good", scripts: { go: "echo ok" } }), + }, + broken: { + "package.json": "this is { not valid json", + }, + }, + "package.json": JSON.stringify({ name: "ws", workspaces: ["packages/*"] }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "--filter", "*", "go"], + cwd: dir, + env: { ...bunEnv, NO_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const sep = process.platform === "win32" ? "\\" : "/"; + expect(stderr).toContain(`broken${sep}package.json`); + expect(stderr).toContain("skipping this workspace package"); + }); }); From c3293c106df92d31e76ef10d20c418d477aec856 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:47:38 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- test/bundler/cli.test.ts | 6 +++--- test/cli/install/isolated-install.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/bundler/cli.test.ts b/test/bundler/cli.test.ts index 1f7954efc2a..e47d0ae11df 100644 --- a/test/bundler/cli.test.ts +++ b/test/bundler/cli.test.ts @@ -480,7 +480,7 @@ describe("CLI argument error messages", () => { const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout, stderr }).toEqual({ stdout: "", - stderr: expect.stringContaining("--format: \"commonjs\""), + stderr: expect.stringContaining('--format: "commonjs"'), }); expect(stderr).toContain("'esm', 'cjs', or 'iife'"); expect(exitCode).toBe(1); @@ -497,7 +497,7 @@ describe("CLI argument error messages", () => { }); const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toContain("--loader"); - expect(stderr).toContain("\"text\""); + expect(stderr).toContain('"text"'); expect(stderr).toContain(".ext:loader"); expect(exitCode).toBe(1); }); @@ -513,7 +513,7 @@ describe("CLI argument error messages", () => { }); const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toContain("--define"); - expect(stderr).toContain("\"FOO\""); + expect(stderr).toContain('"FOO"'); expect(stderr).toContain("key=value"); expect(exitCode).toBe(1); }); diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index cbc609a5153..fb4928baeb0 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -2039,7 +2039,7 @@ test("invalid --linker value is echoed back in the error", async () => { stderr: "pipe", }); const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toContain("--linker: \"isoalted\""); + expect(stderr).toContain('--linker: "isoalted"'); expect(stderr).toContain("'isolated' or 'hoisted'"); expect(exitCode).toBe(1); }); From 77625c7d637dd1311ea398ac05ddd8f6afff4341 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:00:10 +0000 Subject: [PATCH 3/3] test: assert stdout and exit code in filter-workspace broken-pkg test --- test/cli/run/filter-workspace.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 15a0c01935d..34becb5417e 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -611,9 +611,13 @@ describe("bun", () => { stdout: "pipe", stderr: "pipe", }); - const [, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // The good package still runs; the broken one is skipped with a warning + // that names its path. + expect(stdout).toContain("ok"); const sep = process.platform === "win32" ? "\\" : "/"; expect(stderr).toContain(`broken${sep}package.json`); expect(stderr).toContain("skipping this workspace package"); + expect(exitCode).toBe(0); }); });