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