@solidjs/router 2.0.0-next.27 → 2.0.0-next.28
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 +61 -11
- package/dist/data/liveQuery.d.ts +5 -5
- package/dist/data/liveQuery.js +13 -11
- package/dist/fs.d.ts +7 -1
- package/dist/fs.js +29 -5
- package/dist/fsServer.d.ts +15 -0
- package/dist/fsServer.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +150 -121
- package/dist/routers/factory.d.ts +10 -1
- package/dist/routers/factory.jsx +14 -2
- package/dist/routing.js +27 -9
- package/dist/serverRouteComponent.d.ts +10 -9
- package/dist/serverRouteComponent.js +45 -34
- package/dist/serverRouteShared.js +13 -22
- package/dist/types.d.ts +23 -10
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +7 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -371,14 +371,14 @@ Resolution is cached per thunk and append-only: the tree never changes shape aft
|
|
|
371
371
|
|
|
372
372
|
### Server Component Routes (experimental)
|
|
373
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`
|
|
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` takes a `query` over one and uses it as a route's `component` directly — the router owns the URL → call translation a client wrapper used to spell out:
|
|
375
375
|
|
|
376
376
|
```tsx
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
import type { ServerRouteArgs } from "@solidjs/router";
|
|
377
|
+
// story.tsx — the source owns its key
|
|
378
|
+
import { query, type ServerRouteArgs } from "@solidjs/router";
|
|
380
379
|
|
|
381
|
-
export
|
|
380
|
+
export const getStory = query(async ({ params }: ServerRouteArgs<{ id: string }>) => {
|
|
381
|
+
"use server";
|
|
382
382
|
const story = await db.stories.get(params.id);
|
|
383
383
|
return props => (
|
|
384
384
|
<article>
|
|
@@ -386,20 +386,46 @@ export async function storyView({ params }: ServerRouteArgs<{ id: string }>) {
|
|
|
386
386
|
{props.children}
|
|
387
387
|
</article>
|
|
388
388
|
);
|
|
389
|
-
}
|
|
389
|
+
}, "story");
|
|
390
390
|
|
|
391
391
|
// routes.ts
|
|
392
|
-
defineRoute({ path: "/stories/:id", component: serverRouteComponent(
|
|
392
|
+
defineRoute({ path: "/stories/:id", component: serverRouteComponent(getStory) });
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
The
|
|
395
|
+
The source 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
396
|
|
|
397
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
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
399
|
|
|
400
|
-
|
|
400
|
+
A route view is route-shaped on purpose — the address stays stable and `defineRoute` can check its params against the pattern — which means it is only callable as a route. When the same server component is also used elsewhere, keep it a plain (non-exported, non-endpoint) function and have the route view call it with `params.id`. The value `serverRouteComponent` returns is a component only so it fits the `component` field; mounting it any other way (through `lazy()`, or by hand) throws, since outside the match there are only merged params to call with.
|
|
401
|
+
|
|
402
|
+
The router mounts the resolved component with the outlet as `children`, so a server component can be a layout, and it calls the same source under preload intent — link hover, `preloadRoute`, the [single-flight collector](#server-integration) — with the same derived args. What that call _means_ is the source's: the router does not choose the cache strategy or own the key.
|
|
403
|
+
|
|
404
|
+
- `query(fn, key)`: link intent warms the entry the render reads, `revalidate("story")` and action responses refetch it, and the collector 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.
|
|
405
|
+
- [`liveQuery(fn, key)`](#livequery-experimental): the frame stream stays open and the channel owns it — hover connects it (held through the preload window, so a hovered link is an open stream), `revalidate(key)` reconnects, and the mutation sweep and the single-flight collector both leave it alone — nothing pulls it server-side, and the stream is its own freshness.
|
|
406
|
+
|
|
407
|
+
Anything else callable with the args works too; wrapping is what gives dedupe, preload, and revalidation.
|
|
408
|
+
|
|
409
|
+
Mutations reach these routes the way they reach any query, and the cost model is worth knowing: a swept server route is a server-side re-render plus its markup in the response, not a small data blob. From a server action, `redirect(url)` re-collects everything the target shows and a bare `reload()` everything the current page shows — server routes included, shell and layouts too. On a server-component-heavy page, name what a mutation actually changed instead. `Router.keysFor(url)` answers the query keys of the server routes a URL shows, root to leaf — the app's own keys, from pure matching, so it works in an action and never spells a key:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
export const addComment = action(async (form: FormData) => {
|
|
413
|
+
"use server";
|
|
414
|
+
await db.comments.add(form);
|
|
415
|
+
return reload({ revalidate: Router.keysFor(paths.stories(form.get("id"))) });
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Route keys, not addresses: every story page is refetched, which is what prefix matching gives a hand-written `revalidate("story")` too.
|
|
420
|
+
|
|
421
|
+
An app shell is a pathless layout route whose `component` is a server component. With no pattern, its args are the constant `{ params: {}, search: undefined }` — one call, one address, persisting across every navigation — and as a route it gets preload, collection, and `revalidate` like any other. The `<Router>` root slot exists for client providers that need router context without following route rules; a server component has neither, so it does not go there:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
const routes = [{ component: serverRouteComponent(query(appShell, "shell")), children: pages }];
|
|
425
|
+
render(() => <Router routes={routes} />, document.body);
|
|
426
|
+
```
|
|
401
427
|
|
|
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
|
|
428
|
+
`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}` — needs no client component at all.
|
|
403
429
|
|
|
404
430
|
### File-System Routes
|
|
405
431
|
|
|
@@ -432,6 +458,30 @@ export default function Post(props: RouteProps<typeof route>) {
|
|
|
432
458
|
|
|
433
459
|
The pattern string is a typing witness — at runtime the manifest's path (from the filename) is the source of truth. With the plugin's `types` option generating a literal declaration for the virtual module, the file paths flow into `paths` and the typed hooks like a hand-written tree — `paths.blog(42)` typechecks, filters and search schemas included, and `useParams(paths.blog)` works as usual anywhere under the route.
|
|
434
460
|
|
|
461
|
+
#### Server pages
|
|
462
|
+
|
|
463
|
+
A route file's page can be a [server component](#server-component-routes-experimental): make the default export a `"use server"` function of the route args. With `fileRoutes({ serverComponents: true })` on the `file-routes` plugin, the scanner flags such a file and the adapter builds the route the hand-written tree spells out — `serverRouteComponent(query(fn, key))` with the file's path as the key. No `query`, key, or wrapper in the route file. (That option turns on server component _routes_; server components themselves are Solid's plugin's `serverFunctions: { components: true }`, the same switch a hand-written server route needs.)
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
// routes/stories/[id].tsx
|
|
467
|
+
import { defineFileRoute } from "@solidjs/router/fs";
|
|
468
|
+
import type { ServerRouteArgs } from "@solidjs/router";
|
|
469
|
+
|
|
470
|
+
export const route = defineFileRoute("/stories/:id", { search: storySearch });
|
|
471
|
+
|
|
472
|
+
export default async function Story({ params, search }: ServerRouteArgs<typeof route>) {
|
|
473
|
+
"use server";
|
|
474
|
+
const story = await db.stories.get(params.id);
|
|
475
|
+
return props => <article>{story.title}{props.children}</article>;
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
`ServerRouteArgs<typeof route>` reads the config as a witness like `RouteProps` does: `params` from the pattern, `search` as the schema's output (`undefined` with no schema). The key is the file — `Router.keysFor(paths.stories(7))` answers `["src/routes/stories/[id].tsx"]` (plus any server layouts above it), so an action names the page without spelling a path; because keys match by prefix, `revalidate("src/routes/stories")` refetches every page under the directory (pathless layouts have no route path of their own, so the file is what tells them apart). The directive has to be the first statement of the inline default export — behind a wrapper call or a re-export the scanner cannot see it, the page is code-split like a client page, and in development the adapter throws a directed error when the chunk resolves.
|
|
480
|
+
|
|
481
|
+
A live page names its wrapper in the `route` config — `defineFileRoute("/feed", { query: liveQuery })` — and the adapter sources through that instead of `query`. The route file imports `liveQuery`, so only an app with a live page carries it.
|
|
482
|
+
|
|
483
|
+
Apps with no server page pay nothing for any of this. The plugin folds the scan into `filesystem-routing/flags`, and the adapter's only path to `serverRouteComponent` and `query` is a module gated on that constant, so the branch — and with it the frames runtime — tree-shakes away. `pnpm build` asserts it.
|
|
484
|
+
|
|
435
485
|
## Typed Paths
|
|
436
486
|
|
|
437
487
|
`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:
|
|
@@ -594,7 +644,7 @@ Live queries ride the router's existing machinery rather than adding their own:
|
|
|
594
644
|
- **Preload** — calling one in a preload function warms the connection, so navigation renders against an already-delivered value instead of holding the transition on connect.
|
|
595
645
|
- **SSR** — the document face renders the first value; hydration adopts it and reconnects. Server-side, consumers of a key within one request observe the same value.
|
|
596
646
|
- **Revalidation** — explicit `revalidate(key)` reconnects (the producer re-yields current state by contract). The post-mutation sweep leaves healthy connections alone: the stream is its own freshness mechanism.
|
|
597
|
-
- **Single-flight** — a mutation
|
|
647
|
+
- **Single-flight** — live keys are neither collected nor swept: the stream is the freshness mechanism, and a mutation reaches a live query through its producer (the `watch` yielding the changed state), not through the mutation response. The flight-data seam that pushes payload values into open channels stays available to an integration that collects live keys itself.
|
|
598
648
|
|
|
599
649
|
The callable carries the `query` conventions (`key`, `keyFor`) plus a reactive `status(...args)` read (`"idle" | "connecting" | "connected" | "reconnecting" | "closed"`) for surfacing connection state in UI.
|
|
600
650
|
|
package/dist/data/liveQuery.d.ts
CHANGED
|
@@ -18,11 +18,11 @@ export type LiveFunction<T extends (...args: any) => any> = T extends (...args:
|
|
|
18
18
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
19
19
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
20
20
|
* a first-connect failure.
|
|
21
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
22
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
23
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
24
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
25
|
+
* live key an integration collects itself.)
|
|
26
26
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
27
27
|
* it open through the preload window), so navigation renders against an
|
|
28
28
|
* already-connected stream instead of holding the transition on connect
|
package/dist/data/liveQuery.js
CHANGED
|
@@ -205,7 +205,7 @@ function subscriberIterable(open) {
|
|
|
205
205
|
}
|
|
206
206
|
};
|
|
207
207
|
return {
|
|
208
|
-
next: () =>
|
|
208
|
+
next: () => released ? Promise.resolve({ done: true, value: undefined }) : pull(),
|
|
209
209
|
return(value) {
|
|
210
210
|
release();
|
|
211
211
|
return Promise.resolve({ done: true, value });
|
|
@@ -236,10 +236,12 @@ function push(ch, value) {
|
|
|
236
236
|
// connection is alive and is itself the freshness mechanism — a
|
|
237
237
|
// push-driven producer yields the mutated state on its own, and tearing
|
|
238
238
|
// down healthy connections on every mutation defeats the model.
|
|
239
|
-
// -
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// for
|
|
239
|
+
// - A single-flight payload carrying a live key delivers INTO the open
|
|
240
|
+
// channel rather than reconnecting it. The router's own collector does not
|
|
241
|
+
// produce live keys (the stream is the freshness mechanism; collection
|
|
242
|
+
// for live server components is an open question — a mutation's region
|
|
243
|
+
// would outrank the standing frame stream under the transport's version
|
|
244
|
+
// policy), so this path serves an integration that collects them itself.
|
|
243
245
|
let hooked = false;
|
|
244
246
|
function hookRevalidate() {
|
|
245
247
|
if (hooked)
|
|
@@ -293,11 +295,11 @@ function hookRevalidate() {
|
|
|
293
295
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
294
296
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
295
297
|
* a first-connect failure.
|
|
296
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
298
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
299
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
300
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
301
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
302
|
+
* live key an integration collects itself.)
|
|
301
303
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
302
304
|
* it open through the preload window), so navigation renders against an
|
|
303
305
|
* already-connected stream instead of holding the transition on connect
|
|
@@ -349,7 +351,7 @@ export function liveQuery(fn, name) {
|
|
|
349
351
|
// reads the same guarantee). Channels live in the event, retained
|
|
350
352
|
// past teardown so a later consumer replays the settled value instead
|
|
351
353
|
// of reinvoking; the producer still closes with its last consumer.
|
|
352
|
-
const router =
|
|
354
|
+
const router = e.router || (e.router = {});
|
|
353
355
|
const channels = router.liveChannels || (router.liveChannels = new Map());
|
|
354
356
|
return subscriberIterable(() => openChannel(channels, key, () => fn(...args), false, true));
|
|
355
357
|
}
|
package/dist/fs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServerPageQuery } from "./fsServer.js";
|
|
1
2
|
import type { DefinedRouteFilters, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteInfo, RouteSectionComponent, StandardSchemaV1, TypedRouteConfig, ValidFilters } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* The type `defineFileRoute` hands back: `matchFilters` and `search` stay
|
|
@@ -14,6 +15,7 @@ export type FileRouteConfig<S extends string = string, T = unknown, F = undefine
|
|
|
14
15
|
}) & {
|
|
15
16
|
preload?: RoutePreloadFunc<T> | undefined;
|
|
16
17
|
info?: RouteInfo | undefined;
|
|
18
|
+
query?: ServerPageQuery | undefined;
|
|
17
19
|
};
|
|
18
20
|
/**
|
|
19
21
|
* Identity helper for a route file's `route` export that types `preload`'s
|
|
@@ -39,6 +41,8 @@ export declare function defineFileRoute<S extends string, T = unknown, const F =
|
|
|
39
41
|
/** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
|
|
40
42
|
search?: Sch;
|
|
41
43
|
info?: RouteInfo | undefined;
|
|
44
|
+
/** For a server page: the wrapper its source goes through — `query` unless named, e.g. `liveQuery`. */
|
|
45
|
+
query?: ServerPageQuery | undefined;
|
|
42
46
|
}): FileRouteConfig<S, T, F, Sch>;
|
|
43
47
|
/** A code-split module ref: delivered as a dynamic import. */
|
|
44
48
|
export interface FileRouteLazyRef<M = Record<string, unknown>> {
|
|
@@ -54,7 +58,9 @@ export interface FileRouteEagerRef<M = Record<string, unknown>> {
|
|
|
54
58
|
export interface FileRouteEntry {
|
|
55
59
|
path: string;
|
|
56
60
|
page?: boolean;
|
|
57
|
-
/**
|
|
61
|
+
/** The page component is a `"use server"` function; its `$component` is an eager ref to the stub. */
|
|
62
|
+
server?: boolean;
|
|
63
|
+
/** Code-split by default; an eager ref when delivered with `codeSplitting: false` or for a server page. */
|
|
58
64
|
$component?: FileRouteLazyRef<any> | FileRouteEagerRef<any> | undefined;
|
|
59
65
|
$$route?: FileRouteEagerRef<any> | undefined;
|
|
60
66
|
children?: readonly FileRouteEntry[] | undefined;
|
package/dist/fs.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { lazy } from "solid-js";
|
|
1
|
+
import { DEV, lazy } from "solid-js";
|
|
2
|
+
import { isServerFunction } from "@solidjs/web";
|
|
3
|
+
import { serverRoutes } from "filesystem-routing/flags";
|
|
4
|
+
import * as server from "./fsServer.js";
|
|
2
5
|
/**
|
|
3
6
|
* Identity helper for a route file's `route` export that types `preload`'s
|
|
4
7
|
* `args.params` from a pattern witness — the file's path pattern, which the
|
|
@@ -31,15 +34,36 @@ export function defineFileRoute(path, config) {
|
|
|
31
34
|
*/
|
|
32
35
|
export function fileRoutes(entries) {
|
|
33
36
|
const components = new Map();
|
|
34
|
-
const componentOf = (
|
|
37
|
+
const componentOf = (entry, config) => {
|
|
38
|
+
const ref = entry.$component;
|
|
35
39
|
if ("require" in ref) {
|
|
36
|
-
|
|
40
|
+
const component = ref.require()
|
|
41
|
+
.default;
|
|
42
|
+
// A server page: the stub is the source; the file is the key (unique,
|
|
43
|
+
// and a directory prefix is a subtree for `revalidate`).
|
|
44
|
+
return serverRoutes && entry.server
|
|
45
|
+
? server.serverRoute(component, (ref.src || entry.path).split("?")[0], config)
|
|
46
|
+
: component;
|
|
37
47
|
}
|
|
38
48
|
let component = components.get(ref.src);
|
|
39
49
|
if (!component) {
|
|
40
50
|
// moduleUrl is lazy()'s third argument as of solid 2.0.0-rc.1 (options
|
|
41
51
|
// moved second); route components are default exports, so no { export }.
|
|
42
|
-
component = lazy(
|
|
52
|
+
component = lazy(
|
|
53
|
+
// A `"use server"` default the scanner did not see (behind a wrapper
|
|
54
|
+
// call or a re-export) — or scanned without the plugin's
|
|
55
|
+
// `serverComponents` option — was code-split like a client page:
|
|
56
|
+
// say so in development rather than mount a stub as a component.
|
|
57
|
+
DEV
|
|
58
|
+
? () => ref.import().then(mod => {
|
|
59
|
+
if (isServerFunction(mod.default))
|
|
60
|
+
throw new Error(`Route module "${ref.src}" exports a server function as its page, but it ` +
|
|
61
|
+
"was delivered as a client page. Enable `serverComponents: true` on the " +
|
|
62
|
+
'file-routes plugin and make the `"use server"` directive the first ' +
|
|
63
|
+
"statement of the inline default export.");
|
|
64
|
+
return mod;
|
|
65
|
+
})
|
|
66
|
+
: ref.import, undefined, ref.src);
|
|
43
67
|
components.set(ref.src, component);
|
|
44
68
|
}
|
|
45
69
|
return component;
|
|
@@ -49,7 +73,7 @@ export function fileRoutes(entries) {
|
|
|
49
73
|
return {
|
|
50
74
|
...config,
|
|
51
75
|
path: entry.path,
|
|
52
|
-
component: entry.$component ? componentOf(entry
|
|
76
|
+
component: entry.$component ? componentOf(entry, config) : undefined,
|
|
53
77
|
info: { ...config.info, filesystem: true },
|
|
54
78
|
children: entry.children ? entry.children.map(toRoute) : undefined
|
|
55
79
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteSectionComponent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wrapper a server page's source goes through: `query` unless the route
|
|
4
|
+
* config names another — `liveQuery`, imported by the route file that wants
|
|
5
|
+
* it, so only that app carries it.
|
|
6
|
+
*/
|
|
7
|
+
export type ServerPageQuery = (fn: (...args: any[]) => any, key: string) => (...args: any[]) => any;
|
|
8
|
+
/**
|
|
9
|
+
* The server page half of the fs adapter — the only importer of `query` and
|
|
10
|
+
* `serverRouteComponent`. fs.ts reaches it behind `serverRoutes` from
|
|
11
|
+
* `filesystem-routing/flags`, so an app with no server page never bundles it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function serverRoute(fn: (...args: any[]) => any, key: string, config?: {
|
|
14
|
+
query?: ServerPageQuery | undefined;
|
|
15
|
+
}): RouteSectionComponent;
|
package/dist/fsServer.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { query } from "./data/query.js";
|
|
2
|
+
import { serverRouteComponent } from "./serverRouteComponent.js";
|
|
3
|
+
/**
|
|
4
|
+
* The server page half of the fs adapter — the only importer of `query` and
|
|
5
|
+
* `serverRouteComponent`. fs.ts reaches it behind `serverRoutes` from
|
|
6
|
+
* `filesystem-routing/flags`, so an app with no server page never bundles it.
|
|
7
|
+
*/
|
|
8
|
+
export function serverRoute(fn, key, config) {
|
|
9
|
+
return serverRouteComponent((config?.query ?? query)(fn, key));
|
|
10
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export { int } from "./paths.js";
|
|
|
26
26
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
27
27
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
28
28
|
export * from "./data/index.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";
|
|
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, ServerRouteParams, ServerRouteFunction, ServerRouteView, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
2
|
+
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
3
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
|
|
|
@@ -38,6 +38,13 @@ function invariant(value, message) {
|
|
|
38
38
|
function joinPaths(from, to) {
|
|
39
39
|
return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Run a Standard Schema synchronously — the only mode search params support. */
|
|
43
|
+
function validateSearch(schema, raw) {
|
|
44
|
+
const outcome = schema["~standard"].validate(raw);
|
|
45
|
+
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
46
|
+
return outcome;
|
|
47
|
+
}
|
|
41
48
|
function extractSearchParams(url) {
|
|
42
49
|
const params = {};
|
|
43
50
|
url.searchParams.forEach((value, key) => {
|
|
@@ -536,7 +543,7 @@ const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
|
536
543
|
|
|
537
544
|
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
538
545
|
function serverRouteOf(component) {
|
|
539
|
-
return
|
|
546
|
+
return component?.[SERVER_ROUTE];
|
|
540
547
|
}
|
|
541
548
|
|
|
542
549
|
/**
|
|
@@ -547,17 +554,15 @@ function serverRouteOf(component) {
|
|
|
547
554
|
*/
|
|
548
555
|
function serverRouteArgs(route, params, query) {
|
|
549
556
|
const schema = route.key?.search;
|
|
550
|
-
let search
|
|
557
|
+
let search;
|
|
551
558
|
if (schema) {
|
|
552
|
-
const
|
|
559
|
+
const raw = {
|
|
553
560
|
...query
|
|
554
|
-
}
|
|
555
|
-
|
|
561
|
+
};
|
|
562
|
+
const outcome = validateSearch(schema, raw);
|
|
556
563
|
// Issues leave the raw values in place, matching useSearchParams: search
|
|
557
564
|
// strings are user input, so defaults belong in the schema itself.
|
|
558
|
-
search = outcome.issues ?
|
|
559
|
-
...query
|
|
560
|
-
} : outcome.value;
|
|
565
|
+
search = outcome.issues ? raw : outcome.value;
|
|
561
566
|
}
|
|
562
567
|
return {
|
|
563
568
|
params: {
|
|
@@ -566,15 +571,7 @@ function serverRouteArgs(route, params, query) {
|
|
|
566
571
|
search
|
|
567
572
|
};
|
|
568
573
|
}
|
|
569
|
-
|
|
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
|
-
}
|
|
574
|
+
const shallowEqual = (a, b) => a === b || !!a && !!b && typeof a === "object" && typeof b === "object" && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(k => a[k] === b[k]);
|
|
578
575
|
|
|
579
576
|
/**
|
|
580
577
|
* Structural equality for the args memo: a navigation produces fresh match
|
|
@@ -830,8 +827,7 @@ function useSearchParams(path) {
|
|
|
830
827
|
for (const match of router.matches()) {
|
|
831
828
|
const schema = match.route.key.search;
|
|
832
829
|
if (!schema) continue;
|
|
833
|
-
const outcome = schema
|
|
834
|
-
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
830
|
+
const outcome = validateSearch(schema, raw);
|
|
835
831
|
if (!outcome.issues) result = Object.assign(result || {
|
|
836
832
|
...raw
|
|
837
833
|
}, outcome.value);
|
|
@@ -1528,33 +1524,49 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1528
1524
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1529
1525
|
}
|
|
1530
1526
|
function preloadRoute(url, preloadData) {
|
|
1531
|
-
const
|
|
1527
|
+
const next = getRouteMatches(branches(), url.pathname);
|
|
1532
1528
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1533
1529
|
// component.preload (below) kicks the table load; once it lands,
|
|
1534
1530
|
// preload again so the real inner routes warm too. Preloads are
|
|
1535
1531
|
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
1536
1532
|
// here — the real navigation surfaces and retries it.
|
|
1537
|
-
const boundary =
|
|
1533
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1538
1534
|
if (boundary) {
|
|
1539
1535
|
try {
|
|
1540
1536
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1541
1537
|
} catch {}
|
|
1542
1538
|
}
|
|
1539
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
1540
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
1541
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
1542
|
+
// already this selective (a matching level is reused and re-reads through
|
|
1543
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
1544
|
+
let current;
|
|
1545
|
+
try {
|
|
1546
|
+
current = untrack(matches);
|
|
1547
|
+
} catch {}
|
|
1548
|
+
const query = extractSearchParams(url);
|
|
1549
|
+
const inputs = (p, q, s, r) => {
|
|
1550
|
+
const a = serverRouteArgs(r, p, q);
|
|
1551
|
+
a.search === undefined && (a.search = s);
|
|
1552
|
+
return a;
|
|
1553
|
+
};
|
|
1543
1554
|
const prevIntent = preloadIntent;
|
|
1544
1555
|
preloadIntent = "preload";
|
|
1545
|
-
for (let match in
|
|
1556
|
+
for (let match in next) {
|
|
1546
1557
|
const {
|
|
1547
1558
|
route,
|
|
1548
1559
|
params
|
|
1549
|
-
} =
|
|
1560
|
+
} = next[match];
|
|
1550
1561
|
route.component && route.component.preload && route.component.preload();
|
|
1551
1562
|
const {
|
|
1552
1563
|
preload,
|
|
1553
1564
|
component
|
|
1554
1565
|
} = route;
|
|
1566
|
+
const now = current && current[match];
|
|
1567
|
+
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1555
1568
|
inPreloadFn = true;
|
|
1556
|
-
preloadData && runWithOwner(getContext(), () => {
|
|
1557
|
-
const query = extractSearchParams(url);
|
|
1569
|
+
preloadData && !unchanged && runWithOwner(getContext(), () => {
|
|
1558
1570
|
// A server component route's data IS its call: warm the same
|
|
1559
1571
|
// query entry the render will read, under the same derived args.
|
|
1560
1572
|
const server = serverRouteOf(component);
|
|
@@ -2341,6 +2353,8 @@ function createRouter(config) {
|
|
|
2341
2353
|
};
|
|
2342
2354
|
const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
|
|
2343
2355
|
const matchPath = pathname => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
2356
|
+
// a `paths` node carries its logical pathname under the Href brand
|
|
2357
|
+
const pathnameOf = url => typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2344
2358
|
function RouterComponent(props) {
|
|
2345
2359
|
// One router per app: the session (location, history, delegation, link
|
|
2346
2360
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -2398,7 +2412,7 @@ function createRouter(config) {
|
|
|
2398
2412
|
routes: config.routes,
|
|
2399
2413
|
config,
|
|
2400
2414
|
match(url) {
|
|
2401
|
-
return matchPath(
|
|
2415
|
+
return matchPath(pathnameOf(url)).map(({
|
|
2402
2416
|
route,
|
|
2403
2417
|
path,
|
|
2404
2418
|
params
|
|
@@ -2409,6 +2423,17 @@ function createRouter(config) {
|
|
|
2409
2423
|
params,
|
|
2410
2424
|
info: route.info
|
|
2411
2425
|
}));
|
|
2426
|
+
},
|
|
2427
|
+
keysFor(url) {
|
|
2428
|
+
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2429
|
+
const keys = [];
|
|
2430
|
+
for (const {
|
|
2431
|
+
route
|
|
2432
|
+
} of matchPath(pathname)) {
|
|
2433
|
+
const key = serverRouteOf(route.component)?.call?.key;
|
|
2434
|
+
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
2435
|
+
}
|
|
2436
|
+
return keys;
|
|
2412
2437
|
}
|
|
2413
2438
|
});
|
|
2414
2439
|
// Built on first access (a getter via Object.assign would run during the
|
|
@@ -2501,6 +2526,93 @@ const useBeforeLeave = listener => {
|
|
|
2501
2526
|
onCleanup(s);
|
|
2502
2527
|
};
|
|
2503
2528
|
|
|
2529
|
+
// Server component routes (experimental — rides the experimental server
|
|
2530
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
2531
|
+
//
|
|
2532
|
+
// `serverRouteComponent(source)` turns a function of route arguments that
|
|
2533
|
+
// resolves to a component into a route `component`. It replaces the client
|
|
2534
|
+
// wrapper a server-component route used to need:
|
|
2535
|
+
//
|
|
2536
|
+
// // before: a client component exists only to make the call
|
|
2537
|
+
// const getStory = query(storyView, "story");
|
|
2538
|
+
// component: props => {
|
|
2539
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
2540
|
+
// return <View>{props.children}</View>;
|
|
2541
|
+
// }
|
|
2542
|
+
// // after
|
|
2543
|
+
// component: serverRouteComponent(query(storyView, "story"))
|
|
2544
|
+
//
|
|
2545
|
+
// The source is the app's: `query(fn, key)` for a request/response server
|
|
2546
|
+
// component, `liveQuery(fn, key)` for one that streams successive versions,
|
|
2547
|
+
// or any function of the args. The router does not choose the cache
|
|
2548
|
+
// strategy and does not own the key — `revalidate("story")` is the app's,
|
|
2549
|
+
// as it is for any query. What the router adds is the URL → call
|
|
2550
|
+
// translation: it derives the call's arguments from the match, mounts the
|
|
2551
|
+
// resolved component with the outlet as `children`, and calls the same
|
|
2552
|
+
// source under preload intent (link hover, `preloadRoute`, the single-flight
|
|
2553
|
+
// collector), so the query or live channel is warm before the navigation
|
|
2554
|
+
// renders against it.
|
|
2555
|
+
//
|
|
2556
|
+
// Arguments are derived, not read from a live location: the call's
|
|
2557
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
2558
|
+
// so the args must name precisely what the route depends on. They are the
|
|
2559
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
2560
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
2561
|
+
// when the route declares a `search` schema, its validated output.
|
|
2562
|
+
//
|
|
2563
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
2564
|
+
// A server component that takes other client positions — handlers, refs,
|
|
2565
|
+
// named slots — has a client half, and that half is a client component; the
|
|
2566
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
2567
|
+
/**
|
|
2568
|
+
* Use a server component as a route (experimental). `source` is a function
|
|
2569
|
+
* of the router-derived {@link ServerRouteArgs} — this route's `params`, and
|
|
2570
|
+
* `search` when the route declares a schema — resolving to a server
|
|
2571
|
+
* component: typically a `"use server"` function wrapped in `query()` or
|
|
2572
|
+
* `liveQuery()`, whose key the app names and revalidates. The router mounts
|
|
2573
|
+
* the result with the outlet as `children` and calls the same source under
|
|
2574
|
+
* preload intent, so link hover and single-flight collection warm it.
|
|
2575
|
+
*
|
|
2576
|
+
* `children` is the only client position the router fills, so the server
|
|
2577
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
2578
|
+
* slots has a client half — write an ordinary route component for it.
|
|
2579
|
+
*
|
|
2580
|
+
* ```ts
|
|
2581
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
2582
|
+
* ```
|
|
2583
|
+
*/
|
|
2584
|
+
function serverRouteComponent(
|
|
2585
|
+
// A source that ignores its args (an app shell) is `(...args: never[])`
|
|
2586
|
+
// once `query()` has typed it; that is the same call, so accept it too.
|
|
2587
|
+
source) {
|
|
2588
|
+
const call = source;
|
|
2589
|
+
|
|
2590
|
+
// The value is a component only so it fits the `component` field; the
|
|
2591
|
+
// router core never calls it — it mounts through the brand, with THIS
|
|
2592
|
+
// level's params and the declared search output. A direct mount (a
|
|
2593
|
+
// `lazy()` wrapper around the helper, a hand-written `<Route />`) would
|
|
2594
|
+
// have only the merged params to call with: the same view under a second
|
|
2595
|
+
// address, missing the cache and owning its own frame. Refuse it.
|
|
2596
|
+
const route = () => {
|
|
2597
|
+
throw new Error("serverRouteComponent(): mount it as a route's `component`" + (DEV ? " — the router derives the call from the match. It was rendered directly (through " + "lazy(), as the <Router> root, or outside a route). For an app shell, make it a " + "pathless layout route with children." : ""));
|
|
2598
|
+
};
|
|
2599
|
+
function render(args, routeProps) {
|
|
2600
|
+
// The source may answer a component, a promise of one, or (a live query)
|
|
2601
|
+
// successive components; `dynamic`'s memo lands each the same way.
|
|
2602
|
+
const View = dynamic(() => call(args()));
|
|
2603
|
+
return createComponent(View, {
|
|
2604
|
+
get children() {
|
|
2605
|
+
return routeProps.children;
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
route[SERVER_ROUTE] = {
|
|
2610
|
+
call,
|
|
2611
|
+
render
|
|
2612
|
+
};
|
|
2613
|
+
return route;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2504
2616
|
const LocationHeader = "Location";
|
|
2505
2617
|
const PRELOAD_TIMEOUT$1 = 5000;
|
|
2506
2618
|
const CACHE_TIMEOUT = 180000;
|
|
@@ -2853,91 +2965,6 @@ function isPlainObject(obj) {
|
|
|
2853
2965
|
return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype);
|
|
2854
2966
|
}
|
|
2855
2967
|
|
|
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
|
-
|
|
2941
2968
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
2942
2969
|
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
2943
2970
|
const invokeSymbol = Symbol("routerActionInvoke");
|
|
@@ -3569,10 +3596,12 @@ function push(ch, value) {
|
|
|
3569
3596
|
// connection is alive and is itself the freshness mechanism — a
|
|
3570
3597
|
// push-driven producer yields the mutated state on its own, and tearing
|
|
3571
3598
|
// down healthy connections on every mutation defeats the model.
|
|
3572
|
-
// -
|
|
3573
|
-
//
|
|
3574
|
-
//
|
|
3575
|
-
// for
|
|
3599
|
+
// - A single-flight payload carrying a live key delivers INTO the open
|
|
3600
|
+
// channel rather than reconnecting it. The router's own collector does not
|
|
3601
|
+
// produce live keys (the stream is the freshness mechanism; collection
|
|
3602
|
+
// for live server components is an open question — a mutation's region
|
|
3603
|
+
// would outrank the standing frame stream under the transport's version
|
|
3604
|
+
// policy), so this path serves an integration that collects them itself.
|
|
3576
3605
|
let hooked = false;
|
|
3577
3606
|
function hookRevalidate() {
|
|
3578
3607
|
if (hooked) return;
|
|
@@ -3618,11 +3647,11 @@ function hookRevalidate() {
|
|
|
3618
3647
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
3619
3648
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
3620
3649
|
* a first-connect failure.
|
|
3621
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
3622
|
-
*
|
|
3623
|
-
*
|
|
3624
|
-
*
|
|
3625
|
-
*
|
|
3650
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
3651
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
3652
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
3653
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
3654
|
+
* live key an integration collects itself.)
|
|
3626
3655
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
3627
3656
|
* it open through the preload window), so navigation renders against an
|
|
3628
3657
|
* already-connected stream instead of holding the transition on connect
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { RoutePaths } from "../paths.js";
|
|
3
|
-
import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, ValidFilters } from "../types.js";
|
|
3
|
+
import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, TypedPath, ValidFilters } from "../types.js";
|
|
4
4
|
import type { RouterHistory } from "./history.js";
|
|
5
5
|
/**
|
|
6
6
|
* Identity helper that preserves literal types when the route tree is
|
|
@@ -116,6 +116,15 @@ export interface RouterInstance<R extends readonly RouteDefinition[] = RouteDefi
|
|
|
116
116
|
readonly config: RouterConfig<R>;
|
|
117
117
|
/** Pure matching against an arbitrary URL — no rendering or request context involved. Root→leaf; `[]` when nothing matches. */
|
|
118
118
|
match(url: string): OutputMatch[];
|
|
119
|
+
/**
|
|
120
|
+
* The query keys of the server component routes a URL shows, root→leaf —
|
|
121
|
+
* what to name in `reload`/`respond`/`redirect`'s `revalidate` (or the
|
|
122
|
+
* client's `revalidate()`) to refetch those routes and nothing else. Route
|
|
123
|
+
* keys, not addresses: the app's `query` key for a hand-written route, the
|
|
124
|
+
* file for a file-system route. Pure matching, so it works in a server
|
|
125
|
+
* action; a `paths` node is accepted.
|
|
126
|
+
*/
|
|
127
|
+
keysFor(url: string | TypedPath): string[];
|
|
119
128
|
}
|
|
120
129
|
export declare function createRouter<const R extends readonly RouteDefinition[]>(config: RouterConfig<R>): RouterInstance<R>;
|
|
121
130
|
export {};
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -7,7 +7,8 @@ import { DEV, OBSERVE } from "solid-js";
|
|
|
7
7
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
8
8
|
import { setupLinkClaims } from "../claims.js";
|
|
9
9
|
import { setupNativeEvents } from "../data/events.js";
|
|
10
|
-
import { createPathsProxy } from "../paths.js";
|
|
10
|
+
import { createPathsProxy, HREF } from "../paths.js";
|
|
11
|
+
import { serverRouteOf } from "../serverRouteShared.js";
|
|
11
12
|
import { createBranches, createRouterContext, getRouteMatches, mergeParams, registerFlightRouter, resolveLocationWrite, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
|
|
12
13
|
import { mockBase } from "../utils.js";
|
|
13
14
|
import { Root, Routes } from "./components.jsx";
|
|
@@ -186,6 +187,8 @@ export function createRouter(config) {
|
|
|
186
187
|
};
|
|
187
188
|
const renderPath = (config.history && config.history.utils && config.history.utils.renderPath) || undefined;
|
|
188
189
|
const matchPath = (pathname) => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
190
|
+
// a `paths` node carries its logical pathname under the Href brand
|
|
191
|
+
const pathnameOf = (url) => typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
189
192
|
function RouterComponent(props) {
|
|
190
193
|
// One router per app: the session (location, history, delegation, link
|
|
191
194
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -235,13 +238,22 @@ export function createRouter(config) {
|
|
|
235
238
|
routes: config.routes,
|
|
236
239
|
config,
|
|
237
240
|
match(url) {
|
|
238
|
-
return matchPath(
|
|
241
|
+
return matchPath(pathnameOf(url)).map(({ route, path, params }) => ({
|
|
239
242
|
path: route.originalPath,
|
|
240
243
|
pattern: route.pattern,
|
|
241
244
|
match: path,
|
|
242
245
|
params,
|
|
243
246
|
info: route.info
|
|
244
247
|
}));
|
|
248
|
+
},
|
|
249
|
+
keysFor(url) {
|
|
250
|
+
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
251
|
+
const keys = [];
|
|
252
|
+
for (const { route } of matchPath(pathname)) {
|
|
253
|
+
const key = serverRouteOf(route.component)?.call?.key;
|
|
254
|
+
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
255
|
+
}
|
|
256
|
+
return keys;
|
|
245
257
|
}
|
|
246
258
|
});
|
|
247
259
|
// Built on first access (a getter via Object.assign would run during the
|
package/dist/routing.js
CHANGED
|
@@ -4,7 +4,7 @@ import { runWithOwner } from "solid-js";
|
|
|
4
4
|
import { DEV } from "solid-js";
|
|
5
5
|
import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
|
|
6
6
|
import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
|
|
7
|
-
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
7
|
+
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
9
9
|
import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
|
|
10
10
|
const MAX_REDIRECTS = 100;
|
|
@@ -197,9 +197,7 @@ export function useSearchParams(path) {
|
|
|
197
197
|
const schema = match.route.key.search;
|
|
198
198
|
if (!schema)
|
|
199
199
|
continue;
|
|
200
|
-
const outcome = schema
|
|
201
|
-
if (outcome instanceof Promise)
|
|
202
|
-
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
200
|
+
const outcome = validateSearch(schema, raw);
|
|
203
201
|
if (!outcome.issues)
|
|
204
202
|
result = Object.assign(result || { ...raw }, outcome.value);
|
|
205
203
|
}
|
|
@@ -881,31 +879,51 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
881
879
|
return ((to, options) => navigateFromRoute(route, to, options));
|
|
882
880
|
}
|
|
883
881
|
function preloadRoute(url, preloadData) {
|
|
884
|
-
const
|
|
882
|
+
const next = getRouteMatches(branches(), url.pathname);
|
|
885
883
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
886
884
|
// component.preload (below) kicks the table load; once it lands,
|
|
887
885
|
// preload again so the real inner routes warm too. Preloads are
|
|
888
886
|
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
889
887
|
// here — the real navigation surfaces and retries it.
|
|
890
|
-
const boundary =
|
|
888
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
891
889
|
if (boundary) {
|
|
892
890
|
try {
|
|
893
891
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
|
|
894
892
|
}
|
|
895
893
|
catch { }
|
|
896
894
|
}
|
|
895
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
896
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
897
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
898
|
+
// already this selective (a matching level is reused and re-reads through
|
|
899
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
900
|
+
let current;
|
|
901
|
+
try {
|
|
902
|
+
current = untrack(matches);
|
|
903
|
+
}
|
|
904
|
+
catch { }
|
|
905
|
+
const query = extractSearchParams(url);
|
|
906
|
+
const inputs = (p, q, s, r) => {
|
|
907
|
+
const a = serverRouteArgs(r, p, q);
|
|
908
|
+
a.search === undefined && (a.search = s);
|
|
909
|
+
return a;
|
|
910
|
+
};
|
|
897
911
|
const prevIntent = preloadIntent;
|
|
898
912
|
preloadIntent = "preload";
|
|
899
|
-
for (let match in
|
|
900
|
-
const { route, params } =
|
|
913
|
+
for (let match in next) {
|
|
914
|
+
const { route, params } = next[match];
|
|
901
915
|
route.component &&
|
|
902
916
|
route.component.preload &&
|
|
903
917
|
route.component.preload();
|
|
904
918
|
const { preload, component } = route;
|
|
919
|
+
const now = current && current[match];
|
|
920
|
+
const unchanged = now &&
|
|
921
|
+
now.route.key === route.key &&
|
|
922
|
+
serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
905
923
|
inPreloadFn = true;
|
|
906
924
|
preloadData &&
|
|
925
|
+
!unchanged &&
|
|
907
926
|
runWithOwner(getContext(), () => {
|
|
908
|
-
const query = extractSearchParams(url);
|
|
909
927
|
// A server component route's data IS its call: warm the same
|
|
910
928
|
// query entry the render will read, under the same derived args.
|
|
911
929
|
const server = serverRouteOf(component);
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import type { Component } from "solid-js";
|
|
2
|
-
import type { Params, RouteSectionProps, ServerRouteFunction } from "./types.js";
|
|
2
|
+
import type { Params, RouteSectionProps, ServerRouteFunction, ServerRouteParams, TypedRouteConfig } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Use a server component as a route (experimental). `
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Use a server component as a route (experimental). `source` is a function
|
|
5
|
+
* of the router-derived {@link ServerRouteArgs} — this route's `params`, and
|
|
6
|
+
* `search` when the route declares a schema — resolving to a server
|
|
7
|
+
* component: typically a `"use server"` function wrapped in `query()` or
|
|
8
|
+
* `liveQuery()`, whose key the app names and revalidates. The router mounts
|
|
9
|
+
* the result with the outlet as `children` and calls the same source under
|
|
10
|
+
* preload intent, so link hover and single-flight collection warm it.
|
|
10
11
|
*
|
|
11
12
|
* `children` is the only client position the router fills, so the server
|
|
12
13
|
* component may declare no other. One that takes client handlers, refs, or
|
|
13
14
|
* slots has a client half — write an ordinary route component for it.
|
|
14
15
|
*
|
|
15
16
|
* ```ts
|
|
16
|
-
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
17
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
17
18
|
* ```
|
|
18
19
|
*/
|
|
19
|
-
export declare function serverRouteComponent<P extends Params = Params, S = undefined>(
|
|
20
|
+
export declare function serverRouteComponent<P extends Params | TypedRouteConfig = Params, S = undefined>(source: ServerRouteFunction<P, S> | ((...args: never[]) => ReturnType<ServerRouteFunction<P, S>>)): Component<RouteSectionProps<unknown, ServerRouteParams<P>>>;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
// Server component routes (experimental — rides the experimental server
|
|
2
2
|
// components surface in @solidjs/web; the arg shape may change).
|
|
3
3
|
//
|
|
4
|
-
// `serverRouteComponent(
|
|
5
|
-
// component into a route `component`. It replaces the client
|
|
6
|
-
// server-component route used to need:
|
|
4
|
+
// `serverRouteComponent(source)` turns a function of route arguments that
|
|
5
|
+
// resolves to a component into a route `component`. It replaces the client
|
|
6
|
+
// wrapper a server-component route used to need:
|
|
7
7
|
//
|
|
8
8
|
// // before: a client component exists only to make the call
|
|
9
|
+
// const getStory = query(storyView, "story");
|
|
9
10
|
// component: props => {
|
|
10
11
|
// const View = dynamic(() => getStory(props.params.id));
|
|
11
12
|
// return <View>{props.children}</View>;
|
|
12
13
|
// }
|
|
13
14
|
// // after
|
|
14
|
-
// component: serverRouteComponent(
|
|
15
|
+
// component: serverRouteComponent(query(storyView, "story"))
|
|
15
16
|
//
|
|
16
|
-
// The
|
|
17
|
-
// (
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// is
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
17
|
+
// The source is the app's: `query(fn, key)` for a request/response server
|
|
18
|
+
// component, `liveQuery(fn, key)` for one that streams successive versions,
|
|
19
|
+
// or any function of the args. The router does not choose the cache
|
|
20
|
+
// strategy and does not own the key — `revalidate("story")` is the app's,
|
|
21
|
+
// as it is for any query. What the router adds is the URL → call
|
|
22
|
+
// translation: it derives the call's arguments from the match, mounts the
|
|
23
|
+
// resolved component with the outlet as `children`, and calls the same
|
|
24
|
+
// source under preload intent (link hover, `preloadRoute`, the single-flight
|
|
25
|
+
// collector), so the query or live channel is warm before the navigation
|
|
26
|
+
// renders against it.
|
|
24
27
|
//
|
|
25
28
|
// Arguments are derived, not read from a live location: the call's
|
|
26
29
|
// `(function, arguments)` address keys the frame store and the query cache,
|
|
@@ -34,39 +37,47 @@
|
|
|
34
37
|
// named slots — has a client half, and that half is a client component; the
|
|
35
38
|
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
36
39
|
import { dynamic } from "@solidjs/web";
|
|
37
|
-
import { createComponent } from "solid-js";
|
|
38
|
-
import { query } from "./data/query.js";
|
|
40
|
+
import { createComponent, DEV } from "solid-js";
|
|
39
41
|
import { SERVER_ROUTE } from "./serverRouteShared.js";
|
|
40
42
|
/**
|
|
41
|
-
* Use a server component as a route (experimental). `
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* Use a server component as a route (experimental). `source` is a function
|
|
44
|
+
* of the router-derived {@link ServerRouteArgs} — this route's `params`, and
|
|
45
|
+
* `search` when the route declares a schema — resolving to a server
|
|
46
|
+
* component: typically a `"use server"` function wrapped in `query()` or
|
|
47
|
+
* `liveQuery()`, whose key the app names and revalidates. The router mounts
|
|
48
|
+
* the result with the outlet as `children` and calls the same source under
|
|
49
|
+
* preload intent, so link hover and single-flight collection warm it.
|
|
47
50
|
*
|
|
48
51
|
* `children` is the only client position the router fills, so the server
|
|
49
52
|
* component may declare no other. One that takes client handlers, refs, or
|
|
50
53
|
* slots has a client half — write an ordinary route component for it.
|
|
51
54
|
*
|
|
52
55
|
* ```ts
|
|
53
|
-
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(storyView) });
|
|
56
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
54
57
|
* ```
|
|
55
58
|
*/
|
|
56
|
-
export function serverRouteComponent(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
export function serverRouteComponent(
|
|
60
|
+
// A source that ignores its args (an app shell) is `(...args: never[])`
|
|
61
|
+
// once `query()` has typed it; that is the same call, so accept it too.
|
|
62
|
+
source) {
|
|
63
|
+
const call = source;
|
|
64
|
+
// The value is a component only so it fits the `component` field; the
|
|
65
|
+
// router core never calls it — it mounts through the brand, with THIS
|
|
66
|
+
// level's params and the declared search output. A direct mount (a
|
|
67
|
+
// `lazy()` wrapper around the helper, a hand-written `<Route />`) would
|
|
68
|
+
// have only the merged params to call with: the same view under a second
|
|
69
|
+
// address, missing the cache and owning its own frame. Refuse it.
|
|
70
|
+
const route = () => {
|
|
71
|
+
throw new Error("serverRouteComponent(): mount it as a route's `component`" +
|
|
72
|
+
(DEV
|
|
73
|
+
? " — the router derives the call from the match. It was rendered directly (through " +
|
|
74
|
+
"lazy(), as the <Router> root, or outside a route). For an app shell, make it a " +
|
|
75
|
+
"pathless layout route with children."
|
|
76
|
+
: ""));
|
|
77
|
+
};
|
|
69
78
|
function render(args, routeProps) {
|
|
79
|
+
// The source may answer a component, a promise of one, or (a live query)
|
|
80
|
+
// successive components; `dynamic`'s memo lands each the same way.
|
|
70
81
|
const View = dynamic(() => call(args()));
|
|
71
82
|
return createComponent(View, {
|
|
72
83
|
get children() {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { validateSearch } from "./utils.js";
|
|
1
2
|
export const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
2
3
|
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
3
4
|
export function serverRouteOf(component) {
|
|
4
|
-
return
|
|
5
|
-
? component[SERVER_ROUTE]
|
|
6
|
-
: undefined;
|
|
5
|
+
return component?.[SERVER_ROUTE];
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
8
|
* Derive a server route's call arguments from a match: the match's params
|
|
@@ -13,31 +12,23 @@ export function serverRouteOf(component) {
|
|
|
13
12
|
*/
|
|
14
13
|
export function serverRouteArgs(route, params, query) {
|
|
15
14
|
const schema = route.key?.search;
|
|
16
|
-
let search
|
|
15
|
+
let search;
|
|
17
16
|
if (schema) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
17
|
+
const raw = { ...query };
|
|
18
|
+
const outcome = validateSearch(schema, raw);
|
|
21
19
|
// Issues leave the raw values in place, matching useSearchParams: search
|
|
22
20
|
// strings are user input, so defaults belong in the schema itself.
|
|
23
|
-
search = outcome.issues ?
|
|
21
|
+
search = outcome.issues ? raw : outcome.value;
|
|
24
22
|
}
|
|
25
23
|
return { params: { ...params }, search };
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
}
|
|
25
|
+
const shallowEqual = (a, b) => a === b ||
|
|
26
|
+
(!!a &&
|
|
27
|
+
!!b &&
|
|
28
|
+
typeof a === "object" &&
|
|
29
|
+
typeof b === "object" &&
|
|
30
|
+
Object.keys(a).length === Object.keys(b).length &&
|
|
31
|
+
Object.keys(a).every(k => a[k] === b[k]));
|
|
41
32
|
/**
|
|
42
33
|
* Structural equality for the args memo: a navigation produces fresh match
|
|
43
34
|
* objects even when this level's params are unchanged, and an equal call
|
package/dist/types.d.ts
CHANGED
|
@@ -146,19 +146,31 @@ export type RouteSectionComponent<T = unknown, P extends Params = Params> = Comp
|
|
|
146
146
|
* the route's pattern (and its ancestors') declares, plus — only when the
|
|
147
147
|
* route declares a `search` schema — that schema's validated output.
|
|
148
148
|
*/
|
|
149
|
-
export interface ServerRouteArgs<P extends Params = Params, S = undefined> {
|
|
150
|
-
params: P
|
|
151
|
-
search: S;
|
|
149
|
+
export interface ServerRouteArgs<P extends Params | TypedRouteConfig = Params, S = undefined> {
|
|
150
|
+
params: ServerRouteParams<P>;
|
|
151
|
+
search: P extends TypedRouteConfig ? SearchOutputOf<P> : S;
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
|
|
154
|
+
* `ServerRouteArgs<typeof route>`: a `defineFileRoute` config is a witness —
|
|
155
|
+
* params from its pattern, search from its schema — the way
|
|
156
|
+
* `RouteProps<typeof route>` reads it for a client page.
|
|
157
|
+
*/
|
|
158
|
+
export type ServerRouteParams<P> = P extends TypedRouteConfig<infer Pattern> ? RouteParams<Pattern> : Extract<P, Params>;
|
|
159
|
+
type SearchOutputOf<Def> = Def extends {
|
|
160
|
+
search: infer Sch extends StandardSchemaV1<any, any>;
|
|
161
|
+
} ? NonNullable<Sch["~standard"]["types"]>["output"] : undefined;
|
|
162
|
+
/**
|
|
163
|
+
* The shape `serverRouteComponent()` accepts (experimental): a function of
|
|
164
|
+
* the router-derived {@link ServerRouteArgs} answering a server component —
|
|
165
|
+
* once (a `query`-wrapped `"use server"` function), or as successive
|
|
166
|
+
* versions (a `liveQuery`). The component's only client position is
|
|
167
|
+
* `children` — the route outlet, which the router fills.
|
|
158
168
|
*/
|
|
159
|
-
export type ServerRouteFunction<P extends Params = Params, S = undefined> = (args: ServerRouteArgs<P, S>) => Promise<
|
|
169
|
+
export type ServerRouteFunction<P extends Params | TypedRouteConfig = Params, S = undefined> = (args: ServerRouteArgs<P, S>) => ServerRouteView | Promise<ServerRouteView> | AsyncIterable<ServerRouteView>;
|
|
170
|
+
/** The component a server route resolves to: `children` is its only client position. */
|
|
171
|
+
export type ServerRouteView = Component<{
|
|
160
172
|
children?: JSX.Element;
|
|
161
|
-
}
|
|
173
|
+
}>;
|
|
162
174
|
declare const ROUTE_PATTERN: unique symbol;
|
|
163
175
|
export interface TypedRouteConfig<S extends string = string> {
|
|
164
176
|
readonly [ROUTE_PATTERN]: S;
|
|
@@ -354,7 +366,8 @@ export type Submission<T, U> = {
|
|
|
354
366
|
readonly error: any;
|
|
355
367
|
readonly url: string;
|
|
356
368
|
clear: () => void;
|
|
357
|
-
|
|
369
|
+
/** Re-run the action with the same input; resolves to its result, like the original call. */
|
|
370
|
+
retry: () => Promise<U>;
|
|
358
371
|
};
|
|
359
372
|
export interface MaybePreloadableComponent extends Component {
|
|
360
373
|
preload?: () => void;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
|
|
1
|
+
import type { StandardSchemaV1, MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
|
|
2
2
|
export declare const mockBase = "http://sr";
|
|
3
3
|
export declare function normalizePath(path: string, omitSlash?: boolean): string;
|
|
4
4
|
/** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
|
|
@@ -6,6 +6,8 @@ export declare const comparablePath: (path: string) => string;
|
|
|
6
6
|
export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
|
|
7
7
|
export declare function invariant<T>(value: T | null | undefined, message: string): T;
|
|
8
8
|
export declare function joinPaths(from: string, to: string): string;
|
|
9
|
+
/** Run a Standard Schema synchronously — the only mode search params support. */
|
|
10
|
+
export declare function validateSearch(schema: StandardSchemaV1<any, any>, raw: Record<string, any>): import("./types.js").StandardSchemaResult<any>;
|
|
9
11
|
export declare function extractSearchParams(url: URL): SearchParams;
|
|
10
12
|
export declare function createMatcher<S extends string>(path: S, partial?: boolean, matchFilters?: MatchFilters<S>): (location: string) => PathMatch | null;
|
|
11
13
|
export declare function scoreRoute(route: RouteDescription): number;
|
package/dist/utils.js
CHANGED
|
@@ -35,6 +35,13 @@ export function invariant(value, message) {
|
|
|
35
35
|
export function joinPaths(from, to) {
|
|
36
36
|
return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
|
|
37
37
|
}
|
|
38
|
+
/** Run a Standard Schema synchronously — the only mode search params support. */
|
|
39
|
+
export function validateSearch(schema, raw) {
|
|
40
|
+
const outcome = schema["~standard"].validate(raw);
|
|
41
|
+
if (outcome instanceof Promise)
|
|
42
|
+
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
43
|
+
return outcome;
|
|
44
|
+
}
|
|
38
45
|
export function extractSearchParams(url) {
|
|
39
46
|
const params = {};
|
|
40
47
|
url.searchParams.forEach((value, key) => {
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"Ryan Turnquist"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"version": "2.0.0-next.
|
|
9
|
+
"version": "2.0.0-next.28",
|
|
10
10
|
"homepage": "https://github.com/solidjs/solid-router#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -52,10 +52,16 @@
|
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@solidjs/web": "^2.0.0-rc.9",
|
|
55
|
+
"filesystem-routing": ">=0.4.0",
|
|
55
56
|
"solid-js": "^2.0.0-rc.9"
|
|
56
57
|
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"filesystem-routing": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
57
63
|
"scripts": {
|
|
58
|
-
"build": "rm -rf dist && tsc && rollup -c",
|
|
64
|
+
"build": "rm -rf dist && tsc && rollup -c && node scripts/check-fs-gate.mjs",
|
|
59
65
|
"test": "vitest run && vitest run --config vitest.config.server.ts && npm run test:types",
|
|
60
66
|
"test:watch": "vitest",
|
|
61
67
|
"test:server": "vitest run --config vitest.config.server.ts",
|