Skip to content

bun build --production: write NODE_ENV after the process environment loads - #45101

Open
robobun wants to merge 2 commits into
mainfrom
robobun/5001abba/build-production-node-env
Open

robobun wants to merge 2 commits into
mainfrom
robobun/5001abba/build-production-node-env

Conversation

@robobun

@robobun robobun commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Question for a maintainer: does bun build --production outrank an exported NODE_ENV or BUN_ENV? This PR says yes. Nearest ruling: #23821: the flag outranks tsconfig "since they're passing the CLI flag explicitly".

Problem

  • With NODE_ENV=development exported, bun build --production exits 0 and inlines "development". It keeps development-only branches and emits jsxDEV. bun build --app does the same.
  • The flag wrote NODE_ENV into the env loader map before load_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 writes NODE_ENV, and BUN_ENV when one is exported. Both sites call it.
  • Correct because a later load_process() returns at its guard: the flag writes last.
  • Verified: 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.
  • Self-reviewed: 20 concerns raised, 19 addressed. Rejected: a --compile test row (13 s per run on a debug build).

Background

  • Every mode consumer reads one map in bun_dotenv::Loader: the NODE_ENV define, export conditions, JSX, .env.<mode> selection, macros. get_node_env() reads BUN_ENV first.
  • Found by fuzzing, no user report. Supersedes stale-closed bun build --production: override ambient NODE_ENV instead of defaulting #33855, which changed shared code behind 14 configure_defines() callers. This PR changes nothing in src/bundler.

Downsides

  • Behavior change: NODE_ENV=staging bun build --production inlines "production". To keep the old output, replace --production with --minify.
  • Still wrong, as on main: a BUN_ENV= line in a .env file outranks the flag under --env.
  • Cost (release builds): .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 --production

Each cell was run on release builds of main 6c1eb067e3 and of this change.

Given next to --production No --env (default) --env inline or a matching prefix
NODE_ENV exported flag wins (main: environment wins) flag wins, .env.production is selected (main: environment wins)
BUN_ENV exported flag wins (main: environment wins) flag wins (main: environment wins)
--define process.env.NODE_ENV=... define wins, for that expression only flag wins, on main too (#44882 owns this order)
--define process.env.BUN_ENV=..., BUN_ENV not exported define wins define wins
NODE_ENV= line in a .env file file is not read flag wins, on main too
BUN_ENV= line in a .env file bundle not affected, a macro reads the value of the file file wins, as on main (prefix: a development build, inline: the BUN_ENV literal only)
--define:K=V (colon spelling) dropped by the argument parser (#40558, #40560) same
tsconfig "jsx": "react-jsxdev" flag wins (#23821). On main an exported NODE_ENV=development made this emit jsxDEV. same

Sequencing

Not covered

  • BUN_ENV from a .env file (row 6 of the table). A write of BUN_ENV when 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_ENV inside 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 --app runs its build-time VM on a second Loader (src/runtime/bake/production.rs:189). With nothing exported on main, a page that prints Bun.env.NODE_ENV at prerender gets undefined and reads .env.development. This PR fixes the mode that the bundles inline, not the VM. A mode for the VM changes every --app build.
  • Bun.serve HTML routes (src/runtime/server/HTMLBundle.rs:466-474): development: false inlines process.env.BUN_ENV as "development", and development: { hmr: false } follows an exported NODE_ENV=production. They run on the Loader of the VM, so they cannot use this method.
  • --compile does not imply --production although the help text says so (bun build --compile does 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).
  • The test harness removes NODE_ENV from bunEnv but not BUN_ENV (test/harness.ts:120). The new rows clear both.

Measurements

Release builds (ThinLTO, bun-profile) of main 6c1eb067e3 and of this change on top of it. Tools: size, nm -S, objdump, gdb. valgrind, perf, strace and bloaty are not installed on the machine.

  • release binary .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, bake build_with_vm -62, Loader::override_node_env 266 bytes (nm -S).
  • without --production: shared functions identical in 4 of 4 symbols (objdump, addresses and ThinLTO hash suffixes stripped). New method entered 0 times across bun build / bun run / bun test. Allocator calls in the build set-up window (entry of BuildCommand::exec to the return of configure_defines, main thread): 984 on main, 984 on the PR (gdb hit counts, 3 of 3 runs agree).
  • bun build --production set-up, 4 environments (nothing, NODE_ENV=development, both exported, nothing with --env inline): load_process calls 1 -> 2 (the second returns at the guard, the loop runs once), mi_malloc 954 / 955 / 957 / 1001 -> 954 / 955 / 958 / 1001, mi_realloc 7 / 7 / 7 / 8 -> the same, mi_free 27 / 28 / 28 / 50 -> 27 / 28 / 29 / 50, syscalls 7 / 7 / 7 / 33 -> the same. One of nine PR runs of the --env inline cell counted 35 syscalls.
  • outputs, 11 forms (default, --target bun, --target node, --no-bundle, --compile and run, --env inline, --env 'PUBLIC_*', --env disable, an .html entry, --define process.env.NODE_ENV="development", a macro entry): nothing exported: 0 of 11 change vs main. With NODE_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 overwritten BUN_ENV).
  • a second set of 19 nothing-exported cells (BUN_ENV defines, --env-file, ${BUN_ENV} expansion, macros that start children, key dumps): 1 of 19 differs from main. Object.entries(Bun.env) inside a macro lists NODE_ENV last, not first.
  • new pub fn: 1 (Loader::override_node_env). Lines changed in src/bundler + src/resolver: 0. In src/dotenv: +9/-0.

Tests

  • test/bundler/bundler_minify.test.ts: 13 of the 16 new rows fail on a release build of main 6c1eb067e3 and on 1.4.3-canary. The other 3 are pins that pass on both: a define outranks the flag without --env, a BUN_ENV define under --env inline is kept, and a macro sees no BUN_ENV when 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 (serverMode and clientMode are development, jsxDEV is used) and passes on the debug build, run as bun bd test test/bake/dev/production.test.ts --timeout 300000 (14 pass, 0 fail). Each bun build --app takes 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.
  • Also run on the debug build: bundler_jsx 64/64, bundler_cjs2esm 113/113, bundler_html 24/24, bundler_env 7/7, jsx-tsconfig-react-jsx 7/7, regression/issue/19652 1/1, bundler_edgecase 181 pass and 1 fail (edgecase/DeepImportDiamondDAG hit its own 60 s spawn timeout in the full run and passes alone, it does not use the flag).
  • bun test still sets NODE_ENV=test when it is unset and keeps an exported NODE_ENV=development.
  • A repro script with 6 forms (NODE_ENV development, test, staging, two targets, a .env file): 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.js on this change is byte-identical to NODE_ENV=staging bun build --production a.js on 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

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

robobun commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:32 PM PT - Oct 12th, 2026

✅ @autofix-ci[bot], your commit 80fdb0664b0165bbc8f9f7c7d0a018c948ad0c79 passed in Build #124952! 🎉


🧪   To try this PR locally:

bunx bun-pr 45101

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

bun-45101 --bun

@robobun

robobun commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: the bug reproduces on main and this PR fixes it.

How to reproduce (release build of main 6c1eb067e3, also 1.4.3, 1.4.2, 1.3.14):

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, if (process.env.NODE_ENV !== "production") branches stay in the bundle and JSX compiles to jsxDEV. NODE_ENV=test and NODE_ENV=staging give "test" and "staging".

PR: #45101

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

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.

Comment thread src/dotenv/env_loader.rs
Comment thread src/runtime/bake/production.rs
@coderabbitai

coderabbitai Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 191ebe64-01bb-4967-9cbc-95ab781bf400
📥 Commits

Reviewing files that changed from the base of the PR and between 110efa0 and 80fdb06.

📒 Files selected for processing (5)
  • src/dotenv/env_loader.rs
  • src/runtime/bake/production.rs
  • src/runtime/cli/build_command.rs
  • test/bake/dev/production.test.ts
  • test/bundler/bundler_minify.test.ts

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

Walkthrough

Production build paths now apply the production NODE_ENV value through a loader method after loading process variables. The method updates BUN_ENV only when that key exists. Regression tests cover ambient-variable precedence, server and client output, dotenv selection, macros, and explicit defines.

Changes

Production environment override

Layer / File(s) Summary
Loader override and production wiring
src/dotenv/env_loader.rs, src/runtime/bake/production.rs, src/runtime/cli/build_command.rs
The loader method loads process variables, updates BUN_ENV when present, and sets NODE_ENV. Production build paths call the method and propagate errors.
Production build regression tests
test/bundler/bundler_minify.test.ts, test/bake/dev/production.test.ts
Tests cover ambient environment values, no-bundle and full-stack builds, macros, dotenv selection, explicit defines, and production values in server and client output.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 80fdb

The reviewed change addresses exported environment values in production builds. No actionable issue is established; normal checks can proceed, with .env-supplied BUN_ENV remaining outside the validated cases.

Pre-merge checks · Passed 4
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the main change: writing NODE_ENV after loading the process environment for bun build --production.
Description check Passed The description explains the problem, fix, behavior changes, limitations, and verification results. It provides the information required by both template sections, although it uses Problem and Fix hea…
Linked Issues check Passed Issue #33855 is closed and supplies historical context only. No active directly linked issue remains, so no linked-issue coding requirements apply. The changes implement the current PR intent by apply…
Out of Scope Changes check Passed The diff is limited to production-mode environment precedence and related tests. The loader change and both call-site changes support the stated PR intent. The bundler and bake tests validate exported…
  • 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

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

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.

2 participants