Skip to content

bun build: error when both --outdir and --outfile are set - #48214

Open
robobun wants to merge 2 commits into
mainfrom
robobun/8f3d5cf0/build-outfile-outdir-error
Open

robobun wants to merge 2 commits into
mainfrom
robobun/8f3d5cf0/build-outfile-outdir-error

Conversation

@robobun

@robobun robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #21406

Problem

  • bun build ./index.ts --outfile x.js --outdir out exits 0, writes out/index.js, and prints nothing about the dropped --outfile.
  • parse_build_command_options (src/runtime/cli/Arguments.rs:2997) read --outfile only in the else arm of the --outdir test.

Fix

  • Commit 1: read the two flags independently. An empty value counts as not set.
  • Commit 2: when both values are set, print error: cannot use both --outdir and --outfile and exit 1 before the build starts. A note names the edit to make.
  • Correct because esbuild does the same: 0 of 9 pair rows differ from esbuild 0.21.5.
  • Verified: test/bundler/cli.test.ts, 26 new rows, main fails 21. Also standalone, bun-build-dts, compile-library. Self-reviewed: 17 concerns raised, 17 addressed.

Background

  • --outdir names a folder for all outputs, --outfile the path of one output, --entry-naming the entry file inside --outdir.
  • Revives don't allow --outfile at same time as --outdir in bun build #21442 by @RiskyMH (approved by @alii, closed by a stale sweep). Same error text. The empty-value rule and the notes are new.
  • Considered a join to <outdir>/<outfile>: it moves output with no message. Considered a check in build_command.rs: --app returns before it.

Downsides

  • Breaking: a command with both values set now exits 1, not 0. In 300 public package.json files, 25 commands do that, and 15 of them write the intended file today (mcpdotdirect/evm-mcp-server build:http). Notes has the warning alternative.
  • --outfile x.js --outdir "" printed the bundle to stdout. It now writes x.js.
  • bun build --outdir out does one more flag lookup: 28087 to 28338 instructions in the parse function. .text grows 256 bytes.
Notes

Commands that behave differently now. Release builds of main (7ca0042) and of this change on the same commit. index.ts is one console.log.

Command (after bun) main This PR
build index.ts --outfile x.js --outdir out exit 0, writes out/index.js exit 1
build index.ts --outdir dist --outfile index.js exit 0, writes dist/index.js exit 1
build index.ts --outdir dist --outfile dist/index.js exit 0, writes dist/index.js exit 1
build index.ts second.ts --outdir dist --outfile x.js exit 0, writes dist/index.js and dist/second.js exit 1
the same pair with --no-bundle, --splitting, --sourcemap=external exit 0, writes into out exit 1
build --compile --target=browser index.html --outfile x.html --outdir out (also bun compile) exit 0, writes out/index.html exit 1
build index.ts --outfile x.js --outdir "" (also --outdir=) exit 0, bundle on stdout, no file exit 0, writes x.js
build --compile --target=browser index.html --outfile x.html --outdir "" exit 0, writes over index.html exit 0, writes x.html
build --compile index.ts --outfile bun --outdir "" exit 0, writes an executable named index exit 1, the name bun is refused

Commands that already fail on main and print another message now:

Command (after bun) main This PR
build --compile index.ts --outfile x --outdir out (also bun compile, also with --target=browser) error: cannot use --compile with --outdir the pair error and note: remove --outdir and put the folder in --outfile
build --app index.ts --outfile x.js --outdir out a Bake config error (ERR_INVALID_ARG_TYPE) the pair error, no note

Not changed: --compile --outdir out without --outfile still prints cannot use --compile with --outdir. --outfile "" --outdir out still writes out/index.js. --outfile= --outdir= still prints the bundle.

The messages.

$ bun build index.ts --outfile x.js --outdir out
error: cannot use both --outdir and --outfile
note: remove --outfile, or replace it with --entry-naming to name the entry file inside --outdir

$ bun build --compile index.ts --outfile x --outdir out
error: cannot use both --outdir and --outfile
note: remove --outdir and put the folder in --outfile
  • "Remove --outfile" gives the output that the command wrote before this change.
  • --outdir out --entry-naming x.js writes out/x.js. --compile --target=browser index.html --outfile out/x.html writes out/x.html. Two rows pin these.
  • --app gets no note. It reads neither flag on main (bake: resolve one output directory for bun build --app from --outdir #47446 changes that).
  • The text is the same for every build mode. Considered cannot use --compile with --outdir for executable builds with both flags: the parser cannot tell an executable from a standalone HTML build without the entry points, and build_command.rs owns that rule.

Two questions for a maintainer.

  1. Error or warning? The error matches don't allow --outfile at same time as --outdir in bun build #21442, esbuild, and the review rule "throw on mutually-exclusive combinations rather than ignoring one". It also breaks commands that write the intended file today. The baseline that CLI: fixes for bun run, flags, bunfig.toml, .env, --filter, bun init and completions #46007 states for CLI changes ("a command that exits 0 on 1.4.3 and does the right thing should do the same thing") points the other way. The alternative is a warning with --outdir still winning. Only the second commit changes for that.
  2. Is an empty value set? This PR says no: that is esbuild's rule, and the old block already ignored an empty value of each flag. don't allow --outfile at same time as --outdir in bun build #21442 tested presence, and REVIEW.md says "gate on presence, not truthiness". With the presence rule, --outfile "" --outdir out (exit 0 on main) also exits 1, and --outfile x.js --outdir "" exits 1 where it now writes x.js.

How many scripts pass both flags. GitHub code search for "bun build" "--outdir" "--outfile" filename:package.json reports 1,580 files. This count covers the scripts of the first 300 (260 repositories). 28 bun build commands in 25 repositories pass both flags to one command.

  • 3 also pass --compile. They fail on main.
  • 15 write exactly the file that --outfile names. 10 have the shape --outdir dist --outfile index.js with an entry named index. 5 have the shape --outdir dist --outfile dist/server.js. Three are the same build:http line, in mcpdotdirect/template-mcp-server, mcpdotdirect/evm-mcp-server and abdussalam-mustapha/sei-mcp-server.
  • 10 write another file. For example, --outdir dist --format cjs --outfile index.cjs writes dist/index.js.

Not changed here.

Order against open PRs.

Measurements. Release builds, linux-x64, main at 7ca0042 against the same commit with this change.

  • parse_build_command_options, gdb stepi count over the function, 3 identical runs each:
    • bun build ./index.ts --outdir out: 42 to 43 Args::option calls, 28087 to 28338 instructions.
    • bun build ./index.ts --outfile x.js: 43 to 43 calls, 28327 to 28337 instructions.
    • bun build ./index.ts: 43 to 43 calls, 28272 to 28286 instructions.
    • mimalloc entries 1 to 1, syscall instructions 0 to 0 (--outdir out).
  • bun -e 0 and bun ./index.ts: 0 entries into parse_build_command_options (gdb breakpoint). arguments::parse is 25947 B in both builds.
  • Size: parse_build_command_options 11498 B to 11814 B. .text 110627497 B to 110627753 B. .rodata 26772460 B in both. The stripped binary is 140478752 B in both.
  • bun build ./index.ts --outfile x.js --outdir out, ptrace syscall counter, 3 runs: main exits 0 after 398 to 522 syscalls and writes out/index.js. This PR exits 1 after 93 syscalls, with no mkdir or open of out or x.js.
  • strace, perf and bloaty are not in the dev image. gdb, llvm-nm and llvm-size were used.

Tests. 26 rows in test/bundler/cli.test.ts.

  • On a release build of main (7ca0042): 21 fail, 5 pass. With this change: 26 pass (release and debug).
  • Of the 21: 12 go from exit 0 to exit 1, 5 fail on main with another message, 3 change where the output goes, and 1 (--outfile bun --outdir "") shows that --compile reads --outfile.
  • The 5 that pass both ways are guards: --compile --outdir out keeps its own error, --outfile "" --outdir out and --outfile= --outdir= (an empty value is not set), and the two commands that the notes recommend.
  • The first commit alone passes its 6 rows, and a pair still lets --outdir win there.
  • esbuild 0.21.5 against this PR on 9 pair rows (both set in both orders, each one empty, both empty, a nested outfile, a repeated --outdir, two entry points, --splitting): 0 differ in exit status or files written.
  • Debug build of this branch on main at 2596ac5, with --timeout: test/bundler/cli.test.ts (70 pass, 2 skip), test/bundler/standalone.test.ts (26 pass), test/bundler/bun-build-dts.test.ts (11 pass), test/cli/compile/compile-library.test.ts (57 pass). In test/cli/compile/compile.test.ts, 6 rows that compile ahead of time need more than their own time limit with the 1.6 GB debug binary. No row there passes both flags, and the one tried alone passes in 115 s.

Self-review. 17 concerns, 17 addressed. The main changes: the note now starts with the edit that keeps the old output, the error text no longer depends on a guess of the build mode, standalone HTML and executable builds get a note, rows for a pair that agrees, the sized break above, and the split into two commits. Declined in part: a note on the two exec-time copies of the --compile error (see "Not changed here"), and a tracking issue for --outfile with --entry-naming.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/cli.test.ts

robobun and others added 2 commits November 8, 2026 05:07
`bun build` read `--outfile` only when `--outdir` was absent, so an
empty `--outdir` dropped it. `--outfile x.js --outdir ""` printed the
bundle to stdout and wrote no file.

Read the two flags independently. An empty value counts as not set, as
before for each flag alone. When both values are set, `--outdir` still
wins.
With both flags, `bun build` wrote `<outdir>/<entry>.js` with exit 0 and
`--outfile` had no effect.

Print `cannot use both --outdir and --outfile` and exit 1 before the
build starts. A note names the edit: remove `--outfile` or replace it
with `--entry-naming` for a bundle, put the folder in `--outfile` for
`--compile`.

Co-authored-by: RiskyMH <git@riskymh.dev>
@robobun

robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. One decision is open for a maintainer: an error (this PR) or a warning when both flags are set. See "Two questions for a maintainer" in the PR notes.

CI on 5e4454f (build 138624): 184 of 188 jobs passed. The build is red because the 4 darwin any x64 - test-bun jobs expired after 6.7 hours in the queue with no agent. That lane ran hours behind for most of 2026-11-08 and 2026-11-09, for every build. At 12:15 UTC on 2026-11-09, no build from the last 3 hours had started those jobs. So the cause is the lane and not this diff, and a retry of those 4 jobs is enough to complete the build. test/bundler/cli.test.ts has no failure and no retry on any lane that ran. Three tests outside this diff needed one retry: test/aot/runner-suites/mock-module-runs-3.test.ts, test/js/bun/webview/webview-chrome.test.ts and test/js/node/test/parallel/test-cluster-shared-leak.js.

Reproduced #21406 on a release build of main (7ca0042) and on Bun 1.4.3-canary:

mkdir r && cd r && echo 'console.log(1)' > index.ts
bun build ./index.ts --outfile x.js --outdir out
# exit 0, writes out/index.js, no x.js, no message about --outfile

With this PR (#48214) the same command prints error: cannot use both --outdir and --outfile and exits 1 before the build starts.

@github-actions github-actions Bot added the claude label Nov 8, 2026
@robobun

robobun commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:05 AM PT - Nov 8th, 2026

❌ @robobun, your commit 5e4454f has some failures in Build #138624 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 48214

That installs a local version of the PR into your bun-48214 executable, so you can run:

bun-48214 --bun

@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The build CLI now rejects nonempty --outdir and --outfile supplied together. Tests cover the error messages, exit status, absence of output files, and accepted option combinations. The documentation describes the conflict and points to --entry-naming.

Changes

Build output options

Layer / File(s) Summary
Conflict handling and output-option behavior
docs/snippets/cli/build.mdx, src/runtime/cli/Arguments.rs, test/bundler/cli.test.ts
The CLI exits with an error when both nonempty options are set. Non---app builds include a note that varies by compile mode. The documentation describes the conflict. Tests cover failures, empty values, and accepted output options.

Possibly related PRs

  • oven-sh/bun#41879: Changes the --outfile contract and its interaction with --entry-naming, which this PR also references.

Suggested reviewers: jarred-sumner, alii

Severity of issue fixed: Medium

Merge Risk: Minimal · up to 5e445

The change rejects conflicting nonempty output options while preserving documented alternatives. No actionable merge-blocking risk remains identified.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: bun build now reports an error when both --outdir and --outfile are set. The changeset matches this. Arguments.rs rejects both nonempty flags with the error text, the …
Linked Issues check Passed Issue #21406 asks that bun build either error on --outdir with --outfile, or infer <outdir>/<outfile> for one entry point. It also asks for an error with multiple entry points. The issue state…
Out of Scope Changes check Passed The reviewed diff changes three areas. (1) src/runtime/cli/Arguments.rs: the --outdir/--outfile handling block. It adds the conflict error, the notes, and the independent reads of both flags. This dir…
Description check Passed The description follows the repository template with both required sections, "What does this PR do?" and "How did you verify your code works?". The first section gives a clear problem statement, the f…
Full details: Title check

Explanation

The title names the main change: bun build now reports an error when both --outdir and --outfile are set. The changeset matches this. Arguments.rs rejects both nonempty flags with the error text, the CLI tests cover the conflict, and the docs note the restriction. The title is specific and concise.

Full details: Linked Issues check

Explanation

Issue #21406 asks that bun build either error on --outdir with --outfile, or infer &lt;outdir&gt;/&lt;outfile&gt; for one entry point. It also asks for an error with multiple entry points. The issue states that esbuild errors in all cases. The diff in src/runtime/cli/Arguments.rs (parse_build_command_options) reads both flags with an empty-value filter. When both are nonempty, it prints error: cannot use both --outdir and --outfile and calls Global::exit(1) before the build starts. This applies to any number of entry points, so the error requirement holds for single- and multiple-entry builds. The error alternative for single entry points is met. The inference alternative is not implemented. The issue allows either option, so this is not required. The docs change in docs/snippets/cli/build.mdx states the new restriction and points to --entry-naming. Automated coverage is described in the PR as 26 rows in test/bundler/cli.test.ts. I did not inspect that test file, so the test claim rests on the PR summary only. The coding requirement is met by the implementation shown in the diff.

Full details: Out of Scope Changes check

Explanation

The reviewed diff changes three areas. (1) src/runtime/cli/Arguments.rs: the --outdir/--outfile handling block. It adds the conflict error, the notes, and the independent reads of both flags. This directly implements #21406. (2) docs/snippets/cli/build.mdx: the --outfile description now states the conflict and names --entry-naming. This documents the new behavior of the same flags. (3) test/bundler/cli.test.ts: CLI tests for these flags, per the PR summary. The PR description also covers other items (esbuild comparison, timing measurements, related open PRs). These are analysis and do not add code changes. The reviewed code diff contains no change to unrelated build, compile, or runtime logic. The shift in behavior for empty values (for example --outfile x.js --outdir "" now writes x.js) follows from the same flag-reading logic that the issue covers.

Full details: Description check

Explanation

The description follows the repository template with both required sections, "What does this PR do?" and "How did you verify your code works?". The first section gives a clear problem statement, the fix in two commits, and the reasoning behind the chosen design. The second section lists the verification: 26 new rows in test/bundler/cli.test.ts (21 fail on main, 26 pass with the change), esbuild comparison on 9 pair rows, debug-build runs of related test files, and self-review. Its one gap is that the Tests section reports the release-build run only in summary form and does not name the platform-specific tests it deferred to CI, which the robobun evidence block flags. The description is far more detailed than the template needs. It also includes a long Notes block with breaking-change tables, which makes the core content harder to find.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2596ac5 and 5e4454f.

Files selected for processing: 3

  • docs/snippets/cli/build.mdx
  • src/runtime/cli/Arguments.rs
  • test/bundler/cli.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the --library interaction with the new match: --library rejects any --outfile earlier in the function (even an empty one), so the both-set arm is unreachable there, --library --outdir custom still overrides the pre-set default via the (Some, None) arm, and plain --library keeps the default via (None, None) — behavior is preserved. I also searched test/ for existing commands that pass both flags and found none, so no existing assertion flips. I did not run the new test block.

Extended reasoning...

The change replaces the outdir/outfile else-if chain in parse_build_command_options with a match that errors on the pair, updates one docs ParamField, and adds a concurrent subprocess test matrix; it touches no security-sensitive surface. Two inline findings are posted (the breaking-change scope and the untreated --outfile/--entry-naming sibling), so a human needs to weigh them; the --library pre-set outdir path and the existing test suite were examined and ruled out as affected.

Comment on lines +3000 to +3012
(Some(_), Some(_)) => {
Output::err_generic("cannot use both --outdir and --outfile", ());
// No note for --app: it reads neither flag.
if !ctx.bundler_options.bake {
if compiles {
bun_core::note!("remove --outdir and put the folder in --outfile");
} else {
bun_core::note!(
"remove --outfile, or replace it with --entry-naming to name the entry file inside --outdir"
);
}
}
Global::exit(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Users whose existing bun build script passes both flags, which today writes the file they wanted, get exit 1 and no output after upgrading. The new (Some(_), Some(_)) arm at src/runtime/cli/Arguments.rs:3000 exits before the build for every non-empty pair, including --outdir dist --outfile index.js and --outdir dist --outfile dist/index.js, which on the base branch write exactly dist/index.js. This follows from the stated purpose; the description sizes it at 15 of 300 sampled public package.json files whose build scripts break on upgrade. Fix: if the error stays, keep it but let the two shapes that already produce the requested file keep working, or print warn: and continue with --outdir winning so no working script exits 1; either way, document it as a breaking change.

Why this was flagged

A package.json script such as bun build src/index.ts --outdir dist --outfile index.js reaches parse_build_command_options via bun run build. On the base branch the old chain at the same spot set outdir and ignored outfile, so the build wrote dist/index.js, which is the file the script author intended, and exited 0. After this change the match at src/runtime/cli/Arguments.rs:3000-3012 prints error: cannot use both --outdir and --outfile and calls Global::exit(1) before any build runs, so bun run build fails and nothing is written. The PR description sizes this population at 15 of 28 sampled commands across 300 public package.json files that pass both flags and write the intended file today, and names the warning alternative as unimplemented. No safeguard applies: the check runs for every non-empty pair regardless of whether the pair would have produced the requested file.

Verification: Triggering condition: a bun build invocation that passes non-empty values for both --outdir and --outfile. Mechanism, src/runtime/cli/Arguments.rs:2997-3012: the (Some(_), Some(_)) arm does not inspect the values, so --outdir dist --outfile index.js exits 1 before any build runs and writes nothing. On the base, outfile was dropped, so those scripts exited 0 with the intended output.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the purpose of the second commit. The PR body lists it as breaking: see Downsides and the first table in Notes.

The two shapes cannot keep working from the parser:

  • The linker computes the output name (entry naming, root, loader). For an --outdir build the linker also writes the files (write_output_files_to_disk, called from src/bundler/linker_context/generateChunksInParallel.rs). The CLI cannot compare the name with --outfile before the write.
  • A comparison of the flag text at parse time is a guess. The 15 sampled commands that work today use two readings of --outfile: 10 name a file inside --outdir, 5 give the full path.

The warning is possible. It prints warn: with the same note, --outdir still wins, and the exit code stays 0. Only the second commit changes for that.

The error is the default here for three reasons: #21442 was approved in that shape, esbuild rejects the pair, and an error is the first of the two results that #21406 asks for. The choice is question 1 in Notes. I will switch to the warning if a maintainer prefers it.

Comment thread src/runtime/cli/Arguments.rs Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun build: --outfile is ignored when used with --outdir, should error or infer output for single entry point

1 participant