Skip to content

fix: invalidate Vite module graph on template/style HMR updates#158

Merged
Brooooooklyn merged 1 commit intovoidzero-dev:mainfrom
BenjaminDobler:fix/hmr-stale-transform-cache
Mar 23, 2026
Merged

fix: invalidate Vite module graph on template/style HMR updates#158
Brooooooklyn merged 1 commit intovoidzero-dev:mainfrom
BenjaminDobler:fix/hmr-stale-transform-cache

Conversation

@BenjaminDobler
Copy link
Contributor

Problem

When a template or style file changes during development, the Vite plugin's custom fs.watch callback correctly:

  1. Clears the internal resourceCache
  2. Sends an angular:component-update WebSocket event to the browser
  3. Serves a fresh HMR update module via the middleware endpoint

However, it never calls server.moduleGraph.invalidateModule() for the owning component module. This means Vite's transform cache retains the stale compiled output from the previous template/style content.

Symptoms

  • Stale content after full page reload: HMR live updates work correctly (the middleware reads the template fresh and compiles a new HMR module), but a full page reload serves the old compiled component because Vite returns its cached transform result.

  • Unreliable HMR when global styles are present: When global stylesheets are in the module graph (e.g., injected via <link> tags), Vite occasionally re-evaluates module dependencies using the stale transform cache, causing some HMR updates to appear to be skipped. Users see the update applied only after saving multiple times.

Reproduction

  1. Run a dev server with liveReload: true
  2. Edit a component's template .html file
  3. Observe the HMR update applies correctly in the browser
  4. Do a full page reload (F5) — the page shows the old template content

Root cause

In configureServer, the fs.watch callback (line 344) sends the HMR WebSocket event but does not invalidate the component module in Vite's module graph:

// Before: only clears internal cache, Vite's transform cache goes stale
resourceCache.delete(normalizedFile)
// ... sends angular:component-update WS event

Fix

Add server.moduleGraph.invalidateModule() after the WebSocket event is sent. This ensures Vite re-transforms the component module on the next full request while the live HMR update is unaffected:

server.ws.send({
  type: 'custom',
  event: 'angular:component-update',
  data: eventData,
})

// Invalidate Vite's module transform cache so that a full page reload
// picks up the new template/style content instead of serving stale output.
const mod = server.moduleGraph.getModuleById(componentFile)
if (mod) {
  server.moduleGraph.invalidateModule(mod)
}

The invalidation is placed after ws.send so the in-flight HMR update completes via the custom middleware path. The invalidateModule call only clears Vite's cached transform result — it does not trigger a reload or interfere with the custom HMR mechanism.

Test plan

  • Edit a template file → verify HMR live update works
  • Full page reload after HMR → verify fresh content is shown (not stale)
  • Add a global stylesheet (via <link> tag) → verify template HMR remains reliable
  • Edit a component .ts file → verify full reload still triggers (unchanged behavior)

Two fixes for HMR reliability:

1. After sending the angular:component-update WS event, call
   server.moduleGraph.invalidateModule() so that Vite's transform cache
   is cleared and full page reloads serve fresh content.

2. In handleHotUpdate, skip the full-reload for component .ts files that
   have a pending HMR update (pendingHmrUpdates). Template/style changes
   trigger handleHotUpdate for the owning .ts file via Vite's module
   graph dependency tracking, but the .ts file wasn't actually edited.
   Without this check, the full-reload overrides the HMR update.
@BenjaminDobler BenjaminDobler force-pushed the fix/hmr-stale-transform-cache branch from a985d43 to 2a03a3d Compare March 22, 2026 17:47
@Brooooooklyn Brooooooklyn merged commit 1cff1c2 into voidzero-dev:main Mar 23, 2026
2 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