test(dev): assert incremental scan state matches a fresh full build after each HMR step#10115
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 improve performance by 3.56%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
0928e71 to
a9f1c19
Compare
a6a617c to
41808d3
Compare
22a64d3 to
d7c462d
Compare
16f06e7 to
df2e97a
Compare
d7c462d to
818e66b
Compare
df2e97a to
adb030b
Compare
818e66b to
b4374de
Compare
Merge activity
|
08c85ae to
bbaa42b
Compare
b4374de to
b7d9a8e
Compare
…fter each HMR step (#10115) ### Description Part of #7416. Stacked on #10112. This adds the test #7416 asks for: one that ensures an incremental build and a full build produce the same states. #### What it does After each HMR step of a dev fixture, the test harness now builds a fresh bundler with the same options and plugins against the current file state and runs a full build. The fresh bundler has no history, so its scan state is by definition the correct answer for the files on disk. The harness then asserts the incremental state matches it: - `importers` and `dynamic_importers` of every module, compared as sets of module ids. Importer entries pointing at modules the fresh build does not know are ignored: those are the outgoing records of orphaned modules, which stay in the state by design (issue #7416). A stale importer entry from a module the fresh build *does* have is still reported. - `importers_idx`, mapped back to module ids since the two sides use different index spaces, with the same orphan filter. - Every import record's kind and resolved target. - Entry points, where the fresh set must be a subset of the incremental set. Extras on the incremental side are allowed for now, because removed modules and stale dynamic import entries are separate open items of #7416. Any difference fails the test with a per module, per field report. #### Why per step instead of once at the end An end of run comparison has a blind spot: a divergence on a cached module is silently repaired as soon as a later step happens to rescan that module, so only the intermediate state is wrong, which is exactly when HMR reads it. This was verified by temporarily reintroducing the bug fixed in #10107 (a cached module keeping a stale importer set). The end of run check passed, the per step check caught it immediately and reported the exact missing importer. #### When it is skipped - Steps whose file state does not build, e.g. the error recovery fixtures while their syntax error is in place. A state that cannot be built has no full build to compare against. - Steps whose incremental build failed. A failed scan is reverted, so the state intentionally stays at the last good build and cannot mirror the current files until a later scan retries them. - Fixtures with lazy barrel enabled. It loads import records on demand, so the loaded subset depends on request history and legitimately differs between a session and a fresh build. #### Opting out `dev.checkStateParity` defaults to `true`, so every existing and future dev fixture gets the check for free (all 38 current HMR fixtures pass with it enabled). A fixture that intentionally documents a known divergence can set it to `false`, and the flag in its config then serves as a visible marker of the known gap until the underlying fix lands. #### Implementation notes - The comparison lives inside the `rolldown` crate behind the existing `testing` feature (`Bundler::assert_scan_state_parity_with`), so it can read the private cache directly and no new public API surface is added. - `DevEngine` gains a `testing` gated `bundler()` accessor, following the existing `get_watched_files` pattern. - `ensure_task_with_changed_files` now sends the whole step as one watch event batch. One event per message spawned one build per file while the awaited future only covered the first, so a multi file step could race the assertion. - The config schema is regenerated by the existing build script. <!-- - 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! -->
b7d9a8e to
aabb746
Compare
✅ Deploy Preview for rolldown-rs canceled.
|
## [1.1.5] - 2026-07-08 ### 🚀 Features - detect top-level import-binding reads as execution-order sensitive (#10180) by @hyf0 - sourcemap_filenames: add a sourcemapFileNames option (#9271) by @V1OL3TF0X - binding: record plugin hook result kind in tracing spans (#10154) by @IWANABETHATGUY - linking: skip side-effect-free modules in per-entry reachability (#10111) by @IWANABETHATGUY - improve error message for unresolved virtual imports (#10156) by @sapphi-red - add descriptive metadata to plugin API (#10106) by @sapphi-red - add `--configLoader=native` option (#10118) by @sapphi-red ### 🐛 Bug Fixes - improve invalid annotation warnings (#10185) by @hyf0 - keep deduplicated asset filenames stable once they can be observed (#10191) by @shulaoda - sourcemap_filenames: use public option name in pattern errors (#10188) by @IWANABETHATGUY - sourcemap_filenames: hash prepared sourcemap content (#10178) by @hyf0 - tree-shake unused circular declarators exported via export list (#10166) by @IWANABETHATGUY - dev: don't panic when an HMR rebuild hits an unresolved import (#10162) by @shulaoda - propagate errors from output.globals function (#9880) by @shulaoda - dev: revert cache mutations when a partial scan fails (#10110) by @shulaoda - dev: update importer relationships of cached modules in incremental build (#10107) by @shulaoda - hmr: fall back to full reload when a changed module is not registered as executed (#10132) by @shulaoda - chunk-optimizer: follow entry facade edges in runtime placement cycle check (#10101) by @hyf0 - dev: ignore watcher events after close (#10113) by @hyf0 - emit async wrapper for TLA modules under onDemandWrapping (#10086) by @IWANABETHATGUY - gate sideEffects:false modules' side effects on body demand (#10080) by @IWANABETHATGUY - rolldown_plugin_vite_resolve: return empty object for `browser: false` mapped modules (#10082) by @sapphi-red - reset the word-boundary state on newline in Hires::Boundary sourcemaps (#10025) by @shulaoda - trim an emptied chunk's outro/intro instead of skipping past it (#10029) by @shulaoda - test each edited chunk's own start against indent exclude ranges (#10026) by @shulaoda - preserve sourcemap mappings for indented lines when a CJS module shares the chunk (#10074) by @hyf0 ### 🚜 Refactor - separate tree-shaking side effects from execution order sensitivity (#10168) by @hyf0 - type construct_vite_preload_call to take an ObjectPattern (#10135) by @shulaoda - treeshake: single-source the own-export classification shared with the lazy-barrel loader (#10098) by @IWANABETHATGUY - dev: reuse Vite's bundledDev server (#10081) by @h-a-n-a - clippy: ban std HashMap/HashSet in favour of FxHashMap/FxHashSet (#10108) by @Boshen - treeshake: make body demand a second module bit instead of a stmt multimap (#10097) by @IWANABETHATGUY - seal used_symbol_refs by construction after its last writer (#10091) by @hyf0 - treeshake: replace inclusion mutual recursion with a worklist engine (#10096) by @IWANABETHATGUY - treeshake: split include_statements.rs into focused modules (#10095) by @IWANABETHATGUY - drop redundant is_user_defined filter on partitioned entries (#10050) by @shulaoda - project the retained export interface out of used_symbol_refs (#10089) by @hyf0 - track used external symbols separately from used_symbol_refs (#10088) by @hyf0 - make module namespace inclusion an explicit linking metadata field (#10087) by @hyf0 - rename statement evaluation metadata (#10078) by @hyf0 ### 📚 Documentation - virtual modules user-facing id convention (#10155) by @sapphi-red - cli: clarify disabling boolean/object flags like codeSplitting (#10153) by @IWANABETHATGUY - chore: remove Vite+ alpha banner (#10105) by @mdong1909 - write down the used_symbol_refs contract (#10090) by @hyf0 - dev/lazy: update design and implementation (#10079) by @h-a-n-a ### ⚡ Performance - ast_scanner: stop order-sensitivity checks once a module is flagged (#10190) by @IWANABETHATGUY - return impl ExactSizeIterator from slice-backed accessors (#10133) by @Boshen - binding: box dev and watcher napi futures (#10103) by @Boshen ### 🧪 Testing - move string_wizard replace unit tests to the JS magic-string suite (#10176) by @IWANABETHATGUY - dev: assert incremental scan state matches a fresh full build after each HMR step (#10115) by @shulaoda - dev: restore runtime assertions of delete_file_not_used_anymore (#10112) by @shulaoda - dev: fix flaky dev server tests in CI (#10152) by @h-a-n-a - add regression test for #10099 (lazyBarrel drops default-import binding but keeps its property reads) (#10109) by @IWANABETHATGUY ### ⚙️ Miscellaneous Tasks - deploy website to Void via GitHub OIDC (#10192) by @Boshen - deps: update oxc to 0.139.0 (#10161) by @shulaoda - deps: update test262 submodule for tests (#10160) by @rolldown-guard[bot] - rolldown_plugin_utils: remove dead asset-url and css scaffolding (#10131) by @shulaoda - deps: revert vite-plus to v0.2.1 (#10148) by @shulaoda - deps: update github actions (#10141) by @renovate[bot] - deps: update dependency rust to v1.96.1 (#10145) by @renovate[bot] - deps: update npm packages (#10142) by @renovate[bot] - deps: update rust crates (#10143) by @renovate[bot] - deps: update napi to v3.10.3 (#10121) by @renovate[bot] - rolldown_utils: remove unused time module (#10138) by @shulaoda - remove dead CopyModulePlugin::is_active method (#10129) by @shulaoda - remove dead LazyCompilationContext::is_lazy_module method (#10128) by @shulaoda - remove dead BuildDiagnostic::downcast_ref method (#10127) by @shulaoda - deps: update dependency vite-plus to v0.2.2 (#10084) by @renovate[bot] - deps: update rust crate oxc_sourcemap to v8.1.0 (#10122) by @renovate[bot] - deps: update crate-ci/typos action to v1.48.0 (#10124) by @renovate[bot] - enable more clippy restriction lints (#10114) by @Boshen - deps: update rust dependencies (#10100) by @Boshen - deps: update oxc resolver to v11.23.0 (#10083) by @renovate[bot] ###◀️ Revert - Revert "chore(deps): revert vite-plus to v0.2.1" (#10157) by @h-a-n-a - "fix(hmr): fall back to full reload when a changed module is not registered as executed (#10132)" (#10151) by @shulaoda ### ❤️ New Contributors * @V1OL3TF0X made their first contribution in [#9271](#9271) Co-authored-by: shulaoda <[email protected]>

Description
Part of #7416. Stacked on #10112. This adds the test #7416 asks for: one that ensures an incremental build and a full build produce the same states.
What it does
After each HMR step of a dev fixture, the test harness now builds a fresh bundler with the same options and plugins against the current file state and runs a full build. The fresh bundler has no history, so its scan state is by definition the correct answer for the files on disk. The harness then asserts the incremental state matches it:
importersanddynamic_importersof every module, compared as sets of module ids. Importer entries pointing at modules the fresh build does not know are ignored: those are the outgoing records of orphaned modules, which stay in the state by design (issue Properly update internal states for incremental builds #7416). A stale importer entry from a module the fresh build does have is still reported.importers_idx, mapped back to module ids since the two sides use different index spaces, with the same orphan filter.Any difference fails the test with a per module, per field report.
Why per step instead of once at the end
An end of run comparison has a blind spot: a divergence on a cached module is silently repaired as soon as a later step happens to rescan that module, so only the intermediate state is wrong, which is exactly when HMR reads it. This was verified by temporarily reintroducing the bug fixed in #10107 (a cached module keeping a stale importer set). The end of run check passed, the per step check caught it immediately and reported the exact missing importer.
When it is skipped
Opting out
dev.checkStateParitydefaults totrue, so every existing and future dev fixture gets the check for free (all 38 current HMR fixtures pass with it enabled). A fixture that intentionally documents a known divergence can set it tofalse, and the flag in its config then serves as a visible marker of the known gap until the underlying fix lands.Implementation notes
rolldowncrate behind the existingtestingfeature (Bundler::assert_scan_state_parity_with), so it can read the private cache directly and no new public API surface is added.DevEnginegains atestinggatedbundler()accessor, following the existingget_watched_filespattern.ensure_task_with_changed_filesnow sends the whole step as one watch event batch. One event per message spawned one build per file while the awaited future only covered the first, so a multi file step could race the assertion.