Skip to content

fix(rust): circular inter-chunk imports when external dynamic imports exist#8596

Merged
IWANABETHATGUY merged 9 commits intomainfrom
fix/circular-chunk-external-entries
Mar 11, 2026
Merged

fix(rust): circular inter-chunk imports when external dynamic imports exist#8596
IWANABETHATGUY merged 9 commits intomainfrom
fix/circular-chunk-external-entries

Conversation

@Dunqing
Copy link
Collaborator

@Dunqing Dunqing commented Mar 9, 2026

Summary

Fixes #8595. Found while investigating oxc-project/oxc#20033. Also fixes #8522

  • External dynamic imports create entry bit positions without corresponding chunks. The chunk optimizer uses 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.
  • Adds bit_to_chunk_idx: Vec<Option<ChunkIdx>> mapping to ChunkGraph, populated during init_entry_point, used in chunk optimizer instead of ChunkIdx::from_raw()
  • Adds bounds check to BitSet::has_bit for safety

Test plan

  • Added snapshot test in crates/rolldown/tests/rolldown/issues/circular_chunk_from_external_entries/ (minimal repro)
  • Added snapshot test in crates/rolldown/tests/rolldown/issues/8595/ (real-world repro from prettier-plugin-tailwindcss)
  • Verified test fails without fix (parser-a.js <-> plugin.js circular dependency)
  • Verified test passes with fix (each chunk contains only its own module, no circular imports)
  • CI passes

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 9, 2026 07:24
@Dunqing Dunqing changed the title fix: circular inter-chunk imports when external dynamic imports exist fix(rust): circular inter-chunk imports when external dynamic imports exist Mar 9, 2026
@netlify
Copy link

netlify bot commented Mar 9, 2026

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit f1755bd
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69ae75c03281c00008f01ae5
😎 Deploy Preview https://deploy-preview-8596--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
Copy link

netlify bot commented Mar 9, 2026

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 3d8c2fe
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69b1099b296bbf000850339f
😎 Deploy Preview https://deploy-preview-8596--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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_idx mapping to ChunkGraph and 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_idx instead of ChunkIdx::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.

Copilot AI review requested due to automatic review settings March 9, 2026 07:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings March 9, 2026 07:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Benchmarks Rust

  • target: main(f84f2ce)
  • pr: fix/circular-chunk-external-entries(3d8c2fe)
group                                                        pr                                     target
-----                                                        --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol              1.00     79.4±1.61ms        ? ?/sec    1.00     79.5±1.89ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap    1.00     86.7±2.04ms        ? ?/sec    1.01     87.5±1.69ms        ? ?/sec
bundle/bundle@rome_ts                                        1.00    162.5±4.28ms        ? ?/sec    1.01    164.7±4.04ms        ? ?/sec
bundle/bundle@rome_ts-sourcemap                              1.00    182.0±4.76ms        ? ?/sec    1.00    182.6±3.76ms        ? ?/sec
bundle/bundle@threejs                                        1.00     73.6±2.06ms        ? ?/sec    1.00     73.5±2.78ms        ? ?/sec
bundle/bundle@threejs-sourcemap                              1.00     80.4±1.94ms        ? ?/sec    1.00     80.1±2.41ms        ? ?/sec
bundle/bundle@threejs10x                                     1.00    767.5±6.52ms        ? ?/sec    1.00   767.9±10.25ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                           1.00    880.3±9.32ms        ? ?/sec    1.01   884.8±10.35ms        ? ?/sec
scan/scan@rome_ts                                            1.00     76.3±1.59ms        ? ?/sec    1.01     77.2±1.54ms        ? ?/sec
scan/scan@threejs                                            1.00     27.4±1.67ms        ? ?/sec    1.01     27.7±1.73ms        ? ?/sec
scan/scan@threejs10x                                         1.00    264.3±3.73ms        ? ?/sec    1.01    267.2±4.14ms        ? ?/sec

@hyf0 hyf0 requested a review from IWANABETHATGUY March 9, 2026 09:13
Copilot AI review requested due to automatic review settings March 9, 2026 12:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

@IWANABETHATGUY IWANABETHATGUY self-assigned this Mar 9, 2026
@IWANABETHATGUY
Copy link
Member

I believe adding just one test case could be sufficient.

@Dunqing
Copy link
Collaborator Author

Dunqing commented Mar 11, 2026

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

@sapphi-red
Copy link
Member

I think it's fine to keep both. But I think we need to either

  • add description to the test cases what is the expected output (e.g. the output does not contain a cycle)
  • add _test.mjs that ensures that the output is expected

as it's not obvious now.

Dunqing and others added 7 commits March 11, 2026 14:13
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>
Dunqing and others added 2 commits March 11, 2026 14:14
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>
Copilot AI review requested due to automatic review settings March 11, 2026 06:20
@Dunqing Dunqing force-pushed the fix/circular-chunk-external-entries branch from 4f08e23 to 3d8c2fe Compare March 11, 2026 06:20
@Dunqing Dunqing added the test: vite-tests Trigger Vite tests workflow on this PR label Mar 11, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.

@Dunqing Dunqing requested a review from IWANABETHATGUY March 11, 2026 06:49
@Dunqing
Copy link
Collaborator Author

Dunqing commented Mar 11, 2026

I think it's fine to keep both. But I think we need to either

  • add description to the test cases what is the expected output (e.g. the output does not contain a cycle)
  • add _test.mjs that ensures that the output is expected

as it's not obvious now.

added

@IWANABETHATGUY IWANABETHATGUY merged commit 61d1817 into main Mar 11, 2026
65 of 66 checks passed
@IWANABETHATGUY IWANABETHATGUY deleted the fix/circular-chunk-external-entries branch March 11, 2026 07:48
@github-actions github-actions bot mentioned this pull request Mar 11, 2026
shulaoda added a commit that referenced this pull request Mar 11, 2026
## [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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test: vite-tests Trigger Vite tests workflow on this PR

Projects

None yet

4 participants