@solidjs/router 2.0.0-next.37 → 2.0.0-next.38
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 +74 -34
- package/dist/claims.d.ts +1 -1
- package/dist/claims.js +12 -3
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +37 -41
- package/dist/index.d.ts +4 -2
- package/dist/index.js +381 -155
- package/dist/index.jsx +3 -1
- package/dist/pending.js +2 -2
- package/dist/preload.d.ts +53 -0
- package/dist/preload.js +175 -0
- package/dist/preloadRoute.d.ts +23 -0
- package/dist/preloadRoute.js +92 -0
- package/dist/routers/factory.d.ts +13 -3
- package/dist/routers/factory.jsx +5 -2
- package/dist/routing.d.ts +1 -15
- package/dist/routing.js +11 -89
- package/dist/types.d.ts +19 -1
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- package/package.json +1 -1
package/dist/index.jsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
2
|
export * from "./lifecycle.js";
|
|
3
|
-
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate,
|
|
3
|
+
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
4
|
export { pendingLinks } from "./pending.js";
|
|
5
|
+
export { eagerPreload, intentPreload, tapPreload, viewportPreload } from "./preload.js";
|
|
6
|
+
export { usePreloadRoute } from "./preloadRoute.js";
|
|
5
7
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
6
8
|
export { int } from "./paths.js";
|
|
7
9
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
package/dist/pending.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createMemo, DEV, isPending, latest, NotReadyError, runWithOwner } from "solid-js";
|
|
2
|
-
import {
|
|
2
|
+
import { linkMatcher } from "./utils.js";
|
|
3
3
|
/**
|
|
4
4
|
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
5
5
|
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
@@ -45,7 +45,7 @@ export function pendingTarget(router) {
|
|
|
45
45
|
/** Whether `to` is the destination of the in-flight navigation. */
|
|
46
46
|
export function linkPending(router, to, base, end) {
|
|
47
47
|
const target = pendingTarget(router);
|
|
48
|
-
return !!target &&
|
|
48
|
+
return !!target && linkMatcher({ pathname: target.value, search: "" }, base, end)(to).active;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LinkPreload } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Preload a link when the pointer rests on it for `delay` ms (default 20),
|
|
4
|
+
* when it takes focus, and on touchstart. Moving over a link that already
|
|
5
|
+
* preloaded does not preload it again until the pointer leaves. These
|
|
6
|
+
* listeners do not check modifier keys or `defaultPrevented`; clicks still do.
|
|
7
|
+
* Warms route code and, unless `data` is false (default true), runs the
|
|
8
|
+
* matched routes' `preload` functions.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const Router = createRouter({ routes, preloadLinks: intentPreload() });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const intentPreload: ({ delay, data }?: {
|
|
16
|
+
delay?: number;
|
|
17
|
+
data?: boolean;
|
|
18
|
+
}) => LinkPreload;
|
|
19
|
+
/**
|
|
20
|
+
* Preload a link on `pointerdown` (mouse, touch, or pen), ahead of its click.
|
|
21
|
+
* Does not check modifier keys or `defaultPrevented`. Warms route code and,
|
|
22
|
+
* unless `data` is false (default true), route data.
|
|
23
|
+
*/
|
|
24
|
+
export declare const tapPreload: ({ data }?: {
|
|
25
|
+
data?: boolean;
|
|
26
|
+
}) => LinkPreload;
|
|
27
|
+
/**
|
|
28
|
+
* Preload a link once it has stayed in the viewport for `delay` ms (default
|
|
29
|
+
* 100), when the browser is next idle (`requestIdleCallback`, or `setTimeout`
|
|
30
|
+
* where that is missing). Each link preloads once, and again after its `href`
|
|
31
|
+
* changes; a link that leaves before then is dropped. One `IntersectionObserver`
|
|
32
|
+
* is shared. `rootMargin` is passed through to it (the observer's own `"0px"`
|
|
33
|
+
* when omitted). Applies to links with `preload="viewport"`, or with `all`
|
|
34
|
+
* (default false) to every link except `preload="false"`. Skips Save-Data and
|
|
35
|
+
* 2g connections. Warms route code only unless `data` is true (default false).
|
|
36
|
+
*/
|
|
37
|
+
export declare const viewportPreload: ({ all, data, delay, rootMargin }?: {
|
|
38
|
+
all?: boolean;
|
|
39
|
+
data?: boolean;
|
|
40
|
+
delay?: number;
|
|
41
|
+
rootMargin?: string;
|
|
42
|
+
}) => LinkPreload;
|
|
43
|
+
/**
|
|
44
|
+
* Preload links as soon as the page has loaded and the browser is idle,
|
|
45
|
+
* including links mounted later. A link removed before the flush is dropped.
|
|
46
|
+
* Applies to links with `preload="eager"`, or with `all` (default false) to
|
|
47
|
+
* every link except `preload="false"`. Skips Save-Data and 2g connections.
|
|
48
|
+
* Warms route code only unless `data` is true (default false).
|
|
49
|
+
*/
|
|
50
|
+
export declare const eagerPreload: ({ all, data }?: {
|
|
51
|
+
all?: boolean;
|
|
52
|
+
data?: boolean;
|
|
53
|
+
}) => LinkPreload;
|
package/dist/preload.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { registerElementClaim } from "@solidjs/web";
|
|
2
|
+
import { getOwner, onCleanup } from "solid-js";
|
|
3
|
+
import { setLinkPreloader } from "./data/events.js";
|
|
4
|
+
import { preloadRoute } from "./preloadRoute.js";
|
|
5
|
+
/**
|
|
6
|
+
* Anchors as the runtime claims them: at creation and again on `href`
|
|
7
|
+
* writes. `disarm` runs when the creating owner is disposed. Claims fire
|
|
8
|
+
* before dynamic attributes are applied (solidjs/solid#3923), so whether a
|
|
9
|
+
* link is opted in, and where it points, is read when it preloads.
|
|
10
|
+
*/
|
|
11
|
+
function claimAnchors(arm, disarm) {
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
setLinkPreloader(preloadRoute);
|
|
14
|
+
onCleanup(registerElementClaim(node => {
|
|
15
|
+
if (node.nodeName.toUpperCase() !== "A")
|
|
16
|
+
return;
|
|
17
|
+
arm(node);
|
|
18
|
+
// a re-claim runs under the owner writing `href`, not the anchor's
|
|
19
|
+
if (seen.has(node))
|
|
20
|
+
return;
|
|
21
|
+
seen.add(node);
|
|
22
|
+
getOwner() && onCleanup(() => disarm(node));
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
// requests made before the browser is idle run together once it is
|
|
26
|
+
function whenIdle(fn) {
|
|
27
|
+
let scheduled = false;
|
|
28
|
+
return () => {
|
|
29
|
+
if (scheduled)
|
|
30
|
+
return;
|
|
31
|
+
scheduled = true;
|
|
32
|
+
(window.requestIdleCallback || setTimeout)(() => {
|
|
33
|
+
scheduled = false;
|
|
34
|
+
fn();
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Preloads an ambient strategy's anchor: one that names the strategy in its
|
|
40
|
+
* `preload` attribute, or with `all` any but `preload="false"`. Skipped when
|
|
41
|
+
* the user asked to save data or the connection is 2g (where the Network
|
|
42
|
+
* Information API is available).
|
|
43
|
+
*/
|
|
44
|
+
const ambientPreload = ({ url, preload }, name, all, data) => (a) => {
|
|
45
|
+
const value = a.getAttribute("preload");
|
|
46
|
+
const connection = navigator.connection;
|
|
47
|
+
const href = (value === name || (all && value !== "false")) &&
|
|
48
|
+
!(connection && (connection.saveData || /2g/.test(connection.effectiveType))) &&
|
|
49
|
+
url(a);
|
|
50
|
+
href && preload(href, data);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Preload a link when the pointer rests on it for `delay` ms (default 20),
|
|
54
|
+
* when it takes focus, and on touchstart. Moving over a link that already
|
|
55
|
+
* preloaded does not preload it again until the pointer leaves. These
|
|
56
|
+
* listeners do not check modifier keys or `defaultPrevented`; clicks still do.
|
|
57
|
+
* Warms route code and, unless `data` is false (default true), runs the
|
|
58
|
+
* matched routes' `preload` functions.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const Router = createRouter({ routes, preloadLinks: intentPreload() });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const intentPreload = ({ delay = 20, data = true } = {}) => ctx => {
|
|
66
|
+
let timeout;
|
|
67
|
+
let lastElement;
|
|
68
|
+
setLinkPreloader(preloadRoute);
|
|
69
|
+
const onIntent = (evt) => {
|
|
70
|
+
// clear before resolving: a throw must not leave the rest timer armed
|
|
71
|
+
if (evt.type === "mousemove")
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
const res = ctx.anchor(evt);
|
|
74
|
+
const link = res && res[0].getAttribute("preload") !== "false" ? res : undefined;
|
|
75
|
+
if (evt.type !== "mousemove")
|
|
76
|
+
return link && ctx.preload(link[1], data);
|
|
77
|
+
if (!link)
|
|
78
|
+
return (lastElement = undefined);
|
|
79
|
+
const [a, url] = link;
|
|
80
|
+
if (lastElement === a)
|
|
81
|
+
return;
|
|
82
|
+
timeout = setTimeout(() => {
|
|
83
|
+
ctx.preload(url, data);
|
|
84
|
+
lastElement = a;
|
|
85
|
+
}, delay);
|
|
86
|
+
};
|
|
87
|
+
const passive = { passive: true };
|
|
88
|
+
document.addEventListener("focusin", onIntent, passive);
|
|
89
|
+
document.addEventListener("touchstart", onIntent, passive);
|
|
90
|
+
document.addEventListener("mousemove", onIntent, passive);
|
|
91
|
+
onCleanup(() => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
document.removeEventListener("focusin", onIntent);
|
|
94
|
+
document.removeEventListener("touchstart", onIntent);
|
|
95
|
+
document.removeEventListener("mousemove", onIntent);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Preload a link on `pointerdown` (mouse, touch, or pen), ahead of its click.
|
|
100
|
+
* Does not check modifier keys or `defaultPrevented`. Warms route code and,
|
|
101
|
+
* unless `data` is false (default true), route data.
|
|
102
|
+
*/
|
|
103
|
+
export const tapPreload = ({ data = true } = {}) => ctx => {
|
|
104
|
+
setLinkPreloader(preloadRoute);
|
|
105
|
+
const onTap = (evt) => {
|
|
106
|
+
const res = ctx.anchor(evt);
|
|
107
|
+
res && res[0].getAttribute("preload") !== "false" && ctx.preload(res[1], data);
|
|
108
|
+
};
|
|
109
|
+
document.addEventListener("pointerdown", onTap, { passive: true });
|
|
110
|
+
onCleanup(() => document.removeEventListener("pointerdown", onTap));
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Preload a link once it has stayed in the viewport for `delay` ms (default
|
|
114
|
+
* 100), when the browser is next idle (`requestIdleCallback`, or `setTimeout`
|
|
115
|
+
* where that is missing). Each link preloads once, and again after its `href`
|
|
116
|
+
* changes; a link that leaves before then is dropped. One `IntersectionObserver`
|
|
117
|
+
* is shared. `rootMargin` is passed through to it (the observer's own `"0px"`
|
|
118
|
+
* when omitted). Applies to links with `preload="viewport"`, or with `all`
|
|
119
|
+
* (default false) to every link except `preload="false"`. Skips Save-Data and
|
|
120
|
+
* 2g connections. Warms route code only unless `data` is true (default false).
|
|
121
|
+
*/
|
|
122
|
+
export const viewportPreload = ({ all, data = false, delay = 100, rootMargin } = {}) => ctx => {
|
|
123
|
+
if (typeof IntersectionObserver === "undefined")
|
|
124
|
+
return;
|
|
125
|
+
const warm = ambientPreload(ctx, "viewport", all, data);
|
|
126
|
+
// visible anchors: a pending dwell timer, or `true` once due
|
|
127
|
+
const visible = new Map();
|
|
128
|
+
let observer;
|
|
129
|
+
const leave = (a) => {
|
|
130
|
+
clearTimeout(visible.get(a));
|
|
131
|
+
visible.delete(a);
|
|
132
|
+
};
|
|
133
|
+
const flush = whenIdle(() => visible.forEach((due, a) => {
|
|
134
|
+
if (due !== true)
|
|
135
|
+
return;
|
|
136
|
+
observer.unobserve(a);
|
|
137
|
+
visible.delete(a);
|
|
138
|
+
warm(a);
|
|
139
|
+
}));
|
|
140
|
+
const intersect = (entries) => entries.forEach(({ target: a, isIntersecting }) => isIntersecting
|
|
141
|
+
? visible.set(a, setTimeout(() => (visible.set(a, true), flush()), delay))
|
|
142
|
+
: leave(a));
|
|
143
|
+
claimAnchors(a => (observer ||= new IntersectionObserver(intersect, { rootMargin })).observe(a), a => {
|
|
144
|
+
observer.unobserve(a);
|
|
145
|
+
leave(a);
|
|
146
|
+
});
|
|
147
|
+
onCleanup(() => {
|
|
148
|
+
observer && observer.disconnect();
|
|
149
|
+
visible.forEach((_, a) => leave(a));
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Preload links as soon as the page has loaded and the browser is idle,
|
|
154
|
+
* including links mounted later. A link removed before the flush is dropped.
|
|
155
|
+
* Applies to links with `preload="eager"`, or with `all` (default false) to
|
|
156
|
+
* every link except `preload="false"`. Skips Save-Data and 2g connections.
|
|
157
|
+
* Warms route code only unless `data` is true (default false).
|
|
158
|
+
*/
|
|
159
|
+
export const eagerPreload = ({ all, data = false } = {}) => ctx => {
|
|
160
|
+
const warm = ambientPreload(ctx, "eager", all, data);
|
|
161
|
+
const queue = new Set();
|
|
162
|
+
const flush = whenIdle(() => {
|
|
163
|
+
queue.forEach(warm);
|
|
164
|
+
queue.clear();
|
|
165
|
+
});
|
|
166
|
+
addEventListener("load", flush, { once: true });
|
|
167
|
+
claimAnchors(a => {
|
|
168
|
+
queue.add(a);
|
|
169
|
+
document.readyState === "complete" && flush();
|
|
170
|
+
}, a => queue.delete(a));
|
|
171
|
+
onCleanup(() => {
|
|
172
|
+
removeEventListener("load", flush);
|
|
173
|
+
queue.clear();
|
|
174
|
+
});
|
|
175
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RouterContext, TypedPath } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Warms the routes `url` matches: their lazy components (and any lazy
|
|
4
|
+
* subtree on the way) load, and with `preloadData` their `preload` functions
|
|
5
|
+
* run under `intent: "preload"`. Only link preload strategies and
|
|
6
|
+
* `usePreloadRoute` import it, so apps using neither don't ship it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function preloadRoute(router: RouterContext, url: URL, preloadData?: boolean): void;
|
|
9
|
+
/**
|
|
10
|
+
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
11
|
+
* work link preloading triggers: the matched routes' lazy components load,
|
|
12
|
+
* and with `preloadData` their `preload` functions run.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```js
|
|
16
|
+
* const preload = usePreloadRoute();
|
|
17
|
+
*
|
|
18
|
+
* preload(paths.users(2).settings, { preloadData: true });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const usePreloadRoute: () => (url: string | URL | TypedPath, options?: {
|
|
22
|
+
preloadData?: boolean;
|
|
23
|
+
}) => void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { runWithOwner, untrack } from "solid-js";
|
|
2
|
+
import { resolveLazySubtree, setInPreloadFn, setPreloadIntent, useRouter } from "./routing.js";
|
|
3
|
+
import { serverRouteArgs, serverRouteArgsEqual, serverRouteOf } from "./serverRouteShared.js";
|
|
4
|
+
import { extractSearchParams, mockBase } from "./utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* Warms the routes `url` matches: their lazy components (and any lazy
|
|
7
|
+
* subtree on the way) load, and with `preloadData` their `preload` functions
|
|
8
|
+
* run under `intent: "preload"`. Only link preload strategies and
|
|
9
|
+
* `usePreloadRoute` import it, so apps using neither don't ship it.
|
|
10
|
+
*/
|
|
11
|
+
export function preloadRoute(router, url, preloadData) {
|
|
12
|
+
const next = router._match(url.pathname);
|
|
13
|
+
// An unresolved lazy subtree in the chain: the placeholder's
|
|
14
|
+
// component.preload (below) kicks the table load; once it lands,
|
|
15
|
+
// preload again so the real inner routes warm too. Preloads are
|
|
16
|
+
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
17
|
+
// here — the real navigation surfaces and retries it.
|
|
18
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
19
|
+
if (boundary) {
|
|
20
|
+
try {
|
|
21
|
+
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(router, url, preloadData), () => { });
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
26
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
27
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
28
|
+
// already this selective (a matching level is reused and re-reads through
|
|
29
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
30
|
+
let current;
|
|
31
|
+
try {
|
|
32
|
+
current = untrack(router.matches);
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
const { location } = router;
|
|
36
|
+
const query = extractSearchParams(url);
|
|
37
|
+
const inputs = (p, q, s, r) => {
|
|
38
|
+
const a = serverRouteArgs(r, p, q);
|
|
39
|
+
a.search === undefined && (a.search = s);
|
|
40
|
+
return a;
|
|
41
|
+
};
|
|
42
|
+
const prevIntent = setPreloadIntent("preload");
|
|
43
|
+
for (let match in next) {
|
|
44
|
+
const { route, params } = next[match];
|
|
45
|
+
const { preload, component } = route;
|
|
46
|
+
component?.preload?.();
|
|
47
|
+
const now = current && current[match];
|
|
48
|
+
const unchanged = now &&
|
|
49
|
+
now.route.key === route.key &&
|
|
50
|
+
serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
51
|
+
setInPreloadFn(true);
|
|
52
|
+
preloadData &&
|
|
53
|
+
!unchanged &&
|
|
54
|
+
runWithOwner(router._routeOwner(), () => {
|
|
55
|
+
// A server component route's data IS its call: warm the same
|
|
56
|
+
// query entry the render will read, under the same derived args.
|
|
57
|
+
const server = serverRouteOf(component);
|
|
58
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
59
|
+
preload &&
|
|
60
|
+
preload({
|
|
61
|
+
params,
|
|
62
|
+
location: {
|
|
63
|
+
pathname: url.pathname,
|
|
64
|
+
search: url.search,
|
|
65
|
+
hash: url.hash,
|
|
66
|
+
query,
|
|
67
|
+
state: null,
|
|
68
|
+
key: ""
|
|
69
|
+
},
|
|
70
|
+
intent: "preload"
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
setInPreloadFn(false);
|
|
74
|
+
}
|
|
75
|
+
setPreloadIntent(prevIntent);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
79
|
+
* work link preloading triggers: the matched routes' lazy components load,
|
|
80
|
+
* and with `preloadData` their `preload` functions run.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```js
|
|
84
|
+
* const preload = usePreloadRoute();
|
|
85
|
+
*
|
|
86
|
+
* preload(paths.users(2).settings, { preloadData: true });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export const usePreloadRoute = () => {
|
|
90
|
+
const router = useRouter();
|
|
91
|
+
return (url, options) => preloadRoute(router, url instanceof URL ? url : new URL(String(url), mockBase), options?.preloadData);
|
|
92
|
+
};
|
|
@@ -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, LinksPlugin, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, TypedPath, ValidFilters } from "../types.js";
|
|
3
|
+
import type { DefinedRouteFilters, LazyRouteChildren, LinkPreload, LinksPlugin, 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
|
|
@@ -96,8 +96,18 @@ export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefini
|
|
|
96
96
|
* ```
|
|
97
97
|
*/
|
|
98
98
|
links?: LinksPlugin;
|
|
99
|
-
/**
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Link preload strategies: when to warm a link's route code and data ahead
|
|
101
|
+
* of the click. None by default. Client-only.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { createRouter, intentPreload } from "@solidjs/router";
|
|
106
|
+
*
|
|
107
|
+
* const Router = createRouter({ routes, preloadLinks: intentPreload() });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
preloadLinks?: LinkPreload | readonly LinkPreload[];
|
|
101
111
|
/**
|
|
102
112
|
* Explicit scroll restoration for back/forward navigation: positions are
|
|
103
113
|
* saved per history entry and restored once the navigation settles,
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -218,6 +218,10 @@ function staticIntegration(url, utils) {
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
export function createRouter(config) {
|
|
221
|
+
if (DEV && typeof config.preloadLinks === "boolean") {
|
|
222
|
+
console.warn("`preloadLinks` takes preload strategies, not a boolean, and preloads nothing by default. " +
|
|
223
|
+
"Use `preloadLinks: intentPreload()` for hover/focus preloading.");
|
|
224
|
+
}
|
|
221
225
|
const basePath = config.base || "";
|
|
222
226
|
// Routes are immutable per instance, so compilation is shared by every
|
|
223
227
|
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
@@ -272,8 +276,7 @@ export function createRouter(config) {
|
|
|
272
276
|
setupNativeEvents({
|
|
273
277
|
preload: config.preloadLinks,
|
|
274
278
|
explicitLinks: config.explicitLinks,
|
|
275
|
-
actionBase: config.actionBase
|
|
276
|
-
transformUrl: config.transformUrl
|
|
279
|
+
actionBase: config.actionBase
|
|
277
280
|
})(routerState);
|
|
278
281
|
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
279
282
|
if (routerState.singleFlight)
|
package/dist/routing.d.ts
CHANGED
|
@@ -104,21 +104,6 @@ export declare const useMatch: <S extends string | TypedPath>(path: () => S, mat
|
|
|
104
104
|
* ```
|
|
105
105
|
*/
|
|
106
106
|
export declare const useRouteMatches: () => () => RouteMatch[];
|
|
107
|
-
/**
|
|
108
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
109
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
110
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* ```js
|
|
114
|
-
* const preload = usePreloadRoute();
|
|
115
|
-
*
|
|
116
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
export declare const usePreloadRoute: () => (url: string | URL | TypedPath, options?: {
|
|
120
|
-
preloadData?: boolean;
|
|
121
|
-
}) => void;
|
|
122
107
|
/**
|
|
123
108
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
124
109
|
*
|
|
@@ -256,6 +241,7 @@ export declare function provideFlightConsumer(factory: (router: RouterContext) =
|
|
|
256
241
|
type FlashDecoder = (cookieHeader: string | null) => Promise<FlashSubmission | undefined>;
|
|
257
242
|
export declare function provideFlashDecoder(decoder: FlashDecoder): void;
|
|
258
243
|
export declare function getIntent(): Intent | undefined;
|
|
244
|
+
export declare function setPreloadIntent(intent: Intent | undefined): Intent | undefined;
|
|
259
245
|
export declare function getInPreloadFn(): boolean;
|
|
260
246
|
export declare function setInPreloadFn(value: boolean): void;
|
|
261
247
|
export declare function createRouterContext(integration: RouterIntegration, branches: () => Branch[], getContext?: () => any, options?: {
|
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, NotReadyError, untrack, useContext } from "solid-js";
|
|
6
6
|
import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
|
|
7
|
-
import { mockBase, createMemoObject, extractSearchParams, invariant,
|
|
7
|
+
import { mockBase, createMemoObject, extractSearchParams, invariant, linkMatcher, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
9
9
|
import { linkPending, routingState } from "./pending.js";
|
|
10
10
|
import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
|
|
@@ -148,22 +148,6 @@ export const useRouteMatches = () => {
|
|
|
148
148
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
149
149
|
return () => router.matches().slice();
|
|
150
150
|
};
|
|
151
|
-
/**
|
|
152
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
153
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
154
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* ```js
|
|
158
|
-
* const preload = usePreloadRoute();
|
|
159
|
-
*
|
|
160
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
161
|
-
* ```
|
|
162
|
-
*/
|
|
163
|
-
export const usePreloadRoute = () => {
|
|
164
|
-
const pre = useRouter().preloadRoute;
|
|
165
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
166
|
-
};
|
|
167
151
|
export function useParams(_path) {
|
|
168
152
|
return useRoute().params;
|
|
169
153
|
}
|
|
@@ -231,7 +215,7 @@ export const useLinkState = (href, options = {}) => {
|
|
|
231
215
|
const location = router.location;
|
|
232
216
|
const to = useResolvedPath(() => String(href()));
|
|
233
217
|
const base = router.base.path();
|
|
234
|
-
const state = createMemo(() =>
|
|
218
|
+
const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
|
|
235
219
|
return {
|
|
236
220
|
active: createMemo(() => state().active),
|
|
237
221
|
current: createMemo(() => state().current),
|
|
@@ -551,6 +535,11 @@ let preloadIntent;
|
|
|
551
535
|
export function getIntent() {
|
|
552
536
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
553
537
|
}
|
|
538
|
+
export function setPreloadIntent(intent) {
|
|
539
|
+
const prev = preloadIntent;
|
|
540
|
+
preloadIntent = intent;
|
|
541
|
+
return prev;
|
|
542
|
+
}
|
|
554
543
|
let inPreloadFn = false;
|
|
555
544
|
export function getInPreloadFn() {
|
|
556
545
|
return inPreloadFn;
|
|
@@ -663,11 +652,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
663
652
|
}
|
|
664
653
|
return read();
|
|
665
654
|
};
|
|
655
|
+
const matchPath = (pathname) => getRouteMatches(branches(), options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
666
656
|
const matches = createMemo(() => {
|
|
667
|
-
const
|
|
668
|
-
? options.transformUrl(location.pathname)
|
|
669
|
-
: location.pathname;
|
|
670
|
-
const m = getRouteMatches(branches(), pathname);
|
|
657
|
+
const m = matchPath(location.pathname);
|
|
671
658
|
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
672
659
|
// navigation transition (or the SSR stream) holds until the table lands.
|
|
673
660
|
// NotReadyError (not a returned promise) because a match chain is full
|
|
@@ -726,12 +713,13 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
726
713
|
intent: transitionIntent,
|
|
727
714
|
_source: source,
|
|
728
715
|
_owner: routerOwner,
|
|
716
|
+
_routeOwner: getContext,
|
|
717
|
+
_match: matchPath,
|
|
729
718
|
renderPath,
|
|
730
719
|
parsePath,
|
|
731
720
|
navigatorFactory,
|
|
732
721
|
matches,
|
|
733
722
|
beforeLeave,
|
|
734
|
-
preloadRoute,
|
|
735
723
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
736
724
|
get submissions() {
|
|
737
725
|
return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
|
|
@@ -860,72 +848,6 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
860
848
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
861
849
|
return ((to, options) => navigateFromRoute(route, to, options));
|
|
862
850
|
}
|
|
863
|
-
function preloadRoute(url, preloadData) {
|
|
864
|
-
const next = getRouteMatches(branches(), url.pathname);
|
|
865
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
866
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
867
|
-
// preload again so the real inner routes warm too. Preloads are
|
|
868
|
-
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
869
|
-
// here — the real navigation surfaces and retries it.
|
|
870
|
-
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
871
|
-
if (boundary) {
|
|
872
|
-
try {
|
|
873
|
-
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
|
|
874
|
-
}
|
|
875
|
-
catch { }
|
|
876
|
-
}
|
|
877
|
-
// Data preloads run only for levels a navigation would mount fresh or
|
|
878
|
-
// reuse with changed inputs: this level's params, and search as the
|
|
879
|
-
// declared schema's output or else the raw string. Navigation itself is
|
|
880
|
-
// already this selective (a matching level is reused and re-reads through
|
|
881
|
-
// tracked params), so an unchanged level has nothing new to warm.
|
|
882
|
-
let current;
|
|
883
|
-
try {
|
|
884
|
-
current = untrack(matches);
|
|
885
|
-
}
|
|
886
|
-
catch { }
|
|
887
|
-
const query = extractSearchParams(url);
|
|
888
|
-
const inputs = (p, q, s, r) => {
|
|
889
|
-
const a = serverRouteArgs(r, p, q);
|
|
890
|
-
a.search === undefined && (a.search = s);
|
|
891
|
-
return a;
|
|
892
|
-
};
|
|
893
|
-
const prevIntent = preloadIntent;
|
|
894
|
-
preloadIntent = "preload";
|
|
895
|
-
for (let match in next) {
|
|
896
|
-
const { route, params } = next[match];
|
|
897
|
-
const { preload, component } = route;
|
|
898
|
-
component?.preload?.();
|
|
899
|
-
const now = current && current[match];
|
|
900
|
-
const unchanged = now &&
|
|
901
|
-
now.route.key === route.key &&
|
|
902
|
-
serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
903
|
-
inPreloadFn = true;
|
|
904
|
-
preloadData &&
|
|
905
|
-
!unchanged &&
|
|
906
|
-
runWithOwner(getContext(), () => {
|
|
907
|
-
// A server component route's data IS its call: warm the same
|
|
908
|
-
// query entry the render will read, under the same derived args.
|
|
909
|
-
const server = serverRouteOf(component);
|
|
910
|
-
server && server.call(serverRouteArgs(route, params, query));
|
|
911
|
-
preload &&
|
|
912
|
-
preload({
|
|
913
|
-
params,
|
|
914
|
-
location: {
|
|
915
|
-
pathname: url.pathname,
|
|
916
|
-
search: url.search,
|
|
917
|
-
hash: url.hash,
|
|
918
|
-
query,
|
|
919
|
-
state: null,
|
|
920
|
-
key: ""
|
|
921
|
-
},
|
|
922
|
-
intent: "preload"
|
|
923
|
-
});
|
|
924
|
-
});
|
|
925
|
-
inPreloadFn = false;
|
|
926
|
-
}
|
|
927
|
-
preloadIntent = prevIntent;
|
|
928
|
-
}
|
|
929
851
|
// Seeds the initial submission from a no-JS form post: the server
|
|
930
852
|
// function runtime redirected back with the outcome in a one-shot flash
|
|
931
853
|
// cookie (its default no-JS convention), consumed eagerly above and
|
package/dist/types.d.ts
CHANGED
|
@@ -336,11 +336,14 @@ export interface RouterContext {
|
|
|
336
336
|
_source: () => LocationChange;
|
|
337
337
|
/** @internal The owner the router's own computations live under. */
|
|
338
338
|
_owner: Owner | null;
|
|
339
|
+
/** @internal The owner route components and their `preload` functions run under. */
|
|
340
|
+
_routeOwner: () => Owner;
|
|
341
|
+
/** @internal Route matches for a pathname, after `transformUrl`. */
|
|
342
|
+
_match: (pathname: string) => RouteMatch[];
|
|
339
343
|
matches: () => RouteMatch[];
|
|
340
344
|
renderPath(path: string): string;
|
|
341
345
|
parsePath(str: string): string;
|
|
342
346
|
beforeLeave: BeforeLeaveSlot;
|
|
343
|
-
preloadRoute: (url: URL, preloadData?: boolean) => void;
|
|
344
347
|
singleFlight: boolean;
|
|
345
348
|
submissions: Signal<Submission<any, any>[]>;
|
|
346
349
|
}
|
|
@@ -360,6 +363,21 @@ export type LinksPlugin = (router: RouterContext, base: string) => {
|
|
|
360
363
|
/** Whether an anchor resolving to `target` is the in-flight destination; read when `track()` is truthy. */
|
|
361
364
|
pending(target: string | undefined): boolean;
|
|
362
365
|
};
|
|
366
|
+
/** @internal What the router hands a link preload strategy. */
|
|
367
|
+
export interface LinkPreloadContext {
|
|
368
|
+
/** The router-managed anchor on an event's composed path, with its URL. */
|
|
369
|
+
anchor(evt: Event): readonly [HTMLAnchorElement | SVGAElement, URL] | undefined;
|
|
370
|
+
/** An anchor's URL, resolved now, when the router manages it. */
|
|
371
|
+
url(a: HTMLAnchorElement | SVGAElement): URL | undefined;
|
|
372
|
+
/** Warm `url`: route code always, `preload` functions too when `data` is true. */
|
|
373
|
+
preload(url: URL, data: boolean): void;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A link preload strategy for the router's `preloadLinks` option, such as
|
|
377
|
+
* `intentPreload()`. Client-only: runs once under the router's owner and
|
|
378
|
+
* cleans up with it.
|
|
379
|
+
*/
|
|
380
|
+
export type LinkPreload = (ctx: LinkPreloadContext) => void;
|
|
363
381
|
export interface BeforeLeaveEventArgs {
|
|
364
382
|
from: Location;
|
|
365
383
|
to: string | number;
|