@solidjs/router 2.0.0-next.36 → 2.0.0-next.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -40
- package/dist/claims.d.ts +6 -4
- package/dist/claims.js +26 -17
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +45 -45
- package/dist/index.d.ts +5 -2
- package/dist/index.js +608 -260
- package/dist/index.jsx +4 -1
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- package/dist/preload.d.ts +53 -0
- package/dist/preload.js +175 -0
- package/dist/preloadRoute.d.ts +23 -0
- package/dist/preloadRoute.js +92 -0
- package/dist/routers/components.jsx +7 -4
- package/dist/routers/factory.d.ts +29 -4
- package/dist/routers/factory.jsx +27 -15
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +8 -16
- package/dist/routing.js +39 -127
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +46 -11
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- package/package.json +6 -6
|
@@ -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
|
|
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
|
-
/**
|
|
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(
|
|
56
|
+
create(): void;
|
|
33
57
|
};
|
|
34
58
|
export type ScrollRestoration = ReturnType<typeof createScrollRestoration>;
|
|
35
59
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
107
|
-
// initial
|
|
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
|
|
@@ -102,21 +104,6 @@ export declare const useMatch: <S extends string | TypedPath>(path: () => S, mat
|
|
|
102
104
|
* ```
|
|
103
105
|
*/
|
|
104
106
|
export declare const useRouteMatches: () => () => RouteMatch[];
|
|
105
|
-
/**
|
|
106
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
107
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
108
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```js
|
|
112
|
-
* const preload = usePreloadRoute();
|
|
113
|
-
*
|
|
114
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
export declare const usePreloadRoute: () => (url: string | URL | TypedPath, options?: {
|
|
118
|
-
preloadData?: boolean;
|
|
119
|
-
}) => void;
|
|
120
107
|
/**
|
|
121
108
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
122
109
|
*
|
|
@@ -177,7 +164,11 @@ export interface LinkState {
|
|
|
177
164
|
* parameter order and hash aside — what `aria-current="page"` reflects.
|
|
178
165
|
*/
|
|
179
166
|
current: () => boolean;
|
|
180
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* This link is the target of an in-flight navigation (back/forward
|
|
169
|
+
* excluded). Works without the `pendingLinks` plugin, which only adds
|
|
170
|
+
* `data-pending` to plain anchors.
|
|
171
|
+
*/
|
|
181
172
|
pending: () => boolean;
|
|
182
173
|
}
|
|
183
174
|
/**
|
|
@@ -250,6 +241,7 @@ export declare function provideFlightConsumer(factory: (router: RouterContext) =
|
|
|
250
241
|
type FlashDecoder = (cookieHeader: string | null) => Promise<FlashSubmission | undefined>;
|
|
251
242
|
export declare function provideFlashDecoder(decoder: FlashDecoder): void;
|
|
252
243
|
export declare function getIntent(): Intent | undefined;
|
|
244
|
+
export declare function setPreloadIntent(intent: Intent | undefined): Intent | undefined;
|
|
253
245
|
export declare function getInPreloadFn(): boolean;
|
|
254
246
|
export declare function setInPreloadFn(value: boolean): void;
|
|
255
247
|
export declare function createRouterContext(integration: RouterIntegration, branches: () => Branch[], getContext?: () => any, options?: {
|
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,
|
|
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,
|
|
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
|
+
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()
|
|
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.
|
|
@@ -145,22 +148,6 @@ export const useRouteMatches = () => {
|
|
|
145
148
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
146
149
|
return () => router.matches().slice();
|
|
147
150
|
};
|
|
148
|
-
/**
|
|
149
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
150
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
151
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```js
|
|
155
|
-
* const preload = usePreloadRoute();
|
|
156
|
-
*
|
|
157
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
export const usePreloadRoute = () => {
|
|
161
|
-
const pre = useRouter().preloadRoute;
|
|
162
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
163
|
-
};
|
|
164
151
|
export function useParams(_path) {
|
|
165
152
|
return useRoute().params;
|
|
166
153
|
}
|
|
@@ -228,7 +215,7 @@ export const useLinkState = (href, options = {}) => {
|
|
|
228
215
|
const location = router.location;
|
|
229
216
|
const to = useResolvedPath(() => String(href()));
|
|
230
217
|
const base = router.base.path();
|
|
231
|
-
const state = createMemo(() =>
|
|
218
|
+
const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
|
|
232
219
|
return {
|
|
233
220
|
active: createMemo(() => state().active),
|
|
234
221
|
current: createMemo(() => state().current),
|
|
@@ -237,10 +224,7 @@ export const useLinkState = (href, options = {}) => {
|
|
|
237
224
|
// observe the committed location during a transition
|
|
238
225
|
pending: createMemo(() => {
|
|
239
226
|
state(); // location dependency: mid-flight target swaps recompute
|
|
240
|
-
return (router
|
|
241
|
-
!!router.pendingTarget &&
|
|
242
|
-
matchLink({ pathname: router.pendingTarget.value, search: "" }, to(), base, options.end)
|
|
243
|
-
.active);
|
|
227
|
+
return linkPending(router, to(), base, options.end);
|
|
244
228
|
})
|
|
245
229
|
};
|
|
246
230
|
};
|
|
@@ -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;
|
|
@@ -577,7 +566,11 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
577
566
|
_navigation: isServer ? undefined : 1
|
|
578
567
|
});
|
|
579
568
|
}
|
|
580
|
-
|
|
569
|
+
// The flushed world — what `latest(source)` answers — as the location's own
|
|
570
|
+
// memo sees it: it recomputes under the transition that carries a write,
|
|
571
|
+
// never for a write of this same tick (A28).
|
|
572
|
+
let flushed = initialSource;
|
|
573
|
+
const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
|
|
581
574
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
582
575
|
// must be appended before streaming flushes the response headers, and an
|
|
583
576
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -643,7 +636,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
643
636
|
// NotReadyError's source must be a reactive async node, not the raw
|
|
644
637
|
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
645
638
|
// is delivered through the graph's error channel and resolution wakes the
|
|
646
|
-
// parked matches computation.
|
|
639
|
+
// parked matches computation. `transparent`: the reader exists on the
|
|
640
|
+
// client only (the server parks `matches` on the promise directly), so it
|
|
641
|
+
// must not take a hydration id the server never allocated.
|
|
647
642
|
const routerOwner = getOwner();
|
|
648
643
|
const lazyReaders = new WeakMap();
|
|
649
644
|
const readLazySubtree = (record) => {
|
|
@@ -652,16 +647,14 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
652
647
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
653
648
|
const result = resolveLazySubtree(record);
|
|
654
649
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
655
|
-
}, DEV && { name: "lazyRoutes" }));
|
|
650
|
+
}, { transparent: true, ...(DEV && { name: "lazyRoutes" }) }));
|
|
656
651
|
lazyReaders.set(record, read);
|
|
657
652
|
}
|
|
658
653
|
return read();
|
|
659
654
|
};
|
|
655
|
+
const matchPath = (pathname) => getRouteMatches(branches(), options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
660
656
|
const matches = createMemo(() => {
|
|
661
|
-
const
|
|
662
|
-
? options.transformUrl(location.pathname)
|
|
663
|
-
: location.pathname;
|
|
664
|
-
const m = getRouteMatches(branches(), pathname);
|
|
657
|
+
const m = matchPath(location.pathname);
|
|
665
658
|
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
666
659
|
// navigation transition (or the SSR stream) holds until the table lands.
|
|
667
660
|
// NotReadyError (not a returned promise) because a match chain is full
|
|
@@ -688,30 +681,16 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
688
681
|
}
|
|
689
682
|
return m;
|
|
690
683
|
}, DEV && { name: "matches" });
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
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);
|
|
684
|
+
const headedLocation = () => (isServer ? untrack(source) : flushed);
|
|
685
|
+
// A flushed navigation write whose settle has not run is still in flight.
|
|
686
|
+
const unsettled = (change) => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
|
|
703
687
|
const transitionIntent = () => {
|
|
704
|
-
|
|
688
|
+
const head = headedLocation();
|
|
689
|
+
if (!unsettled(head))
|
|
705
690
|
return;
|
|
706
|
-
const navigation =
|
|
691
|
+
const navigation = head._navigation;
|
|
707
692
|
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
708
693
|
};
|
|
709
|
-
const pendingNavigation = () => {
|
|
710
|
-
if (!isRouting())
|
|
711
|
-
return;
|
|
712
|
-
const target = latest(source);
|
|
713
|
-
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
714
|
-
};
|
|
715
694
|
const buildParams = () => mergeParams(matches());
|
|
716
695
|
const wrapParams = utils.paramsWrapper
|
|
717
696
|
? (getParams) => utils.paramsWrapper(getParams, branches)
|
|
@@ -731,17 +710,16 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
731
710
|
location,
|
|
732
711
|
params,
|
|
733
712
|
wrapParams,
|
|
734
|
-
isRouting,
|
|
735
713
|
intent: transitionIntent,
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
714
|
+
_source: source,
|
|
715
|
+
_owner: routerOwner,
|
|
716
|
+
_routeOwner: getContext,
|
|
717
|
+
_match: matchPath,
|
|
739
718
|
renderPath,
|
|
740
719
|
parsePath,
|
|
741
720
|
navigatorFactory,
|
|
742
721
|
matches,
|
|
743
722
|
beforeLeave,
|
|
744
|
-
preloadRoute,
|
|
745
723
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
746
724
|
get submissions() {
|
|
747
725
|
return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
|
|
@@ -795,10 +773,10 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
795
773
|
// the destination the leave guard is told all read it. A write of this
|
|
796
774
|
// same tick is not in it (A28) — see `compose` below.
|
|
797
775
|
//
|
|
798
|
-
// A redirect hop: the previous navigation
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
const headed =
|
|
776
|
+
// A redirect hop: the previous navigation's write has not settled — it
|
|
777
|
+
// is still held, or a guard is redirecting in its landing flush — so its
|
|
778
|
+
// destination was never shown.
|
|
779
|
+
const headed = headedLocation();
|
|
802
780
|
// A composed target (`setSearchParams`) is a function of where the
|
|
803
781
|
// router is heading, an unflushed write of this tick included. Only
|
|
804
782
|
// the integration's functional updater serves that value, so the
|
|
@@ -834,7 +812,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
834
812
|
const navigationDepth = !isServer &&
|
|
835
813
|
headed._navigation !== undefined &&
|
|
836
814
|
headed._navigation > 0 &&
|
|
837
|
-
(
|
|
815
|
+
unsettled(headed)
|
|
838
816
|
? headed._navigation
|
|
839
817
|
: 0;
|
|
840
818
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
@@ -870,72 +848,6 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
870
848
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
871
849
|
return ((to, options) => navigateFromRoute(route, to, options));
|
|
872
850
|
}
|
|
873
|
-
function preloadRoute(url, preloadData) {
|
|
874
|
-
const next = getRouteMatches(branches(), url.pathname);
|
|
875
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
876
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
877
|
-
// preload again so the real inner routes warm too. Preloads are
|
|
878
|
-
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
879
|
-
// here — the real navigation surfaces and retries it.
|
|
880
|
-
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
881
|
-
if (boundary) {
|
|
882
|
-
try {
|
|
883
|
-
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
|
|
884
|
-
}
|
|
885
|
-
catch { }
|
|
886
|
-
}
|
|
887
|
-
// Data preloads run only for levels a navigation would mount fresh or
|
|
888
|
-
// reuse with changed inputs: this level's params, and search as the
|
|
889
|
-
// declared schema's output or else the raw string. Navigation itself is
|
|
890
|
-
// already this selective (a matching level is reused and re-reads through
|
|
891
|
-
// tracked params), so an unchanged level has nothing new to warm.
|
|
892
|
-
let current;
|
|
893
|
-
try {
|
|
894
|
-
current = untrack(matches);
|
|
895
|
-
}
|
|
896
|
-
catch { }
|
|
897
|
-
const query = extractSearchParams(url);
|
|
898
|
-
const inputs = (p, q, s, r) => {
|
|
899
|
-
const a = serverRouteArgs(r, p, q);
|
|
900
|
-
a.search === undefined && (a.search = s);
|
|
901
|
-
return a;
|
|
902
|
-
};
|
|
903
|
-
const prevIntent = preloadIntent;
|
|
904
|
-
preloadIntent = "preload";
|
|
905
|
-
for (let match in next) {
|
|
906
|
-
const { route, params } = next[match];
|
|
907
|
-
const { preload, component } = route;
|
|
908
|
-
component?.preload?.();
|
|
909
|
-
const now = current && current[match];
|
|
910
|
-
const unchanged = now &&
|
|
911
|
-
now.route.key === route.key &&
|
|
912
|
-
serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
913
|
-
inPreloadFn = true;
|
|
914
|
-
preloadData &&
|
|
915
|
-
!unchanged &&
|
|
916
|
-
runWithOwner(getContext(), () => {
|
|
917
|
-
// A server component route's data IS its call: warm the same
|
|
918
|
-
// query entry the render will read, under the same derived args.
|
|
919
|
-
const server = serverRouteOf(component);
|
|
920
|
-
server && server.call(serverRouteArgs(route, params, query));
|
|
921
|
-
preload &&
|
|
922
|
-
preload({
|
|
923
|
-
params,
|
|
924
|
-
location: {
|
|
925
|
-
pathname: url.pathname,
|
|
926
|
-
search: url.search,
|
|
927
|
-
hash: url.hash,
|
|
928
|
-
query,
|
|
929
|
-
state: null,
|
|
930
|
-
key: ""
|
|
931
|
-
},
|
|
932
|
-
intent: "preload"
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
|
-
inPreloadFn = false;
|
|
936
|
-
}
|
|
937
|
-
preloadIntent = prevIntent;
|
|
938
|
-
}
|
|
939
851
|
// Seeds the initial submission from a no-JS form post: the server
|
|
940
852
|
// function runtime redirected back with the outcome in a one-shot flash
|
|
941
853
|
// cookie (its default no-JS convention), consumed eagerly above and
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// // before: a client component exists only to make the call
|
|
9
9
|
// const getStory = query(storyView, "story");
|
|
10
10
|
// component: props => {
|
|
11
|
-
// const View =
|
|
11
|
+
// const View = dynamicComponent(() => getStory(props.params.id));
|
|
12
12
|
// return <View>{props.children}</View>;
|
|
13
13
|
// }
|
|
14
14
|
// // after
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
// A server component that takes other client positions — handlers, refs,
|
|
37
37
|
// named slots — has a client half, and that half is a client component; the
|
|
38
38
|
// helper does not pretend otherwise. Write the wrapper for that route.
|
|
39
|
-
import {
|
|
39
|
+
import { dynamicComponent } from "@solidjs/web";
|
|
40
40
|
import { createComponent, DEV } from "solid-js";
|
|
41
41
|
import { SERVER_ROUTE } from "./serverRouteShared.js";
|
|
42
42
|
/**
|
|
@@ -77,8 +77,9 @@ source) {
|
|
|
77
77
|
};
|
|
78
78
|
function render(args, routeProps) {
|
|
79
79
|
// The source may answer a component, a promise of one, or (a live query)
|
|
80
|
-
// successive components; `
|
|
81
|
-
|
|
80
|
+
// successive components; `dynamicComponent`'s memo lands each the same
|
|
81
|
+
// way. Never a tag, so the element runtime `dynamic` retains stays out.
|
|
82
|
+
const View = dynamicComponent(() => call(args()));
|
|
82
83
|
return createComponent(View, {
|
|
83
84
|
get children() {
|
|
84
85
|
return routeProps.children;
|