feat(ssr): defer serving and build wiring to external servers - #283
feat(ssr): defer serving and build wiring to external servers#283brenelz wants to merge 1 commit into
Conversation
Turnkey SSR assumes it owns the server. When a server integration (nitro, a Cloudflare worker, a custom harness) drives the build and serves requests, two assumptions break. The ssr build input was overwritten with virtual:solid-ssr-handler unconditionally, leaving the integration with no server entry — under nitro no renderer route gets registered and every page 404s in preview. A configured environments.ssr.build.rollupOptions.input now signals external ownership: entries, handler and client manifest config are still contributed, but the ssr input, dist/* outDirs, builder flag and dev HTML middleware are left alone. Entry CSS is collected in that dev HTML middleware, so it never runs once something else serves, and the handler usually runs in another runtime (a dev worker, workerd) that can reach neither Vite's module graph nor the globalThis resolver registry — dev SSR streamed unstyled markup and flashed on load. The stylesheets are now also exposed on a dev-only endpoint that the generated handler fetches when the caller supplies no devHead, per request so HMR-edited CSS stays current. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 4d265e9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
| // the dev server over HTTP. Per request, so HMR edits stay current. | ||
| `async function fetchDevEntryStyles(request) {`, | ||
| ` try {`, | ||
| ` const response = await fetch(new URL(${JSON.stringify(DEV_ENTRY_CSS_PATH)}, request.url));`, |
There was a problem hiding this comment.
hmm this feels a bit weird to do
| // that gets the streamed SSR render. | ||
| return () => { | ||
| // An external server (see `externalServer` above) serves HTML itself. | ||
| if (externalServer) return; |
There was a problem hiding this comment.
hmm maybe we can avoid returning early here. not sure
|
Opened #284 as an alternative for the dev-CSS half: instead of an endpoint the handler fetches, it collects the styles in the Vite process and splices them into the streamed response before |
Turnkey SSR (
ssr: {}) assumes it owns the server. When a server integration drives the build and serves requests — nitro, a Cloudflare worker, a custom harness — two assumptions break. Both showed up building a Solid 2 + nitro app against3.0.0-next.16.The ssr build input is overwritten unconditionally
config()always sets the ssr environment's input tovirtual:solid-ssr-handler(plusdist/*outDirs and thebuilderflag). An integration that has its own server entry is left without one: nitro'sgetEntryfinds nothing, so it registers no renderer route and every page 404s in preview — the built route table contains only the app's own API routes.Now a user-configured
environments.ssr.build.rollupOptions.inputsignals external ownership. Turnkey keeps doing the parts only it can do — resolving/generating entries, emitting the handler, configuringmanifest: trueand the client input — and leaves the ssr input, outDirs,builderflag and dev HTML middleware alone. Nothing changes for plain-Vite turnkey apps, which don't set that input.Dev entry CSS never reaches an external handler
Entry stylesheets are collected in the dev HTML middleware (
collectDevStyles→devHead→handleRequest). That middleware doesn't run once something else serves, and the handler generally runs in a different runtime — a dev worker, workerd — which can reach neither Vite's module graph nor theglobalThisresolver registry. Result: dev SSR streams unstyled markup and flashes on load. (I verified the registry genuinely isn't visible from nitro's dev worker.)The styles are now also exposed on a dev-only endpoint, and the generated handler fetches it when the caller passes no
devHead. Collection stays per-request so HMR-edited CSS is current, and the tags are the existingrenderDevStyleTagoutput, sodevStylePatchdedupes them against Vite's client injection exactly as before. Build output is untouched — the fetch only exists in the dev branch of the codegen.Testing
Against a Solid 2 + nitro app (
vite-plugin-solid@3.0.0-next.16,nitro@3.0.260610-beta, Vite 8.0.10), generated entries with a smallsrc/ssr.tsadapter exposing the handler as nitro's{ fetch }service:<head>so first paint is styled; exactly one<style>tag in the DOM after hydration (Vite's client adopts the SSR'd twin); clean hydration, no console errors; server functions round-trip. Editing a stylesheet is reflected in the next SSR response./and/about200, hashed client entry and stylesheet both injected, API routes intact.Notes
ssr: { serve: false }, or similar) may be preferable — happy to switch if you'd rather make it declarative.ssrLoadModule→ Environment API migration, including the server-functions middleware, which this doesn't touch. This does cover the "delegate to the provider-owned environment" case for the turnkey middleware.🤖 Generated with Claude Code