@solidjs/router 1.0.0-next.9 → 2.0.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, flush, createComponent, createRoot, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
- import { registerElementClaim, delegateEvents, isServer, getRequestEvent, createComponent as createComponent$1, memo, isResponseEnvelope } from '@solidjs/web';
3
- import { isServerFunction, getServerFunctionMetadata, GET, decodeResponse, subscribeFlightData, createServerReference } from '@solidjs/web/server-functions';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, flush, createComponent, createRoot, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
+ import { registerElementClaim, delegateEvents, isServer, getRequestEvent, createComponent as createComponent$1, memo, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
+ import { hasFlashCookie, clearFlashCookie, isServerFunction, getServerFunctionMetadata, GET, decodeResponse, subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
4
+ import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
4
5
 
5
6
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
6
7
  const trimPathRegex = /^\/+|(\/)\/+$/g;
@@ -504,31 +505,6 @@ function createPathsProxy(renderPath = p => p, base = "") {
504
505
  return node(normalizePath(base));
505
506
  }
506
507
 
507
- // The flash cookie's name and one-shot clearing — split from the codec in
508
- // flash.ts so the router core can consume the cookie eagerly (the clear must
509
- // be appended before streaming flushes the response headers, and an unread
510
- // outcome must not haunt a later request) without carrying the encode/decode
511
- // machinery into client bundles that never load the action layer.
512
-
513
- const FLASH_COOKIE = "flash";
514
- const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
515
-
516
- /** Whether a Cookie header carries a flash cookie (readable or not). */
517
- function hasFlashCookie(cookieHeader) {
518
- return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
519
- }
520
-
521
- /** The raw encoded flash payload out of a Cookie header, if present. */
522
- function matchFlashCookie(cookieHeader) {
523
- const match = cookieHeader && cookieHeader.match(FLASH_MATCHER);
524
- return match ? match[1] : undefined;
525
- }
526
-
527
- /** The Set-Cookie value clearing the flash cookie after it has been read. */
528
- function clearFlashCookie() {
529
- return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
530
- }
531
-
532
508
  const MAX_REDIRECTS = 100;
533
509
 
534
510
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -608,17 +584,27 @@ const useIsRouting = () => useRouter().isRouting;
608
584
  /**
609
585
  * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
610
586
  * Useful for determining if a given path matches the current route.
611
- *
587
+ *
588
+ * Accepts a pattern string — the match's `params` are typed from it — or a
589
+ * typed path node (a concrete URL, so no params to speak of):
590
+ *
612
591
  * @example
613
592
  * ```js
614
593
  * const match = useMatch(() => props.href);
615
- *
594
+ *
616
595
  * return <div classList={{ active: Boolean(match()) }} />;
596
+ *
597
+ * const section = useMatch(() => "/docs/:page");
598
+ * section()?.params.page; // string
599
+ *
600
+ * const here = useMatch(() => paths.users(2));
617
601
  * ```
618
602
  */
619
603
  const useMatch = (path, matchFilters) => {
620
604
  const location = useLocation();
621
- const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
605
+ const matchers = createMemo(() =>
606
+ // `String()` folds typed path nodes to their URL
607
+ expandOptionals(String(path())).map(path => createMatcher(path, undefined, matchFilters)));
622
608
  return createMemo(() => {
623
609
  for (const matcher of matchers()) {
624
610
  const match = matcher(location.pathname);
@@ -1058,7 +1044,7 @@ function provideFlightConsumer(factory) {
1058
1044
  /**
1059
1045
  * The flash-cookie codec, provided by the action side (data/action.ts) so
1060
1046
  * the router core never carries it: the core consumes the cookie eagerly
1061
- * per request (detection + one-shot clear via the tiny flashCookie.ts half)
1047
+ * per request (detection + one-shot clear via the runtime's isomorphic half)
1062
1048
  * but defers decoding to this slot, read when the submissions signal
1063
1049
  * initializes. Actions are created at module scope, so on the server the
1064
1050
  * decoder is always installed before useSubmission can read — and a
@@ -1100,7 +1086,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1100
1086
  scroll: false
1101
1087
  });
1102
1088
  }
1103
- const [isRouting, setIsRouting] = createSignal(false, {
1089
+ const [isNavigating, setIsRouting] = createSignal(false, {
1104
1090
  ownedWrite: true
1105
1091
  });
1106
1092
 
@@ -1120,7 +1106,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1120
1106
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
1121
1107
  // must be appended before streaming flushes the response headers, and an
1122
1108
  // unread outcome must not haunt a later request's render. Only detection
1123
- // and clearing happen here (the tiny flashCookie.ts half); the raw header
1109
+ // and clearing happen here (the runtime's isomorphic half); the raw header
1124
1110
  // is stashed and decoding waits for the action-provided codec, read when
1125
1111
  // the lazily allocated submissions signal below first initializes.
1126
1112
  let flashCookieHeader;
@@ -1150,6 +1136,14 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1150
1136
  if (pending.length) throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
1151
1137
  return m;
1152
1138
  });
1139
+
1140
+ // Every write is a transition in Solid 2, so a native history pop forks the
1141
+ // source signal exactly like programmatic navigation does. isRouting is
1142
+ // therefore derived: the manual flag covers navigateFromRoute's explicit
1143
+ // window, and isPending over the location/matches read reports any
1144
+ // in-flight fork — including popstate traversals and the lazy-subtree
1145
+ // resolution matches() parks on.
1146
+ const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
1153
1147
  const buildParams = () => mergeParams(matches());
1154
1148
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1155
1149
  const params = wrapParams(buildParams);
@@ -1321,8 +1315,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1321
1315
  }
1322
1316
 
1323
1317
  // Seeds the initial submission from a no-JS form post: the server
1324
- // function handler redirected back with the outcome in a one-shot flash
1325
- // cookie (see src/server.ts's handleNoJS), consumed eagerly above and
1318
+ // function runtime redirected back with the outcome in a one-shot flash
1319
+ // cookie (its default no-JS convention), consumed eagerly above and
1326
1320
  // decoded here — so the post-redirect SSR renders useSubmission() state
1327
1321
  // exactly as a scripted submission would. An explicitly pre-seeded
1328
1322
  // `event.router.submission` (framework integrations) takes precedence.
@@ -1730,6 +1724,127 @@ function memoryHistory(initial = "/") {
1730
1724
  };
1731
1725
  }
1732
1726
 
1727
+ const STORAGE_KEY = "solid-router:scroll";
1728
+
1729
+ /**
1730
+ * Explicit scroll restoration for back/forward navigation. The browser's
1731
+ * native same-document heuristic is unreliable for suspense-driven rendering:
1732
+ * if the destination route forces a layout while the document is still short,
1733
+ * the saved offset for the previous entry is clamped and lost (#577).
1734
+ *
1735
+ * Positions are captured continuously from the scroll event, keyed by the
1736
+ * `_depth` the router already stamps on every history entry — capturing at
1737
+ * scroll time (rather than at exit) stays correct through `useBeforeLeave`
1738
+ * blocked/reverted traversals. The map persists to sessionStorage on pagehide
1739
+ * so restoration survives reloads, which `scrollRestoration = "manual"`
1740
+ * otherwise disables.
1741
+ *
1742
+ * Restoration is a single scroll once routing settles — the same strategy
1743
+ * SvelteKit, TanStack Router and React Router use. Settling after the
1744
+ * transition commits is what makes the offset reachable; chasing a still-
1745
+ * growing document afterwards (a ResizeObserver re-asserting the offset as
1746
+ * content arrives) was tried and removed: no peer router does it, an
1747
+ * unbounded observer re-clamps the viewport to the bottom when the target is
1748
+ * never reachable (a list that is genuinely shorter now), and scroll-induced
1749
+ * layout changes can feed it back into itself. Content that commits after the
1750
+ * transition settles — an image without reserved space, a boundary below the
1751
+ * fold — keeps whatever offset the document can hold.
1752
+ */
1753
+ function createScrollRestoration() {
1754
+ window.history.scrollRestoration = "manual";
1755
+ // the current entry needs its depth stamp for captures to have a key, even
1756
+ // if something replaced history.state after the adapter stamped it
1757
+ saveCurrentDepth();
1758
+ let positions = {};
1759
+ try {
1760
+ positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
1761
+ } catch {}
1762
+ const depth = () => window.history.state && window.history.state._depth;
1763
+ let programmatic = false;
1764
+ let pending;
1765
+ const unbind = [bindEvent(window, "scroll", () => {
1766
+ const d = depth();
1767
+ if (d != null) positions[d] = window.scrollY;
1768
+ // the user took over — a pending restore would yank them
1769
+ if (!programmatic) pending = undefined;
1770
+ }), bindEvent(window, "pagehide", () => {
1771
+ try {
1772
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
1773
+ } catch {}
1774
+ })];
1775
+ const restore = () => {
1776
+ if (pending == null) return;
1777
+ const y = positions[pending];
1778
+ pending = undefined;
1779
+ if (y == null) return;
1780
+ // flagged so the resulting scroll event is not mistaken for the user
1781
+ // taking over (which cancels a pending restore)
1782
+ programmatic = true;
1783
+ window.scrollTo(0, y);
1784
+ programmatic = false;
1785
+ };
1786
+ return {
1787
+ /** When the adapter notifies a traversal: mark the target for restoration. */
1788
+ onPop() {
1789
+ pending = depth();
1790
+ },
1791
+ /** After a push: forward entries died, and this depth may be reused. */
1792
+ onPush() {
1793
+ const d = depth();
1794
+ if (d != null) for (const k in positions) +k >= d && delete positions[k];
1795
+ },
1796
+ create(router) {
1797
+ // Restore once the traversal has settled: key on the location (a fully
1798
+ // synchronous pop commits without isRouting ever flipping) and on
1799
+ // isRouting, which reports in-flight transitions — native pops
1800
+ // included — and holds the restore until they commit. restore() no-ops
1801
+ // unless a traversal marked a target, so push navigations are inert.
1802
+ // `transparent` keeps the effect invisible to the hydration id scheme —
1803
+ // same reasoning as the link-claims effect (claims.ts): this setup is
1804
+ // client-only, so an id-consuming node here has no server counterpart
1805
+ // and every hydration id allocated after it shifts by one child slot.
1806
+ // The visible failure is any <Loading> content that settled before the
1807
+ // shell flush (a cache hit, a preloaded query): its serialized value and
1808
+ // inlined markup are keyed under the server's ids, the shifted client
1809
+ // misses both, recomputes, and re-renders the route fresh — duplicating
1810
+ // the server DOM and leaving it inert.
1811
+ createEffect(() => ({
1812
+ url: router.location.pathname + router.location.search + router.location.hash,
1813
+ routing: router.isRouting()
1814
+ }), current => {
1815
+ if (!current.routing) restore();
1816
+ }, {
1817
+ transparent: true
1818
+ });
1819
+ onCleanup(() => unbind.forEach(u => u()));
1820
+ // reload/back_forward document loads land on an existing entry (a fresh
1821
+ // navigation starts a new one and belongs at the top); the effect's
1822
+ // initial run performs the restore after first render
1823
+ const [nav] = performance.getEntriesByType && performance.getEntriesByType("navigation");
1824
+ if (nav && nav.type !== "navigate") pending = depth();
1825
+ }
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Threads restoration through a history adapter: pushes prune dead forward
1830
+ * entries, and adapter notifications (unblocked pops) mark the traversal
1831
+ * target. Notification runs after the adapter's depth bookkeeping, so the
1832
+ * marked depth is the entry being restored to.
1833
+ */
1834
+ function withScrollRestoration(history, restoration) {
1835
+ return {
1836
+ ...history,
1837
+ set(next) {
1838
+ history.set(next);
1839
+ next.replace || restoration.onPush();
1840
+ },
1841
+ init: history.init && (notify => history.init(value => {
1842
+ restoration.onPop();
1843
+ notify(value);
1844
+ }))
1845
+ };
1846
+ }
1847
+
1733
1848
  /**
1734
1849
  * Identity helper that preserves literal types when the route tree is
1735
1850
  * declared as a separate variable. `createRouter` infers literally from
@@ -1740,6 +1855,41 @@ function memoryHistory(initial = "/") {
1740
1855
  function defineRoutes(routes) {
1741
1856
  return routes;
1742
1857
  }
1858
+
1859
+ // The single contextual signature for `component` in `defineRoute`. The
1860
+ // public `RouteSectionComponent` union can't be used here: TypeScript only
1861
+ // contextually types a lambda's parameter from a single call signature, and
1862
+ // the whole point of `defineRoute` is that `props` infers. `children` is
1863
+ // `any` so `VoidComponent` pages (whose props declare `children?: never`,
1864
+ // #347) still assign under contravariance.
1865
+
1866
+ /**
1867
+ * The type `defineRoute` hands back: `path`, `matchFilters`, `children`, and
1868
+ * `search` stay literal — and only present when provided precisely, which is
1869
+ * what the `paths` machinery keys on — while `component`/`preload` widen back
1870
+ * to the plain `RouteDefinition` contract so the route drops into any route
1871
+ * tree. Generics that stayed at their fallback (not provided, or deferred by
1872
+ * a context-sensitive value inside) are omitted rather than widening the
1873
+ * whole route.
1874
+ */
1875
+
1876
+ /**
1877
+ * Identity helper that types a single route from its own `path` pattern:
1878
+ * inside `component` and `preload`, `props.params`/`args.params` carry the
1879
+ * params the pattern guarantees (`:id` is `string`, `:tab?` is
1880
+ * `string | undefined`) instead of the open `Params` record. Params
1881
+ * inherited from parent routes remain accessible as `string | undefined`.
1882
+ *
1883
+ * Purely a definition-site convenience — plain object routes behave
1884
+ * identically at runtime, and nested `children` only get typed params if
1885
+ * they use `defineRoute` themselves.
1886
+ */
1887
+
1888
+ // pathless (layout) route — params stay the open `Params` record
1889
+
1890
+ function defineRoute(route) {
1891
+ return route;
1892
+ }
1743
1893
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
1744
1894
  function createIntegration(history) {
1745
1895
  let ignore = false;
@@ -1768,27 +1918,26 @@ function createIntegration(history) {
1768
1918
 
1769
1919
  /**
1770
1920
  * Server default: a static view of the request URL — no signal machinery, a
1771
- * server render never navigates. Without a request event (SSG scripts,
1772
- * server-side tests) the configured history adapter provides the location,
1773
- * so e.g. `memoryHistory("/page")` works isomorphically.
1921
+ * server render never navigates. The request event (when the harness scopes
1922
+ * one) wins; the provider's `url` prop is the fallback for renders outside a
1923
+ * request scope (SSG scripts, server-side tests, runtimes without
1924
+ * `node:async_hooks`). History adapters are a client navigation concern and
1925
+ * play no part in locating a server render.
1774
1926
  */
1775
- function staticIntegration(history) {
1927
+ function staticIntegration(url, utils) {
1776
1928
  const e = getRequestEvent();
1929
+ const source = e ? e.request.url : url;
1777
1930
  let value = "";
1778
- if (e) {
1779
- const url = new URL(e.request.url);
1780
- value = url.pathname + url.search;
1781
- } else if (history) {
1782
- value = history.get();
1931
+ if (source) {
1932
+ const u = new URL(source, mockBase);
1933
+ value = u.pathname + u.search;
1783
1934
  }
1784
- const obj = typeof value === "string" ? {
1935
+ const obj = {
1785
1936
  value
1786
- } : {
1787
- ...value
1788
1937
  };
1789
1938
  return {
1790
1939
  signal: [() => obj, next => Object.assign(obj, next)],
1791
- utils: history && history.utils
1940
+ utils
1792
1941
  };
1793
1942
  }
1794
1943
  function createRouter(config) {
@@ -1818,7 +1967,13 @@ function createRouter(config) {
1818
1967
  console.warn("Mounting a router inside another router is not supported. " + "Compose route trees in one createRouter config instead.");
1819
1968
  }
1820
1969
  const root = untrack(() => props.children);
1821
- const integration = isServer ? staticIntegration(config.history) : createIntegration(config.history || browserHistory());
1970
+ let restoration;
1971
+ let history = config.history;
1972
+ if (!isServer && (config.scrollRestoration ?? !history)) {
1973
+ restoration = createScrollRestoration();
1974
+ history = withScrollRestoration(history || browserHistory(), restoration);
1975
+ }
1976
+ const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory());
1822
1977
  let context;
1823
1978
  const routerState = createRouterContext(integration, branches, () => context, {
1824
1979
  base: basePath,
@@ -1834,6 +1989,7 @@ function createRouter(config) {
1834
1989
  })(routerState);
1835
1990
  setupLinkClaims(routerState, config.explicitLinks);
1836
1991
  if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
1992
+ restoration && restoration.create(routerState);
1837
1993
  }
1838
1994
  return createComponent$1(RouterContextObj, {
1839
1995
  value: routerState,
@@ -1963,52 +2119,6 @@ const useBeforeLeave = listener => {
1963
2119
  onCleanup(s);
1964
2120
  };
1965
2121
 
1966
- // The no-JS form convention's cookie codec. When a form posts to a server
1967
- // function without the client runtime (no instance header), the server
1968
- // handler redirects back carrying the outcome in a one-shot "flash" cookie;
1969
- // the next SSR pass reads it and seeds the router's submission state so
1970
- // useSubmission() renders the result exactly as a scripted submission would.
1971
- // Both codec halves live in this module so the write (src/server.ts, the
1972
- // handler's handleNoJS) and the read (provided to the router core by
1973
- // data/action.ts) can never drift apart. The cookie's name/clearing live in
1974
- // flashCookie.ts — the only piece the router core itself consumes — so this
1975
- // codec stays out of bundles that never load the action layer.
1976
-
1977
- function decodeInputValue(value) {
1978
- if (value && typeof value === "object") {
1979
- if (Array.isArray(value.$f)) {
1980
- const form = new FormData();
1981
- for (const [k, v] of value.$f) form.append(k, v);
1982
- return form;
1983
- }
1984
- if (Array.isArray(value.$u)) return new URLSearchParams(value.$u);
1985
- }
1986
- return value;
1987
- }
1988
-
1989
- /**
1990
- * Decodes the flash cookie out of a request's Cookie header. Returns
1991
- * undefined when absent or unreadable (a malformed cookie must never take
1992
- * down SSR — it is cleared either way).
1993
- */
1994
- function decodeFlashCookie(cookieHeader) {
1995
- const match = matchFlashCookie(cookieHeader);
1996
- if (!match) return;
1997
- try {
1998
- const payload = JSON.parse(decodeURIComponent(match));
1999
- if (!payload || !payload.result) return;
2000
- const result = payload.error ? new Error(payload.result) : payload.result;
2001
- return {
2002
- input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
2003
- url: payload.url,
2004
- result: payload.thrown ? undefined : result,
2005
- error: payload.thrown ? result : undefined
2006
- };
2007
- } catch (error) {
2008
- console.error(error);
2009
- }
2010
- }
2011
-
2012
2122
  const LocationHeader = "Location";
2013
2123
  const PRELOAD_TIMEOUT = 5000;
2014
2124
  const CACHE_TIMEOUT = 180000;
@@ -2326,8 +2436,11 @@ let integrationsInstalled = false;
2326
2436
  function installRouterIntegrations() {
2327
2437
  if (integrationsInstalled) return;
2328
2438
  integrationsInstalled = true;
2329
- provideFlashDecoder(decodeFlashCookie);
2330
- if (!isServer) {
2439
+ if (isServer) {
2440
+ // Server-only: initSubmissions only decodes during SSR, so client builds
2441
+ // tree-shake the codec (which now lives behind the runtime's server entry).
2442
+ provideFlashDecoder(decodeFlashCookie);
2443
+ } else {
2331
2444
  setRouterFormHandler(handleFormAction);
2332
2445
  provideFlightConsumer(setupFlightDataConsumer);
2333
2446
  }
@@ -2399,13 +2512,12 @@ function actionImpl(fn, options = {}) {
2399
2512
  } finally {
2400
2513
  form && setFormBusy(form, -1);
2401
2514
  }
2402
- if (!response) return undefined;
2403
2515
  let submission;
2404
2516
  submission = {
2405
2517
  input: variables,
2406
2518
  url,
2407
- result: response.data,
2408
- error: response.error,
2519
+ result: response && response.data,
2520
+ error: response && response.error,
2409
2521
  clear() {
2410
2522
  router.submissions[1](entries => entries.filter(entry => entry !== submission));
2411
2523
  },
@@ -2417,10 +2529,18 @@ function actionImpl(fn, options = {}) {
2417
2529
  }, variables, current);
2418
2530
  }
2419
2531
  };
2420
- router.submissions[1](entries => [...entries, submission]);
2532
+ // Book-keeping is intentional: only outcomes worth showing or retrying
2533
+ // (a result or an error) enter the submissions list, so the typical void
2534
+ // mutation leaves nothing behind. Settled hooks still see every
2535
+ // completion — void, metadata-only, and redirects included — one
2536
+ // `onSettled` per invocation (#580).
2537
+ response && router.submissions[1](entries => [...entries, submission]);
2421
2538
  for (const hook of settledHooks.values()) hook(submission);
2422
- if (response.error && !form) throw response.error;
2423
- return response.data;
2539
+ if (response) {
2540
+ if (response.error && !form) throw response.error;
2541
+ return response.data;
2542
+ }
2543
+ return undefined;
2424
2544
  }
2425
2545
  const o = typeof options === "string" ? {
2426
2546
  name: options
@@ -2523,10 +2643,10 @@ function setupFlightDataConsumer(router) {
2523
2643
  * the flight-data consumer and the action response path (which still sees
2524
2644
  * metadata-bearing responses when no flight data was collected).
2525
2645
  */
2526
- async function applyResponseMetadata(metadata, navigate, flightData) {
2646
+ function applyResponseMetadata(metadata, navigate, flightData) {
2527
2647
  let keys;
2528
2648
  if (metadata) {
2529
- if (metadata.headers.has("X-Revalidate")) keys = metadata.headers.get("X-Revalidate").split(",");
2649
+ if (metadata.headers.has(REVALIDATE_HEADER)) keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
2530
2650
  if (metadata.headers.has("Location")) {
2531
2651
  const locationUrl = metadata.headers.get("Location") || "/";
2532
2652
  if (locationUrl.startsWith("http")) {
@@ -2541,7 +2661,7 @@ async function applyResponseMetadata(metadata, navigate, flightData) {
2541
2661
  // set cache
2542
2662
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
2543
2663
  // trigger revalidation
2544
- await revalidate(keys, false);
2664
+ revalidate(keys, false);
2545
2665
  }
2546
2666
  async function handleResponse(response, error, navigate, metadataHandled) {
2547
2667
  let data;
@@ -2557,14 +2677,11 @@ async function handleResponse(response, error, navigate, metadataHandled) {
2557
2677
  // carry a codec-encoded body the router decodes itself. With the
2558
2678
  // flight-data consumer registered single-flight payloads never reach
2559
2679
  // this path, but a manually opted-in call (no consumer) still can —
2560
- // unwrap the standardized { value, data } shape for it too.
2680
+ // the runtime splits its own envelope shape.
2561
2681
  if (response.body) {
2562
- data = await decodeResponse(response);
2563
- if (response.headers.has("X-Single-Flight")) {
2564
- const payload = data;
2565
- data = payload.value;
2566
- flightData = payload.data;
2567
- }
2682
+ const payload = await decodeResponsePayload(response);
2683
+ data = payload.value;
2684
+ flightData = payload.flightData;
2568
2685
  }
2569
2686
  } else if (error) return {
2570
2687
  error: response
@@ -2572,7 +2689,7 @@ async function handleResponse(response, error, navigate, metadataHandled) {
2572
2689
  // The transport consumer applies metadata before returning a server
2573
2690
  // function's unwrapped value. Do not treat that value as a second plain
2574
2691
  // action response and invalidate the freshly seeded query cache again.
2575
- if (!metadataHandled || metadata || flightData) await applyResponseMetadata(metadata, navigate, flightData);
2692
+ if (!metadataHandled || metadata || flightData) applyResponseMetadata(metadata, navigate, flightData);
2576
2693
  return data != null ? {
2577
2694
  data
2578
2695
  } : undefined;
@@ -2588,4 +2705,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
2588
2705
  submitServerForm: submitServerForm
2589
2706
  });
2590
2707
 
2591
- export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoutes, hashHistory, int, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
2708
+ export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from "@solidjs/web";
2
2
  import type { RoutePaths } from "../paths.js";
3
- import type { OutputMatch, RouteDefinition, RoutePreloadFunc, RouteSectionProps } from "../types.js";
3
+ import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, ValidFilters } from "../types.js";
4
4
  import type { RouterHistory } from "./history.js";
5
5
  /**
6
6
  * Identity helper that preserves literal types when the route tree is
@@ -10,6 +10,60 @@ import type { RouterHistory } from "./history.js";
10
10
  * declared `as const` — `defineRoutes` makes that impossible to forget.
11
11
  */
12
12
  export declare function defineRoutes<const R extends readonly RouteDefinition[]>(routes: R): R;
13
+ type RouteChildren = RouteDefinition | readonly RouteDefinition[] | LazyRouteChildren;
14
+ type DefinedRouteComponent<T, P extends Params> = (props: RouteSectionProps<T, P> & {
15
+ children?: any;
16
+ }) => JSX.Element;
17
+ /**
18
+ * The type `defineRoute` hands back: `path`, `matchFilters`, `children`, and
19
+ * `search` stay literal — and only present when provided precisely, which is
20
+ * what the `paths` machinery keys on — while `component`/`preload` widen back
21
+ * to the plain `RouteDefinition` contract so the route drops into any route
22
+ * tree. Generics that stayed at their fallback (not provided, or deferred by
23
+ * a context-sensitive value inside) are omitted rather than widening the
24
+ * whole route.
25
+ */
26
+ export type DefinedRoute<S = undefined, T = unknown, F = undefined, C = undefined, Sch = undefined> = ([S] extends [undefined] ? {} : {
27
+ path: S;
28
+ }) & ([F] extends [undefined] ? {} : DefinedRouteFilters<S> extends F ? {} : {
29
+ matchFilters: F;
30
+ }) & ([C] extends [undefined] ? {} : [RouteChildren | undefined] extends [C] ? {} : {
31
+ children: C;
32
+ }) & ([Sch] extends [undefined] ? {} : {
33
+ search: Sch;
34
+ }) & {
35
+ component?: RouteSectionComponent<T>;
36
+ preload?: RoutePreloadFunc<T>;
37
+ info?: RouteInfo;
38
+ };
39
+ /**
40
+ * Identity helper that types a single route from its own `path` pattern:
41
+ * inside `component` and `preload`, `props.params`/`args.params` carry the
42
+ * params the pattern guarantees (`:id` is `string`, `:tab?` is
43
+ * `string | undefined`) instead of the open `Params` record. Params
44
+ * inherited from parent routes remain accessible as `string | undefined`.
45
+ *
46
+ * Purely a definition-site convenience — plain object routes behave
47
+ * identically at runtime, and nested `children` only get typed params if
48
+ * they use `defineRoute` themselves.
49
+ */
50
+ export declare function defineRoute<const S extends string | readonly string[], T = unknown, const F = DefinedRouteFilters<S>, const C extends RouteChildren | undefined = RouteChildren | undefined, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(route: {
51
+ path: S;
52
+ matchFilters?: F & ValidFilters<F, S>;
53
+ preload?: (args: RoutePreloadFuncArgs<RouteParams<S>>) => T;
54
+ component?: DefinedRouteComponent<T, RouteParams<S>>;
55
+ children?: C;
56
+ /** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
57
+ search?: Sch;
58
+ info?: RouteInfo;
59
+ }): DefinedRoute<S, T, F, C, Sch>;
60
+ export declare function defineRoute<T = unknown, const C extends RouteChildren | undefined = RouteChildren | undefined, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(route: {
61
+ preload?: (args: RoutePreloadFuncArgs) => T;
62
+ component?: DefinedRouteComponent<T, Params>;
63
+ children?: C;
64
+ search?: Sch;
65
+ info?: RouteInfo;
66
+ }): DefinedRoute<undefined, T, undefined, C, Sch>;
13
67
  export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefinition[]> {
14
68
  /** The route tree. Immutable per instance — it is the source of truth for matching *and* types. */
15
69
  routes: R;
@@ -20,20 +74,41 @@ export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefini
20
74
  * `props.data`.
21
75
  */
22
76
  preload?: RoutePreloadFunc;
23
- /** History adapter; defaults to browser history on the client and the request URL on the server. */
77
+ /**
78
+ * History adapter for client navigation; defaults to browser history. On
79
+ * the server only its `utils` apply — the location comes from the request
80
+ * event or the provider's `url` prop.
81
+ */
24
82
  history?: RouterHistory;
25
83
  singleFlight?: boolean;
26
84
  actionBase?: string;
27
85
  explicitLinks?: boolean;
28
86
  /** Preload route code/data on link hover and focus. Defaults to `true`. */
29
87
  preloadLinks?: boolean;
88
+ /**
89
+ * Explicit scroll restoration for back/forward navigation: positions are
90
+ * saved per history entry and restored once the navigation settles,
91
+ * replacing the browser heuristic that loses offsets when the destination
92
+ * route forces a layout while rendering. Defaults to `true` with the
93
+ * default browser history; a custom `history` adapter owns its session and
94
+ * must opt in explicitly.
95
+ */
96
+ scrollRestoration?: boolean;
30
97
  transformUrl?: (url: string) => string;
31
98
  }
99
+ export interface RouterProps {
100
+ /**
101
+ * Server-only: the location for this render when no request event is in
102
+ * scope (SSG scripts, tests, runtimes without `node:async_hooks`). A
103
+ * request event established by the server harness takes precedence.
104
+ * Ignored on the client, where the history adapter owns the location.
105
+ */
106
+ url?: string;
107
+ children?: (props: RouteSectionProps) => JSX.Element;
108
+ }
32
109
  export interface RouterInstance<R extends readonly RouteDefinition[] = RouteDefinition[]> {
33
110
  /** The instance is the provider component; the render-prop child receives the matched content as `props.children`. */
34
- (props: {
35
- children?: (props: RouteSectionProps) => JSX.Element;
36
- }): JSX.Element;
111
+ (props: RouterProps): JSX.Element;
37
112
  /** Typed path proxy — builds URLs through property access and calls. */
38
113
  readonly paths: RoutePaths<R>;
39
114
  readonly routes: R;
@@ -43,3 +118,4 @@ export interface RouterInstance<R extends readonly RouteDefinition[] = RouteDefi
43
118
  match(url: string): OutputMatch[];
44
119
  }
45
120
  export declare function createRouter<const R extends readonly RouteDefinition[]>(config: RouterConfig<R>): RouterInstance<R>;
121
+ export {};