Skip to content

feat(vite-plugin): full HMR + reload matrix matching Angular CLI#264

Merged
Brooooooklyn merged 1 commit intovoidzero-dev:mainfrom
ashley-hunter:feat-hmr-dispatcher-vite-watcher
May 7, 2026
Merged

feat(vite-plugin): full HMR + reload matrix matching Angular CLI#264
Brooooooklyn merged 1 commit intovoidzero-dev:mainfrom
ashley-hunter:feat-hmr-dispatcher-vite-watcher

Conversation

@ashley-hunter
Copy link
Copy Markdown
Contributor

@ashley-hunter ashley-hunter commented May 6, 2026

I noticed when using AI tools to modify code (in a project using the vite plugin) the browser didn't update with the changes as I'd expect, while editing the same files manually in an editor worked fine. After some digging it turns out different tools save files in different ways, and the plugin's per-file node:fs.watch doesn't deal with all of them.

@oxc-angular/vite watches each component template/stylesheet with its own fs.watch(file, …) inside configureServer, and the handler only reacts to eventType === 'change'. Tools that save by writing a temp file and renaming over the target — vim's default, IntelliJ's "safe write", and the Edit pipeline in several AI tools — produce 'rename' events that get dropped on the floor. On macOS it gets worse than that: once the file has been replaced by a rename, fs.watch is bound to the original inode which no longer exists, so even subsequent in-place writes won't fire until the dev server restarts. Most editors save in place, which is why manual saves keep working. And because configureServer calls server.watcher.unwatch(file), Vite's own chokidar — which watches the project recursively and handles all of this fine — isn't around as a fallback.

While I was in there I lined the plugin up against @angular/build (CLI's esbuild dev server, Angular 17+) and noticed two more gaps. Inline styles: ['…'] changes fall through to full reload — the CLI HMRs them, same as inline templates. Plain non-component .ts edits don't reload at all: Vite's default propagation accepts at the nearest component boundary, Angular's runtime sees no template/style metadata change and does nothing, and the DOM stays stale.

This PR replaces the custom watcher with a handleHotUpdate dispatcher driven by Vite's chokidar. Inline-style HMR is added symmetric to inline-template. Plain .ts edits full-reload. liveReload: false still disables everything. Final matrix matches @angular/build.

Tests: FileModifier gains a WriteStrategy parameter (in-place, fsync, atomic-rename, truncate-then-write) to guard against this regression specifically. New specs cover the strategy matrix, inline-style HMR, and plain-.ts reload, plus unit coverage for the new dispatcher branches. Three pre-existing hmr-ts.spec.ts tests were silently timing out due to a quote-style mismatch in the modify step — fixed.

@ashley-hunter
Copy link
Copy Markdown
Contributor Author

Looking into e2e failures - looks like it is a linux only issue

Replaces the per-file node:fs.watch watcher with Vite's chokidar watcher
(server.watcher) and adds a handleHotUpdate dispatcher that mirrors the
Angular CLI HMR/reload decision matrix:

- External template (.html) change  → angular:component-update (HMR)
- External style (.css) change      → angular:component-update (HMR)
- Inline template/styles change     → angular:component-update (HMR)
- Component class-body change (.ts) → full reload
- Plain non-component .ts change    → full reload
- node_modules .ts change           → pass through to Vite

Fixes a Linux-only race condition where truncate-then-write saves (used
by some AI coding tools) caused inotify to fire two discrete change events.
The first event arrived while the file was empty; pendingHmrUpdates.delete()
was called unconditionally before reading the template, consuming the pending
slot before any content could be served. The second request found no entry
and returned an empty HMR module.

Fix: move pendingHmrUpdates.delete() into the success branch (non-empty
template compiled and served) and the error/catch branch only. The
empty-file transient fallthrough preserves the entry so the subsequent
request can serve real content.

Adds unit tests covering:
- The double-request race (truncate-then-write transient empty state)
- Pending entry consumption after successful HMR
- Pending entry consumption and angular:invalidate dispatch on error

Removes truncate-then-write from the e2e write-strategy matrix — chokidar
can throttle the second inotify event on Linux making e2e coverage unreliable
for that pattern. The race condition is covered deterministically at the unit
level instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ashley-hunter ashley-hunter force-pushed the feat-hmr-dispatcher-vite-watcher branch from 7b8451b to 2fbbb0f Compare May 7, 2026 08:38
@Brooooooklyn Brooooooklyn merged commit c550c27 into voidzero-dev:main May 7, 2026
9 checks passed
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.

2 participants