Skip to content

bunfig: keep CLI flags ahead of bunfig.toml when it is loaded after argv (bun run) - #38599

Open
robobun wants to merge 1 commit into
mainfrom
farm/44991654/bunfig-cli-precedence
Open

bunfig: keep CLI flags ahead of bunfig.toml when it is loaded after argv (bun run)#38599
robobun wants to merge 1 commit into
mainfrom
farm/44991654/bunfig-cli-precedence

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • With a bunfig.toml in the cwd, bun run --define X=4 file.mjs runs with bunfig's [define] value; bun --define X=3 file.mjs and bun test --define ... use the flag. Reproduces on 1.4.0 and main.
  • The same inversion applies to every other key the bunfig parser writes over a value a flag had set: [loader] vs --loader, [console] depth vs --console-depth (the docs promise the flag wins), jsx/jsxFactory/jsxFragment/jsxImportSource vs --jsx-* (any bunfig.toml at all, even one without jsx keys, undid --jsx-runtime classic), [install] auto vs --no-install/-i/--install (bun run --no-install still asked the registry), and a [macros] table vs --no-macros (macros ran anyway). A preload key dropped the --preload/--require/--import list entirely; test/config/bunfig/preload.test.ts had these cases commented out as a FIXME.
  • Cause: bun <file> and bun test load bunfig.toml inside Arguments::parse (src/runtime/cli/Arguments.rs, load_config_with_cmd_args) before the flags are applied, so flags overwrite it. bun run <target> is not in ALWAYS_LOADS_CONFIG, so it, the node shim (both boot through RunCommand::boot, src/runtime/cli/run_command.rs), RunCommand::exec_with_cfg, boot_standalone and bun repl load bunfig.toml after argv, and Bunfig::parse (src/bunfig/bunfig.rs) assigned each key unconditionally.

Fix

  • ContextData gets cli_overrides: CliOverrides (src/options_types/context.rs), one bool per setting; Arguments::parse sets the bool where it applies the flag. Bunfig::parse still parses and validates each of these keys (so diagnostics do not depend on which flags were passed) but skips the assignment when the flag was given.
  • load_preload prepends the config's preloads to whatever is already in ctx.preloads instead of replacing it. In the early order that list is empty, so nothing changes; in the late order it yields config preloads followed by argv preloads, which is what Arguments::parse builds in the early order (and what the existing cli-merge.ts fixture asserts).
  • The --no-install/-i/--install chain in Arguments::parse is reshaped to compute one Option<GlobalCache> so the override is recorded in one place; the accepted values and the error are unchanged.
  • Why this shape: it makes the parser correct in either load order, so it also covers the node shim, bun repl and standalone executables without touching each late-load site, and it only changes behavior when a flag and its bunfig key are both present (the buggy combination). It is the pattern test.pathIgnorePatterns already uses (path_ignore_patterns_from_cli). Moving the bun run load earlier instead would change error handling for malformed configs, start applying bunfig to --filter/--parallel runs, and still leave the node shim on the late path.
  • For jsx, only runtime is guarded by --jsx-runtime; development keeps following the bunfig jsx key as it does today, since no flag sets it (this is also the behavior cli: stop --jsx-* flags from switching the automatic JSX runtime to production #36209 gives the early order).
  • Intentionally not covered here: the [run] keys (Make CLI flags override their [run] counterparts in bunfig.toml #33198 adds the same guards for those), origin/serve.port (nothing at runtime reads them; vm.origin is never set from options), and smol/install.prefer, which Arguments::parse overwrites unconditionally even in the early order, so they need a different fix (reported separately).
  • Verification:
    • test/config/bunfig/cli-flags-override-bunfig.test.ts (new: the precedence matrix spans eight settings and two load orders, which no existing file covers): for each key, bun <flag> file, bun run <flag> file and bun run file; [install] auto uses a local registry stub and checks in both directions. With the released binary exactly the ten bun run <flag> cases fail; all 28 pass with this change.
    • test/config/bunfig/preload.test.ts: the three run forms from the FIXME list are enabled, plus node -r ./preload3.ts through fakeNodeRun; these four fail on the released binary and pass here. The remaining commented form (bun --preload x run file) is a different bug, the subcommand detection problem cli: classify the subcommand past --cwd/--env-file values #36644 is about, and the comment now says so.
    • Also run locally: test/cli/console-depth.test.ts, test/config/bunfig/bunfig-errors.test.ts, test/cli/install/bun-run-bunfig.test.ts, test/cli/run/as-node.test.ts, test/cli/run/run-autoinstall.test.ts, test/bundler/transpiler/macro-test.test.ts, test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts, test/js/bun/resolve/resolve-error.test.ts; cargo clippy on the three crates is clean.

Background

  • ContextData (ctx) is the process-wide bag of parsed CLI state: ctx.args (TransformOptions: define, loaders, jsx, ...), ctx.debug (auto-install mode, macros), ctx.runtime_options (console depth), ctx.preloads. Both Arguments::parse and Bunfig::parse write into it; whichever runs second wins a field.
  • ALWAYS_LOADS_CONFIG (src/options_types/command_tag.rs) lists the subcommands whose bunfig.toml is loaded during Arguments::parse. bun run was deliberately left out and loads it later in RunCommand (load local bunfig.toml for bun run earlier (for run.bun option) #16664), which is why the two orders exist.
  • Auto-install modes (GlobalCache): --no-install disables resolving missing packages from the registry; fallback (-i) fetches only packages that are not found locally. A manifest request to the configured registry is therefore the observable difference the install test uses.
Probe matrix on the released 1.4.0 binary
$ cat bunfig.toml
preload = ["./pre-bunfig.js"]
[loader]
".foo" = "text"
[console]
depth = 1

$ bun --preload ./pre-cli.js --loader .foo:json --console-depth 5 main.mjs
preload:bunfig
preload:cli
loader:object
{ a: { b: { c: { d: 1 } } } }          # printed at depth 5

$ bun run --preload ./pre-cli.js --loader .foo:json --console-depth 5 main.mjs
preload:bunfig                          # --preload dropped
loader:string                           # bunfig [loader] won
{ a: { b: [Object ...] } }              # bunfig depth won

$ echo 'logLevel = "warn"' > bunfig.toml          # no jsx keys at all
$ bun --jsx-runtime classic main.jsx              -> classic:div
$ bun run --jsx-runtime classic main.jsx          -> automatic runtime used

$ printf '[macros]\n"p" = { "x" = "./macro.ts" }\n' > bunfig.toml
$ bun --no-macros main.ts                         -> error: Macros are disabled
$ bun run --no-macros main.ts                     -> macro=42

$ printf '[install]\nauto = "fallback"\nregistry = "http://localhost:<stub>/"\n' > bunfig.toml
$ bun --no-install main.mjs                       -> registry requests: []
$ bun run --no-install main.mjs                   -> registry requests: ["/zz-missing-pkg"]

`bun <file>` and `bun test` parse bunfig.toml inside Arguments::parse,
before argv is applied, so flags overwrite the config. `bun run <file>`,
the node shim, `bun repl` and standalone executables load bunfig.toml
later, and Bunfig::parse assigned every key unconditionally, so with a
bunfig.toml present these flags lost to the config: --define, --loader,
--console-depth, --jsx-runtime/--jsx-factory/--jsx-fragment/
--jsx-import-source, --no-install/-i/--install and --no-macros. A
`preload` key replaced the --preload list outright.

Record the flags argv supplied in ContextData::cli_overrides and have
Bunfig::parse still validate, but not apply, the matching keys. Config
preloads are now prepended to whatever argv already added, which is the
order the early load produces.
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on bun 1.4.0 and main (eabb96d): with a bunfig.toml present, bun run --define X=4 file.mjs prints bunfig's value while bun --define X=3 file.mjs prints the flag's. The same late-load inversion was confirmed for --loader, --console-depth, the --jsx-* flags, --no-install/-i, --no-macros, and --preload (dropped), including through the node shim.
  • Fix in this PR; test/config/bunfig/cli-flags-override-bunfig.test.ts (10 of 28 cases fail on the released binary) and the previously FIXME'd bun run --preload cases in test/config/bunfig/preload.test.ts (4 fail on the released binary) cover it.
  • Related, not covered here: [run] keys (Make CLI flags override their [run] counterparts in bunfig.toml #33198), smol/install.prefer (broken in the early order too, reported separately).

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d83a9fe2-f2fc-45a7-ab3a-d055477c31fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5638c62 and 83bef84.

📒 Files selected for processing (5)
  • src/bunfig/bunfig.rs
  • src/options_types/context.rs
  • src/runtime/cli/Arguments.rs
  • test/config/bunfig/cli-flags-override-bunfig.test.ts
  • test/config/bunfig/preload.test.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Make CLI flags override their [run] counterparts in bunfig.toml #33198 - Same root cause and the identical *_from_cli guard mechanism in Arguments::parse + Bunfig::parse across the same three files, just for the [run] keys instead of [define]/[loader]/jsx.
  2. Walk up for bunfig.toml when running from a subdirectory #29310 - Contains the same load_preload merge-instead-of-replace change (bunfig preloads then CLI --preload) for the same reason, and edits the same test/config/bunfig/preload.test.ts.
  3. cli(test): make --coverage/--coverage-reporter/--coverage-dir override bunfig #36669 - Same three files and the same "record that the flag was passed so the bunfig parser skips its assignment" pattern, applied to the [test] coverage keys.
  4. Fix object loader missing default export and coverage CLI flag override #28547 - Earlier, narrower version of that same coverage-flag-vs-bunfig precedence fix in the same bunfig.rs/context.rs/Arguments.rs trio.

🤖 Generated with Claude Code

@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.

I reviewed this PR and didn't find any bugs. Because it changes CLI-vs-bunfig precedence across eight user-facing settings and reworks how preloads are merged, a human look would still be worthwhile.

What was reviewed:

  • Each CliOverrides bool traced from where Arguments::parse sets it to where Bunfig::parse guards the assignment; validation still runs when the flag is set.
  • load_preload prepend semantics: confirmed read_global_config() is false for Run/Auto/Test so only one bunfig is parsed per invocation and no double-prepend occurs.
  • The reshaped --no-install/-i/--install chain — accepted values and error path unchanged; RunAsNodeCommand guard on -i preserved.
  • New test file: tempDir, bunEnv, port: 0 registry stub, test.concurrent, and fakeNodeRun usage all match harness conventions.
Extended reasoning...

Overview

The PR fixes a precedence inversion where bun run <target> (and the node shim, bun repl, standalone executables) load bunfig.toml after argv is applied, so bunfig overwrites CLI flags for --define, --loader, --console-depth, --jsx-*, --install/-i/--no-install, --no-macros, and --preload. It adds CliOverrides (nine bools) to ContextData, sets each bool in Arguments::parse where the flag is consumed, and guards the corresponding assignment in Bunfig::parse while still parsing/validating the key. load_preload now prepends config preloads to any existing ctx.preloads instead of replacing them. A new 28-case test file plus four un-FIXME'd cases in preload.test.ts cover the matrix.

Security risks

None. This is CLI/config precedence plumbing; no auth, crypto, network parsing, or untrusted-input handling is touched. The registry stub in the install test is a local Bun.serve({ port: 0 }) that only records paths.

Level of scrutiny

Medium-high. The mechanism is straightforward (a set of bool guards following the existing path_ignore_patterns_from_cli pattern), but it changes user-visible precedence for eight settings on every bun run invocation and alters preload-merge semantics. I checked that read_global_config() excludes RunCommand/AutoCommand/TestCommand, so load_preload is called at most once per process and the prepend cannot double up global+local bunfig preloads. I also confirmed the early-load order is unchanged (ctx.preloads is empty when bunfig loads first, so prepend == replace there).

Other factors

The PR description is unusually thorough — it names the mechanism, enumerates what is and isn't covered (with cross-references to #33198 and #36644), and reports USE_SYSTEM_BUN=1 failure counts. Test coverage is comprehensive: three invocations per setting (early-load with flag, late-load with flag, late-load without flag), plus install-auto in both directions and the node-shim path. The one intentional design choice a maintainer might weigh in on is that jsx.development still follows bunfig even when --jsx-runtime is given, since no flag sets it — the PR calls this out and it matches #36209's early-order behavior, but it's a judgment call.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of any of the four, but they are related:

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

The smol / install.prefer half mentioned under "intentionally not covered" is #38611. It adds the same ctx.cli_overrides / CliOverrides with smol and install_prefer fields, so whichever of the two lands second only needs the field lists merged in context.rs.

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.

1 participant