Repository navigation
Conversation
bun build (bundled) with a tsconfig that sets "jsx": "react-jsx" or "react-jsxdev" lost its dev/prod selection as soon as any --jsx-* CLI flag (or a Bun.build jsx option) was passed: bundle_v2 overwrote the per-file tsconfig-merged jsx.development with the global options.jsx.development whenever force_node_env was Unspecified. The --no-bundle path reads resolve_result.jsx directly and was already correct. Leave task.jsx.development alone on Unspecified (it is the resolver's tsconfig-merged value). To keep --production and an explicit NODE_ENV=production overriding tsconfig like before, have those paths set force_node_env=Production, mirroring the existing Development handling.
WalkthroughChangesJSX runtime selection
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 9:40 PM PT - Jul 28th, 2026
⏳ @robobun, your commit e7f8d3f is still building in
|
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
…v::jsx_development_override()
|
Re the duplicate flag: #31651 adds the same two |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/bundler/bundle_v2.rs:2649-2655— Bake's dev server relies on the removedUnspecified => t.options.jsx.developmentfallback:bake/mod.rs:301andbake_body.rs:1248setout.options.jsx.development = mode == Mode::Developmentafterconfigure_defines()and never setforce_node_env, so that write is now dead for every disk-resolved module. With a stocktsconfig.json"jsx": "react-jsx"(or ambientNODE_ENV=productionvia the new transpiler.rs:598-599), the bake dev server now servesreact/jsx-runtimewhile React Fast Refresh (mod.rs:240) is enabled. Bake should setout.options.force_node_env/resolver.opts.force_node_envfrommodealongside those two lines, mirroring what this PR adds inbuild_command.rs:464-465.Extended reasoning...
What changed
The four
bundle_v2.rssites used to doUnspecified => t.options.jsx.development, reading the bundler-globaloptions.jsx.developmentat parse-task creation time. This PR changes them toUnspecified => {}, keeping whateverresolve_result.jsx.developmentthe resolver produced. That is correct forbun build/Bun.build, which this PR audits — but bake'sFramework::init_transpilerwas another consumer of the removed fallback and is not updated here.At
src/runtime/bake/mod.rs:297-301(mirrored atsrc/runtime/bake/bake_body.rs:1243-1248):out.sync_resolver_opts(); out.configure_linker(); out.configure_defines()?; out.options.jsx.development = mode == Mode::Development;
Bake never touches
force_node_env. Line 301/1248 was load-bearing only via the removedUnspecifiedarm; after this PR nothing reads it for disk-resolved modules.Trigger (a): tsconfig
"jsx": "react-jsx"— no env neededThis is the default
tsc --init/ create-react-app / Vite React config.- Bake creates the transpiler with
TransformOptions::default()(mod.rs:214-219). No NODE_ENV/BUN_ENV in env, no--define, so inconfigure_defines()had_explicit_node_envstays false andforce_node_envremainsUnspecified. - The resolver builds
resolve_result.jsxper file viaTSConfigJSON::merge_jsx.RUNTIME_MAP[b"react-jsx"].development = Some(false)setsJsxField::Development(tsconfig_json.rs:467-474), somerge_jsxwritesout.development = false(tsconfig_json.rs:245-247) for every source file under that tsconfig — regardless ofresolver.opts.jsx.development, so the latersync_resolver_opts()at mod.rs:348 does not help. DevServerdrivesBundleV2(DevServer.rs:3303). Atenqueue_entry_item(bundle_v2.rs:2649-2655) andresolve_import_records(bundle_v2.rs:6422-6427),force_node_env == Unspecified→{}→task.jsx.developmentstaysfalse.- Before this PR: the same arm read
bundler.options.jsx.development, i.e. bake's line-301true→react/jsx-dev-runtime. After this PR: →react/jsx-runtime.
Trigger (b): ambient
NODE_ENV=production- Bake's
env.behaviordefaults todisable, whose arm inrun_env_loadercallsenv.load_process()(transpiler.rs:785-786), so ambient processNODE_ENV=productionmakesis_productiontrue at transpiler.rs:544. - The new lines at transpiler.rs:598-599 set
options.force_node_env = Production. - Bake then writes
options.jsx.development = true(line 301) but leavesforce_node_env = Production. - All four modified
bundle_v2sites now matchProduction => task.jsx.development = false. Before this PRforce_node_envwas neverProductionhere (line 595 only calledset_production(true)), so theUnspecifiedarm read bake'strue.
Note that guarding transpiler.rs:598-599 on
force_node_env == Unspecified(a plausible fix for the HTMLBundle case) does not help trigger (b) — bake'sforce_node_envis genuinelyUnspecifiedbeforeconfigure_defines()runs, so the guarded write would still fire — and does nothing for trigger (a), which never reaches those lines.Impact
The bake dev server (
mode == Mode::Development) enablesreact_fast_refreshat mod.rs:240-242 andhot_module_reloadingat mod.rs:236, but now emits the production JSX runtime (jsx/jsxs, no source-location args) instead ofjsxDEV. React's error overlay and Fast Refresh source locations are degraded/broken on the standard React tsconfig with no user action.test/bake/dev/react-spa.test.tshas no tsconfig so would not catch trigger (a), and the PR's green-suite list does not include anybakesuite.Fix
Bake should set
force_node_envalongsidejsx.development, exactly as this PR does for--productioninbuild_command.rs:464-465:out.configure_defines()?; out.options.jsx.development = mode == Mode::Development; out.options.force_node_env = match mode { Mode::Development => options::ForceNodeEnv::Development, _ => options::ForceNodeEnv::Production, }; out.resolver.opts.force_node_env = out.options.force_node_env;
at both
bake/mod.rs:301andbake_body.rs:1248. This makes bake's explicit mode authoritative under the new precedence (force_node_env > tsconfig), covers both triggers, and is symmetric with the CLI change already in this PR. Per REVIEW.md ("Before changing anything shared, enumerate every consumer" / "fix the whole class in the same PR"), the fourbundle_v2sites andconfigure_definesare shared acrossbuild_command,JSBundler,HTMLBundle, and bake — bake is a sibling caller of the same class and belongs in this PR. - Bake creates the transpiler with
-
🔴
src/bundler/bundle_v2.rs:6421-6427— There's a fifth sibling site this PR misses:enqueue_parse_taskat bundle_v2.rs:3478 doestask.jsx = self.transpiler_for_target(known_target).options.jsx.clone()right afterParseTask::initsettask.jsx = resolve_result.jsx, so it still clobbers the tsconfig-merged value and never consultsforce_node_env. That path is hit viarun_resolver(:2414) from theonResolve-pluginNoMatchfallback (:4514), so with tsconfig"react-jsxdev"+ any--jsx-*flag + anyonResolveplugin, the entry (via fixed :2649) now emitsjsx-dev-runtimewhile every imported.jsx(via :3478) emitsjsx-runtime— a mixed dev/prod bundle where before this PR both paths at least agreed. Drop the :3478 overwrite and apply the sameforce_node_envmatch as the four fixed sites (enqueue_parse_task2at :3540 has the same.clone()shape).Extended reasoning...
What the bug is
The PR fixes four
bundle_v2sites wheretask.jsx.developmentwas overwritten with the globaloptions.jsx.developmentonForceNodeEnv::Unspecified, but there is a fifth sibling that was not touched.enqueue_parse_taskat bundle_v2.rs:3478 does:task.jsx = self.transpiler_for_target(known_target).options.jsx.clone();
This runs immediately after
ParseTask::init(&resolve_result, ..)(line 3470) has already settask.jsx = resolve_result.jsx.clone()(ParseTask.rs:262), so it wholesale-overwrites the resolver's tsconfig-merged JSX pragma — including.development— with the global CLI/API default. It also does not consultforce_node_envat all.Code path that triggers it
enqueue_parse_taskis reached fromrun_resolver(bundle_v2.rs:2414 disk path, :2172 file-map path).run_resolveris invoked at bundle_v2.rs:4514, inside theResolveValue::NoMatcharm of theonResolve-plugin fallback, for non-entry-point imports in thefilenamespace. Entry points in that same arm go throughenqueue_entry_item(:4499), which is one of the four sites this PR fixed.Without a plugin, the very same non-entry import goes through the site this PR fixes at :6421 (
resolve_import_records). So the plugin-fallback path and the no-plugin path for the same import record now diverge on.development.Why the fix doesn't cover it
The PR's four changed sites now leave
task.jsx.developmentalone onUnspecified, so the tsconfig value survives. Line 3478 predates and sits outside those changes; it still assigns the wholeoptions.jsxstruct, and there is noforce_node_envmatch around it. The PR's own comment at :6425 — "resolve_result.jsx is already tsconfig-merged; keep it" — describes exactly what :3478 fails to do. The new tests only exercise the no-plugin path (b.jsxresolved via :6421), so this is not caught.Step-by-step proof (concrete divergence introduced by this PR)
Setup, no
NODE_ENV, no--define:tsconfig.json:{"compilerOptions":{"jsx":"react-jsxdev","jsxImportSource":"shim"}}entry.jsximports./b.jsx; both contain JSX.- CLI:
bun build entry.jsx --external shim* --jsx-import-source=shim - Any
onResolveplugin registered (even one that always returnsundefined).
Trace:
--jsx-import-source=shimmakestransform_options.jsx = Some(...)inArguments.rs, soconfigure_linker_with_auto_jsxskips the tsconfig copy andtranspiler.options.jsx.developmentstays at the CLI default (false).force_node_env = Unspecified.- Entry point
entry.jsx→enqueue_entry_item(bundle_v2.rs:2649, fixed by this PR).Unspecifiedarm now no-ops, sotask.jsx.developmentkeeps the resolver'strue(from tsconfigreact-jsxdev) → parser emitsimport { jsxDEV } from "shim/jsx-dev-runtime". - Import
./b.jsx→onResolveplugin runs → returnsundefined→ResolveValue::NoMatchat :4484 → non-entry,filenamespace →run_resolverat :4514 → resolver returnsresolve_result.jsx.development = true→enqueue_parse_taskat :2414 →ParseTask::initsetstask.jsx.development = true→ line 3478 overwritestask.jsx = options.jsx.clone()→task.jsx.development = false→ parser emitsimport { jsx } from "shim/jsx-runtime". - Bundle output contains both
"shim/jsx-dev-runtime"(entry) and"shim/jsx-runtime"(import), i.e.jsxDEV(...)andjsx(...)calls in the same bundle.
Before this PR, step 2's
Unspecifiedarm also readoptions.jsx.development = false, so both files emittedshim/jsx-runtime— wrong per the PR's stated goal, but internally consistent. After this PR the two paths disagree, which is a new user-visible inconsistency (and can break at runtime if the shim's dev and prod runtimes are not both present).Fix
Drop the wholesale overwrite at :3478 —
ParseTask::initalready wroteresolve_result.jsx— and apply the samematch force_node_env { Development => true, Production => false, Unspecified => {} }as the four fixed sites.enqueue_parse_task2at :3540-3553 has the sameoptions.jsx.clone()shape and, to the extent it has a resolve result available, should get the same treatment.Per REVIEW.md "Fix the whole class in the same PR — grep for every sibling site sharing the pattern":
enqueue_parse_taskis the direct plugin-path sibling of the site fixed at :6421, and merging without it leaves the plugin path both still-broken and now inconsistent with the non-plugin path.
…ler/plugin-fallback paths
force_node_env also gates set_production and the development export
condition; setting it from jsx: { development: false } meant
NODE_ENV=production stopped setting options.production. Record the
explicit jsx value in a dedicated Config field and apply it to
force_node_env only after configure_defines and the condition check.
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/bundler/bundle_v2.rs:6413-6416—enqueue_parse_task(bundle_v2.rs:3474) is an unfixed 5th sibling: it doestask.jsx = self.transpiler_for_target(known_target).options.jsx.clone(), discarding the tsconfig-mergedresolve_result.jsxthatParseTask::initjust copied, and applies nojsx_development_override(). It is reached with a real diskresolve_resultviaon_resolve→run_resolver(:4510 → :2414) when anonResolvefilter matched but the callback returnedundefined. Before this PR that site and the four fixed sites all wrote the same globaloptions.jsx.developmentonUnspecified; now :6413 keeps the per-file tsconfig value while :3474 still writes the global, soBun.build+ tsconfig"react-jsx"+ a pass-through onResolve plugin emits bothreact/jsx-runtimeandreact/jsx-dev-runtimein one bundle depending on whether the specifier matched the plugin's filter. Preserveresolve_result.jsxfor the disk-resolve caller (:2414) and applyjsx_development_override()like the other four sites.Extended reasoning...
What the bug is
The PR replaces the four
force_node_envmatch blocks inbundle_v2.rs(at :2541, :2651, :6047, :6413) withjsx_development_override(), soUnspecifiednow leavestask.jsx.developmentat the resolver's tsconfig-merged value. But there is a fifth sibling that never had aforce_node_envmatch block and so was missed by the mechanical replacement:enqueue_parse_taskat bundle_v2.rs:3474 doeslet task: &mut ParseTask = self.arena_create(ParseTask::init(resolve_result, ..)); task.loader = Some(loader); task.jsx = self.transpiler_for_target(known_target).options.jsx.clone();
ParseTask::init(ParseTask.rs:262) has already setjsx: resolve_result.jsx.clone()— the per-file tsconfig-merged value. Line 3474 then unconditionally overwrites the whole struct with the globaltranspiler.options.jsx, and applies noforce_node_env.jsx_development_override()afterwards.The specific code path that reaches it with a disk
resolve_resultenqueue_parse_taskis called fromrun_resolverat bundle_v2.rs:2414 with a real diskresolve_resultproduced bytranspiler.resolver.resolve(source_dir, specifier, kind)— i.e. carrying the tsconfig-merged JSX.run_resolveris reached viaon_resolveat bundle_v2.rs:4510: when anonResolveplugin's filter matched the import specifier (enqueue_on_resolve_plugin_if_neededat :5927 returnedtrueand the loopcontinued), the plugin callback returnedundefined(ResolveValue::NoMatch), namespace is"file"and kind ≠EntryPointBuild. This is the standard fallback for pass-through onResolve plugins. So two files under the same tsconfig can go through :6413 (direct disk resolve) or :3474 (plugin-fallback disk resolve) based solely on whether their specifier matches anonResolvefilter regex.Why this is a NEW inconsistency introduced by this PR
Before this PR, on
Unspecifiedthe four fixed sites overwrotetask.jsx.developmentwithtranspiler.options.jsx.development, andenqueue_parse_taskoverwrote the wholetask.jsxwithtranspiler.options.jsx.clone()— both produced the samedevelopmentvalue, so every file in a bundle agreed (consistently wrong, which is what this PR is fixing). After this PR, site :6413 keepsresolve_result.jsx.developmentonUnspecified, but :3474 still writes the globaltranspiler.options.jsx.development. When those differ (see below), files under the same tsconfig now emit different JSX runtimes in one bundle.Why the global differs from tsconfig for
Bun.buildjs_bundle_completion_task.rs:1047unconditionally setstransform_options.jsx = Some(config.jsx.clone()), soconfigure_linker_with_auto_jsxattranspiler.rs:700(if self.options.transform_options.jsx.is_none()) skips the root-tsconfig copy andtranspiler.options.jsx.developmentstays atconfig.jsx's default (true, JSBundler.rs:191). With noNODE_ENV/BUN_ENVand noprocess.env.NODE_ENVdefine,configure_definesleavesforce_node_env = Unspecified.Step-by-step proof
Setup:
// tsconfig.json {"compilerOptions":{"jsx":"react-jsx"}} // a.jsx import "./b.jsx"; import "@x/c"; export const a = <div/>; // b.jsx / node_modules/@x/c/index.jsx: any JSX under the same tsconfig await Bun.build({ entrypoints: ["a.jsx"], plugins: [{ name: "p", setup(b) { b.onResolve({ filter: /^@x\// }, () => undefined) } }], });
No
NODE_ENV, nodefine.step ./b.jsx(no filter match)@x/c(filter match, plugin returnsundefined)import loop :5927 enqueue_on_resolve_plugin_if_needed→false→ true,continueresolve path direct disk resolve → resolve_result.jsx.development = false(tsconfigreact-jsx)on_resolveNoMatch (:4480) →run_resolver(:4510) → disk resolve →resolve_result.jsx.development = falsejsx assignment :6413 resolve_task.jsx = resolve_result.jsx.clone():3474 task.jsx = transpiler.options.jsx.clone()→development = trueUnspecifiedhandlingjsx_development_override()→None→ keepfalse(no override applied) → stays trueemitted import react/jsx-runtimereact/jsx-dev-runtimeOne bundle, one tsconfig, two JSX runtimes. Before this PR both files emitted
react/jsx-dev-runtime(both tooktranspiler.options.jsx.development = true).Impact and fix
Per REVIEW.md ("Fix the whole class in the same PR — grep for every sibling site sharing the pattern"), this is exactly the bug class the PR targets — per-file tsconfig
jsx.developmentclobbered by the global inbundle_v2— and leaving :3474 unfixed both misses part of the class and introduces a within-bundle divergence that did not exist before. Fix: for the disk-resolve caller (:2414), drop the unconditionaltask.jsx = transpiler.options.jsx.clone()(keepParseTask::init'sresolve_result.jsx) and applyforce_node_env.jsx_development_override()afterwards, matching sites :2541/:2651/:6413. The other caller ofenqueue_parse_task(:2172, virtual/file-map path with a syntheticresolve_result) may still want the global overwrite, so splitting behavior on the caller (or on whetherresolve_result.jsxcame from the resolver) is likely cleanest.Not a duplicate of the two prior comments — the JSBundler explicit-
developmentdemotion and the HTMLBundleforce_node_envclobber both concern howforce_node_env/options.jsxare set; this concerns a fifthbundle_v2site that overwritestask.jsxand never consultsforce_node_envat all.
… options.jsx.development coupled - on_resolve_async Success arm (:4664) now applies jsx_development_override() - run_resolver FileMap path (:2172) seeds file_map_result.jsx from options.jsx so enqueue_parse_task keeps the user's importSource/factory/fragment for virtual files - js_bundle_completion_task re-writes options.jsx.development alongside force_node_env so any reader that clones options.jsx without the override stays consistent
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/bundler/bundle_v2.rs:6059-6066— The open comment onjs_bundle_completion_task.rs:978flagsbundle_v2.rs:4664as the missedoptions.jsx.clone()site and proposes mirroring this hunk'sjsx_development_override()pattern there — but that doesn't cover the PR's own headline scenario (noNODE_ENV, nojsxoption, tsconfig"react-jsx", pluginonResolve→ disk path):force_node_envisUnspecifiedso the override returnsNone, andjsx_development_explicitisNoneso the other proposed fix (writingoptions.jsx.developmentin that block) never runs either. Disk-resolved siblings now emitreact/jsx-runtime(this PR's fix) while :4664 still clonesoptions.jsx.development = trueand emitsreact/jsx-dev-runtime— a mixed-runtime bundle newly introduced by removing theUnspecifiedoverwrite. :4664 needs a structurally different fix than :6061 got: either look up the enclosing tsconfig for the plugin-returned path, or stop unconditionally settingtransform_options.jsx = Some(...)increate_and_configure_transpilerwhen nojsxoption was passed soconfigure_linker_with_auto_jsxseedsoptions.jsxfrom the root tsconfig and :4664's clone matches its siblings.Extended reasoning...
What the bug is
The still-open review comment on
js_bundle_completion_task.rs:978correctly identifiesbundle_v2.rs:4664-4668(the pluginonResolve→ResolveValue::Successarm) as the oneoptions.jsx.clone()site the PR's sweep didn't touch, and offers two fixes: (a) also writetranspiler.options.jsx.development = devin thejsx_development_explicitblock, or (b) applyforce_node_env.jsx_development_override()after the clone at :4664, mirroring what this hunk does at :6061-6066. Both fixes work for that comment's trigger (ambientNODE_ENV=production+ explicitjsx.development). This finding shows a broader trigger — the PR's own headline repro applied through a plugin — for which neither proposed fix works, so following the existing comment verbatim leaves :4664 diverging.The uncovered path
Bun.buildwith nojsxoption still hitscreate_and_configure_transpiler(js_bundle_completion_task.rs:~1047), which setstransform_options.jsx = Some(config.jsx.clone())unconditionally.configure_bundlerthen writestranspiler.options.jsx.development = config.jsx.development= true (theConfig::default()value at JSBundler.rs:192).configure_linker_with_auto_jsx(transpiler.rs:702) seestransform_options.jsx.is_some()and skips the root-tsconfig copy, sooptions.jsx.developmentstaystrueregardless of tsconfig. With noNODE_ENV/BUN_ENVand nodefine,configure_definesleavesforce_node_env = Unspecified; with nojsxoption,jsx_development_explicit = Noneand the block at :972-978 doesn't run.Step-by-step proof
Environment: no
NODE_ENV/BUN_ENV, tsconfig{"compilerOptions":{"jsx":"react-jsx"}}.await Bun.build({ entrypoints: ['a.jsx'], // a.jsx: import 'virt'; export const a = <div/>; external: ['react*'], plugins: [{ name: 'p', setup(b) { b.onResolve({filter:/^virt$/}, () => ({ path: cwd + '/v.jsx', namespace: 'file' })) }}], })
step value config.jsx.development(JSBundler.rs:192, no userjsxoption)trueconfig.jsx_development_explicitNonetransform_options.jsx(create_and_configure_transpiler)Some(...)— unconditionalconfigure_linker_with_auto_jsx(transpiler.rs:702)is_some()→ skip tsconfig copy →options.jsx.development = trueconfigure_defines(noNODE_ENV)force_node_env = Unspecified:972-978 ( jsx_development_explicit = None)block does not run Now bundle:
file path task.jsx.developmentemits a.jsx(disk-resolved):2645 → task.jsx = resolve_result.jsx(tsconfig-merged, dev=false);Unspecified.jsx_development_override() = None→ stays falsefalsereact/jsx-runtime✓v.jsx(plugin-resolved):4664 → jsx: options.jsx.clone()→ dev=true; no override appliedtruereact/jsx-dev-runtime✗Two different JSX runtimes in one bundle. Before this PR, the removed
Unspecified => options.jsx.developmentoverwrite forced disk-resolveda.jsxtotruetoo, so both were consistentlyreact/jsx-dev-runtime— the divergence is newly introduced by this PR.Why neither of the existing comment's proposed fixes covers this
- Fix (a) — "also write
transpiler.options.jsx.development = devin thejsx_development_explicitblock":jsx_development_explicitisNonehere (the user passed nojsxoption), so the block at :972-978 never runs.options.jsx.developmentstaystrue. - Fix (b) — "apply
force_node_env.jsx_development_override()after the clone at :4664, like :6061-6066":force_node_envisUnspecified, sojsx_development_override()returnsNoneand the cloneddev=trueis kept.
The :6061-6066 pattern (clone
options.jsx+ apply override) is sufficient there because :6061 is the FileMap path — no tsconfig applies to a virtualfiles: {…}entry anyway, sooptions.jsxis the correct fallback. At :4664 the plugin returned a real disk path whose enclosing tsconfig should apply, but the resolver never ran on it — there is noresolve_result.jsxto seed from, andoptions.jsxis not tsconfig-merged (see above).How to fix
Two options; either restores consistency with the six sites this PR swept:
- Seed
options.jsxfrom the root tsconfig forBun.build: increate_and_configure_transpiler, only settransform_options.jsx = Some(...)when the user actually passed ajsxoption (track ajsx_was_setbool alongsidejsx_development_explicit). Thenconfigure_linker_with_auto_jsxcopies the root tsconfig intooptions.jsx, and :4664's clone matches disk-resolved siblings in the common single-tsconfig case (still not per-file, but no longer diverging). - Look up the enclosing tsconfig at :4664: for
namespace == "file", run the resolver's tsconfig lookup on the plugin-returned path and usetsconfig.merge_jsx(options.jsx)instead of the raw clone. This is the per-file-correct fix and matches what disk-resolved paths get viaresolve_result.jsx.
Per REVIEW.md "Fix the whole class in the same PR": :4664 is a sibling of the six sites this PR swept, and this is the PR's own headline repro (no env, no
jsxoption, tsconfig"react-jsx") applied through a pluginonResolve— a common shape for alias/redirect plugins. - Fix (a) — "also write
The onResolve Success arm builds a ParseTask without a resolver-produced resolve_result, so clone options.jsx and then merge the enclosing tsconfig for file-namespace paths the same way finalize_result does for disk-resolved files. Keeps plugin-resolved and disk-resolved files in the same bundle on the same JSX runtime when tsconfig carries the dev/prod choice.
|
The additional finding re: plugin |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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-tsconfig-react-jsx.test.ts`:
- Around line 95-109: Update all four Bun subprocess invocations in
test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts at lines 95-109 and
121-132 to run “bd build”, and at lines 153-171 and 189-213 to run “bd -e”
instead of the direct Bun commands. Preserve the existing arguments and
assertions.
- Around line 72-76: Update the describe block comment for “bun build: --jsx-*
flags preserve tsconfig dev/prod” to scope the invariant only to JSX CLI flags
that do not set development mode. Remove the bundle_v2 implementation-history
explanation, and explicitly preserve --jsx-development as an override that takes
precedence over tsconfig.
🪄 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: 05dc061a-fd50-4f25-91d6-e09a36022f17
📒 Files selected for processing (10)
src/bundler/bundle_v2.rssrc/bundler/transpiler.rssrc/options_types/bundle_enums.rssrc/runtime/api/JSBundler.rssrc/runtime/api/js_bundle_completion_task.rssrc/runtime/bake/bake_body.rssrc/runtime/bake/mod.rssrc/runtime/cli/build_command.rstest/bundler/bundler_jsx.test.tstest/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts
|
Diff is ready for review at e7f8d3f. All automated review threads are addressed and resolved. CI on builds 84498 and 84695: the Rust build (including
Locally: 19/19 in the new test block, plus |
What
bun build(bundled mode) with a tsconfig that sets"jsx": "react-jsx"or"react-jsxdev"loses that dev/prod selection as soon as any--jsx-*CLI flag is passed. The same applies toBun.build({ jsx: {...} })where thejsxoption does not setdevelopment. The--no-bundlepath andbun runreadresolve_result.jsxper file and were already correct.Repro
Cause
ParseTask::initcopies the per-fileresolve_result.jsx(which the resolver already merged from the nearest tsconfig) intotask.jsx. Immediately afterwards,bundle_v2overwrotetask.jsx.developmentwith the globaloptions.jsx.developmentwhenforce_node_env == Unspecified.options.jsxnormally tracks the root tsconfig (viaconfigure_linker_with_auto_jsx), so the overwrite was a no-op by default, but any--jsx-*flag makestransform_options.jsxSome, skips that tsconfig copy, and the per-file value was discarded.Fix
bundle_v2.rs: onUnspecified, leavetask.jsx.developmentalone; it is already the resolver's tsconfig-merged value. The four match blocks and theenqueue_parse_taskplugin-fallback sibling (which wholesale-clonedoptions.jsxover the per-file value) now call a sharedForceNodeEnv::jsx_development_override() -> Option<bool>helper.configure_defines: an explicitNODE_ENV=production(env or--define) now setsforce_node_env = Production, mirroring the existingDevelopmentarm. The whole block is guarded onforce_node_env == Unspecifiedso a caller-set value (HTMLBundle dev, bake) is not clobbered by ambientNODE_ENV.build_command:--productionsetsforce_node_env = Productionalongside the existingjsx.development = false.mod.rs,bake_body.rs): setforce_node_envfrommodeso the dev server keeps emittingjsx-dev-runtimewith a"jsx": "react-jsx"tsconfig and stays dev under ambientNODE_ENV=production.JSBundler: whenjsx.developmentor a dev/prod-carryingjsx.runtimeis explicitly passed, record it in a newjsx_development_explicitfield and apply it toforce_node_envafterconfigure_defines()and thedevelopmentexport-condition check, so the explicit API value still overrides a conflicting tsconfig without disturbingoptions.productionor condition resolution.Precedence in bundled mode is now:
--jsx-import-source,--jsx-fragment,--jsx-factory, and--jsx-runtime=automaticno longer participate in dev/prod selection at all.Behavior change
bun buildwith envNODE_ENV=productionand tsconfig"jsx": "react-jsxdev"now emits the production runtime (was development). This is symmetric with the existingNODE_ENV=developmenthandling (which already overrode tsconfig viaforce_node_env = Development) and matches whatBun.build({ define: { "process.env.NODE_ENV": '"production"' } })already did per the #3768 test matrix.Verification
New tests in
test/bundler/transpiler/jsx-tsconfig-react-jsx.test.ts:bun build m.jsx --external shim*with each of--jsx-runtime=automatic,--jsx-import-source=shim,--jsx-fragment=Fragment,--jsx-factory=hagainst both"react-jsx"and"react-jsxdev"tsconfigs (entry point + imported file), plus a--productionguard.Bun.build({ jsx: {...} })with explicitdevelopment: true/falseandruntime: 'react-jsx'/'react-jsxdev'/'automatic'against a conflicting tsconfig.5 of 17 cases fail on main, all 17 pass with the fix.
jsx/sideEffectsTrueTsconfigAutomaticsnapshot updated: it has tsconfig"react-jsx"and was encoding the dev runtime the old clobber produced; its assertion (no/* @__PURE__ */) still holds.Existing suites green:
bundler_jsx,esbuild/tsconfig,jsx-production,bundler_env,bun-build-api.Related
#36209 fixes the
bun run(non-bundled) half of--jsx-*flipping dev/prod by defaultingArguments.rstodevelopment: true. That change and this one are independent; with only #36209 applied the repro above flips which tsconfig value is discarded instead of fixing it.#31651 adds the same two
force_node_env = Productionlines inconfigure_defines, but its premise and second test case predate #34422 (on current main"react-jsx"already selects the production runtime withoutNODE_ENV). This PR supersedes that change.#33855 also sets
force_node_env = Productionfor--production(for a different bug, ambientNODE_ENVleaking into the define). The two-line overlap inbuild_command.rsis trivially resolvable whichever lands first.[review] gate passed · iteration 0 · 10 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 4 passed · 0 rejected · iteration 0
evidence per changed file