Skip to content

cli: stop --jsx-* flags from switching the automatic JSX runtime to production - #36209

Open
robobun wants to merge 6 commits into
mainfrom
farm/f5de3a86/jsx-cli-flags-preserve-dev
Open

cli: stop --jsx-* flags from switching the automatic JSX runtime to production#36209
robobun wants to merge 6 commits into
mainfrom
farm/f5de3a86/jsx-cli-flags-preserve-dev

Conversation

@robobun

@robobun robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

Passing any --jsx-* CLI flag (--jsx-import-source, --jsx-fragment, --jsx-factory, --jsx-runtime) to bun <file.jsx> silently flipped the automatic JSX runtime from development (jsxDEV / <source>/jsx-dev-runtime) to production (jsx / <source>/jsx-runtime), even when the flag had nothing to do with dev/prod selection. Setting NODE_ENV=development in the environment did not restore it; only an explicit --define process.env.NODE_ENV='"development"' did.

Repro

d=$(mktemp -d); cd $d
mkdir -p node_modules/react/jsx-runtime node_modules/react/jsx-dev-runtime
echo 'exports.jsx=(t)=>({rt:"PROD"});exports.jsxs=exports.jsx' > node_modules/react/jsx-runtime/index.js
echo 'exports.jsxDEV=(t)=>({rt:"DEV"})' > node_modules/react/jsx-dev-runtime/index.js
printf '{"name":"react"}' > node_modules/react/package.json
echo 'console.log(JSON.stringify(<div/>))' > a.jsx

bun a.jsx                                                  # {"rt":"DEV"}   ok
bun --jsx-import-source=react a.jsx                        # {"rt":"PROD"}  dev lost, even though source == default
bun --jsx-fragment=Fragment a.jsx                          # {"rt":"PROD"}  unrelated flag also flips it
NODE_ENV=development bun --jsx-import-source=react a.jsx   # {"rt":"PROD"}  env NODE_ENV ignored

Cause

src/runtime/cli/Arguments.rs constructs an api::Jsx when any --jsx-* flag is present and hardcoded development: false in both the fresh-construct branch and the merge-with-bunfig branch. With no --jsx-* flags, transform.jsx stays None and BundleOptions::from_api keeps Pragma::default(), which has development: true. So the presence of any flag re-derived JSX options without the development default, and nothing downstream on the bun run path restores it unless NODE_ENV reaches the define map (the LoadAllWithoutInlining env behavior used by bun run skips that injection).

Fix

Default development: true to match Pragma::default(), bunfig, and JSBundler. In the bunfig-merge branch, preserve prev.development instead of overwriting it. NODE_ENV=production still switches to the production runtime via set_production() exactly as before.

Verification

New parameterized test in test/bundler/transpiler/jsx-cli-flags.test.ts spawns bun a.jsx with a shim react package whose jsx-runtime and jsx-dev-runtime report which one was imported, across the matrix of --jsx-* flags and NODE_ENV values. 6 of the 12 cases fail before this change (all --jsx-* + dev cases emit PROD); all 12 pass after.

The test lives in its own file rather than jsx-production.test.ts because the existing describe block in that file spawns a heavy fixture (react-dom render + Bun.build + dynamic import) that races the 5s default timeout under debug+ASAN builds, independently of this change.


[review] gate passed · iteration 3 · 2 files touched

fails on main (without fix)
ASAN without fix: 9 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/jsx-cli-flags.test.ts
bun test v1.4.0 (e4812883e)

test/bundler/transpiler/jsx-cli-flags.test.ts:
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=<unset> [no bunfig] -> DEV [996.95ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=development [no bunfig] -> DEV [757.11ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=production [no bunfig] -> PROD [748.38ms]
64 |         stdout: "pipe",
65 |         stderr: "pipe",
66 |       });
67 |       const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68 |       expect(stderr).toBe("");
69 |       expect(stdout.trim()).toBe(JSON.stringify({ rt: expected }));
                                 ^
error: expect(received).toBe(expected)

Expected: "{"rt":"DEV"}"
Received: "{"rt":"PROD"}"

      at <anonymous> (/workspace/bun/test/bundler/transpiler/jsx-cli-flags.test.ts:69:29)
(fail) jsx: --jsx-* CLI flags pre
... (truncated)

release without fix: 9 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/transpiler/jsx-cli-flags.test.ts:
64 |         stdout: "pipe",
65 |         stderr: "pipe",
66 |       });
67 |       const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68 |       expect(stderr).toBe("");
69 |       expect(stdout.trim()).toBe(JSON.stringify({ rt: expected }));
                                 ^
error: expect(received).toBe(expected)

Expected: "{"rt":"DEV"}"
Received: "{"rt":"PROD"}"

      at <anonymous> (/workspace/bun/test/bundler/transpiler/jsx-cli-flags.test.ts:69:29)
64 |         stdout: "pipe",
65 |         stderr: "pipe",
66 |       });
67 |       const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68 |       expect(stderr).toBe("");
69 |       expect(stdout.trim()).toBe(JSON.stringify({ rt: expected }));
                                 ^
error: expect(received).toBe(expected)

Expected: "{"rt":"DEV"}"
Received: "{"rt":"PROD"}"

      at <anonymous> (/workspace/bun/test/bundler/transpiler/jsx-cli-flags.test.ts:69:29)
64 |         stdout: "pipe",
65 |         stderr: "pipe",
66 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/jsx-cli-flags.test.ts
bun test v1.4.0 (e4812883e)

test/bundler/transpiler/jsx-cli-flags.test.ts:
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=<unset> [no bunfig] -> DEV [1043.19ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=production [no bunfig] -> PROD [1064.69ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun (no flags) NODE_ENV=development [no bunfig] -> DEV [1198.58ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun --jsx-import-source=react NODE_ENV=<unset> [no bunfig] -> DEV [1190.26ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun --jsx-import-source=react NODE_ENV=production [no bunfig] -> PROD [1054.65ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun --jsx-import-source=react NODE_ENV=development [no bunfig] -> DEV [2363.22ms]
(pass) jsx: --jsx-* CLI flags preserve development runtime > bun --jsx-fragment=Fragment NODE_ENV=development [no bunfig] ->
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     e4812883e7
  features     baseline

22 deps, 108 codegen, 1171 objects in 9631ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] gen bindgenv2
[2/1234] fetch zlib
[zlib] up to date
[3/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[4/1234] fetch tinycc
[tinycc] up to date
[5/1234] fetch picohttpparser
[picohttpparser] up to date
[6/1234] gen ErrorCode+*.h
[7/1234] gen .bind.ts → GeneratedBindings.cpp
[8/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[9/1234] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[10/1234] gen ProcessBindingBuffer.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingBuffer.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingBuffer.cpp
[11/1234] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindi
... (truncated)
diff hotspot
src/runtime/cli/Arguments.rs                  |  4 +-
 test/bundler/transpiler/jsx-cli-flags.test.ts | 73 +++++++++++++++++++++++++++
 2 files changed, 75 insertions(+), 2 deletions(-)

gate history · 4 passed · 1 rejected · iteration 3

evidence per changed file
file                                           reads  edits  tests
src/runtime/cli/Arguments.rs                       7      3      0
test/bundler/transpiler/jsx-cli-flags.test.ts      1      2      0

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

CLI JSX override handling now preserves the JSX development setting instead of resetting it. A concurrent bundler test suite covers flag combinations, environment values, optional bunfig.toml, runtime selection, stderr, and exit status.

Changes

JSX runtime mode

Layer / File(s) Summary
Preserve development mode through CLI overrides
src/runtime/cli/Arguments.rs, test/bundler/transpiler/jsx-cli-flags.test.ts
api::Jsx creation and updates now retain the appropriate development value, while parameterized tests verify DEV/PROD runtime selection across CLI and environment configurations.

Possibly related PRs

  • oven-sh/bun#35557: Also addresses preservation of JSX development mode during JSX runtime selection.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise, specific, and matches the main fix in the PR.
Description check ✅ Passed The description covers the problem, fix, and verification, so it mostly satisfies the template.

Comment @coderabbitai help to get the list of available commands.

Comment thread src/runtime/cli/Arguments.rs Outdated
@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:41 PM PT - Jul 28th, 2026

@robobun, your commit e481288 has 1 failures in Build #84493 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36209

That installs a local version of the PR into your bun-36209 executable, so you can run:

bun-36209 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the two-line default flip is correct and now matches Pragma::default(), bunfig's jsx_dev = true, and JSBundler; NODE_ENV=production still reaches set_production() on the run path. The inline nit about test-file placement is non-blocking.

Extended reasoning...

Overview

Two-line change in src/runtime/cli/Arguments.rs: when any --jsx-* CLI flag is present and an api::Jsx is constructed, development now defaults to true (fresh-construct branch) or preserves prev.development (bunfig-merge branch) instead of hardcoding false. A new 12-case parameterized test in test/bundler/transpiler/jsx-cli-flags.test.ts spawns bun a.jsx against a shim react package that reports which runtime entrypoint was imported, across --jsx-import-source / --jsx-fragment / --jsx-factory / --jsx-runtime × NODE_ENV ∈ {unset, development, production}.

Correctness verification

I traced the claim end-to-end:

  • Pragma::default() at src/options_types/jsx.rs:201 sets development: true.
  • Pragma::from_api at jsx.rs:329 copies jsx.development verbatim, so the old false here overrode the default only when a flag was present — exactly the reported inconsistency.
  • Bunfig's parser (src/bunfig/bunfig.rs:953) defaults jsx_dev = true and only sets false for jsx = "react-jsx"; the merge branch now preserves that via prev.development instead of clobbering it.
  • JSBundler at src/runtime/api/JSBundler.rs:191 also defaults development: true.
  • NODE_ENV=production still flips to prod via set_production(true) in src/bundler/transpiler.rs:734/786, which the test's production cases cover.

The finder-raised concern that the merge branch still overwrites prev.side_effects was checked and is pre-existing behavior unrelated to this fix.

Security risks

None. This is transpiler-option defaulting; no untrusted input parsing, no auth/crypto/fs surface.

Level of scrutiny

Low-to-moderate. A two-line default change in CLI arg assembly with a well-argued root cause and three independent in-tree precedents for the new value. The test is hermetic (tempDir, shim package, port-free), drains stdout/stderr/exited concurrently, spreads bunEnv, and asserts exact JSON — it follows harness conventions.

Other factors

The only finding is a nit about test-file placement (new jsx-cli-flags.test.ts vs. appending to jsx-production.test.ts, which the PR description still names). That's organizational and was a deliberate follow-up commit; it doesn't affect correctness. The comment-cop bot's earlier note about an explanatory code comment was addressed in d8f05e4.

Comment thread test/bundler/transpiler/jsx-cli-flags.test.ts
robobun added 3 commits July 28, 2026 21:00
…ction

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.
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.
@robobun
robobun force-pushed the farm/f5de3a86/jsx-cli-flags-preserve-dev branch from d8f05e4 to 1ac228a Compare July 28, 2026 21:00
Comment thread test/bundler/transpiler/jsx-cli-flags.test.ts Outdated
Comment thread src/runtime/cli/Arguments.rs
robobun and others added 2 commits July 28, 2026 21:23
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/bundler/transpiler/jsx-cli-flags.test.ts`:
- Around line 26-49: Expand the `cases` matrix to add explicit development and
production entries for `--jsx-factory=h`, an explicit development entry for
`--jsx-runtime=automatic`, and separated-argument forms such as
`["--jsx-factory", "h"]` with both relevant environment outcomes. Preserve the
expected DEV/PROD behavior established by the existing cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eab96ecd-b9ed-4950-912d-9bd0b16d7a77

📥 Commits

Reviewing files that changed from the base of the PR and between e532ad9 and ab00356.

📒 Files selected for processing (2)
  • src/runtime/cli/Arguments.rs
  • test/bundler/transpiler/jsx-cli-flags.test.ts

Comment thread test/bundler/transpiler/jsx-cli-flags.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the two-line fix aligns --jsx-* CLI flag handling with Pragma::default(), bunfig, and JSBundler (all default development: true), and NODE_ENV=production still overrides via set_production().

What was reviewed:

  • Verified development: true matches Pragma::default() (options_types/jsx.rs:201), bunfig's jsx_dev = true default (bunfig.rs:953), and JSBundler.rs:191.
  • Confirmed the bunfig-merge test cases added in c9f4327 exercise line 1467 — bunfig.rs:990-1013 unconditionally populates ctx.args.jsx for any bunfig, and the jsx = "react-jsx" / "react-jsxDEV" cases map correctly to jsx_dev = false/true.
  • Checked bun build is unaffected: without --jsx-* flags it already used Pragma::default() (dev), and --production still forces jsx.development = false at build_command.rs:461-464.
Extended reasoning...

Overview

Two-line change in src/runtime/cli/Arguments.rs: when any --jsx-* CLI flag is passed and an api::Jsx struct is constructed, the development field now defaults to true (fresh-construct branch, line 1451) or preserves prev.development from bunfig (merge branch, line 1467), instead of hardcoding false in both. A new 17-case parameterized test in test/bundler/transpiler/jsx-cli-flags.test.ts covers the flag × NODE_ENV × bunfig matrix.

Security risks

None. This is JSX transpilation dev/prod-mode selection — no auth, crypto, filesystem, or network surface involved.

Level of scrutiny

Low-to-medium. The source change is 2 lines and mechanically aligns a default with three existing sources of truth (Pragma::default() at options_types/jsx.rs:201, bunfig.rs:953, JSBundler.rs:191). The behavior change is user-visible but strictly a bugfix: previously, adding an unrelated --jsx-* flag silently flipped dev→prod, which was inconsistent with the no-flag path. NODE_ENV=production continues to override via set_production() in transpiler.rs, so the production path is unaffected. I also confirmed bun build behavior is consistent — it already defaulted to dev JSX without --jsx-* flags (via Pragma::default()), and --production still forces development: false explicitly at build_command.rs:461-464.

Other factors

All three of my earlier review threads are resolved: the test-file-location nit was addressed by updating the PR description with a stated reason for the separate file; the missing bunfig-merge-branch coverage was fixed in c9f4327 (author verified reverting line 1467 alone now fails 3 cases); the pre-existing --jsx-side-effects gate omission was acknowledged and reasonably deferred to a follow-up since it needs a different test shape (bun build output assertion vs. runtime import). CodeRabbit's suggestion to expand the matrix was declined with sound reasoning (all flags share one gate, no per-flag branching) and withdrawn. The test follows harness conventions (tempDir, bunEnv spread, concurrent pipe drain, exit-code asserted last, describe.concurrent), and bunEnv already deletes NODE_ENV so the undefined cases are hermetic.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the bunfig-merge coverage gap I flagged is now closed (17-case matrix exercises both the fresh-construct and prev.development branches, and both bunfig react-jsx/react-jsxDEV directions), and the separate-file rationale is documented. Two-line fix aligns with Pragma::default() (development: true at src/options_types/jsx.rs:201); NODE_ENV=production cases in the matrix confirm the prod path is unaffected.

Extended reasoning...

Overview

Two-line change in src/runtime/cli/Arguments.rs: when any --jsx-* CLI flag triggers construction of api::Jsx, default development to true (fresh-construct branch, matching Pragma::default() at src/options_types/jsx.rs:201) and preserve prev.development (bunfig-merge branch) instead of hardcoding false. New test file test/bundler/transpiler/jsx-cli-flags.test.ts with a 17-case concurrent matrix over --jsx-* flags × NODE_ENV × bunfig presence/content, asserting which of jsx-runtime/jsx-dev-runtime a shim react package reports.

Security risks

None. This is CLI option-plumbing that only affects which JSX runtime import specifier the transpiler emits; no untrusted-input parsing, auth, crypto, or filesystem semantics change.

Level of scrutiny

Low-to-medium. The source change is two field initializers whose new values are demonstrably the defaults already used everywhere else (Pragma::default(), bunfig, JSBundler). The test proves both that the regression is fixed and that NODE_ENV=production still selects the production runtime on both branches, so no behavior is lost. I verified bunfig.rs:961-966 accepts the exact "react-jsx"/"react-jsxDEV" spellings the test uses and sets jsx_dev accordingly, so the merge-branch cases exercise prev.development in both true and false states.

Other factors

All prior review threads are resolved: the comment-cop note was addressed (comment dropped), my test-organization nit was answered with a concrete debug+ASAN timeout justification now recorded in the PR description, my coverage-gap finding was fixed in c9f4327 (author confirmed reverting either changed line independently now fails tests), and CodeRabbit withdrew its matrix-expansion suggestion. The pre-existing --jsx-side-effects gate omission I noted was explicitly and reasonably deferred as out-of-scope (different test shape, tracked separately). The test follows harness conventions: tempDir, bunEnv spread with NODE_ENV explicitly deleted when unset, concurrent pipe drain, stderr/stdout asserted before exit code, describe.concurrent for independent subprocess spawns.

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. test/bundler/transpiler/jsx-cli-flags.test.ts passed on every lane that ran tests in both #84370 and #84493.

Remaining red in #84493 is unrelated to this change:

  • test/js/bun/http/proxy-stress-protocol.test.ts ECONNRESET on debian x64-asan (1/102 subtests, HTTP proxy upload stress; reported to main-break triage)
  • everything else is parallel-batch flake that passed when re-run alone (spawn-streaming-stdout, fetch-tls-abortsignal-timeout, 28004 MySQL timeout, bun-serve-date, fastutf8stream-reopen, etc.)

Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants