@solidjs/router 2.0.0-next.23 → 2.0.0-next.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/data/action.js +5 -6
- package/dist/data/query.d.ts +9 -0
- package/dist/data/query.js +51 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +263 -90
- package/dist/routers/factory.jsx +112 -28
- package/dist/routing.d.ts +10 -1
- package/dist/routing.js +112 -61
- package/dist/types.d.ts +24 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -750,6 +750,8 @@ const isRouting = useIsRouting();
|
|
|
750
750
|
return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
751
751
|
```
|
|
752
752
|
|
|
753
|
+
In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. `attribution.navigations()` and `feedback().navigations` list them; nothing of this exists in production builds.
|
|
754
|
+
|
|
753
755
|
### useMatch
|
|
754
756
|
|
|
755
757
|
Tests a path *pattern you supply* against the current location; returns a memo of match information or `undefined`. It never consults the route tree — the pattern doesn't have to correspond to a defined route. The match's `params` are typed from the pattern, and a typed path node works too (a concrete URL — useful for "am I here" checks):
|
package/dist/data/action.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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, decodeRedirectHeaderValue, decodeResponsePayload,
|
|
3
|
+
import { createServerReference, decodeRedirectHeaderValue, decodeResponsePayload, parseServerFunctionActionUrl, 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";
|
|
7
7
|
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
8
8
|
import { setRouterFormHandler } from "./events.js";
|
|
9
9
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
10
|
-
import { cacheKeyOp, deliverFlightData, hashKey, revalidate, query } from "./query.js";
|
|
10
|
+
import { cacheKeyOp, deliverFlightData, hashKey, readRevalidateKeys, revalidate, query } from "./query.js";
|
|
11
11
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
12
12
|
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
13
13
|
const invokeSymbol = Symbol("routerActionInvoke");
|
|
@@ -77,7 +77,7 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
77
77
|
* later real registration overrides it).
|
|
78
78
|
*/
|
|
79
79
|
function createServerFormAction(url) {
|
|
80
|
-
const id =
|
|
80
|
+
const id = parseServerFunctionActionUrl(url);
|
|
81
81
|
if (!id)
|
|
82
82
|
return undefined;
|
|
83
83
|
// typecheck resolves the server half of the dual module; this path only
|
|
@@ -267,8 +267,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
267
267
|
// Only remove the registration if it still belongs to this instance —
|
|
268
268
|
// a re-created action (e.g. a new `.with()` binding after revalidation)
|
|
269
269
|
// may have registered itself under the same URL since.
|
|
270
|
-
getOwner() &&
|
|
271
|
-
onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
270
|
+
getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
272
271
|
}
|
|
273
272
|
return fn;
|
|
274
273
|
}
|
|
@@ -326,7 +325,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
326
325
|
let keys;
|
|
327
326
|
if (metadata) {
|
|
328
327
|
if (metadata.headers.has(REVALIDATE_HEADER))
|
|
329
|
-
keys = metadata.headers.get(REVALIDATE_HEADER)
|
|
328
|
+
keys = readRevalidateKeys(metadata.headers.get(REVALIDATE_HEADER));
|
|
330
329
|
// The carrier delivers the target RESOLVED to an absolute url
|
|
331
330
|
// (solidjs/solid#3102), so the soft/hard split is a real origin
|
|
332
331
|
// comparison — never a guess from how the author spelled the target,
|
package/dist/data/query.d.ts
CHANGED
|
@@ -23,5 +23,14 @@ export declare namespace query {
|
|
|
23
23
|
export var clear: () => void;
|
|
24
24
|
export { _a as delete };
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Reads an `X-Revalidate` header value into the keys the cache ops take.
|
|
28
|
+
* Named keys are prefix-matched; an empty declaration (`revalidate: []`)
|
|
29
|
+
* yields `[""]`, which `matchKey` matches to nothing; the reserved `*`
|
|
30
|
+
* yields `undefined` — this router's own spelling of everything, the same
|
|
31
|
+
* scope an action with no declaration gets, so an author who wrote "all"
|
|
32
|
+
* explicitly lands on the same path as the default.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readRevalidateKeys(value: string): string[] | undefined;
|
|
26
35
|
export declare function matchKey(key: string, keys: string[]): boolean;
|
|
27
36
|
export declare function hashKey<T extends Array<any>>(args: T): string;
|
package/dist/data/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
|
|
1
|
+
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
|
|
2
2
|
// Everything server-function-shaped comes off the CORE entry: detection
|
|
3
3
|
// (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
|
|
4
4
|
// the late-bound RPC seam (getServerFunctionRPC). The server-functions
|
|
@@ -95,7 +95,15 @@ export function query(fn, name) {
|
|
|
95
95
|
}
|
|
96
96
|
const cachedFn = ((...args) => {
|
|
97
97
|
const cache = getCache();
|
|
98
|
-
|
|
98
|
+
// The intent is a fact about the moment of this read — which navigation
|
|
99
|
+
// (if any) is asking — not a dependency of the value: `intent()` probes
|
|
100
|
+
// `isPending`/`latest` of the router's location, and tracked here every
|
|
101
|
+
// consumer memo would re-run as each navigation starts and lands. Under a
|
|
102
|
+
// redirect that leaves a route section, the leaving section's memo is a
|
|
103
|
+
// zombie the transaction is removing; dirtied through the probe's lane
|
|
104
|
+
// channel it runs anyway (solid #3444), sees the entry the redirect just
|
|
105
|
+
// invalidated, and fires the phantom refetch the sweep exists to avoid.
|
|
106
|
+
const intent = untrack(getIntent);
|
|
99
107
|
const inPreloadFn = getInPreloadFn();
|
|
100
108
|
const owner = getOwner();
|
|
101
109
|
const router = owner ? useRouter() : undefined;
|
|
@@ -189,7 +197,13 @@ export function query(fn, name) {
|
|
|
189
197
|
!isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
|
|
190
198
|
}
|
|
191
199
|
else {
|
|
192
|
-
cache.set(key, (cached = [
|
|
200
|
+
cache.set(key, (cached = [
|
|
201
|
+
stamp,
|
|
202
|
+
res,
|
|
203
|
+
,
|
|
204
|
+
intent,
|
|
205
|
+
createSignal(stamp, { ownedWrite: true })
|
|
206
|
+
]));
|
|
193
207
|
cached[4].count = 0;
|
|
194
208
|
}
|
|
195
209
|
if (tracking) {
|
|
@@ -240,9 +254,13 @@ export function query(fn, name) {
|
|
|
240
254
|
const url = v.headers.get(LocationHeader);
|
|
241
255
|
if (url !== null) {
|
|
242
256
|
// invalidate the redirect's revalidation keys before navigating so
|
|
243
|
-
// the destination's preloads see the miss and fetch fresh (#580 thread)
|
|
244
|
-
|
|
245
|
-
|
|
257
|
+
// the destination's preloads see the miss and fetch fresh (#580 thread).
|
|
258
|
+
// A read that redirects declares nothing by default (unlike an
|
|
259
|
+
// action), so only a header present here sweeps: named keys their
|
|
260
|
+
// matches, `*` everything.
|
|
261
|
+
const declared = !isServer ? v.headers.get(REVALIDATE_HEADER) : null;
|
|
262
|
+
const keys = declared !== null ? readRevalidateKeys(declared) : undefined;
|
|
263
|
+
declared !== null && cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
246
264
|
// client + server relative redirect
|
|
247
265
|
const soft = navigate && url.startsWith("/");
|
|
248
266
|
if (soft)
|
|
@@ -254,7 +272,7 @@ export function query(fn, name) {
|
|
|
254
272
|
// sweep the live signals inside the same transition as the
|
|
255
273
|
// navigation: surviving consumers (shared layouts) refetch and
|
|
256
274
|
// hold the commit, so the destination never paints stale data
|
|
257
|
-
|
|
275
|
+
declared !== null && revalidate(keys, false);
|
|
258
276
|
// Hold the read pending on the client: the navigation unmounts this
|
|
259
277
|
// consumer, and resolving `undefined` instead hands a missing value
|
|
260
278
|
// to whatever renders before the transition commits (a downstream
|
|
@@ -305,12 +323,37 @@ query.set = (key, value) => {
|
|
|
305
323
|
cached[3] = "preload";
|
|
306
324
|
}
|
|
307
325
|
else {
|
|
308
|
-
cache.set(key, (cached = [
|
|
326
|
+
cache.set(key, (cached = [
|
|
327
|
+
now,
|
|
328
|
+
Promise.resolve(value),
|
|
329
|
+
value,
|
|
330
|
+
"preload",
|
|
331
|
+
createSignal(now, { ownedWrite: true })
|
|
332
|
+
]));
|
|
309
333
|
cached[4].count = 0;
|
|
310
334
|
}
|
|
311
335
|
};
|
|
312
336
|
query.delete = (key) => getCache().delete(key);
|
|
313
337
|
query.clear = () => getCache().clear();
|
|
338
|
+
/**
|
|
339
|
+
* The reserved `X-Revalidate` key meaning every entry — `revalidate: "*"`
|
|
340
|
+
* on `@solidjs/web`'s response helpers (`REVALIDATE_ALL` there). The
|
|
341
|
+
* host-independent spelling of "all", for hosts whose default is narrower
|
|
342
|
+
* than this router's.
|
|
343
|
+
*/
|
|
344
|
+
const REVALIDATE_ALL = "*";
|
|
345
|
+
/**
|
|
346
|
+
* Reads an `X-Revalidate` header value into the keys the cache ops take.
|
|
347
|
+
* Named keys are prefix-matched; an empty declaration (`revalidate: []`)
|
|
348
|
+
* yields `[""]`, which `matchKey` matches to nothing; the reserved `*`
|
|
349
|
+
* yields `undefined` — this router's own spelling of everything, the same
|
|
350
|
+
* scope an action with no declaration gets, so an author who wrote "all"
|
|
351
|
+
* explicitly lands on the same path as the default.
|
|
352
|
+
*/
|
|
353
|
+
export function readRevalidateKeys(value) {
|
|
354
|
+
const keys = value.split(",");
|
|
355
|
+
return keys.includes(REVALIDATE_ALL) ? undefined : keys;
|
|
356
|
+
}
|
|
314
357
|
export function matchKey(key, keys) {
|
|
315
358
|
for (let k of keys) {
|
|
316
359
|
if (k && key.startsWith(k))
|
package/dist/index.d.ts
CHANGED
|
@@ -25,4 +25,4 @@ export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
|
25
25
|
export { int } from "./paths.js";
|
|
26
26
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
27
27
|
export * from "./data/index.js";
|
|
28
|
-
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
|
28
|
+
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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';
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, 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, decodeRedirectHeaderValue, REDIRECT_HEADER, decodeResponsePayload,
|
|
3
|
+
import { subscribeFlightData, decodeRedirectHeaderValue, REDIRECT_HEADER, decodeResponsePayload, parseServerFunctionActionUrl, 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;
|
|
@@ -519,6 +519,29 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
519
519
|
|
|
520
520
|
const MAX_REDIRECTS = 100;
|
|
521
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Resolves a write against `headed`, the location the router is heading to
|
|
524
|
+
* — the integration's current value as its functional updater serves it, an
|
|
525
|
+
* unflushed write of this same tick included. `undefined` means nothing to
|
|
526
|
+
* write: the composer declined, or the router is already heading there (same
|
|
527
|
+
* `value` and `state`). The one place the no-op rule lives, for every
|
|
528
|
+
* integration (see `LocationWrite`).
|
|
529
|
+
*/
|
|
530
|
+
function resolveLocationWrite(headed, next) {
|
|
531
|
+
const value = typeof next.value === "function" ? next.value(headed) : next.value;
|
|
532
|
+
if (value === undefined || value === headed.value && next.state === headed.state) return;
|
|
533
|
+
return {
|
|
534
|
+
...next,
|
|
535
|
+
value
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* A destination composed on the location the router is heading to, resolved
|
|
541
|
+
* inside the integration's write (see `LocationWrite`). Internal: the
|
|
542
|
+
* navigator `useSearchParams` builds on accepts it; `Navigator` does not.
|
|
543
|
+
*/
|
|
544
|
+
|
|
522
545
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
523
546
|
const RouterContextObj = createContext();
|
|
524
547
|
const RouteContextObj = createContext();
|
|
@@ -711,16 +734,16 @@ function useSearchParams(path) {
|
|
|
711
734
|
const location = router.location;
|
|
712
735
|
const navigate = useNavigate();
|
|
713
736
|
const setSearchParams = (params, options) => {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
737
|
+
// Merged onto the location the router is heading to, inside the write:
|
|
738
|
+
// a write is invisible to every read channel until its flush (A28), so
|
|
739
|
+
// an earlier write of the same tick — a second synchronous call, or a
|
|
740
|
+
// `navigate()` just before — is only seen by composing on the updater's
|
|
741
|
+
// `headed`. Consecutive calls both apply; a call during a pending
|
|
742
|
+
// navigation lands on that navigation's target.
|
|
743
|
+
navigate(headed => {
|
|
744
|
+
const url = new URL(headed.value[0] === "/" ? mockBase + headed.value : headed.value, mockBase);
|
|
745
|
+
return url.pathname + mergeSearchString(url.search, params) + url.hash;
|
|
746
|
+
}, {
|
|
724
747
|
scroll: false,
|
|
725
748
|
resolve: false,
|
|
726
749
|
...options
|
|
@@ -1221,6 +1244,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1221
1244
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1222
1245
|
const result = resolveLazySubtree(record);
|
|
1223
1246
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1247
|
+
}, {
|
|
1248
|
+
name: "lazyRoutes"
|
|
1224
1249
|
}));
|
|
1225
1250
|
lazyReaders.set(record, read);
|
|
1226
1251
|
}
|
|
@@ -1252,6 +1277,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1252
1277
|
}
|
|
1253
1278
|
}
|
|
1254
1279
|
return m;
|
|
1280
|
+
}, {
|
|
1281
|
+
name: "matches"
|
|
1255
1282
|
});
|
|
1256
1283
|
const routingPending = createMemo(() => isPending(() => {
|
|
1257
1284
|
try {
|
|
@@ -1261,7 +1288,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1261
1288
|
}
|
|
1262
1289
|
location.search;
|
|
1263
1290
|
location.hash;
|
|
1264
|
-
})
|
|
1291
|
+
}), {
|
|
1292
|
+
name: "routingPending"
|
|
1293
|
+
});
|
|
1265
1294
|
const isRouting = () => routingPending() || isPending(source);
|
|
1266
1295
|
const transitionIntent = () => {
|
|
1267
1296
|
if (!isPending(source)) return;
|
|
@@ -1319,18 +1348,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1319
1348
|
}
|
|
1320
1349
|
return;
|
|
1321
1350
|
}
|
|
1322
|
-
// A paths node carries its logical path under the Href brand — read
|
|
1323
|
-
// that rather than coercing: toString() renders the *display* href
|
|
1324
|
-
// (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
|
|
1325
|
-
// Foreign Href-branded values without the slot still coerce.
|
|
1326
|
-
if (typeof to !== "string") to = to[HREF] || to.toString();
|
|
1327
|
-
// Display hrefs can still arrive as plain strings: terminating paths
|
|
1328
|
-
// calls type as `string`, and redirect Location headers round-trip
|
|
1329
|
-
// through here. Under hash mode those start with `#` — a spelling no
|
|
1330
|
-
// logical path uses — so map them back through the integration's
|
|
1331
|
-
// parser, exactly like the anchor click handler does. Elsewhere
|
|
1332
|
-
// parsePath is identity and `#...` keeps its URL meaning below.
|
|
1333
|
-
if (to[0] === "#") to = parsePath(to);
|
|
1334
1351
|
const {
|
|
1335
1352
|
replace,
|
|
1336
1353
|
resolve,
|
|
@@ -1342,49 +1359,98 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1342
1359
|
scroll: true,
|
|
1343
1360
|
...options
|
|
1344
1361
|
};
|
|
1362
|
+
const resolveTarget = to => {
|
|
1363
|
+
// Display hrefs can still arrive as plain strings: terminating paths
|
|
1364
|
+
// calls type as `string`, and redirect Location headers round-trip
|
|
1365
|
+
// through here. Under hash mode those start with `#` — a spelling no
|
|
1366
|
+
// logical path uses — so map them back through the integration's
|
|
1367
|
+
// parser, exactly like the anchor click handler does. Elsewhere
|
|
1368
|
+
// parsePath is identity and `#...` keeps its URL meaning below.
|
|
1369
|
+
if (to[0] === "#") to = parsePath(to);
|
|
1370
|
+
// A string means what the same string means as an href. Leading "/"
|
|
1371
|
+
// stays base-prefixed; anything else resolves URL-style against the
|
|
1372
|
+
// current location — `new URL` collapses `..` and handles `?`/`#`-only
|
|
1373
|
+
// references natively (#502). A cross-origin result (scheme or
|
|
1374
|
+
// protocol-relative input) falls through as unroutable.
|
|
1375
|
+
if (!resolve) return resolvePath((!to || to[0] === "?") && location.pathname || "", to);
|
|
1376
|
+
if (to[0] === "/") return route.resolvePath(to);
|
|
1377
|
+
const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
|
|
1378
|
+
return url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
|
|
1379
|
+
};
|
|
1380
|
+
const unroutable = to => new Error(`Path '${to}' is not a routable path`);
|
|
1381
|
+
|
|
1382
|
+
// The flushed world: the pending navigation when one is held, else the
|
|
1383
|
+
// committed location. Hop depth, the history policy a hop inherits and
|
|
1384
|
+
// the destination the leave guard is told all read it. A write of this
|
|
1385
|
+
// same tick is not in it (A28) — see `compose` below.
|
|
1386
|
+
//
|
|
1387
|
+
// A redirect hop: the previous navigation is still pending, or has landed
|
|
1388
|
+
// but not yet reached history (a guard redirecting in the landing flush
|
|
1389
|
+
// — its destination was never shown either way).
|
|
1390
|
+
const headed = latest(source);
|
|
1345
1391
|
|
|
1346
|
-
// A
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1392
|
+
// A composed target (`setSearchParams`) is a function of where the
|
|
1393
|
+
// router is heading, an unflushed write of this tick included. Only
|
|
1394
|
+
// the integration's functional updater serves that value, so the
|
|
1395
|
+
// destination is resolved inside the write; `resolvedTo` here is the
|
|
1396
|
+
// flushed world's reading of it — what the leave guard is asked about,
|
|
1397
|
+
// and what the server (which never has an unflushed write) redirects to.
|
|
1398
|
+
let compose;
|
|
1351
1399
|
let resolvedTo;
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1400
|
+
// A paths node carries its logical path under the Href brand — read
|
|
1401
|
+
// that rather than coercing: toString() renders the *display* href
|
|
1402
|
+
// (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
|
|
1403
|
+
// Foreign Href-branded values without the slot still coerce. The node
|
|
1404
|
+
// is callable, so the brand is checked ahead of the composer form.
|
|
1405
|
+
const href = typeof to !== "string" ? to[HREF] : undefined;
|
|
1406
|
+
if (href === undefined && typeof to === "function") {
|
|
1407
|
+
const build = to;
|
|
1408
|
+
compose = headed => {
|
|
1409
|
+
const raw = build(headed);
|
|
1410
|
+
const resolved = resolveTarget(raw);
|
|
1411
|
+
if (resolved === undefined) throw unroutable(raw);
|
|
1412
|
+
return resolved;
|
|
1413
|
+
};
|
|
1414
|
+
resolvedTo = compose(headed);
|
|
1415
|
+
} else {
|
|
1416
|
+
if (typeof to !== "string") to = href || to.toString();
|
|
1417
|
+
resolvedTo = resolveTarget(to);
|
|
1418
|
+
if (resolvedTo === undefined) throw unroutable(to);
|
|
1358
1419
|
}
|
|
1359
|
-
const
|
|
1360
|
-
const navigationDepth = !isServer && isPending(source) && headed._navigation !== undefined && headed._navigation > 0 ? headed._navigation : 0;
|
|
1420
|
+
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && (isPending(source) || integration.inflight?.() === headed) ? headed._navigation : 0;
|
|
1361
1421
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
1362
1422
|
throw new Error("Too many redirects");
|
|
1363
1423
|
}
|
|
1364
|
-
if (
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1424
|
+
if (isServer) {
|
|
1425
|
+
// A server render has no unflushed write: the static integration's
|
|
1426
|
+
// value is the whole world, so the no-op rule is applied here, before
|
|
1427
|
+
// the redirect is recorded on the response.
|
|
1428
|
+
if (resolvedTo === headed.value && nextState === headed.state) return;
|
|
1429
|
+
const e = getRequestEvent();
|
|
1430
|
+
e && (e.response = {
|
|
1431
|
+
status: 302,
|
|
1432
|
+
headers: new Headers({
|
|
1433
|
+
Location: resolvedTo
|
|
1434
|
+
})
|
|
1435
|
+
});
|
|
1436
|
+
setSource({
|
|
1437
|
+
value: resolvedTo,
|
|
1438
|
+
replace,
|
|
1439
|
+
scroll,
|
|
1440
|
+
state: nextState
|
|
1441
|
+
});
|
|
1442
|
+
} else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
1443
|
+
// Heading there already (same value and state) writes nothing — the
|
|
1444
|
+
// integration decides that against the updater's `headed`, so a
|
|
1445
|
+
// navigation issued behind another write of the same tick compares
|
|
1446
|
+
// with that write, not with the flushed world.
|
|
1447
|
+
runWithOwner(null, () => setSource({
|
|
1448
|
+
value: compose || resolvedTo,
|
|
1449
|
+
state: nextState,
|
|
1450
|
+
replace: navigationDepth ? headed.replace : replace,
|
|
1451
|
+
scroll: navigationDepth ? headed.scroll : scroll,
|
|
1452
|
+
_navigation: navigationDepth + 1
|
|
1453
|
+
}));
|
|
1388
1454
|
}
|
|
1389
1455
|
});
|
|
1390
1456
|
}
|
|
@@ -2023,32 +2089,103 @@ function defineRoutes(routes) {
|
|
|
2023
2089
|
function defineRoute(route) {
|
|
2024
2090
|
return route;
|
|
2025
2091
|
}
|
|
2092
|
+
/**
|
|
2093
|
+
* What one location write is, for solid's observe tier: the parametrized
|
|
2094
|
+
* route it heads to, the params, where it came from, and — when the write is
|
|
2095
|
+
* the router chasing a redirect while the previous target is still pending —
|
|
2096
|
+
* which hop of that navigation it is. The engine times the navigation from
|
|
2097
|
+
* this declaration (or from the user event enclosing it) until its writes
|
|
2098
|
+
* are through, and names holds and re-runs after it.
|
|
2099
|
+
*
|
|
2100
|
+
* `name` and `params` are getters: the engine reads them when the navigation
|
|
2101
|
+
* settles, not when it starts, so a lazy route subtree that loaded during the
|
|
2102
|
+
* hold names the exact route it resolved to rather than its placeholder.
|
|
2103
|
+
* Redirect depth comes from `_navigation` (1 = a navigation, n = its
|
|
2104
|
+
* (n - 1)th redirect hop, -1 = the browser moved: back/forward, hash).
|
|
2105
|
+
*/
|
|
2106
|
+
function describeNavigation(match, next, written, from) {
|
|
2107
|
+
// A composed destination (`LocationWrite.value` a function) is only known
|
|
2108
|
+
// once the write has resolved it; the engine re-reads `to` at settle, as it
|
|
2109
|
+
// does `name` and `params`. A plain one is known up front.
|
|
2110
|
+
const to = () => {
|
|
2111
|
+
const w = written();
|
|
2112
|
+
return w ? w.value : typeof next.value === "string" ? next.value : undefined;
|
|
2113
|
+
};
|
|
2114
|
+
const pathname = () => {
|
|
2115
|
+
const t = to();
|
|
2116
|
+
return t === undefined ? undefined : new URL(t, mockBase).pathname;
|
|
2117
|
+
};
|
|
2118
|
+
const matches = pathname => untrack(() => match(pathname));
|
|
2119
|
+
const ref = {
|
|
2120
|
+
kind: "navigation",
|
|
2121
|
+
get to() {
|
|
2122
|
+
return to();
|
|
2123
|
+
},
|
|
2124
|
+
from,
|
|
2125
|
+
get name() {
|
|
2126
|
+
const p = pathname();
|
|
2127
|
+
if (p === undefined) return undefined;
|
|
2128
|
+
const m = matches(p);
|
|
2129
|
+
return m.length ? m[m.length - 1].route.pattern || "/" : p;
|
|
2130
|
+
},
|
|
2131
|
+
get params() {
|
|
2132
|
+
const p = pathname();
|
|
2133
|
+
const m = p === undefined ? [] : matches(p);
|
|
2134
|
+
return m.length ? mergeParams(m) : undefined;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
|
|
2138
|
+
return ref;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2026
2141
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2027
|
-
function createIntegration(history) {
|
|
2142
|
+
function createIntegration(history, match) {
|
|
2028
2143
|
let committing = false;
|
|
2144
|
+
// Written, not yet in history (see `RouterIntegration.inflight`).
|
|
2145
|
+
let inflight;
|
|
2029
2146
|
const wrap = value => typeof value === "string" ? {
|
|
2030
2147
|
value
|
|
2031
2148
|
} : value;
|
|
2032
2149
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
2033
2150
|
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
2034
|
-
ownedWrite: true
|
|
2151
|
+
ownedWrite: true,
|
|
2152
|
+
name: "location"
|
|
2035
2153
|
});
|
|
2036
2154
|
const signal = [read, next => {
|
|
2037
2155
|
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
|
|
2038
|
-
write
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2156
|
+
// What the write resolved to, or undefined when there was nothing to
|
|
2157
|
+
// write (see `resolveLocationWrite`).
|
|
2158
|
+
let written;
|
|
2159
|
+
const commit = () => {
|
|
2160
|
+
// The functional updater is the one channel that serves an unflushed
|
|
2161
|
+
// write of this same tick (A28): the destination composes on it and
|
|
2162
|
+
// the no-op rule compares against it, so `navigate()` behind another
|
|
2163
|
+
// write in one handler sees that write rather than the flushed world.
|
|
2164
|
+
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
2165
|
+
if (written && written._navigation && written._navigation > 0) {
|
|
2166
|
+
const next = written;
|
|
2167
|
+
inflight = next;
|
|
2168
|
+
// Register out of band so a destination error boundary replacing the
|
|
2169
|
+
// Router subtree cannot suppress the winning history commit.
|
|
2170
|
+
runWithOwner(null, () => onSettled(() => {
|
|
2171
|
+
if (inflight === next) inflight = undefined;
|
|
2172
|
+
if (read() !== next) return;
|
|
2173
|
+
committing = true;
|
|
2174
|
+
try {
|
|
2175
|
+
history.set(next);
|
|
2176
|
+
} finally {
|
|
2177
|
+
committing = false;
|
|
2178
|
+
}
|
|
2179
|
+
}));
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
// Every client location write passes here — navigate(), a redirect hop,
|
|
2183
|
+
// the browser's own back/forward — so this is the one place the
|
|
2184
|
+
// navigation is declared. `read()` still holds the committed location
|
|
2185
|
+
// while a navigation is pending, which is the `from` a hop wants too.
|
|
2186
|
+
// A write the no-op rule drops is still declared: the engine settles a
|
|
2187
|
+
// navigation whose write "did not survive the equality gate" on the spot.
|
|
2188
|
+
OBSERVE ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, untrack(read).value), commit) : commit();
|
|
2052
2189
|
}];
|
|
2053
2190
|
history.init && onCleanup(history.init((value = history.get()) => {
|
|
2054
2191
|
if (committing) return;
|
|
@@ -2059,6 +2196,7 @@ function createIntegration(history) {
|
|
|
2059
2196
|
}));
|
|
2060
2197
|
return {
|
|
2061
2198
|
signal,
|
|
2199
|
+
inflight: () => inflight,
|
|
2062
2200
|
utils: history.utils
|
|
2063
2201
|
};
|
|
2064
2202
|
}
|
|
@@ -2083,7 +2221,10 @@ function staticIntegration(url, utils) {
|
|
|
2083
2221
|
value
|
|
2084
2222
|
};
|
|
2085
2223
|
return {
|
|
2086
|
-
signal: [() => obj, next =>
|
|
2224
|
+
signal: [() => obj, next => {
|
|
2225
|
+
const written = resolveLocationWrite(obj, next);
|
|
2226
|
+
written && Object.assign(obj, written);
|
|
2227
|
+
}],
|
|
2087
2228
|
utils
|
|
2088
2229
|
};
|
|
2089
2230
|
}
|
|
@@ -2104,6 +2245,7 @@ function createRouter(config) {
|
|
|
2104
2245
|
return compiled;
|
|
2105
2246
|
};
|
|
2106
2247
|
const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
|
|
2248
|
+
const matchPath = pathname => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
2107
2249
|
function RouterComponent(props) {
|
|
2108
2250
|
// One router per app: the session (location, history, delegation, link
|
|
2109
2251
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -2120,7 +2262,7 @@ function createRouter(config) {
|
|
|
2120
2262
|
restoration = createScrollRestoration();
|
|
2121
2263
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
2122
2264
|
}
|
|
2123
|
-
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory());
|
|
2265
|
+
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
2124
2266
|
let context;
|
|
2125
2267
|
const routerState = createRouterContext(integration, branches, () => context, {
|
|
2126
2268
|
base: basePath,
|
|
@@ -2161,9 +2303,7 @@ function createRouter(config) {
|
|
|
2161
2303
|
routes: config.routes,
|
|
2162
2304
|
config,
|
|
2163
2305
|
match(url) {
|
|
2164
|
-
|
|
2165
|
-
const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
|
|
2166
|
-
return getRouteMatches(branches(), pathname).map(({
|
|
2306
|
+
return matchPath(new URL(url, mockBase).pathname).map(({
|
|
2167
2307
|
route,
|
|
2168
2308
|
path,
|
|
2169
2309
|
params
|
|
@@ -2347,7 +2487,15 @@ function query(fn, name) {
|
|
|
2347
2487
|
}
|
|
2348
2488
|
const cachedFn = (...args) => {
|
|
2349
2489
|
const cache = getCache();
|
|
2350
|
-
|
|
2490
|
+
// The intent is a fact about the moment of this read — which navigation
|
|
2491
|
+
// (if any) is asking — not a dependency of the value: `intent()` probes
|
|
2492
|
+
// `isPending`/`latest` of the router's location, and tracked here every
|
|
2493
|
+
// consumer memo would re-run as each navigation starts and lands. Under a
|
|
2494
|
+
// redirect that leaves a route section, the leaving section's memo is a
|
|
2495
|
+
// zombie the transaction is removing; dirtied through the probe's lane
|
|
2496
|
+
// channel it runs anyway (solid #3444), sees the entry the redirect just
|
|
2497
|
+
// invalidated, and fires the phantom refetch the sweep exists to avoid.
|
|
2498
|
+
const intent = untrack(getIntent);
|
|
2351
2499
|
const inPreloadFn = getInPreloadFn();
|
|
2352
2500
|
const owner = getOwner();
|
|
2353
2501
|
const router = owner ? useRouter() : undefined;
|
|
@@ -2474,9 +2622,13 @@ function query(fn, name) {
|
|
|
2474
2622
|
const url = v.headers.get(LocationHeader);
|
|
2475
2623
|
if (url !== null) {
|
|
2476
2624
|
// invalidate the redirect's revalidation keys before navigating so
|
|
2477
|
-
// the destination's preloads see the miss and fetch fresh (#580 thread)
|
|
2478
|
-
|
|
2479
|
-
|
|
2625
|
+
// the destination's preloads see the miss and fetch fresh (#580 thread).
|
|
2626
|
+
// A read that redirects declares nothing by default (unlike an
|
|
2627
|
+
// action), so only a header present here sweeps: named keys their
|
|
2628
|
+
// matches, `*` everything.
|
|
2629
|
+
const declared = !isServer ? v.headers.get(REVALIDATE_HEADER) : null;
|
|
2630
|
+
const keys = declared !== null ? readRevalidateKeys(declared) : undefined;
|
|
2631
|
+
declared !== null && cacheKeyOp(keys, entry => entry[0] = 0);
|
|
2480
2632
|
|
|
2481
2633
|
// client + server relative redirect
|
|
2482
2634
|
const soft = navigate && url.startsWith("/");
|
|
@@ -2487,7 +2639,7 @@ function query(fn, name) {
|
|
|
2487
2639
|
// sweep the live signals inside the same transition as the
|
|
2488
2640
|
// navigation: surviving consumers (shared layouts) refetch and
|
|
2489
2641
|
// hold the commit, so the destination never paints stale data
|
|
2490
|
-
|
|
2642
|
+
declared !== null && revalidate(keys, false);
|
|
2491
2643
|
|
|
2492
2644
|
// Hold the read pending on the client: the navigation unmounts this
|
|
2493
2645
|
// consumer, and resolving `undefined` instead hands a missing value
|
|
@@ -2542,6 +2694,27 @@ query.set = (key, value) => {
|
|
|
2542
2694
|
};
|
|
2543
2695
|
query.delete = key => getCache().delete(key);
|
|
2544
2696
|
query.clear = () => getCache().clear();
|
|
2697
|
+
|
|
2698
|
+
/**
|
|
2699
|
+
* The reserved `X-Revalidate` key meaning every entry — `revalidate: "*"`
|
|
2700
|
+
* on `@solidjs/web`'s response helpers (`REVALIDATE_ALL` there). The
|
|
2701
|
+
* host-independent spelling of "all", for hosts whose default is narrower
|
|
2702
|
+
* than this router's.
|
|
2703
|
+
*/
|
|
2704
|
+
const REVALIDATE_ALL = "*";
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* Reads an `X-Revalidate` header value into the keys the cache ops take.
|
|
2708
|
+
* Named keys are prefix-matched; an empty declaration (`revalidate: []`)
|
|
2709
|
+
* yields `[""]`, which `matchKey` matches to nothing; the reserved `*`
|
|
2710
|
+
* yields `undefined` — this router's own spelling of everything, the same
|
|
2711
|
+
* scope an action with no declaration gets, so an author who wrote "all"
|
|
2712
|
+
* explicitly lands on the same path as the default.
|
|
2713
|
+
*/
|
|
2714
|
+
function readRevalidateKeys(value) {
|
|
2715
|
+
const keys = value.split(",");
|
|
2716
|
+
return keys.includes(REVALIDATE_ALL) ? undefined : keys;
|
|
2717
|
+
}
|
|
2545
2718
|
function matchKey(key, keys) {
|
|
2546
2719
|
for (let k of keys) {
|
|
2547
2720
|
if (k && key.startsWith(k)) return true;
|
|
@@ -2629,7 +2802,7 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
2629
2802
|
* later real registration overrides it).
|
|
2630
2803
|
*/
|
|
2631
2804
|
function createServerFormAction(url) {
|
|
2632
|
-
const id =
|
|
2805
|
+
const id = parseServerFunctionActionUrl(url);
|
|
2633
2806
|
if (!id) return undefined;
|
|
2634
2807
|
// typecheck resolves the server half of the dual module; this path only
|
|
2635
2808
|
// runs in the browser, where the client transport's signature applies
|
|
@@ -2888,7 +3061,7 @@ function setupFlightDataConsumer(router) {
|
|
|
2888
3061
|
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
2889
3062
|
let keys;
|
|
2890
3063
|
if (metadata) {
|
|
2891
|
-
if (metadata.headers.has(REVALIDATE_HEADER)) keys = metadata.headers.get(REVALIDATE_HEADER)
|
|
3064
|
+
if (metadata.headers.has(REVALIDATE_HEADER)) keys = readRevalidateKeys(metadata.headers.get(REVALIDATE_HEADER));
|
|
2892
3065
|
// The carrier delivers the target RESOLVED to an absolute url
|
|
2893
3066
|
// (solidjs/solid#3102), so the soft/hard split is a real origin
|
|
2894
3067
|
// comparison — never a guess from how the author spelled the target,
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
2
|
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, sharedConfig, untrack } from "solid-js";
|
|
3
|
-
// standalone
|
|
4
|
-
//
|
|
5
|
-
|
|
3
|
+
// standalone imports: `DEV` is undefined in solid's production build and
|
|
4
|
+
// `OBSERVE` outside its observe/dev builds, so app bundlers fold the
|
|
5
|
+
// `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
|
|
6
|
+
import { DEV, OBSERVE } from "solid-js";
|
|
6
7
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
7
8
|
import { setupLinkClaims } from "../claims.js";
|
|
8
9
|
import { setupNativeEvents } from "../data/events.js";
|
|
9
10
|
import { createPathsProxy } from "../paths.js";
|
|
10
|
-
import { createBranches, createRouterContext, getRouteMatches, registerFlightRouter, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
|
|
11
|
+
import { createBranches, createRouterContext, getRouteMatches, mergeParams, registerFlightRouter, resolveLocationWrite, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
|
|
11
12
|
import { mockBase } from "../utils.js";
|
|
12
13
|
import { Root, Routes } from "./components.jsx";
|
|
13
14
|
import { browserHistory } from "./history.js";
|
|
@@ -25,35 +26,110 @@ export function defineRoutes(routes) {
|
|
|
25
26
|
export function defineRoute(route) {
|
|
26
27
|
return route;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* What one location write is, for solid's observe tier: the parametrized
|
|
31
|
+
* route it heads to, the params, where it came from, and — when the write is
|
|
32
|
+
* the router chasing a redirect while the previous target is still pending —
|
|
33
|
+
* which hop of that navigation it is. The engine times the navigation from
|
|
34
|
+
* this declaration (or from the user event enclosing it) until its writes
|
|
35
|
+
* are through, and names holds and re-runs after it.
|
|
36
|
+
*
|
|
37
|
+
* `name` and `params` are getters: the engine reads them when the navigation
|
|
38
|
+
* settles, not when it starts, so a lazy route subtree that loaded during the
|
|
39
|
+
* hold names the exact route it resolved to rather than its placeholder.
|
|
40
|
+
* Redirect depth comes from `_navigation` (1 = a navigation, n = its
|
|
41
|
+
* (n - 1)th redirect hop, -1 = the browser moved: back/forward, hash).
|
|
42
|
+
*/
|
|
43
|
+
function describeNavigation(match, next, written, from) {
|
|
44
|
+
// A composed destination (`LocationWrite.value` a function) is only known
|
|
45
|
+
// once the write has resolved it; the engine re-reads `to` at settle, as it
|
|
46
|
+
// does `name` and `params`. A plain one is known up front.
|
|
47
|
+
const to = () => {
|
|
48
|
+
const w = written();
|
|
49
|
+
return w ? w.value : typeof next.value === "string" ? next.value : undefined;
|
|
50
|
+
};
|
|
51
|
+
const pathname = () => {
|
|
52
|
+
const t = to();
|
|
53
|
+
return t === undefined ? undefined : new URL(t, mockBase).pathname;
|
|
54
|
+
};
|
|
55
|
+
const matches = (pathname) => untrack(() => match(pathname));
|
|
56
|
+
const ref = {
|
|
57
|
+
kind: "navigation",
|
|
58
|
+
get to() {
|
|
59
|
+
return to();
|
|
60
|
+
},
|
|
61
|
+
from,
|
|
62
|
+
get name() {
|
|
63
|
+
const p = pathname();
|
|
64
|
+
if (p === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const m = matches(p);
|
|
67
|
+
return m.length ? m[m.length - 1].route.pattern || "/" : p;
|
|
68
|
+
},
|
|
69
|
+
get params() {
|
|
70
|
+
const p = pathname();
|
|
71
|
+
const m = p === undefined ? [] : matches(p);
|
|
72
|
+
return m.length ? mergeParams(m) : undefined;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (next._navigation !== undefined && next._navigation > 1)
|
|
76
|
+
ref.redirect = next._navigation - 1;
|
|
77
|
+
return ref;
|
|
78
|
+
}
|
|
28
79
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
29
|
-
function createIntegration(history) {
|
|
80
|
+
function createIntegration(history, match) {
|
|
30
81
|
let committing = false;
|
|
82
|
+
// Written, not yet in history (see `RouterIntegration.inflight`).
|
|
83
|
+
let inflight;
|
|
31
84
|
const wrap = (value) => (typeof value === "string" ? { value } : value);
|
|
32
85
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
33
86
|
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
34
|
-
ownedWrite: true
|
|
87
|
+
ownedWrite: true,
|
|
88
|
+
name: "location"
|
|
35
89
|
});
|
|
36
90
|
const signal = [
|
|
37
91
|
read,
|
|
38
92
|
(next) => {
|
|
39
93
|
if (sharedConfig.registry && !sharedConfig.done)
|
|
40
94
|
sharedConfig.done = true;
|
|
41
|
-
write
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
// What the write resolved to, or undefined when there was nothing to
|
|
96
|
+
// write (see `resolveLocationWrite`).
|
|
97
|
+
let written;
|
|
98
|
+
const commit = () => {
|
|
99
|
+
// The functional updater is the one channel that serves an unflushed
|
|
100
|
+
// write of this same tick (A28): the destination composes on it and
|
|
101
|
+
// the no-op rule compares against it, so `navigate()` behind another
|
|
102
|
+
// write in one handler sees that write rather than the flushed world.
|
|
103
|
+
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
104
|
+
if (written && written._navigation && written._navigation > 0) {
|
|
105
|
+
const next = written;
|
|
106
|
+
inflight = next;
|
|
107
|
+
// Register out of band so a destination error boundary replacing the
|
|
108
|
+
// Router subtree cannot suppress the winning history commit.
|
|
109
|
+
runWithOwner(null, () => onSettled(() => {
|
|
110
|
+
if (inflight === next)
|
|
111
|
+
inflight = undefined;
|
|
112
|
+
if (read() !== next)
|
|
113
|
+
return;
|
|
114
|
+
committing = true;
|
|
115
|
+
try {
|
|
116
|
+
history.set(next);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
committing = false;
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// Every client location write passes here — navigate(), a redirect hop,
|
|
125
|
+
// the browser's own back/forward — so this is the one place the
|
|
126
|
+
// navigation is declared. `read()` still holds the committed location
|
|
127
|
+
// while a navigation is pending, which is the `from` a hop wants too.
|
|
128
|
+
// A write the no-op rule drops is still declared: the engine settles a
|
|
129
|
+
// navigation whose write "did not survive the equality gate" on the spot.
|
|
130
|
+
OBSERVE
|
|
131
|
+
? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, untrack(read).value), commit)
|
|
132
|
+
: commit();
|
|
57
133
|
}
|
|
58
134
|
];
|
|
59
135
|
history.init &&
|
|
@@ -62,7 +138,7 @@ function createIntegration(history) {
|
|
|
62
138
|
return;
|
|
63
139
|
signal[1]({ ...wrap(value), _navigation: -1 });
|
|
64
140
|
}));
|
|
65
|
-
return { signal, utils: history.utils };
|
|
141
|
+
return { signal, inflight: () => inflight, utils: history.utils };
|
|
66
142
|
}
|
|
67
143
|
/**
|
|
68
144
|
* Server default: a static view of the request URL — no signal machinery, a
|
|
@@ -81,7 +157,16 @@ function staticIntegration(url, utils) {
|
|
|
81
157
|
value = u.pathname + u.search;
|
|
82
158
|
}
|
|
83
159
|
const obj = { value };
|
|
84
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
signal: [
|
|
162
|
+
() => obj,
|
|
163
|
+
next => {
|
|
164
|
+
const written = resolveLocationWrite(obj, next);
|
|
165
|
+
written && Object.assign(obj, written);
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
utils
|
|
169
|
+
};
|
|
85
170
|
}
|
|
86
171
|
export function createRouter(config) {
|
|
87
172
|
const basePath = config.base || "";
|
|
@@ -100,6 +185,7 @@ export function createRouter(config) {
|
|
|
100
185
|
return compiled;
|
|
101
186
|
};
|
|
102
187
|
const renderPath = (config.history && config.history.utils && config.history.utils.renderPath) || undefined;
|
|
188
|
+
const matchPath = (pathname) => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
|
|
103
189
|
function RouterComponent(props) {
|
|
104
190
|
// One router per app: the session (location, history, delegation, link
|
|
105
191
|
// claims, preloading) has a single owner, and a second instance would
|
|
@@ -119,7 +205,7 @@ export function createRouter(config) {
|
|
|
119
205
|
}
|
|
120
206
|
const integration = isServer
|
|
121
207
|
? staticIntegration(props.url, config.history && config.history.utils)
|
|
122
|
-
: createIntegration(history || browserHistory());
|
|
208
|
+
: createIntegration(history || browserHistory(), matchPath);
|
|
123
209
|
let context;
|
|
124
210
|
const routerState = createRouterContext(integration, branches, () => context, {
|
|
125
211
|
base: basePath,
|
|
@@ -149,9 +235,7 @@ export function createRouter(config) {
|
|
|
149
235
|
routes: config.routes,
|
|
150
236
|
config,
|
|
151
237
|
match(url) {
|
|
152
|
-
|
|
153
|
-
const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
|
|
154
|
-
return getRouteMatches(branches(), pathname).map(({ route, path, params }) => ({
|
|
238
|
+
return matchPath(new URL(url, mockBase).pathname).map(({ route, path, params }) => ({
|
|
155
239
|
path: route.originalPath,
|
|
156
240
|
pattern: route.pattern,
|
|
157
241
|
match: path,
|
package/dist/routing.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { JSX } from "@solidjs/web";
|
|
2
|
-
import type { Branch, Intent, LazyBoundary, Location, MatchFilters, NavigateOptions, Navigator, Params, PathMatch, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouteParams, RouterContext, RouterIntegration, SearchParams, SetSearchParams, TypedPath, TypedSearchPath } from "./types.js";
|
|
2
|
+
import type { Branch, Intent, LazyBoundary, Location, LocationChange, LocationWrite, MatchFilters, NavigateOptions, Navigator, Params, PathMatch, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouteParams, RouterContext, RouterIntegration, SearchParams, SetSearchParams, TypedPath, TypedSearchPath } from "./types.js";
|
|
3
3
|
import type { FlashSubmission } from "@solidjs/web/server-functions/server";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a write against `headed`, the location the router is heading to
|
|
6
|
+
* — the integration's current value as its functional updater serves it, an
|
|
7
|
+
* unflushed write of this same tick included. `undefined` means nothing to
|
|
8
|
+
* write: the composer declined, or the router is already heading there (same
|
|
9
|
+
* `value` and `state`). The one place the no-op rule lives, for every
|
|
10
|
+
* integration (see `LocationWrite`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveLocationWrite(headed: LocationChange, next: LocationWrite): LocationChange | undefined;
|
|
4
13
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
5
14
|
export declare const RouterContextObj: import("solid-js").Context<RouterContext>;
|
|
6
15
|
export declare const RouteContextObj: import("solid-js").Context<RouteContext>;
|
package/dist/routing.js
CHANGED
|
@@ -7,6 +7,20 @@ import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@so
|
|
|
7
7
|
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
8
8
|
import { HREF } from "./paths.js";
|
|
9
9
|
const MAX_REDIRECTS = 100;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a write against `headed`, the location the router is heading to
|
|
12
|
+
* — the integration's current value as its functional updater serves it, an
|
|
13
|
+
* unflushed write of this same tick included. `undefined` means nothing to
|
|
14
|
+
* write: the composer declined, or the router is already heading there (same
|
|
15
|
+
* `value` and `state`). The one place the no-op rule lives, for every
|
|
16
|
+
* integration (see `LocationWrite`).
|
|
17
|
+
*/
|
|
18
|
+
export function resolveLocationWrite(headed, next) {
|
|
19
|
+
const value = typeof next.value === "function" ? next.value(headed) : next.value;
|
|
20
|
+
if (value === undefined || (value === headed.value && next.state === headed.state))
|
|
21
|
+
return;
|
|
22
|
+
return { ...next, value };
|
|
23
|
+
}
|
|
10
24
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
11
25
|
export const RouterContextObj = createContext();
|
|
12
26
|
export const RouteContextObj = createContext();
|
|
@@ -154,16 +168,16 @@ export function useSearchParams(path) {
|
|
|
154
168
|
const location = router.location;
|
|
155
169
|
const navigate = useNavigate();
|
|
156
170
|
const setSearchParams = (params, options) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
// Merged onto the location the router is heading to, inside the write:
|
|
172
|
+
// a write is invisible to every read channel until its flush (A28), so
|
|
173
|
+
// an earlier write of the same tick — a second synchronous call, or a
|
|
174
|
+
// `navigate()` just before — is only seen by composing on the updater's
|
|
175
|
+
// `headed`. Consecutive calls both apply; a call during a pending
|
|
176
|
+
// navigation lands on that navigation's target.
|
|
177
|
+
navigate(headed => {
|
|
178
|
+
const url = new URL(headed.value[0] === "/" ? mockBase + headed.value : headed.value, mockBase);
|
|
179
|
+
return url.pathname + mergeSearchString(url.search, params) + url.hash;
|
|
180
|
+
}, {
|
|
167
181
|
scroll: false,
|
|
168
182
|
resolve: false,
|
|
169
183
|
...options
|
|
@@ -647,7 +661,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
647
661
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
648
662
|
const result = resolveLazySubtree(record);
|
|
649
663
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
650
|
-
}));
|
|
664
|
+
}, { name: "lazyRoutes" }));
|
|
651
665
|
lazyReaders.set(record, read);
|
|
652
666
|
}
|
|
653
667
|
return read();
|
|
@@ -682,7 +696,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
682
696
|
}
|
|
683
697
|
}
|
|
684
698
|
return m;
|
|
685
|
-
});
|
|
699
|
+
}, { name: "matches" });
|
|
686
700
|
const routingPending = createMemo(() => isPending(() => {
|
|
687
701
|
try {
|
|
688
702
|
matches();
|
|
@@ -693,7 +707,7 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
693
707
|
}
|
|
694
708
|
location.search;
|
|
695
709
|
location.hash;
|
|
696
|
-
}));
|
|
710
|
+
}), { name: "routingPending" });
|
|
697
711
|
const isRouting = () => routingPending() || isPending(source);
|
|
698
712
|
const transitionIntent = () => {
|
|
699
713
|
if (!isPending(source))
|
|
@@ -757,76 +771,113 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
757
771
|
}
|
|
758
772
|
return;
|
|
759
773
|
}
|
|
760
|
-
// A paths node carries its logical path under the Href brand — read
|
|
761
|
-
// that rather than coercing: toString() renders the *display* href
|
|
762
|
-
// (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
|
|
763
|
-
// Foreign Href-branded values without the slot still coerce.
|
|
764
|
-
if (typeof to !== "string")
|
|
765
|
-
to = to[HREF] || to.toString();
|
|
766
|
-
// Display hrefs can still arrive as plain strings: terminating paths
|
|
767
|
-
// calls type as `string`, and redirect Location headers round-trip
|
|
768
|
-
// through here. Under hash mode those start with `#` — a spelling no
|
|
769
|
-
// logical path uses — so map them back through the integration's
|
|
770
|
-
// parser, exactly like the anchor click handler does. Elsewhere
|
|
771
|
-
// parsePath is identity and `#...` keeps its URL meaning below.
|
|
772
|
-
if (to[0] === "#")
|
|
773
|
-
to = parsePath(to);
|
|
774
774
|
const { replace, resolve, scroll, state: nextState } = {
|
|
775
775
|
replace: false,
|
|
776
776
|
resolve: true,
|
|
777
777
|
scroll: true,
|
|
778
778
|
...options
|
|
779
779
|
};
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
780
|
+
const resolveTarget = (to) => {
|
|
781
|
+
// Display hrefs can still arrive as plain strings: terminating paths
|
|
782
|
+
// calls type as `string`, and redirect Location headers round-trip
|
|
783
|
+
// through here. Under hash mode those start with `#` — a spelling no
|
|
784
|
+
// logical path uses — so map them back through the integration's
|
|
785
|
+
// parser, exactly like the anchor click handler does. Elsewhere
|
|
786
|
+
// parsePath is identity and `#...` keeps its URL meaning below.
|
|
787
|
+
if (to[0] === "#")
|
|
788
|
+
to = parsePath(to);
|
|
789
|
+
// A string means what the same string means as an href. Leading "/"
|
|
790
|
+
// stays base-prefixed; anything else resolves URL-style against the
|
|
791
|
+
// current location — `new URL` collapses `..` and handles `?`/`#`-only
|
|
792
|
+
// references natively (#502). A cross-origin result (scheme or
|
|
793
|
+
// protocol-relative input) falls through as unroutable.
|
|
794
|
+
if (!resolve)
|
|
795
|
+
return resolvePath(((!to || to[0] === "?") && location.pathname) || "", to);
|
|
796
|
+
if (to[0] === "/")
|
|
797
|
+
return route.resolvePath(to);
|
|
791
798
|
const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
|
|
792
|
-
|
|
793
|
-
|
|
799
|
+
return url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
|
|
800
|
+
};
|
|
801
|
+
const unroutable = (to) => new Error(`Path '${to}' is not a routable path`);
|
|
802
|
+
// The flushed world: the pending navigation when one is held, else the
|
|
803
|
+
// committed location. Hop depth, the history policy a hop inherits and
|
|
804
|
+
// the destination the leave guard is told all read it. A write of this
|
|
805
|
+
// same tick is not in it (A28) — see `compose` below.
|
|
806
|
+
//
|
|
807
|
+
// A redirect hop: the previous navigation is still pending, or has landed
|
|
808
|
+
// but not yet reached history (a guard redirecting in the landing flush
|
|
809
|
+
// — its destination was never shown either way).
|
|
810
|
+
const headed = latest(source);
|
|
811
|
+
// A composed target (`setSearchParams`) is a function of where the
|
|
812
|
+
// router is heading, an unflushed write of this tick included. Only
|
|
813
|
+
// the integration's functional updater serves that value, so the
|
|
814
|
+
// destination is resolved inside the write; `resolvedTo` here is the
|
|
815
|
+
// flushed world's reading of it — what the leave guard is asked about,
|
|
816
|
+
// and what the server (which never has an unflushed write) redirects to.
|
|
817
|
+
let compose;
|
|
818
|
+
let resolvedTo;
|
|
819
|
+
// A paths node carries its logical path under the Href brand — read
|
|
820
|
+
// that rather than coercing: toString() renders the *display* href
|
|
821
|
+
// (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
|
|
822
|
+
// Foreign Href-branded values without the slot still coerce. The node
|
|
823
|
+
// is callable, so the brand is checked ahead of the composer form.
|
|
824
|
+
const href = typeof to !== "string" ? to[HREF] : undefined;
|
|
825
|
+
if (href === undefined && typeof to === "function") {
|
|
826
|
+
const build = to;
|
|
827
|
+
compose = headed => {
|
|
828
|
+
const raw = build(headed);
|
|
829
|
+
const resolved = resolveTarget(raw);
|
|
830
|
+
if (resolved === undefined)
|
|
831
|
+
throw unroutable(raw);
|
|
832
|
+
return resolved;
|
|
833
|
+
};
|
|
834
|
+
resolvedTo = compose(headed);
|
|
794
835
|
}
|
|
795
|
-
|
|
796
|
-
|
|
836
|
+
else {
|
|
837
|
+
if (typeof to !== "string")
|
|
838
|
+
to = href || to.toString();
|
|
839
|
+
resolvedTo = resolveTarget(to);
|
|
840
|
+
if (resolvedTo === undefined)
|
|
841
|
+
throw unroutable(to);
|
|
797
842
|
}
|
|
798
|
-
const headed = latest(source);
|
|
799
843
|
const navigationDepth = !isServer &&
|
|
800
|
-
isPending(source) &&
|
|
801
844
|
headed._navigation !== undefined &&
|
|
802
|
-
headed._navigation > 0
|
|
845
|
+
headed._navigation > 0 &&
|
|
846
|
+
(isPending(source) || integration.inflight?.() === headed)
|
|
803
847
|
? headed._navigation
|
|
804
848
|
: 0;
|
|
805
849
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
806
850
|
throw new Error("Too many redirects");
|
|
807
851
|
}
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
852
|
+
if (isServer) {
|
|
853
|
+
// A server render has no unflushed write: the static integration's
|
|
854
|
+
// value is the whole world, so the no-op rule is applied here, before
|
|
855
|
+
// the redirect is recorded on the response.
|
|
856
|
+
if (resolvedTo === headed.value && nextState === headed.state)
|
|
857
|
+
return;
|
|
858
|
+
const e = getRequestEvent();
|
|
859
|
+
e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
|
|
860
|
+
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
861
|
+
}
|
|
862
|
+
else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
863
|
+
// Heading there already (same value and state) writes nothing — the
|
|
864
|
+
// integration decides that against the updater's `headed`, so a
|
|
865
|
+
// navigation issued behind another write of the same tick compares
|
|
866
|
+
// with that write, not with the flushed world.
|
|
867
|
+
runWithOwner(null, () => setSource({
|
|
868
|
+
value: compose || resolvedTo,
|
|
869
|
+
state: nextState,
|
|
870
|
+
replace: navigationDepth ? headed.replace : replace,
|
|
871
|
+
scroll: navigationDepth ? headed.scroll : scroll,
|
|
872
|
+
_navigation: navigationDepth + 1
|
|
873
|
+
}));
|
|
823
874
|
}
|
|
824
875
|
});
|
|
825
876
|
}
|
|
826
877
|
function navigatorFactory(route) {
|
|
827
878
|
// Workaround for vite issue (https://github.com/vitejs/vite/issues/3803)
|
|
828
879
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
829
|
-
return (to, options) => navigateFromRoute(route, to, options);
|
|
880
|
+
return ((to, options) => navigateFromRoute(route, to, options));
|
|
830
881
|
}
|
|
831
882
|
function preloadRoute(url, preloadData) {
|
|
832
883
|
const matches = getRouteMatches(branches(), url.pathname);
|
package/dist/types.d.ts
CHANGED
|
@@ -100,8 +100,31 @@ export interface LocationChange<S = unknown> {
|
|
|
100
100
|
/** @internal Positive for programmatic navigation depth; `-1` for native history. */
|
|
101
101
|
_navigation?: number;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* What the router hands the integration to write. `value` may compose on
|
|
105
|
+
* `headed` — the location the router is heading to: the last write, whether
|
|
106
|
+
* or not a flush has carried it yet. A write is invisible to every read
|
|
107
|
+
* channel until its flush (solid's A28 rule: `latest()` answers the
|
|
108
|
+
* pre-write value, `isPending()` is false), so a second synchronous write
|
|
109
|
+
* that builds on the first — `setSearchParams` twice in one handler, or once
|
|
110
|
+
* behind a `navigate()` — composes through the writer's own channel, the
|
|
111
|
+
* functional updater, rather than reading the first back. `undefined` from
|
|
112
|
+
* the composer, or a destination the router is already heading to (same
|
|
113
|
+
* `value` and `state`), writes nothing.
|
|
114
|
+
*/
|
|
115
|
+
export interface LocationWrite<S = unknown> extends Omit<LocationChange<S>, "value"> {
|
|
116
|
+
value: string | ((headed: LocationChange) => string | undefined);
|
|
117
|
+
}
|
|
103
118
|
export interface RouterIntegration {
|
|
104
|
-
signal: [get: () => LocationChange, set: (next:
|
|
119
|
+
signal: [get: () => LocationChange, set: (next: LocationWrite) => void];
|
|
120
|
+
/**
|
|
121
|
+
* The navigation written but not yet committed to history — the window
|
|
122
|
+
* between its location write and the settle that pushes it. A destination
|
|
123
|
+
* in that window was never shown, so a `navigate()` issued inside it (a
|
|
124
|
+
* guard redirecting as the held route lands) is a hop of that navigation
|
|
125
|
+
* rather than a new one, exactly as one issued while it is still pending.
|
|
126
|
+
*/
|
|
127
|
+
inflight?: () => LocationChange | undefined;
|
|
105
128
|
utils?: Partial<RouterUtils>;
|
|
106
129
|
}
|
|
107
130
|
export type Intent = "initial" | "native" | "navigate" | "preload";
|
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.25",
|
|
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.9",
|
|
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.9",
|
|
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.9",
|
|
55
|
+
"solid-js": "^2.0.0-rc.9"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "rm -rf dist && tsc && rollup -c",
|