Skip to content

fix(solid-start): harden SSR asset manifest wiring for SSR'd lazy() components - #8050

Draft
ryansolid wants to merge 4 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/solid-start-ssr-asset-manifest
Draft

fix(solid-start): harden SSR asset manifest wiring for SSR'd lazy() components#8050
ryansolid wants to merge 4 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/solid-start-ssr-asset-manifest

Conversation

@ryansolid

Copy link
Copy Markdown

Problem

99756c0 wired vite-plugin-solid's client-assets manifest
(virtual:solid-manifest) into the Solid SSR renderers, closing the gap where
TanStack Start never fed Vite's client asset manifest into Solid's rendering:
SSR'd lazy() components (Solid's lazy, not route-level splitting) rendered
without their chunk's CSS, without <link rel="modulepreload"> hints, and
without the serialized asset map the client needs to gate hydration of the
boundary on those chunks. Start's own manifest is route-keyed and intentionally
excludes dynamic-import chunks, so it cannot answer those per-module lookups.

Two things were still missing after that commit:

  1. Test coverage for two of the three deliverables. The start-manifest
    e2e asserted the stylesheet side of the fix only. Nothing pinned the head
    modulepreload hints or the hydration asset map — a silent regression to the
    route-keyed manifest fallback (e.g. the plugin's stub swap failing to
    resolve) would keep the suite green while losing early chunk discovery and
    asset-gated hydration.
  2. The pinned vite-plugin-solid (3.0.0-next.23) predates fixes to the
    exact bridge this feature consumes.
    3.0.0-next.24 makes the dev asset
    resolver answer synchronously once cached (nested lazy() under
    re-created router outlets — Start's shape — could loop the SSR pass into a
    stack overflow against the always-async bridge), keeps transient dev bridge
    failures retryable instead of permanently stripping a module's assets for
    the dev session, and makes lazy-asset manifest keys survive module query
    strings and non-root Vite base paths.

Mechanism

No production code changes; the injection seam from 99756c0 is untouched
and already general:

  • The only thing Start supplies is the manifest render option
    (manifest ?? clientAssetsManifest ?? router.ssr?.manifest in
    renderRouterToStream/renderRouterToString). No hint-specific logic lives
    in Start.
  • Solid's renderer owns head emission: pre-shell assets splice in before the
    in-tree </head> of the user Document; post-shell assets stream as
    modulepreload / load-gated stylesheet links; arbitrary head tags ride the
    same channel. A future head-management (useHead-style) API flows through
    this seam with zero further Start changes — modulepreload hints are just its
    first consumer.

The commits:

  1. Upgrade vite-plugin-solid to 3.0.0-next.24 — same monorepo-wide range
    bump convention as the beta-32 upgrade (101 package.json pins +
    lockfile). next.24 carries the bridge hardening described above.
  2. Two request-level e2e tests in the existing start-manifest suite,
    using its established helpers/style, against the built app:
    • direct SSR entry to /lazy-css-lazy emits exactly one head
      modulepreload for the lazy chunk (SharedWidgetLazy-*.js) and one for
      its statically imported widget chunk, plus the chunk stylesheet — and
      routes that never render the component emit no hints for it;
    • the hydration payload serializes a boundary→chunk _assets map pointing
      at the same chunk URL the head hinted, which the client awaits before
      hydrating the boundary.
  3. Upgrade vite-plugin-solid to 3.0.0-next.26 — keeps the pin current.
    next.25/next.26 are patch releases with no changes to the manifest bridge
    (custom-extensions native-compiler fix, start.env prefix guard,
    streaming backpressure fix, Fetchable SSR service entry, vitest
    browser-mode jsdom default).

The tests read only the served HTML: emitted link tags and the documented
_$HY.r["…_assets"] hydration payload shape. They touch zero Solid
internals
and no Start internals — if either renderer or bridge changes how
assets are resolved, the assertions keep expressing the user-visible contract.

Verification

All against this branch rebased on solid-router-v2-pre (solid-js
2.0.0-beta.32, vite-plugin-solid 3.0.0-next.26):

  • e2e/solid-start/start-manifest: 12 passed (10 pre-existing + 2 new).
  • e2e/solid-start/basic: 80 passed, 4 skipped (pre-existing skips).
  • e2e/solid-start/selective-ssr: 11 passed.
  • Unit and type suites for @tanstack/solid-router, @tanstack/solid-start,
    @tanstack/solid-start-client, @tanstack/solid-start-server: all pass,
    no type errors.

Served HTML for a prod build of the start-manifest app, direct request to
/lazy-css-lazy — initial <head> (route-manifest hints carry _hk,
Solid-renderer hints for the SSR'd lazy() follow):

<link _hk= rel="modulepreload" href="/assets/index-g1ZHl8-f.js" />
<link _hk= rel="modulepreload" href="/assets/lazy-css-lazy-6_rXvEDt.js" />
<link rel="stylesheet" href="/assets/SharedWidget-BAtMnCRs.css">
<link rel="modulepreload" href="/assets/SharedWidgetLazy-JHp4Eeyx.js">
<link rel="modulepreload" href="/assets/web-DOt6yccJ.js">
<link rel="modulepreload" href="/assets/SharedWidget-cFj1aX4_.js">

and the hydration-gating map in the payload:

_$HY.r["…_assets"]=($R[3]={"…000":"/assets/SharedWidgetLazy-JHp4Eeyx.js"});

Dev mode verified manually (vite dev, same route): modulepreload for the dev
module URL (/src/components/SharedWidgetLazy.tsx), _assets map serialized,
no asset-manifest warnings in the server log.

ryansolid and others added 3 commits August 12, 2026 03:45
next.24 hardens the client-assets manifest bridge that solid-start swaps
into @tanstack/solid-router's SSR renderers (99756c0):

- the dev asset resolver answers synchronously once cached, so nested
  lazy() under re-created router outlets converges instead of looping the
  SSR pass into a stack overflow against the always-async bridge
- transient dev bridge failures stay retryable instead of permanently
  stripping a module's client assets (and its hydration preload entry)
  for the rest of the dev session
- lazy-asset manifest keys survive module query strings and non-root
  Vite base paths, so lazy(() => import('./X?query')) and apps served
  under a base prefix resolve their production manifest entries

Co-authored-by: Cursor <cursoragent@cursor.com>
… asset map

The client-assets manifest wiring (99756c0) gives SSR'd lazy()
components three observable outputs in the served HTML: modulepreload
hints for the lazy chunk and its static imports in the initial <head>,
the chunk's stylesheet link, and the serialized boundary→chunk asset map
that gates client hydration on those imports. The start-manifest e2e
only asserted the stylesheet side, so a silent regression to the
route-keyed manifest fallback would keep the suite green while losing
hints and hydration gating.

Two request-level tests against the built app pin the rest: direct SSR
entry to /lazy-css-lazy emits exactly one head modulepreload for the
lazy chunk and one for its statically imported widget chunk (and none of
them on routes that never render the component), and the hydration
payload's _assets map points at the same chunk URL the head hinted.

Co-authored-by: Cursor <cursoragent@cursor.com>
next.25 and next.26 are patch releases with no changes to the
client-assets manifest bridge this branch exercises (custom-extension
native-compiler fix, start.env prefix guard, sendWebResponse
backpressure fix, Fetchable SSR service entry, vitest browser-mode
jsdom default). Re-verified against the new pin: start-manifest e2e
12/12, basic e2e 80 passed / 4 skipped, selective-ssr 11/11, and the
solid package unit + type suites all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae3de881-87e8-4da5-bb0a-86bc5b5690bc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ryansolid
ryansolid marked this pull request as draft August 12, 2026 11:05
vite-plugin-solid is now published as @solidjs/vite-plugin; 3.0.0-next.27
is identical code to 3.0.0-next.26 plus the rename. Every dependency this
branch had pinned to the 3.0.0-next.26 line swaps to the new name (workspace
solid packages' devDependencies plus the solid e2e/example/benchmark apps),
along with the import specifiers in those projects' configs and the
plugin-name mentions in the Start manifest bridge comments. The
virtual:solid-manifest module id is unchanged upstream, so no runtime logic
moves. v1-line references stay: router-devtools-core (vite-plugin-solid
^2.11.10) and router-plugin's published optional peer range.

Re-verified against the new pin: start-manifest e2e 12/12, basic e2e
80 passed / 4 skipped, selective-ssr 11/11.

Co-authored-by: Cursor <cursoragent@cursor.com>
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