@solidjs/router 0.17.0-next.3 → 0.17.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +2 -2
- package/dist/components.jsx +3 -2
- package/dist/data/action.d.ts +1 -1
- package/dist/data/action.js +5 -1
- package/dist/index.js +109 -47
- package/dist/lifecycle.js +8 -1
- package/dist/routers/HashRouter.d.ts +1 -1
- package/dist/routers/MemoryRouter.d.ts +1 -1
- package/dist/routers/Router.d.ts +1 -1
- package/dist/routers/Router.js +1 -1
- package/dist/routers/StaticRouter.d.ts +1 -1
- package/dist/routers/components.d.ts +4 -3
- package/dist/routers/components.jsx +18 -5
- package/dist/routers/createRouter.d.ts +1 -1
- package/dist/routing.d.ts +6 -6
- package/dist/routing.js +51 -20
- package/dist/types.d.ts +10 -4
- package/dist/utils.js +9 -1
- package/package.json +7 -7
package/dist/components.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { Location, Navigator } from "./types.js";
|
|
3
|
-
declare module "
|
|
3
|
+
declare module "@solidjs/web/jsx-runtime" {
|
|
4
4
|
namespace JSX {
|
|
5
5
|
interface AnchorHTMLAttributes<T> {
|
|
6
6
|
state?: string;
|
package/dist/components.jsx
CHANGED
|
@@ -26,8 +26,9 @@ export function A(props) {
|
|
|
26
26
|
const to_ = to();
|
|
27
27
|
if (to_ === undefined)
|
|
28
28
|
return [false, false];
|
|
29
|
-
|
|
30
|
-
const
|
|
29
|
+
// trailing slashes are ignored so `/route` and `/route/` share active state
|
|
30
|
+
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
31
|
+
const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
|
|
31
32
|
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
32
33
|
});
|
|
33
34
|
const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass]
|
package/dist/data/action.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JSX } from "
|
|
1
|
+
import { type JSX } from "@solidjs/web";
|
|
2
2
|
import type { Submission, NarrowResponse } from "../types.js";
|
|
3
3
|
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
4
4
|
url: string;
|
package/dist/data/action.js
CHANGED
|
@@ -122,7 +122,11 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
122
122
|
fn[invokeSymbol] = invoke;
|
|
123
123
|
if (!isServer) {
|
|
124
124
|
actions.set(url, fn);
|
|
125
|
-
|
|
125
|
+
// Only remove the registration if it still belongs to this instance —
|
|
126
|
+
// a re-created action (e.g. a new `.with()` binding after revalidation)
|
|
127
|
+
// may have registered itself under the same URL since.
|
|
128
|
+
getOwner() &&
|
|
129
|
+
onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
126
130
|
}
|
|
127
131
|
return fn;
|
|
128
132
|
}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,14 @@ function createBeforeLeave() {
|
|
|
17
17
|
preventDefault: () => e.defaultPrevented = true
|
|
18
18
|
};
|
|
19
19
|
for (const l of listeners) l.listener({
|
|
20
|
-
|
|
20
|
+
to,
|
|
21
|
+
options,
|
|
22
|
+
// delegate to the shared event so later listeners' preventDefault
|
|
23
|
+
// calls are observable from earlier listeners
|
|
24
|
+
get defaultPrevented() {
|
|
25
|
+
return e.defaultPrevented;
|
|
26
|
+
},
|
|
27
|
+
preventDefault: e.preventDefault,
|
|
21
28
|
from: l.location,
|
|
22
29
|
retry: force => {
|
|
23
30
|
force && (ignore = true);
|
|
@@ -121,7 +128,12 @@ function createMatcher(path, partial, matchFilters) {
|
|
|
121
128
|
const segments = pattern.split("/").filter(Boolean);
|
|
122
129
|
const len = segments.length;
|
|
123
130
|
return location => {
|
|
124
|
-
const locSegments = location.split("/")
|
|
131
|
+
const locSegments = location.split("/");
|
|
132
|
+
// tolerate a single leading and trailing slash, but reject empty interior
|
|
133
|
+
// segments so `/foo//bar` doesn't silently match `/foo/bar` (#567)
|
|
134
|
+
if (locSegments[0] === "") locSegments.shift();
|
|
135
|
+
if (locSegments.length && locSegments[locSegments.length - 1] === "") locSegments.pop();
|
|
136
|
+
if (locSegments.includes("")) return null;
|
|
125
137
|
const lenDiff = locSegments.length - len;
|
|
126
138
|
if (lenDiff < 0 || lenDiff > 0 && splat === undefined && !partial) {
|
|
127
139
|
return null;
|
|
@@ -370,7 +382,11 @@ const useMatch = (path, matchFilters) => {
|
|
|
370
382
|
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
371
383
|
* ```
|
|
372
384
|
*/
|
|
373
|
-
const useCurrentMatches = () =>
|
|
385
|
+
const useCurrentMatches = () => {
|
|
386
|
+
const router = useRouter();
|
|
387
|
+
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
388
|
+
return () => router.matches().slice();
|
|
389
|
+
};
|
|
374
390
|
|
|
375
391
|
/**
|
|
376
392
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
@@ -383,7 +399,7 @@ const useCurrentMatches = () => useRouter().matches;
|
|
|
383
399
|
* const getUser = query(() => fetchUser(params.id), "user");
|
|
384
400
|
* ```
|
|
385
401
|
*/
|
|
386
|
-
const useParams = () =>
|
|
402
|
+
const useParams = () => useRoute().params;
|
|
387
403
|
|
|
388
404
|
/**
|
|
389
405
|
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
|
|
@@ -413,11 +429,20 @@ const useParams = () => useRouter().params;
|
|
|
413
429
|
* ```
|
|
414
430
|
*/
|
|
415
431
|
const useSearchParams = () => {
|
|
416
|
-
const
|
|
432
|
+
const router = useRouter();
|
|
433
|
+
const location = router.location;
|
|
417
434
|
const navigate = useNavigate();
|
|
418
435
|
const setSearchParams = (params, options) => {
|
|
419
|
-
const
|
|
420
|
-
|
|
436
|
+
const to = untrack(() => {
|
|
437
|
+
// merge onto the in-flight navigation target (if any) so consecutive
|
|
438
|
+
// synchronous calls compose instead of the later one winning
|
|
439
|
+
const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
|
|
440
|
+
const pathname = pending ? pending.pathname : location.pathname;
|
|
441
|
+
const search = pending ? pending.search : location.search;
|
|
442
|
+
const hash = pending ? pending.hash : location.hash;
|
|
443
|
+
return pathname + mergeSearchString(search, params) + hash;
|
|
444
|
+
});
|
|
445
|
+
navigate(to, {
|
|
421
446
|
scroll: false,
|
|
422
447
|
resolve: false,
|
|
423
448
|
...options
|
|
@@ -461,6 +486,12 @@ const useBeforeLeave = listener => {
|
|
|
461
486
|
});
|
|
462
487
|
onCleanup(s);
|
|
463
488
|
};
|
|
489
|
+
|
|
490
|
+
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
491
|
+
// pchar characters (sub-delims / ":" / "@") literal, matching how browsers
|
|
492
|
+
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
493
|
+
// still percent-encoded exactly as before, since browsers encode those too.
|
|
494
|
+
const encodeSegment = s => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
|
|
464
495
|
function createRoutes(routeDef, base = "") {
|
|
465
496
|
const {
|
|
466
497
|
component,
|
|
@@ -480,7 +511,7 @@ function createRoutes(routeDef, base = "") {
|
|
|
480
511
|
const path = joinPaths(base, expandedPath);
|
|
481
512
|
let pattern = isLeaf ? path : path.split("/*", 1)[0];
|
|
482
513
|
pattern = pattern.split("/").map(s => {
|
|
483
|
-
return s.startsWith(":") || s.startsWith("*") ? s :
|
|
514
|
+
return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
|
|
484
515
|
}).join("/");
|
|
485
516
|
acc.push({
|
|
486
517
|
...shared,
|
|
@@ -549,12 +580,21 @@ function getRouteMatches(branches, location) {
|
|
|
549
580
|
}
|
|
550
581
|
return [];
|
|
551
582
|
}
|
|
583
|
+
function mergeParams(matches) {
|
|
584
|
+
const params = {};
|
|
585
|
+
for (let i = 0; i < matches.length; i++) {
|
|
586
|
+
Object.assign(params, matches[i].params);
|
|
587
|
+
}
|
|
588
|
+
return params;
|
|
589
|
+
}
|
|
552
590
|
function createLocation(path, state, queryWrapper) {
|
|
553
591
|
const origin = new URL(mockBase);
|
|
554
592
|
const url = createMemo((prev = origin) => {
|
|
555
593
|
const path_ = path();
|
|
556
594
|
try {
|
|
557
|
-
|
|
595
|
+
// anchor rooted paths against the origin explicitly - a path with
|
|
596
|
+
// doubled leading slashes would otherwise parse as protocol-relative
|
|
597
|
+
return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
|
|
558
598
|
} catch (err) {
|
|
559
599
|
console.error(`Invalid path ${path_}`);
|
|
560
600
|
return prev;
|
|
@@ -642,17 +682,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
642
682
|
}
|
|
643
683
|
return getRouteMatches(branches(), location.pathname);
|
|
644
684
|
});
|
|
645
|
-
const buildParams = () =>
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
for (let i = 0; i < m.length; i++) {
|
|
649
|
-
Object.assign(params, m[i].params);
|
|
650
|
-
}
|
|
651
|
-
return params;
|
|
652
|
-
};
|
|
653
|
-
const params = utils.paramsWrapper ? utils.paramsWrapper(buildParams, branches) : createMemoObject(buildParams);
|
|
685
|
+
const buildParams = () => mergeParams(matches());
|
|
686
|
+
const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
|
|
687
|
+
const params = wrapParams(buildParams);
|
|
654
688
|
const baseRoute = {
|
|
655
689
|
pattern: basePath,
|
|
690
|
+
params,
|
|
656
691
|
path: () => basePath,
|
|
657
692
|
outlet: () => null,
|
|
658
693
|
resolvePath(to) {
|
|
@@ -663,7 +698,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
663
698
|
base: baseRoute,
|
|
664
699
|
location,
|
|
665
700
|
params,
|
|
701
|
+
wrapParams,
|
|
666
702
|
isRouting,
|
|
703
|
+
get pendingTarget() {
|
|
704
|
+
return lastTransitionTarget;
|
|
705
|
+
},
|
|
667
706
|
renderPath,
|
|
668
707
|
parsePath,
|
|
669
708
|
navigatorFactory,
|
|
@@ -808,11 +847,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
808
847
|
}];
|
|
809
848
|
}
|
|
810
849
|
}
|
|
811
|
-
function createRouteContext(router, parent, outlet, match) {
|
|
850
|
+
function createRouteContext(router, parent, outlet, match, matches = () => [match()]) {
|
|
812
851
|
const {
|
|
813
852
|
base,
|
|
814
853
|
location,
|
|
815
|
-
|
|
854
|
+
wrapParams
|
|
816
855
|
} = router;
|
|
817
856
|
const {
|
|
818
857
|
pattern,
|
|
@@ -820,6 +859,10 @@ function createRouteContext(router, parent, outlet, match) {
|
|
|
820
859
|
preload
|
|
821
860
|
} = match().route;
|
|
822
861
|
const path = createMemo(() => match().path);
|
|
862
|
+
// Params scoped to this route's lifetime. `matches` is expected to retain
|
|
863
|
+
// its last valid value while this route is being torn down, so outgoing
|
|
864
|
+
// components and preloads never observe another route's params.
|
|
865
|
+
const params = wrapParams(() => mergeParams(matches()));
|
|
823
866
|
component && component.preload && component.preload();
|
|
824
867
|
inPreloadFn = true;
|
|
825
868
|
const data = preload ? preload({
|
|
@@ -831,6 +874,7 @@ function createRouteContext(router, parent, outlet, match) {
|
|
|
831
874
|
const route = {
|
|
832
875
|
parent,
|
|
833
876
|
pattern,
|
|
877
|
+
params,
|
|
834
878
|
path,
|
|
835
879
|
outlet: () => component ? createComponent(component, {
|
|
836
880
|
params,
|
|
@@ -940,6 +984,13 @@ function Routes(props) {
|
|
|
940
984
|
const disposers = [];
|
|
941
985
|
let root;
|
|
942
986
|
let prevMatches;
|
|
987
|
+
// dispose the detached per-route roots when this component unmounts, otherwise
|
|
988
|
+
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
989
|
+
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
990
|
+
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
991
|
+
// are created under the owner of this component rather than the memo's
|
|
992
|
+
// computation (which disposes its children every time it re-runs).
|
|
993
|
+
const owner = getOwner();
|
|
943
994
|
const routeStates = createMemo(prev => {
|
|
944
995
|
const nextMatches = props.routerState.matches();
|
|
945
996
|
const previousMatches = prevMatches;
|
|
@@ -955,13 +1006,19 @@ function Routes(props) {
|
|
|
955
1006
|
if (disposers[i]) {
|
|
956
1007
|
disposers[i]();
|
|
957
1008
|
}
|
|
958
|
-
createRoot(dispose => {
|
|
1009
|
+
runWithOwner(owner, () => createRoot(dispose => {
|
|
959
1010
|
disposers[i] = dispose;
|
|
960
|
-
|
|
1011
|
+
const routeKey = nextMatch.route.key;
|
|
1012
|
+
// Retain the last matches in which this route participated so
|
|
1013
|
+
// that its components and preloads never observe another
|
|
1014
|
+
// route's params/path while this route is being torn down.
|
|
1015
|
+
const matchesAtLevel = createMemo(prev => {
|
|
961
1016
|
const routeMatches = props.routerState.matches();
|
|
962
|
-
|
|
1017
|
+
const m = routeMatches[i];
|
|
1018
|
+
return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
|
|
963
1019
|
});
|
|
964
|
-
|
|
1020
|
+
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
|
|
1021
|
+
}));
|
|
965
1022
|
}
|
|
966
1023
|
}
|
|
967
1024
|
disposers.splice(nextMatches.length).forEach(dispose => dispose());
|
|
@@ -1386,7 +1443,10 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
1386
1443
|
fn[invokeSymbol] = invoke;
|
|
1387
1444
|
if (!isServer) {
|
|
1388
1445
|
actions.set(url, fn);
|
|
1389
|
-
|
|
1446
|
+
// Only remove the registration if it still belongs to this instance —
|
|
1447
|
+
// a re-created action (e.g. a new `.with()` binding after revalidation)
|
|
1448
|
+
// may have registered itself under the same URL since.
|
|
1449
|
+
getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
1390
1450
|
}
|
|
1391
1451
|
return fn;
|
|
1392
1452
|
}
|
|
@@ -1556,7 +1616,7 @@ function setupNativeEvents({
|
|
|
1556
1616
|
function Router(props) {
|
|
1557
1617
|
if (isServer) return StaticRouter(props);
|
|
1558
1618
|
const getSource = () => {
|
|
1559
|
-
const url = window.location.pathname
|
|
1619
|
+
const url = window.location.pathname + window.location.search;
|
|
1560
1620
|
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
1561
1621
|
return {
|
|
1562
1622
|
value: url + window.location.hash,
|
|
@@ -1714,6 +1774,9 @@ function MemoryRouter(props) {
|
|
|
1714
1774
|
}
|
|
1715
1775
|
|
|
1716
1776
|
var _tmpl$ = /*#__PURE__*/template(`<a>`);
|
|
1777
|
+
|
|
1778
|
+
// the JSX namespace lives in @solidjs/web's jsx-runtime module as of 2.0
|
|
1779
|
+
|
|
1717
1780
|
function toClassName(value) {
|
|
1718
1781
|
if (!value) return "";
|
|
1719
1782
|
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
@@ -1735,30 +1798,29 @@ function A(props) {
|
|
|
1735
1798
|
const isActive = createMemo(() => {
|
|
1736
1799
|
const to_ = to();
|
|
1737
1800
|
if (to_ === undefined) return [false, false];
|
|
1738
|
-
|
|
1739
|
-
const
|
|
1801
|
+
// trailing slashes are ignored so `/route` and `/route/` share active state
|
|
1802
|
+
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
1803
|
+
const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
|
|
1740
1804
|
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
1741
1805
|
});
|
|
1742
1806
|
const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass].filter(Boolean).join(" "));
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
return _el$;
|
|
1761
|
-
})();
|
|
1807
|
+
var _el$ = _tmpl$();
|
|
1808
|
+
spread(_el$, mergeProps(rest, {
|
|
1809
|
+
get href() {
|
|
1810
|
+
return href() || props.href;
|
|
1811
|
+
},
|
|
1812
|
+
get state() {
|
|
1813
|
+
return JSON.stringify(props.state);
|
|
1814
|
+
},
|
|
1815
|
+
get ["class"]() {
|
|
1816
|
+
return className();
|
|
1817
|
+
},
|
|
1818
|
+
"link": true,
|
|
1819
|
+
get ["aria-current"]() {
|
|
1820
|
+
return isActive()[1] ? "page" : undefined;
|
|
1821
|
+
}
|
|
1822
|
+
}), false);
|
|
1823
|
+
return _el$;
|
|
1762
1824
|
}
|
|
1763
1825
|
function Navigate(props) {
|
|
1764
1826
|
const navigate = useNavigate();
|
package/dist/lifecycle.js
CHANGED
|
@@ -17,7 +17,14 @@ export function createBeforeLeave() {
|
|
|
17
17
|
};
|
|
18
18
|
for (const l of listeners)
|
|
19
19
|
l.listener({
|
|
20
|
-
|
|
20
|
+
to,
|
|
21
|
+
options,
|
|
22
|
+
// delegate to the shared event so later listeners' preventDefault
|
|
23
|
+
// calls are observable from earlier listeners
|
|
24
|
+
get defaultPrevented() {
|
|
25
|
+
return e.defaultPrevented;
|
|
26
|
+
},
|
|
27
|
+
preventDefault: e.preventDefault,
|
|
21
28
|
from: l.location,
|
|
22
29
|
retry: (force) => {
|
|
23
30
|
force && (ignore = true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LocationChange } from "../types.js";
|
|
2
2
|
import type { BaseRouterProps } from "./components.jsx";
|
|
3
|
-
import type { JSX } from "
|
|
3
|
+
import type { JSX } from "@solidjs/web";
|
|
4
4
|
export type MemoryHistory = {
|
|
5
5
|
get: () => string;
|
|
6
6
|
set: (change: LocationChange) => void;
|
package/dist/routers/Router.d.ts
CHANGED
package/dist/routers/Router.js
CHANGED
|
@@ -7,7 +7,7 @@ export function Router(props) {
|
|
|
7
7
|
if (isServer)
|
|
8
8
|
return StaticRouter(props);
|
|
9
9
|
const getSource = () => {
|
|
10
|
-
const url = window.location.pathname
|
|
10
|
+
const url = window.location.pathname + window.location.search;
|
|
11
11
|
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
12
12
|
return {
|
|
13
13
|
value: url + window.location.hash,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Component
|
|
2
|
-
import type {
|
|
1
|
+
import type { Component } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import type { MatchFilters, RouteDefinition, RoutePreloadFunc, RouterIntegration, RouteSectionComponent, RouteSectionProps } from "../types.js";
|
|
3
4
|
export type BaseRouterProps = {
|
|
4
5
|
base?: string;
|
|
5
6
|
/**
|
|
@@ -17,7 +18,7 @@ export type RouteProps<S extends string, T = unknown> = {
|
|
|
17
18
|
children?: JSX.Element;
|
|
18
19
|
preload?: RoutePreloadFunc<T>;
|
|
19
20
|
matchFilters?: MatchFilters<S>;
|
|
20
|
-
component?:
|
|
21
|
+
component?: RouteSectionComponent<T>;
|
|
21
22
|
info?: Record<string, any>;
|
|
22
23
|
};
|
|
23
24
|
export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { children, createMemo, createRoot, getOwner, merge, untrack } from "solid-js";
|
|
2
|
+
import { children, createMemo, createRoot, getOwner, merge, onCleanup, runWithOwner, untrack } from "solid-js";
|
|
3
3
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
4
|
import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";
|
|
5
5
|
export const createRouterComponent = (router) => function IntegratedRouter(props) {
|
|
@@ -64,6 +64,13 @@ function Routes(props) {
|
|
|
64
64
|
const disposers = [];
|
|
65
65
|
let root;
|
|
66
66
|
let prevMatches;
|
|
67
|
+
// dispose the detached per-route roots when this component unmounts, otherwise
|
|
68
|
+
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
69
|
+
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
70
|
+
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
71
|
+
// are created under the owner of this component rather than the memo's
|
|
72
|
+
// computation (which disposes its children every time it re-runs).
|
|
73
|
+
const owner = getOwner();
|
|
67
74
|
const routeStates = createMemo((prev) => {
|
|
68
75
|
const nextMatches = props.routerState.matches();
|
|
69
76
|
const previousMatches = prevMatches;
|
|
@@ -80,13 +87,19 @@ function Routes(props) {
|
|
|
80
87
|
if (disposers[i]) {
|
|
81
88
|
disposers[i]();
|
|
82
89
|
}
|
|
83
|
-
createRoot(dispose => {
|
|
90
|
+
runWithOwner(owner, () => createRoot(dispose => {
|
|
84
91
|
disposers[i] = dispose;
|
|
85
|
-
|
|
92
|
+
const routeKey = nextMatch.route.key;
|
|
93
|
+
// Retain the last matches in which this route participated so
|
|
94
|
+
// that its components and preloads never observe another
|
|
95
|
+
// route's params/path while this route is being torn down.
|
|
96
|
+
const matchesAtLevel = createMemo((prev) => {
|
|
86
97
|
const routeMatches = props.routerState.matches();
|
|
87
|
-
|
|
98
|
+
const m = routeMatches[i];
|
|
99
|
+
return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
|
|
88
100
|
});
|
|
89
|
-
|
|
101
|
+
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
|
|
102
|
+
}));
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
disposers.splice(nextMatches.length).forEach(dispose => dispose());
|
|
@@ -5,6 +5,6 @@ export declare function createRouter(config: {
|
|
|
5
5
|
init?: (notify: (value?: string | LocationChange) => void) => () => void;
|
|
6
6
|
create?: (router: RouterContext) => void;
|
|
7
7
|
utils?: Partial<RouterUtils>;
|
|
8
|
-
}): (props: import("./components.jsx").BaseRouterProps) => import("
|
|
8
|
+
}): (props: import("./components.jsx").BaseRouterProps) => import("@solidjs/web").JSX.Element;
|
|
9
9
|
export declare function bindEvent(target: EventTarget, type: string, handler: EventListener): () => void;
|
|
10
10
|
export declare function scrollToHash(hash: string, fallbackTop?: boolean): void;
|
package/dist/routing.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { JSX
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { BeforeLeaveEventArgs, Branch, Intent, Location, MatchFilters, NavigateOptions, Navigator, Params, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SearchParams, SetSearchParams } from "./types.js";
|
|
3
3
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
4
|
-
export declare const RouterContextObj: import("solid-js").Context<RouterContext
|
|
5
|
-
export declare const RouteContextObj: import("solid-js").Context<RouteContext
|
|
4
|
+
export declare const RouterContextObj: import("solid-js").Context<RouterContext>;
|
|
5
|
+
export declare const RouteContextObj: import("solid-js").Context<RouteContext>;
|
|
6
6
|
export declare const useRouter: () => RouterContext;
|
|
7
7
|
export declare const useRoute: () => RouteContext;
|
|
8
|
-
export declare const useResolvedPath: (path: () => string) =>
|
|
8
|
+
export declare const useResolvedPath: (path: () => string) => import("solid-js").SourceAccessor<string | undefined>;
|
|
9
9
|
export declare const useHref: <T extends string | undefined>(to: () => T) => () => string | T;
|
|
10
10
|
/**
|
|
11
11
|
* Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
|
|
@@ -79,7 +79,7 @@ export declare const usePreloadRoute: () => (url: string | URL, options?: {
|
|
|
79
79
|
* return <div classList={{ active: Boolean(match()) }} />;
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
-
export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S>) =>
|
|
82
|
+
export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S>) => import("solid-js").SourceAccessor<import("./types.js").PathMatch | undefined>;
|
|
83
83
|
/**
|
|
84
84
|
* `useCurrentMatches` returns all the matches for the current matched route.
|
|
85
85
|
* Useful for getting all the route information.
|
|
@@ -172,4 +172,4 @@ export declare function createRouterContext(integration: RouterIntegration, bran
|
|
|
172
172
|
singleFlight?: boolean;
|
|
173
173
|
transformUrl?: (url: string) => string;
|
|
174
174
|
}): RouterContext;
|
|
175
|
-
export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch): RouteContext;
|
|
175
|
+
export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, matches?: () => RouteMatch[]): RouteContext;
|
package/dist/routing.js
CHANGED
|
@@ -124,7 +124,11 @@ export const useMatch = (path, matchFilters) => {
|
|
|
124
124
|
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
125
125
|
* ```
|
|
126
126
|
*/
|
|
127
|
-
export const useCurrentMatches = () =>
|
|
127
|
+
export const useCurrentMatches = () => {
|
|
128
|
+
const router = useRouter();
|
|
129
|
+
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
130
|
+
return () => router.matches().slice();
|
|
131
|
+
};
|
|
128
132
|
/**
|
|
129
133
|
* Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
|
|
130
134
|
*
|
|
@@ -136,7 +140,7 @@ export const useCurrentMatches = () => useRouter().matches;
|
|
|
136
140
|
* const getUser = query(() => fetchUser(params.id), "user");
|
|
137
141
|
* ```
|
|
138
142
|
*/
|
|
139
|
-
export const useParams = () =>
|
|
143
|
+
export const useParams = () => useRoute().params;
|
|
140
144
|
/**
|
|
141
145
|
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
|
|
142
146
|
* The object is a proxy so you must access properties to subscribe to reactive updates.
|
|
@@ -165,11 +169,20 @@ export const useParams = () => useRouter().params;
|
|
|
165
169
|
* ```
|
|
166
170
|
*/
|
|
167
171
|
export const useSearchParams = () => {
|
|
168
|
-
const
|
|
172
|
+
const router = useRouter();
|
|
173
|
+
const location = router.location;
|
|
169
174
|
const navigate = useNavigate();
|
|
170
175
|
const setSearchParams = (params, options) => {
|
|
171
|
-
const
|
|
172
|
-
|
|
176
|
+
const to = untrack(() => {
|
|
177
|
+
// merge onto the in-flight navigation target (if any) so consecutive
|
|
178
|
+
// synchronous calls compose instead of the later one winning
|
|
179
|
+
const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
|
|
180
|
+
const pathname = pending ? pending.pathname : location.pathname;
|
|
181
|
+
const search = pending ? pending.search : location.search;
|
|
182
|
+
const hash = pending ? pending.hash : location.hash;
|
|
183
|
+
return pathname + mergeSearchString(search, params) + hash;
|
|
184
|
+
});
|
|
185
|
+
navigate(to, {
|
|
173
186
|
scroll: false,
|
|
174
187
|
resolve: false,
|
|
175
188
|
...options
|
|
@@ -212,6 +225,11 @@ export const useBeforeLeave = (listener) => {
|
|
|
212
225
|
});
|
|
213
226
|
onCleanup(s);
|
|
214
227
|
};
|
|
228
|
+
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
229
|
+
// pchar characters (sub-delims / ":" / "@") literal, matching how browsers
|
|
230
|
+
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
231
|
+
// still percent-encoded exactly as before, since browsers encode those too.
|
|
232
|
+
const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
|
|
215
233
|
export function createRoutes(routeDef, base = "") {
|
|
216
234
|
const { component, preload, children, info } = routeDef;
|
|
217
235
|
const isLeaf = !children || (Array.isArray(children) && !children.length);
|
|
@@ -228,7 +246,7 @@ export function createRoutes(routeDef, base = "") {
|
|
|
228
246
|
pattern = pattern
|
|
229
247
|
.split("/")
|
|
230
248
|
.map((s) => {
|
|
231
|
-
return s.startsWith(":") || s.startsWith("*") ? s :
|
|
249
|
+
return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
|
|
232
250
|
})
|
|
233
251
|
.join("/");
|
|
234
252
|
acc.push({
|
|
@@ -299,12 +317,21 @@ export function getRouteMatches(branches, location) {
|
|
|
299
317
|
}
|
|
300
318
|
return [];
|
|
301
319
|
}
|
|
320
|
+
function mergeParams(matches) {
|
|
321
|
+
const params = {};
|
|
322
|
+
for (let i = 0; i < matches.length; i++) {
|
|
323
|
+
Object.assign(params, matches[i].params);
|
|
324
|
+
}
|
|
325
|
+
return params;
|
|
326
|
+
}
|
|
302
327
|
function createLocation(path, state, queryWrapper) {
|
|
303
328
|
const origin = new URL(mockBase);
|
|
304
329
|
const url = createMemo((prev = origin) => {
|
|
305
330
|
const path_ = path();
|
|
306
331
|
try {
|
|
307
|
-
|
|
332
|
+
// anchor rooted paths against the origin explicitly - a path with
|
|
333
|
+
// doubled leading slashes would otherwise parse as protocol-relative
|
|
334
|
+
return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
|
|
308
335
|
}
|
|
309
336
|
catch (err) {
|
|
310
337
|
console.error(`Invalid path ${path_}`);
|
|
@@ -382,19 +409,14 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
382
409
|
}
|
|
383
410
|
return getRouteMatches(branches(), location.pathname);
|
|
384
411
|
});
|
|
385
|
-
const buildParams = () =>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
return params;
|
|
392
|
-
};
|
|
393
|
-
const params = utils.paramsWrapper
|
|
394
|
-
? utils.paramsWrapper(buildParams, branches)
|
|
395
|
-
: createMemoObject(buildParams);
|
|
412
|
+
const buildParams = () => mergeParams(matches());
|
|
413
|
+
const wrapParams = utils.paramsWrapper
|
|
414
|
+
? (getParams) => utils.paramsWrapper(getParams, branches)
|
|
415
|
+
: (getParams) => createMemoObject(getParams);
|
|
416
|
+
const params = wrapParams(buildParams);
|
|
396
417
|
const baseRoute = {
|
|
397
418
|
pattern: basePath,
|
|
419
|
+
params,
|
|
398
420
|
path: () => basePath,
|
|
399
421
|
outlet: () => null,
|
|
400
422
|
resolvePath(to) {
|
|
@@ -405,7 +427,11 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
405
427
|
base: baseRoute,
|
|
406
428
|
location,
|
|
407
429
|
params,
|
|
430
|
+
wrapParams,
|
|
408
431
|
isRouting,
|
|
432
|
+
get pendingTarget() {
|
|
433
|
+
return lastTransitionTarget;
|
|
434
|
+
},
|
|
409
435
|
renderPath,
|
|
410
436
|
parsePath,
|
|
411
437
|
navigatorFactory,
|
|
@@ -539,10 +565,14 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
539
565
|
];
|
|
540
566
|
}
|
|
541
567
|
}
|
|
542
|
-
export function createRouteContext(router, parent, outlet, match) {
|
|
543
|
-
const { base, location,
|
|
568
|
+
export function createRouteContext(router, parent, outlet, match, matches = () => [match()]) {
|
|
569
|
+
const { base, location, wrapParams } = router;
|
|
544
570
|
const { pattern, component, preload } = match().route;
|
|
545
571
|
const path = createMemo(() => match().path);
|
|
572
|
+
// Params scoped to this route's lifetime. `matches` is expected to retain
|
|
573
|
+
// its last valid value while this route is being torn down, so outgoing
|
|
574
|
+
// components and preloads never observe another route's params.
|
|
575
|
+
const params = wrapParams(() => mergeParams(matches()));
|
|
546
576
|
component &&
|
|
547
577
|
component.preload &&
|
|
548
578
|
component.preload();
|
|
@@ -552,6 +582,7 @@ export function createRouteContext(router, parent, outlet, match) {
|
|
|
552
582
|
const route = {
|
|
553
583
|
parent,
|
|
554
584
|
pattern,
|
|
585
|
+
params,
|
|
555
586
|
path,
|
|
556
587
|
outlet: () => component
|
|
557
588
|
? createComponent(component, {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Component,
|
|
1
|
+
import type { Component, Signal } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
2
3
|
declare module "@solidjs/web" {
|
|
3
4
|
interface RequestEvent {
|
|
4
5
|
response: {
|
|
@@ -55,7 +56,7 @@ export interface LocationChange<S = unknown> {
|
|
|
55
56
|
rawPath?: string;
|
|
56
57
|
}
|
|
57
58
|
export interface RouterIntegration {
|
|
58
|
-
signal:
|
|
59
|
+
signal: [get: () => LocationChange, set: (next: LocationChange) => void];
|
|
59
60
|
create?: (router: RouterContext) => void;
|
|
60
61
|
utils?: Partial<RouterUtils>;
|
|
61
62
|
}
|
|
@@ -72,12 +73,13 @@ export interface RouteSectionProps<T = unknown> {
|
|
|
72
73
|
data: T;
|
|
73
74
|
children?: JSX.Element;
|
|
74
75
|
}
|
|
75
|
-
export type
|
|
76
|
+
export type RouteSectionComponent<T = unknown> = Component<RouteSectionProps<T>> | Component<Omit<RouteSectionProps<T>, "children">> | Component<{}>;
|
|
77
|
+
export type RouteDefinition<S extends string | string[] = any, T = any> = {
|
|
76
78
|
path?: S;
|
|
77
79
|
matchFilters?: MatchFilters<S>;
|
|
78
80
|
preload?: RoutePreloadFunc<T>;
|
|
79
81
|
children?: RouteDefinition | RouteDefinition[];
|
|
80
|
-
component?:
|
|
82
|
+
component?: RouteSectionComponent<T>;
|
|
81
83
|
info?: Record<string, any>;
|
|
82
84
|
};
|
|
83
85
|
export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
|
|
@@ -118,6 +120,7 @@ export interface RouteContext {
|
|
|
118
120
|
parent?: RouteContext;
|
|
119
121
|
child?: RouteContext;
|
|
120
122
|
pattern: string;
|
|
123
|
+
params: Params;
|
|
121
124
|
path: () => string;
|
|
122
125
|
outlet: () => JSX.Element;
|
|
123
126
|
resolvePath(to: string): string | undefined;
|
|
@@ -134,8 +137,11 @@ export interface RouterContext {
|
|
|
134
137
|
base: RouteContext;
|
|
135
138
|
location: Location;
|
|
136
139
|
params: Params;
|
|
140
|
+
wrapParams: (getParams: () => Params) => Params;
|
|
137
141
|
navigatorFactory: NavigatorFactory;
|
|
138
142
|
isRouting: () => boolean;
|
|
143
|
+
/** The target of the in-flight navigation transition, if any. Not reactive. */
|
|
144
|
+
readonly pendingTarget?: LocationChange;
|
|
139
145
|
matches: () => RouteMatch[];
|
|
140
146
|
renderPath(path: string): string;
|
|
141
147
|
parsePath(str: string): string;
|
package/dist/utils.js
CHANGED
|
@@ -52,7 +52,15 @@ export function createMatcher(path, partial, matchFilters) {
|
|
|
52
52
|
const segments = pattern.split("/").filter(Boolean);
|
|
53
53
|
const len = segments.length;
|
|
54
54
|
return (location) => {
|
|
55
|
-
const locSegments = location.split("/")
|
|
55
|
+
const locSegments = location.split("/");
|
|
56
|
+
// tolerate a single leading and trailing slash, but reject empty interior
|
|
57
|
+
// segments so `/foo//bar` doesn't silently match `/foo/bar` (#567)
|
|
58
|
+
if (locSegments[0] === "")
|
|
59
|
+
locSegments.shift();
|
|
60
|
+
if (locSegments.length && locSegments[locSegments.length - 1] === "")
|
|
61
|
+
locSegments.pop();
|
|
62
|
+
if (locSegments.includes(""))
|
|
63
|
+
return null;
|
|
56
64
|
const lenDiff = locSegments.length - len;
|
|
57
65
|
if (lenDiff < 0 || (lenDiff > 0 && splat === undefined && !partial)) {
|
|
58
66
|
return null;
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"Ryan Turnquist"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"version": "0.17.0-next.
|
|
9
|
+
"version": "0.17.0-next.4",
|
|
10
10
|
"homepage": "https://github.com/solidjs/solid-router#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@solidjs/web": "2.0.0-beta.
|
|
32
|
+
"@solidjs/web": "2.0.0-beta.18",
|
|
33
33
|
"@babel/core": "^7.26.0",
|
|
34
34
|
"@babel/preset-typescript": "^7.26.0",
|
|
35
35
|
"@changesets/cli": "^2.27.10",
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
"@rollup/plugin-terser": "0.4.4",
|
|
39
39
|
"@types/jest": "^29.5.14",
|
|
40
40
|
"@types/node": "^22.10.0",
|
|
41
|
-
"babel-preset-solid": "2.0.0-beta.
|
|
41
|
+
"babel-preset-solid": "2.0.0-beta.18",
|
|
42
42
|
"jsdom": "^25.0.1",
|
|
43
43
|
"prettier": "^3.4.1",
|
|
44
44
|
"rollup": "^4.27.4",
|
|
45
|
-
"solid-js": "2.0.0-beta.
|
|
45
|
+
"solid-js": "2.0.0-beta.18",
|
|
46
46
|
"typescript": "^5.7.2",
|
|
47
47
|
"vite": "^6.0.0",
|
|
48
|
-
"vite-plugin-solid": "3.0.0-next.
|
|
48
|
+
"vite-plugin-solid": "3.0.0-next.10",
|
|
49
49
|
"vitest": "^2.1.6"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@solidjs/web": ">=2.0.0-beta.
|
|
53
|
-
"solid-js": ">=2.0.0-beta.
|
|
52
|
+
"@solidjs/web": ">=2.0.0-beta.18 <2.0.0-experimental.0",
|
|
53
|
+
"solid-js": ">=2.0.0-beta.18 <2.0.0-experimental.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "rm -rf dist && tsc && rollup -c",
|