Skip to content

feat(ssr): defer serving and build wiring to external servers - #283

Closed
brenelz wants to merge 1 commit into
solidjs:nextfrom
brenelz:turnkey-external-server
Closed

feat(ssr): defer serving and build wiring to external servers#283
brenelz wants to merge 1 commit into
solidjs:nextfrom
brenelz:turnkey-external-server

Conversation

@brenelz

@brenelz brenelz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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 against 3.0.0-next.16.

The ssr build input is overwritten unconditionally

config() always sets the ssr environment's input to virtual:solid-ssr-handler (plus dist/* outDirs and the builder flag). An integration that has its own server entry is left without one: nitro's getEntry finds 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.input signals external ownership. Turnkey keeps doing the parts only it can do — resolving/generating entries, emitting the handler, configuring manifest: true and the client input — and leaves the ssr input, outDirs, builder flag 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 (collectDevStylesdevHeadhandleRequest). 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 the globalThis resolver 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 existing renderDevStyleTag output, so devStylePatch dedupes 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 small src/ssr.ts adapter exposing the handler as nitro's { fetch } service:

  • Dev — page 200, entry CSS server-rendered into <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.
  • Build/preview — renderer route present again, / and /about 200, hashed client entry and stylesheet both injected, API routes intact.
  • Plain-Vite turnkey behaviour unchanged (no ssr input configured → identical config output and middleware registration as before).

Notes

  • The trigger is a heuristic. An explicit option (ssr: { serve: false }, or similar) may be preferable — happy to switch if you'd rather make it declarative.
  • Related to Turnkey SSR dev fails with non-runnable Cloudflare ssr environment #281, but not a fix for it: that needs the 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

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-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4d265e9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
vite-plugin-solid Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vite-plugin-solid@283

commit: 4d265e9

Comment thread src/ssr/index.ts
// 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));`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this feels a bit weird to do

Comment thread src/ssr/index.ts
// that gets the streamed SSR render.
return () => {
// An external server (see `externalServer` above) serves HTML itself.
if (externalServer) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm maybe we can avoid returning early here. not sure

@brenelz

brenelz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

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 </head>. That variant works even when the integration's entry doesn't go through handleRequest, and avoids the same-origin fetch (which I couldn't confirm behaves inside workerd). The external-server detection is identical in both — only one should land.

@brenelz brenelz closed this Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant