@solidjs/router 2.0.0-next.12 → 2.0.0-next.13
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/action.js +5 -1
- package/dist/data/query.js +38 -12
- package/dist/index.js +65 -10
- package/dist/routing.js +30 -5
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/data/action.js
CHANGED
|
@@ -330,7 +330,11 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
330
330
|
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
331
331
|
// set cache
|
|
332
332
|
flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
333
|
-
// trigger revalidation
|
|
333
|
+
// trigger revalidation inside the same transition as the navigation, so
|
|
334
|
+
// the redirect commits atomically with fresh data: surviving consumers
|
|
335
|
+
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
336
|
+
// commit rather than painting stale and updating after. Seeded entries
|
|
337
|
+
// are fresh again by now, so the sweep re-reads them from cache.
|
|
334
338
|
revalidate(keys, false);
|
|
335
339
|
}
|
|
336
340
|
async function handleResponse(response, error, navigate, metadataHandled) {
|
package/dist/data/query.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
|
|
2
|
-
import { getRequestEvent, isResponseEnvelope, isServer } from "@solidjs/web";
|
|
2
|
+
import { getRequestEvent, isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
|
|
3
3
|
import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
|
|
4
|
-
import {
|
|
4
|
+
import { useRouter, useRoute, getIntent, getInPreloadFn } from "../routing.js";
|
|
5
5
|
const LocationHeader = "Location";
|
|
6
6
|
const PRELOAD_TIMEOUT = 5000;
|
|
7
7
|
const CACHE_TIMEOUT = 180000;
|
|
@@ -58,7 +58,9 @@ export function query(fn, name) {
|
|
|
58
58
|
const intent = getIntent();
|
|
59
59
|
const inPreloadFn = getInPreloadFn();
|
|
60
60
|
const owner = getOwner();
|
|
61
|
-
const
|
|
61
|
+
const router = owner ? useRouter() : undefined;
|
|
62
|
+
const route = owner ? useRoute() : undefined;
|
|
63
|
+
const navigate = router && router.navigatorFactory();
|
|
62
64
|
const now = Date.now();
|
|
63
65
|
const key = name + hashKey(args);
|
|
64
66
|
let cached = cache.get(key);
|
|
@@ -82,17 +84,27 @@ export function query(fn, name) {
|
|
|
82
84
|
tracking = true;
|
|
83
85
|
onCleanup(() => cached[4].count--);
|
|
84
86
|
}
|
|
87
|
+
// A read under a route section the in-flight navigation is leaving keeps
|
|
88
|
+
// its invalidated value instead of refetching: this render is disposed at
|
|
89
|
+
// commit and never paints, and refiring would put a second request on
|
|
90
|
+
// what single flight promises is one. The entry stays a cache miss, so
|
|
91
|
+
// the fetch happens at its next real use — a later navigation's preload
|
|
92
|
+
// or read — unless the flight payload seeds it first. Retained sections
|
|
93
|
+
// still refetch here, inside the navigation's transition, so the
|
|
94
|
+
// redirect commits with fresh data.
|
|
95
|
+
const leaving = cached && !cached[0] && route && router.leaving?.(route);
|
|
85
96
|
if (cached &&
|
|
86
|
-
cached[0]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
(cached[0]
|
|
98
|
+
? isServer ||
|
|
99
|
+
intent === "native" ||
|
|
100
|
+
cached[4].count ||
|
|
101
|
+
Date.now() - cached[0] < PRELOAD_TIMEOUT
|
|
102
|
+
: leaving)) {
|
|
91
103
|
if (tracking) {
|
|
92
104
|
cached[4].count++;
|
|
93
105
|
cached[4][0](); // track
|
|
94
106
|
}
|
|
95
|
-
if (cached[3] === "preload" && intent !== "preload") {
|
|
107
|
+
if (!leaving && cached[3] === "preload" && intent !== "preload") {
|
|
96
108
|
cached[0] = now;
|
|
97
109
|
}
|
|
98
110
|
let res = cached[1];
|
|
@@ -101,7 +113,7 @@ export function query(fn, name) {
|
|
|
101
113
|
"then" in cached[1]
|
|
102
114
|
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
103
115
|
: handleResponse(false)(cached[1]);
|
|
104
|
-
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
116
|
+
!leaving && !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
105
117
|
}
|
|
106
118
|
inPreloadFn && "then" in res && res.catch(() => { });
|
|
107
119
|
return res;
|
|
@@ -171,14 +183,28 @@ export function query(fn, name) {
|
|
|
171
183
|
}
|
|
172
184
|
const url = v.headers.get(LocationHeader);
|
|
173
185
|
if (url !== null) {
|
|
186
|
+
// invalidate the redirect's revalidation keys before navigating so
|
|
187
|
+
// the destination's preloads see the miss and fetch fresh (#580 thread)
|
|
188
|
+
const keys = !isServer && v.headers.get(REVALIDATE_HEADER)?.split(",");
|
|
189
|
+
keys && cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
174
190
|
// client + server relative redirect
|
|
175
|
-
|
|
191
|
+
const soft = navigate && url.startsWith("/");
|
|
192
|
+
if (soft)
|
|
176
193
|
navigate(url, { replace: true });
|
|
177
194
|
else if (!isServer)
|
|
178
195
|
window.location.href = url;
|
|
179
196
|
else if (e)
|
|
180
197
|
e.response.status = 302;
|
|
181
|
-
|
|
198
|
+
// sweep the live signals inside the same transition as the
|
|
199
|
+
// navigation: surviving consumers (shared layouts) refetch and
|
|
200
|
+
// hold the commit, so the destination never paints stale data
|
|
201
|
+
keys && revalidate(keys, false);
|
|
202
|
+
// Hold the read pending on the client: the navigation unmounts this
|
|
203
|
+
// consumer, and resolving `undefined` instead hands a missing value
|
|
204
|
+
// to whatever renders before the transition commits (a downstream
|
|
205
|
+
// `.map()` crashes). The server keeps resolving — its render can't
|
|
206
|
+
// await a navigation and the 302 discards the body anyway.
|
|
207
|
+
return isServer ? undefined : new Promise(() => { });
|
|
182
208
|
}
|
|
183
209
|
if (hasEnveloped)
|
|
184
210
|
v = enveloped;
|
package/dist/index.js
CHANGED
|
@@ -1165,6 +1165,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1165
1165
|
get pendingTarget() {
|
|
1166
1166
|
return lastTransitionTarget;
|
|
1167
1167
|
},
|
|
1168
|
+
leaving(route) {
|
|
1169
|
+
const t = lastTransitionTarget;
|
|
1170
|
+
if (!t || !route.retained) return false;
|
|
1171
|
+
const pathname = t.value.split(/[?#]/, 1)[0];
|
|
1172
|
+
return !route.retained(options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
1173
|
+
},
|
|
1168
1174
|
renderPath,
|
|
1169
1175
|
parsePath,
|
|
1170
1176
|
navigatorFactory,
|
|
@@ -1191,7 +1197,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1191
1197
|
}
|
|
1192
1198
|
// typed path proxy nodes coerce to their href
|
|
1193
1199
|
if (typeof to !== "string") to = to.toString();
|
|
1194
|
-
const queryOnly = !to || to[0] === "?";
|
|
1195
1200
|
const {
|
|
1196
1201
|
replace,
|
|
1197
1202
|
resolve,
|
|
@@ -1199,11 +1204,21 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1199
1204
|
state: nextState
|
|
1200
1205
|
} = {
|
|
1201
1206
|
replace: false,
|
|
1202
|
-
resolve:
|
|
1207
|
+
resolve: true,
|
|
1203
1208
|
scroll: true,
|
|
1204
1209
|
...options
|
|
1205
1210
|
};
|
|
1206
|
-
|
|
1211
|
+
|
|
1212
|
+
// A string means what the same string means as an href. Leading "/"
|
|
1213
|
+
// stays base-prefixed; anything else resolves URL-style against the
|
|
1214
|
+
// current location — `new URL` collapses `..` and handles `?`/`#`-only
|
|
1215
|
+
// references natively (#502). A cross-origin result (scheme or
|
|
1216
|
+
// protocol-relative input) falls through as unroutable.
|
|
1217
|
+
let resolvedTo;
|
|
1218
|
+
if (!resolve) resolvedTo = resolvePath((!to || to[0] === "?") && location.pathname || "", to);else if (to[0] === "/") resolvedTo = route.resolvePath(to);else {
|
|
1219
|
+
const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
|
|
1220
|
+
resolvedTo = url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
|
|
1221
|
+
}
|
|
1207
1222
|
if (resolvedTo === undefined) {
|
|
1208
1223
|
throw new Error(`Path '${to}' is not a routable path`);
|
|
1209
1224
|
} else if (referrers.length >= MAX_REDIRECTS) {
|
|
@@ -1360,6 +1375,13 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1360
1375
|
pattern,
|
|
1361
1376
|
params,
|
|
1362
1377
|
path,
|
|
1378
|
+
retained(target) {
|
|
1379
|
+
const m = match();
|
|
1380
|
+
const p = m.route.matcher(target);
|
|
1381
|
+
// params derive from the matched path, so equal paths mean this
|
|
1382
|
+
// section instance (layouts match prefixes via partial matchers)
|
|
1383
|
+
return !!p && p.path === m.path;
|
|
1384
|
+
},
|
|
1363
1385
|
outlet: () => component ? createComponent(component, {
|
|
1364
1386
|
params,
|
|
1365
1387
|
location,
|
|
@@ -2174,7 +2196,9 @@ function query(fn, name) {
|
|
|
2174
2196
|
const intent = getIntent();
|
|
2175
2197
|
const inPreloadFn = getInPreloadFn();
|
|
2176
2198
|
const owner = getOwner();
|
|
2177
|
-
const
|
|
2199
|
+
const router = owner ? useRouter() : undefined;
|
|
2200
|
+
const route = owner ? useRoute() : undefined;
|
|
2201
|
+
const navigate = router && router.navigatorFactory();
|
|
2178
2202
|
const now = Date.now();
|
|
2179
2203
|
const key = name + hashKey(args);
|
|
2180
2204
|
let cached = cache.get(key);
|
|
@@ -2197,18 +2221,28 @@ function query(fn, name) {
|
|
|
2197
2221
|
tracking = true;
|
|
2198
2222
|
onCleanup(() => cached[4].count--);
|
|
2199
2223
|
}
|
|
2200
|
-
|
|
2224
|
+
|
|
2225
|
+
// A read under a route section the in-flight navigation is leaving keeps
|
|
2226
|
+
// its invalidated value instead of refetching: this render is disposed at
|
|
2227
|
+
// commit and never paints, and refiring would put a second request on
|
|
2228
|
+
// what single flight promises is one. The entry stays a cache miss, so
|
|
2229
|
+
// the fetch happens at its next real use — a later navigation's preload
|
|
2230
|
+
// or read — unless the flight payload seeds it first. Retained sections
|
|
2231
|
+
// still refetch here, inside the navigation's transition, so the
|
|
2232
|
+
// redirect commits with fresh data.
|
|
2233
|
+
const leaving = cached && !cached[0] && route && router.leaving?.(route);
|
|
2234
|
+
if (cached && (cached[0] ? isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT : leaving)) {
|
|
2201
2235
|
if (tracking) {
|
|
2202
2236
|
cached[4].count++;
|
|
2203
2237
|
cached[4][0](); // track
|
|
2204
2238
|
}
|
|
2205
|
-
if (cached[3] === "preload" && intent !== "preload") {
|
|
2239
|
+
if (!leaving && cached[3] === "preload" && intent !== "preload") {
|
|
2206
2240
|
cached[0] = now;
|
|
2207
2241
|
}
|
|
2208
2242
|
let res = cached[1];
|
|
2209
2243
|
if (intent !== "preload") {
|
|
2210
2244
|
res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
|
|
2211
|
-
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
2245
|
+
!leaving && !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
2212
2246
|
}
|
|
2213
2247
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
2214
2248
|
return res;
|
|
@@ -2267,11 +2301,28 @@ function query(fn, name) {
|
|
|
2267
2301
|
}
|
|
2268
2302
|
const url = v.headers.get(LocationHeader);
|
|
2269
2303
|
if (url !== null) {
|
|
2304
|
+
// invalidate the redirect's revalidation keys before navigating so
|
|
2305
|
+
// the destination's preloads see the miss and fetch fresh (#580 thread)
|
|
2306
|
+
const keys = !isServer && v.headers.get(REVALIDATE_HEADER)?.split(",");
|
|
2307
|
+
keys && cacheKeyOp(keys, entry => entry[0] = 0);
|
|
2308
|
+
|
|
2270
2309
|
// client + server relative redirect
|
|
2271
|
-
|
|
2310
|
+
const soft = navigate && url.startsWith("/");
|
|
2311
|
+
if (soft) navigate(url, {
|
|
2272
2312
|
replace: true
|
|
2273
2313
|
});else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;
|
|
2274
|
-
|
|
2314
|
+
|
|
2315
|
+
// sweep the live signals inside the same transition as the
|
|
2316
|
+
// navigation: surviving consumers (shared layouts) refetch and
|
|
2317
|
+
// hold the commit, so the destination never paints stale data
|
|
2318
|
+
keys && revalidate(keys, false);
|
|
2319
|
+
|
|
2320
|
+
// Hold the read pending on the client: the navigation unmounts this
|
|
2321
|
+
// consumer, and resolving `undefined` instead hands a missing value
|
|
2322
|
+
// to whatever renders before the transition commits (a downstream
|
|
2323
|
+
// `.map()` crashes). The server keeps resolving — its render can't
|
|
2324
|
+
// await a navigation and the 302 discards the body anyway.
|
|
2325
|
+
return isServer ? undefined : new Promise(() => {});
|
|
2275
2326
|
}
|
|
2276
2327
|
if (hasEnveloped) v = enveloped;else if (v.body) {
|
|
2277
2328
|
// responses the transport hands over whole (revalidation) carry a
|
|
@@ -2660,7 +2711,11 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
2660
2711
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
2661
2712
|
// set cache
|
|
2662
2713
|
flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
2663
|
-
// trigger revalidation
|
|
2714
|
+
// trigger revalidation inside the same transition as the navigation, so
|
|
2715
|
+
// the redirect commits atomically with fresh data: surviving consumers
|
|
2716
|
+
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
2717
|
+
// commit rather than painting stale and updating after. Seeded entries
|
|
2718
|
+
// are fresh again by now, so the sweep re-reads them from cache.
|
|
2664
2719
|
revalidate(keys, false);
|
|
2665
2720
|
}
|
|
2666
2721
|
async function handleResponse(response, error, navigate, metadataHandled) {
|
package/dist/routing.js
CHANGED
|
@@ -621,6 +621,13 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
621
621
|
get pendingTarget() {
|
|
622
622
|
return lastTransitionTarget;
|
|
623
623
|
},
|
|
624
|
+
leaving(route) {
|
|
625
|
+
const t = lastTransitionTarget;
|
|
626
|
+
if (!t || !route.retained)
|
|
627
|
+
return false;
|
|
628
|
+
const pathname = t.value.split(/[?#]/, 1)[0];
|
|
629
|
+
return !route.retained(options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
630
|
+
},
|
|
624
631
|
renderPath,
|
|
625
632
|
parsePath,
|
|
626
633
|
navigatorFactory,
|
|
@@ -650,16 +657,27 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
650
657
|
// typed path proxy nodes coerce to their href
|
|
651
658
|
if (typeof to !== "string")
|
|
652
659
|
to = to.toString();
|
|
653
|
-
const queryOnly = !to || to[0] === "?";
|
|
654
660
|
const { replace, resolve, scroll, state: nextState } = {
|
|
655
661
|
replace: false,
|
|
656
|
-
resolve:
|
|
662
|
+
resolve: true,
|
|
657
663
|
scroll: true,
|
|
658
664
|
...options
|
|
659
665
|
};
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
666
|
+
// A string means what the same string means as an href. Leading "/"
|
|
667
|
+
// stays base-prefixed; anything else resolves URL-style against the
|
|
668
|
+
// current location — `new URL` collapses `..` and handles `?`/`#`-only
|
|
669
|
+
// references natively (#502). A cross-origin result (scheme or
|
|
670
|
+
// protocol-relative input) falls through as unroutable.
|
|
671
|
+
let resolvedTo;
|
|
672
|
+
if (!resolve)
|
|
673
|
+
resolvedTo = resolvePath(((!to || to[0] === "?") && location.pathname) || "", to);
|
|
674
|
+
else if (to[0] === "/")
|
|
675
|
+
resolvedTo = route.resolvePath(to);
|
|
676
|
+
else {
|
|
677
|
+
const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
|
|
678
|
+
resolvedTo =
|
|
679
|
+
url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
|
|
680
|
+
}
|
|
663
681
|
if (resolvedTo === undefined) {
|
|
664
682
|
throw new Error(`Path '${to}' is not a routable path`);
|
|
665
683
|
}
|
|
@@ -797,6 +815,13 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
797
815
|
pattern,
|
|
798
816
|
params,
|
|
799
817
|
path,
|
|
818
|
+
retained(target) {
|
|
819
|
+
const m = match();
|
|
820
|
+
const p = m.route.matcher(target);
|
|
821
|
+
// params derive from the matched path, so equal paths mean this
|
|
822
|
+
// section instance (layouts match prefixes via partial matchers)
|
|
823
|
+
return !!p && p.path === m.path;
|
|
824
|
+
},
|
|
800
825
|
outlet: () => component
|
|
801
826
|
? createComponent(component, {
|
|
802
827
|
params,
|
package/dist/types.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface Location<S = unknown> extends Path {
|
|
|
74
74
|
key: string;
|
|
75
75
|
}
|
|
76
76
|
export interface NavigateOptions<S = unknown> {
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the target like an href: leading-`/` paths are prefixed with the
|
|
79
|
+
* router base, anything else resolves URL-style against the current
|
|
80
|
+
* location (`..` collapses, `?`/`#`-only strings keep the path). `false`
|
|
81
|
+
* takes the string as the final path. Defaults to `true`.
|
|
82
|
+
*/
|
|
77
83
|
resolve: boolean;
|
|
78
84
|
replace: boolean;
|
|
79
85
|
scroll: boolean;
|
|
@@ -238,6 +244,8 @@ export interface RouteContext {
|
|
|
238
244
|
path: () => string;
|
|
239
245
|
outlet: () => JSX.Element;
|
|
240
246
|
resolvePath(to: string): string | undefined;
|
|
247
|
+
/** Whether this route section still matches `target` (same matched path), i.e. survives a navigation there. Absent on the base route, which always survives. */
|
|
248
|
+
retained?(target: string): boolean;
|
|
241
249
|
}
|
|
242
250
|
export interface RouterUtils {
|
|
243
251
|
renderPath(path: string): string;
|
|
@@ -256,6 +264,8 @@ export interface RouterContext {
|
|
|
256
264
|
isRouting: () => boolean;
|
|
257
265
|
/** The target of the in-flight navigation transition, if any. Not reactive. */
|
|
258
266
|
readonly pendingTarget?: LocationChange;
|
|
267
|
+
/** Whether `route` is a section the in-flight navigation is leaving. False when no navigation is pending. */
|
|
268
|
+
leaving?(route: RouteContext): boolean;
|
|
259
269
|
matches: () => RouteMatch[];
|
|
260
270
|
renderPath(path: string): string;
|
|
261
271
|
parsePath(str: string): string;
|