Skip to content

feat(ssr): defer serving to external servers, inject dev CSS on the response - #284

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

feat(ssr): defer serving to external servers, inject dev CSS on the response#284
brenelz wants to merge 1 commit into
solidjs:nextfrom
brenelz:turnkey-external-server-inject

Conversation

@brenelz

@brenelz brenelz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Alternative to #283 — same first half, different answer for dev CSS. Opened separately so the two mechanisms can be compared; only one should land.

Both PRs fix the same two problems that surface when a server integration (nitro, a Cloudflare worker, a custom harness) owns the server under turnkey ssr: {}.

Shared with #283: the ssr build input is overwritten unconditionally

config() always points the ssr environment at virtual:solid-ssr-handler (plus dist/* outDirs and builder). An integration with its own server entry is left without one — nitro's getEntry finds nothing, registers no renderer route, and every page 404s in preview. A user-configured environments.ssr.build.rollupOptions.input now signals external ownership, and turnkey leaves the ssr input, outDirs, builder flag and dev HTML middleware alone while still contributing entries, handler and client manifest config. Plain-Vite turnkey apps don't set that input, so nothing changes for them.

The difference: how dev entry CSS reaches the page

Entry stylesheets are inlined by the dev HTML middleware, which never runs once something else serves — so dev SSR streams unstyled markup and flashes.

  • feat(ssr): defer serving and build wiring to external servers #283 exposes them on a dev endpoint that the generated handler fetches (options.devHead fallback).
  • This PR collects them in the Vite process — where the module graph and collectDevStyles live — and splices them into the streamed HTML before </head> as it flows back out, via a pre middleware.

Why I think this one is better:

The cost is that it patches res.write/res.end to rewrite a streamed body, and drops content-length when it injects. Two details worth review: it only rewrites the first chunk containing </head> (fine for renderToStream, whose shell arrives in one chunk, but it does not stitch a </head> split across chunk boundaries), and chunks arrive as Uint8Array from web-stream bodies rather than Buffer, which the transform handles explicitly.

Testing

Against a Solid 2 + nitro app (vite-plugin-solid@3.0.0-next.16, nitro@3.0.260610-beta, Vite 8.0.10) using generated entries and a small src/ssr.ts adapter exposing the handler as nitro's { fetch } service:

  • Dev — entry CSS server-rendered into <head>, styled first paint; exactly one <style> tag after hydration (Vite's client adopts the SSR'd data-asset twin via devStylePatch); clean hydration, no console errors; server functions round-trip. Appending a rule to a stylesheet appears in the next SSR response and disappears when reverted, so HMR edits stay current.
  • Build/preview — renderer route present, / 200 with hashed client entry and stylesheet injected; no dev-only code in the server bundle.
  • Plain-Vite turnkey unchanged (no ssr input → identical config output and middleware registration).

Note

The trigger is a heuristic; an explicit option (ssr: { serve: false } or similar) may be preferable, happy to switch. Also related to #281: the if (externalServer) return guard skips the middleware whose ssrLoadModule call is what crashes there, so this covers turnkey's half of that issue for any setup that trips the trigger — but not the server-functions middleware, which still needs the Environment API migration.

🤖 Generated with Claude Code

…esponse

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 inlined by that dev HTML middleware, so it stops reaching the
page once something else serves and dev SSR flashes unstyled. The styles are
still collected here, in the Vite process where the module graph lives, and
spliced into the response before </head> on its way back out — as a pre
middleware, since an external server generally answers before Vite's own
middlewares, and on the response rather than through the handler, so it
works whether or not the integration renders via handleRequest.

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: bd1e93d

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@284

commit: bd1e93d

@brenelz

brenelz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Hmm not sure i really like this either

@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