From e322649fb1755d88e326252ca2cf649a110aaefa Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:21:11 +0000 Subject: [PATCH 1/6] cli: stop --jsx-* flags from switching the automatic runtime to production Passing any --jsx-* CLI flag (--jsx-import-source, --jsx-fragment, --jsx-factory, --jsx-runtime) populated the api::Jsx struct with a hardcoded development=false. With no flags, Pragma::default() has development=true, so adding an unrelated flag like --jsx-import-source=react silently flipped bun run from react/jsx-dev-runtime (jsxDEV) to react/jsx-runtime (jsx). Default to development=true (matching Pragma::default(), bunfig, and JSBundler) and preserve the prior bunfig value instead of clobbering it. NODE_ENV=production still wins via set_production(), same as before. --- src/runtime/cli/Arguments.rs | 8 ++- .../bundler/transpiler/jsx-production.test.ts | 62 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 9cd13cc7e6d6..8bf13ceff7fb 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -1448,7 +1448,11 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result) -> crate::Result { } } }); + +// Passing a --jsx-* CLI flag must not flip the automatic runtime from +// jsx-dev-runtime (development) to jsx-runtime (production). The flag only +// overrides the field it names; dev/prod selection follows NODE_ENV exactly as +// it does when no --jsx-* flag is present. +describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => { + const shimFiles = { + "node_modules/react/package.json": JSON.stringify({ + name: "react", + version: "1.0.0", + exports: { + ".": "./index.js", + "./jsx-runtime": "./prod.js", + "./jsx-dev-runtime": "./dev.js", + }, + }), + "node_modules/react/index.js": "module.exports = {};", + "node_modules/react/prod.js": + "exports.jsx = () => ({ rt: 'PROD' }); exports.jsxs = exports.jsx; exports.Fragment = {};", + "node_modules/react/dev.js": "exports.jsxDEV = () => ({ rt: 'DEV' }); exports.Fragment = {};", + "a.jsx": "console.log(JSON.stringify(
));", + }; + + const cases: Array<[extraArgs: string[], nodeEnv: string | undefined, expected: "DEV" | "PROD"]> = [ + // Baselines (no --jsx-* flags): dev by default, prod only when NODE_ENV=production. + [[], undefined, "DEV"], + [[], "development", "DEV"], + [[], "production", "PROD"], + // Any --jsx-* flag must not change the dev/prod selection. + [["--jsx-import-source=react"], undefined, "DEV"], + [["--jsx-import-source=react"], "development", "DEV"], + [["--jsx-import-source=react"], "production", "PROD"], + [["--jsx-fragment=Fragment"], undefined, "DEV"], + [["--jsx-fragment=Fragment"], "development", "DEV"], + [["--jsx-fragment=Fragment"], "production", "PROD"], + [["--jsx-factory=h"], undefined, "DEV"], + [["--jsx-runtime=automatic"], undefined, "DEV"], + [["--jsx-runtime=automatic"], "production", "PROD"], + ]; + + for (const [extraArgs, nodeEnv, expected] of cases) { + const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} -> ${expected}`; + test(label, async () => { + using dir = tempDir("jsx-cli-dev", shimFiles); + const env: Record = { ...bunEnv, NODE_ENV: nodeEnv }; + if (nodeEnv === undefined) delete env.NODE_ENV; + await using proc = Bun.spawn({ + cmd: [bunExe(), ...extraArgs, "a.jsx"], + env, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe(JSON.stringify({ rt: expected })); + expect(exitCode).toBe(0); + }); + } +}); From 54a8ef5fd5a97377c8124e2de700544ea07c938b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:34:54 +0000 Subject: [PATCH 2/6] test: move --jsx-* dev-runtime coverage to its own file The existing jsx-production.test.ts describe.concurrent block spawns a heavy fixture (react-dom render + Bun.build + dynamic import) that races the 5s default timeout under debug+ASAN. Keeping the new --jsx-* CLI flag tests in a separate file gives them a clean pass/fail independent of that pre-existing timing. --- test/bundler/transpiler/jsx-cli-flags.test.ts | 62 +++++++++++++++++++ .../bundler/transpiler/jsx-production.test.ts | 62 +------------------ 2 files changed, 63 insertions(+), 61 deletions(-) create mode 100644 test/bundler/transpiler/jsx-cli-flags.test.ts diff --git a/test/bundler/transpiler/jsx-cli-flags.test.ts b/test/bundler/transpiler/jsx-cli-flags.test.ts new file mode 100644 index 000000000000..b37b11295f63 --- /dev/null +++ b/test/bundler/transpiler/jsx-cli-flags.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// Passing a --jsx-* CLI flag must not flip the automatic runtime from +// jsx-dev-runtime (development) to jsx-runtime (production). The flag only +// overrides the field it names; dev/prod selection follows NODE_ENV exactly as +// it does when no --jsx-* flag is present. +describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => { + const shimFiles = { + "node_modules/react/package.json": JSON.stringify({ + name: "react", + version: "1.0.0", + exports: { + ".": "./index.js", + "./jsx-runtime": "./prod.js", + "./jsx-dev-runtime": "./dev.js", + }, + }), + "node_modules/react/index.js": "module.exports = {};", + "node_modules/react/prod.js": + "exports.jsx = () => ({ rt: 'PROD' }); exports.jsxs = exports.jsx; exports.Fragment = {};", + "node_modules/react/dev.js": "exports.jsxDEV = () => ({ rt: 'DEV' }); exports.Fragment = {};", + "a.jsx": "console.log(JSON.stringify(
));", + }; + + const cases: Array<[extraArgs: string[], nodeEnv: string | undefined, expected: "DEV" | "PROD"]> = [ + // Baselines (no --jsx-* flags): dev by default, prod only when NODE_ENV=production. + [[], undefined, "DEV"], + [[], "development", "DEV"], + [[], "production", "PROD"], + // Any --jsx-* flag must not change the dev/prod selection. + [["--jsx-import-source=react"], undefined, "DEV"], + [["--jsx-import-source=react"], "development", "DEV"], + [["--jsx-import-source=react"], "production", "PROD"], + [["--jsx-fragment=Fragment"], undefined, "DEV"], + [["--jsx-fragment=Fragment"], "development", "DEV"], + [["--jsx-fragment=Fragment"], "production", "PROD"], + [["--jsx-factory=h"], undefined, "DEV"], + [["--jsx-runtime=automatic"], undefined, "DEV"], + [["--jsx-runtime=automatic"], "production", "PROD"], + ]; + + for (const [extraArgs, nodeEnv, expected] of cases) { + const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} -> ${expected}`; + test(label, async () => { + using dir = tempDir("jsx-cli-dev", shimFiles); + const env: Record = { ...bunEnv, NODE_ENV: nodeEnv }; + if (nodeEnv === undefined) delete env.NODE_ENV; + await using proc = Bun.spawn({ + cmd: [bunExe(), ...extraArgs, "a.jsx"], + env, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe(JSON.stringify({ rt: expected })); + expect(exitCode).toBe(0); + }); + } +}); diff --git a/test/bundler/transpiler/jsx-production.test.ts b/test/bundler/transpiler/jsx-production.test.ts index 0613502247da..537efad04e7b 100644 --- a/test/bundler/transpiler/jsx-production.test.ts +++ b/test/bundler/transpiler/jsx-production.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe } from "harness"; import path from "path"; // https://github.com/oven-sh/bun/issues/3768 @@ -44,63 +44,3 @@ describe.concurrent("jsx", () => { } } }); - -// Passing a --jsx-* CLI flag must not flip the automatic runtime from -// jsx-dev-runtime (development) to jsx-runtime (production). The flag only -// overrides the field it names; dev/prod selection follows NODE_ENV exactly as -// it does when no --jsx-* flag is present. -describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => { - const shimFiles = { - "node_modules/react/package.json": JSON.stringify({ - name: "react", - version: "1.0.0", - exports: { - ".": "./index.js", - "./jsx-runtime": "./prod.js", - "./jsx-dev-runtime": "./dev.js", - }, - }), - "node_modules/react/index.js": "module.exports = {};", - "node_modules/react/prod.js": - "exports.jsx = () => ({ rt: 'PROD' }); exports.jsxs = exports.jsx; exports.Fragment = {};", - "node_modules/react/dev.js": "exports.jsxDEV = () => ({ rt: 'DEV' }); exports.Fragment = {};", - "a.jsx": "console.log(JSON.stringify(
));", - }; - - const cases: Array<[extraArgs: string[], nodeEnv: string | undefined, expected: "DEV" | "PROD"]> = [ - // Baselines (no --jsx-* flags): dev by default, prod only when NODE_ENV=production. - [[], undefined, "DEV"], - [[], "development", "DEV"], - [[], "production", "PROD"], - // Any --jsx-* flag must not change the dev/prod selection. - [["--jsx-import-source=react"], undefined, "DEV"], - [["--jsx-import-source=react"], "development", "DEV"], - [["--jsx-import-source=react"], "production", "PROD"], - [["--jsx-fragment=Fragment"], undefined, "DEV"], - [["--jsx-fragment=Fragment"], "development", "DEV"], - [["--jsx-fragment=Fragment"], "production", "PROD"], - [["--jsx-factory=h"], undefined, "DEV"], - [["--jsx-runtime=automatic"], undefined, "DEV"], - [["--jsx-runtime=automatic"], "production", "PROD"], - ]; - - for (const [extraArgs, nodeEnv, expected] of cases) { - const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} -> ${expected}`; - test(label, async () => { - using dir = tempDir("jsx-cli-dev", shimFiles); - const env: Record = { ...bunEnv, NODE_ENV: nodeEnv }; - if (nodeEnv === undefined) delete env.NODE_ENV; - await using proc = Bun.spawn({ - cmd: [bunExe(), ...extraArgs, "a.jsx"], - env, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - expect(stdout.trim()).toBe(JSON.stringify({ rt: expected })); - expect(exitCode).toBe(0); - }); - } -}); From 1ac228a69f23bf65eeef4665aee88506b69f4ca9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:36:45 +0000 Subject: [PATCH 3/6] drop explanatory comment on development:true --- src/runtime/cli/Arguments.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 8bf13ceff7fb..239101be0008 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -1448,10 +1448,6 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result Date: Tue, 28 Jul 2026 21:23:41 +0000 Subject: [PATCH 4/6] test: cover the bunfig-merge branch of --jsx-* arg parsing Add bunfig.toml cases (empty, jsx=react-jsx, jsx=react-jsxDEV) so the else arm at Arguments.rs:1467 that now reads prev.development is exercised. Reverting that line alone fails three of the new cases. --- test/bundler/transpiler/jsx-cli-flags.test.ts | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/test/bundler/transpiler/jsx-cli-flags.test.ts b/test/bundler/transpiler/jsx-cli-flags.test.ts index b37b11295f63..a2def8ff2151 100644 --- a/test/bundler/transpiler/jsx-cli-flags.test.ts +++ b/test/bundler/transpiler/jsx-cli-flags.test.ts @@ -23,27 +23,44 @@ describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => "a.jsx": "console.log(JSON.stringify(
));", }; - const cases: Array<[extraArgs: string[], nodeEnv: string | undefined, expected: "DEV" | "PROD"]> = [ + type Case = [ + extraArgs: string[], + nodeEnv: string | undefined, + bunfig: string | undefined, + expected: "DEV" | "PROD", + ]; + const cases: Case[] = [ // Baselines (no --jsx-* flags): dev by default, prod only when NODE_ENV=production. - [[], undefined, "DEV"], - [[], "development", "DEV"], - [[], "production", "PROD"], - // Any --jsx-* flag must not change the dev/prod selection. - [["--jsx-import-source=react"], undefined, "DEV"], - [["--jsx-import-source=react"], "development", "DEV"], - [["--jsx-import-source=react"], "production", "PROD"], - [["--jsx-fragment=Fragment"], undefined, "DEV"], - [["--jsx-fragment=Fragment"], "development", "DEV"], - [["--jsx-fragment=Fragment"], "production", "PROD"], - [["--jsx-factory=h"], undefined, "DEV"], - [["--jsx-runtime=automatic"], undefined, "DEV"], - [["--jsx-runtime=automatic"], "production", "PROD"], + [[], undefined, undefined, "DEV"], + [[], "development", undefined, "DEV"], + [[], "production", undefined, "PROD"], + // Any --jsx-* flag must not change the dev/prod selection (no bunfig: fresh-construct branch). + [["--jsx-import-source=react"], undefined, undefined, "DEV"], + [["--jsx-import-source=react"], "development", undefined, "DEV"], + [["--jsx-import-source=react"], "production", undefined, "PROD"], + [["--jsx-fragment=Fragment"], undefined, undefined, "DEV"], + [["--jsx-fragment=Fragment"], "development", undefined, "DEV"], + [["--jsx-fragment=Fragment"], "production", undefined, "PROD"], + [["--jsx-factory=h"], undefined, undefined, "DEV"], + [["--jsx-runtime=automatic"], undefined, undefined, "DEV"], + [["--jsx-runtime=automatic"], "production", undefined, "PROD"], + // With a bunfig present, opts.jsx is already populated and CLI flags go through the + // merge-with-bunfig branch, which must preserve bunfig's `development` value. + [["--jsx-import-source=react"], undefined, "", "DEV"], + [["--jsx-import-source=react"], "production", "", "PROD"], + [["--jsx-fragment=Fragment"], undefined, "", "DEV"], + [["--jsx-import-source=react"], undefined, 'jsx = "react-jsx"\n', "PROD"], + [["--jsx-import-source=react"], undefined, 'jsx = "react-jsxDEV"\n', "DEV"], ]; - for (const [extraArgs, nodeEnv, expected] of cases) { - const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} -> ${expected}`; + for (const [extraArgs, nodeEnv, bunfig, expected] of cases) { + const bunfigLabel = + bunfig === undefined ? "no bunfig" : bunfig === "" ? "empty bunfig" : `bunfig ${bunfig.trim()}`; + const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} [${bunfigLabel}] -> ${expected}`; test(label, async () => { - using dir = tempDir("jsx-cli-dev", shimFiles); + const files: Record = { ...shimFiles }; + if (bunfig !== undefined) files["bunfig.toml"] = bunfig; + using dir = tempDir("jsx-cli-dev", files); const env: Record = { ...bunEnv, NODE_ENV: nodeEnv }; if (nodeEnv === undefined) delete env.NODE_ENV; await using proc = Bun.spawn({ From ab00356188604d970908137ed91b095a912d8373 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:25:54 +0000 Subject: [PATCH 5/6] [autofix.ci] apply automated fixes --- test/bundler/transpiler/jsx-cli-flags.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/bundler/transpiler/jsx-cli-flags.test.ts b/test/bundler/transpiler/jsx-cli-flags.test.ts index a2def8ff2151..384cf3f1c574 100644 --- a/test/bundler/transpiler/jsx-cli-flags.test.ts +++ b/test/bundler/transpiler/jsx-cli-flags.test.ts @@ -23,12 +23,7 @@ describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => "a.jsx": "console.log(JSON.stringify(
));", }; - type Case = [ - extraArgs: string[], - nodeEnv: string | undefined, - bunfig: string | undefined, - expected: "DEV" | "PROD", - ]; + type Case = [extraArgs: string[], nodeEnv: string | undefined, bunfig: string | undefined, expected: "DEV" | "PROD"]; const cases: Case[] = [ // Baselines (no --jsx-* flags): dev by default, prod only when NODE_ENV=production. [[], undefined, undefined, "DEV"], @@ -54,8 +49,7 @@ describe.concurrent("jsx: --jsx-* CLI flags preserve development runtime", () => ]; for (const [extraArgs, nodeEnv, bunfig, expected] of cases) { - const bunfigLabel = - bunfig === undefined ? "no bunfig" : bunfig === "" ? "empty bunfig" : `bunfig ${bunfig.trim()}`; + const bunfigLabel = bunfig === undefined ? "no bunfig" : bunfig === "" ? "empty bunfig" : `bunfig ${bunfig.trim()}`; const label = `bun ${extraArgs.join(" ") || "(no flags)"} NODE_ENV=${nodeEnv ?? ""} [${bunfigLabel}] -> ${expected}`; test(label, async () => { const files: Record = { ...shimFiles }; From e4812883e727c5a0bac2cd66a8ede90a4199853b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:06:59 +0000 Subject: [PATCH 6/6] ci: retrigger