@solidjs/router 2.0.0-next.30 → 2.0.0-next.32
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 +1 -1
- package/dist/data/action.js +31 -22
- package/dist/data/events.js +1 -1
- package/dist/data/query.js +70 -33
- package/dist/fs.d.ts +1 -1
- package/dist/fs.js +1 -1
- package/dist/index.js +181 -111
- package/dist/paths.js +2 -6
- package/dist/routers/components.jsx +1 -3
- package/dist/routers/factory.jsx +64 -17
- package/dist/routers/history.js +7 -11
- package/dist/routers/scrollRestoration.js +1 -2
- package/dist/routing.js +11 -18
- package/dist/utils.js +15 -24
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError,
|
|
2
|
-
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, isHydratable, $TRACK, action as action$1 } from 'solid-js';
|
|
2
|
+
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
|
|
5
5
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
@@ -45,13 +45,16 @@ function validateSearch(schema, raw) {
|
|
|
45
45
|
return outcome;
|
|
46
46
|
}
|
|
47
47
|
function extractSearchParams(url) {
|
|
48
|
-
const params =
|
|
48
|
+
const params = Object.create(null);
|
|
49
49
|
url.searchParams.forEach((value, key) => {
|
|
50
50
|
if (key in params) {
|
|
51
51
|
if (Array.isArray(params[key])) params[key].push(value);else params[key] = [params[key], value];
|
|
52
52
|
} else params[key] = value;
|
|
53
53
|
});
|
|
54
|
-
|
|
54
|
+
// Keep __proto__ as an own property on the plain object returned to callers.
|
|
55
|
+
return {
|
|
56
|
+
...params
|
|
57
|
+
};
|
|
55
58
|
}
|
|
56
59
|
function createMatcher(path, partial, matchFilters) {
|
|
57
60
|
const [pattern, splat] = path.split("/*", 2);
|
|
@@ -72,13 +75,13 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
72
75
|
path: len ? "" : "/",
|
|
73
76
|
params: {}
|
|
74
77
|
};
|
|
75
|
-
const
|
|
78
|
+
const filters = matchFilters;
|
|
76
79
|
for (let i = 0; i < len; i++) {
|
|
77
80
|
const segment = segments[i];
|
|
78
81
|
const dynamic = segment[0] === ":";
|
|
79
82
|
const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
|
|
80
83
|
const key = dynamic ? segment.slice(1) : segment.toLowerCase();
|
|
81
|
-
if (dynamic && matchSegment(locSegment,
|
|
84
|
+
if (dynamic && matchSegment(locSegment, filters?.[key])) {
|
|
82
85
|
match.params[key] = locSegment;
|
|
83
86
|
} else if (dynamic || !matchSegment(locSegment, key)) {
|
|
84
87
|
return null;
|
|
@@ -87,7 +90,7 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
87
90
|
}
|
|
88
91
|
if (splat) {
|
|
89
92
|
const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
|
|
90
|
-
if (matchSegment(remainder,
|
|
93
|
+
if (matchSegment(remainder, filters?.[splat])) {
|
|
91
94
|
match.params[splat] = remainder;
|
|
92
95
|
} else {
|
|
93
96
|
return null;
|
|
@@ -96,21 +99,9 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
96
99
|
return match;
|
|
97
100
|
};
|
|
98
101
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return true;
|
|
103
|
-
} else if (typeof filter === "string") {
|
|
104
|
-
return isEqual(filter);
|
|
105
|
-
} else if (typeof filter === "function") {
|
|
106
|
-
return filter(input);
|
|
107
|
-
} else if (Array.isArray(filter)) {
|
|
108
|
-
return filter.some(isEqual);
|
|
109
|
-
} else if (filter instanceof RegExp) {
|
|
110
|
-
return filter.test(input);
|
|
111
|
-
}
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
102
|
+
|
|
103
|
+
// a string filter is the segment itself; anything else not listed never matches
|
|
104
|
+
const matchSegment = (input, filter) => filter === undefined || (typeof filter === "function" ? filter(input) : Array.isArray(filter) ? filter.includes(input) : filter instanceof RegExp ? filter.test(input) : filter === input);
|
|
114
105
|
function scoreRoute(route) {
|
|
115
106
|
const [pattern, splat] = route.pattern.split("/*", 2);
|
|
116
107
|
const segments = pattern.split("/").filter(Boolean);
|
|
@@ -340,7 +331,7 @@ function setupNativeEvents({
|
|
|
340
331
|
const target = svg ? a.target.baseVal : a.target;
|
|
341
332
|
if (target || !href && !a.hasAttribute("state")) return;
|
|
342
333
|
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
343
|
-
if (a.hasAttribute("download") || rel
|
|
334
|
+
if (a.hasAttribute("download") || rel.includes("external")) return;
|
|
344
335
|
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
345
336
|
// Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
|
|
346
337
|
// inherit the page origin, so the origin check below won't reject them. #382
|
|
@@ -514,8 +505,8 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
514
505
|
};
|
|
515
506
|
return new Proxy(build, {
|
|
516
507
|
get(_, prop) {
|
|
517
|
-
if (prop === "toString") return () => toHref(pathname);
|
|
518
|
-
if (typeof prop === "symbol") return prop ===
|
|
508
|
+
if (prop === "toString" || prop === Symbol.toPrimitive) return () => toHref(pathname);
|
|
509
|
+
if (typeof prop === "symbol") return prop === HREF ? pathname || "/" : undefined;
|
|
519
510
|
return node(`${pathname}/${prop}`);
|
|
520
511
|
}
|
|
521
512
|
});
|
|
@@ -1023,9 +1014,7 @@ function createRoutes(routeDef, base = "") {
|
|
|
1023
1014
|
for (const expandedPath of expandOptionals(originalPath)) {
|
|
1024
1015
|
const path = joinPaths(base, expandedPath);
|
|
1025
1016
|
let pattern = isLeaf ? path : path.split("/*", 1)[0];
|
|
1026
|
-
pattern = pattern.split("/").map(s =>
|
|
1027
|
-
return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
|
|
1028
|
-
}).join("/");
|
|
1017
|
+
pattern = pattern.split("/").map(s => /^[:*]/.test(s) ? s : encodeSegment(s)).join("/");
|
|
1029
1018
|
acc.push({
|
|
1030
1019
|
...shared,
|
|
1031
1020
|
originalPath,
|
|
@@ -1116,13 +1105,12 @@ function mergeParams(matches) {
|
|
|
1116
1105
|
return params;
|
|
1117
1106
|
}
|
|
1118
1107
|
function createLocation(path, state, queryWrapper) {
|
|
1119
|
-
const
|
|
1120
|
-
const url = createMemo((prev = origin) => {
|
|
1108
|
+
const url = createMemo((prev = new URL(mockBase)) => {
|
|
1121
1109
|
const path_ = path();
|
|
1122
1110
|
try {
|
|
1123
1111
|
// anchor rooted paths against the origin explicitly - a path with
|
|
1124
1112
|
// doubled leading slashes would otherwise parse as protocol-relative
|
|
1125
|
-
return new URL(path_[0] === "/" ? mockBase + path_ : path_,
|
|
1113
|
+
return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
|
|
1126
1114
|
} catch (err) {
|
|
1127
1115
|
DEV && console.error(`Invalid path ${path_}`);
|
|
1128
1116
|
return prev;
|
|
@@ -1133,8 +1121,8 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1133
1121
|
const pathname = createMemo(() => url().pathname);
|
|
1134
1122
|
const search = createMemo(() => url().search);
|
|
1135
1123
|
const hash = createMemo(() => url().hash);
|
|
1136
|
-
|
|
1137
|
-
const queryFn = createMemo(() => extractSearchParams(
|
|
1124
|
+
// Pathname and hash changes do not affect the query.
|
|
1125
|
+
const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
|
|
1138
1126
|
return {
|
|
1139
1127
|
get pathname() {
|
|
1140
1128
|
return pathname();
|
|
@@ -1149,7 +1137,7 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1149
1137
|
return state();
|
|
1150
1138
|
},
|
|
1151
1139
|
get key() {
|
|
1152
|
-
return
|
|
1140
|
+
return "";
|
|
1153
1141
|
},
|
|
1154
1142
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
1155
1143
|
};
|
|
@@ -1315,7 +1303,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1315
1303
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1316
1304
|
const result = resolveLazySubtree(record);
|
|
1317
1305
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1318
|
-
}, {
|
|
1306
|
+
}, DEV && {
|
|
1319
1307
|
name: "lazyRoutes"
|
|
1320
1308
|
}));
|
|
1321
1309
|
lazyReaders.set(record, read);
|
|
@@ -1348,7 +1336,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1348
1336
|
}
|
|
1349
1337
|
}
|
|
1350
1338
|
return m;
|
|
1351
|
-
}, {
|
|
1339
|
+
}, DEV && {
|
|
1352
1340
|
name: "matches"
|
|
1353
1341
|
});
|
|
1354
1342
|
const routingPending = createMemo(() => isPending(() => {
|
|
@@ -1359,7 +1347,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1359
1347
|
}
|
|
1360
1348
|
location.search;
|
|
1361
1349
|
location.hash;
|
|
1362
|
-
}), {
|
|
1350
|
+
}), DEV && {
|
|
1363
1351
|
name: "routingPending"
|
|
1364
1352
|
});
|
|
1365
1353
|
const isRouting = () => routingPending() || isPending(source);
|
|
@@ -1565,11 +1553,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1565
1553
|
route,
|
|
1566
1554
|
params
|
|
1567
1555
|
} = next[match];
|
|
1568
|
-
route.component && route.component.preload && route.component.preload();
|
|
1569
1556
|
const {
|
|
1570
1557
|
preload,
|
|
1571
1558
|
component
|
|
1572
1559
|
} = route;
|
|
1560
|
+
component?.preload?.();
|
|
1573
1561
|
const now = current && current[match];
|
|
1574
1562
|
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1575
1563
|
inPreloadFn = true;
|
|
@@ -1633,7 +1621,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1633
1621
|
// its last valid value while this route is being torn down, so outgoing
|
|
1634
1622
|
// components and preloads never observe another route's params.
|
|
1635
1623
|
const params = wrapParams(() => mergeParams(matches()));
|
|
1636
|
-
component
|
|
1624
|
+
component?.preload?.();
|
|
1637
1625
|
inPreloadFn = true;
|
|
1638
1626
|
const data = preload ? preload({
|
|
1639
1627
|
params,
|
|
@@ -1760,9 +1748,7 @@ function Routes(props) {
|
|
|
1760
1748
|
next[i] = prev[i];
|
|
1761
1749
|
} else {
|
|
1762
1750
|
equal = false;
|
|
1763
|
-
|
|
1764
|
-
disposers[i]();
|
|
1765
|
-
}
|
|
1751
|
+
disposers[i]?.();
|
|
1766
1752
|
runWithOwner(owner, () => createRoot(dispose => {
|
|
1767
1753
|
disposers[i] = dispose;
|
|
1768
1754
|
const routeKey = nextMatch.route.key;
|
|
@@ -1851,13 +1837,14 @@ function bindEvent(target, type, handler) {
|
|
|
1851
1837
|
|
|
1852
1838
|
let depth;
|
|
1853
1839
|
function saveCurrentDepth() {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1840
|
+
const h = window.history;
|
|
1841
|
+
if (!h.state || h.state._depth == null) {
|
|
1842
|
+
h.replaceState({
|
|
1843
|
+
...h.state,
|
|
1844
|
+
_depth: h.length - 1
|
|
1858
1845
|
}, "");
|
|
1859
1846
|
}
|
|
1860
|
-
depth =
|
|
1847
|
+
depth = h.state?._depth ?? h.length - 1;
|
|
1861
1848
|
}
|
|
1862
1849
|
function keepDepth(state) {
|
|
1863
1850
|
return {
|
|
@@ -1901,11 +1888,10 @@ function scrollToHash(hash, fallbackTop) {
|
|
|
1901
1888
|
|
|
1902
1889
|
function browserHistory() {
|
|
1903
1890
|
const getSource = () => {
|
|
1904
|
-
const
|
|
1905
|
-
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
1891
|
+
const s = window.history.state;
|
|
1906
1892
|
return {
|
|
1907
|
-
value:
|
|
1908
|
-
state
|
|
1893
|
+
value: window.location.pathname + window.location.search + window.location.hash,
|
|
1894
|
+
state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
|
|
1909
1895
|
};
|
|
1910
1896
|
};
|
|
1911
1897
|
const beforeLeave = {};
|
|
@@ -2133,7 +2119,7 @@ function createScrollRestoration() {
|
|
|
2133
2119
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
2134
2120
|
// navigation starts a new one and belongs at the top); the effect's
|
|
2135
2121
|
// initial run performs the restore after first render
|
|
2136
|
-
const [nav] = performance.getEntriesByType
|
|
2122
|
+
const [nav] = performance.getEntriesByType?.("navigation");
|
|
2137
2123
|
if (nav && nav.type !== "navigate") pending = depth();
|
|
2138
2124
|
}
|
|
2139
2125
|
};
|
|
@@ -2229,7 +2215,6 @@ function describeNavigation(match, next, written, from) {
|
|
|
2229
2215
|
const t = to();
|
|
2230
2216
|
return t === undefined ? undefined : new URL(t, mockBase).pathname;
|
|
2231
2217
|
};
|
|
2232
|
-
const matches = pathname => untrack(() => match(pathname));
|
|
2233
2218
|
const ref = {
|
|
2234
2219
|
kind: "navigation",
|
|
2235
2220
|
get to() {
|
|
@@ -2237,21 +2222,62 @@ function describeNavigation(match, next, written, from) {
|
|
|
2237
2222
|
},
|
|
2238
2223
|
from,
|
|
2239
2224
|
get name() {
|
|
2240
|
-
|
|
2241
|
-
if (p === undefined) return undefined;
|
|
2242
|
-
const m = matches(p);
|
|
2243
|
-
return m.length ? m[m.length - 1].route.pattern || "/" : p;
|
|
2225
|
+
return routeName(match, pathname());
|
|
2244
2226
|
},
|
|
2245
2227
|
get params() {
|
|
2246
|
-
|
|
2247
|
-
const m = p === undefined ? [] : matches(p);
|
|
2248
|
-
return m.length ? mergeParams(m) : undefined;
|
|
2228
|
+
return routeParams(match, pathname());
|
|
2249
2229
|
}
|
|
2250
2230
|
};
|
|
2251
2231
|
if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
|
|
2252
2232
|
return ref;
|
|
2253
2233
|
}
|
|
2254
2234
|
|
|
2235
|
+
/** The parametrized route `pathname` matches — the last match's pattern, `/` for the root; the pathname itself when nothing matched. */
|
|
2236
|
+
function routeName(match, pathname) {
|
|
2237
|
+
if (pathname === undefined) return undefined;
|
|
2238
|
+
const m = untrack(() => match(pathname));
|
|
2239
|
+
return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/** The params the matched chain bound, merged; `undefined` when nothing matched. */
|
|
2243
|
+
function routeParams(match, pathname) {
|
|
2244
|
+
if (pathname === undefined) return undefined;
|
|
2245
|
+
const m = untrack(() => match(pathname));
|
|
2246
|
+
return m.length ? mergeParams(m) : undefined;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* The route the document arrived on, for solid's observe tier: the same
|
|
2251
|
+
* declaration as a navigation's, around the work that establishes the
|
|
2252
|
+
* router's initial match — building its context — since a fresh document has
|
|
2253
|
+
* no location write to wrap. On the client the engine opens it at the time
|
|
2254
|
+
* origin and settles it as the context is built (the first `"navigation"`
|
|
2255
|
+
* record, `initial: true`); on the server the same call names the request's
|
|
2256
|
+
* `"render"` record (`RenderEvent.route`). Every field is a getter read at
|
|
2257
|
+
* settle, as for a navigation: `to` is the location as the router then holds
|
|
2258
|
+
* it — the arrival normalized onto the base path when it came in empty, a
|
|
2259
|
+
* write the frame itself took (see the setter) — and `name`/`params` match
|
|
2260
|
+
* it then, so a lazy subtree that resolved while the server rendered names
|
|
2261
|
+
* the exact route.
|
|
2262
|
+
*/
|
|
2263
|
+
function describeInitial(match, location) {
|
|
2264
|
+
const to = () => untrack(location).value;
|
|
2265
|
+
const pathname = () => new URL(to(), mockBase).pathname;
|
|
2266
|
+
return {
|
|
2267
|
+
kind: "navigation",
|
|
2268
|
+
initial: true,
|
|
2269
|
+
get to() {
|
|
2270
|
+
return to();
|
|
2271
|
+
},
|
|
2272
|
+
get name() {
|
|
2273
|
+
return routeName(match, pathname());
|
|
2274
|
+
},
|
|
2275
|
+
get params() {
|
|
2276
|
+
return routeParams(match, pathname());
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2255
2281
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2256
2282
|
function createIntegration(history, match) {
|
|
2257
2283
|
let committing = false;
|
|
@@ -2266,7 +2292,6 @@ function createIntegration(history, match) {
|
|
|
2266
2292
|
name: "location"
|
|
2267
2293
|
});
|
|
2268
2294
|
const signal = [read, next => {
|
|
2269
|
-
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
2270
2295
|
// What the write resolved to, or undefined when there was nothing to
|
|
2271
2296
|
// write (see `resolveLocationWrite`).
|
|
2272
2297
|
let written;
|
|
@@ -2299,7 +2324,14 @@ function createIntegration(history, match) {
|
|
|
2299
2324
|
// while a navigation is pending, which is the `from` a hop wants too.
|
|
2300
2325
|
// A write the no-op rule drops is still declared: the engine settles a
|
|
2301
2326
|
// navigation whose write "did not survive the equality gate" on the spot.
|
|
2302
|
-
|
|
2327
|
+
// A write from NO location is not a navigation: it is the arrival being
|
|
2328
|
+
// normalized (`createRouterContext` moving an empty location — a hash
|
|
2329
|
+
// history with no hash, a memory history seeded with "" — onto the base
|
|
2330
|
+
// path), and the arrival is already declared by the initial frame open
|
|
2331
|
+
// around the context build (`describeInitial`), which this write then
|
|
2332
|
+
// stamps. The location is never empty again after that first write.
|
|
2333
|
+
const from = untrack(read).value;
|
|
2334
|
+
OBSERVE && from ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, from), commit) : commit();
|
|
2303
2335
|
}];
|
|
2304
2336
|
history.init && onCleanup(history.init((value = history.get()) => {
|
|
2305
2337
|
if (committing) return;
|
|
@@ -2380,11 +2412,14 @@ function createRouter(config) {
|
|
|
2380
2412
|
}
|
|
2381
2413
|
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
2382
2414
|
let context;
|
|
2383
|
-
const
|
|
2415
|
+
const buildContext = () => createRouterContext(integration, branches, () => context, {
|
|
2384
2416
|
base: basePath,
|
|
2385
2417
|
singleFlight: config.singleFlight,
|
|
2386
2418
|
transformUrl: config.transformUrl
|
|
2387
2419
|
});
|
|
2420
|
+
// The route the document arrived on, declared around the initial match
|
|
2421
|
+
// (see `describeInitial`); folded out of shipped bundles with `OBSERVE`.
|
|
2422
|
+
const routerState = OBSERVE ? OBSERVE.attribution.withOrigin(describeInitial(matchPath, integration.signal[0]), buildContext) : buildContext();
|
|
2388
2423
|
if (!isServer) {
|
|
2389
2424
|
setupNativeEvents({
|
|
2390
2425
|
preload: config.preloadLinks,
|
|
@@ -2432,11 +2467,10 @@ function createRouter(config) {
|
|
|
2432
2467
|
}));
|
|
2433
2468
|
},
|
|
2434
2469
|
keysFor(url) {
|
|
2435
|
-
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2436
2470
|
const keys = [];
|
|
2437
2471
|
for (const {
|
|
2438
2472
|
route
|
|
2439
|
-
} of matchPath(
|
|
2473
|
+
} of matchPath(pathnameOf(url))) {
|
|
2440
2474
|
const key = serverRouteOf(route.component)?.call?.key;
|
|
2441
2475
|
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
2442
2476
|
}
|
|
@@ -2623,10 +2657,22 @@ source) {
|
|
|
2623
2657
|
const LocationHeader = "Location";
|
|
2624
2658
|
const PRELOAD_TIMEOUT$1 = 5000;
|
|
2625
2659
|
const CACHE_TIMEOUT = 180000;
|
|
2626
|
-
|
|
2660
|
+
const responseBodyHeaders = /* #__PURE__ */new Set(["content-type", "content-length", "content-encoding", "transfer-encoding", "content-disposition", "content-language", "content-location", "content-range", "accept-ranges", "etag", "last-modified"]);
|
|
2661
|
+
const forwardedCookies = /* #__PURE__ */new WeakMap();
|
|
2662
|
+
// When this client booted. Flight-registry entries (takeHydrationValue)
|
|
2627
2663
|
// hold values the server computed while rendering THIS page, so their age is
|
|
2628
2664
|
// anchored here — not at whenever a late query() call happens to consume one.
|
|
2629
2665
|
const bootTime = Date.now();
|
|
2666
|
+
|
|
2667
|
+
// Every hydratable read ships its entry: the writer keeps the first write
|
|
2668
|
+
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
2669
|
+
// from an island that reads the same key later.
|
|
2670
|
+
function serializeForHydration(key, res) {
|
|
2671
|
+
const writer = getHydrationWriter();
|
|
2672
|
+
if (!writer || !writer.async || !isHydratable()) return;
|
|
2673
|
+
const e = getRequestEvent();
|
|
2674
|
+
(!e || !e.serverOnly) && writer.write(key, res);
|
|
2675
|
+
}
|
|
2630
2676
|
let cacheMap = new Map();
|
|
2631
2677
|
|
|
2632
2678
|
// cleanup forward/back cache
|
|
@@ -2751,10 +2797,11 @@ function query(fn, name) {
|
|
|
2751
2797
|
}
|
|
2752
2798
|
let res = cached[1];
|
|
2753
2799
|
if (intent !== "preload") {
|
|
2754
|
-
res =
|
|
2800
|
+
res = settle(res);
|
|
2755
2801
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
2756
2802
|
}
|
|
2757
2803
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2804
|
+
isServer && serializeForHydration(key, res);
|
|
2758
2805
|
return res;
|
|
2759
2806
|
}
|
|
2760
2807
|
let res;
|
|
@@ -2771,13 +2818,11 @@ function query(fn, name) {
|
|
|
2771
2818
|
// active navigation (or its preload) accepts the payload only while a
|
|
2772
2819
|
// real preload of the same age would still satisfy it — anything older
|
|
2773
2820
|
// refetches instead of serving a minutes-old value on a fresh navigation.
|
|
2774
|
-
if (!isServer &&
|
|
2775
|
-
const
|
|
2776
|
-
if (
|
|
2821
|
+
if (!isServer && (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1))) {
|
|
2822
|
+
const entry = takeHydrationValue(key); // hydrating
|
|
2823
|
+
if (entry) {
|
|
2777
2824
|
adopted = true;
|
|
2778
|
-
res =
|
|
2779
|
-
// @ts-ignore at least until we add a delete method to sharedConfig
|
|
2780
|
-
delete globalThis._$HY.r[key];
|
|
2825
|
+
res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? Promise.resolve(entry.value) : Promise.reject(entry.error);
|
|
2781
2826
|
}
|
|
2782
2827
|
// A payload too old for this intent stays in the registry: it is
|
|
2783
2828
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -2793,6 +2838,7 @@ function query(fn, name) {
|
|
|
2793
2838
|
// it would for a preload performed when the server fetched the data.
|
|
2794
2839
|
const stamp = adopted ? bootTime : now;
|
|
2795
2840
|
if (cached) {
|
|
2841
|
+
isServer && forwardedCookies.delete(cached);
|
|
2796
2842
|
cached[0] = stamp;
|
|
2797
2843
|
cached[1] = res;
|
|
2798
2844
|
cached[3] = intent;
|
|
@@ -2811,17 +2857,18 @@ function query(fn, name) {
|
|
|
2811
2857
|
const e = getRequestEvent();
|
|
2812
2858
|
if (e && e.router.dataOnly) return e.router.data[key] = res;
|
|
2813
2859
|
}
|
|
2814
|
-
if (intent !== "preload")
|
|
2815
|
-
res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
|
|
2816
|
-
}
|
|
2860
|
+
if (intent !== "preload") res = settle(res);
|
|
2817
2861
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2818
|
-
|
|
2819
|
-
if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
|
|
2820
|
-
const e = getRequestEvent();
|
|
2821
|
-
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
2822
|
-
}
|
|
2862
|
+
isServer && serializeForHydration(key, res);
|
|
2823
2863
|
return res;
|
|
2824
|
-
|
|
2864
|
+
|
|
2865
|
+
// a settled read: the response handling applied to the value or the promise
|
|
2866
|
+
function settle(v) {
|
|
2867
|
+
let cookies = isServer ? forwardedCookies.get(cached) : undefined;
|
|
2868
|
+
if (isServer && !cookies) forwardedCookies.set(cached, cookies = new WeakSet());
|
|
2869
|
+
return "then" in v ? v.then(handleResponse(false, cookies), handleResponse(true, cookies)) : handleResponse(false, cookies)(v);
|
|
2870
|
+
}
|
|
2871
|
+
function handleResponse(error, cookies) {
|
|
2825
2872
|
return async v => {
|
|
2826
2873
|
let enveloped;
|
|
2827
2874
|
let hasEnveloped = false;
|
|
@@ -2834,8 +2881,18 @@ function query(fn, name) {
|
|
|
2834
2881
|
if (v instanceof Response) {
|
|
2835
2882
|
const e = getRequestEvent();
|
|
2836
2883
|
if (e) {
|
|
2884
|
+
const headers = e.response.headers;
|
|
2885
|
+
const skipCookies = cookies?.has(headers);
|
|
2837
2886
|
for (const [key, value] of v.headers) {
|
|
2838
|
-
|
|
2887
|
+
// Keep metadata for the query's body off the document response.
|
|
2888
|
+
if (responseBodyHeaders.has(key)) continue;
|
|
2889
|
+
if (key === "set-cookie") {
|
|
2890
|
+
if (!skipCookies) headers.append(key, value);
|
|
2891
|
+
} else if (headers.get(key) !== value) headers.set(key, value);
|
|
2892
|
+
}
|
|
2893
|
+
// Each cached result contributes its cookies once per document.
|
|
2894
|
+
if (!skipCookies && v.headers.has("set-cookie")) {
|
|
2895
|
+
cookies?.add(headers);
|
|
2839
2896
|
}
|
|
2840
2897
|
}
|
|
2841
2898
|
let url = v.headers.get(LocationHeader);
|
|
@@ -2918,6 +2975,7 @@ query.set = (key, value) => {
|
|
|
2918
2975
|
const now = Date.now();
|
|
2919
2976
|
let cached = cache.get(key);
|
|
2920
2977
|
if (cached) {
|
|
2978
|
+
isServer && forwardedCookies.delete(cached);
|
|
2921
2979
|
cached[0] = now;
|
|
2922
2980
|
cached[1] = Promise.resolve(value);
|
|
2923
2981
|
cached[2] = value;
|
|
@@ -2965,7 +3023,7 @@ function hashKey(args) {
|
|
|
2965
3023
|
return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
|
|
2966
3024
|
result[key] = val[key];
|
|
2967
3025
|
return result;
|
|
2968
|
-
},
|
|
3026
|
+
}, Object.create(null)) : val);
|
|
2969
3027
|
}
|
|
2970
3028
|
function isPlainObject(obj) {
|
|
2971
3029
|
let proto;
|
|
@@ -2997,7 +3055,8 @@ const actions = /* #__PURE__ */new Map();
|
|
|
2997
3055
|
*/
|
|
2998
3056
|
function handleFormAction(evt, router, actionBase) {
|
|
2999
3057
|
if (evt.defaultPrevented) return;
|
|
3000
|
-
|
|
3058
|
+
const form = evt.target;
|
|
3059
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : form.getAttribute("action");
|
|
3001
3060
|
if (!actionRef) return;
|
|
3002
3061
|
const serverAction = !actionRef.startsWith("https://action/");
|
|
3003
3062
|
if (serverAction) {
|
|
@@ -3006,7 +3065,7 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3006
3065
|
actionRef = router.parsePath(url.pathname + url.search);
|
|
3007
3066
|
if (!actionRef.startsWith(actionBase)) return;
|
|
3008
3067
|
}
|
|
3009
|
-
if (
|
|
3068
|
+
if (form.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
3010
3069
|
// A registry miss on a server-action url is a direct bind whose module
|
|
3011
3070
|
// never loaded client-side (server components): the url is self-describing
|
|
3012
3071
|
// (the id in the path, bound `?args` in the query), so a generic invocation
|
|
@@ -3017,11 +3076,11 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3017
3076
|
const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
|
|
3018
3077
|
if (handler) {
|
|
3019
3078
|
evt.preventDefault();
|
|
3020
|
-
const data = new FormData(
|
|
3079
|
+
const data = new FormData(form, evt.submitter);
|
|
3021
3080
|
handler.call({
|
|
3022
3081
|
r: router,
|
|
3023
|
-
f:
|
|
3024
|
-
},
|
|
3082
|
+
f: form
|
|
3083
|
+
}, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
3025
3084
|
}
|
|
3026
3085
|
}
|
|
3027
3086
|
|
|
@@ -3128,23 +3187,21 @@ function actionImpl(fn, options = {}) {
|
|
|
3128
3187
|
const runMutation = () => fn(...variables);
|
|
3129
3188
|
const run = action$1(async function* (context) {
|
|
3130
3189
|
context.optimistic?.();
|
|
3190
|
+
let value;
|
|
3191
|
+
let error = false;
|
|
3131
3192
|
try {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
value
|
|
3137
|
-
};
|
|
3138
|
-
} catch (error) {
|
|
3139
|
-
yield;
|
|
3140
|
-
return {
|
|
3141
|
-
error: true,
|
|
3142
|
-
value: error
|
|
3143
|
-
};
|
|
3193
|
+
value = await context.call();
|
|
3194
|
+
} catch (e) {
|
|
3195
|
+
value = e;
|
|
3196
|
+
error = true;
|
|
3144
3197
|
}
|
|
3198
|
+
const read = await readResponse(value, error);
|
|
3199
|
+
yield;
|
|
3200
|
+
// Apply inside the transition so the default revalidation's refetch and
|
|
3201
|
+
// the release of the caller's optimistic writes commit as one frame (#619).
|
|
3202
|
+
return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
3145
3203
|
});
|
|
3146
3204
|
form && setFormBusy(form, 1);
|
|
3147
|
-
let settled;
|
|
3148
3205
|
let response;
|
|
3149
3206
|
// The transport consumer is awaited before a single-flight mutation
|
|
3150
3207
|
// resolves, so a counter delta over the call tells whether this action's
|
|
@@ -3154,13 +3211,12 @@ function actionImpl(fn, options = {}) {
|
|
|
3154
3211
|
// which misses every response the server returned without flight data.
|
|
3155
3212
|
const flightApplicationsBefore = flightApplications;
|
|
3156
3213
|
try {
|
|
3157
|
-
|
|
3214
|
+
response = await settleActionResult(run({
|
|
3158
3215
|
call: runMutation,
|
|
3159
3216
|
optimistic: submitHooks.size ? () => {
|
|
3160
3217
|
for (const hook of submitHooks.values()) hook(...variables);
|
|
3161
3218
|
} : undefined
|
|
3162
3219
|
}));
|
|
3163
|
-
response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
3164
3220
|
} finally {
|
|
3165
3221
|
form && setFormBusy(form, -1);
|
|
3166
3222
|
}
|
|
@@ -3168,8 +3224,8 @@ function actionImpl(fn, options = {}) {
|
|
|
3168
3224
|
submission = {
|
|
3169
3225
|
input: variables,
|
|
3170
3226
|
url,
|
|
3171
|
-
result: response
|
|
3172
|
-
error: response
|
|
3227
|
+
result: response?.data,
|
|
3228
|
+
error: response?.error,
|
|
3173
3229
|
clear() {
|
|
3174
3230
|
router.submissions[1](entries => entries.filter(entry => entry !== submission));
|
|
3175
3231
|
},
|
|
@@ -3215,8 +3271,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
3215
3271
|
fn.with = function (...args) {
|
|
3216
3272
|
const uri = new URL(url, mockBase);
|
|
3217
3273
|
uri.searchParams.set("args", hashKey(args));
|
|
3218
|
-
|
|
3219
|
-
return next;
|
|
3274
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
|
|
3220
3275
|
};
|
|
3221
3276
|
fn.onSubmit = function (hook) {
|
|
3222
3277
|
const id = Symbol("actionOnSubmitHook");
|
|
@@ -3335,8 +3390,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
3335
3390
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
3336
3391
|
// set cache — and fan the payload out to other keyed stores (live query
|
|
3337
3392
|
// channels adopt delivered values instead of reconnecting)
|
|
3338
|
-
|
|
3339
|
-
|
|
3393
|
+
if (flightData) {
|
|
3394
|
+
Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
3395
|
+
deliverFlightData(flightData);
|
|
3396
|
+
}
|
|
3340
3397
|
// trigger revalidation inside the same transition as the navigation, so
|
|
3341
3398
|
// the redirect commits atomically with fresh data: surviving consumers
|
|
3342
3399
|
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
@@ -3344,7 +3401,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
3344
3401
|
// are fresh again by now, so the sweep re-reads them from cache.
|
|
3345
3402
|
revalidate(keys, false);
|
|
3346
3403
|
}
|
|
3347
|
-
async function
|
|
3404
|
+
async function readResponse(response, error) {
|
|
3348
3405
|
let data;
|
|
3349
3406
|
let flightData;
|
|
3350
3407
|
let metadata;
|
|
@@ -3367,6 +3424,19 @@ async function handleResponse(response, error, navigate, metadataHandled) {
|
|
|
3367
3424
|
} else if (error) return {
|
|
3368
3425
|
error: response
|
|
3369
3426
|
};else data = response;
|
|
3427
|
+
return {
|
|
3428
|
+
data,
|
|
3429
|
+
flightData,
|
|
3430
|
+
metadata
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
function applyResponse(read, navigate, metadataHandled) {
|
|
3434
|
+
if ("error" in read) return read;
|
|
3435
|
+
const {
|
|
3436
|
+
data,
|
|
3437
|
+
flightData,
|
|
3438
|
+
metadata
|
|
3439
|
+
} = read;
|
|
3370
3440
|
// The transport consumer applies metadata before returning a server
|
|
3371
3441
|
// function's unwrapped value. Do not treat that value as a second plain
|
|
3372
3442
|
// action response and invalidate the freshly seeded query cache again.
|
package/dist/paths.js
CHANGED
|
@@ -37,14 +37,10 @@ export function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
37
37
|
};
|
|
38
38
|
return new Proxy(build, {
|
|
39
39
|
get(_, prop) {
|
|
40
|
-
if (prop === "toString")
|
|
40
|
+
if (prop === "toString" || prop === Symbol.toPrimitive)
|
|
41
41
|
return () => toHref(pathname);
|
|
42
42
|
if (typeof prop === "symbol")
|
|
43
|
-
return prop ===
|
|
44
|
-
? () => toHref(pathname)
|
|
45
|
-
: prop === HREF
|
|
46
|
-
? pathname || "/"
|
|
47
|
-
: undefined;
|
|
43
|
+
return prop === HREF ? pathname || "/" : undefined;
|
|
48
44
|
return node(`${pathname}/${prop}`);
|
|
49
45
|
}
|
|
50
46
|
});
|