@solidjs/router 2.0.0-next.36 → 2.0.0-next.37

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.
@@ -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 { matchLink } 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 && matchLink({ pathname: target.value, search: "" }, to, base, end).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
+ });
@@ -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, 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,28 @@ export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefini
83
83
  singleFlight?: boolean;
84
84
  actionBase?: string;
85
85
  explicitLinks?: 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;
86
99
  /** Preload route code/data on link hover and focus. Defaults to `true`. */
87
100
  preloadLinks?: boolean;
88
101
  /**
89
102
  * Explicit scroll restoration for back/forward navigation: positions are
90
103
  * saved per history entry and restored once the navigation settles,
91
104
  * replacing the browser heuristic that loses offsets when the destination
92
- * route forces a layout while rendering. Defaults to `true` with the
105
+ * route forces a layout while rendering. Reloads of a server-rendered page
106
+ * are left to the browser's native restoration, which waits for the
107
+ * streamed document to finish loading. Defaults to `true` with the
93
108
  * default browser history; a custom `history` adapter owns its session and
94
109
  * must opt in explicitly.
95
110
  */
@@ -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
@@ -246,7 +251,7 @@ export function createRouter(config) {
246
251
  let restoration;
247
252
  let history = config.history;
248
253
  if (!isServer && (config.scrollRestoration ?? !history)) {
249
- restoration = createScrollRestoration();
254
+ restoration = createScrollRestoration(isHydrating());
250
255
  history = withScrollRestoration(history || browserHistory(), restoration);
251
256
  }
252
257
  const integration = isServer
@@ -270,11 +275,15 @@ export function createRouter(config) {
270
275
  actionBase: config.actionBase,
271
276
  transformUrl: config.transformUrl
272
277
  })(routerState);
273
- setupLinkClaims(routerState, config.explicitLinks);
278
+ setupLinkClaims(routerState, config.explicitLinks, config.links);
274
279
  if (routerState.singleFlight)
275
280
  onCleanup(registerFlightRouter(routerState));
276
- restoration && restoration.create(routerState);
281
+ restoration && restoration.create();
277
282
  }
283
+ // Registered on both sides, outside the client-only branch: an owned
284
+ // onSettled takes a hydration id on the server too, so the ids line up.
285
+ // A no-op when the arrival is left to the browser's native restore.
286
+ onSettled(() => restoration && restoration.settled());
278
287
  return (<RouterContextObj value={routerState}>
279
288
  <Root routerState={routerState} root={root} preload={config.preload}>
280
289
  {(context = getOwner()) && null}
@@ -1,4 +1,3 @@
1
- import type { RouterContext } from "../types.js";
2
1
  import type { RouterHistory } from "./history.js";
3
2
  /**
4
3
  * Explicit scroll restoration for back/forward navigation. The browser's
@@ -13,7 +12,7 @@ import type { RouterHistory } from "./history.js";
13
12
  * so restoration survives reloads, which `scrollRestoration = "manual"`
14
13
  * otherwise disables.
15
14
  *
16
- * Restoration is a single scroll once routing settles — the same strategy
15
+ * Restoration is a single scroll once the traversal settles — the same strategy
17
16
  * SvelteKit, TanStack Router and React Router use. Settling after the
18
17
  * transition commits is what makes the offset reachable; chasing a still-
19
18
  * growing document afterwards (a ResizeObserver re-asserting the offset as
@@ -23,13 +22,38 @@ import type { RouterHistory } from "./history.js";
23
22
  * layout changes can feed it back into itself. Content that commits after the
24
23
  * transition settles — an image without reserved space, a boundary below the
25
24
  * fold — keeps whatever offset the document can hold.
25
+ *
26
+ * A server-rendered document load is the exception: the browser restores it
27
+ * natively once the document — streamed boundaries included — has loaded,
28
+ * which a restore at the first client flush cannot match. So an entry is
29
+ * handed back to the browser (`auto`) whenever its document may unload
30
+ * (pagehide), and a hydrating router arriving on a handed-back entry leaves
31
+ * the restore to the browser: it does not scroll, and it takes the entry back
32
+ * (`manual`) only after load or at the first client navigation, whichever
33
+ * comes first. A programmatic scroll during the load cancels the native
34
+ * restore in Firefox and WebKit, and WebKit restores just after the load
35
+ * event, so a `manual` write before then — inside a load listener included —
36
+ * suppresses it; the write waits a task. Handed-back depths are tracked here
37
+ * rather than read from `history.scrollRestoration`, which Firefox reports as
38
+ * the mode before pagehide after a reload. An entry this router pushed and
39
+ * never handed back stays `manual` and gets no native restore, so the router
40
+ * restores it as on a client-rendered load.
26
41
  */
27
- export declare function createScrollRestoration(): {
28
- /** When the adapter notifies a traversal: mark the target for restoration. */
42
+ export declare function createScrollRestoration(hydrating?: boolean): {
43
+ /**
44
+ * When the adapter notifies a traversal: mark the target and restore once
45
+ * the transition carrying it settles. The adapter notifies right before
46
+ * the location write, in the same tick, so the settle is that write's —
47
+ * a same-URL traversal included, which no location key would see.
48
+ */
29
49
  onPop(): void;
50
+ /** The initial page settled: restore a reload/back_forward arrival. */
51
+ settled: () => void;
52
+ /** Before a client navigation writes history: the entry it creates copies the current mode. */
53
+ beforeWrite: () => void;
30
54
  /** After a push: forward entries died, and this depth may be reused. */
31
55
  onPush(): void;
32
- create(router: RouterContext): void;
56
+ create(): void;
33
57
  };
34
58
  export type ScrollRestoration = ReturnType<typeof createScrollRestoration>;
35
59
  /**
@@ -1,6 +1,7 @@
1
- import { createEffect, onCleanup } from "solid-js";
1
+ import { onCleanup, onSettled, runWithOwner } from "solid-js";
2
2
  import { bindEvent, saveCurrentDepth } from "./history.js";
3
3
  const STORAGE_KEY = "solid-router:scroll";
4
+ const AUTO_KEY = "solid-router:scroll-auto";
4
5
  /**
5
6
  * Explicit scroll restoration for back/forward navigation. The browser's
6
7
  * native same-document heuristic is unreliable for suspense-driven rendering:
@@ -14,7 +15,7 @@ const STORAGE_KEY = "solid-router:scroll";
14
15
  * so restoration survives reloads, which `scrollRestoration = "manual"`
15
16
  * otherwise disables.
16
17
  *
17
- * Restoration is a single scroll once routing settles — the same strategy
18
+ * Restoration is a single scroll once the traversal settles — the same strategy
18
19
  * SvelteKit, TanStack Router and React Router use. Settling after the
19
20
  * transition commits is what makes the offset reachable; chasing a still-
20
21
  * growing document afterwards (a ResizeObserver re-asserting the offset as
@@ -24,18 +25,53 @@ const STORAGE_KEY = "solid-router:scroll";
24
25
  * layout changes can feed it back into itself. Content that commits after the
25
26
  * transition settles — an image without reserved space, a boundary below the
26
27
  * fold — keeps whatever offset the document can hold.
28
+ *
29
+ * A server-rendered document load is the exception: the browser restores it
30
+ * natively once the document — streamed boundaries included — has loaded,
31
+ * which a restore at the first client flush cannot match. So an entry is
32
+ * handed back to the browser (`auto`) whenever its document may unload
33
+ * (pagehide), and a hydrating router arriving on a handed-back entry leaves
34
+ * the restore to the browser: it does not scroll, and it takes the entry back
35
+ * (`manual`) only after load or at the first client navigation, whichever
36
+ * comes first. A programmatic scroll during the load cancels the native
37
+ * restore in Firefox and WebKit, and WebKit restores just after the load
38
+ * event, so a `manual` write before then — inside a load listener included —
39
+ * suppresses it; the write waits a task. Handed-back depths are tracked here
40
+ * rather than read from `history.scrollRestoration`, which Firefox reports as
41
+ * the mode before pagehide after a reload. An entry this router pushed and
42
+ * never handed back stays `manual` and gets no native restore, so the router
43
+ * restores it as on a client-rendered load.
27
44
  */
28
- export function createScrollRestoration() {
29
- window.history.scrollRestoration = "manual";
45
+ export function createScrollRestoration(hydrating) {
46
+ const h = window.history;
30
47
  // the current entry needs its depth stamp for captures to have a key, even
31
48
  // if something replaced history.state after the adapter stamped it
32
49
  saveCurrentDepth();
50
+ const depth = () => window.history.state && window.history.state._depth;
33
51
  let positions = {};
52
+ let handedBack = {};
34
53
  try {
35
54
  positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
55
+ handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
36
56
  }
37
57
  catch { }
38
- const depth = () => window.history.state && window.history.state._depth;
58
+ const manual = () => {
59
+ h.scrollRestoration = "manual";
60
+ const d = depth();
61
+ if (d != null)
62
+ delete handedBack[d];
63
+ };
64
+ const d = depth();
65
+ let deferred = !!hydrating && (h.scrollRestoration === "auto" || (d != null && !!handedBack[d]));
66
+ if (!deferred)
67
+ manual();
68
+ const claim = () => {
69
+ if (!deferred)
70
+ return;
71
+ deferred = false;
72
+ manual();
73
+ };
74
+ let timer;
39
75
  let programmatic = false;
40
76
  let pending;
41
77
  const unbind = [
@@ -48,11 +84,22 @@ export function createScrollRestoration() {
48
84
  pending = undefined;
49
85
  }),
50
86
  bindEvent(window, "pagehide", () => {
87
+ h.scrollRestoration = "auto";
88
+ const d = depth();
89
+ if (d != null)
90
+ handedBack[d] = 1;
51
91
  try {
52
92
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
93
+ sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
53
94
  }
54
95
  catch { }
55
- })
96
+ }),
97
+ bindEvent(window, "pageshow", e => {
98
+ if (e.persisted)
99
+ manual();
100
+ }),
101
+ bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))),
102
+ () => clearTimeout(timer)
56
103
  ];
57
104
  const restore = () => {
58
105
  if (pending == null)
@@ -68,43 +115,40 @@ export function createScrollRestoration() {
68
115
  programmatic = false;
69
116
  };
70
117
  return {
71
- /** When the adapter notifies a traversal: mark the target for restoration. */
118
+ /**
119
+ * When the adapter notifies a traversal: mark the target and restore once
120
+ * the transition carrying it settles. The adapter notifies right before
121
+ * the location write, in the same tick, so the settle is that write's —
122
+ * a same-URL traversal included, which no location key would see.
123
+ */
72
124
  onPop() {
125
+ claim();
73
126
  pending = depth();
127
+ runWithOwner(null, () => onSettled(restore));
74
128
  },
129
+ /** The initial page settled: restore a reload/back_forward arrival. */
130
+ settled: () => restore(),
131
+ /** Before a client navigation writes history: the entry it creates copies the current mode. */
132
+ beforeWrite: claim,
75
133
  /** After a push: forward entries died, and this depth may be reused. */
76
134
  onPush() {
77
135
  const d = depth();
78
- if (d != null)
136
+ if (d != null) {
79
137
  for (const k in positions)
80
138
  +k >= d && delete positions[k];
139
+ for (const k in handedBack)
140
+ +k >= d && delete handedBack[k];
141
+ }
81
142
  },
82
- create(router) {
83
- // Restore once the traversal has settled: key on the location (a fully
84
- // synchronous pop commits without isRouting ever flipping) and on
85
- // isRouting, which reports in-flight transitions — native pops
86
- // included — and holds the restore until they commit. restore() no-ops
87
- // unless a traversal marked a target, so push navigations are inert.
88
- // `transparent` keeps the effect invisible to the hydration id scheme —
89
- // same reasoning as the link-claims effect (claims.ts): this setup is
90
- // client-only, so an id-consuming node here has no server counterpart
91
- // and every hydration id allocated after it shifts by one child slot.
92
- // The visible failure is any <Loading> content that settled before the
93
- // shell flush (a cache hit, a preloaded query): its serialized value and
94
- // inlined markup are keyed under the server's ids, the shifted client
95
- // misses both, recomputes, and re-renders the route fresh — duplicating
96
- // the server DOM and leaving it inert.
97
- createEffect(() => ({
98
- url: router.location.pathname + router.location.search + router.location.hash,
99
- routing: router.isRouting()
100
- }), current => {
101
- if (!current.routing)
102
- restore();
103
- }, { transparent: true });
143
+ create() {
104
144
  onCleanup(() => unbind.forEach(u => u()));
105
145
  // reload/back_forward document loads land on an existing entry (a fresh
106
- // navigation starts a new one and belongs at the top); the effect's
107
- // initial run performs the restore after first render
146
+ // navigation starts a new one and belongs at the top); the router calls
147
+ // `settled` once the initial page has; a deferred arrival is the browser's
148
+ if (deferred) {
149
+ document.readyState === "complete" && claim();
150
+ return;
151
+ }
108
152
  const [nav] = performance.getEntriesByType?.("navigation");
109
153
  if (nav && nav.type !== "navigate")
110
154
  pending = depth();
@@ -121,6 +165,7 @@ export function withScrollRestoration(history, restoration) {
121
165
  return {
122
166
  ...history,
123
167
  set(next) {
168
+ restoration.beforeWrite();
124
169
  history.set(next);
125
170
  next.replace || restoration.onPush();
126
171
  },
package/dist/routing.d.ts CHANGED
@@ -54,6 +54,8 @@ export declare const useLocation: <S = unknown>() => Location<S>;
54
54
  /**
55
55
  * Retrieves a signal that indicates whether the router is currently processing a navigation.
56
56
  * Useful for showing pending navigation state while the next route and its data settle.
57
+ * This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
58
+ * For the in-flight destination, read the location with Solid's `isPending`/`latest`.
57
59
  *
58
60
  * @example
59
61
  * ```js
@@ -177,7 +179,11 @@ export interface LinkState {
177
179
  * parameter order and hash aside — what `aria-current="page"` reflects.
178
180
  */
179
181
  current: () => boolean;
180
- /** This link is the target of an in-flight navigation. Styling: `data-pending`. */
182
+ /**
183
+ * This link is the target of an in-flight navigation (back/forward
184
+ * excluded). Works without the `pendingLinks` plugin, which only adds
185
+ * `data-pending` to plain anchors.
186
+ */
181
187
  pending: () => boolean;
182
188
  }
183
189
  /**
package/dist/routing.js CHANGED
@@ -2,10 +2,11 @@ import { runWithOwner } from "solid-js";
2
2
  // standalone import: `DEV` is undefined in solid's production build, so app
3
3
  // bundlers fold `DEV &&` diagnostics out of shipped bundles
4
4
  import { DEV } from "solid-js";
5
- import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
5
+ import { createComponent, createContext, createMemo, createSignal, getOwner, NotReadyError, untrack, useContext } from "solid-js";
6
6
  import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
7
7
  import { mockBase, createMemoObject, extractSearchParams, invariant, matchLink, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
8
8
  import { HREF } from "./paths.js";
9
+ import { linkPending, routingState } from "./pending.js";
9
10
  import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
10
11
  const MAX_REDIRECTS = 100;
11
12
  /**
@@ -81,6 +82,8 @@ export const useLocation = () => useRouter().location;
81
82
  /**
82
83
  * Retrieves a signal that indicates whether the router is currently processing a navigation.
83
84
  * Useful for showing pending navigation state while the next route and its data settle.
85
+ * This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
86
+ * For the in-flight destination, read the location with Solid's `isPending`/`latest`.
84
87
  *
85
88
  * @example
86
89
  * ```js
@@ -93,7 +96,7 @@ export const useLocation = () => useRouter().location;
93
96
  * );
94
97
  * ```
95
98
  */
96
- export const useIsRouting = () => useRouter().isRouting;
99
+ export const useIsRouting = () => routingState(useRouter());
97
100
  /**
98
101
  * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
99
102
  * Useful for determining if a given path matches the current route.
@@ -237,10 +240,7 @@ export const useLinkState = (href, options = {}) => {
237
240
  // observe the committed location during a transition
238
241
  pending: createMemo(() => {
239
242
  state(); // location dependency: mid-flight target swaps recompute
240
- return (router.isRouting() &&
241
- !!router.pendingTarget &&
242
- matchLink({ pathname: router.pendingTarget.value, search: "" }, to(), base, options.end)
243
- .active);
243
+ return linkPending(router, to(), base, options.end);
244
244
  })
245
245
  };
246
246
  };
@@ -577,7 +577,11 @@ export function createRouterContext(integration, branches, getContext, options =
577
577
  _navigation: isServer ? undefined : 1
578
578
  });
579
579
  }
580
- const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
580
+ // The flushed world — what `latest(source)` answers — as the location's own
581
+ // memo sees it: it recomputes under the transition that carries a write,
582
+ // never for a write of this same tick (A28).
583
+ let flushed = initialSource;
584
+ const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
581
585
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
582
586
  // must be appended before streaming flushes the response headers, and an
583
587
  // unread outcome must not haunt a later request's render. Only detection
@@ -643,7 +647,9 @@ export function createRouterContext(integration, branches, getContext, options =
643
647
  // NotReadyError's source must be a reactive async node, not the raw
644
648
  // Promise. Keep one reader per boundary for this router owner so rejection
645
649
  // is delivered through the graph's error channel and resolution wakes the
646
- // parked matches computation.
650
+ // parked matches computation. `transparent`: the reader exists on the
651
+ // client only (the server parks `matches` on the promise directly), so it
652
+ // must not take a hydration id the server never allocated.
647
653
  const routerOwner = getOwner();
648
654
  const lazyReaders = new WeakMap();
649
655
  const readLazySubtree = (record) => {
@@ -652,7 +658,7 @@ export function createRouterContext(integration, branches, getContext, options =
652
658
  read = runWithOwner(routerOwner, () => createMemo(() => {
653
659
  const result = resolveLazySubtree(record);
654
660
  return result instanceof Promise ? result.then(() => undefined) : undefined;
655
- }, DEV && { name: "lazyRoutes" }));
661
+ }, { transparent: true, ...(DEV && { name: "lazyRoutes" }) }));
656
662
  lazyReaders.set(record, read);
657
663
  }
658
664
  return read();
@@ -688,30 +694,16 @@ export function createRouterContext(integration, branches, getContext, options =
688
694
  }
689
695
  return m;
690
696
  }, DEV && { name: "matches" });
691
- const routingPending = createMemo(() => isPending(() => {
692
- try {
693
- matches();
694
- }
695
- catch (e) {
696
- if (e instanceof NotReadyError)
697
- throw e;
698
- }
699
- location.search;
700
- location.hash;
701
- }), DEV && { name: "routingPending" });
702
- const isRouting = () => routingPending() || isPending(source);
697
+ const headedLocation = () => (isServer ? untrack(source) : flushed);
698
+ // A flushed navigation write whose settle has not run is still in flight.
699
+ const unsettled = (change) => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
703
700
  const transitionIntent = () => {
704
- if (!isPending(source))
701
+ const head = headedLocation();
702
+ if (!unsettled(head))
705
703
  return;
706
- const navigation = latest(source)._navigation;
704
+ const navigation = head._navigation;
707
705
  return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
708
706
  };
709
- const pendingNavigation = () => {
710
- if (!isRouting())
711
- return;
712
- const target = latest(source);
713
- return target._navigation && target._navigation > 0 ? target : undefined;
714
- };
715
707
  const buildParams = () => mergeParams(matches());
716
708
  const wrapParams = utils.paramsWrapper
717
709
  ? (getParams) => utils.paramsWrapper(getParams, branches)
@@ -731,11 +723,9 @@ export function createRouterContext(integration, branches, getContext, options =
731
723
  location,
732
724
  params,
733
725
  wrapParams,
734
- isRouting,
735
726
  intent: transitionIntent,
736
- get pendingTarget() {
737
- return pendingNavigation();
738
- },
727
+ _source: source,
728
+ _owner: routerOwner,
739
729
  renderPath,
740
730
  parsePath,
741
731
  navigatorFactory,
@@ -795,10 +785,10 @@ export function createRouterContext(integration, branches, getContext, options =
795
785
  // the destination the leave guard is told all read it. A write of this
796
786
  // same tick is not in it (A28) — see `compose` below.
797
787
  //
798
- // A redirect hop: the previous navigation is still pending, or has landed
799
- // but not yet reached history (a guard redirecting in the landing flush
800
- // — its destination was never shown either way).
801
- const headed = latest(source);
788
+ // A redirect hop: the previous navigation's write has not settled — it
789
+ // is still held, or a guard is redirecting in its landing flush — so its
790
+ // destination was never shown.
791
+ const headed = headedLocation();
802
792
  // A composed target (`setSearchParams`) is a function of where the
803
793
  // router is heading, an unflushed write of this tick included. Only
804
794
  // the integration's functional updater serves that value, so the
@@ -834,7 +824,7 @@ export function createRouterContext(integration, branches, getContext, options =
834
824
  const navigationDepth = !isServer &&
835
825
  headed._navigation !== undefined &&
836
826
  headed._navigation > 0 &&
837
- (isPending(source) || integration.inflight?.() === headed)
827
+ unsettled(headed)
838
828
  ? headed._navigation
839
829
  : 0;
840
830
  if (navigationDepth >= MAX_REDIRECTS) {