@solidjs/router 2.0.0-next.26 → 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 +168 -70
- 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 +2 -1
- package/dist/index.js +247 -38
- package/dist/index.jsx +1 -0
- package/dist/routers/components.jsx +4 -0
- package/dist/routers/factory.d.ts +10 -1
- package/dist/routers/factory.jsx +14 -2
- package/dist/routing.js +65 -26
- package/dist/server.js +6 -0
- package/dist/serverRouteComponent.d.ts +20 -0
- package/dist/serverRouteComponent.js +90 -0
- package/dist/serverRouteShared.d.ts +33 -0
- package/dist/serverRouteShared.js +39 -0
- package/dist/types.d.ts +41 -4
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +7 -0
- package/package.json +8 -2
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { provideRequestEvent } from "@solidjs/web/storage";
|
|
25
25
|
import { createBranches, getRouteMatches, mergeParams, peekLazySubtrees, resolveLazySubtree } from "./routing.js";
|
|
26
26
|
import { extractSearchParams } from "./utils.js";
|
|
27
|
+
import { serverRouteArgs, serverRouteOf } from "./serverRouteShared.js";
|
|
27
28
|
// the instance is the provider component, so it (unlike an options object) is a function
|
|
28
29
|
function isRouterInstance(options) {
|
|
29
30
|
return typeof options === "function";
|
|
@@ -146,6 +147,11 @@ function runPreloads(event, branches, url, previousUrl, rootPreload) {
|
|
|
146
147
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
147
148
|
event.router.dataOnly = true;
|
|
148
149
|
const { route, params } = matches[match];
|
|
150
|
+
// A server component route's markup is collected like any query result:
|
|
151
|
+
// the same call (function id + derived args) the client is showing, so
|
|
152
|
+
// the response's region addresses what is mounted.
|
|
153
|
+
const server = serverRouteOf(route.component);
|
|
154
|
+
server && server.call(serverRouteArgs(route, params, location.query));
|
|
149
155
|
route.preload &&
|
|
150
156
|
route.preload({
|
|
151
157
|
params,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Component } from "solid-js";
|
|
2
|
+
import type { Params, RouteSectionProps, ServerRouteFunction, ServerRouteParams, TypedRouteConfig } from "./types.js";
|
|
3
|
+
/**
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* `children` is the only client position the router fills, so the server
|
|
13
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
14
|
+
* slots has a client half — write an ordinary route component for it.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
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>>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Server component routes (experimental — rides the experimental server
|
|
2
|
+
// components surface in @solidjs/web; the arg shape may change).
|
|
3
|
+
//
|
|
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
|
+
//
|
|
8
|
+
// // before: a client component exists only to make the call
|
|
9
|
+
// const getStory = query(storyView, "story");
|
|
10
|
+
// component: props => {
|
|
11
|
+
// const View = dynamic(() => getStory(props.params.id));
|
|
12
|
+
// return <View>{props.children}</View>;
|
|
13
|
+
// }
|
|
14
|
+
// // after
|
|
15
|
+
// component: serverRouteComponent(query(storyView, "story"))
|
|
16
|
+
//
|
|
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.
|
|
27
|
+
//
|
|
28
|
+
// Arguments are derived, not read from a live location: the call's
|
|
29
|
+
// `(function, arguments)` address keys the frame store and the query cache,
|
|
30
|
+
// so the args must name precisely what the route depends on. They are the
|
|
31
|
+
// params this route's pattern (with its ancestors') declares — never a
|
|
32
|
+
// child's, so a layout does not refetch when a leaf param changes — and, only
|
|
33
|
+
// when the route declares a `search` schema, its validated output.
|
|
34
|
+
//
|
|
35
|
+
// The router fills exactly one client position: `children`, with the outlet.
|
|
36
|
+
// A server component that takes other client positions — handlers, refs,
|
|
37
|
+
// named slots — has a client half, and that half is a client component; the
|
|
38
|
+
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
39
|
+
import { dynamic } from "@solidjs/web";
|
|
40
|
+
import { createComponent, DEV } from "solid-js";
|
|
41
|
+
import { SERVER_ROUTE } from "./serverRouteShared.js";
|
|
42
|
+
/**
|
|
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.
|
|
50
|
+
*
|
|
51
|
+
* `children` is the only client position the router fills, so the server
|
|
52
|
+
* component may declare no other. One that takes client handlers, refs, or
|
|
53
|
+
* slots has a client half — write an ordinary route component for it.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
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
|
+
};
|
|
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.
|
|
81
|
+
const View = dynamic(() => call(args()));
|
|
82
|
+
return createComponent(View, {
|
|
83
|
+
get children() {
|
|
84
|
+
return routeProps.children;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
route[SERVER_ROUTE] = { call, render };
|
|
89
|
+
return route;
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Accessor } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import type { Params, RouteDescription, RouteSectionProps, SearchParams, ServerRouteArgs } from "./types.js";
|
|
4
|
+
export declare const SERVER_ROUTE: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* What `serverRouteComponent()` attaches to the route component it returns, and
|
|
7
|
+
* what the router core drives it through: `call` is the query-wrapped server
|
|
8
|
+
* function (preload and single-flight collection warm/collect through it),
|
|
9
|
+
* `render` mounts the resolved server component for the current args with
|
|
10
|
+
* the route's props (`children` is the outlet).
|
|
11
|
+
*/
|
|
12
|
+
export interface ServerRouteBrand {
|
|
13
|
+
call: (args: ServerRouteArgs<Params, unknown>) => unknown;
|
|
14
|
+
render: (args: Accessor<ServerRouteArgs<Params, unknown>>, route: RouteSectionProps) => JSX.Element;
|
|
15
|
+
}
|
|
16
|
+
export type BrandedRouteComponent = Function & {
|
|
17
|
+
[SERVER_ROUTE]?: ServerRouteBrand;
|
|
18
|
+
};
|
|
19
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
20
|
+
export declare function serverRouteOf(component: unknown): ServerRouteBrand | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
23
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
24
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
25
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
26
|
+
*/
|
|
27
|
+
export declare function serverRouteArgs(route: RouteDescription, params: Params, query: SearchParams): ServerRouteArgs<Params, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
30
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
31
|
+
* must not re-enter the source (and refetch).
|
|
32
|
+
*/
|
|
33
|
+
export declare function serverRouteArgsEqual(a: ServerRouteArgs<Params, unknown>, b: ServerRouteArgs<Params, unknown>): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { validateSearch } from "./utils.js";
|
|
2
|
+
export const SERVER_ROUTE = Symbol("solid-router.serverRoute");
|
|
3
|
+
/** The brand carried by a route component built with `serverRouteComponent()`, if any. */
|
|
4
|
+
export function serverRouteOf(component) {
|
|
5
|
+
return component?.[SERVER_ROUTE];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Derive a server route's call arguments from a match: the match's params
|
|
9
|
+
* (this route's pattern and its ancestors' — never a child's) and the route's
|
|
10
|
+
* `search` schema output when one is declared. Reads `query` only when a
|
|
11
|
+
* schema exists, so a search-agnostic route never tracks the query string.
|
|
12
|
+
*/
|
|
13
|
+
export function serverRouteArgs(route, params, query) {
|
|
14
|
+
const schema = route.key?.search;
|
|
15
|
+
let search;
|
|
16
|
+
if (schema) {
|
|
17
|
+
const raw = { ...query };
|
|
18
|
+
const outcome = validateSearch(schema, raw);
|
|
19
|
+
// Issues leave the raw values in place, matching useSearchParams: search
|
|
20
|
+
// strings are user input, so defaults belong in the schema itself.
|
|
21
|
+
search = outcome.issues ? raw : outcome.value;
|
|
22
|
+
}
|
|
23
|
+
return { params: { ...params }, search };
|
|
24
|
+
}
|
|
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]));
|
|
32
|
+
/**
|
|
33
|
+
* Structural equality for the args memo: a navigation produces fresh match
|
|
34
|
+
* objects even when this level's params are unchanged, and an equal call
|
|
35
|
+
* must not re-enter the source (and refetch).
|
|
36
|
+
*/
|
|
37
|
+
export function serverRouteArgsEqual(a, b) {
|
|
38
|
+
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
39
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -141,6 +141,36 @@ export interface RouteSectionProps<T = unknown, P extends Params = Params> {
|
|
|
141
141
|
children?: JSX.Element;
|
|
142
142
|
}
|
|
143
143
|
export type RouteSectionComponent<T = unknown, P extends Params = Params> = Component<RouteSectionProps<T, P>> | Component<Omit<RouteSectionProps<T, P>, "children">> | Component<{}>;
|
|
144
|
+
/**
|
|
145
|
+
* What a server component route is called with (experimental): the params
|
|
146
|
+
* the route's pattern (and its ancestors') declares, plus — only when the
|
|
147
|
+
* route declares a `search` schema — that schema's validated output.
|
|
148
|
+
*/
|
|
149
|
+
export interface ServerRouteArgs<P extends Params | TypedRouteConfig = Params, S = undefined> {
|
|
150
|
+
params: ServerRouteParams<P>;
|
|
151
|
+
search: P extends TypedRouteConfig ? SearchOutputOf<P> : S;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
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.
|
|
168
|
+
*/
|
|
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<{
|
|
172
|
+
children?: JSX.Element;
|
|
173
|
+
}>;
|
|
144
174
|
declare const ROUTE_PATTERN: unique symbol;
|
|
145
175
|
export interface TypedRouteConfig<S extends string = string> {
|
|
146
176
|
readonly [ROUTE_PATTERN]: S;
|
|
@@ -336,12 +366,19 @@ export type Submission<T, U> = {
|
|
|
336
366
|
readonly error: any;
|
|
337
367
|
readonly url: string;
|
|
338
368
|
clear: () => void;
|
|
339
|
-
|
|
369
|
+
/** Re-run the action with the same input; resolves to its result, like the original call. */
|
|
370
|
+
retry: () => Promise<U>;
|
|
340
371
|
};
|
|
341
372
|
export interface MaybePreloadableComponent extends Component {
|
|
342
373
|
preload?: () => void;
|
|
343
374
|
}
|
|
344
|
-
export type CacheEntry = [
|
|
345
|
-
|
|
346
|
-
|
|
375
|
+
export type CacheEntry = [
|
|
376
|
+
number,
|
|
377
|
+
Promise<any>,
|
|
378
|
+
any,
|
|
379
|
+
Intent | undefined,
|
|
380
|
+
Signal<number> & {
|
|
381
|
+
count: number;
|
|
382
|
+
}
|
|
383
|
+
];
|
|
347
384
|
export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
|
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",
|