@solidjs/router 1.0.0 → 2.0.0-next.13
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 +606 -697
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +40 -11
- package/dist/data/action.js +307 -101
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +24 -23
- package/dist/data/index.d.ts +2 -4
- package/dist/data/index.js +2 -4
- package/dist/data/query.d.ts +1 -3
- package/dist/data/query.js +75 -32
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +1949 -1204
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +12 -26
- package/dist/routers/components.jsx +68 -54
- package/dist/routers/factory.d.ts +121 -0
- package/dist/routers/factory.jsx +154 -0
- package/dist/routers/history.d.ts +26 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +5 -11
- package/dist/routers/index.js +2 -6
- package/dist/routers/scrollRestoration.d.ts +10 -1
- package/dist/routers/scrollRestoration.js +49 -11
- package/dist/routing.d.ts +96 -52
- package/dist/routing.js +429 -172
- package/dist/server.d.ts +47 -0
- package/dist/server.js +156 -0
- package/dist/types.d.ts +158 -44
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +12 -7
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -40
- package/dist/data/createAsync.d.ts +0 -32
- package/dist/data/createAsync.js +0 -96
- package/dist/data/response.d.ts +0 -4
- package/dist/data/response.js +0 -42
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -17
- package/dist/routers/Router.js +0 -59
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -41
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import type { CollectFlightDataHook, ServerFunctionOutcome } from "@solidjs/web/server-functions/server";
|
|
3
|
+
import type { RouteDefinition, RoutePreloadFunc } from "./types.js";
|
|
4
|
+
export type { CollectFlightDataHook, ServerFunctionOutcome };
|
|
5
|
+
export interface FlightDataCollectorOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The app's route tree — the same config objects the `createRouter`
|
|
8
|
+
* factory receives, an array of them, or a thunk producing either (for
|
|
9
|
+
* lazily/per-request built trees). Flight data is produced by the pure
|
|
10
|
+
* preload runner: the target URL is matched against the tree and the
|
|
11
|
+
* matched routes' `preload` functions run in data-only mode.
|
|
12
|
+
*/
|
|
13
|
+
routes: RouteDefinition | readonly RouteDefinition[] | (() => RouteDefinition | readonly RouteDefinition[]);
|
|
14
|
+
/**
|
|
15
|
+
* The root layout's preload — the same function the app passes to the
|
|
16
|
+
* `createRouter` factory's `preload` option. Runs before the matched
|
|
17
|
+
* routes' preloads with the semantics the root gets during a real server
|
|
18
|
+
* render: the merged params of every match and `intent: "initial"`.
|
|
19
|
+
*/
|
|
20
|
+
rootPreload?: RoutePreloadFunc;
|
|
21
|
+
/** The app's base path, for compiling the route tree's matchers. */
|
|
22
|
+
base?: string;
|
|
23
|
+
}
|
|
24
|
+
/** A `createRouter` instance carries everything the collector needs. */
|
|
25
|
+
interface RouterInstanceLike {
|
|
26
|
+
(props: any): JSX.Element;
|
|
27
|
+
readonly routes: readonly RouteDefinition[];
|
|
28
|
+
readonly config: {
|
|
29
|
+
base?: string;
|
|
30
|
+
preload?: RoutePreloadFunc;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Produces the `collectFlightData` implementation for
|
|
35
|
+
* `configureServerFunctionsServer` (or `handleServerFunctionRequest`
|
|
36
|
+
* options). Accepts a `createRouter` instance directly — its routes, base,
|
|
37
|
+
* and `preload` are the single source of truth — or an options object for
|
|
38
|
+
* trees not created through the factory.
|
|
39
|
+
*
|
|
40
|
+
* Strategy: rerun the route data for the URL the client will show after
|
|
41
|
+
* the mutation (the outcome's pre-digested `targetUrl`), collecting each
|
|
42
|
+
* `query` result under its cache key, scoped to the outcome's
|
|
43
|
+
* `revalidateKeys` when present (routes newly entered via redirect always
|
|
44
|
+
* collect fully). The returned payload seeds the client router's cache
|
|
45
|
+
* through its registered flight-data consumer.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createFlightDataCollector(options: FlightDataCollectorOptions | RouterInstanceLike): CollectFlightDataHook;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The router's server-side integration with the server function runtime
|
|
2
|
+
// (@solidjs/web/server-functions). Server-only entry (`@solidjs/router/server`):
|
|
3
|
+
// it reaches for node async context via @solidjs/web/storage — keep it out of
|
|
4
|
+
// client bundles.
|
|
5
|
+
//
|
|
6
|
+
// One policy lives here — the vocabulary (query keys, route trees, preloads)
|
|
7
|
+
// is the router's:
|
|
8
|
+
//
|
|
9
|
+
// - single-flight mutations: `createFlightDataCollector` produces the
|
|
10
|
+
// `collectFlightData` hook the app hands to
|
|
11
|
+
// `configureServerFunctionsServer` (or the handler options). The runtime
|
|
12
|
+
// pre-digests the generic halves onto the outcome (target URL,
|
|
13
|
+
// revalidation keys, post-mutation cookie headers); the collector runs
|
|
14
|
+
// the matched routes' preloads for the target and returns their `query`
|
|
15
|
+
// results, which the handler folds into the response — mutation and
|
|
16
|
+
// fresh data in one round trip.
|
|
17
|
+
//
|
|
18
|
+
// The no-JS form convention used to live here too; the runtime now owns it
|
|
19
|
+
// outright — it applies to browser form posts by default (redirect back with
|
|
20
|
+
// the outcome in a one-shot flash cookie), and `createNoJSHandler` is
|
|
21
|
+
// configured through @solidjs/web/server-functions/server. The router keeps
|
|
22
|
+
// only its read side: SSR initialization seeds the decoded cookie into
|
|
23
|
+
// submission state (routing.ts).
|
|
24
|
+
import { provideRequestEvent } from "@solidjs/web/storage";
|
|
25
|
+
import { createBranches, getRouteMatches, mergeParams, peekLazySubtrees, resolveLazySubtree } from "./routing.js";
|
|
26
|
+
import { extractSearchParams } from "./utils.js";
|
|
27
|
+
// the instance is the provider component, so it (unlike an options object) is a function
|
|
28
|
+
function isRouterInstance(options) {
|
|
29
|
+
return typeof options === "function";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Produces the `collectFlightData` implementation for
|
|
33
|
+
* `configureServerFunctionsServer` (or `handleServerFunctionRequest`
|
|
34
|
+
* options). Accepts a `createRouter` instance directly — its routes, base,
|
|
35
|
+
* and `preload` are the single source of truth — or an options object for
|
|
36
|
+
* trees not created through the factory.
|
|
37
|
+
*
|
|
38
|
+
* Strategy: rerun the route data for the URL the client will show after
|
|
39
|
+
* the mutation (the outcome's pre-digested `targetUrl`), collecting each
|
|
40
|
+
* `query` result under its cache key, scoped to the outcome's
|
|
41
|
+
* `revalidateKeys` when present (routes newly entered via redirect always
|
|
42
|
+
* collect fully). The returned payload seeds the client router's cache
|
|
43
|
+
* through its registered flight-data consumer.
|
|
44
|
+
*/
|
|
45
|
+
export function createFlightDataCollector(options) {
|
|
46
|
+
const { routes, rootPreload, base = "" } = isRouterInstance(options)
|
|
47
|
+
? { routes: options.routes, rootPreload: options.config.preload, base: options.config.base }
|
|
48
|
+
: options;
|
|
49
|
+
if (!routes)
|
|
50
|
+
throw new Error("createFlightDataCollector requires `routes`");
|
|
51
|
+
let branches;
|
|
52
|
+
let compiledVersion = -1;
|
|
53
|
+
// thunks are called at first collection so per-request trees build lazily;
|
|
54
|
+
// recompiled when a lazy subtree resolves (shared, append-only state)
|
|
55
|
+
const resolveBranches = () => {
|
|
56
|
+
const version = peekLazySubtrees();
|
|
57
|
+
if (!branches || compiledVersion !== version) {
|
|
58
|
+
branches = createBranches(typeof routes === "function" ? routes() : routes, base);
|
|
59
|
+
compiledVersion = version;
|
|
60
|
+
}
|
|
61
|
+
return branches;
|
|
62
|
+
};
|
|
63
|
+
return async (sourceEvent, outcome) => {
|
|
64
|
+
// The generic halves arrive pre-digested on the outcome: no target
|
|
65
|
+
// (non-browser caller without a referer, or a redirect leaving the
|
|
66
|
+
// app's origin) means nothing to produce data for.
|
|
67
|
+
const { targetUrl, revalidateKeys } = outcome;
|
|
68
|
+
if (!targetUrl)
|
|
69
|
+
return undefined;
|
|
70
|
+
// a target implies a referer — it is the fallback target
|
|
71
|
+
const previousUrl = outcome.request.headers.get("referer");
|
|
72
|
+
// The flight event: the source event (its `response` rides along, so
|
|
73
|
+
// cookies/headers set during preloads still reach the real response)
|
|
74
|
+
// pointed at the target URL, with the mutation's cookie effects already
|
|
75
|
+
// folded in (the outcome's `foldedHeaders`), in data-only router mode.
|
|
76
|
+
const event = { ...sourceEvent };
|
|
77
|
+
event.request = new Request(targetUrl, { headers: outcome.foldedHeaders });
|
|
78
|
+
event.router = {
|
|
79
|
+
dataOnly: revalidateKeys || true,
|
|
80
|
+
previousUrl,
|
|
81
|
+
data: {}
|
|
82
|
+
};
|
|
83
|
+
return provideRequestEvent(event, async () => {
|
|
84
|
+
try {
|
|
85
|
+
await resolveLazyMatches(resolveBranches, targetUrl, previousUrl);
|
|
86
|
+
runPreloads(event, resolveBranches(), targetUrl, previousUrl, rootPreload);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(error);
|
|
90
|
+
}
|
|
91
|
+
const data = event.router.data;
|
|
92
|
+
if (!data)
|
|
93
|
+
return undefined;
|
|
94
|
+
let containsKey = false;
|
|
95
|
+
for (const key in data) {
|
|
96
|
+
if (data[key] === undefined)
|
|
97
|
+
delete data[key];
|
|
98
|
+
else
|
|
99
|
+
containsKey = true;
|
|
100
|
+
}
|
|
101
|
+
return containsKey ? data : undefined;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Lazy subtrees matched by the target (or previous) URL must resolve before
|
|
106
|
+
// the preload pass — the collector would otherwise be blind to inner routes'
|
|
107
|
+
// data. Resolution is shared with the client-side machinery (append-only,
|
|
108
|
+
// cached per thunk); the loop handles boundaries nested inside boundaries.
|
|
109
|
+
async function resolveLazyMatches(resolveBranches, url, previousUrl) {
|
|
110
|
+
for (;;) {
|
|
111
|
+
const branches = resolveBranches();
|
|
112
|
+
const target = new URL(url);
|
|
113
|
+
const matches = [
|
|
114
|
+
...getRouteMatches(branches, target.pathname),
|
|
115
|
+
...getRouteMatches(branches, new URL(previousUrl, target).pathname)
|
|
116
|
+
];
|
|
117
|
+
const pending = matches.filter(m => m.route.lazy && !m.route.lazy.resolved);
|
|
118
|
+
if (!pending.length)
|
|
119
|
+
return;
|
|
120
|
+
await Promise.all(pending.map(m => resolveLazySubtree(m.route.lazy)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// The pure preload runner: the same collection loop the router's <Routes>
|
|
124
|
+
// component runs when it detects a data-only render (routers/components.tsx),
|
|
125
|
+
// driven directly off the route tree — matched routes' preloads run and
|
|
126
|
+
// their `query` calls store results on `event.router.data`. Routes not
|
|
127
|
+
// matched by the previous URL flip collection from the revalidation-key
|
|
128
|
+
// filter to everything (their queries have no client cache yet).
|
|
129
|
+
function runPreloads(event, branches, url, previousUrl, rootPreload) {
|
|
130
|
+
const target = new URL(url);
|
|
131
|
+
const prevMatches = getRouteMatches(branches, new URL(previousUrl, target).pathname);
|
|
132
|
+
const matches = getRouteMatches(branches, target.pathname);
|
|
133
|
+
const location = {
|
|
134
|
+
pathname: target.pathname,
|
|
135
|
+
search: target.search,
|
|
136
|
+
hash: target.hash,
|
|
137
|
+
query: extractSearchParams(target),
|
|
138
|
+
state: null,
|
|
139
|
+
key: ""
|
|
140
|
+
};
|
|
141
|
+
// the root layout preloads first, exactly as <Root> would during a real
|
|
142
|
+
// server render: merged params of every match, intent "initial" (there is
|
|
143
|
+
// no navigation intent on the server), before any dataOnly-filter flip
|
|
144
|
+
rootPreload && rootPreload({ params: mergeParams(matches), location, intent: "initial" });
|
|
145
|
+
for (let match = 0; match < matches.length; match++) {
|
|
146
|
+
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
147
|
+
event.router.dataOnly = true;
|
|
148
|
+
const { route, params } = matches[match];
|
|
149
|
+
route.preload &&
|
|
150
|
+
route.preload({
|
|
151
|
+
params,
|
|
152
|
+
location,
|
|
153
|
+
intent: "preload"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Component,
|
|
2
|
-
|
|
1
|
+
import type { Component, Signal } from "solid-js";
|
|
2
|
+
import type { JSX, ResponseEnvelope } from "@solidjs/web";
|
|
3
|
+
declare module "@solidjs/web" {
|
|
3
4
|
interface RequestEvent {
|
|
4
5
|
response: {
|
|
5
6
|
status?: number;
|
|
@@ -11,7 +12,8 @@ declare module "solid-js/web" {
|
|
|
11
12
|
cache?: Map<string, CacheEntry>;
|
|
12
13
|
submission?: {
|
|
13
14
|
input: any;
|
|
14
|
-
result
|
|
15
|
+
result?: any;
|
|
16
|
+
error?: any;
|
|
15
17
|
url: string;
|
|
16
18
|
};
|
|
17
19
|
dataOnly?: boolean | string[];
|
|
@@ -23,6 +25,42 @@ declare module "solid-js/web" {
|
|
|
23
25
|
}
|
|
24
26
|
export type Params = Record<string, string | undefined>;
|
|
25
27
|
export type SearchParams = Record<string, string | string[] | undefined>;
|
|
28
|
+
declare const PATH_PARAMS: unique symbol;
|
|
29
|
+
export interface TypedPath<P extends Params = Params> extends JSX.SerializableAttributeValue {
|
|
30
|
+
readonly [PATH_PARAMS]: P;
|
|
31
|
+
toString(): string;
|
|
32
|
+
}
|
|
33
|
+
declare const PATH_SEARCH: unique symbol;
|
|
34
|
+
export interface TypedSearchPath<In = SetSearchParams, Out = SearchParams> {
|
|
35
|
+
readonly [PATH_SEARCH]: {
|
|
36
|
+
input: In;
|
|
37
|
+
output: Out;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The Standard Schema interface (https://standardschema.dev) — vendored as a
|
|
42
|
+
* type so any conforming validator (Valibot, Zod, ArkType, ...) can type a
|
|
43
|
+
* route's `search` without the router depending on a validation library.
|
|
44
|
+
*/
|
|
45
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
46
|
+
readonly "~standard": {
|
|
47
|
+
readonly version: 1;
|
|
48
|
+
readonly vendor: string;
|
|
49
|
+
readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
50
|
+
readonly types?: {
|
|
51
|
+
readonly input: Input;
|
|
52
|
+
readonly output: Output;
|
|
53
|
+
} | undefined;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export type StandardSchemaResult<Output> = {
|
|
57
|
+
readonly value: Output;
|
|
58
|
+
readonly issues?: undefined;
|
|
59
|
+
} | {
|
|
60
|
+
readonly issues: ReadonlyArray<{
|
|
61
|
+
readonly message: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
26
64
|
export type SetParams = Record<string, string | number | boolean | null | undefined>;
|
|
27
65
|
export type SetSearchParams = Record<string, string | string[] | number | number[] | boolean | boolean[] | null | undefined>;
|
|
28
66
|
export interface Path {
|
|
@@ -36,13 +74,19 @@ export interface Location<S = unknown> extends Path {
|
|
|
36
74
|
key: string;
|
|
37
75
|
}
|
|
38
76
|
export interface NavigateOptions<S = unknown> {
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the target like an href: leading-`/` paths are prefixed with the
|
|
79
|
+
* router base, anything else resolves URL-style against the current
|
|
80
|
+
* location (`..` collapses, `?`/`#`-only strings keep the path). `false`
|
|
81
|
+
* takes the string as the final path. Defaults to `true`.
|
|
82
|
+
*/
|
|
39
83
|
resolve: boolean;
|
|
40
84
|
replace: boolean;
|
|
41
85
|
scroll: boolean;
|
|
42
86
|
state: S;
|
|
43
87
|
}
|
|
44
88
|
export interface Navigator {
|
|
45
|
-
(to: string | number, options?: Partial<NavigateOptions>): void;
|
|
89
|
+
(to: string | TypedPath | number, options?: Partial<NavigateOptions>): void;
|
|
46
90
|
(delta: number): void;
|
|
47
91
|
}
|
|
48
92
|
export type NavigatorFactory = (route?: RouteContext) => Navigator;
|
|
@@ -54,41 +98,109 @@ export interface LocationChange<S = unknown> {
|
|
|
54
98
|
rawPath?: string;
|
|
55
99
|
}
|
|
56
100
|
export interface RouterIntegration {
|
|
57
|
-
signal:
|
|
58
|
-
create?: (router: RouterContext) => void;
|
|
101
|
+
signal: [get: () => LocationChange, set: (next: LocationChange) => void];
|
|
59
102
|
utils?: Partial<RouterUtils>;
|
|
60
103
|
}
|
|
61
104
|
export type Intent = "initial" | "native" | "navigate" | "preload";
|
|
62
|
-
export interface RoutePreloadFuncArgs {
|
|
63
|
-
params:
|
|
105
|
+
export interface RoutePreloadFuncArgs<P extends Params = Params> {
|
|
106
|
+
params: P;
|
|
64
107
|
location: Location;
|
|
65
108
|
intent: Intent;
|
|
66
109
|
}
|
|
67
|
-
export type RoutePreloadFunc<T = unknown> = (args: RoutePreloadFuncArgs) => T;
|
|
68
|
-
export interface RouteSectionProps<T = unknown> {
|
|
69
|
-
params:
|
|
110
|
+
export type RoutePreloadFunc<T = unknown, P extends Params = Params> = (args: RoutePreloadFuncArgs<P>) => T;
|
|
111
|
+
export interface RouteSectionProps<T = unknown, P extends Params = Params> {
|
|
112
|
+
params: P;
|
|
70
113
|
location: Location;
|
|
71
114
|
data: T;
|
|
72
115
|
children?: JSX.Element;
|
|
73
116
|
}
|
|
74
|
-
export type RouteSectionComponent<T = unknown> = Component<RouteSectionProps<T>> | Component<Omit<RouteSectionProps<T>, "children">> | Component<{}>;
|
|
117
|
+
export type RouteSectionComponent<T = unknown, P extends Params = Params> = Component<RouteSectionProps<T, P>> | Component<Omit<RouteSectionProps<T, P>, "children">> | Component<{}>;
|
|
118
|
+
declare const ROUTE_PATTERN: unique symbol;
|
|
119
|
+
export interface TypedRouteConfig<S extends string = string> {
|
|
120
|
+
readonly [ROUTE_PATTERN]: S;
|
|
121
|
+
}
|
|
122
|
+
type RouteDataOf<Path> = Path extends TypedRouteConfig & {
|
|
123
|
+
preload?: RoutePreloadFunc<infer T>;
|
|
124
|
+
} ? T : unknown;
|
|
125
|
+
/**
|
|
126
|
+
* Props for a route component declared away from its definition, typed by a
|
|
127
|
+
* path witness — the same `paths` node you navigate with, the pattern string
|
|
128
|
+
* when no instance is in scope, or a `defineFileRoute` config (which also
|
|
129
|
+
* types `data` from its `preload`):
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* function Story(props: RouteProps<typeof Router.paths.stories>) {}
|
|
133
|
+
* function Story(props: RouteProps<"/stories/:id", StoryData>) {}
|
|
134
|
+
* function Post(props: RouteProps<typeof route>) {} // file-system route files
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export type RouteProps<Path, T = RouteDataOf<Path>> = RouteSectionProps<T, Path extends TypedPath<infer P> ? SimplifyRecord<P> & Params : Path extends TypedRouteConfig<infer S> ? RouteParams<S> : RouteParams<Path>>;
|
|
138
|
+
/**
|
|
139
|
+
* Component-type counterpart of `RouteProps` — annotate the component itself
|
|
140
|
+
* and the props parameter infers contextually:
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* const Post: RouteComponent<typeof route> = props => props.params.id;
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export type RouteComponent<Path, T = RouteDataOf<Path>> = Component<RouteProps<Path, T>>;
|
|
147
|
+
/**
|
|
148
|
+
* A lazy route subtree: a thunk producing the child definitions — typically
|
|
149
|
+
* `() => import("./feature/routes")` (the module's `default` or `routes`
|
|
150
|
+
* export is used). The subtree's code loads on demand (hover intent or
|
|
151
|
+
* navigation kicks it, and the load folds into the navigation transition)
|
|
152
|
+
* but its *types* still flow into `paths` through the import's promise type.
|
|
153
|
+
* Thunks must be deterministic — resolution is cached append-only and shared
|
|
154
|
+
* by every mount, request, and the server's flight collector.
|
|
155
|
+
*/
|
|
156
|
+
export type LazyRouteChildren = () => readonly RouteDefinition[] | Promise<readonly RouteDefinition[] | {
|
|
157
|
+
default: readonly RouteDefinition[];
|
|
158
|
+
} | {
|
|
159
|
+
routes: readonly RouteDefinition[];
|
|
160
|
+
}>;
|
|
75
161
|
export type RouteDefinition<S extends string | string[] = any, T = any> = {
|
|
76
162
|
path?: S;
|
|
77
163
|
matchFilters?: MatchFilters<S>;
|
|
78
164
|
preload?: RoutePreloadFunc<T>;
|
|
79
|
-
children?: RouteDefinition | RouteDefinition[];
|
|
165
|
+
children?: RouteDefinition | readonly RouteDefinition[] | LazyRouteChildren;
|
|
80
166
|
component?: RouteSectionComponent<T>;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
167
|
+
/** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
|
|
168
|
+
search?: StandardSchemaV1<any, any>;
|
|
169
|
+
info?: RouteInfo;
|
|
84
170
|
};
|
|
171
|
+
import type { RouteInfo } from "./index.jsx";
|
|
172
|
+
export type { RouteInfo };
|
|
85
173
|
export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
|
|
86
174
|
export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : [];
|
|
87
175
|
export type MatchFilters<P extends string | readonly string[] = any> = P extends string ? {
|
|
88
176
|
[K in PathParams<P>[number]]?: MatchFilter;
|
|
89
177
|
} : Record<string, MatchFilter>;
|
|
90
|
-
export
|
|
91
|
-
|
|
178
|
+
export type DefinedRouteFilters<S> = S extends readonly (infer Member extends string)[] ? MatchFilters<Member> : S extends string | readonly string[] ? MatchFilters<S> : MatchFilters;
|
|
179
|
+
type FilterKeysOf<S> = S extends readonly (infer Member extends string)[] ? PathParams<Member>[number] : S extends string ? PathParams<S>[number] : string;
|
|
180
|
+
export type ValidFilters<F, S> = {
|
|
181
|
+
[K in keyof F]: string extends K ? F[K] : K extends FilterKeysOf<S> ? MatchFilter : never;
|
|
182
|
+
};
|
|
183
|
+
type PatternParams<P extends string> = P extends `${infer Head}/${infer Tail}` ? PatternParams<Head> & PatternParams<Tail> : P extends `:${infer Name}?` ? {
|
|
184
|
+
[K in Name]?: string;
|
|
185
|
+
} : P extends `:${infer Name}` ? {
|
|
186
|
+
[K in Name]: string;
|
|
187
|
+
} : P extends `*${infer Name}` ? {
|
|
188
|
+
[K in Name]: string;
|
|
189
|
+
} : {};
|
|
190
|
+
type SimplifyRecord<T> = {
|
|
191
|
+
[K in keyof T]: T[K];
|
|
192
|
+
} & {};
|
|
193
|
+
/**
|
|
194
|
+
* The params a route pattern guarantees, as a precise record — `:id` is
|
|
195
|
+
* `string`, `:tab?` is `string | undefined`, `*rest` is `string`. The record
|
|
196
|
+
* is intersected with the open `Params` shape so params inherited from
|
|
197
|
+
* parent routes stay accessible (as `string | undefined`). Array patterns
|
|
198
|
+
* yield the union of their members' records; non-literal patterns degrade
|
|
199
|
+
* to plain `Params`.
|
|
200
|
+
*/
|
|
201
|
+
export type RouteParams<S> = (S extends readonly (infer Member extends string)[] ? SimplifyRecord<PatternParams<Member>> : S extends string ? string extends S ? {} : SimplifyRecord<PatternParams<S>> : {}) & Params;
|
|
202
|
+
export interface PathMatch<P extends Params = Params> {
|
|
203
|
+
params: P;
|
|
92
204
|
path: string;
|
|
93
205
|
}
|
|
94
206
|
export interface RouteMatch extends PathMatch {
|
|
@@ -99,7 +211,7 @@ export interface OutputMatch {
|
|
|
99
211
|
pattern: string;
|
|
100
212
|
match: string;
|
|
101
213
|
params: Params;
|
|
102
|
-
info?:
|
|
214
|
+
info?: RouteInfo;
|
|
103
215
|
}
|
|
104
216
|
export interface RouteDescription {
|
|
105
217
|
key: unknown;
|
|
@@ -109,7 +221,15 @@ export interface RouteDescription {
|
|
|
109
221
|
preload?: RoutePreloadFunc;
|
|
110
222
|
matcher: (location: string) => PathMatch | null;
|
|
111
223
|
matchFilters?: MatchFilters;
|
|
112
|
-
info?:
|
|
224
|
+
info?: RouteInfo;
|
|
225
|
+
/** Present on the placeholder that stands in for an unresolved lazy subtree. */
|
|
226
|
+
lazy?: LazyBoundary;
|
|
227
|
+
}
|
|
228
|
+
/** Resolution state for a lazy subtree boundary, shared per thunk. */
|
|
229
|
+
export interface LazyBoundary {
|
|
230
|
+
thunk: LazyRouteChildren;
|
|
231
|
+
promise?: Promise<readonly RouteDefinition[]>;
|
|
232
|
+
resolved?: readonly RouteDefinition[];
|
|
113
233
|
}
|
|
114
234
|
export interface Branch {
|
|
115
235
|
routes: RouteDescription[];
|
|
@@ -120,15 +240,18 @@ export interface RouteContext {
|
|
|
120
240
|
parent?: RouteContext;
|
|
121
241
|
child?: RouteContext;
|
|
122
242
|
pattern: string;
|
|
243
|
+
params: Params;
|
|
123
244
|
path: () => string;
|
|
124
245
|
outlet: () => JSX.Element;
|
|
125
246
|
resolvePath(to: string): string | undefined;
|
|
247
|
+
/** Whether this route section still matches `target` (same matched path), i.e. survives a navigation there. Absent on the base route, which always survives. */
|
|
248
|
+
retained?(target: string): boolean;
|
|
126
249
|
}
|
|
127
250
|
export interface RouterUtils {
|
|
128
251
|
renderPath(path: string): string;
|
|
129
252
|
parsePath(str: string): string;
|
|
130
253
|
go(delta: number): void;
|
|
131
|
-
beforeLeave:
|
|
254
|
+
beforeLeave: BeforeLeaveSlot;
|
|
132
255
|
paramsWrapper: (getParams: () => Params, branches: () => Branch[]) => Params;
|
|
133
256
|
queryWrapper: (getQuery: () => SearchParams) => SearchParams;
|
|
134
257
|
}
|
|
@@ -136,14 +259,17 @@ export interface RouterContext {
|
|
|
136
259
|
base: RouteContext;
|
|
137
260
|
location: Location;
|
|
138
261
|
params: Params;
|
|
262
|
+
wrapParams: (getParams: () => Params) => Params;
|
|
139
263
|
navigatorFactory: NavigatorFactory;
|
|
140
264
|
isRouting: () => boolean;
|
|
141
265
|
/** The target of the in-flight navigation transition, if any. Not reactive. */
|
|
142
266
|
readonly pendingTarget?: LocationChange;
|
|
267
|
+
/** Whether `route` is a section the in-flight navigation is leaving. False when no navigation is pending. */
|
|
268
|
+
leaving?(route: RouteContext): boolean;
|
|
143
269
|
matches: () => RouteMatch[];
|
|
144
270
|
renderPath(path: string): string;
|
|
145
271
|
parsePath(str: string): string;
|
|
146
|
-
beforeLeave:
|
|
272
|
+
beforeLeave: BeforeLeaveSlot;
|
|
147
273
|
preloadRoute: (url: URL, preloadData?: boolean) => void;
|
|
148
274
|
singleFlight: boolean;
|
|
149
275
|
submissions: Signal<Submission<any, any>[]>;
|
|
@@ -165,39 +291,27 @@ export interface BeforeLeaveLifecycle {
|
|
|
165
291
|
subscribe(listener: BeforeLeaveListener): () => void;
|
|
166
292
|
confirm(to: string | number, options?: Partial<NavigateOptions>): boolean;
|
|
167
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Lazily-filled holder for the beforeLeave guard. The router and history
|
|
296
|
+
* adapters only carry this empty object; `useBeforeLeave` installs the actual
|
|
297
|
+
* lifecycle on first use so its machinery tree-shakes out of apps that never
|
|
298
|
+
* block navigation.
|
|
299
|
+
*/
|
|
300
|
+
export interface BeforeLeaveSlot {
|
|
301
|
+
current?: BeforeLeaveLifecycle;
|
|
302
|
+
}
|
|
168
303
|
export type Submission<T, U> = {
|
|
169
304
|
readonly input: T;
|
|
170
305
|
readonly result?: U;
|
|
171
306
|
readonly error: any;
|
|
172
|
-
readonly pending: boolean;
|
|
173
307
|
readonly url: string;
|
|
174
308
|
clear: () => void;
|
|
175
309
|
retry: () => void;
|
|
176
310
|
};
|
|
177
|
-
export type SubmissionStub = {
|
|
178
|
-
readonly input: undefined;
|
|
179
|
-
readonly result: undefined;
|
|
180
|
-
readonly error: undefined;
|
|
181
|
-
readonly pending: undefined;
|
|
182
|
-
readonly url: undefined;
|
|
183
|
-
clear: () => void;
|
|
184
|
-
retry: () => void;
|
|
185
|
-
};
|
|
186
311
|
export interface MaybePreloadableComponent extends Component {
|
|
187
312
|
preload?: () => void;
|
|
188
313
|
}
|
|
189
314
|
export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
|
|
190
315
|
count: number;
|
|
191
316
|
}];
|
|
192
|
-
export type NarrowResponse<T> = T extends
|
|
193
|
-
export type RouterResponseInit = Omit<ResponseInit, "body"> & {
|
|
194
|
-
revalidate?: string | string[];
|
|
195
|
-
};
|
|
196
|
-
export type CustomResponse<T> = Omit<Response, "clone"> & {
|
|
197
|
-
customBody: () => T;
|
|
198
|
-
clone(...args: readonly unknown[]): CustomResponse<T>;
|
|
199
|
-
};
|
|
200
|
-
/** @deprecated */
|
|
201
|
-
export type RouteLoadFunc = RoutePreloadFunc;
|
|
202
|
-
/** @deprecated */
|
|
203
|
-
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
|
|
317
|
+
export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { 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
|
+
/** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
|
|
5
|
+
export declare const comparablePath: (path: string) => string;
|
|
4
6
|
export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
|
|
5
7
|
export declare function invariant<T>(value: T | null | undefined, message: string): T;
|
|
6
8
|
export declare function joinPaths(from: string, to: string): string;
|
package/dist/utils.js
CHANGED
|
@@ -6,6 +6,8 @@ export function normalizePath(path, omitSlash = false) {
|
|
|
6
6
|
const s = path.replace(trimPathRegex, "$1");
|
|
7
7
|
return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
|
|
8
8
|
}
|
|
9
|
+
/** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
|
|
10
|
+
export const comparablePath = (path) => normalizePath(path.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
9
11
|
export function resolvePath(base, path, from) {
|
|
10
12
|
if (hasSchemeRegex.test(path)) {
|
|
11
13
|
return undefined;
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"Ryan Turnquist"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "2.0.0-next.13",
|
|
10
10
|
"homepage": "https://github.com/solidjs/solid-router#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -22,13 +22,16 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"solid": "./dist/index.jsx",
|
|
24
24
|
"default": "./dist/index.js"
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"./server": "./dist/server.js",
|
|
27
|
+
"./fs": "./dist/fs.js"
|
|
26
28
|
},
|
|
27
29
|
"files": [
|
|
28
30
|
"dist"
|
|
29
31
|
],
|
|
30
32
|
"sideEffects": false,
|
|
31
33
|
"devDependencies": {
|
|
34
|
+
"@solidjs/web": "2.0.0-beta.28",
|
|
32
35
|
"@babel/core": "^7.26.0",
|
|
33
36
|
"@babel/preset-typescript": "^7.26.0",
|
|
34
37
|
"@changesets/cli": "^2.27.10",
|
|
@@ -37,23 +40,25 @@
|
|
|
37
40
|
"@rollup/plugin-terser": "0.4.4",
|
|
38
41
|
"@types/jest": "^29.5.14",
|
|
39
42
|
"@types/node": "^22.10.0",
|
|
40
|
-
"babel-preset-solid": "
|
|
43
|
+
"babel-preset-solid": "2.0.0-beta.28",
|
|
41
44
|
"jsdom": "^25.0.1",
|
|
42
45
|
"prettier": "^3.4.1",
|
|
43
46
|
"rollup": "^4.27.4",
|
|
44
|
-
"solid-js": "
|
|
47
|
+
"solid-js": "2.0.0-beta.28",
|
|
45
48
|
"typescript": "^5.7.2",
|
|
46
49
|
"vite": "^6.0.0",
|
|
47
|
-
"vite-plugin-solid": "
|
|
50
|
+
"vite-plugin-solid": "3.0.0-next.10",
|
|
48
51
|
"vitest": "^2.1.6"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"
|
|
54
|
+
"@solidjs/web": ">=2.0.0-beta.28 <2.0.0-experimental.0",
|
|
55
|
+
"solid-js": ">=2.0.0-beta.28 <2.0.0-experimental.0"
|
|
52
56
|
},
|
|
53
57
|
"scripts": {
|
|
54
58
|
"build": "rm -rf dist && tsc && rollup -c",
|
|
55
|
-
"test": "vitest run && npm run test:types",
|
|
59
|
+
"test": "vitest run && vitest run --config vitest.config.server.ts && npm run test:types",
|
|
56
60
|
"test:watch": "vitest",
|
|
61
|
+
"test:server": "vitest run --config vitest.config.server.ts",
|
|
57
62
|
"test:types": "tsc --project tsconfig.test.json",
|
|
58
63
|
"pretty": "prettier --write \"{src,test}/**/*.{ts,tsx}\"",
|
|
59
64
|
"release": "pnpm build && changeset publish"
|
package/dist/components.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { JSX } from "solid-js";
|
|
2
|
-
import type { Location, Navigator } from "./types.js";
|
|
3
|
-
declare module "solid-js" {
|
|
4
|
-
namespace JSX {
|
|
5
|
-
interface AnchorHTMLAttributes<T> {
|
|
6
|
-
state?: string;
|
|
7
|
-
noScroll?: boolean;
|
|
8
|
-
replace?: boolean;
|
|
9
|
-
preload?: boolean;
|
|
10
|
-
link?: boolean;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
|
|
15
|
-
href: string;
|
|
16
|
-
replace?: boolean | undefined;
|
|
17
|
-
noScroll?: boolean | undefined;
|
|
18
|
-
state?: unknown | undefined;
|
|
19
|
-
inactiveClass?: string | undefined;
|
|
20
|
-
activeClass?: string | undefined;
|
|
21
|
-
end?: boolean | undefined;
|
|
22
|
-
}
|
|
23
|
-
export declare function A(props: AnchorProps): JSX.Element;
|
|
24
|
-
export interface NavigateProps {
|
|
25
|
-
href: ((args: {
|
|
26
|
-
navigate: Navigator;
|
|
27
|
-
location: Location;
|
|
28
|
-
}) => string) | string;
|
|
29
|
-
state?: unknown;
|
|
30
|
-
}
|
|
31
|
-
export declare function Navigate(props: NavigateProps): null;
|
package/dist/components.jsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { createMemo, mergeProps, splitProps } from "solid-js";
|
|
2
|
-
import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
|
|
3
|
-
import { normalizePath } from "./utils.js";
|
|
4
|
-
export function A(props) {
|
|
5
|
-
props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
|
|
6
|
-
const [, rest] = splitProps(props, [
|
|
7
|
-
"href",
|
|
8
|
-
"state",
|
|
9
|
-
"class",
|
|
10
|
-
"activeClass",
|
|
11
|
-
"inactiveClass",
|
|
12
|
-
"end"
|
|
13
|
-
]);
|
|
14
|
-
const to = useResolvedPath(() => props.href);
|
|
15
|
-
const href = useHref(to);
|
|
16
|
-
const location = useLocation();
|
|
17
|
-
const isActive = createMemo(() => {
|
|
18
|
-
const to_ = to();
|
|
19
|
-
if (to_ === undefined)
|
|
20
|
-
return [false, false];
|
|
21
|
-
// trailing slashes are ignored so `/route` and `/route/` share active state
|
|
22
|
-
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
23
|
-
const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
|
|
24
|
-
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
25
|
-
});
|
|
26
|
-
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
|
|
27
|
-
...(props.class && { [props.class]: true }),
|
|
28
|
-
[props.inactiveClass]: !isActive()[0],
|
|
29
|
-
[props.activeClass]: isActive()[0],
|
|
30
|
-
...rest.classList
|
|
31
|
-
}} link aria-current={isActive()[1] ? "page" : undefined}/>);
|
|
32
|
-
}
|
|
33
|
-
export function Navigate(props) {
|
|
34
|
-
const navigate = useNavigate();
|
|
35
|
-
const location = useLocation();
|
|
36
|
-
const { href, state } = props;
|
|
37
|
-
const path = typeof href === "function" ? href({ navigate, location }) : href;
|
|
38
|
-
navigate(path, { replace: true, state });
|
|
39
|
-
return null;
|
|
40
|
-
}
|