From f6f91f4923edd14c12ee6b1076a173c97069a6d5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 6 Sep 2026 06:34:26 +0000 Subject: [PATCH 1/5] Boot a compiled executable as a production runtime A bun build --compile executable never set the production flag on the transpiler, so Bun.serve defaulted to development mode and served the dev error page (message, stack, source) for an uncaught route error, and the dotenv loader picked .env.development. Set production in apply_standalone_runtime_flags unless NODE_ENV or BUN_ENV is development or test. configure_defines still honours an explicit NODE_ENV afterwards, and development: true in Bun.serve still wins. --- docs/bundler/executables.mdx | 4 ++ packages/bun-types/serve.d.ts | 5 ++ src/bun.js.rs | 13 +++++ src/bundler/options.rs | 2 +- test/bundler/bundler_compile.test.ts | 49 +++++++++++++++++++ test/bundler/bundler_compile_autoload.test.ts | 19 +++++++ 6 files changed, 91 insertions(+), 1 deletion(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 80d19ef7ea6d..642c88eee5d6 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -364,6 +364,8 @@ Standalone executables can automatically load configuration files from the direc - **`tsconfig.json`** and **`package.json`** loading is **disabled** — these are typically only needed at development time, and the bundler already uses them when compiling - **`.env`** and **`bunfig.toml`** loading is **enabled** — these often contain runtime configuration that may vary per deployment +A standalone executable runs in production mode unless `NODE_ENV` (or `BUN_ENV`) is set to `development` or `test`. So it loads `.env.production`, not `.env.development`, and `Bun.serve()` defaults to `development: false`. + In a future version of Bun, `.env` and `bunfig.toml` may also be disabled by default for more deterministic behavior. @@ -551,6 +553,8 @@ The result is a single file you can deploy anywhere without installing Node.js, Bun serves the frontend assets with the correct MIME types and cache headers. Bun replaces the HTML import with a manifest object that `Bun.serve` uses to serve the pre-bundled assets. +Inside a compiled executable, `Bun.serve()` defaults to `development: false`. An uncaught error in a route handler returns a plain `500` response instead of the development error page, which includes the error message, the stack trace, and the source. To get the development behavior, run the executable with `NODE_ENV=development` or pass `development: true` to `Bun.serve()`. See [Automatic config loading](#automatic-config-loading). + For more on building full-stack applications, see the [full-stack guide](/bundler/fullstack). --- diff --git a/packages/bun-types/serve.d.ts b/packages/bun-types/serve.d.ts index 356c15491724..c51553205045 100644 --- a/packages/bun-types/serve.d.ts +++ b/packages/bun-types/serve.d.ts @@ -765,6 +765,11 @@ declare module "bun" { /** * Whether to render contextual errors with Bun's error page + * + * Inside a standalone executable (`bun build --compile`), the default + * is `false` unless the `NODE_ENV` (or `BUN_ENV`) environment variable + * is `development` or `test`. + * * @default process.env.NODE_ENV !== 'production' */ development?: Development; diff --git a/src/bun.js.rs b/src/bun.js.rs index 2cd789320286..49783ab5cf4c 100644 --- a/src/bun.js.rs +++ b/src/bun.js.rs @@ -25,6 +25,19 @@ pub(crate) fn apply_standalone_runtime_flags( b.resolver.opts.load_package_json = !graph .flags .contains(GraphFlags::DISABLE_AUTOLOAD_PACKAGE_JSON); + + // A compiled executable is a deployment artifact: boot as production (so + // `Bun.serve` defaults to `development: false` and `.env.production` is + // the dotenv suffix) unless the process env asks for development or test. + // `configure_defines` runs after this and still honours an explicit + // `NODE_ENV` / `BUN_ENV`. + let env = b.env_mut(); + bun_core::handle_oom(env.load_process()); + let node_env = env.get_node_env().unwrap_or(b""); + if node_env != b"development" && node_env != b"test" { + b.options.set_production(true); + b.resolver.opts.set_production(true); + } } #[cold] diff --git a/src/bundler/options.rs b/src/bundler/options.rs index fb2eefed2506..2e514a03bb4c 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -1406,7 +1406,7 @@ impl<'a> BundleOptions<'a> { self.rewrite_jest_for_tests } - pub(crate) fn set_production(&mut self, value: bool) { + pub fn set_production(&mut self, value: bool) { if self.force_node_env == ForceNodeEnv::Unspecified { self.production = value; self.jsx.development = !value; diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index ac147a1d9dbd..03124c03ee1d 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -1298,6 +1298,55 @@ describe("bundler", () => { }, run: { stdout: JSON.stringify({ server: "client sees the text", clientHasLiteral: true, clientHasBunfs: false }) }, }); + + // A compiled executable defaults Bun.serve to production. The dev error page + // (message, stack, source) must not be served unless the user opts in. + const serveDevelopmentEntry = (options: string) => /* js */ ` + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + ${options} + routes: { "/boom": () => { throw new Error("secret-stack-detail"); } }, + fetch() { return new Response("nf", { status: 404 }); }, + }); + const res = await fetch(server.url + "boom", { headers: { accept: "text/html" } }); + const body = await res.text(); + console.log(JSON.stringify({ + development: server.development, + status: res.status, + contentType: res.headers.get("content-type"), + leaks: body.includes("secret-stack-detail"), + })); + server.stop(true); + process.exit(0); + `; + const serveProduction = JSON.stringify({ + development: false, + status: 500, + contentType: "text/plain", + leaks: false, + }); + const serveDevelopment = JSON.stringify({ + development: true, + status: 500, + contentType: "text/html;charset=utf-8", + leaks: true, + }); + itBundled("compile/ServeDefaultsToProduction", { + compile: true, + files: { "/entry.ts": serveDevelopmentEntry("") }, + run: [ + { stdout: serveProduction }, + { env: { NODE_ENV: "production" }, stdout: serveProduction }, + { env: { NODE_ENV: "development" }, stdout: serveDevelopment }, + { env: { BUN_ENV: "development" }, stdout: serveDevelopment }, + ], + }); + itBundled("compile/ServeExplicitDevelopment", { + compile: true, + files: { "/entry.ts": serveDevelopmentEntry("development: true,") }, + run: { stdout: serveDevelopment }, + }); itBundled("compile/Utf8", { compile: true, files: { diff --git a/test/bundler/bundler_compile_autoload.test.ts b/test/bundler/bundler_compile_autoload.test.ts index 6b4338c26985..f19b78e8ad91 100644 --- a/test/bundler/bundler_compile_autoload.test.ts +++ b/test/bundler/bundler_compile_autoload.test.ts @@ -81,6 +81,25 @@ describe("bundler", () => { }, }); + // A standalone executable runs as production, so it loads .env.production + // unless NODE_ENV asks for development. + itBundled("compile/AutoloadDotenvProductionSuffix", { + compile: true, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env.development": `TEST_VAR=from_development`, + "/.env.production": `TEST_VAR=from_production`, + }, + run: [ + { stdout: "from_production", setCwd: true }, + { stdout: "from_development", setCwd: true, env: { NODE_ENV: "development" } }, + ], + }); + // Test that bunfig.toml is loaded by default (preload is executed) itBundled("compile/AutoloadBunfigDefault", { compile: true, From e9ceedb9106c736b94777ee93a760abeacba2a37 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 6 Sep 2026 07:32:26 +0000 Subject: [PATCH 2/5] docs: state the compiled executable NODE_ENV default precisely --- docs/bundler/executables.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 642c88eee5d6..f9c524bf6871 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -364,7 +364,7 @@ Standalone executables can automatically load configuration files from the direc - **`tsconfig.json`** and **`package.json`** loading is **disabled** — these are typically only needed at development time, and the bundler already uses them when compiling - **`.env`** and **`bunfig.toml`** loading is **enabled** — these often contain runtime configuration that may vary per deployment -A standalone executable runs in production mode unless `NODE_ENV` (or `BUN_ENV`) is set to `development` or `test`. So it loads `.env.production`, not `.env.development`, and `Bun.serve()` defaults to `development: false`. +A standalone executable treats an unset `NODE_ENV` (or `BUN_ENV`) as `production`. It loads `.env.production`, not `.env.development`, and `Bun.serve()` defaults to `development: false`. Set `NODE_ENV=development` in the environment to get the development behavior. In a future version of Bun, `.env` and `bunfig.toml` may also be disabled by default for more deterministic behavior. From 647c843352ffbcddca358fa0a20f0428cd57177b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 6 Sep 2026 07:48:46 +0000 Subject: [PATCH 3/5] Cover NODE_ENV=test and other values in the compiled serve default test --- docs/bundler/executables.mdx | 2 +- test/bundler/bundler_compile.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index f9c524bf6871..a787bb25ceb9 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -364,7 +364,7 @@ Standalone executables can automatically load configuration files from the direc - **`tsconfig.json`** and **`package.json`** loading is **disabled** — these are typically only needed at development time, and the bundler already uses them when compiling - **`.env`** and **`bunfig.toml`** loading is **enabled** — these often contain runtime configuration that may vary per deployment -A standalone executable treats an unset `NODE_ENV` (or `BUN_ENV`) as `production`. It loads `.env.production`, not `.env.development`, and `Bun.serve()` defaults to `development: false`. Set `NODE_ENV=development` in the environment to get the development behavior. +A standalone executable runs as `production` unless `NODE_ENV` (or `BUN_ENV`) is `development` or `test`. An unset variable, or any other value, counts as production. It loads `.env.production`, not `.env.development`, and `Bun.serve()` defaults to `development: false`. Set `NODE_ENV=development` in the environment to get the development behavior. In a future version of Bun, `.env` and `bunfig.toml` may also be disabled by default for more deterministic behavior. diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 03124c03ee1d..86a266b89d97 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -1340,6 +1340,8 @@ describe("bundler", () => { { env: { NODE_ENV: "production" }, stdout: serveProduction }, { env: { NODE_ENV: "development" }, stdout: serveDevelopment }, { env: { BUN_ENV: "development" }, stdout: serveDevelopment }, + { env: { NODE_ENV: "test" }, stdout: serveDevelopment }, + { env: { NODE_ENV: "staging" }, stdout: serveProduction }, ], }); itBundled("compile/ServeExplicitDevelopment", { From ceaa4aa5cd27b6ccede0048d3ff44884f5b5fc8f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 6 Sep 2026 07:49:39 +0000 Subject: [PATCH 4/5] Shorten the standalone production comment --- src/bun.js.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bun.js.rs b/src/bun.js.rs index 49783ab5cf4c..4d02470bd1b2 100644 --- a/src/bun.js.rs +++ b/src/bun.js.rs @@ -26,11 +26,7 @@ pub(crate) fn apply_standalone_runtime_flags( .flags .contains(GraphFlags::DISABLE_AUTOLOAD_PACKAGE_JSON); - // A compiled executable is a deployment artifact: boot as production (so - // `Bun.serve` defaults to `development: false` and `.env.production` is - // the dotenv suffix) unless the process env asks for development or test. - // `configure_defines` runs after this and still honours an explicit - // `NODE_ENV` / `BUN_ENV`. + // A compiled executable boots as production unless NODE_ENV/BUN_ENV says otherwise. let env = b.env_mut(); bun_core::handle_oom(env.load_process()); let node_env = env.get_node_env().unwrap_or(b""); From ae117b5ff05b0f2cb34817217a2c69775925de0f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:17:20 +0000 Subject: [PATCH 5/5] Clear BUN_ENV in the compiled production default tests --- test/bundler/bundler_compile.test.ts | 16 +++++++++------- test/bundler/bundler_compile_autoload.test.ts | 4 ++-- test/bundler/expectBundled.ts | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 86a266b89d97..06cad99bfff9 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -1332,22 +1332,24 @@ describe("bundler", () => { contentType: "text/html;charset=utf-8", leaks: true, }); + // bunEnv clears NODE_ENV but not BUN_ENV. Clear both so the shell cannot leak in. + const nodeEnv = (env: Record = {}) => ({ NODE_ENV: undefined, BUN_ENV: undefined, ...env }); itBundled("compile/ServeDefaultsToProduction", { compile: true, files: { "/entry.ts": serveDevelopmentEntry("") }, run: [ - { stdout: serveProduction }, - { env: { NODE_ENV: "production" }, stdout: serveProduction }, - { env: { NODE_ENV: "development" }, stdout: serveDevelopment }, - { env: { BUN_ENV: "development" }, stdout: serveDevelopment }, - { env: { NODE_ENV: "test" }, stdout: serveDevelopment }, - { env: { NODE_ENV: "staging" }, stdout: serveProduction }, + { env: nodeEnv(), stdout: serveProduction }, + { env: nodeEnv({ NODE_ENV: "production" }), stdout: serveProduction }, + { env: nodeEnv({ NODE_ENV: "development" }), stdout: serveDevelopment }, + { env: nodeEnv({ BUN_ENV: "development" }), stdout: serveDevelopment }, + { env: nodeEnv({ NODE_ENV: "test" }), stdout: serveDevelopment }, + { env: nodeEnv({ NODE_ENV: "staging" }), stdout: serveProduction }, ], }); itBundled("compile/ServeExplicitDevelopment", { compile: true, files: { "/entry.ts": serveDevelopmentEntry("development: true,") }, - run: { stdout: serveDevelopment }, + run: { env: nodeEnv(), stdout: serveDevelopment }, }); itBundled("compile/Utf8", { compile: true, diff --git a/test/bundler/bundler_compile_autoload.test.ts b/test/bundler/bundler_compile_autoload.test.ts index f19b78e8ad91..9a9de6db7d6e 100644 --- a/test/bundler/bundler_compile_autoload.test.ts +++ b/test/bundler/bundler_compile_autoload.test.ts @@ -95,8 +95,8 @@ describe("bundler", () => { "/.env.production": `TEST_VAR=from_production`, }, run: [ - { stdout: "from_production", setCwd: true }, - { stdout: "from_development", setCwd: true, env: { NODE_ENV: "development" } }, + { stdout: "from_production", setCwd: true, env: { NODE_ENV: undefined, BUN_ENV: undefined } }, + { stdout: "from_development", setCwd: true, env: { NODE_ENV: "development", BUN_ENV: undefined } }, ], }); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index dbb9b72a3372..a831bc33654c 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -424,7 +424,8 @@ export interface BundlerTestRunOptions { */ errorLineMatch?: RegExp; - env?: Record; + /** An `undefined` value removes the variable inherited from `bunEnv`. */ + env?: Record; runtime?: "bun" | "node";