Skip to content

Commit 60f8b9c

Browse files
authored
fix: hmr for inline use directive extracted virtual modules (#356)
Virtual modules extracted by `use-directive-inline` were not invalidated on file change in the client environment. Vite 8's module graph stores client-env virtual modules under root-relative paths, but the file watcher fires with absolute paths, so `getModulesByFile()` never found them during HMR. Added a `hotUpdate` hook that tracks virtual module ids per source file, looks them up via `getModuleById()`, and eagerly invalidates them so stale transforms are cleared even if a downstream plugin discards the modules from the HMR update list.
1 parent 55ac332 commit 60f8b9c

1 file changed

Lines changed: 54 additions & 4 deletions

File tree

packages/react-server/lib/plugins/use-directive-inline.mjs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ function buildExtractedModule(
324324
*/
325325
export default function useDirectiveInline(configs) {
326326
const moduleCache = new Map();
327+
// Track which virtual module IDs originate from each source file.
328+
// Keys are absolute file paths; values are Sets of resolved virtual IDs
329+
// (which may be root-relative in client env or absolute in rsc/ssr env).
330+
const sourceToVirtualIds = new Map();
327331
let root = "";
328332

329333
// Build lookup maps
@@ -365,7 +369,9 @@ export default function useDirectiveInline(configs) {
365369
},
366370

367371
async resolveId(source, importer) {
368-
if (matchQueryKey(source)) return source;
372+
if (matchQueryKey(source)) {
373+
return source;
374+
}
369375

370376
// Resolve relative imports from our extracted virtual modules.
371377
// Vite can't determine the correct directory for virtual module IDs
@@ -384,8 +390,6 @@ export default function useDirectiveInline(configs) {
384390
const match = matchQueryKey(id);
385391
if (!match) return;
386392

387-
const cached = moduleCache.get(id);
388-
if (cached) return cached;
389393
const { cfg, marker } = match;
390394
const qIdx = id.indexOf(marker);
391395
const rawPath = id.slice(0, qIdx);
@@ -396,6 +400,20 @@ export default function useDirectiveInline(configs) {
396400
? rawPath.slice(0, rawPath.indexOf("?"))
397401
: rawPath;
398402
const filePath = basePath.startsWith(root) ? basePath : root + basePath;
403+
404+
// Track this virtual module ID so our hotUpdate hook can find it
405+
if (!sourceToVirtualIds.has(filePath)) {
406+
sourceToVirtualIds.set(filePath, new Set());
407+
}
408+
sourceToVirtualIds.get(filePath).add(id);
409+
410+
// Check if transform already built this module during its pass
411+
const cached = moduleCache.get(id);
412+
if (cached) {
413+
moduleCache.delete(id);
414+
return cached;
415+
}
416+
399417
const sourceCode = await readFile(filePath, "utf-8");
400418
const ast = await parse(sourceCode, filePath);
401419
if (!ast) return;
@@ -427,7 +445,6 @@ export default function useDirectiveInline(configs) {
427445
cfg.injectCapturedParams,
428446
rawPath
429447
);
430-
moduleCache.set(id, extractedCode);
431448
return extractedCode;
432449
},
433450

@@ -744,5 +761,38 @@ export default function useDirectiveInline(configs) {
744761
return modifiedCode;
745762
},
746763
},
764+
765+
// When a source file changes, find any virtual modules derived from it
766+
// in this environment and include them in the HMR update.
767+
// Vite's built-in mechanism misses client-env virtual modules because
768+
// their fileToModulesMap keys are root-relative while the file watcher
769+
// reports absolute paths.
770+
hotUpdate(options) {
771+
const virtualIds = sourceToVirtualIds.get(options.file);
772+
if (!virtualIds || virtualIds.size === 0) return;
773+
774+
const graph = this.environment.moduleGraph;
775+
const additionalModules = [];
776+
777+
for (const vid of virtualIds) {
778+
const mod = graph.getModuleById(vid);
779+
if (mod) {
780+
// Clear any stale one-shot cache entry so the load hook
781+
// re-reads from disk and re-parses.
782+
moduleCache.delete(vid);
783+
// Eagerly invalidate the module so its cached transform result
784+
// is cleared. A downstream plugin (e.g. use-client) may remove
785+
// the module from the HMR list, which would prevent Vite's
786+
// updateModules from invalidating it. Without invalidation the
787+
// browser would receive stale code on the next request.
788+
graph.invalidateModule(mod);
789+
additionalModules.push(mod);
790+
}
791+
}
792+
793+
if (additionalModules.length > 0) {
794+
return [...options.modules, ...additionalModules];
795+
}
796+
},
747797
};
748798
}

0 commit comments

Comments
 (0)