From c486af0e6abadc15761f11c24d75f4e56679165d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:31:34 +0000 Subject: [PATCH 1/4] bun run: let env NODE_ENV=development override tsconfig jsx dev/prod configure_defines() sampled is_production from the env loader directly but derived is_development only from define.dots, which bun run never populates (its LoadAllWithoutInlining env behavior skips the NODE_ENV define injection). The result was one-directional: env NODE_ENV=production forced the production JSX runtime over tsconfig react-jsxdev, but env NODE_ENV=development could not force the dev runtime over tsconfig react-jsx. Sample is_development from the env loader the same way is_production is. When the define map does carry a NODE_ENV (via --define, or on the bun build path), it still takes precedence and now clears the opposite flag so --define=production continues to beat env=development. --- src/bundler/transpiler.rs | 7 ++- .../transpiler/jsx-tsconfig-react-jsx.test.ts | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index cd24d1c280b..698a3e1293d 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -542,6 +542,10 @@ impl<'a> Transpiler<'a> { let env_loader = self.env_mut(); let mut is_production = env_loader.is_production(); + // `bun run` uses `LoadAllWithoutInlining`, which skips injecting env + // `NODE_ENV` into `define.dots` — so the define-map check below never + // sees it. Sample development here too so it is symmetric with production. + let mut is_development = env_loader.get_node_env() == Some(b"development"); // `load_defines` injects a default `process.env.NODE_ENV`; sample the // explicit sources first so that default isn't mistaken for user intent @@ -572,15 +576,16 @@ impl<'a> Transpiler<'a> { // inside the `&mut self` scope without `unsafe`. self.options.load_defines(self.arena, Some(env_loader))?; - let mut is_development = false; if had_explicit_node_env { if let Some(node_env) = self.options.define.dots.get(b"NODE_ENV".as_slice()) { if !node_env.is_empty() { if let Some(s) = node_env[0].data.value.e_string() { if s.eql_comptime(b"production") { is_production = true; + is_development = false; } else if s.eql_comptime(b"development") { is_development = true; + is_production = false; } } } diff --git a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts index 0b46adc2be8..977f46568dd 100644 --- a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts +++ b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts @@ -28,6 +28,57 @@ const shimFiles = { "m.jsx": `const a =
x
;\nglobalThis.s = a;\n`, }; +// `NODE_ENV` in the environment overrides tsconfig dev/prod for `bun run` in +// both directions; `--define process.env.NODE_ENV` overrides both. +describe.each(["NODE_ENV", "BUN_ENV"])("bun run: env %s overrides tsconfig jsx dev/prod", envVar => { + test.concurrent.each([ + // [tsconfig jsx, env value, expected runtime] + ["react-jsx", "development", "dev jsxDEV"], // env wins (was: tsconfig won -> prod) + ["react-jsx", "production", "prod jsx"], // both agree + ["react-jsxdev", "development", "dev jsxDEV"], // both agree + ["react-jsxdev", "production", "prod jsx"], // env wins + ] as const)(`tsconfig "%s" + ${envVar}=%s -> %s`, async (jsx, envValue, expected) => { + using dir = tempDir("jsx-tsconfig-env", { + ...shimFiles, + "tsconfig.json": JSON.stringify({ compilerOptions: { jsx, jsxImportSource: "shim" } }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "m.jsx"], + env: { ...bunEnv, NODE_ENV: undefined, BUN_ENV: undefined, [envVar]: envValue }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stderr, stdout: stdout.trim(), exitCode }).toEqual({ stderr: "", stdout: expected, exitCode: 0 }); + }); +}); + +describe("bun run: --define process.env.NODE_ENV overrides env NODE_ENV for jsx dev/prod", () => { + test.concurrent.each([ + // [env NODE_ENV, --define value, tsconfig jsx, expected] + ["development", '"production"', "react-jsxdev", "prod jsx"], // --define > env > tsconfig + ["production", '"development"', "react-jsx", "dev jsxDEV"], // --define > env > tsconfig + ] as const)( + "env NODE_ENV=%s + --define process.env.NODE_ENV=%s -> %s", + async (envValue, defineValue, jsx, expected) => { + using dir = tempDir("jsx-tsconfig-define", { + ...shimFiles, + "tsconfig.json": JSON.stringify({ compilerOptions: { jsx, jsxImportSource: "shim" } }), + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "--define", `process.env.NODE_ENV=${defineValue}`, "m.jsx"], + env: { ...bunEnv, NODE_ENV: envValue, BUN_ENV: undefined }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stderr, stdout: stdout.trim(), exitCode }).toEqual({ stderr: "", stdout: expected, exitCode: 0 }); + }, + ); +}); + describe("tsconfig compilerOptions.jsx", () => { test.each([ ["react-jsx", "prod jsx", "shim/jsx-runtime"], From c86d65ab930ddf3f2ea62a17b1b5ec394cea555e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:12:54 +0000 Subject: [PATCH 2/4] test: fix --define test.each title to show expected runtime The 3-placeholder format string bound the trailing -> %s to the tsconfig value instead of the expected output. --- test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts index 977f46568dd..9c3dd67fa1d 100644 --- a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts +++ b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts @@ -60,7 +60,7 @@ describe("bun run: --define process.env.NODE_ENV overrides env NODE_ENV for jsx ["development", '"production"', "react-jsxdev", "prod jsx"], // --define > env > tsconfig ["production", '"development"', "react-jsx", "dev jsxDEV"], // --define > env > tsconfig ] as const)( - "env NODE_ENV=%s + --define process.env.NODE_ENV=%s -> %s", + "env NODE_ENV=%s + --define process.env.NODE_ENV=%s (tsconfig %s) -> %s", async (envValue, defineValue, jsx, expected) => { using dir = tempDir("jsx-tsconfig-define", { ...shimFiles, From 19d49184da69e2d3e18f7fc25ee976dc7a1ed52f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:14:23 +0000 Subject: [PATCH 3/4] drop explanatory comment on is_development env sample --- src/bundler/transpiler.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 698a3e1293d..0217b875dcb 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -542,9 +542,6 @@ impl<'a> Transpiler<'a> { let env_loader = self.env_mut(); let mut is_production = env_loader.is_production(); - // `bun run` uses `LoadAllWithoutInlining`, which skips injecting env - // `NODE_ENV` into `define.dots` — so the define-map check below never - // sees it. Sample development here too so it is symmetric with production. let mut is_development = env_loader.get_node_env() == Some(b"development"); // `load_defines` injects a default `process.env.NODE_ENV`; sample the From ad86bb4a2ec308d4d9e677a2f069c0632373b13a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:16:29 +0000 Subject: [PATCH 4/4] test: drop pre-fix history note from jsx env-override case comment --- test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts index 9c3dd67fa1d..334083e5988 100644 --- a/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts +++ b/test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts @@ -33,7 +33,7 @@ const shimFiles = { describe.each(["NODE_ENV", "BUN_ENV"])("bun run: env %s overrides tsconfig jsx dev/prod", envVar => { test.concurrent.each([ // [tsconfig jsx, env value, expected runtime] - ["react-jsx", "development", "dev jsxDEV"], // env wins (was: tsconfig won -> prod) + ["react-jsx", "development", "dev jsxDEV"], // env wins ["react-jsx", "production", "prod jsx"], // both agree ["react-jsxdev", "development", "dev jsxDEV"], // both agree ["react-jsxdev", "production", "prod jsx"], // env wins