@solidjs/router 2.0.0-next.29 → 2.0.0-next.31
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 +9 -10
- package/dist/fs.d.ts +1 -1
- package/dist/fs.js +1 -1
- package/dist/index.js +89 -85
- package/dist/paths.js +2 -6
- package/dist/routers/components.jsx +1 -3
- package/dist/routers/factory.jsx +1 -2
- package/dist/routers/history.js +7 -11
- package/dist/routers/scrollRestoration.js +1 -2
- package/dist/routing.js +9 -17
- package/dist/serverRouteShared.js +7 -3
- package/dist/types.d.ts +13 -4
- package/dist/utils.js +12 -22
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -850,7 +850,7 @@ const isRouting = useIsRouting();
|
|
|
850
850
|
return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
851
851
|
```
|
|
852
852
|
|
|
853
|
-
In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. `attribution.
|
|
853
|
+
In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. `attribution.history("navigation")` and `feedback().navigations` list them; nothing of this exists in production builds.
|
|
854
854
|
|
|
855
855
|
### useMatch
|
|
856
856
|
|
package/dist/data/action.js
CHANGED
|
@@ -29,9 +29,10 @@ export const actions = /* #__PURE__ */ new Map();
|
|
|
29
29
|
export function handleFormAction(evt, router, actionBase) {
|
|
30
30
|
if (evt.defaultPrevented)
|
|
31
31
|
return;
|
|
32
|
+
const form = evt.target;
|
|
32
33
|
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
33
34
|
? evt.submitter.getAttribute("formaction")
|
|
34
|
-
:
|
|
35
|
+
: form.getAttribute("action");
|
|
35
36
|
if (!actionRef)
|
|
36
37
|
return;
|
|
37
38
|
const serverAction = !actionRef.startsWith("https://action/");
|
|
@@ -42,7 +43,7 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
42
43
|
if (!actionRef.startsWith(actionBase))
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
if (
|
|
46
|
+
if (form.method.toUpperCase() !== "POST")
|
|
46
47
|
throw new Error("Only POST forms are supported for Actions");
|
|
47
48
|
// A registry miss on a server-action url is a direct bind whose module
|
|
48
49
|
// never loaded client-side (server components): the url is self-describing
|
|
@@ -54,10 +55,8 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
54
55
|
const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
55
56
|
if (handler) {
|
|
56
57
|
evt.preventDefault();
|
|
57
|
-
const data = new FormData(
|
|
58
|
-
handler.call({ r: router, f:
|
|
59
|
-
? data
|
|
60
|
-
: new URLSearchParams(data));
|
|
58
|
+
const data = new FormData(form, evt.submitter);
|
|
59
|
+
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
/**
|
|
@@ -159,18 +158,22 @@ function actionImpl(fn, options = {}) {
|
|
|
159
158
|
const runMutation = () => fn(...variables);
|
|
160
159
|
const run = createSolidAction(async function* (context) {
|
|
161
160
|
context.optimistic?.();
|
|
161
|
+
let value;
|
|
162
|
+
let error = false;
|
|
162
163
|
try {
|
|
163
|
-
|
|
164
|
-
yield;
|
|
165
|
-
return { error: false, value };
|
|
164
|
+
value = await context.call();
|
|
166
165
|
}
|
|
167
|
-
catch (
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
catch (e) {
|
|
167
|
+
value = e;
|
|
168
|
+
error = true;
|
|
170
169
|
}
|
|
170
|
+
const read = await readResponse(value, error);
|
|
171
|
+
yield;
|
|
172
|
+
// Apply inside the transition so the default revalidation's refetch and
|
|
173
|
+
// the release of the caller's optimistic writes commit as one frame (#619).
|
|
174
|
+
return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
171
175
|
});
|
|
172
176
|
form && setFormBusy(form, 1);
|
|
173
|
-
let settled;
|
|
174
177
|
let response;
|
|
175
178
|
// The transport consumer is awaited before a single-flight mutation
|
|
176
179
|
// resolves, so a counter delta over the call tells whether this action's
|
|
@@ -180,7 +183,7 @@ function actionImpl(fn, options = {}) {
|
|
|
180
183
|
// which misses every response the server returned without flight data.
|
|
181
184
|
const flightApplicationsBefore = flightApplications;
|
|
182
185
|
try {
|
|
183
|
-
|
|
186
|
+
response = await settleActionResult(run({
|
|
184
187
|
call: runMutation,
|
|
185
188
|
optimistic: submitHooks.size
|
|
186
189
|
? () => {
|
|
@@ -189,7 +192,6 @@ function actionImpl(fn, options = {}) {
|
|
|
189
192
|
}
|
|
190
193
|
: undefined
|
|
191
194
|
}));
|
|
192
|
-
response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
193
195
|
}
|
|
194
196
|
finally {
|
|
195
197
|
form && setFormBusy(form, -1);
|
|
@@ -198,8 +200,8 @@ function actionImpl(fn, options = {}) {
|
|
|
198
200
|
submission = {
|
|
199
201
|
input: variables,
|
|
200
202
|
url,
|
|
201
|
-
result: response
|
|
202
|
-
error: response
|
|
203
|
+
result: response?.data,
|
|
204
|
+
error: response?.error,
|
|
203
205
|
clear() {
|
|
204
206
|
router.submissions[1](entries => entries.filter(entry => entry !== submission));
|
|
205
207
|
},
|
|
@@ -244,8 +246,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
244
246
|
fn.with = function (...args) {
|
|
245
247
|
const uri = new URL(url, mockBase);
|
|
246
248
|
uri.searchParams.set("args", hashKey(args));
|
|
247
|
-
|
|
248
|
-
return next;
|
|
249
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
|
|
249
250
|
};
|
|
250
251
|
fn.onSubmit = function (hook) {
|
|
251
252
|
const id = Symbol("actionOnSubmitHook");
|
|
@@ -362,8 +363,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
362
363
|
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
363
364
|
// set cache — and fan the payload out to other keyed stores (live query
|
|
364
365
|
// channels adopt delivered values instead of reconnecting)
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
if (flightData) {
|
|
367
|
+
Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
368
|
+
deliverFlightData(flightData);
|
|
369
|
+
}
|
|
367
370
|
// trigger revalidation inside the same transition as the navigation, so
|
|
368
371
|
// the redirect commits atomically with fresh data: surviving consumers
|
|
369
372
|
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
@@ -371,7 +374,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
371
374
|
// are fresh again by now, so the sweep re-reads them from cache.
|
|
372
375
|
revalidate(keys, false);
|
|
373
376
|
}
|
|
374
|
-
async function
|
|
377
|
+
async function readResponse(response, error) {
|
|
375
378
|
let data;
|
|
376
379
|
let flightData;
|
|
377
380
|
let metadata;
|
|
@@ -397,6 +400,12 @@ async function handleResponse(response, error, navigate, metadataHandled) {
|
|
|
397
400
|
return { error: response };
|
|
398
401
|
else
|
|
399
402
|
data = response;
|
|
403
|
+
return { data, flightData, metadata };
|
|
404
|
+
}
|
|
405
|
+
function applyResponse(read, navigate, metadataHandled) {
|
|
406
|
+
if ("error" in read)
|
|
407
|
+
return read;
|
|
408
|
+
const { data, flightData, metadata } = read;
|
|
400
409
|
// The transport consumer applies metadata before returning a server
|
|
401
410
|
// function's unwrapped value. Do not treat that value as a second plain
|
|
402
411
|
// action response and invalidate the freshly seeded query cache again.
|
package/dist/data/events.js
CHANGED
|
@@ -32,7 +32,7 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
32
32
|
if (target || (!href && !a.hasAttribute("state")))
|
|
33
33
|
return;
|
|
34
34
|
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
35
|
-
if (a.hasAttribute("download") ||
|
|
35
|
+
if (a.hasAttribute("download") || rel.includes("external"))
|
|
36
36
|
return;
|
|
37
37
|
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
38
38
|
// Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
|
package/dist/data/query.js
CHANGED
|
@@ -156,10 +156,7 @@ export function query(fn, name) {
|
|
|
156
156
|
}
|
|
157
157
|
let res = cached[1];
|
|
158
158
|
if (intent !== "preload") {
|
|
159
|
-
res =
|
|
160
|
-
"then" in cached[1]
|
|
161
|
-
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
162
|
-
: handleResponse(false)(cached[1]);
|
|
159
|
+
res = settle(res);
|
|
163
160
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
164
161
|
}
|
|
165
162
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
@@ -225,12 +222,8 @@ export function query(fn, name) {
|
|
|
225
222
|
if (e && e.router.dataOnly)
|
|
226
223
|
return (e.router.data[key] = res);
|
|
227
224
|
}
|
|
228
|
-
if (intent !== "preload")
|
|
229
|
-
res =
|
|
230
|
-
"then" in res
|
|
231
|
-
? res.then(handleResponse(false), handleResponse(true))
|
|
232
|
-
: handleResponse(false)(res);
|
|
233
|
-
}
|
|
225
|
+
if (intent !== "preload")
|
|
226
|
+
res = settle(res);
|
|
234
227
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
235
228
|
// serialize on server
|
|
236
229
|
if (isServer &&
|
|
@@ -241,6 +234,12 @@ export function query(fn, name) {
|
|
|
241
234
|
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
242
235
|
}
|
|
243
236
|
return res;
|
|
237
|
+
// a settled read: the response handling applied to the value or the promise
|
|
238
|
+
function settle(v) {
|
|
239
|
+
return "then" in v
|
|
240
|
+
? v.then(handleResponse(false), handleResponse(true))
|
|
241
|
+
: handleResponse(false)(v);
|
|
242
|
+
}
|
|
244
243
|
function handleResponse(error) {
|
|
245
244
|
return async (v) => {
|
|
246
245
|
let enveloped;
|
package/dist/fs.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type FileRouteConfig<S extends string = string, T = unknown, F = undefine
|
|
|
35
35
|
* `matchFilters` are validated against the pattern's params, and their
|
|
36
36
|
* types (and any `search` schema) flow through the manifest into `paths`.
|
|
37
37
|
*/
|
|
38
|
-
export declare function defineFileRoute<S extends string, T = unknown, const F = DefinedRouteFilters<S>, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(path: S, config
|
|
38
|
+
export declare function defineFileRoute<S extends string, T = unknown, const F = DefinedRouteFilters<S>, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(path: S, config?: {
|
|
39
39
|
matchFilters?: (F & ValidFilters<F, S>) | undefined;
|
|
40
40
|
preload?: ((args: RoutePreloadFuncArgs<RouteParams<S>>) => T) | undefined;
|
|
41
41
|
/** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
|
package/dist/fs.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as server from "./fsServer.js";
|
|
|
20
20
|
* `matchFilters` are validated against the pattern's params, and their
|
|
21
21
|
* types (and any `search` schema) flow through the manifest into `paths`.
|
|
22
22
|
*/
|
|
23
|
-
export function defineFileRoute(path, config) {
|
|
23
|
+
export function defineFileRoute(path, config = {}) {
|
|
24
24
|
return config;
|
|
25
25
|
}
|
|
26
26
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError,
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
|
|
2
2
|
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
|
|
@@ -72,13 +72,13 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
72
72
|
path: len ? "" : "/",
|
|
73
73
|
params: {}
|
|
74
74
|
};
|
|
75
|
-
const
|
|
75
|
+
const filters = matchFilters;
|
|
76
76
|
for (let i = 0; i < len; i++) {
|
|
77
77
|
const segment = segments[i];
|
|
78
78
|
const dynamic = segment[0] === ":";
|
|
79
79
|
const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
|
|
80
80
|
const key = dynamic ? segment.slice(1) : segment.toLowerCase();
|
|
81
|
-
if (dynamic && matchSegment(locSegment,
|
|
81
|
+
if (dynamic && matchSegment(locSegment, filters?.[key])) {
|
|
82
82
|
match.params[key] = locSegment;
|
|
83
83
|
} else if (dynamic || !matchSegment(locSegment, key)) {
|
|
84
84
|
return null;
|
|
@@ -87,7 +87,7 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
87
87
|
}
|
|
88
88
|
if (splat) {
|
|
89
89
|
const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
|
|
90
|
-
if (matchSegment(remainder,
|
|
90
|
+
if (matchSegment(remainder, filters?.[splat])) {
|
|
91
91
|
match.params[splat] = remainder;
|
|
92
92
|
} else {
|
|
93
93
|
return null;
|
|
@@ -96,21 +96,9 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
96
96
|
return match;
|
|
97
97
|
};
|
|
98
98
|
}
|
|
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
|
-
}
|
|
99
|
+
|
|
100
|
+
// a string filter is the segment itself; anything else not listed never matches
|
|
101
|
+
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
102
|
function scoreRoute(route) {
|
|
115
103
|
const [pattern, splat] = route.pattern.split("/*", 2);
|
|
116
104
|
const segments = pattern.split("/").filter(Boolean);
|
|
@@ -340,7 +328,7 @@ function setupNativeEvents({
|
|
|
340
328
|
const target = svg ? a.target.baseVal : a.target;
|
|
341
329
|
if (target || !href && !a.hasAttribute("state")) return;
|
|
342
330
|
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
343
|
-
if (a.hasAttribute("download") || rel
|
|
331
|
+
if (a.hasAttribute("download") || rel.includes("external")) return;
|
|
344
332
|
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
345
333
|
// Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
|
|
346
334
|
// inherit the page origin, so the origin check below won't reject them. #382
|
|
@@ -514,8 +502,8 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
514
502
|
};
|
|
515
503
|
return new Proxy(build, {
|
|
516
504
|
get(_, prop) {
|
|
517
|
-
if (prop === "toString") return () => toHref(pathname);
|
|
518
|
-
if (typeof prop === "symbol") return prop ===
|
|
505
|
+
if (prop === "toString" || prop === Symbol.toPrimitive) return () => toHref(pathname);
|
|
506
|
+
if (typeof prop === "symbol") return prop === HREF ? pathname || "/" : undefined;
|
|
519
507
|
return node(`${pathname}/${prop}`);
|
|
520
508
|
}
|
|
521
509
|
});
|
|
@@ -553,7 +541,6 @@ function serverRouteOf(component) {
|
|
|
553
541
|
*/
|
|
554
542
|
function serverRouteArgs(route, params, query) {
|
|
555
543
|
const schema = route.key?.search;
|
|
556
|
-
let search;
|
|
557
544
|
if (schema) {
|
|
558
545
|
const raw = {
|
|
559
546
|
...query
|
|
@@ -561,13 +548,22 @@ function serverRouteArgs(route, params, query) {
|
|
|
561
548
|
const outcome = validateSearch(schema, raw);
|
|
562
549
|
// Issues leave the raw values in place, matching useSearchParams: search
|
|
563
550
|
// strings are user input, so defaults belong in the schema itself.
|
|
564
|
-
|
|
551
|
+
return {
|
|
552
|
+
params: {
|
|
553
|
+
...params
|
|
554
|
+
},
|
|
555
|
+
search: outcome.issues ? raw : outcome.value
|
|
556
|
+
};
|
|
565
557
|
}
|
|
558
|
+
// No schema: leave the key out rather than carrying `search: undefined` —
|
|
559
|
+
// the args are sent as plain JSON by default and the runtime's JSON-safety
|
|
560
|
+
// check rejects explicit `undefined` (#615). Destructuring on the server
|
|
561
|
+
// reads the same either way, and the args-memo equality treats an absent
|
|
562
|
+
// key and undefined alike.
|
|
566
563
|
return {
|
|
567
564
|
params: {
|
|
568
565
|
...params
|
|
569
|
-
}
|
|
570
|
-
search
|
|
566
|
+
}
|
|
571
567
|
};
|
|
572
568
|
}
|
|
573
569
|
const shallowEqual = (a, b) => a === b || !!a && !!b && typeof a === "object" && typeof b === "object" && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(k => a[k] === b[k]);
|
|
@@ -1015,9 +1011,7 @@ function createRoutes(routeDef, base = "") {
|
|
|
1015
1011
|
for (const expandedPath of expandOptionals(originalPath)) {
|
|
1016
1012
|
const path = joinPaths(base, expandedPath);
|
|
1017
1013
|
let pattern = isLeaf ? path : path.split("/*", 1)[0];
|
|
1018
|
-
pattern = pattern.split("/").map(s =>
|
|
1019
|
-
return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
|
|
1020
|
-
}).join("/");
|
|
1014
|
+
pattern = pattern.split("/").map(s => /^[:*]/.test(s) ? s : encodeSegment(s)).join("/");
|
|
1021
1015
|
acc.push({
|
|
1022
1016
|
...shared,
|
|
1023
1017
|
originalPath,
|
|
@@ -1108,13 +1102,12 @@ function mergeParams(matches) {
|
|
|
1108
1102
|
return params;
|
|
1109
1103
|
}
|
|
1110
1104
|
function createLocation(path, state, queryWrapper) {
|
|
1111
|
-
const
|
|
1112
|
-
const url = createMemo((prev = origin) => {
|
|
1105
|
+
const url = createMemo((prev = new URL(mockBase)) => {
|
|
1113
1106
|
const path_ = path();
|
|
1114
1107
|
try {
|
|
1115
1108
|
// anchor rooted paths against the origin explicitly - a path with
|
|
1116
1109
|
// doubled leading slashes would otherwise parse as protocol-relative
|
|
1117
|
-
return new URL(path_[0] === "/" ? mockBase + path_ : path_,
|
|
1110
|
+
return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
|
|
1118
1111
|
} catch (err) {
|
|
1119
1112
|
DEV && console.error(`Invalid path ${path_}`);
|
|
1120
1113
|
return prev;
|
|
@@ -1125,7 +1118,6 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1125
1118
|
const pathname = createMemo(() => url().pathname);
|
|
1126
1119
|
const search = createMemo(() => url().search);
|
|
1127
1120
|
const hash = createMemo(() => url().hash);
|
|
1128
|
-
const key = () => "";
|
|
1129
1121
|
const queryFn = createMemo(() => extractSearchParams(url()));
|
|
1130
1122
|
return {
|
|
1131
1123
|
get pathname() {
|
|
@@ -1141,7 +1133,7 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1141
1133
|
return state();
|
|
1142
1134
|
},
|
|
1143
1135
|
get key() {
|
|
1144
|
-
return
|
|
1136
|
+
return "";
|
|
1145
1137
|
},
|
|
1146
1138
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
1147
1139
|
};
|
|
@@ -1307,7 +1299,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1307
1299
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1308
1300
|
const result = resolveLazySubtree(record);
|
|
1309
1301
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1310
|
-
}, {
|
|
1302
|
+
}, DEV && {
|
|
1311
1303
|
name: "lazyRoutes"
|
|
1312
1304
|
}));
|
|
1313
1305
|
lazyReaders.set(record, read);
|
|
@@ -1340,7 +1332,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1340
1332
|
}
|
|
1341
1333
|
}
|
|
1342
1334
|
return m;
|
|
1343
|
-
}, {
|
|
1335
|
+
}, DEV && {
|
|
1344
1336
|
name: "matches"
|
|
1345
1337
|
});
|
|
1346
1338
|
const routingPending = createMemo(() => isPending(() => {
|
|
@@ -1351,7 +1343,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1351
1343
|
}
|
|
1352
1344
|
location.search;
|
|
1353
1345
|
location.hash;
|
|
1354
|
-
}), {
|
|
1346
|
+
}), DEV && {
|
|
1355
1347
|
name: "routingPending"
|
|
1356
1348
|
});
|
|
1357
1349
|
const isRouting = () => routingPending() || isPending(source);
|
|
@@ -1557,11 +1549,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1557
1549
|
route,
|
|
1558
1550
|
params
|
|
1559
1551
|
} = next[match];
|
|
1560
|
-
route.component && route.component.preload && route.component.preload();
|
|
1561
1552
|
const {
|
|
1562
1553
|
preload,
|
|
1563
1554
|
component
|
|
1564
1555
|
} = route;
|
|
1556
|
+
component?.preload?.();
|
|
1565
1557
|
const now = current && current[match];
|
|
1566
1558
|
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1567
1559
|
inPreloadFn = true;
|
|
@@ -1625,7 +1617,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1625
1617
|
// its last valid value while this route is being torn down, so outgoing
|
|
1626
1618
|
// components and preloads never observe another route's params.
|
|
1627
1619
|
const params = wrapParams(() => mergeParams(matches()));
|
|
1628
|
-
component
|
|
1620
|
+
component?.preload?.();
|
|
1629
1621
|
inPreloadFn = true;
|
|
1630
1622
|
const data = preload ? preload({
|
|
1631
1623
|
params,
|
|
@@ -1752,9 +1744,7 @@ function Routes(props) {
|
|
|
1752
1744
|
next[i] = prev[i];
|
|
1753
1745
|
} else {
|
|
1754
1746
|
equal = false;
|
|
1755
|
-
|
|
1756
|
-
disposers[i]();
|
|
1757
|
-
}
|
|
1747
|
+
disposers[i]?.();
|
|
1758
1748
|
runWithOwner(owner, () => createRoot(dispose => {
|
|
1759
1749
|
disposers[i] = dispose;
|
|
1760
1750
|
const routeKey = nextMatch.route.key;
|
|
@@ -1843,13 +1833,14 @@ function bindEvent(target, type, handler) {
|
|
|
1843
1833
|
|
|
1844
1834
|
let depth;
|
|
1845
1835
|
function saveCurrentDepth() {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1836
|
+
const h = window.history;
|
|
1837
|
+
if (!h.state || h.state._depth == null) {
|
|
1838
|
+
h.replaceState({
|
|
1839
|
+
...h.state,
|
|
1840
|
+
_depth: h.length - 1
|
|
1850
1841
|
}, "");
|
|
1851
1842
|
}
|
|
1852
|
-
depth =
|
|
1843
|
+
depth = h.state?._depth ?? h.length - 1;
|
|
1853
1844
|
}
|
|
1854
1845
|
function keepDepth(state) {
|
|
1855
1846
|
return {
|
|
@@ -1893,11 +1884,10 @@ function scrollToHash(hash, fallbackTop) {
|
|
|
1893
1884
|
|
|
1894
1885
|
function browserHistory() {
|
|
1895
1886
|
const getSource = () => {
|
|
1896
|
-
const
|
|
1897
|
-
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
1887
|
+
const s = window.history.state;
|
|
1898
1888
|
return {
|
|
1899
|
-
value:
|
|
1900
|
-
state
|
|
1889
|
+
value: window.location.pathname + window.location.search + window.location.hash,
|
|
1890
|
+
state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
|
|
1901
1891
|
};
|
|
1902
1892
|
};
|
|
1903
1893
|
const beforeLeave = {};
|
|
@@ -2125,7 +2115,7 @@ function createScrollRestoration() {
|
|
|
2125
2115
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
2126
2116
|
// navigation starts a new one and belongs at the top); the effect's
|
|
2127
2117
|
// initial run performs the restore after first render
|
|
2128
|
-
const [nav] = performance.getEntriesByType
|
|
2118
|
+
const [nav] = performance.getEntriesByType?.("navigation");
|
|
2129
2119
|
if (nav && nav.type !== "navigate") pending = depth();
|
|
2130
2120
|
}
|
|
2131
2121
|
};
|
|
@@ -2424,11 +2414,10 @@ function createRouter(config) {
|
|
|
2424
2414
|
}));
|
|
2425
2415
|
},
|
|
2426
2416
|
keysFor(url) {
|
|
2427
|
-
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
2428
2417
|
const keys = [];
|
|
2429
2418
|
for (const {
|
|
2430
2419
|
route
|
|
2431
|
-
} of matchPath(
|
|
2420
|
+
} of matchPath(pathnameOf(url))) {
|
|
2432
2421
|
const key = serverRouteOf(route.component)?.call?.key;
|
|
2433
2422
|
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
2434
2423
|
}
|
|
@@ -2743,7 +2732,7 @@ function query(fn, name) {
|
|
|
2743
2732
|
}
|
|
2744
2733
|
let res = cached[1];
|
|
2745
2734
|
if (intent !== "preload") {
|
|
2746
|
-
res =
|
|
2735
|
+
res = settle(res);
|
|
2747
2736
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
2748
2737
|
}
|
|
2749
2738
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
@@ -2803,9 +2792,7 @@ function query(fn, name) {
|
|
|
2803
2792
|
const e = getRequestEvent();
|
|
2804
2793
|
if (e && e.router.dataOnly) return e.router.data[key] = res;
|
|
2805
2794
|
}
|
|
2806
|
-
if (intent !== "preload")
|
|
2807
|
-
res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
|
|
2808
|
-
}
|
|
2795
|
+
if (intent !== "preload") res = settle(res);
|
|
2809
2796
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2810
2797
|
// serialize on server
|
|
2811
2798
|
if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
|
|
@@ -2813,6 +2800,11 @@ function query(fn, name) {
|
|
|
2813
2800
|
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
2814
2801
|
}
|
|
2815
2802
|
return res;
|
|
2803
|
+
|
|
2804
|
+
// a settled read: the response handling applied to the value or the promise
|
|
2805
|
+
function settle(v) {
|
|
2806
|
+
return "then" in v ? v.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(v);
|
|
2807
|
+
}
|
|
2816
2808
|
function handleResponse(error) {
|
|
2817
2809
|
return async v => {
|
|
2818
2810
|
let enveloped;
|
|
@@ -2989,7 +2981,8 @@ const actions = /* #__PURE__ */new Map();
|
|
|
2989
2981
|
*/
|
|
2990
2982
|
function handleFormAction(evt, router, actionBase) {
|
|
2991
2983
|
if (evt.defaultPrevented) return;
|
|
2992
|
-
|
|
2984
|
+
const form = evt.target;
|
|
2985
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : form.getAttribute("action");
|
|
2993
2986
|
if (!actionRef) return;
|
|
2994
2987
|
const serverAction = !actionRef.startsWith("https://action/");
|
|
2995
2988
|
if (serverAction) {
|
|
@@ -2998,7 +2991,7 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
2998
2991
|
actionRef = router.parsePath(url.pathname + url.search);
|
|
2999
2992
|
if (!actionRef.startsWith(actionBase)) return;
|
|
3000
2993
|
}
|
|
3001
|
-
if (
|
|
2994
|
+
if (form.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
3002
2995
|
// A registry miss on a server-action url is a direct bind whose module
|
|
3003
2996
|
// never loaded client-side (server components): the url is self-describing
|
|
3004
2997
|
// (the id in the path, bound `?args` in the query), so a generic invocation
|
|
@@ -3009,11 +3002,11 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3009
3002
|
const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
|
|
3010
3003
|
if (handler) {
|
|
3011
3004
|
evt.preventDefault();
|
|
3012
|
-
const data = new FormData(
|
|
3005
|
+
const data = new FormData(form, evt.submitter);
|
|
3013
3006
|
handler.call({
|
|
3014
3007
|
r: router,
|
|
3015
|
-
f:
|
|
3016
|
-
},
|
|
3008
|
+
f: form
|
|
3009
|
+
}, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
3017
3010
|
}
|
|
3018
3011
|
}
|
|
3019
3012
|
|
|
@@ -3120,23 +3113,21 @@ function actionImpl(fn, options = {}) {
|
|
|
3120
3113
|
const runMutation = () => fn(...variables);
|
|
3121
3114
|
const run = action$1(async function* (context) {
|
|
3122
3115
|
context.optimistic?.();
|
|
3116
|
+
let value;
|
|
3117
|
+
let error = false;
|
|
3123
3118
|
try {
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
value
|
|
3129
|
-
};
|
|
3130
|
-
} catch (error) {
|
|
3131
|
-
yield;
|
|
3132
|
-
return {
|
|
3133
|
-
error: true,
|
|
3134
|
-
value: error
|
|
3135
|
-
};
|
|
3119
|
+
value = await context.call();
|
|
3120
|
+
} catch (e) {
|
|
3121
|
+
value = e;
|
|
3122
|
+
error = true;
|
|
3136
3123
|
}
|
|
3124
|
+
const read = await readResponse(value, error);
|
|
3125
|
+
yield;
|
|
3126
|
+
// Apply inside the transition so the default revalidation's refetch and
|
|
3127
|
+
// the release of the caller's optimistic writes commit as one frame (#619).
|
|
3128
|
+
return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
3137
3129
|
});
|
|
3138
3130
|
form && setFormBusy(form, 1);
|
|
3139
|
-
let settled;
|
|
3140
3131
|
let response;
|
|
3141
3132
|
// The transport consumer is awaited before a single-flight mutation
|
|
3142
3133
|
// resolves, so a counter delta over the call tells whether this action's
|
|
@@ -3146,13 +3137,12 @@ function actionImpl(fn, options = {}) {
|
|
|
3146
3137
|
// which misses every response the server returned without flight data.
|
|
3147
3138
|
const flightApplicationsBefore = flightApplications;
|
|
3148
3139
|
try {
|
|
3149
|
-
|
|
3140
|
+
response = await settleActionResult(run({
|
|
3150
3141
|
call: runMutation,
|
|
3151
3142
|
optimistic: submitHooks.size ? () => {
|
|
3152
3143
|
for (const hook of submitHooks.values()) hook(...variables);
|
|
3153
3144
|
} : undefined
|
|
3154
3145
|
}));
|
|
3155
|
-
response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
3156
3146
|
} finally {
|
|
3157
3147
|
form && setFormBusy(form, -1);
|
|
3158
3148
|
}
|
|
@@ -3160,8 +3150,8 @@ function actionImpl(fn, options = {}) {
|
|
|
3160
3150
|
submission = {
|
|
3161
3151
|
input: variables,
|
|
3162
3152
|
url,
|
|
3163
|
-
result: response
|
|
3164
|
-
error: response
|
|
3153
|
+
result: response?.data,
|
|
3154
|
+
error: response?.error,
|
|
3165
3155
|
clear() {
|
|
3166
3156
|
router.submissions[1](entries => entries.filter(entry => entry !== submission));
|
|
3167
3157
|
},
|
|
@@ -3207,8 +3197,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
3207
3197
|
fn.with = function (...args) {
|
|
3208
3198
|
const uri = new URL(url, mockBase);
|
|
3209
3199
|
uri.searchParams.set("args", hashKey(args));
|
|
3210
|
-
|
|
3211
|
-
return next;
|
|
3200
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
|
|
3212
3201
|
};
|
|
3213
3202
|
fn.onSubmit = function (hook) {
|
|
3214
3203
|
const id = Symbol("actionOnSubmitHook");
|
|
@@ -3327,8 +3316,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
3327
3316
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
3328
3317
|
// set cache — and fan the payload out to other keyed stores (live query
|
|
3329
3318
|
// channels adopt delivered values instead of reconnecting)
|
|
3330
|
-
|
|
3331
|
-
|
|
3319
|
+
if (flightData) {
|
|
3320
|
+
Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
3321
|
+
deliverFlightData(flightData);
|
|
3322
|
+
}
|
|
3332
3323
|
// trigger revalidation inside the same transition as the navigation, so
|
|
3333
3324
|
// the redirect commits atomically with fresh data: surviving consumers
|
|
3334
3325
|
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
@@ -3336,7 +3327,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
3336
3327
|
// are fresh again by now, so the sweep re-reads them from cache.
|
|
3337
3328
|
revalidate(keys, false);
|
|
3338
3329
|
}
|
|
3339
|
-
async function
|
|
3330
|
+
async function readResponse(response, error) {
|
|
3340
3331
|
let data;
|
|
3341
3332
|
let flightData;
|
|
3342
3333
|
let metadata;
|
|
@@ -3359,6 +3350,19 @@ async function handleResponse(response, error, navigate, metadataHandled) {
|
|
|
3359
3350
|
} else if (error) return {
|
|
3360
3351
|
error: response
|
|
3361
3352
|
};else data = response;
|
|
3353
|
+
return {
|
|
3354
|
+
data,
|
|
3355
|
+
flightData,
|
|
3356
|
+
metadata
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
function applyResponse(read, navigate, metadataHandled) {
|
|
3360
|
+
if ("error" in read) return read;
|
|
3361
|
+
const {
|
|
3362
|
+
data,
|
|
3363
|
+
flightData,
|
|
3364
|
+
metadata
|
|
3365
|
+
} = read;
|
|
3362
3366
|
// The transport consumer applies metadata before returning a server
|
|
3363
3367
|
// function's unwrapped value. Do not treat that value as a second plain
|
|
3364
3368
|
// 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
|
});
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -247,9 +247,8 @@ export function createRouter(config) {
|
|
|
247
247
|
}));
|
|
248
248
|
},
|
|
249
249
|
keysFor(url) {
|
|
250
|
-
const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
|
|
251
250
|
const keys = [];
|
|
252
|
-
for (const { route } of matchPath(
|
|
251
|
+
for (const { route } of matchPath(pathnameOf(url))) {
|
|
253
252
|
const key = serverRouteOf(route.component)?.call?.key;
|
|
254
253
|
typeof key === "string" && key && !keys.includes(key) && keys.push(key);
|
|
255
254
|
}
|
package/dist/routers/history.js
CHANGED
|
@@ -9,10 +9,11 @@ export function bindEvent(target, type, handler) {
|
|
|
9
9
|
// guard machinery itself lives behind the lazy `beforeLeave` slot.
|
|
10
10
|
let depth;
|
|
11
11
|
export function saveCurrentDepth() {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const h = window.history;
|
|
13
|
+
if (!h.state || h.state._depth == null) {
|
|
14
|
+
h.replaceState({ ...h.state, _depth: h.length - 1 }, "");
|
|
14
15
|
}
|
|
15
|
-
depth =
|
|
16
|
+
depth = h.state?._depth ?? h.length - 1;
|
|
16
17
|
}
|
|
17
18
|
function keepDepth(state) {
|
|
18
19
|
return {
|
|
@@ -50,15 +51,10 @@ function scrollToHash(hash, fallbackTop) {
|
|
|
50
51
|
}
|
|
51
52
|
export function browserHistory() {
|
|
52
53
|
const getSource = () => {
|
|
53
|
-
const
|
|
54
|
-
const state = window.history.state &&
|
|
55
|
-
window.history.state._depth &&
|
|
56
|
-
Object.keys(window.history.state).length === 1
|
|
57
|
-
? undefined
|
|
58
|
-
: window.history.state;
|
|
54
|
+
const s = window.history.state;
|
|
59
55
|
return {
|
|
60
|
-
value:
|
|
61
|
-
state
|
|
56
|
+
value: window.location.pathname + window.location.search + window.location.hash,
|
|
57
|
+
state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
|
|
62
58
|
};
|
|
63
59
|
};
|
|
64
60
|
const beforeLeave = {};
|
|
@@ -105,8 +105,7 @@ export function createScrollRestoration() {
|
|
|
105
105
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
106
106
|
// navigation starts a new one and belongs at the top); the effect's
|
|
107
107
|
// initial run performs the restore after first render
|
|
108
|
-
const [nav] =
|
|
109
|
-
performance.getEntriesByType("navigation"));
|
|
108
|
+
const [nav] = performance.getEntriesByType?.("navigation");
|
|
110
109
|
if (nav && nav.type !== "navigate")
|
|
111
110
|
pending = depth();
|
|
112
111
|
}
|
package/dist/routing.js
CHANGED
|
@@ -391,9 +391,7 @@ export function createRoutes(routeDef, base = "") {
|
|
|
391
391
|
let pattern = isLeaf ? path : path.split("/*", 1)[0];
|
|
392
392
|
pattern = pattern
|
|
393
393
|
.split("/")
|
|
394
|
-
.map((s)
|
|
395
|
-
return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
|
|
396
|
-
})
|
|
394
|
+
.map(s => (/^[:*]/.test(s) ? s : encodeSegment(s)))
|
|
397
395
|
.join("/");
|
|
398
396
|
acc.push({
|
|
399
397
|
...shared,
|
|
@@ -487,13 +485,12 @@ export function mergeParams(matches) {
|
|
|
487
485
|
return params;
|
|
488
486
|
}
|
|
489
487
|
function createLocation(path, state, queryWrapper) {
|
|
490
|
-
const
|
|
491
|
-
const url = createMemo((prev = origin) => {
|
|
488
|
+
const url = createMemo((prev = new URL(mockBase)) => {
|
|
492
489
|
const path_ = path();
|
|
493
490
|
try {
|
|
494
491
|
// anchor rooted paths against the origin explicitly - a path with
|
|
495
492
|
// doubled leading slashes would otherwise parse as protocol-relative
|
|
496
|
-
return new URL(path_[0] === "/" ? mockBase + path_ : path_,
|
|
493
|
+
return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
|
|
497
494
|
}
|
|
498
495
|
catch (err) {
|
|
499
496
|
DEV && console.error(`Invalid path ${path_}`);
|
|
@@ -505,7 +502,6 @@ function createLocation(path, state, queryWrapper) {
|
|
|
505
502
|
const pathname = createMemo(() => url().pathname);
|
|
506
503
|
const search = createMemo(() => url().search);
|
|
507
504
|
const hash = createMemo(() => url().hash);
|
|
508
|
-
const key = () => "";
|
|
509
505
|
const queryFn = createMemo(() => extractSearchParams(url()));
|
|
510
506
|
return {
|
|
511
507
|
get pathname() {
|
|
@@ -521,7 +517,7 @@ function createLocation(path, state, queryWrapper) {
|
|
|
521
517
|
return state();
|
|
522
518
|
},
|
|
523
519
|
get key() {
|
|
524
|
-
return
|
|
520
|
+
return "";
|
|
525
521
|
},
|
|
526
522
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
527
523
|
};
|
|
@@ -660,7 +656,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
660
656
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
661
657
|
const result = resolveLazySubtree(record);
|
|
662
658
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
663
|
-
}, { name: "lazyRoutes" }));
|
|
659
|
+
}, DEV && { name: "lazyRoutes" }));
|
|
664
660
|
lazyReaders.set(record, read);
|
|
665
661
|
}
|
|
666
662
|
return read();
|
|
@@ -695,7 +691,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
695
691
|
}
|
|
696
692
|
}
|
|
697
693
|
return m;
|
|
698
|
-
}, { name: "matches" });
|
|
694
|
+
}, DEV && { name: "matches" });
|
|
699
695
|
const routingPending = createMemo(() => isPending(() => {
|
|
700
696
|
try {
|
|
701
697
|
matches();
|
|
@@ -706,7 +702,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
706
702
|
}
|
|
707
703
|
location.search;
|
|
708
704
|
location.hash;
|
|
709
|
-
}), { name: "routingPending" });
|
|
705
|
+
}), DEV && { name: "routingPending" });
|
|
710
706
|
const isRouting = () => routingPending() || isPending(source);
|
|
711
707
|
const transitionIntent = () => {
|
|
712
708
|
if (!isPending(source))
|
|
@@ -912,10 +908,8 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
912
908
|
preloadIntent = "preload";
|
|
913
909
|
for (let match in next) {
|
|
914
910
|
const { route, params } = next[match];
|
|
915
|
-
route.component &&
|
|
916
|
-
route.component.preload &&
|
|
917
|
-
route.component.preload();
|
|
918
911
|
const { preload, component } = route;
|
|
912
|
+
component?.preload?.();
|
|
919
913
|
const now = current && current[match];
|
|
920
914
|
const unchanged = now &&
|
|
921
915
|
now.route.key === route.key &&
|
|
@@ -978,9 +972,7 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
978
972
|
// its last valid value while this route is being torn down, so outgoing
|
|
979
973
|
// components and preloads never observe another route's params.
|
|
980
974
|
const params = wrapParams(() => mergeParams(matches()));
|
|
981
|
-
component
|
|
982
|
-
component.preload &&
|
|
983
|
-
component.preload();
|
|
975
|
+
component?.preload?.();
|
|
984
976
|
inPreloadFn = true;
|
|
985
977
|
const data = preload
|
|
986
978
|
? preload({ params, location, intent: router.intent?.() || "initial" })
|
|
@@ -12,15 +12,19 @@ export function serverRouteOf(component) {
|
|
|
12
12
|
*/
|
|
13
13
|
export function serverRouteArgs(route, params, query) {
|
|
14
14
|
const schema = route.key?.search;
|
|
15
|
-
let search;
|
|
16
15
|
if (schema) {
|
|
17
16
|
const raw = { ...query };
|
|
18
17
|
const outcome = validateSearch(schema, raw);
|
|
19
18
|
// Issues leave the raw values in place, matching useSearchParams: search
|
|
20
19
|
// strings are user input, so defaults belong in the schema itself.
|
|
21
|
-
search
|
|
20
|
+
return { params: { ...params }, search: outcome.issues ? raw : outcome.value };
|
|
22
21
|
}
|
|
23
|
-
|
|
22
|
+
// No schema: leave the key out rather than carrying `search: undefined` —
|
|
23
|
+
// the args are sent as plain JSON by default and the runtime's JSON-safety
|
|
24
|
+
// check rejects explicit `undefined` (#615). Destructuring on the server
|
|
25
|
+
// reads the same either way, and the args-memo equality treats an absent
|
|
26
|
+
// key and undefined alike.
|
|
27
|
+
return { params: { ...params } };
|
|
24
28
|
}
|
|
25
29
|
const shallowEqual = (a, b) => a === b ||
|
|
26
30
|
(!!a &&
|
package/dist/types.d.ts
CHANGED
|
@@ -144,12 +144,21 @@ export type RouteSectionComponent<T = unknown, P extends Params = Params> = Comp
|
|
|
144
144
|
/**
|
|
145
145
|
* What a server component route is called with (experimental): the params
|
|
146
146
|
* the route's pattern (and its ancestors') declares, plus — only when the
|
|
147
|
-
* route declares a `search` schema — that schema's validated output.
|
|
147
|
+
* route declares a `search` schema — that schema's validated output. Without
|
|
148
|
+
* a schema the `search` key is ABSENT, not `undefined`: the args travel as
|
|
149
|
+
* plain JSON by default, and the runtime's JSON-safety check rejects
|
|
150
|
+
* explicit `undefined` (#615). The type mirrors that — optional exactly when
|
|
151
|
+
* the search type admits undefined, required when a schema narrows it.
|
|
148
152
|
*/
|
|
149
|
-
export
|
|
153
|
+
export type ServerRouteArgs<P extends Params | TypedRouteConfig = Params, S = undefined> = {
|
|
150
154
|
params: ServerRouteParams<P>;
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
} & (undefined extends ServerRouteSearch<P, S> ? {
|
|
156
|
+
search?: ServerRouteSearch<P, S>;
|
|
157
|
+
} : {
|
|
158
|
+
search: ServerRouteSearch<P, S>;
|
|
159
|
+
});
|
|
160
|
+
/** The search half of {@link ServerRouteArgs}: schema output when declared. */
|
|
161
|
+
export type ServerRouteSearch<P, S> = P extends TypedRouteConfig ? SearchOutputOf<P> : S;
|
|
153
162
|
/**
|
|
154
163
|
* `ServerRouteArgs<typeof route>`: a `defineFileRoute` config is a witness —
|
|
155
164
|
* params from its pattern, search from its schema — the way
|
package/dist/utils.js
CHANGED
|
@@ -78,13 +78,13 @@ export function createMatcher(path, partial, matchFilters) {
|
|
|
78
78
|
path: len ? "" : "/",
|
|
79
79
|
params: {}
|
|
80
80
|
};
|
|
81
|
-
const
|
|
81
|
+
const filters = matchFilters;
|
|
82
82
|
for (let i = 0; i < len; i++) {
|
|
83
83
|
const segment = segments[i];
|
|
84
84
|
const dynamic = segment[0] === ":";
|
|
85
85
|
const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
|
|
86
86
|
const key = dynamic ? segment.slice(1) : segment.toLowerCase();
|
|
87
|
-
if (dynamic && matchSegment(locSegment,
|
|
87
|
+
if (dynamic && matchSegment(locSegment, filters?.[key])) {
|
|
88
88
|
match.params[key] = locSegment;
|
|
89
89
|
}
|
|
90
90
|
else if (dynamic || !matchSegment(locSegment, key)) {
|
|
@@ -94,7 +94,7 @@ export function createMatcher(path, partial, matchFilters) {
|
|
|
94
94
|
}
|
|
95
95
|
if (splat) {
|
|
96
96
|
const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
|
|
97
|
-
if (matchSegment(remainder,
|
|
97
|
+
if (matchSegment(remainder, filters?.[splat])) {
|
|
98
98
|
match.params[splat] = remainder;
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
@@ -104,25 +104,15 @@ export function createMatcher(path, partial, matchFilters) {
|
|
|
104
104
|
return match;
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return filter(input);
|
|
117
|
-
}
|
|
118
|
-
else if (Array.isArray(filter)) {
|
|
119
|
-
return filter.some(isEqual);
|
|
120
|
-
}
|
|
121
|
-
else if (filter instanceof RegExp) {
|
|
122
|
-
return filter.test(input);
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
107
|
+
// a string filter is the segment itself; anything else not listed never matches
|
|
108
|
+
const matchSegment = (input, filter) => filter === undefined ||
|
|
109
|
+
(typeof filter === "function"
|
|
110
|
+
? filter(input)
|
|
111
|
+
: Array.isArray(filter)
|
|
112
|
+
? filter.includes(input)
|
|
113
|
+
: filter instanceof RegExp
|
|
114
|
+
? filter.test(input)
|
|
115
|
+
: filter === input);
|
|
126
116
|
export function scoreRoute(route) {
|
|
127
117
|
const [pattern, splat] = route.pattern.split("/*", 2);
|
|
128
118
|
const segments = pattern.split("/").filter(Boolean);
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"Ryan Turnquist"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"version": "2.0.0-next.
|
|
9
|
+
"version": "2.0.0-next.31",
|
|
10
10
|
"homepage": "https://github.com/solidjs/solid-router#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -38,22 +38,22 @@
|
|
|
38
38
|
"@rollup/plugin-node-resolve": "15.3.0",
|
|
39
39
|
"@rollup/plugin-terser": "0.4.4",
|
|
40
40
|
"@solidjs/vite-plugin": "3.0.0-next.35",
|
|
41
|
-
"@solidjs/web": "^2.0.0-rc.
|
|
41
|
+
"@solidjs/web": "^2.0.0-rc.11",
|
|
42
42
|
"@types/jest": "^29.5.14",
|
|
43
43
|
"@types/node": "^22.10.0",
|
|
44
44
|
"babel-preset-solid": "^2.0.0-rc.2",
|
|
45
45
|
"jsdom": "^25.0.1",
|
|
46
46
|
"prettier": "^3.4.1",
|
|
47
47
|
"rollup": "^4.27.4",
|
|
48
|
-
"solid-js": "^2.0.0-rc.
|
|
48
|
+
"solid-js": "^2.0.0-rc.11",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vite": "^8.2.2",
|
|
51
51
|
"vitest": "^4.1.11"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@solidjs/web": "^2.0.0-rc.
|
|
54
|
+
"@solidjs/web": "^2.0.0-rc.10",
|
|
55
55
|
"filesystem-routing": ">=0.4.0",
|
|
56
|
-
"solid-js": "^2.0.0-rc.
|
|
56
|
+
"solid-js": "^2.0.0-rc.10"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"filesystem-routing": {
|