Skip to content

feat(dev): client-side HMR#10164

Merged
graphite-app[bot] merged 1 commit into
mainfrom
07-07-feat_dev_client-side_hmr
Jul 15, 2026
Merged

feat(dev): client-side HMR#10164
graphite-app[bot] merged 1 commit into
mainfrom
07-07-feat_dev_client-side_hmr

Conversation

@h-a-n-a

@h-a-n-a h-a-n-a commented Jul 7, 2026

Copy link
Copy Markdown
Member

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.

h-a-n-a commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add 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.

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 8b914a0
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a4c6fb3284ece000818d499
😎 Deploy Preview https://deploy-preview-10164--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 84afd72
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a572e34e0d7fd00080a1ad7
😎 Deploy Preview https://deploy-preview-10164--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 4 times, most recently from 1a4a603 to 1162141 Compare July 7, 2026 13:40
@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 4 times, most recently from ae16bb9 to 069e292 Compare July 8, 2026 06:04
@h-a-n-a

h-a-n-a commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@codex review this

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/rolldown/src/hmr/hmr_stage.rs
Comment thread crates/rolldown/src/hmr/hmr_stage.rs
Comment thread packages/test-dev-server/src/utils/decode-client-message.ts Outdated
@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 3 times, most recently from dbaa9df to 0cb9954 Compare July 9, 2026 09:24
@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch from 0cb9954 to 68eac41 Compare July 9, 2026 12:14
@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 3 times, most recently from e5793fc to 49a06d5 Compare July 10, 2026 06:15
@hyfdev

This comment was marked as resolved.

@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 2 times, most recently from a90f3de to 671f1e7 Compare July 13, 2026 10:24
@h-a-n-a

h-a-n-a commented Jul 14, 2026

Copy link
Copy Markdown
Member Author
  1. The server-computed factory “superset” is not a superset
    Nice finding. This is actually a case where we're not able to cover for now. Just the same as Vite.
  2. A cold scope-hoisted module can execute stale code after its file changed
    This should be regarded as a side-effect, which should be handled by dispose() API.
  3. Factory registration has no cross-channel version ordering
    Yes, this is not expected. I haven't though of a better way to solve this without bloating size. Given, it's pretty edge. I will create a new PR for this in the future.

@h-a-n-a
h-a-n-a marked this pull request as ready for review July 14, 2026 05:36
@h-a-n-a
h-a-n-a requested a review from sapphi-red as a code owner July 14, 2026 05:36
@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch 4 times, most recently from f9ee937 to 8d37643 Compare July 15, 2026 05:01
@hyfdev

hyfdev commented Jul 15, 2026

Copy link
Copy Markdown
Member

Second-round review at f9ee9377a

This pass was done by Claude Fable 5; findings were verified against the code before posting.

Follow-up to my first-round review. Status there, as I read the current head: #1 stands as an accepted limitation (should be spelled out in #10271 — and focus item 1 below is a second hole in the same invariant); #2/#3 acknowledged but have no owning PR in the stack; #4 unanswered and still visible in the tree (the test runtime walks the post-import graph at crates/rolldown_testing/src/integration_test.rs:889, the FBM client walks the pre-import graph, the default runtime has no walk at all).


Review focus — needs a decision or a fix in this PR

1. A re-run factory can hit a newly-added static dep that is in the graph, never executed, and never shipped

The shipped superset covers the upward importer walk plus new_added_modules (graph-new modules only). A re-run executes new code, and new code can add downward static edges to modules that fail all three coverage conditions:

// routes/heavy.js — in the graph, but only reachable through a never-triggered import()
// m.js — the edit adds:
import { heavy } from './routes/heavy.js';
import.meta.hot.accept();

Server walk from m: self-accepting → superset = {m}. heavy is not graph-new and not on the up-walk → factory not shipped. Client: the pre-check only covers the update set (hmr-runtime.js:91-95), so m is evicted, its factory re-runs and hits initModule("routes/heavy.js") (impl_traverse_for_hmr_ast_finalizer.rs:45-58) → not in the module cache, no factory → MissingFactoryError mid-apply, after m was already re-registered with empty exports.

With lazy compilation on, the single-tab case is mostly covered (the real module tends to be graph-new at that point), but the multi-tab variant survives: tab A triggers the lazy compile (the module enters the table), tab B never loads that chunk; editing m ships only m to both tabs, and tab B throws mid-apply.

Same class as first-round #1, different mechanism (down-edges of re-run code vs. acceptance divergence). If the verdict is "accepted limitation, falls back to reload", it belongs in #10271's invariant statement: superset = upward static chain ∪ graph-new modules; runtime-acceptance divergence and new down-edges of re-run code are not covered; any escape falls back to reload. A real fix could ship the changed modules' newly-added static deps that no payload has carried yet.

2. Dev mode doesn't validate the output format — non-ESM output silently loses the module graph

The graph prelude is rendered only in format/esm.rs (render_chunk_content); cjs.rs/iife.rs/umd.rs have no equivalent. Nothing in dev normalization (prepare_build_context.rs:277-283) or the JS dev API forces or validates format: 'esm'. A dev build with format: 'cjs' produces output with no registerGraph at all, so the client walk has no edges. A one-line hard validation error for non-ESM formats in dev mode closes it.

3. NoBoundary/circular now broadcasts FullReload to every tab — the per-client Noop is gone

collect_client_update_superset returns one verdict fanned out to all clients (hmr_stage.rs:216-224). Previously a client that had never executed the changed module got a Noop; now a tab that never loaded a lazy route reloads because someone edited an orphan module under it. The Codex P2 comment raised this and it's unanswered. I assume #10207 ("move full-reload to client side") owns this — please confirm it restores per-tab decisions (send changedIds, let the client decide reload vs. ignore). If it doesn't, this is a real behavior regression with no owner.

4. connectClient doesn't await registerClient — updates computed in the connect window are lost

void this.#devEngine.registerClient(clientId) (full-bundle-dev-environment.ts:116) is fire-and-forget; the Rust session appears a tick later. A change computed in that window snapshots clients without the new one (bundling_task.rs:209-214), so that tab gets neither a patch nor a reload, and its initial bundle may predate the edit. There's no WS reconnect in the default runtime to recover. Also raised by Codex on 07-14. Awaiting registration before acking the connection closes it.

5. The "Restored" fixtures push hasn't landed

The "Restored. This was deleted by mistake." reply — load-dependencies/transform-dependencies/multiple-edits are still absent at f9ee9377a. Either land that push, or decide the playground ports (hmr-watch-file-load/hmr-watch-file-transform/hmr-multiple-edits) supersede the node fixtures and close the thread that way.


Follow-up material — fine after this PR, listed so it doesn't get lost

6. The default runtime keeps a dead accept/invalidate surface

On the non-Vite path, hmr:update only imports the patch (no walk/evict/re-run — runtime-extra-dev-default.js:100-107), accept callbacks are pushed but never read, and invalidate() still sends hmr:invalidate that the server deliberately drops (dev-server.ts:202-204; the Rust handler is deleted). #handleHmrUpdates's fromInvalidate parameter has no caller passing true (full-bundle-dev-environment.ts:166). If "the FBM client is the only real consumer" is the decision, remove the dead surface (or at least the dead invalidate() send + fromInvalidate) and say so; otherwise the shared walk from first-round #4 is the fix.

7. notifyPayloadDelivered(filename) cannot attribute patches to clients as shaped — check at #10208

The stub's key is filename-only (dev_engine.rs:283), but patches are rendered once and broadcast to all clients under one filename (hmr_stage.rs:229-241), while lazy chunks are per-request. For #10208's ship map to work, either payload filenames become per-(client, payload) unique or the signal needs a client id. Relatedly, the node env writes its own ${seed}.js per client (N identical files per batch, never cleaned up) and ignores the Rust-side patch.filename entirely (full-bundle-dev-environment.ts:210), which makes filename-keyed tracking unusable on that environment.

8. Two test assertions didn't survive anywhere in-repo — candidates for #10181

  • watchChange firing during a dev-server HMR edit — the deleted edit fixture asserted it via an injected transform value; the only remaining watchChange coverage is standalone watch mode, a different code path from the dev engine.
  • invalidate multi-level propagation order — the deleted invalidation fixture asserted exact ordering across two levels via a records array; hmr-client-invalidate covers the three basic outcomes only. invalidate also has zero crate-level coverage.

9. Nit: comment/code mismatch in module_graph_delta.rs

Lines 54-55 claim "HotAccept, new URL, css url … are skipped", but the filter only excludes HotAccept; NewUrl records to asset modules pass Module::Normal and genuinely become edges (parity with the server's importers_idx, so behavior is fine — the comment isn't).

10. The red Vite CI is stable pin drift, not flakiness

tsconfig-json-load-error fails because the pinned Vite commit's error-message regex predates main's tsconfig error changes; it will stay red until the pin moves. +1 to the submodule-freshness CI ask; the pin being an unmerged working commit also means merging here needs the Vite side coordinated.

11. Still unowned from the first round

Protocol versioning (first-round #3, acknowledged as "a new PR in the future") and the single shared walk implementation (first-round #4) have no owning PR in the stack — suggest tracking issues so they don't evaporate.


Checked and not issues

For completeness, things I chased and ruled out: ghost importer rows after module deletion are symmetric with the server (rebuild_importer_sets only rebuilds a module's own records, on both sides), so both walks reload in lockstep — pre-existing, no divergence; static-dep init completeness holds for existing deps (execution order guarantees residency; focus item 1 is specifically about added deps); the graph-prelude placement in esm.rs is correct in all four paths (verified against snapshots, including the sources: None and grouped cases); multi-chunk row ownership is sound (registerGraph writes only i < localCount); seq stamping has no ordering bug (builds are serialized, and reload → new clientId → fresh session makes a reset unnecessary).

@sapphi-red sapphi-red left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no more comments from my side. I'll leave the implementation part to @hyfdev.

@h-a-n-a
h-a-n-a force-pushed the 07-07-feat_dev_client-side_hmr branch from 8d37643 to fad060c Compare July 15, 2026 06:17

@hyfdev hyfdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done.

@graphite-app

graphite-app Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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.
@graphite-app
graphite-app Bot force-pushed the 07-07-feat_dev_client-side_hmr branch from fad060c to 84afd72 Compare July 15, 2026 06:52

@shulaoda shulaoda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@graphite-app
graphite-app Bot merged commit 84afd72 into main Jul 15, 2026
33 of 34 checks passed
@graphite-app
graphite-app Bot deleted the 07-07-feat_dev_client-side_hmr branch July 15, 2026 06:57
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
<!--
- 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
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
<!--
- 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.
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
…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.
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
…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.
graphite-app Bot pushed a commit that referenced this pull request Jul 15, 2026
<!--
- 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 |
@rolldown-guard rolldown-guard Bot mentioned this pull request Jul 15, 2026
shulaoda added a commit that referenced this pull request Jul 15, 2026
## [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]>
Brooooooklyn added a commit that referenced this pull request Jul 16, 2026
…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
Brooooooklyn added a commit that referenced this pull request Jul 16, 2026
…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
graphite-app Bot pushed a commit that referenced this pull request Jul 17, 2026
…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!
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants