@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/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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, getObserver, getOwner,
|
|
1
|
+
import { createSignal, getObserver, getOwner, isHydratable, onCleanup, untrack } from "solid-js";
|
|
2
2
|
// Everything server-function-shaped comes off the CORE entry: detection
|
|
3
3
|
// (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
|
|
4
4
|
// the late-bound RPC seam (getServerFunctionRPC). The transport itself —
|
|
@@ -9,7 +9,7 @@ import { createSignal, getObserver, getOwner, onCleanup, sharedConfig, untrack }
|
|
|
9
9
|
// into the seam when a `'use server'` reference is created (compiled
|
|
10
10
|
// output, module scope), so by the time a server function can reach
|
|
11
11
|
// query() the seam is filled; plain-fetch apps read undefined forever.
|
|
12
|
-
import { getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, isServer, isServerFunction, REVALIDATE_HEADER } from "@solidjs/web";
|
|
12
|
+
import { getHydrationWriter, getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, isServer, isServerFunction, REVALIDATE_HEADER, takeHydrationValue } from "@solidjs/web";
|
|
13
13
|
// The redirect carrier's name and decoder are the exception: two pure,
|
|
14
14
|
// dependency-free bindings off a `sideEffects: false` entry, so they
|
|
15
15
|
// tree-shake to a few hundred bytes without dragging the codec in (the same
|
|
@@ -19,10 +19,34 @@ import { useRouter, getIntent, getInPreloadFn } from "../routing.js";
|
|
|
19
19
|
const LocationHeader = "Location";
|
|
20
20
|
const PRELOAD_TIMEOUT = 5000;
|
|
21
21
|
const CACHE_TIMEOUT = 180000;
|
|
22
|
-
|
|
22
|
+
const responseBodyHeaders = /* #__PURE__ */ new Set([
|
|
23
|
+
"content-type",
|
|
24
|
+
"content-length",
|
|
25
|
+
"content-encoding",
|
|
26
|
+
"transfer-encoding",
|
|
27
|
+
"content-disposition",
|
|
28
|
+
"content-language",
|
|
29
|
+
"content-location",
|
|
30
|
+
"content-range",
|
|
31
|
+
"accept-ranges",
|
|
32
|
+
"etag",
|
|
33
|
+
"last-modified"
|
|
34
|
+
]);
|
|
35
|
+
const forwardedCookies = /* #__PURE__ */ new WeakMap();
|
|
36
|
+
// When this client booted. Flight-registry entries (takeHydrationValue)
|
|
23
37
|
// hold values the server computed while rendering THIS page, so their age is
|
|
24
38
|
// anchored here — not at whenever a late query() call happens to consume one.
|
|
25
39
|
const bootTime = Date.now();
|
|
40
|
+
// Every hydratable read ships its entry: the writer keeps the first write
|
|
41
|
+
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
42
|
+
// from an island that reads the same key later.
|
|
43
|
+
function serializeForHydration(key, res) {
|
|
44
|
+
const writer = getHydrationWriter();
|
|
45
|
+
if (!writer || !writer.async || !isHydratable())
|
|
46
|
+
return;
|
|
47
|
+
const e = getRequestEvent();
|
|
48
|
+
(!e || !e.serverOnly) && writer.write(key, res);
|
|
49
|
+
}
|
|
26
50
|
let cacheMap = new Map();
|
|
27
51
|
// cleanup forward/back cache
|
|
28
52
|
if (!isServer) {
|
|
@@ -156,13 +180,11 @@ export function query(fn, name) {
|
|
|
156
180
|
}
|
|
157
181
|
let res = cached[1];
|
|
158
182
|
if (intent !== "preload") {
|
|
159
|
-
res =
|
|
160
|
-
"then" in cached[1]
|
|
161
|
-
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
162
|
-
: handleResponse(false)(cached[1]);
|
|
183
|
+
res = settle(res);
|
|
163
184
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
164
185
|
}
|
|
165
186
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
187
|
+
isServer && serializeForHydration(key, res);
|
|
166
188
|
return res;
|
|
167
189
|
}
|
|
168
190
|
let res;
|
|
@@ -179,13 +201,17 @@ export function query(fn, name) {
|
|
|
179
201
|
// active navigation (or its preload) accepts the payload only while a
|
|
180
202
|
// real preload of the same age would still satisfy it — anything older
|
|
181
203
|
// refetches instead of serving a minutes-old value on a fresh navigation.
|
|
182
|
-
if (!isServer &&
|
|
183
|
-
|
|
184
|
-
|
|
204
|
+
if (!isServer &&
|
|
205
|
+
(!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT))) {
|
|
206
|
+
const entry = takeHydrationValue(key); // hydrating
|
|
207
|
+
if (entry) {
|
|
185
208
|
adopted = true;
|
|
186
|
-
res =
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
res =
|
|
210
|
+
entry.status === "pending"
|
|
211
|
+
? entry.promise
|
|
212
|
+
: entry.status === "resolved"
|
|
213
|
+
? Promise.resolve(entry.value)
|
|
214
|
+
: Promise.reject(entry.error);
|
|
189
215
|
}
|
|
190
216
|
// A payload too old for this intent stays in the registry: it is
|
|
191
217
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -201,6 +227,7 @@ export function query(fn, name) {
|
|
|
201
227
|
// it would for a preload performed when the server fetched the data.
|
|
202
228
|
const stamp = adopted ? bootTime : now;
|
|
203
229
|
if (cached) {
|
|
230
|
+
isServer && forwardedCookies.delete(cached);
|
|
204
231
|
cached[0] = stamp;
|
|
205
232
|
cached[1] = res;
|
|
206
233
|
cached[3] = intent;
|
|
@@ -225,23 +252,21 @@ export function query(fn, name) {
|
|
|
225
252
|
if (e && e.router.dataOnly)
|
|
226
253
|
return (e.router.data[key] = res);
|
|
227
254
|
}
|
|
228
|
-
if (intent !== "preload")
|
|
229
|
-
res =
|
|
230
|
-
"then" in res
|
|
231
|
-
? res.then(handleResponse(false), handleResponse(true))
|
|
232
|
-
: handleResponse(false)(res);
|
|
233
|
-
}
|
|
255
|
+
if (intent !== "preload")
|
|
256
|
+
res = settle(res);
|
|
234
257
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
235
|
-
|
|
236
|
-
if (isServer &&
|
|
237
|
-
sharedConfig.context &&
|
|
238
|
-
sharedConfig.context.async &&
|
|
239
|
-
!sharedConfig.context.noHydrate) {
|
|
240
|
-
const e = getRequestEvent();
|
|
241
|
-
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
242
|
-
}
|
|
258
|
+
isServer && serializeForHydration(key, res);
|
|
243
259
|
return res;
|
|
244
|
-
|
|
260
|
+
// a settled read: the response handling applied to the value or the promise
|
|
261
|
+
function settle(v) {
|
|
262
|
+
let cookies = isServer ? forwardedCookies.get(cached) : undefined;
|
|
263
|
+
if (isServer && !cookies)
|
|
264
|
+
forwardedCookies.set(cached, (cookies = new WeakSet()));
|
|
265
|
+
return "then" in v
|
|
266
|
+
? v.then(handleResponse(false, cookies), handleResponse(true, cookies))
|
|
267
|
+
: handleResponse(false, cookies)(v);
|
|
268
|
+
}
|
|
269
|
+
function handleResponse(error, cookies) {
|
|
245
270
|
return async (v) => {
|
|
246
271
|
let enveloped;
|
|
247
272
|
let hasEnveloped = false;
|
|
@@ -254,11 +279,22 @@ export function query(fn, name) {
|
|
|
254
279
|
if (v instanceof Response) {
|
|
255
280
|
const e = getRequestEvent();
|
|
256
281
|
if (e) {
|
|
282
|
+
const headers = e.response.headers;
|
|
283
|
+
const skipCookies = cookies?.has(headers);
|
|
257
284
|
for (const [key, value] of v.headers) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
285
|
+
// Keep metadata for the query's body off the document response.
|
|
286
|
+
if (responseBodyHeaders.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
if (key === "set-cookie") {
|
|
289
|
+
if (!skipCookies)
|
|
290
|
+
headers.append(key, value);
|
|
291
|
+
}
|
|
292
|
+
else if (headers.get(key) !== value)
|
|
293
|
+
headers.set(key, value);
|
|
294
|
+
}
|
|
295
|
+
// Each cached result contributes its cookies once per document.
|
|
296
|
+
if (!skipCookies && v.headers.has("set-cookie")) {
|
|
297
|
+
cookies?.add(headers);
|
|
262
298
|
}
|
|
263
299
|
}
|
|
264
300
|
let url = v.headers.get(LocationHeader);
|
|
@@ -347,6 +383,7 @@ query.set = (key, value) => {
|
|
|
347
383
|
const now = Date.now();
|
|
348
384
|
let cached = cache.get(key);
|
|
349
385
|
if (cached) {
|
|
386
|
+
isServer && forwardedCookies.delete(cached);
|
|
350
387
|
cached[0] = now;
|
|
351
388
|
cached[1] = Promise.resolve(value);
|
|
352
389
|
cached[2] = value;
|
|
@@ -400,7 +437,7 @@ export function hashKey(args) {
|
|
|
400
437
|
.reduce((result, key) => {
|
|
401
438
|
result[key] = val[key];
|
|
402
439
|
return result;
|
|
403
|
-
},
|
|
440
|
+
}, Object.create(null))
|
|
404
441
|
: val);
|
|
405
442
|
}
|
|
406
443
|
function isPlainObject(obj) {
|
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
|
/**
|