@solidjs/router 1.0.0-next.9 → 1.0.0

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