fix(rolldown_plugin_lazy_compilation): escape request ID in proxy modules#9102
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.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
8c7a0ae to
86fbe10
Compare
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR fixes Windows path corruption in the lazy compilation plugin by properly escaping module IDs when embedding them into the generated proxy module JavaScript, preventing backslashes from being interpreted as JS escape sequences.
Changes:
- Replace manual
format!("\"{id}\"")string splicing withserde_json::to_stringto emit valid JS/JSON string literals. - Extract placeholder substitution into a dedicated
render_proxy_templatehelper. - Enable and add unit tests covering Windows and Unix path rendering; add
serde_jsondependency.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| crates/rolldown_plugin_lazy_compilation/src/lazy_compilation_plugin.rs | Uses serde_json::to_string for safe ID embedding, factors rendering into a helper, and adds cross-platform unit tests. |
| crates/rolldown_plugin_lazy_compilation/Cargo.toml | Adds serde_json dependency and enables running crate unit tests. |
| Cargo.lock | Records the new serde_json dependency for the crate. |
Merge activity
|
|
Looks like we're good for copilot as well |
…ules (#9102) ## Summary The lazy compilation plugin's `load` hook splices module IDs directly into the proxy module template via `format!("\"{id}\"")`. On Windows, absolute paths contain backslashes that the JS lexer then parses as escape sequences — `\U`, `\f`, `\r`, `\t`, `\b` silently corrupt the embedded path, breaking the `/@vite/lazy?id=...` URL handed to the dev server. Switch to `serde_json::to_string` so IDs are emitted as proper JSON string literals with backslashes, quotes, and control characters all escaped. Extract the substitution into `render_proxy_template` so it can be unit-tested cross-platform. ## Test plan - [x] `cargo test -p rolldown_plugin_lazy_compilation` — new `windows_path` / `unix_path` unit tests pass ## Related issues Partially fixed #8995
86fbe10 to
f1cb5a2
Compare
## [1.0.0-rc.16] - 2026-04-16 ### 🚀 Features - const enum cross-module inlining support (#8796) by @Dunqing - implement module tagging system for code splitting (#9045) by @hyf0 ### 🐛 Bug Fixes - rolldown_plugin_vite_manifest: handle duplicate chunk names for CSS entries (#9059) by @sapphi-red - improve error message for invalid return values in function options (#9125) by @shulaoda - await async export-star init wrappers (#9101) by @thezzisu - never panic during diagnostic emission (#9091) by @IWANABETHATGUY - include array rest pattern in binding_identifiers (#9112) by @IWANABETHATGUY - rolldown: set worker thread count with ROLLDOWN_WORKER_THREADS (#9086) by @fpotter - rolldown_plugin_lazy_compilation: escape request ID in proxy modules (#9102) by @h-a-n-a - treat namespace member access as side-effect-free (#9099) by @IWANABETHATGUY - relax overly conservative side-effect leak check in chunk optimizer (#9085) by @IWANABETHATGUY - runtime: release `cb` reference after `__commonJS` factory initialization (#9067) by @hyf0-agent - `@__NO_SIDE_EFFECTS__` wrapper should not remove dynamic imports (#9075) by @IWANABETHATGUY - rolldown_plugin_vite_import_glob: use POSIX path join/normalize for glob resolution (#9077) by @shulaoda - emit REQUIRE_TLA error when require() loads a module with top-level await (#9071) by @jaehafe - emit namespace declaration for empty modules in manual chunks (#8993) by @privatenumber - rolldown_plugin_vite_import_glob: keep common base on path segment boundary (#9070) by @shulaoda - prevent circular runtime helper imports during facade elimination (#8989) (#9057) by @IWANABETHATGUY - correct circular dependency check in facade elimination (#9047) by @h-a-n-a - docs: correct dead link in CodeSplittingGroup.tags JSDoc (#9051) by @hyf0 - emit DUPLICATE_SHEBANG warning when banner contains shebang (#9026) by @IWANABETHATGUY ### 🚜 Refactor - use semantic reference flags for member write detection (#9060) by @Dunqing - extract UsedSymbolRefs newtype wrapper (#9130) by @IWANABETHATGUY - dedupe await wrapping in export-star init emit (#9119) by @IWANABETHATGUY - calculate side-effect-free function symbols on demand (#9120) by @IWANABETHATGUY - extract duplicated top-level await handling into shared helper (#9087) by @IWANABETHATGUY - rolldown_plugin_vite_import_glob: use split_first for get_common_base (#9069) by @shulaoda - simplify ESM init deduplication with idiomatic insert check (#9044) by @IWANABETHATGUY ### 📚 Documentation - document runtime module placement strategy in code-splitting design (#9062) by @IWANABETHATGUY - clarify `options` hook behavior difference with Rollup in watch mode (#9053) by @sapphi-red - meta/design: introduce module tags (#9017) by @hyf0 ### ⚡ Performance - convert `generate_transitive_esm_init` to iterative (#9046) by @IWANABETHATGUY ### 🧪 Testing - merge strict/non_strict test variants using configVariants (#9089) by @IWANABETHATGUY ### ⚙️ Miscellaneous Tasks - disable Renovate auto-updates for oxc packages (#9129) by @IWANABETHATGUY - upgrade [email protected] (#9127) by @Dunqing - deps: update napi to v3.8.5 (#9126) by @renovate[bot] - deps: update dependency @napi-rs/cli to v3.6.2 (#9123) by @renovate[bot] - move lazy-compilation design doc (#9117) by @h-a-n-a - deps: update dependency vite-plus to v0.1.18 (#9118) by @renovate[bot] - deps: update dependency vite-plus to v0.1.17 (#9113) by @renovate[bot] - deps: update oxc to v0.125.0 (#9094) by @renovate[bot] - deps: update dependency follow-redirects to v1.16.0 [security] (#9103) by @renovate[bot] - deps: update test262 submodule for tests (#9097) by @sapphi-red - deps: update crate-ci/typos action to v1.45.1 (#9096) by @renovate[bot] - deps: update rust crates (#9081) by @renovate[bot] - deps: update npm packages (#9080) by @renovate[bot] - remove outdated TODO in determine_module_exports_kind (#9072) by @jaehafe - rust/test: support `extendedTests: false` shorthand in test config (#9050) by @hyf0 - ci: extract shared infra-changes anchor in path filters (#9054) by @hyf0 - add docs build check to catch dead links in PRs (#9052) by @hyf0 ### ❤️ New Contributors * @thezzisu made their first contribution in [#9101](#9101) * @fpotter made their first contribution in [#9086](#9086) * @jaehafe made their first contribution in [#9071](#9071) * @privatenumber made their first contribution in [#8993](#8993) Co-authored-by: shulaoda <[email protected]>
### Description
Rewrites `internal-docs/lazy-compilation/{design,implementation}.md` to match the current implementation. The docs dated back to the initial POC (split out in #9826) and had drifted from the code in several load-bearing places. Every claim was re-verified against current `main` (file/line spot-checks, cross-checked with the playground specs).
#### Corrections to stale / wrong content
- **Proxy templates**: both inline code blocks are now the real `proxy-module-template{,-fetched}.js` verbatim. The stub evicts its runtime cache entry and ends with `return await loadExports($STABLE_PROXY_MODULE_ID)['rolldown:exports']` — the two-level promise chain from #9981 that makes init errors catchable at the consumer's `await import()`. The fetched template returns `loadExports($STABLE_MODULE_ID)` instead of the import namespace (export-name preservation in shared chunks, #9132).
- **Module IDs**: design.md's TL;DR claimed absolute paths are used "consistently throughout the runtime" — runtime lookups actually use stable (cwd-relative) ids; absolute paths survive only in the `/@vite/lazy?id=` param and the fetched template's `import($MODULE_ID)`. Also documents the four template placeholders and their JSON-quoted rendering (#9102).
- **Error handling**: replaced "`Err` or panic is fine for POC" with the actual contract — unknown ids are rejected as a cache-key-only security gate (#9969), napi surfaces `Failed to compile lazy entry:` → HTTP 500, and init errors are catchable on both cold and warm paths (#9975/#9981).
- **Dedup**: the race-condition section's "potential future enhancement" runtime guard is long implemented — lazy chunks render `createEsm/CjsInitializer(stableId, factory, 1)` with a dedup flag (HMR patches deliberately omit it so updates re-execute). Rewrote the section around the two-layer dedup: per-client `executed_modules` pruning + the runtime flag.
- **Rebuild id normalization**: the old note said the `ModuleChanged` rebuild "should resolve or normalize" the proxy id to a real module id — the raw proxy id is deliberately correct (it is the incremental-cache invalidation key for the proxy whose content changed; normalizing would leave the stale stub cached). Note removed and the actual mechanism documented.
- Misc: `lazyMagic` helper (never existed), `__export` → `__exportAll`, `snippet` → `ast_factory` in the Lessons Learned snippets, `registerModule(stableId, { exports })` shape.
#### New coverage
- **Enabling / wiring**: `experimental.devMode.lazy: true`, inner-plugin registration, the shared `LazyCompilationContext` handed to the `DevEngine`
- **Lazy chunk rendering**: initializer wrappers + `__rolldown_module_id__` param, the nested-lazy `import()` → `/@vite/lazy` rewrite, entry init call, `lazy_compile_{n}.js` naming
- **Emitted assets**: delivered via `onAdditionalAssets` before the code returns (#9815)
- **Build output refresh**: `update_watch_paths()` runs first (what makes post-fetch edits watchable at all), silent output swap for connected clients, failure paths (#9903)
- **Editing a fetched lazy module**: per-client HMR outcomes; a non-accepting proxy escalates to FullReload → HmrRebuild with a deferred reload
- **Client sessions**: implicit creation on `hmr:module-registered`, removal on ws disconnect, the `"rolldown-tests"` escape hatch; clientId prunes the patch — nothing is "routed"
- **Known limitations**: link-stage-synthesized exports (JSON/text/base64/dataurl register `{}` inside lazy chunks until rebuild + refresh), CSS error deferral, assets require load-hook plugins, sourcemaps (inline-only for lazy chunks — the map asset is discarded on the lazy path, unlike `HmrPatch`)
- **Test coverage table**: the 7 playground specs + `dev-lazy-compile.test.ts` and what each pins
Docs only — no runtime change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Summary
The lazy compilation plugin's
loadhook splices module IDs directly into the proxy module template viaformat!("\"{id}\""). On Windows, absolute paths contain backslashes that the JS lexer then parses as escape sequences —\U,\f,\r,\t,\bsilently corrupt the embedded path, breaking the/@vite/lazy?id=...URL handed to the dev server.Switch to
serde_json::to_stringso IDs are emitted as proper JSON string literals with backslashes, quotes, and control characters all escaped. Extract the substitution intorender_proxy_templateso it can be unit-tested cross-platform.Test plan
cargo test -p rolldown_plugin_lazy_compilation— newwindows_path/unix_pathunit tests passRelated issues
Partially fixed #8995