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