Skip to content

fix: track CJS re-export import records to fix inline const and tree-shaking#8925

Merged
graphite-app[bot] merged 1 commit into
mainfrom
03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking
Mar 27, 2026
Merged

fix: track CJS re-export import records to fix inline const and tree-shaking#8925
graphite-app[bot] merged 1 commit into
mainfrom
03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking

Conversation

@h-a-n-a

@h-a-n-a h-a-n-a commented Mar 26, 2026

Copy link
Copy Markdown
Member

Summary

When a CJS module conditionally re-exports from multiple sources (e.g., React's jsx-dev-runtime.js), the linker previously only followed the first import record. This could lead to incorrect inline const values and incomplete tree-shaking.

This PR:

  • Records which import records correspond to module.exports = require(...) during scanning, rather than relying on import_records.first()
  • Follows all CJS re-export targets when building resolved_exports
  • Tracks conflicting CJS exports when multiple targets provide the same name
  • Skips inline const for exports with CJS conflicts, since the runtime value depends on which branch executes
  • Uses the recorded import record indices to simplify the tree-shaking bailout heuristic

Background

// jsx-dev-runtime.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./production');   // exports.jsxDEV = void 0
} else {
  module.exports = require('./development');  // exports.jsxDEV = function() {...}
}

Previously, add_exports_for_export_star used import_records.first() to find the re-export target. This meant only the first require() target's exports entered resolved_exports — the rest were silently dropped. This caused jsxDEV to be incorrectly inlined as void 0.

Related issues

Test plan

  • New inline_const_cjs_reexport - validates inline const works with multiple cjs re-exports
  • New tree_shaking/cjs_reexport_conditional — validates ESM+CJS mixed re-export (ESM first, CJS second)
  • just test-rust all green

h-a-n-a commented Mar 26, 2026

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add the label graphite: merge-when-ready to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@netlify

netlify Bot commented Mar 26, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 448a337
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69c564a393a1af00084deb97
😎 Deploy Preview https://deploy-preview-8925--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Mar 26, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 724f077
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69c64ed7cd25ad0008b75d3c
😎 Deploy Preview https://deploy-preview-8925--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@h-a-n-a h-a-n-a marked this pull request as ready for review March 26, 2026 17:12
@h-a-n-a h-a-n-a requested review from IWANABETHATGUY, Copilot and hyf0 and removed request for IWANABETHATGUY and hyf0 March 26, 2026 17:12

Copilot AI 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.

Pull request overview

Fixes incorrect export resolution for CommonJS “re-export” modules (module.exports = require(...)) in conditional patterns by tracking the specific import records involved, so the linker follows all possible re-export targets. This improves correctness for both inline-const and tree-shaking in mixed/conditional CJS re-export scenarios (e.g. React jsx-dev-runtime-style branching).

Changes:

  • Record the import record indices corresponding to module.exports = require(...) during AST scanning and persist them on EcmaView.
  • In link stage, follow all recorded CJS re-export targets when building resolved_exports, and track CJS export name conflicts to avoid unsafe inline-const.
  • Simplify the CJS tree-shaking bailout heuristic and add/extend integration tests + snapshots covering the regressions.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/rolldown_common/src/types/resolved_export.rs Adds cjs_conflicting_symbol_refs to represent conditional CJS export name conflicts.
crates/rolldown_common/src/ecmascript/ecma_view.rs Stores cjs_reexport_import_record_ids on EcmaView for downstream link/tree-shaking logic.
crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs Uses recorded re-export import record ids to decide when to bail out for conditional CJS re-exports.
crates/rolldown/src/stages/link_stage/bind_imports_and_exports.rs Builds resolved_exports using recorded CJS re-export targets; records ambiguities/conflicts.
crates/rolldown/src/module_loader/runtime_module_task.rs Initializes new EcmaView field for the runtime module.
crates/rolldown/src/module_finalizers/mod.rs Skips inline-const when exports have recorded CJS conflicts.
crates/rolldown/src/ecmascript/ecma_module_view_factory.rs Plumbs scanner output into EcmaView (re-export import record ids).
crates/rolldown/src/ast_scanner/mod.rs Tracks module.exports = require(...) require spans and resolves them to import record indices post-scan.
crates/rolldown/src/ast_scanner/impl_visit.rs Updates pattern match for CommonJsAstType::Reexport variant shape.
crates/rolldown/src/ast_scanner/cjs_export_analyzer.rs Extends Reexport classification to carry the require() call span used for mapping to import records.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/reexporter.js New conditional CJS re-export fixture (ESM vs CJS targets).
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/main.js New tree-shaking test entry importing a named export from the re-exporter.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/esm-impl.mjs New ESM implementation branch fixture.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/cjs-impl.js New CJS implementation branch fixture.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/artifacts.snap Snapshot asserting both branches remain and exports are accessible.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/_test.mjs Runtime assertion validating branch behavior under default env.
crates/rolldown/tests/rolldown/tree_shaking/cjs_reexport_conditional/_config.json Enables treeshake for the new test case.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/main.js New HMR + inline-const regression fixture for jsx-dev-runtime-like conditional re-export.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/jsx-dev-runtime.production.js Production branch fixture exporting void 0.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/jsx-dev-runtime.js Conditional module.exports = require(...) re-exporter fixture.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/jsx-dev-runtime.development.js Development branch fixture exporting a function.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/artifacts.snap Snapshot ensuring jsxDEV is not incorrectly inlined to void 0.
crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/_config.json Enables devMode and inlineConst for the new HMR regression.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/reexporter.js New regression: multiple requires, only one is the module.exports = require(...) target.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/real-exports.js Fixture providing the actual export value.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/main.js Entry importing named export from the re-exporter.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/debug.js Fixture “decoy” require that should not be treated as the re-export target.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/artifacts.snap Snapshot asserting inline-const selects the real export target, not the first import record.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/_test.mjs Runtime assertion for the inline-const regression.
crates/rolldown/tests/rolldown/optimization/inline_const/cjs_reexport_first_import_record/_config.json Enables inlineConst for the new optimization regression.

Comment thread crates/rolldown/src/stages/link_stage/bind_imports_and_exports.rs
Comment thread crates/rolldown/src/ast_scanner/impl_visit.rs
@codspeed-hq

codspeed-hq Bot commented Mar 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking (724f077) with main (cffcd5e)2

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (724f077) during the generation of this report, so cffcd5e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@h-a-n-a h-a-n-a requested a review from sapphi-red March 26, 2026 17:31
@h-a-n-a h-a-n-a force-pushed the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch from a5331f8 to eda6437 Compare March 26, 2026 17:38
Comment thread crates/rolldown_common/src/types/resolved_export.rs Outdated
@h-a-n-a h-a-n-a requested a review from shulaoda March 26, 2026 18:11
Comment thread crates/rolldown/tests/rolldown/topics/hmr/inline_const_cjs_reexport/main.js Outdated
@h-a-n-a h-a-n-a force-pushed the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch from eda6437 to 2313351 Compare March 27, 2026 04:27
@h-a-n-a h-a-n-a requested a review from IWANABETHATGUY March 27, 2026 04:36
@h-a-n-a h-a-n-a force-pushed the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch from 2313351 to 5b5bdf0 Compare March 27, 2026 09:18
@h-a-n-a h-a-n-a force-pushed the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch 2 times, most recently from 387dec1 to 8ca08e3 Compare March 27, 2026 09:25
@graphite-app

graphite-app Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Merge activity

…shaking (#8925)

## Summary

When a CJS module conditionally re-exports from multiple sources (e.g., React's `jsx-dev-runtime.js`), the linker previously only followed the first import record. This could lead to incorrect inline const values and incomplete tree-shaking.

This PR:
- Records which import records correspond to `module.exports = require(...)` during scanning, rather than relying on `import_records.first()`
- Follows all CJS re-export targets when building `resolved_exports`
- Tracks conflicting CJS exports when multiple targets provide the same name
- Skips inline const for exports with CJS conflicts, since the runtime value depends on which branch executes
- Uses the recorded import record indices to simplify the tree-shaking bailout heuristic

## Background

```js
// jsx-dev-runtime.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./production');   // exports.jsxDEV = void 0
} else {
  module.exports = require('./development');  // exports.jsxDEV = function() {...}
}
```

Previously, `add_exports_for_export_star` used `import_records.first()` to find the re-export target. This meant only the first `require()` target's exports entered `resolved_exports` — the rest were silently dropped. This caused `jsxDEV` to be incorrectly inlined as `void 0`.

## Related issues

- vitejs/vite#21884 — the `void 0` inlining case fixed by this PR
- #8345 (comment) — related discussion on the underlying issue
- vitejs/vite#21843 — related Vite issue, worked around by vitejs/vite#21865 (disabling `inlineConst` for affected packages). This PR fixes the root cause in rolldown so we can reenable `inlineConst` in Vite. cc @sapphi-red

## Test plan

- [x] New `inline_const_cjs_reexport` - validates inline const works with multiple cjs re-exports
- [x] New `tree_shaking/cjs_reexport_conditional` — validates ESM+CJS mixed re-export (ESM first, CJS second)
- [x] `just test-rust` all green
@graphite-app graphite-app Bot force-pushed the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch from bf42ed3 to 724f077 Compare March 27, 2026 09:33
@graphite-app graphite-app Bot merged commit 724f077 into main Mar 27, 2026
31 checks passed
@graphite-app graphite-app Bot deleted the 03-27-fix_track_cjs_re-export_import_records_to_fix_inline_const_and_tree-shaking branch March 27, 2026 09:36
This was referenced Apr 1, 2026
shulaoda added a commit that referenced this pull request Apr 1, 2026
## [1.0.0-rc.13] - 2026-04-01

### 🚀 Features

- add friendly error for unloadable virtual modules (#8955) by @sapphi-red
- better error message for unsupported CSS error (#8911) by @sapphi-red

### 🐛 Bug Fixes

- prevent chunk merging from leaking entry side effects (#8979) by @IWANABETHATGUY
- correct inlining based on module's def format and esModule flag (#8975) by @h-a-n-a
- generate init calls for excluded re-exports in strict execution order (#8858) by @IWANABETHATGUY
- consistent order for `meta.chunks` in `renderChunk` hook (#8956) by @sapphi-red
- subpath imports in glob imports failing to find files (#8885) by @kalvenschraut
- browser: bundle binding types in dts output (#8930) by @nyan-left
- ci: guard artifact download step in `vite-test-ubuntu` when build is skipped (#8934) by @Copilot
- track CJS re-export import records to fix inline const and tree-shaking (#8925) by @h-a-n-a
- use ImportKind::Import for common-chunk root computation (#8899) by @IWANABETHATGUY
- watch: clear emitted_filenames between rebuilds (#8914) by @IWANABETHATGUY
- ci: cache esbuild snapshots to avoid 429 rate limiting (#8921) by @IWANABETHATGUY
- always check circular deps in chunk optimizer (#8915) by @IWANABETHATGUY
- don't mark calls to reassigned bindings as pure (#8917) by @IWANABETHATGUY
- magic-string: throw TypeError for non-string content args (#8905) by @IWANABETHATGUY
- magic-string: add split-point validation and overwrite/update options (#8904) by @IWANABETHATGUY

### 🚜 Refactor

- pre-compute has_side_effects on ChunkCandidate (#8981) by @IWANABETHATGUY
- cleanup and simplify in dynamic_import.rs (#8927) by @ulrichstark
- rename came_from_cjs to came_from_commonjs for consistency (#8938) by @IWANABETHATGUY
- inline `create_ecma_view` return destructuring and remove redundant binding (#8932) by @shulaoda

### 📚 Documentation

- document ensure_lazy_module_initialization_order in code-splitting design doc (#8931) by @IWANABETHATGUY

### 🧪 Testing

- add regression test for runtime helper circular dependency (#8958) by @h-a-n-a
- enable 8 previously-skipped MagicString remove tests (#8945) by @IWANABETHATGUY
- add test for why PureAnnotation is needed in execution order check (#8933) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- add `@emnapi/runtime` and `@emnapi/core` as direct deps of `@rolldown/browser` (#8978) by @Copilot
- deps: update dependency vite-plus to v0.1.15 (#8970) by @renovate[bot]
- deps: update dependency oxfmt to ^0.43.0 (#8969) by @renovate[bot]
- deps: upgrade oxc to 0.123.0 (#8967) by @shulaoda
- justfile: deduplicate update-submodule as alias of setup-submodule (#8968) by @shulaoda
- deps: update rollup submodule for tests to v4.60.1 (#8965) by @sapphi-red
- deps: update test262 submodule for tests (#8966) by @sapphi-red
- remove unused `type-check` scripts (#8957) by @sapphi-red
- deps: update actions/cache action to v5 (#8953) by @renovate[bot]
- deps: update npm packages to v6 (major) (#8954) by @renovate[bot]
- deps: update npm packages (#8948) by @renovate[bot]
- deps: update rust crates (#8949) by @renovate[bot]
- deps: update github-actions (#8947) by @renovate[bot]
- deps: update napi (#8943) by @renovate[bot]
- deps: update dependency rolldown-plugin-dts to ^0.23.0 (#8944) by @renovate[bot]
- regenerate testing snapshots (#8928) by @ulrichstark
- deps: update dependency rust to v1.94.1 (#8923) by @renovate[bot]

### ❤️ New Contributors

* @kalvenschraut made their first contribution in [#8885](#8885)
* @nyan-left made their first contribution in [#8930](#8930)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants