@solidjs/router 2.0.0-next.19 → 2.0.0-next.21
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 +41 -15
- package/dist/index.js +143 -122
- 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 +5 -5
package/dist/data/action.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
2
|
import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
|
|
3
|
-
import { createServerReference, decodeResponsePayload, parseServerFunctionUrl, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
3
|
+
import { createServerReference, decodeRedirectHeaderValue, decodeResponsePayload, parseServerFunctionUrl, REDIRECT_HEADER, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
4
|
// The explicit /server specifier is safe here: the only call site is
|
|
5
5
|
// server-guarded, so client builds tree-shake the codec away.
|
|
6
6
|
import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
|
|
@@ -68,11 +68,13 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
68
68
|
* carries everything an invocation needs — the function id in the path
|
|
69
69
|
* (`<endpoint>/<id>`) and any bound `.with()` arguments (plain JSON in
|
|
70
70
|
* `?args`, which the server prepends for natural-encoding bodies exactly as
|
|
71
|
-
* it does for no-JS posts) — so the FormData is posted
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
71
|
+
* it does for no-JS posts) — so the FormData is posted through the
|
|
72
|
+
* server-function transport, which addresses its own scripted calls at the
|
|
73
|
+
* url's data sibling (`<endpoint>/data/<id>`, solidjs/solid#3094) with the
|
|
74
|
+
* bound query intact: submissions, `aria-busy`, redirects, revalidation,
|
|
75
|
+
* and single-flight all flow through the normal action machinery.
|
|
76
|
+
* Registered under the rendered url, so repeat submits reuse it (and a
|
|
77
|
+
* later real registration overrides it).
|
|
76
78
|
*/
|
|
77
79
|
function createServerFormAction(url) {
|
|
78
80
|
const id = parseServerFunctionUrl(url);
|
|
@@ -290,6 +292,10 @@ async function settleActionResult(result) {
|
|
|
290
292
|
// response's metadata (and so the default revalidation pass must not run
|
|
291
293
|
// again and wipe the freshly seeded cache).
|
|
292
294
|
let flightApplications = 0;
|
|
295
|
+
// The statuses fetch follows (Fetch §2.2.3) — the set the server masks into
|
|
296
|
+
// the redirect carrier for scripted calls, and the set a locally-produced
|
|
297
|
+
// redirect() envelope wears for real.
|
|
298
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
293
299
|
/**
|
|
294
300
|
* Registers the router as the single-flight consumer of the server function
|
|
295
301
|
* transport. Subscribing is the opt-in: while registered, the transport
|
|
@@ -309,23 +315,43 @@ export function setupFlightDataConsumer(router) {
|
|
|
309
315
|
}
|
|
310
316
|
/**
|
|
311
317
|
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
312
|
-
* keys invalidate,
|
|
313
|
-
* data seeds the query cache, and matching entries
|
|
314
|
-
* the flight-data consumer and the action response
|
|
315
|
-
* metadata-bearing responses when no flight data was
|
|
318
|
+
* keys invalidate, the redirect carrier navigates (soft when same-origin,
|
|
319
|
+
* hard otherwise), flight data seeds the query cache, and matching entries
|
|
320
|
+
* revalidate. Shared by the flight-data consumer and the action response
|
|
321
|
+
* path (which still sees metadata-bearing responses when no flight data was
|
|
322
|
+
* collected).
|
|
316
323
|
*/
|
|
317
324
|
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
318
325
|
let keys;
|
|
319
326
|
if (metadata) {
|
|
320
327
|
if (metadata.headers.has(REVALIDATE_HEADER))
|
|
321
328
|
keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
// The carrier delivers the target RESOLVED to an absolute url
|
|
330
|
+
// (solidjs/solid#3102), so the soft/hard split is a real origin
|
|
331
|
+
// comparison — never a guess from how the author spelled the target,
|
|
332
|
+
// which sent `redirect("/")` and `redirect(new URL("/", url).href)`
|
|
333
|
+
// down different navigation paths (solidjs/solid#3107). A redirect
|
|
334
|
+
// produced locally (a client-side action's `redirect()`) never crossed
|
|
335
|
+
// the wire, so no carrier was attached: it is the real 3xx with its
|
|
336
|
+
// Location, resolved against the page it runs in. A `Location` on any
|
|
337
|
+
// other status is the author's data (a 201's created-at) and never
|
|
338
|
+
// navigates. Same-origin targets navigate softly under the router;
|
|
339
|
+
// anything else leaves the app, so the document goes with it.
|
|
340
|
+
// `replace` matches what HTTP gives a form post: the target takes the
|
|
341
|
+
// submission's place in history rather than stacking on it.
|
|
342
|
+
const carried = decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER));
|
|
343
|
+
const local = !carried && REDIRECT_STATUSES.has(metadata.status) && metadata.headers.get("Location");
|
|
344
|
+
const target = carried
|
|
345
|
+
? new URL(carried.url)
|
|
346
|
+
: local
|
|
347
|
+
? new URL(local, window.location.href)
|
|
348
|
+
: undefined;
|
|
349
|
+
if (target) {
|
|
350
|
+
if (target.origin === window.location.origin) {
|
|
351
|
+
navigate(target.pathname + target.search + target.hash, { replace: true });
|
|
326
352
|
}
|
|
327
353
|
else {
|
|
328
|
-
|
|
354
|
+
window.location.href = target.href;
|
|
329
355
|
}
|
|
330
356
|
}
|
|
331
357
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
import { subscribeFlightData, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
3
|
+
import { subscribeFlightData, decodeRedirectHeaderValue, REDIRECT_HEADER, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
|
|
5
5
|
|
|
6
6
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
@@ -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,
|
|
@@ -2566,11 +2562,13 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
2566
2562
|
* carries everything an invocation needs — the function id in the path
|
|
2567
2563
|
* (`<endpoint>/<id>`) and any bound `.with()` arguments (plain JSON in
|
|
2568
2564
|
* `?args`, which the server prepends for natural-encoding bodies exactly as
|
|
2569
|
-
* it does for no-JS posts) — so the FormData is posted
|
|
2570
|
-
*
|
|
2571
|
-
*
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2565
|
+
* it does for no-JS posts) — so the FormData is posted through the
|
|
2566
|
+
* server-function transport, which addresses its own scripted calls at the
|
|
2567
|
+
* url's data sibling (`<endpoint>/data/<id>`, solidjs/solid#3094) with the
|
|
2568
|
+
* bound query intact: submissions, `aria-busy`, redirects, revalidation,
|
|
2569
|
+
* and single-flight all flow through the normal action machinery.
|
|
2570
|
+
* Registered under the rendered url, so repeat submits reuse it (and a
|
|
2571
|
+
* later real registration overrides it).
|
|
2574
2572
|
*/
|
|
2575
2573
|
function createServerFormAction(url) {
|
|
2576
2574
|
const id = parseServerFunctionUrl(url);
|
|
@@ -2795,6 +2793,11 @@ async function settleActionResult(result) {
|
|
|
2795
2793
|
// again and wipe the freshly seeded cache).
|
|
2796
2794
|
let flightApplications = 0;
|
|
2797
2795
|
|
|
2796
|
+
// The statuses fetch follows (Fetch §2.2.3) — the set the server masks into
|
|
2797
|
+
// the redirect carrier for scripted calls, and the set a locally-produced
|
|
2798
|
+
// redirect() envelope wears for real.
|
|
2799
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
2800
|
+
|
|
2798
2801
|
/**
|
|
2799
2802
|
* Registers the router as the single-flight consumer of the server function
|
|
2800
2803
|
* transport. Subscribing is the opt-in: while registered, the transport
|
|
@@ -2817,21 +2820,39 @@ function setupFlightDataConsumer(router) {
|
|
|
2817
2820
|
|
|
2818
2821
|
/**
|
|
2819
2822
|
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
2820
|
-
* keys invalidate,
|
|
2821
|
-
* data seeds the query cache, and matching entries
|
|
2822
|
-
* the flight-data consumer and the action response
|
|
2823
|
-
* metadata-bearing responses when no flight data was
|
|
2823
|
+
* keys invalidate, the redirect carrier navigates (soft when same-origin,
|
|
2824
|
+
* hard otherwise), flight data seeds the query cache, and matching entries
|
|
2825
|
+
* revalidate. Shared by the flight-data consumer and the action response
|
|
2826
|
+
* path (which still sees metadata-bearing responses when no flight data was
|
|
2827
|
+
* collected).
|
|
2824
2828
|
*/
|
|
2825
2829
|
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
2826
2830
|
let keys;
|
|
2827
2831
|
if (metadata) {
|
|
2828
2832
|
if (metadata.headers.has(REVALIDATE_HEADER)) keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
+
// The carrier delivers the target RESOLVED to an absolute url
|
|
2834
|
+
// (solidjs/solid#3102), so the soft/hard split is a real origin
|
|
2835
|
+
// comparison — never a guess from how the author spelled the target,
|
|
2836
|
+
// which sent `redirect("/")` and `redirect(new URL("/", url).href)`
|
|
2837
|
+
// down different navigation paths (solidjs/solid#3107). A redirect
|
|
2838
|
+
// produced locally (a client-side action's `redirect()`) never crossed
|
|
2839
|
+
// the wire, so no carrier was attached: it is the real 3xx with its
|
|
2840
|
+
// Location, resolved against the page it runs in. A `Location` on any
|
|
2841
|
+
// other status is the author's data (a 201's created-at) and never
|
|
2842
|
+
// navigates. Same-origin targets navigate softly under the router;
|
|
2843
|
+
// anything else leaves the app, so the document goes with it.
|
|
2844
|
+
// `replace` matches what HTTP gives a form post: the target takes the
|
|
2845
|
+
// submission's place in history rather than stacking on it.
|
|
2846
|
+
const carried = decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER));
|
|
2847
|
+
const local = !carried && REDIRECT_STATUSES.has(metadata.status) && metadata.headers.get("Location");
|
|
2848
|
+
const target = carried ? new URL(carried.url) : local ? new URL(local, window.location.href) : undefined;
|
|
2849
|
+
if (target) {
|
|
2850
|
+
if (target.origin === window.location.origin) {
|
|
2851
|
+
navigate(target.pathname + target.search + target.hash, {
|
|
2852
|
+
replace: true
|
|
2853
|
+
});
|
|
2833
2854
|
} else {
|
|
2834
|
-
|
|
2855
|
+
window.location.href = target.href;
|
|
2835
2856
|
}
|
|
2836
2857
|
}
|
|
2837
2858
|
}
|
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[];
|
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.21",
|
|
10
10
|
"homepage": "https://github.com/solidjs/solid-router#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -38,21 +38,21 @@
|
|
|
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.5",
|
|
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.5",
|
|
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.
|
|
55
|
-
"solid-js": "^2.0.0-rc.
|
|
54
|
+
"@solidjs/web": "^2.0.0-rc.5",
|
|
55
|
+
"solid-js": "^2.0.0-rc.5"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "rm -rf dist && tsc && rollup -c",
|