@real-router/solid 0.3.1 → 0.4.0
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 +11 -0
- package/dist/cjs/index.js +168 -65
- package/dist/esm/index.mjs +169 -66
- package/dist/types/RouterProvider.d.ts +1 -0
- package/dist/types/RouterProvider.d.ts.map +1 -1
- package/dist/types/components/Link.d.ts.map +1 -1
- package/dist/types/components/RouteView/components.d.ts.map +1 -1
- package/dist/types/components/RouteView/helpers.d.ts +2 -4
- package/dist/types/components/RouteView/helpers.d.ts.map +1 -1
- package/dist/types/components/RouterErrorBoundary.d.ts.map +1 -1
- package/dist/types/createSignalFromSource.d.ts.map +1 -1
- package/dist/types/createStoreFromSource.d.ts.map +1 -1
- package/dist/types/dom-utils/index.d.ts +1 -1
- package/dist/types/dom-utils/index.d.ts.map +1 -1
- package/dist/types/dom-utils/link-utils.d.ts +2 -1
- package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
- package/dist/types/dom-utils/route-announcer.d.ts.map +1 -1
- package/dist/types/hooks/sharedNodeSource.d.ts +4 -0
- package/dist/types/hooks/sharedNodeSource.d.ts.map +1 -0
- package/dist/types/hooks/useRouteNode.d.ts.map +1 -1
- package/dist/types/hooks/useRouteNodeStore.d.ts.map +1 -1
- package/dist/types/hooks/useRouteStore.d.ts.map +1 -1
- package/dist/types/hooks/useRouterTransition.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/RouterProvider.tsx +1 -1
- package/src/components/Link.tsx +51 -8
- package/src/components/RouteView/RouteView.tsx +13 -13
- package/src/components/RouteView/components.tsx +12 -6
- package/src/components/RouteView/helpers.tsx +19 -17
- package/src/components/RouterErrorBoundary.tsx +4 -6
- package/src/createSignalFromSource.ts +9 -1
- package/src/createStoreFromSource.ts +1 -3
- package/src/hooks/sharedNodeSource.ts +30 -0
- package/src/hooks/useRouteNode.tsx +2 -4
- package/src/hooks/useRouteNodeStore.tsx +2 -3
- package/src/hooks/useRouteStore.tsx +0 -8
- package/src/hooks/useRouterTransition.tsx +15 -3
package/src/components/Link.tsx
CHANGED
|
@@ -9,12 +9,48 @@ import {
|
|
|
9
9
|
buildHref,
|
|
10
10
|
buildActiveClassName,
|
|
11
11
|
} from "../dom-utils/index.js";
|
|
12
|
-
import { useRouter } from "../hooks/useRouter";
|
|
13
12
|
|
|
14
13
|
import type { LinkProps } from "../types";
|
|
15
|
-
import type { Params } from "@real-router/core";
|
|
14
|
+
import type { Params, Router } from "@real-router/core";
|
|
15
|
+
import type { RouterSource } from "@real-router/sources";
|
|
16
16
|
import type { JSX } from "solid-js";
|
|
17
17
|
|
|
18
|
+
// Slow-path source cache: shared per-router, keyed by routeName + params + flags.
|
|
19
|
+
// Captured slow-path values are stable per Link (props captured at init), so the
|
|
20
|
+
// cache key is guaranteed stable for the lifetime of any consumer.
|
|
21
|
+
const activeSourceCache = new WeakMap<
|
|
22
|
+
Router,
|
|
23
|
+
Map<string, RouterSource<boolean>>
|
|
24
|
+
>();
|
|
25
|
+
|
|
26
|
+
function getOrCreateActiveSource(
|
|
27
|
+
router: Router,
|
|
28
|
+
routeName: string,
|
|
29
|
+
routeParams: Params,
|
|
30
|
+
activeStrict: boolean,
|
|
31
|
+
ignoreQueryParams: boolean,
|
|
32
|
+
): RouterSource<boolean> {
|
|
33
|
+
let perRouter = activeSourceCache.get(router);
|
|
34
|
+
|
|
35
|
+
if (!perRouter) {
|
|
36
|
+
perRouter = new Map();
|
|
37
|
+
activeSourceCache.set(router, perRouter);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const key = `${routeName}|${JSON.stringify(routeParams)}|${activeStrict}|${ignoreQueryParams}`;
|
|
41
|
+
let source = perRouter.get(key);
|
|
42
|
+
|
|
43
|
+
if (!source) {
|
|
44
|
+
source = createActiveRouteSource(router, routeName, routeParams, {
|
|
45
|
+
strict: activeStrict,
|
|
46
|
+
ignoreQueryParams,
|
|
47
|
+
});
|
|
48
|
+
perRouter.set(key, source);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return source;
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
export function Link<P extends Params = Params>(
|
|
19
55
|
props: Readonly<LinkProps<P>>,
|
|
20
56
|
): JSX.Element {
|
|
@@ -42,11 +78,15 @@ export function Link<P extends Params = Params>(
|
|
|
42
78
|
"children",
|
|
43
79
|
]);
|
|
44
80
|
|
|
45
|
-
const router = useRouter();
|
|
46
81
|
const ctx = useContext(RouterContext);
|
|
47
82
|
|
|
83
|
+
if (!ctx) {
|
|
84
|
+
throw new Error("Link must be used within a RouterProvider");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const router = ctx.router;
|
|
88
|
+
|
|
48
89
|
const useFastPath =
|
|
49
|
-
ctx?.routeSelector &&
|
|
50
90
|
!local.activeStrict &&
|
|
51
91
|
local.ignoreQueryParams &&
|
|
52
92
|
local.routeParams === EMPTY_PARAMS;
|
|
@@ -54,10 +94,13 @@ export function Link<P extends Params = Params>(
|
|
|
54
94
|
const isActive = useFastPath
|
|
55
95
|
? () => ctx.routeSelector(local.routeName)
|
|
56
96
|
: createSignalFromSource(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
getOrCreateActiveSource(
|
|
98
|
+
router,
|
|
99
|
+
local.routeName,
|
|
100
|
+
local.routeParams,
|
|
101
|
+
local.activeStrict,
|
|
102
|
+
local.ignoreQueryParams,
|
|
103
|
+
),
|
|
61
104
|
);
|
|
62
105
|
|
|
63
106
|
const href = createMemo(() =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { children as resolveChildren } from "solid-js";
|
|
1
|
+
import { children as resolveChildren, createMemo } from "solid-js";
|
|
2
2
|
|
|
3
3
|
import { Match, NotFound } from "./components";
|
|
4
4
|
import { buildRenderList, collectElements } from "./helpers";
|
|
@@ -13,6 +13,14 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
|
|
|
13
13
|
|
|
14
14
|
const resolved = resolveChildren(() => props.children);
|
|
15
15
|
|
|
16
|
+
const elements = createMemo(() => {
|
|
17
|
+
const arr: RouteViewMarker[] = [];
|
|
18
|
+
|
|
19
|
+
collectElements(resolved(), arr);
|
|
20
|
+
|
|
21
|
+
return arr;
|
|
22
|
+
});
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
25
|
<>
|
|
18
26
|
{(() => {
|
|
@@ -22,24 +30,16 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
|
|
|
22
30
|
return null;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
collectElements(resolved(), elements);
|
|
28
|
-
|
|
29
|
-
const { rendered } = buildRenderList(
|
|
30
|
-
elements,
|
|
33
|
+
const rendered = buildRenderList(
|
|
34
|
+
elements(),
|
|
31
35
|
state.route.name,
|
|
32
36
|
props.nodeName,
|
|
33
37
|
);
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
return rendered;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return null;
|
|
39
|
+
return rendered.length > 0 ? rendered : null;
|
|
40
40
|
})()}
|
|
41
41
|
</>
|
|
42
|
-
)
|
|
42
|
+
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
RouteViewRoot.displayName = "RouteView";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { MatchProps, NotFoundProps } from "./types";
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Local (non-global) Symbols — Symbol.for() would expose markers to spoofing
|
|
5
|
+
// via the global Symbol registry. See Gotchas section "RouteView Marker Objects".
|
|
6
|
+
export const MATCH_MARKER = Symbol("RouteView.Match");
|
|
5
7
|
|
|
6
|
-
export const NOT_FOUND_MARKER = Symbol
|
|
8
|
+
export const NOT_FOUND_MARKER = Symbol("RouteView.NotFound");
|
|
7
9
|
|
|
8
10
|
export interface MatchMarker {
|
|
9
11
|
$$type: typeof MATCH_MARKER;
|
|
@@ -21,7 +23,7 @@ export interface NotFoundMarker {
|
|
|
21
23
|
export type RouteViewMarker = MatchMarker | NotFoundMarker;
|
|
22
24
|
|
|
23
25
|
export function Match(props: MatchProps): JSX.Element {
|
|
24
|
-
const result = {
|
|
26
|
+
const result: MatchMarker = {
|
|
25
27
|
$$type: MATCH_MARKER,
|
|
26
28
|
segment: props.segment,
|
|
27
29
|
exact: props.exact ?? false,
|
|
@@ -29,21 +31,25 @@ export function Match(props: MatchProps): JSX.Element {
|
|
|
29
31
|
get children(): JSX.Element {
|
|
30
32
|
return props.children;
|
|
31
33
|
},
|
|
32
|
-
}
|
|
34
|
+
};
|
|
33
35
|
|
|
36
|
+
// Marker object is identified by $$type Symbol in RouteView/helpers.tsx,
|
|
37
|
+
// not rendered as JSX. Cast required because JSX.Element does not include
|
|
38
|
+
// arbitrary marker shapes.
|
|
34
39
|
return result as unknown as JSX.Element;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
Match.displayName = "RouteView.Match";
|
|
38
43
|
|
|
39
44
|
export function NotFound(props: NotFoundProps): JSX.Element {
|
|
40
|
-
const result = {
|
|
45
|
+
const result: NotFoundMarker = {
|
|
41
46
|
$$type: NOT_FOUND_MARKER,
|
|
42
47
|
get children(): JSX.Element {
|
|
43
48
|
return props.children;
|
|
44
49
|
},
|
|
45
|
-
}
|
|
50
|
+
};
|
|
46
51
|
|
|
52
|
+
// See Match for the marker-pattern rationale.
|
|
47
53
|
return result as unknown as JSX.Element;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "./components";
|
|
12
12
|
import type { JSX } from "solid-js";
|
|
13
13
|
|
|
14
|
-
function isSegmentMatch(
|
|
14
|
+
export function isSegmentMatch(
|
|
15
15
|
routeName: string,
|
|
16
16
|
fullSegmentName: string,
|
|
17
17
|
exact: boolean,
|
|
@@ -66,7 +66,7 @@ export function buildRenderList(
|
|
|
66
66
|
elements: RouteViewMarker[],
|
|
67
67
|
routeName: string,
|
|
68
68
|
nodeName: string,
|
|
69
|
-
):
|
|
69
|
+
): JSX.Element[] {
|
|
70
70
|
let notFoundChildren: JSX.Element | null = null;
|
|
71
71
|
let activeMatchFound = false;
|
|
72
72
|
const rendered: JSX.Element[] = [];
|
|
@@ -77,23 +77,25 @@ export function buildRenderList(
|
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (activeMatchFound) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
const { segment, exact, fallback } = child;
|
|
81
85
|
const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (isActive) {
|
|
86
|
-
activeMatchFound = true;
|
|
87
|
-
const matchContent = child.children;
|
|
88
|
-
const content =
|
|
89
|
-
fallback === undefined ? (
|
|
90
|
-
matchContent
|
|
91
|
-
) : (
|
|
92
|
-
<Suspense fallback={fallback}>{matchContent}</Suspense>
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
rendered.push(content);
|
|
86
|
+
|
|
87
|
+
if (!isSegmentMatch(routeName, fullSegmentName, exact)) {
|
|
88
|
+
continue;
|
|
96
89
|
}
|
|
90
|
+
|
|
91
|
+
activeMatchFound = true;
|
|
92
|
+
rendered.push(
|
|
93
|
+
fallback === undefined ? (
|
|
94
|
+
child.children
|
|
95
|
+
) : (
|
|
96
|
+
<Suspense fallback={fallback}>{child.children}</Suspense>
|
|
97
|
+
),
|
|
98
|
+
);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
if (
|
|
@@ -104,5 +106,5 @@ export function buildRenderList(
|
|
|
104
106
|
rendered.push(notFoundChildren);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
return
|
|
109
|
+
return rendered;
|
|
108
110
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, createMemo, createSignal } from "solid-js";
|
|
1
|
+
import { createEffect, createMemo, createSignal, Show } from "solid-js";
|
|
2
2
|
|
|
3
3
|
import { useRouterError } from "../hooks/useRouterError";
|
|
4
4
|
|
|
@@ -43,11 +43,9 @@ export function RouterErrorBoundary(
|
|
|
43
43
|
return (
|
|
44
44
|
<>
|
|
45
45
|
{props.children}
|
|
46
|
-
{(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return error ? props.fallback(error, resetError) : null;
|
|
50
|
-
})()}
|
|
46
|
+
<Show when={visibleError()}>
|
|
47
|
+
{(error) => props.fallback(error(), resetError)}
|
|
48
|
+
</Show>
|
|
51
49
|
</>
|
|
52
50
|
);
|
|
53
51
|
}
|
|
@@ -8,10 +8,18 @@ export function createSignalFromSource<T>(
|
|
|
8
8
|
): Accessor<T> {
|
|
9
9
|
const [value, setValue] = createSignal<T>(source.getSnapshot());
|
|
10
10
|
|
|
11
|
+
const sync = (): T => source.getSnapshot();
|
|
12
|
+
|
|
11
13
|
const unsubscribe = source.subscribe(() => {
|
|
12
|
-
setValue(
|
|
14
|
+
setValue(sync);
|
|
13
15
|
});
|
|
14
16
|
|
|
17
|
+
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
18
|
+
// onFirstSubscribe (when reused after disconnect via cache). Listener is not
|
|
19
|
+
// notified for that internal update, so we must sync the signal manually.
|
|
20
|
+
// No-op when snapshot is unchanged (signal equality check).
|
|
21
|
+
setValue(sync);
|
|
22
|
+
|
|
15
23
|
onCleanup(() => {
|
|
16
24
|
unsubscribe();
|
|
17
25
|
});
|
|
@@ -6,9 +6,7 @@ import type { RouterSource } from "@real-router/sources";
|
|
|
6
6
|
export function createStoreFromSource<T extends object>(
|
|
7
7
|
source: RouterSource<T>,
|
|
8
8
|
): T {
|
|
9
|
-
const [state, setState] = createStore<T>(
|
|
10
|
-
structuredClone(source.getSnapshot()),
|
|
11
|
-
);
|
|
9
|
+
const [state, setState] = createStore<T>({ ...source.getSnapshot() });
|
|
12
10
|
|
|
13
11
|
const unsubscribe = source.subscribe(() => {
|
|
14
12
|
setState(reconcile(source.getSnapshot()));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRouteNodeSource } from "@real-router/sources";
|
|
2
|
+
|
|
3
|
+
import type { Router } from "@real-router/core";
|
|
4
|
+
import type { RouteNodeSnapshot, RouterSource } from "@real-router/sources";
|
|
5
|
+
|
|
6
|
+
const cache = new WeakMap<
|
|
7
|
+
Router,
|
|
8
|
+
Map<string, RouterSource<RouteNodeSnapshot>>
|
|
9
|
+
>();
|
|
10
|
+
|
|
11
|
+
export function getOrCreateNodeSource(
|
|
12
|
+
router: Router,
|
|
13
|
+
nodeName: string,
|
|
14
|
+
): RouterSource<RouteNodeSnapshot> {
|
|
15
|
+
let perRouter = cache.get(router);
|
|
16
|
+
|
|
17
|
+
if (!perRouter) {
|
|
18
|
+
perRouter = new Map();
|
|
19
|
+
cache.set(router, perRouter);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let source = perRouter.get(nodeName);
|
|
23
|
+
|
|
24
|
+
if (!source) {
|
|
25
|
+
source = createRouteNodeSource(router, nodeName);
|
|
26
|
+
perRouter.set(nodeName, source);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createRouteNodeSource } from "@real-router/sources";
|
|
2
|
-
|
|
3
1
|
import { createSignalFromSource } from "../createSignalFromSource";
|
|
2
|
+
import { getOrCreateNodeSource } from "./sharedNodeSource";
|
|
4
3
|
import { useRouter } from "./useRouter";
|
|
5
4
|
|
|
6
5
|
import type { RouteState } from "../types";
|
|
@@ -8,7 +7,6 @@ import type { Accessor } from "solid-js";
|
|
|
8
7
|
|
|
9
8
|
export function useRouteNode(nodeName: string): Accessor<RouteState> {
|
|
10
9
|
const router = useRouter();
|
|
11
|
-
const store = createRouteNodeSource(router, nodeName);
|
|
12
10
|
|
|
13
|
-
return createSignalFromSource(
|
|
11
|
+
return createSignalFromSource(getOrCreateNodeSource(router, nodeName));
|
|
14
12
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createRouteNodeSource } from "@real-router/sources";
|
|
2
|
-
|
|
3
1
|
import { createStoreFromSource } from "../createStoreFromSource";
|
|
2
|
+
import { getOrCreateNodeSource } from "./sharedNodeSource";
|
|
4
3
|
import { useRouter } from "./useRouter";
|
|
5
4
|
|
|
6
5
|
import type { RouteState } from "../types";
|
|
@@ -8,5 +7,5 @@ import type { RouteState } from "../types";
|
|
|
8
7
|
export function useRouteNodeStore(nodeName: string): RouteState {
|
|
9
8
|
const router = useRouter();
|
|
10
9
|
|
|
11
|
-
return createStoreFromSource(
|
|
10
|
+
return createStoreFromSource(getOrCreateNodeSource(router, nodeName));
|
|
12
11
|
}
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { createRouteSource } from "@real-router/sources";
|
|
2
|
-
import { useContext } from "solid-js";
|
|
3
2
|
|
|
4
|
-
import { RouteContext } from "../context";
|
|
5
3
|
import { createStoreFromSource } from "../createStoreFromSource";
|
|
6
4
|
import { useRouter } from "./useRouter";
|
|
7
5
|
|
|
8
6
|
import type { RouteState } from "../types";
|
|
9
7
|
|
|
10
8
|
export function useRouteStore(): RouteState {
|
|
11
|
-
const ctx = useContext(RouteContext);
|
|
12
|
-
|
|
13
|
-
if (!ctx) {
|
|
14
|
-
throw new Error("useRouteStore must be used within a RouterProvider");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
9
|
const router = useRouter();
|
|
18
10
|
|
|
19
11
|
return createStoreFromSource(createRouteSource(router));
|
|
@@ -3,12 +3,24 @@ import { createTransitionSource } from "@real-router/sources";
|
|
|
3
3
|
import { createSignalFromSource } from "../createSignalFromSource";
|
|
4
4
|
import { useRouter } from "./useRouter";
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { Router } from "@real-router/core";
|
|
7
|
+
import type {
|
|
8
|
+
RouterSource,
|
|
9
|
+
RouterTransitionSnapshot,
|
|
10
|
+
} from "@real-router/sources";
|
|
7
11
|
import type { Accessor } from "solid-js";
|
|
8
12
|
|
|
13
|
+
const cache = new WeakMap<Router, RouterSource<RouterTransitionSnapshot>>();
|
|
14
|
+
|
|
9
15
|
export function useRouterTransition(): Accessor<RouterTransitionSnapshot> {
|
|
10
16
|
const router = useRouter();
|
|
11
|
-
const store = createTransitionSource(router);
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
let source = cache.get(router);
|
|
19
|
+
|
|
20
|
+
if (!source) {
|
|
21
|
+
source = createTransitionSource(router);
|
|
22
|
+
cache.set(router, source);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return createSignalFromSource(source);
|
|
14
26
|
}
|