fix(dev): onOutput called twice when initial build fails#9552
Conversation
✅ Deploy Preview for rolldown-rs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Fixes a dev-mode regression where DevEngine initial build failures caused onOutput to be invoked twice by preventing BundleCoordinator::ensure_latest_bundle_output from automatically re-queuing a rebuild after a failed full build.
Changes:
- Stop auto-scheduling a new
FullBuildfromensure_latest_bundle_outputwhen the coordinator is inFullBuildFailed/Failed. - Add a regression test asserting
onOutputis called only once when the initial build fails.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| crates/rolldown_dev/src/bundle_coordinator.rs | Changes ensure_latest_bundle_output behavior in failure states to avoid implicit rebuild retries that duplicate onOutput errors. |
| packages/rolldown/tests/dev/dev-watch.test.ts | Adds a regression test covering the “initial build fails → onOutput called once” scenario. |
Merging this PR will not alter performance
Comparing Footnotes
|
5a5f167 to
5a6fa04
Compare
e2ae591 to
09a233b
Compare
Merge activity
|
## Summary Fixes #7835 Also fixes the infinite `FullBuildFailed ↔ FullBuildInProgress` loop reported by @h-a-n-a in [rolldown-customer-supports#4 (comment)](voidzero-dev/rolldown-customer-supports#4 (comment)). When the initial build fails, `DevEngine::run()` calls `ensure_latest_bundle_output`, which loops back and hits the `FullBuildFailed` retry path — scheduling a second identical build and firing `onOutput` twice. The same retry path causes an infinite loop when other callers (e.g., Vite middleware/HMR handlers) repeatedly call `ensureLatestBuildOutput` in failed states. The retry in `FullBuildFailed`/`Failed` was not intentional design — it was added in #6974 while fixing an infinite loop bug, without considering the interaction with callers. ### Fix - `ensure_latest_bundle_output` returns `None` in `FullBuildFailed`/`Failed` — the failure is the latest output, recovery is driven by the watcher. - New `triggerFullBuild` API (fire-and-forget) for explicit manual rebuild. Callers that need to wait compose it with `ensureLatestBuildOutput`. ### Test - Added regression test: `onOutput should be called only once when initial build fails` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
09a233b to
c7bbe2d
Compare
…9573) ## Summary Follow-up to #9552. The `trigger_full_build` + `ensure_latest_bundle_output` composition added to `ensureLatestBuildOutputForEachStep` was causing unintended side effects: - `recover_after_generate_bundle_error`: duplicate Build Output errors in Step 0, lost HMR Code/Meta in Step 1 - `from_rebuild_syntax_error`: extra Build Output from trigger that wasn't needed Revert to only calling `ensure_latest_bundle_output` and restore snapshots to their correct state. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## [1.0.3] - 2026-05-27 ### 🚀 Features - transform: respect decorator strictNullChecks option (#9580) by @kylecannon - drop `defer` keyword (#9503) by @TheAlexLichter ### 🐛 Bug Fixes - ci: create target dir before cargo release-oxc update (#9584) by @shulaoda - ci: reorder prepare-release steps to avoid dirty git check failure (#9583) by @shulaoda - testing: canonicalize temp dir early and use platform-specific separator in test262 (#9582) by @shulaoda - testing: resolve symlinked temp dir in test262 snapshot normalization (#9581) by @shulaoda - testing: canonicalize temp dir path in test262 snapshot normalization (#9579) by @shulaoda - dev: `onOutput` called twice when initial build fails (#9552) by @hyf0 - dev: make `ensureCurrentBuildFinish` not returning error when engine closes (#9564) by @h-a-n-a - oxc-runtime: route require() to CJS helper variant (#9263) (#9526) by @IWANABETHATGUY - generator: use exporter chunk's export mode for CJS default re-exports (#9299) (#9529) by @IWANABETHATGUY - rolldown: always run reduced-atom static cycle check (#9441) (#9514) by @IWANABETHATGUY - apply transform.dropLabels before scanning (#9521) (#9522) by @IWANABETHATGUY - rolldown_watcher: take `rolldown` dep through the workspace (#9510) by @Boshen - cache: keep the scan-stage cache consistent when a build fails (#9495) by @h-a-n-a - skip JSON default-import namespace optimization for write targets (#9484) (#9489) by @IWANABETHATGUY - deps: skip pnpm frozen-lockfile on Netlify to dodge catalog mismatch bug (#9471) by @Boshen ### 🚜 Refactor - oxc-runtime: use Cow for helper path construction (#9538) by @IWANABETHATGUY - fold import defer phase drop into PreProcessor (#9524) by @IWANABETHATGUY - distinguish `map: null` vs `map: undefined` in transform hook output (#9497) by @sapphi-red ### 📚 Documentation - explain the policy for Rust crates (#9547) by @sapphi-red - cache: add design doc for cache (#9544) by @h-a-n-a - guide/troubleshooting: add TDZ error section (#9537) by @sapphi-red - dev-engine: add design doc for dev-engine (#9479) by @h-a-n-a - lazy-barrel: tweak some words (#9483) by @shulaoda - lazy-barrel: expand reasoning behind LARGE_BARREL_MODULES advice (#9477) by @shulaoda ### ⚡ Performance - generate: thread ast_table by value into codegen consumer (#9555) by @Boshen - finalizers: replace `_reExport` construction with a direct call to avoid calling `clone_in` (#9501) by @Dunqing - reorder hot-path boolean checks to short-circuit on cheap predicates first (#9523) by @Boshen ### 🧪 Testing - rolldown: regression fixture for #9401 (#9418) by @IWANABETHATGUY - failing test for #9441 (#9504) by @TheAlexLichter ### ⚙️ Miscellaneous Tasks - deps: upgrade oxc to 0.133.0 (#9563) by @Dunqing - deps: update crate-ci/typos action to v1.46.3 (#9576) by @renovate[bot] - deps: update mimalloc-safe to 0.1.62 (#9577) by @shulaoda - mimalloc-safe: update to a bug-fix branch for verification (#9569) by @shulaoda - deps: update test262 submodule for tests (#9551) by @rolldown-guard[bot] - point published crates' readme to root README.md (#9553) by @Boshen - replace actions-cool/issues-helper with gh CLI (#9543) by @Boshen - deps: update cargo-shear to 1.12.4 (#9541) by @Boshen - deps: update taiki-e/install-action action to v2.79.4 (#9535) by @renovate[bot] - deps: update github actions (#9532) by @renovate[bot] - deps: update rust crates (#9534) by @renovate[bot] - deps: update npm packages (#9533) by @renovate[bot] - gate experimental/testing-only items to silence dead_code in publish builds (#9517) by @Boshen - docs: deploy to Void (#9509) by @Boshen - release: set up cargo-release-oxc for publishing crates (#9476) by @Boshen - rolldown_plugin_lazy_compilation: add missing description (#9507) by @Boshen - mimalloc-safe: update to a bug-fix branch for verification (#9506) by @shulaoda - deps: update crate-ci/typos action to v1.46.2 (#9468) by @renovate[bot] ### ❤️ New Contributors * @kylecannon made their first contribution in [#9580](#9580)
…9573) ## Summary Follow-up to #9552. The `trigger_full_build` + `ensure_latest_bundle_output` composition added to `ensureLatestBuildOutputForEachStep` was causing unintended side effects: - `recover_after_generate_bundle_error`: duplicate Build Output errors in Step 0, lost HMR Code/Meta in Step 1 - `from_rebuild_syntax_error`: extra Build Output from trigger that wasn't needed Revert to only calling `ensure_latest_bundle_output` and restore snapshots to their correct state. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## [1.0.3] - 2026-05-27 ### 🚀 Features - transform: respect decorator strictNullChecks option (#9580) by @kylecannon - drop `defer` keyword (#9503) by @TheAlexLichter ### 🐛 Bug Fixes - ci: create target dir before cargo release-oxc update (#9584) by @shulaoda - ci: reorder prepare-release steps to avoid dirty git check failure (#9583) by @shulaoda - testing: canonicalize temp dir early and use platform-specific separator in test262 (#9582) by @shulaoda - testing: resolve symlinked temp dir in test262 snapshot normalization (#9581) by @shulaoda - testing: canonicalize temp dir path in test262 snapshot normalization (#9579) by @shulaoda - dev: `onOutput` called twice when initial build fails (#9552) by @hyf0 - dev: make `ensureCurrentBuildFinish` not returning error when engine closes (#9564) by @h-a-n-a - oxc-runtime: route require() to CJS helper variant (#9263) (#9526) by @IWANABETHATGUY - generator: use exporter chunk's export mode for CJS default re-exports (#9299) (#9529) by @IWANABETHATGUY - rolldown: always run reduced-atom static cycle check (#9441) (#9514) by @IWANABETHATGUY - apply transform.dropLabels before scanning (#9521) (#9522) by @IWANABETHATGUY - rolldown_watcher: take `rolldown` dep through the workspace (#9510) by @Boshen - cache: keep the scan-stage cache consistent when a build fails (#9495) by @h-a-n-a - skip JSON default-import namespace optimization for write targets (#9484) (#9489) by @IWANABETHATGUY - deps: skip pnpm frozen-lockfile on Netlify to dodge catalog mismatch bug (#9471) by @Boshen ### 🚜 Refactor - oxc-runtime: use Cow for helper path construction (#9538) by @IWANABETHATGUY - fold import defer phase drop into PreProcessor (#9524) by @IWANABETHATGUY - distinguish `map: null` vs `map: undefined` in transform hook output (#9497) by @sapphi-red ### 📚 Documentation - explain the policy for Rust crates (#9547) by @sapphi-red - cache: add design doc for cache (#9544) by @h-a-n-a - guide/troubleshooting: add TDZ error section (#9537) by @sapphi-red - dev-engine: add design doc for dev-engine (#9479) by @h-a-n-a - lazy-barrel: tweak some words (#9483) by @shulaoda - lazy-barrel: expand reasoning behind LARGE_BARREL_MODULES advice (#9477) by @shulaoda ### ⚡ Performance - generate: thread ast_table by value into codegen consumer (#9555) by @Boshen - finalizers: replace `_reExport` construction with a direct call to avoid calling `clone_in` (#9501) by @Dunqing - reorder hot-path boolean checks to short-circuit on cheap predicates first (#9523) by @Boshen ### 🧪 Testing - rolldown: regression fixture for #9401 (#9418) by @IWANABETHATGUY - failing test for #9441 (#9504) by @TheAlexLichter ### ⚙️ Miscellaneous Tasks - deps: upgrade oxc to 0.133.0 (#9563) by @Dunqing - deps: update crate-ci/typos action to v1.46.3 (#9576) by @renovate[bot] - deps: update mimalloc-safe to 0.1.62 (#9577) by @shulaoda - mimalloc-safe: update to a bug-fix branch for verification (#9569) by @shulaoda - deps: update test262 submodule for tests (#9551) by @rolldown-guard[bot] - point published crates' readme to root README.md (#9553) by @Boshen - replace actions-cool/issues-helper with gh CLI (#9543) by @Boshen - deps: update cargo-shear to 1.12.4 (#9541) by @Boshen - deps: update taiki-e/install-action action to v2.79.4 (#9535) by @renovate[bot] - deps: update github actions (#9532) by @renovate[bot] - deps: update rust crates (#9534) by @renovate[bot] - deps: update npm packages (#9533) by @renovate[bot] - gate experimental/testing-only items to silence dead_code in publish builds (#9517) by @Boshen - docs: deploy to Void (#9509) by @Boshen - release: set up cargo-release-oxc for publishing crates (#9476) by @Boshen - rolldown_plugin_lazy_compilation: add missing description (#9507) by @Boshen - mimalloc-safe: update to a bug-fix branch for verification (#9506) by @shulaoda - deps: update crate-ci/typos action to v1.46.2 (#9468) by @renovate[bot] ### ❤️ New Contributors * @kylecannon made their first contribution in [#9580](#9580)
Summary
Fixes #7835
Also fixes the infinite
FullBuildFailed ↔ FullBuildInProgressloop reported by @h-a-n-a in rolldown-customer-supports#4 (comment).When the initial build fails,
DevEngine::run()callsensure_latest_bundle_output, which loops back and hits theFullBuildFailedretry path — scheduling a second identical build and firingonOutputtwice. The same retry path causes an infinite loop when other callers (e.g., Vite middleware/HMR handlers) repeatedly callensureLatestBuildOutputin failed states.The retry in
FullBuildFailed/Failedwas not intentional design — it was added in #6974 while fixing an infinite loop bug, without considering the interaction with callers.Fix
ensure_latest_bundle_outputreturnsNoneinFullBuildFailed/Failed— the failure is the latest output, recovery is driven by the watcher.triggerFullBuildAPI (fire-and-forget) for explicit manual rebuild. Callers that need to wait compose it withensureLatestBuildOutput.Test
onOutput should be called only once when initial build fails🤖 Generated with Claude Code