Repository navigation
Conversation
…loads `bun build --production` put NODE_ENV=production into the env loader map before `load_process()` ran. `load_process()` then replaced the entry with the NODE_ENV of the shell, so the bundle inlined "development", kept development-only branches, resolved the `development` export condition and compiled JSX with jsxDEV. `bun build --app` had the same order. Add `Loader::override_node_env`. It loads the process environment first, then writes NODE_ENV. It also overwrites BUN_ENV when the process environment has one, because `get_node_env()` reads BUN_ENV first. A later `load_process()` returns at its guard, so the flag is the last writer. `BuildCommand::exec` and the bake production build call it. A build with neither variable exported is unchanged.
|
Updated 7:32 PM PT - Oct 12th, 2026
✅ @autofix-ci[bot], your commit 80fdb0664b0165bbc8f9f7c7d0a018c948ad0c79 passed in 🧪 To try this PR locally: bunx bun-pr 45101That installs a local version of the PR into your bun-45101 --bun |
|
Status: the bug reproduces on main and this PR fixes it. How to reproduce (release build of main echo 'console.log(process.env.NODE_ENV);' > a.js
NODE_ENV=development bun build --production a.js
# main: console.log("development");
# this PR: console.log("production");The build exits 0 with no warning. In the same build, PR: #45101 |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked the two refactor hazards: the unsafe (*this_transpiler.env).map.put to env_mut() swap in build_command.rs dereferences the same pointer under the existing SAFETY contract in transpiler.rs:194, and calling load_process() early is safe against the later run_env_loader because the did_load_process guard makes the second call a no-op, so the flag's write stays last.
Extended reasoning...
The change adds Loader::override_node_env in src/dotenv/env_loader.rs and rewires two production-mode sites (bake production.rs and the CLI build_command.rs) to it; no security-sensitive surface is touched. Two inline findings remain (a .env-file BUN_ENV still outranking the flag under --env, and the bake prerender VM running on a separate Loader), so a human should weigh the precedence decision the PR itself asks about.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughProduction build paths now apply the production ChangesProduction environment override
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
Question for a maintainer: does
bun build --productionoutrank an exportedNODE_ENVorBUN_ENV? This PR says yes. Nearest ruling: #23821: the flag outranks tsconfig "since they're passing the CLI flag explicitly".Problem
NODE_ENV=developmentexported,bun build --productionexits 0 and inlines"development". It keeps development-only branches and emitsjsxDEV.bun build --appdoes the same.NODE_ENVinto the env loader map beforeload_process()copied the process environment over it (src/runtime/cli/build_command.rs:485,src/runtime/bake/production.rs:418).Fix
Loader::override_node_env()loads the process environment first, then writesNODE_ENV, andBUN_ENVwhen one is exported. Both sites call it.load_process()returns at its guard: the flag writes last.test/bundler/bundler_minify.test.ts(16 rows, 13 fail on main),test/bake/dev/production.test.ts(1 row). Also bundler_edgecase, bundler_jsx, bundler_cjs2esm, bundler_html, bundler_env.--compiletest row (13 s per run on a debug build).Background
bun_dotenv::Loader: theNODE_ENVdefine, export conditions, JSX,.env.<mode>selection, macros.get_node_env()readsBUN_ENVfirst.configure_defines()callers. This PR changes nothing insrc/bundler.Downsides
NODE_ENV=staging bun build --productioninlines"production". To keep the old output, replace--productionwith--minify.BUN_ENV=line in a.envfile outranks the flag under--env..text+0 bytes. Without the flag the new method runs 0 times. With nothing exported, 0 of 11 build forms change.Notes
Order of precedence next to
--productionEach cell was run on release builds of main
6c1eb067e3and of this change.--production--env(default)--env inlineor a matching prefixNODE_ENVexported.env.productionis selected (main: environment wins)BUN_ENVexported--define process.env.NODE_ENV=...--define process.env.BUN_ENV=...,BUN_ENVnot exportedNODE_ENV=line in a.envfileBUN_ENV=line in a.envfileBUN_ENVliteral only)--define:K=V(colon spelling)"jsx": "react-jsxdev"NODE_ENV=developmentmade this emitjsxDEV.Sequencing
map.put(b"NODE_ENV", b"production"). Each conflicts with this PR in one hunk. If the conflict takes their side, the bug returns and the new rows fail. The resolution is the one call tooverride_node_env.Bun.buildproductionoption) documents the opposite order as equal to the CLI flag. If this PR merges, that sentence is wrong.ForceNodeEnvplus env map writes insiderun_env_loader. It does not compile on main.Not covered
BUN_ENVfrom a.envfile (row 6 of the table). A write ofBUN_ENVwhen none is exported fixes it. 3f71323 removed that write from Fix assertion failure when using --production flag #23821, and it changes builds with nothing exported:Bun.env.BUN_ENVinside a macro, the children a macro starts, and--env inline --define process.env.BUN_ENV=.... An earlier shape of this PR had it and changed 14 of 19 nothing-exported cells. It needs its own decision.bun build --appruns its build-time VM on a second Loader (src/runtime/bake/production.rs:189). With nothing exported on main, a page that printsBun.env.NODE_ENVat prerender getsundefinedand reads.env.development. This PR fixes the mode that the bundles inline, not the VM. A mode for the VM changes every--appbuild.src/runtime/server/HTMLBundle.rs:466-474):development: falseinlinesprocess.env.BUN_ENVas"development", anddevelopment: { hmr: false }follows an exportedNODE_ENV=production. They run on the Loader of the VM, so they cannot use this method.--compiledoes not imply--productionalthough the help text says so (bun build --compiledoes not actually imply--production? #26373, Unable to compile to single-file executable due to module resolution error #22589, Default a compiled executable to production mode #41506).NODE_ENVfrombunEnvbut notBUN_ENV(test/harness.ts:120). The new rows clear both.Measurements
Release builds (ThinLTO,
bun-profile) of main6c1eb067e3and of this change on top of it. Tools:size,nm -S,objdump,gdb.valgrind,perf,straceandbloatyare not installed on the machine..text: +0 bytes (size -A: 65,818,853 on both).configure_defines/run_env_loader/load_process/get_node_env: +0 / +0 / +0 / +0 bytes.BuildCommand::exec-110, bakebuild_with_vm-62,Loader::override_node_env266 bytes (nm -S).--production: shared functions identical in 4 of 4 symbols (objdump, addresses and ThinLTO hash suffixes stripped). New method entered 0 times acrossbun build/bun run/bun test. Allocator calls in the build set-up window (entry ofBuildCommand::execto the return ofconfigure_defines, main thread): 984 on main, 984 on the PR (gdbhit counts, 3 of 3 runs agree).bun build --productionset-up, 4 environments (nothing,NODE_ENV=development, both exported, nothing with--env inline):load_processcalls 1 -> 2 (the second returns at the guard, the loop runs once),mi_malloc954 / 955 / 957 / 1001 -> 954 / 955 / 958 / 1001,mi_realloc7 / 7 / 7 / 8 -> the same,mi_free27 / 28 / 28 / 50 -> 27 / 28 / 29 / 50, syscalls 7 / 7 / 7 / 33 -> the same. One of nine PR runs of the--env inlinecell counted 35 syscalls.--target bun,--target node,--no-bundle,--compileand run,--env inline,--env 'PUBLIC_*',--env disable, an.htmlentry,--define process.env.NODE_ENV="development", a macro entry): nothing exported: 0 of 11 change vs main. WithNODE_ENV=development/NODE_ENV=staging/BUN_ENV=development: 33 of 33 outputs differ from the nothing-exported output on main, 1 of 33 on the PR (a macro reads the overwrittenBUN_ENV).BUN_ENVdefines,--env-file,${BUN_ENV}expansion, macros that start children, key dumps): 1 of 19 differs from main.Object.entries(Bun.env)inside a macro listsNODE_ENVlast, not first.Loader::override_node_env). Lines changed insrc/bundler+src/resolver: 0. Insrc/dotenv: +9/-0.Tests
test/bundler/bundler_minify.test.ts: 13 of the 16 new rows fail on a release build of main6c1eb067e3and on 1.4.3-canary. The other 3 are pins that pass on both: a define outranks the flag without--env, aBUN_ENVdefine under--env inlineis kept, and a macro sees noBUN_ENVwhen none is exported. All 81 rows of the file pass on the debug build of this branch.test/bake/dev/production.test.ts: the new row fails on a release build of main (serverModeandclientModearedevelopment,jsxDEVis used) and passes on the debug build, run asbun bd test test/bake/dev/production.test.ts --timeout 300000(14 pass, 0 fail). Eachbun build --apptakes 8 to 15 s on a debug ASAN build, above the 5 s default. The older rows of the file time out the same way at the default.edgecase/DeepImportDiamondDAGhit its own 60 s spawn timeout in the full run and passes alone, it does not use the flag).bun teststill setsNODE_ENV=testwhen it is unset and keeps an exportedNODE_ENV=development.NODE_ENVdevelopment, test, staging, two targets, a.envfile): 5 of 6 wrong on a release build of main, 0 of 6 on this change.Old output for a pipeline that exported another value
NODE_ENV=staging bun build --minify a.json this change is byte-identical toNODE_ENV=staging bun build --production a.json main for code without JSX. With JSX it is identical when tsconfig has"jsx": "react-jsx".--define process.env.NODE_ENV='"staging"'next to the flag replaces only that expression:BUN_ENV, export conditions and macros stay production.no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/production.test.ts