Description
SSR frameworks that stream HTML serialize per-module URLs into the document itself, ahead of any client code:
<link rel="modulepreload" href="..."> hints, so the browser fetches lazy route chunks in parallel with the entry bundle during HTML parse, and
- a hydration module map (e.g.
_$HY.r["…_assets"] = { hydrationKey: url }) written by inline <script> chunks in the stream, which the client runtime import()s before resuming server-rendered DOM.
Concrete case: SolidStart on solid-js 2.0 / vite-plugin-solid. In (unbundled) dev, the framework's asset resolver answers "what URL serves module X?" by identity — the module's dev URL, e.g.:
/src/routes/(basic).tsx?pick=default&pick=$css
Under experimental.bundledDev (Vite 8.1) that contract breaks:
- The served HTML (200) still contains those source-path URLs in the modulepreload hints and the hydration module map — the SSR side has nothing else to emit.
- Requesting the URL itself returns 404: only bundled output files are served (
memoryFilesMiddleware looks up memoryFiles.get(pathname.slice(1)) and falls through for anything that isn't an output fileName).
- The client runtime's pre-hydration
import(url) therefore rejects, hydration falls back to a fresh client render, and the page ends up with duplicated DOM. In SolidStart's test app this is 18 failing e2e specs (apps/tests e2e:bundled-dev) that all pass in normal dev.
The underlying gap: a server-side plugin has no API to translate a module id into its served URL in bundled dev.
BundledDev.memoryFiles maps output fileName → { source, etag }. There is no facade/module-id → fileName mapping exposed, so you can't go from src/routes/(basic).tsx?pick=default&pick=$css to assets/_basic_-DhY….js.
- The lazy-compile endpoint
/@vite/lazy?id=…&clientId=… (triggerLazyBundlingMiddleware → devEngine.compileEntry(moduleId, clientId)) requires a live HMR clientId and returns compiled code, not a URL — unusable from an SSR render pass that must emit URLs into HTML before any client connects.
Put differently: in normal dev, module → URL is answerable by identity (the URL is the module path); in production builds it's answerable via manifest.json. Bundled dev is currently the only topology that provides neither.
We want to stress this is a correctness requirement for streamed SSR hydration in bundled dev, not an optimization: the URLs are fixed server-side and shipped in the HTML before any client code executes, so there is no client-side recovery — either the server can emit servable URLs or hydration of code-split routes fails. We understand bundledDev is experimental and evolving; if there's an intended pattern for this that we've missed, pointers welcome, and we're glad to help test an API.
Suggested solution
A stable server-side API — on the client Environment or the dev server — that, under bundledDev, maps module id → served URL(s) (the entry chunk for that module, ideally with its static import chain for complete preload hints), e.g.:
const urls = await server.environments.client.bundledDev.resolveModuleUrl(
"src/routes/(basic).tsx?pick=default&pick=$css"
);
// → ["/assets/_basic_-DhYx12ab.js", ...]
Semantics we'd need: awaits the current build if one is in flight (like waitForInitialBuildFinish), and reflects lazy-compiled entries once they exist (or triggers compilation without requiring a browser clientId).
Alternative
For the client entry specifically, SolidStart already works around this by hardcoding the served path, assets/${basename(entry)}.js, which is only possible because the entry's entryFileNames is predictable. Hashed lazy route chunks have no equivalent workaround — their served names are only knowable from the bundle graph Vite holds. Emulating a manifest by watching memoryFiles doesn't work either, since there's no exposed mapping from output files back to the facade module ids the framework knows about.
Additional context
Reproduction:
- Full suite:
solidjs/solid-start repo, branch upgrade-to-solid-2-beta, apps/tests, pnpm e2e:bundled-dev (playwright.bundled-dev.config.ts) — 18 hydration-related failures; the same specs pass with pnpm e2e (unbundled dev).
- Minimal sketch: any Vite 8.1 app with
experimental.bundledDev and an SSR handler that renders a code-split (import()-ed) component and emits <link rel="modulepreload"> / a script that import()s that module's dev URL (/src/Comp.tsx). The HTML streams fine; the module request 404s; anything gated on it fails — while the same URL works in unbundled dev.
Related precedent: solidjs/solid-start#2210 (the hardcoded entry-URL workaround mentioned above).
Validations
Description
SSR frameworks that stream HTML serialize per-module URLs into the document itself, ahead of any client code:
<link rel="modulepreload" href="...">hints, so the browser fetches lazy route chunks in parallel with the entry bundle during HTML parse, and_$HY.r["…_assets"] = { hydrationKey: url }) written by inline<script>chunks in the stream, which the client runtimeimport()s before resuming server-rendered DOM.Concrete case: SolidStart on solid-js 2.0 / vite-plugin-solid. In (unbundled) dev, the framework's asset resolver answers "what URL serves module X?" by identity — the module's dev URL, e.g.:
Under
experimental.bundledDev(Vite 8.1) that contract breaks:memoryFilesMiddlewarelooks upmemoryFiles.get(pathname.slice(1))and falls through for anything that isn't an outputfileName).import(url)therefore rejects, hydration falls back to a fresh client render, and the page ends up with duplicated DOM. In SolidStart's test app this is 18 failing e2e specs (apps/testse2e:bundled-dev) that all pass in normal dev.The underlying gap: a server-side plugin has no API to translate a module id into its served URL in bundled dev.
BundledDev.memoryFilesmaps outputfileName → { source, etag }. There is no facade/module-id → fileName mapping exposed, so you can't go fromsrc/routes/(basic).tsx?pick=default&pick=$csstoassets/_basic_-DhY….js./@vite/lazy?id=…&clientId=…(triggerLazyBundlingMiddleware→devEngine.compileEntry(moduleId, clientId)) requires a live HMR clientId and returns compiled code, not a URL — unusable from an SSR render pass that must emit URLs into HTML before any client connects.Put differently: in normal dev, module → URL is answerable by identity (the URL is the module path); in production builds it's answerable via
manifest.json. Bundled dev is currently the only topology that provides neither.We want to stress this is a correctness requirement for streamed SSR hydration in bundled dev, not an optimization: the URLs are fixed server-side and shipped in the HTML before any client code executes, so there is no client-side recovery — either the server can emit servable URLs or hydration of code-split routes fails. We understand
bundledDevis experimental and evolving; if there's an intended pattern for this that we've missed, pointers welcome, and we're glad to help test an API.Suggested solution
A stable server-side API — on the client
Environmentor the dev server — that, underbundledDev, maps module id → served URL(s) (the entry chunk for that module, ideally with its static import chain for complete preload hints), e.g.:Semantics we'd need: awaits the current build if one is in flight (like
waitForInitialBuildFinish), and reflects lazy-compiled entries once they exist (or triggers compilation without requiring a browser clientId).Alternative
For the client entry specifically, SolidStart already works around this by hardcoding the served path,
assets/${basename(entry)}.js, which is only possible because the entry'sentryFileNamesis predictable. Hashed lazy route chunks have no equivalent workaround — their served names are only knowable from the bundle graph Vite holds. Emulating a manifest by watchingmemoryFilesdoesn't work either, since there's no exposed mapping from output files back to the facade module ids the framework knows about.Additional context
Reproduction:
solidjs/solid-startrepo, branchupgrade-to-solid-2-beta,apps/tests,pnpm e2e:bundled-dev(playwright.bundled-dev.config.ts) — 18 hydration-related failures; the same specs pass withpnpm e2e(unbundled dev).experimental.bundledDevand an SSR handler that renders a code-split (import()-ed) component and emits<link rel="modulepreload">/ a script thatimport()s that module's dev URL (/src/Comp.tsx). The HTML streams fine; the module request 404s; anything gated on it fails — while the same URL works in unbundled dev.Related precedent: solidjs/solid-start#2210 (the hardcoded entry-URL workaround mentioned above).
Validations