fix(rust): circular inter-chunk imports when external dynamic imports exist#8596
fix(rust): circular inter-chunk imports when external dynamic imports exist#8596IWANABETHATGUY merged 9 commits intomainfrom
Conversation
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Fixes a code-splitting bug where external dynamic-import “entries” create gaps in entry bit positions, causing the chunk optimizer to map bits to the wrong ChunkIdx and potentially introduce circular inter-chunk imports from otherwise acyclic module graphs.
Changes:
- Add a
bit_to_chunk_idxmapping toChunkGraphand populate it during entry chunk creation to correctly translate entry-bit positions to real chunk indices (even when externals are skipped). - Update the chunk optimizer to use
bit_to_chunk_idxinstead ofChunkIdx::from_raw(bit_position). - Add an out-of-bounds guard to
BitSet::has_bit, plus a new snapshot fixture covering the reported circular-chunk scenario.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| crates/rolldown_utils/src/bitset.rs | Makes has_bit return false when queried out of range. |
| crates/rolldown/src/chunk_graph.rs | Adds bit_to_chunk_idx to track entry-bit-position → ChunkIdx mapping. |
| crates/rolldown/src/stages/generate_stage/code_splitting.rs | Initializes and fills bit_to_chunk_idx while creating entry chunks (skipping externals). |
| crates/rolldown/src/stages/generate_stage/chunk_optimizer.rs | Uses bit_to_chunk_idx to map entry bits to chunk indices safely/correctly. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/_config.json | New fixture config reproducing the external dynamic import gap scenario. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/entry-a.js | Fixture entry A. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/entry-b.js | Fixture entry B. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/apis.js | Fixture module containing dynamic imports that connect the graph. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/core.js | Fixture module with dynamic imports of parsers. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/plugin.js | Fixture module that triggers an external dynamic import. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/parser-a.js | Fixture parser A. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/parser-b.js | Fixture parser B. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/parser-c.js | Fixture parser C. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/shared.js | Fixture shared helper. |
| crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/artifacts.snap | Snapshot asserting the non-circular, correctly split output. |
Benchmarks Rust |
crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
Outdated
Show resolved
Hide resolved
|
I believe adding just one test case could be sufficient. |
I think it is fine to keep two, as it won't take much time to test, and these two tests have different configurations. @sapphi-red What do you think? If we just keep one, I prefer to keep the test I added, since it is cleaner and the configuration is simpler |
|
I think it's fine to keep both. But I think we need to either
as it's not obvious now. |
External dynamic imports create entry bit positions without corresponding chunks. When bit positions don't map 1:1 to chunk indices, the chunk optimizer uses ChunkIdx::from_raw(bit_position) which produces wrong chunk indices, causing shared modules to be placed in wrong chunks and creating circular inter-chunk imports from acyclic source modules. Fix by adding a bit_to_chunk_idx mapping to ChunkGraph, populated during init_entry_point, and using it in the chunk optimizer instead of ChunkIdx::from_raw(). Also adds a bounds check to BitSet::has_bit. Closes #8595 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the code splitting pipeline: bit position assignment, BitSet reachability propagation, chunk creation, and chunk optimization. Highlights the critical invariant that bit positions != chunk indices when external entries exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- determine_reachable_modules_for_entry is called inside split_chunks, not as a separate top-level step - Add apply_manual_code_splitting step - Clarify that module assignment happens after reachability propagation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add context on why BitSet-based reachability was chosen over webpack's heuristic approach, how rolldown's design compares to Rollup and esbuild, and the trade-offs at each decision point. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the original reproduction case that triggered the circular chunk bug: 2 entries, 4 external dynamic imports, and multiple plugin files with varied import patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The entry_chunk_idx set collected all chunk indices from the table, so the filter was always true. With bit_to_chunk_idx already mapping only to valid entry chunks, this guard is unnecessary. Also updates filename_with_hash snapshot for the new 8595 fixture. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add targeted assertions for the specific circular imports that the bug produced: - circular_chunk_from_external_entries: parser-a.js must not import plugin.js - 8595: babel.js must not import tt.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4f08e23 to
3d8c2fe
Compare
added |
## [1.0.0-rc.9] - 2026-03-11 ### 💥 BREAKING CHANGES - rename exported BindingMagicString to RolldownMagicString (#8626) by @IWANABETHATGUY ### 🚀 Features - rolldown: add isRolldownMagicString property for reliable native detection (#8614) by @IWANABETHATGUY - cli: align object type with rollup (#8598) by @h-a-n-a ### 🐛 Bug Fixes - rust: circular inter-chunk imports when external dynamic imports exist (#8596) by @Dunqing - update minify default docs from `false` to `'dce-only'` (#8620) by @shulaoda ### 💼 Other - fix early exit in script build-node (#8617) by @h-a-n-a ### 🚜 Refactor - binding: remove outdated TODO comment in MagicString to_string() (#8613) by @IWANABETHATGUY ### 📚 Documentation - add viteplus alpha announcement banner (#8615) by @mdong1909 - update VitePress theme to 4.8.2 for narrow-screen layout regression (#8612) by @Copilot ### ⚡ Performance - merge 4 integration test binaries into 1 (#8610) by @Boshen ### 🧪 Testing - replace heavy filename_with_hash test with targeted hash fixtures (#8597) by @Boshen ### ⚙️ Miscellaneous Tasks - ci: remove redundant `--no-run` build step from cargo-test (#8623) by @Boshen - rust: use `cargo-shear` to toggle Cargo.toml [lib] test = bool (#8622) by @Boshen - deps: update test262 submodule for tests (#8611) by @sapphi-red - skip macOS CI jobs on pull requests (#8608) by @Copilot - add rust cache to repo validation job (#8607) by @Boshen - skip running empty bin test targets (#8605) by @Boshen - skip building examples in cargo-test to reduce build time (#8603) by @Boshen - switch plain workflow checkouts to taiki-e action (#8601) by @Boshen - skip Windows CI jobs on PRs (#8600) by @Boshen - remove unused asset module (#8594) by @shulaoda ###◀️ Revert - "docs: add viteplus alpha announcement banner (#8615)" (#8616) by @shulaoda Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
Summary
Fixes #8595. Found while investigating oxc-project/oxc#20033. Also fixes #8522
ChunkIdx::from_raw(bit_position)which maps to wrong chunks when externals create gaps, causing shared modules to be placed in wrong chunks and producing circular inter-chunk imports from acyclic source modules.bit_to_chunk_idx: Vec<Option<ChunkIdx>>mapping toChunkGraph, populated duringinit_entry_point, used in chunk optimizer instead ofChunkIdx::from_raw()BitSet::has_bitfor safetyTest plan
crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/(minimal repro)crates/rolldown/tests/rolldown/issues/8595/(real-world repro from prettier-plugin-tailwindcss)parser-a.js <-> plugin.jscircular dependency)🤖 Generated with Claude Code