Repository navigation
Conversation
… routes and Workers A Bun.build(), a macro VM, a Bun.serve() HTML route, the dev server and a Bun.Transpiler with a macro run Transpiler::configure_defines with default options on the env loader of the process. So they read the default .env files when the process was told not to: --no-env-file, bunfig env = false, --env-file, bun as node, an executable without dotenv autoload, and the VM of a bun lint plugin. The switch is a fact about the process. run_env_loader now records it in bun_dotenv when the options of a transpiler exclude the default files, and Loader::load reads no default file after that, for any loader. bun build --app gives its transpilers a second loader that nothing fills when the default files are excluded. It now starts as a copy of the command's loader, so a page macro gets --env-file and not .env. Nothing changes when no switch is set.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughDefault ChangesDefault Environment File Loading
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
|
Updated 9:37 AM PT - Oct 18th, 2026
✅ @robobun, your commit f62e76253bf80510d92baaf9d48d39de3bb8380d passed in 🧪 To try this PR locally: bunx bun-pr 45445That installs a local version of the PR into your bun-45445 --bun |
|
Status: ready for review. How it was reproduced (main f2405c3, release build, linux x64): mkdir /tmp/r && cd /tmp/r
printf 'API_KEY=key-from-dot-env\n' > .env
echo 'console.log(process.env.API_KEY);' > entry.ts
cat > build.ts <<'END'
const built = await Bun.build({ entrypoints: ["./entry.ts"], env: "inline" });
console.log(await built.outputs[0].text());
END
bun --no-env-file build.ts
bun build --no-env-file --env=inline entry.ts
The same holds for |
There was a problem hiding this comment.
Beyond the inline findings, I also checked the changed DOTENV_SINGLETON seeding in src/runtime/bake/production.rs — clone_for_worker copies the already-loaded map rather than re-reading files, so an --env-file pipe still works and the no-switch path is unchanged — and the --env-file=X-only case in src/dotenv/env_loader.rs, where dropping .env matches what bun build --env-file=X already does.
Extended reasoning...
The change adds a sticky process-global AtomicBool in src/dotenv/env_loader.rs that Loader::load consults, set from Transpiler::run_env_loader whenever the current options do not load default files, plus a reseeded loader for bun build --app and new tests across four files. It touches no auth, crypto or injection surface, but it does alter which env values reach bundles, Workers and child processes. The inline finding on --test-worker regressing bun test --parallel is a real behavior change, and the process-global flag is a design choice a maintainer should weigh, so this is not an approve.
Findings marked 🟡 are optional suggestions and need no follow-up push.
…opt-out-covers-builds
A `bun test --parallel` worker reads no env file when it starts, because the coordinator read them. It said so with `disable_default_env_files`, which `run_env_loader` now takes for the switch of the process. So with no switch, a `Bun.build()` in a worker stopped reading a default `.env`. The skip is now `env_files_loaded_by_parent`, which only `configure_defines` reads. The coordinator forwards `--no-env-file` to the workers when its own options exclude the default files. Tests: `bun test` and `bun test --parallel` rows with and without a switch. The fixtures no longer depend on `API_KEY` being unset in the environment of the test run, and the `bun build --app` rows have no timeout of their own.
…opt-out-covers-builds
Problem
--no-env-file, bunfigenv = falseor--env-file, a process still reads default.envfiles: in a macro, inBun.build()withenv: "inline"or a prefix, in an HTML route. The values reach bundles, child processes and Workers.Transpiler::configure_defineswith default options on the process's env loader (js_bundle_completion_task.rs:1250).Fix
run_env_loaderrecords the switch in one process flag inbun_dotenv.Loader::load, the only opener of default.envfiles, checks it.bun build --app's second loader is a copy of the command's: page macros get--env-file, not.env.bun test --parallelworker always skips env files at start. That skip is now its own option, and the coordinator forwards the switch.test/cli/run/no-envfile.test.ts(21 new tests, 15 fail without the fix),production.test.ts,js-plugins.test.ts,bundler_compile_autoload.test.ts. Self-reviewed: 32 concerns raised, 18 addressed, 12 in Notes, 2 rejected (both keep a boot-time close).Background
src/dotenv/env_loader.rs) is the map behindprocess.env, child processes and Workers.bun build --app's second loader.Downsides
--env-file=X, bun asnode, or an executable compiled with dotenv autoload off, an in-process build no longer inlines a key that only.envhas.bunstarted by a package script, an executable compiled with autoload on.Notes
No change with no switch. Each of these gives the same output on main and on this branch:
process.chdir(pkg)thenBun.build({ env: "inline" })(inlinespkg/.env), a Worker after a key was appended to.env(sees the key),NODE_ENV=productionset only in.env(a build also inlines.env.production), andNODE_ENV=development bun test(a build inlines.env.local). In abun test --parallelworker: a build afterprocess.chdir(pkg)(inlinespkg/.env), and a Worker after a key was appended to.env(does not see the key). The counts of default-file opens with no switch are also the same in every row of the table below.Default
.envopens after startup (inotifyIN_OPEN, release builds, linux x64)--no-env-fileBun.build({ env: "inline" })env: "disable"Bun.Transpiler#transformwith a macrodevelopment: falsedevelopment: truebun build --appbun test --parallelworkersWith no switch: 3 at boot and 0 after in the first five rows, 9 for 3 Workers, 3 for
bun build --app, 2 for the 2bun test --parallelworkers. Same on both builds.The switches in the tests:
bun --no-env-file,bun run --no-env-file,BUN_OPTIONS, bunfigenv = falseand[env] file = false, bun invoked asnode,bun test --no-env-file,--env-file(only that file), an executable withautoloadDotenv: false, the VM of abun lintplugin, andbun test --parallelwith--no-env-fileand with--env-file. Each of the first eight runs against seven places:env: "inline",env: "PREFIX_*", a build macro, a transform macro, a child process, a Worker, and the first read ofprocess.env. The HTML-route rows have a no-switch control, so a[serve.static] envkey that does not apply cannot pass them.--env-file. A file passed with--env-filereachesBun.build()and its macros, before and after this change: its values are in the loader's map. What changes is that the default files no longer come with it. Underbun --env-file=custom.env build.ts, main inlines a key that only.envhas (console.log("key-from-custom-env", "only-from-dot-env")). This branch givesconsole.log("key-from-custom-env", process.env.ONLY), which is whatbun build --env-file=custom.env --env=inlinegives on both.Native readers. The map is also what native code reads. With
HTTP_PROXYset only in.env, afetchunderbun --no-env-filegoes to the origin beforeBun.build({ env: "inline" })and to the proxy after it on main. On this branch it goes to the origin both times. The child-process place in the tests reads the same map, so there is no separate test forfetch.bun build(the CLI) boots no VM. Its macro VM gets the options of the command, as before. What that macro starts did not: aBun.Transpiler#transformwith a macro of its own read.envunder--no-env-file. The process flag covers it.bun build --app --env-file=Xchanges for page macros: before they got the default.envand not X, now X and not.env, the same as the configuration andbun build --env-file. The copy is made withLoader::clone_for_worker, which does not read the file again, so an--env-filethat is a pipe still works. The app now has every value of the file: aBUN_ENV=developmentin it gives the development bundle, asBUN_ENVin the process environment does on main.bun test --parallel. A worker gets the environment of the coordinator, which read the env files. So a worker reads none when it starts (#40837). It said so withdisable_default_env_files = true, with or without a switch. The first revision of this PR took that for a switch and set the flag in every worker: a test that didprocess.chdir(pkg)and thenBun.build({ env: "inline" })gotpkg/.envunderbun testand not underbun test --parallel. A review comment found it. Now the skip of a worker isenv_files_loaded_by_parent, an option that onlyconfigure_definesreads, and the coordinator adds--no-env-fileto the arguments of a worker when its own options exclude the default files. Fourbun testrows inno-envfile.test.tscover both directions.The race that is not fixed here. With
.envholding onlyNODE_ENV=productionand a.env.productionof 20,000 keys,Bun.build({ env: "inline" })loads the second file on the bundle thread while the JS thread spawns children. On main f2405c3 a child process got a part of the 20,000 keys in 5 of 6 runs (16380, 19916, 8189, 12285, and in one run 2045, 7680 and 18763). This branch does the same: it does not touch that path. Two fixes exist. #37784 gives each build a copy of the loader. The other is to close the default files of a VM's loader when it has booted, which stops the read. That one also ends each behaviour in "No change with no switch" above, so it is a change of default and is not in this PR.Self-review. The review ran before this PR was opened. It asked for a smaller change than the version it read, which closed the default files of every VM's loader after boot. Addressed: the close is gone, the flag is per process and not per loader,
bun build --app --env-filegives page macros the file, the docs sentence says only what is tested, the--approws are inproduction.test.ts, the HTML rows have controls, the lint VM has a test, and a skip of the cwd listing copy is gone (about 3 microseconds, not part of a bug fix). Rejected: two concerns that asked to keep the boot-time close, one for the race above and one because main already has that rule for the security scanner, the script hop and the debugger VM. Both are arguments for the separate change, not for this one. Listed and not changed: the items in "Not covered" and "Also noted" below, and the race above. A second check of the final diff ran both release builds against each other from four angles. It raised 5 more points. Fixed: the new tests did not clearAPI_KEYfrom the environment they pass on. Listed: the two compiled-executable gaps and the--parallelcases in "Not covered", and the switches that no user types (bun asnode, an executable compiled with autoload off, the VM of abun lintplugin), which the Downsides name. Thebun build --approws need--timeouton a debug build, as the older React tests of that file do. CI passes one.Not covered
bun --no-env-file run build, where the script isbun build.ts: the innerbunis a new process with no switch. bunfigenv = falsedoes reach it..envunderBUN_OPTIONS=--no-env-file, under--compile-exec-argv=--no-env-fileand under a bunfigenv = falsein the working directory. One compiled with autoload off does not read an explicit--env-file. The flags of the executable overwrite the options (src/bun.js.rs:16). Same on main.bun test --parallelworker also takes the switch from its own bunfig andBUN_OPTIONS. That differs from the coordinator in two cases.--config=<file>is not passed to the workers, so they load./bunfig.toml(on main too: its[test] preloadruns in each worker). A default.envfile that setsBUN_OPTIONSreaches the workers and not the coordinator. In both, anenv = falseor--no-env-filethat only the workers see now stops their builds from reading a default.env.Bun.buildinlines the loader's map, not the liveprocess.env(Bun.build: inline the live process.env for env: "inline" and env: "PREFIX_*" #41927).Also noted
bun initReact templates set[serve.static] env = "BUN_PUBLIC_*", so a server made from a template is on the HTML-route path with noBun.buildcall of its own.--no-macrosis another process switch that an in-process build does not follow:bun --no-macros build.tsruns the macros of aBun.build()call, wherebun build --no-macrosgiveserror: Macros are disabled. That is a separate switch and a separate change..envdiscovery out ofconfigure_defines, into the VM boot andbun build. The macro VM ofbun buildand the transpilers ofbun build --appdiscover.envafter boot today, so that shape changes them or needs two more discovery sites. Not done here.Measured costs
Release builds of main f0c6cbc and of this branch on top of it, linux x64, from one build directory.
size: text 106486320 -> 106487600 (+1280 B), data and bss unchanged.Loadergets no field. The flag is onestatic AtomicBool.TransformOptionsandoptions::Envget onebooleach.Loader::load80 -> 82,run_env_loader519 -> 525,configure_defines1249 -> 1252,run_as_coordinator(the worker arguments are inlined in it) 3114 -> 3131.perfis not installed here, so there is no dynamic count.Loader::load, one two-field test inrun_env_loader, one morebooltest inconfigure_defines. No allocation, system call or file read is added.Inside
Transpiler::run_env_loader, for oneBun.build({ env: "inline" })in a directory of 23 entries (gdb breakpoints on themi_*entry points, gdbcatch syscall). These counts are from main f2405c3 and the first revision of this PR on top of it.run_env_loaderandLoader::loadhave the same sizes on the current pair:--no-env-file, boot--no-env-file, build threadOther suites run on the release build of this branch:
test/cli/run/env.test.ts,test/cli/test/bun-test.test.ts,test/bundler/{bundler_env,bun-build-api,bundler_edgecase,bundler_html}.test.ts,test/bundler/transpiler/macro-test.test.ts,test/bake/dev/{bundle,html,esm,production}.test.ts,test/js/bun/http/bun-serve-html{,-entry}.test.ts,test/js/web/workers/worker.test.ts,test/js/node/worker_threads/worker_threads.test.ts,test/js/node/process/process.test.js,test/cli/lint/{config-files,js-plugins}.test.ts,test/cli/install/bun-install-env-file.test.ts,test/cli/run/{run-autoinstall,filter-workspace}.test.ts,test/regression/issue/test_env_loader_threading.test.ts. All pass.test/cli/test/parallel.test.tspasses 45 of 45 on the debug build. On a release build--parallel --dots prints one status character per testfails in about half of the runs, on main as well (3 of 6 on main, 4 of 6 here).no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/lint/js-plugins.test.ts, test/bake/dev/production.test.ts