@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/README.md +100 -7
- package/dist/data/action.js +34 -25
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +233 -116
- package/dist/routers/factory.d.ts +81 -5
- package/dist/routers/factory.jsx +25 -14
- package/dist/routers/history.d.ts +2 -0
- package/dist/routers/history.js +2 -2
- package/dist/routers/index.d.ts +3 -2
- package/dist/routers/index.js +1 -1
- package/dist/routers/scrollRestoration.d.ts +41 -0
- package/dist/routers/scrollRestoration.js +134 -0
- package/dist/routing.d.ts +11 -3
- package/dist/routing.js +25 -8
- package/dist/server.d.ts +7 -35
- package/dist/server.js +35 -143
- package/dist/types.d.ts +66 -12
- package/package.json +8 -7
- package/dist/data/flash.d.ts +0 -21
- package/dist/data/flash.js +0 -78
- package/dist/data/flashCookie.d.ts +0 -7
- package/dist/data/flashCookie.js +0 -20
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(() =>
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
1325
|
-
// cookie (
|
|
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.
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
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(
|
|
1927
|
+
function staticIntegration(url, utils) {
|
|
1776
1928
|
const e = getRequestEvent();
|
|
1929
|
+
const source = e ? e.request.url : url;
|
|
1777
1930
|
let value = "";
|
|
1778
|
-
if (
|
|
1779
|
-
const
|
|
1780
|
-
value =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2330
|
-
|
|
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
|
-
|
|
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
|
|
2423
|
-
|
|
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
|
-
|
|
2646
|
+
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
2527
2647
|
let keys;
|
|
2528
2648
|
if (metadata) {
|
|
2529
|
-
if (metadata.headers.has(
|
|
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
|
-
|
|
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
|
-
//
|
|
2680
|
+
// the runtime splits its own envelope shape.
|
|
2561
2681
|
if (response.body) {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
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)
|
|
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
|
-
/**
|
|
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 {};
|