@solidjs/router 0.17.0-next.5 → 1.0.0-next.7

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