@solidjs/router 2.0.0-next.35 → 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.
package/dist/index.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./lifecycle.js";
3
3
  export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
4
+ export { pendingLinks } from "./pending.js";
4
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
5
6
  export { int } from "./paths.js";
6
7
  export { serverRouteComponent } from "./serverRouteComponent.js";
package/dist/paths.d.ts CHANGED
@@ -82,9 +82,29 @@ type TuplePaths<R extends readonly unknown[], PAcc extends Params> = R extends r
82
82
  ...infer T extends readonly unknown[]
83
83
  ] ? RouteContrib<H, PAcc> & TuplePaths<T, PAcc> : {};
84
84
  type PathLeaf<Sch extends SearchTypes, C, PAcc extends Params> = PathEnd<Sch, Flat<PAcc>> & ChildPaths<C, PAcc>;
85
- type PathNode<Segs extends readonly string[], F, Sch extends SearchTypes, C, PAcc extends Params> = Segs extends readonly [infer H extends string, ...infer R extends readonly string[]] ? H extends `:${infer N}?` ? ((arg: ParamArg<N, F>) => PathNode<R, F, Sch, C, PAcc & {
85
+ declare const PARAM_NEXT: unique symbol;
86
+ /**
87
+ * Sibling routes that share a param prefix (`/posts/:id`, `/posts/:id/edit`)
88
+ * each contribute a call at that node, and a call on an intersection of
89
+ * function types resolves to its first overload only. So the call reads its
90
+ * result back through `this` — the whole intersection — where the
91
+ * continuations of every sibling with the same arity have merged.
92
+ */
93
+ interface ParamCall<A extends readonly unknown[]> {
94
+ (...args: A): this extends {
95
+ readonly [PARAM_NEXT]: {
96
+ [K in A["length"]]: infer N;
97
+ };
98
+ } ? N : never;
99
+ }
100
+ type ParamCallTo<A extends readonly unknown[], N> = ParamCall<A> & {
101
+ readonly [PARAM_NEXT]: {
102
+ [K in A["length"]]: N;
103
+ };
104
+ };
105
+ type PathNode<Segs extends readonly string[], F, Sch extends SearchTypes, C, PAcc extends Params> = Segs extends readonly [infer H extends string, ...infer R extends readonly string[]] ? H extends `:${infer N}?` ? ParamCallTo<[ParamArg<N, F>], PathNode<R, F, Sch, C, PAcc & {
86
106
  [K in N]?: string;
87
- }>) & PathNode<R, F, Sch, C, PAcc & {
107
+ }>> & PathNode<R, F, Sch, C, PAcc & {
88
108
  [K in N]?: string;
89
109
  }> : H extends `:${string}` | `*${string}` ? ParamCallNode<Segs, F, Sch, C, PAcc> : {
90
110
  [K in H]: PathNode<R, F, Sch, C, PAcc>;
@@ -93,7 +113,7 @@ type ParamCallNode<Segs extends readonly string[], F, Sch extends SearchTypes, C
93
113
  args: infer A extends readonly unknown[];
94
114
  params: infer P2 extends Params;
95
115
  rest: infer R2 extends readonly string[];
96
- } ? ((...args: A) => PathNode<R2, F, Sch, C, PAcc & P2>) & (R2 extends readonly [] ? {
116
+ } ? ParamCallTo<A, PathNode<R2, F, Sch, C, PAcc & P2>> & (R2 extends readonly [] ? {
97
117
  (...args: [...A, Sch["input"], string?]): string;
98
118
  } : {}) & TypedPath<Flat<PAcc & P2>> : never;
99
119
  type RouteContrib<Def, PAcc extends Params> = Def extends {
@@ -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
  /**