Environment
- nitro: 3.0.260610-beta (also reproduced against current
main sources)
- vite: 8.0.10
- vite-plugin-solid: 3.0.0-next.15 (solid-js 2.0.0-beta.23, SSR via a
ssr service environment)
- Node.js 22 / macOS
- dev runner: node-worker (default)
Reproduction
Any Vite + Nitro app where SSR renders modules that are also part of the client graph (i.e. every route component). With the dev server running:
- Load a page (SSR renders it).
- Edit the route component (or any module shared between client and server envs).
- Reload the browser: the server still renders the old module — hydration keys/markup no longer match the freshly HMR-updated client, producing hydration mismatches (unclaimed server nodes, and in our case a hard
Cannot read properties of null (reading 'nextSibling') crash during hydration).
- Only restarting the dev server picks up the change.
This is the same end-user symptom as #4020 (closed, but the fix never landed — reload() on main still doesn't clear the cache) and #4043. Tracing the full chain with NITRO_DEBUG=1 and instrumented hotUpdate hooks turned up three independent gaps, each of which is sufficient to keep SSR stale:
Describe the bug
1. hotUpdate never sends full-reload for shared modules (src/build/vite/plugin.ts)
Modules that also exist in a client environment go into sharedModules and are neither invalidated in the server graph nor do they trigger env.hot.send({ type: "full-reload" }). Editing a route component therefore never reloads the server runner at all. (This is #4043; PR #4044 addressed the invalidation half but was closed unmerged.)
2. modules is empty for server environments anyway — file→module associations are missing
Even if (1) were fixed, the modules array hotUpdate receives for the server environments is empty: environment.moduleGraph.getModulesByFile(file) returns nothing, although the module is present in idToModuleMap under its resolved id (verified: id /abs/path/src/routes/Index.tsx exists while getModulesByFile('/abs/path/src/routes/Index.tsx') is empty). Since Vite's own file-change invalidation also goes through getModulesByFile, the server env's transform cache is never invalidated for edited files either. Modules that enter the graph via the module-runner fetchModule path apparently never get registered in fileToModulesMap.
3. ViteEnvRunner.reload() reuses stale evaluations (src/runtime/internal/vite/dev-worker.mjs)
When a full-reload does reach the dev worker, reload() calls this.runner.import(this.entryPath) without clearing evaluatedModules. fetchModule answers {cache: true} for any module whose transform is populated on the Vite side at fetch time — and during the reload itself, plugins can repopulate transforms for the whole graph (e.g. vite-plugin-solid's ?assets crawl), so the runner keeps the old evaluations for exactly the modules that changed. Observed with NITRO_DEBUG=1: after a reload the entry re-executes, but shared app modules are "fetching"-only (no "executing") and old code keeps rendering. Vite's own full-reload handler calls runner.evaluatedModules.clear() before re-importing; the worker's reload() skips that step. This is exactly the root cause described in #4020, which is closed but unfixed on main.
Additional context
Workaround we're using today: a userland plugin that, for server-consumer environments, matches the changed file against idToModuleMap ids directly (bypassing the broken file association), calls moduleGraph.invalidateAll(), and sends { type: "full-reload" } — combined with patching reload() to clear evaluatedModules. With both in place, edits to shared modules are reflected in SSR within a couple of seconds and hydration is clean again.
A PR with the one-line fix for item (3) follows.
Logs
# NITRO_DEBUG=1, after editing src/routes/Index.tsx and sending full-reload:
[module runner] fetching /Users/.../src/entry-server.tsx
[module runner] executing file:///Users/.../src/entry-server.tsx <- entry re-evaluates
[module runner] fetching /src/App.tsx <- fetched...
[module runner] fetching /src/HtmlDocument.tsx <- ...but never "executing":
{cache: true} keeps old evaluation
# subsequent SSR requests trigger no module activity and render the old component
Environment
mainsources)ssrservice environment)Reproduction
Any Vite + Nitro app where SSR renders modules that are also part of the client graph (i.e. every route component). With the dev server running:
Cannot read properties of null (reading 'nextSibling')crash during hydration).This is the same end-user symptom as #4020 (closed, but the fix never landed —
reload()onmainstill doesn't clear the cache) and #4043. Tracing the full chain withNITRO_DEBUG=1and instrumentedhotUpdatehooks turned up three independent gaps, each of which is sufficient to keep SSR stale:Describe the bug
1.
hotUpdatenever sendsfull-reloadfor shared modules (src/build/vite/plugin.ts)Modules that also exist in a client environment go into
sharedModulesand are neither invalidated in the server graph nor do they triggerenv.hot.send({ type: "full-reload" }). Editing a route component therefore never reloads the server runner at all. (This is #4043; PR #4044 addressed the invalidation half but was closed unmerged.)2.
modulesis empty for server environments anyway — file→module associations are missingEven if (1) were fixed, the
modulesarrayhotUpdatereceives for the server environments is empty:environment.moduleGraph.getModulesByFile(file)returns nothing, although the module is present inidToModuleMapunder its resolved id (verified: id/abs/path/src/routes/Index.tsxexists whilegetModulesByFile('/abs/path/src/routes/Index.tsx')is empty). Since Vite's own file-change invalidation also goes throughgetModulesByFile, the server env's transform cache is never invalidated for edited files either. Modules that enter the graph via the module-runnerfetchModulepath apparently never get registered infileToModulesMap.3.
ViteEnvRunner.reload()reuses stale evaluations (src/runtime/internal/vite/dev-worker.mjs)When a
full-reloaddoes reach the dev worker,reload()callsthis.runner.import(this.entryPath)without clearingevaluatedModules.fetchModuleanswers{cache: true}for any module whose transform is populated on the Vite side at fetch time — and during the reload itself, plugins can repopulate transforms for the whole graph (e.g. vite-plugin-solid's?assetscrawl), so the runner keeps the old evaluations for exactly the modules that changed. Observed withNITRO_DEBUG=1: after a reload the entry re-executes, but shared app modules are "fetching"-only (no "executing") and old code keeps rendering. Vite's ownfull-reloadhandler callsrunner.evaluatedModules.clear()before re-importing; the worker'sreload()skips that step. This is exactly the root cause described in #4020, which is closed but unfixed onmain.Additional context
Workaround we're using today: a userland plugin that, for server-consumer environments, matches the changed file against
idToModuleMapids directly (bypassing the broken file association), callsmoduleGraph.invalidateAll(), and sends{ type: "full-reload" }— combined with patchingreload()to clearevaluatedModules. With both in place, edits to shared modules are reflected in SSR within a couple of seconds and hydration is clean again.A PR with the one-line fix for item (3) follows.
Logs