@solidjs/router 2.0.0-next.36 → 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 +103 -40
- package/dist/claims.d.ts +6 -4
- package/dist/claims.js +26 -17
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +45 -45
- package/dist/index.d.ts +5 -2
- package/dist/index.js +608 -260
- package/dist/index.jsx +4 -1
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- 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/components.jsx +7 -4
- package/dist/routers/factory.d.ts +29 -4
- package/dist/routers/factory.jsx +27 -15
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +8 -16
- package/dist/routing.js +39 -127
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +46 -11
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- package/package.json +6 -6
package/dist/index.jsx
CHANGED
|
@@ -1,6 +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
|
+
export { pendingLinks } from "./pending.js";
|
|
5
|
+
export { eagerPreload, intentPreload, tapPreload, viewportPreload } from "./preload.js";
|
|
6
|
+
export { usePreloadRoute } from "./preloadRoute.js";
|
|
4
7
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
5
8
|
export { int } from "./paths.js";
|
|
6
9
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LinksPlugin, LocationChange, RouterContext } from "./types.js";
|
|
2
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
3
|
+
export declare function routingState(router: RouterContext): () => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
6
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pendingTarget(router: RouterContext): LocationChange | undefined;
|
|
9
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
10
|
+
export declare function linkPending(router: RouterContext, to: string | undefined, base: string, end?: boolean): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
13
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
14
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
15
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
20
|
+
*
|
|
21
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
22
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const pendingLinks: LinksPlugin;
|
package/dist/pending.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createMemo, DEV, isPending, latest, NotReadyError, runWithOwner } from "solid-js";
|
|
2
|
+
import { linkMatcher } from "./utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
5
|
+
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
6
|
+
* and `latest` are UI affordances — the router's own coordination reads its
|
|
7
|
+
* location writes and their `onSettled` instead — so an app that renders
|
|
8
|
+
* none of these never bundles Solid's verdict machinery.
|
|
9
|
+
*/
|
|
10
|
+
const routing = new WeakMap();
|
|
11
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
12
|
+
export function routingState(router) {
|
|
13
|
+
let read = routing.get(router);
|
|
14
|
+
if (!read) {
|
|
15
|
+
const { location, matches, _source: source } = router;
|
|
16
|
+
// `transparent`: created on first use rather than at router setup, so it
|
|
17
|
+
// must not take a hydration id — the server may never create it, or
|
|
18
|
+
// create it at a different point.
|
|
19
|
+
const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
|
|
20
|
+
try {
|
|
21
|
+
matches();
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e instanceof NotReadyError)
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
location.search;
|
|
28
|
+
location.hash;
|
|
29
|
+
}), { transparent: true, ...(DEV && { name: "routingPending" }) }));
|
|
30
|
+
read = () => pending() || isPending(source);
|
|
31
|
+
routing.set(router, read);
|
|
32
|
+
}
|
|
33
|
+
return read;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
37
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
38
|
+
*/
|
|
39
|
+
export function pendingTarget(router) {
|
|
40
|
+
if (!routingState(router)())
|
|
41
|
+
return;
|
|
42
|
+
const target = latest(router._source);
|
|
43
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
46
|
+
export function linkPending(router, to, base, end) {
|
|
47
|
+
const target = pendingTarget(router);
|
|
48
|
+
return !!target && linkMatcher({ pathname: target.value, search: "" }, base, end)(to).active;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
52
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
53
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
54
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
59
|
+
*
|
|
60
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
61
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const pendingLinks = (router, base) => ({
|
|
65
|
+
track: routingState(router),
|
|
66
|
+
pending: target => linkPending(router, target, base)
|
|
67
|
+
});
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createMemo,
|
|
2
|
+
import { createMemo, createOwner, createRoot, onCleanup, runWithOwner, untrack, Show } from "solid-js";
|
|
3
3
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
4
|
import { createRouteContext, getIntent, getRouteMatches, resolveLazySubtree, RouteContextObj, setInPreloadFn, unresolvedLazyMatches } from "../routing.js";
|
|
5
5
|
import { serverRouteArgs, serverRouteOf } from "../serverRouteShared.js";
|
|
@@ -55,9 +55,12 @@ export function Routes(props) {
|
|
|
55
55
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
56
56
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
57
57
|
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
58
|
-
// are created under
|
|
59
|
-
// computation (which disposes its children every time it re-runs).
|
|
60
|
-
|
|
58
|
+
// are created under an owner of this component rather than the memo's
|
|
59
|
+
// computation (which disposes its children every time it re-runs). A
|
|
60
|
+
// dedicated one, created before the memo: the roots draw hydration ids from
|
|
61
|
+
// its counter, so the ids don't depend on whether the memo first parked on
|
|
62
|
+
// a lazy subtree (the server's render can park where the client's doesn't).
|
|
63
|
+
const owner = createOwner();
|
|
61
64
|
const routeStates = createMemo((prev) => {
|
|
62
65
|
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
63
66
|
// computation parks with it — no route contexts are created against
|
|
@@ -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, 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
|
|
@@ -83,13 +83,38 @@ export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefini
|
|
|
83
83
|
singleFlight?: boolean;
|
|
84
84
|
actionBase?: string;
|
|
85
85
|
explicitLinks?: boolean;
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Link claims plugin. `aria-current` and `data-active` are automatic; pass
|
|
88
|
+
* `pendingLinks` to also mark links covering the in-flight destination of a
|
|
89
|
+
* link click or `navigate()` with `data-pending`. Client-only.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
94
|
+
*
|
|
95
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
links?: LinksPlugin;
|
|
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[];
|
|
88
111
|
/**
|
|
89
112
|
* Explicit scroll restoration for back/forward navigation: positions are
|
|
90
113
|
* saved per history entry and restored once the navigation settles,
|
|
91
114
|
* replacing the browser heuristic that loses offsets when the destination
|
|
92
|
-
* route forces a layout while rendering.
|
|
115
|
+
* route forces a layout while rendering. Reloads of a server-rendered page
|
|
116
|
+
* are left to the browser's native restoration, which waits for the
|
|
117
|
+
* streamed document to finish loading. Defaults to `true` with the
|
|
93
118
|
* default browser history; a custom `history` adapter owns its session and
|
|
94
119
|
* must opt in explicitly.
|
|
95
120
|
*/
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
|
|
2
|
+
import { createSignal, getOwner, isHydrating, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
|
|
3
3
|
// standalone imports: `DEV` is undefined in solid's production build and
|
|
4
4
|
// `OBSERVE` outside its observe/dev builds, so app bundlers fold the
|
|
5
5
|
// `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
|
|
@@ -118,8 +118,8 @@ function describeInitial(match, location) {
|
|
|
118
118
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
119
119
|
function createIntegration(history, match) {
|
|
120
120
|
let committing = false;
|
|
121
|
-
//
|
|
122
|
-
|
|
121
|
+
// Writes whose transition has landed (see `RouterIntegration.settled`).
|
|
122
|
+
const settled = new WeakSet();
|
|
123
123
|
const wrap = (value) => (typeof value === "string" ? { value } : value);
|
|
124
124
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
125
125
|
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
@@ -138,15 +138,16 @@ function createIntegration(history, match) {
|
|
|
138
138
|
// the no-op rule compares against it, so `navigate()` behind another
|
|
139
139
|
// write in one handler sees that write rather than the flushed world.
|
|
140
140
|
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
141
|
-
if (written && written._navigation
|
|
141
|
+
if (written && written._navigation) {
|
|
142
142
|
const next = written;
|
|
143
|
-
inflight = next;
|
|
144
143
|
// Register out of band so a destination error boundary replacing the
|
|
145
|
-
// Router subtree cannot suppress the winning history commit.
|
|
144
|
+
// Router subtree cannot suppress the winning history commit. The
|
|
145
|
+
// settle is tied to the transition carrying the write: it fires when
|
|
146
|
+
// that lands, a rejection included, and a superseded write's settle
|
|
147
|
+
// fires with the transition that absorbed it.
|
|
146
148
|
runWithOwner(null, () => onSettled(() => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (read() !== next)
|
|
149
|
+
settled.add(next);
|
|
150
|
+
if (next._navigation < 0 || read() !== next)
|
|
150
151
|
return;
|
|
151
152
|
committing = true;
|
|
152
153
|
try {
|
|
@@ -182,7 +183,11 @@ function createIntegration(history, match) {
|
|
|
182
183
|
return;
|
|
183
184
|
signal[1]({ ...wrap(value), _navigation: -1 });
|
|
184
185
|
}));
|
|
185
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
signal,
|
|
188
|
+
settled: write => settled.has(write),
|
|
189
|
+
utils: history.utils
|
|
190
|
+
};
|
|
186
191
|
}
|
|
187
192
|
/**
|
|
188
193
|
* Server default: a static view of the request URL — no signal machinery, a
|
|
@@ -213,6 +218,10 @@ function staticIntegration(url, utils) {
|
|
|
213
218
|
};
|
|
214
219
|
}
|
|
215
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
|
+
}
|
|
216
225
|
const basePath = config.base || "";
|
|
217
226
|
// Routes are immutable per instance, so compilation is shared by every
|
|
218
227
|
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
@@ -246,7 +255,7 @@ export function createRouter(config) {
|
|
|
246
255
|
let restoration;
|
|
247
256
|
let history = config.history;
|
|
248
257
|
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
249
|
-
restoration = createScrollRestoration();
|
|
258
|
+
restoration = createScrollRestoration(isHydrating());
|
|
250
259
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
251
260
|
}
|
|
252
261
|
const integration = isServer
|
|
@@ -267,14 +276,17 @@ export function createRouter(config) {
|
|
|
267
276
|
setupNativeEvents({
|
|
268
277
|
preload: config.preloadLinks,
|
|
269
278
|
explicitLinks: config.explicitLinks,
|
|
270
|
-
actionBase: config.actionBase
|
|
271
|
-
transformUrl: config.transformUrl
|
|
279
|
+
actionBase: config.actionBase
|
|
272
280
|
})(routerState);
|
|
273
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
281
|
+
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
274
282
|
if (routerState.singleFlight)
|
|
275
283
|
onCleanup(registerFlightRouter(routerState));
|
|
276
|
-
restoration && restoration.create(
|
|
284
|
+
restoration && restoration.create();
|
|
277
285
|
}
|
|
286
|
+
// Registered on both sides, outside the client-only branch: an owned
|
|
287
|
+
// onSettled takes a hydration id on the server too, so the ids line up.
|
|
288
|
+
// A no-op when the arrival is left to the browser's native restore.
|
|
289
|
+
onSettled(() => restoration && restoration.settled());
|
|
278
290
|
return (<RouterContextObj value={routerState}>
|
|
279
291
|
<Root routerState={routerState} root={root} preload={config.preload}>
|
|
280
292
|
{(context = getOwner()) && null}
|