b445b892 feat(solid-router): native SSR match transfer, hydration-claiming boot, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels carry the Router + Query pairing
solid-query v6's QueryClientProvider serializes the request's cache into
Solid's hydration registry during SSR and primes the client cache from
it, so running the ssr-query transport alongside it ships every query
payload twice. The package's two runtime conveniences are each a few
lines of userland composition on public APIs: the provider wrap via the
router's Wrap option, and cache-driven redirect() errors handed to
router.navigate from the caches' config.onError.
Converts the three Solid Start e2e apps to the composition (all suites
green, including the redirect-from-query tests) and marks the package
deprecated for the v2 line.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
Solid's single-flight channel is becoming multi-source (solidjs/solid
653dd41e): mutation responses carry a keyed envelope of per-cache
slices, each routed to the consumer subscribed under its source id.
Today Start claims the single unnamed slot on both halves, which means
any other cache wanting mutation-response data (e.g. solid-query, whose
provider subscribes under "sq" in TanStack/query#11326) displaces the
router's — whichever registers last wins, silently.
The router's flight data now rides its own source id ("tsr"): the
server collector registers additively with registerFlightDataSource and
the client subscribes its consumer under the same id, so router
loader/match state and other caches' slices coexist on one round trip.
A user-supplied collectFlightData hook keeps the unnamed slot to itself,
adding data alongside the router's instead of displacing it.
Both halves feature-detect the protocol on the installed @solidjs/web
(it ships in the release after 2.0.0-rc.4) and fall back to the exact
previous unnamed-slot behavior on older versions; since client and
server resolve the same install, the halves cannot disagree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight trigger
The router's half of flight collection as a public primitive: derive the
flight request for the mutation's target, run the matched routes' data
functions, hand the loaded router to the caller's collect() — any cache
(the router's own state, a query client) composes its extraction on top.
Start's collector now consumes it; errors are contained per Solid
Router's collector convention (flight data is an optimization, never a
mutation error).
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(solid-start): require named flight sources, drop the pre-rc.5 fallback
@solidjs/web 2.0.0-rc.5 ships the multi-source single-flight protocol, so
the feature detection and cast shims bridging unreleased types come out:
the client subscribes directly under SOLID_START_FLIGHT_SOURCE, the server
registers its collector via registerFlightDataSource unconditionally (the
unnamed collectFlightData slot now always belongs to the user), and the
@solidjs/web peer floor moves to rc.5.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(e2e): expect the named single-flight source header
With the pre-rc.5 fallback stripped, the client advertises its named
source and the server echoes what it folded: X-Single-Flight is "tsr" on
both sides, not the legacy "true".
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-rc.6
Repo-wide (packages, examples, e2e apps, benchmarks) — a scoped bump
leaves the workspace mixed, and examples/benchmarks then build workspace
solid-start dists (which import registerFlightDataSource, rc.5+) against
their own @solidjs/web rc.4 resolution. rc.6 ships the named flight-data
source API this branch requires plus the settle-walk fix that unblocks
the Solid Query pairing. @tanstack/solid-start's peer floor moves to
rc.6.
The SSR bench helpers move onto rc.6's wire shape: scripted callers use
the data address (`<endpoint>/data/<id>`) — the bare address now answers
document traffic with the no-JS convention.
Co-authored-by: Cursor <cursoragent@cursor.com>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming boot
Proves the RFC's Phase 1 claims on the external-SSR harness, against
published core (web 2.0.0-rc.5):
- Server render serializes each match's loaderData/status into Solid's
hydration registry, content-addressed (`tsr:<matchId>`), the identical
mechanism solid-query v6 ships queries through — no `__TSR_SSR__`
script channel.
- The client boot matches synchronously, primes match state from the
registry (populated at document parse), and commits — no
`router.load()` before hydrate, loaders do not re-run (0 client runs),
hydration claims the server DOM identically, and post-hydration
navigation with an unresolved chunk still shows pending UI and settles
under its boundary.
Spike-level notes: the commit must happen before hydrate() (store writes
inside the hydration render are owned-scope writes), the transfer covers
settled matches (promise-valued entries for pending loaders are the same
serialize call, next step), and the serialization-context guard keys on
`ctx.serialize` presence (`ctx.async` is not set under this web
version's renderToString).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (Phase 1)
Grows the spike into the adapter. RouterProvider serializes each settled
match's state (loaderData, status, error, notFound, beforeLoadContext,
ssr) into Solid's hydration registry during server render —
content-addressed (`tsr:<matchId>`), the same channel solid-query v6
ships queries through, no `__TSR_SSR__` script injection. The Router
constructor owns the client half: when the registry holds entries for the
synchronously matched routes, it primes and commits match state at
creation — always outside a render, after the document (and therefore the
entries) parsed, before hydrate(). No load pass before hydration, no
loader re-runs; route chunks resolve at the read point under the
boundaries the server rendered.
Both halves are inert outside the bare pairing: the server skips when
`router.serverSsr` marks the Start contract, and the boot falls through
on the first missing entry (SPA pages, Start's own channel).
Placement is load-bearing: committing inside the hydration render — even
with writes moved off the owner — leaves the claiming walk's registry
bookkeeping desynced (nodes reuse correctly but audit as unclaimed).
Router creation is the client's natural pre-render moment, and the
harness A/B proved it clean.
Validated: external-SSR harness green end to end (registry primed, zero
client loader runs, identical DOM claimed, pending UI on post-hydration
nav), solid-router unit suite 865 tests green, and all 37 Solid Start e2e
tests green against the rc.6-candidate core (workspace tarballs) with the
named-source strips in place. The harness now contains zero transfer
code. Pending matches are skipped, not deferred — promise-valued entries
(streaming SSR) are the next increment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
RouterProvider now owns router.load() on the server, parking the render
on it through an async memo — no more explicit await router.load() in
entries; blocking semantics ride Solid's async SSR. The bare-pairing
harness moves to renderToStream and proves deferred loaderData promises
stream natively (fallback in the shell chunk, value in a later chunk,
settled through hydration without <Await>).
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
Two things were failing the Test job:
- @tanstack/solid-router test:eslint: the repro-external-ssr harness
is not part of the package tsconfig, so the typed parser rejected
its .tsx files. Ignore the harness in the package eslint config; it
is a standalone vite script, not shipped code.
- Four example builds (basic-solid-query, basic-solid-query-file-based,
kitchen-sink-file-based, kitchen-sink-solid-query) run tsc, and the
rc.6 types no longer accept the one-argument createEffect form.
Convert the eleven call sites to the two-argument
createEffect(source, effect) form already used by the sibling
examples and e2e apps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> 96f50d94 feat(solid): named single-flight source + loadFlightTarget, the cache-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels carry the Router + Query pairing
solid-query v6's QueryClientProvider serializes the request's cache into
Solid's hydration registry during SSR and primes the client cache from
it, so running the ssr-query transport alongside it ships every query
payload twice. The package's two runtime conveniences are each a few
lines of userland composition on public APIs: the provider wrap via the
router's Wrap option, and cache-driven redirect() errors handed to
router.navigate from the caches' config.onError.
Converts the three Solid Start e2e apps to the composition (all suites
green, including the redirect-from-query tests) and marks the package
deprecated for the v2 line.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
Solid's single-flight channel is becoming multi-source (solidjs/solid
653dd41e): mutation responses carry a keyed envelope of per-cache
slices, each routed to the consumer subscribed under its source id.
Today Start claims the single unnamed slot on both halves, which means
any other cache wanting mutation-response data (e.g. solid-query, whose
provider subscribes under "sq" in TanStack/query#11326) displaces the
router's — whichever registers last wins, silently.
The router's flight data now rides its own source id ("tsr"): the
server collector registers additively with registerFlightDataSource and
the client subscribes its consumer under the same id, so router
loader/match state and other caches' slices coexist on one round trip.
A user-supplied collectFlightData hook keeps the unnamed slot to itself,
adding data alongside the router's instead of displacing it.
Both halves feature-detect the protocol on the installed @solidjs/web
(it ships in the release after 2.0.0-rc.4) and fall back to the exact
previous unnamed-slot behavior on older versions; since client and
server resolve the same install, the halves cannot disagree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight trigger
The router's half of flight collection as a public primitive: derive the
flight request for the mutation's target, run the matched routes' data
functions, hand the loaded router to the caller's collect() — any cache
(the router's own state, a query client) composes its extraction on top.
Start's collector now consumes it; errors are contained per Solid
Router's collector convention (flight data is an optimization, never a
mutation error).
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(solid-start): require named flight sources, drop the pre-rc.5 fallback
@solidjs/web 2.0.0-rc.5 ships the multi-source single-flight protocol, so
the feature detection and cast shims bridging unreleased types come out:
the client subscribes directly under SOLID_START_FLIGHT_SOURCE, the server
registers its collector via registerFlightDataSource unconditionally (the
unnamed collectFlightData slot now always belongs to the user), and the
@solidjs/web peer floor moves to rc.5.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(e2e): expect the named single-flight source header
With the pre-rc.5 fallback stripped, the client advertises its named
source and the server echoes what it folded: X-Single-Flight is "tsr" on
both sides, not the legacy "true".
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-rc.6
Repo-wide (packages, examples, e2e apps, benchmarks) — a scoped bump
leaves the workspace mixed, and examples/benchmarks then build workspace
solid-start dists (which import registerFlightDataSource, rc.5+) against
their own @solidjs/web rc.4 resolution. rc.6 ships the named flight-data
source API this branch requires plus the settle-walk fix that unblocks
the Solid Query pairing. @tanstack/solid-start's peer floor moves to
rc.6.
The SSR bench helpers move onto rc.6's wire shape: scripted callers use
the data address (`<endpoint>/data/<id>`) — the bare address now answers
document traffic with the no-JS convention.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> 252caa85 feat(solid-router): delegate lazyRouteComponent to Solid 2's lazy() (#8081)
* feat(solid-router): register head tags through Solid's useHead registry
HeadContent now feeds the route-derived tags (useTags output, semantics
unchanged) to Solid's head registry as one reactive group instead of
rendering <Asset> elements in-tree. The registry owns head emission on
both runtimes: SSR splices/streams resolved winners into <head>, the
client patches document.head reactively — replacing the manual
relocate-into-head, imperative script injection, and document.title
syncing that Asset.tsx carried for head tags.
The dev-only dev-styles handling (filter DEV_STYLES_ATTR links post
hydration) is a no-op in production, so it folds into the single
HeadContent — retiring HeadContent.dev.tsx, index.dev.tsx, and the
package's development export conditions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(solid-start): pin SSR'd lazy() modulepreload hints and hydration asset map
The client-assets manifest wiring (99756c0710) 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>
* refactor(solid-router): lean on the head registry for tag lifecycle
Drop the pieces of the head pipeline that existed only for the old
in-tree render path: the reference-reuse memo and replaceEqualTags (DOM
node stability for a <For> that no longer exists) and the dev-styles
post-hydration filtering (HeadContent is now a pure useHead
registration). Add the changeset for the useHead adoption.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: apply automated fixes
* feat(solid-router): delegate lazyRouteComponent to Solid's lazy()
Route components now render through solid-js lazy(): the importer wrapper
selects the configured export and forwards the module's $$moduleUrl (the
bundler's SSR transform appends it), so SSR'd route chunks get client
asset resolution — stylesheet links, modulepreload hints, and hydration
gating — through the same client-assets manifest seam as any other
lazy() component, with positional hydration as the fallback.
Carries an interim pnpm patch for solid-js 2.0.0-rc.0 (to be upstreamed,
same play as the vite-plugin-solid isEntry patch): lazy() no longer
caches a rejected module promise, so a failed chunk download is retried
by the next preload or render instead of being sealed in for the page's
lifetime — this is what lets TanStack's download-retry contract ride on
lazy() directly instead of wrapping it in a retry state machine. The
patch also fixes an unhandled-rejection leak (lazy's internal .then had
no rejection handler) and, on the server, stops a transient import
failure from poisoning every later SSR request.
Retry semantics improve: a failed hover-preload is retried at render
(previously the recorded error was thrown without re-attempting the
import); the error boundary + route-reload retry flow is unchanged when
the render-time attempt also fails. preload() keeps its contract — a
memoized Promise<void> that never rejects, cleared on failure so a later
preload retries, one import per server. The module-not-found reload-once
dance is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(solid-router): stable head-registry identities for inline tags + CI repairs
Inline scripts and styles get a stable content-derived key in toHeadTags:
the registry otherwise assigns per-runtime unique identities that can
never match between server and client, so hydration appended a client
copy next to the server-rendered tag (caught by the solid-start basic
script-duplication e2e).
The ssr head benchmark's sanity assertion now counts name="…" attribute
occurrences instead of raw substring hits — the registry writes the
identity into data-dh on the same element, which doubled the old count.
remountDeps tests migrate onMount to Solid 2's onSettled (pre-existing
branch failure; the API was removed in the rc line).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(solid-router): make route-boundary resets converge (issue 7986 + background recovery)
Two compounding defects kept a captured route error from retrying
cleanly on Solid 2:
- CatchBoundary's reset effect re-ran on every recompute of the reset
key, not only on value changes, so any store write recreated the
retried subtree while it settled. The effect is now gated on the key
value. The reset handle deliberately stays un-consumed: a reset
applied at the wrong moment can no-op (the boundary stays errored and
the fallback never re-runs), and only a repeated attempt on a later
key change recovers it — consuming the handle at apply time or at
children re-render both proved to lose exactly that retry (verified
against the background-recovery test).
- Match keyed its boundary on the whole matches store, so every
unrelated transition fired a premature reset that re-threw the
still-stale error and flapped the error UI. The key is now scoped to
the guarded match and its descendants (whose errors bubble here when
they have no errorComponent), mirroring react-router's per-match
keying, as a value string because the store emits fresh objects for
content-equal writes.
The issue-7986 pending assertion queries fresh under waitFor: the
retried subtree renders the pending fallback from two nested slots in
quick succession (Show fallback, then the Loading boundary once the
child suspends), so a cached element handle can go stale across the
swap.
First fully green unit run on this branch: 59 files, 864 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(solid-router): hydrate lazy route components correctly; scope route assets to TanStack's manifest
Extends the interim solid-js patch with two hydration-critical fixes
(both upstream candidates, same play as the retry fix it already
carries):
- lazy()'s preloaded-module hydration shortcut (_$HY.modules) now only
applies when the callsite has a bundler-injected moduleUrl. The
shortcut assumes the registered chunk's default export is the
component; for wrappers selecting a named export it silently rendered
undefined, never claimed the SSR'd DOM, and navigation later couldn't
dispose it — 20 basic-suite e2e failures (orphaned route content
stacking up across navigations).
- $$moduleUrl: null on a lazy()-resolved module is an explicit opt-out
of per-module asset registration, without the server warning that
undefined draws.
lazyRouteComponent uses that opt-out: route chunks bundle many routes,
so registering the chunk emitted the union of their CSS on every route
and broke per-route stylesheet isolation (4 start-manifest e2e
failures). Route assets stay on TanStack's route-keyed manifest, which
is per-route precise; hydration is synchronous because preload() warms
lazy()'s component cache before the router renders. User-level lazy()
components inside routes remain fully registered — the opt-out is
scoped to the route-component wrapper.
Verified: solid-start basic e2e 80 passed (was 60/20); start-manifest
10 passed (including the previously failing lazy-to-static shared-CSS
navigation); unit suites green apart from the four pre-existing
failures fixed on the useHead branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(solid-router): route chunks opt in to Solid's client-assets manifest
With the head registry (useHead branch) underneath, lazyRouteComponent
forwards the module's real $$moduleUrl: split route modules have
per-module client-manifest entries (the query is part of the module
identity), so server-side lazy() registers exactly the route's own CSS
and modulepreload hints, and the registry's URL-keyed resource dedupe
absorbs the overlap with TanStack's route-keyed manifest links — the
duplicate-stylesheet failures that forced the earlier $$moduleUrl null
opt-out were double emission between Solid's channel and TanStack's
in-tree links, which the registry now reconciles.
The now-unused null opt-out hunk is dropped from the solid-js patch,
leaving exactly the two upstream-tracked fixes (rejection retry:
solidjs/solid#2999, fixed on next in 3c68ab21; hydration-shortcut
moduleUrl gate: solidjs/solid#3011).
Verified on the stack: unit 59 files / 865 tests all green (first fully
green combination), solid-start basic e2e 80 passed, start-manifest 12
passed including the ported modulepreload-hint and hydration asset-map
tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(solid-router): adopt lazy()'s { export } option via upstream backport
Upstream resolved solidjs/solid#3011 with first-class named-export
support instead of the fallback gate this patch carried: lazy(fn,
{ export }, moduleUrl?) names which export of the resolved module is
the component, as a call-site literal available on both runtimes —
so hydration claims the component synchronously from the preloaded
module namespace, and runtime wrappers that pick an export inside the
import thunk fail loudly in dev instead of silently orphaning SSR'd
DOM.
The solid-js patch is now a faithful backport of solidjs/solid@56ca6470
(which also carries the #2999 rejection-retry fix in its final
load-inside-memo shape) onto the rc.0 dists, plus one deliberate
deviation: lazy() still accepts the legacy string second argument
(moduleUrl) that rc.0's bundler transform injects, normalizing it to
the third position, until the updated transform (dom-expressions
0856717a) ships. Type declarations carry the new overloads.
lazyRouteComponent takes its final upstream-aligned shape: the module
namespace passes through untouched ($$moduleUrl included, so route
chunks keep registering their real client assets), { export } selects
the component, and only the module-not-found reload dance and the
memoized never-rejecting preload contract remain as wrapper logic.
When an rc ships with both upstream commits, the migration is deleting
the patch.
Verified: unit 59 files / 865 tests green; solid-start basic e2e 80
passed; start-manifest 12 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(solid): bump to solid-js 2.0.0-rc.1 and drop the solid-js patch
rc.1 ships everything the interim patch carried: the lazy() rejection
retry fix (solidjs/solid#2999, 3c68ab21) and the { export } option with
its loud-in-dev hydration guard (solidjs/solid#3011, 56ca6470 +
dom-expressions 0856717a — the updated transform arrives through
babel-preset-solid 2.0.0-rc.1 via @solidjs/vite-plugin 3.0.0-next.30).
patches/ and the patchedDependencies entry are deleted;
lazyRouteComponent is unchanged, already in its final shape against the
real API.
Monorepo-wide range bump: solid-js and @solidjs/web ^2.0.0-rc.0 →
^2.0.0-rc.1, @solidjs/vite-plugin ^3.0.0-next.28 → ^3.0.0-next.30.
Verified on vanilla rc.1: unit 59 files / 865 tests green; solid-start
basic e2e 80 passed; start-manifest 12 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> b60b741a feat(solid-router): adopt Solid 2's useHead registry for head management (#8080)
* feat(solid-router): register head tags through Solid's useHead registry
HeadContent now feeds the route-derived tags (useTags output, semantics
unchanged) to Solid's head registry as one reactive group instead of
rendering <Asset> elements in-tree. The registry owns head emission on
both runtimes: SSR splices/streams resolved winners into <head>, the
client patches document.head reactively — replacing the manual
relocate-into-head, imperative script injection, and document.title
syncing that Asset.tsx carried for head tags.
The dev-only dev-styles handling (filter DEV_STYLES_ATTR links post
hydration) is a no-op in production, so it folds into the single
HeadContent — retiring HeadContent.dev.tsx, index.dev.tsx, and the
package's development export conditions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(solid-start): pin SSR'd lazy() modulepreload hints and hydration asset map
The client-assets manifest wiring (99756c0710) 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>
* refactor(solid-router): lean on the head registry for tag lifecycle
Drop the pieces of the head pipeline that existed only for the old
in-tree render path: the reference-reuse memo and replaceEqualTags (DOM
node stability for a <For> that no longer exists) and the dev-styles
post-hydration filtering (HeadContent is now a pure useHead
registration). Add the changeset for the useHead adoption.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: apply automated fixes
* fix(solid-router): stable head-registry identities for inline tags + CI repairs
Inline scripts and styles get a stable content-derived key in toHeadTags:
the registry otherwise assigns per-runtime unique identities that can
never match between server and client, so hydration appended a client
copy next to the server-rendered tag (caught by the solid-start basic
script-duplication e2e).
The ssr head benchmark's sanity assertion now counts name="…" attribute
occurrences instead of raw substring hits — the registry writes the
identity into data-dh on the same element, which doubled the old count.
remountDeps tests migrate onMount to Solid 2's onSettled (pre-existing
branch failure; the API was removed in the rc line).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(solid-router): make route-boundary resets converge (issue 7986 + background recovery)
Two compounding defects kept a captured route error from retrying
cleanly on Solid 2:
- CatchBoundary's reset effect re-ran on every recompute of the reset
key, not only on value changes, so any store write recreated the
retried subtree while it settled. The effect is now gated on the key
value. The reset handle deliberately stays un-consumed: a reset
applied at the wrong moment can no-op (the boundary stays errored and
the fallback never re-runs), and only a repeated attempt on a later
key change recovers it — consuming the handle at apply time or at
children re-render both proved to lose exactly that retry (verified
against the background-recovery test).
- Match keyed its boundary on the whole matches store, so every
unrelated transition fired a premature reset that re-threw the
still-stale error and flapped the error UI. The key is now scoped to
the guarded match and its descendants (whose errors bubble here when
they have no errorComponent), mirroring react-router's per-match
keying, as a value string because the store emits fresh objects for
content-equal writes.
The issue-7986 pending assertion queries fresh under waitFor: the
retried subtree renders the pending fallback from two nested slots in
quick succession (Show fallback, then the Loading boundary once the
child suspends), so a cached element handle can go stale across the
swap.
First fully green unit run on this branch: 59 files, 864 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> 41cffa00 fix(solid-router): make the global loading boundary opt-in and server/client-symmetric (#8027)
* fix(solid-router): make the global loading boundary opt-in and server/client-symmetric
`Matches` wrapped the match tree in a client-only `Solid.Loading`
boundary unless `disableGlobalCatchBoundary`, `isServer`, or
`router.ssr` was set, while the server side always rendered
`SafeFragment`. Apps that server-render without the `$_TSR` stream
protocol (own server entry: memory history, awaited `router.load()`,
`renderToStream`) never set `router.ssr`, so the client hydrated a
boundary the server never rendered. That desyncs Solid's positional
node claiming and surfaces as an opaque `reconcileArrays` crash
(`NotFoundError: insertBefore`).
The wrapper is now opt-in and the decision is evaluated identically on
the server and the client. It renders only when all of these hold:
- the app configured root pending UI (root `pendingComponent` or
`defaultPendingComponent`) — new;
- `router.ssr` is not set (the `$_TSR` protocol short-circuit,
unchanged and pinned by tests);
- `disableGlobalCatchBoundary` is not set (unchanged).
There is no `isServer` arm, no hydration probe, and no match-state
inspection — the decision reads only router configuration, so the
server tree and the hydrating client tree cannot disagree.
With nothing configured there is no wrapper at all: pending propagates
as ordinary Solid 2 async. The previous behavior there was an invisible
`Solid.Loading` with a `null` fallback, which also converted errors to
pending. With pending UI configured, the boundary now renders on both
sides, so external SSR hydrates cleanly (server nodes claimed and
reused, no pending flash) and the pending UI keeps working for
post-hydration navigations — a client-only wrapper, even a settled one,
shifts hydration-key derivation and leaves the server nodes unclaimed.
Tests: matches-hydration-boundary.test.tsx pins the opt-in decision,
the protocol short-circuit, `disableGlobalCatchBoundary`, and that the
decision is identical whether or not a hydration pass is in flight (a
hydration-keyed decision would freeze `SafeFragment` into hydrated apps
permanently); server-mode matchesLoadingBoundary.test.tsx pins the
symmetric server output. Verified additionally against a real
`renderToString` -> `hydrate` round trip: zero hydration errors, zero
unclaimed nodes, server DOM reused by identity.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>