@solidjs/router 2.0.0-next.37 → 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 +74 -34
- package/dist/claims.d.ts +1 -1
- package/dist/claims.js +12 -3
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +37 -41
- package/dist/index.d.ts +4 -2
- package/dist/index.js +381 -155
- package/dist/index.jsx +3 -1
- package/dist/pending.js +2 -2
- 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/factory.d.ts +13 -3
- package/dist/routers/factory.jsx +5 -2
- package/dist/routing.d.ts +1 -15
- package/dist/routing.js +11 -89
- package/dist/types.d.ts +19 -1
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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) {
|
|
@@ -253,7 +260,7 @@ function setFormClaimHandler(handler) {
|
|
|
253
260
|
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
254
261
|
* plugin, claims never read pending state and the sweep does not track it.
|
|
255
262
|
*
|
|
256
|
-
* The matching rule is `
|
|
263
|
+
* The matching rule is `linkMatcher`, shared with `useLinkState`. The router
|
|
257
264
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
258
265
|
* stepper's `"step"`, a static `"page"`) is left in place, current or not.
|
|
259
266
|
*
|
|
@@ -298,6 +305,11 @@ function setupLinkClaims(router, explicitLinks, links) {
|
|
|
298
305
|
if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
|
|
299
306
|
return url;
|
|
300
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;
|
|
301
313
|
function linkState(a) {
|
|
302
314
|
// read reactive sources unconditionally so the owning effect stays
|
|
303
315
|
// subscribed even while the anchor is not router-managed
|
|
@@ -305,11 +317,16 @@ function setupLinkClaims(router, explicitLinks, links) {
|
|
|
305
317
|
const routing = plugin && plugin.track();
|
|
306
318
|
const url = managedUrl(a);
|
|
307
319
|
const target = url && url.pathname + url.search;
|
|
320
|
+
const key = location.pathname + location.search;
|
|
308
321
|
// no per-anchor `end` opt-out like useLinkState has
|
|
322
|
+
if (key !== matched) {
|
|
323
|
+
matched = key;
|
|
324
|
+
match = linkMatcher(location, basePath);
|
|
325
|
+
}
|
|
309
326
|
const {
|
|
310
327
|
active,
|
|
311
328
|
current
|
|
312
|
-
} =
|
|
329
|
+
} = match(target);
|
|
313
330
|
const pending = !!routing && plugin.pending(target);
|
|
314
331
|
return {
|
|
315
332
|
active,
|
|
@@ -381,6 +398,12 @@ function setupLinkClaims(router, explicitLinks, links) {
|
|
|
381
398
|
}));
|
|
382
399
|
}
|
|
383
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
|
+
|
|
384
407
|
/**
|
|
385
408
|
* The submit delegation consults this slot instead of importing the action
|
|
386
409
|
* module: the action side installs its handler on first action creation
|
|
@@ -392,24 +415,43 @@ let formHandler;
|
|
|
392
415
|
function setRouterFormHandler(handler) {
|
|
393
416
|
formHandler = handler;
|
|
394
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
|
+
}
|
|
395
429
|
function setupNativeEvents({
|
|
396
|
-
preload
|
|
430
|
+
preload,
|
|
397
431
|
explicitLinks = false,
|
|
398
|
-
actionBase = "/_server"
|
|
399
|
-
transformUrl
|
|
432
|
+
actionBase = "/_server"
|
|
400
433
|
} = {}) {
|
|
401
434
|
return router => {
|
|
402
435
|
const basePath = router.base.path();
|
|
403
436
|
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
404
|
-
let preloadTimeout;
|
|
405
|
-
let lastElement;
|
|
406
437
|
function isSvg(el) {
|
|
407
438
|
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
408
439
|
}
|
|
409
440
|
function handleAnchor(evt) {
|
|
410
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) {
|
|
411
447
|
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
412
|
-
|
|
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;
|
|
413
455
|
const svg = isSvg(a);
|
|
414
456
|
const href = svg ? a.href.baseVal : a.href;
|
|
415
457
|
const target = svg ? a.target.baseVal : a.target;
|
|
@@ -421,7 +463,7 @@ function setupNativeEvents({
|
|
|
421
463
|
// inherit the page origin, so the origin check below won't reject them. #382
|
|
422
464
|
if (url.protocol !== "https:" && url.protocol !== "http:") return;
|
|
423
465
|
if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
|
|
424
|
-
return
|
|
466
|
+
return url;
|
|
425
467
|
}
|
|
426
468
|
function handleAnchorClick(evt) {
|
|
427
469
|
const res = handleAnchor(evt);
|
|
@@ -439,25 +481,6 @@ function setupNativeEvents({
|
|
|
439
481
|
state: state ? JSON.parse(state) : undefined
|
|
440
482
|
}));
|
|
441
483
|
}
|
|
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);
|
|
460
|
-
}
|
|
461
484
|
function handleFormSubmit(evt) {
|
|
462
485
|
if (formHandler) return formHandler(evt, router, actionBase);
|
|
463
486
|
// No form handler means no action module in the client graph at all
|
|
@@ -473,37 +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
|
}
|
|
480
508
|
const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
481
509
|
|
|
482
510
|
// ensure delegated event run first
|
|
483
511
|
delegateEvents(["click", "submit"]);
|
|
484
512
|
document.addEventListener("click", handleAnchorClick);
|
|
485
|
-
// preloads are not interactions: those listeners run outside any frame
|
|
486
|
-
if (preload) {
|
|
487
|
-
document.addEventListener("mousemove", handleAnchorMove, {
|
|
488
|
-
passive: true
|
|
489
|
-
});
|
|
490
|
-
document.addEventListener("focusin", handleAnchorPreload, {
|
|
491
|
-
passive: true
|
|
492
|
-
});
|
|
493
|
-
document.addEventListener("touchstart", handleAnchorPreload, {
|
|
494
|
-
passive: true
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
513
|
document.addEventListener("submit", handleSubmit);
|
|
498
514
|
onCleanup(() => {
|
|
499
515
|
document.removeEventListener("click", handleAnchorClick);
|
|
500
|
-
if (preload) {
|
|
501
|
-
document.removeEventListener("mousemove", handleAnchorMove);
|
|
502
|
-
document.removeEventListener("focusin", handleAnchorPreload);
|
|
503
|
-
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
504
|
-
}
|
|
505
516
|
document.removeEventListener("submit", handleSubmit);
|
|
506
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
|
+
}));
|
|
507
524
|
};
|
|
508
525
|
}
|
|
509
526
|
|
|
@@ -731,10 +748,10 @@ function pendingTarget(router) {
|
|
|
731
748
|
/** Whether `to` is the destination of the in-flight navigation. */
|
|
732
749
|
function linkPending(router, to, base, end) {
|
|
733
750
|
const target = pendingTarget(router);
|
|
734
|
-
return !!target &&
|
|
751
|
+
return !!target && linkMatcher({
|
|
735
752
|
pathname: target.value,
|
|
736
753
|
search: ""
|
|
737
|
-
},
|
|
754
|
+
}, base, end)(to).active;
|
|
738
755
|
}
|
|
739
756
|
|
|
740
757
|
/**
|
|
@@ -909,23 +926,6 @@ const useRouteMatches = () => {
|
|
|
909
926
|
return () => router.matches().slice();
|
|
910
927
|
};
|
|
911
928
|
|
|
912
|
-
/**
|
|
913
|
-
* `usePreloadRoute` returns a function for warming a route by hand — the same
|
|
914
|
-
* work link hover/focus intent triggers automatically: the matched routes'
|
|
915
|
-
* lazy components load, and with `preloadData` their `preload` functions run.
|
|
916
|
-
*
|
|
917
|
-
* @example
|
|
918
|
-
* ```js
|
|
919
|
-
* const preload = usePreloadRoute();
|
|
920
|
-
*
|
|
921
|
-
* preload(paths.users(2).settings, { preloadData: true });
|
|
922
|
-
* ```
|
|
923
|
-
*/
|
|
924
|
-
const usePreloadRoute = () => {
|
|
925
|
-
const pre = useRouter().preloadRoute;
|
|
926
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
|
|
927
|
-
};
|
|
928
|
-
|
|
929
929
|
/**
|
|
930
930
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
931
931
|
*
|
|
@@ -1034,7 +1034,7 @@ const useLinkState = (href, options = {}) => {
|
|
|
1034
1034
|
const location = router.location;
|
|
1035
1035
|
const to = useResolvedPath(() => String(href()));
|
|
1036
1036
|
const base = router.base.path();
|
|
1037
|
-
const state = createMemo(() =>
|
|
1037
|
+
const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
|
|
1038
1038
|
return {
|
|
1039
1039
|
active: createMemo(() => state().active),
|
|
1040
1040
|
current: createMemo(() => state().current),
|
|
@@ -1373,6 +1373,11 @@ let preloadIntent;
|
|
|
1373
1373
|
function getIntent() {
|
|
1374
1374
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
1375
1375
|
}
|
|
1376
|
+
function setPreloadIntent(intent) {
|
|
1377
|
+
const prev = preloadIntent;
|
|
1378
|
+
preloadIntent = intent;
|
|
1379
|
+
return prev;
|
|
1380
|
+
}
|
|
1376
1381
|
let inPreloadFn = false;
|
|
1377
1382
|
function getInPreloadFn() {
|
|
1378
1383
|
return inPreloadFn;
|
|
@@ -1504,9 +1509,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1504
1509
|
}
|
|
1505
1510
|
return read();
|
|
1506
1511
|
};
|
|
1512
|
+
const matchPath = pathname => getRouteMatches(branches(), options.transformUrl ? options.transformUrl(pathname) : pathname);
|
|
1507
1513
|
const matches = createMemo(() => {
|
|
1508
|
-
const
|
|
1509
|
-
const m = getRouteMatches(branches(), pathname);
|
|
1514
|
+
const m = matchPath(location.pathname);
|
|
1510
1515
|
// An unresolved lazy subtree parks readers on not-ready semantics — the
|
|
1511
1516
|
// navigation transition (or the SSR stream) holds until the table lands.
|
|
1512
1517
|
// NotReadyError (not a returned promise) because a match chain is full
|
|
@@ -1562,12 +1567,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1562
1567
|
intent: transitionIntent,
|
|
1563
1568
|
_source: source,
|
|
1564
1569
|
_owner: routerOwner,
|
|
1570
|
+
_routeOwner: getContext,
|
|
1571
|
+
_match: matchPath,
|
|
1565
1572
|
renderPath,
|
|
1566
1573
|
parsePath,
|
|
1567
1574
|
navigatorFactory,
|
|
1568
1575
|
matches,
|
|
1569
1576
|
beforeLeave,
|
|
1570
|
-
preloadRoute,
|
|
1571
1577
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
1572
1578
|
get submissions() {
|
|
1573
1579
|
return submissions ||= createSignal(isServer ? initSubmissions() : [], {
|
|
@@ -1697,71 +1703,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1697
1703
|
route = route || useOptionalContext(RouteContextObj) || baseRoute;
|
|
1698
1704
|
return (to, options) => navigateFromRoute(route, to, options);
|
|
1699
1705
|
}
|
|
1700
|
-
function preloadRoute(url, preloadData) {
|
|
1701
|
-
const next = getRouteMatches(branches(), url.pathname);
|
|
1702
|
-
// An unresolved lazy subtree in the chain: the placeholder's
|
|
1703
|
-
// component.preload (below) kicks the table load; once it lands,
|
|
1704
|
-
// preload again so the real inner routes warm too. Preloads are
|
|
1705
|
-
// speculative: a failed load (held sync throw or rejection) is ignored
|
|
1706
|
-
// here — the real navigation surfaces and retries it.
|
|
1707
|
-
const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
|
|
1708
|
-
if (boundary) {
|
|
1709
|
-
try {
|
|
1710
|
-
resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
|
|
1711
|
-
} catch {}
|
|
1712
|
-
}
|
|
1713
|
-
// Data preloads run only for levels a navigation would mount fresh or
|
|
1714
|
-
// reuse with changed inputs: this level's params, and search as the
|
|
1715
|
-
// declared schema's output or else the raw string. Navigation itself is
|
|
1716
|
-
// already this selective (a matching level is reused and re-reads through
|
|
1717
|
-
// tracked params), so an unchanged level has nothing new to warm.
|
|
1718
|
-
let current;
|
|
1719
|
-
try {
|
|
1720
|
-
current = untrack(matches);
|
|
1721
|
-
} catch {}
|
|
1722
|
-
const query = extractSearchParams(url);
|
|
1723
|
-
const inputs = (p, q, s, r) => {
|
|
1724
|
-
const a = serverRouteArgs(r, p, q);
|
|
1725
|
-
a.search === undefined && (a.search = s);
|
|
1726
|
-
return a;
|
|
1727
|
-
};
|
|
1728
|
-
const prevIntent = preloadIntent;
|
|
1729
|
-
preloadIntent = "preload";
|
|
1730
|
-
for (let match in next) {
|
|
1731
|
-
const {
|
|
1732
|
-
route,
|
|
1733
|
-
params
|
|
1734
|
-
} = next[match];
|
|
1735
|
-
const {
|
|
1736
|
-
preload,
|
|
1737
|
-
component
|
|
1738
|
-
} = route;
|
|
1739
|
-
component?.preload?.();
|
|
1740
|
-
const now = current && current[match];
|
|
1741
|
-
const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
|
|
1742
|
-
inPreloadFn = true;
|
|
1743
|
-
preloadData && !unchanged && runWithOwner(getContext(), () => {
|
|
1744
|
-
// A server component route's data IS its call: warm the same
|
|
1745
|
-
// query entry the render will read, under the same derived args.
|
|
1746
|
-
const server = serverRouteOf(component);
|
|
1747
|
-
server && server.call(serverRouteArgs(route, params, query));
|
|
1748
|
-
preload && preload({
|
|
1749
|
-
params,
|
|
1750
|
-
location: {
|
|
1751
|
-
pathname: url.pathname,
|
|
1752
|
-
search: url.search,
|
|
1753
|
-
hash: url.hash,
|
|
1754
|
-
query,
|
|
1755
|
-
state: null,
|
|
1756
|
-
key: ""
|
|
1757
|
-
},
|
|
1758
|
-
intent: "preload"
|
|
1759
|
-
});
|
|
1760
|
-
});
|
|
1761
|
-
inPreloadFn = false;
|
|
1762
|
-
}
|
|
1763
|
-
preloadIntent = prevIntent;
|
|
1764
|
-
}
|
|
1765
1706
|
|
|
1766
1707
|
// Seeds the initial submission from a no-JS form post: the server
|
|
1767
1708
|
// function runtime redirected back with the outcome in a one-shot flash
|
|
@@ -2595,6 +2536,9 @@ function staticIntegration(url, utils) {
|
|
|
2595
2536
|
};
|
|
2596
2537
|
}
|
|
2597
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
|
+
}
|
|
2598
2542
|
const basePath = config.base || "";
|
|
2599
2543
|
// Routes are immutable per instance, so compilation is shared by every
|
|
2600
2544
|
// mount, request, and `match()` call — recompiled only when a lazy subtree
|
|
@@ -2644,8 +2588,7 @@ function createRouter(config) {
|
|
|
2644
2588
|
setupNativeEvents({
|
|
2645
2589
|
preload: config.preloadLinks,
|
|
2646
2590
|
explicitLinks: config.explicitLinks,
|
|
2647
|
-
actionBase: config.actionBase
|
|
2648
|
-
transformUrl: config.transformUrl
|
|
2591
|
+
actionBase: config.actionBase
|
|
2649
2592
|
})(routerState);
|
|
2650
2593
|
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
2651
2594
|
if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
|
|
@@ -2791,6 +2734,289 @@ const useBeforeLeave = listener => {
|
|
|
2791
2734
|
onCleanup(s);
|
|
2792
2735
|
};
|
|
2793
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
|
+
|
|
2794
3020
|
// Server component routes (experimental — rides the experimental server
|
|
2795
3021
|
// components surface in @solidjs/web; the arg shape may change).
|
|
2796
3022
|
//
|
|
@@ -4220,4 +4446,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
4220
4446
|
submitServerForm: submitServerForm
|
|
4221
4447
|
});
|
|
4222
4448
|
|
|
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 };
|
|
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 };
|