refactor: always split runtime module first#9419
Conversation
How to use the Graphite Merge QueueAdd 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. |
@rolldown/browser
@rolldown/debug
rolldown
@rolldown/binding-android-arm64
@rolldown/binding-darwin-arm64
@rolldown/binding-darwin-x64
@rolldown/binding-freebsd-x64
@rolldown/binding-linux-arm-gnueabihf
@rolldown/binding-linux-arm64-gnu
@rolldown/binding-linux-arm64-musl
@rolldown/binding-linux-ppc64-gnu
@rolldown/binding-linux-s390x-gnu
@rolldown/binding-linux-x64-gnu
@rolldown/binding-linux-x64-musl
@rolldown/binding-openharmony-arm64
@rolldown/binding-wasm32-wasi
@rolldown/binding-win32-arm64-msvc
@rolldown/binding-win32-x64-msvc
commit: |
943891d to
554178f
Compare
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Pull request overview
Refactors Rolldown’s code-splitting pipeline so the runtime module is always extracted into a dedicated “standalone runtime” chunk first, then optionally merged back into a proven-safe host chunk (with cycle/TLA safety checks). This addresses static-cycle regressions like #9597 and aligns option validation with render-time behavior when chunks are tombstoned after optimization.
Changes:
- Implement standalone-first runtime chunk extraction and a new
try_merge_runtime_chunk()path to merge the runtime into a safe host (or keep it standalone). - Fix single-file (
output.file) validation to count only live (rendered) chunks, ignoring tombstoned/removed chunks. - Update design docs and test fixtures/snapshots; add a regression test for #9597 and skip a few Rollup-alignment tests that assert only chunk-count parity.
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/rollup-tests/src/status.md | Updates aggregated Rollup test status counts after new skips. |
| packages/rollup-tests/src/status.json | Updates machine-readable Rollup test status counts after new skips. |
| packages/rollup-tests/src/failed-tests.json | Adds three chunking-form samples to the skip list (chunk-count mismatch vs Rollup). |
| meta/design/code-splitting.md | Updates runtime placement design notes to reflect standalone-first runtime chunking and safe merge strategy. |
| crates/rolldown/tests/rolldown/topics/preserve_semantic_of_entries_exports/named_export_in_wrapped_and_shared_entries/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/topics/exports/entry_esm_named_named/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/topics/exports/entry_esm_named_default/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/topics/exports/entry_cjs_named_named/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/topics/exports/common_esm_named_named/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/topics/exports/common_cjs_named_default/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/optimization/chunk_merging/dynamic_entry_merged_in_common_chunk2/artifacts.snap | Snapshot updates reflecting runtime merge into an existing host chunk. |
| crates/rolldown/tests/rolldown/optimization/chunk_merging/dynamic_entry_merged_in_common_chunk/artifacts.snap | Snapshot updates reflecting runtime merge into an existing host chunk. |
| crates/rolldown/tests/rolldown/issues/9597/node3.js | Adds reproduction fixture module for #9597 regression test. |
| crates/rolldown/tests/rolldown/issues/9597/node2.cjs | Adds reproduction fixture module for #9597 regression test. |
| crates/rolldown/tests/rolldown/issues/9597/node1.js | Adds reproduction fixture module for #9597 regression test. |
| crates/rolldown/tests/rolldown/issues/9597/node0.cjs | Adds reproduction fixture module for #9597 regression test. |
| crates/rolldown/tests/rolldown/issues/9597/_test.mjs | Adds regression assertion for absence of static import cycles and runtime smoke test. |
| crates/rolldown/tests/rolldown/issues/9597/_config.json | Adds test harness config for #9597 fixture. |
| crates/rolldown/tests/rolldown/issues/8595/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/issues/3650/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/issues/1722/2/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/issues/1722/1/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/issue_8910/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/issue_5303/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/advanced_chunks/tags_initial/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/advanced_chunks/split_node_modules/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/advanced_chunks/include_dependencies_recursively/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/function/advanced_chunks/basic_cjs/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/code_splitting/issue_8809/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/code_splitting/issue_8184/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/code_splitting/issue_5276/artifacts.snap | Snapshot updates reflecting runtime merge into an existing host chunk. |
| crates/rolldown/tests/rolldown/code_splitting/issue_5276_2/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/rolldown/code_splitting/facade_elimination_with_shared_cjs_dep/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/esbuild/splitting/splitting_hybrid_esm_and_cjs_issue617/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/tests/esbuild/default/export_forms_with_minify_identifiers_and_no_bundle/diff.md | Updates expected diff output for new chunk/runtime layout. |
| crates/rolldown/tests/esbuild/default/export_forms_with_minify_identifiers_and_no_bundle/artifacts.snap | Snapshot updates reflecting new runtime placement/merging behavior. |
| crates/rolldown/src/stages/generate_stage/mod.rs | Fixes single-file output validation by counting only live (non-tombstoned) chunks. |
| crates/rolldown/src/stages/generate_stage/manual_code_splitting.rs | Removes manual-stage runtime extraction and ensures recursively included dependencies skip already-assigned modules (e.g., runtime). |
| crates/rolldown/src/stages/generate_stage/code_splitting.rs | Introduces extract_standalone_runtime_chunk() and ensures it runs before manual/normal chunk materialization, plus final merge attempt. |
| crates/rolldown/src/stages/generate_stage/chunk_optimizer.rs | Replaces runtime rehoming with try_merge_runtime_chunk() that merges runtime into a safe host using consumer/bitset/dominator checks and cycle/TLA constraints. |
Files not reviewed (1)
- crates/rolldown/tests/rolldown/function/advanced_chunks/split_node_modules/artifacts.snap: Language not supported
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 40 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- crates/rolldown/tests/rolldown/function/advanced_chunks/split_node_modules/artifacts.snap: Language not supported
|
Reviewed this PR — the standalone-first runtime placement is a good direction and the cycle/TLA reasoning holds up. I found one confirmed correctness regression (verified with a live A/B run) and two lower-priority items (reasoned from the code, but not rigorously verified). Details below. 🔴 Confirmed (verified): runtime merge ignores
|
hyf0
left a comment
There was a problem hiding this comment.
LGTM. Please take a look at the high risk issue mentioned in #9419 (comment)
24361d4 to
54c8a13
Compare
addressed |
Merge activity
|
## Summary Reworks runtime-module chunk placement (see `meta/design/code-splitting.md` → "Runtime Module Placement"): the runtime module is always extracted into its own chunk first, then merged back into a proven-safe host chunk (the sole runtime consumer / a chunk with the same reachability bitset / a consumer-set dominator), guarded by TLA- and static-cycle checks. The initial layout is cycle-safe by construction instead of place-by-bitset-then-peel-on-cycle. A follow-up commit fixes a regression this surfaced: single-file `output.file` (es/cjs) builds that pull in a runtime helper (e.g. a reified namespace) were wrongly rejected with `[INVALID_OPTION] InvalidOutputFile`. The multi-chunk guard in `generate()` counted the merged-away runtime chunk's tombstone (still in `chunk_table`, but skipped at render time). It now counts only live chunks, mirroring the render-time filter. ## Why some Rollup alignment tests are skipped Three `chunking-form` samples are added to `packages/rollup-tests/src/failed-tests.json`. **These are not workarounds for a bug** — Rolldown's output is behaviorally correct; it just emits one fewer chunk than Rollup. The `chunking-form` suite only asserts that Rolldown emits the **same number of chunks** as Rollup (`assert.strictEqual(actualChunkCount, expectedChunkCount)`); it does not compare chunk contents. With the runtime now folded into a same-bitset host chunk, these multi-entry topologies no longer need a standalone runtime chunk, so Rolldown emits one fewer chunk than Rollup: Rollup hoists shared content into a `generated-*.js` chunk, while Rolldown folds that content (plus the runtime) into an existing entry chunk. Each was verified by **executing** both Rolldown's `_actual/` and Rollup's `_expected/` output and comparing observable behavior — entry exports, console output, and computed values all match. The only differences are the chunk count and cosmetic representation (Rolldown's namespace objects carry the spec-compliant `Symbol.toStringTag: "Module"`, which Rollup's frozen-object namespaces omit). Affected samples (both `generates es` and `generates cjs`): - `entry-point-without-own-code` — Handles entry points that contain no own code except imports and exports - `namespace-object-import` — namespace object import - `namespace-reexports` — namespace rendering with reexports To re-check one, run the sample and diff its `_actual/` against `_expected/`, e.g. `pnpm --filter rollup-tests test --grep "namespace-object-import: namespace object import"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) closed #9597
19c6ece to
35054a1
Compare
|
@IWANABETHATGUY Would you run https://github.com/sapphi-red/zarara with the changes to ensure there's no regression? (if you haven't) |
I have run it twice locally, each time with 6000 seeds. Not sure if it is enough. |
|
Ah, ok. That should be sufficient. |
#9685) ### Summary Restore the `rolldown-runtime` name for the standalone runtime chunk, which was accidentally dropped in #9419. ### Background #9419 ("refactor: always split runtime module first") reworked how the runtime module is placed: it is now always extracted into its own chunk first and merged back into a safe host when possible. When it stays standalone, the chunk is created in `extract_standalone_runtime_chunk` (`crates/rolldown/src/stages/generate_stage/code_splitting.rs`) via `Chunk::new(None, …)`. The old code path passed `Some("rolldown-runtime".into())` as the name, so that name was lost in the rewrite. Consequently the standalone runtime chunk is now emitted as a generic `chunk-[hash].js` rather than `rolldown-runtime-[hash].js`. The content, hash and placement are identical — only the name changed. ### Why it matters The `rolldown-runtime` name is the documented, recognizable name for the runtime chunk ("why there's always a runtime.js chunk"), and downstream tooling relies on it. It surfaced as a `@vitejs/plugin-rsc` ecosystem-ci failure: a test that excludes the runtime chunk by name (`imports.filter((f) => !f.includes('rolldown-runtime'))`) no longer matched, so a vendor chunk wrongly looked like it had a non-runtime import. ### Fix Pass `Some("rolldown-runtime".into())` as the chunk name in `extract_standalone_runtime_chunk`, restoring the pre-#9419 name. The runtime placement logic from #9419 is otherwise unchanged.
## [1.1.1] - 2026-06-11 ### 🚀 Features - ecmascript_utils: introduce AstFactory for AST construction (#9682) by @hyf0 - drop no-op init calls for empty wrapped-ESM modules (#9678) by @IWANABETHATGUY ### 🐛 Bug Fixes - resolver: honor package.json#type for .jsx/.tsx extensions (#9690) (#9699) by @IWANABETHATGUY - hmr: report the full-reload reason for the invalidate-loop case (#9708) by @hyf0 - explicit `moduleSideEffects` from a hook must take priority over the `package.json#sideEffects` (#9688) by @sapphi-red - keep the `rolldown-runtime` name for the standalone runtime chunk (#9685) by @shulaoda - lazy-barrel: request all exports for entry barrels on first encounter (#9672) by @shulaoda - finalizer: skip init_*() for tree-shaken wrapped ESM owners (#9669) by @IWANABETHATGUY - order `chunk.imports` by execution order (#9654) by @chuganzy - vite-resolve: preserve slash separators for Sass partial exports (#9546) by @cjc0013 - cross-chunk CJS wrapper shadowed by author-local binding (#9648) by @IWANABETHATGUY ### 🚜 Refactor - precompute wrapped-ESM init metadata in generate stage (#9712) by @IWANABETHATGUY - ecmascript_utils: fold construction ext traits onto AstFactory and delete AstSnippet (#9702) by @hyf0 - finalizer: finish ScopeHoistingFinalizer migration to AstFactory (#9701) by @hyf0 - finalizer: migrate module_finalizers/mod.rs to AstFactory (#9700) by @hyf0 - hmr: migrate hmr finalizer to AstFactory (#9695) by @hyf0 - plugin: migrate vite_build_import_analysis to AstFactory (#9693) by @hyf0 - scanner: migrate tweak_ast_for_scanning to AstFactory (#9683) by @hyf0 - always split runtime module first (#9419) by @IWANABETHATGUY ### 📚 Documentation - tsconfig: correct auto-discovery resolution to match TypeScript (#9714) by @shulaoda - design: plan to unify all internal AST construction (#9673) by @hyf0 - tsconfig: align reference resolution docs with TypeScript behavior (#9641) by @shulaoda ### ⚡ Performance - avoid per-module join Strings in scope-hoisting concatenation (#9645) by @Boshen - avoid intermediate Strings in the ESM export clause (#9644) by @Boshen - reuse a scratch buffer for facade namespace names in the scanner (#9642) by @Boshen - reuse the import-matching tracker stack across named imports (#9643) by @Boshen - avoid cloning the per-chunk export-items map in render_chunk_exports (#9639) by @Boshen - avoid CompactStr allocation in sorted-exports membership check (#9640) by @Boshen ### 🧪 Testing - add more `moduleSideEffects` precedence tests (#9689) by @sapphi-red - 9651: drop shimMissingExports from regression fixture (#9674) by @IWANABETHATGUY ### ⚙️ Miscellaneous Tasks - update react repo links (#9711) by @iiio2 - remove outdated pnpm configurations (#9666) by @btea - deps: update github actions to v2.81.5 (#9665) by @renovate[bot] - testing: migrate test harness off deprecated inlineDynamicImports (#9710) by @IWANABETHATGUY - hmr: delete commented-out dead code left from old register-module codegen (#9707) by @hyf0 - deps: update napi-rs toolchain (#9706) by @shulaoda - deps: update rollup submodule for tests to v4.61.1 (#9676) by @rolldown-guard[bot] - deps: update test262 submodule for tests (#9677) by @rolldown-guard[bot] - deps: update oxc to 0.135.0 (#9670) by @Boshen - pass ALGOLIA_APP_ID and ALGOLIA_API_KEY to void deploy (#9667) by @Boshen - deps: update github actions (#9662) by @renovate[bot] - deps: update rust crates (#9663) by @renovate[bot] - deps: update rolldown-plugin-dts to v0.25.2 (#9661) by @renovate[bot] - deps: update typos to v1.47.2 (#9660) by @renovate[bot] - deps: update docs dependencies (#9657) by @bddjr - deps: update typos to v1.47.1 (#9655) by @renovate[bot] - deploy website in its own workflow, only on docs output change (#9650) by @Boshen - publish to pkg.pr.new add pm and `commentWithDev` option (#9638) by @btea ### ❤️ New Contributors * @chuganzy made their first contribution in [#9654](#9654) * @cjc0013 made their first contribution in [#9546](#9546)

Summary
Reworks runtime-module chunk placement (see
meta/design/code-splitting.md→"Runtime Module Placement"): the runtime module is always extracted into its own
chunk first, then merged back into a proven-safe host chunk (the sole runtime
consumer / a chunk with the same reachability bitset / a consumer-set dominator),
guarded by TLA- and static-cycle checks. The initial layout is cycle-safe by
construction instead of place-by-bitset-then-peel-on-cycle.
A follow-up commit fixes a regression this surfaced: single-file
output.file(es/cjs) builds that pull in a runtime helper (e.g. a reified namespace) were
wrongly rejected with
[INVALID_OPTION] InvalidOutputFile. The multi-chunk guardin
generate()counted the merged-away runtime chunk's tombstone (still inchunk_table, but skipped at render time). It now counts only live chunks,mirroring the render-time filter.
Why some Rollup alignment tests are skipped
Three
chunking-formsamples are added topackages/rollup-tests/src/failed-tests.json. These are not workarounds for abug — Rolldown's output is behaviorally correct; it just emits one fewer chunk
than Rollup.
The
chunking-formsuite only asserts that Rolldown emits the same number ofchunks as Rollup (
assert.strictEqual(actualChunkCount, expectedChunkCount));it does not compare chunk contents. With the runtime now folded into a
same-bitset host chunk, these multi-entry topologies no longer need a standalone
runtime chunk, so Rolldown emits one fewer chunk than Rollup: Rollup hoists shared
content into a
generated-*.jschunk, while Rolldown folds that content (plus theruntime) into an existing entry chunk.
Each was verified by executing both Rolldown's
_actual/and Rollup's_expected/output and comparing observable behavior — entry exports, consoleoutput, and computed values all match. The only differences are the chunk count
and cosmetic representation (Rolldown's namespace objects carry the
spec-compliant
Symbol.toStringTag: "Module", which Rollup's frozen-objectnamespaces omit).
Affected samples (both
generates esandgenerates cjs):entry-point-without-own-code— Handles entry points that contain no own code except imports and exportsnamespace-object-import— namespace object importnamespace-reexports— namespace rendering with reexportsTo re-check one, run the sample and diff its
_actual/against_expected/, e.g.pnpm --filter rollup-tests test --grep "namespace-object-import: namespace object import".🤖 Generated with Claude Code
closed #9597