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