refactor(rolldown): extract the ns star-external __reExport emission rule into LinkingMetadata#10238
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. |
✅ Deploy Preview for rolldown-rs canceled.
|
Merging this PR will not alter performance
Comparing Footnotes
|
Merge activity
|
…rule into LinkingMetadata (#10238) ### What is this PR solving? Refactor only, no behavior change. Groundwork for the #9374 stack (#10239, #10237). The module finalizer decided inline whether an `export * from '<external>'` record emits a `__reExport(ns, <external>)` call when the namespace object renders: in ESM output the call is skipped for `EntryLevelExternal` records (the re-export is representable as a chunk-level `export * from` statement) unless the namespace object is genuinely observed (`ModuleNamespaceIncludedReason::Unknown`); in CJS-family formats it is always emitted. This PR extracts that condition into `LinkingMetadata::ns_star_external_re_export_emitted` so that any pass needing to *predict* the emission (the unused-runtime sweep added upstack in #10237) calls the same rule as the emitter instead of re-deriving it — the two cannot drift apart. Verified pure: the full integration suite passes with zero snapshot drift on this commit.
a8fe95a to
f78c863
Compare
…rule into LinkingMetadata (#10238) ### What is this PR solving? Refactor only, no behavior change. Groundwork for the #9374 stack (#10239, #10237). The module finalizer decided inline whether an `export * from '<external>'` record emits a `__reExport(ns, <external>)` call when the namespace object renders: in ESM output the call is skipped for `EntryLevelExternal` records (the re-export is representable as a chunk-level `export * from` statement) unless the namespace object is genuinely observed (`ModuleNamespaceIncludedReason::Unknown`); in CJS-family formats it is always emitted. This PR extracts that condition into `LinkingMetadata::ns_star_external_re_export_emitted` so that any pass needing to *predict* the emission (the unused-runtime sweep added upstack in #10237) calls the same rule as the emitter instead of re-deriving it — the two cannot drift apart. Verified pure: the full integration suite passes with zero snapshot drift on this commit.
f78c863 to
000c9ba
Compare
…porters (#10239) ### What is this PR solving? Part of the #9374 stack (#10238 ← this ← #10237). `find_entry_level_external_module` flattens star re-export chains that end at an external module into chunk-level re-exports, marks the records `EntryLevelExternal`, and re-propagates `has_dynamic_exports = false` for the affected modules. A BFS is supposed to extend that invalidation to every *transitive* star importer — a module whose own star chain passes through a seed derived its flag from the seed's pre-flattening value. That BFS was dead on arrival: it tested set membership on pop — ```rust while let Some(idx) = q.pop_front() { if !invalidated_modules.insert(idx) { continue; } ... q.extend(module.importers_idx.iter()); } ``` — but the seeds are pre-inserted into `invalidated_modules`, so every seed skipped itself and no importer was ever enqueued. Only the modules with a *direct* external star re-export were ever walked back. Consequences for a chain like `entry_a → export * → entry_b → export * → 'external'`: - `entry_a` kept a stale `has_dynamic_exports = true`, so `finalized_module_namespace_ref_usage` retained its namespace object. The finalizer rendered `var entry_a_exports = /* @__PURE__ */ __exportAll({...})` for nothing; the default dce-only minifier usually deleted it again, but the unbalanced `//#region` markers it left behind are visible in several existing snapshots. - In chains of **three or more** modules, the middle module's stale flag made the finalizer emit a genuinely dead `__reExport(a_exports, b_exports)` call — a call expression, so not pure-droppable — which shipped to users. The fix enqueues importers on first discovery instead of testing membership on pop. ### Effects on existing tests Five snapshots change, all region-marker corrections from the namespace declarations that no longer render-then-vanish (`5923`, `6992`, `7115`, `7233`, `7233_chain`). The dead `rolldown-runtime` files still visible in those snapshots are removed by #10237 upstack — inclusion is already frozen when this pass runs, so fixing the flag alone cannot un-include the runtime module.
…nal flattening (#10237) ### What is this PR solving? Top of the #9374 stack (#10238 ← #10239 ← this). For a star re-export chain that ends at an external module: ```js // entry_a.js export * from './entry_b.js'; export const a = 'a'; // entry_b.js export * from 'external'; export const b = 'b'; ``` both entry chunks emit a literal `export * from "external"` and never call a runtime helper — yet the build still ships a dead `rolldown-runtime-*.js` chunk that exports `__reExport`/`__exportAll` to nobody, with a bare import of it in every entry. Root cause: tree-shaking includes the runtime helpers at link time, when `entry_b` still counts as having dynamic exports. The generate stage later proves the whole chain can be represented statically (`find_entry_level_external_module` marks the records `EntryLevelExternal` and re-propagates `has_dynamic_exports = false` — including to transitive star importers since #10239; `finalized_module_namespace_ref_usage` drops the namespace objects that only served the chain), so the module finalizer emits no helper call — but by then the runtime module is already included and already placed in a chunk, and nothing un-includes it. ### The fix Add `sweep_unused_runtime_module` (new pass at the tail of `generate_chunks()`, after the walk-back passes and before chunk exec-order assignment). It re-derives runtime helper demand from the same post-walk-back facts the finalizer renders from, through four channels: per-module `depended_runtime_helper` flags (with `ReExport` discounted unless some included star importee still has `has_dynamic_exports` — CommonJS importees always do, so wrapped-CJS star re-exports are unaffected), the namespace-object channel gated on `namespace_included` (calling the `LinkingMetadata::ns_star_external_re_export_emitted` rule extracted in #10238, so the prediction cannot drift from the emission), runtime-owned symbols referenced by included statements, and entry-chunk synthesized references. If zero demand remains, the runtime module is un-included, its symbols are purged from `used_symbol_refs` (which severs every cross-chunk edge — all downstream consumers are liveness-filtered), and its now-empty chunk is tombstoned via the same `PostChunkOptimizationOperation::Removed` lifecycle the chunk optimizer uses. Any surviving demand, or any bail-out (tree-shaking disabled, side-effectful dev/HMR runtime), leaves everything exactly as tree-shaking decided. ### What other alternatives have you explored? Teaching the link stage to skip the eager helper inclusion for the statically decidable cases was rejected: the flattening decision depends on chunk shapes (dynamic entries, `SimulateFacadeChunk`), so a link-time copy of the predicate would have to stay in sync with the generate stage forever, and drift in the optimistic direction produces broken output (a rendered helper that was never included) rather than dead bytes. Recomputing demand after the walk-back, where all facts are final, fails conservative instead. ### Effects on existing tests Six snapshots change, all in the fixed direction: `6992`, `7115`, `7233`, `7233_chain` lose their dead runtime files and the bare imports/requires of them (the same bug shape under `preserveModules`, in both ESM and CJS output); `4472`/`5923` lose the last phantom `//#region` markers (the bulk of the marker cleanup happened in #10239). ### Tests New fixture `crates/rolldown/tests/rolldown/issues/9374/` — fails on this PR's base (#10239's head still ships the runtime chunk; verified) and passes here. It uses `minify: false` so vestigial namespace declarations would also surface in the snapshot. Verified the legitimate cases still keep the runtime: `import * as ns` observation over the same chain (helpers are genuinely used to merge the external's exports into the namespace), and star re-exports of CJS modules (`__commonJSMin` wrapper kept). `internal-docs/code-splitting/implementation.md` is updated with the new pipeline ordering and the sweep's liveness invariants. fixes #9374
## [1.2.0] - 2026-07-15 ### 🚀 Features - dev: skip shipping factories for newly imported top-level modules (#10223) by @h-a-n-a - dev: per-client ship map for HMR patch sizing (#10208) by @h-a-n-a - dev: client-side HMR (#10164) by @h-a-n-a - dev: send a full-reload update to clients when a tsconfig changes (#10262) by @shulaoda - treat `import.meta['url']` and `import.meta['ROLLUP_FILE_URL_*']` as side-effect free (#10267) by @sapphi-red - rewrite `import.meta['url']` (#10251) by @sapphi-red - add `FILE_NOT_FOUND` error (#10220) by @sapphi-red - treat `import.meta.ROLLUP_FILE_URL_*` as side-effect free (#10217) by @sapphi-red ### 🐛 Bug Fixes - sourcemap: preserve unmapped boundaries during composition (#10254) by @hyfdev - `[format]` in `*FileNames` option for ESM format should be `es` instead of `esm` (#10214) by @sapphi-red - sourcemap: preserve coarse mappings during composition (#10249) by @hyfdev - rolldown_plugin_vite_import_glob: support tsconfig paths with `import.meta.glob` (#10167) by @sapphi-red - dev: clear tsconfig caches for bare full builds (#10276) by @shulaoda - dev: force a full rebuild when a tsconfig changes (#10261) by @shulaoda - treat rooted drive-less module ids as absolute in preserveModules naming (#10235) by @IWANABETHATGUY - watch: rebuild when tsconfig files change (#10258) by @shulaoda - watch: drop tsconfig-merged transform options on each rebuild (#10257) by @shulaoda - incorrect `EMPTY_IMPORT_META` warning for `import.meta.ROLLUP_FILE_URL_*` for CJS output (#10221) by @sapphi-red - deconflict: rename CJS locals shadowing wrapped-ESM namespace objects (#9970) by @IWANABETHATGUY - rolldown: drop the unused runtime module after entry-level external flattening (#10237) by @IWANABETHATGUY - rolldown: re-propagate has_dynamic_exports to transitive star importers (#10239) by @IWANABETHATGUY - tree-shaking: tree-shake destructured dynamic import namespace bindings (#10213) by @logaretm - s390x: use json-escape-simd 3.1.1 for big-endian JSON escaping fix (#10211) by @satyamg1620 ### 🚜 Refactor - dev: move full-reload to client side (#10207) by @h-a-n-a - readability follow-ups to the ReplaceWith migration (#10286) by @IWANABETHATGUY - replace take_in-then-write-back with ReplaceWith and by-value moves (#10285) by @Boshen - share the main resolver's cache with the transformer's tsconfig lookups (#10205) by @shulaoda - rolldown: extract the ns star-external __reExport emission rule into LinkingMetadata (#10238) by @IWANABETHATGUY - rolldown: unify link/generate diagnostics into a Diagnostics accumulator (#10234) by @IWANABETHATGUY - sourcemap_filenames: drop dead sourcemap-filename plumbing (#10189) by @IWANABETHATGUY - extract external import symbol merging into a method (#10224) by @IWANABETHATGUY - rolldown: skip CJS namespace merging under strict execution order (#10203) by @hyfdev - resolve the manual tsconfig per file instead of once at startup (#10200) by @shulaoda - rolldown: route interop ESM init emission through a shared init-target view (#10202) by @hyfdev - rolldown: collapse vestigial wrap-kind state and share chunk sort helper (#10201) by @hyfdev ### 📚 Documentation - show plugin kinds in JSDoc and each hook's description (#10218) by @sapphi-red - add an explanation about removing imports from external modules without any messages (#10215) by @sapphi-red ### ⚡ Performance - sourcemap: owned merge in SourceJoiner::join (4005->5 allocs/chunk) (#10250) by @Boshen - avoid redundant sourcemap string copies in collapse and minify paths (#10093) by @Boshen ### 🧪 Testing - code-splitting: establish strict-order review baselines (#10287) by @hyfdev - dev: add hot API test cases (#10181) by @h-a-n-a - code-splitting: normalize strict execution order variants (#10277) by @hyfdev - code-splitting: harden strict execution order coverage (#10252) by @hyfdev - code-splitting: add strict execution order regressions (#10253) by @hyfdev ### ⚙️ Miscellaneous Tasks - deps: update github actions (#10241) by @renovate[bot] - deps: update oxc to 0.140.0 (#10274) by @shulaoda - update Yunfei's GitHub username (#10275) by @hyfdev - deps: update napi (#10260) by @renovate[bot] - deps: update test262 submodule for tests (#10266) by @rolldown-guard[bot] - deps: update dependency vite-plus to v0.2.4 (#10256) by @renovate[bot] - deps: update napi (#10240) by @renovate[bot] - deps: update oxc resolver to v11.24.2 (#10245) by @renovate[bot] - deps: update rust crates (#10244) by @renovate[bot] - disable Renovate updates for idna_adapter (#10248) by @shulaoda - deps: update oxc resolver to v11.24.1 (#10232) by @renovate[bot] - deps: update rust crate oxc_sourcemap to v8.1.1 (#10233) by @renovate[bot] - deps: update dependency rolldown-plugin-dts to ^0.27.0 (#10206) by @renovate[bot] - deps: upgrade sugar_path to v3 (#10230) by @hyfdev - add `dist-*` to `.gitignore` in sourcemap-filenames/hash-final-content fixture (#10216) by @sapphi-red - deps: update dependency rust to v1.97.0 (#10209) by @renovate[bot] ### ❤️ New Contributors * @satyamg1620 made their first contribution in [#10211](#10211) Co-authored-by: shulaoda <[email protected]>

What is this PR solving?
Refactor only, no behavior change. Groundwork for the #9374 stack (#10239, #10237).
The module finalizer decided inline whether an
export * from '<external>'record emits a__reExport(ns, <external>)call when the namespace object renders: in ESM output the call is skipped forEntryLevelExternalrecords (the re-export is representable as a chunk-levelexport * fromstatement) unless the namespace object is genuinely observed (ModuleNamespaceIncludedReason::Unknown); in CJS-family formats it is always emitted.This PR extracts that condition into
LinkingMetadata::ns_star_external_re_export_emittedso that any pass needing to predict the emission (the unused-runtime sweep added upstack in #10237) calls the same rule as the emitter instead of re-deriving it — the two cannot drift apart.Verified pure: the full integration suite passes with zero snapshot drift on this commit.