@solidjs/router 2.0.0-next.36 → 2.0.0-next.38
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 +103 -40
- package/dist/claims.d.ts +6 -4
- package/dist/claims.js +26 -17
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +45 -45
- package/dist/index.d.ts +5 -2
- package/dist/index.js +608 -260
- package/dist/index.jsx +4 -1
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- package/dist/preload.d.ts +53 -0
- package/dist/preload.js +175 -0
- package/dist/preloadRoute.d.ts +23 -0
- package/dist/preloadRoute.js +92 -0
- package/dist/routers/components.jsx +7 -4
- package/dist/routers/factory.d.ts +29 -4
- package/dist/routers/factory.jsx +27 -15
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +8 -16
- package/dist/routing.js +39 -127
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +46 -11
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- 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;
|
|
@@ -37,28 +37,35 @@ const comparableQuery = search => {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* The link-state rule shared by claimed anchors and `useLinkState`,
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* The link-state rule shared by claimed anchors and `useLinkState`, bound to
|
|
41
|
+
* a location so links matched against the same one share its parse. The
|
|
42
|
+
* returned matcher takes a link's already-resolved target (path, optional
|
|
43
|
+
* query and hash):
|
|
43
44
|
*
|
|
44
45
|
* - `current` — same pathname and same query (parameter order and hash aside)
|
|
45
46
|
* - `active` — same pathname or one under it; the router's root (`base`, or
|
|
46
47
|
* `/` without one) only matches exactly, since it is a prefix of every
|
|
47
48
|
* page. `end` makes every link exact-only.
|
|
48
49
|
*/
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
function linkMatcher({
|
|
51
|
+
pathname,
|
|
52
|
+
search
|
|
53
|
+
}, base, end) {
|
|
54
|
+
const loc = comparablePath(pathname);
|
|
55
|
+
const root = comparablePath(base);
|
|
56
|
+
return target => {
|
|
57
|
+
if (target === undefined) return {
|
|
58
|
+
active: false,
|
|
59
|
+
current: false
|
|
60
|
+
};
|
|
61
|
+
const path = comparablePath(target);
|
|
62
|
+
const exact = loc === path;
|
|
63
|
+
const hashless = target.split("#", 1)[0];
|
|
64
|
+
const q = hashless.indexOf("?");
|
|
65
|
+
return {
|
|
66
|
+
active: exact || !end && path !== "" && path !== root && loc.startsWith(path + "/"),
|
|
67
|
+
current: exact && comparableQuery(search) === comparableQuery(q < 0 ? "" : hashless.slice(q))
|
|
68
|
+
};
|
|
62
69
|
};
|
|
63
70
|
}
|
|
64
71
|
function resolvePath(base, path, from) {
|
|
@@ -249,9 +256,11 @@ function setFormClaimHandler(handler) {
|
|
|
249
256
|
* included (parameter order aside)
|
|
250
257
|
* - `data-active` — pathname exact or prefix match (the router's root, its
|
|
251
258
|
* base path, exact only)
|
|
252
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
259
|
+
* - `data-pending` — the link is the target of an in-flight navigation;
|
|
260
|
+
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
261
|
+
* plugin, claims never read pending state and the sweep does not track it.
|
|
253
262
|
*
|
|
254
|
-
* The matching rule is `
|
|
263
|
+
* The matching rule is `linkMatcher`, shared with `useLinkState`. The router
|
|
255
264
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
256
265
|
* stepper's `"step"`, a static `"page"`) is left in place, current or not.
|
|
257
266
|
*
|
|
@@ -264,8 +273,9 @@ function setFormClaimHandler(handler) {
|
|
|
264
273
|
* are the same one-shot untracked refresh, reading the element's current
|
|
265
274
|
* `href` from the DOM.
|
|
266
275
|
*/
|
|
267
|
-
function setupLinkClaims(router, explicitLinks) {
|
|
276
|
+
function setupLinkClaims(router, explicitLinks, links) {
|
|
268
277
|
const basePath = router.base.path();
|
|
278
|
+
const plugin = links && links(router, basePath);
|
|
269
279
|
// per-element record; `owned` is whether the `aria-current` on the element
|
|
270
280
|
// is the router's, so it never writes over or removes an authored one
|
|
271
281
|
const claimed = new WeakMap();
|
|
@@ -295,25 +305,29 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
295
305
|
if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
|
|
296
306
|
return url;
|
|
297
307
|
}
|
|
308
|
+
|
|
309
|
+
// every anchor matches against the same location, so its parse is shared
|
|
310
|
+
// until the location changes
|
|
311
|
+
let matched;
|
|
312
|
+
let match;
|
|
298
313
|
function linkState(a) {
|
|
299
314
|
// read reactive sources unconditionally so the owning effect stays
|
|
300
315
|
// subscribed even while the anchor is not router-managed
|
|
301
316
|
const location = router.location;
|
|
302
|
-
const routing =
|
|
317
|
+
const routing = plugin && plugin.track();
|
|
303
318
|
const url = managedUrl(a);
|
|
304
319
|
const target = url && url.pathname + url.search;
|
|
320
|
+
const key = location.pathname + location.search;
|
|
305
321
|
// no per-anchor `end` opt-out like useLinkState has
|
|
322
|
+
if (key !== matched) {
|
|
323
|
+
matched = key;
|
|
324
|
+
match = linkMatcher(location, basePath);
|
|
325
|
+
}
|
|
306
326
|
const {
|
|
307
327
|
active,
|
|
308
328
|
current
|
|
309
|
-
} =
|
|
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;
|
|
329
|
+
} = match(target);
|
|
330
|
+
const pending = !!routing && plugin.pending(target);
|
|
317
331
|
return {
|
|
318
332
|
active,
|
|
319
333
|
pending,
|
|
@@ -326,7 +340,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
326
340
|
current
|
|
327
341
|
}) {
|
|
328
342
|
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
329
|
-
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
343
|
+
if (plugin) pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
330
344
|
// Ownership is read against the element, not just the record. A
|
|
331
345
|
// server-component morph resets attributes to the server HTML, which
|
|
332
346
|
// never carries router link state, then re-claims: an owned value that
|
|
@@ -346,9 +360,10 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
346
360
|
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
347
361
|
|
|
348
362
|
// The one subscription for every anchor: compute tracks the sources
|
|
349
|
-
// linkState derives from (the
|
|
350
|
-
//
|
|
351
|
-
// assigned), the effect phase sweeps the
|
|
363
|
+
// linkState derives from (with the plugin, its pending read — the
|
|
364
|
+
// in-flight target is readable in the effect phase because the isRouting
|
|
365
|
+
// write flushes after the target is assigned), the effect phase sweeps the
|
|
366
|
+
// registry untracked.
|
|
352
367
|
//
|
|
353
368
|
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
354
369
|
// This setup is client-only, so an id-consuming node here has no server
|
|
@@ -356,7 +371,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
356
371
|
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
357
372
|
// unclaimed. (The option is honored by the runtime but missing from the
|
|
358
373
|
// published EffectOptions type, hence the cast.)
|
|
359
|
-
createRenderEffect(() => (router.location.pathname, router.location.search,
|
|
374
|
+
createRenderEffect(() => (router.location.pathname, router.location.search, plugin && plugin.track()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
|
|
360
375
|
transparent: true
|
|
361
376
|
});
|
|
362
377
|
onCleanup(registerElementClaim(node => {
|
|
@@ -383,6 +398,12 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
383
398
|
}));
|
|
384
399
|
}
|
|
385
400
|
|
|
401
|
+
/**
|
|
402
|
+
* App bundlers define this (`"true"` when server components are on, including
|
|
403
|
+
* `'external'`, otherwise `"false"`). Not assigned here: the published build
|
|
404
|
+
* keeps the `typeof` check so they can fold the fallback away.
|
|
405
|
+
*/
|
|
406
|
+
|
|
386
407
|
/**
|
|
387
408
|
* The submit delegation consults this slot instead of importing the action
|
|
388
409
|
* module: the action side installs its handler on first action creation
|
|
@@ -394,24 +415,43 @@ let formHandler;
|
|
|
394
415
|
function setRouterFormHandler(handler) {
|
|
395
416
|
formHandler = handler;
|
|
396
417
|
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Link preload strategies reach route preloading through this slot instead
|
|
421
|
+
* of the event wiring importing it: a strategy installs it when it runs (see
|
|
422
|
+
* preload.ts), so an app with no strategy and no `usePreloadRoute` never
|
|
423
|
+
* ships route preloading.
|
|
424
|
+
*/
|
|
425
|
+
let linkPreloader;
|
|
426
|
+
function setLinkPreloader(preloader) {
|
|
427
|
+
linkPreloader = preloader;
|
|
428
|
+
}
|
|
397
429
|
function setupNativeEvents({
|
|
398
|
-
preload
|
|
430
|
+
preload,
|
|
399
431
|
explicitLinks = false,
|
|
400
|
-
actionBase = "/_server"
|
|
401
|
-
transformUrl
|
|
432
|
+
actionBase = "/_server"
|
|
402
433
|
} = {}) {
|
|
403
434
|
return router => {
|
|
404
435
|
const basePath = router.base.path();
|
|
405
436
|
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
406
|
-
let preloadTimeout;
|
|
407
|
-
let lastElement;
|
|
408
437
|
function isSvg(el) {
|
|
409
438
|
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
410
439
|
}
|
|
411
440
|
function handleAnchor(evt) {
|
|
412
441
|
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
442
|
+
return findAnchor(evt);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// no button or modifier gate: focus and touch events carry neither
|
|
446
|
+
function findAnchor(evt) {
|
|
413
447
|
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
414
|
-
|
|
448
|
+
const url = a && anchorUrl(a);
|
|
449
|
+
return url && [a, url];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The anchor's URL when the router manages it, else `undefined`. */
|
|
453
|
+
function anchorUrl(a) {
|
|
454
|
+
if (explicitLinks && !a.hasAttribute("link")) return;
|
|
415
455
|
const svg = isSvg(a);
|
|
416
456
|
const href = svg ? a.href.baseVal : a.href;
|
|
417
457
|
const target = svg ? a.target.baseVal : a.target;
|
|
@@ -423,7 +463,7 @@ function setupNativeEvents({
|
|
|
423
463
|
// inherit the page origin, so the origin check below won't reject them. #382
|
|
424
464
|
if (url.protocol !== "https:" && url.protocol !== "http:") return;
|
|
425
465
|
if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
|
|
426
|
-
return
|
|
466
|
+
return url;
|
|
427
467
|
}
|
|
428
468
|
function handleAnchorClick(evt) {
|
|
429
469
|
const res = handleAnchor(evt);
|
|
@@ -432,31 +472,14 @@ function setupNativeEvents({
|
|
|
432
472
|
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
433
473
|
const state = a.getAttribute("state");
|
|
434
474
|
evt.preventDefault();
|
|
435
|
-
|
|
475
|
+
// Only a click the router acts on joins the event's interaction: this
|
|
476
|
+
// listener hears every click on the document.
|
|
477
|
+
dispatchAsInteraction(evt, () => navigateFromRoute(to, {
|
|
436
478
|
resolve: false,
|
|
437
479
|
replace: a.hasAttribute("replace"),
|
|
438
480
|
scroll: !a.hasAttribute("noscroll"),
|
|
439
481
|
state: state ? JSON.parse(state) : undefined
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
function handleAnchorPreload(evt) {
|
|
443
|
-
const res = handleAnchor(evt);
|
|
444
|
-
if (!res) return;
|
|
445
|
-
const [a, url] = res;
|
|
446
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
447
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
448
|
-
}
|
|
449
|
-
function handleAnchorMove(evt) {
|
|
450
|
-
clearTimeout(preloadTimeout);
|
|
451
|
-
const res = handleAnchor(evt);
|
|
452
|
-
if (!res) return lastElement = null;
|
|
453
|
-
const [a, url] = res;
|
|
454
|
-
if (lastElement === a) return;
|
|
455
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
456
|
-
preloadTimeout = setTimeout(() => {
|
|
457
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
458
|
-
lastElement = a;
|
|
459
|
-
}, 20);
|
|
482
|
+
}));
|
|
460
483
|
}
|
|
461
484
|
function handleFormSubmit(evt) {
|
|
462
485
|
if (formHandler) return formHandler(evt, router, actionBase);
|
|
@@ -473,35 +496,31 @@ function setupNativeEvents({
|
|
|
473
496
|
const url = new URL(ref, document.baseURI);
|
|
474
497
|
const path = router.parsePath(url.pathname + url.search);
|
|
475
498
|
if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST") return;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
499
|
+
// The import sits inside the check so bundlers drop the server-forms
|
|
500
|
+
// chunk. An early return above a still-present import() does not.
|
|
501
|
+
// Missing stays off: an undefined constant does not fold.
|
|
502
|
+
if (typeof __SOLID_SERVER_COMPONENTS__ !== "undefined" && __SOLID_SERVER_COMPONENTS__) {
|
|
503
|
+
evt.preventDefault();
|
|
504
|
+
const data = new FormData(form, evt.submitter);
|
|
505
|
+
Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
|
|
506
|
+
}
|
|
479
507
|
}
|
|
508
|
+
const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
480
509
|
|
|
481
510
|
// ensure delegated event run first
|
|
482
511
|
delegateEvents(["click", "submit"]);
|
|
483
512
|
document.addEventListener("click", handleAnchorClick);
|
|
484
|
-
|
|
485
|
-
document.addEventListener("mousemove", handleAnchorMove, {
|
|
486
|
-
passive: true
|
|
487
|
-
});
|
|
488
|
-
document.addEventListener("focusin", handleAnchorPreload, {
|
|
489
|
-
passive: true
|
|
490
|
-
});
|
|
491
|
-
document.addEventListener("touchstart", handleAnchorPreload, {
|
|
492
|
-
passive: true
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
document.addEventListener("submit", handleFormSubmit);
|
|
513
|
+
document.addEventListener("submit", handleSubmit);
|
|
496
514
|
onCleanup(() => {
|
|
497
515
|
document.removeEventListener("click", handleAnchorClick);
|
|
498
|
-
|
|
499
|
-
document.removeEventListener("mousemove", handleAnchorMove);
|
|
500
|
-
document.removeEventListener("focusin", handleAnchorPreload);
|
|
501
|
-
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
502
|
-
}
|
|
503
|
-
document.removeEventListener("submit", handleFormSubmit);
|
|
516
|
+
document.removeEventListener("submit", handleSubmit);
|
|
504
517
|
});
|
|
518
|
+
// the pre-strategy boolean option is ignored (a dev warning names it)
|
|
519
|
+
if (preload && preload !== true) [].concat(preload).forEach(strategy => strategy({
|
|
520
|
+
anchor: findAnchor,
|
|
521
|
+
url: anchorUrl,
|
|
522
|
+
preload: (url, data) => linkPreloader(router, url, data)
|
|
523
|
+
}));
|
|
505
524
|
};
|
|
506
525
|
}
|
|
507
526
|
|
|
@@ -610,7 +629,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
610
629
|
|
|
611
630
|
// The light half of server component routes: the brand the router core reads
|
|
612
631
|
// off a route `component`, and the derivation of a route's call arguments.
|
|
613
|
-
// Deliberately free of `query`/`
|
|
632
|
+
// Deliberately free of `query`/`dynamicComponent` — routing.ts imports this module,
|
|
614
633
|
// and routing.ts is in every app's eager graph. The heavy half (the helper
|
|
615
634
|
// that builds the branded component) lives in serverRouteComponent.ts and only
|
|
616
635
|
// enters a bundle when an app calls `serverRouteComponent()`.
|
|
@@ -674,6 +693,86 @@ function serverRouteArgsEqual(a, b) {
|
|
|
674
693
|
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
675
694
|
}
|
|
676
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
698
|
+
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
699
|
+
* and `latest` are UI affordances — the router's own coordination reads its
|
|
700
|
+
* location writes and their `onSettled` instead — so an app that renders
|
|
701
|
+
* none of these never bundles Solid's verdict machinery.
|
|
702
|
+
*/
|
|
703
|
+
|
|
704
|
+
const routing = new WeakMap();
|
|
705
|
+
|
|
706
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
707
|
+
function routingState(router) {
|
|
708
|
+
let read = routing.get(router);
|
|
709
|
+
if (!read) {
|
|
710
|
+
const {
|
|
711
|
+
location,
|
|
712
|
+
matches,
|
|
713
|
+
_source: source
|
|
714
|
+
} = router;
|
|
715
|
+
// `transparent`: created on first use rather than at router setup, so it
|
|
716
|
+
// must not take a hydration id — the server may never create it, or
|
|
717
|
+
// create it at a different point.
|
|
718
|
+
const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
|
|
719
|
+
try {
|
|
720
|
+
matches();
|
|
721
|
+
} catch (e) {
|
|
722
|
+
if (e instanceof NotReadyError) throw e;
|
|
723
|
+
}
|
|
724
|
+
location.search;
|
|
725
|
+
location.hash;
|
|
726
|
+
}), {
|
|
727
|
+
transparent: true,
|
|
728
|
+
...(DEV && {
|
|
729
|
+
name: "routingPending"
|
|
730
|
+
})
|
|
731
|
+
}));
|
|
732
|
+
read = () => pending() || isPending(source);
|
|
733
|
+
routing.set(router, read);
|
|
734
|
+
}
|
|
735
|
+
return read;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
740
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
741
|
+
*/
|
|
742
|
+
function pendingTarget(router) {
|
|
743
|
+
if (!routingState(router)()) return;
|
|
744
|
+
const target = latest(router._source);
|
|
745
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
749
|
+
function linkPending(router, to, base, end) {
|
|
750
|
+
const target = pendingTarget(router);
|
|
751
|
+
return !!target && linkMatcher({
|
|
752
|
+
pathname: target.value,
|
|
753
|
+
search: ""
|
|
754
|
+
}, base, end)(to).active;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
759
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
760
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
761
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* ```ts
|
|
765
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
766
|
+
*
|
|
767
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
768
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
const pendingLinks = (router, base) => ({
|
|
772
|
+
track: routingState(router),
|
|
773
|
+
pending: target => linkPending(router, target, base)
|
|
774
|
+
});
|
|
775
|
+
|
|
677
776
|
const MAX_REDIRECTS = 100;
|
|
678
777
|
|
|
679
778
|
/**
|
|
@@ -759,6 +858,8 @@ const useLocation = () => useRouter().location;
|
|
|
759
858
|
/**
|
|
760
859
|
* Retrieves a signal that indicates whether the router is currently processing a navigation.
|
|
761
860
|
* Useful for showing pending navigation state while the next route and its data settle.
|
|
861
|
+
* This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
|
|
862
|
+
* For the in-flight destination, read the location with Solid's `isPending`/`latest`.
|
|
762
863
|
*
|
|
763
864
|
* @example
|
|
764
865
|
* ```js
|
|
@@ -771,7 +872,7 @@ const useLocation = () => useRouter().location;
|
|
|
771
872
|
* );
|
|
772
873
|
* ```
|
|
773
874
|
*/
|
|
774
|
-
const useIsRouting = () => useRouter()
|
|
875
|
+
const useIsRouting = () => routingState(useRouter());
|
|
775
876
|
|
|
776
877
|
/**
|
|
777
878
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
@@ -825,23 +926,6 @@ const useRouteMatches = () => {
|
|
|
825
926
|
return () => router.matches().slice();
|
|
826
927
|
};
|
|
827
928
|
|
|
828
|
-
/**
|
|
829
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
830
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
831
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
832
|
-
*
|
|
833
|
-
* @example
|
|
834
|
-
* ```js
|
|
835
|
-
* const preload = usePreloadRoute();
|
|
836
|
-
*
|
|
837
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
838
|
-
* ```
|
|
839
|
-
*/
|
|
840
|
-
const usePreloadRoute = () => {
|
|
841
|
-
const pre = useRouter().preloadRoute;
|
|
842
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
843
|
-
};
|
|
844
|
-
|
|
845
929
|
/**
|
|
846
930
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
847
931
|
*
|
|
@@ -950,7 +1034,7 @@ const useLinkState = (href, options = {}) => {
|
|
|
950
1034
|
const location = router.location;
|
|
951
1035
|
const to = useResolvedPath(() => String(href()));
|
|
952
1036
|
const base = router.base.path();
|
|
953
|
-
const state = createMemo(() =>
|
|
1037
|
+
const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
|
|
954
1038
|
return {
|
|
955
1039
|
active: createMemo(() => state().active),
|
|
956
1040
|
current: createMemo(() => state().current),
|
|
@@ -959,10 +1043,7 @@ const useLinkState = (href, options = {}) => {
|
|
|
959
1043
|
// observe the committed location during a transition
|
|
960
1044
|
pending: createMemo(() => {
|
|
961
1045
|
state(); // location dependency: mid-flight target swaps recompute
|
|
962
|
-
return router
|
|
963
|
-
pathname: router.pendingTarget.value,
|
|
964
|
-
search: ""
|
|
965
|
-
}, to(), base, options.end).active;
|
|
1046
|
+
return linkPending(router, to(), base, options.end);
|
|
966
1047
|
})
|
|
967
1048
|
};
|
|
968
1049
|
};
|
|
@@ -1292,6 +1373,11 @@ let preloadIntent;
|
|
|
1292
1373
|
function getIntent() {
|
|
1293
1374
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
1294
1375
|
}
|
|
1376
|
+
function setPreloadIntent(intent) {
|
|
1377
|
+
const prev = preloadIntent;
|
|
1378
|
+
preloadIntent = intent;
|
|
1379
|
+
return prev;
|
|
1380
|
+
}
|
|
1295
1381
|
let inPreloadFn = false;
|
|
1296
1382
|
function getInPreloadFn() {
|
|
1297
1383
|
return inPreloadFn;
|
|
@@ -1320,7 +1406,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1320
1406
|
_navigation: isServer ? undefined : 1
|
|
1321
1407
|
});
|
|
1322
1408
|
}
|
|
1323
|
-
|
|
1409
|
+
|
|
1410
|
+
// The flushed world — what `latest(source)` answers — as the location's own
|
|
1411
|
+
// memo sees it: it recomputes under the transition that carries a write,
|
|
1412
|
+
// never for a write of this same tick (A28).
|
|
1413
|
+
let flushed = initialSource;
|
|
1414
|
+
const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
|
|
1324
1415
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
1325
1416
|
// must be appended before streaming flushes the response headers, and an
|
|
1326
1417
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -1397,7 +1488,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1397
1488
|
// NotReadyError's source must be a reactive async node, not the raw
|
|
1398
1489
|
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
1399
1490
|
// is delivered through the graph's error channel and resolution wakes the
|
|
1400
|
-
// parked matches computation.
|
|
1491
|
+
// parked matches computation. `transparent`: the reader exists on the
|
|
1492
|
+
// client only (the server parks `matches` on the promise directly), so it
|
|
1493
|
+
// must not take a hydration id the server never allocated.
|
|
1401
1494
|
const routerOwner = getOwner();
|
|
1402
1495
|
const lazyReaders = new WeakMap();
|
|
1403
1496
|
const readLazySubtree = record => {
|
|
@@ -1406,16 +1499,19 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1406
1499
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1407
1500
|
const result = resolveLazySubtree(record);
|
|
1408
1501
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1409
|
-
},
|
|
1410
|
-
|
|
1502
|
+
}, {
|
|
1503
|
+
transparent: true,
|
|
1504
|
+
...(DEV && {
|
|
1505
|
+
name: "lazyRoutes"
|
|
1506
|
+
})
|
|
1411
1507
|
}));
|
|
1412
1508
|
lazyReaders.set(record, read);
|
|
1413
1509
|
}
|
|
1414
1510
|
return read();
|
|
1415
1511
|
};
|
|
1512
|
+
const matchPath = pathname => getRouteMatches(branches(), options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
1416
1513
|
const matches = createMemo(() => {
|
|
1417
|
-
const
|
|
1418
|
-
const m = getRouteMatches(branches(), pathname);
|
|
1514
|
+
const m = matchPath(location.pathname);
|
|
1419
1515
|
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
1420
1516
|
// navigation transition (or the SSR stream) holds until the table lands.
|
|
1421
1517
|
// NotReadyError (not a returned promise) because a match chain is full
|
|
@@ -1442,28 +1538,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1442
1538
|
}, DEV && {
|
|
1443
1539
|
name: "matches"
|
|
1444
1540
|
});
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
} catch (e) {
|
|
1449
|
-
if (e instanceof NotReadyError) throw e;
|
|
1450
|
-
}
|
|
1451
|
-
location.search;
|
|
1452
|
-
location.hash;
|
|
1453
|
-
}), DEV && {
|
|
1454
|
-
name: "routingPending"
|
|
1455
|
-
});
|
|
1456
|
-
const isRouting = () => routingPending() || isPending(source);
|
|
1541
|
+
const headedLocation = () => isServer ? untrack(source) : flushed;
|
|
1542
|
+
// A flushed navigation write whose settle has not run is still in flight.
|
|
1543
|
+
const unsettled = change => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
|
|
1457
1544
|
const transitionIntent = () => {
|
|
1458
|
-
|
|
1459
|
-
|
|
1545
|
+
const head = headedLocation();
|
|
1546
|
+
if (!unsettled(head)) return;
|
|
1547
|
+
const navigation = head._navigation;
|
|
1460
1548
|
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
1461
1549
|
};
|
|
1462
|
-
const pendingNavigation = () => {
|
|
1463
|
-
if (!isRouting()) return;
|
|
1464
|
-
const target = latest(source);
|
|
1465
|
-
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
1466
|
-
};
|
|
1467
1550
|
const buildParams = () => mergeParams(matches());
|
|
1468
1551
|
const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
|
|
1469
1552
|
const params = wrapParams(buildParams);
|
|
@@ -1481,17 +1564,16 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1481
1564
|
location,
|
|
1482
1565
|
params,
|
|
1483
1566
|
wrapParams,
|
|
1484
|
-
isRouting,
|
|
1485
1567
|
intent: transitionIntent,
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1568
|
+
_source: source,
|
|
1569
|
+
_owner: routerOwner,
|
|
1570
|
+
_routeOwner: getContext,
|
|
1571
|
+
_match: matchPath,
|
|
1489
1572
|
renderPath,
|
|
1490
1573
|
parsePath,
|
|
1491
1574
|
navigatorFactory,
|
|
1492
1575
|
matches,
|
|
1493
1576
|
beforeLeave,
|
|
1494
|
-
preloadRoute,
|
|
1495
1577
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
1496
1578
|
get submissions() {
|
|
1497
1579
|
return submissions ||= createSignal(isServer ? initSubmissions() : [], {
|
|
@@ -1546,10 +1628,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1546
1628
|
// the destination the leave guard is told all read it. A write of this
|
|
1547
1629
|
// same tick is not in it (A28) — see `compose` below.
|
|
1548
1630
|
//
|
|
1549
|
-
// A redirect hop: the previous navigation
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
const headed =
|
|
1631
|
+
// A redirect hop: the previous navigation's write has not settled — it
|
|
1632
|
+
// is still held, or a guard is redirecting in its landing flush — so its
|
|
1633
|
+
// destination was never shown.
|
|
1634
|
+
const headed = headedLocation();
|
|
1553
1635
|
|
|
1554
1636
|
// A composed target (`setSearchParams`) is a function of where the
|
|
1555
1637
|
// router is heading, an unflushed write of this tick included. Only
|
|
@@ -1579,7 +1661,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1579
1661
|
resolvedTo = resolveTarget(to);
|
|
1580
1662
|
if (resolvedTo === undefined) throw unroutable(to);
|
|
1581
1663
|
}
|
|
1582
|
-
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && (
|
|
1664
|
+
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && unsettled(headed) ? headed._navigation : 0;
|
|
1583
1665
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
1584
1666
|
throw new Error("Too many redirects");
|
|
1585
1667
|
}
|
|
@@ -1621,71 +1703,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1621
1703
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
1622
1704
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1623
1705
|
}
|
|
1624
|
-
function preloadRoute(url, preloadData) {
|
|
1625
|
-
const next = getRouteMatches(branches(), url.pathname);
|
|
1626
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1627
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
1628
|
-
// preload again so the real inner routes warm too. Preloads are
|
|
1629
|
-
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
1630
|
-
// here — the real navigation surfaces and retries it.
|
|
1631
|
-
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1632
|
-
if (boundary) {
|
|
1633
|
-
try {
|
|
1634
|
-
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1635
|
-
} catch {}
|
|
1636
|
-
}
|
|
1637
|
-
// Data preloads run only for levels a navigation would mount fresh or
|
|
1638
|
-
// reuse with changed inputs: this level's params, and search as the
|
|
1639
|
-
// declared schema's output or else the raw string. Navigation itself is
|
|
1640
|
-
// already this selective (a matching level is reused and re-reads through
|
|
1641
|
-
// tracked params), so an unchanged level has nothing new to warm.
|
|
1642
|
-
let current;
|
|
1643
|
-
try {
|
|
1644
|
-
current = untrack(matches);
|
|
1645
|
-
} catch {}
|
|
1646
|
-
const query = extractSearchParams(url);
|
|
1647
|
-
const inputs = (p, q, s, r) => {
|
|
1648
|
-
const a = serverRouteArgs(r, p, q);
|
|
1649
|
-
a.search === undefined && (a.search = s);
|
|
1650
|
-
return a;
|
|
1651
|
-
};
|
|
1652
|
-
const prevIntent = preloadIntent;
|
|
1653
|
-
preloadIntent = "preload";
|
|
1654
|
-
for (let match in next) {
|
|
1655
|
-
const {
|
|
1656
|
-
route,
|
|
1657
|
-
params
|
|
1658
|
-
} = next[match];
|
|
1659
|
-
const {
|
|
1660
|
-
preload,
|
|
1661
|
-
component
|
|
1662
|
-
} = route;
|
|
1663
|
-
component?.preload?.();
|
|
1664
|
-
const now = current && current[match];
|
|
1665
|
-
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1666
|
-
inPreloadFn = true;
|
|
1667
|
-
preloadData && !unchanged && runWithOwner(getContext(), () => {
|
|
1668
|
-
// A server component route's data IS its call: warm the same
|
|
1669
|
-
// query entry the render will read, under the same derived args.
|
|
1670
|
-
const server = serverRouteOf(component);
|
|
1671
|
-
server && server.call(serverRouteArgs(route, params, query));
|
|
1672
|
-
preload && preload({
|
|
1673
|
-
params,
|
|
1674
|
-
location: {
|
|
1675
|
-
pathname: url.pathname,
|
|
1676
|
-
search: url.search,
|
|
1677
|
-
hash: url.hash,
|
|
1678
|
-
query,
|
|
1679
|
-
state: null,
|
|
1680
|
-
key: ""
|
|
1681
|
-
},
|
|
1682
|
-
intent: "preload"
|
|
1683
|
-
});
|
|
1684
|
-
});
|
|
1685
|
-
inPreloadFn = false;
|
|
1686
|
-
}
|
|
1687
|
-
preloadIntent = prevIntent;
|
|
1688
|
-
}
|
|
1689
1706
|
|
|
1690
1707
|
// Seeds the initial submission from a no-JS form post: the server
|
|
1691
1708
|
// function runtime redirected back with the outcome in a one-shot flash
|
|
@@ -1833,9 +1850,12 @@ function Routes(props) {
|
|
|
1833
1850
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
1834
1851
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
1835
1852
|
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
1836
|
-
// are created under
|
|
1837
|
-
// computation (which disposes its children every time it re-runs).
|
|
1838
|
-
|
|
1853
|
+
// are created under an owner of this component rather than the memo's
|
|
1854
|
+
// computation (which disposes its children every time it re-runs). A
|
|
1855
|
+
// dedicated one, created before the memo: the roots draw hydration ids from
|
|
1856
|
+
// its counter, so the ids don't depend on whether the memo first parked on
|
|
1857
|
+
// a lazy subtree (the server's render can park where the client's doesn't).
|
|
1858
|
+
const owner = createOwner();
|
|
1839
1859
|
const routeStates = createMemo(prev => {
|
|
1840
1860
|
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
1841
1861
|
// computation parks with it — no route contexts are created against
|
|
@@ -2127,6 +2147,7 @@ function memoryHistory(initial = "/") {
|
|
|
2127
2147
|
}
|
|
2128
2148
|
|
|
2129
2149
|
const STORAGE_KEY = "solid-router:scroll";
|
|
2150
|
+
const AUTO_KEY = "solid-router:scroll-auto";
|
|
2130
2151
|
|
|
2131
2152
|
/**
|
|
2132
2153
|
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
@@ -2141,7 +2162,7 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2141
2162
|
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
2142
2163
|
* otherwise disables.
|
|
2143
2164
|
*
|
|
2144
|
-
* Restoration is a single scroll once
|
|
2165
|
+
* Restoration is a single scroll once the traversal settles — the same strategy
|
|
2145
2166
|
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
2146
2167
|
* transition commits is what makes the offset reachable; chasing a still-
|
|
2147
2168
|
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
@@ -2151,17 +2172,49 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2151
2172
|
* layout changes can feed it back into itself. Content that commits after the
|
|
2152
2173
|
* transition settles — an image without reserved space, a boundary below the
|
|
2153
2174
|
* fold — keeps whatever offset the document can hold.
|
|
2175
|
+
*
|
|
2176
|
+
* A server-rendered document load is the exception: the browser restores it
|
|
2177
|
+
* natively once the document — streamed boundaries included — has loaded,
|
|
2178
|
+
* which a restore at the first client flush cannot match. So an entry is
|
|
2179
|
+
* handed back to the browser (`auto`) whenever its document may unload
|
|
2180
|
+
* (pagehide), and a hydrating router arriving on a handed-back entry leaves
|
|
2181
|
+
* the restore to the browser: it does not scroll, and it takes the entry back
|
|
2182
|
+
* (`manual`) only after load or at the first client navigation, whichever
|
|
2183
|
+
* comes first. A programmatic scroll during the load cancels the native
|
|
2184
|
+
* restore in Firefox and WebKit, and WebKit restores just after the load
|
|
2185
|
+
* event, so a `manual` write before then — inside a load listener included —
|
|
2186
|
+
* suppresses it; the write waits a task. Handed-back depths are tracked here
|
|
2187
|
+
* rather than read from `history.scrollRestoration`, which Firefox reports as
|
|
2188
|
+
* the mode before pagehide after a reload. An entry this router pushed and
|
|
2189
|
+
* never handed back stays `manual` and gets no native restore, so the router
|
|
2190
|
+
* restores it as on a client-rendered load.
|
|
2154
2191
|
*/
|
|
2155
|
-
function createScrollRestoration() {
|
|
2156
|
-
window.history
|
|
2192
|
+
function createScrollRestoration(hydrating) {
|
|
2193
|
+
const h = window.history;
|
|
2157
2194
|
// the current entry needs its depth stamp for captures to have a key, even
|
|
2158
2195
|
// if something replaced history.state after the adapter stamped it
|
|
2159
2196
|
saveCurrentDepth();
|
|
2197
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
2160
2198
|
let positions = {};
|
|
2199
|
+
let handedBack = {};
|
|
2161
2200
|
try {
|
|
2162
2201
|
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
2202
|
+
handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
|
|
2163
2203
|
} catch {}
|
|
2164
|
-
const
|
|
2204
|
+
const manual = () => {
|
|
2205
|
+
h.scrollRestoration = "manual";
|
|
2206
|
+
const d = depth();
|
|
2207
|
+
if (d != null) delete handedBack[d];
|
|
2208
|
+
};
|
|
2209
|
+
const d = depth();
|
|
2210
|
+
let deferred = !!hydrating && (h.scrollRestoration === "auto" || d != null && !!handedBack[d]);
|
|
2211
|
+
if (!deferred) manual();
|
|
2212
|
+
const claim = () => {
|
|
2213
|
+
if (!deferred) return;
|
|
2214
|
+
deferred = false;
|
|
2215
|
+
manual();
|
|
2216
|
+
};
|
|
2217
|
+
let timer;
|
|
2165
2218
|
let programmatic = false;
|
|
2166
2219
|
let pending;
|
|
2167
2220
|
const unbind = [bindEvent(window, "scroll", () => {
|
|
@@ -2170,10 +2223,16 @@ function createScrollRestoration() {
|
|
|
2170
2223
|
// the user took over — a pending restore would yank them
|
|
2171
2224
|
if (!programmatic) pending = undefined;
|
|
2172
2225
|
}), bindEvent(window, "pagehide", () => {
|
|
2226
|
+
h.scrollRestoration = "auto";
|
|
2227
|
+
const d = depth();
|
|
2228
|
+
if (d != null) handedBack[d] = 1;
|
|
2173
2229
|
try {
|
|
2174
2230
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
2231
|
+
sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
|
|
2175
2232
|
} catch {}
|
|
2176
|
-
})
|
|
2233
|
+
}), bindEvent(window, "pageshow", e => {
|
|
2234
|
+
if (e.persisted) manual();
|
|
2235
|
+
}), bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))), () => clearTimeout(timer)];
|
|
2177
2236
|
const restore = () => {
|
|
2178
2237
|
if (pending == null) return;
|
|
2179
2238
|
const y = positions[pending];
|
|
@@ -2186,42 +2245,38 @@ function createScrollRestoration() {
|
|
|
2186
2245
|
programmatic = false;
|
|
2187
2246
|
};
|
|
2188
2247
|
return {
|
|
2189
|
-
/**
|
|
2248
|
+
/**
|
|
2249
|
+
* When the adapter notifies a traversal: mark the target and restore once
|
|
2250
|
+
* the transition carrying it settles. The adapter notifies right before
|
|
2251
|
+
* the location write, in the same tick, so the settle is that write's —
|
|
2252
|
+
* a same-URL traversal included, which no location key would see.
|
|
2253
|
+
*/
|
|
2190
2254
|
onPop() {
|
|
2255
|
+
claim();
|
|
2191
2256
|
pending = depth();
|
|
2257
|
+
runWithOwner(null, () => onSettled(restore));
|
|
2192
2258
|
},
|
|
2259
|
+
/** The initial page settled: restore a reload/back_forward arrival. */
|
|
2260
|
+
settled: () => restore(),
|
|
2261
|
+
/** Before a client navigation writes history: the entry it creates copies the current mode. */
|
|
2262
|
+
beforeWrite: claim,
|
|
2193
2263
|
/** After a push: forward entries died, and this depth may be reused. */
|
|
2194
2264
|
onPush() {
|
|
2195
2265
|
const d = depth();
|
|
2196
|
-
if (d != null)
|
|
2266
|
+
if (d != null) {
|
|
2267
|
+
for (const k in positions) +k >= d && delete positions[k];
|
|
2268
|
+
for (const k in handedBack) +k >= d && delete handedBack[k];
|
|
2269
|
+
}
|
|
2197
2270
|
},
|
|
2198
|
-
create(
|
|
2199
|
-
// Restore once the traversal has settled: key on the location (a fully
|
|
2200
|
-
// synchronous pop commits without isRouting ever flipping) and on
|
|
2201
|
-
// isRouting, which reports in-flight transitions — native pops
|
|
2202
|
-
// included — and holds the restore until they commit. restore() no-ops
|
|
2203
|
-
// unless a traversal marked a target, so push navigations are inert.
|
|
2204
|
-
// `transparent` keeps the effect invisible to the hydration id scheme —
|
|
2205
|
-
// same reasoning as the link-claims effect (claims.ts): this setup is
|
|
2206
|
-
// client-only, so an id-consuming node here has no server counterpart
|
|
2207
|
-
// and every hydration id allocated after it shifts by one child slot.
|
|
2208
|
-
// The visible failure is any <Loading> content that settled before the
|
|
2209
|
-
// shell flush (a cache hit, a preloaded query): its serialized value and
|
|
2210
|
-
// inlined markup are keyed under the server's ids, the shifted client
|
|
2211
|
-
// misses both, recomputes, and re-renders the route fresh — duplicating
|
|
2212
|
-
// the server DOM and leaving it inert.
|
|
2213
|
-
createEffect(() => ({
|
|
2214
|
-
url: router.location.pathname + router.location.search + router.location.hash,
|
|
2215
|
-
routing: router.isRouting()
|
|
2216
|
-
}), current => {
|
|
2217
|
-
if (!current.routing) restore();
|
|
2218
|
-
}, {
|
|
2219
|
-
transparent: true
|
|
2220
|
-
});
|
|
2271
|
+
create() {
|
|
2221
2272
|
onCleanup(() => unbind.forEach(u => u()));
|
|
2222
2273
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
2223
|
-
// navigation starts a new one and belongs at the top); the
|
|
2224
|
-
// initial
|
|
2274
|
+
// navigation starts a new one and belongs at the top); the router calls
|
|
2275
|
+
// `settled` once the initial page has; a deferred arrival is the browser's
|
|
2276
|
+
if (deferred) {
|
|
2277
|
+
document.readyState === "complete" && claim();
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2225
2280
|
const [nav] = performance.getEntriesByType?.("navigation");
|
|
2226
2281
|
if (nav && nav.type !== "navigate") pending = depth();
|
|
2227
2282
|
}
|
|
@@ -2237,6 +2292,7 @@ function withScrollRestoration(history, restoration) {
|
|
|
2237
2292
|
return {
|
|
2238
2293
|
...history,
|
|
2239
2294
|
set(next) {
|
|
2295
|
+
restoration.beforeWrite();
|
|
2240
2296
|
history.set(next);
|
|
2241
2297
|
next.replace || restoration.onPush();
|
|
2242
2298
|
},
|
|
@@ -2384,8 +2440,8 @@ function describeInitial(match, location) {
|
|
|
2384
2440
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2385
2441
|
function createIntegration(history, match) {
|
|
2386
2442
|
let committing = false;
|
|
2387
|
-
//
|
|
2388
|
-
|
|
2443
|
+
// Writes whose transition has landed (see `RouterIntegration.settled`).
|
|
2444
|
+
const settled = new WeakSet();
|
|
2389
2445
|
const wrap = value => typeof value === "string" ? {
|
|
2390
2446
|
value
|
|
2391
2447
|
} : value;
|
|
@@ -2404,14 +2460,16 @@ function createIntegration(history, match) {
|
|
|
2404
2460
|
// the no-op rule compares against it, so `navigate()` behind another
|
|
2405
2461
|
// write in one handler sees that write rather than the flushed world.
|
|
2406
2462
|
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
2407
|
-
if (written && written._navigation
|
|
2463
|
+
if (written && written._navigation) {
|
|
2408
2464
|
const next = written;
|
|
2409
|
-
inflight = next;
|
|
2410
2465
|
// Register out of band so a destination error boundary replacing the
|
|
2411
|
-
// Router subtree cannot suppress the winning history commit.
|
|
2466
|
+
// Router subtree cannot suppress the winning history commit. The
|
|
2467
|
+
// settle is tied to the transition carrying the write: it fires when
|
|
2468
|
+
// that lands, a rejection included, and a superseded write's settle
|
|
2469
|
+
// fires with the transition that absorbed it.
|
|
2412
2470
|
runWithOwner(null, () => onSettled(() => {
|
|
2413
|
-
|
|
2414
|
-
if (read() !== next) return;
|
|
2471
|
+
settled.add(next);
|
|
2472
|
+
if (next._navigation < 0 || read() !== next) return;
|
|
2415
2473
|
committing = true;
|
|
2416
2474
|
try {
|
|
2417
2475
|
history.set(next);
|
|
@@ -2445,7 +2503,7 @@ function createIntegration(history, match) {
|
|
|
2445
2503
|
}));
|
|
2446
2504
|
return {
|
|
2447
2505
|
signal,
|
|
2448
|
-
|
|
2506
|
+
settled: write => settled.has(write),
|
|
2449
2507
|
utils: history.utils
|
|
2450
2508
|
};
|
|
2451
2509
|
}
|
|
@@ -2478,6 +2536,9 @@ function staticIntegration(url, utils) {
|
|
|
2478
2536
|
};
|
|
2479
2537
|
}
|
|
2480
2538
|
function createRouter(config) {
|
|
2539
|
+
if (DEV && typeof config.preloadLinks === "boolean") {
|
|
2540
|
+
console.warn("`preloadLinks` takes preload strategies, not a boolean, and preloads nothing by default. " + "Use `preloadLinks: intentPreload()` for hover/focus preloading.");
|
|
2541
|
+
}
|
|
2481
2542
|
const basePath = config.base || "";
|
|
2482
2543
|
// Routes are immutable per instance, so compilation is shared by every
|
|
2483
2544
|
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
@@ -2510,7 +2571,7 @@ function createRouter(config) {
|
|
|
2510
2571
|
let restoration;
|
|
2511
2572
|
let history = config.history;
|
|
2512
2573
|
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
2513
|
-
restoration = createScrollRestoration();
|
|
2574
|
+
restoration = createScrollRestoration(isHydrating());
|
|
2514
2575
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
2515
2576
|
}
|
|
2516
2577
|
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
@@ -2527,13 +2588,16 @@ function createRouter(config) {
|
|
|
2527
2588
|
setupNativeEvents({
|
|
2528
2589
|
preload: config.preloadLinks,
|
|
2529
2590
|
explicitLinks: config.explicitLinks,
|
|
2530
|
-
actionBase: config.actionBase
|
|
2531
|
-
transformUrl: config.transformUrl
|
|
2591
|
+
actionBase: config.actionBase
|
|
2532
2592
|
})(routerState);
|
|
2533
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
2593
|
+
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
2534
2594
|
if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
|
|
2535
|
-
restoration && restoration.create(
|
|
2595
|
+
restoration && restoration.create();
|
|
2536
2596
|
}
|
|
2597
|
+
// Registered on both sides, outside the client-only branch: an owned
|
|
2598
|
+
// onSettled takes a hydration id on the server too, so the ids line up.
|
|
2599
|
+
// A no-op when the arrival is left to the browser's native restore.
|
|
2600
|
+
onSettled(() => restoration && restoration.settled());
|
|
2537
2601
|
return createComponent$1(RouterContextObj, {
|
|
2538
2602
|
value: routerState,
|
|
2539
2603
|
get children() {
|
|
@@ -2670,6 +2734,289 @@ const useBeforeLeave = listener => {
|
|
|
2670
2734
|
onCleanup(s);
|
|
2671
2735
|
};
|
|
2672
2736
|
|
|
2737
|
+
/**
|
|
2738
|
+
* Warms the routes `url` matches: their lazy components (and any lazy
|
|
2739
|
+
* subtree on the way) load, and with `preloadData` their `preload` functions
|
|
2740
|
+
* run under `intent: "preload"`. Only link preload strategies and
|
|
2741
|
+
* `usePreloadRoute` import it, so apps using neither don't ship it.
|
|
2742
|
+
*/
|
|
2743
|
+
function preloadRoute(router, url, preloadData) {
|
|
2744
|
+
const next = router._match(url.pathname);
|
|
2745
|
+
// An unresolved lazy subtree in the chain: the placeholder's
|
|
2746
|
+
// component.preload (below) kicks the table load; once it lands,
|
|
2747
|
+
// preload again so the real inner routes warm too. Preloads are
|
|
2748
|
+
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
2749
|
+
// here — the real navigation surfaces and retries it.
|
|
2750
|
+
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
2751
|
+
if (boundary) {
|
|
2752
|
+
try {
|
|
2753
|
+
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(router, url, preloadData), () => {});
|
|
2754
|
+
} catch {}
|
|
2755
|
+
}
|
|
2756
|
+
// Data preloads run only for levels a navigation would mount fresh or
|
|
2757
|
+
// reuse with changed inputs: this level's params, and search as the
|
|
2758
|
+
// declared schema's output or else the raw string. Navigation itself is
|
|
2759
|
+
// already this selective (a matching level is reused and re-reads through
|
|
2760
|
+
// tracked params), so an unchanged level has nothing new to warm.
|
|
2761
|
+
let current;
|
|
2762
|
+
try {
|
|
2763
|
+
current = untrack(router.matches);
|
|
2764
|
+
} catch {}
|
|
2765
|
+
const {
|
|
2766
|
+
location
|
|
2767
|
+
} = router;
|
|
2768
|
+
const query = extractSearchParams(url);
|
|
2769
|
+
const inputs = (p, q, s, r) => {
|
|
2770
|
+
const a = serverRouteArgs(r, p, q);
|
|
2771
|
+
a.search === undefined && (a.search = s);
|
|
2772
|
+
return a;
|
|
2773
|
+
};
|
|
2774
|
+
const prevIntent = setPreloadIntent("preload");
|
|
2775
|
+
for (let match in next) {
|
|
2776
|
+
const {
|
|
2777
|
+
route,
|
|
2778
|
+
params
|
|
2779
|
+
} = next[match];
|
|
2780
|
+
const {
|
|
2781
|
+
preload,
|
|
2782
|
+
component
|
|
2783
|
+
} = route;
|
|
2784
|
+
component?.preload?.();
|
|
2785
|
+
const now = current && current[match];
|
|
2786
|
+
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
2787
|
+
setInPreloadFn(true);
|
|
2788
|
+
preloadData && !unchanged && runWithOwner(router._routeOwner(), () => {
|
|
2789
|
+
// A server component route's data IS its call: warm the same
|
|
2790
|
+
// query entry the render will read, under the same derived args.
|
|
2791
|
+
const server = serverRouteOf(component);
|
|
2792
|
+
server && server.call(serverRouteArgs(route, params, query));
|
|
2793
|
+
preload && preload({
|
|
2794
|
+
params,
|
|
2795
|
+
location: {
|
|
2796
|
+
pathname: url.pathname,
|
|
2797
|
+
search: url.search,
|
|
2798
|
+
hash: url.hash,
|
|
2799
|
+
query,
|
|
2800
|
+
state: null,
|
|
2801
|
+
key: ""
|
|
2802
|
+
},
|
|
2803
|
+
intent: "preload"
|
|
2804
|
+
});
|
|
2805
|
+
});
|
|
2806
|
+
setInPreloadFn(false);
|
|
2807
|
+
}
|
|
2808
|
+
setPreloadIntent(prevIntent);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
2813
|
+
* work link preloading triggers: the matched routes' lazy components load,
|
|
2814
|
+
* and with `preloadData` their `preload` functions run.
|
|
2815
|
+
*
|
|
2816
|
+
* @example
|
|
2817
|
+
* ```js
|
|
2818
|
+
* const preload = usePreloadRoute();
|
|
2819
|
+
*
|
|
2820
|
+
* preload(paths.users(2).settings, { preloadData: true });
|
|
2821
|
+
* ```
|
|
2822
|
+
*/
|
|
2823
|
+
const usePreloadRoute = () => {
|
|
2824
|
+
const router = useRouter();
|
|
2825
|
+
return (url, options) => preloadRoute(router, url instanceof URL ? url : new URL(String(url), mockBase), options?.preloadData);
|
|
2826
|
+
};
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* Anchors as the runtime claims them: at creation and again on `href`
|
|
2830
|
+
* writes. `disarm` runs when the creating owner is disposed. Claims fire
|
|
2831
|
+
* before dynamic attributes are applied (solidjs/solid#3923), so whether a
|
|
2832
|
+
* link is opted in, and where it points, is read when it preloads.
|
|
2833
|
+
*/
|
|
2834
|
+
function claimAnchors(arm, disarm) {
|
|
2835
|
+
const seen = new WeakSet();
|
|
2836
|
+
setLinkPreloader(preloadRoute);
|
|
2837
|
+
onCleanup(registerElementClaim(node => {
|
|
2838
|
+
if (node.nodeName.toUpperCase() !== "A") return;
|
|
2839
|
+
arm(node);
|
|
2840
|
+
// a re-claim runs under the owner writing `href`, not the anchor's
|
|
2841
|
+
if (seen.has(node)) return;
|
|
2842
|
+
seen.add(node);
|
|
2843
|
+
getOwner() && onCleanup(() => disarm(node));
|
|
2844
|
+
}));
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// requests made before the browser is idle run together once it is
|
|
2848
|
+
function whenIdle(fn) {
|
|
2849
|
+
let scheduled = false;
|
|
2850
|
+
return () => {
|
|
2851
|
+
if (scheduled) return;
|
|
2852
|
+
scheduled = true;
|
|
2853
|
+
(window.requestIdleCallback || setTimeout)(() => {
|
|
2854
|
+
scheduled = false;
|
|
2855
|
+
fn();
|
|
2856
|
+
});
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/**
|
|
2861
|
+
* Preloads an ambient strategy's anchor: one that names the strategy in its
|
|
2862
|
+
* `preload` attribute, or with `all` any but `preload="false"`. Skipped when
|
|
2863
|
+
* the user asked to save data or the connection is 2g (where the Network
|
|
2864
|
+
* Information API is available).
|
|
2865
|
+
*/
|
|
2866
|
+
const ambientPreload = ({
|
|
2867
|
+
url,
|
|
2868
|
+
preload
|
|
2869
|
+
}, name, all, data) => a => {
|
|
2870
|
+
const value = a.getAttribute("preload");
|
|
2871
|
+
const connection = navigator.connection;
|
|
2872
|
+
const href = (value === name || all && value !== "false") && !(connection && (connection.saveData || /2g/.test(connection.effectiveType))) && url(a);
|
|
2873
|
+
href && preload(href, data);
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
/**
|
|
2877
|
+
* Preload a link when the pointer rests on it for `delay` ms (default 20),
|
|
2878
|
+
* when it takes focus, and on touchstart. Moving over a link that already
|
|
2879
|
+
* preloaded does not preload it again until the pointer leaves. These
|
|
2880
|
+
* listeners do not check modifier keys or `defaultPrevented`; clicks still do.
|
|
2881
|
+
* Warms route code and, unless `data` is false (default true), runs the
|
|
2882
|
+
* matched routes' `preload` functions.
|
|
2883
|
+
*
|
|
2884
|
+
* @example
|
|
2885
|
+
* ```ts
|
|
2886
|
+
* const Router = createRouter({ routes, preloadLinks: intentPreload() });
|
|
2887
|
+
* ```
|
|
2888
|
+
*/
|
|
2889
|
+
const intentPreload = ({
|
|
2890
|
+
delay = 20,
|
|
2891
|
+
data = true
|
|
2892
|
+
} = {}) => ctx => {
|
|
2893
|
+
let timeout;
|
|
2894
|
+
let lastElement;
|
|
2895
|
+
setLinkPreloader(preloadRoute);
|
|
2896
|
+
const onIntent = evt => {
|
|
2897
|
+
// clear before resolving: a throw must not leave the rest timer armed
|
|
2898
|
+
if (evt.type === "mousemove") clearTimeout(timeout);
|
|
2899
|
+
const res = ctx.anchor(evt);
|
|
2900
|
+
const link = res && res[0].getAttribute("preload") !== "false" ? res : undefined;
|
|
2901
|
+
if (evt.type !== "mousemove") return link && ctx.preload(link[1], data);
|
|
2902
|
+
if (!link) return lastElement = undefined;
|
|
2903
|
+
const [a, url] = link;
|
|
2904
|
+
if (lastElement === a) return;
|
|
2905
|
+
timeout = setTimeout(() => {
|
|
2906
|
+
ctx.preload(url, data);
|
|
2907
|
+
lastElement = a;
|
|
2908
|
+
}, delay);
|
|
2909
|
+
};
|
|
2910
|
+
const passive = {
|
|
2911
|
+
passive: true
|
|
2912
|
+
};
|
|
2913
|
+
document.addEventListener("focusin", onIntent, passive);
|
|
2914
|
+
document.addEventListener("touchstart", onIntent, passive);
|
|
2915
|
+
document.addEventListener("mousemove", onIntent, passive);
|
|
2916
|
+
onCleanup(() => {
|
|
2917
|
+
clearTimeout(timeout);
|
|
2918
|
+
document.removeEventListener("focusin", onIntent);
|
|
2919
|
+
document.removeEventListener("touchstart", onIntent);
|
|
2920
|
+
document.removeEventListener("mousemove", onIntent);
|
|
2921
|
+
});
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
/**
|
|
2925
|
+
* Preload a link on `pointerdown` (mouse, touch, or pen), ahead of its click.
|
|
2926
|
+
* Does not check modifier keys or `defaultPrevented`. Warms route code and,
|
|
2927
|
+
* unless `data` is false (default true), route data.
|
|
2928
|
+
*/
|
|
2929
|
+
const tapPreload = ({
|
|
2930
|
+
data = true
|
|
2931
|
+
} = {}) => ctx => {
|
|
2932
|
+
setLinkPreloader(preloadRoute);
|
|
2933
|
+
const onTap = evt => {
|
|
2934
|
+
const res = ctx.anchor(evt);
|
|
2935
|
+
res && res[0].getAttribute("preload") !== "false" && ctx.preload(res[1], data);
|
|
2936
|
+
};
|
|
2937
|
+
document.addEventListener("pointerdown", onTap, {
|
|
2938
|
+
passive: true
|
|
2939
|
+
});
|
|
2940
|
+
onCleanup(() => document.removeEventListener("pointerdown", onTap));
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
/**
|
|
2944
|
+
* Preload a link once it has stayed in the viewport for `delay` ms (default
|
|
2945
|
+
* 100), when the browser is next idle (`requestIdleCallback`, or `setTimeout`
|
|
2946
|
+
* where that is missing). Each link preloads once, and again after its `href`
|
|
2947
|
+
* changes; a link that leaves before then is dropped. One `IntersectionObserver`
|
|
2948
|
+
* is shared. `rootMargin` is passed through to it (the observer's own `"0px"`
|
|
2949
|
+
* when omitted). Applies to links with `preload="viewport"`, or with `all`
|
|
2950
|
+
* (default false) to every link except `preload="false"`. Skips Save-Data and
|
|
2951
|
+
* 2g connections. Warms route code only unless `data` is true (default false).
|
|
2952
|
+
*/
|
|
2953
|
+
const viewportPreload = ({
|
|
2954
|
+
all,
|
|
2955
|
+
data = false,
|
|
2956
|
+
delay = 100,
|
|
2957
|
+
rootMargin
|
|
2958
|
+
} = {}) => ctx => {
|
|
2959
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
2960
|
+
const warm = ambientPreload(ctx, "viewport", all, data);
|
|
2961
|
+
// visible anchors: a pending dwell timer, or `true` once due
|
|
2962
|
+
const visible = new Map();
|
|
2963
|
+
let observer;
|
|
2964
|
+
const leave = a => {
|
|
2965
|
+
clearTimeout(visible.get(a));
|
|
2966
|
+
visible.delete(a);
|
|
2967
|
+
};
|
|
2968
|
+
const flush = whenIdle(() => visible.forEach((due, a) => {
|
|
2969
|
+
if (due !== true) return;
|
|
2970
|
+
observer.unobserve(a);
|
|
2971
|
+
visible.delete(a);
|
|
2972
|
+
warm(a);
|
|
2973
|
+
}));
|
|
2974
|
+
const intersect = entries => entries.forEach(({
|
|
2975
|
+
target: a,
|
|
2976
|
+
isIntersecting
|
|
2977
|
+
}) => isIntersecting ? visible.set(a, setTimeout(() => (visible.set(a, true), flush()), delay)) : leave(a));
|
|
2978
|
+
claimAnchors(a => (observer ||= new IntersectionObserver(intersect, {
|
|
2979
|
+
rootMargin
|
|
2980
|
+
})).observe(a), a => {
|
|
2981
|
+
observer.unobserve(a);
|
|
2982
|
+
leave(a);
|
|
2983
|
+
});
|
|
2984
|
+
onCleanup(() => {
|
|
2985
|
+
observer && observer.disconnect();
|
|
2986
|
+
visible.forEach((_, a) => leave(a));
|
|
2987
|
+
});
|
|
2988
|
+
};
|
|
2989
|
+
|
|
2990
|
+
/**
|
|
2991
|
+
* Preload links as soon as the page has loaded and the browser is idle,
|
|
2992
|
+
* including links mounted later. A link removed before the flush is dropped.
|
|
2993
|
+
* Applies to links with `preload="eager"`, or with `all` (default false) to
|
|
2994
|
+
* every link except `preload="false"`. Skips Save-Data and 2g connections.
|
|
2995
|
+
* Warms route code only unless `data` is true (default false).
|
|
2996
|
+
*/
|
|
2997
|
+
const eagerPreload = ({
|
|
2998
|
+
all,
|
|
2999
|
+
data = false
|
|
3000
|
+
} = {}) => ctx => {
|
|
3001
|
+
const warm = ambientPreload(ctx, "eager", all, data);
|
|
3002
|
+
const queue = new Set();
|
|
3003
|
+
const flush = whenIdle(() => {
|
|
3004
|
+
queue.forEach(warm);
|
|
3005
|
+
queue.clear();
|
|
3006
|
+
});
|
|
3007
|
+
addEventListener("load", flush, {
|
|
3008
|
+
once: true
|
|
3009
|
+
});
|
|
3010
|
+
claimAnchors(a => {
|
|
3011
|
+
queue.add(a);
|
|
3012
|
+
document.readyState === "complete" && flush();
|
|
3013
|
+
}, a => queue.delete(a));
|
|
3014
|
+
onCleanup(() => {
|
|
3015
|
+
removeEventListener("load", flush);
|
|
3016
|
+
queue.clear();
|
|
3017
|
+
});
|
|
3018
|
+
};
|
|
3019
|
+
|
|
2673
3020
|
// Server component routes (experimental — rides the experimental server
|
|
2674
3021
|
// components surface in @solidjs/web; the arg shape may change).
|
|
2675
3022
|
//
|
|
@@ -2680,7 +3027,7 @@ const useBeforeLeave = listener => {
|
|
|
2680
3027
|
// // before: a client component exists only to make the call
|
|
2681
3028
|
// const getStory = query(storyView, "story");
|
|
2682
3029
|
// component: props => {
|
|
2683
|
-
// const View =
|
|
3030
|
+
// const View = dynamicComponent(() => getStory(props.params.id));
|
|
2684
3031
|
// return <View>{props.children}</View>;
|
|
2685
3032
|
// }
|
|
2686
3033
|
// // after
|
|
@@ -2742,8 +3089,9 @@ source) {
|
|
|
2742
3089
|
};
|
|
2743
3090
|
function render(args, routeProps) {
|
|
2744
3091
|
// The source may answer a component, a promise of one, or (a live query)
|
|
2745
|
-
// successive components; `
|
|
2746
|
-
|
|
3092
|
+
// successive components; `dynamicComponent`'s memo lands each the same
|
|
3093
|
+
// way. Never a tag, so the element runtime `dynamic` retains stays out.
|
|
3094
|
+
const View = dynamicComponent(() => call(args()));
|
|
2747
3095
|
return createComponent(View, {
|
|
2748
3096
|
get children() {
|
|
2749
3097
|
return routeProps.children;
|
|
@@ -4098,4 +4446,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
4098
4446
|
submitServerForm: submitServerForm
|
|
4099
4447
|
});
|
|
4100
4448
|
|
|
4101
|
-
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 };
|
|
4449
|
+
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, eagerPreload, hashHistory, int, intentPreload, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, tapPreload, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions, viewportPreload };
|