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> 8fbf8d71 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> fa1e030a 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>