@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/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, 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
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 { matchLink } from "./utils.js";
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 && matchLink({ pathname: target.value, search: "" }, to, base, end).active;
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;
@@ -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
- /** Preload route code/data on link hover and focus. Defaults to `true`. */
100
- preloadLinks?: boolean;
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,
@@ -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, matchLink, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
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(() => matchLink(location, to(), base, options.end));
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 pathname = typeof options.transformUrl === "function"
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;