@solidjs/router 1.0.0 → 2.0.0-next.13
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 +606 -697
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +40 -11
- package/dist/data/action.js +307 -101
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +24 -23
- package/dist/data/index.d.ts +2 -4
- package/dist/data/index.js +2 -4
- package/dist/data/query.d.ts +1 -3
- package/dist/data/query.js +75 -32
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +1949 -1204
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +12 -26
- package/dist/routers/components.jsx +68 -54
- package/dist/routers/factory.d.ts +121 -0
- package/dist/routers/factory.jsx +154 -0
- package/dist/routers/history.d.ts +26 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +5 -11
- package/dist/routers/index.js +2 -6
- package/dist/routers/scrollRestoration.d.ts +10 -1
- package/dist/routers/scrollRestoration.js +49 -11
- package/dist/routing.d.ts +96 -52
- package/dist/routing.js +429 -172
- package/dist/server.d.ts +47 -0
- package/dist/server.js +156 -0
- package/dist/types.d.ts +158 -44
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +12 -7
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -40
- package/dist/data/createAsync.d.ts +0 -32
- package/dist/data/createAsync.js +0 -96
- package/dist/data/response.d.ts +0 -4
- package/dist/data/response.js +0 -42
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -17
- package/dist/routers/Router.js +0 -59
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -41
package/dist/routing.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createComponent, createContext, createMemo,
|
|
3
|
-
import { isServer, getRequestEvent } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { flush, runWithOwner } from "solid-js";
|
|
2
|
+
import { createComponent, createContext, createMemo, createSignal, isPending, NotReadyError, untrack, useContext } from "solid-js";
|
|
3
|
+
import { isServer, getRequestEvent } from "@solidjs/web";
|
|
4
|
+
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
5
|
+
import { clearFlashCookie, hasFlashCookie } from "@solidjs/web/server-functions";
|
|
6
6
|
const MAX_REDIRECTS = 100;
|
|
7
7
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
8
8
|
export const RouterContextObj = createContext();
|
|
9
9
|
export const RouteContextObj = createContext();
|
|
10
|
+
export function useOptionalContext(context) {
|
|
11
|
+
try {
|
|
12
|
+
return useContext(context);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
export const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
|
|
11
19
|
let TempRoute;
|
|
12
|
-
export const useRoute = () => TempRoute ||
|
|
20
|
+
export const useRoute = () => TempRoute || useOptionalContext(RouteContextObj) || useRouter().base;
|
|
13
21
|
export const useResolvedPath = (path) => {
|
|
14
22
|
const route = useRoute();
|
|
15
23
|
return createMemo(() => route.resolvePath(path()));
|
|
@@ -53,8 +61,8 @@ export const useNavigate = () => useRouter().navigatorFactory();
|
|
|
53
61
|
*/
|
|
54
62
|
export const useLocation = () => useRouter().location;
|
|
55
63
|
/**
|
|
56
|
-
* Retrieves signal that indicates whether the
|
|
57
|
-
* Useful for showing
|
|
64
|
+
* Retrieves a signal that indicates whether the router is currently processing a navigation.
|
|
65
|
+
* Useful for showing pending navigation state while the next route and its data settle.
|
|
58
66
|
*
|
|
59
67
|
* @example
|
|
60
68
|
* ```js
|
|
@@ -68,35 +76,30 @@ export const useLocation = () => useRouter().location;
|
|
|
68
76
|
* ```
|
|
69
77
|
*/
|
|
70
78
|
export const useIsRouting = () => useRouter().isRouting;
|
|
71
|
-
/**
|
|
72
|
-
* usePreloadRoute returns a function that can be used to preload a route manual.
|
|
73
|
-
* This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* ```js
|
|
77
|
-
* const preload = usePreloadRoute();
|
|
78
|
-
*
|
|
79
|
-
* preload(`/users/settings`, { preloadData: true });
|
|
80
|
-
* ```
|
|
81
|
-
*/
|
|
82
|
-
export const usePreloadRoute = () => {
|
|
83
|
-
const pre = useRouter().preloadRoute;
|
|
84
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
|
|
85
|
-
};
|
|
86
79
|
/**
|
|
87
80
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
88
81
|
* Useful for determining if a given path matches the current route.
|
|
89
82
|
*
|
|
83
|
+
* Accepts a pattern string — the match's `params` are typed from it — or a
|
|
84
|
+
* typed path node (a concrete URL, so no params to speak of):
|
|
85
|
+
*
|
|
90
86
|
* @example
|
|
91
87
|
* ```js
|
|
92
88
|
* const match = useMatch(() => props.href);
|
|
93
89
|
*
|
|
94
90
|
* return <div classList={{ active: Boolean(match()) }} />;
|
|
91
|
+
*
|
|
92
|
+
* const section = useMatch(() => "/docs/:page");
|
|
93
|
+
* section()?.params.page; // string
|
|
94
|
+
*
|
|
95
|
+
* const here = useMatch(() => paths.users(2));
|
|
95
96
|
* ```
|
|
96
97
|
*/
|
|
97
98
|
export const useMatch = (path, matchFilters) => {
|
|
98
99
|
const location = useLocation();
|
|
99
|
-
const matchers = createMemo(() =>
|
|
100
|
+
const matchers = createMemo(() =>
|
|
101
|
+
// `String()` folds typed path nodes to their URL
|
|
102
|
+
expandOptionals(String(path())).map(path => createMatcher(path, undefined, matchFilters)));
|
|
100
103
|
return createMemo(() => {
|
|
101
104
|
for (const matcher of matchers()) {
|
|
102
105
|
const match = matcher(location.pathname);
|
|
@@ -106,61 +109,44 @@ export const useMatch = (path, matchFilters) => {
|
|
|
106
109
|
});
|
|
107
110
|
};
|
|
108
111
|
/**
|
|
109
|
-
* `
|
|
110
|
-
*
|
|
112
|
+
* `useRouteMatches` returns an accessor of the router's resolved matches for
|
|
113
|
+
* the current location — the chain of route definitions producing the current
|
|
114
|
+
* render, outermost first. Useful for reading `info` metadata off the matched
|
|
115
|
+
* chain. Unlike `useMatch`, which tests a path pattern you supply against the
|
|
116
|
+
* location, this reflects the route tree itself.
|
|
111
117
|
*
|
|
112
118
|
* @example
|
|
113
119
|
* ```js
|
|
114
|
-
* const matches =
|
|
120
|
+
* const matches = useRouteMatches();
|
|
115
121
|
*
|
|
116
|
-
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
122
|
+
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
|
|
117
123
|
* ```
|
|
118
124
|
*/
|
|
119
|
-
export const
|
|
125
|
+
export const useRouteMatches = () => {
|
|
120
126
|
const router = useRouter();
|
|
121
127
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
122
128
|
return () => router.matches().slice();
|
|
123
129
|
};
|
|
124
130
|
/**
|
|
125
|
-
*
|
|
131
|
+
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
132
|
+
* work link hover/focus intent triggers automatically: the matched routes'
|
|
133
|
+
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
126
134
|
*
|
|
127
135
|
* @example
|
|
128
136
|
* ```js
|
|
129
|
-
* const
|
|
130
|
-
*
|
|
131
|
-
* // fetch user based on the id path parameter
|
|
132
|
-
* const [user] = createResource(() => params.id, fetchUser);
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
export const useParams = () => useRouter().params;
|
|
136
|
-
/**
|
|
137
|
-
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
|
|
138
|
-
* The object is a proxy so you must access properties to subscribe to reactive updates.
|
|
139
|
-
* **Note** that values will be strings and property names will retain their casing.
|
|
140
|
-
*
|
|
141
|
-
* The setter method accepts an object whose entries will be merged into the current query string.
|
|
142
|
-
* Values `''`, `undefined` and `null` will remove the key from the resulting query string.
|
|
143
|
-
* Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
|
|
144
|
-
*
|
|
145
|
-
* @examples
|
|
146
|
-
* ```js
|
|
147
|
-
* const [searchParams, setSearchParams] = useSearchParams();
|
|
137
|
+
* const preload = usePreloadRoute();
|
|
148
138
|
*
|
|
149
|
-
*
|
|
150
|
-
* <div>
|
|
151
|
-
* <span>Page: {searchParams.page}</span>
|
|
152
|
-
* <button
|
|
153
|
-
* onClick={() =>
|
|
154
|
-
* setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
|
|
155
|
-
* }
|
|
156
|
-
* >
|
|
157
|
-
* Next Page
|
|
158
|
-
* </button>
|
|
159
|
-
* </div>
|
|
160
|
-
* );
|
|
139
|
+
* preload(paths.users(2).settings, { preloadData: true });
|
|
161
140
|
* ```
|
|
162
141
|
*/
|
|
163
|
-
export const
|
|
142
|
+
export const usePreloadRoute = () => {
|
|
143
|
+
const pre = useRouter().preloadRoute;
|
|
144
|
+
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
145
|
+
};
|
|
146
|
+
export function useParams(_path) {
|
|
147
|
+
return useRoute().params;
|
|
148
|
+
}
|
|
149
|
+
export function useSearchParams(path) {
|
|
164
150
|
const router = useRouter();
|
|
165
151
|
const location = router.location;
|
|
166
152
|
const navigate = useNavigate();
|
|
@@ -180,55 +166,172 @@ export const useSearchParams = () => {
|
|
|
180
166
|
...options
|
|
181
167
|
});
|
|
182
168
|
};
|
|
183
|
-
|
|
184
|
-
|
|
169
|
+
// Passing a paths node opts into schema parsing. The node itself is a
|
|
170
|
+
// type-level reference; the schemas that run come from the currently
|
|
171
|
+
// matched routes (root→leaf), whose outputs merge over the raw query.
|
|
172
|
+
// A schema that reports issues is skipped, leaving raw values — search
|
|
173
|
+
// strings are user input, so defaults belong in the schema itself.
|
|
174
|
+
const query = path
|
|
175
|
+
? createMemoObject(createMemo(() => {
|
|
176
|
+
const raw = { ...location.query };
|
|
177
|
+
let result;
|
|
178
|
+
for (const match of router.matches()) {
|
|
179
|
+
const schema = match.route.key.search;
|
|
180
|
+
if (!schema)
|
|
181
|
+
continue;
|
|
182
|
+
const outcome = schema["~standard"].validate(raw);
|
|
183
|
+
if (outcome instanceof Promise)
|
|
184
|
+
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
185
|
+
if (!outcome.issues)
|
|
186
|
+
result = Object.assign(result || { ...raw }, outcome.value);
|
|
187
|
+
}
|
|
188
|
+
return result || raw;
|
|
189
|
+
}))
|
|
190
|
+
: location.query;
|
|
191
|
+
return [query, setSearchParams];
|
|
192
|
+
}
|
|
185
193
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* - from (*Location*): current location (before change).
|
|
190
|
-
* - to (*string | number*): path passed to `navigate`.
|
|
191
|
-
* - options (*NavigateOptions*): options passed to navigate.
|
|
192
|
-
* - preventDefault (*function*): call to block the route change.
|
|
193
|
-
* - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
|
|
194
|
-
* - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
|
|
194
|
+
* Reactive link state for custom link components — the programmatic
|
|
195
|
+
* counterpart of the attribute vocabulary plain anchors receive
|
|
196
|
+
* (`aria-current`, `data-active`, `data-pending`).
|
|
195
197
|
*
|
|
196
198
|
* @example
|
|
197
|
-
* ```
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* }
|
|
207
|
-
* }, 100);
|
|
208
|
-
* }
|
|
209
|
-
* });
|
|
199
|
+
* ```tsx
|
|
200
|
+
* function TabLink(props: { href: string; children: JSX.Element }) {
|
|
201
|
+
* const link = useLinkState(() => props.href);
|
|
202
|
+
* return (
|
|
203
|
+
* <a href={props.href} class="tab" data-selected={link.active() || undefined}>
|
|
204
|
+
* {props.children}
|
|
205
|
+
* </a>
|
|
206
|
+
* );
|
|
207
|
+
* }
|
|
210
208
|
* ```
|
|
211
209
|
*/
|
|
212
|
-
export const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
export const useLinkState = (href, options = {}) => {
|
|
211
|
+
const router = useRouter();
|
|
212
|
+
const location = router.location;
|
|
213
|
+
const to = useResolvedPath(() => String(href()));
|
|
214
|
+
// trailing slashes are ignored so `/route` and `/route/` share state
|
|
215
|
+
const path = createMemo(() => {
|
|
216
|
+
const to_ = to();
|
|
217
|
+
return to_ === undefined ? undefined : comparablePath(to_);
|
|
217
218
|
});
|
|
218
|
-
|
|
219
|
+
const matches = (loc) => {
|
|
220
|
+
const path_ = path();
|
|
221
|
+
if (path_ === undefined)
|
|
222
|
+
return [false, false];
|
|
223
|
+
const exact = loc === path_;
|
|
224
|
+
return [exact || (!options.end && loc.startsWith(path_ + "/")), exact];
|
|
225
|
+
};
|
|
226
|
+
const state = createMemo(() => matches(decodeURI(comparablePath(location.pathname))));
|
|
227
|
+
return {
|
|
228
|
+
active: () => state()[0],
|
|
229
|
+
current: () => state()[1],
|
|
230
|
+
// match the in-flight target explicitly (rather than active-while-routing)
|
|
231
|
+
// so the answer is the same from pure reads and from effects, which
|
|
232
|
+
// observe the committed location during a transition
|
|
233
|
+
pending: createMemo(() => {
|
|
234
|
+
state(); // location dependency: mid-flight target swaps recompute
|
|
235
|
+
return (router.isRouting() &&
|
|
236
|
+
!!router.pendingTarget &&
|
|
237
|
+
matches(decodeURI(comparablePath(router.pendingTarget.value)))[0]);
|
|
238
|
+
})
|
|
239
|
+
};
|
|
219
240
|
};
|
|
220
241
|
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
221
242
|
// pchar characters (sub-delims / ":" / "@") literal, matching how browsers
|
|
222
243
|
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
223
244
|
// still percent-encoded exactly as before, since browsers encode those too.
|
|
224
245
|
const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Lazy route subtrees
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
//
|
|
250
|
+
// A `children` thunk (`() => import("./feature/routes")`) is a *boundary*:
|
|
251
|
+
// until it resolves, the compiled tree carries a param-less catch-all
|
|
252
|
+
// placeholder branch under the boundary's pattern (splat-scored, so static
|
|
253
|
+
// siblings still win). Resolution is append-only and cached per thunk, then a
|
|
254
|
+
// module-level version signal bumps and every `branches()` consumer
|
|
255
|
+
// recompiles — matches, params, and route states all react. The placeholder's
|
|
256
|
+
// component reads a memo of the resolution promise, which keeps the enclosing
|
|
257
|
+
// navigation transition pending exactly like a `lazy()` route component; its
|
|
258
|
+
// `preload` *is* the resolver, so hover-intent preloading kicks the table
|
|
259
|
+
// load through the existing component-preload path.
|
|
260
|
+
const lazyBoundaries = new WeakMap();
|
|
261
|
+
// Module scope: boundary resolution is global, deterministic state (same
|
|
262
|
+
// thunk -> same routes), shared by every factory instance and the server's
|
|
263
|
+
// flight collector.
|
|
264
|
+
const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
|
|
265
|
+
/** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
|
|
266
|
+
export function trackLazySubtrees() {
|
|
267
|
+
return lazyTreeVersion();
|
|
268
|
+
}
|
|
269
|
+
/** Non-reactive read, for cache-busting outside the reactive graph (server collectors). */
|
|
270
|
+
export function peekLazySubtrees() {
|
|
271
|
+
return untrack(lazyTreeVersion);
|
|
272
|
+
}
|
|
273
|
+
function getLazyBoundary(thunk) {
|
|
274
|
+
let record = lazyBoundaries.get(thunk);
|
|
275
|
+
if (!record)
|
|
276
|
+
lazyBoundaries.set(thunk, (record = { thunk }));
|
|
277
|
+
return record;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Kicks (or joins) a boundary's resolution. Returns the resolved routes
|
|
281
|
+
* synchronously once available, the in-flight promise otherwise. Commit is
|
|
282
|
+
* always async — even for thunks returning arrays — so the version bump
|
|
283
|
+
* never writes a signal from inside a render computation.
|
|
284
|
+
*/
|
|
285
|
+
export function resolveLazySubtree(record) {
|
|
286
|
+
if (record.resolved)
|
|
287
|
+
return record.resolved;
|
|
288
|
+
return (record.promise ||= Promise.resolve(record.thunk()).then(m => {
|
|
289
|
+
const routes = Array.isArray(m)
|
|
290
|
+
? m
|
|
291
|
+
: m
|
|
292
|
+
.default ||
|
|
293
|
+
m.routes ||
|
|
294
|
+
[];
|
|
295
|
+
record.resolved = routes;
|
|
296
|
+
setLazyTreeVersion(v => v + 1);
|
|
297
|
+
return record.resolved;
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The unresolved boundaries in a match chain. Rendering gates on these (the
|
|
302
|
+
* route-states memo suspends until they land — see routers/components.tsx)
|
|
303
|
+
* and the server's flight collector awaits them before its preload pass.
|
|
304
|
+
*/
|
|
305
|
+
export function unresolvedLazyMatches(matches) {
|
|
306
|
+
const pending = [];
|
|
307
|
+
for (const match of matches)
|
|
308
|
+
if (match.route.lazy && !match.route.lazy.resolved)
|
|
309
|
+
pending.push(match.route.lazy);
|
|
310
|
+
return pending;
|
|
311
|
+
}
|
|
312
|
+
function createLazyPlaceholder(pattern, record) {
|
|
313
|
+
// The placeholder never renders and needs no component — `matches` parks
|
|
314
|
+
// on unresolved boundaries before route contexts are created (kicking the
|
|
315
|
+
// resolver as it does), preloadRoute kicks it directly, and the version
|
|
316
|
+
// bump swaps in the real routes. `path + "/*"` with no splat name matches
|
|
317
|
+
// the boundary itself and everything beneath it without recording a param
|
|
318
|
+
// (createMatcher skips empty splat names).
|
|
319
|
+
const placeholderPattern = pattern + "/*";
|
|
320
|
+
return {
|
|
321
|
+
key: record,
|
|
322
|
+
originalPath: "*",
|
|
323
|
+
pattern: placeholderPattern,
|
|
324
|
+
matcher: createMatcher(placeholderPattern),
|
|
325
|
+
lazy: record
|
|
326
|
+
};
|
|
327
|
+
}
|
|
225
328
|
export function createRoutes(routeDef, base = "") {
|
|
226
|
-
const { component, preload,
|
|
329
|
+
const { component, preload, children, info } = routeDef;
|
|
227
330
|
const isLeaf = !children || (Array.isArray(children) && !children.length);
|
|
228
331
|
const shared = {
|
|
229
332
|
key: routeDef,
|
|
230
333
|
component,
|
|
231
|
-
preload
|
|
334
|
+
preload,
|
|
232
335
|
info
|
|
233
336
|
};
|
|
234
337
|
return asArray(routeDef.path).reduce((acc, originalPath) => {
|
|
@@ -285,9 +388,24 @@ export function createBranches(routeDef, base = "", stack = [], branches = []) {
|
|
|
285
388
|
const routes = createRoutes(def, base);
|
|
286
389
|
for (const route of routes) {
|
|
287
390
|
stack.push(route);
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
391
|
+
let children = def.children;
|
|
392
|
+
if (typeof children === "function") {
|
|
393
|
+
const record = getLazyBoundary(children);
|
|
394
|
+
if (record.resolved) {
|
|
395
|
+
children = record.resolved;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
// unresolved boundary: a catch-all placeholder holds its ground
|
|
399
|
+
stack.push(createLazyPlaceholder(route.pattern, record));
|
|
400
|
+
branches.push(createBranch([...stack], branches.length));
|
|
401
|
+
stack.pop();
|
|
402
|
+
stack.pop();
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const isEmptyArray = Array.isArray(children) && children.length === 0;
|
|
407
|
+
if (children && !isEmptyArray) {
|
|
408
|
+
createBranches(children, route.pattern, stack, branches);
|
|
291
409
|
}
|
|
292
410
|
else {
|
|
293
411
|
const branch = createBranch([...stack], branches.length);
|
|
@@ -309,9 +427,16 @@ export function getRouteMatches(branches, location) {
|
|
|
309
427
|
}
|
|
310
428
|
return [];
|
|
311
429
|
}
|
|
430
|
+
export function mergeParams(matches) {
|
|
431
|
+
const params = {};
|
|
432
|
+
for (let i = 0; i < matches.length; i++) {
|
|
433
|
+
Object.assign(params, matches[i].params);
|
|
434
|
+
}
|
|
435
|
+
return params;
|
|
436
|
+
}
|
|
312
437
|
function createLocation(path, state, queryWrapper) {
|
|
313
438
|
const origin = new URL(mockBase);
|
|
314
|
-
const url = createMemo(prev => {
|
|
439
|
+
const url = createMemo((prev = origin) => {
|
|
315
440
|
const path_ = path();
|
|
316
441
|
try {
|
|
317
442
|
// anchor rooted paths against the origin explicitly - a path with
|
|
@@ -322,14 +447,14 @@ function createLocation(path, state, queryWrapper) {
|
|
|
322
447
|
console.error(`Invalid path ${path_}`);
|
|
323
448
|
return prev;
|
|
324
449
|
}
|
|
325
|
-
},
|
|
450
|
+
}, {
|
|
326
451
|
equals: (a, b) => a.href === b.href
|
|
327
452
|
});
|
|
328
453
|
const pathname = createMemo(() => url().pathname);
|
|
329
|
-
const search = createMemo(() => url().search
|
|
454
|
+
const search = createMemo(() => url().search);
|
|
330
455
|
const hash = createMemo(() => url().hash);
|
|
331
456
|
const key = () => "";
|
|
332
|
-
const queryFn =
|
|
457
|
+
const queryFn = createMemo(() => extractSearchParams(url()));
|
|
333
458
|
return {
|
|
334
459
|
get pathname() {
|
|
335
460
|
return pathname();
|
|
@@ -349,6 +474,49 @@ function createLocation(path, state, queryWrapper) {
|
|
|
349
474
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
350
475
|
};
|
|
351
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Rendezvous between the router and the data layer's single-flight consumer.
|
|
479
|
+
* The Router registers itself at mount (unless `singleFlight={false}`); the
|
|
480
|
+
* action side provides the consumer factory when the first action is created
|
|
481
|
+
* (see data/action.ts). Whichever side arrives first waits for the other, so
|
|
482
|
+
* an action module loaded lazily (a code-split route) still attaches to the
|
|
483
|
+
* already-mounted router — and a router-only app, where no action ever
|
|
484
|
+
* loads, never subscribes to the transport, so the server is never asked to
|
|
485
|
+
* collect.
|
|
486
|
+
*/
|
|
487
|
+
let flightConsumerFactory;
|
|
488
|
+
const flightRouters = new Map();
|
|
489
|
+
export function registerFlightRouter(router) {
|
|
490
|
+
flightRouters.set(router, flightConsumerFactory && flightConsumerFactory(router));
|
|
491
|
+
return () => {
|
|
492
|
+
const unsubscribe = flightRouters.get(router);
|
|
493
|
+
flightRouters.delete(router);
|
|
494
|
+
unsubscribe && unsubscribe();
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
export function provideFlightConsumer(factory) {
|
|
498
|
+
if (flightConsumerFactory)
|
|
499
|
+
return;
|
|
500
|
+
flightConsumerFactory = factory;
|
|
501
|
+
for (const [router, unsubscribe] of flightRouters) {
|
|
502
|
+
if (!unsubscribe)
|
|
503
|
+
flightRouters.set(router, factory(router));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The flash-cookie codec, provided by the action side (data/action.ts) so
|
|
508
|
+
* the router core never carries it: the core consumes the cookie eagerly
|
|
509
|
+
* per request (detection + one-shot clear via the runtime's isomorphic half)
|
|
510
|
+
* but defers decoding to this slot, read when the submissions signal
|
|
511
|
+
* initializes. Actions are created at module scope, so on the server the
|
|
512
|
+
* decoder is always installed before useSubmission can read — and a
|
|
513
|
+
* router-only app, where it never installs, has no actions that could have
|
|
514
|
+
* produced a flash cookie in the first place.
|
|
515
|
+
*/
|
|
516
|
+
let flashDecoder;
|
|
517
|
+
export function provideFlashDecoder(decoder) {
|
|
518
|
+
flashDecoder || (flashDecoder = decoder);
|
|
519
|
+
}
|
|
352
520
|
let intent;
|
|
353
521
|
export function getIntent() {
|
|
354
522
|
return intent;
|
|
@@ -364,86 +532,102 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
364
532
|
const { signal: [source, setSource], utils = {} } = integration;
|
|
365
533
|
const parsePath = utils.parsePath || (p => p);
|
|
366
534
|
const renderPath = utils.renderPath || (p => p);
|
|
367
|
-
|
|
535
|
+
// An empty slot until `useBeforeLeave` installs the guard on first use.
|
|
536
|
+
const beforeLeave = utils.beforeLeave || {};
|
|
368
537
|
const basePath = resolvePath("", options.base || "");
|
|
538
|
+
const initialSource = untrack(source);
|
|
369
539
|
if (basePath === undefined) {
|
|
370
540
|
throw new Error(`${basePath} is not a valid base path`);
|
|
371
541
|
}
|
|
372
|
-
else if (basePath && !
|
|
542
|
+
else if (basePath && !initialSource.value) {
|
|
373
543
|
setSource({ value: basePath, replace: true, scroll: false });
|
|
374
544
|
}
|
|
375
|
-
const [
|
|
376
|
-
//
|
|
545
|
+
const [isNavigating, setIsRouting] = createSignal(false, { ownedWrite: true });
|
|
546
|
+
// Navigate override written from event handlers.
|
|
547
|
+
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
|
|
548
|
+
ownedWrite: true
|
|
549
|
+
});
|
|
550
|
+
// Keep track of last target, so that last call to navigate wins
|
|
377
551
|
let lastTransitionTarget;
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (lastTransitionTarget === undefined)
|
|
383
|
-
setIsRouting(true);
|
|
384
|
-
intent = newIntent;
|
|
385
|
-
lastTransitionTarget = newTarget;
|
|
386
|
-
startTransition(() => {
|
|
387
|
-
if (lastTransitionTarget !== newTarget)
|
|
388
|
-
return;
|
|
389
|
-
setReference(lastTransitionTarget.value);
|
|
390
|
-
setState(lastTransitionTarget.state);
|
|
391
|
-
resetErrorBoundaries();
|
|
392
|
-
if (!isServer)
|
|
393
|
-
submissions[1](subs => subs.filter(s => s.pending));
|
|
394
|
-
}).finally(() => {
|
|
395
|
-
if (lastTransitionTarget !== newTarget)
|
|
396
|
-
return;
|
|
397
|
-
// Batch, in order for isRouting and final source update to happen together
|
|
398
|
-
batch(() => {
|
|
399
|
-
intent = undefined;
|
|
400
|
-
if (newIntent === "navigate")
|
|
401
|
-
navigateEnd(lastTransitionTarget);
|
|
402
|
-
setIsRouting(false);
|
|
403
|
-
lastTransitionTarget = undefined;
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
};
|
|
407
|
-
const [reference, setReference] = createSignal(source().value);
|
|
408
|
-
const [state, setState] = createSignal(source().state);
|
|
409
|
-
const location = createLocation(reference, state, utils.queryWrapper);
|
|
552
|
+
// source() remains canonical for native history changes; navigateTarget()
|
|
553
|
+
// temporarily overrides it for in-flight programmatic navigation.
|
|
554
|
+
const effective = createMemo(() => navigateTarget() ?? source());
|
|
555
|
+
const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
|
|
410
556
|
const referrers = [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
557
|
+
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
558
|
+
// must be appended before streaming flushes the response headers, and an
|
|
559
|
+
// unread outcome must not haunt a later request's render. Only detection
|
|
560
|
+
// and clearing happen here (the runtime's isomorphic half); the raw header
|
|
561
|
+
// is stashed and decoding waits for the action-provided codec, read when
|
|
562
|
+
// the lazily allocated submissions signal below first initializes.
|
|
563
|
+
let flashCookieHeader;
|
|
564
|
+
if (isServer) {
|
|
565
|
+
const e = getRequestEvent();
|
|
566
|
+
if (e && !(e.router && e.router.submission)) {
|
|
567
|
+
const cookieHeader = e.request.headers.get("cookie");
|
|
568
|
+
if (hasFlashCookie(cookieHeader)) {
|
|
569
|
+
flashCookieHeader = cookieHeader;
|
|
570
|
+
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
571
|
+
if (e.response && e.response.headers)
|
|
572
|
+
e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
573
|
+
}
|
|
415
574
|
}
|
|
416
|
-
|
|
575
|
+
}
|
|
576
|
+
let submissions;
|
|
577
|
+
const matches = createMemo(() => {
|
|
578
|
+
const pathname = typeof options.transformUrl === "function"
|
|
579
|
+
? options.transformUrl(location.pathname)
|
|
580
|
+
: location.pathname;
|
|
581
|
+
const m = getRouteMatches(branches(), pathname);
|
|
582
|
+
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
583
|
+
// navigation transition (or the SSR stream) holds until the table lands.
|
|
584
|
+
// NotReadyError (not a returned promise) because a match chain is full
|
|
585
|
+
// of component functions the hydration serializer must never see. The
|
|
586
|
+
// recompute comes from the version-signal dependency on the client and
|
|
587
|
+
// from the carried promise's retry on the server; a boundary nested
|
|
588
|
+
// inside a boundary just parks the recomputed chain again.
|
|
589
|
+
const pending = unresolvedLazyMatches(m);
|
|
590
|
+
if (pending.length)
|
|
591
|
+
throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
|
|
592
|
+
return m;
|
|
417
593
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
? utils.paramsWrapper(
|
|
428
|
-
: createMemoObject(
|
|
594
|
+
// Every write is a transition in Solid 2, so a native history pop forks the
|
|
595
|
+
// source signal exactly like programmatic navigation does. isRouting is
|
|
596
|
+
// therefore derived: the manual flag covers navigateFromRoute's explicit
|
|
597
|
+
// window, and isPending over the location/matches read reports any
|
|
598
|
+
// in-flight fork — including popstate traversals and the lazy-subtree
|
|
599
|
+
// resolution matches() parks on.
|
|
600
|
+
const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
|
|
601
|
+
const buildParams = () => mergeParams(matches());
|
|
602
|
+
const wrapParams = utils.paramsWrapper
|
|
603
|
+
? (getParams) => utils.paramsWrapper(getParams, branches)
|
|
604
|
+
: (getParams) => createMemoObject(getParams);
|
|
605
|
+
const params = wrapParams(buildParams);
|
|
429
606
|
const baseRoute = {
|
|
430
607
|
pattern: basePath,
|
|
608
|
+
params,
|
|
431
609
|
path: () => basePath,
|
|
432
610
|
outlet: () => null,
|
|
433
611
|
resolvePath(to) {
|
|
434
612
|
return resolvePath(basePath, to);
|
|
435
613
|
}
|
|
436
614
|
};
|
|
437
|
-
// Create a native transition, when source updates
|
|
438
|
-
createRenderEffect(on(source, source => transition("native", source), { defer: true }));
|
|
439
615
|
return {
|
|
440
616
|
base: baseRoute,
|
|
441
617
|
location,
|
|
442
618
|
params,
|
|
619
|
+
wrapParams,
|
|
443
620
|
isRouting,
|
|
444
621
|
get pendingTarget() {
|
|
445
622
|
return lastTransitionTarget;
|
|
446
623
|
},
|
|
624
|
+
leaving(route) {
|
|
625
|
+
const t = lastTransitionTarget;
|
|
626
|
+
if (!t || !route.retained)
|
|
627
|
+
return false;
|
|
628
|
+
const pathname = t.value.split(/[?#]/, 1)[0];
|
|
629
|
+
return !route.retained(options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
630
|
+
},
|
|
447
631
|
renderPath,
|
|
448
632
|
parsePath,
|
|
449
633
|
navigatorFactory,
|
|
@@ -451,7 +635,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
451
635
|
beforeLeave,
|
|
452
636
|
preloadRoute,
|
|
453
637
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
454
|
-
submissions
|
|
638
|
+
get submissions() {
|
|
639
|
+
return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
|
|
640
|
+
}
|
|
455
641
|
};
|
|
456
642
|
function navigateFromRoute(route, to, options) {
|
|
457
643
|
// Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
|
|
@@ -468,42 +654,77 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
468
654
|
}
|
|
469
655
|
return;
|
|
470
656
|
}
|
|
471
|
-
|
|
657
|
+
// typed path proxy nodes coerce to their href
|
|
658
|
+
if (typeof to !== "string")
|
|
659
|
+
to = to.toString();
|
|
472
660
|
const { replace, resolve, scroll, state: nextState } = {
|
|
473
661
|
replace: false,
|
|
474
|
-
resolve:
|
|
662
|
+
resolve: true,
|
|
475
663
|
scroll: true,
|
|
476
664
|
...options
|
|
477
665
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
666
|
+
// A string means what the same string means as an href. Leading "/"
|
|
667
|
+
// stays base-prefixed; anything else resolves URL-style against the
|
|
668
|
+
// current location — `new URL` collapses `..` and handles `?`/`#`-only
|
|
669
|
+
// references natively (#502). A cross-origin result (scheme or
|
|
670
|
+
// protocol-relative input) falls through as unroutable.
|
|
671
|
+
let resolvedTo;
|
|
672
|
+
if (!resolve)
|
|
673
|
+
resolvedTo = resolvePath(((!to || to[0] === "?") && location.pathname) || "", to);
|
|
674
|
+
else if (to[0] === "/")
|
|
675
|
+
resolvedTo = route.resolvePath(to);
|
|
676
|
+
else {
|
|
677
|
+
const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
|
|
678
|
+
resolvedTo =
|
|
679
|
+
url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
|
|
680
|
+
}
|
|
481
681
|
if (resolvedTo === undefined) {
|
|
482
682
|
throw new Error(`Path '${to}' is not a routable path`);
|
|
483
683
|
}
|
|
484
684
|
else if (referrers.length >= MAX_REDIRECTS) {
|
|
485
685
|
throw new Error("Too many redirects");
|
|
486
686
|
}
|
|
487
|
-
const current =
|
|
488
|
-
if (resolvedTo !== current || nextState !== state
|
|
687
|
+
const current = effective();
|
|
688
|
+
if (resolvedTo !== current.value || nextState !== current.state) {
|
|
489
689
|
if (isServer) {
|
|
490
690
|
const e = getRequestEvent();
|
|
491
691
|
e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
|
|
492
692
|
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
493
693
|
}
|
|
494
|
-
else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
495
|
-
referrers.push({ value: current, replace, scroll, state: state
|
|
496
|
-
|
|
694
|
+
else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
695
|
+
referrers.push({ value: current.value, replace, scroll, state: current.state });
|
|
696
|
+
const newTarget = {
|
|
497
697
|
value: resolvedTo,
|
|
498
698
|
state: nextState
|
|
499
|
-
}
|
|
699
|
+
};
|
|
700
|
+
const firstNavigation = lastTransitionTarget === undefined;
|
|
701
|
+
intent = "navigate";
|
|
702
|
+
// assign the target before flushing so effects that run for the
|
|
703
|
+
// isRouting flip (e.g. pending link state) can read it
|
|
704
|
+
lastTransitionTarget = newTarget;
|
|
705
|
+
if (firstNavigation) {
|
|
706
|
+
setIsRouting(true);
|
|
707
|
+
flush();
|
|
708
|
+
}
|
|
709
|
+
if (lastTransitionTarget === newTarget) {
|
|
710
|
+
setNavigateTarget({ ...lastTransitionTarget });
|
|
711
|
+
queueMicrotask(() => {
|
|
712
|
+
if (lastTransitionTarget !== newTarget)
|
|
713
|
+
return;
|
|
714
|
+
intent = undefined;
|
|
715
|
+
navigateEnd(lastTransitionTarget);
|
|
716
|
+
setNavigateTarget(undefined);
|
|
717
|
+
setIsRouting(false);
|
|
718
|
+
lastTransitionTarget = undefined;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
500
721
|
}
|
|
501
722
|
}
|
|
502
723
|
});
|
|
503
724
|
}
|
|
504
725
|
function navigatorFactory(route) {
|
|
505
726
|
// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
|
|
506
|
-
route = route ||
|
|
727
|
+
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
507
728
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
508
729
|
}
|
|
509
730
|
function navigateEnd(next) {
|
|
@@ -519,6 +740,12 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
519
740
|
}
|
|
520
741
|
function preloadRoute(url, preloadData) {
|
|
521
742
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
743
|
+
// An unresolved lazy subtree in the chain: the placeholder's
|
|
744
|
+
// component.preload (below) kicks the table load; once it lands,
|
|
745
|
+
// preload again so the real inner routes warm too.
|
|
746
|
+
const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
747
|
+
boundary &&
|
|
748
|
+
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
|
|
522
749
|
const prevIntent = intent;
|
|
523
750
|
intent = "preload";
|
|
524
751
|
for (let match in matches) {
|
|
@@ -546,15 +773,37 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
546
773
|
}
|
|
547
774
|
intent = prevIntent;
|
|
548
775
|
}
|
|
549
|
-
|
|
776
|
+
// Seeds the initial submission from a no-JS form post: the server
|
|
777
|
+
// function runtime redirected back with the outcome in a one-shot flash
|
|
778
|
+
// cookie (its default no-JS convention), consumed eagerly above and
|
|
779
|
+
// decoded here — so the post-redirect SSR renders useSubmission() state
|
|
780
|
+
// exactly as a scripted submission would. An explicitly pre-seeded
|
|
781
|
+
// `event.router.submission` (framework integrations) takes precedence.
|
|
782
|
+
function initSubmissions() {
|
|
550
783
|
const e = getRequestEvent();
|
|
551
|
-
|
|
784
|
+
const submission = (e && e.router && e.router.submission) ||
|
|
785
|
+
(flashDecoder && flashCookieHeader !== undefined
|
|
786
|
+
? flashDecoder(flashCookieHeader)
|
|
787
|
+
: undefined);
|
|
788
|
+
if (!submission)
|
|
789
|
+
return [];
|
|
790
|
+
return [
|
|
791
|
+
{
|
|
792
|
+
...submission,
|
|
793
|
+
clear() { },
|
|
794
|
+
retry() { }
|
|
795
|
+
}
|
|
796
|
+
];
|
|
552
797
|
}
|
|
553
798
|
}
|
|
554
|
-
export function createRouteContext(router, parent, outlet, match) {
|
|
555
|
-
const { base, location,
|
|
799
|
+
export function createRouteContext(router, parent, outlet, match, matches = () => [match()]) {
|
|
800
|
+
const { base, location, wrapParams } = router;
|
|
556
801
|
const { pattern, component, preload } = match().route;
|
|
557
802
|
const path = createMemo(() => match().path);
|
|
803
|
+
// Params scoped to this route's lifetime. `matches` is expected to retain
|
|
804
|
+
// its last valid value while this route is being torn down, so outgoing
|
|
805
|
+
// components and preloads never observe another route's params.
|
|
806
|
+
const params = wrapParams(() => mergeParams(matches()));
|
|
558
807
|
component &&
|
|
559
808
|
component.preload &&
|
|
560
809
|
component.preload();
|
|
@@ -564,7 +813,15 @@ export function createRouteContext(router, parent, outlet, match) {
|
|
|
564
813
|
const route = {
|
|
565
814
|
parent,
|
|
566
815
|
pattern,
|
|
816
|
+
params,
|
|
567
817
|
path,
|
|
818
|
+
retained(target) {
|
|
819
|
+
const m = match();
|
|
820
|
+
const p = m.route.matcher(target);
|
|
821
|
+
// params derive from the matched path, so equal paths mean this
|
|
822
|
+
// section instance (layouts match prefixes via partial matchers)
|
|
823
|
+
return !!p && p.path === m.path;
|
|
824
|
+
},
|
|
568
825
|
outlet: () => component
|
|
569
826
|
? createComponent(component, {
|
|
570
827
|
params,
|