@solidjs/router 1.0.0 → 2.0.0-next.12

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