@solidjs/router 2.0.0-next.25 → 2.0.0-next.27
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 +119 -69
- package/dist/data/query.js +36 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +228 -25
- package/dist/index.jsx +1 -0
- package/dist/routers/components.jsx +4 -0
- package/dist/routing.js +39 -18
- package/dist/server.js +6 -0
- package/dist/serverRouteComponent.d.ts +19 -0
- package/dist/serverRouteComponent.js +79 -0
- package/dist/serverRouteShared.d.ts +33 -0
- package/dist/serverRouteShared.js +48 -0
- package/dist/types.d.ts +27 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
**Solid Router** brings fine-grained reactivity to route navigation. Routes are config objects — the single source of truth for matching
|
|
13
|
+
**Solid Router** brings fine-grained reactivity to route navigation. Routes are config objects — the single source of truth for matching _and_ types — and the router upgrades HTML's own interaction verbs instead of wrapping them: `<a href={path}>` and `<form action={action}>` carry typed, URL-addressable values on real platform elements, intercepted by delegation, decorated with a shared attribute vocabulary, and fully functional without JavaScript.
|
|
14
14
|
|
|
15
15
|
Explore the official [documentation](https://docs.solidjs.com/solid-router) for detailed guides and examples.
|
|
16
16
|
|
|
@@ -36,6 +36,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
36
36
|
- [Multiple Paths](#multiple-paths)
|
|
37
37
|
- [Nested Routes](#nested-routes)
|
|
38
38
|
- [Lazy Route Subtrees](#lazy-route-subtrees)
|
|
39
|
+
- [Server Component Routes (experimental)](#server-component-routes-experimental)
|
|
39
40
|
- [File-System Routes](#file-system-routes)
|
|
40
41
|
- [Typed Paths](#typed-paths)
|
|
41
42
|
- [Links](#links)
|
|
@@ -89,7 +90,13 @@ When a tree is composed across files (feature subtrees), wrap the extracted arra
|
|
|
89
90
|
```tsx
|
|
90
91
|
// features/admin/routes.ts
|
|
91
92
|
export const adminRoutes = defineRoutes([
|
|
92
|
-
{
|
|
93
|
+
{
|
|
94
|
+
path: "/admin",
|
|
95
|
+
component: Admin,
|
|
96
|
+
children: [
|
|
97
|
+
/* ... */
|
|
98
|
+
]
|
|
99
|
+
}
|
|
93
100
|
]);
|
|
94
101
|
|
|
95
102
|
// app/router.ts
|
|
@@ -138,19 +145,19 @@ The API splits across two surfaces, and the line between them is precise: **coul
|
|
|
138
145
|
|
|
139
146
|
The instance is shared — one module-level object serving every mount, every request, every test. It is deliberately non-stateful (on the server there are many "current locations" at once), so it carries only the app's static routing vocabulary. Hooks read the live session from context.
|
|
140
147
|
|
|
141
|
-
| Instance — facts about the
|
|
142
|
-
|
|
|
143
|
-
| `paths` — how to spell URLs
|
|
144
|
-
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm
|
|
145
|
-
| `routes`, `config` — what exists
|
|
148
|
+
| Instance — facts about the _app_ | Hooks — facts about the _session_ |
|
|
149
|
+
| ------------------------------------ | ----------------------------------------------------------------- |
|
|
150
|
+
| `paths` — how to spell URLs | `useLocation`, `useParams` — where am I |
|
|
151
|
+
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm |
|
|
152
|
+
| `routes`, `config` — what exists | `useIsRouting`, `useRouteMatches`, `useSearchParams` — live state |
|
|
146
153
|
|
|
147
154
|
They compose as noun and verb — the instance supplies a typed URL, the hook acts on the current session:
|
|
148
155
|
|
|
149
156
|
```tsx
|
|
150
157
|
const navigate = useNavigate();
|
|
151
|
-
navigate(paths.users(2));
|
|
158
|
+
navigate(paths.users(2)); // verb(noun)
|
|
152
159
|
|
|
153
|
-
const params = useParams(paths.users);
|
|
160
|
+
const params = useParams(paths.users); // hook, typed by the instance
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
**Hooks are the default; import the router only when you need typed URLs or matching outside a render.** Components that only read their session (params, location, string-path navigation) never need the instance — which also means component files don't form import cycles with the router module that references them in its config.
|
|
@@ -159,17 +166,17 @@ const params = useParams(paths.users); // hook, typed by the instance
|
|
|
159
166
|
|
|
160
167
|
A route definition supports:
|
|
161
168
|
|
|
162
|
-
| key | type
|
|
163
|
-
| -------------- |
|
|
164
|
-
| `path` | `string \| string[]`
|
|
165
|
-
| `component` | `Component`
|
|
169
|
+
| key | type | description |
|
|
170
|
+
| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
171
|
+
| `path` | `string \| string[]` | Path partial for this route segment |
|
|
172
|
+
| `component` | `Component` | Component rendered for the matched segment |
|
|
166
173
|
| `children` | `RouteDefinition \| RouteDefinition[] \| () => Promise<...>` | Nested route definitions, or a thunk for a [lazy subtree](#lazy-route-subtrees) |
|
|
167
|
-
| `preload` | `RoutePreloadFunc`
|
|
168
|
-
| `matchFilters` | `MatchFilters`
|
|
169
|
-
| `search` | `StandardSchemaV1`
|
|
170
|
-
| `info` | `Record<string, any>`
|
|
174
|
+
| `preload` | `RoutePreloadFunc` | Called on preload intent (hover/focus) and navigation |
|
|
175
|
+
| `matchFilters` | `MatchFilters` | Additional constraints for matching parameters |
|
|
176
|
+
| `search` | `StandardSchemaV1` | Search-param validator; its types flow into `paths` and hooks |
|
|
177
|
+
| `info` | `Record<string, any>` | Arbitrary metadata, readable via `useRouteMatches` |
|
|
171
178
|
|
|
172
|
-
The tree is **immutable and there is one router per app** — that's what makes `paths` and the typed hooks truthful, it lets matching compile once and be shared by every mount, request, and `match()` call, and it means delegation, link state, and preloading all have a single owner. Compose large apps by spreading subtrees into the config (see `defineRoutes` above); mounting a router inside another router is not supported (nested `<Routes>` has been gone since 0.10) and warns in development. Sections whose
|
|
179
|
+
The tree is **immutable and there is one router per app** — that's what makes `paths` and the typed hooks truthful, it lets matching compile once and be shared by every mount, request, and `match()` call, and it means delegation, link state, and preloading all have a single owner. Compose large apps by spreading subtrees into the config (see `defineRoutes` above); mounting a router inside another router is not supported (nested `<Routes>` has been gone since 0.10) and warns in development. Sections whose _code_ shouldn't load up front are [lazy route subtrees](#lazy-route-subtrees) — still one tree, still typed.
|
|
173
180
|
|
|
174
181
|
### Dynamic Routes
|
|
175
182
|
|
|
@@ -204,8 +211,8 @@ const story = defineRoute({
|
|
|
204
211
|
preload: ({ params }) => getStory(params.id), // params.id: string
|
|
205
212
|
component: props => (
|
|
206
213
|
<Story
|
|
207
|
-
id={props.params.id}
|
|
208
|
-
tab={props.params.tab}
|
|
214
|
+
id={props.params.id} // string — the pattern guarantees it
|
|
215
|
+
tab={props.params.tab} // string | undefined — optional param
|
|
209
216
|
/>
|
|
210
217
|
)
|
|
211
218
|
});
|
|
@@ -242,8 +249,8 @@ Each parameter can be validated with a `MatchFilter` — an enum array, a regex,
|
|
|
242
249
|
import { int, type MatchFilters } from "@solidjs/router";
|
|
243
250
|
|
|
244
251
|
const filters: MatchFilters = {
|
|
245
|
-
parent: ["mom", "dad"],
|
|
246
|
-
id: /^\d+$/,
|
|
252
|
+
parent: ["mom", "dad"], // enum values
|
|
253
|
+
id: /^\d+$/, // only numbers
|
|
247
254
|
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
248
255
|
};
|
|
249
256
|
|
|
@@ -254,7 +261,7 @@ const routes = defineRoutes([
|
|
|
254
261
|
|
|
255
262
|
So `/users/mom/123/contact.html` matches, while `/users/aunt/123/contact.html` (invalid `parent`) and `/users/mom/me/contact.html` (non-numeric `id`) don't.
|
|
256
263
|
|
|
257
|
-
The built-in `int` filter is
|
|
264
|
+
The built-in `int` filter is _typed_: it constrains matching to integers at runtime and types the param as `number` at `paths` callsites:
|
|
258
265
|
|
|
259
266
|
```tsx
|
|
260
267
|
{ path: "/users/:id", matchFilters: { id: int }, component: User }
|
|
@@ -362,6 +369,38 @@ The import only fires when something needs the subtree — hovering a link into
|
|
|
362
369
|
|
|
363
370
|
Resolution is cached per thunk and append-only: the tree never changes shape after a subtree lands, it just gets more specific. Keep thunks deterministic — `() => import(...)` — rather than switching tables on runtime state.
|
|
364
371
|
|
|
372
|
+
### Server Component Routes (experimental)
|
|
373
|
+
|
|
374
|
+
Solid's experimental [server components](https://github.com/solidjs/solid/blob/main/documentation/solid-2.0/11-server-components.md) are `"use server"` functions that return a component. `serverRouteComponent` uses one as a route's `component` directly — the router owns the URL → call translation a client wrapper used to spell out:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
// views.tsx
|
|
378
|
+
"use server";
|
|
379
|
+
import type { ServerRouteArgs } from "@solidjs/router";
|
|
380
|
+
|
|
381
|
+
export async function storyView({ params }: ServerRouteArgs<{ id: string }>) {
|
|
382
|
+
const story = await db.stories.get(params.id);
|
|
383
|
+
return props => (
|
|
384
|
+
<article>
|
|
385
|
+
<h1>{story.title}</h1>
|
|
386
|
+
{props.children}
|
|
387
|
+
</article>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// routes.ts
|
|
392
|
+
defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
The function is called with **derived** arguments, not a live location — the call's `(function, arguments)` address keys both the query cache and the frame store, so the args name exactly what the route depends on:
|
|
396
|
+
|
|
397
|
+
- `params`: the params this route's pattern (and its ancestors') declares — never a child's, so a layout does not refetch when a leaf param changes. `defineRoute` checks them against the pattern.
|
|
398
|
+
- `search`: the validated output of the route's [`search` schema](#typed-search-params), only when one is declared. Otherwise `undefined`, and the route never tracks the query string.
|
|
399
|
+
|
|
400
|
+
The router mounts the resolved component with the outlet as `children`, so a server component can be a layout. The call is wrapped in `query` (keyed by the function id): link intent warms the same entry the render reads, actions revalidate it, and the [single-flight collector](#server-integration) reproduces it so a mutation's response carries the route's fresh markup. Argument changes deliver into the mounted boundary — it morphs in place rather than remounting.
|
|
401
|
+
|
|
402
|
+
`children` is the only client position the router fills, and the helper's type says so: a server component that requires other props — event handlers, refs, named slots — is rejected. Those come from the client, so that route has a client half; write it as an ordinary route component around `dynamic()`. Interaction that lives on the server — form posts to server actions via `action={addTodo.url}` — needs no client component at all.
|
|
403
|
+
|
|
365
404
|
### File-System Routes
|
|
366
405
|
|
|
367
406
|
The `@solidjs/router/fs` adapter turns a `file-routes` manifest into route definitions — the app imports the virtual module, the adapter maps it:
|
|
@@ -387,7 +426,7 @@ export const route = defineFileRoute("/blog/:id", {
|
|
|
387
426
|
|
|
388
427
|
export default function Post(props: RouteProps<typeof route>) {
|
|
389
428
|
props.params.id; // string
|
|
390
|
-
props.data;
|
|
429
|
+
props.data; // ReturnType of the preload above
|
|
391
430
|
}
|
|
392
431
|
```
|
|
393
432
|
|
|
@@ -398,11 +437,11 @@ The pattern string is a typing witness — at runtime the manifest's path (from
|
|
|
398
437
|
`paths` is a proxy inferred from the route tree. Property access descends into static segments, calls bind params, and it mirrors URL anatomy — params, then a search object, then a hash string:
|
|
399
438
|
|
|
400
439
|
```tsx
|
|
401
|
-
paths.users(123)
|
|
402
|
-
paths.users(2).settings
|
|
403
|
-
paths.users(2, { tab: "x" }, "comments")
|
|
404
|
-
paths.about()
|
|
405
|
-
paths()
|
|
440
|
+
paths.users(123); // ok — matchFilters flow into the callsite
|
|
441
|
+
paths.users(2).settings; // chainable into children
|
|
442
|
+
paths.users(2, { tab: "x" }, "comments"); // "/users/2?tab=x#comments"
|
|
443
|
+
paths.about(); // zero-arg/search calls terminate to a plain string
|
|
444
|
+
paths(); // "/" — the root
|
|
406
445
|
```
|
|
407
446
|
|
|
408
447
|
Every node coerces via `toString`, so nodes drop straight into `href`, `navigate()`, and `redirect()` without explicit termination. Accessing a segment that doesn't exist in the tree, or binding a param with the wrong type, is a compile error.
|
|
@@ -413,14 +452,14 @@ There is no link component. Use `<a>`; the router intercepts same-origin clicks
|
|
|
413
452
|
|
|
414
453
|
Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
|
|
415
454
|
|
|
416
|
-
| attribute | description
|
|
417
|
-
| ---------- |
|
|
418
|
-
| `replace` | Replace the history entry instead of pushing
|
|
419
|
-
| `noscroll` | Turn off scrolling to the top after navigation
|
|
455
|
+
| attribute | description |
|
|
456
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------- |
|
|
457
|
+
| `replace` | Replace the history entry instead of pushing |
|
|
458
|
+
| `noscroll` | Turn off scrolling to the top after navigation |
|
|
420
459
|
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack |
|
|
421
|
-
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading
|
|
422
|
-
| `link` | Marks a router link when `explicitLinks` is enabled
|
|
423
|
-
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling
|
|
460
|
+
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading |
|
|
461
|
+
| `link` | Marks a router link when `explicitLinks` is enabled |
|
|
462
|
+
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling |
|
|
424
463
|
|
|
425
464
|
```tsx
|
|
426
465
|
<a href={paths.login} replace>Log in</a>
|
|
@@ -431,9 +470,15 @@ Behavior modifiers are attributes, so they work identically in client, server-re
|
|
|
431
470
|
Active and pending state is styled with CSS — one vocabulary for every kind of link:
|
|
432
471
|
|
|
433
472
|
```css
|
|
434
|
-
nav a[aria-current="page"] {
|
|
435
|
-
|
|
436
|
-
|
|
473
|
+
nav a[aria-current="page"] {
|
|
474
|
+
font-weight: 600;
|
|
475
|
+
} /* exact match */
|
|
476
|
+
nav a[data-active] {
|
|
477
|
+
color: var(--accent);
|
|
478
|
+
} /* exact or prefix match */
|
|
479
|
+
a[data-pending] {
|
|
480
|
+
opacity: 0.6;
|
|
481
|
+
} /* target of in-flight navigation */
|
|
437
482
|
```
|
|
438
483
|
|
|
439
484
|
(The root path only ever matches exactly, so `href={paths()}` doesn't light up on every page.)
|
|
@@ -471,10 +516,10 @@ const routes = defineRoutes([{ path: "/users/:id", component: User, preload: pre
|
|
|
471
516
|
|
|
472
517
|
The preload function receives:
|
|
473
518
|
|
|
474
|
-
| key | type
|
|
475
|
-
| -------- |
|
|
476
|
-
| params | object
|
|
477
|
-
| location | `{ pathname, search, hash, query, state, key }`
|
|
519
|
+
| key | type | description |
|
|
520
|
+
| -------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
521
|
+
| params | object | The route parameters (same value as `useParams()` inside the route component) |
|
|
522
|
+
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation)) |
|
|
478
523
|
| intent | `"initial" \| "navigate" \| "native" \| "preload"` | Why this is being called: `initial` — first render; `navigate` — router navigation; `native` — browser back/forward; `preload` — link hover/focus, not navigating |
|
|
479
524
|
|
|
480
525
|
The factory-level `preload` option is the app-wide counterpart: it runs once per mount/request with the merged params of every match, and its result reaches the root render-prop as `props.data`.
|
|
@@ -513,12 +558,14 @@ const todos = createProjection(() => getTodos(), []);
|
|
|
513
558
|
Keys support targeted invalidation:
|
|
514
559
|
|
|
515
560
|
```ts
|
|
516
|
-
getUser.key;
|
|
561
|
+
getUser.key; // "users"
|
|
517
562
|
getUser.keyFor(5); // "users[5]"
|
|
518
563
|
```
|
|
519
564
|
|
|
520
565
|
Revalidate with the `revalidate` export or by setting `revalidate` keys on action responses — the whole key invalidates every entry for the query, `keyFor` invalidates one.
|
|
521
566
|
|
|
567
|
+
A query may also redirect — a guard read that throws or returns `redirect()` (from `@solidjs/web`) navigates instead of resolving: same-origin targets navigate softly with `replace`, other origins leave the document, any `revalidate` keys on the response invalidate first, and the read itself stays pending so nothing renders the redirect as data. This holds for `"use server"` queries too, where the transport carries the redirect to the client rather than letting `fetch` follow it.
|
|
568
|
+
|
|
522
569
|
### `liveQuery` (experimental)
|
|
523
570
|
|
|
524
571
|
`query`'s live sibling: a keyed query over a value-shaped stream. The function is an async iterable (typically an async generator server function) whose yields are successive **values of one logical query** — each yield is the current state, not an event — with the contract that it re-yields current state on every invocation:
|
|
@@ -553,7 +600,7 @@ The callable carries the `query` conventions (`key`, `keyFor`) plus a reactive `
|
|
|
553
600
|
|
|
554
601
|
### `action`
|
|
555
602
|
|
|
556
|
-
A router action is
|
|
603
|
+
A router action is _an action with a URL_ — Solid's mutation primitive plus URL addressability, submission tracking, and response handling. Data helpers come from the router; response helpers (`redirect`, `reload`) come from `@solidjs/web` — they're protocol-level and work without the router:
|
|
557
604
|
|
|
558
605
|
```tsx
|
|
559
606
|
import { action } from "@solidjs/router";
|
|
@@ -578,7 +625,10 @@ const updateUser = action(async (form: FormData) => {
|
|
|
578
625
|
Actions only work with POST requests, so put `method="post"` on your form. Submitting forms get `aria-busy="true"` automatically while the action (including its revalidation) is in flight — the same CSS story as links:
|
|
579
626
|
|
|
580
627
|
```css
|
|
581
|
-
form[aria-busy] button {
|
|
628
|
+
form[aria-busy] button {
|
|
629
|
+
pointer-events: none;
|
|
630
|
+
opacity: 0.6;
|
|
631
|
+
}
|
|
582
632
|
```
|
|
583
633
|
|
|
584
634
|
Forms work without JavaScript: a real POST, a redirect back, and the result seeded into submission state through a one-shot flash cookie. Single-flight mutations are on by default — the mutation response carries the refreshed route data in the same round trip.
|
|
@@ -662,7 +712,7 @@ const routes = defineRoutes([
|
|
|
662
712
|
|
|
663
713
|
```tsx
|
|
664
714
|
const [search, setSearch] = useSearchParams(paths.search);
|
|
665
|
-
search.page;
|
|
715
|
+
search.page; // number (parsed, not "2")
|
|
666
716
|
setSearch({ page: search.page + 1 }); // typed setter
|
|
667
717
|
|
|
668
718
|
<a href={paths.search({ q: "solid", page: 2 })}>Search</a>; // typed builder
|
|
@@ -676,26 +726,26 @@ Without a schema, `useSearchParams()` behaves as before: raw string values, merg
|
|
|
676
726
|
createRouter(config);
|
|
677
727
|
```
|
|
678
728
|
|
|
679
|
-
| option | type
|
|
680
|
-
| --------------- |
|
|
681
|
-
| `routes` | `RouteDefinition[]`
|
|
682
|
-
| `base` | `string`
|
|
683
|
-
| `preload` | `RoutePreloadFunc`
|
|
684
|
-
| `history` | `RouterHistory`
|
|
685
|
-
| `singleFlight` | `boolean`
|
|
686
|
-
| `actionBase` | `string`
|
|
687
|
-
| `preloadLinks` | `boolean`
|
|
688
|
-
| `explicitLinks` | `boolean`
|
|
689
|
-
| `transformUrl` | `(url: string) => string`
|
|
729
|
+
| option | type | description |
|
|
730
|
+
| --------------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
731
|
+
| `routes` | `RouteDefinition[]` | The route tree — inline arrays infer literally; wrap extracted trees in `defineRoutes` |
|
|
732
|
+
| `base` | `string` | Base url to use for matching routes |
|
|
733
|
+
| `preload` | `RoutePreloadFunc` | App-wide preload: once per mount/request, result reaches the root render-prop as `props.data` |
|
|
734
|
+
| `history` | `RouterHistory` | History adapter; defaults to browser history on the client and the request URL on the server |
|
|
735
|
+
| `singleFlight` | `boolean` | Single-flight mutations, default `true` |
|
|
736
|
+
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
737
|
+
| `preloadLinks` | `boolean` | Preload route code/data on link hover and focus, default `true` |
|
|
738
|
+
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
739
|
+
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
690
740
|
|
|
691
741
|
The returned instance is the provider component and carries the static surface:
|
|
692
742
|
|
|
693
|
-
| member
|
|
694
|
-
|
|
|
695
|
-
| `paths`
|
|
696
|
-
| `match`
|
|
697
|
-
| `routes`
|
|
698
|
-
| `config`
|
|
743
|
+
| member | description |
|
|
744
|
+
| -------- | ------------------------------------------------------------------------------------------------- |
|
|
745
|
+
| `paths` | The [typed path proxy](#typed-paths) |
|
|
746
|
+
| `match` | Pure matching against an arbitrary URL — no rendering or request context; root→leaf, `[]` if none |
|
|
747
|
+
| `routes` | The config tree |
|
|
748
|
+
| `config` | The full config — lets server integrations consume the instance directly |
|
|
699
749
|
|
|
700
750
|
## Router Primitives
|
|
701
751
|
|
|
@@ -706,7 +756,7 @@ Hooks read the live session off router context.
|
|
|
706
756
|
Retrieves a reactive, store-like object of the current route's path parameters. Pass a paths node for typing:
|
|
707
757
|
|
|
708
758
|
```tsx
|
|
709
|
-
const params = useParams();
|
|
759
|
+
const params = useParams(); // Params (strings)
|
|
710
760
|
const params = useParams(paths.users); // { id: string } — typed from the tree
|
|
711
761
|
```
|
|
712
762
|
|
|
@@ -754,7 +804,7 @@ In Solid's dev and observe builds the router also declares every navigation to t
|
|
|
754
804
|
|
|
755
805
|
### useMatch
|
|
756
806
|
|
|
757
|
-
Tests a path
|
|
807
|
+
Tests a path _pattern you supply_ against the current location; returns a memo of match information or `undefined`. It never consults the route tree — the pattern doesn't have to correspond to a defined route. The match's `params` are typed from the pattern, and a typed path node works too (a concrete URL — useful for "am I here" checks):
|
|
758
808
|
|
|
759
809
|
```tsx
|
|
760
810
|
const match = useMatch(() => "/admin/*rest");
|
|
@@ -766,7 +816,7 @@ const here = useMatch(() => paths.users(2));
|
|
|
766
816
|
|
|
767
817
|
### useRouteMatches
|
|
768
818
|
|
|
769
|
-
Returns an accessor of the router's
|
|
819
|
+
Returns an accessor of the router's _resolved_ matches for the current location — the chain of route definitions producing the current render, outermost first. This is the counterpart to `useMatch`: one reflects the route tree, the other tests a pattern. Useful for reading `info` metadata:
|
|
770
820
|
|
|
771
821
|
```tsx
|
|
772
822
|
const matches = useRouteMatches();
|
|
@@ -888,7 +938,7 @@ This guide maps from the stable 0.x releases (Solid 1). 1.0 removes the componen
|
|
|
888
938
|
<Router root={App}>
|
|
889
939
|
<Route path="/users" component={Users} />
|
|
890
940
|
<Route path="/users/:id" component={User} />
|
|
891
|
-
</Router
|
|
941
|
+
</Router>;
|
|
892
942
|
|
|
893
943
|
// 1.0
|
|
894
944
|
const Router = createRouter({
|
|
@@ -898,7 +948,7 @@ const Router = createRouter({
|
|
|
898
948
|
]
|
|
899
949
|
});
|
|
900
950
|
|
|
901
|
-
<Router>{props => <App {...props} />}</Router
|
|
951
|
+
<Router>{props => <App {...props} />}</Router>;
|
|
902
952
|
```
|
|
903
953
|
|
|
904
954
|
- `<HashRouter>` → `createRouter({ routes, history: hashHistory() })`
|
package/dist/data/query.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
|
|
2
2
|
// Everything server-function-shaped comes off the CORE entry: detection
|
|
3
3
|
// (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
|
|
4
|
-
// the late-bound RPC seam (getServerFunctionRPC). The
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// the late-bound RPC seam (getServerFunctionRPC). The transport itself —
|
|
5
|
+
// the fetch RPC client + the seroval codec behind it — is deliberately NOT
|
|
6
|
+
// imported here: query() is in every router app's eager graph, and a static
|
|
7
|
+
// import of `decodeResponse` made every zero-server-function app ship
|
|
8
8
|
// ~9 KB gz of codec it could never invoke. The transport registers itself
|
|
9
9
|
// into the seam when a `'use server'` reference is created (compiled
|
|
10
10
|
// output, module scope), so by the time a server function can reach
|
|
11
11
|
// query() the seam is filled; plain-fetch apps read undefined forever.
|
|
12
12
|
import { getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, isServer, isServerFunction, REVALIDATE_HEADER } from "@solidjs/web";
|
|
13
|
+
// The redirect carrier's name and decoder are the exception: two pure,
|
|
14
|
+
// dependency-free bindings off a `sideEffects: false` entry, so they
|
|
15
|
+
// tree-shake to a few hundred bytes without dragging the codec in (the same
|
|
16
|
+
// bindings action.ts already imports statically).
|
|
17
|
+
import { decodeRedirectHeaderValue, REDIRECT_HEADER } from "@solidjs/web/server-functions";
|
|
13
18
|
import { useRouter, getIntent, getInPreloadFn } from "../routing.js";
|
|
14
19
|
const LocationHeader = "Location";
|
|
15
20
|
const PRELOAD_TIMEOUT = 5000;
|
|
@@ -62,7 +67,12 @@ export function revalidate(key, force = true) {
|
|
|
62
67
|
const now = Date.now();
|
|
63
68
|
cacheKeyOp(key, entry => {
|
|
64
69
|
force && (entry[0] = 0); //force cache miss
|
|
65
|
-
|
|
70
|
+
// retrigger live signals. The version is the entry's fetch stamp, and
|
|
71
|
+
// a signal write of an equal value is a no-op — an entry fetched within
|
|
72
|
+
// this same millisecond (a mount whose redirect lands before the clock
|
|
73
|
+
// ticks) would otherwise never be told to refetch, and the surviving
|
|
74
|
+
// consumer paints stale. A sweep must notify unconditionally.
|
|
75
|
+
entry[4][1](v => (v === now ? now + 1 : now));
|
|
66
76
|
});
|
|
67
77
|
const keys = key === undefined ? undefined : Array.isArray(key) ? key : [key];
|
|
68
78
|
for (const hook of revalidateHooks)
|
|
@@ -251,7 +261,27 @@ export function query(fn, name) {
|
|
|
251
261
|
e.response.headers.set(key, value);
|
|
252
262
|
}
|
|
253
263
|
}
|
|
254
|
-
|
|
264
|
+
let url = v.headers.get(LocationHeader);
|
|
265
|
+
// A `"use server"` redirect reaches a client-side read masked: the
|
|
266
|
+
// transport answers scripted callers with a 200, drops `Location`
|
|
267
|
+
// and carries "<status> <absolute-url>" in REDIRECT_HEADER instead.
|
|
268
|
+
// Decode it with the runtime's own reader, as action() does. The
|
|
269
|
+
// carrier arrives RESOLVED to an absolute url, so same-origin vs
|
|
270
|
+
// cross-origin is a real origin comparison: same-origin folds to
|
|
271
|
+
// a path the soft branch below navigates under the router, any
|
|
272
|
+
// other origin keeps its href and the document goes with it. This
|
|
273
|
+
// stays synchronous on purpose — navigate runs in the same tick as
|
|
274
|
+
// the `Location` branch would, so the transition semantics match.
|
|
275
|
+
if (url === null && !isServer && v.headers.has(REDIRECT_HEADER)) {
|
|
276
|
+
const carried = decodeRedirectHeaderValue(v.headers.get(REDIRECT_HEADER));
|
|
277
|
+
if (carried) {
|
|
278
|
+
const target = new URL(carried.url);
|
|
279
|
+
url =
|
|
280
|
+
target.origin === window.location.origin
|
|
281
|
+
? target.pathname + target.search + target.hash
|
|
282
|
+
: target.href;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
255
285
|
if (url !== null) {
|
|
256
286
|
// invalidate the redirect's revalidation keys before navigating so
|
|
257
287
|
// the destination's preloads see the miss and fetch fresh (#580 thread).
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate
|
|
|
23
23
|
export type { LinkState } from "./routing.js";
|
|
24
24
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
25
25
|
export { int } from "./paths.js";
|
|
26
|
+
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
26
27
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
27
28
|
export * from "./data/index.js";
|
|
28
|
-
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
|
29
|
+
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, ServerRouteArgs, ServerRouteFunction, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
|
|
2
|
-
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
3
|
-
import {
|
|
2
|
+
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER, dynamic } from '@solidjs/web';
|
|
3
|
+
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
|
|
5
5
|
|
|
6
6
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
@@ -517,6 +517,74 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
517
517
|
return node(normalizePath(base));
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// The light half of server component routes: the brand the router core reads
|
|
521
|
+
// off a route `component`, and the derivation of a route's call arguments.
|
|
522
|
+
// Deliberately free of `query`/`dynamic` — routing.ts imports this module,
|
|
523
|
+
// and routing.ts is in every app's eager graph. The heavy half (the helper
|
|
524
|
+
// that builds the branded component) lives in serverRouteComponent.ts and only
|
|
525
|
+
// enters a bundle when an app calls `serverRouteComponent()`.
|
|
526
|
+
|
|
527
|
+
const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* What `serverRouteComponent()` attaches to the route component it returns, and
|
|
531
|
+
* what the router core drives it through: `call` is the query-wrapped server
|
|
532
|
+
* function (preload and single-flight collection warm/collect through it),
|
|
533
|
+
* `render` mounts the resolved server component for the current args with
|
|
534
|
+
* the route's props (`children` is the outlet).
|
|
535
|
+
*/
|
|
536
|
+
|
|
537
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
538
|
+
function serverRouteOf(component) {
|
|
539
|
+
return typeof component === "function" ? component[SERVER_ROUTE] : undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
544
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
545
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
546
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
547
|
+
*/
|
|
548
|
+
function serverRouteArgs(route, params, query) {
|
|
549
|
+
const schema = route.key?.search;
|
|
550
|
+
let search = undefined;
|
|
551
|
+
if (schema) {
|
|
552
|
+
const outcome = schema["~standard"].validate({
|
|
553
|
+
...query
|
|
554
|
+
});
|
|
555
|
+
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
556
|
+
// Issues leave the raw values in place, matching useSearchParams: search
|
|
557
|
+
// strings are user input, so defaults belong in the schema itself.
|
|
558
|
+
search = outcome.issues ? {
|
|
559
|
+
...query
|
|
560
|
+
} : outcome.value;
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
params: {
|
|
564
|
+
...params
|
|
565
|
+
},
|
|
566
|
+
search
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function shallowEqual(a, b) {
|
|
570
|
+
if (a === b) return true;
|
|
571
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
572
|
+
const ka = Object.keys(a);
|
|
573
|
+
const kb = Object.keys(b);
|
|
574
|
+
if (ka.length !== kb.length) return false;
|
|
575
|
+
for (const k of ka) if (a[k] !== b[k]) return false;
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
581
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
582
|
+
* must not re-enter the source (and refetch).
|
|
583
|
+
*/
|
|
584
|
+
function serverRouteArgsEqual(a, b) {
|
|
585
|
+
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
586
|
+
}
|
|
587
|
+
|
|
520
588
|
const MAX_REDIRECTS = 100;
|
|
521
589
|
|
|
522
590
|
/**
|
|
@@ -1481,21 +1549,29 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1481
1549
|
} = matches[match];
|
|
1482
1550
|
route.component && route.component.preload && route.component.preload();
|
|
1483
1551
|
const {
|
|
1484
|
-
preload
|
|
1552
|
+
preload,
|
|
1553
|
+
component
|
|
1485
1554
|
} = route;
|
|
1486
1555
|
inPreloadFn = true;
|
|
1487
|
-
preloadData &&
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1556
|
+
preloadData && runWithOwner(getContext(), () => {
|
|
1557
|
+
const query = extractSearchParams(url);
|
|
1558
|
+
// A server component route's data IS its call: warm the same
|
|
1559
|
+
// query entry the render will read, under the same derived args.
|
|
1560
|
+
const server = serverRouteOf(component);
|
|
1561
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
1562
|
+
preload && preload({
|
|
1563
|
+
params,
|
|
1564
|
+
location: {
|
|
1565
|
+
pathname: url.pathname,
|
|
1566
|
+
search: url.search,
|
|
1567
|
+
hash: url.hash,
|
|
1568
|
+
query,
|
|
1569
|
+
state: null,
|
|
1570
|
+
key: ""
|
|
1571
|
+
},
|
|
1572
|
+
intent: "preload"
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1499
1575
|
inPreloadFn = false;
|
|
1500
1576
|
}
|
|
1501
1577
|
preloadIntent = prevIntent;
|
|
@@ -1551,14 +1627,30 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1551
1627
|
pattern,
|
|
1552
1628
|
params,
|
|
1553
1629
|
path,
|
|
1554
|
-
outlet: () =>
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1630
|
+
outlet: () => {
|
|
1631
|
+
if (!component) return outlet();
|
|
1632
|
+
const routeProps = {
|
|
1633
|
+
params,
|
|
1634
|
+
location,
|
|
1635
|
+
data,
|
|
1636
|
+
get children() {
|
|
1637
|
+
return outlet();
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
// A `serverRouteComponent()` route: the router derives the call from
|
|
1641
|
+
// the match — THIS level's params (never a child's) and the declared
|
|
1642
|
+
// search output — and mounts through the brand. Structural equality
|
|
1643
|
+
// keeps a navigation that leaves this level's args unchanged from
|
|
1644
|
+
// re-entering the call. See serverRouteComponent.ts.
|
|
1645
|
+
const server = serverRouteOf(component);
|
|
1646
|
+
if (server) {
|
|
1647
|
+
const args = createMemo(() => serverRouteArgs(match().route, match().params, location.query), {
|
|
1648
|
+
equals: serverRouteArgsEqual
|
|
1649
|
+
});
|
|
1650
|
+
return server.render(args, routeProps);
|
|
1560
1651
|
}
|
|
1561
|
-
|
|
1652
|
+
return createComponent(component, routeProps);
|
|
1653
|
+
},
|
|
1562
1654
|
resolvePath(to) {
|
|
1563
1655
|
return resolvePath(base.path(), to, path());
|
|
1564
1656
|
}
|
|
@@ -1717,6 +1809,9 @@ function dataOnly(event, routerState, branches) {
|
|
|
1717
1809
|
route,
|
|
1718
1810
|
params
|
|
1719
1811
|
} = matches[match];
|
|
1812
|
+
// a server component route's markup is collected like any query result
|
|
1813
|
+
const server = serverRouteOf(route.component);
|
|
1814
|
+
server && server.call(serverRouteArgs(route, params, routerState.location.query));
|
|
1720
1815
|
route.preload && route.preload({
|
|
1721
1816
|
params,
|
|
1722
1817
|
location: routerState.location,
|
|
@@ -2457,7 +2552,12 @@ function revalidate(key, force = true) {
|
|
|
2457
2552
|
const now = Date.now();
|
|
2458
2553
|
cacheKeyOp(key, entry => {
|
|
2459
2554
|
force && (entry[0] = 0); //force cache miss
|
|
2460
|
-
|
|
2555
|
+
// retrigger live signals. The version is the entry's fetch stamp, and
|
|
2556
|
+
// a signal write of an equal value is a no-op — an entry fetched within
|
|
2557
|
+
// this same millisecond (a mount whose redirect lands before the clock
|
|
2558
|
+
// ticks) would otherwise never be told to refetch, and the surviving
|
|
2559
|
+
// consumer paints stale. A sweep must notify unconditionally.
|
|
2560
|
+
entry[4][1](v => v === now ? now + 1 : now);
|
|
2461
2561
|
});
|
|
2462
2562
|
const keys = key === undefined ? undefined : Array.isArray(key) ? key : [key];
|
|
2463
2563
|
for (const hook of revalidateHooks) hook(keys, force);
|
|
@@ -2619,7 +2719,25 @@ function query(fn, name) {
|
|
|
2619
2719
|
if (key == "set-cookie") e.response.headers.append("set-cookie", value);else e.response.headers.set(key, value);
|
|
2620
2720
|
}
|
|
2621
2721
|
}
|
|
2622
|
-
|
|
2722
|
+
let url = v.headers.get(LocationHeader);
|
|
2723
|
+
|
|
2724
|
+
// A `"use server"` redirect reaches a client-side read masked: the
|
|
2725
|
+
// transport answers scripted callers with a 200, drops `Location`
|
|
2726
|
+
// and carries "<status> <absolute-url>" in REDIRECT_HEADER instead.
|
|
2727
|
+
// Decode it with the runtime's own reader, as action() does. The
|
|
2728
|
+
// carrier arrives RESOLVED to an absolute url, so same-origin vs
|
|
2729
|
+
// cross-origin is a real origin comparison: same-origin folds to
|
|
2730
|
+
// a path the soft branch below navigates under the router, any
|
|
2731
|
+
// other origin keeps its href and the document goes with it. This
|
|
2732
|
+
// stays synchronous on purpose — navigate runs in the same tick as
|
|
2733
|
+
// the `Location` branch would, so the transition semantics match.
|
|
2734
|
+
if (url === null && !isServer && v.headers.has(REDIRECT_HEADER)) {
|
|
2735
|
+
const carried = decodeRedirectHeaderValue(v.headers.get(REDIRECT_HEADER));
|
|
2736
|
+
if (carried) {
|
|
2737
|
+
const target = new URL(carried.url);
|
|
2738
|
+
url = target.origin === window.location.origin ? target.pathname + target.search + target.hash : target.href;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2623
2741
|
if (url !== null) {
|
|
2624
2742
|
// invalidate the redirect's revalidation keys before navigating so
|
|
2625
2743
|
// the destination's preloads see the miss and fetch fresh (#580 thread).
|
|
@@ -2735,6 +2853,91 @@ function isPlainObject(obj) {
|
|
|
2735
2853
|
return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype);
|
|
2736
2854
|
}
|
|
2737
2855
|
|
|
2856
|
+
// Server component routes (experimental — rides the experimental server
|
|
2857
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
2858
|
+
//
|
|
2859
|
+
// `serverRouteComponent(fn)` turns a `"use server"` function that returns a
|
|
2860
|
+
// component into a route `component`. It replaces the client wrapper a
|
|
2861
|
+
// server-component route used to need:
|
|
2862
|
+
//
|
|
2863
|
+
// // before: a client component exists only to make the call
|
|
2864
|
+
// component: props => {
|
|
2865
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
2866
|
+
// return <View>{props.children}</View>;
|
|
2867
|
+
// }
|
|
2868
|
+
// // after
|
|
2869
|
+
// component: serverRouteComponent(storyRoute) // async ({ params }) => { "use server"; ... }
|
|
2870
|
+
//
|
|
2871
|
+
// The mechanism is the same one the wrapper used — `query` for cache identity
|
|
2872
|
+
// (preload participation, single-flight collection, revalidation after
|
|
2873
|
+
// actions) and `dynamic` for the equals-gated mount that morphs in place —
|
|
2874
|
+
// applied by the router rather than restated per route. What the router adds
|
|
2875
|
+
// is the URL → call translation: it derives the call's arguments from the
|
|
2876
|
+
// match and drives the same call from hover preload and from the single
|
|
2877
|
+
// flight collector, so one entry serves render, preload, and mutation
|
|
2878
|
+
// responses.
|
|
2879
|
+
//
|
|
2880
|
+
// Arguments are derived, not read from a live location: the call's
|
|
2881
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
2882
|
+
// so the args must name precisely what the route depends on. They are the
|
|
2883
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
2884
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
2885
|
+
// when the route declares a `search` schema, its validated output.
|
|
2886
|
+
//
|
|
2887
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
2888
|
+
// A server component that takes other client positions — handlers, refs,
|
|
2889
|
+
// named slots — has a client half, and that half is a client component; the
|
|
2890
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
2891
|
+
/**
|
|
2892
|
+
* Use a server component as a route (experimental). `fn` is a `"use server"`
|
|
2893
|
+
* function taking the router-derived {@link ServerRouteArgs} — this route's
|
|
2894
|
+
* `params`, and `search` when the route declares a schema — and resolving to
|
|
2895
|
+
* a server component. The router mounts it with the outlet as `children`,
|
|
2896
|
+
* warms the same call on link intent, and collects it for single-flight
|
|
2897
|
+
* mutation responses.
|
|
2898
|
+
*
|
|
2899
|
+
* `children` is the only client position the router fills, so the server
|
|
2900
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
2901
|
+
* slots has a client half — write an ordinary route component for it.
|
|
2902
|
+
*
|
|
2903
|
+
* ```ts
|
|
2904
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
2905
|
+
* ```
|
|
2906
|
+
*/
|
|
2907
|
+
function serverRouteComponent(fn) {
|
|
2908
|
+
const reference = fn;
|
|
2909
|
+
// One query entry per reference: the cache key (the function id) has to
|
|
2910
|
+
// agree between the render that mounts the route, the hover preload that
|
|
2911
|
+
// warms it, and the single-flight collector that re-produces its region.
|
|
2912
|
+
// A server function reference always carries its build-stable id; a
|
|
2913
|
+
// hand-built brand (tests) falls back to the function name.
|
|
2914
|
+
const call = query(reference, "route:" + (reference.id || reference.name));
|
|
2915
|
+
|
|
2916
|
+
// The component itself is the fallback for a mount the router core does
|
|
2917
|
+
// not drive (a plain `createComponent`): it has only the merged params to
|
|
2918
|
+
// go on, so it calls with those. The core mounts through the brand with
|
|
2919
|
+
// this level's params instead.
|
|
2920
|
+
const route = routeProps => render(() => ({
|
|
2921
|
+
params: {
|
|
2922
|
+
...routeProps.params
|
|
2923
|
+
},
|
|
2924
|
+
search: undefined
|
|
2925
|
+
}), routeProps);
|
|
2926
|
+
function render(args, routeProps) {
|
|
2927
|
+
const View = dynamic(() => call(args()));
|
|
2928
|
+
return createComponent(View, {
|
|
2929
|
+
get children() {
|
|
2930
|
+
return routeProps.children;
|
|
2931
|
+
}
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
route[SERVER_ROUTE] = {
|
|
2935
|
+
call,
|
|
2936
|
+
render
|
|
2937
|
+
};
|
|
2938
|
+
return route;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2738
2941
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
2739
2942
|
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
2740
2943
|
const invokeSymbol = Symbol("routerActionInvoke");
|
|
@@ -3503,4 +3706,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
3503
3706
|
submitServerForm: submitServerForm
|
|
3504
3707
|
});
|
|
3505
3708
|
|
|
3506
|
-
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
|
3709
|
+
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
package/dist/index.jsx
CHANGED
|
@@ -3,4 +3,5 @@ export * from "./lifecycle.js";
|
|
|
3
3
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
4
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
5
5
|
export { int } from "./paths.js";
|
|
6
|
+
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
6
7
|
export * from "./data/index.js";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createMemo, createRoot, getOwner, onCleanup, runWithOwner, untrack, Show } from "solid-js";
|
|
3
3
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
4
|
import { createRouteContext, getIntent, getRouteMatches, resolveLazySubtree, RouteContextObj, setInPreloadFn, unresolvedLazyMatches } from "../routing.js";
|
|
5
|
+
import { serverRouteArgs, serverRouteOf } from "../serverRouteShared.js";
|
|
5
6
|
export function Root(props) {
|
|
6
7
|
const location = props.routerState.location;
|
|
7
8
|
const params = props.routerState.params;
|
|
@@ -129,6 +130,9 @@ function dataOnly(event, routerState, branches) {
|
|
|
129
130
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
130
131
|
event.router.dataOnly = true;
|
|
131
132
|
const { route, params } = matches[match];
|
|
133
|
+
// a server component route's markup is collected like any query result
|
|
134
|
+
const server = serverRouteOf(route.component);
|
|
135
|
+
server && server.call(serverRouteArgs(route, params, routerState.location.query));
|
|
132
136
|
route.preload &&
|
|
133
137
|
route.preload({
|
|
134
138
|
params,
|
package/dist/routing.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createComponent, createContext, createMemo, createSignal, getOwner, isP
|
|
|
6
6
|
import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
|
|
7
7
|
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
9
|
+
import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
|
|
9
10
|
const MAX_REDIRECTS = 100;
|
|
10
11
|
/**
|
|
11
12
|
* Resolves a write against `headed`, the location the router is heading to
|
|
@@ -900,22 +901,29 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
900
901
|
route.component &&
|
|
901
902
|
route.component.preload &&
|
|
902
903
|
route.component.preload();
|
|
903
|
-
const { preload } = route;
|
|
904
|
+
const { preload, component } = route;
|
|
904
905
|
inPreloadFn = true;
|
|
905
906
|
preloadData &&
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
907
|
+
runWithOwner(getContext(), () => {
|
|
908
|
+
const query = extractSearchParams(url);
|
|
909
|
+
// A server component route's data IS its call: warm the same
|
|
910
|
+
// query entry the render will read, under the same derived args.
|
|
911
|
+
const server = serverRouteOf(component);
|
|
912
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
913
|
+
preload &&
|
|
914
|
+
preload({
|
|
915
|
+
params,
|
|
916
|
+
location: {
|
|
917
|
+
pathname: url.pathname,
|
|
918
|
+
search: url.search,
|
|
919
|
+
hash: url.hash,
|
|
920
|
+
query,
|
|
921
|
+
state: null,
|
|
922
|
+
key: ""
|
|
923
|
+
},
|
|
924
|
+
intent: "preload"
|
|
925
|
+
});
|
|
926
|
+
});
|
|
919
927
|
inPreloadFn = false;
|
|
920
928
|
}
|
|
921
929
|
preloadIntent = prevIntent;
|
|
@@ -965,16 +973,29 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
965
973
|
pattern,
|
|
966
974
|
params,
|
|
967
975
|
path,
|
|
968
|
-
outlet: () =>
|
|
969
|
-
|
|
976
|
+
outlet: () => {
|
|
977
|
+
if (!component)
|
|
978
|
+
return outlet();
|
|
979
|
+
const routeProps = {
|
|
970
980
|
params,
|
|
971
981
|
location,
|
|
972
982
|
data,
|
|
973
983
|
get children() {
|
|
974
984
|
return outlet();
|
|
975
985
|
}
|
|
976
|
-
}
|
|
977
|
-
|
|
986
|
+
};
|
|
987
|
+
// A `serverRouteComponent()` route: the router derives the call from
|
|
988
|
+
// the match — THIS level's params (never a child's) and the declared
|
|
989
|
+
// search output — and mounts through the brand. Structural equality
|
|
990
|
+
// keeps a navigation that leaves this level's args unchanged from
|
|
991
|
+
// re-entering the call. See serverRouteComponent.ts.
|
|
992
|
+
const server = serverRouteOf(component);
|
|
993
|
+
if (server) {
|
|
994
|
+
const args = createMemo(() => serverRouteArgs(match().route, match().params, location.query), { equals: serverRouteArgsEqual });
|
|
995
|
+
return server.render(args, routeProps);
|
|
996
|
+
}
|
|
997
|
+
return createComponent(component, routeProps);
|
|
998
|
+
},
|
|
978
999
|
resolvePath(to) {
|
|
979
1000
|
return resolvePath(base.path(), to, path());
|
|
980
1001
|
}
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { provideRequestEvent } from "@solidjs/web/storage";
|
|
25
25
|
import { createBranches, getRouteMatches, mergeParams, peekLazySubtrees, resolveLazySubtree } from "./routing.js";
|
|
26
26
|
import { extractSearchParams } from "./utils.js";
|
|
27
|
+
import { serverRouteArgs, serverRouteOf } from "./serverRouteShared.js";
|
|
27
28
|
// the instance is the provider component, so it (unlike an options object) is a function
|
|
28
29
|
function isRouterInstance(options) {
|
|
29
30
|
return typeof options === "function";
|
|
@@ -146,6 +147,11 @@ function runPreloads(event, branches, url, previousUrl, rootPreload) {
|
|
|
146
147
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
147
148
|
event.router.dataOnly = true;
|
|
148
149
|
const { route, params } = matches[match];
|
|
150
|
+
// A server component route's markup is collected like any query result:
|
|
151
|
+
// the same call (function id + derived args) the client is showing, so
|
|
152
|
+
// the response's region addresses what is mounted.
|
|
153
|
+
const server = serverRouteOf(route.component);
|
|
154
|
+
server && server.call(serverRouteArgs(route, params, location.query));
|
|
149
155
|
route.preload &&
|
|
150
156
|
route.preload({
|
|
151
157
|
params,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Component } from "solid-js";
|
|
2
|
+
import type { Params, RouteSectionProps, ServerRouteFunction } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Use a server component as a route (experimental). `fn` is a `"use server"`
|
|
5
|
+
* function taking the router-derived {@link ServerRouteArgs} — this route's
|
|
6
|
+
* `params`, and `search` when the route declares a schema — and resolving to
|
|
7
|
+
* a server component. The router mounts it with the outlet as `children`,
|
|
8
|
+
* warms the same call on link intent, and collects it for single-flight
|
|
9
|
+
* mutation responses.
|
|
10
|
+
*
|
|
11
|
+
* `children` is the only client position the router fills, so the server
|
|
12
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
13
|
+
* slots has a client half — write an ordinary route component for it.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function serverRouteComponent<P extends Params = Params, S = undefined>(fn: ServerRouteFunction<P, S>): Component<RouteSectionProps<unknown, P>>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Server component routes (experimental — rides the experimental server
|
|
2
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
3
|
+
//
|
|
4
|
+
// `serverRouteComponent(fn)` turns a `"use server"` function that returns a
|
|
5
|
+
// component into a route `component`. It replaces the client wrapper a
|
|
6
|
+
// server-component route used to need:
|
|
7
|
+
//
|
|
8
|
+
// // before: a client component exists only to make the call
|
|
9
|
+
// component: props => {
|
|
10
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
11
|
+
// return <View>{props.children}</View>;
|
|
12
|
+
// }
|
|
13
|
+
// // after
|
|
14
|
+
// component: serverRouteComponent(storyRoute) // async ({ params }) => { "use server"; ... }
|
|
15
|
+
//
|
|
16
|
+
// The mechanism is the same one the wrapper used — `query` for cache identity
|
|
17
|
+
// (preload participation, single-flight collection, revalidation after
|
|
18
|
+
// actions) and `dynamic` for the equals-gated mount that morphs in place —
|
|
19
|
+
// applied by the router rather than restated per route. What the router adds
|
|
20
|
+
// is the URL → call translation: it derives the call's arguments from the
|
|
21
|
+
// match and drives the same call from hover preload and from the single
|
|
22
|
+
// flight collector, so one entry serves render, preload, and mutation
|
|
23
|
+
// responses.
|
|
24
|
+
//
|
|
25
|
+
// Arguments are derived, not read from a live location: the call's
|
|
26
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
27
|
+
// so the args must name precisely what the route depends on. They are the
|
|
28
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
29
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
30
|
+
// when the route declares a `search` schema, its validated output.
|
|
31
|
+
//
|
|
32
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
33
|
+
// A server component that takes other client positions — handlers, refs,
|
|
34
|
+
// named slots — has a client half, and that half is a client component; the
|
|
35
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
36
|
+
import { dynamic } from "@solidjs/web";
|
|
37
|
+
import { createComponent } from "solid-js";
|
|
38
|
+
import { query } from "./data/query.js";
|
|
39
|
+
import { SERVER_ROUTE } from "./serverRouteShared.js";
|
|
40
|
+
/**
|
|
41
|
+
* Use a server component as a route (experimental). `fn` is a `"use server"`
|
|
42
|
+
* function taking the router-derived {@link ServerRouteArgs} — this route's
|
|
43
|
+
* `params`, and `search` when the route declares a schema — and resolving to
|
|
44
|
+
* a server component. The router mounts it with the outlet as `children`,
|
|
45
|
+
* warms the same call on link intent, and collects it for single-flight
|
|
46
|
+
* mutation responses.
|
|
47
|
+
*
|
|
48
|
+
* `children` is the only client position the router fills, so the server
|
|
49
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
50
|
+
* slots has a client half — write an ordinary route component for it.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function serverRouteComponent(fn) {
|
|
57
|
+
const reference = fn;
|
|
58
|
+
// One query entry per reference: the cache key (the function id) has to
|
|
59
|
+
// agree between the render that mounts the route, the hover preload that
|
|
60
|
+
// warms it, and the single-flight collector that re-produces its region.
|
|
61
|
+
// A server function reference always carries its build-stable id; a
|
|
62
|
+
// hand-built brand (tests) falls back to the function name.
|
|
63
|
+
const call = query(reference, "route:" + (reference.id || reference.name));
|
|
64
|
+
// The component itself is the fallback for a mount the router core does
|
|
65
|
+
// not drive (a plain `createComponent`): it has only the merged params to
|
|
66
|
+
// go on, so it calls with those. The core mounts through the brand with
|
|
67
|
+
// this level's params instead.
|
|
68
|
+
const route = (routeProps) => render(() => ({ params: { ...routeProps.params }, search: undefined }), routeProps);
|
|
69
|
+
function render(args, routeProps) {
|
|
70
|
+
const View = dynamic(() => call(args()));
|
|
71
|
+
return createComponent(View, {
|
|
72
|
+
get children() {
|
|
73
|
+
return routeProps.children;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
route[SERVER_ROUTE] = { call, render };
|
|
78
|
+
return route;
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Accessor } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import type { Params, RouteDescription, RouteSectionProps, SearchParams, ServerRouteArgs } from "./types.js";
|
|
4
|
+
export declare const SERVER_ROUTE: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* What `serverRouteComponent()` attaches to the route component it returns, and
|
|
7
|
+
* what the router core drives it through: `call` is the query-wrapped server
|
|
8
|
+
* function (preload and single-flight collection warm/collect through it),
|
|
9
|
+
* `render` mounts the resolved server component for the current args with
|
|
10
|
+
* the route's props (`children` is the outlet).
|
|
11
|
+
*/
|
|
12
|
+
export interface ServerRouteBrand {
|
|
13
|
+
call: (args: ServerRouteArgs<Params, unknown>) => unknown;
|
|
14
|
+
render: (args: Accessor<ServerRouteArgs<Params, unknown>>, route: RouteSectionProps) => JSX.Element;
|
|
15
|
+
}
|
|
16
|
+
export type BrandedRouteComponent = Function & {
|
|
17
|
+
[SERVER_ROUTE]?: ServerRouteBrand;
|
|
18
|
+
};
|
|
19
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
20
|
+
export declare function serverRouteOf(component: unknown): ServerRouteBrand | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
23
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
24
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
25
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
26
|
+
*/
|
|
27
|
+
export declare function serverRouteArgs(route: RouteDescription, params: Params, query: SearchParams): ServerRouteArgs<Params, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
30
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
31
|
+
* must not re-enter the source (and refetch).
|
|
32
|
+
*/
|
|
33
|
+
export declare function serverRouteArgsEqual(a: ServerRouteArgs<Params, unknown>, b: ServerRouteArgs<Params, unknown>): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
2
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
3
|
+
export function serverRouteOf(component) {
|
|
4
|
+
return typeof component === "function"
|
|
5
|
+
? component[SERVER_ROUTE]
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
10
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
11
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
12
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
13
|
+
*/
|
|
14
|
+
export function serverRouteArgs(route, params, query) {
|
|
15
|
+
const schema = route.key?.search;
|
|
16
|
+
let search = undefined;
|
|
17
|
+
if (schema) {
|
|
18
|
+
const outcome = schema["~standard"].validate({ ...query });
|
|
19
|
+
if (outcome instanceof Promise)
|
|
20
|
+
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
21
|
+
// Issues leave the raw values in place, matching useSearchParams: search
|
|
22
|
+
// strings are user input, so defaults belong in the schema itself.
|
|
23
|
+
search = outcome.issues ? { ...query } : outcome.value;
|
|
24
|
+
}
|
|
25
|
+
return { params: { ...params }, search };
|
|
26
|
+
}
|
|
27
|
+
function shallowEqual(a, b) {
|
|
28
|
+
if (a === b)
|
|
29
|
+
return true;
|
|
30
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
31
|
+
return false;
|
|
32
|
+
const ka = Object.keys(a);
|
|
33
|
+
const kb = Object.keys(b);
|
|
34
|
+
if (ka.length !== kb.length)
|
|
35
|
+
return false;
|
|
36
|
+
for (const k of ka)
|
|
37
|
+
if (a[k] !== b[k])
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
43
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
44
|
+
* must not re-enter the source (and refetch).
|
|
45
|
+
*/
|
|
46
|
+
export function serverRouteArgsEqual(a, b) {
|
|
47
|
+
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
48
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -141,6 +141,24 @@ export interface RouteSectionProps<T = unknown, P extends Params = Params> {
|
|
|
141
141
|
children?: JSX.Element;
|
|
142
142
|
}
|
|
143
143
|
export type RouteSectionComponent<T = unknown, P extends Params = Params> = Component<RouteSectionProps<T, P>> | Component<Omit<RouteSectionProps<T, P>, "children">> | Component<{}>;
|
|
144
|
+
/**
|
|
145
|
+
* What a server component route is called with (experimental): the params
|
|
146
|
+
* the route's pattern (and its ancestors') declares, plus — only when the
|
|
147
|
+
* route declares a `search` schema — that schema's validated output.
|
|
148
|
+
*/
|
|
149
|
+
export interface ServerRouteArgs<P extends Params = Params, S = undefined> {
|
|
150
|
+
params: P;
|
|
151
|
+
search: S;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The shape `serverRouteComponent()` accepts (experimental): a `"use server"`
|
|
155
|
+
* function taking the router-derived {@link ServerRouteArgs} and resolving
|
|
156
|
+
* to a server component whose only client position is `children` — the route
|
|
157
|
+
* outlet, which the router fills.
|
|
158
|
+
*/
|
|
159
|
+
export type ServerRouteFunction<P extends Params = Params, S = undefined> = (args: ServerRouteArgs<P, S>) => Promise<Component<{
|
|
160
|
+
children?: JSX.Element;
|
|
161
|
+
}>>;
|
|
144
162
|
declare const ROUTE_PATTERN: unique symbol;
|
|
145
163
|
export interface TypedRouteConfig<S extends string = string> {
|
|
146
164
|
readonly [ROUTE_PATTERN]: S;
|
|
@@ -341,7 +359,13 @@ export type Submission<T, U> = {
|
|
|
341
359
|
export interface MaybePreloadableComponent extends Component {
|
|
342
360
|
preload?: () => void;
|
|
343
361
|
}
|
|
344
|
-
export type CacheEntry = [
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
export type CacheEntry = [
|
|
363
|
+
number,
|
|
364
|
+
Promise<any>,
|
|
365
|
+
any,
|
|
366
|
+
Intent | undefined,
|
|
367
|
+
Signal<number> & {
|
|
368
|
+
count: number;
|
|
369
|
+
}
|
|
370
|
+
];
|
|
347
371
|
export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
|