@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/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, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
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;
@@ -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;
@@ -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, createRoot, getOwner, onCleanup, runWithOwner, untrack, Show } from "solid-js";
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 the owner of this component rather than the memo's
59
- // computation (which disposes its children every time it re-runs).
60
- const owner = getOwner();
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
- /** Preload route code/data on link hover and focus. Defaults to `true`. */
87
- preloadLinks?: boolean;
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. Defaults to `true` with the
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
  */
@@ -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
- // Written, not yet in history (see `RouterIntegration.inflight`).
122
- let inflight;
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 && written._navigation > 0) {
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
- if (inflight === next)
148
- inflight = undefined;
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 { signal, inflight: () => inflight, utils: history.utils };
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(routerState);
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}