internal-docs/dev-engine/design.md
rolldown_dev, Full Bundle Mode)Implementation map — component layering, the
CoordinatorMsgprotocol, theCoordinatorStatemachine, theTaskInputwork types, and the per-stage data-flow pipelines: see implementation.md. The§Nsection references below point to that file.
The dev engine (rolldown_dev crate) is rolldown's dev-mode build
orchestration layer in Full Bundle Mode. It sits between the file watcher
/ dev server and the core Bundler, deciding what build to run — an HMR
patch, an incremental rebuild, or a full build — and when. It is
structured as a DevEngine (the public async API surface) driving a
single message-loop BundleCoordinator (a state machine plus a work
queue) that spawns one BundlingTask at a time.
This document captures the why — the principles that govern when the engine rebuilds and how its errors flow out to the binding consumer. For the machinery that realizes them, see implementation.md.
Four principles govern when the dev engine rebuilds and how its errors flow out to the binding consumer. They define rolldown_dev's contract with its consumer (typically Vite) and constrain the implementation in §7, §13, and §16.
Rebuilds happen only when the bundle is stale — when input has changed since the last build attempt. Page access and browser reconnect on their own never trigger a rebuild. In particular: if the previous build failed, an access request does not retry — without new input the same error would recur.
Realized in: BundleCoordinator::ensure_latest_bundle_output returning
None for Failed / FullBuildFailed (§13b, §13e).
rolldown_dev surfaces build errors to the binding consumer on every
build via the on_output / on_hmr_updates callbacks (§16b). It never
silently retries past an error, never silently swallows one, and never
caches one across requests — rolldown_dev is stateless across HTTP
requests. The binding consumer (Vite) is responsible for retaining the
most recent error and replaying it on each client reconnect, so the
error overlay appears even after a browser refresh.
Vite-side realization (in fullBundleEnvironment.ts): a single
lastBuildError: Error | null field caches the most recent error from
either channel — it is set in both onOutput (full-build errors)
and onHmrUpdates (HMR errors), and cleared back to null on a
successful build from either channel (a successful onOutput or
a successful onHmrUpdates, since an HMR patch that computes cleanly
supersedes a previously cached error). It is replayed on the vite:client:connect
event for every freshly connected client (including a post-refresh
reconnect), so the error overlay reappears after a browser refresh.
The two channels differ only in their live
delivery: an onOutput error is additionally logged to the terminal
(logger.error) so a build break is visible without a browser, and is
broadcast to all clients via hot.send; an onHmrUpdates error is sent
to each connected client individually and is not logged to the terminal.
After a failed build, the engine waits for a file change before
rebuilding. Both Vite config edits and user-land source edits are
valid triggers. Inside rolldown_dev nothing else counts as recovery —
not page refresh, not elapsed time, not manual UI dismissal:
ensure_latest_bundle_output no-ops in every failed state (§13b), so
access never rebuilds on its own.
One consumer-side exception — page refresh after an HMR-stage
failure. When the last failure originated in HMR generation
(last_error_stage == Hmr), the consumer is permitted to treat a page
refresh as a recovery trigger: on access it calls triggerFullBuild
(§13e) to force a full rebuild that bypasses the possibly-buggy HMR
path, instead of replaying the cached error. This stays scoped to the
consumer — rolldown_dev itself does not change behavior; the escalation
is the consumer's decision, keyed on the last_error_stage it reads
from BundleState (§12). A Rebuild-stage or full-build failure gets
no such exception — only a file change recovers those. (Wired up in the
in-repo reference consumer: triggerBundleRegenerationIfStale in
packages/test-dev-server/src/environments/full-bundle-dev-environment.ts.)
Realized in: handle_file_changes (§7) is the sole producer of
post-failure rebuild tasks. triggerFullBuild (§13e) is an explicit
escape hatch for cases the watcher cannot observe (e.g. missing-import
resolution; see Unresolved Questions).
Corollary: a file change after a failed build must schedule work that can undo the failure. In practice this means tracking where the failure originated (HMR computation vs incremental rebuild) so the next task covers the stage that broke (§7).
Every error reaching the consumer via on_output / on_hmr_updates
is treated as a user error — caused by source code or plugin
behavior, recoverable by editing source. Rolldown and Vite themselves
are assumed bug-free in this model. The only state not recoverable
through a file-change cycle is a panic, which signals an invariant
violation in rolldown_dev itself (§16g).
watch_paths. Creating it does not trigger a rebuild — the
user must either touch a watched file or use triggerFullBuild. A
fix: during resolution, when a file is not found, record its path and
add its parent directory to the watcher. A directory-level create event
matching a previously-missing path would then trigger a rebuild
automatically. The existing watcher tests acknowledge this gap
(watch.test.ts: "the missing file's directory is not auto-watched,
so we need to touch a watched file").BundleMode,
Bundle / BundleFactory, and the ScanStageCache lifecycle the dev
engine's incremental builds run throughBundler struct and build
lifecycle the dev engine drivesrolldown_watcher, the actor-based
watch architecture; rolldown_dev reuses the same actor patternDevEngine::compile_lazy_entry and the ModuleChanged
message