Skip to content

fix(hmr): resolve virtual module URLs correctly for HMR updates and invalidation#22098

Open
bolasblack wants to merge 1 commit intovitejs:mainfrom
bolasblack:fix/virtual-module-url
Open

fix(hmr): resolve virtual module URLs correctly for HMR updates and invalidation#22098
bolasblack wants to merge 1 commit intovitejs:mainfrom
bolasblack:fix/virtual-module-url

Conversation

@bolasblack
Copy link
Copy Markdown

@bolasblack bolasblack commented Apr 1, 2026

import.meta.hot.invalidate() and HMR js-update silently fail for virtual modules.

Root cause

Virtual modules have a URL mismatch between client and server:

Location URL format
Browser (client) /@id/__x00__virtual:my-module
mod.url (server) virtual:my-module

This is because mod.url doesn't contain the \0 prefix — it stores the bare specifier. When normalizeHmrUrl(mod.url) runs wrapId(), it produces /@id/virtual:my-module (missing __x00__), which doesn't match the browser-side URL.

This breaks two things:

  1. js-update for virtual module HMR boundaries — accept() callback never fires
  2. invalidate() — server can't find the module in urlToModuleMap

Fix

  • hmr.ts: Use mod.id (which has \0) instead of mod.url for virtual modules, so wrapId generates the correct /@id/__x00__... path.
  • environment.ts: Fall back to idToModuleMap.get(unwrapId(path)) in invalidateModule.

Test

Added 'invalidate virtual module propagates to importers' in playground/hmr — a virtual module that accept()s then invalidate()s. Fails without fix, passes with it.

…nvalidation

Virtual modules (with `\0` prefix) have a URL mismatch between client
and server: the browser uses `/@id/__x00__virtual:...` but the server's
module graph stores `mod.url` as the bare specifier `virtual:...`
(without the `\0` prefix). This causes two issues:

1. `js-update` messages for virtual module HMR boundaries use the wrong
   path, so the browser silently ignores the update.

2. `import.meta.hot.invalidate()` sends the client-side URL back to the
   server, which fails to look up the module in `urlToModuleMap`.

Fix: In `updateModules`, use `mod.id` (which preserves the `\0` prefix)
instead of `mod.url` for virtual modules, so `wrapId` correctly generates
the `/@id/__x00__...` URL. In `invalidateModule`, fall back to
`idToModuleMap.get(unwrapId(path))` to resolve the client-side URL.
bolasblack added a commit to bolasblack/shadow-cljs-vite-plugin that referenced this pull request Apr 1, 2026
- AGD-007: add 'Known Issue' section with verified root cause (URL
  mismatch between client /@id/__x00__ and server mod.url), link to
  vitejs/vite#22098, and simplified code path after fix is merged
- README: add 'Upstream Contributions' section (shadow-cljs#1249, vite#22098)
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.

1 participant