@solidjs/router 1.0.0-next.8 → 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 -274
- 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 -1578
- 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/index.js
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
|
|
2
|
+
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, createEffect, splitProps, createResource, catchError } from 'solid-js';
|
|
3
|
+
import { createStore, reconcile, unwrap } from 'solid-js/store';
|
|
4
|
+
|
|
5
|
+
function createBeforeLeave() {
|
|
6
|
+
let listeners = new Set();
|
|
7
|
+
function subscribe(listener) {
|
|
8
|
+
listeners.add(listener);
|
|
9
|
+
return () => listeners.delete(listener);
|
|
10
|
+
}
|
|
11
|
+
let ignore = false;
|
|
12
|
+
function confirm(to, options) {
|
|
13
|
+
if (ignore) return !(ignore = false);
|
|
14
|
+
const e = {
|
|
15
|
+
to,
|
|
16
|
+
options,
|
|
17
|
+
defaultPrevented: false,
|
|
18
|
+
preventDefault: () => e.defaultPrevented = true
|
|
19
|
+
};
|
|
20
|
+
for (const l of listeners) l.listener({
|
|
21
|
+
to,
|
|
22
|
+
options,
|
|
23
|
+
// delegate to the shared event so later listeners' preventDefault
|
|
24
|
+
// calls are observable from earlier listeners
|
|
25
|
+
get defaultPrevented() {
|
|
26
|
+
return e.defaultPrevented;
|
|
27
|
+
},
|
|
28
|
+
preventDefault: e.preventDefault,
|
|
29
|
+
from: l.location,
|
|
30
|
+
retry: force => {
|
|
31
|
+
force && (ignore = true);
|
|
32
|
+
l.navigate(to, {
|
|
33
|
+
...options,
|
|
34
|
+
resolve: false
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return !e.defaultPrevented;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
subscribe,
|
|
42
|
+
confirm
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The following supports browser initiated blocking (eg back/forward)
|
|
47
|
+
|
|
48
|
+
let depth;
|
|
49
|
+
function saveCurrentDepth() {
|
|
50
|
+
if (!window.history.state || window.history.state._depth == null) {
|
|
51
|
+
window.history.replaceState({
|
|
52
|
+
...window.history.state,
|
|
53
|
+
_depth: window.history.length - 1
|
|
54
|
+
}, "");
|
|
55
|
+
}
|
|
56
|
+
depth = window.history.state._depth;
|
|
57
|
+
}
|
|
58
|
+
if (!isServer) {
|
|
59
|
+
saveCurrentDepth();
|
|
60
|
+
}
|
|
61
|
+
function keepDepth(state) {
|
|
62
|
+
return {
|
|
63
|
+
...state,
|
|
64
|
+
_depth: window.history.state && window.history.state._depth
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function notifyIfNotBlocked(notify, block) {
|
|
68
|
+
let ignore = false;
|
|
69
|
+
return () => {
|
|
70
|
+
const prevDepth = depth;
|
|
71
|
+
saveCurrentDepth();
|
|
72
|
+
const delta = prevDepth == null ? null : depth - prevDepth;
|
|
73
|
+
if (ignore) {
|
|
74
|
+
ignore = false;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (delta && block(delta)) {
|
|
78
|
+
ignore = true;
|
|
79
|
+
window.history.go(-delta);
|
|
80
|
+
} else {
|
|
81
|
+
notify();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
4
85
|
|
|
5
86
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
6
87
|
const trimPathRegex = /^\/+|(\/)\/+$/g;
|
|
@@ -9,9 +90,6 @@ function normalizePath(path, omitSlash = false) {
|
|
|
9
90
|
const s = path.replace(trimPathRegex, "$1");
|
|
10
91
|
return s ? omitSlash || /^[?#]/.test(s) ? s : "/" + s : "";
|
|
11
92
|
}
|
|
12
|
-
|
|
13
|
-
/** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
|
|
14
|
-
const comparablePath = path => normalizePath(path.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
15
93
|
function resolvePath(base, path, from) {
|
|
16
94
|
if (hasSchemeRegex.test(path)) {
|
|
17
95
|
return undefined;
|
|
@@ -180,369 +258,13 @@ function setFunctionName(obj, value) {
|
|
|
180
258
|
return obj;
|
|
181
259
|
}
|
|
182
260
|
|
|
183
|
-
/**
|
|
184
|
-
* The compiler claims every `a[href]` (and `form[action]`, which this handler
|
|
185
|
-
* ignores) at creation, and the runtime re-claims on `href` writes. This
|
|
186
|
-
* consumer gives each router-managed anchor the link-state vocabulary without
|
|
187
|
-
* a wrapper component:
|
|
188
|
-
*
|
|
189
|
-
* - `aria-current="page"` — the location matches the link exactly
|
|
190
|
-
* - `data-active` — exact or prefix match
|
|
191
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
192
|
-
*
|
|
193
|
-
* Elements are claimed at creation, so late mounts (`<Show>`, `<For>`,
|
|
194
|
-
* portals) are correct immediately. One render effect (owned by the router)
|
|
195
|
-
* subscribes to the location and sweeps a registry of claimed anchors —
|
|
196
|
-
* anchors themselves carry no reactive machinery, just a registry entry
|
|
197
|
-
* removed by their creating owner's cleanup. State is applied once at claim
|
|
198
|
-
* so it is correct before the next navigation; re-claims (an `href` write)
|
|
199
|
-
* are the same one-shot untracked refresh, reading the element's current
|
|
200
|
-
* `href` from the DOM.
|
|
201
|
-
*/
|
|
202
|
-
function setupLinkClaims(router, explicitLinks) {
|
|
203
|
-
const basePath = router.base.path();
|
|
204
|
-
// per-element record; `current` remembers whether we set `aria-current`,
|
|
205
|
-
// so user-authored values (steppers, breadcrumbs) are never stripped
|
|
206
|
-
const claimed = new WeakMap();
|
|
207
|
-
const registry = new Set();
|
|
208
|
-
function isSvg(el) {
|
|
209
|
-
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** The comparable pathname when the router manages this anchor, else `undefined`. */
|
|
213
|
-
function managedPath(a) {
|
|
214
|
-
if (explicitLinks && !a.hasAttribute("link")) return;
|
|
215
|
-
const svg = isSvg(a);
|
|
216
|
-
// claims fire at creation while the element is still in the template's
|
|
217
|
-
// inert fragment, where the `href` property is not resolved — resolve the
|
|
218
|
-
// raw attribute against the live document instead
|
|
219
|
-
const href = svg ? a.href.baseVal : a.getAttribute("href");
|
|
220
|
-
const target = svg ? a.target.baseVal : a.target;
|
|
221
|
-
if (target || !href) return;
|
|
222
|
-
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
223
|
-
if (a.hasAttribute("download") || rel.includes("external")) return;
|
|
224
|
-
let url;
|
|
225
|
-
try {
|
|
226
|
-
url = new URL(href, document.baseURI);
|
|
227
|
-
} catch {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
231
|
-
return comparablePath(url.pathname);
|
|
232
|
-
}
|
|
233
|
-
function linkState(a) {
|
|
234
|
-
// read reactive sources unconditionally so the owning effect stays
|
|
235
|
-
// subscribed even while the anchor is not router-managed
|
|
236
|
-
const loc = decodeURI(comparablePath(router.location.pathname));
|
|
237
|
-
const routing = router.isRouting();
|
|
238
|
-
const path = managedPath(a);
|
|
239
|
-
// the root path is a prefix of everything, so it only matches exactly —
|
|
240
|
-
// there is no per-anchor `end` opt-out like useLinkState has
|
|
241
|
-
const matches = target => path !== undefined && (target === path || path !== "" && target.startsWith(path + "/"));
|
|
242
|
-
// effects observe the committed location during a transition, so the
|
|
243
|
-
// in-flight target comes from pendingTarget — readable here because the
|
|
244
|
-
// isRouting write flushes after the target is assigned
|
|
245
|
-
const pending = routing && !!router.pendingTarget && matches(decodeURI(comparablePath(router.pendingTarget.value)));
|
|
246
|
-
return {
|
|
247
|
-
active: matches(loc),
|
|
248
|
-
pending,
|
|
249
|
-
exact: path !== undefined && loc === path
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
function apply(a, rec, {
|
|
253
|
-
active,
|
|
254
|
-
pending,
|
|
255
|
-
exact
|
|
256
|
-
}) {
|
|
257
|
-
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
258
|
-
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
259
|
-
if (exact !== rec.current) {
|
|
260
|
-
exact ? a.setAttribute("aria-current", "page") : a.removeAttribute("aria-current");
|
|
261
|
-
rec.current = exact;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
265
|
-
|
|
266
|
-
// The one subscription for every anchor: compute tracks the sources
|
|
267
|
-
// linkState derives from (the in-flight pendingTarget is readable in the
|
|
268
|
-
// effect phase because the isRouting write flushes after the target is
|
|
269
|
-
// assigned), the effect phase sweeps the registry untracked.
|
|
270
|
-
//
|
|
271
|
-
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
272
|
-
// This setup is client-only, so an id-consuming node here has no server
|
|
273
|
-
// counterpart and every subsequent hydration id would shift by one child
|
|
274
|
-
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
275
|
-
// unclaimed. (The option is honored by the runtime but missing from the
|
|
276
|
-
// published EffectOptions type, hence the cast.)
|
|
277
|
-
createRenderEffect(() => (router.location.pathname, router.isRouting()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
|
|
278
|
-
transparent: true
|
|
279
|
-
});
|
|
280
|
-
onCleanup(registerElementClaim(node => {
|
|
281
|
-
if (node.nodeName.toUpperCase() !== "A") return;
|
|
282
|
-
const a = node;
|
|
283
|
-
// re-claim (href changed): the claiming write runs inside another
|
|
284
|
-
// effect, so refresh without leaking subscriptions into it
|
|
285
|
-
const existing = claimed.get(a);
|
|
286
|
-
if (existing) return refresh(a, existing);
|
|
287
|
-
const rec = {
|
|
288
|
-
current: false
|
|
289
|
-
};
|
|
290
|
-
claimed.set(a, rec);
|
|
291
|
-
// claims fire during component setup, so an owner is present in
|
|
292
|
-
// practice to bound the registry entry's lifetime; without one, state
|
|
293
|
-
// is still applied once at creation
|
|
294
|
-
if (getOwner()) {
|
|
295
|
-
registry.add(a);
|
|
296
|
-
onCleanup(() => registry.delete(a));
|
|
297
|
-
}
|
|
298
|
-
refresh(a, rec);
|
|
299
|
-
}));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* The submit delegation consults this slot instead of importing the action
|
|
304
|
-
* module: the action side installs its handler on first action creation
|
|
305
|
-
* (see data/action.ts), so an app that never creates an action never pulls
|
|
306
|
-
* the data layer into its bundle through the router's event wiring.
|
|
307
|
-
*/
|
|
308
|
-
|
|
309
|
-
let formHandler;
|
|
310
|
-
function setRouterFormHandler(handler) {
|
|
311
|
-
formHandler = handler;
|
|
312
|
-
}
|
|
313
|
-
function setupNativeEvents({
|
|
314
|
-
preload = true,
|
|
315
|
-
explicitLinks = false,
|
|
316
|
-
actionBase = "/_server",
|
|
317
|
-
transformUrl
|
|
318
|
-
} = {}) {
|
|
319
|
-
return router => {
|
|
320
|
-
const basePath = router.base.path();
|
|
321
|
-
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
322
|
-
let preloadTimeout;
|
|
323
|
-
let lastElement;
|
|
324
|
-
function isSvg(el) {
|
|
325
|
-
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
326
|
-
}
|
|
327
|
-
function handleAnchor(evt) {
|
|
328
|
-
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
329
|
-
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
330
|
-
if (!a || explicitLinks && !a.hasAttribute("link")) return;
|
|
331
|
-
const svg = isSvg(a);
|
|
332
|
-
const href = svg ? a.href.baseVal : a.href;
|
|
333
|
-
const target = svg ? a.target.baseVal : a.target;
|
|
334
|
-
if (target || !href && !a.hasAttribute("state")) return;
|
|
335
|
-
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
336
|
-
if (a.hasAttribute("download") || rel && rel.includes("external")) return;
|
|
337
|
-
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
338
|
-
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
339
|
-
return [a, url];
|
|
340
|
-
}
|
|
341
|
-
function handleAnchorClick(evt) {
|
|
342
|
-
const res = handleAnchor(evt);
|
|
343
|
-
if (!res) return;
|
|
344
|
-
const [a, url] = res;
|
|
345
|
-
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
346
|
-
const state = a.getAttribute("state");
|
|
347
|
-
evt.preventDefault();
|
|
348
|
-
navigateFromRoute(to, {
|
|
349
|
-
resolve: false,
|
|
350
|
-
replace: a.hasAttribute("replace"),
|
|
351
|
-
scroll: !a.hasAttribute("noscroll"),
|
|
352
|
-
state: state ? JSON.parse(state) : undefined
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
function handleAnchorPreload(evt) {
|
|
356
|
-
const res = handleAnchor(evt);
|
|
357
|
-
if (!res) return;
|
|
358
|
-
const [a, url] = res;
|
|
359
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
360
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
361
|
-
}
|
|
362
|
-
function handleAnchorMove(evt) {
|
|
363
|
-
clearTimeout(preloadTimeout);
|
|
364
|
-
const res = handleAnchor(evt);
|
|
365
|
-
if (!res) return lastElement = null;
|
|
366
|
-
const [a, url] = res;
|
|
367
|
-
if (lastElement === a) return;
|
|
368
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
369
|
-
preloadTimeout = setTimeout(() => {
|
|
370
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
371
|
-
lastElement = a;
|
|
372
|
-
}, 20);
|
|
373
|
-
}
|
|
374
|
-
function handleFormSubmit(evt) {
|
|
375
|
-
if (formHandler) return formHandler(evt, router, actionBase);
|
|
376
|
-
// No form handler means no action module in the client graph at all
|
|
377
|
-
// (e.g. server components binding forms straight to server functions).
|
|
378
|
-
// A POST to a url under actionBase is self-describing, so delegation
|
|
379
|
-
// is still sufficient: intercept synchronously — the no-JS treatment
|
|
380
|
-
// is reserved for clients with no JS — capture the FormData, and load
|
|
381
|
-
// the handler lazily. Apps that never submit one never load it.
|
|
382
|
-
if (evt.defaultPrevented) return;
|
|
383
|
-
const form = evt.target;
|
|
384
|
-
const ref = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : form.getAttribute("action");
|
|
385
|
-
if (!ref || ref.startsWith("https://action/")) return;
|
|
386
|
-
const url = new URL(ref, document.baseURI);
|
|
387
|
-
const path = router.parsePath(url.pathname + url.search);
|
|
388
|
-
if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST") return;
|
|
389
|
-
evt.preventDefault();
|
|
390
|
-
const data = new FormData(form, evt.submitter);
|
|
391
|
-
Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ensure delegated event run first
|
|
395
|
-
delegateEvents(["click", "submit"]);
|
|
396
|
-
document.addEventListener("click", handleAnchorClick);
|
|
397
|
-
if (preload) {
|
|
398
|
-
document.addEventListener("mousemove", handleAnchorMove, {
|
|
399
|
-
passive: true
|
|
400
|
-
});
|
|
401
|
-
document.addEventListener("focusin", handleAnchorPreload, {
|
|
402
|
-
passive: true
|
|
403
|
-
});
|
|
404
|
-
document.addEventListener("touchstart", handleAnchorPreload, {
|
|
405
|
-
passive: true
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
document.addEventListener("submit", handleFormSubmit);
|
|
409
|
-
onCleanup(() => {
|
|
410
|
-
document.removeEventListener("click", handleAnchorClick);
|
|
411
|
-
if (preload) {
|
|
412
|
-
document.removeEventListener("mousemove", handleAnchorMove);
|
|
413
|
-
document.removeEventListener("focusin", handleAnchorPreload);
|
|
414
|
-
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
415
|
-
}
|
|
416
|
-
document.removeEventListener("submit", handleFormSubmit);
|
|
417
|
-
});
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/*
|
|
422
|
-
* The typed path proxy. Property access descends into static segments, calls
|
|
423
|
-
* bind params, and a zero-arg or search-object call terminates to a plain
|
|
424
|
-
* string. The runtime is tree-agnostic — it just builds strings — while the
|
|
425
|
-
* `RoutePaths<R>` type derived from the route config constrains which
|
|
426
|
-
* accesses and calls exist. Every node also coerces via `toString`, so nodes
|
|
427
|
-
* drop straight into `href`, `navigate()`, and `redirect()`.
|
|
428
|
-
*/
|
|
429
|
-
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
|
-
// Typed match filters
|
|
432
|
-
// ---------------------------------------------------------------------------
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* A match filter that also declares the TypeScript type accepted for the
|
|
436
|
-
* param at `paths` callsites (the runtime param remains a string).
|
|
437
|
-
*/
|
|
438
|
-
|
|
439
|
-
/** Matches integer segments; types the param as `number` in the path proxy. */
|
|
440
|
-
const int = s => /^-?\d+$/.test(s);
|
|
441
|
-
|
|
442
|
-
// ---------------------------------------------------------------------------
|
|
443
|
-
// RoutePaths<R> — the proxy's type, derived from the route tree
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
|
|
446
|
-
/** Collects a maximal run of required params (and a trailing splat) into one call's argument tuple. */
|
|
447
|
-
|
|
448
|
-
/** The search param types a route end carries: input builds URLs, output is what parsing returns. */
|
|
449
|
-
|
|
450
|
-
/** Terminating calls available on every route end: zero-arg, or search object plus optional hash. */
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Sees through a lazy `children` thunk: the routes the import's promise
|
|
454
|
-
* resolves to (its `default` or `routes` export, matching the runtime) type
|
|
455
|
-
* exactly like inline children. Only tables genuinely built at runtime —
|
|
456
|
-
* where the thunk's return type is a plain `RouteDefinition[]` — degrade to
|
|
457
|
-
* untyped, definitionally.
|
|
458
|
-
*/
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* The type of a router instance's `paths` proxy for a given route tree.
|
|
462
|
-
* Requires the tree to be a literal tuple (`as const` or a `const` type
|
|
463
|
-
* param); non-literal trees fall back to an untyped proxy.
|
|
464
|
-
*/
|
|
465
|
-
|
|
466
|
-
/** Extracts the params record a paths node binds, as runtime (string-valued) params. */
|
|
467
|
-
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
|
-
// Runtime
|
|
470
|
-
// ---------------------------------------------------------------------------
|
|
471
|
-
|
|
472
|
-
const encodeParam = value => String(value).split("/").map(encodeURIComponent).join("/");
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
|
|
476
|
-
* from the router's history adapter (eg. hash routing prefixes `#`), and
|
|
477
|
-
* `base` is baked into every produced path.
|
|
478
|
-
*/
|
|
479
|
-
function createPathsProxy(renderPath = p => p, base = "") {
|
|
480
|
-
const toHref = (pathname, suffix = "") => renderPath(pathname || "/") + suffix;
|
|
481
|
-
function node(pathname) {
|
|
482
|
-
const build = (...args) => {
|
|
483
|
-
let path = pathname;
|
|
484
|
-
for (let i = 0; i < args.length; i++) {
|
|
485
|
-
const arg = args[i];
|
|
486
|
-
if (typeof arg === "object" && arg !== null) {
|
|
487
|
-
// a search object terminates; an optional hash string may follow
|
|
488
|
-
const hash = typeof args[i + 1] === "string" ? `#${args[i + 1]}` : "";
|
|
489
|
-
return toHref(path, mergeSearchString("", arg) + hash);
|
|
490
|
-
}
|
|
491
|
-
path += `/${encodeParam(arg)}`;
|
|
492
|
-
}
|
|
493
|
-
// zero-arg calls terminate; param-only calls stay chainable
|
|
494
|
-
return args.length ? node(path) : toHref(path);
|
|
495
|
-
};
|
|
496
|
-
return new Proxy(build, {
|
|
497
|
-
get(_, prop) {
|
|
498
|
-
if (prop === "toString") return () => toHref(pathname);
|
|
499
|
-
if (typeof prop === "symbol") return prop === Symbol.toPrimitive ? () => toHref(pathname) : undefined;
|
|
500
|
-
return node(`${pathname}/${prop}`);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
return node(normalizePath(base));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// The flash cookie's name and one-shot clearing — split from the codec in
|
|
508
|
-
// flash.ts so the router core can consume the cookie eagerly (the clear must
|
|
509
|
-
// be appended before streaming flushes the response headers, and an unread
|
|
510
|
-
// outcome must not haunt a later request) without carrying the encode/decode
|
|
511
|
-
// machinery into client bundles that never load the action layer.
|
|
512
|
-
|
|
513
|
-
const FLASH_COOKIE = "flash";
|
|
514
|
-
const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
|
|
515
|
-
|
|
516
|
-
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
517
|
-
function hasFlashCookie(cookieHeader) {
|
|
518
|
-
return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** The raw encoded flash payload out of a Cookie header, if present. */
|
|
522
|
-
function matchFlashCookie(cookieHeader) {
|
|
523
|
-
const match = cookieHeader && cookieHeader.match(FLASH_MATCHER);
|
|
524
|
-
return match ? match[1] : undefined;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
528
|
-
function clearFlashCookie() {
|
|
529
|
-
return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
261
|
const MAX_REDIRECTS = 100;
|
|
533
262
|
|
|
534
263
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
535
264
|
const RouterContextObj = createContext();
|
|
536
265
|
const RouteContextObj = createContext();
|
|
537
|
-
function useOptionalContext(context) {
|
|
538
|
-
try {
|
|
539
|
-
return useContext(context);
|
|
540
|
-
} catch {
|
|
541
|
-
return undefined;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
266
|
const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
|
|
545
|
-
const useRoute = () =>
|
|
267
|
+
const useRoute = () => useContext(RouteContextObj) || useRouter().base;
|
|
546
268
|
const useResolvedPath = path => {
|
|
547
269
|
const route = useRoute();
|
|
548
270
|
return createMemo(() => route.resolvePath(path()));
|
|
@@ -589,8 +311,8 @@ const useNavigate = () => useRouter().navigatorFactory();
|
|
|
589
311
|
const useLocation = () => useRouter().location;
|
|
590
312
|
|
|
591
313
|
/**
|
|
592
|
-
* Retrieves
|
|
593
|
-
* Useful for showing pending
|
|
314
|
+
* Retrieves signal that indicates whether the route is currently in a *Transition*.
|
|
315
|
+
* Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering.
|
|
594
316
|
*
|
|
595
317
|
* @example
|
|
596
318
|
* ```js
|
|
@@ -605,6 +327,22 @@ const useLocation = () => useRouter().location;
|
|
|
605
327
|
*/
|
|
606
328
|
const useIsRouting = () => useRouter().isRouting;
|
|
607
329
|
|
|
330
|
+
/**
|
|
331
|
+
* usePreloadRoute returns a function that can be used to preload a route manual.
|
|
332
|
+
* This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```js
|
|
336
|
+
* const preload = usePreloadRoute();
|
|
337
|
+
*
|
|
338
|
+
* preload(`/users/settings`, { preloadData: true });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
const usePreloadRoute = () => {
|
|
342
|
+
const pre = useRouter().preloadRoute;
|
|
343
|
+
return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
|
|
344
|
+
};
|
|
345
|
+
|
|
608
346
|
/**
|
|
609
347
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
610
348
|
* Useful for determining if a given path matches the current route.
|
|
@@ -628,42 +366,22 @@ const useMatch = (path, matchFilters) => {
|
|
|
628
366
|
};
|
|
629
367
|
|
|
630
368
|
/**
|
|
631
|
-
* `
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* chain. Unlike `useMatch`, which tests a path pattern you supply against the
|
|
635
|
-
* location, this reflects the route tree itself.
|
|
636
|
-
*
|
|
369
|
+
* `useCurrentMatches` returns all the matches for the current matched route.
|
|
370
|
+
* Useful for getting all the route information.
|
|
371
|
+
*
|
|
637
372
|
* @example
|
|
638
373
|
* ```js
|
|
639
|
-
* const matches =
|
|
640
|
-
*
|
|
641
|
-
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
374
|
+
* const matches = useCurrentMatches();
|
|
375
|
+
*
|
|
376
|
+
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
642
377
|
* ```
|
|
643
378
|
*/
|
|
644
|
-
const
|
|
379
|
+
const useCurrentMatches = () => {
|
|
645
380
|
const router = useRouter();
|
|
646
381
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
647
382
|
return () => router.matches().slice();
|
|
648
383
|
};
|
|
649
384
|
|
|
650
|
-
/**
|
|
651
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
652
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
653
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
654
|
-
*
|
|
655
|
-
* @example
|
|
656
|
-
* ```js
|
|
657
|
-
* const preload = usePreloadRoute();
|
|
658
|
-
*
|
|
659
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
660
|
-
* ```
|
|
661
|
-
*/
|
|
662
|
-
const usePreloadRoute = () => {
|
|
663
|
-
const pre = useRouter().preloadRoute;
|
|
664
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
665
|
-
};
|
|
666
|
-
|
|
667
385
|
/**
|
|
668
386
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
669
387
|
*
|
|
@@ -672,13 +390,10 @@ const usePreloadRoute = () => {
|
|
|
672
390
|
* const params = useParams();
|
|
673
391
|
*
|
|
674
392
|
* // fetch user based on the id path parameter
|
|
675
|
-
* const
|
|
393
|
+
* const [user] = createResource(() => params.id, fetchUser);
|
|
676
394
|
* ```
|
|
677
395
|
*/
|
|
678
|
-
|
|
679
|
-
function useParams(_path) {
|
|
680
|
-
return useRoute().params;
|
|
681
|
-
}
|
|
396
|
+
const useParams = () => useRouter().params;
|
|
682
397
|
|
|
683
398
|
/**
|
|
684
399
|
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
|
|
@@ -707,8 +422,7 @@ function useParams(_path) {
|
|
|
707
422
|
* );
|
|
708
423
|
* ```
|
|
709
424
|
*/
|
|
710
|
-
|
|
711
|
-
function useSearchParams(path) {
|
|
425
|
+
const useSearchParams = () => {
|
|
712
426
|
const router = useRouter();
|
|
713
427
|
const location = router.location;
|
|
714
428
|
const navigate = useNavigate();
|
|
@@ -728,73 +442,43 @@ function useSearchParams(path) {
|
|
|
728
442
|
...options
|
|
729
443
|
});
|
|
730
444
|
};
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
// A schema that reports issues is skipped, leaving raw values — search
|
|
735
|
-
// strings are user input, so defaults belong in the schema itself.
|
|
736
|
-
const query = path ? createMemoObject(createMemo(() => {
|
|
737
|
-
const raw = {
|
|
738
|
-
...location.query
|
|
739
|
-
};
|
|
740
|
-
let result;
|
|
741
|
-
for (const match of router.matches()) {
|
|
742
|
-
const schema = match.route.key.search;
|
|
743
|
-
if (!schema) continue;
|
|
744
|
-
const outcome = schema["~standard"].validate(raw);
|
|
745
|
-
if (outcome instanceof Promise) throw new Error("Async Standard Schema validation is not supported for search params");
|
|
746
|
-
if (!outcome.issues) result = Object.assign(result || {
|
|
747
|
-
...raw
|
|
748
|
-
}, outcome.value);
|
|
749
|
-
}
|
|
750
|
-
return result || raw;
|
|
751
|
-
})) : location.query;
|
|
752
|
-
return [query, setSearchParams];
|
|
753
|
-
}
|
|
445
|
+
return [location.query, setSearchParams];
|
|
446
|
+
};
|
|
447
|
+
|
|
754
448
|
/**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
449
|
+
* useBeforeLeave takes a function that will be called prior to leaving a route.
|
|
450
|
+
* The function will be called with:
|
|
451
|
+
*
|
|
452
|
+
* - from (*Location*): current location (before change).
|
|
453
|
+
* - to (*string | number*): path passed to `navigate`.
|
|
454
|
+
* - options (*NavigateOptions*): options passed to navigate.
|
|
455
|
+
* - preventDefault (*function*): call to block the route change.
|
|
456
|
+
* - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
|
|
457
|
+
* - 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).
|
|
458
|
+
*
|
|
759
459
|
* @example
|
|
760
|
-
* ```
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
460
|
+
* ```js
|
|
461
|
+
* useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
462
|
+
* if (form.isDirty && !e.defaultPrevented) {
|
|
463
|
+
* // preventDefault to block immediately and prompt user async
|
|
464
|
+
* e.preventDefault();
|
|
465
|
+
* setTimeout(() => {
|
|
466
|
+
* if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
467
|
+
* // user wants to proceed anyway so retry with force=true
|
|
468
|
+
* e.retry(true);
|
|
469
|
+
* }
|
|
470
|
+
* }, 100);
|
|
471
|
+
* }
|
|
472
|
+
* });
|
|
769
473
|
* ```
|
|
770
474
|
*/
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const path = createMemo(() => {
|
|
777
|
-
const to_ = to();
|
|
778
|
-
return to_ === undefined ? undefined : comparablePath(to_);
|
|
475
|
+
const useBeforeLeave = listener => {
|
|
476
|
+
const s = useRouter().beforeLeave.subscribe({
|
|
477
|
+
listener,
|
|
478
|
+
location: useLocation(),
|
|
479
|
+
navigate: useNavigate()
|
|
779
480
|
});
|
|
780
|
-
|
|
781
|
-
const path_ = path();
|
|
782
|
-
if (path_ === undefined) return [false, false];
|
|
783
|
-
const exact = loc === path_;
|
|
784
|
-
return [exact || !options.end && loc.startsWith(path_ + "/"), exact];
|
|
785
|
-
};
|
|
786
|
-
const state = createMemo(() => matches(decodeURI(comparablePath(location.pathname))));
|
|
787
|
-
return {
|
|
788
|
-
active: () => state()[0],
|
|
789
|
-
current: () => state()[1],
|
|
790
|
-
// match the in-flight target explicitly (rather than active-while-routing)
|
|
791
|
-
// so the answer is the same from pure reads and from effects, which
|
|
792
|
-
// observe the committed location during a transition
|
|
793
|
-
pending: createMemo(() => {
|
|
794
|
-
state(); // location dependency: mid-flight target swaps recompute
|
|
795
|
-
return router.isRouting() && !!router.pendingTarget && matches(decodeURI(comparablePath(router.pendingTarget.value)))[0];
|
|
796
|
-
})
|
|
797
|
-
};
|
|
481
|
+
onCleanup(s);
|
|
798
482
|
};
|
|
799
483
|
|
|
800
484
|
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
@@ -802,86 +486,11 @@ const useLinkState = (href, options = {}) => {
|
|
|
802
486
|
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
803
487
|
// still percent-encoded exactly as before, since browsers encode those too.
|
|
804
488
|
const encodeSegment = s => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
|
|
805
|
-
|
|
806
|
-
// ---------------------------------------------------------------------------
|
|
807
|
-
// Lazy route subtrees
|
|
808
|
-
// ---------------------------------------------------------------------------
|
|
809
|
-
//
|
|
810
|
-
// A `children` thunk (`() => import("./feature/routes")`) is a *boundary*:
|
|
811
|
-
// until it resolves, the compiled tree carries a param-less catch-all
|
|
812
|
-
// placeholder branch under the boundary's pattern (splat-scored, so static
|
|
813
|
-
// siblings still win). Resolution is append-only and cached per thunk, then a
|
|
814
|
-
// module-level version signal bumps and every `branches()` consumer
|
|
815
|
-
// recompiles — matches, params, and route states all react. The placeholder's
|
|
816
|
-
// component reads a memo of the resolution promise, which keeps the enclosing
|
|
817
|
-
// navigation transition pending exactly like a `lazy()` route component; its
|
|
818
|
-
// `preload` *is* the resolver, so hover-intent preloading kicks the table
|
|
819
|
-
// load through the existing component-preload path.
|
|
820
|
-
|
|
821
|
-
const lazyBoundaries = new WeakMap();
|
|
822
|
-
// Module scope: boundary resolution is global, deterministic state (same
|
|
823
|
-
// thunk -> same routes), shared by every factory instance and the server's
|
|
824
|
-
// flight collector.
|
|
825
|
-
const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
|
|
826
|
-
|
|
827
|
-
/** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
|
|
828
|
-
function trackLazySubtrees() {
|
|
829
|
-
return lazyTreeVersion();
|
|
830
|
-
}
|
|
831
|
-
function getLazyBoundary(thunk) {
|
|
832
|
-
let record = lazyBoundaries.get(thunk);
|
|
833
|
-
if (!record) lazyBoundaries.set(thunk, record = {
|
|
834
|
-
thunk
|
|
835
|
-
});
|
|
836
|
-
return record;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Kicks (or joins) a boundary's resolution. Returns the resolved routes
|
|
841
|
-
* synchronously once available, the in-flight promise otherwise. Commit is
|
|
842
|
-
* always async — even for thunks returning arrays — so the version bump
|
|
843
|
-
* never writes a signal from inside a render computation.
|
|
844
|
-
*/
|
|
845
|
-
function resolveLazySubtree(record) {
|
|
846
|
-
if (record.resolved) return record.resolved;
|
|
847
|
-
return record.promise ||= Promise.resolve(record.thunk()).then(m => {
|
|
848
|
-
const routes = Array.isArray(m) ? m : m.default || m.routes || [];
|
|
849
|
-
record.resolved = routes;
|
|
850
|
-
setLazyTreeVersion(v => v + 1);
|
|
851
|
-
return record.resolved;
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* The unresolved boundaries in a match chain. Rendering gates on these (the
|
|
857
|
-
* route-states memo suspends until they land — see routers/components.tsx)
|
|
858
|
-
* and the server's flight collector awaits them before its preload pass.
|
|
859
|
-
*/
|
|
860
|
-
function unresolvedLazyMatches(matches) {
|
|
861
|
-
const pending = [];
|
|
862
|
-
for (const match of matches) if (match.route.lazy && !match.route.lazy.resolved) pending.push(match.route.lazy);
|
|
863
|
-
return pending;
|
|
864
|
-
}
|
|
865
|
-
function createLazyPlaceholder(pattern, record) {
|
|
866
|
-
// The placeholder never renders and needs no component — `matches` parks
|
|
867
|
-
// on unresolved boundaries before route contexts are created (kicking the
|
|
868
|
-
// resolver as it does), preloadRoute kicks it directly, and the version
|
|
869
|
-
// bump swaps in the real routes. `path + "/*"` with no splat name matches
|
|
870
|
-
// the boundary itself and everything beneath it without recording a param
|
|
871
|
-
// (createMatcher skips empty splat names).
|
|
872
|
-
const placeholderPattern = pattern + "/*";
|
|
873
|
-
return {
|
|
874
|
-
key: record,
|
|
875
|
-
originalPath: "*",
|
|
876
|
-
pattern: placeholderPattern,
|
|
877
|
-
matcher: createMatcher(placeholderPattern),
|
|
878
|
-
lazy: record
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
489
|
function createRoutes(routeDef, base = "") {
|
|
882
490
|
const {
|
|
883
491
|
component,
|
|
884
492
|
preload,
|
|
493
|
+
load,
|
|
885
494
|
children,
|
|
886
495
|
info
|
|
887
496
|
} = routeDef;
|
|
@@ -889,7 +498,7 @@ function createRoutes(routeDef, base = "") {
|
|
|
889
498
|
const shared = {
|
|
890
499
|
key: routeDef,
|
|
891
500
|
component,
|
|
892
|
-
preload,
|
|
501
|
+
preload: preload || load,
|
|
893
502
|
info
|
|
894
503
|
};
|
|
895
504
|
return asArray(routeDef.path).reduce((acc, originalPath) => {
|
|
@@ -942,23 +551,9 @@ function createBranches(routeDef, base = "", stack = [], branches = []) {
|
|
|
942
551
|
const routes = createRoutes(def, base);
|
|
943
552
|
for (const route of routes) {
|
|
944
553
|
stack.push(route);
|
|
945
|
-
|
|
946
|
-
if (
|
|
947
|
-
|
|
948
|
-
if (record.resolved) {
|
|
949
|
-
children = record.resolved;
|
|
950
|
-
} else {
|
|
951
|
-
// unresolved boundary: a catch-all placeholder holds its ground
|
|
952
|
-
stack.push(createLazyPlaceholder(route.pattern, record));
|
|
953
|
-
branches.push(createBranch([...stack], branches.length));
|
|
954
|
-
stack.pop();
|
|
955
|
-
stack.pop();
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
const isEmptyArray = Array.isArray(children) && children.length === 0;
|
|
960
|
-
if (children && !isEmptyArray) {
|
|
961
|
-
createBranches(children, route.pattern, stack, branches);
|
|
554
|
+
const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
|
|
555
|
+
if (def.children && !isEmptyArray) {
|
|
556
|
+
createBranches(def.children, route.pattern, stack, branches);
|
|
962
557
|
} else {
|
|
963
558
|
const branch = createBranch([...stack], branches.length);
|
|
964
559
|
branches.push(branch);
|
|
@@ -980,16 +575,9 @@ function getRouteMatches(branches, location) {
|
|
|
980
575
|
}
|
|
981
576
|
return [];
|
|
982
577
|
}
|
|
983
|
-
function mergeParams(matches) {
|
|
984
|
-
const params = {};
|
|
985
|
-
for (let i = 0; i < matches.length; i++) {
|
|
986
|
-
Object.assign(params, matches[i].params);
|
|
987
|
-
}
|
|
988
|
-
return params;
|
|
989
|
-
}
|
|
990
578
|
function createLocation(path, state, queryWrapper) {
|
|
991
579
|
const origin = new URL(mockBase);
|
|
992
|
-
const url = createMemo(
|
|
580
|
+
const url = createMemo(prev => {
|
|
993
581
|
const path_ = path();
|
|
994
582
|
try {
|
|
995
583
|
// anchor rooted paths against the origin explicitly - a path with
|
|
@@ -999,14 +587,14 @@ function createLocation(path, state, queryWrapper) {
|
|
|
999
587
|
console.error(`Invalid path ${path_}`);
|
|
1000
588
|
return prev;
|
|
1001
589
|
}
|
|
1002
|
-
}, {
|
|
590
|
+
}, origin, {
|
|
1003
591
|
equals: (a, b) => a.href === b.href
|
|
1004
592
|
});
|
|
1005
593
|
const pathname = createMemo(() => url().pathname);
|
|
1006
|
-
const search = createMemo(() => url().search);
|
|
594
|
+
const search = createMemo(() => url().search, true);
|
|
1007
595
|
const hash = createMemo(() => url().hash);
|
|
1008
596
|
const key = () => "";
|
|
1009
|
-
const queryFn =
|
|
597
|
+
const queryFn = on(search, () => extractSearchParams(url()));
|
|
1010
598
|
return {
|
|
1011
599
|
get pathname() {
|
|
1012
600
|
return pathname();
|
|
@@ -1026,49 +614,6 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1026
614
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
1027
615
|
};
|
|
1028
616
|
}
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* Rendezvous between the router and the data layer's single-flight consumer.
|
|
1032
|
-
* The Router registers itself at mount (unless `singleFlight={false}`); the
|
|
1033
|
-
* action side provides the consumer factory when the first action is created
|
|
1034
|
-
* (see data/action.ts). Whichever side arrives first waits for the other, so
|
|
1035
|
-
* an action module loaded lazily (a code-split route) still attaches to the
|
|
1036
|
-
* already-mounted router — and a router-only app, where no action ever
|
|
1037
|
-
* loads, never subscribes to the transport, so the server is never asked to
|
|
1038
|
-
* collect.
|
|
1039
|
-
*/
|
|
1040
|
-
let flightConsumerFactory;
|
|
1041
|
-
const flightRouters = new Map();
|
|
1042
|
-
function registerFlightRouter(router) {
|
|
1043
|
-
flightRouters.set(router, flightConsumerFactory && flightConsumerFactory(router));
|
|
1044
|
-
return () => {
|
|
1045
|
-
const unsubscribe = flightRouters.get(router);
|
|
1046
|
-
flightRouters.delete(router);
|
|
1047
|
-
unsubscribe && unsubscribe();
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
function provideFlightConsumer(factory) {
|
|
1051
|
-
if (flightConsumerFactory) return;
|
|
1052
|
-
flightConsumerFactory = factory;
|
|
1053
|
-
for (const [router, unsubscribe] of flightRouters) {
|
|
1054
|
-
if (!unsubscribe) flightRouters.set(router, factory(router));
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/**
|
|
1059
|
-
* The flash-cookie codec, provided by the action side (data/action.ts) so
|
|
1060
|
-
* the router core never carries it: the core consumes the cookie eagerly
|
|
1061
|
-
* per request (detection + one-shot clear via the tiny flashCookie.ts half)
|
|
1062
|
-
* but defers decoding to this slot, read when the submissions signal
|
|
1063
|
-
* initializes. Actions are created at module scope, so on the server the
|
|
1064
|
-
* decoder is always installed before useSubmission can read — and a
|
|
1065
|
-
* router-only app, where it never installs, has no actions that could have
|
|
1066
|
-
* produced a flash cookie in the first place.
|
|
1067
|
-
*/
|
|
1068
|
-
let flashDecoder;
|
|
1069
|
-
function provideFlashDecoder(decoder) {
|
|
1070
|
-
flashDecoder || (flashDecoder = decoder);
|
|
1071
|
-
}
|
|
1072
617
|
let intent;
|
|
1073
618
|
function getIntent() {
|
|
1074
619
|
return intent;
|
|
@@ -1087,86 +632,83 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1087
632
|
} = integration;
|
|
1088
633
|
const parsePath = utils.parsePath || (p => p);
|
|
1089
634
|
const renderPath = utils.renderPath || (p => p);
|
|
1090
|
-
|
|
1091
|
-
const beforeLeave = utils.beforeLeave || {};
|
|
635
|
+
const beforeLeave = utils.beforeLeave || createBeforeLeave();
|
|
1092
636
|
const basePath = resolvePath("", options.base || "");
|
|
1093
|
-
const initialSource = untrack(source);
|
|
1094
637
|
if (basePath === undefined) {
|
|
1095
638
|
throw new Error(`${basePath} is not a valid base path`);
|
|
1096
|
-
} else if (basePath && !
|
|
639
|
+
} else if (basePath && !source().value) {
|
|
1097
640
|
setSource({
|
|
1098
641
|
value: basePath,
|
|
1099
642
|
replace: true,
|
|
1100
643
|
scroll: false
|
|
1101
644
|
});
|
|
1102
645
|
}
|
|
1103
|
-
const [isRouting, setIsRouting] = createSignal(false
|
|
1104
|
-
ownedWrite: true
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
// Navigate override written from event handlers.
|
|
1108
|
-
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
|
|
1109
|
-
ownedWrite: true
|
|
1110
|
-
});
|
|
646
|
+
const [isRouting, setIsRouting] = createSignal(false);
|
|
1111
647
|
|
|
1112
|
-
// Keep track of last target, so that last call to
|
|
648
|
+
// Keep track of last target, so that last call to transition wins
|
|
1113
649
|
let lastTransitionTarget;
|
|
1114
650
|
|
|
1115
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
651
|
+
// Transition the location to a new value
|
|
652
|
+
const transition = (newIntent, newTarget) => {
|
|
653
|
+
if (newTarget.value === reference() && newTarget.state === state()) return;
|
|
654
|
+
if (lastTransitionTarget === undefined) setIsRouting(true);
|
|
655
|
+
intent = newIntent;
|
|
656
|
+
lastTransitionTarget = newTarget;
|
|
657
|
+
startTransition(() => {
|
|
658
|
+
if (lastTransitionTarget !== newTarget) return;
|
|
659
|
+
setReference(lastTransitionTarget.value);
|
|
660
|
+
setState(lastTransitionTarget.state);
|
|
661
|
+
resetErrorBoundaries();
|
|
662
|
+
if (!isServer) submissions[1](subs => subs.filter(s => s.pending));
|
|
663
|
+
}).finally(() => {
|
|
664
|
+
if (lastTransitionTarget !== newTarget) return;
|
|
665
|
+
|
|
666
|
+
// Batch, in order for isRouting and final source update to happen together
|
|
667
|
+
batch(() => {
|
|
668
|
+
intent = undefined;
|
|
669
|
+
if (newIntent === "navigate") navigateEnd(lastTransitionTarget);
|
|
670
|
+
setIsRouting(false);
|
|
671
|
+
lastTransitionTarget = undefined;
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
};
|
|
675
|
+
const [reference, setReference] = createSignal(source().value);
|
|
676
|
+
const [state, setState] = createSignal(source().state);
|
|
677
|
+
const location = createLocation(reference, state, utils.queryWrapper);
|
|
1119
678
|
const referrers = [];
|
|
1120
|
-
|
|
1121
|
-
// must be appended before streaming flushes the response headers, and an
|
|
1122
|
-
// unread outcome must not haunt a later request's render. Only detection
|
|
1123
|
-
// and clearing happen here (the tiny flashCookie.ts half); the raw header
|
|
1124
|
-
// is stashed and decoding waits for the action-provided codec, read when
|
|
1125
|
-
// the lazily allocated submissions signal below first initializes.
|
|
1126
|
-
let flashCookieHeader;
|
|
1127
|
-
if (isServer) {
|
|
1128
|
-
const e = getRequestEvent();
|
|
1129
|
-
if (e && !(e.router && e.router.submission)) {
|
|
1130
|
-
const cookieHeader = e.request.headers.get("cookie");
|
|
1131
|
-
if (hasFlashCookie(cookieHeader)) {
|
|
1132
|
-
flashCookieHeader = cookieHeader;
|
|
1133
|
-
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
1134
|
-
if (e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
let submissions;
|
|
679
|
+
const submissions = createSignal(isServer ? initFromFlash() : []);
|
|
1139
680
|
const matches = createMemo(() => {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
// NotReadyError (not a returned promise) because a match chain is full
|
|
1145
|
-
// of component functions the hydration serializer must never see. The
|
|
1146
|
-
// recompute comes from the version-signal dependency on the client and
|
|
1147
|
-
// from the carried promise's retry on the server; a boundary nested
|
|
1148
|
-
// inside a boundary just parks the recomputed chain again.
|
|
1149
|
-
const pending = unresolvedLazyMatches(m);
|
|
1150
|
-
if (pending.length) throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
|
|
1151
|
-
return m;
|
|
681
|
+
if (typeof options.transformUrl === "function") {
|
|
682
|
+
return getRouteMatches(branches(), options.transformUrl(location.pathname));
|
|
683
|
+
}
|
|
684
|
+
return getRouteMatches(branches(), location.pathname);
|
|
1152
685
|
});
|
|
1153
|
-
const buildParams = () =>
|
|
1154
|
-
|
|
1155
|
-
|
|
686
|
+
const buildParams = () => {
|
|
687
|
+
const m = matches();
|
|
688
|
+
const params = {};
|
|
689
|
+
for (let i = 0; i < m.length; i++) {
|
|
690
|
+
Object.assign(params, m[i].params);
|
|
691
|
+
}
|
|
692
|
+
return params;
|
|
693
|
+
};
|
|
694
|
+
const params = utils.paramsWrapper ? utils.paramsWrapper(buildParams, branches) : createMemoObject(buildParams);
|
|
1156
695
|
const baseRoute = {
|
|
1157
696
|
pattern: basePath,
|
|
1158
|
-
params,
|
|
1159
697
|
path: () => basePath,
|
|
1160
698
|
outlet: () => null,
|
|
1161
699
|
resolvePath(to) {
|
|
1162
700
|
return resolvePath(basePath, to);
|
|
1163
701
|
}
|
|
1164
702
|
};
|
|
703
|
+
|
|
704
|
+
// Create a native transition, when source updates
|
|
705
|
+
createRenderEffect(on(source, source => transition("native", source), {
|
|
706
|
+
defer: true
|
|
707
|
+
}));
|
|
1165
708
|
return {
|
|
1166
709
|
base: baseRoute,
|
|
1167
710
|
location,
|
|
1168
711
|
params,
|
|
1169
|
-
wrapParams,
|
|
1170
712
|
isRouting,
|
|
1171
713
|
get pendingTarget() {
|
|
1172
714
|
return lastTransitionTarget;
|
|
@@ -1178,11 +720,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1178
720
|
beforeLeave,
|
|
1179
721
|
preloadRoute,
|
|
1180
722
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
1181
|
-
|
|
1182
|
-
return submissions ||= createSignal(isServer ? initSubmissions() : [], {
|
|
1183
|
-
ownedWrite: true
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
723
|
+
submissions
|
|
1186
724
|
};
|
|
1187
725
|
function navigateFromRoute(route, to, options) {
|
|
1188
726
|
// Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
|
|
@@ -1195,8 +733,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1195
733
|
}
|
|
1196
734
|
return;
|
|
1197
735
|
}
|
|
1198
|
-
// typed path proxy nodes coerce to their href
|
|
1199
|
-
if (typeof to !== "string") to = to.toString();
|
|
1200
736
|
const queryOnly = !to || to[0] === "?";
|
|
1201
737
|
const {
|
|
1202
738
|
replace,
|
|
@@ -1215,8 +751,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1215
751
|
} else if (referrers.length >= MAX_REDIRECTS) {
|
|
1216
752
|
throw new Error("Too many redirects");
|
|
1217
753
|
}
|
|
1218
|
-
const current =
|
|
1219
|
-
if (resolvedTo !== current
|
|
754
|
+
const current = reference();
|
|
755
|
+
if (resolvedTo !== current || nextState !== state()) {
|
|
1220
756
|
if (isServer) {
|
|
1221
757
|
const e = getRequestEvent();
|
|
1222
758
|
e && (e.response = {
|
|
@@ -1231,46 +767,24 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1231
767
|
scroll,
|
|
1232
768
|
state: nextState
|
|
1233
769
|
});
|
|
1234
|
-
} else if (
|
|
770
|
+
} else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
1235
771
|
referrers.push({
|
|
1236
|
-
value: current
|
|
772
|
+
value: current,
|
|
1237
773
|
replace,
|
|
1238
774
|
scroll,
|
|
1239
|
-
state:
|
|
775
|
+
state: state()
|
|
1240
776
|
});
|
|
1241
|
-
|
|
777
|
+
transition("navigate", {
|
|
1242
778
|
value: resolvedTo,
|
|
1243
779
|
state: nextState
|
|
1244
|
-
};
|
|
1245
|
-
const firstNavigation = lastTransitionTarget === undefined;
|
|
1246
|
-
intent = "navigate";
|
|
1247
|
-
// assign the target before flushing so effects that run for the
|
|
1248
|
-
// isRouting flip (e.g. pending link state) can read it
|
|
1249
|
-
lastTransitionTarget = newTarget;
|
|
1250
|
-
if (firstNavigation) {
|
|
1251
|
-
setIsRouting(true);
|
|
1252
|
-
flush();
|
|
1253
|
-
}
|
|
1254
|
-
if (lastTransitionTarget === newTarget) {
|
|
1255
|
-
setNavigateTarget({
|
|
1256
|
-
...lastTransitionTarget
|
|
1257
|
-
});
|
|
1258
|
-
queueMicrotask(() => {
|
|
1259
|
-
if (lastTransitionTarget !== newTarget) return;
|
|
1260
|
-
intent = undefined;
|
|
1261
|
-
navigateEnd(lastTransitionTarget);
|
|
1262
|
-
setNavigateTarget(undefined);
|
|
1263
|
-
setIsRouting(false);
|
|
1264
|
-
lastTransitionTarget = undefined;
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
780
|
+
});
|
|
1267
781
|
}
|
|
1268
782
|
}
|
|
1269
783
|
});
|
|
1270
784
|
}
|
|
1271
785
|
function navigatorFactory(route) {
|
|
1272
786
|
// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
|
|
1273
|
-
route = route ||
|
|
787
|
+
route = route || useContext(RouteContextObj) || baseRoute;
|
|
1274
788
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1275
789
|
}
|
|
1276
790
|
function navigateEnd(next) {
|
|
@@ -1286,11 +800,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1286
800
|
}
|
|
1287
801
|
function preloadRoute(url, preloadData) {
|
|
1288
802
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
1289
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1290
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
1291
|
-
// preload again so the real inner routes warm too.
|
|
1292
|
-
const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1293
|
-
boundary && resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
|
|
1294
803
|
const prevIntent = intent;
|
|
1295
804
|
intent = "preload";
|
|
1296
805
|
for (let match in matches) {
|
|
@@ -1319,29 +828,16 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1319
828
|
}
|
|
1320
829
|
intent = prevIntent;
|
|
1321
830
|
}
|
|
1322
|
-
|
|
1323
|
-
// Seeds the initial submission from a no-JS form post: the server
|
|
1324
|
-
// function handler redirected back with the outcome in a one-shot flash
|
|
1325
|
-
// cookie (see src/server.ts's handleNoJS), consumed eagerly above and
|
|
1326
|
-
// decoded here — so the post-redirect SSR renders useSubmission() state
|
|
1327
|
-
// exactly as a scripted submission would. An explicitly pre-seeded
|
|
1328
|
-
// `event.router.submission` (framework integrations) takes precedence.
|
|
1329
|
-
function initSubmissions() {
|
|
831
|
+
function initFromFlash() {
|
|
1330
832
|
const e = getRequestEvent();
|
|
1331
|
-
|
|
1332
|
-
if (!submission) return [];
|
|
1333
|
-
return [{
|
|
1334
|
-
...submission,
|
|
1335
|
-
clear() {},
|
|
1336
|
-
retry() {}
|
|
1337
|
-
}];
|
|
833
|
+
return e && e.router && e.router.submission ? [e.router.submission] : [];
|
|
1338
834
|
}
|
|
1339
835
|
}
|
|
1340
|
-
function createRouteContext(router, parent, outlet, match
|
|
836
|
+
function createRouteContext(router, parent, outlet, match) {
|
|
1341
837
|
const {
|
|
1342
838
|
base,
|
|
1343
839
|
location,
|
|
1344
|
-
|
|
840
|
+
params
|
|
1345
841
|
} = router;
|
|
1346
842
|
const {
|
|
1347
843
|
pattern,
|
|
@@ -1349,10 +845,6 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1349
845
|
preload
|
|
1350
846
|
} = match().route;
|
|
1351
847
|
const path = createMemo(() => match().path);
|
|
1352
|
-
// Params scoped to this route's lifetime. `matches` is expected to retain
|
|
1353
|
-
// its last valid value while this route is being torn down, so outgoing
|
|
1354
|
-
// components and preloads never observe another route's params.
|
|
1355
|
-
const params = wrapParams(() => mergeParams(matches()));
|
|
1356
848
|
component && component.preload && component.preload();
|
|
1357
849
|
inPreloadFn = true;
|
|
1358
850
|
const data = preload ? preload({
|
|
@@ -1364,7 +856,6 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1364
856
|
const route = {
|
|
1365
857
|
parent,
|
|
1366
858
|
pattern,
|
|
1367
|
-
params,
|
|
1368
859
|
path,
|
|
1369
860
|
outlet: () => component ? createComponent(component, {
|
|
1370
861
|
params,
|
|
@@ -1381,24 +872,63 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1381
872
|
return route;
|
|
1382
873
|
}
|
|
1383
874
|
|
|
875
|
+
const createRouterComponent = router => props => {
|
|
876
|
+
const {
|
|
877
|
+
base
|
|
878
|
+
} = props;
|
|
879
|
+
const routeDefs = children(() => props.children);
|
|
880
|
+
const branches = createMemo(() => createBranches(routeDefs(), props.base || ""));
|
|
881
|
+
let context;
|
|
882
|
+
const routerState = createRouterContext(router, branches, () => context, {
|
|
883
|
+
base,
|
|
884
|
+
singleFlight: props.singleFlight,
|
|
885
|
+
transformUrl: props.transformUrl
|
|
886
|
+
});
|
|
887
|
+
router.create && router.create(routerState);
|
|
888
|
+
return createComponent$1(RouterContextObj.Provider, {
|
|
889
|
+
value: routerState,
|
|
890
|
+
get children() {
|
|
891
|
+
return createComponent$1(Root, {
|
|
892
|
+
routerState: routerState,
|
|
893
|
+
get root() {
|
|
894
|
+
return props.root;
|
|
895
|
+
},
|
|
896
|
+
get preload() {
|
|
897
|
+
return props.rootPreload || props.rootLoad;
|
|
898
|
+
},
|
|
899
|
+
get children() {
|
|
900
|
+
return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
|
|
901
|
+
routerState: routerState,
|
|
902
|
+
get branches() {
|
|
903
|
+
return branches();
|
|
904
|
+
}
|
|
905
|
+
})];
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
};
|
|
1384
911
|
function Root(props) {
|
|
1385
912
|
const location = props.routerState.location;
|
|
1386
913
|
const params = props.routerState.params;
|
|
1387
914
|
const data = createMemo(() => props.preload && untrack(() => {
|
|
1388
915
|
setInPreloadFn(true);
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
} finally {
|
|
1396
|
-
setInPreloadFn(false);
|
|
1397
|
-
}
|
|
916
|
+
props.preload({
|
|
917
|
+
params,
|
|
918
|
+
location,
|
|
919
|
+
intent: getIntent() || "initial"
|
|
920
|
+
});
|
|
921
|
+
setInPreloadFn(false);
|
|
1398
922
|
}));
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
923
|
+
return createComponent$1(Show, {
|
|
924
|
+
get when() {
|
|
925
|
+
return props.root;
|
|
926
|
+
},
|
|
927
|
+
keyed: true,
|
|
928
|
+
get fallback() {
|
|
929
|
+
return props.children;
|
|
930
|
+
},
|
|
931
|
+
children: Root => createComponent$1(Root, {
|
|
1402
932
|
params: params,
|
|
1403
933
|
location: location,
|
|
1404
934
|
get data() {
|
|
@@ -1407,58 +937,38 @@ function Root(props) {
|
|
|
1407
937
|
get children() {
|
|
1408
938
|
return props.children;
|
|
1409
939
|
}
|
|
1410
|
-
})
|
|
1411
|
-
}
|
|
1412
|
-
return props.children;
|
|
940
|
+
})
|
|
941
|
+
});
|
|
1413
942
|
}
|
|
1414
943
|
function Routes(props) {
|
|
1415
944
|
if (isServer) {
|
|
1416
945
|
const e = getRequestEvent();
|
|
1417
946
|
if (e && e.router && e.router.dataOnly) {
|
|
1418
|
-
dataOnly(e, props.routerState, props.branches
|
|
947
|
+
dataOnly(e, props.routerState, props.branches);
|
|
1419
948
|
return;
|
|
1420
949
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}) => ({
|
|
1433
|
-
path: route.originalPath,
|
|
1434
|
-
pattern: route.pattern,
|
|
1435
|
-
match: path,
|
|
1436
|
-
params,
|
|
1437
|
-
info: route.info
|
|
1438
|
-
}))
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
950
|
+
e && ((e.router || (e.router = {})).matches || (e.router.matches = props.routerState.matches().map(({
|
|
951
|
+
route,
|
|
952
|
+
path,
|
|
953
|
+
params
|
|
954
|
+
}) => ({
|
|
955
|
+
path: route.originalPath,
|
|
956
|
+
pattern: route.pattern,
|
|
957
|
+
match: path,
|
|
958
|
+
params,
|
|
959
|
+
info: route.info
|
|
960
|
+
}))));
|
|
1441
961
|
}
|
|
1442
962
|
const disposers = [];
|
|
1443
963
|
let root;
|
|
1444
|
-
let prevMatches;
|
|
1445
964
|
// dispose the detached per-route roots when this component unmounts, otherwise
|
|
1446
965
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
1447
966
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
// computation (which disposes its children every time it re-runs).
|
|
1451
|
-
const owner = getOwner();
|
|
1452
|
-
const routeStates = createMemo(prev => {
|
|
1453
|
-
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
1454
|
-
// computation parks with it — no route contexts are created against
|
|
1455
|
-
// placeholder matches.
|
|
1456
|
-
const nextMatches = props.routerState.matches();
|
|
1457
|
-
const previousMatches = prevMatches;
|
|
1458
|
-
let equal = previousMatches && nextMatches.length === previousMatches.length;
|
|
967
|
+
const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => {
|
|
968
|
+
let equal = prevMatches && nextMatches.length === prevMatches.length;
|
|
1459
969
|
const next = [];
|
|
1460
970
|
for (let i = 0, len = nextMatches.length; i < len; i++) {
|
|
1461
|
-
const prevMatch =
|
|
971
|
+
const prevMatch = prevMatches && prevMatches[i];
|
|
1462
972
|
const nextMatch = nextMatches[i];
|
|
1463
973
|
if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {
|
|
1464
974
|
next[i] = prev[i];
|
|
@@ -1467,46 +977,45 @@ function Routes(props) {
|
|
|
1467
977
|
if (disposers[i]) {
|
|
1468
978
|
disposers[i]();
|
|
1469
979
|
}
|
|
1470
|
-
|
|
980
|
+
createRoot(dispose => {
|
|
1471
981
|
disposers[i] = dispose;
|
|
1472
|
-
|
|
1473
|
-
// Retain the last matches in which this route participated so
|
|
1474
|
-
// that its components and preloads never observe another
|
|
1475
|
-
// route's params/path while this route is being torn down.
|
|
1476
|
-
const matchesAtLevel = createMemo(prev => {
|
|
982
|
+
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => {
|
|
1477
983
|
const routeMatches = props.routerState.matches();
|
|
1478
|
-
|
|
1479
|
-
return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
|
|
984
|
+
return routeMatches[i] ?? routeMatches[0];
|
|
1480
985
|
});
|
|
1481
|
-
|
|
1482
|
-
}));
|
|
986
|
+
});
|
|
1483
987
|
}
|
|
1484
988
|
}
|
|
1485
989
|
disposers.splice(nextMatches.length).forEach(dispose => dispose());
|
|
1486
990
|
if (prev && equal) {
|
|
1487
|
-
prevMatches = nextMatches;
|
|
1488
991
|
return prev;
|
|
1489
992
|
}
|
|
1490
993
|
root = next[0];
|
|
1491
|
-
prevMatches = nextMatches;
|
|
1492
994
|
return next;
|
|
1493
|
-
});
|
|
1494
|
-
|
|
1495
|
-
return memo(outlet);
|
|
995
|
+
}));
|
|
996
|
+
return createOutlet(() => routeStates() && root)();
|
|
1496
997
|
}
|
|
1497
998
|
const createOutlet = child => {
|
|
1498
|
-
return () => {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
999
|
+
return () => createComponent$1(Show, {
|
|
1000
|
+
get when() {
|
|
1001
|
+
return child();
|
|
1002
|
+
},
|
|
1003
|
+
keyed: true,
|
|
1004
|
+
children: child => createComponent$1(RouteContextObj.Provider, {
|
|
1005
|
+
value: child,
|
|
1006
|
+
get children() {
|
|
1007
|
+
return child.outlet();
|
|
1008
|
+
}
|
|
1009
|
+
})
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
const Route = props => {
|
|
1013
|
+
const childRoutes = children(() => props.children);
|
|
1014
|
+
return mergeProps(props, {
|
|
1015
|
+
get children() {
|
|
1016
|
+
return childRoutes();
|
|
1507
1017
|
}
|
|
1508
|
-
|
|
1509
|
-
};
|
|
1018
|
+
});
|
|
1510
1019
|
};
|
|
1511
1020
|
|
|
1512
1021
|
// for data only mode with single flight mutations
|
|
@@ -1514,10 +1023,6 @@ function dataOnly(event, routerState, branches) {
|
|
|
1514
1023
|
const url = new URL(event.request.url);
|
|
1515
1024
|
const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
|
|
1516
1025
|
const matches = getRouteMatches(branches, url.pathname);
|
|
1517
|
-
// This pass is synchronous — an unresolved lazy subtree can only be
|
|
1518
|
-
// kicked, not awaited (the flight collector's own runner awaits; see
|
|
1519
|
-
// src/server.ts). Best effort so a subsequent pass sees the table.
|
|
1520
|
-
unresolvedLazyMatches([...prevMatches, ...matches]).forEach(resolveLazySubtree);
|
|
1521
1026
|
for (let match = 0; match < matches.length; match++) {
|
|
1522
1027
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true;
|
|
1523
1028
|
const {
|
|
@@ -1532,50 +1037,36 @@ function dataOnly(event, routerState, branches) {
|
|
|
1532
1037
|
}
|
|
1533
1038
|
}
|
|
1534
1039
|
|
|
1040
|
+
function intercept([value, setValue], get, set) {
|
|
1041
|
+
return [value, set ? v => setValue(set(v)) : setValue];
|
|
1042
|
+
}
|
|
1043
|
+
function createRouter(config) {
|
|
1044
|
+
let ignore = false;
|
|
1045
|
+
const wrap = value => typeof value === "string" ? {
|
|
1046
|
+
value
|
|
1047
|
+
} : value;
|
|
1048
|
+
const signal = intercept(createSignal(wrap(config.get()), {
|
|
1049
|
+
equals: (a, b) => a.value === b.value && a.state === b.state
|
|
1050
|
+
}), undefined, next => {
|
|
1051
|
+
!ignore && config.set(next);
|
|
1052
|
+
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
1053
|
+
return next;
|
|
1054
|
+
});
|
|
1055
|
+
config.init && onCleanup(config.init((value = config.get()) => {
|
|
1056
|
+
ignore = true;
|
|
1057
|
+
signal[1](wrap(value));
|
|
1058
|
+
ignore = false;
|
|
1059
|
+
}));
|
|
1060
|
+
return createRouterComponent({
|
|
1061
|
+
signal,
|
|
1062
|
+
create: config.create,
|
|
1063
|
+
utils: config.utils
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1535
1066
|
function bindEvent(target, type, handler) {
|
|
1536
1067
|
target.addEventListener(type, handler);
|
|
1537
1068
|
return () => target.removeEventListener(type, handler);
|
|
1538
1069
|
}
|
|
1539
|
-
|
|
1540
|
-
// Depth stamping supports blocking browser-initiated navigation (back/forward)
|
|
1541
|
-
// for `useBeforeLeave`. It stays always-on — a couple of history.state writes —
|
|
1542
|
-
// so blocking stays exact no matter when the first guard subscribes, while the
|
|
1543
|
-
// guard machinery itself lives behind the lazy `beforeLeave` slot.
|
|
1544
|
-
|
|
1545
|
-
let depth;
|
|
1546
|
-
function saveCurrentDepth() {
|
|
1547
|
-
if (!window.history.state || window.history.state._depth == null) {
|
|
1548
|
-
window.history.replaceState({
|
|
1549
|
-
...window.history.state,
|
|
1550
|
-
_depth: window.history.length - 1
|
|
1551
|
-
}, "");
|
|
1552
|
-
}
|
|
1553
|
-
depth = window.history.state._depth;
|
|
1554
|
-
}
|
|
1555
|
-
function keepDepth(state) {
|
|
1556
|
-
return {
|
|
1557
|
-
...state,
|
|
1558
|
-
_depth: window.history.state && window.history.state._depth
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
function notifyIfNotBlocked(notify, block) {
|
|
1562
|
-
let ignore = false;
|
|
1563
|
-
return () => {
|
|
1564
|
-
const prevDepth = depth;
|
|
1565
|
-
saveCurrentDepth();
|
|
1566
|
-
const delta = prevDepth == null ? null : depth - prevDepth;
|
|
1567
|
-
if (ignore) {
|
|
1568
|
-
ignore = false;
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
if (delta && block(delta)) {
|
|
1572
|
-
ignore = true;
|
|
1573
|
-
window.history.go(-delta);
|
|
1574
|
-
} else {
|
|
1575
|
-
notify();
|
|
1576
|
-
}
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
1070
|
function scrollToHash(hash, fallbackTop) {
|
|
1580
1071
|
const el = hash && document.getElementById(hash);
|
|
1581
1072
|
if (el) {
|
|
@@ -1585,428 +1076,18 @@ function scrollToHash(hash, fallbackTop) {
|
|
|
1585
1076
|
}
|
|
1586
1077
|
}
|
|
1587
1078
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const getSource = () => {
|
|
1597
|
-
const url = window.location.pathname + window.location.search;
|
|
1598
|
-
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
1599
|
-
return {
|
|
1600
|
-
value: url + window.location.hash,
|
|
1601
|
-
state
|
|
1602
|
-
};
|
|
1603
|
-
};
|
|
1604
|
-
const beforeLeave = {};
|
|
1605
|
-
if (!isServer) saveCurrentDepth();
|
|
1606
|
-
return {
|
|
1607
|
-
get: getSource,
|
|
1608
|
-
set({
|
|
1609
|
-
value,
|
|
1610
|
-
replace,
|
|
1611
|
-
scroll,
|
|
1612
|
-
state
|
|
1613
|
-
}) {
|
|
1614
|
-
if (replace) {
|
|
1615
|
-
window.history.replaceState(keepDepth(state), "", value);
|
|
1616
|
-
} else {
|
|
1617
|
-
window.history.pushState(state, "", value);
|
|
1618
|
-
}
|
|
1619
|
-
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
1620
|
-
saveCurrentDepth();
|
|
1621
|
-
},
|
|
1622
|
-
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
1623
|
-
const guard = beforeLeave.current;
|
|
1624
|
-
if (!guard) return false;
|
|
1625
|
-
if (delta) {
|
|
1626
|
-
return !guard.confirm(delta);
|
|
1627
|
-
} else {
|
|
1628
|
-
const s = getSource();
|
|
1629
|
-
return !guard.confirm(s.value, {
|
|
1630
|
-
state: s.state
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
})),
|
|
1634
|
-
utils: {
|
|
1635
|
-
go: delta => window.history.go(delta),
|
|
1636
|
-
beforeLeave
|
|
1637
|
-
}
|
|
1079
|
+
function getPath(url) {
|
|
1080
|
+
const u = new URL(url);
|
|
1081
|
+
return u.pathname + u.search;
|
|
1082
|
+
}
|
|
1083
|
+
function StaticRouter(props) {
|
|
1084
|
+
let e;
|
|
1085
|
+
const obj = {
|
|
1086
|
+
value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || ""
|
|
1638
1087
|
};
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
// Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
|
|
1643
|
-
// `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
|
|
1644
|
-
// to the current path so we can handle these in-page anchors correctly.
|
|
1645
|
-
if (!to.startsWith("/")) {
|
|
1646
|
-
const [, path = "/"] = window.location.hash.split("#", 2);
|
|
1647
|
-
return `${path}#${to}`;
|
|
1648
|
-
}
|
|
1649
|
-
return to;
|
|
1650
|
-
}
|
|
1651
|
-
function hashHistory() {
|
|
1652
|
-
const getSource = () => window.location.hash.slice(1);
|
|
1653
|
-
const beforeLeave = {};
|
|
1654
|
-
if (!isServer) saveCurrentDepth();
|
|
1655
|
-
return {
|
|
1656
|
-
get: getSource,
|
|
1657
|
-
set({
|
|
1658
|
-
value,
|
|
1659
|
-
replace,
|
|
1660
|
-
scroll,
|
|
1661
|
-
state
|
|
1662
|
-
}) {
|
|
1663
|
-
if (replace) {
|
|
1664
|
-
window.history.replaceState(keepDepth(state), "", "#" + value);
|
|
1665
|
-
} else {
|
|
1666
|
-
window.history.pushState(state, "", "#" + value);
|
|
1667
|
-
}
|
|
1668
|
-
const hashIndex = value.indexOf("#");
|
|
1669
|
-
const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
|
|
1670
|
-
scrollToHash(hash, scroll);
|
|
1671
|
-
saveCurrentDepth();
|
|
1672
|
-
},
|
|
1673
|
-
init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => {
|
|
1674
|
-
const guard = beforeLeave.current;
|
|
1675
|
-
return !!guard && !guard.confirm(delta && delta < 0 ? delta : getSource());
|
|
1676
|
-
})),
|
|
1677
|
-
utils: {
|
|
1678
|
-
go: delta => window.history.go(delta),
|
|
1679
|
-
renderPath: path => `#${path}`,
|
|
1680
|
-
parsePath: hashParser,
|
|
1681
|
-
beforeLeave
|
|
1682
|
-
}
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
function memoryHistory(initial = "/") {
|
|
1686
|
-
const entries = [initial];
|
|
1687
|
-
let index = 0;
|
|
1688
|
-
const listeners = [];
|
|
1689
|
-
const go = n => {
|
|
1690
|
-
// https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245
|
|
1691
|
-
index = Math.max(0, Math.min(index + n, entries.length - 1));
|
|
1692
|
-
const value = entries[index];
|
|
1693
|
-
listeners.forEach(listener => listener(value));
|
|
1694
|
-
};
|
|
1695
|
-
const listen = listener => {
|
|
1696
|
-
listeners.push(listener);
|
|
1697
|
-
return () => {
|
|
1698
|
-
const i = listeners.indexOf(listener);
|
|
1699
|
-
listeners.splice(i, 1);
|
|
1700
|
-
};
|
|
1701
|
-
};
|
|
1702
|
-
return {
|
|
1703
|
-
get: () => entries[index],
|
|
1704
|
-
set: ({
|
|
1705
|
-
value,
|
|
1706
|
-
scroll,
|
|
1707
|
-
replace
|
|
1708
|
-
}) => {
|
|
1709
|
-
if (replace) {
|
|
1710
|
-
entries[index] = value;
|
|
1711
|
-
} else {
|
|
1712
|
-
entries.splice(index + 1, entries.length - index, value);
|
|
1713
|
-
index++;
|
|
1714
|
-
}
|
|
1715
|
-
listeners.forEach(listener => listener(value));
|
|
1716
|
-
setTimeout(() => {
|
|
1717
|
-
if (scroll) {
|
|
1718
|
-
scrollToHash(value.split("#")[1] || "", true);
|
|
1719
|
-
}
|
|
1720
|
-
}, 0);
|
|
1721
|
-
},
|
|
1722
|
-
back: () => go(-1),
|
|
1723
|
-
forward: () => go(1),
|
|
1724
|
-
go,
|
|
1725
|
-
listen,
|
|
1726
|
-
init: listen,
|
|
1727
|
-
utils: {
|
|
1728
|
-
go
|
|
1729
|
-
}
|
|
1730
|
-
};
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
/**
|
|
1734
|
-
* Identity helper that preserves literal types when the route tree is
|
|
1735
|
-
* declared as a separate variable. `createRouter` infers literally from
|
|
1736
|
-
* inline arrays, but an extracted `const routes = [...]` widens paths to
|
|
1737
|
-
* `string` and silently degrades `paths` and the typed hooks unless it is
|
|
1738
|
-
* declared `as const` — `defineRoutes` makes that impossible to forget.
|
|
1739
|
-
*/
|
|
1740
|
-
function defineRoutes(routes) {
|
|
1741
|
-
return routes;
|
|
1742
|
-
}
|
|
1743
|
-
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
1744
|
-
function createIntegration(history) {
|
|
1745
|
-
let ignore = false;
|
|
1746
|
-
const wrap = value => typeof value === "string" ? {
|
|
1747
|
-
value
|
|
1748
|
-
} : value;
|
|
1749
|
-
const [read, write] = createSignal(wrap(history.get()), {
|
|
1750
|
-
equals: (a, b) => a.value === b.value && a.state === b.state,
|
|
1751
|
-
ownedWrite: true
|
|
1752
|
-
});
|
|
1753
|
-
const signal = [read, next => {
|
|
1754
|
-
!ignore && history.set(next);
|
|
1755
|
-
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
1756
|
-
write(next);
|
|
1757
|
-
}];
|
|
1758
|
-
history.init && onCleanup(history.init((value = history.get()) => {
|
|
1759
|
-
ignore = true;
|
|
1760
|
-
signal[1](wrap(value));
|
|
1761
|
-
ignore = false;
|
|
1762
|
-
}));
|
|
1763
|
-
return {
|
|
1764
|
-
signal,
|
|
1765
|
-
utils: history.utils
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
/**
|
|
1770
|
-
* Server default: a static view of the request URL — no signal machinery, a
|
|
1771
|
-
* server render never navigates. Without a request event (SSG scripts,
|
|
1772
|
-
* server-side tests) the configured history adapter provides the location,
|
|
1773
|
-
* so e.g. `memoryHistory("/page")` works isomorphically.
|
|
1774
|
-
*/
|
|
1775
|
-
function staticIntegration(history) {
|
|
1776
|
-
const e = getRequestEvent();
|
|
1777
|
-
let value = "";
|
|
1778
|
-
if (e) {
|
|
1779
|
-
const url = new URL(e.request.url);
|
|
1780
|
-
value = url.pathname + url.search;
|
|
1781
|
-
} else if (history) {
|
|
1782
|
-
value = history.get();
|
|
1783
|
-
}
|
|
1784
|
-
const obj = typeof value === "string" ? {
|
|
1785
|
-
value
|
|
1786
|
-
} : {
|
|
1787
|
-
...value
|
|
1788
|
-
};
|
|
1789
|
-
return {
|
|
1790
|
-
signal: [() => obj, next => Object.assign(obj, next)],
|
|
1791
|
-
utils: history && history.utils
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
function createRouter(config) {
|
|
1795
|
-
const basePath = config.base || "";
|
|
1796
|
-
// Routes are immutable per instance, so compilation is shared by every
|
|
1797
|
-
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
1798
|
-
// resolves (append-only: resolution, not mutation). Reading the version
|
|
1799
|
-
// inside a computation subscribes it; plain calls just see current state.
|
|
1800
|
-
let compiled;
|
|
1801
|
-
let compiledVersion = -1;
|
|
1802
|
-
const branches = () => {
|
|
1803
|
-
const version = trackLazySubtrees();
|
|
1804
|
-
if (!compiled || compiledVersion !== version) {
|
|
1805
|
-
compiled = createBranches(config.routes, basePath);
|
|
1806
|
-
compiledVersion = version;
|
|
1807
|
-
}
|
|
1808
|
-
return compiled;
|
|
1809
|
-
};
|
|
1810
|
-
const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
|
|
1811
|
-
function RouterComponent(props) {
|
|
1812
|
-
// One router per app: the session (location, history, delegation, link
|
|
1813
|
-
// claims, preloading) has a single owner, and a second instance would
|
|
1814
|
-
// fight it — stale content on click navigations, conflicting link
|
|
1815
|
-
// attributes. Compose route trees instead; lazy subtrees are the planned
|
|
1816
|
-
// answer for definitions unknown at build time.
|
|
1817
|
-
if (useOptionalContext(RouterContextObj)) {
|
|
1818
|
-
console.warn("Mounting a router inside another router is not supported. " + "Compose route trees in one createRouter config instead.");
|
|
1819
|
-
}
|
|
1820
|
-
const root = untrack(() => props.children);
|
|
1821
|
-
const integration = isServer ? staticIntegration(config.history) : createIntegration(config.history || browserHistory());
|
|
1822
|
-
let context;
|
|
1823
|
-
const routerState = createRouterContext(integration, branches, () => context, {
|
|
1824
|
-
base: basePath,
|
|
1825
|
-
singleFlight: config.singleFlight,
|
|
1826
|
-
transformUrl: config.transformUrl
|
|
1827
|
-
});
|
|
1828
|
-
if (!isServer) {
|
|
1829
|
-
setupNativeEvents({
|
|
1830
|
-
preload: config.preloadLinks,
|
|
1831
|
-
explicitLinks: config.explicitLinks,
|
|
1832
|
-
actionBase: config.actionBase,
|
|
1833
|
-
transformUrl: config.transformUrl
|
|
1834
|
-
})(routerState);
|
|
1835
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
1836
|
-
if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
|
|
1837
|
-
}
|
|
1838
|
-
return createComponent$1(RouterContextObj, {
|
|
1839
|
-
value: routerState,
|
|
1840
|
-
get children() {
|
|
1841
|
-
return createComponent$1(Root, {
|
|
1842
|
-
routerState: routerState,
|
|
1843
|
-
root: root,
|
|
1844
|
-
get preload() {
|
|
1845
|
-
return config.preload;
|
|
1846
|
-
},
|
|
1847
|
-
get children() {
|
|
1848
|
-
return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
|
|
1849
|
-
routerState: routerState,
|
|
1850
|
-
branches: branches
|
|
1851
|
-
})];
|
|
1852
|
-
}
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
});
|
|
1856
|
-
}
|
|
1857
|
-
const instance = Object.assign(RouterComponent, {
|
|
1858
|
-
routes: config.routes,
|
|
1859
|
-
config,
|
|
1860
|
-
match(url) {
|
|
1861
|
-
const u = new URL(url, mockBase);
|
|
1862
|
-
const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
|
|
1863
|
-
return getRouteMatches(branches(), pathname).map(({
|
|
1864
|
-
route,
|
|
1865
|
-
path,
|
|
1866
|
-
params
|
|
1867
|
-
}) => ({
|
|
1868
|
-
path: route.originalPath,
|
|
1869
|
-
pattern: route.pattern,
|
|
1870
|
-
match: path,
|
|
1871
|
-
params,
|
|
1872
|
-
info: route.info
|
|
1873
|
-
}));
|
|
1874
|
-
}
|
|
1875
|
-
});
|
|
1876
|
-
// Built on first access (a getter via Object.assign would run during the
|
|
1877
|
-
// copy) so runtimes without Proxy — some older TVs — can still route as
|
|
1878
|
-
// long as they never touch typed paths.
|
|
1879
|
-
let paths;
|
|
1880
|
-
Object.defineProperty(instance, "paths", {
|
|
1881
|
-
get: () => paths || (paths = createPathsProxy(renderPath, basePath))
|
|
1882
|
-
});
|
|
1883
|
-
return instance;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
function createBeforeLeave() {
|
|
1887
|
-
let listeners = new Set();
|
|
1888
|
-
function subscribe(listener) {
|
|
1889
|
-
listeners.add(listener);
|
|
1890
|
-
return () => listeners.delete(listener);
|
|
1891
|
-
}
|
|
1892
|
-
let ignore = false;
|
|
1893
|
-
function confirm(to, options) {
|
|
1894
|
-
if (ignore) return !(ignore = false);
|
|
1895
|
-
const e = {
|
|
1896
|
-
to,
|
|
1897
|
-
options,
|
|
1898
|
-
defaultPrevented: false,
|
|
1899
|
-
preventDefault: () => e.defaultPrevented = true
|
|
1900
|
-
};
|
|
1901
|
-
for (const l of listeners) l.listener({
|
|
1902
|
-
to,
|
|
1903
|
-
options,
|
|
1904
|
-
// delegate to the shared event so later listeners' preventDefault
|
|
1905
|
-
// calls are observable from earlier listeners
|
|
1906
|
-
get defaultPrevented() {
|
|
1907
|
-
return e.defaultPrevented;
|
|
1908
|
-
},
|
|
1909
|
-
preventDefault: e.preventDefault,
|
|
1910
|
-
from: l.location,
|
|
1911
|
-
retry: force => {
|
|
1912
|
-
force && (ignore = true);
|
|
1913
|
-
l.navigate(to, {
|
|
1914
|
-
...options,
|
|
1915
|
-
resolve: false
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
});
|
|
1919
|
-
return !e.defaultPrevented;
|
|
1920
|
-
}
|
|
1921
|
-
return {
|
|
1922
|
-
subscribe,
|
|
1923
|
-
confirm
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
/**
|
|
1928
|
-
* useBeforeLeave takes a function that will be called prior to leaving a route.
|
|
1929
|
-
* The function will be called with:
|
|
1930
|
-
*
|
|
1931
|
-
* - from (*Location*): current location (before change).
|
|
1932
|
-
* - to (*string | number*): path passed to `navigate`.
|
|
1933
|
-
* - options (*NavigateOptions*): options passed to navigate.
|
|
1934
|
-
* - preventDefault (*function*): call to block the route change.
|
|
1935
|
-
* - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
|
|
1936
|
-
* - 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).
|
|
1937
|
-
*
|
|
1938
|
-
* @example
|
|
1939
|
-
* ```js
|
|
1940
|
-
* useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
1941
|
-
* if (form.isDirty && !e.defaultPrevented) {
|
|
1942
|
-
* // preventDefault to block immediately and prompt user async
|
|
1943
|
-
* e.preventDefault();
|
|
1944
|
-
* setTimeout(() => {
|
|
1945
|
-
* if (window.confirm("Discard unsaved changes - are you sure?")) {
|
|
1946
|
-
* // user wants to proceed anyway so retry with force=true
|
|
1947
|
-
* e.retry(true);
|
|
1948
|
-
* }
|
|
1949
|
-
* }, 100);
|
|
1950
|
-
* }
|
|
1951
|
-
* });
|
|
1952
|
-
* ```
|
|
1953
|
-
*/
|
|
1954
|
-
const useBeforeLeave = listener => {
|
|
1955
|
-
// Installing the guard here (instead of eagerly in the router/history) keeps
|
|
1956
|
-
// all of createBeforeLeave tree-shakeable for apps that never block leaves.
|
|
1957
|
-
const slot = useRouter().beforeLeave;
|
|
1958
|
-
const s = (slot.current || (slot.current = createBeforeLeave())).subscribe({
|
|
1959
|
-
listener,
|
|
1960
|
-
location: useLocation(),
|
|
1961
|
-
navigate: useNavigate()
|
|
1962
|
-
});
|
|
1963
|
-
onCleanup(s);
|
|
1964
|
-
};
|
|
1965
|
-
|
|
1966
|
-
// The no-JS form convention's cookie codec. When a form posts to a server
|
|
1967
|
-
// function without the client runtime (no instance header), the server
|
|
1968
|
-
// handler redirects back carrying the outcome in a one-shot "flash" cookie;
|
|
1969
|
-
// the next SSR pass reads it and seeds the router's submission state so
|
|
1970
|
-
// useSubmission() renders the result exactly as a scripted submission would.
|
|
1971
|
-
// Both codec halves live in this module so the write (src/server.ts, the
|
|
1972
|
-
// handler's handleNoJS) and the read (provided to the router core by
|
|
1973
|
-
// data/action.ts) can never drift apart. The cookie's name/clearing live in
|
|
1974
|
-
// flashCookie.ts — the only piece the router core itself consumes — so this
|
|
1975
|
-
// codec stays out of bundles that never load the action layer.
|
|
1976
|
-
|
|
1977
|
-
function decodeInputValue(value) {
|
|
1978
|
-
if (value && typeof value === "object") {
|
|
1979
|
-
if (Array.isArray(value.$f)) {
|
|
1980
|
-
const form = new FormData();
|
|
1981
|
-
for (const [k, v] of value.$f) form.append(k, v);
|
|
1982
|
-
return form;
|
|
1983
|
-
}
|
|
1984
|
-
if (Array.isArray(value.$u)) return new URLSearchParams(value.$u);
|
|
1985
|
-
}
|
|
1986
|
-
return value;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
/**
|
|
1990
|
-
* Decodes the flash cookie out of a request's Cookie header. Returns
|
|
1991
|
-
* undefined when absent or unreadable (a malformed cookie must never take
|
|
1992
|
-
* down SSR — it is cleared either way).
|
|
1993
|
-
*/
|
|
1994
|
-
function decodeFlashCookie(cookieHeader) {
|
|
1995
|
-
const match = matchFlashCookie(cookieHeader);
|
|
1996
|
-
if (!match) return;
|
|
1997
|
-
try {
|
|
1998
|
-
const payload = JSON.parse(decodeURIComponent(match));
|
|
1999
|
-
if (!payload || !payload.result) return;
|
|
2000
|
-
const result = payload.error ? new Error(payload.result) : payload.result;
|
|
2001
|
-
return {
|
|
2002
|
-
input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
|
|
2003
|
-
url: payload.url,
|
|
2004
|
-
result: payload.thrown ? undefined : result,
|
|
2005
|
-
error: payload.thrown ? result : undefined
|
|
2006
|
-
};
|
|
2007
|
-
} catch (error) {
|
|
2008
|
-
console.error(error);
|
|
2009
|
-
}
|
|
1088
|
+
return createRouterComponent({
|
|
1089
|
+
signal: [() => obj, next => Object.assign(obj, next)]
|
|
1090
|
+
})(props);
|
|
2010
1091
|
}
|
|
2011
1092
|
|
|
2012
1093
|
const LocationHeader = "Location";
|
|
@@ -2037,9 +1118,11 @@ function getCache() {
|
|
|
2037
1118
|
*/
|
|
2038
1119
|
function revalidate(key, force = true) {
|
|
2039
1120
|
const now = Date.now();
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
1121
|
+
// force the cache miss synchronously so a `refetch` in the same tick sees it —
|
|
1122
|
+
// startTransition defers its callback to a microtask (#497)
|
|
1123
|
+
force && cacheKeyOp(key, entry => entry[0] = 0);
|
|
1124
|
+
return startTransition(() => {
|
|
1125
|
+
cacheKeyOp(key, entry => entry[4][1](now)); // retrigger live signals
|
|
2043
1126
|
});
|
|
2044
1127
|
}
|
|
2045
1128
|
function cacheKeyOp(key, fn) {
|
|
@@ -2049,16 +1132,8 @@ function cacheKeyOp(key, fn) {
|
|
|
2049
1132
|
}
|
|
2050
1133
|
}
|
|
2051
1134
|
function query(fn, name) {
|
|
2052
|
-
//
|
|
2053
|
-
|
|
2054
|
-
// at query-creation (module scope) — the server half records the method
|
|
2055
|
-
// declaration for dispatch, the client half swaps in the GET transport.
|
|
2056
|
-
// An explicit `GET(fn)` already carries the declaration on the metadata
|
|
2057
|
-
// channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
|
|
2058
|
-
// through; non-server functions are untouched.
|
|
2059
|
-
if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
|
|
2060
|
-
fn = GET(fn);
|
|
2061
|
-
}
|
|
1135
|
+
// prioritize GET for server functions
|
|
1136
|
+
if (fn.GET) fn = fn.GET;
|
|
2062
1137
|
const cachedFn = (...args) => {
|
|
2063
1138
|
const cache = getCache();
|
|
2064
1139
|
const intent = getIntent();
|
|
@@ -2083,7 +1158,7 @@ function query(fn, name) {
|
|
|
2083
1158
|
}
|
|
2084
1159
|
}
|
|
2085
1160
|
}
|
|
2086
|
-
if (
|
|
1161
|
+
if (getListener() && !isServer) {
|
|
2087
1162
|
tracking = true;
|
|
2088
1163
|
onCleanup(() => cached[4].count--);
|
|
2089
1164
|
}
|
|
@@ -2098,7 +1173,7 @@ function query(fn, name) {
|
|
|
2098
1173
|
let res = cached[1];
|
|
2099
1174
|
if (intent !== "preload") {
|
|
2100
1175
|
res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
|
|
2101
|
-
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
1176
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
2102
1177
|
}
|
|
2103
1178
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2104
1179
|
return res;
|
|
@@ -2113,11 +1188,9 @@ function query(fn, name) {
|
|
|
2113
1188
|
cached[0] = now;
|
|
2114
1189
|
cached[1] = res;
|
|
2115
1190
|
cached[3] = intent;
|
|
2116
|
-
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
1191
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
2117
1192
|
} else {
|
|
2118
|
-
cache.set(key, cached = [now, res,, intent, createSignal(now
|
|
2119
|
-
ownedWrite: true
|
|
2120
|
-
})]);
|
|
1193
|
+
cache.set(key, cached = [now, res,, intent, createSignal(now)]);
|
|
2121
1194
|
cached[4].count = 0;
|
|
2122
1195
|
}
|
|
2123
1196
|
if (tracking) {
|
|
@@ -2140,14 +1213,6 @@ function query(fn, name) {
|
|
|
2140
1213
|
return res;
|
|
2141
1214
|
function handleResponse(error) {
|
|
2142
1215
|
return async v => {
|
|
2143
|
-
let enveloped;
|
|
2144
|
-
let hasEnveloped = false;
|
|
2145
|
-
if (isResponseEnvelope(v)) {
|
|
2146
|
-
// respond(): the value rides in memory beside the metadata
|
|
2147
|
-
enveloped = v.value;
|
|
2148
|
-
hasEnveloped = true;
|
|
2149
|
-
v = v.response;
|
|
2150
|
-
}
|
|
2151
1216
|
if (v instanceof Response) {
|
|
2152
1217
|
const e = getRequestEvent();
|
|
2153
1218
|
if (e) {
|
|
@@ -2158,17 +1223,14 @@ function query(fn, name) {
|
|
|
2158
1223
|
const url = v.headers.get(LocationHeader);
|
|
2159
1224
|
if (url !== null) {
|
|
2160
1225
|
// client + server relative redirect
|
|
2161
|
-
if (navigate && url.startsWith("/"))
|
|
2162
|
-
|
|
1226
|
+
if (navigate && url.startsWith("/")) startTransition(() => {
|
|
1227
|
+
navigate(url, {
|
|
1228
|
+
replace: true
|
|
1229
|
+
});
|
|
2163
1230
|
});else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;
|
|
2164
1231
|
return;
|
|
2165
1232
|
}
|
|
2166
|
-
if (
|
|
2167
|
-
// responses the transport hands over whole (revalidation) carry a
|
|
2168
|
-
// codec-encoded body; anything else (a raw user Response) stays whole
|
|
2169
|
-
const decoded = await decodeResponse(v);
|
|
2170
|
-
if (decoded !== undefined) v = decoded;
|
|
2171
|
-
}
|
|
1233
|
+
if (v.customBody) v = await v.customBody();
|
|
2172
1234
|
}
|
|
2173
1235
|
if (error) throw v;
|
|
2174
1236
|
cached[2] = v;
|
|
@@ -2194,14 +1256,15 @@ query.set = (key, value) => {
|
|
|
2194
1256
|
cached[2] = value;
|
|
2195
1257
|
cached[3] = "preload";
|
|
2196
1258
|
} else {
|
|
2197
|
-
cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now
|
|
2198
|
-
ownedWrite: true
|
|
2199
|
-
})]);
|
|
1259
|
+
cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]);
|
|
2200
1260
|
cached[4].count = 0;
|
|
2201
1261
|
}
|
|
2202
1262
|
};
|
|
2203
1263
|
query.delete = key => getCache().delete(key);
|
|
2204
1264
|
query.clear = () => getCache().clear();
|
|
1265
|
+
|
|
1266
|
+
/** @deprecated use query instead */
|
|
1267
|
+
const cache = query;
|
|
2205
1268
|
function matchKey(key, keys) {
|
|
2206
1269
|
for (let k of keys) {
|
|
2207
1270
|
if (k && key.startsWith(k)) return true;
|
|
@@ -2222,122 +1285,14 @@ function isPlainObject(obj) {
|
|
|
2222
1285
|
return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype);
|
|
2223
1286
|
}
|
|
2224
1287
|
|
|
2225
|
-
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
2226
|
-
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
2227
|
-
const invokeSymbol = Symbol("routerActionInvoke");
|
|
2228
|
-
|
|
2229
|
-
// Forms submitted through delegation are marked `aria-busy` while their
|
|
2230
|
-
// action is in flight — the form half of the attribute vocabulary links get
|
|
2231
|
-
// (`data-active`/`data-pending`). Style with `form[aria-busy] button { ... }`.
|
|
2232
|
-
// A counter (not a boolean) keeps the attribute through overlapping
|
|
2233
|
-
// submissions from the same form.
|
|
2234
|
-
const busyForms = /* #__PURE__ */new WeakMap();
|
|
2235
|
-
function setFormBusy(form, delta) {
|
|
2236
|
-
const count = (busyForms.get(form) || 0) + delta;
|
|
2237
|
-
busyForms.set(form, count);
|
|
2238
|
-
count > 0 ? form.setAttribute("aria-busy", "true") : form.removeAttribute("aria-busy");
|
|
2239
|
-
}
|
|
2240
1288
|
const actions = /* #__PURE__ */new Map();
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* The document-delegation submit handler for router actions. Lives here —
|
|
2244
|
-
* not in events.ts — so the router's event wiring holds no static reference
|
|
2245
|
-
* to the action module; `installRouterIntegrations` slots it in when the
|
|
2246
|
-
* first action is created on the client.
|
|
2247
|
-
*/
|
|
2248
|
-
function handleFormAction(evt, router, actionBase) {
|
|
2249
|
-
if (evt.defaultPrevented) return;
|
|
2250
|
-
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : evt.target.getAttribute("action");
|
|
2251
|
-
if (!actionRef) return;
|
|
2252
|
-
const serverAction = !actionRef.startsWith("https://action/");
|
|
2253
|
-
if (serverAction) {
|
|
2254
|
-
// normalize server actions
|
|
2255
|
-
const url = new URL(actionRef, mockBase);
|
|
2256
|
-
actionRef = router.parsePath(url.pathname + url.search);
|
|
2257
|
-
if (!actionRef.startsWith(actionBase)) return;
|
|
2258
|
-
}
|
|
2259
|
-
if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
2260
|
-
// A registry miss on a server-action url is a direct bind whose module
|
|
2261
|
-
// never loaded client-side (server components): the url is self-describing
|
|
2262
|
-
// (`?id`, bound `?args`), so a generic invocation is synthesized from it —
|
|
2263
|
-
// delegation alone is sufficient, the no-JS path stays a no-JS fallback.
|
|
2264
|
-
// Client-only actions (`https://action/`) are their module's JS by
|
|
2265
|
-
// definition, so a miss there falls through to native submission.
|
|
2266
|
-
const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
|
|
2267
|
-
if (handler) {
|
|
2268
|
-
evt.preventDefault();
|
|
2269
|
-
const data = new FormData(evt.target, evt.submitter);
|
|
2270
|
-
handler.call({
|
|
2271
|
-
r: router,
|
|
2272
|
-
f: evt.target
|
|
2273
|
-
}, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
/**
|
|
2278
|
-
* Synthesizes a router action for a server-rendered action url. The url
|
|
2279
|
-
* carries everything an invocation needs — the function id and any bound
|
|
2280
|
-
* `.with()` arguments (plain JSON in `?args`, which the server prepends for
|
|
2281
|
-
* natural-encoding bodies exactly as it does for no-JS posts) — so the
|
|
2282
|
-
* FormData is posted to it verbatim through the server-function transport:
|
|
2283
|
-
* submissions, `aria-busy`, redirects, revalidation, and single-flight all
|
|
2284
|
-
* flow through the normal action machinery. Registered under the url, so
|
|
2285
|
-
* repeat submits reuse it (and a later real registration overrides it).
|
|
2286
|
-
*/
|
|
2287
|
-
function createServerFormAction(url) {
|
|
2288
|
-
const id = new URL(url, mockBase).searchParams.get("id");
|
|
2289
|
-
if (!id) return undefined;
|
|
2290
|
-
// typecheck resolves the server half of the dual module; this path only
|
|
2291
|
-
// runs in the browser, where the client transport's signature applies
|
|
2292
|
-
const stub = createServerReference(id, undefined, url);
|
|
2293
|
-
const caller = Object.assign(form => stub(form), {
|
|
2294
|
-
url
|
|
2295
|
-
});
|
|
2296
|
-
return actionImpl(caller);
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
/**
|
|
2300
|
-
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
2301
|
-
* handler was ever installed — no action module in the client graph at all —
|
|
2302
|
-
* the router intercepts posts to server-action urls synchronously and loads
|
|
2303
|
-
* this module to run them. The FormData was captured at submit time; only
|
|
2304
|
-
* the enctype conversion and the generic invocation happen here.
|
|
2305
|
-
*/
|
|
2306
|
-
function submitServerForm(router, url, form, data) {
|
|
2307
|
-
const handler = actions.get(url) || createServerFormAction(url);
|
|
2308
|
-
// no `?id` — not the server function convention; nothing can run it,
|
|
2309
|
-
// resubmit natively (submit() bypasses the delegated handler)
|
|
2310
|
-
if (!handler) return form.submit();
|
|
2311
|
-
handler.call({
|
|
2312
|
-
r: router,
|
|
2313
|
-
f: form
|
|
2314
|
-
}, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
// Wires the action layer into the router's slots exactly once, triggered by
|
|
2318
|
-
// the first action creation. Not an import side effect — with
|
|
2319
|
-
// `sideEffects: false`, module evaluation only happens when action() is
|
|
2320
|
-
// actually used, which is precisely when the wiring is wanted: no action in
|
|
2321
|
-
// the graph means no form interception, no single-flight subscription (the
|
|
2322
|
-
// server is never asked to collect), and no flash cookies to decode. On the
|
|
2323
|
-
// server, actions are created at module scope, so the flash decoder is
|
|
2324
|
-
// always installed before useSubmission can read the submissions signal.
|
|
2325
|
-
let integrationsInstalled = false;
|
|
2326
|
-
function installRouterIntegrations() {
|
|
2327
|
-
if (integrationsInstalled) return;
|
|
2328
|
-
integrationsInstalled = true;
|
|
2329
|
-
provideFlashDecoder(decodeFlashCookie);
|
|
2330
|
-
if (!isServer) {
|
|
2331
|
-
setRouterFormHandler(handleFormAction);
|
|
2332
|
-
provideFlightConsumer(setupFlightDataConsumer);
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
1289
|
function useSubmissions(fn, filter) {
|
|
2336
1290
|
const router = useRouter();
|
|
2337
1291
|
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
2338
1292
|
return new Proxy([], {
|
|
2339
1293
|
get(_, property) {
|
|
2340
1294
|
if (property === $TRACK) return subs();
|
|
1295
|
+
if (property === "pending") return subs().some(sub => !sub.result);
|
|
2341
1296
|
return subs()[property];
|
|
2342
1297
|
},
|
|
2343
1298
|
has(_, property) {
|
|
@@ -2345,118 +1300,99 @@ function useSubmissions(fn, filter) {
|
|
|
2345
1300
|
}
|
|
2346
1301
|
});
|
|
2347
1302
|
}
|
|
1303
|
+
function useSubmission(fn, filter) {
|
|
1304
|
+
const submissions = useSubmissions(fn, filter);
|
|
1305
|
+
return new Proxy({}, {
|
|
1306
|
+
get(_, property) {
|
|
1307
|
+
if (submissions.length === 0 && (property === "clear" || property === "retry")) return () => {};
|
|
1308
|
+
return submissions[submissions.length - 1]?.[property];
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
2348
1312
|
function useAction(action) {
|
|
2349
1313
|
const r = useRouter();
|
|
2350
1314
|
return (...args) => action.apply({
|
|
2351
1315
|
r
|
|
2352
1316
|
}, args);
|
|
2353
1317
|
}
|
|
2354
|
-
function
|
|
2355
|
-
|
|
1318
|
+
function action(fn, options = {}) {
|
|
1319
|
+
function mutate(...variables) {
|
|
2356
1320
|
const router = this.r;
|
|
2357
1321
|
const form = this.f;
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
// flight-data consumer (see setupFlightDataConsumer) makes the transport
|
|
2362
|
-
// send the request header itself, so the mutation is just called.
|
|
2363
|
-
const runMutation = () => fn(...variables);
|
|
2364
|
-
const run = action$1(async function* (context) {
|
|
2365
|
-
context.optimistic?.();
|
|
2366
|
-
try {
|
|
2367
|
-
const value = await context.call();
|
|
2368
|
-
yield;
|
|
2369
|
-
return {
|
|
2370
|
-
error: false,
|
|
2371
|
-
value
|
|
2372
|
-
};
|
|
2373
|
-
} catch (error) {
|
|
2374
|
-
yield;
|
|
2375
|
-
return {
|
|
2376
|
-
error: true,
|
|
2377
|
-
value: error
|
|
2378
|
-
};
|
|
1322
|
+
const p = (router.singleFlight && fn.withOptions ? fn.withOptions({
|
|
1323
|
+
headers: {
|
|
1324
|
+
"X-Single-Flight": "true"
|
|
2379
1325
|
}
|
|
2380
|
-
});
|
|
2381
|
-
|
|
2382
|
-
let settled;
|
|
2383
|
-
let response;
|
|
2384
|
-
try {
|
|
2385
|
-
settled = await settleActionResult(run({
|
|
2386
|
-
call: runMutation,
|
|
2387
|
-
optimistic: submitHooks.size ? () => {
|
|
2388
|
-
for (const hook of submitHooks.values()) hook(...variables);
|
|
2389
|
-
} : undefined
|
|
2390
|
-
}));
|
|
2391
|
-
response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
|
|
2392
|
-
} finally {
|
|
2393
|
-
form && setFormBusy(form, -1);
|
|
2394
|
-
}
|
|
2395
|
-
if (!response) return undefined;
|
|
1326
|
+
}) : fn)(...variables);
|
|
1327
|
+
const [result, setResult] = createSignal();
|
|
2396
1328
|
let submission;
|
|
2397
|
-
|
|
1329
|
+
function handler(error) {
|
|
1330
|
+
return async res => {
|
|
1331
|
+
const result = await handleResponse(res, error, router.navigatorFactory());
|
|
1332
|
+
let retry = null;
|
|
1333
|
+
o.onComplete?.({
|
|
1334
|
+
...submission,
|
|
1335
|
+
result: result?.data,
|
|
1336
|
+
error: result?.error,
|
|
1337
|
+
pending: false,
|
|
1338
|
+
retry() {
|
|
1339
|
+
return retry = submission.retry();
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
if (retry) return retry;
|
|
1343
|
+
if (!result) return submission.clear();
|
|
1344
|
+
setResult(result);
|
|
1345
|
+
if (result.error && !form) throw result.error;
|
|
1346
|
+
return result.data;
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
router.submissions[1](s => [...s, submission = {
|
|
2398
1350
|
input: variables,
|
|
2399
1351
|
url,
|
|
2400
|
-
result
|
|
2401
|
-
|
|
1352
|
+
get result() {
|
|
1353
|
+
return result()?.data;
|
|
1354
|
+
},
|
|
1355
|
+
get error() {
|
|
1356
|
+
return result()?.error;
|
|
1357
|
+
},
|
|
1358
|
+
get pending() {
|
|
1359
|
+
return !result();
|
|
1360
|
+
},
|
|
2402
1361
|
clear() {
|
|
2403
|
-
router.submissions[1](
|
|
1362
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
2404
1363
|
},
|
|
2405
1364
|
retry() {
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
f: form
|
|
2410
|
-
}, variables, current);
|
|
1365
|
+
setResult(undefined);
|
|
1366
|
+
const p = fn(...variables);
|
|
1367
|
+
return p.then(handler(), handler(true));
|
|
2411
1368
|
}
|
|
2412
|
-
};
|
|
2413
|
-
|
|
2414
|
-
for (const hook of settledHooks.values()) hook(submission);
|
|
2415
|
-
if (response.error && !form) throw response.error;
|
|
2416
|
-
return response.data;
|
|
1369
|
+
}]);
|
|
1370
|
+
return p.then(handler(), handler(true));
|
|
2417
1371
|
}
|
|
2418
1372
|
const o = typeof options === "string" ? {
|
|
2419
1373
|
name: options
|
|
2420
1374
|
} : options;
|
|
2421
1375
|
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
2422
1376
|
const url = fn.url || name && `https://action/${name}` || "";
|
|
2423
|
-
|
|
2424
|
-
if (name) setFunctionName(
|
|
2425
|
-
return
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
|
|
2429
|
-
const fn = function (...args) {
|
|
2430
|
-
return invoke.call(this, [...boundArgs, ...args], fn);
|
|
2431
|
-
};
|
|
1377
|
+
mutate.base = url;
|
|
1378
|
+
if (name) setFunctionName(mutate, name);
|
|
1379
|
+
return toAction(mutate, url);
|
|
1380
|
+
}
|
|
1381
|
+
function toAction(fn, url) {
|
|
2432
1382
|
fn.toString = () => {
|
|
2433
1383
|
if (!url) throw new Error("Client Actions need explicit names if server rendered");
|
|
2434
1384
|
return url;
|
|
2435
1385
|
};
|
|
2436
1386
|
fn.with = function (...args) {
|
|
1387
|
+
const newFn = function (...passedArgs) {
|
|
1388
|
+
return fn.call(this, ...args, ...passedArgs);
|
|
1389
|
+
};
|
|
1390
|
+
newFn.base = fn.base;
|
|
2437
1391
|
const uri = new URL(url, mockBase);
|
|
2438
1392
|
uri.searchParams.set("args", hashKey(args));
|
|
2439
|
-
|
|
2440
|
-
return next;
|
|
2441
|
-
};
|
|
2442
|
-
fn.onSubmit = function (hook) {
|
|
2443
|
-
const id = Symbol("actionOnSubmitHook");
|
|
2444
|
-
submitHooks.set(id, hook);
|
|
2445
|
-
getOwner() && onCleanup(() => submitHooks.delete(id));
|
|
2446
|
-
return this;
|
|
2447
|
-
};
|
|
2448
|
-
fn.onSettled = function (hook) {
|
|
2449
|
-
const id = Symbol("actionOnSettledHook");
|
|
2450
|
-
settledHooks.set(id, hook);
|
|
2451
|
-
getOwner() && onCleanup(() => settledHooks.delete(id));
|
|
2452
|
-
return this;
|
|
1393
|
+
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
|
|
2453
1394
|
};
|
|
2454
1395
|
fn.url = url;
|
|
2455
|
-
fn.base = base;
|
|
2456
|
-
fn[submitHooksSymbol] = submitHooks;
|
|
2457
|
-
fn[settledHooksSymbol] = settledHooks;
|
|
2458
|
-
fn[invokeSymbol] = invoke;
|
|
2459
|
-
installRouterIntegrations();
|
|
2460
1396
|
if (!isServer) {
|
|
2461
1397
|
actions.set(url, fn);
|
|
2462
1398
|
// Only remove the registration if it still belongs to this instance —
|
|
@@ -2467,106 +1403,616 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
2467
1403
|
return fn;
|
|
2468
1404
|
}
|
|
2469
1405
|
const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt(0) | 0, 0);
|
|
2470
|
-
async function
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
return result.then(value => value);
|
|
2474
|
-
}
|
|
2475
|
-
if (value && typeof value.next === "function") {
|
|
2476
|
-
const iterator = value;
|
|
2477
|
-
let next = await iterator.next();
|
|
2478
|
-
while (!next.done) {
|
|
2479
|
-
next = await iterator.next();
|
|
2480
|
-
}
|
|
2481
|
-
return next.value;
|
|
2482
|
-
}
|
|
2483
|
-
return result;
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
/**
|
|
2487
|
-
* Registers the router as the single-flight consumer of the server function
|
|
2488
|
-
* transport. Subscribing is the opt-in: while registered, the transport
|
|
2489
|
-
* sends the `X-Single-Flight` request header on mutations and delivers the
|
|
2490
|
-
* folded payload here — fresh route data is seeded into the `query` cache
|
|
2491
|
-
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
2492
|
-
* applied, all before the action sees its plain return value. Called by the
|
|
2493
|
-
* Router component on the client unless `singleFlight={false}`, which now
|
|
2494
|
-
* simply means "never subscribe" — no consumer, no request header, no
|
|
2495
|
-
* collection work on the server. Returns the unsubscribe function.
|
|
2496
|
-
*/
|
|
2497
|
-
function setupFlightDataConsumer(router) {
|
|
2498
|
-
return subscribeFlightData((data, {
|
|
2499
|
-
response
|
|
2500
|
-
}) => applyResponseMetadata(response, router.navigatorFactory(), data));
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
/**
|
|
2504
|
-
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
2505
|
-
* keys invalidate, `Location` navigates (hard for absolute urls), flight
|
|
2506
|
-
* data seeds the query cache, and matching entries revalidate. Shared by
|
|
2507
|
-
* the flight-data consumer and the action response path (which still sees
|
|
2508
|
-
* metadata-bearing responses when no flight data was collected).
|
|
2509
|
-
*/
|
|
2510
|
-
async function applyResponseMetadata(metadata, navigate, flightData) {
|
|
1406
|
+
async function handleResponse(response, error, navigate) {
|
|
1407
|
+
let data;
|
|
1408
|
+
let custom;
|
|
2511
1409
|
let keys;
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
if (
|
|
2515
|
-
|
|
1410
|
+
let flightKeys;
|
|
1411
|
+
if (response instanceof Response) {
|
|
1412
|
+
if (response.headers.has("X-Revalidate")) keys = response.headers.get("X-Revalidate").split(",");
|
|
1413
|
+
if (response.customBody) {
|
|
1414
|
+
data = custom = await response.customBody();
|
|
1415
|
+
if (response.headers.has("X-Single-Flight")) {
|
|
1416
|
+
data = data._$value;
|
|
1417
|
+
delete custom._$value;
|
|
1418
|
+
flightKeys = Object.keys(custom);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (response.headers.has("Location")) {
|
|
1422
|
+
const locationUrl = response.headers.get("Location") || "/";
|
|
2516
1423
|
if (locationUrl.startsWith("http")) {
|
|
2517
1424
|
window.location.href = locationUrl;
|
|
2518
1425
|
} else {
|
|
2519
1426
|
navigate(locationUrl);
|
|
2520
1427
|
}
|
|
2521
1428
|
}
|
|
2522
|
-
}
|
|
1429
|
+
} else if (error) return {
|
|
1430
|
+
error: response
|
|
1431
|
+
};else data = response;
|
|
2523
1432
|
// invalidate
|
|
2524
1433
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
2525
1434
|
// set cache
|
|
2526
|
-
|
|
1435
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
2527
1436
|
// trigger revalidation
|
|
2528
1437
|
await revalidate(keys, false);
|
|
2529
|
-
}
|
|
2530
|
-
async function handleResponse(response, error, navigate) {
|
|
2531
|
-
let data;
|
|
2532
|
-
let flightData;
|
|
2533
|
-
let metadata;
|
|
2534
|
-
if (isResponseEnvelope(response)) {
|
|
2535
|
-
// client-only respond(): the value rides in memory beside the metadata
|
|
2536
|
-
data = response.value;
|
|
2537
|
-
metadata = response.response;
|
|
2538
|
-
} else if (response instanceof Response) {
|
|
2539
|
-
metadata = response;
|
|
2540
|
-
// responses the transport hands over whole (redirects, revalidation)
|
|
2541
|
-
// carry a codec-encoded body the router decodes itself. With the
|
|
2542
|
-
// flight-data consumer registered single-flight payloads never reach
|
|
2543
|
-
// this path, but a manually opted-in call (no consumer) still can —
|
|
2544
|
-
// unwrap the standardized { value, data } shape for it too.
|
|
2545
|
-
if (response.body) {
|
|
2546
|
-
data = await decodeResponse(response);
|
|
2547
|
-
if (response.headers.has("X-Single-Flight")) {
|
|
2548
|
-
const payload = data;
|
|
2549
|
-
data = payload.value;
|
|
2550
|
-
flightData = payload.data;
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
} else if (error) return {
|
|
2554
|
-
error: response
|
|
2555
|
-
};else data = response;
|
|
2556
|
-
await applyResponseMetadata(metadata, navigate, flightData);
|
|
2557
1438
|
return data != null ? {
|
|
2558
1439
|
data
|
|
2559
1440
|
} : undefined;
|
|
2560
1441
|
}
|
|
2561
1442
|
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
1443
|
+
function setupNativeEvents({
|
|
1444
|
+
preload = true,
|
|
1445
|
+
explicitLinks = false,
|
|
1446
|
+
actionBase = "/_server",
|
|
1447
|
+
transformUrl
|
|
1448
|
+
} = {}) {
|
|
1449
|
+
return router => {
|
|
1450
|
+
const basePath = router.base.path();
|
|
1451
|
+
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
1452
|
+
let preloadTimeout;
|
|
1453
|
+
let lastElement;
|
|
1454
|
+
function isSvg(el) {
|
|
1455
|
+
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
1456
|
+
}
|
|
1457
|
+
function handleAnchor(evt) {
|
|
1458
|
+
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
1459
|
+
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
1460
|
+
if (!a || explicitLinks && !a.hasAttribute("link")) return;
|
|
1461
|
+
const svg = isSvg(a);
|
|
1462
|
+
const href = svg ? a.href.baseVal : a.href;
|
|
1463
|
+
const target = svg ? a.target.baseVal : a.target;
|
|
1464
|
+
if (target || !href && !a.hasAttribute("state")) return;
|
|
1465
|
+
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
1466
|
+
if (a.hasAttribute("download") || rel && rel.includes("external")) return;
|
|
1467
|
+
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
1468
|
+
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
1469
|
+
return [a, url];
|
|
1470
|
+
}
|
|
1471
|
+
function handleAnchorClick(evt) {
|
|
1472
|
+
const res = handleAnchor(evt);
|
|
1473
|
+
if (!res) return;
|
|
1474
|
+
const [a, url] = res;
|
|
1475
|
+
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
1476
|
+
const state = a.getAttribute("state");
|
|
1477
|
+
evt.preventDefault();
|
|
1478
|
+
navigateFromRoute(to, {
|
|
1479
|
+
resolve: false,
|
|
1480
|
+
replace: a.hasAttribute("replace"),
|
|
1481
|
+
scroll: !a.hasAttribute("noscroll"),
|
|
1482
|
+
state: state ? JSON.parse(state) : undefined
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
function handleAnchorPreload(evt) {
|
|
1486
|
+
const res = handleAnchor(evt);
|
|
1487
|
+
if (!res) return;
|
|
1488
|
+
const [a, url] = res;
|
|
1489
|
+
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
1490
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1491
|
+
}
|
|
1492
|
+
function handleAnchorMove(evt) {
|
|
1493
|
+
clearTimeout(preloadTimeout);
|
|
1494
|
+
const res = handleAnchor(evt);
|
|
1495
|
+
if (!res) return lastElement = null;
|
|
1496
|
+
const [a, url] = res;
|
|
1497
|
+
if (lastElement === a) return;
|
|
1498
|
+
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
1499
|
+
preloadTimeout = setTimeout(() => {
|
|
1500
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1501
|
+
lastElement = a;
|
|
1502
|
+
}, 20);
|
|
1503
|
+
}
|
|
1504
|
+
function handleFormSubmit(evt) {
|
|
1505
|
+
if (evt.defaultPrevented) return;
|
|
1506
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : evt.target.getAttribute("action");
|
|
1507
|
+
if (!actionRef) return;
|
|
1508
|
+
if (!actionRef.startsWith("https://action/")) {
|
|
1509
|
+
// normalize server actions
|
|
1510
|
+
const url = new URL(actionRef, mockBase);
|
|
1511
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
1512
|
+
if (!actionRef.startsWith(actionBase)) return;
|
|
1513
|
+
}
|
|
1514
|
+
if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
1515
|
+
const handler = actions.get(actionRef);
|
|
1516
|
+
if (handler) {
|
|
1517
|
+
evt.preventDefault();
|
|
1518
|
+
const data = new FormData(evt.target, evt.submitter);
|
|
1519
|
+
handler.call({
|
|
1520
|
+
r: router,
|
|
1521
|
+
f: evt.target
|
|
1522
|
+
}, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// ensure delegated event run first
|
|
1527
|
+
delegateEvents(["click", "submit"]);
|
|
1528
|
+
document.addEventListener("click", handleAnchorClick);
|
|
1529
|
+
if (preload) {
|
|
1530
|
+
document.addEventListener("mousemove", handleAnchorMove, {
|
|
1531
|
+
passive: true
|
|
1532
|
+
});
|
|
1533
|
+
document.addEventListener("focusin", handleAnchorPreload, {
|
|
1534
|
+
passive: true
|
|
1535
|
+
});
|
|
1536
|
+
document.addEventListener("touchstart", handleAnchorPreload, {
|
|
1537
|
+
passive: true
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
document.addEventListener("submit", handleFormSubmit);
|
|
1541
|
+
onCleanup(() => {
|
|
1542
|
+
document.removeEventListener("click", handleAnchorClick);
|
|
1543
|
+
if (preload) {
|
|
1544
|
+
document.removeEventListener("mousemove", handleAnchorMove);
|
|
1545
|
+
document.removeEventListener("focusin", handleAnchorPreload);
|
|
1546
|
+
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
1547
|
+
}
|
|
1548
|
+
document.removeEventListener("submit", handleFormSubmit);
|
|
1549
|
+
});
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const STORAGE_KEY = "solid-router:scroll";
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
1557
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
1558
|
+
* if the destination route forces a layout while the document is still short,
|
|
1559
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
1560
|
+
*
|
|
1561
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
1562
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
1563
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
1564
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
1565
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
1566
|
+
* otherwise disables.
|
|
1567
|
+
*
|
|
1568
|
+
* Restoration is a single scroll once routing settles — the same strategy
|
|
1569
|
+
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
1570
|
+
* transition commits is what makes the offset reachable; chasing a still-
|
|
1571
|
+
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
1572
|
+
* content arrives) was tried and removed: no peer router does it, an
|
|
1573
|
+
* unbounded observer re-clamps the viewport to the bottom when the target is
|
|
1574
|
+
* never reachable (a list that is genuinely shorter now), and scroll-induced
|
|
1575
|
+
* layout changes can feed it back into itself. Content that commits after the
|
|
1576
|
+
* transition settles — an image without reserved space, a boundary below the
|
|
1577
|
+
* fold — keeps whatever offset the document can hold.
|
|
1578
|
+
*/
|
|
1579
|
+
function createScrollRestoration() {
|
|
1580
|
+
window.history.scrollRestoration = "manual";
|
|
1581
|
+
// the current entry needs its depth stamp for captures to have a key, even
|
|
1582
|
+
// if something replaced history.state after the lifecycle module loaded
|
|
1583
|
+
saveCurrentDepth();
|
|
1584
|
+
let positions = {};
|
|
1585
|
+
try {
|
|
1586
|
+
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
1587
|
+
} catch {}
|
|
1588
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
1589
|
+
let programmatic = false;
|
|
1590
|
+
let pending;
|
|
1591
|
+
const unbind = [bindEvent(window, "scroll", () => {
|
|
1592
|
+
const d = depth();
|
|
1593
|
+
if (d != null) positions[d] = window.scrollY;
|
|
1594
|
+
// the user took over — a pending restore would yank them
|
|
1595
|
+
if (!programmatic) pending = undefined;
|
|
1596
|
+
}), bindEvent(window, "pagehide", () => {
|
|
1597
|
+
try {
|
|
1598
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
1599
|
+
} catch {}
|
|
1600
|
+
})];
|
|
1601
|
+
const restore = () => {
|
|
1602
|
+
if (pending == null) return;
|
|
1603
|
+
const y = positions[pending];
|
|
1604
|
+
pending = undefined;
|
|
1605
|
+
if (y == null) return;
|
|
1606
|
+
// flagged so the resulting scroll event is not mistaken for the user
|
|
1607
|
+
// taking over (which cancels a pending restore)
|
|
1608
|
+
programmatic = true;
|
|
1609
|
+
window.scrollTo(0, y);
|
|
1610
|
+
programmatic = false;
|
|
1611
|
+
};
|
|
1612
|
+
return {
|
|
1613
|
+
/** Before the router reacts to a popstate: mark the traversal target. */
|
|
1614
|
+
onPop() {
|
|
1615
|
+
pending = depth();
|
|
1616
|
+
},
|
|
1617
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
1618
|
+
onPush() {
|
|
1619
|
+
const d = depth();
|
|
1620
|
+
if (d != null) for (const k in positions) +k >= d && delete positions[k];
|
|
1621
|
+
},
|
|
1622
|
+
create(router) {
|
|
1623
|
+
createEffect(on(router.isRouting, routing => routing || restore(), {
|
|
1624
|
+
defer: true
|
|
1625
|
+
}));
|
|
1626
|
+
onCleanup(() => unbind.forEach(u => u()));
|
|
1627
|
+
// reload/back_forward document loads land on an existing entry; a fresh
|
|
1628
|
+
// navigation starts a new one and belongs at the top
|
|
1629
|
+
const [nav] = performance.getEntriesByType && performance.getEntriesByType("navigation");
|
|
1630
|
+
if (nav && nav.type !== "navigate") {
|
|
1631
|
+
pending = depth();
|
|
1632
|
+
restore();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function Router(props) {
|
|
1639
|
+
if (isServer) return StaticRouter(props);
|
|
1640
|
+
const restoration = props.scrollRestoration ? createScrollRestoration() : undefined;
|
|
1641
|
+
const getSource = () => {
|
|
1642
|
+
const url = window.location.pathname + window.location.search;
|
|
1643
|
+
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
1644
|
+
return {
|
|
1645
|
+
value: url + window.location.hash,
|
|
1646
|
+
state
|
|
1647
|
+
};
|
|
1648
|
+
};
|
|
1649
|
+
const beforeLeave = createBeforeLeave();
|
|
1650
|
+
return createRouter({
|
|
1651
|
+
get: getSource,
|
|
1652
|
+
set({
|
|
1653
|
+
value,
|
|
1654
|
+
replace,
|
|
1655
|
+
scroll,
|
|
1656
|
+
state
|
|
1657
|
+
}) {
|
|
1658
|
+
if (replace) {
|
|
1659
|
+
window.history.replaceState(keepDepth(state), "", value);
|
|
1660
|
+
} else {
|
|
1661
|
+
window.history.pushState(state, "", value);
|
|
1662
|
+
}
|
|
1663
|
+
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
1664
|
+
saveCurrentDepth();
|
|
1665
|
+
restoration && !replace && restoration.onPush();
|
|
1666
|
+
},
|
|
1667
|
+
init: notify => {
|
|
1668
|
+
const handler = notifyIfNotBlocked(notify, delta => {
|
|
1669
|
+
if (delta) {
|
|
1670
|
+
return !beforeLeave.confirm(delta);
|
|
1671
|
+
} else {
|
|
1672
|
+
const s = getSource();
|
|
1673
|
+
return !beforeLeave.confirm(s.value, {
|
|
1674
|
+
state: s.state
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
return bindEvent(window, "popstate", restoration ? () => {
|
|
1679
|
+
restoration.onPop();
|
|
1680
|
+
handler();
|
|
1681
|
+
} : handler);
|
|
1682
|
+
},
|
|
1683
|
+
create: router => {
|
|
1684
|
+
setupNativeEvents({
|
|
1685
|
+
preload: props.preload,
|
|
1686
|
+
explicitLinks: props.explicitLinks,
|
|
1687
|
+
actionBase: props.actionBase,
|
|
1688
|
+
transformUrl: props.transformUrl
|
|
1689
|
+
})(router);
|
|
1690
|
+
restoration && restoration.create(router);
|
|
1691
|
+
},
|
|
1692
|
+
utils: {
|
|
1693
|
+
go: delta => window.history.go(delta),
|
|
1694
|
+
beforeLeave
|
|
1695
|
+
}
|
|
1696
|
+
})(props);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function hashParser(str) {
|
|
1700
|
+
const to = str.replace(/^.*?#/, "");
|
|
1701
|
+
// Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
|
|
1702
|
+
// `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
|
|
1703
|
+
// to the current path so we can handle these in-page anchors correctly.
|
|
1704
|
+
if (!to.startsWith("/")) {
|
|
1705
|
+
const [, path = "/"] = window.location.hash.split("#", 2);
|
|
1706
|
+
return `${path}#${to}`;
|
|
1707
|
+
}
|
|
1708
|
+
return to;
|
|
1709
|
+
}
|
|
1710
|
+
function HashRouter(props) {
|
|
1711
|
+
const getSource = () => window.location.hash.slice(1);
|
|
1712
|
+
const beforeLeave = createBeforeLeave();
|
|
1713
|
+
return createRouter({
|
|
1714
|
+
get: getSource,
|
|
1715
|
+
set({
|
|
1716
|
+
value,
|
|
1717
|
+
replace,
|
|
1718
|
+
scroll,
|
|
1719
|
+
state
|
|
1720
|
+
}) {
|
|
1721
|
+
if (replace) {
|
|
1722
|
+
window.history.replaceState(keepDepth(state), "", "#" + value);
|
|
1723
|
+
} else {
|
|
1724
|
+
window.history.pushState(state, "", "#" + value);
|
|
1725
|
+
}
|
|
1726
|
+
const hashIndex = value.indexOf("#");
|
|
1727
|
+
const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
|
|
1728
|
+
scrollToHash(hash, scroll);
|
|
1729
|
+
saveCurrentDepth();
|
|
1730
|
+
},
|
|
1731
|
+
init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => !beforeLeave.confirm(delta && delta < 0 ? delta : getSource()))),
|
|
1732
|
+
create: setupNativeEvents({
|
|
1733
|
+
preload: props.preload,
|
|
1734
|
+
explicitLinks: props.explicitLinks,
|
|
1735
|
+
actionBase: props.actionBase
|
|
1736
|
+
}),
|
|
1737
|
+
utils: {
|
|
1738
|
+
go: delta => window.history.go(delta),
|
|
1739
|
+
renderPath: path => `#${path}`,
|
|
1740
|
+
parsePath: hashParser,
|
|
1741
|
+
beforeLeave
|
|
1742
|
+
}
|
|
1743
|
+
})(props);
|
|
1744
|
+
}
|
|
2566
1745
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
1746
|
+
function createMemoryHistory() {
|
|
1747
|
+
const entries = ["/"];
|
|
1748
|
+
let index = 0;
|
|
1749
|
+
const listeners = [];
|
|
1750
|
+
const go = n => {
|
|
1751
|
+
// https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245
|
|
1752
|
+
index = Math.max(0, Math.min(index + n, entries.length - 1));
|
|
1753
|
+
const value = entries[index];
|
|
1754
|
+
listeners.forEach(listener => listener(value));
|
|
1755
|
+
};
|
|
1756
|
+
return {
|
|
1757
|
+
get: () => entries[index],
|
|
1758
|
+
set: ({
|
|
1759
|
+
value,
|
|
1760
|
+
scroll,
|
|
1761
|
+
replace
|
|
1762
|
+
}) => {
|
|
1763
|
+
if (replace) {
|
|
1764
|
+
entries[index] = value;
|
|
1765
|
+
} else {
|
|
1766
|
+
entries.splice(index + 1, entries.length - index, value);
|
|
1767
|
+
index++;
|
|
1768
|
+
}
|
|
1769
|
+
listeners.forEach(listener => listener(value));
|
|
1770
|
+
setTimeout(() => {
|
|
1771
|
+
if (scroll) {
|
|
1772
|
+
scrollToHash(value.split("#")[1] || "", true);
|
|
1773
|
+
}
|
|
1774
|
+
}, 0);
|
|
1775
|
+
},
|
|
1776
|
+
back: () => {
|
|
1777
|
+
go(-1);
|
|
1778
|
+
},
|
|
1779
|
+
forward: () => {
|
|
1780
|
+
go(1);
|
|
1781
|
+
},
|
|
1782
|
+
go,
|
|
1783
|
+
listen: listener => {
|
|
1784
|
+
listeners.push(listener);
|
|
1785
|
+
return () => {
|
|
1786
|
+
const index = listeners.indexOf(listener);
|
|
1787
|
+
listeners.splice(index, 1);
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function MemoryRouter(props) {
|
|
1793
|
+
const memoryHistory = props.history || createMemoryHistory();
|
|
1794
|
+
return createRouter({
|
|
1795
|
+
get: memoryHistory.get,
|
|
1796
|
+
set: memoryHistory.set,
|
|
1797
|
+
init: memoryHistory.listen,
|
|
1798
|
+
create: setupNativeEvents({
|
|
1799
|
+
preload: props.preload,
|
|
1800
|
+
explicitLinks: props.explicitLinks,
|
|
1801
|
+
actionBase: props.actionBase
|
|
1802
|
+
}),
|
|
1803
|
+
utils: {
|
|
1804
|
+
go: memoryHistory.go
|
|
1805
|
+
}
|
|
1806
|
+
})(props);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
var _tmpl$ = /*#__PURE__*/template(`<a>`);
|
|
1810
|
+
function A(props) {
|
|
1811
|
+
props = mergeProps({
|
|
1812
|
+
inactiveClass: "inactive",
|
|
1813
|
+
activeClass: "active"
|
|
1814
|
+
}, props);
|
|
1815
|
+
const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
|
|
1816
|
+
const to = useResolvedPath(() => props.href);
|
|
1817
|
+
const href = useHref(to);
|
|
1818
|
+
const location = useLocation();
|
|
1819
|
+
const isActive = createMemo(() => {
|
|
1820
|
+
const to_ = to();
|
|
1821
|
+
if (to_ === undefined) return [false, false];
|
|
1822
|
+
// trailing slashes are ignored so `/route` and `/route/` share active state
|
|
1823
|
+
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
1824
|
+
const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
|
|
1825
|
+
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
1826
|
+
});
|
|
1827
|
+
return (() => {
|
|
1828
|
+
var _el$ = _tmpl$();
|
|
1829
|
+
spread(_el$, mergeProps$1(rest, {
|
|
1830
|
+
get href() {
|
|
1831
|
+
return href() || props.href;
|
|
1832
|
+
},
|
|
1833
|
+
get state() {
|
|
1834
|
+
return JSON.stringify(props.state);
|
|
1835
|
+
},
|
|
1836
|
+
get classList() {
|
|
1837
|
+
return {
|
|
1838
|
+
...(props.class && {
|
|
1839
|
+
[props.class]: true
|
|
1840
|
+
}),
|
|
1841
|
+
[props.inactiveClass]: !isActive()[0],
|
|
1842
|
+
[props.activeClass]: isActive()[0],
|
|
1843
|
+
...rest.classList
|
|
1844
|
+
};
|
|
1845
|
+
},
|
|
1846
|
+
"link": "",
|
|
1847
|
+
get ["aria-current"]() {
|
|
1848
|
+
return isActive()[1] ? "page" : undefined;
|
|
1849
|
+
}
|
|
1850
|
+
}), false, false);
|
|
1851
|
+
return _el$;
|
|
1852
|
+
})();
|
|
1853
|
+
}
|
|
1854
|
+
function Navigate(props) {
|
|
1855
|
+
const navigate = useNavigate();
|
|
1856
|
+
const location = useLocation();
|
|
1857
|
+
const {
|
|
1858
|
+
href,
|
|
1859
|
+
state
|
|
1860
|
+
} = props;
|
|
1861
|
+
const path = typeof href === "function" ? href({
|
|
1862
|
+
navigate,
|
|
1863
|
+
location
|
|
1864
|
+
}) : href;
|
|
1865
|
+
navigate(path, {
|
|
1866
|
+
replace: true,
|
|
1867
|
+
state
|
|
1868
|
+
});
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
|
|
1874
|
+
*/
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* As `createAsync` and `createAsyncStore` are wrappers for `createResource`,
|
|
1878
|
+
* this type allows to support `latest` field for these primitives.
|
|
1879
|
+
* It will be removed in the future.
|
|
1880
|
+
*/
|
|
1881
|
+
|
|
1882
|
+
function createAsync(fn, options) {
|
|
1883
|
+
let resource;
|
|
1884
|
+
let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
|
|
1885
|
+
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
|
|
1886
|
+
const resultAccessor = () => resource();
|
|
1887
|
+
if (options?.name) setFunctionName(resultAccessor, options.name);
|
|
1888
|
+
Object.defineProperty(resultAccessor, "latest", {
|
|
1889
|
+
get() {
|
|
1890
|
+
return resource.latest;
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
return resultAccessor;
|
|
1894
|
+
}
|
|
1895
|
+
function createAsyncStore(fn, options = {}) {
|
|
1896
|
+
let resource;
|
|
1897
|
+
let prev = () => !resource || resource.state === "unresolved" ? undefined : unwrap(resource.latest);
|
|
1898
|
+
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, {
|
|
1899
|
+
...options,
|
|
1900
|
+
storage: init => createDeepSignal(init, options.reconcile)
|
|
1901
|
+
});
|
|
1902
|
+
const resultAccessor = () => resource();
|
|
1903
|
+
Object.defineProperty(resultAccessor, "latest", {
|
|
1904
|
+
get() {
|
|
1905
|
+
return resource.latest;
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
return resultAccessor;
|
|
1909
|
+
}
|
|
1910
|
+
function createDeepSignal(value, options) {
|
|
1911
|
+
const [store, setStore] = createStore({
|
|
1912
|
+
value: structuredClone(value)
|
|
1913
|
+
});
|
|
1914
|
+
return [() => store.value, v => {
|
|
1915
|
+
typeof v === "function" && (v = v());
|
|
1916
|
+
setStore("value", reconcile(structuredClone(v), options));
|
|
1917
|
+
return store.value;
|
|
1918
|
+
}];
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// mock promise while hydrating to prevent fetching
|
|
1922
|
+
class MockPromise {
|
|
1923
|
+
static all() {
|
|
1924
|
+
return new MockPromise();
|
|
1925
|
+
}
|
|
1926
|
+
static allSettled() {
|
|
1927
|
+
return new MockPromise();
|
|
1928
|
+
}
|
|
1929
|
+
static any() {
|
|
1930
|
+
return new MockPromise();
|
|
1931
|
+
}
|
|
1932
|
+
static race() {
|
|
1933
|
+
return new MockPromise();
|
|
1934
|
+
}
|
|
1935
|
+
static reject() {
|
|
1936
|
+
return new MockPromise();
|
|
1937
|
+
}
|
|
1938
|
+
static resolve() {
|
|
1939
|
+
return new MockPromise();
|
|
1940
|
+
}
|
|
1941
|
+
catch() {
|
|
1942
|
+
return new MockPromise();
|
|
1943
|
+
}
|
|
1944
|
+
then() {
|
|
1945
|
+
return new MockPromise();
|
|
1946
|
+
}
|
|
1947
|
+
finally() {
|
|
1948
|
+
return new MockPromise();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function subFetch(fn, prev) {
|
|
1952
|
+
if (isServer || !sharedConfig.context) return fn(prev);
|
|
1953
|
+
const ogFetch = fetch;
|
|
1954
|
+
const ogPromise = Promise;
|
|
1955
|
+
try {
|
|
1956
|
+
window.fetch = () => new MockPromise();
|
|
1957
|
+
Promise = MockPromise;
|
|
1958
|
+
return fn(prev);
|
|
1959
|
+
} finally {
|
|
1960
|
+
window.fetch = ogFetch;
|
|
1961
|
+
Promise = ogPromise;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function redirect(url, init = 302) {
|
|
1966
|
+
let responseInit;
|
|
1967
|
+
let revalidate;
|
|
1968
|
+
if (typeof init === "number") {
|
|
1969
|
+
responseInit = {
|
|
1970
|
+
status: init
|
|
1971
|
+
};
|
|
1972
|
+
} else {
|
|
1973
|
+
({
|
|
1974
|
+
revalidate,
|
|
1975
|
+
...responseInit
|
|
1976
|
+
} = init);
|
|
1977
|
+
if (typeof responseInit.status === "undefined") {
|
|
1978
|
+
responseInit.status = 302;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const headers = new Headers(responseInit.headers);
|
|
1982
|
+
headers.set("Location", url);
|
|
1983
|
+
revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
|
|
1984
|
+
const response = new Response(null, {
|
|
1985
|
+
...responseInit,
|
|
1986
|
+
headers: headers
|
|
1987
|
+
});
|
|
1988
|
+
return response;
|
|
1989
|
+
}
|
|
1990
|
+
function reload(init = {}) {
|
|
1991
|
+
const {
|
|
1992
|
+
revalidate,
|
|
1993
|
+
...responseInit
|
|
1994
|
+
} = init;
|
|
1995
|
+
const headers = new Headers(responseInit.headers);
|
|
1996
|
+
revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
|
|
1997
|
+
return new Response(null, {
|
|
1998
|
+
...responseInit,
|
|
1999
|
+
headers
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
function json(data, init = {}) {
|
|
2003
|
+
const {
|
|
2004
|
+
revalidate,
|
|
2005
|
+
...responseInit
|
|
2006
|
+
} = init;
|
|
2007
|
+
const headers = new Headers(responseInit.headers);
|
|
2008
|
+
revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
|
|
2009
|
+
headers.set("Content-Type", "application/json");
|
|
2010
|
+
const response = new Response(JSON.stringify(data), {
|
|
2011
|
+
...responseInit,
|
|
2012
|
+
headers
|
|
2013
|
+
});
|
|
2014
|
+
response.customBody = () => data;
|
|
2015
|
+
return response;
|
|
2016
|
+
}
|
|
2571
2017
|
|
|
2572
|
-
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action,
|
|
2018
|
+
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|