@solidjs/router 2.0.0-next.27 → 2.0.0-next.29
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/action.js +10 -7
- 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 +160 -126
- 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/action.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
2
|
import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
|
|
3
3
|
import { createServerReference, decodeRedirectHeaderValue, decodeResponsePayload, parseServerFunctionActionUrl, REDIRECT_HEADER, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
|
-
// The explicit /server specifier is safe here: the only call site is
|
|
5
|
-
// server-guarded, so client builds tree-shake the codec away.
|
|
6
|
-
import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
|
|
7
4
|
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
8
5
|
import { setRouterFormHandler } from "./events.js";
|
|
9
6
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
@@ -116,10 +113,16 @@ function installRouterIntegrations() {
|
|
|
116
113
|
return;
|
|
117
114
|
integrationsInstalled = true;
|
|
118
115
|
if (isServer) {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
116
|
+
// The codec lives behind the runtime's SERVER entry, which must stay off
|
|
117
|
+
// the client module graph entirely — a static import here shipped it to
|
|
118
|
+
// every Vite dev page (#616; prod tree-shook it, dev doesn't shake). The
|
|
119
|
+
// decoder slot must still fill synchronously (a cold-start POST-redirect-
|
|
120
|
+
// GET reads submissions on the very first render), so install a wrapper
|
|
121
|
+
// and let the first decode pull the module: the codec is async anyway
|
|
122
|
+
// (encrypted cookie, @solidjs/web 2.0.0-rc.7+) and the seeding read
|
|
123
|
+
// already parks on the decode promise; a failed load reads as "no flash",
|
|
124
|
+
// the runtime's own malformed-cookie semantics.
|
|
125
|
+
provideFlashDecoder(cookieHeader => import("@solidjs/web/server-functions/server").then(m => m.decodeFlashCookie(cookieHeader)));
|
|
123
126
|
}
|
|
124
127
|
else {
|
|
125
128
|
setRouterFormHandler(handleFormAction);
|
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,7 +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
|
|
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
|
-
import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
|
|
5
4
|
|
|
6
5
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
7
6
|
const trimPathRegex = /^\/+|(\/)\/+$/g;
|
|
@@ -38,6 +37,13 @@ function invariant(value, message) {
|
|
|
38
37
|
function joinPaths(from, to) {
|
|
39
38
|
return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
|
|
40
39
|
}
|
|
40
|
+
|
|
41
|
+
/** Run a Standard Schema synchronously — the only mode search params support. */
|
|
42
|
+
function validateSearch(schema, raw) {
|
|
43
|
+
const outcome = schema["~standard"].validate(raw);
|
|
44
|
+
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
45
|
+
return outcome;
|
|
46
|
+
}
|
|
41
47
|
function extractSearchParams(url) {
|
|
42
48
|
const params = {};
|
|
43
49
|
url.searchParams.forEach((value, key) => {
|
|
@@ -536,7 +542,7 @@ const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
|
536
542
|
|
|
537
543
|
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
538
544
|
function serverRouteOf(component) {
|
|
539
|
-
return
|
|
545
|
+
return component?.[SERVER_ROUTE];
|
|
540
546
|
}
|
|
541
547
|
|
|
542
548
|
/**
|
|
@@ -547,17 +553,15 @@ function serverRouteOf(component) {
|
|
|
547
553
|
*/
|
|
548
554
|
function serverRouteArgs(route, params, query) {
|
|
549
555
|
const schema = route.key?.search;
|
|
550
|
-
let search
|
|
556
|
+
let search;
|
|
551
557
|
if (schema) {
|
|
552
|
-
const
|
|
558
|
+
const raw = {
|
|
553
559
|
...query
|
|
554
|
-
}
|
|
555
|
-
|
|
560
|
+
};
|
|
561
|
+
const outcome = validateSearch(schema, raw);
|
|
556
562
|
// Issues leave the raw values in place, matching useSearchParams: search
|
|
557
563
|
// strings are user input, so defaults belong in the schema itself.
|
|
558
|
-
search = outcome.issues ?
|
|
559
|
-
...query
|
|
560
|
-
} : outcome.value;
|
|
564
|
+
search = outcome.issues ? raw : outcome.value;
|
|
561
565
|
}
|
|
562
566
|
return {
|
|
563
567
|
params: {
|
|
@@ -566,15 +570,7 @@ function serverRouteArgs(route, params, query) {
|
|
|
566
570
|
search
|
|
567
571
|
};
|
|
568
572
|
}
|
|
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
|
-
}
|
|
573
|
+
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
574
|
|
|
579
575
|
/**
|
|
580
576
|
* Structural equality for the args memo: a navigation produces fresh match
|
|
@@ -830,8 +826,7 @@ function useSearchParams(path) {
|
|
|
830
826
|
for (const match of router.matches()) {
|
|
831
827
|
const schema = match.route.key.search;
|
|
832
828
|
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");
|
|
829
|
+
const outcome = validateSearch(schema, raw);
|
|
835
830
|
if (!outcome.issues) result = Object.assign(result || {
|
|
836
831
|
...raw
|
|
837
832
|
}, outcome.value);
|
|
@@ -1528,33 +1523,49 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1528
1523
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1529
1524
|
}
|
|
1530
1525
|
function preloadRoute(url, preloadData) {
|
|
1531
|
-
const
|
|
1526
|
+
const next = getRouteMatches(branches(), url.pathname);
|
|
1532
1527
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1533
1528
|
// component.preload (below) kicks the table load; once it lands,
|
|
1534
1529
|
// preload again so the real inner routes warm too. Preloads are
|
|
1535
1530
|
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
1536
1531
|
// here — the real navigation surfaces and retries it.
|
|
1537
|
-
const boundary =
|
|
1532
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1538
1533
|
if (boundary) {
|
|
1539
1534
|
try {
|
|
1540
1535
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1541
1536
|
} catch {}
|
|
1542
1537
|
}
|
|
1538
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
1539
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
1540
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
1541
|
+
// already this selective (a matching level is reused and re-reads through
|
|
1542
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
1543
|
+
let current;
|
|
1544
|
+
try {
|
|
1545
|
+
current = untrack(matches);
|
|
1546
|
+
} catch {}
|
|
1547
|
+
const query = extractSearchParams(url);
|
|
1548
|
+
const inputs = (p, q, s, r) => {
|
|
1549
|
+
const a = serverRouteArgs(r, p, q);
|
|
1550
|
+
a.search === undefined && (a.search = s);
|
|
1551
|
+
return a;
|
|
1552
|
+
};
|
|
1543
1553
|
const prevIntent = preloadIntent;
|
|
1544
1554
|
preloadIntent = "preload";
|
|
1545
|
-
for (let match in
|
|
1555
|
+
for (let match in next) {
|
|
1546
1556
|
const {
|
|
1547
1557
|
route,
|
|
1548
1558
|
params
|
|
1549
|
-
} =
|
|
1559
|
+
} = next[match];
|
|
1550
1560
|
route.component && route.component.preload && route.component.preload();
|
|
1551
1561
|
const {
|
|
1552
1562
|
preload,
|
|
1553
1563
|
component
|
|
1554
1564
|
} = route;
|
|
1565
|
+
const now = current && current[match];
|
|
1566
|
+
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1555
1567
|
inPreloadFn = true;
|
|
1556
|
-
preloadData && runWithOwner(getContext(), () => {
|
|
1557
|
-
const query = extractSearchParams(url);
|
|
1568
|
+
preloadData && !unchanged && runWithOwner(getContext(), () => {
|
|
1558
1569
|
// A server component route's data IS its call: warm the same
|
|
1559
1570
|
// query entry the render will read, under the same derived args.
|
|
1560
1571
|
const server = serverRouteOf(component);
|
|
@@ -2341,6 +2352,8 @@ function createRouter(config) {
|
|
|
2341
2352
|
};
|
|
2342
2353
|
const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
|
|
2343
2354
|
const matchPath = pathname => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
2355
|
+
// a `paths` node carries its logical pathname under the Href brand
|
|
2356
|
+
const pathnameOf = url => typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2344
2357
|
function RouterComponent(props) {
|
|
2345
2358
|
// One router per app: the session (location, history, delegation, link
|
|
2346
2359
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -2398,7 +2411,7 @@ function createRouter(config) {
|
|
|
2398
2411
|
routes: config.routes,
|
|
2399
2412
|
config,
|
|
2400
2413
|
match(url) {
|
|
2401
|
-
return matchPath(
|
|
2414
|
+
return matchPath(pathnameOf(url)).map(({
|
|
2402
2415
|
route,
|
|
2403
2416
|
path,
|
|
2404
2417
|
params
|
|
@@ -2409,6 +2422,17 @@ function createRouter(config) {
|
|
|
2409
2422
|
params,
|
|
2410
2423
|
info: route.info
|
|
2411
2424
|
}));
|
|
2425
|
+
},
|
|
2426
|
+
keysFor(url) {
|
|
2427
|
+
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2428
|
+
const keys = [];
|
|
2429
|
+
for (const {
|
|
2430
|
+
route
|
|
2431
|
+
} of matchPath(pathname)) {
|
|
2432
|
+
const key = serverRouteOf(route.component)?.call?.key;
|
|
2433
|
+
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
2434
|
+
}
|
|
2435
|
+
return keys;
|
|
2412
2436
|
}
|
|
2413
2437
|
});
|
|
2414
2438
|
// Built on first access (a getter via Object.assign would run during the
|
|
@@ -2501,6 +2525,93 @@ const useBeforeLeave = listener => {
|
|
|
2501
2525
|
onCleanup(s);
|
|
2502
2526
|
};
|
|
2503
2527
|
|
|
2528
|
+
// Server component routes (experimental — rides the experimental server
|
|
2529
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
2530
|
+
//
|
|
2531
|
+
// `serverRouteComponent(source)` turns a function of route arguments that
|
|
2532
|
+
// resolves to a component into a route `component`. It replaces the client
|
|
2533
|
+
// wrapper a server-component route used to need:
|
|
2534
|
+
//
|
|
2535
|
+
// // before: a client component exists only to make the call
|
|
2536
|
+
// const getStory = query(storyView, "story");
|
|
2537
|
+
// component: props => {
|
|
2538
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
2539
|
+
// return <View>{props.children}</View>;
|
|
2540
|
+
// }
|
|
2541
|
+
// // after
|
|
2542
|
+
// component: serverRouteComponent(query(storyView, "story"))
|
|
2543
|
+
//
|
|
2544
|
+
// The source is the app's: `query(fn, key)` for a request/response server
|
|
2545
|
+
// component, `liveQuery(fn, key)` for one that streams successive versions,
|
|
2546
|
+
// or any function of the args. The router does not choose the cache
|
|
2547
|
+
// strategy and does not own the key — `revalidate("story")` is the app's,
|
|
2548
|
+
// as it is for any query. What the router adds is the URL → call
|
|
2549
|
+
// translation: it derives the call's arguments from the match, mounts the
|
|
2550
|
+
// resolved component with the outlet as `children`, and calls the same
|
|
2551
|
+
// source under preload intent (link hover, `preloadRoute`, the single-flight
|
|
2552
|
+
// collector), so the query or live channel is warm before the navigation
|
|
2553
|
+
// renders against it.
|
|
2554
|
+
//
|
|
2555
|
+
// Arguments are derived, not read from a live location: the call's
|
|
2556
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
2557
|
+
// so the args must name precisely what the route depends on. They are the
|
|
2558
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
2559
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
2560
|
+
// when the route declares a `search` schema, its validated output.
|
|
2561
|
+
//
|
|
2562
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
2563
|
+
// A server component that takes other client positions — handlers, refs,
|
|
2564
|
+
// named slots — has a client half, and that half is a client component; the
|
|
2565
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
2566
|
+
/**
|
|
2567
|
+
* Use a server component as a route (experimental). `source` is a function
|
|
2568
|
+
* of the router-derived {@link ServerRouteArgs} — this route's `params`, and
|
|
2569
|
+
* `search` when the route declares a schema — resolving to a server
|
|
2570
|
+
* component: typically a `"use server"` function wrapped in `query()` or
|
|
2571
|
+
* `liveQuery()`, whose key the app names and revalidates. The router mounts
|
|
2572
|
+
* the result with the outlet as `children` and calls the same source under
|
|
2573
|
+
* preload intent, so link hover and single-flight collection warm it.
|
|
2574
|
+
*
|
|
2575
|
+
* `children` is the only client position the router fills, so the server
|
|
2576
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
2577
|
+
* slots has a client half — write an ordinary route component for it.
|
|
2578
|
+
*
|
|
2579
|
+
* ```ts
|
|
2580
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
2581
|
+
* ```
|
|
2582
|
+
*/
|
|
2583
|
+
function serverRouteComponent(
|
|
2584
|
+
// A source that ignores its args (an app shell) is `(...args: never[])`
|
|
2585
|
+
// once `query()` has typed it; that is the same call, so accept it too.
|
|
2586
|
+
source) {
|
|
2587
|
+
const call = source;
|
|
2588
|
+
|
|
2589
|
+
// The value is a component only so it fits the `component` field; the
|
|
2590
|
+
// router core never calls it — it mounts through the brand, with THIS
|
|
2591
|
+
// level's params and the declared search output. A direct mount (a
|
|
2592
|
+
// `lazy()` wrapper around the helper, a hand-written `<Route />`) would
|
|
2593
|
+
// have only the merged params to call with: the same view under a second
|
|
2594
|
+
// address, missing the cache and owning its own frame. Refuse it.
|
|
2595
|
+
const route = () => {
|
|
2596
|
+
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." : ""));
|
|
2597
|
+
};
|
|
2598
|
+
function render(args, routeProps) {
|
|
2599
|
+
// The source may answer a component, a promise of one, or (a live query)
|
|
2600
|
+
// successive components; `dynamic`'s memo lands each the same way.
|
|
2601
|
+
const View = dynamic(() => call(args()));
|
|
2602
|
+
return createComponent(View, {
|
|
2603
|
+
get children() {
|
|
2604
|
+
return routeProps.children;
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
route[SERVER_ROUTE] = {
|
|
2609
|
+
call,
|
|
2610
|
+
render
|
|
2611
|
+
};
|
|
2612
|
+
return route;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2504
2615
|
const LocationHeader = "Location";
|
|
2505
2616
|
const PRELOAD_TIMEOUT$1 = 5000;
|
|
2506
2617
|
const CACHE_TIMEOUT = 180000;
|
|
@@ -2853,91 +2964,6 @@ function isPlainObject(obj) {
|
|
|
2853
2964
|
return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype);
|
|
2854
2965
|
}
|
|
2855
2966
|
|
|
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
2967
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
2942
2968
|
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
2943
2969
|
const invokeSymbol = Symbol("routerActionInvoke");
|
|
@@ -3048,10 +3074,16 @@ function installRouterIntegrations() {
|
|
|
3048
3074
|
if (integrationsInstalled) return;
|
|
3049
3075
|
integrationsInstalled = true;
|
|
3050
3076
|
if (isServer) {
|
|
3051
|
-
//
|
|
3052
|
-
//
|
|
3053
|
-
//
|
|
3054
|
-
|
|
3077
|
+
// The codec lives behind the runtime's SERVER entry, which must stay off
|
|
3078
|
+
// the client module graph entirely — a static import here shipped it to
|
|
3079
|
+
// every Vite dev page (#616; prod tree-shook it, dev doesn't shake). The
|
|
3080
|
+
// decoder slot must still fill synchronously (a cold-start POST-redirect-
|
|
3081
|
+
// GET reads submissions on the very first render), so install a wrapper
|
|
3082
|
+
// and let the first decode pull the module: the codec is async anyway
|
|
3083
|
+
// (encrypted cookie, @solidjs/web 2.0.0-rc.7+) and the seeding read
|
|
3084
|
+
// already parks on the decode promise; a failed load reads as "no flash",
|
|
3085
|
+
// the runtime's own malformed-cookie semantics.
|
|
3086
|
+
provideFlashDecoder(cookieHeader => import('@solidjs/web/server-functions/server').then(m => m.decodeFlashCookie(cookieHeader)));
|
|
3055
3087
|
} else {
|
|
3056
3088
|
setRouterFormHandler(handleFormAction);
|
|
3057
3089
|
provideFlightConsumer(setupFlightDataConsumer);
|
|
@@ -3569,10 +3601,12 @@ function push(ch, value) {
|
|
|
3569
3601
|
// connection is alive and is itself the freshness mechanism — a
|
|
3570
3602
|
// push-driven producer yields the mutated state on its own, and tearing
|
|
3571
3603
|
// down healthy connections on every mutation defeats the model.
|
|
3572
|
-
// -
|
|
3573
|
-
//
|
|
3574
|
-
//
|
|
3575
|
-
// for
|
|
3604
|
+
// - A single-flight payload carrying a live key delivers INTO the open
|
|
3605
|
+
// channel rather than reconnecting it. The router's own collector does not
|
|
3606
|
+
// produce live keys (the stream is the freshness mechanism; collection
|
|
3607
|
+
// for live server components is an open question — a mutation's region
|
|
3608
|
+
// would outrank the standing frame stream under the transport's version
|
|
3609
|
+
// policy), so this path serves an integration that collects them itself.
|
|
3576
3610
|
let hooked = false;
|
|
3577
3611
|
function hookRevalidate() {
|
|
3578
3612
|
if (hooked) return;
|
|
@@ -3618,11 +3652,11 @@ function hookRevalidate() {
|
|
|
3618
3652
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
3619
3653
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
3620
3654
|
* a first-connect failure.
|
|
3621
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
3622
|
-
*
|
|
3623
|
-
*
|
|
3624
|
-
*
|
|
3625
|
-
*
|
|
3655
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
3656
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
3657
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
3658
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
3659
|
+
* live key an integration collects itself.)
|
|
3626
3660
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
3627
3661
|
* it open through the preload window), so navigation renders against an
|
|
3628
3662
|
* 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.29",
|
|
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",
|