@solidjs/router 2.0.0-next.19 → 2.0.0-next.20
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/index.js +103 -107
- package/dist/routers/factory.jsx +21 -7
- package/dist/routing.d.ts +9 -12
- package/dist/routing.js +106 -101
- package/dist/types.d.ts +7 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV,
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
|
|
2
2
|
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
|
|
3
3
|
import { subscribeFlightData, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
|
|
@@ -844,18 +844,15 @@ function getLazyBoundary(thunk) {
|
|
|
844
844
|
* synchronously once available, the in-flight promise otherwise.
|
|
845
845
|
*
|
|
846
846
|
* Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
|
|
847
|
-
* re-fetches a failed dynamic import):
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
* the
|
|
855
|
-
*
|
|
856
|
-
* and hold nothing — each request retries. Commit is always async — even for
|
|
857
|
-
* thunks returning arrays — so the version bump never writes a signal from
|
|
858
|
-
* inside a render computation.
|
|
847
|
+
* re-fetches a failed dynamic import): each client router's async boundary
|
|
848
|
+
* reader retains the rejection as reactive error status, so the destination
|
|
849
|
+
* error boundary receives it without refiring the import. The shared record
|
|
850
|
+
* holds that rejection through the current turn for concurrent readers, then
|
|
851
|
+
* clears so a later tracked retry — boundary reset() or a new navigation —
|
|
852
|
+
* starts a fresh import. Server records are shared across requests and hold
|
|
853
|
+
* nothing, so each request retries. Commit is always async — even for thunks
|
|
854
|
+
* returning arrays — so the version bump never writes a signal from inside a
|
|
855
|
+
* render computation.
|
|
859
856
|
*/
|
|
860
857
|
function resolveLazySubtree(record) {
|
|
861
858
|
if (record.resolved) return record.resolved;
|
|
@@ -874,7 +871,15 @@ function resolveLazySubtree(record) {
|
|
|
874
871
|
return record.resolved;
|
|
875
872
|
}, e => {
|
|
876
873
|
// ?? Error(): a held undefined would read as "no failure" and refire
|
|
877
|
-
if (!isServer)
|
|
874
|
+
if (!isServer) {
|
|
875
|
+
record.error = e ?? new Error();
|
|
876
|
+
record.sweep = true;
|
|
877
|
+
// Existing per-router async readers retain the rejection in their
|
|
878
|
+
// reactive error status. Hold the shared record through this turn so
|
|
879
|
+
// concurrent readers receive the same failure, then permit remounts
|
|
880
|
+
// and boundary resets to create a fresh import attempt.
|
|
881
|
+
queueMicrotask(() => record.error = record.sweep = undefined);
|
|
882
|
+
}
|
|
878
883
|
record.promise = undefined;
|
|
879
884
|
throw e;
|
|
880
885
|
});
|
|
@@ -1097,9 +1102,9 @@ let flashDecoder;
|
|
|
1097
1102
|
function provideFlashDecoder(decoder) {
|
|
1098
1103
|
flashDecoder || (flashDecoder = decoder);
|
|
1099
1104
|
}
|
|
1100
|
-
let
|
|
1105
|
+
let preloadIntent;
|
|
1101
1106
|
function getIntent() {
|
|
1102
|
-
return intent;
|
|
1107
|
+
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
1103
1108
|
}
|
|
1104
1109
|
let inPreloadFn = false;
|
|
1105
1110
|
function getInPreloadFn() {
|
|
@@ -1125,26 +1130,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1125
1130
|
setSource({
|
|
1126
1131
|
value: basePath,
|
|
1127
1132
|
replace: true,
|
|
1128
|
-
scroll: false
|
|
1133
|
+
scroll: false,
|
|
1134
|
+
_navigation: isServer ? undefined : 1
|
|
1129
1135
|
});
|
|
1130
1136
|
}
|
|
1131
|
-
const
|
|
1132
|
-
ownedWrite: true
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
// Navigate override written from event handlers.
|
|
1136
|
-
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
|
|
1137
|
-
ownedWrite: true
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
// Keep track of last target, so that last call to navigate wins
|
|
1141
|
-
let lastTransitionTarget;
|
|
1142
|
-
|
|
1143
|
-
// source() remains canonical for native history changes; navigateTarget()
|
|
1144
|
-
// temporarily overrides it for in-flight programmatic navigation.
|
|
1145
|
-
const effective = createMemo(() => navigateTarget() ?? source());
|
|
1146
|
-
const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
|
|
1147
|
-
const referrers = [];
|
|
1137
|
+
const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
|
|
1148
1138
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
1149
1139
|
// must be appended before streaming flushes the response headers, and an
|
|
1150
1140
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -1164,6 +1154,24 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1164
1154
|
}
|
|
1165
1155
|
}
|
|
1166
1156
|
let submissions;
|
|
1157
|
+
|
|
1158
|
+
// NotReadyError's source must be a reactive async node, not the raw
|
|
1159
|
+
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
1160
|
+
// is delivered through the graph's error channel and resolution wakes the
|
|
1161
|
+
// parked matches computation.
|
|
1162
|
+
const routerOwner = getOwner();
|
|
1163
|
+
const lazyReaders = new WeakMap();
|
|
1164
|
+
const readLazySubtree = record => {
|
|
1165
|
+
let read = lazyReaders.get(record);
|
|
1166
|
+
if (!read) {
|
|
1167
|
+
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1168
|
+
const result = resolveLazySubtree(record);
|
|
1169
|
+
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1170
|
+
}));
|
|
1171
|
+
lazyReaders.set(record, read);
|
|
1172
|
+
}
|
|
1173
|
+
return read();
|
|
1174
|
+
};
|
|
1167
1175
|
const matches = createMemo(() => {
|
|
1168
1176
|
const pathname = typeof options.transformUrl === "function" ? options.transformUrl(location.pathname) : location.pathname;
|
|
1169
1177
|
const m = getRouteMatches(branches(), pathname);
|
|
@@ -1176,26 +1184,22 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1176
1184
|
// inside a boundary just parks the recomputed chain again.
|
|
1177
1185
|
const pending = unresolvedLazyMatches(m);
|
|
1178
1186
|
if (pending.length) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1187
|
+
if (isServer) {
|
|
1188
|
+
// SSR carries the Promise through NotReadyError so the streaming
|
|
1189
|
+
// renderer can resume without attempting to serialize route
|
|
1190
|
+
// definitions (which contain component functions).
|
|
1191
|
+
const all = Promise.all(pending.map(resolveLazySubtree));
|
|
1192
|
+
all.catch(() => {});
|
|
1193
|
+
throw new NotReadyError(all);
|
|
1194
|
+
} else {
|
|
1195
|
+
// On the client the source must be a reactive async node so transition
|
|
1196
|
+
// settlement and rejection delivery remain inside the signals graph.
|
|
1197
|
+
for (const boundary of pending) readLazySubtree(boundary);
|
|
1198
|
+
}
|
|
1185
1199
|
}
|
|
1186
1200
|
return m;
|
|
1187
1201
|
});
|
|
1188
|
-
|
|
1189
|
-
// Every write is a transition in Solid 2, so a native history pop forks the
|
|
1190
|
-
// source signal exactly like programmatic navigation does. isRouting is
|
|
1191
|
-
// therefore derived: the manual flag covers navigateFromRoute's explicit
|
|
1192
|
-
// window, and isPending over the location/matches read reports any
|
|
1193
|
-
// in-flight fork — including popstate traversals and the lazy-subtree
|
|
1194
|
-
// resolution matches() parks on.
|
|
1195
|
-
const isRouting = createMemo(() => isNavigating() || isPending(() => {
|
|
1196
|
-
// A real error means the navigation settled (failed) — it surfaces
|
|
1197
|
-
// to the app through render reads, not through this derivation.
|
|
1198
|
-
// Not-ready must keep propagating so isPending sees the parking.
|
|
1202
|
+
const routingPending = createMemo(() => isPending(() => {
|
|
1199
1203
|
try {
|
|
1200
1204
|
matches();
|
|
1201
1205
|
} catch (e) {
|
|
@@ -1204,6 +1208,17 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1204
1208
|
location.search;
|
|
1205
1209
|
location.hash;
|
|
1206
1210
|
}));
|
|
1211
|
+
const isRouting = () => routingPending() || isPending(source);
|
|
1212
|
+
const transitionIntent = () => {
|
|
1213
|
+
if (!isPending(source)) return;
|
|
1214
|
+
const navigation = latest(source)._navigation;
|
|
1215
|
+
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
1216
|
+
};
|
|
1217
|
+
const pendingNavigation = () => {
|
|
1218
|
+
if (!isRouting()) return;
|
|
1219
|
+
const target = latest(source);
|
|
1220
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
1221
|
+
};
|
|
1207
1222
|
const buildParams = () => mergeParams(matches());
|
|
1208
1223
|
const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
|
|
1209
1224
|
const params = wrapParams(buildParams);
|
|
@@ -1222,8 +1237,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1222
1237
|
params,
|
|
1223
1238
|
wrapParams,
|
|
1224
1239
|
isRouting,
|
|
1240
|
+
intent: transitionIntent,
|
|
1225
1241
|
get pendingTarget() {
|
|
1226
|
-
return
|
|
1242
|
+
return pendingNavigation();
|
|
1227
1243
|
},
|
|
1228
1244
|
renderPath,
|
|
1229
1245
|
parsePath,
|
|
@@ -1285,11 +1301,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1285
1301
|
}
|
|
1286
1302
|
if (resolvedTo === undefined) {
|
|
1287
1303
|
throw new Error(`Path '${to}' is not a routable path`);
|
|
1288
|
-
}
|
|
1304
|
+
}
|
|
1305
|
+
const headed = latest(source);
|
|
1306
|
+
const navigationDepth = !isServer && isPending(source) && headed._navigation !== undefined && headed._navigation > 0 ? headed._navigation : 0;
|
|
1307
|
+
if (navigationDepth >= MAX_REDIRECTS) {
|
|
1289
1308
|
throw new Error("Too many redirects");
|
|
1290
1309
|
}
|
|
1291
|
-
|
|
1292
|
-
if (resolvedTo !== current.value || nextState !== current.state) {
|
|
1310
|
+
if (resolvedTo !== headed.value || nextState !== headed.state) {
|
|
1293
1311
|
if (isServer) {
|
|
1294
1312
|
const e = getRequestEvent();
|
|
1295
1313
|
e && (e.response = {
|
|
@@ -1305,38 +1323,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1305
1323
|
state: nextState
|
|
1306
1324
|
});
|
|
1307
1325
|
} else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
1308
|
-
|
|
1309
|
-
value: current.value,
|
|
1310
|
-
replace,
|
|
1311
|
-
scroll,
|
|
1312
|
-
state: current.state
|
|
1313
|
-
});
|
|
1314
|
-
const newTarget = {
|
|
1326
|
+
runWithOwner(null, () => setSource({
|
|
1315
1327
|
value: resolvedTo,
|
|
1316
|
-
state: nextState
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
// isRouting flip (e.g. pending link state) can read it
|
|
1322
|
-
lastTransitionTarget = newTarget;
|
|
1323
|
-
if (firstNavigation) {
|
|
1324
|
-
setIsRouting(true);
|
|
1325
|
-
flush();
|
|
1326
|
-
}
|
|
1327
|
-
if (lastTransitionTarget === newTarget) {
|
|
1328
|
-
setNavigateTarget({
|
|
1329
|
-
...lastTransitionTarget
|
|
1330
|
-
});
|
|
1331
|
-
queueMicrotask(() => {
|
|
1332
|
-
if (lastTransitionTarget !== newTarget) return;
|
|
1333
|
-
intent = undefined;
|
|
1334
|
-
navigateEnd(lastTransitionTarget);
|
|
1335
|
-
setNavigateTarget(undefined);
|
|
1336
|
-
setIsRouting(false);
|
|
1337
|
-
lastTransitionTarget = undefined;
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1328
|
+
state: nextState,
|
|
1329
|
+
replace: navigationDepth ? headed.replace : replace,
|
|
1330
|
+
scroll: navigationDepth ? headed.scroll : scroll,
|
|
1331
|
+
_navigation: navigationDepth + 1
|
|
1332
|
+
}));
|
|
1340
1333
|
}
|
|
1341
1334
|
}
|
|
1342
1335
|
});
|
|
@@ -1346,17 +1339,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1346
1339
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
1347
1340
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1348
1341
|
}
|
|
1349
|
-
function navigateEnd(next) {
|
|
1350
|
-
const first = referrers[0];
|
|
1351
|
-
if (first) {
|
|
1352
|
-
setSource({
|
|
1353
|
-
...next,
|
|
1354
|
-
replace: first.replace,
|
|
1355
|
-
scroll: first.scroll
|
|
1356
|
-
});
|
|
1357
|
-
referrers.length = 0;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
1342
|
function preloadRoute(url, preloadData) {
|
|
1361
1343
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
1362
1344
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
@@ -1370,8 +1352,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1370
1352
|
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1371
1353
|
} catch {}
|
|
1372
1354
|
}
|
|
1373
|
-
const prevIntent =
|
|
1374
|
-
|
|
1355
|
+
const prevIntent = preloadIntent;
|
|
1356
|
+
preloadIntent = "preload";
|
|
1375
1357
|
for (let match in matches) {
|
|
1376
1358
|
const {
|
|
1377
1359
|
route,
|
|
@@ -1396,7 +1378,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1396
1378
|
}));
|
|
1397
1379
|
inPreloadFn = false;
|
|
1398
1380
|
}
|
|
1399
|
-
|
|
1381
|
+
preloadIntent = prevIntent;
|
|
1400
1382
|
}
|
|
1401
1383
|
|
|
1402
1384
|
// Seeds the initial submission from a no-JS form post: the server
|
|
@@ -1437,7 +1419,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
|
|
|
1437
1419
|
const data = preload ? preload({
|
|
1438
1420
|
params,
|
|
1439
1421
|
location,
|
|
1440
|
-
intent: intent || "initial"
|
|
1422
|
+
intent: router.intent?.() || "initial"
|
|
1441
1423
|
}) : undefined;
|
|
1442
1424
|
inPreloadFn = false;
|
|
1443
1425
|
const route = {
|
|
@@ -1985,23 +1967,37 @@ function defineRoute(route) {
|
|
|
1985
1967
|
}
|
|
1986
1968
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
1987
1969
|
function createIntegration(history) {
|
|
1988
|
-
let
|
|
1970
|
+
let committing = false;
|
|
1989
1971
|
const wrap = value => typeof value === "string" ? {
|
|
1990
1972
|
value
|
|
1991
1973
|
} : value;
|
|
1992
1974
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
1993
|
-
equals: (a, b) => a.value === b.value && a.state === b.state,
|
|
1975
|
+
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
1994
1976
|
ownedWrite: true
|
|
1995
1977
|
});
|
|
1996
1978
|
const signal = [read, next => {
|
|
1997
|
-
!ignore && history.set(next);
|
|
1998
1979
|
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
1999
1980
|
write(next);
|
|
1981
|
+
if (next._navigation && next._navigation > 0) {
|
|
1982
|
+
// Register out of band so a destination error boundary replacing the
|
|
1983
|
+
// Router subtree cannot suppress the winning history commit.
|
|
1984
|
+
runWithOwner(null, () => onSettled(() => {
|
|
1985
|
+
if (read() !== next) return;
|
|
1986
|
+
committing = true;
|
|
1987
|
+
try {
|
|
1988
|
+
history.set(next);
|
|
1989
|
+
} finally {
|
|
1990
|
+
committing = false;
|
|
1991
|
+
}
|
|
1992
|
+
}));
|
|
1993
|
+
}
|
|
2000
1994
|
}];
|
|
2001
1995
|
history.init && onCleanup(history.init((value = history.get()) => {
|
|
2002
|
-
|
|
2003
|
-
signal[1](
|
|
2004
|
-
|
|
1996
|
+
if (committing) return;
|
|
1997
|
+
signal[1]({
|
|
1998
|
+
...wrap(value),
|
|
1999
|
+
_navigation: -1
|
|
2000
|
+
});
|
|
2005
2001
|
}));
|
|
2006
2002
|
return {
|
|
2007
2003
|
signal,
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createSignal, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
|
|
2
|
+
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, sharedConfig, untrack } from "solid-js";
|
|
3
3
|
// standalone import: `DEV` is undefined in solid's production build, so app
|
|
4
4
|
// bundlers fold `DEV &&` diagnostics out of shipped bundles
|
|
5
5
|
import { DEV } from "solid-js";
|
|
@@ -27,26 +27,40 @@ export function defineRoute(route) {
|
|
|
27
27
|
}
|
|
28
28
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
29
29
|
function createIntegration(history) {
|
|
30
|
-
let
|
|
30
|
+
let committing = false;
|
|
31
31
|
const wrap = (value) => (typeof value === "string" ? { value } : value);
|
|
32
32
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
33
|
-
equals: (a, b) => a.value === b.value && a.state === b.state,
|
|
33
|
+
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
34
34
|
ownedWrite: true
|
|
35
35
|
});
|
|
36
36
|
const signal = [
|
|
37
37
|
read,
|
|
38
38
|
(next) => {
|
|
39
|
-
!ignore && history.set(next);
|
|
40
39
|
if (sharedConfig.registry && !sharedConfig.done)
|
|
41
40
|
sharedConfig.done = true;
|
|
42
41
|
write(next);
|
|
42
|
+
if (next._navigation && next._navigation > 0) {
|
|
43
|
+
// Register out of band so a destination error boundary replacing the
|
|
44
|
+
// Router subtree cannot suppress the winning history commit.
|
|
45
|
+
runWithOwner(null, () => onSettled(() => {
|
|
46
|
+
if (read() !== next)
|
|
47
|
+
return;
|
|
48
|
+
committing = true;
|
|
49
|
+
try {
|
|
50
|
+
history.set(next);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
committing = false;
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
58
|
];
|
|
45
59
|
history.init &&
|
|
46
60
|
onCleanup(history.init((value = history.get()) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
if (committing)
|
|
62
|
+
return;
|
|
63
|
+
signal[1]({ ...wrap(value), _navigation: -1 });
|
|
50
64
|
}));
|
|
51
65
|
return { signal, utils: history.utils };
|
|
52
66
|
}
|
package/dist/routing.d.ts
CHANGED
|
@@ -192,18 +192,15 @@ export declare function peekLazySubtrees(): number;
|
|
|
192
192
|
* synchronously once available, the in-flight promise otherwise.
|
|
193
193
|
*
|
|
194
194
|
* Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
|
|
195
|
-
* re-fetches a failed dynamic import):
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* the
|
|
203
|
-
*
|
|
204
|
-
* and hold nothing — each request retries. Commit is always async — even for
|
|
205
|
-
* thunks returning arrays — so the version bump never writes a signal from
|
|
206
|
-
* inside a render computation.
|
|
195
|
+
* re-fetches a failed dynamic import): each client router's async boundary
|
|
196
|
+
* reader retains the rejection as reactive error status, so the destination
|
|
197
|
+
* error boundary receives it without refiring the import. The shared record
|
|
198
|
+
* holds that rejection through the current turn for concurrent readers, then
|
|
199
|
+
* clears so a later tracked retry — boundary reset() or a new navigation —
|
|
200
|
+
* starts a fresh import. Server records are shared across requests and hold
|
|
201
|
+
* nothing, so each request retries. Commit is always async — even for thunks
|
|
202
|
+
* returning arrays — so the version bump never writes a signal from inside a
|
|
203
|
+
* render computation.
|
|
207
204
|
*/
|
|
208
205
|
export declare function resolveLazySubtree(record: LazyBoundary): readonly RouteDefinition[] | Promise<readonly RouteDefinition[]>;
|
|
209
206
|
/**
|
package/dist/routing.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runWithOwner } from "solid-js";
|
|
2
2
|
// standalone import: `DEV` is undefined in solid's production build, so app
|
|
3
3
|
// bundlers fold `DEV &&` diagnostics out of shipped bundles
|
|
4
4
|
import { DEV } from "solid-js";
|
|
5
|
-
import { createComponent, createContext, createMemo, createSignal, isPending, NotReadyError, untrack, useContext } from "solid-js";
|
|
5
|
+
import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
|
|
6
6
|
import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
|
|
7
7
|
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
@@ -289,18 +289,15 @@ function getLazyBoundary(thunk) {
|
|
|
289
289
|
* synchronously once available, the in-flight promise otherwise.
|
|
290
290
|
*
|
|
291
291
|
* Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
|
|
292
|
-
* re-fetches a failed dynamic import):
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* the
|
|
300
|
-
*
|
|
301
|
-
* and hold nothing — each request retries. Commit is always async — even for
|
|
302
|
-
* thunks returning arrays — so the version bump never writes a signal from
|
|
303
|
-
* inside a render computation.
|
|
292
|
+
* re-fetches a failed dynamic import): each client router's async boundary
|
|
293
|
+
* reader retains the rejection as reactive error status, so the destination
|
|
294
|
+
* error boundary receives it without refiring the import. The shared record
|
|
295
|
+
* holds that rejection through the current turn for concurrent readers, then
|
|
296
|
+
* clears so a later tracked retry — boundary reset() or a new navigation —
|
|
297
|
+
* starts a fresh import. Server records are shared across requests and hold
|
|
298
|
+
* nothing, so each request retries. Commit is always async — even for thunks
|
|
299
|
+
* returning arrays — so the version bump never writes a signal from inside a
|
|
300
|
+
* render computation.
|
|
304
301
|
*/
|
|
305
302
|
export function resolveLazySubtree(record) {
|
|
306
303
|
if (record.resolved)
|
|
@@ -325,8 +322,15 @@ export function resolveLazySubtree(record) {
|
|
|
325
322
|
return record.resolved;
|
|
326
323
|
}, e => {
|
|
327
324
|
// ?? Error(): a held undefined would read as "no failure" and refire
|
|
328
|
-
if (!isServer)
|
|
325
|
+
if (!isServer) {
|
|
329
326
|
record.error = e ?? new Error();
|
|
327
|
+
record.sweep = true;
|
|
328
|
+
// Existing per-router async readers retain the rejection in their
|
|
329
|
+
// reactive error status. Hold the shared record through this turn so
|
|
330
|
+
// concurrent readers receive the same failure, then permit remounts
|
|
331
|
+
// and boundary resets to create a fresh import attempt.
|
|
332
|
+
queueMicrotask(() => (record.error = record.sweep = undefined));
|
|
333
|
+
}
|
|
330
334
|
record.promise = undefined;
|
|
331
335
|
throw e;
|
|
332
336
|
}));
|
|
@@ -551,9 +555,9 @@ let flashDecoder;
|
|
|
551
555
|
export function provideFlashDecoder(decoder) {
|
|
552
556
|
flashDecoder || (flashDecoder = decoder);
|
|
553
557
|
}
|
|
554
|
-
let
|
|
558
|
+
let preloadIntent;
|
|
555
559
|
export function getIntent() {
|
|
556
|
-
return intent;
|
|
560
|
+
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
557
561
|
}
|
|
558
562
|
let inPreloadFn = false;
|
|
559
563
|
export function getInPreloadFn() {
|
|
@@ -574,20 +578,14 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
574
578
|
throw new Error(`${basePath} is not a valid base path`);
|
|
575
579
|
}
|
|
576
580
|
else if (basePath && !initialSource.value) {
|
|
577
|
-
setSource({
|
|
581
|
+
setSource({
|
|
582
|
+
value: basePath,
|
|
583
|
+
replace: true,
|
|
584
|
+
scroll: false,
|
|
585
|
+
_navigation: isServer ? undefined : 1
|
|
586
|
+
});
|
|
578
587
|
}
|
|
579
|
-
const
|
|
580
|
-
// Navigate override written from event handlers.
|
|
581
|
-
const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
|
|
582
|
-
ownedWrite: true
|
|
583
|
-
});
|
|
584
|
-
// Keep track of last target, so that last call to navigate wins
|
|
585
|
-
let lastTransitionTarget;
|
|
586
|
-
// source() remains canonical for native history changes; navigateTarget()
|
|
587
|
-
// temporarily overrides it for in-flight programmatic navigation.
|
|
588
|
-
const effective = createMemo(() => navigateTarget() ?? source());
|
|
589
|
-
const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
|
|
590
|
-
const referrers = [];
|
|
588
|
+
const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
|
|
591
589
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
592
590
|
// must be appended before streaming flushes the response headers, and an
|
|
593
591
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -608,6 +606,23 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
608
606
|
}
|
|
609
607
|
}
|
|
610
608
|
let submissions;
|
|
609
|
+
// NotReadyError's source must be a reactive async node, not the raw
|
|
610
|
+
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
611
|
+
// is delivered through the graph's error channel and resolution wakes the
|
|
612
|
+
// parked matches computation.
|
|
613
|
+
const routerOwner = getOwner();
|
|
614
|
+
const lazyReaders = new WeakMap();
|
|
615
|
+
const readLazySubtree = (record) => {
|
|
616
|
+
let read = lazyReaders.get(record);
|
|
617
|
+
if (!read) {
|
|
618
|
+
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
619
|
+
const result = resolveLazySubtree(record);
|
|
620
|
+
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
621
|
+
}));
|
|
622
|
+
lazyReaders.set(record, read);
|
|
623
|
+
}
|
|
624
|
+
return read();
|
|
625
|
+
};
|
|
611
626
|
const matches = createMemo(() => {
|
|
612
627
|
const pathname = typeof options.transformUrl === "function"
|
|
613
628
|
? options.transformUrl(location.pathname)
|
|
@@ -622,36 +637,47 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
622
637
|
// inside a boundary just parks the recomputed chain again.
|
|
623
638
|
const pending = unresolvedLazyMatches(m);
|
|
624
639
|
if (pending.length) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
640
|
+
if (isServer) {
|
|
641
|
+
// SSR carries the Promise through NotReadyError so the streaming
|
|
642
|
+
// renderer can resume without attempting to serialize route
|
|
643
|
+
// definitions (which contain component functions).
|
|
644
|
+
const all = Promise.all(pending.map(resolveLazySubtree));
|
|
645
|
+
all.catch(() => { });
|
|
646
|
+
throw new NotReadyError(all);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
// On the client the source must be a reactive async node so transition
|
|
650
|
+
// settlement and rejection delivery remain inside the signals graph.
|
|
651
|
+
for (const boundary of pending)
|
|
652
|
+
readLazySubtree(boundary);
|
|
653
|
+
}
|
|
631
654
|
}
|
|
632
655
|
return m;
|
|
633
656
|
});
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
657
|
+
const routingPending = createMemo(() => isPending(() => {
|
|
658
|
+
try {
|
|
659
|
+
matches();
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
if (e instanceof NotReadyError)
|
|
663
|
+
throw e;
|
|
664
|
+
}
|
|
665
|
+
location.search;
|
|
666
|
+
location.hash;
|
|
667
|
+
}));
|
|
668
|
+
const isRouting = () => routingPending() || isPending(source);
|
|
669
|
+
const transitionIntent = () => {
|
|
670
|
+
if (!isPending(source))
|
|
671
|
+
return;
|
|
672
|
+
const navigation = latest(source)._navigation;
|
|
673
|
+
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
674
|
+
};
|
|
675
|
+
const pendingNavigation = () => {
|
|
676
|
+
if (!isRouting())
|
|
677
|
+
return;
|
|
678
|
+
const target = latest(source);
|
|
679
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
680
|
+
};
|
|
655
681
|
const buildParams = () => mergeParams(matches());
|
|
656
682
|
const wrapParams = utils.paramsWrapper
|
|
657
683
|
? (getParams) => utils.paramsWrapper(getParams, branches)
|
|
@@ -672,8 +698,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
672
698
|
params,
|
|
673
699
|
wrapParams,
|
|
674
700
|
isRouting,
|
|
701
|
+
intent: transitionIntent,
|
|
675
702
|
get pendingTarget() {
|
|
676
|
-
return
|
|
703
|
+
return pendingNavigation();
|
|
677
704
|
},
|
|
678
705
|
renderPath,
|
|
679
706
|
parsePath,
|
|
@@ -739,43 +766,30 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
739
766
|
if (resolvedTo === undefined) {
|
|
740
767
|
throw new Error(`Path '${to}' is not a routable path`);
|
|
741
768
|
}
|
|
742
|
-
|
|
769
|
+
const headed = latest(source);
|
|
770
|
+
const navigationDepth = !isServer &&
|
|
771
|
+
isPending(source) &&
|
|
772
|
+
headed._navigation !== undefined &&
|
|
773
|
+
headed._navigation > 0
|
|
774
|
+
? headed._navigation
|
|
775
|
+
: 0;
|
|
776
|
+
if (navigationDepth >= MAX_REDIRECTS) {
|
|
743
777
|
throw new Error("Too many redirects");
|
|
744
778
|
}
|
|
745
|
-
|
|
746
|
-
if (resolvedTo !== current.value || nextState !== current.state) {
|
|
779
|
+
if (resolvedTo !== headed.value || nextState !== headed.state) {
|
|
747
780
|
if (isServer) {
|
|
748
781
|
const e = getRequestEvent();
|
|
749
782
|
e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
|
|
750
783
|
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
751
784
|
}
|
|
752
785
|
else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
753
|
-
|
|
754
|
-
const newTarget = {
|
|
786
|
+
runWithOwner(null, () => setSource({
|
|
755
787
|
value: resolvedTo,
|
|
756
|
-
state: nextState
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
// isRouting flip (e.g. pending link state) can read it
|
|
762
|
-
lastTransitionTarget = newTarget;
|
|
763
|
-
if (firstNavigation) {
|
|
764
|
-
setIsRouting(true);
|
|
765
|
-
flush();
|
|
766
|
-
}
|
|
767
|
-
if (lastTransitionTarget === newTarget) {
|
|
768
|
-
setNavigateTarget({ ...lastTransitionTarget });
|
|
769
|
-
queueMicrotask(() => {
|
|
770
|
-
if (lastTransitionTarget !== newTarget)
|
|
771
|
-
return;
|
|
772
|
-
intent = undefined;
|
|
773
|
-
navigateEnd(lastTransitionTarget);
|
|
774
|
-
setNavigateTarget(undefined);
|
|
775
|
-
setIsRouting(false);
|
|
776
|
-
lastTransitionTarget = undefined;
|
|
777
|
-
});
|
|
778
|
-
}
|
|
788
|
+
state: nextState,
|
|
789
|
+
replace: navigationDepth ? headed.replace : replace,
|
|
790
|
+
scroll: navigationDepth ? headed.scroll : scroll,
|
|
791
|
+
_navigation: navigationDepth + 1
|
|
792
|
+
}));
|
|
779
793
|
}
|
|
780
794
|
}
|
|
781
795
|
});
|
|
@@ -785,17 +799,6 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
785
799
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
786
800
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
787
801
|
}
|
|
788
|
-
function navigateEnd(next) {
|
|
789
|
-
const first = referrers[0];
|
|
790
|
-
if (first) {
|
|
791
|
-
setSource({
|
|
792
|
-
...next,
|
|
793
|
-
replace: first.replace,
|
|
794
|
-
scroll: first.scroll
|
|
795
|
-
});
|
|
796
|
-
referrers.length = 0;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
802
|
function preloadRoute(url, preloadData) {
|
|
800
803
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
801
804
|
// An unresolved lazy subtree in the chain: the placeholder's
|
|
@@ -810,8 +813,8 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
810
813
|
}
|
|
811
814
|
catch { }
|
|
812
815
|
}
|
|
813
|
-
const prevIntent =
|
|
814
|
-
|
|
816
|
+
const prevIntent = preloadIntent;
|
|
817
|
+
preloadIntent = "preload";
|
|
815
818
|
for (let match in matches) {
|
|
816
819
|
const { route, params } = matches[match];
|
|
817
820
|
route.component &&
|
|
@@ -835,7 +838,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
835
838
|
}));
|
|
836
839
|
inPreloadFn = false;
|
|
837
840
|
}
|
|
838
|
-
|
|
841
|
+
preloadIntent = prevIntent;
|
|
839
842
|
}
|
|
840
843
|
// Seeds the initial submission from a no-JS form post: the server
|
|
841
844
|
// function runtime redirected back with the outcome in a one-shot flash
|
|
@@ -872,7 +875,9 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
|
|
|
872
875
|
component.preload &&
|
|
873
876
|
component.preload();
|
|
874
877
|
inPreloadFn = true;
|
|
875
|
-
const data = preload
|
|
878
|
+
const data = preload
|
|
879
|
+
? preload({ params, location, intent: router.intent?.() || "initial" })
|
|
880
|
+
: undefined;
|
|
876
881
|
inPreloadFn = false;
|
|
877
882
|
const route = {
|
|
878
883
|
parent,
|
package/dist/types.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export interface LocationChange<S = unknown> {
|
|
|
97
97
|
scroll?: boolean;
|
|
98
98
|
state?: S;
|
|
99
99
|
rawPath?: string;
|
|
100
|
+
/** @internal Positive for programmatic navigation depth; `-1` for native history. */
|
|
101
|
+
_navigation?: number;
|
|
100
102
|
}
|
|
101
103
|
export interface RouterIntegration {
|
|
102
104
|
signal: [get: () => LocationChange, set: (next: LocationChange) => void];
|
|
@@ -231,9 +233,9 @@ export interface LazyBoundary {
|
|
|
231
233
|
thunk: LazyRouteChildren;
|
|
232
234
|
promise?: Promise<readonly RouteDefinition[]>;
|
|
233
235
|
resolved?: readonly RouteDefinition[];
|
|
234
|
-
/** A client load failure,
|
|
235
|
-
*
|
|
236
|
-
*
|
|
236
|
+
/** A client load failure, retained by each router's async reader and held
|
|
237
|
+
* here through the rejection turn for concurrent readers; then cleared so
|
|
238
|
+
* a later boundary reset or navigation can retry the import. */
|
|
237
239
|
error?: unknown;
|
|
238
240
|
/** The pending clear of `error` has been scheduled. */
|
|
239
241
|
sweep?: boolean;
|
|
@@ -267,6 +269,8 @@ export interface RouterContext {
|
|
|
267
269
|
wrapParams: (getParams: () => Params) => Params;
|
|
268
270
|
navigatorFactory: NavigatorFactory;
|
|
269
271
|
isRouting: () => boolean;
|
|
272
|
+
/** @internal Intent of the in-flight canonical location transition. */
|
|
273
|
+
intent?: () => Intent | undefined;
|
|
270
274
|
/** The target of the in-flight navigation transition, if any. Not reactive. */
|
|
271
275
|
readonly pendingTarget?: LocationChange;
|
|
272
276
|
matches: () => RouteMatch[];
|