@solidjs/router 2.0.0-next.31 → 2.0.0-next.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/query.js +81 -30
- package/dist/index.js +125 -38
- package/dist/routers/factory.jsx +63 -15
- package/dist/routing.js +2 -1
- package/dist/utils.js +3 -2
- package/package.json +5 -5
package/dist/data/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, getObserver, getOwner,
|
|
1
|
+
import { createSignal, getObserver, getOwner, isHydratable, isHydrating, 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,41 @@ 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
|
+
// Captured at module load: a hydrating client runs a serialized node's
|
|
41
|
+
// compute once as a dependency trace with the global `Promise` swapped for a
|
|
42
|
+
// never-settling mock, and a query() read inside that trace still consumes
|
|
43
|
+
// and caches its flight entry. Wrapped with the global there, a settled
|
|
44
|
+
// entry would cache a promise that never resolves, hanging every navigation
|
|
45
|
+
// that reuses it.
|
|
46
|
+
const NativePromise = Promise;
|
|
47
|
+
// Every hydratable read ships its entry: the writer keeps the first write
|
|
48
|
+
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
49
|
+
// from an island that reads the same key later.
|
|
50
|
+
function serializeForHydration(key, res) {
|
|
51
|
+
const writer = getHydrationWriter();
|
|
52
|
+
if (!writer || !writer.async || !isHydratable())
|
|
53
|
+
return;
|
|
54
|
+
const e = getRequestEvent();
|
|
55
|
+
(!e || !e.serverOnly) && writer.write(key, res);
|
|
56
|
+
}
|
|
26
57
|
let cacheMap = new Map();
|
|
27
58
|
// cleanup forward/back cache
|
|
28
59
|
if (!isServer) {
|
|
@@ -141,11 +172,18 @@ export function query(fn, name) {
|
|
|
141
172
|
tracking = true;
|
|
142
173
|
onCleanup(() => cached[4].count--);
|
|
143
174
|
}
|
|
175
|
+
// A read with no navigation in flight while hydrating is claiming server
|
|
176
|
+
// DOM, so it takes the cached entry at any age — the rule adoption below
|
|
177
|
+
// applies to flight entries. It can come from a boundary resuming long
|
|
178
|
+
// after boot, once the entry was adopted and its first reader disposed;
|
|
179
|
+
// refetching there would run inside Solid's hydration trace, where fetch
|
|
180
|
+
// and Promise never settle, and cache that.
|
|
144
181
|
if (cached &&
|
|
145
182
|
cached[0] &&
|
|
146
183
|
(isServer ||
|
|
147
184
|
intent === "native" ||
|
|
148
185
|
cached[4].count ||
|
|
186
|
+
(!intent && isHydrating()) ||
|
|
149
187
|
Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
|
|
150
188
|
if (tracking) {
|
|
151
189
|
cached[4].count++;
|
|
@@ -160,6 +198,7 @@ export function query(fn, name) {
|
|
|
160
198
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
161
199
|
}
|
|
162
200
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
201
|
+
isServer && serializeForHydration(key, res);
|
|
163
202
|
return res;
|
|
164
203
|
}
|
|
165
204
|
let res;
|
|
@@ -176,13 +215,17 @@ export function query(fn, name) {
|
|
|
176
215
|
// active navigation (or its preload) accepts the payload only while a
|
|
177
216
|
// real preload of the same age would still satisfy it — anything older
|
|
178
217
|
// refetches instead of serving a minutes-old value on a fresh navigation.
|
|
179
|
-
if (!isServer &&
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
if (!isServer &&
|
|
219
|
+
(!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT))) {
|
|
220
|
+
const entry = takeHydrationValue(key); // hydrating
|
|
221
|
+
if (entry) {
|
|
182
222
|
adopted = true;
|
|
183
|
-
res =
|
|
184
|
-
|
|
185
|
-
|
|
223
|
+
res =
|
|
224
|
+
entry.status === "pending"
|
|
225
|
+
? entry.promise
|
|
226
|
+
: entry.status === "resolved"
|
|
227
|
+
? NativePromise.resolve(entry.value)
|
|
228
|
+
: NativePromise.reject(entry.error);
|
|
186
229
|
}
|
|
187
230
|
// A payload too old for this intent stays in the registry: it is
|
|
188
231
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -191,13 +234,13 @@ export function query(fn, name) {
|
|
|
191
234
|
if (!adopted)
|
|
192
235
|
res = fn(...args);
|
|
193
236
|
// Adopted entries are stamped at boot — when their data was actually
|
|
194
|
-
// fetched — not at consumption.
|
|
195
|
-
// through the
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// it would for a preload performed when the server fetched the data.
|
|
237
|
+
// fetched — not at consumption. Later hydration reads still reuse them
|
|
238
|
+
// through the claim rule above, while a navigation that comes along later
|
|
239
|
+
// sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
|
|
240
|
+
// as it would for a preload performed when the server fetched the data.
|
|
199
241
|
const stamp = adopted ? bootTime : now;
|
|
200
242
|
if (cached) {
|
|
243
|
+
isServer && forwardedCookies.delete(cached);
|
|
201
244
|
cached[0] = stamp;
|
|
202
245
|
cached[1] = res;
|
|
203
246
|
cached[3] = intent;
|
|
@@ -225,22 +268,18 @@ export function query(fn, name) {
|
|
|
225
268
|
if (intent !== "preload")
|
|
226
269
|
res = settle(res);
|
|
227
270
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
228
|
-
|
|
229
|
-
if (isServer &&
|
|
230
|
-
sharedConfig.context &&
|
|
231
|
-
sharedConfig.context.async &&
|
|
232
|
-
!sharedConfig.context.noHydrate) {
|
|
233
|
-
const e = getRequestEvent();
|
|
234
|
-
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
235
|
-
}
|
|
271
|
+
isServer && serializeForHydration(key, res);
|
|
236
272
|
return res;
|
|
237
273
|
// a settled read: the response handling applied to the value or the promise
|
|
238
274
|
function settle(v) {
|
|
275
|
+
let cookies = isServer ? forwardedCookies.get(cached) : undefined;
|
|
276
|
+
if (isServer && !cookies)
|
|
277
|
+
forwardedCookies.set(cached, (cookies = new WeakSet()));
|
|
239
278
|
return "then" in v
|
|
240
|
-
? v.then(handleResponse(false), handleResponse(true))
|
|
241
|
-
: handleResponse(false)(v);
|
|
279
|
+
? v.then(handleResponse(false, cookies), handleResponse(true, cookies))
|
|
280
|
+
: handleResponse(false, cookies)(v);
|
|
242
281
|
}
|
|
243
|
-
function handleResponse(error) {
|
|
282
|
+
function handleResponse(error, cookies) {
|
|
244
283
|
return async (v) => {
|
|
245
284
|
let enveloped;
|
|
246
285
|
let hasEnveloped = false;
|
|
@@ -253,11 +292,22 @@ export function query(fn, name) {
|
|
|
253
292
|
if (v instanceof Response) {
|
|
254
293
|
const e = getRequestEvent();
|
|
255
294
|
if (e) {
|
|
295
|
+
const headers = e.response.headers;
|
|
296
|
+
const skipCookies = cookies?.has(headers);
|
|
256
297
|
for (const [key, value] of v.headers) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
298
|
+
// Keep metadata for the query's body off the document response.
|
|
299
|
+
if (responseBodyHeaders.has(key))
|
|
300
|
+
continue;
|
|
301
|
+
if (key === "set-cookie") {
|
|
302
|
+
if (!skipCookies)
|
|
303
|
+
headers.append(key, value);
|
|
304
|
+
}
|
|
305
|
+
else if (headers.get(key) !== value)
|
|
306
|
+
headers.set(key, value);
|
|
307
|
+
}
|
|
308
|
+
// Each cached result contributes its cookies once per document.
|
|
309
|
+
if (!skipCookies && v.headers.has("set-cookie")) {
|
|
310
|
+
cookies?.add(headers);
|
|
261
311
|
}
|
|
262
312
|
}
|
|
263
313
|
let url = v.headers.get(LocationHeader);
|
|
@@ -346,6 +396,7 @@ query.set = (key, value) => {
|
|
|
346
396
|
const now = Date.now();
|
|
347
397
|
let cached = cache.get(key);
|
|
348
398
|
if (cached) {
|
|
399
|
+
isServer && forwardedCookies.delete(cached);
|
|
349
400
|
cached[0] = now;
|
|
350
401
|
cached[1] = Promise.resolve(value);
|
|
351
402
|
cached[2] = value;
|
|
@@ -399,7 +450,7 @@ export function hashKey(args) {
|
|
|
399
450
|
.reduce((result, key) => {
|
|
400
451
|
result[key] = val[key];
|
|
401
452
|
return result;
|
|
402
|
-
},
|
|
453
|
+
}, Object.create(null))
|
|
403
454
|
: val);
|
|
404
455
|
}
|
|
405
456
|
function isPlainObject(obj) {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect,
|
|
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, isHydrating, 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);
|
|
@@ -1118,7 +1121,8 @@ function createLocation(path, state, queryWrapper) {
|
|
|
1118
1121
|
const pathname = createMemo(() => url().pathname);
|
|
1119
1122
|
const search = createMemo(() => url().search);
|
|
1120
1123
|
const hash = createMemo(() => url().hash);
|
|
1121
|
-
|
|
1124
|
+
// Pathname and hash changes do not affect the query.
|
|
1125
|
+
const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
|
|
1122
1126
|
return {
|
|
1123
1127
|
get pathname() {
|
|
1124
1128
|
return pathname();
|
|
@@ -2211,7 +2215,6 @@ function describeNavigation(match, next, written, from) {
|
|
|
2211
2215
|
const t = to();
|
|
2212
2216
|
return t === undefined ? undefined : new URL(t, mockBase).pathname;
|
|
2213
2217
|
};
|
|
2214
|
-
const matches = pathname => untrack(() => match(pathname));
|
|
2215
2218
|
const ref = {
|
|
2216
2219
|
kind: "navigation",
|
|
2217
2220
|
get to() {
|
|
@@ -2219,21 +2222,62 @@ function describeNavigation(match, next, written, from) {
|
|
|
2219
2222
|
},
|
|
2220
2223
|
from,
|
|
2221
2224
|
get name() {
|
|
2222
|
-
|
|
2223
|
-
if (p === undefined) return undefined;
|
|
2224
|
-
const m = matches(p);
|
|
2225
|
-
return m.length ? m[m.length - 1].route.pattern || "/" : p;
|
|
2225
|
+
return routeName(match, pathname());
|
|
2226
2226
|
},
|
|
2227
2227
|
get params() {
|
|
2228
|
-
|
|
2229
|
-
const m = p === undefined ? [] : matches(p);
|
|
2230
|
-
return m.length ? mergeParams(m) : undefined;
|
|
2228
|
+
return routeParams(match, pathname());
|
|
2231
2229
|
}
|
|
2232
2230
|
};
|
|
2233
2231
|
if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
|
|
2234
2232
|
return ref;
|
|
2235
2233
|
}
|
|
2236
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
|
+
|
|
2237
2281
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2238
2282
|
function createIntegration(history, match) {
|
|
2239
2283
|
let committing = false;
|
|
@@ -2248,7 +2292,6 @@ function createIntegration(history, match) {
|
|
|
2248
2292
|
name: "location"
|
|
2249
2293
|
});
|
|
2250
2294
|
const signal = [read, next => {
|
|
2251
|
-
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
2252
2295
|
// What the write resolved to, or undefined when there was nothing to
|
|
2253
2296
|
// write (see `resolveLocationWrite`).
|
|
2254
2297
|
let written;
|
|
@@ -2281,7 +2324,14 @@ function createIntegration(history, match) {
|
|
|
2281
2324
|
// while a navigation is pending, which is the `from` a hop wants too.
|
|
2282
2325
|
// A write the no-op rule drops is still declared: the engine settles a
|
|
2283
2326
|
// navigation whose write "did not survive the equality gate" on the spot.
|
|
2284
|
-
|
|
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();
|
|
2285
2335
|
}];
|
|
2286
2336
|
history.init && onCleanup(history.init((value = history.get()) => {
|
|
2287
2337
|
if (committing) return;
|
|
@@ -2362,11 +2412,14 @@ function createRouter(config) {
|
|
|
2362
2412
|
}
|
|
2363
2413
|
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
2364
2414
|
let context;
|
|
2365
|
-
const
|
|
2415
|
+
const buildContext = () => createRouterContext(integration, branches, () => context, {
|
|
2366
2416
|
base: basePath,
|
|
2367
2417
|
singleFlight: config.singleFlight,
|
|
2368
2418
|
transformUrl: config.transformUrl
|
|
2369
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();
|
|
2370
2423
|
if (!isServer) {
|
|
2371
2424
|
setupNativeEvents({
|
|
2372
2425
|
preload: config.preloadLinks,
|
|
@@ -2604,10 +2657,29 @@ source) {
|
|
|
2604
2657
|
const LocationHeader = "Location";
|
|
2605
2658
|
const PRELOAD_TIMEOUT$1 = 5000;
|
|
2606
2659
|
const CACHE_TIMEOUT = 180000;
|
|
2607
|
-
|
|
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)
|
|
2608
2663
|
// hold values the server computed while rendering THIS page, so their age is
|
|
2609
2664
|
// anchored here — not at whenever a late query() call happens to consume one.
|
|
2610
2665
|
const bootTime = Date.now();
|
|
2666
|
+
// Captured at module load: a hydrating client runs a serialized node's
|
|
2667
|
+
// compute once as a dependency trace with the global `Promise` swapped for a
|
|
2668
|
+
// never-settling mock, and a query() read inside that trace still consumes
|
|
2669
|
+
// and caches its flight entry. Wrapped with the global there, a settled
|
|
2670
|
+
// entry would cache a promise that never resolves, hanging every navigation
|
|
2671
|
+
// that reuses it.
|
|
2672
|
+
const NativePromise = Promise;
|
|
2673
|
+
|
|
2674
|
+
// Every hydratable read ships its entry: the writer keeps the first write
|
|
2675
|
+
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
2676
|
+
// from an island that reads the same key later.
|
|
2677
|
+
function serializeForHydration(key, res) {
|
|
2678
|
+
const writer = getHydrationWriter();
|
|
2679
|
+
if (!writer || !writer.async || !isHydratable()) return;
|
|
2680
|
+
const e = getRequestEvent();
|
|
2681
|
+
(!e || !e.serverOnly) && writer.write(key, res);
|
|
2682
|
+
}
|
|
2611
2683
|
let cacheMap = new Map();
|
|
2612
2684
|
|
|
2613
2685
|
// cleanup forward/back cache
|
|
@@ -2722,7 +2794,14 @@ function query(fn, name) {
|
|
|
2722
2794
|
tracking = true;
|
|
2723
2795
|
onCleanup(() => cached[4].count--);
|
|
2724
2796
|
}
|
|
2725
|
-
|
|
2797
|
+
|
|
2798
|
+
// A read with no navigation in flight while hydrating is claiming server
|
|
2799
|
+
// DOM, so it takes the cached entry at any age — the rule adoption below
|
|
2800
|
+
// applies to flight entries. It can come from a boundary resuming long
|
|
2801
|
+
// after boot, once the entry was adopted and its first reader disposed;
|
|
2802
|
+
// refetching there would run inside Solid's hydration trace, where fetch
|
|
2803
|
+
// and Promise never settle, and cache that.
|
|
2804
|
+
if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || !intent && isHydrating() || Date.now() - cached[0] < PRELOAD_TIMEOUT$1)) {
|
|
2726
2805
|
if (tracking) {
|
|
2727
2806
|
cached[4].count++;
|
|
2728
2807
|
cached[4][0](); // track
|
|
@@ -2736,6 +2815,7 @@ function query(fn, name) {
|
|
|
2736
2815
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
2737
2816
|
}
|
|
2738
2817
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2818
|
+
isServer && serializeForHydration(key, res);
|
|
2739
2819
|
return res;
|
|
2740
2820
|
}
|
|
2741
2821
|
let res;
|
|
@@ -2752,13 +2832,11 @@ function query(fn, name) {
|
|
|
2752
2832
|
// active navigation (or its preload) accepts the payload only while a
|
|
2753
2833
|
// real preload of the same age would still satisfy it — anything older
|
|
2754
2834
|
// refetches instead of serving a minutes-old value on a fresh navigation.
|
|
2755
|
-
if (!isServer &&
|
|
2756
|
-
const
|
|
2757
|
-
if (
|
|
2835
|
+
if (!isServer && (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1))) {
|
|
2836
|
+
const entry = takeHydrationValue(key); // hydrating
|
|
2837
|
+
if (entry) {
|
|
2758
2838
|
adopted = true;
|
|
2759
|
-
res =
|
|
2760
|
-
// @ts-ignore at least until we add a delete method to sharedConfig
|
|
2761
|
-
delete globalThis._$HY.r[key];
|
|
2839
|
+
res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? NativePromise.resolve(entry.value) : NativePromise.reject(entry.error);
|
|
2762
2840
|
}
|
|
2763
2841
|
// A payload too old for this intent stays in the registry: it is
|
|
2764
2842
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -2767,13 +2845,13 @@ function query(fn, name) {
|
|
|
2767
2845
|
if (!adopted) res = fn(...args);
|
|
2768
2846
|
|
|
2769
2847
|
// Adopted entries are stamped at boot — when their data was actually
|
|
2770
|
-
// fetched — not at consumption.
|
|
2771
|
-
// through the
|
|
2772
|
-
//
|
|
2773
|
-
//
|
|
2774
|
-
// it would for a preload performed when the server fetched the data.
|
|
2848
|
+
// fetched — not at consumption. Later hydration reads still reuse them
|
|
2849
|
+
// through the claim rule above, while a navigation that comes along later
|
|
2850
|
+
// sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
|
|
2851
|
+
// as it would for a preload performed when the server fetched the data.
|
|
2775
2852
|
const stamp = adopted ? bootTime : now;
|
|
2776
2853
|
if (cached) {
|
|
2854
|
+
isServer && forwardedCookies.delete(cached);
|
|
2777
2855
|
cached[0] = stamp;
|
|
2778
2856
|
cached[1] = res;
|
|
2779
2857
|
cached[3] = intent;
|
|
@@ -2794,18 +2872,16 @@ function query(fn, name) {
|
|
|
2794
2872
|
}
|
|
2795
2873
|
if (intent !== "preload") res = settle(res);
|
|
2796
2874
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2797
|
-
|
|
2798
|
-
if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
|
|
2799
|
-
const e = getRequestEvent();
|
|
2800
|
-
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
2801
|
-
}
|
|
2875
|
+
isServer && serializeForHydration(key, res);
|
|
2802
2876
|
return res;
|
|
2803
2877
|
|
|
2804
2878
|
// a settled read: the response handling applied to the value or the promise
|
|
2805
2879
|
function settle(v) {
|
|
2806
|
-
|
|
2880
|
+
let cookies = isServer ? forwardedCookies.get(cached) : undefined;
|
|
2881
|
+
if (isServer && !cookies) forwardedCookies.set(cached, cookies = new WeakSet());
|
|
2882
|
+
return "then" in v ? v.then(handleResponse(false, cookies), handleResponse(true, cookies)) : handleResponse(false, cookies)(v);
|
|
2807
2883
|
}
|
|
2808
|
-
function handleResponse(error) {
|
|
2884
|
+
function handleResponse(error, cookies) {
|
|
2809
2885
|
return async v => {
|
|
2810
2886
|
let enveloped;
|
|
2811
2887
|
let hasEnveloped = false;
|
|
@@ -2818,8 +2894,18 @@ function query(fn, name) {
|
|
|
2818
2894
|
if (v instanceof Response) {
|
|
2819
2895
|
const e = getRequestEvent();
|
|
2820
2896
|
if (e) {
|
|
2897
|
+
const headers = e.response.headers;
|
|
2898
|
+
const skipCookies = cookies?.has(headers);
|
|
2821
2899
|
for (const [key, value] of v.headers) {
|
|
2822
|
-
|
|
2900
|
+
// Keep metadata for the query's body off the document response.
|
|
2901
|
+
if (responseBodyHeaders.has(key)) continue;
|
|
2902
|
+
if (key === "set-cookie") {
|
|
2903
|
+
if (!skipCookies) headers.append(key, value);
|
|
2904
|
+
} else if (headers.get(key) !== value) headers.set(key, value);
|
|
2905
|
+
}
|
|
2906
|
+
// Each cached result contributes its cookies once per document.
|
|
2907
|
+
if (!skipCookies && v.headers.has("set-cookie")) {
|
|
2908
|
+
cookies?.add(headers);
|
|
2823
2909
|
}
|
|
2824
2910
|
}
|
|
2825
2911
|
let url = v.headers.get(LocationHeader);
|
|
@@ -2902,6 +2988,7 @@ query.set = (key, value) => {
|
|
|
2902
2988
|
const now = Date.now();
|
|
2903
2989
|
let cached = cache.get(key);
|
|
2904
2990
|
if (cached) {
|
|
2991
|
+
isServer && forwardedCookies.delete(cached);
|
|
2905
2992
|
cached[0] = now;
|
|
2906
2993
|
cached[1] = Promise.resolve(value);
|
|
2907
2994
|
cached[2] = value;
|
|
@@ -2949,7 +3036,7 @@ function hashKey(args) {
|
|
|
2949
3036
|
return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
|
|
2950
3037
|
result[key] = val[key];
|
|
2951
3038
|
return result;
|
|
2952
|
-
},
|
|
3039
|
+
}, Object.create(null)) : val);
|
|
2953
3040
|
}
|
|
2954
3041
|
function isPlainObject(obj) {
|
|
2955
3042
|
let proto;
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner,
|
|
2
|
+
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
|
|
3
3
|
// standalone imports: `DEV` is undefined in solid's production build and
|
|
4
4
|
// `OBSERVE` outside its observe/dev builds, so app bundlers fold the
|
|
5
5
|
// `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
|
|
@@ -53,7 +53,6 @@ function describeNavigation(match, next, written, from) {
|
|
|
53
53
|
const t = to();
|
|
54
54
|
return t === undefined ? undefined : new URL(t, mockBase).pathname;
|
|
55
55
|
};
|
|
56
|
-
const matches = (pathname) => untrack(() => match(pathname));
|
|
57
56
|
const ref = {
|
|
58
57
|
kind: "navigation",
|
|
59
58
|
get to() {
|
|
@@ -61,22 +60,61 @@ function describeNavigation(match, next, written, from) {
|
|
|
61
60
|
},
|
|
62
61
|
from,
|
|
63
62
|
get name() {
|
|
64
|
-
|
|
65
|
-
if (p === undefined)
|
|
66
|
-
return undefined;
|
|
67
|
-
const m = matches(p);
|
|
68
|
-
return m.length ? m[m.length - 1].route.pattern || "/" : p;
|
|
63
|
+
return routeName(match, pathname());
|
|
69
64
|
},
|
|
70
65
|
get params() {
|
|
71
|
-
|
|
72
|
-
const m = p === undefined ? [] : matches(p);
|
|
73
|
-
return m.length ? mergeParams(m) : undefined;
|
|
66
|
+
return routeParams(match, pathname());
|
|
74
67
|
}
|
|
75
68
|
};
|
|
76
69
|
if (next._navigation !== undefined && next._navigation > 1)
|
|
77
70
|
ref.redirect = next._navigation - 1;
|
|
78
71
|
return ref;
|
|
79
72
|
}
|
|
73
|
+
/** The parametrized route `pathname` matches — the last match's pattern, `/` for the root; the pathname itself when nothing matched. */
|
|
74
|
+
function routeName(match, pathname) {
|
|
75
|
+
if (pathname === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const m = untrack(() => match(pathname));
|
|
78
|
+
return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
|
|
79
|
+
}
|
|
80
|
+
/** The params the matched chain bound, merged; `undefined` when nothing matched. */
|
|
81
|
+
function routeParams(match, pathname) {
|
|
82
|
+
if (pathname === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
const m = untrack(() => match(pathname));
|
|
85
|
+
return m.length ? mergeParams(m) : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The route the document arrived on, for solid's observe tier: the same
|
|
89
|
+
* declaration as a navigation's, around the work that establishes the
|
|
90
|
+
* router's initial match — building its context — since a fresh document has
|
|
91
|
+
* no location write to wrap. On the client the engine opens it at the time
|
|
92
|
+
* origin and settles it as the context is built (the first `"navigation"`
|
|
93
|
+
* record, `initial: true`); on the server the same call names the request's
|
|
94
|
+
* `"render"` record (`RenderEvent.route`). Every field is a getter read at
|
|
95
|
+
* settle, as for a navigation: `to` is the location as the router then holds
|
|
96
|
+
* it — the arrival normalized onto the base path when it came in empty, a
|
|
97
|
+
* write the frame itself took (see the setter) — and `name`/`params` match
|
|
98
|
+
* it then, so a lazy subtree that resolved while the server rendered names
|
|
99
|
+
* the exact route.
|
|
100
|
+
*/
|
|
101
|
+
function describeInitial(match, location) {
|
|
102
|
+
const to = () => untrack(location).value;
|
|
103
|
+
const pathname = () => new URL(to(), mockBase).pathname;
|
|
104
|
+
return {
|
|
105
|
+
kind: "navigation",
|
|
106
|
+
initial: true,
|
|
107
|
+
get to() {
|
|
108
|
+
return to();
|
|
109
|
+
},
|
|
110
|
+
get name() {
|
|
111
|
+
return routeName(match, pathname());
|
|
112
|
+
},
|
|
113
|
+
get params() {
|
|
114
|
+
return routeParams(match, pathname());
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
80
118
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
81
119
|
function createIntegration(history, match) {
|
|
82
120
|
let committing = false;
|
|
@@ -91,8 +129,6 @@ function createIntegration(history, match) {
|
|
|
91
129
|
const signal = [
|
|
92
130
|
read,
|
|
93
131
|
(next) => {
|
|
94
|
-
if (sharedConfig.registry && !sharedConfig.done)
|
|
95
|
-
sharedConfig.done = true;
|
|
96
132
|
// What the write resolved to, or undefined when there was nothing to
|
|
97
133
|
// write (see `resolveLocationWrite`).
|
|
98
134
|
let written;
|
|
@@ -128,8 +164,15 @@ function createIntegration(history, match) {
|
|
|
128
164
|
// while a navigation is pending, which is the `from` a hop wants too.
|
|
129
165
|
// A write the no-op rule drops is still declared: the engine settles a
|
|
130
166
|
// navigation whose write "did not survive the equality gate" on the spot.
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
// A write from NO location is not a navigation: it is the arrival being
|
|
168
|
+
// normalized (`createRouterContext` moving an empty location — a hash
|
|
169
|
+
// history with no hash, a memory history seeded with "" — onto the base
|
|
170
|
+
// path), and the arrival is already declared by the initial frame open
|
|
171
|
+
// around the context build (`describeInitial`), which this write then
|
|
172
|
+
// stamps. The location is never empty again after that first write.
|
|
173
|
+
const from = untrack(read).value;
|
|
174
|
+
OBSERVE && from
|
|
175
|
+
? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, from), commit)
|
|
133
176
|
: commit();
|
|
134
177
|
}
|
|
135
178
|
];
|
|
@@ -210,11 +253,16 @@ export function createRouter(config) {
|
|
|
210
253
|
? staticIntegration(props.url, config.history && config.history.utils)
|
|
211
254
|
: createIntegration(history || browserHistory(), matchPath);
|
|
212
255
|
let context;
|
|
213
|
-
const
|
|
256
|
+
const buildContext = () => createRouterContext(integration, branches, () => context, {
|
|
214
257
|
base: basePath,
|
|
215
258
|
singleFlight: config.singleFlight,
|
|
216
259
|
transformUrl: config.transformUrl
|
|
217
260
|
});
|
|
261
|
+
// The route the document arrived on, declared around the initial match
|
|
262
|
+
// (see `describeInitial`); folded out of shipped bundles with `OBSERVE`.
|
|
263
|
+
const routerState = OBSERVE
|
|
264
|
+
? OBSERVE.attribution.withOrigin(describeInitial(matchPath, integration.signal[0]), buildContext)
|
|
265
|
+
: buildContext();
|
|
218
266
|
if (!isServer) {
|
|
219
267
|
setupNativeEvents({
|
|
220
268
|
preload: config.preloadLinks,
|
package/dist/routing.js
CHANGED
|
@@ -502,7 +502,8 @@ function createLocation(path, state, queryWrapper) {
|
|
|
502
502
|
const pathname = createMemo(() => url().pathname);
|
|
503
503
|
const search = createMemo(() => url().search);
|
|
504
504
|
const hash = createMemo(() => url().hash);
|
|
505
|
-
|
|
505
|
+
// Pathname and hash changes do not affect the query.
|
|
506
|
+
const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
|
|
506
507
|
return {
|
|
507
508
|
get pathname() {
|
|
508
509
|
return pathname();
|
package/dist/utils.js
CHANGED
|
@@ -43,7 +43,7 @@ export function validateSearch(schema, raw) {
|
|
|
43
43
|
return outcome;
|
|
44
44
|
}
|
|
45
45
|
export function extractSearchParams(url) {
|
|
46
|
-
const params =
|
|
46
|
+
const params = Object.create(null);
|
|
47
47
|
url.searchParams.forEach((value, key) => {
|
|
48
48
|
if (key in params) {
|
|
49
49
|
if (Array.isArray(params[key]))
|
|
@@ -54,7 +54,8 @@ export function extractSearchParams(url) {
|
|
|
54
54
|
else
|
|
55
55
|
params[key] = value;
|
|
56
56
|
});
|
|
57
|
-
|
|
57
|
+
// Keep __proto__ as an own property on the plain object returned to callers.
|
|
58
|
+
return { ...params };
|
|
58
59
|
}
|
|
59
60
|
export function createMatcher(path, partial, matchFilters) {
|
|
60
61
|
const [pattern, splat] = path.split("/*", 2);
|
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.33",
|
|
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.13",
|
|
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.13",
|
|
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.13",
|
|
55
55
|
"filesystem-routing": ">=0.4.0",
|
|
56
|
-
"solid-js": "^2.0.0-rc.
|
|
56
|
+
"solid-js": "^2.0.0-rc.13"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"filesystem-routing": {
|