feat(dev): client-side HMR#10164
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 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
1a4a603 to
1162141
Compare
ae16bb9 to
069e292
Compare
|
@codex review this |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 069e292b97
ℹ️ 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".
dbaa9df to
0cb9954
Compare
0cb9954 to
68eac41
Compare
e5793fc to
49a06d5
Compare
This comment was marked as resolved.
This comment was marked as resolved.
a90f3de to
671f1e7
Compare
|
f9ee937 to
8d37643
Compare
Second-round review at
|
sapphi-red
left a comment
There was a problem hiding this comment.
no more comments from my side. I'll leave the implementation part to @hyfdev.
8d37643 to
fad060c
Compare
Merge activity
|
<!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary This stack of PRs focus on moving bundled-dev (full-bundle mode) HMR to client-side. This PR adds the two client-side data structures (a **module factory map** next to the existing module cache, and a **module graph** shipped as a payload with `__rolldown_runtime__.registerGraph` in the bundle and patched by deltas), so boundary computation, bubbling, and disposal move from the server to the client runtime. ## Shape changes - Initial/rebuild bundle: Added `__rolldown_runtime__.registerGraph` for storing module graph payload in runtime. - HMR patch: - Replaced `createEsmInitializer` / `createCjsInitializer` with `__rolldown_runtime__.registerFactory` that can be reused for HMR invalidation - Moved `applyUpdates` to the place where web-socket messages are received. It applies the change as soon as the new HMR patch has been executed. (Note: This happens in Vite's dev server `FbmHMRClient`) ## Design and Principles ### Pros - **Each browser tab maintains its own state.** (This works today too, but with a delay: the browser currently tells the server over WebSocket which modules have executed, and whether a module has executed affects HMR boundary computation.) - **Smaller HMR payloads** — send only changed, not-yet-sent factories. Today every update sends the browser all modules on the path from the changed modules to the boundary; we want to send only the changed modules. - **Remove the lazy-compilation dedup code**, and at the same time fix the oversized lazy chunk problem. Today a lazy chunk contains all modules, so multiple lazy modules ship duplicate modules. ### Building blocks of client-side HMR - **Module Factory map** (register / update) — when a module updates, the module code along the whole update chain must re-execute. - **Module Cache** (register / clear) — tells whether a module is registered, which affects HMR boundary computation. - **Module Graph** — drives client-side bubbling. - **Client-side HMR boundary computation, client bubble, module disposal** (clearing the module cache). ### Breaking down Webpack - The Module Factory map and Module Cache exist natively. - Module graph: Webpack gets it by rewriting inside `require`. - The key part of HMR is knowing whether a module has executed, since that affects boundary computation. Dynamic import supports this natively — just check at runtime whether the module cache entry exists. Module execution errors also do not matter, because registering exports is the first step of a Webpack module's execution. ### Rolldown principles - Keep the semantics of the Rolldown initial bundle and the full-rebuild bundle; use scope hoisting. - The server does not compute the HMR boundary. - Smaller patches, smaller lazy chunks. ### Rolldown design - **Module Cache** already exists; we only need to add a **Module Factory map**. - **Module graph:** does not exist today. Because the initial bundle is scope-hoisted, we need another way to deliver the module graph. We can insert a module graph payload into the initial bundle (or a rebuild bundle) that records the edges; during HMR the client then computes importers from it. - **"Has this module executed" check:** once HMR moves to the client, this is natively supported, and there is no delay that makes the HMR boundary computation inaccurate. - **Factory optimizations:** - **Incremental Module Factory** — the initial bundle carries the module graph and registers it directly as a payload. Later HMR updates send only module graph deltas, applied incrementally. - **The server is stateful** — it keeps a per-tab record (the ship map) of each browser tab's factory registration state and registered versions, and uses it to decide whether a factory must be re-sent to the browser. - **After a browser refresh**, the factories along the update chain must be re-sent during HMR. ## Testing Vite commit in `packages/test-dev-server` is checked out to a dedicated commit solely for this refactor. `packages/vite-tests` is now based on the same commit as the one in `test-dev-server` in order to let `hmr` tests pass.
fad060c to
84afd72
Compare
shulaoda
left a comment
There was a problem hiding this comment.
We don't need to solve everything in a single PR. Some of the smaller issues can be addressed in follow-up PRs on top of this stack.
<!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary For full design and principles, please refer to #10164 (comment). This PR adds the server-side **ship map**: a per-client record of which factory versions each browser tab already has, so patches and lazy chunks carry only changed, not-yet-shipped factories. ## Example The stateful server's ship map would incrementally ship factories that are only outdated or never shipped. ### The module graph ```mermaid flowchart LR app["app.js"] --> foo["foo.js<br/>(import.meta.hot.accept — boundary)"] foo --> bar["bar.js"] bar --> baz["baz.js<br/>(edited twice)"] ``` ### Server & client interaction ```mermaid sequenceDiagram participant S as Server (keeps shipped[C]) participant C as Browser tab C Note over C: initial bundle is scope-hoisted — no factories registered on the client yet Note over S: shipped[C] = { } Note over S,C: ① first edit of baz.js S->>C: patch 1 — factories foo@v0 + bar@v0 + baz@v1 (whole re-run chain, nothing shipped yet) C->>C: re-run baz → bar → foo, foo accepts Note over S: shipped[C] = { foo@v0, bar@v0, baz@v1 } Note over S,C: ② second edit of baz.js S->>C: patch 2 — factory baz@v2 only (foo@v0 and bar@v0 are still current in shipped[C]) C->>C: re-run baz → bar → foo, foo accepts Note over S: shipped[C] = { foo@v0, bar@v0, baz@v2 } ``` The first edit ships the whole re-run chain because the initial bundle is scope-hoisted, so the client holds no factories yet. The second edit ships only `baz` — the ship map says `foo` and `bar` were already delivered and are not stale. Rendered, the flow reads: app.js ──> foo.js ──> bar.js ──> baz.js (accept) (edited) edit ① patch = [foo@v0, bar@v0, baz@v1] shipped[C]: {} -> {foo@v0, bar@v0, baz@v1} edit ② patch = [baz@v2] shipped[C]: baz v1 -> v2, rest untouched
<!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary For full design and principles, please refer to #10164 (comment). This PR removes the full-reload decision from the server's HMR update type, so the client decides to reload from its own boundary result.
…es (#10223) <!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary For full design and principles, please refer to #10164 (comment). The dev server tracks which module factories it has shipped per client in #10208. But modules that the entry chunk evaluated at top level never in the ship map. A lazy compile that subtracts only the ship man therefor re-ships factories the client already holds, which causes a size bloat in lazy-compilation chunks. This PR introduces a second per-client record - the `top_level_evaluated` map. This map contains the statically evaluated modules so that these modules are not shipped as factories in lazy compilation chunks again when the factories are not changed and only their exports are needed.
…es (#10223) <!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary For full design and principles, please refer to #10164 (comment). The dev server tracks which module factories it has shipped per client in #10208. But modules that the entry chunk evaluated at top level never in the ship map. A lazy compile that subtracts only the ship man therefor re-ships factories the client already holds, which causes a size bloat in lazy-compilation chunks. This PR introduces a second per-client record - the `top_level_evaluated` map. This map contains the statically evaluated modules so that these modules are not shipped as factories in lazy compilation chunks again when the factories are not changed and only their exports are needed.
<!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! --> ## Summary For full design and principles, please refer to #10164 (comment). ## The playgrounds Each playground is a small app (`index.html` + JS modules + `dev.config.mjs` + `package.json`) with a `__tests__/*.spec.ts` that edits a file and asserts on the running page. Several port behavior expectations from Vite's HMR tests (the `hmr-hot-off` fixture says so explicitly). | Group | Playgrounds | What they exercise | |---|---|---| | Accept | `hmr-accept-exports`, `hmr-nested-dep-accept` | `hot.accept` receiving new exports; accepting a dep deeper in the graph | | Circular graphs | `hmr-circular-accept-outside`, `hmr-circular-self-accept`, `hmr-invalidate-circular` | accept / self-accept / `hot.invalidate` when the edited module sits in an import cycle | | Lifecycle / state | `hmr-dispose-data`, `hmr-prune` | `hot.dispose` passing state through `hot.data`; `hot.prune` when a module is no longer imported | | Events | `hmr-hot-events`, `hmr-hot-off` | `hot.on` for built-in `vite:beforeUpdate` / `vite:afterUpdate`; `hot.off` removing one listener while another stays | | Dynamic import + lazy compilation | `hmr-lazy-dynamic-import-accept-dep`, `hmr-lazy-dynamic-import-self-accept`, `hmr-not-loaded-dynamic-import` | updates to dynamically imported modules, including under lazy compilation and when the module was never loaded | | Full reload | `hmr-before-full-reload` | behavior around an update that must fall back to a full page reload |
## [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]>
…n the main merge Pre-merge, compile_entry routed errors through dev_engine_binding_result like run/ensure_current_build_finish, so an onAdditionalAssets rejection reached JS as the original error object. The #10164 merge reconciliation kept main's map_err(from_reason(format!())), which flattens it into a GenericFailure string. Route the {code, filename} result through the passthrough again and unwrap the BindingResult in the TS wrapper. Also drop the dead 'auto' arm in bindingifyRebuildStrategy: main removed BindingRebuildStrategy::Auto (the server no longer decides full reloads), and the leftover arm no longer type-checks against the merged enum. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T34fMrYCf2V13Mg8BKmnda
…dev APIs node-test-ubuntu runs the regular async-runtime binding, which does not export the __rolldownTest* lifecycle probes; the probe suites' guards only checked asyncRuntimeBuild/backend, so their child fixtures threw instead of skipping. Gate each test on the exact probe its fixture needs. The probe CI lane's ROLLDOWN_TEST_REQUIRE_SHARED_ASYNC_RUNTIME=1 bypasses the gate so a probe binding missing its probes fails loudly there instead of silently skipping. dev-close: drop asserts on invalidate/registerModules (removed by #10164) and match compileEntry's merged {code, filename} return shape. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T34fMrYCf2V13Mg8BKmnda
…main (#10318) ## Problem Since #10164 (and #10293 which unified the checkouts), `packages/vite-tests/run.ts` ran Vite's test suite on the commit pinned by the vite submodule gitlink. The `rolldown-canary` branch of vitejs/vite is regularly rebased and force-pushed, so any pinned commit rots quickly: the current pin `4d1480ca` is no longer on any branch. Worse, test adjustments landing on `rolldown-canary` cannot reach rolldown CI without a manual pin bump. That is exactly why CI on `main` is red right now: the `js-sourcemap` inline snapshot was updated on `rolldown-canary` on 2026-07-15 (for the output change from #10249), but the pin predates that update. ## Change Restore the pre-#10164 approach from #7633. `run.ts` now does: ``` git clone --branch rolldown-canary https://github.com/vitejs/vite.git git rebase origin/main ``` so test adjustments landing on `rolldown-canary` take effect right away, and new tests from Vite `main` surface incompatibilities with rolldown early. The rebase identity comes from the existing "Configure Git" step in the vite-test CI jobs. All inline spec patches are removed from `run.ts`. Test adjustments belong on the `rolldown-canary` branch itself, not in a patch layer inside this repo: - The `css-codesplit` style-/style2- patch was already dead code: upstream Vite `main` has had the swapped assertions since vitejs/vite#22922. - The `assets` raw-query skip (#8839) is obsolete: the test passes on current rolldown `main`, in both serve and build. - The `hmr-full-bundle-mode` invalidate patch is removed. This one still needs a spec fix on `rolldown-canary`: with client-side HMR there is no "hmr invalidate" server log anymore, so the spec should assert that `.invalidation-parent` becomes `child updated` instead. Until that lands on the canary branch, `test-serve` fails this single test. Also reverts the `tsconfig.json` include added for the now-removed `checkout.ts` import, and updates the `repo-structure.md` description. ## Verification Full local run against current rolldown `main` (debug build): | Suite | Result | | --- | --- | | test-unit | 65 files passed | | test-serve | 1 real failure: `hmr-full-bundle-mode > invalidate` (expected, see above) | | test-build | 93 files passed, `js-sourcemap` snapshot green again | `environment-react-ssr > deps reload` failed once under full-suite load but passes in isolation (timing flake with the slow debug binding, not a regression). <!-- - What is this PR solving? Write a clear and concise description. - Reference the issues it solves (e.g. `fixes #123`). - What other alternatives have you explored? - Are there any parts you think require more attention from reviewers? Also, please make sure you do the following: - Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it. - Update the corresponding documentation if needed. - Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why. Thank you for contributing to Rolldown! -->

Summary
This stack of PRs focus on moving bundled-dev (full-bundle mode) HMR to client-side.
This PR adds the two client-side data structures (a module factory map next to the existing module cache, and a module graph shipped as a payload with
__rolldown_runtime__.registerGraphin the bundle and patched by deltas), so boundary computation, bubbling, and disposal move from the server to the client runtime.Shape changes
__rolldown_runtime__.registerGraphfor storing module graph payload in runtime.createEsmInitializer/createCjsInitializerwith__rolldown_runtime__.registerFactorythat can be reused for HMR invalidationapplyUpdatesto the place where web-socket messages are received. It applies the change as soon as the new HMR patch has been executed. (Note: This happens in Vite's dev serverFbmHMRClient)Design and Principles
Pros
Building blocks of client-side HMR
Breaking down Webpack
require.Rolldown principles
Rolldown design
Testing
Vite commit in
packages/test-dev-serveris checked out to a dedicated commit solely for this refactor.packages/vite-testsis now based on the same commit as the one intest-dev-serverin order to lethmrtests pass.