@solidjs/router 1.0.0-next.9 → 1.0.0
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 +715 -531
- package/dist/components.d.ts +31 -0
- package/dist/components.jsx +40 -0
- package/dist/data/action.d.ts +11 -40
- package/dist/data/action.js +100 -293
- package/dist/data/createAsync.d.ts +32 -0
- package/dist/data/createAsync.js +96 -0
- package/dist/data/events.d.ts +0 -8
- package/dist/data/events.js +23 -24
- package/dist/data/index.d.ts +4 -2
- package/dist/data/index.js +4 -2
- package/dist/data/query.d.ts +3 -1
- package/dist/data/query.js +22 -39
- package/dist/data/response.d.ts +4 -0
- package/dist/data/response.js +42 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1024 -1597
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +4 -29
- package/dist/lifecycle.js +37 -40
- package/dist/routers/HashRouter.d.ts +9 -0
- package/dist/routers/HashRouter.js +41 -0
- package/dist/routers/MemoryRouter.d.ts +24 -0
- package/dist/routers/MemoryRouter.js +57 -0
- package/dist/routers/Router.d.ts +17 -0
- package/dist/routers/Router.js +59 -0
- package/dist/routers/StaticRouter.d.ts +6 -0
- package/dist/routers/StaticRouter.js +15 -0
- package/dist/routers/components.d.ts +26 -12
- package/dist/routers/components.jsx +54 -68
- package/dist/routers/createRouter.d.ts +10 -0
- package/dist/routers/createRouter.js +41 -0
- package/dist/routers/index.d.ts +11 -4
- package/dist/routers/index.js +6 -2
- package/dist/routers/scrollRestoration.d.ts +32 -0
- package/dist/routers/scrollRestoration.js +96 -0
- package/dist/routing.d.ts +52 -88
- package/dist/routing.js +166 -381
- package/dist/types.d.ts +33 -83
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -2
- package/package.json +7 -11
- package/dist/claims.d.ts +0 -21
- package/dist/claims.js +0 -115
- package/dist/data/flash.d.ts +0 -21
- package/dist/data/flash.js +0 -78
- package/dist/data/flashCookie.d.ts +0 -7
- package/dist/data/flashCookie.js +0 -20
- package/dist/data/serverForms.d.ts +0 -1
- package/dist/data/serverForms.js +0 -5
- package/dist/paths.d.ts +0 -117
- package/dist/paths.js +0 -41
- package/dist/routers/factory.d.ts +0 -45
- package/dist/routers/factory.jsx +0 -143
- package/dist/routers/history.d.ts +0 -24
- package/dist/routers/history.js +0 -180
- package/dist/server.d.ts +0 -75
- package/dist/server.js +0 -264
package/dist/routing.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createComponent, createContext, createMemo, createSignal,
|
|
3
|
-
import { isServer, getRequestEvent } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { runWithOwner, batch } from "solid-js";
|
|
2
|
+
import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js";
|
|
3
|
+
import { isServer, getRequestEvent } from "solid-js/web";
|
|
4
|
+
import { createBeforeLeave } from "./lifecycle.js";
|
|
5
|
+
import { mockBase, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
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
|
-
}
|
|
18
10
|
export const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
|
|
19
11
|
let TempRoute;
|
|
20
|
-
export const useRoute = () => TempRoute ||
|
|
12
|
+
export const useRoute = () => TempRoute || useContext(RouteContextObj) || useRouter().base;
|
|
21
13
|
export const useResolvedPath = (path) => {
|
|
22
14
|
const route = useRoute();
|
|
23
15
|
return createMemo(() => route.resolvePath(path()));
|
|
@@ -61,8 +53,8 @@ export const useNavigate = () => useRouter().navigatorFactory();
|
|
|
61
53
|
*/
|
|
62
54
|
export const useLocation = () => useRouter().location;
|
|
63
55
|
/**
|
|
64
|
-
* Retrieves
|
|
65
|
-
* Useful for showing pending
|
|
56
|
+
* Retrieves signal that indicates whether the route is currently in a *Transition*.
|
|
57
|
+
* Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
|
|
66
58
|
*
|
|
67
59
|
* @example
|
|
68
60
|
* ```js
|
|
@@ -76,6 +68,21 @@ export const useLocation = () => useRouter().location;
|
|
|
76
68
|
* ```
|
|
77
69
|
*/
|
|
78
70
|
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
|
+
};
|
|
79
86
|
/**
|
|
80
87
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
81
88
|
* Useful for determining if a given path matches the current route.
|
|
@@ -99,44 +106,61 @@ export const useMatch = (path, matchFilters) => {
|
|
|
99
106
|
});
|
|
100
107
|
};
|
|
101
108
|
/**
|
|
102
|
-
* `
|
|
103
|
-
*
|
|
104
|
-
* render, outermost first. Useful for reading `info` metadata off the matched
|
|
105
|
-
* chain. Unlike `useMatch`, which tests a path pattern you supply against the
|
|
106
|
-
* location, this reflects the route tree itself.
|
|
109
|
+
* `useCurrentMatches` returns all the matches for the current matched route.
|
|
110
|
+
* Useful for getting all the route information.
|
|
107
111
|
*
|
|
108
112
|
* @example
|
|
109
113
|
* ```js
|
|
110
|
-
* const matches =
|
|
114
|
+
* const matches = useCurrentMatches();
|
|
111
115
|
*
|
|
112
|
-
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
116
|
+
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
113
117
|
* ```
|
|
114
118
|
*/
|
|
115
|
-
export const
|
|
119
|
+
export const useCurrentMatches = () => {
|
|
116
120
|
const router = useRouter();
|
|
117
121
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
118
122
|
return () => router.matches().slice();
|
|
119
123
|
};
|
|
120
124
|
/**
|
|
121
|
-
*
|
|
122
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
123
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
125
|
+
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
124
126
|
*
|
|
125
127
|
* @example
|
|
126
128
|
* ```js
|
|
127
|
-
* const
|
|
129
|
+
* const params = useParams();
|
|
128
130
|
*
|
|
129
|
-
*
|
|
131
|
+
* // fetch user based on the id path parameter
|
|
132
|
+
* const [user] = createResource(() => params.id, fetchUser);
|
|
130
133
|
* ```
|
|
131
134
|
*/
|
|
132
|
-
export const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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();
|
|
148
|
+
*
|
|
149
|
+
* return (
|
|
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
|
+
* );
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export const useSearchParams = () => {
|
|
140
164
|
const router = useRouter();
|
|
141
165
|
const location = router.location;
|
|
142
166
|
const navigate = useNavigate();
|
|
@@ -156,172 +180,55 @@ export function useSearchParams(path) {
|
|
|
156
180
|
...options
|
|
157
181
|
});
|
|
158
182
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// matched routes (root→leaf), whose outputs merge over the raw query.
|
|
162
|
-
// A schema that reports issues is skipped, leaving raw values — search
|
|
163
|
-
// strings are user input, so defaults belong in the schema itself.
|
|
164
|
-
const query = path
|
|
165
|
-
? createMemoObject(createMemo(() => {
|
|
166
|
-
const raw = { ...location.query };
|
|
167
|
-
let result;
|
|
168
|
-
for (const match of router.matches()) {
|
|
169
|
-
const schema = match.route.key.search;
|
|
170
|
-
if (!schema)
|
|
171
|
-
continue;
|
|
172
|
-
const outcome = schema["~standard"].validate(raw);
|
|
173
|
-
if (outcome instanceof Promise)
|
|
174
|
-
throw new Error("Async Standard Schema validation is not supported for search params");
|
|
175
|
-
if (!outcome.issues)
|
|
176
|
-
result = Object.assign(result || { ...raw }, outcome.value);
|
|
177
|
-
}
|
|
178
|
-
return result || raw;
|
|
179
|
-
}))
|
|
180
|
-
: location.query;
|
|
181
|
-
return [query, setSearchParams];
|
|
182
|
-
}
|
|
183
|
+
return [location.query, setSearchParams];
|
|
184
|
+
};
|
|
183
185
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
186
|
+
* useBeforeLeave takes a function that will be called prior to leaving a route.
|
|
187
|
+
* The function will be called with:
|
|
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).
|
|
187
195
|
*
|
|
188
196
|
* @example
|
|
189
|
-
* ```
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
197
|
+
* ```js
|
|
198
|
+
* useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
199
|
+
* if (form.isDirty && !e.defaultPrevented) {
|
|
200
|
+
* // preventDefault to block immediately and prompt user async
|
|
201
|
+
* e.preventDefault();
|
|
202
|
+
* setTimeout(() => {
|
|
203
|
+
* if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
204
|
+
* // user wants to proceed anyway so retry with force=true
|
|
205
|
+
* e.retry(true);
|
|
206
|
+
* }
|
|
207
|
+
* }, 100);
|
|
208
|
+
* }
|
|
209
|
+
* });
|
|
198
210
|
* ```
|
|
199
211
|
*/
|
|
200
|
-
export const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const path = createMemo(() => {
|
|
206
|
-
const to_ = to();
|
|
207
|
-
return to_ === undefined ? undefined : comparablePath(to_);
|
|
212
|
+
export const useBeforeLeave = (listener) => {
|
|
213
|
+
const s = useRouter().beforeLeave.subscribe({
|
|
214
|
+
listener,
|
|
215
|
+
location: useLocation(),
|
|
216
|
+
navigate: useNavigate()
|
|
208
217
|
});
|
|
209
|
-
|
|
210
|
-
const path_ = path();
|
|
211
|
-
if (path_ === undefined)
|
|
212
|
-
return [false, false];
|
|
213
|
-
const exact = loc === path_;
|
|
214
|
-
return [exact || (!options.end && loc.startsWith(path_ + "/")), exact];
|
|
215
|
-
};
|
|
216
|
-
const state = createMemo(() => matches(decodeURI(comparablePath(location.pathname))));
|
|
217
|
-
return {
|
|
218
|
-
active: () => state()[0],
|
|
219
|
-
current: () => state()[1],
|
|
220
|
-
// match the in-flight target explicitly (rather than active-while-routing)
|
|
221
|
-
// so the answer is the same from pure reads and from effects, which
|
|
222
|
-
// observe the committed location during a transition
|
|
223
|
-
pending: createMemo(() => {
|
|
224
|
-
state(); // location dependency: mid-flight target swaps recompute
|
|
225
|
-
return (router.isRouting() &&
|
|
226
|
-
!!router.pendingTarget &&
|
|
227
|
-
matches(decodeURI(comparablePath(router.pendingTarget.value)))[0]);
|
|
228
|
-
})
|
|
229
|
-
};
|
|
218
|
+
onCleanup(s);
|
|
230
219
|
};
|
|
231
220
|
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
232
221
|
// pchar characters (sub-delims / ":" / "@") literal, matching how browsers
|
|
233
222
|
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
234
223
|
// still percent-encoded exactly as before, since browsers encode those too.
|
|
235
224
|
const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// Lazy route subtrees
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
//
|
|
240
|
-
// A `children` thunk (`() => import("./feature/routes")`) is a *boundary*:
|
|
241
|
-
// until it resolves, the compiled tree carries a param-less catch-all
|
|
242
|
-
// placeholder branch under the boundary's pattern (splat-scored, so static
|
|
243
|
-
// siblings still win). Resolution is append-only and cached per thunk, then a
|
|
244
|
-
// module-level version signal bumps and every `branches()` consumer
|
|
245
|
-
// recompiles — matches, params, and route states all react. The placeholder's
|
|
246
|
-
// component reads a memo of the resolution promise, which keeps the enclosing
|
|
247
|
-
// navigation transition pending exactly like a `lazy()` route component; its
|
|
248
|
-
// `preload` *is* the resolver, so hover-intent preloading kicks the table
|
|
249
|
-
// load through the existing component-preload path.
|
|
250
|
-
const lazyBoundaries = new WeakMap();
|
|
251
|
-
// Module scope: boundary resolution is global, deterministic state (same
|
|
252
|
-
// thunk -> same routes), shared by every factory instance and the server's
|
|
253
|
-
// flight collector.
|
|
254
|
-
const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
|
|
255
|
-
/** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
|
|
256
|
-
export function trackLazySubtrees() {
|
|
257
|
-
return lazyTreeVersion();
|
|
258
|
-
}
|
|
259
|
-
/** Non-reactive read, for cache-busting outside the reactive graph (server collectors). */
|
|
260
|
-
export function peekLazySubtrees() {
|
|
261
|
-
return untrack(lazyTreeVersion);
|
|
262
|
-
}
|
|
263
|
-
function getLazyBoundary(thunk) {
|
|
264
|
-
let record = lazyBoundaries.get(thunk);
|
|
265
|
-
if (!record)
|
|
266
|
-
lazyBoundaries.set(thunk, (record = { thunk }));
|
|
267
|
-
return record;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Kicks (or joins) a boundary's resolution. Returns the resolved routes
|
|
271
|
-
* synchronously once available, the in-flight promise otherwise. Commit is
|
|
272
|
-
* always async — even for thunks returning arrays — so the version bump
|
|
273
|
-
* never writes a signal from inside a render computation.
|
|
274
|
-
*/
|
|
275
|
-
export function resolveLazySubtree(record) {
|
|
276
|
-
if (record.resolved)
|
|
277
|
-
return record.resolved;
|
|
278
|
-
return (record.promise ||= Promise.resolve(record.thunk()).then(m => {
|
|
279
|
-
const routes = Array.isArray(m)
|
|
280
|
-
? m
|
|
281
|
-
: m
|
|
282
|
-
.default ||
|
|
283
|
-
m.routes ||
|
|
284
|
-
[];
|
|
285
|
-
record.resolved = routes;
|
|
286
|
-
setLazyTreeVersion(v => v + 1);
|
|
287
|
-
return record.resolved;
|
|
288
|
-
}));
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* The unresolved boundaries in a match chain. Rendering gates on these (the
|
|
292
|
-
* route-states memo suspends until they land — see routers/components.tsx)
|
|
293
|
-
* and the server's flight collector awaits them before its preload pass.
|
|
294
|
-
*/
|
|
295
|
-
export function unresolvedLazyMatches(matches) {
|
|
296
|
-
const pending = [];
|
|
297
|
-
for (const match of matches)
|
|
298
|
-
if (match.route.lazy && !match.route.lazy.resolved)
|
|
299
|
-
pending.push(match.route.lazy);
|
|
300
|
-
return pending;
|
|
301
|
-
}
|
|
302
|
-
function createLazyPlaceholder(pattern, record) {
|
|
303
|
-
// The placeholder never renders and needs no component — `matches` parks
|
|
304
|
-
// on unresolved boundaries before route contexts are created (kicking the
|
|
305
|
-
// resolver as it does), preloadRoute kicks it directly, and the version
|
|
306
|
-
// bump swaps in the real routes. `path + "/*"` with no splat name matches
|
|
307
|
-
// the boundary itself and everything beneath it without recording a param
|
|
308
|
-
// (createMatcher skips empty splat names).
|
|
309
|
-
const placeholderPattern = pattern + "/*";
|
|
310
|
-
return {
|
|
311
|
-
key: record,
|
|
312
|
-
originalPath: "*",
|
|
313
|
-
pattern: placeholderPattern,
|
|
314
|
-
matcher: createMatcher(placeholderPattern),
|
|
315
|
-
lazy: record
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
225
|
export function createRoutes(routeDef, base = "") {
|
|
319
|
-
const { component, preload, children, info } = routeDef;
|
|
226
|
+
const { component, preload, load, children, info } = routeDef;
|
|
320
227
|
const isLeaf = !children || (Array.isArray(children) && !children.length);
|
|
321
228
|
const shared = {
|
|
322
229
|
key: routeDef,
|
|
323
230
|
component,
|
|
324
|
-
preload,
|
|
231
|
+
preload: preload || load,
|
|
325
232
|
info
|
|
326
233
|
};
|
|
327
234
|
return asArray(routeDef.path).reduce((acc, originalPath) => {
|
|
@@ -378,24 +285,9 @@ export function createBranches(routeDef, base = "", stack = [], branches = []) {
|
|
|
378
285
|
const routes = createRoutes(def, base);
|
|
379
286
|
for (const route of routes) {
|
|
380
287
|
stack.push(route);
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
if (record.resolved) {
|
|
385
|
-
children = record.resolved;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
// unresolved boundary: a catch-all placeholder holds its ground
|
|
389
|
-
stack.push(createLazyPlaceholder(route.pattern, record));
|
|
390
|
-
branches.push(createBranch([...stack], branches.length));
|
|
391
|
-
stack.pop();
|
|
392
|
-
stack.pop();
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
const isEmptyArray = Array.isArray(children) && children.length === 0;
|
|
397
|
-
if (children && !isEmptyArray) {
|
|
398
|
-
createBranches(children, route.pattern, stack, branches);
|
|
288
|
+
const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
|
|
289
|
+
if (def.children && !isEmptyArray) {
|
|
290
|
+
createBranches(def.children, route.pattern, stack, branches);
|
|
399
291
|
}
|
|
400
292
|
else {
|
|
401
293
|
const branch = createBranch([...stack], branches.length);
|
|
@@ -417,16 +309,9 @@ export function getRouteMatches(branches, location) {
|
|
|
417
309
|
}
|
|
418
310
|
return [];
|
|
419
311
|
}
|
|
420
|
-
export function mergeParams(matches) {
|
|
421
|
-
const params = {};
|
|
422
|
-
for (let i = 0; i < matches.length; i++) {
|
|
423
|
-
Object.assign(params, matches[i].params);
|
|
424
|
-
}
|
|
425
|
-
return params;
|
|
426
|
-
}
|
|
427
312
|
function createLocation(path, state, queryWrapper) {
|
|
428
313
|
const origin = new URL(mockBase);
|
|
429
|
-
const url = createMemo(
|
|
314
|
+
const url = createMemo(prev => {
|
|
430
315
|
const path_ = path();
|
|
431
316
|
try {
|
|
432
317
|
// anchor rooted paths against the origin explicitly - a path with
|
|
@@ -437,14 +322,14 @@ function createLocation(path, state, queryWrapper) {
|
|
|
437
322
|
console.error(`Invalid path ${path_}`);
|
|
438
323
|
return prev;
|
|
439
324
|
}
|
|
440
|
-
}, {
|
|
325
|
+
}, origin, {
|
|
441
326
|
equals: (a, b) => a.href === b.href
|
|
442
327
|
});
|
|
443
328
|
const pathname = createMemo(() => url().pathname);
|
|
444
|
-
const search = createMemo(() => url().search);
|
|
329
|
+
const search = createMemo(() => url().search, true);
|
|
445
330
|
const hash = createMemo(() => url().hash);
|
|
446
331
|
const key = () => "";
|
|
447
|
-
const queryFn =
|
|
332
|
+
const queryFn = on(search, () => extractSearchParams(url()));
|
|
448
333
|
return {
|
|
449
334
|
get pathname() {
|
|
450
335
|
return pathname();
|
|
@@ -464,49 +349,6 @@ function createLocation(path, state, queryWrapper) {
|
|
|
464
349
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
465
350
|
};
|
|
466
351
|
}
|
|
467
|
-
/**
|
|
468
|
-
* Rendezvous between the router and the data layer's single-flight consumer.
|
|
469
|
-
* The Router registers itself at mount (unless `singleFlight={false}`); the
|
|
470
|
-
* action side provides the consumer factory when the first action is created
|
|
471
|
-
* (see data/action.ts). Whichever side arrives first waits for the other, so
|
|
472
|
-
* an action module loaded lazily (a code-split route) still attaches to the
|
|
473
|
-
* already-mounted router — and a router-only app, where no action ever
|
|
474
|
-
* loads, never subscribes to the transport, so the server is never asked to
|
|
475
|
-
* collect.
|
|
476
|
-
*/
|
|
477
|
-
let flightConsumerFactory;
|
|
478
|
-
const flightRouters = new Map();
|
|
479
|
-
export function registerFlightRouter(router) {
|
|
480
|
-
flightRouters.set(router, flightConsumerFactory && flightConsumerFactory(router));
|
|
481
|
-
return () => {
|
|
482
|
-
const unsubscribe = flightRouters.get(router);
|
|
483
|
-
flightRouters.delete(router);
|
|
484
|
-
unsubscribe && unsubscribe();
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
export function provideFlightConsumer(factory) {
|
|
488
|
-
if (flightConsumerFactory)
|
|
489
|
-
return;
|
|
490
|
-
flightConsumerFactory = factory;
|
|
491
|
-
for (const [router, unsubscribe] of flightRouters) {
|
|
492
|
-
if (!unsubscribe)
|
|
493
|
-
flightRouters.set(router, factory(router));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* The flash-cookie codec, provided by the action side (data/action.ts) so
|
|
498
|
-
* the router core never carries it: the core consumes the cookie eagerly
|
|
499
|
-
* per request (detection + one-shot clear via the tiny flashCookie.ts half)
|
|
500
|
-
* but defers decoding to this slot, read when the submissions signal
|
|
501
|
-
* initializes. Actions are created at module scope, so on the server the
|
|
502
|
-
* decoder is always installed before useSubmission can read — and a
|
|
503
|
-
* router-only app, where it never installs, has no actions that could have
|
|
504
|
-
* produced a flash cookie in the first place.
|
|
505
|
-
*/
|
|
506
|
-
let flashDecoder;
|
|
507
|
-
export function provideFlashDecoder(decoder) {
|
|
508
|
-
flashDecoder || (flashDecoder = decoder);
|
|
509
|
-
}
|
|
510
352
|
let intent;
|
|
511
353
|
export function getIntent() {
|
|
512
354
|
return intent;
|
|
@@ -522,84 +364,82 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
522
364
|
const { signal: [source, setSource], utils = {} } = integration;
|
|
523
365
|
const parsePath = utils.parsePath || (p => p);
|
|
524
366
|
const renderPath = utils.renderPath || (p => p);
|
|
525
|
-
|
|
526
|
-
const beforeLeave = utils.beforeLeave || {};
|
|
367
|
+
const beforeLeave = utils.beforeLeave || createBeforeLeave();
|
|
527
368
|
const basePath = resolvePath("", options.base || "");
|
|
528
|
-
const initialSource = untrack(source);
|
|
529
369
|
if (basePath === undefined) {
|
|
530
370
|
throw new Error(`${basePath} is not a valid base path`);
|
|
531
371
|
}
|
|
532
|
-
else if (basePath && !
|
|
372
|
+
else if (basePath && !source().value) {
|
|
533
373
|
setSource({ value: basePath, replace: true, scroll: false });
|
|
534
374
|
}
|
|
535
|
-
const [isRouting, setIsRouting] = createSignal(false
|
|
536
|
-
//
|
|
537
|
-
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
|
|
538
|
-
ownedWrite: true
|
|
539
|
-
});
|
|
540
|
-
// Keep track of last target, so that last call to navigate wins
|
|
375
|
+
const [isRouting, setIsRouting] = createSignal(false);
|
|
376
|
+
// Keep track of last target, so that last call to transition wins
|
|
541
377
|
let lastTransitionTarget;
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
378
|
+
// Transition the location to a new value
|
|
379
|
+
const transition = (newIntent, newTarget) => {
|
|
380
|
+
if (newTarget.value === reference() && newTarget.state === state())
|
|
381
|
+
return;
|
|
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);
|
|
546
410
|
const referrers = [];
|
|
547
|
-
|
|
548
|
-
// must be appended before streaming flushes the response headers, and an
|
|
549
|
-
// unread outcome must not haunt a later request's render. Only detection
|
|
550
|
-
// and clearing happen here (the tiny flashCookie.ts half); the raw header
|
|
551
|
-
// is stashed and decoding waits for the action-provided codec, read when
|
|
552
|
-
// the lazily allocated submissions signal below first initializes.
|
|
553
|
-
let flashCookieHeader;
|
|
554
|
-
if (isServer) {
|
|
555
|
-
const e = getRequestEvent();
|
|
556
|
-
if (e && !(e.router && e.router.submission)) {
|
|
557
|
-
const cookieHeader = e.request.headers.get("cookie");
|
|
558
|
-
if (hasFlashCookie(cookieHeader)) {
|
|
559
|
-
flashCookieHeader = cookieHeader;
|
|
560
|
-
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
561
|
-
if (e.response && e.response.headers)
|
|
562
|
-
e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
let submissions;
|
|
411
|
+
const submissions = createSignal(isServer ? initFromFlash() : []);
|
|
567
412
|
const matches = createMemo(() => {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
573
|
-
// navigation transition (or the SSR stream) holds until the table lands.
|
|
574
|
-
// NotReadyError (not a returned promise) because a match chain is full
|
|
575
|
-
// of component functions the hydration serializer must never see. The
|
|
576
|
-
// recompute comes from the version-signal dependency on the client and
|
|
577
|
-
// from the carried promise's retry on the server; a boundary nested
|
|
578
|
-
// inside a boundary just parks the recomputed chain again.
|
|
579
|
-
const pending = unresolvedLazyMatches(m);
|
|
580
|
-
if (pending.length)
|
|
581
|
-
throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
|
|
582
|
-
return m;
|
|
413
|
+
if (typeof options.transformUrl === "function") {
|
|
414
|
+
return getRouteMatches(branches(), options.transformUrl(location.pathname));
|
|
415
|
+
}
|
|
416
|
+
return getRouteMatches(branches(), location.pathname);
|
|
583
417
|
});
|
|
584
|
-
const buildParams = () =>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
418
|
+
const buildParams = () => {
|
|
419
|
+
const m = matches();
|
|
420
|
+
const params = {};
|
|
421
|
+
for (let i = 0; i < m.length; i++) {
|
|
422
|
+
Object.assign(params, m[i].params);
|
|
423
|
+
}
|
|
424
|
+
return params;
|
|
425
|
+
};
|
|
426
|
+
const params = utils.paramsWrapper
|
|
427
|
+
? utils.paramsWrapper(buildParams, branches)
|
|
428
|
+
: createMemoObject(buildParams);
|
|
589
429
|
const baseRoute = {
|
|
590
430
|
pattern: basePath,
|
|
591
|
-
params,
|
|
592
431
|
path: () => basePath,
|
|
593
432
|
outlet: () => null,
|
|
594
433
|
resolvePath(to) {
|
|
595
434
|
return resolvePath(basePath, to);
|
|
596
435
|
}
|
|
597
436
|
};
|
|
437
|
+
// Create a native transition, when source updates
|
|
438
|
+
createRenderEffect(on(source, source => transition("native", source), { defer: true }));
|
|
598
439
|
return {
|
|
599
440
|
base: baseRoute,
|
|
600
441
|
location,
|
|
601
442
|
params,
|
|
602
|
-
wrapParams,
|
|
603
443
|
isRouting,
|
|
604
444
|
get pendingTarget() {
|
|
605
445
|
return lastTransitionTarget;
|
|
@@ -611,9 +451,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
611
451
|
beforeLeave,
|
|
612
452
|
preloadRoute,
|
|
613
453
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
614
|
-
|
|
615
|
-
return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
|
|
616
|
-
}
|
|
454
|
+
submissions
|
|
617
455
|
};
|
|
618
456
|
function navigateFromRoute(route, to, options) {
|
|
619
457
|
// Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
|
|
@@ -630,9 +468,6 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
630
468
|
}
|
|
631
469
|
return;
|
|
632
470
|
}
|
|
633
|
-
// typed path proxy nodes coerce to their href
|
|
634
|
-
if (typeof to !== "string")
|
|
635
|
-
to = to.toString();
|
|
636
471
|
const queryOnly = !to || to[0] === "?";
|
|
637
472
|
const { replace, resolve, scroll, state: nextState } = {
|
|
638
473
|
replace: false,
|
|
@@ -649,47 +484,26 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
649
484
|
else if (referrers.length >= MAX_REDIRECTS) {
|
|
650
485
|
throw new Error("Too many redirects");
|
|
651
486
|
}
|
|
652
|
-
const current =
|
|
653
|
-
if (resolvedTo !== current
|
|
487
|
+
const current = reference();
|
|
488
|
+
if (resolvedTo !== current || nextState !== state()) {
|
|
654
489
|
if (isServer) {
|
|
655
490
|
const e = getRequestEvent();
|
|
656
491
|
e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
|
|
657
492
|
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
658
493
|
}
|
|
659
|
-
else if (
|
|
660
|
-
referrers.push({ value: current
|
|
661
|
-
|
|
494
|
+
else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
495
|
+
referrers.push({ value: current, replace, scroll, state: state() });
|
|
496
|
+
transition("navigate", {
|
|
662
497
|
value: resolvedTo,
|
|
663
498
|
state: nextState
|
|
664
|
-
};
|
|
665
|
-
const firstNavigation = lastTransitionTarget === undefined;
|
|
666
|
-
intent = "navigate";
|
|
667
|
-
// assign the target before flushing so effects that run for the
|
|
668
|
-
// isRouting flip (e.g. pending link state) can read it
|
|
669
|
-
lastTransitionTarget = newTarget;
|
|
670
|
-
if (firstNavigation) {
|
|
671
|
-
setIsRouting(true);
|
|
672
|
-
flush();
|
|
673
|
-
}
|
|
674
|
-
if (lastTransitionTarget === newTarget) {
|
|
675
|
-
setNavigateTarget({ ...lastTransitionTarget });
|
|
676
|
-
queueMicrotask(() => {
|
|
677
|
-
if (lastTransitionTarget !== newTarget)
|
|
678
|
-
return;
|
|
679
|
-
intent = undefined;
|
|
680
|
-
navigateEnd(lastTransitionTarget);
|
|
681
|
-
setNavigateTarget(undefined);
|
|
682
|
-
setIsRouting(false);
|
|
683
|
-
lastTransitionTarget = undefined;
|
|
684
|
-
});
|
|
685
|
-
}
|
|
499
|
+
});
|
|
686
500
|
}
|
|
687
501
|
}
|
|
688
502
|
});
|
|
689
503
|
}
|
|
690
504
|
function navigatorFactory(route) {
|
|
691
505
|
// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
|
|
692
|
-
route = route ||
|
|
506
|
+
route = route || useContext(RouteContextObj) || baseRoute;
|
|
693
507
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
694
508
|
}
|
|
695
509
|
function navigateEnd(next) {
|
|
@@ -705,12 +519,6 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
705
519
|
}
|
|
706
520
|
function preloadRoute(url, preloadData) {
|
|
707
521
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
708
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
709
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
710
|
-
// preload again so the real inner routes warm too.
|
|
711
|
-
const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
712
|
-
boundary &&
|
|
713
|
-
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
|
|
714
522
|
const prevIntent = intent;
|
|
715
523
|
intent = "preload";
|
|
716
524
|
for (let match in matches) {
|
|
@@ -738,37 +546,15 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
738
546
|
}
|
|
739
547
|
intent = prevIntent;
|
|
740
548
|
}
|
|
741
|
-
|
|
742
|
-
// function handler redirected back with the outcome in a one-shot flash
|
|
743
|
-
// cookie (see src/server.ts's handleNoJS), consumed eagerly above and
|
|
744
|
-
// decoded here — so the post-redirect SSR renders useSubmission() state
|
|
745
|
-
// exactly as a scripted submission would. An explicitly pre-seeded
|
|
746
|
-
// `event.router.submission` (framework integrations) takes precedence.
|
|
747
|
-
function initSubmissions() {
|
|
549
|
+
function initFromFlash() {
|
|
748
550
|
const e = getRequestEvent();
|
|
749
|
-
|
|
750
|
-
(flashDecoder && flashCookieHeader !== undefined
|
|
751
|
-
? flashDecoder(flashCookieHeader)
|
|
752
|
-
: undefined);
|
|
753
|
-
if (!submission)
|
|
754
|
-
return [];
|
|
755
|
-
return [
|
|
756
|
-
{
|
|
757
|
-
...submission,
|
|
758
|
-
clear() { },
|
|
759
|
-
retry() { }
|
|
760
|
-
}
|
|
761
|
-
];
|
|
551
|
+
return (e && e.router && e.router.submission ? [e.router.submission] : []);
|
|
762
552
|
}
|
|
763
553
|
}
|
|
764
|
-
export function createRouteContext(router, parent, outlet, match
|
|
765
|
-
const { base, location,
|
|
554
|
+
export function createRouteContext(router, parent, outlet, match) {
|
|
555
|
+
const { base, location, params } = router;
|
|
766
556
|
const { pattern, component, preload } = match().route;
|
|
767
557
|
const path = createMemo(() => match().path);
|
|
768
|
-
// Params scoped to this route's lifetime. `matches` is expected to retain
|
|
769
|
-
// its last valid value while this route is being torn down, so outgoing
|
|
770
|
-
// components and preloads never observe another route's params.
|
|
771
|
-
const params = wrapParams(() => mergeParams(matches()));
|
|
772
558
|
component &&
|
|
773
559
|
component.preload &&
|
|
774
560
|
component.preload();
|
|
@@ -778,7 +564,6 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
778
564
|
const route = {
|
|
779
565
|
parent,
|
|
780
566
|
pattern,
|
|
781
|
-
params,
|
|
782
567
|
path,
|
|
783
568
|
outlet: () => component
|
|
784
569
|
? createComponent(component, {
|