@solidjs/router 0.17.0-next.6 → 1.0.0-next.11

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