@solidjs/router 2.0.0-next.35 → 2.0.0-next.37
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 +30 -7
- package/dist/claims.d.ts +5 -3
- package/dist/claims.js +14 -14
- package/dist/data/action.js +4 -1
- package/dist/data/events.js +9 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +245 -110
- package/dist/index.jsx +1 -0
- package/dist/paths.d.ts +23 -3
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- package/dist/routers/components.jsx +7 -4
- package/dist/routers/factory.d.ts +17 -2
- package/dist/routers/factory.jsx +22 -13
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +7 -1
- package/dist/routing.js +28 -38
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +27 -10
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack,
|
|
2
|
-
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo,
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, isPending, NotReadyError, DEV, latest, createContext, createSignal, useContext, createComponent, createOwner, createRoot, Show, onSettled, isHydrating, OBSERVE, getObserver, isHydratable, $TRACK, action as action$1 } from 'solid-js';
|
|
2
|
+
import { registerElementClaim, delegateEvents, dispatchAsInteraction, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamicComponent, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
|
|
5
5
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
@@ -249,7 +249,9 @@ function setFormClaimHandler(handler) {
|
|
|
249
249
|
* included (parameter order aside)
|
|
250
250
|
* - `data-active` — pathname exact or prefix match (the router's root, its
|
|
251
251
|
* base path, exact only)
|
|
252
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
252
|
+
* - `data-pending` — the link is the target of an in-flight navigation;
|
|
253
|
+
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
254
|
+
* plugin, claims never read pending state and the sweep does not track it.
|
|
253
255
|
*
|
|
254
256
|
* The matching rule is `matchLink`, shared with `useLinkState`. The router
|
|
255
257
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
@@ -264,8 +266,9 @@ function setFormClaimHandler(handler) {
|
|
|
264
266
|
* are the same one-shot untracked refresh, reading the element's current
|
|
265
267
|
* `href` from the DOM.
|
|
266
268
|
*/
|
|
267
|
-
function setupLinkClaims(router, explicitLinks) {
|
|
269
|
+
function setupLinkClaims(router, explicitLinks, links) {
|
|
268
270
|
const basePath = router.base.path();
|
|
271
|
+
const plugin = links && links(router, basePath);
|
|
269
272
|
// per-element record; `owned` is whether the `aria-current` on the element
|
|
270
273
|
// is the router's, so it never writes over or removes an authored one
|
|
271
274
|
const claimed = new WeakMap();
|
|
@@ -299,7 +302,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
299
302
|
// read reactive sources unconditionally so the owning effect stays
|
|
300
303
|
// subscribed even while the anchor is not router-managed
|
|
301
304
|
const location = router.location;
|
|
302
|
-
const routing =
|
|
305
|
+
const routing = plugin && plugin.track();
|
|
303
306
|
const url = managedUrl(a);
|
|
304
307
|
const target = url && url.pathname + url.search;
|
|
305
308
|
// no per-anchor `end` opt-out like useLinkState has
|
|
@@ -307,13 +310,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
307
310
|
active,
|
|
308
311
|
current
|
|
309
312
|
} = matchLink(location, target, basePath);
|
|
310
|
-
|
|
311
|
-
// in-flight target comes from pendingTarget — readable here because the
|
|
312
|
-
// isRouting write flushes after the target is assigned
|
|
313
|
-
const pending = routing && !!router.pendingTarget && matchLink({
|
|
314
|
-
pathname: router.pendingTarget.value,
|
|
315
|
-
search: ""
|
|
316
|
-
}, target, basePath).active;
|
|
313
|
+
const pending = !!routing && plugin.pending(target);
|
|
317
314
|
return {
|
|
318
315
|
active,
|
|
319
316
|
pending,
|
|
@@ -326,7 +323,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
326
323
|
current
|
|
327
324
|
}) {
|
|
328
325
|
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
329
|
-
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
326
|
+
if (plugin) pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
330
327
|
// Ownership is read against the element, not just the record. A
|
|
331
328
|
// server-component morph resets attributes to the server HTML, which
|
|
332
329
|
// never carries router link state, then re-claims: an owned value that
|
|
@@ -346,9 +343,10 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
346
343
|
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
347
344
|
|
|
348
345
|
// The one subscription for every anchor: compute tracks the sources
|
|
349
|
-
// linkState derives from (the
|
|
350
|
-
//
|
|
351
|
-
// assigned), the effect phase sweeps the
|
|
346
|
+
// linkState derives from (with the plugin, its pending read — the
|
|
347
|
+
// in-flight target is readable in the effect phase because the isRouting
|
|
348
|
+
// write flushes after the target is assigned), the effect phase sweeps the
|
|
349
|
+
// registry untracked.
|
|
352
350
|
//
|
|
353
351
|
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
354
352
|
// This setup is client-only, so an id-consuming node here has no server
|
|
@@ -356,7 +354,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
356
354
|
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
357
355
|
// unclaimed. (The option is honored by the runtime but missing from the
|
|
358
356
|
// published EffectOptions type, hence the cast.)
|
|
359
|
-
createRenderEffect(() => (router.location.pathname, router.location.search,
|
|
357
|
+
createRenderEffect(() => (router.location.pathname, router.location.search, plugin && plugin.track()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
|
|
360
358
|
transparent: true
|
|
361
359
|
});
|
|
362
360
|
onCleanup(registerElementClaim(node => {
|
|
@@ -432,12 +430,14 @@ function setupNativeEvents({
|
|
|
432
430
|
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
433
431
|
const state = a.getAttribute("state");
|
|
434
432
|
evt.preventDefault();
|
|
435
|
-
|
|
433
|
+
// Only a click the router acts on joins the event's interaction: this
|
|
434
|
+
// listener hears every click on the document.
|
|
435
|
+
dispatchAsInteraction(evt, () => navigateFromRoute(to, {
|
|
436
436
|
resolve: false,
|
|
437
437
|
replace: a.hasAttribute("replace"),
|
|
438
438
|
scroll: !a.hasAttribute("noscroll"),
|
|
439
439
|
state: state ? JSON.parse(state) : undefined
|
|
440
|
-
});
|
|
440
|
+
}));
|
|
441
441
|
}
|
|
442
442
|
function handleAnchorPreload(evt) {
|
|
443
443
|
const res = handleAnchor(evt);
|
|
@@ -477,10 +477,12 @@ function setupNativeEvents({
|
|
|
477
477
|
const data = new FormData(form, evt.submitter);
|
|
478
478
|
Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
|
|
479
479
|
}
|
|
480
|
+
const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
480
481
|
|
|
481
482
|
// ensure delegated event run first
|
|
482
483
|
delegateEvents(["click", "submit"]);
|
|
483
484
|
document.addEventListener("click", handleAnchorClick);
|
|
485
|
+
// preloads are not interactions: those listeners run outside any frame
|
|
484
486
|
if (preload) {
|
|
485
487
|
document.addEventListener("mousemove", handleAnchorMove, {
|
|
486
488
|
passive: true
|
|
@@ -492,7 +494,7 @@ function setupNativeEvents({
|
|
|
492
494
|
passive: true
|
|
493
495
|
});
|
|
494
496
|
}
|
|
495
|
-
document.addEventListener("submit",
|
|
497
|
+
document.addEventListener("submit", handleSubmit);
|
|
496
498
|
onCleanup(() => {
|
|
497
499
|
document.removeEventListener("click", handleAnchorClick);
|
|
498
500
|
if (preload) {
|
|
@@ -500,7 +502,7 @@ function setupNativeEvents({
|
|
|
500
502
|
document.removeEventListener("focusin", handleAnchorPreload);
|
|
501
503
|
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
502
504
|
}
|
|
503
|
-
document.removeEventListener("submit",
|
|
505
|
+
document.removeEventListener("submit", handleSubmit);
|
|
504
506
|
});
|
|
505
507
|
};
|
|
506
508
|
}
|
|
@@ -544,6 +546,14 @@ const int = s => /^-?\d+$/.test(s);
|
|
|
544
546
|
* untyped, definitionally.
|
|
545
547
|
*/
|
|
546
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Sibling routes that share a param prefix (`/posts/:id`, `/posts/:id/edit`)
|
|
551
|
+
* each contribute a call at that node, and a call on an intersection of
|
|
552
|
+
* function types resolves to its first overload only. So the call reads its
|
|
553
|
+
* result back through `this` — the whole intersection — where the
|
|
554
|
+
* continuations of every sibling with the same arity have merged.
|
|
555
|
+
*/
|
|
556
|
+
|
|
547
557
|
/**
|
|
548
558
|
* The type of a router instance's `paths` proxy for a given route tree.
|
|
549
559
|
* Requires the tree to be a literal tuple (`as const` or a `const` type
|
|
@@ -602,7 +612,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
602
612
|
|
|
603
613
|
// The light half of server component routes: the brand the router core reads
|
|
604
614
|
// off a route `component`, and the derivation of a route's call arguments.
|
|
605
|
-
// Deliberately free of `query`/`
|
|
615
|
+
// Deliberately free of `query`/`dynamicComponent` — routing.ts imports this module,
|
|
606
616
|
// and routing.ts is in every app's eager graph. The heavy half (the helper
|
|
607
617
|
// that builds the branded component) lives in serverRouteComponent.ts and only
|
|
608
618
|
// enters a bundle when an app calls `serverRouteComponent()`.
|
|
@@ -666,6 +676,86 @@ function serverRouteArgsEqual(a, b) {
|
|
|
666
676
|
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
667
677
|
}
|
|
668
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
681
|
+
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
682
|
+
* and `latest` are UI affordances — the router's own coordination reads its
|
|
683
|
+
* location writes and their `onSettled` instead — so an app that renders
|
|
684
|
+
* none of these never bundles Solid's verdict machinery.
|
|
685
|
+
*/
|
|
686
|
+
|
|
687
|
+
const routing = new WeakMap();
|
|
688
|
+
|
|
689
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
690
|
+
function routingState(router) {
|
|
691
|
+
let read = routing.get(router);
|
|
692
|
+
if (!read) {
|
|
693
|
+
const {
|
|
694
|
+
location,
|
|
695
|
+
matches,
|
|
696
|
+
_source: source
|
|
697
|
+
} = router;
|
|
698
|
+
// `transparent`: created on first use rather than at router setup, so it
|
|
699
|
+
// must not take a hydration id — the server may never create it, or
|
|
700
|
+
// create it at a different point.
|
|
701
|
+
const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
|
|
702
|
+
try {
|
|
703
|
+
matches();
|
|
704
|
+
} catch (e) {
|
|
705
|
+
if (e instanceof NotReadyError) throw e;
|
|
706
|
+
}
|
|
707
|
+
location.search;
|
|
708
|
+
location.hash;
|
|
709
|
+
}), {
|
|
710
|
+
transparent: true,
|
|
711
|
+
...(DEV && {
|
|
712
|
+
name: "routingPending"
|
|
713
|
+
})
|
|
714
|
+
}));
|
|
715
|
+
read = () => pending() || isPending(source);
|
|
716
|
+
routing.set(router, read);
|
|
717
|
+
}
|
|
718
|
+
return read;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
723
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
724
|
+
*/
|
|
725
|
+
function pendingTarget(router) {
|
|
726
|
+
if (!routingState(router)()) return;
|
|
727
|
+
const target = latest(router._source);
|
|
728
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
732
|
+
function linkPending(router, to, base, end) {
|
|
733
|
+
const target = pendingTarget(router);
|
|
734
|
+
return !!target && matchLink({
|
|
735
|
+
pathname: target.value,
|
|
736
|
+
search: ""
|
|
737
|
+
}, to, base, end).active;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
742
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
743
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
744
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
745
|
+
*
|
|
746
|
+
* @example
|
|
747
|
+
* ```ts
|
|
748
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
749
|
+
*
|
|
750
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
751
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
const pendingLinks = (router, base) => ({
|
|
755
|
+
track: routingState(router),
|
|
756
|
+
pending: target => linkPending(router, target, base)
|
|
757
|
+
});
|
|
758
|
+
|
|
669
759
|
const MAX_REDIRECTS = 100;
|
|
670
760
|
|
|
671
761
|
/**
|
|
@@ -751,6 +841,8 @@ const useLocation = () => useRouter().location;
|
|
|
751
841
|
/**
|
|
752
842
|
* Retrieves a signal that indicates whether the router is currently processing a navigation.
|
|
753
843
|
* Useful for showing pending navigation state while the next route and its data settle.
|
|
844
|
+
* This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
|
|
845
|
+
* For the in-flight destination, read the location with Solid's `isPending`/`latest`.
|
|
754
846
|
*
|
|
755
847
|
* @example
|
|
756
848
|
* ```js
|
|
@@ -763,7 +855,7 @@ const useLocation = () => useRouter().location;
|
|
|
763
855
|
* );
|
|
764
856
|
* ```
|
|
765
857
|
*/
|
|
766
|
-
const useIsRouting = () => useRouter()
|
|
858
|
+
const useIsRouting = () => routingState(useRouter());
|
|
767
859
|
|
|
768
860
|
/**
|
|
769
861
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
@@ -951,10 +1043,7 @@ const useLinkState = (href, options = {}) => {
|
|
|
951
1043
|
// observe the committed location during a transition
|
|
952
1044
|
pending: createMemo(() => {
|
|
953
1045
|
state(); // location dependency: mid-flight target swaps recompute
|
|
954
|
-
return router
|
|
955
|
-
pathname: router.pendingTarget.value,
|
|
956
|
-
search: ""
|
|
957
|
-
}, to(), base, options.end).active;
|
|
1046
|
+
return linkPending(router, to(), base, options.end);
|
|
958
1047
|
})
|
|
959
1048
|
};
|
|
960
1049
|
};
|
|
@@ -1312,7 +1401,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1312
1401
|
_navigation: isServer ? undefined : 1
|
|
1313
1402
|
});
|
|
1314
1403
|
}
|
|
1315
|
-
|
|
1404
|
+
|
|
1405
|
+
// The flushed world — what `latest(source)` answers — as the location's own
|
|
1406
|
+
// memo sees it: it recomputes under the transition that carries a write,
|
|
1407
|
+
// never for a write of this same tick (A28).
|
|
1408
|
+
let flushed = initialSource;
|
|
1409
|
+
const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
|
|
1316
1410
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
1317
1411
|
// must be appended before streaming flushes the response headers, and an
|
|
1318
1412
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -1389,7 +1483,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1389
1483
|
// NotReadyError's source must be a reactive async node, not the raw
|
|
1390
1484
|
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
1391
1485
|
// is delivered through the graph's error channel and resolution wakes the
|
|
1392
|
-
// parked matches computation.
|
|
1486
|
+
// parked matches computation. `transparent`: the reader exists on the
|
|
1487
|
+
// client only (the server parks `matches` on the promise directly), so it
|
|
1488
|
+
// must not take a hydration id the server never allocated.
|
|
1393
1489
|
const routerOwner = getOwner();
|
|
1394
1490
|
const lazyReaders = new WeakMap();
|
|
1395
1491
|
const readLazySubtree = record => {
|
|
@@ -1398,8 +1494,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1398
1494
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1399
1495
|
const result = resolveLazySubtree(record);
|
|
1400
1496
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1401
|
-
},
|
|
1402
|
-
|
|
1497
|
+
}, {
|
|
1498
|
+
transparent: true,
|
|
1499
|
+
...(DEV && {
|
|
1500
|
+
name: "lazyRoutes"
|
|
1501
|
+
})
|
|
1403
1502
|
}));
|
|
1404
1503
|
lazyReaders.set(record, read);
|
|
1405
1504
|
}
|
|
@@ -1434,28 +1533,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1434
1533
|
}, DEV && {
|
|
1435
1534
|
name: "matches"
|
|
1436
1535
|
});
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
} catch (e) {
|
|
1441
|
-
if (e instanceof NotReadyError) throw e;
|
|
1442
|
-
}
|
|
1443
|
-
location.search;
|
|
1444
|
-
location.hash;
|
|
1445
|
-
}), DEV && {
|
|
1446
|
-
name: "routingPending"
|
|
1447
|
-
});
|
|
1448
|
-
const isRouting = () => routingPending() || isPending(source);
|
|
1536
|
+
const headedLocation = () => isServer ? untrack(source) : flushed;
|
|
1537
|
+
// A flushed navigation write whose settle has not run is still in flight.
|
|
1538
|
+
const unsettled = change => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
|
|
1449
1539
|
const transitionIntent = () => {
|
|
1450
|
-
|
|
1451
|
-
|
|
1540
|
+
const head = headedLocation();
|
|
1541
|
+
if (!unsettled(head)) return;
|
|
1542
|
+
const navigation = head._navigation;
|
|
1452
1543
|
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
1453
1544
|
};
|
|
1454
|
-
const pendingNavigation = () => {
|
|
1455
|
-
if (!isRouting()) return;
|
|
1456
|
-
const target = latest(source);
|
|
1457
|
-
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
1458
|
-
};
|
|
1459
1545
|
const buildParams = () => mergeParams(matches());
|
|
1460
1546
|
const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
|
|
1461
1547
|
const params = wrapParams(buildParams);
|
|
@@ -1473,11 +1559,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1473
1559
|
location,
|
|
1474
1560
|
params,
|
|
1475
1561
|
wrapParams,
|
|
1476
|
-
isRouting,
|
|
1477
1562
|
intent: transitionIntent,
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
},
|
|
1563
|
+
_source: source,
|
|
1564
|
+
_owner: routerOwner,
|
|
1481
1565
|
renderPath,
|
|
1482
1566
|
parsePath,
|
|
1483
1567
|
navigatorFactory,
|
|
@@ -1538,10 +1622,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1538
1622
|
// the destination the leave guard is told all read it. A write of this
|
|
1539
1623
|
// same tick is not in it (A28) — see `compose` below.
|
|
1540
1624
|
//
|
|
1541
|
-
// A redirect hop: the previous navigation
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
const headed =
|
|
1625
|
+
// A redirect hop: the previous navigation's write has not settled — it
|
|
1626
|
+
// is still held, or a guard is redirecting in its landing flush — so its
|
|
1627
|
+
// destination was never shown.
|
|
1628
|
+
const headed = headedLocation();
|
|
1545
1629
|
|
|
1546
1630
|
// A composed target (`setSearchParams`) is a function of where the
|
|
1547
1631
|
// router is heading, an unflushed write of this tick included. Only
|
|
@@ -1571,7 +1655,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1571
1655
|
resolvedTo = resolveTarget(to);
|
|
1572
1656
|
if (resolvedTo === undefined) throw unroutable(to);
|
|
1573
1657
|
}
|
|
1574
|
-
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && (
|
|
1658
|
+
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && unsettled(headed) ? headed._navigation : 0;
|
|
1575
1659
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
1576
1660
|
throw new Error("Too many redirects");
|
|
1577
1661
|
}
|
|
@@ -1825,9 +1909,12 @@ function Routes(props) {
|
|
|
1825
1909
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
1826
1910
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
1827
1911
|
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
1828
|
-
// are created under
|
|
1829
|
-
// computation (which disposes its children every time it re-runs).
|
|
1830
|
-
|
|
1912
|
+
// are created under an owner of this component rather than the memo's
|
|
1913
|
+
// computation (which disposes its children every time it re-runs). A
|
|
1914
|
+
// dedicated one, created before the memo: the roots draw hydration ids from
|
|
1915
|
+
// its counter, so the ids don't depend on whether the memo first parked on
|
|
1916
|
+
// a lazy subtree (the server's render can park where the client's doesn't).
|
|
1917
|
+
const owner = createOwner();
|
|
1831
1918
|
const routeStates = createMemo(prev => {
|
|
1832
1919
|
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
1833
1920
|
// computation parks with it — no route contexts are created against
|
|
@@ -2119,6 +2206,7 @@ function memoryHistory(initial = "/") {
|
|
|
2119
2206
|
}
|
|
2120
2207
|
|
|
2121
2208
|
const STORAGE_KEY = "solid-router:scroll";
|
|
2209
|
+
const AUTO_KEY = "solid-router:scroll-auto";
|
|
2122
2210
|
|
|
2123
2211
|
/**
|
|
2124
2212
|
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
@@ -2133,7 +2221,7 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2133
2221
|
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
2134
2222
|
* otherwise disables.
|
|
2135
2223
|
*
|
|
2136
|
-
* Restoration is a single scroll once
|
|
2224
|
+
* Restoration is a single scroll once the traversal settles — the same strategy
|
|
2137
2225
|
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
2138
2226
|
* transition commits is what makes the offset reachable; chasing a still-
|
|
2139
2227
|
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
@@ -2143,17 +2231,49 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2143
2231
|
* layout changes can feed it back into itself. Content that commits after the
|
|
2144
2232
|
* transition settles — an image without reserved space, a boundary below the
|
|
2145
2233
|
* fold — keeps whatever offset the document can hold.
|
|
2234
|
+
*
|
|
2235
|
+
* A server-rendered document load is the exception: the browser restores it
|
|
2236
|
+
* natively once the document — streamed boundaries included — has loaded,
|
|
2237
|
+
* which a restore at the first client flush cannot match. So an entry is
|
|
2238
|
+
* handed back to the browser (`auto`) whenever its document may unload
|
|
2239
|
+
* (pagehide), and a hydrating router arriving on a handed-back entry leaves
|
|
2240
|
+
* the restore to the browser: it does not scroll, and it takes the entry back
|
|
2241
|
+
* (`manual`) only after load or at the first client navigation, whichever
|
|
2242
|
+
* comes first. A programmatic scroll during the load cancels the native
|
|
2243
|
+
* restore in Firefox and WebKit, and WebKit restores just after the load
|
|
2244
|
+
* event, so a `manual` write before then — inside a load listener included —
|
|
2245
|
+
* suppresses it; the write waits a task. Handed-back depths are tracked here
|
|
2246
|
+
* rather than read from `history.scrollRestoration`, which Firefox reports as
|
|
2247
|
+
* the mode before pagehide after a reload. An entry this router pushed and
|
|
2248
|
+
* never handed back stays `manual` and gets no native restore, so the router
|
|
2249
|
+
* restores it as on a client-rendered load.
|
|
2146
2250
|
*/
|
|
2147
|
-
function createScrollRestoration() {
|
|
2148
|
-
window.history
|
|
2251
|
+
function createScrollRestoration(hydrating) {
|
|
2252
|
+
const h = window.history;
|
|
2149
2253
|
// the current entry needs its depth stamp for captures to have a key, even
|
|
2150
2254
|
// if something replaced history.state after the adapter stamped it
|
|
2151
2255
|
saveCurrentDepth();
|
|
2256
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
2152
2257
|
let positions = {};
|
|
2258
|
+
let handedBack = {};
|
|
2153
2259
|
try {
|
|
2154
2260
|
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
2261
|
+
handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
|
|
2155
2262
|
} catch {}
|
|
2156
|
-
const
|
|
2263
|
+
const manual = () => {
|
|
2264
|
+
h.scrollRestoration = "manual";
|
|
2265
|
+
const d = depth();
|
|
2266
|
+
if (d != null) delete handedBack[d];
|
|
2267
|
+
};
|
|
2268
|
+
const d = depth();
|
|
2269
|
+
let deferred = !!hydrating && (h.scrollRestoration === "auto" || d != null && !!handedBack[d]);
|
|
2270
|
+
if (!deferred) manual();
|
|
2271
|
+
const claim = () => {
|
|
2272
|
+
if (!deferred) return;
|
|
2273
|
+
deferred = false;
|
|
2274
|
+
manual();
|
|
2275
|
+
};
|
|
2276
|
+
let timer;
|
|
2157
2277
|
let programmatic = false;
|
|
2158
2278
|
let pending;
|
|
2159
2279
|
const unbind = [bindEvent(window, "scroll", () => {
|
|
@@ -2162,10 +2282,16 @@ function createScrollRestoration() {
|
|
|
2162
2282
|
// the user took over — a pending restore would yank them
|
|
2163
2283
|
if (!programmatic) pending = undefined;
|
|
2164
2284
|
}), bindEvent(window, "pagehide", () => {
|
|
2285
|
+
h.scrollRestoration = "auto";
|
|
2286
|
+
const d = depth();
|
|
2287
|
+
if (d != null) handedBack[d] = 1;
|
|
2165
2288
|
try {
|
|
2166
2289
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
2290
|
+
sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
|
|
2167
2291
|
} catch {}
|
|
2168
|
-
})
|
|
2292
|
+
}), bindEvent(window, "pageshow", e => {
|
|
2293
|
+
if (e.persisted) manual();
|
|
2294
|
+
}), bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))), () => clearTimeout(timer)];
|
|
2169
2295
|
const restore = () => {
|
|
2170
2296
|
if (pending == null) return;
|
|
2171
2297
|
const y = positions[pending];
|
|
@@ -2178,42 +2304,38 @@ function createScrollRestoration() {
|
|
|
2178
2304
|
programmatic = false;
|
|
2179
2305
|
};
|
|
2180
2306
|
return {
|
|
2181
|
-
/**
|
|
2307
|
+
/**
|
|
2308
|
+
* When the adapter notifies a traversal: mark the target and restore once
|
|
2309
|
+
* the transition carrying it settles. The adapter notifies right before
|
|
2310
|
+
* the location write, in the same tick, so the settle is that write's —
|
|
2311
|
+
* a same-URL traversal included, which no location key would see.
|
|
2312
|
+
*/
|
|
2182
2313
|
onPop() {
|
|
2314
|
+
claim();
|
|
2183
2315
|
pending = depth();
|
|
2316
|
+
runWithOwner(null, () => onSettled(restore));
|
|
2184
2317
|
},
|
|
2318
|
+
/** The initial page settled: restore a reload/back_forward arrival. */
|
|
2319
|
+
settled: () => restore(),
|
|
2320
|
+
/** Before a client navigation writes history: the entry it creates copies the current mode. */
|
|
2321
|
+
beforeWrite: claim,
|
|
2185
2322
|
/** After a push: forward entries died, and this depth may be reused. */
|
|
2186
2323
|
onPush() {
|
|
2187
2324
|
const d = depth();
|
|
2188
|
-
if (d != null)
|
|
2325
|
+
if (d != null) {
|
|
2326
|
+
for (const k in positions) +k >= d && delete positions[k];
|
|
2327
|
+
for (const k in handedBack) +k >= d && delete handedBack[k];
|
|
2328
|
+
}
|
|
2189
2329
|
},
|
|
2190
|
-
create(
|
|
2191
|
-
// Restore once the traversal has settled: key on the location (a fully
|
|
2192
|
-
// synchronous pop commits without isRouting ever flipping) and on
|
|
2193
|
-
// isRouting, which reports in-flight transitions — native pops
|
|
2194
|
-
// included — and holds the restore until they commit. restore() no-ops
|
|
2195
|
-
// unless a traversal marked a target, so push navigations are inert.
|
|
2196
|
-
// `transparent` keeps the effect invisible to the hydration id scheme —
|
|
2197
|
-
// same reasoning as the link-claims effect (claims.ts): this setup is
|
|
2198
|
-
// client-only, so an id-consuming node here has no server counterpart
|
|
2199
|
-
// and every hydration id allocated after it shifts by one child slot.
|
|
2200
|
-
// The visible failure is any <Loading> content that settled before the
|
|
2201
|
-
// shell flush (a cache hit, a preloaded query): its serialized value and
|
|
2202
|
-
// inlined markup are keyed under the server's ids, the shifted client
|
|
2203
|
-
// misses both, recomputes, and re-renders the route fresh — duplicating
|
|
2204
|
-
// the server DOM and leaving it inert.
|
|
2205
|
-
createEffect(() => ({
|
|
2206
|
-
url: router.location.pathname + router.location.search + router.location.hash,
|
|
2207
|
-
routing: router.isRouting()
|
|
2208
|
-
}), current => {
|
|
2209
|
-
if (!current.routing) restore();
|
|
2210
|
-
}, {
|
|
2211
|
-
transparent: true
|
|
2212
|
-
});
|
|
2330
|
+
create() {
|
|
2213
2331
|
onCleanup(() => unbind.forEach(u => u()));
|
|
2214
2332
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
2215
|
-
// navigation starts a new one and belongs at the top); the
|
|
2216
|
-
// initial
|
|
2333
|
+
// navigation starts a new one and belongs at the top); the router calls
|
|
2334
|
+
// `settled` once the initial page has; a deferred arrival is the browser's
|
|
2335
|
+
if (deferred) {
|
|
2336
|
+
document.readyState === "complete" && claim();
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2217
2339
|
const [nav] = performance.getEntriesByType?.("navigation");
|
|
2218
2340
|
if (nav && nav.type !== "navigate") pending = depth();
|
|
2219
2341
|
}
|
|
@@ -2229,6 +2351,7 @@ function withScrollRestoration(history, restoration) {
|
|
|
2229
2351
|
return {
|
|
2230
2352
|
...history,
|
|
2231
2353
|
set(next) {
|
|
2354
|
+
restoration.beforeWrite();
|
|
2232
2355
|
history.set(next);
|
|
2233
2356
|
next.replace || restoration.onPush();
|
|
2234
2357
|
},
|
|
@@ -2376,8 +2499,8 @@ function describeInitial(match, location) {
|
|
|
2376
2499
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2377
2500
|
function createIntegration(history, match) {
|
|
2378
2501
|
let committing = false;
|
|
2379
|
-
//
|
|
2380
|
-
|
|
2502
|
+
// Writes whose transition has landed (see `RouterIntegration.settled`).
|
|
2503
|
+
const settled = new WeakSet();
|
|
2381
2504
|
const wrap = value => typeof value === "string" ? {
|
|
2382
2505
|
value
|
|
2383
2506
|
} : value;
|
|
@@ -2396,14 +2519,16 @@ function createIntegration(history, match) {
|
|
|
2396
2519
|
// the no-op rule compares against it, so `navigate()` behind another
|
|
2397
2520
|
// write in one handler sees that write rather than the flushed world.
|
|
2398
2521
|
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
2399
|
-
if (written && written._navigation
|
|
2522
|
+
if (written && written._navigation) {
|
|
2400
2523
|
const next = written;
|
|
2401
|
-
inflight = next;
|
|
2402
2524
|
// Register out of band so a destination error boundary replacing the
|
|
2403
|
-
// Router subtree cannot suppress the winning history commit.
|
|
2525
|
+
// Router subtree cannot suppress the winning history commit. The
|
|
2526
|
+
// settle is tied to the transition carrying the write: it fires when
|
|
2527
|
+
// that lands, a rejection included, and a superseded write's settle
|
|
2528
|
+
// fires with the transition that absorbed it.
|
|
2404
2529
|
runWithOwner(null, () => onSettled(() => {
|
|
2405
|
-
|
|
2406
|
-
if (read() !== next) return;
|
|
2530
|
+
settled.add(next);
|
|
2531
|
+
if (next._navigation < 0 || read() !== next) return;
|
|
2407
2532
|
committing = true;
|
|
2408
2533
|
try {
|
|
2409
2534
|
history.set(next);
|
|
@@ -2437,7 +2562,7 @@ function createIntegration(history, match) {
|
|
|
2437
2562
|
}));
|
|
2438
2563
|
return {
|
|
2439
2564
|
signal,
|
|
2440
|
-
|
|
2565
|
+
settled: write => settled.has(write),
|
|
2441
2566
|
utils: history.utils
|
|
2442
2567
|
};
|
|
2443
2568
|
}
|
|
@@ -2502,7 +2627,7 @@ function createRouter(config) {
|
|
|
2502
2627
|
let restoration;
|
|
2503
2628
|
let history = config.history;
|
|
2504
2629
|
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
2505
|
-
restoration = createScrollRestoration();
|
|
2630
|
+
restoration = createScrollRestoration(isHydrating());
|
|
2506
2631
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
2507
2632
|
}
|
|
2508
2633
|
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
@@ -2522,10 +2647,14 @@ function createRouter(config) {
|
|
|
2522
2647
|
actionBase: config.actionBase,
|
|
2523
2648
|
transformUrl: config.transformUrl
|
|
2524
2649
|
})(routerState);
|
|
2525
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
2650
|
+
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
2526
2651
|
if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
|
|
2527
|
-
restoration && restoration.create(
|
|
2652
|
+
restoration && restoration.create();
|
|
2528
2653
|
}
|
|
2654
|
+
// Registered on both sides, outside the client-only branch: an owned
|
|
2655
|
+
// onSettled takes a hydration id on the server too, so the ids line up.
|
|
2656
|
+
// A no-op when the arrival is left to the browser's native restore.
|
|
2657
|
+
onSettled(() => restoration && restoration.settled());
|
|
2529
2658
|
return createComponent$1(RouterContextObj, {
|
|
2530
2659
|
value: routerState,
|
|
2531
2660
|
get children() {
|
|
@@ -2672,7 +2801,7 @@ const useBeforeLeave = listener => {
|
|
|
2672
2801
|
// // before: a client component exists only to make the call
|
|
2673
2802
|
// const getStory = query(storyView, "story");
|
|
2674
2803
|
// component: props => {
|
|
2675
|
-
// const View =
|
|
2804
|
+
// const View = dynamicComponent(() => getStory(props.params.id));
|
|
2676
2805
|
// return <View>{props.children}</View>;
|
|
2677
2806
|
// }
|
|
2678
2807
|
// // after
|
|
@@ -2734,8 +2863,9 @@ source) {
|
|
|
2734
2863
|
};
|
|
2735
2864
|
function render(args, routeProps) {
|
|
2736
2865
|
// The source may answer a component, a promise of one, or (a live query)
|
|
2737
|
-
// successive components; `
|
|
2738
|
-
|
|
2866
|
+
// successive components; `dynamicComponent`'s memo lands each the same
|
|
2867
|
+
// way. Never a tag, so the element runtime `dynamic` retains stays out.
|
|
2868
|
+
const View = dynamicComponent(() => call(args()));
|
|
2739
2869
|
return createComponent(View, {
|
|
2740
2870
|
get children() {
|
|
2741
2871
|
return routeProps.children;
|
|
@@ -3349,7 +3479,12 @@ function installRouterIntegrations() {
|
|
|
3349
3479
|
}
|
|
3350
3480
|
function useSubmissions(fn, filter) {
|
|
3351
3481
|
const router = useRouter();
|
|
3352
|
-
|
|
3482
|
+
// Submissions are immutable records, so the same records in the same order
|
|
3483
|
+
// are the same list: another action's submissions changing re-runs the
|
|
3484
|
+
// filter but must not re-run this list's readers.
|
|
3485
|
+
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))), {
|
|
3486
|
+
equals: (a, b) => a.length === b.length && a.every((s, i) => s === b[i])
|
|
3487
|
+
});
|
|
3353
3488
|
return new Proxy([], {
|
|
3354
3489
|
get(_, property) {
|
|
3355
3490
|
if (property === $TRACK) return subs();
|
|
@@ -4085,4 +4220,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
4085
4220
|
submitServerForm: submitServerForm
|
|
4086
4221
|
});
|
|
4087
4222
|
|
|
4088
|
-
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
|
4223
|
+
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|