@real-router/solid 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -14
- package/dist/cjs/index.d.ts +20 -1
- package/dist/cjs/index.js +0 -0
- package/dist/cjs/ssr.d.ts +163 -0
- package/dist/cjs/ssr.js +263 -0
- package/dist/esm/index.d.mts +20 -1
- package/dist/esm/index.mjs +0 -0
- package/dist/esm/ssr.d.mts +163 -0
- package/dist/esm/ssr.mjs +254 -0
- package/dist/types/RouterProvider.d.ts.map +1 -1
- package/dist/types/components/Await.d.ts +30 -0
- package/dist/types/components/Await.d.ts.map +1 -0
- package/dist/types/components/ClientOnly.d.ts +7 -0
- package/dist/types/components/ClientOnly.d.ts.map +1 -0
- package/dist/types/components/HttpStatusCode.d.ts +52 -0
- package/dist/types/components/HttpStatusCode.d.ts.map +1 -0
- package/dist/types/components/HttpStatusProvider.d.ts +9 -0
- package/dist/types/components/HttpStatusProvider.d.ts.map +1 -0
- package/dist/types/components/Link.d.ts.map +1 -1
- package/dist/types/components/RouteView/RouteView.d.ts.map +1 -1
- package/dist/types/components/RouteView/components.d.ts.map +1 -1
- package/dist/types/components/RouteView/helpers.d.ts.map +1 -1
- package/dist/types/components/ServerOnly.d.ts +7 -0
- package/dist/types/components/ServerOnly.d.ts.map +1 -0
- package/dist/types/components/Streamed.d.ts +18 -0
- package/dist/types/components/Streamed.d.ts.map +1 -0
- package/dist/types/constants.d.ts +20 -2
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/context.d.ts +9 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/createSignalFromSource.d.ts.map +1 -1
- package/dist/types/createStoreFromSource.d.ts +19 -0
- package/dist/types/createStoreFromSource.d.ts.map +1 -1
- package/dist/types/directives/link.d.ts.map +1 -1
- package/dist/types/dom-utils/__test-helpers/expected-fragment.d.ts +31 -0
- package/dist/types/dom-utils/__test-helpers/expected-fragment.d.ts.map +1 -0
- package/dist/types/dom-utils/__test-helpers/index.d.ts +9 -0
- package/dist/types/dom-utils/__test-helpers/index.d.ts.map +1 -0
- package/dist/types/dom-utils/link-utils.d.ts +23 -0
- package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
- package/dist/types/dom-utils/route-announcer.d.ts.map +1 -1
- package/dist/types/dom-utils/scroll-restore.d.ts +38 -1
- package/dist/types/dom-utils/scroll-restore.d.ts.map +1 -1
- package/dist/types/hooks/useDeferred.d.ts +16 -0
- package/dist/types/hooks/useDeferred.d.ts.map +1 -0
- package/dist/types/hooks/useNavigator.d.ts.map +1 -1
- package/dist/types/hooks/useRouteUtils.d.ts.map +1 -1
- package/dist/types/hooks/useRouter.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ssr.d.ts +16 -0
- package/dist/types/ssr.d.ts.map +1 -0
- package/dist/types/utils/createHttpStatusSink.d.ts +29 -0
- package/dist/types/utils/createHttpStatusSink.d.ts.map +1 -0
- package/dist/types/utils/createMountedSignal.d.ts +16 -0
- package/dist/types/utils/createMountedSignal.d.ts.map +1 -0
- package/package.json +20 -4
- package/src/RouterProvider.tsx +36 -30
- package/src/components/Await.tsx +56 -0
- package/src/components/ClientOnly.tsx +20 -0
- package/src/components/HttpStatusCode.tsx +65 -0
- package/src/components/HttpStatusProvider.tsx +21 -0
- package/src/components/Link.tsx +21 -15
- package/src/components/RouteView/RouteView.tsx +19 -18
- package/src/components/RouteView/components.tsx +34 -28
- package/src/components/RouteView/helpers.tsx +0 -0
- package/src/components/ServerOnly.tsx +20 -0
- package/src/components/Streamed.tsx +23 -0
- package/src/constants.ts +20 -2
- package/src/context.ts +21 -1
- package/src/createSignalFromSource.ts +48 -5
- package/src/createStoreFromSource.ts +49 -2
- package/src/directives/link.tsx +41 -16
- package/src/hooks/useDeferred.tsx +36 -0
- package/src/hooks/useNavigator.tsx +3 -12
- package/src/hooks/useRouteUtils.tsx +39 -1
- package/src/hooks/useRouter.tsx +3 -12
- package/src/index.tsx +2 -0
- package/src/ssr.tsx +39 -0
- package/src/utils/createHttpStatusSink.ts +31 -0
- package/src/utils/createMountedSignal.ts +26 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ const router = createRouter([
|
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
31
|
router.usePlugin(browserPluginFactory());
|
|
32
|
-
router.start();
|
|
32
|
+
await router.start();
|
|
33
33
|
|
|
34
34
|
function App() {
|
|
35
35
|
return (
|
|
@@ -61,9 +61,9 @@ All hooks that subscribe to route state return `Accessor<T>` — call the access
|
|
|
61
61
|
| Hook | Returns | Reactive? |
|
|
62
62
|
| ------------------------- | ------------------------------------ | ------------------------------------ |
|
|
63
63
|
| `useRouter()` | `Router` | Never |
|
|
64
|
-
| `useNavigator()` | `Navigator`
|
|
64
|
+
| `useNavigator()` | `Navigator` — `{ navigate, subscribe, subscribeLeave, isLeaveApproved, … }` | Never |
|
|
65
65
|
| `useRoute()` | `Accessor<RouteState>` | Every navigation |
|
|
66
|
-
| `useRouteNode(name)` | `Accessor<RouteState>` |
|
|
66
|
+
| `useRouteNode(name)` | `Accessor<RouteState>` | When the node's slice of state changes (activation, deactivation, params change inside the subtree) |
|
|
67
67
|
| `useRouteUtils()` | `RouteUtils` | Never |
|
|
68
68
|
| `useRouterTransition()` | `Accessor<RouterTransitionSnapshot>` | On transition start/end |
|
|
69
69
|
| `useRouteStore()` | `RouteState` (store) | Granular — per-property |
|
|
@@ -71,6 +71,28 @@ All hooks that subscribe to route state return `Accessor<T>` — call the access
|
|
|
71
71
|
| `useRouteExit(handler, options?)` | `void` — wraps `subscribeLeave` with abort + same-route guards | Never (handler captured at hook call) |
|
|
72
72
|
| `useRouteEnter(handler, options?)` | `void` — fires once on nav-driven mount via `useRoute()` + `transition.from` | Never (handler captured at hook call) |
|
|
73
73
|
|
|
74
|
+
### Typed Route Params (`useRoute<P>`)
|
|
75
|
+
|
|
76
|
+
`useRoute<P>()` accepts an optional generic so `route.params` is typed without an `as` cast at the call site (the cast happens once inside the hook — no runtime overhead):
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useRoute } from "@real-router/solid";
|
|
80
|
+
import type { Params } from "@real-router/core";
|
|
81
|
+
|
|
82
|
+
type SearchParams = { q: string; sort: "asc" | "desc" } & Params;
|
|
83
|
+
|
|
84
|
+
function SearchView() {
|
|
85
|
+
const routeState = useRoute<SearchParams>();
|
|
86
|
+
return <p>Query: {routeState().route.params.q}</p>; // typed as string
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`Link<P>` and `LinkDirectiveOptions<P>` accept the same generic for type-safe `routeParams`.
|
|
91
|
+
|
|
92
|
+
### `useRoute()` throws when no route is active
|
|
93
|
+
|
|
94
|
+
`useRoute()` returns `Accessor<{ route: State<P>; previousRoute?: State }>` where `route` is **non-nullable**. The hook throws if the router has no active state (unstarted, stopped, disposed) at the point of subscription. Use `useRouteNode(name)` or `useRouteStore()` if node inactivity is a legitimate state in your code path — those stay nullable. See `Gotchas` in the CLAUDE.md for the migration pattern.
|
|
95
|
+
|
|
74
96
|
### Store-Based Hooks (Granular Reactivity)
|
|
75
97
|
|
|
76
98
|
`useRouteStore()` and `useRouteNodeStore()` use `createStore` + `reconcile` for property-level reactivity. A component reading `state.route?.params.id` won't re-run when `state.route?.params.page` changes:
|
|
@@ -103,6 +125,28 @@ Two low-level bridges convert `@real-router/sources` `RouterSource<T>` instances
|
|
|
103
125
|
|
|
104
126
|
Both must be called inside a reactive owner (component body or `createRoot`).
|
|
105
127
|
|
|
128
|
+
### Contexts (Advanced)
|
|
129
|
+
|
|
130
|
+
Two Solid contexts are exported for building custom hooks or deeply nested integrations that need direct access to router internals without prop drilling:
|
|
131
|
+
|
|
132
|
+
| Context | Value type | Description |
|
|
133
|
+
| -------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
134
|
+
| `RouterContext` | `RouterContextValue \| null` — `{ router, navigator, routeSelector }` | Stable references. `routeSelector(name)` is the O(1) `createSelector`-backed active check used by `<Link>`. |
|
|
135
|
+
| `RouteContext` | `Accessor<RouteState> \| null` | Reactive signal. Updates on every navigation. Consumed by `useRoute()`. |
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { RouterContext, RouteContext } from "@real-router/solid";
|
|
139
|
+
import { useContext } from "solid-js";
|
|
140
|
+
|
|
141
|
+
function MyCustomHook() {
|
|
142
|
+
const ctx = useContext(RouterContext);
|
|
143
|
+
if (!ctx) throw new Error("Must be used inside <RouterProvider>");
|
|
144
|
+
return ctx.router;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For most use cases the existing hooks (`useRouter`, `useRoute`, `useNavigator`) are sufficient — reach for raw contexts only when building custom primitives.
|
|
149
|
+
|
|
106
150
|
```tsx
|
|
107
151
|
// useRouteNode — updates only when "users.*" changes
|
|
108
152
|
function UsersLayout() {
|
|
@@ -243,6 +287,38 @@ const LazyDashboard = lazy(() => import("./Dashboard"));
|
|
|
243
287
|
|
|
244
288
|
Without `fallback`, no `<Suspense>` boundary is added. The prop is optional.
|
|
245
289
|
|
|
290
|
+
#### `<RouteView.Self>` — render the parent node itself
|
|
291
|
+
|
|
292
|
+
`RouteView` shows `<Match>` children when a **descendant** route is active. To render content when the active route's name equals the parent's `nodeName` **exactly**, use `<RouteView.Self>`:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<RouteView nodeName="users">
|
|
296
|
+
<RouteView.Self>
|
|
297
|
+
<UsersIndex /> {/* active route === "users" exactly */}
|
|
298
|
+
</RouteView.Self>
|
|
299
|
+
<RouteView.Match segment="profile">
|
|
300
|
+
<UserProfile /> {/* active route is "users.profile" or a descendant */}
|
|
301
|
+
</RouteView.Match>
|
|
302
|
+
<RouteView.NotFound>
|
|
303
|
+
<NotFoundPage />
|
|
304
|
+
</RouteView.NotFound>
|
|
305
|
+
</RouteView>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`<RouteView.Self>` accepts an optional `fallback` prop (`JSX.Element`) — when provided, the children are wrapped in `<Suspense>` with that fallback, parallel to `<Match fallback>`.
|
|
309
|
+
|
|
310
|
+
**Precedence rules:**
|
|
311
|
+
|
|
312
|
+
1. `<Match>` first-wins — if any `<Match>` activates, `<Self>` and `<NotFound>` are suppressed.
|
|
313
|
+
2. `<Self>` first-wins — only the first `<RouteView.Self>` contributes; later instances are ignored.
|
|
314
|
+
3. `<Self>` wins over `<NotFound>` if no `<Match>` activates (rare — applies only when `nodeName === UNKNOWN_ROUTE`).
|
|
315
|
+
|
|
316
|
+
| Element | Fires when | Render position |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `<RouteView.Match>` | Active route segment matches `segment` (or descendant if `exact={false}`) | Inline at source position |
|
|
319
|
+
| `<RouteView.Self>` | Active route name **exactly equals** parent's `nodeName` | Appended after Match elements |
|
|
320
|
+
| `<RouteView.NotFound>` | Active route is `UNKNOWN_ROUTE` AND no Match activated | Appended after Match elements |
|
|
321
|
+
|
|
246
322
|
> **Note:** `keepAlive` is not supported. Solid has no equivalent of React's `<Activity>` API. Components dispose completely when navigating away.
|
|
247
323
|
|
|
248
324
|
### `<RouterErrorBoundary>`
|
|
@@ -258,7 +334,13 @@ import { RouterErrorBoundary } from "@real-router/solid";
|
|
|
258
334
|
{error.code} <button onClick={resetError}>Dismiss</button>
|
|
259
335
|
</div>
|
|
260
336
|
)}
|
|
261
|
-
onError={(error
|
|
337
|
+
onError={(error, toRoute, fromRoute) =>
|
|
338
|
+
analytics.track("nav_error", {
|
|
339
|
+
code: error.code,
|
|
340
|
+
to: toRoute?.name,
|
|
341
|
+
from: fromRoute?.name,
|
|
342
|
+
})
|
|
343
|
+
}
|
|
262
344
|
>
|
|
263
345
|
<Link routeName="protected">Go to Protected</Link>
|
|
264
346
|
</RouterErrorBoundary>;
|
|
@@ -266,6 +348,62 @@ import { RouterErrorBoundary } from "@real-router/solid";
|
|
|
266
348
|
|
|
267
349
|
Auto-resets on next successful navigation. Works with both `<Link>` and imperative `router.navigate()`.
|
|
268
350
|
|
|
351
|
+
### `<ClientOnly>` / `<ServerOnly>`
|
|
352
|
+
|
|
353
|
+
Paired SSR-aware boundaries. `<ClientOnly>` renders `fallback` on the server (and on the client first paint, to match SSR HTML), then swaps in `children` after mount. `<ServerOnly>` is the symmetric inverse.
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { ClientOnly, ServerOnly } from "@real-router/solid/ssr";
|
|
357
|
+
|
|
358
|
+
<ClientOnly fallback={<Skeleton />}>
|
|
359
|
+
<BrowserApiWidget />
|
|
360
|
+
</ClientOnly>
|
|
361
|
+
|
|
362
|
+
<ServerOnly>
|
|
363
|
+
<SeoMetaStrip />
|
|
364
|
+
</ServerOnly>;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
All SSR-aware exports (`ClientOnly`, `ServerOnly`, `Streamed`, `Await`, `useDeferred`, `HttpStatusCode`, `HttpStatusProvider`, `createHttpStatusSink`) live at the `/ssr` subpath — the main entry stays client-only.
|
|
368
|
+
|
|
369
|
+
### Render-Time HTTP Status (`<HttpStatusCode />`)
|
|
370
|
+
|
|
371
|
+
For SSR responses that need a non-200 status (404, 410, 503, redirects), declare the code inline during render. `<HttpStatusCode code={N} />` writes through a `<HttpStatusProvider>` sink and returns `null` — no DOM, no hydration mismatch. Last write wins; a no-op without a provider so the same code paths work in CSR.
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
import { renderToString } from "solid-js/web";
|
|
375
|
+
import {
|
|
376
|
+
HttpStatusCode,
|
|
377
|
+
HttpStatusProvider,
|
|
378
|
+
createHttpStatusSink,
|
|
379
|
+
} from "@real-router/solid/ssr";
|
|
380
|
+
|
|
381
|
+
// In a "page not found" view:
|
|
382
|
+
function NotFound() {
|
|
383
|
+
return (
|
|
384
|
+
<>
|
|
385
|
+
<HttpStatusCode code={404} />
|
|
386
|
+
<h1>Page not found</h1>
|
|
387
|
+
</>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// entry-server.tsx
|
|
392
|
+
const sink = createHttpStatusSink();
|
|
393
|
+
const html = renderToString(() => (
|
|
394
|
+
<HttpStatusProvider sink={sink}>
|
|
395
|
+
<App />
|
|
396
|
+
</HttpStatusProvider>
|
|
397
|
+
));
|
|
398
|
+
response.status(sink.code ?? 200).send(html);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
> **Streaming SSR caveat:** with `renderToStream`, the response status MUST be sent before the first body byte flushes. If `<HttpStatusCode />` is mounted **inside a late-resolving `<Suspense>`**, the sink write happens AFTER the headers are already on the wire — the override is lost. Mount the component in the shell (above every `<Suspense>` that could delay it), or use `renderToStringAsync` (awaits all Suspense before returning HTML).
|
|
402
|
+
|
|
403
|
+
> **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on `NaN`, `0`, negative values, or `> 999` — surfaces as a 5xx / dropped connection. Pass a real HTTP status integer (100-999; commonly 4xx/5xx).
|
|
404
|
+
|
|
405
|
+
Implementation: `createSignal(false)` + `onMount(() => setMounted(true))` + `<Show>`. `onMount` is SSR-safe per Solid's runtime contract — it never fires during `renderToString`/`renderToStream`. End-to-end dogfooding lives in [`examples/web/solid/ssr-examples/ssr/`](../../examples/web/solid/ssr-examples/ssr/) (see `e2e/ssr-boundaries.spec.ts`).
|
|
406
|
+
|
|
269
407
|
## Directives
|
|
270
408
|
|
|
271
409
|
### `use:link`
|
|
@@ -299,17 +437,31 @@ import { link } from "@real-router/solid";
|
|
|
299
437
|
|
|
300
438
|
**Options:**
|
|
301
439
|
|
|
302
|
-
| Option | Type | Default | Description |
|
|
303
|
-
| ------------------- | --------- | ------- | --------------------------------------- |
|
|
304
|
-
| `routeName` | `string` | — | Target route name |
|
|
305
|
-
| `routeParams` | `Params` | `{}` | Route parameters |
|
|
306
|
-
| `routeOptions` | `object` | `{}` | Navigation options (replace, etc.) |
|
|
307
|
-
| `activeClassName` | `string` | — | Class added when route is active |
|
|
308
|
-
| `activeStrict` | `boolean` | `false` | Exact match only (no ancestor matching) |
|
|
309
|
-
| `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
|
|
440
|
+
| Option | Type | Required | Default | Description |
|
|
441
|
+
| ------------------- | --------- | -------- | ------- | --------------------------------------- |
|
|
442
|
+
| `routeName` | `string` | ★ yes | — | Target route name |
|
|
443
|
+
| `routeParams` | `Params` | no | `{}` | Route parameters |
|
|
444
|
+
| `routeOptions` | `object` | no | `{}` | Navigation options (replace, etc.) |
|
|
445
|
+
| `activeClassName` | `string` | no | — | Class added when route is active |
|
|
446
|
+
| `activeStrict` | `boolean` | no | `false` | Exact match only (no ancestor matching) |
|
|
447
|
+
| `ignoreQueryParams` | `boolean` | no | `true` | Query params don't affect active state |
|
|
310
448
|
|
|
311
449
|
The directive automatically sets `href` on `<a>` elements and adds `role="link"` + `tabindex="0"` to non-interactive elements for accessibility.
|
|
312
450
|
|
|
451
|
+
> **Must be used inside `<RouterProvider>`.** The directive calls `useRouter()` internally and will throw if the host element is mounted outside the provider tree. See CLAUDE.md gotcha *"use:link Requires useRouter Context"* for the failure mode and the canonical wrapping pattern.
|
|
452
|
+
|
|
453
|
+
**Accessor or object literal — both work.** The directive expects an accessor (`() => options`), and that's the form that gives you static-analysis friendliness across editors. Solid's `babel-preset-solid` also auto-wraps an object literal at compile time, so `use:link={{ routeName: "home" }}` is accepted too:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
// Accessor form (recommended — explicit, plays well with reactive values)
|
|
457
|
+
<a use:link={() => ({ routeName: "home" })}>Home</a>
|
|
458
|
+
|
|
459
|
+
// Object-literal form (accepted — babel-preset-solid wraps it for you)
|
|
460
|
+
<a use:link={{ routeName: "home" }}>Home</a>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Either way the directive captures the options **once** at init (see CLAUDE.md "use:link Options Are Captured Once"). For reactive route switching, use the `<Link>` component instead.
|
|
464
|
+
|
|
313
465
|
## Solid-Specific Patterns
|
|
314
466
|
|
|
315
467
|
### Accessors, Not Values
|
|
@@ -392,12 +544,15 @@ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
|
|
|
392
544
|
|
|
393
545
|
- [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) · [View Transitions](https://github.com/greydragon888/real-router/wiki/View-Transitions)
|
|
394
546
|
- [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition) · [useRouteExit](https://github.com/greydragon888/real-router/wiki/useRouteExit) · [useRouteEnter](https://github.com/greydragon888/real-router/wiki/useRouteEnter)
|
|
547
|
+
- Solid-specific store variants: [useRouteStore / useRouteNodeStore](https://github.com/greydragon888/real-router/wiki/Solid-Integration#store-based-granular-route-state) — `createStore` + `reconcile`, property-level reactivity
|
|
395
548
|
|
|
396
549
|
## Examples
|
|
397
550
|
|
|
398
|
-
|
|
551
|
+
20 runnable examples — each is a standalone Vite app. Run: `cd examples/web/solid/basic && pnpm dev`
|
|
552
|
+
|
|
553
|
+
[basic](../../examples/web/solid/basic) · [nested-routes](../../examples/web/solid/nested-routes) · [auth-guards](../../examples/web/solid/auth-guards) · [data-loading](../../examples/web/solid/data-loading) · [lazy-loading](../../examples/web/solid/lazy-loading) · [async-guards](../../examples/web/solid/async-guards) · [hash-routing](../../examples/web/solid/hash-routing) · [persistent-params](../../examples/web/solid/persistent-params) · [error-handling](../../examples/web/solid/error-handling) · [dynamic-routes](../../examples/web/solid/dynamic-routes) · [store-based-state](../../examples/web/solid/store-based-state) · [use-link-directive](../../examples/web/solid/use-link-directive) · [signal-primitives](../../examples/web/solid/signal-primitives) · [combined](../../examples/web/solid/combined) · [animation-examples](../../examples/web/solid/animation-examples) · [search-schema](../../examples/web/solid/search-schema)
|
|
399
554
|
|
|
400
|
-
|
|
555
|
+
Server-side rendering: [ssr](../../examples/web/solid/ssr-examples/ssr) · [ssr-streaming](../../examples/web/solid/ssr-examples/ssr-streaming) · [ssr-mixed](../../examples/web/solid/ssr-examples/ssr-mixed) · [ssg](../../examples/web/solid/ssr-examples/ssg)
|
|
401
556
|
|
|
402
557
|
## Related Packages
|
|
403
558
|
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -319,7 +319,26 @@ declare const RouteContext: solid_js.Context<Accessor<RouteState<_real_router_co
|
|
|
319
319
|
|
|
320
320
|
declare function createSignalFromSource<T>(source: RouterSource<T>): Accessor<T>;
|
|
321
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Bridges a `RouterSource<T>` into a Solid store (`createStore` + `reconcile`).
|
|
324
|
+
*
|
|
325
|
+
* Unlike `createSignalFromSource` (whole-value replacement via `===`), this
|
|
326
|
+
* bridge uses `reconcile` on every emit so **unchanged nested paths retain
|
|
327
|
+
* their object identity**. Components that read only `state.route.name` will
|
|
328
|
+
* not re-run when `state.route.params` changes — granular reactivity without
|
|
329
|
+
* manual memoisation.
|
|
330
|
+
*
|
|
331
|
+
* **Ownership**: calls `onCleanup` — must be called inside a reactive owner
|
|
332
|
+
* (component body or `createRoot`). Same contract as `createSignalFromSource`.
|
|
333
|
+
*
|
|
334
|
+
* **Lazy-source re-sync**: after `source.subscribe()`, a cached lazy source
|
|
335
|
+
* may reconcile its snapshot in `onFirstSubscribe`. The listener is not
|
|
336
|
+
* notified for that internal update, so we re-read immediately after
|
|
337
|
+
* subscribing (`setState(reconcile(source.getSnapshot()))`) — mirrors the
|
|
338
|
+
* same pattern in `createSignalFromSource`. `reconcile` is a no-op when the
|
|
339
|
+
* snapshot is structurally unchanged, so there is no spurious reactivity cost.
|
|
340
|
+
*/
|
|
322
341
|
declare function createStoreFromSource<T extends object>(source: RouterSource<T>): T;
|
|
323
342
|
|
|
324
343
|
export { Link, RouteContext, RouteView, RouterContext, RouterErrorBoundary, RouterProvider, createSignalFromSource, createStoreFromSource, link, useNavigator, useRoute, useRouteEnter, useRouteExit, useRouteNode, useRouteNodeStore, useRouteStore, useRouteUtils, useRouter, useRouterTransition };
|
|
325
|
-
export type { LinkDirectiveOptions, LinkProps, RouteEnterContext, RouteEnterHandler, RouteExitContext, RouteExitHandler, RouteState, MatchProps as RouteViewMatchProps, NotFoundProps as RouteViewNotFoundProps, RouteViewProps, SelfProps as RouteViewSelfProps, RouterErrorBoundaryProps, UseRouteEnterOptions, UseRouteExitOptions };
|
|
344
|
+
export type { LinkDirectiveOptions, LinkProps, RouteEnterContext, RouteEnterHandler, RouteExitContext, RouteExitHandler, RouteState, MatchProps as RouteViewMatchProps, NotFoundProps as RouteViewNotFoundProps, RouteViewProps, SelfProps as RouteViewSelfProps, RouterContextValue, RouterErrorBoundaryProps, UseRouteEnterOptions, UseRouteExitOptions };
|
package/dist/cjs/index.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { JSX, Accessor } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
interface ClientOnlyProps {
|
|
4
|
+
readonly children: JSX.Element;
|
|
5
|
+
readonly fallback?: JSX.Element;
|
|
6
|
+
}
|
|
7
|
+
declare function ClientOnly(props: ClientOnlyProps): JSX.Element;
|
|
8
|
+
|
|
9
|
+
interface ServerOnlyProps {
|
|
10
|
+
readonly children: JSX.Element;
|
|
11
|
+
readonly fallback?: JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
declare function ServerOnly(props: ServerOnlyProps): JSX.Element;
|
|
14
|
+
|
|
15
|
+
interface AwaitProps<T> {
|
|
16
|
+
/** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Render the resolved value. Surrounding `<Suspense>` shows fallback while
|
|
19
|
+
* pending; rejection bubbles through Solid's `<ErrorBoundary>`. */
|
|
20
|
+
readonly children: (value: T) => JSX.Element;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads `useDeferred(name)` and hands the resolved value to the render-prop.
|
|
24
|
+
* Wraps the deferred promise in `createResource` so Solid's reactivity tracks
|
|
25
|
+
* resolution and `<Suspense>` gets the standard suspend signal.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Streamed fallback={<Spinner />}>
|
|
29
|
+
* <Await<Review[]> name="reviews">
|
|
30
|
+
* {(reviews) => <ReviewList items={reviews} />}
|
|
31
|
+
* </Await>
|
|
32
|
+
* </Streamed>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Implementation: returns a Solid accessor (function child) that reads
|
|
36
|
+
* `resource()` — this both (a) triggers `<Suspense>` suspension while pending
|
|
37
|
+
* and (b) re-throws on `errored` for the nearest `<ErrorBoundary>` to catch.
|
|
38
|
+
* The render-prop is gated on `resource.state === "ready"` rather than on
|
|
39
|
+
* truthiness so falsy resolved values (`0`, `false`, `null`, `""`) still
|
|
40
|
+
* reach `props.children`.
|
|
41
|
+
*/
|
|
42
|
+
declare function Await<T = unknown>(props: AwaitProps<T>): JSX.Element;
|
|
43
|
+
|
|
44
|
+
interface StreamedProps {
|
|
45
|
+
/** Shown while any descendant `<Await>` / `createResource` suspends. */
|
|
46
|
+
readonly fallback: JSX.Element;
|
|
47
|
+
readonly children: JSX.Element;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Cross-adapter alias for Solid's `<Suspense fallback={…}>`. Symmetric naming
|
|
51
|
+
* with the React/Preact/Svelte/Vue/Angular `<Streamed>` components — pick
|
|
52
|
+
* `<Streamed>` for cross-framework consistency, or use Solid's native
|
|
53
|
+
* `<Suspense>` directly when team conventions prefer that.
|
|
54
|
+
*
|
|
55
|
+
* Solid's `<Suspense>` is a built-in primitive; out-of-order resolution +
|
|
56
|
+
* splice scripts during `renderToStream` are part of the runtime. See
|
|
57
|
+
* Solid's SSR docs for the wire-format details.
|
|
58
|
+
*/
|
|
59
|
+
declare function Streamed(props: StreamedProps): JSX.Element;
|
|
60
|
+
|
|
61
|
+
interface HttpStatusCodeProps {
|
|
62
|
+
/** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */
|
|
63
|
+
readonly code: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render-time HTTP status declaration. Mount inside a route component (typical
|
|
67
|
+
* use case: a glob `*` route's NotFound page) when the status is decided by
|
|
68
|
+
* the rendered tree rather than a loader.
|
|
69
|
+
*
|
|
70
|
+
* Writes `code` to the nearest `<HttpStatusProvider>`'s sink during render and
|
|
71
|
+
* returns `null`. With no provider mounted (the standard client-side case)
|
|
72
|
+
* the component is a silent no-op — same component tree hydrates without
|
|
73
|
+
* touching the DOM or warning about mismatches.
|
|
74
|
+
*
|
|
75
|
+
* Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
|
|
76
|
+
* working as before; this component covers render-time decisions only.
|
|
77
|
+
*
|
|
78
|
+
* Last write wins when several `<HttpStatusCode />` instances mount in the
|
|
79
|
+
* same render pass — sink reflects the last component that ran.
|
|
80
|
+
*
|
|
81
|
+
* ```tsx
|
|
82
|
+
* // entry-server.tsx
|
|
83
|
+
* import { renderToString } from "solid-js/web";
|
|
84
|
+
* import { createHttpStatusSink, HttpStatusProvider } from "@real-router/solid/ssr";
|
|
85
|
+
*
|
|
86
|
+
* const sink = createHttpStatusSink();
|
|
87
|
+
* const html = renderToString(() => (
|
|
88
|
+
* <HttpStatusProvider sink={sink}>
|
|
89
|
+
* <RouterProvider router={router}>
|
|
90
|
+
* <App />
|
|
91
|
+
* </RouterProvider>
|
|
92
|
+
* </HttpStatusProvider>
|
|
93
|
+
* ));
|
|
94
|
+
* response.status(sink.code ?? 200).send(html);
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* **Streaming SSR (`renderToStream`):** the response status MUST be sent
|
|
98
|
+
* before the first body byte flushes. If `<HttpStatusCode />` is mounted
|
|
99
|
+
* inside a late-resolving `<Suspense>` boundary, the sink write may happen
|
|
100
|
+
* AFTER the headers are already on the wire — the override is then lost.
|
|
101
|
+
* Mount the component in the shell (above every `<Suspense>` that could
|
|
102
|
+
* delay it), or use `renderToStringAsync` (single-shot, awaits all Suspense
|
|
103
|
+
* before returning HTML).
|
|
104
|
+
*
|
|
105
|
+
* **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
|
|
106
|
+
* `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
|
|
107
|
+
* dropped connection, not silent corruption. Pass a real HTTP status integer
|
|
108
|
+
* (commonly 4xx/5xx; 100-999 is what Node accepts).
|
|
109
|
+
*/
|
|
110
|
+
declare function HttpStatusCode(props: HttpStatusCodeProps): JSX.Element;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Render-scoped HTTP status sink. Created per request on the server, passed to
|
|
114
|
+
* `<HttpStatusProvider sink={...}>`, and read after `renderToString` /
|
|
115
|
+
* `renderToStream` to apply the value to the HTTP response.
|
|
116
|
+
*
|
|
117
|
+
* Last write wins: if the rendered tree mounts more than one
|
|
118
|
+
* `<HttpStatusCode />`, the value reflects the last component that ran during
|
|
119
|
+
* the render pass.
|
|
120
|
+
*
|
|
121
|
+
* No-op on the client — `<HttpStatusCode />` reads the optional context and
|
|
122
|
+
* skips the write when no provider is mounted, so the same component tree can
|
|
123
|
+
* be hydrated without changing behaviour.
|
|
124
|
+
*
|
|
125
|
+
* Constraints:
|
|
126
|
+
* - **Per-request only.** Don't share a sink across requests; the rendered
|
|
127
|
+
* tree mutates `code` in place. Module-level singletons leak status
|
|
128
|
+
* between concurrent requests.
|
|
129
|
+
* - **Don't `Object.freeze` the sink.** The component writes to `.code`;
|
|
130
|
+
* freezing makes the assignment throw under ESM strict mode.
|
|
131
|
+
* - **Hydration symmetry:** mount `<HttpStatusProvider>` on both server and
|
|
132
|
+
* client (with a throwaway client sink). Solid emits `data-hk` markers
|
|
133
|
+
* per component boundary; an extra provider on one side desyncs the
|
|
134
|
+
* counter and breaks the hydration walker.
|
|
135
|
+
*/
|
|
136
|
+
interface HttpStatusSink {
|
|
137
|
+
code: number | undefined;
|
|
138
|
+
}
|
|
139
|
+
declare function createHttpStatusSink(): HttpStatusSink;
|
|
140
|
+
|
|
141
|
+
interface HttpStatusProviderProps {
|
|
142
|
+
readonly sink: HttpStatusSink;
|
|
143
|
+
readonly children: JSX.Element;
|
|
144
|
+
}
|
|
145
|
+
declare function HttpStatusProvider(props: HttpStatusProviderProps): JSX.Element;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
|
|
149
|
+
* inside an SSR data loader.
|
|
150
|
+
*
|
|
151
|
+
* Returns a Solid `Accessor<Promise<T>>` so the value tracks the active route
|
|
152
|
+
* — re-reading on navigation picks up the new state's deferred map. Wrap with
|
|
153
|
+
* `<Await name="key">{(value) => …}</Await>` (this package), which builds on
|
|
154
|
+
* `createResource` + `<Suspense>` for native Solid streaming.
|
|
155
|
+
*
|
|
156
|
+
* Returns a forever-pending promise when the key is missing — surfaces
|
|
157
|
+
* loader/consumer key drift as a visible Suspense fallback rather than a
|
|
158
|
+
* silent runtime error.
|
|
159
|
+
*/
|
|
160
|
+
declare function useDeferred<T = unknown>(key: string): Accessor<Promise<T>>;
|
|
161
|
+
|
|
162
|
+
export { Await, ClientOnly, HttpStatusCode, HttpStatusProvider, ServerOnly, Streamed, createHttpStatusSink, useDeferred };
|
|
163
|
+
export type { AwaitProps, ClientOnlyProps, HttpStatusCodeProps, HttpStatusProviderProps, HttpStatusSink, ServerOnlyProps, StreamedProps };
|