Repository navigation
Conversation
`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>
|
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 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 --outfileWith this PR (#48214) the same command prints |
|
Updated 6:05 AM PT - Nov 8th, 2026
❌ @robobun, your commit 5e4454f has some failures in 🧪 To try this PR locally: bunx bun-pr 48214That installs a local version of the PR into your bun-48214 --bun |
|
No actionable comments were generated in the recent review. WalkthroughThe build CLI now rejects nonempty ChangesBuild output options
Possibly related PRs
Suggested reviewers: Severity of issue fixed: Medium Merge Risk:
|
There was a problem hiding this comment.
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.
| (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); |
There was a problem hiding this comment.
🔴 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.
There was a problem hiding this comment.
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
--outdirbuild the linker also writes the files (write_output_files_to_disk, called fromsrc/bundler/linker_context/generateChunksInParallel.rs). The CLI cannot compare the name with--outfilebefore 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.
Fixes #21406
Problem
bun build ./index.ts --outfile x.js --outdir outexits 0, writesout/index.js, and prints nothing about the dropped--outfile.parse_build_command_options(src/runtime/cli/Arguments.rs:2997) read--outfileonly in theelsearm of the--outdirtest.Fix
error: cannot use both --outdir and --outfileand exit 1 before the build starts. A note names the edit to make.test/bundler/cli.test.ts, 26 new rows, main fails 21. Alsostandalone,bun-build-dts,compile-library. Self-reviewed: 17 concerns raised, 17 addressed.Background
--outdirnames a folder for all outputs,--outfilethe path of one output,--entry-namingthe entry file inside--outdir.--outfileat same time as--outdirin 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.<outdir>/<outfile>: it moves output with no message. Considered a check inbuild_command.rs:--appreturns before it.Downsides
package.jsonfiles, 25 commands do that, and 15 of them write the intended file today (mcpdotdirect/evm-mcp-serverbuild:http). Notes has the warning alternative.--outfile x.js --outdir ""printed the bundle to stdout. It now writesx.js.bun build --outdir outdoes one more flag lookup: 28087 to 28338 instructions in the parse function..textgrows 256 bytes.Notes
Commands that behave differently now. Release builds of main (7ca0042) and of this change on the same commit.
index.tsis oneconsole.log.bun)build index.ts --outfile x.js --outdir outout/index.jsbuild index.ts --outdir dist --outfile index.jsdist/index.jsbuild index.ts --outdir dist --outfile dist/index.jsdist/index.jsbuild index.ts second.ts --outdir dist --outfile x.jsdist/index.jsanddist/second.js--no-bundle,--splitting,--sourcemap=externaloutbuild --compile --target=browser index.html --outfile x.html --outdir out(alsobun compile)out/index.htmlbuild index.ts --outfile x.js --outdir ""(also--outdir=)x.jsbuild --compile --target=browser index.html --outfile x.html --outdir ""index.htmlx.htmlbuild --compile index.ts --outfile bun --outdir ""indexbunis refusedCommands that already fail on main and print another message now:
bun)build --compile index.ts --outfile x --outdir out(alsobun compile, also with--target=browser)error: cannot use --compile with --outdirnote: remove --outdir and put the folder in --outfilebuild --app index.ts --outfile x.js --outdir outERR_INVALID_ARG_TYPE)Not changed:
--compile --outdir outwithout--outfilestill printscannot use --compile with --outdir.--outfile "" --outdir outstill writesout/index.js.--outfile= --outdir=still prints the bundle.The messages.
--outfile" gives the output that the command wrote before this change.--outdir out --entry-naming x.jswritesout/x.js.--compile --target=browser index.html --outfile out/x.htmlwritesout/x.html. Two rows pin these.--appgets no note. It reads neither flag on main (bake: resolve one output directory forbun build --appfrom--outdir#47446 changes that).cannot use --compile with --outdirfor executable builds with both flags: the parser cannot tell an executable from a standalone HTML build without the entry points, andbuild_command.rsowns that rule.Two questions for a maintainer.
--outfileat same time as--outdirin 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--outdirstill winning. Only the second commit changes for that.--outfileat same time as--outdirin bun build #21442 tested presence, andREVIEW.mdsays "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 writesx.js.How many scripts pass both flags. GitHub code search for
"bun build" "--outdir" "--outfile" filename:package.jsonreports 1,580 files. This count covers the scripts of the first 300 (260 repositories). 28bun buildcommands in 25 repositories pass both flags to one command.--compile. They fail on main.--outfilenames. 10 have the shape--outdir dist --outfile index.jswith an entry namedindex. 5 have the shape--outdir dist --outfile dist/server.js. Three are the samebuild:httpline, in mcpdotdirect/template-mcp-server, mcpdotdirect/evm-mcp-server and abdussalam-mustapha/sei-mcp-server.--outdir dist --format cjs --outfile index.cjswritesdist/index.js.Not changed here.
Bun.build()has no top-leveloutfile(AddoutfiletoBun.build#12629).--outfile x.js --entry-naming y.jswritesx.jsand prints nothing. bun build: never derive an output directory from the entry point #41879 rewrites that code.--outfile dist/index.js --sourcemap=linkedwritessrc/index.js(Directories in --outfile are ignored with --sourcemap #19729, fixed by bun build: never derive an output directory from the entry point #41879).--outfilewith--sourcemap=externalfails withcannot use an external source map without --outdir.bun build --compile --target=browser index.htmlwith neither flag still writes overindex.html(build: print standalone HTML to stdout when no --outdir or --outfile is given #42006, bundler: refuse to write an output file over one of the build's inputs #41612).bun build --appreads neither flag (bake: resolve one output directory forbun build --appfrom--outdir#47446).cannot use --compile with --outdir(build_command.rs,compile_command/configuration.rs) get no note here, because bun build: never derive an output directory from the entry point #41879 rewrites those files.Order against open PRs.
--outfileandexternaldifferences from esbuild #48092 adds this sentence to the--outfilerow ofdocs/bundler/esbuild.mdx: "When--outdiris also set, Bun ignores--outfile. esbuild reports an error." It is false after this PR. The PR that merges second must remove it.bun build --appfrom--outdir#47446 do not touch this block.git merge-treereports no conflict with bun build: never derive an output directory from the entry point #41879 or docs: list the--outfileandexternaldifferences from esbuild #48092.Measurements. Release builds, linux-x64, main at 7ca0042 against the same commit with this change.
parse_build_command_options, gdbstepicount over the function, 3 identical runs each:bun build ./index.ts --outdir out: 42 to 43Args::optioncalls, 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.syscallinstructions 0 to 0 (--outdir out).bun -e 0andbun ./index.ts: 0 entries intoparse_build_command_options(gdb breakpoint).arguments::parseis 25947 B in both builds.parse_build_command_options11498 B to 11814 B..text110627497 B to 110627753 B..rodata26772460 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 writesout/index.js. This PR exits 1 after 93 syscalls, with nomkdiroropenofoutorx.js.strace,perfandbloatyare not in the dev image. gdb,llvm-nmandllvm-sizewere used.Tests. 26 rows in
test/bundler/cli.test.ts.--outfile bun --outdir "") shows that--compilereads--outfile.--compile --outdir outkeeps its own error,--outfile "" --outdir outand--outfile= --outdir=(an empty value is not set), and the two commands that the notes recommend.--outdirwin there.--outdir, two entry points,--splitting): 0 differ in exit status or files written.--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). Intest/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
--compileerror (see "Not changed here"), and a tracking issue for--outfilewith--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