fix(code-splitting): let a cross-chunk dynamic importer carry the order trigger - #10456
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. |
Merging this PR will not alter performance
Comparing Footnotes
|
This comment was marked as resolved.
This comment was marked as resolved.
|
Both confirmed, both fixed here — your fixtures are in verbatim, plus a third one for an alias case the fix review caught: On this branch: a two-argument You were right about |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20cb610aed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… the trigger `try_rewrite_import_expression` bails on `expr.options.is_some()`, so a two-argument `import()` keeps its original specifier and still needs a real file at that path. Mark those records in the scanner and record their call sites as chunkless in the importer scan, which keeps the entry's facade. Raised in #10456's review.
Merge activity
|
809bbbd to
f3bea99
Compare
…er trigger (#10456) #10433 lets a dynamic import collapse inline when chunking already placed the target in the importer's own chunk, and the collapse carries the target's deferred initialization. A cross-chunk dynamic importer was left keeping its facade chunk. It does not need one: the cross-chunk rewrite emits `import('./host.js').then(n => (n.init_x(), n.namespace))`, which carries the trigger just as well. Both arms already share that shape through the `esm_init_target` view, so this only stops asking for a file. Emitted JS files, same Vite, only the rolldown package swapped: | project | `strictExecutionOrder` off | before | after | | --- | ---: | ---: | ---: | | lobehub | 1517 | 2584 (+1067) | **1576 (+59)** | | ComfyUI_frontend | 521 | 521 (+0) | 521 (+0) | | plane | 923 | 924 (+1) | 924 (+1) | The 1008 files that come off lobehub are one restored facade per lazy route. ComfyUI_frontend and plane are unchanged: neither has a dynamic entry the chunk optimizer merged away, so neither restored a facade to begin with. lobehub's residual +59 is the other facade source, `create_order_wrap_entry_facades`, and is out of scope here. Those entries still have a chunk of their own, so declining to split one leaves its inline `init_E()` at the top of a chunk other chunks import — the hazard the split exists for. Reaching them needs the chunk demoted without a facade emitted, which is a separate change. ## Motivation A restored facade exists so a merged entry's trigger has a load site of its own. When every live importer of that entry is a dynamic one, every entry path into it is an `import()` whose rewrite can run the trigger, so the load site is redundant — the facade is a file whose whole body is `init_x()`. This is also what `strictExecutionOrder` did before #10104. That release drove SEO through `WrapKind::Esm`, so a merged dynamic entry took the same cross-chunk rewrite and shipped no facade; `v1.2.0` emits `import(\`./FileViewer-9VJj0tgR.js\`).then(e => (e.r(), e.n))` on lobehub and 1471 JS files, 47 fewer than with the flag off. #10104 moved order wrapping onto its own plan, where `wrap_kind` stays `None`, and routed those entries to facade restoration instead. Unchanged: a module with no dynamic importer has no call site to carry anything, and an emitted chunk still needs a real file for its reference id to resolve to. Two more shapes cannot carry the trigger either, and keep their facade for the same reason: a two-argument `import()`, whose specifier `try_rewrite_import_expression` never rewrites, so a file has to stay at the name the source wrote; and a host chunk that could expose a callable `then` to a cross-chunk importer, where promise resolution assimilates the chunk namespace as a thenable and the rewrite's extraction callback never receives it. The `then` check reads both the source-level export alias and the declaring symbol name, since a common chunk emits cross-chunk exports under the latter. ## What this gives up The trigger now runs in the rewrite's `.then`, one microtask after the host chunk settles, instead of inside a facade's own evaluation. A microtask already queued when the chunk settles therefore observes the target uninitialized. `m4_dynamic_facade_race` measures exactly that and its runtime oracle changes: ``` before: ['target', 'checkpoint:true'] after: ['checkpoint:false', 'target'] ``` What an importer sees is unaffected — by the time its own `await` resumes, the target has run — and `cross_chunk_dynamic_importer_uses_call_site_trigger` still asserts at runtime that entering `b` initializes the target without running entry `a`. Counting execution-order violations in lobehub's emitted output (a chunk whose inline `__esm` init another chunk statically imports) gives 58 with the flag off and 0 both before and after this PR. So the trade is 1008 files against that one microtask, on the same side of it that shipped in 1.2.0. ## Tests - `wrapped_dynamic_entry_keeps_facade_after_manual_chunk_merge` becomes `wrapped_dynamic_entry_uses_the_call_site_trigger_after_manual_chunk_merge`: `a` imports the host chunk, the rewrite carries the trigger, and no `target.js` is emitted. - `cross_chunk_dynamic_importer_keeps_facade` becomes `cross_chunk_dynamic_importer_uses_call_site_trigger`. Its runtime assertions are unchanged; only the facade in its snapshot is gone. - `m4_dynamic_facade_race` records the microtask ordering above. - `dynamic_import_options`, `dynamic_import_then_export` and `dynamic_import_then_export_alias` cover the two keep-facade cases above at runtime; all three came out of review feedback on this PR. - Full suite: no other test changes, and no new failures.
e7df604 to
99d2834
Compare
f3bea99 to
c909488
Compare
…#10485) With strict execution order, a dynamically imported entry can still emit a one-purpose facade file even when every live import can initialize it directly. This PR removes those redundant files in both strict modes while preserving the value and initialization observed by the importing promise. ```js // before: the import loads a separate file whose only effect is activation await import("./page-b2.js"); // page-b2.js import { t as init_page_b } from "./page-b.js"; init_page_b(); // after: the import activates the implementation directly await import("./page-b.js").then((n) => (n.t(), n.n)); // page-b2.js is not emitted ``` ## Problem Each affected dynamic entry adds one emitted chunk and an extra module load without adding user code. On LobeHub wrap-all, #10456 still emits 1577 chunks, 59 more than the non-strict build; 58 of those are these activation-only facades. The extra file appears when strict execution order wraps a pure dynamic entry whose implementation still owns its entry chunk. It can happen in both wrap-all and on-demand modes; wrap-all reaches it more often because it wraps every eligible module. Strict lowering has two ways for such a facade to survive. The restore pass (which revives a facade removed by chunk optimization) already uses the call-site trigger policy from #10456. The creation pass, `create_order_wrap_entry_facades` (which splits an inline entry initializer into its own file), still decides from chunk loading alone and never asks whether every live `import()` can carry that initializer. The same output shape therefore keeps or removes a facade depending on which pass produced it. Collapsing the facade also means the implementation chunk must publish the namespace value that the facade used to return. That interface cannot be reconstructed from a stale retained-symbol set: aliases can widen it beyond the dynamic import's actual export usage, and a getter such as `x: () => x` can be rendered even when `x` is neither declared nor imported in the chunk. Reading or spreading that namespace then throws a `ReferenceError`. Non-strict builds are unaffected. Strict builds still retain a real facade for user or emitted entries, entries without a live `import()` call site, two-argument `import()`, TLA-tainted targets, host namespaces that may expose callable `then`, entries whose direct or transitive export-star chain reaches an external module, and facades already restored because another collapse proof failed. For the dynamic entries that do collapse, output-hook metadata changes with the file topology: the implementation is reported as a common chunk, so `isDynamicEntry` becomes `false`, `facadeModuleId` becomes `null`, and `exports` reports the common chunk's emitted linkage names instead of the facade's entry export list. Manifest and plugin consumers that key on entry metadata will therefore stop seeing a separate dynamic entry for exactly these removed facades; this is the same contract choice already made for optimizer-removed facades in #10456. ## Fix Use the same call-site proof when creating and restoring strict entry facades. When a pure dynamic entry has an initializer and every surviving `import()` can be rewritten safely, its implementation becomes a common chunk, each import points to that chunk and runs the initializer before resolving, and no separate facade is emitted. Removing the facade also removes the namespace object it would have exported. Strict lowering now records the exact export names selected by `DynamicImportExportsUsage` and materializes that narrowed interface in the implementation chunk. Its synthetic statement references every non-inlined binding behind those getters, allowing normal cross-chunk linking to keep, export, and import each getter target after the entry becomes a common chunk. Smart inline constants omit that reference only when the individual binding is actually safe to inline. If the namespace also has a real semantic consumer, that complete interface overrides the narrowing. This keeps the `ExportAll` runtime demand without turning a synthetic narrow namespace into an opaque user namespace read or initializing unrelated re-exports. Entry-level external stars are rendered on the facade chunk and keep that facade in every format. ESM would lose its chunk-level `export * from "external"` statement after reclassification. CJS-like formats would replace the facade's deduplicated `Object.keys` merge with one module-local `__reExport` per direct record; that differs for primitive module values and duplicate records, while a transitive external star is not available from the entry module's direct records at all. The provisional chunk's `entry_level_external_module_idx` covers both direct and transitive chains, so one format-independent guard preserves the complete interface and its existing merge behavior. This deliberately takes the same ordering tradeoff as #10456: the initializer runs in the rewritten import's promise continuation, one microtask after the host chunk settles, instead of during facade-module evaluation. The importing promise still resolves only after initialization, but a microtask queued during host evaluation can observe the target before it runs. Keeping the facade would preserve that timing but keep all 58 files; using different timing solely based on which lowering pass produced the facade would be inconsistent. Entries with no `import()` call site, including the remaining LobeHub HTML entry, are intentionally unchanged. LobeHub therefore moves from 1577 to 1519 chunks rather than to the 1518-chunk non-strict baseline. The analogous external-star loss in the pre-existing non-strict chunk optimizer is not changed here. It already reproduces on `main` without strict execution order and is being fixed independently in #10492, rather than adding another dependency to this stack. ## Validation - `retained_star_renamed_cycle` now spreads the emitted synthetic namespace at runtime and asserts its exact keys are `['_', 'common']`. This fails on the reviewed revision because the namespace contains a dangling `x` getter and an alias-widened `commonLeaf` getter. - New `retained_star_opaque_cycle` spreads the source-level dynamic namespace and asserts the complete interface. Its snapshots pin the cross-chunk import that backs `x`, so a genuine semantic namespace consumer cannot be accidentally narrowed. - New `retained_star_smart_constant_cycle` pins the smart inline-constant boundary in both strict modes: a getter's backing reference is omitted only when that exact constant can actually be inlined. - The direct and transitive ESM external-star fixtures are red without the guard because the resolved `import()` namespace loses `externalValue`. With the guard, each snapshot retains a facade carrying `export * from "external"`, and runtime assertions observe `x === 1`, `externalValue === 42`, and the expected initialization order. - New `cjs_dynamic_entry_external_star_keeps_facade` pins why direct CJS stars also keep the facade: a duplicated Proxy external is enumerated once, while a primitive external contributes its enumerable `0` and `1` keys. A collapsed module-local namespace would enumerate the Proxy twice and omit the primitive keys. - New `cjs_dynamic_entry_transitive_external_star` reproduces the reported `target -> barrel -> external` loss. It is red before the guard with `undefined !== 42`; the fixed snapshot retains the facade's chunk-level external merge and the runtime assertion observes `externalValue === 42`. - The related retained-star, fanout, namespace, and external-star fixtures pass, as do all 12 strict-order invariant tests. - LobeHub wrap-all emits 1518 `.js` files and one `.mjs`; all 1519 pass `node --check`. Expanding the external-star guard to every format does not change this ESM measurement, so all 58 chunk reductions remain. This measurement covers production output topology and syntax, not browser E2E behavior. - The output-hook metadata change is source-verified but is not pinned by a dedicated plugin or manifest fixture in this PR. - Targeted clippy for Rolldown, the all-features workspace check, Rust and repository formatting, filename lint, and `git diff --check` pass. Full-workspace clippy remains locally blocked by the pre-existing reasonless `#[ignore]` generated in the isolated-declaration test. Part of #10294.
A chunk is loaded through `import('./chunk.js')`, whose promise resolves with the chunk namespace. Promise resolution assimilates any value with a callable `then` as a thenable, so a chunk that exports `then` hijacks every dynamic import of itself — including the `.then((n) => n.ns)` the finalizer generates to reach a merged dynamic entry, whose callback then receives whatever the exported `then` resolved with instead of the namespace. With `minifyInternalExports: false` an internal cross-chunk export named `then` reaches the host namespace directly or through an alias, since a cross-chunk export is emitted under the declaring symbol's name (`export { then as hostThen }` still emits `then`).
`deconflict_exported_names` now reserves `then` before naming bundler-owned internal exports, so they deconflict to `then$1` like any other collision, and the minified-name generator skips the literal `then` (unreachable in practice at four characters, but the generator is the only other source of internal names). Names the user can observe keep whatever they declare: an entry chunk's public exports arrive with predefined names and are emitted verbatim, and an `emitFile`-preserved export keeps `then` only when `then` is the exported name itself — a preserve-name module whose local `then` leaves under an alias deconflicts like any other symbol, so it cannot hand `then` to the chunk (and two preserved modules cannot emit a duplicate one; the second falls back to the resolver).
No snapshot churn: the full fixture sweep produces exactly the same set of snapshots as `main`, so no existing fixture had an internal export named `then`.
#10456's strict-side guard stays for what renaming can't reach: an entry's own public `then` and `export * from` an external module.
… that holds the module (#10479) A dynamic import written with a second argument — `import('./target.js', {})`, or anything carrying `with { type: … }` — keeps the specifier the source wrote, while the module it names ships inside the bundle under a different name. Loading such a build fails with `ERR_MODULE_NOT_FOUND`. This makes a two-argument call produce exactly what the same call without the second argument produces. ```js // input export const loaded = import('./target.js', {}).then((target) => target.value); ``` ```js // before — the bundle emits target-oUvUsQEi.js, and nothing is at ./target.js const loaded = import("./target.js", {}).then((target) => target.value); // after const loaded = import("./target-oUvUsQEi.js").then((target) => target.value); ``` ## Problem Nothing unusual is needed to hit this: default output settings, no code-splitting configuration, one call site. The failure gets quieter when the target is inlined rather than given its own chunk — with ```js export const loaded = import('./data.json', { with: { type: 'json' } }); ``` the JSON module is inlined into the importing chunk and no file is emitted for it at all, yet the call still names `./data.json`. In a directory where the source tree sits next to the output, that resolves to the original file and the build silently ends up with two copies of the module instead of failing. CJS output has a third shape: the same call also escapes the `import()` → `require()` conversion, so a CJS bundle ships a native `import()` at that site. The dynamic-import rewrite in the module finalizer — the pass that redirects a call at the chunk that ends up holding the module, collapses it when importer and target share a chunk, and converts it for CJS output — returns early whenever the call has a second argument, before it touches the specifier. Nothing downstream picks the call up again. The early return carries no recorded rationale; it predates the current control flow and reads as "not handled yet". That leaves two-argument calls as the only place where import attributes mean anything for a bundled module. Everywhere else they are inert: they are read only when rendering an import of an *external* module, they select no loader (a `.txt` file holding JSON and marked `type: 'json'` still loads as text), they do not affect module identity (the same module imported with and without attributes yields one instance), and a static attributed import of a bundled module already drops them. Only call sites with a second argument are affected. External and unresolved targets are not — that is where attributes are load-bearing and the current behavior is correct — and neither are calls whose specifier is not statically known. ## Fix When the target is a module rolldown bundles, drop the second argument and let the call take the ordinary path. The specifier is then redirected at the chunk that emits the module, the merged-entry collapse and namespace extraction apply, and CJS output converts the call like any other — none of that machinery changes, it simply stops being skipped. Dropping the attributes is what the rest of the pipeline already does for bundled modules, so the result is byte-identical to writing the call without its second argument. External targets, unresolved targets, and computed specifiers keep bailing, and an external call is now explicitly protected from the CJS `require()` conversion, which cannot carry attributes. **Why not keep a file at the name the source wrote.** The first revision of this PR did that: it stopped the chunk optimizer from merging away an entry named by a two-argument call. That only helps when the emitted filename happens to equal the path the source wrote — with hashed names or a chunk subdirectory the specifier still dangles — it costs a merge that was otherwise sound, and it cannot address the inlined case at all, where by construction no file carries that name. This supersedes it: the guard is removed, along with the equivalent guard merged in #10456 and the import-record flag both used. **Non-goals.** Making attributes meaningful for bundled modules — selecting a module type, distinguishing instances per attribute, warning on conflicting attributes the way Rollup does — is a separate and much larger change; this only stops treating a two-argument call as unrewritable. An options *expression* with side effects is no longer evaluated when the target is bundled; the argument is spec'd as an object literal carrying `with`, and such a call site does not load today anyway. ## Validation New fixtures, each red before the finalizer change and green after: - `topics/dynamic_import_options/hashed_chunk_names` — default hashed names, nothing merged. `ERR_MODULE_NOT_FOUND` before; resolves and returns the value after. This is the case the removed guard could never fix. - `topics/dynamic_import_options/json_module_attribute` — `with { type: 'json' }` on an inlined JSON module. Points at the emitting chunk with no `with` clause, and asserts the data at runtime. - `topics/dynamic_import_options/cjs_format` — pins that the call converts like a one-argument one instead of staying a native `import()`. Its CJS cell was runtime-green before, because Node's ESM loader resolves the CJS chunk anyway; only the snapshot catches this one. - `topics/dynamic_import_options/external_target_keeps_attributes` and `…/non_static_specifier_in_cjs` — the two deliberate bails. Byte-identical output before and after. - `chunk_merging/dynamic_import_with_options_rewrites_specifier` (repurposed from `…_keeps_entry`) — the merged case now collapses into the host chunk instead of keeping a facade. - `packages/rolldown/tests/…/dynamic-imports-metadata/options-import` — `dynamicImports` names the chunk the call now points at. - `strict_execution_order_invariants/dynamic_import_options` — updated: no facade, specifier rewritten, trigger carried, runtime assertions unchanged. `chunk_merging/tree_shaken_options_import_does_not_keep_entry` is deleted; it pinned the removed guard's liveness rule. One test on `main` depended on the old behavior and had to be rebuilt: `late_order_wrapping_revalidates_output_file` used a two-argument import to keep a graph single-chunk through chunk optimization and multi-chunk only after order lowering. It now gets that shape from an emitted chunk for the entry module plus a manual group, and is verified non-vacuous — the same build succeeds with `strictExecutionOrder` off. Exactly one pre-existing snapshot changes (the repurposed fixture above); everything else is new files. Full Rust suite matches the baseline failure set by name, and the Node suite is green (883 passed).
## [1.2.1] - 2026-07-29 ### 🚀 Features - dev: support `hotUpdate` hook (#10305) by @h-a-n-a - dev: expose `disableWatcher` through the dev engine bindings (#10474) by @shulaoda - plugin: surface output-option callbacks in [PLUGIN_TIMINGS] (#10411) by @IWANABETHATGUY - `import.meta.ROLLDOWN_FILE_URL_<referenceId>_<urlId>` support to pass context to `resolveFileUrl` hook (#10297) by @sapphi-red - dev: skip the HMR update when a module's rebuilt output is unchanged (#10333) by @h-a-n-a - code-splitting: wrap strict execution order modules on demand (#10104) by @hyfdev - support `import.meta.ROLLDOWN_FILE_URL_*` (#10296) by @sapphi-red - implement `resolveFileUrl` plugin hook (#10291) by @sapphi-red - magic-string: support storeName on overwrite/update (#10312) by @IWANABETHATGUY ### 🐛 Bug Fixes - external: keep `__toESM` when the importing module is tree-shaken (#10516) by @IWANABETHATGUY - dev: resolve re-exports from externals through a real import (#10489) by @tbvjaos510 - code-splitting: route CJS barrel initialization per consumer (#10488) by @hyfdev - error: align filename-pattern diagnostics with Rollup's wording (#10501) by @IWANABETHATGUY - plugin: validate emitted prebuilt-chunk file names (#10399) by @IWANABETHATGUY - code-splitting: point a two-argument dynamic import at the chunk that holds the module (#10479) by @hyfdev - common: align is_path_fragment with Rollup's isPathFragment (#10398) by @IWANABETHATGUY - code-splitting: never name an internal chunk export `then` (#10480) by @hyfdev - code-splitting: avoid creating call-site-triggered entry facades (#10485) by @hyfdev - code-splitting: collect top-level eager order reasons in every strict build (#10463) by @hyfdev - code-splitting: let a cross-chunk dynamic importer carry the order trigger (#10456) by @hyfdev - code-splitting: avoid the dynamic-entry facade when the same-chunk collapse carries the trigger (#10433) by @hyfdev - binding: forward resolveId options.kind to callable builtin plugins (#10440) by @martijnwalraven - dev: keep the export name of `export * as ns from` (#10476) by @tbvjaos510 - code-splitting: only split an entry facade when something else can load its chunk (#10441) by @hyfdev - code-splitting: fold the runtime chunk back after order lowering (#10414) by @hyfdev - runtime: don't leave default undefined for cjs that fakes __esModule (#10453) by @IWANABETHATGUY - import-glob: escape generated string literals (#10438) by @shantanuraj - preserve-modules: don't assume every chunk mirrors a module (#10442) by @hyfdev - filter: treat missing hook inputs as non-matches, not panics (#10443) by @IWANABETHATGUY - code-splitting: keep CommonJS modules wrapped under strict execution order (#10405) by @hyfdev - code-splitting: align dynamicImports metadata with the rewritten import() specifier (#10430) by @hyfdev - dev: skip user plugin hooks for ?rolldown-lazy proxy modules (#10426) by @unknownjedi - treeshake: preserve side-effect-free spread arguments in manual-pure chains (#10432) by @IWANABETHATGUY - treeshake: preserve eagerly evaluated child effects of manual pure chains (#10427) by @Nic-Polumeyv - code-splitting: initialize pure definers behind star re-export barrels under strict execution order (#10409) by @hyfdev - resolveId: reject string `id` filters (must be a RegExp) (#10412) by @IWANABETHATGUY - code-splitting: collect eager order reasons for on-demand wrapping (#10387) by @hyfdev - rolldown: apply output.paths to CJS export-star of external modules (#10406) by @IWANABETHATGUY - dev: sourcemaps of lazily compiled chunks (#10386) by @tbvjaos510 - plugin: validate emitted file names at emitFile time (#10377) by @Nic-Polumeyv - guarantee tokio runtime release when close rejects (#10381) by @shulaoda - code-splitting: make strict transitive init registration deterministic and constant-time (#10320) by @hyfdev - pair tokio runtime acquire and release on wasm to prevent premature shutdown (#10363) by @shulaoda - don't body-demand modules from simulated facade chunk includes (#10351) by @IWANABETHATGUY - tracing: invalid RD_LOG filter and RD_LOG_OUTPUT=json no longer panic (#10343) by @Brooooooklyn - code-splitting: treat class definition-time global reads as order-sensitive (#10322) by @hyfdev - code-splitting: deconflict force-included runtime helper statements (#10336) by @hyfdev - binding: forward all missing parallel JS plugin hooks (#10345) by @Brooooooklyn - code-splitting: re-derive chunk exec order after the runtime sweep (#10334) by @hyfdev - types: add undefined to ExistingRawSourceMap optional properties (#10355) by @ocavue - magic-string: validate relocate ranges before rewiring (#10327) by @IWANABETHATGUY - add EMPTY_IMPORT_META warning for non-node CJS output (#10222) by @sapphi-red - call `resolveFileUrl` in a deterministic order (#10330) by @sapphi-red - magic-string: refuse repeated sendMagicString on a consumed instance (#10331) by @IWANABETHATGUY - magic-string: refuse to use a MagicString after sendMagicString consumed it (#10326) by @IWANABETHATGUY - magic-string: reject non-contiguous moves before rewiring the chunk list (#10311) by @IWANABETHATGUY - handle `./foo/bar` glob pattern the same as `foo/bar` (#10313) by @sapphi-red - magic-string: throw instead of panicking when append/prepend splits an edited chunk (#10301) by @IWANABETHATGUY - output: skip separator for empty hoisted functions (#10310) by @hyfdev - magic-string: don't report a change when edits cancel out (#10299) by @IWANABETHATGUY - sourcemap: map wrapped imported callees (#10288) by @hyfdev - magic-string: count positional inserts at index 0 on an empty source (#10306) by @IWANABETHATGUY - magic-string: return UTF-16 code units from length() (#10295) by @IWANABETHATGUY - output: skip separator for empty import prelude (#10289) by @hyfdev ### 🚜 Refactor - dev: make it easier to customize the runtime (#10338) by @sapphi-red - code-splitting: declare CJS carrier namespaces initializer-free (#10513) by @hyfdev - rollup-tests: avoid relying on `NODE_PATH` env var resolution (#10496) by @sapphi-red - code-splitting: derive symbol chunk ownership as pass-local link data (#10449) by @hyfdev - plugin: share asset emission helpers between copy_module and asset_module (#10450) by @Nic-Polumeyv - plugin: dedupe hook binding boilerplate (#10428) by @Nic-Polumeyv - plugin: share the oxc parse preamble between import-glob and dynamic-import-vars (#10452) by @Nic-Polumeyv - implement `GetAstBuilder` on visitor/pass types (#10424) by @overlookmotel - pass arrays to AST builder methods (#10419) by @overlookmotel - shorten AST builder code (#10418) by @overlookmotel - centralize final ESM init metadata (#10376) by @hyfdev - descriptor-based parallel-plugin detection (getParallelPluginInfo) (#10349) by @Brooooooklyn - remove AstFactory in favor of new_* construction traits (#10353) by @Boshen - enable oxc_ast's `disable_old_builder` feature (#10316) by @Boshen ### 📚 Documentation - development-guide: update config.json link line reference (#10523) by @dogledogle - clarify slash normalization for id hook filter (#10511) by @sapphi-red - code-splitting: record the thenable-namespace rule (#10502) by @hyfdev - clarify that `esmExternalRequirePlugin` must own its externals (#10439) by @TheAlexLichter - add troubleshooting entry for the @rolldown/binding-... resolution error (#10390) by @Nic-Polumeyv - add Code Splitting API reference category (#10382) by @vittorioexp ### ⚡ Performance - use VisitJsMut in JavaScript-only AST passes (#10507) by @Boshen - cache the resolved glob matcher for Id/ImporterId filters (#10410) by @IWANABETHATGUY - scope_hoisting: remove `alloc` field from `ScopeHoistingFinalizer` (#10425) by @overlookmotel - reduce string allocations (#10423) by @overlookmotel - hmr: remove unnecessary string allocations (#10422) by @overlookmotel - avoid allocating static strings via custom builder methods (#10421) by @overlookmotel - pass string literals to AST builder methods (#10420) by @overlookmotel - use VisitJs in JavaScript-only AST passes (#10396) by @Boshen - plugin: shrink erased plugin debug vtables (#10370) by @Boshen - reuse normalized path buffers (#10315) by @hyfdev ### 🧪 Testing - code-splitting: pin CJS barrel fallback triggers and flag-off equivalence (#10514) by @hyfdev - plugin: cover chunk fileName, absolute chunk name, and Windows drive names (#10400) by @IWANABETHATGUY - code-splitting: cover a single-chunk dynamic import of a wrapped module (#10505) by @hyfdev - file: remove the unused .dynamic_import fixture (#10481) by @hyfdev - treeshake: reduce manual-pure fixture to a minimal smoke test, move matrix to unit tests (#10436) by @IWANABETHATGUY - dev: run vite playgrounds under bundled dev (test-serve-bundled) (#10434) by @h-a-n-a - stop snapshots from silently hiding user code inside the runtime region (#10431) by @hyfdev - wasi: add tokio runtime lifecycle regression test (#10379) by @shulaoda - dev: expect preserved hot.data in hmr-whole-chain-dispose (#10380) by @h-a-n-a - code-splitting: pin flag-off wrapped-esm init emission (#10324) by @hyfdev - sourcemap: fix Windows-only failures in composition fixtures (#10368) by @shulaoda - generated-code: mark assert external in symbols_ns2 for deterministic output (#10339) by @hyfdev - code-splitting: cover entries-aware strict init cycle (#10307) by @hyfdev - make `just setup-vite` the only entry point that touches `vite/` (#10332) by @shulaoda - vite-tests: reuse the shared root vite checkout (#10325) by @shulaoda - test-dev-server: drop the vite submodule, track rolldown-canary (#10319) by @shulaoda - vite-tests: track the latest rolldown-canary rebased onto vite main (#10318) by @shulaoda - unify Vite in vite-test and test-dev-server (#10293) by @h-a-n-a ### ⚙️ Miscellaneous Tasks - scope the binding artifact download to `bindings-*` (#10525) by @shulaoda - deps: update napi (#10506) by @renovate[bot] - deps: update rollup submodule for tests to v4.62.3 (#10494) by @rolldown-guard[bot] - deps: upgrade oxc to 0.142.0 (#10497) by @shulaoda - deps: update test262 submodule for tests (#10495) by @rolldown-guard[bot] - deps: update github actions (#10460) by @renovate[bot] - deps: update rust crates (#10462) by @renovate[bot] - deps: update npm packages (#10461) by @renovate[bot] - metric: alert Discord when the metric workflow fails (#10447) by @IWANABETHATGUY - metric: mint a short-lived GitHub App token instead of a PAT (#10446) by @IWANABETHATGUY - repo: repair dangling CLAUDE.md symlink (#10451) by @hanayashiki - publint/package-metadata fixes and small correctness fixes (#10344) by @Brooooooklyn - deps: update dependency vite-plus to v0.2.6 (#10404) by @renovate[bot] - add Semgrep scan workflow (#10395) by @Boshen - enable Windows CI with a PR label (#10375) by @hyfdev - deps: upgrade oxc to 0.141.0 (#10373) by @camc314 - deps: update napi (#10369) by @renovate[bot] - deps: update test262 submodule for tests (#10367) by @rolldown-guard[bot] - deps: update taiki-e/install-action action to v2.83.3 (#10358) by @renovate[bot] - deps: update rust crates (#10357) by @renovate[bot] - deps: update npm packages (#10359) by @renovate[bot] - deps: update rust crate syn to v3 (#10362) by @renovate[bot] - deps: update rust crate oxc_sourcemap to v8.1.2 (#10356) by @renovate[bot] - deps: update dependency vite-plus to v0.2.5 (#10329) by @renovate[bot] - docs: migrate Netlify redirects to Void config (#10282) by @tux-tn - deps: update dependency rust to v1.97.1 (#10317) by @renovate[bot] - runtime: remove unused `runtime/index.js` (#10302) by @IWANABETHATGUY ### ❤️ New Contributors * @dogledogle made their first contribution in [#10523](#10523) * @tbvjaos510 made their first contribution in [#10489](#10489) * @martijnwalraven made their first contribution in [#10440](#10440) * @shantanuraj made their first contribution in [#10438](#10438) * @Nic-Polumeyv made their first contribution in [#10450](#10450) * @hanayashiki made their first contribution in [#10451](#10451) * @unknownjedi made their first contribution in [#10426](#10426) * @vittorioexp made their first contribution in [#10382](#10382) * @tux-tn made their first contribution in [#10282](#10282) Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>

#10433 lets a dynamic import collapse inline when chunking already placed the target in the importer's own chunk, and the collapse carries the target's deferred initialization. A cross-chunk dynamic importer was left keeping its facade chunk. It does not need one: the cross-chunk rewrite emits
import('./host.js').then(n => (n.init_x(), n.namespace)), which carries the trigger just as well. Both arms already share that shape through theesm_init_targetview, so this only stops asking for a file.Emitted JS files, same Vite, only the rolldown package swapped:
strictExecutionOrderoffThe 1008 files that come off lobehub are one restored facade per lazy route. ComfyUI_frontend and plane are unchanged: neither has a dynamic entry the chunk optimizer merged away, so neither restored a facade to begin with.
lobehub's residual +59 is the other facade source,
create_order_wrap_entry_facades, and is out of scope here. Those entries still have a chunk of their own, so declining to split one leaves its inlineinit_E()at the top of a chunk other chunks import — the hazard the split exists for. Reaching them needs the chunk demoted without a facade emitted, which is a separate change.Motivation
A restored facade exists so a merged entry's trigger has a load site of its own. When every live importer of that entry is a dynamic one, every entry path into it is an
import()whose rewrite can run the trigger, so the load site is redundant — the facade is a file whose whole body isinit_x().This is also what
strictExecutionOrderdid before #10104. That release drove SEO throughWrapKind::Esm, so a merged dynamic entry took the same cross-chunk rewrite and shipped no facade;v1.2.0emitsimport(\./FileViewer-9VJj0tgR.js`).then(e => (e.r(), e.n))on lobehub and 1471 JS files, 47 fewer than with the flag off. #10104 moved order wrapping onto its own plan, wherewrap_kindstaysNone`, and routed those entries to facade restoration instead.Unchanged: a module with no dynamic importer has no call site to carry anything, and an emitted chunk still needs a real file for its reference id to resolve to. Two more shapes cannot carry the trigger either, and keep their facade for the same reason: a two-argument
import(), whose specifiertry_rewrite_import_expressionnever rewrites, so a file has to stay at the name the source wrote; and a host chunk that could expose a callablethento a cross-chunk importer, where promise resolution assimilates the chunk namespace as a thenable and the rewrite's extraction callback never receives it. Thethencheck reads both the source-level export alias and the declaring symbol name, since a common chunk emits cross-chunk exports under the latter.What this gives up
The trigger now runs in the rewrite's
.then, one microtask after the host chunk settles, instead of inside a facade's own evaluation. A microtask already queued when the chunk settles therefore observes the target uninitialized.m4_dynamic_facade_racemeasures exactly that and its runtime oracle changes:What an importer sees is unaffected — by the time its own
awaitresumes, the target has run — andcross_chunk_dynamic_importer_uses_call_site_triggerstill asserts at runtime that enteringbinitializes the target without running entrya. Counting execution-order violations in lobehub's emitted output (a chunk whose inline__esminit another chunk statically imports) gives 58 with the flag off and 0 both before and after this PR.So the trade is 1008 files against that one microtask, on the same side of it that shipped in 1.2.0.
Tests
wrapped_dynamic_entry_keeps_facade_after_manual_chunk_mergebecomeswrapped_dynamic_entry_uses_the_call_site_trigger_after_manual_chunk_merge:aimports the host chunk, the rewrite carries the trigger, and notarget.jsis emitted.cross_chunk_dynamic_importer_keeps_facadebecomescross_chunk_dynamic_importer_uses_call_site_trigger. Its runtime assertions are unchanged; only the facade in its snapshot is gone.m4_dynamic_facade_racerecords the microtask ordering above.dynamic_import_options,dynamic_import_then_exportanddynamic_import_then_export_aliascover the two keep-facade cases above at runtime; all three came out of review feedback on this PR.