Skip to content

bundledDev: server-side API to map module id to served URL (needed for SSR modulepreload/hydration hints) #22991

Description

@ryansolid

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=… (triggerLazyBundlingMiddlewaredevEngine.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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions