@octanejs/tanstack-router 0.1.8 → 0.1.10
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/package.json +4 -4
- package/src/Await.tsrx +7 -2
- package/src/CatchBoundary.tsrx +21 -4
- package/src/CatchBoundary.tsrx.d.ts +10 -4
- package/src/ClientOnly.tsrx +2 -1
- package/src/Link.tsrx +4 -1
- package/src/Match.tsrx +24 -19
- package/src/MatchRoute.tsrx +6 -3
- package/src/Navigate.tsrx +2 -1
- package/src/Outlet.tsrx +6 -4
- package/src/RouteNotFound.tsrx +1 -1
- package/src/RouterProvider.tsrx +12 -2
- package/src/RouterProvider.tsrx.d.ts +9 -5
- package/src/SafeFragment.tsrx +4 -1
- package/src/index.ts +12 -0
- package/src/not-found.tsrx +17 -4
- package/src/not-found.tsrx.d.ts +5 -2
- package/src/route.ts +41 -0
- package/src/useBlocker.tsrx +73 -8
- package/src/useBlocker.tsrx.d.ts +58 -2
- package/src/useStore.ts +17 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"@tanstack/store": "^0.9.3"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"octane": "0.1.
|
|
46
|
+
"octane": "0.1.11"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@tanstack/react-router": "1.170.16",
|
|
50
|
-
"@tsrx/react": "^0.2.
|
|
50
|
+
"@tsrx/react": "^0.2.44",
|
|
51
51
|
"esbuild": "^0.28.1",
|
|
52
52
|
"react": "^19.2.0",
|
|
53
53
|
"react-dom": "^19.2.0",
|
|
54
54
|
"vitest": "^4.1.9",
|
|
55
|
-
"octane": "0.1.
|
|
55
|
+
"octane": "0.1.11"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"test": "vitest run"
|
package/src/Await.tsrx
CHANGED
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
// `render` prop (re-passing it as `{children}` would be an identifier, not a literal
|
|
5
5
|
// render-prop, so the consumer's `<Await>{(d) => …}</Await>` stays the public API).
|
|
6
6
|
import { use } from 'octane';
|
|
7
|
+
import type { OctaneNode } from 'octane';
|
|
7
8
|
|
|
8
|
-
function AwaitInner(props) {
|
|
9
|
+
function AwaitInner(props: { promise: Promise<any>; render: (data: any) => OctaneNode }) {
|
|
9
10
|
const data = use(props.promise);
|
|
10
11
|
return props.render(data);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function Await(props
|
|
14
|
+
export function Await(props: {
|
|
15
|
+
promise: Promise<any>;
|
|
16
|
+
fallback?: OctaneNode;
|
|
17
|
+
children: (data: any) => OctaneNode;
|
|
18
|
+
}) @{
|
|
14
19
|
@try {
|
|
15
20
|
<AwaitInner promise={props.promise} render={props.children} />
|
|
16
21
|
} @pending {
|
package/src/CatchBoundary.tsrx
CHANGED
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
// commit later, same observable outcome). Match passes the router's `loadedAt`
|
|
15
15
|
// so navigation clears route error UI.
|
|
16
16
|
import { useState, useRef, useLayoutEffect } from 'octane';
|
|
17
|
+
import type { OctaneNode } from 'octane';
|
|
18
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route.ts';
|
|
17
19
|
|
|
18
|
-
export function CatchBoundary(props
|
|
20
|
+
export function CatchBoundary(props: {
|
|
21
|
+
getResetKey: () => number | string;
|
|
22
|
+
children: OctaneNode;
|
|
23
|
+
errorComponent?: ErrorRouteComponent;
|
|
24
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
25
|
+
}) @{
|
|
19
26
|
@try {
|
|
20
27
|
<>
|
|
21
28
|
{props.children}
|
|
@@ -31,10 +38,16 @@ export function CatchBoundary(props) @{
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
function CatchErrorView(props
|
|
41
|
+
function CatchErrorView(props: {
|
|
42
|
+
error: any;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
getResetKey?: () => number | string;
|
|
45
|
+
errorComponent?: ErrorRouteComponent;
|
|
46
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
47
|
+
}) @{
|
|
35
48
|
// componentDidCatch parity — once per distinct caught error, during render so a
|
|
36
49
|
// rethrow (the notFound-forwarding path) escapes to the parent boundary.
|
|
37
|
-
const reported = useRef(null);
|
|
50
|
+
const reported = useRef<unknown>(null);
|
|
38
51
|
if (reported.current !== props.error) {
|
|
39
52
|
reported.current = props.error;
|
|
40
53
|
if (props.onCatch) props.onCatch(props.error, { componentStack: '' });
|
|
@@ -60,7 +73,11 @@ function CatchErrorView(props) @{
|
|
|
60
73
|
// The default error UI, markup-identical to react-router's ErrorComponent (string
|
|
61
74
|
// styles serialize to the same inline css React produces, so the differential rig
|
|
62
75
|
// can byte-compare it).
|
|
63
|
-
export function ErrorComponent(props
|
|
76
|
+
export function ErrorComponent(props: {
|
|
77
|
+
error: any;
|
|
78
|
+
reset?: () => void;
|
|
79
|
+
info?: { componentStack?: string };
|
|
80
|
+
}) @{
|
|
64
81
|
const [show, setShow] = useState(process.env.NODE_ENV !== 'production');
|
|
65
82
|
|
|
66
83
|
<div style="padding:.5rem;max-width:100%">
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// Type declaration for the .tsrx components (resolved by relative path).
|
|
2
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route';
|
|
3
|
+
|
|
2
4
|
export declare const CatchBoundary: (props: {
|
|
3
|
-
getResetKey
|
|
4
|
-
errorComponent?:
|
|
5
|
-
onCatch?: (error:
|
|
5
|
+
getResetKey: () => number | string;
|
|
6
|
+
errorComponent?: ErrorRouteComponent;
|
|
7
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
6
8
|
children?: unknown;
|
|
7
9
|
}) => unknown;
|
|
8
|
-
export declare const ErrorComponent: (props: {
|
|
10
|
+
export declare const ErrorComponent: (props: {
|
|
11
|
+
error: any;
|
|
12
|
+
reset?: () => void;
|
|
13
|
+
info?: { componentStack?: string };
|
|
14
|
+
}) => unknown;
|
package/src/ClientOnly.tsrx
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// compiler slots the hook calls (and withSlot-disambiguates useHydrated's
|
|
7
7
|
// call sites).
|
|
8
8
|
import { useSyncExternalStore } from 'octane';
|
|
9
|
+
import type { OctaneNode } from 'octane';
|
|
9
10
|
|
|
10
11
|
const subscribe = () => () => {};
|
|
11
12
|
|
|
@@ -13,7 +14,7 @@ export function useHydrated() {
|
|
|
13
14
|
return useSyncExternalStore(subscribe, () => true, () => false);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function ClientOnly(props) @{
|
|
17
|
+
export function ClientOnly(props: { children?: OctaneNode; fallback?: OctaneNode }) @{
|
|
17
18
|
const hydrated = useHydrated();
|
|
18
19
|
<>
|
|
19
20
|
{hydrated ? props.children : props.fallback ?? null}
|
package/src/Link.tsrx
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
import { isChildrenBlock } from 'octane';
|
|
12
12
|
import { useLinkProps } from './link.ts';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Props are permissive (`any`) at the implementation boundary — upstream's Link
|
|
15
|
+
// implementation is likewise `(props: any)` behind the generic `LinkComponent<'a'>`
|
|
16
|
+
// facade; the full type-safe `to`/`params`/`search` surface is a follow-up.
|
|
17
|
+
export function Link(props: any) @{
|
|
15
18
|
const { _asChild: AsChild, children, ...rest } = props;
|
|
16
19
|
const linkProps = useLinkProps(rest);
|
|
17
20
|
const { disabled: _disabled, ...aProps } = linkProps;
|
package/src/Match.tsrx
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
isNotFound,
|
|
25
25
|
rootRouteId,
|
|
26
26
|
} from '@tanstack/router-core';
|
|
27
|
+
import type { NotFoundError, ParsedLocation, RootRouteOptions } from '@tanstack/router-core';
|
|
28
|
+
import type { ErrorInfo, ErrorRouteComponent } from './route.ts';
|
|
27
29
|
import { useStore } from './useStore.ts';
|
|
28
30
|
import { useRouter, matchContext } from './context.ts';
|
|
29
31
|
import { Outlet } from './Outlet.tsrx';
|
|
@@ -32,11 +34,11 @@ import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
|
32
34
|
import { CatchNotFound } from './not-found.tsrx';
|
|
33
35
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
34
36
|
|
|
35
|
-
export function Match(props) @{
|
|
37
|
+
export function Match(props: { matchId: string }) @{
|
|
36
38
|
const router = useRouter();
|
|
37
|
-
const matchStore = router.stores.matchStores.get(props.matchId)
|
|
39
|
+
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
38
40
|
const resetKey = useStore(router.stores.loadedAt, (l) => l);
|
|
39
|
-
const routeId = useStore(matchStore, (m) => m.routeId);
|
|
41
|
+
const routeId = useStore(matchStore, (m) => m.routeId as string);
|
|
40
42
|
const route = router.routesById[routeId];
|
|
41
43
|
|
|
42
44
|
const PendingComponent = route.options.pendingComponent ?? router.options.defaultPendingComponent;
|
|
@@ -50,12 +52,15 @@ export function Match(props) @{
|
|
|
50
52
|
// Suspense boundary when explicitly opted in via wrapInSuspense.
|
|
51
53
|
const SuspenseWrap =
|
|
52
54
|
(!route.isRoot || route.options.wrapInSuspense) &&
|
|
53
|
-
(route.options.wrapInSuspense ?? PendingComponent ??
|
|
55
|
+
(route.options.wrapInSuspense ?? PendingComponent ??
|
|
56
|
+
(routeErrorComponent as ErrorRouteComponent | undefined)?.preload)
|
|
54
57
|
? Suspense
|
|
55
58
|
: SafeFragment;
|
|
56
59
|
const CatchWrap = routeErrorComponent ? CatchBoundary : SafeFragment;
|
|
57
60
|
const NotFoundWrap = routeNotFoundComponent ? CatchNotFound : SafeFragment;
|
|
58
|
-
const ShellComponent = route.isRoot
|
|
61
|
+
const ShellComponent = route.isRoot
|
|
62
|
+
? (route.options as RootRouteOptions).shellComponent ?? SafeFragment
|
|
63
|
+
: SafeFragment;
|
|
59
64
|
|
|
60
65
|
const pendingElement =
|
|
61
66
|
PendingComponent ? createElement(PendingComponent, {}) : null;
|
|
@@ -67,7 +72,7 @@ export function Match(props) @{
|
|
|
67
72
|
<CatchWrap
|
|
68
73
|
getResetKey={() => resetKey}
|
|
69
74
|
errorComponent={routeErrorComponent || ErrorComponent}
|
|
70
|
-
onCatch={(error, errorInfo) => {
|
|
75
|
+
onCatch={(error: Error, errorInfo: ErrorInfo) => {
|
|
71
76
|
if (isNotFound(error)) {
|
|
72
77
|
error.routeId ??= routeId;
|
|
73
78
|
throw error;
|
|
@@ -76,7 +81,7 @@ export function Match(props) @{
|
|
|
76
81
|
}}
|
|
77
82
|
>
|
|
78
83
|
<NotFoundWrap
|
|
79
|
-
fallback={(error) => {
|
|
84
|
+
fallback={(error: NotFoundError) => {
|
|
80
85
|
error.routeId ??= routeId;
|
|
81
86
|
|
|
82
87
|
if (
|
|
@@ -85,7 +90,7 @@ export function Match(props) @{
|
|
|
85
90
|
) {
|
|
86
91
|
throw error;
|
|
87
92
|
}
|
|
88
|
-
return createElement(routeNotFoundComponent, error);
|
|
93
|
+
return createElement(routeNotFoundComponent, error as any);
|
|
89
94
|
}}
|
|
90
95
|
>
|
|
91
96
|
<MatchInner matchId={props.matchId} />
|
|
@@ -99,24 +104,24 @@ export function Match(props) @{
|
|
|
99
104
|
</ShellComponent>
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
function MatchInner(props) {
|
|
107
|
+
function MatchInner(props: { matchId: string }) {
|
|
103
108
|
const router = useRouter();
|
|
104
|
-
const matchStore = router.stores.matchStores.get(props.matchId)
|
|
109
|
+
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
105
110
|
const match = useStore(matchStore, (m) => m);
|
|
106
|
-
const routeId = match.routeId;
|
|
111
|
+
const routeId = match.routeId as string;
|
|
107
112
|
const route = router.routesById[routeId];
|
|
108
113
|
|
|
109
114
|
// The live match in the router (if still mounted there) wins over the
|
|
110
115
|
// snapshot for promise lookups, per upstream getMatchPromise.
|
|
111
|
-
const getMatchPromise = (key
|
|
112
|
-
match._nonReactive[key];
|
|
116
|
+
const getMatchPromise = (key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise') =>
|
|
117
|
+
router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key];
|
|
113
118
|
|
|
114
119
|
if (match._displayPending) {
|
|
115
|
-
use(getMatchPromise('displayPendingPromise'));
|
|
120
|
+
use(getMatchPromise('displayPendingPromise')!);
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
if (match._forcePending) {
|
|
119
|
-
use(getMatchPromise('minPendingPromise'));
|
|
124
|
+
use(getMatchPromise('minPendingPromise')!);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
if (match.status === 'pending') {
|
|
@@ -125,7 +130,7 @@ function MatchInner(props) {
|
|
|
125
130
|
if (pendingMinMs) {
|
|
126
131
|
const routerMatch = router.getMatch(match.id);
|
|
127
132
|
if (routerMatch && !routerMatch._nonReactive.minPendingPromise) {
|
|
128
|
-
const minPendingPromise = createControlledPromise();
|
|
133
|
+
const minPendingPromise = createControlledPromise<void>();
|
|
129
134
|
routerMatch._nonReactive.minPendingPromise = minPendingPromise;
|
|
130
135
|
setTimeout(() => {
|
|
131
136
|
minPendingPromise.resolve();
|
|
@@ -133,7 +138,7 @@ function MatchInner(props) {
|
|
|
133
138
|
}, pendingMinMs);
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
|
-
use(getMatchPromise('loadPromise'));
|
|
141
|
+
use(getMatchPromise('loadPromise')!);
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
if (match.status === 'notFound') {
|
|
@@ -143,7 +148,7 @@ function MatchInner(props) {
|
|
|
143
148
|
if (match.status === 'redirected') {
|
|
144
149
|
// Observed mid-transition while a redirect is in flight — suspend on the
|
|
145
150
|
// load so this stale render is abandoned and the redirect completes.
|
|
146
|
-
use(getMatchPromise('loadPromise'));
|
|
151
|
+
use(getMatchPromise('loadPromise')!);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
if (match.status === 'error') {
|
|
@@ -174,7 +179,7 @@ function MatchInner(props) {
|
|
|
174
179
|
// `resolvedLocation` to the new location.
|
|
175
180
|
function OnRendered() @{
|
|
176
181
|
const router = useRouter();
|
|
177
|
-
const prevResolvedLocationRef = useRef(undefined);
|
|
182
|
+
const prevResolvedLocationRef = useRef<ParsedLocation<any> | undefined>(undefined);
|
|
178
183
|
const renderedLocationKey = useStore(
|
|
179
184
|
router.stores.resolvedLocation,
|
|
180
185
|
(loc) => loc?.state.__TSR_key,
|
package/src/MatchRoute.tsrx
CHANGED
|
@@ -11,13 +11,16 @@ import { useStore } from './useStore.ts';
|
|
|
11
11
|
export function useMatchRoute() {
|
|
12
12
|
const router = useRouter();
|
|
13
13
|
useStore(router.stores.matchRouteDeps, (d) => d);
|
|
14
|
-
return useCallback((opts) => {
|
|
14
|
+
return useCallback((opts: Record<string, any>) => {
|
|
15
15
|
const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts;
|
|
16
|
-
return router.matchRoute(rest, { pending, caseSensitive, fuzzy, includeSearch });
|
|
16
|
+
return router.matchRoute(rest as any, { pending, caseSensitive, fuzzy, includeSearch });
|
|
17
17
|
}, [router]);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Props are permissive (`Record<string, any>` + optional render-prop children) —
|
|
21
|
+
// upstream's MakeMatchRouteOptions generics are the type-safe facade; the octane
|
|
22
|
+
// binding keeps the loose v1 surface (see Link).
|
|
23
|
+
export function MatchRoute(props: Record<string, any>) @{
|
|
21
24
|
const matchRoute = useMatchRoute();
|
|
22
25
|
const params = matchRoute(props);
|
|
23
26
|
const out =
|
package/src/Navigate.tsrx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Imperative navigation as a component: navigates once on mount, renders nothing.
|
|
2
2
|
import { useLayoutEffect, useRef } from 'octane';
|
|
3
|
+
import type { NavigateOptions } from '@tanstack/router-core';
|
|
3
4
|
import { useNavigate } from './hooks.ts';
|
|
4
5
|
|
|
5
|
-
export function Navigate(props) @{
|
|
6
|
+
export function Navigate(props: NavigateOptions) @{
|
|
6
7
|
const navigate = useNavigate();
|
|
7
8
|
const done = useRef(false);
|
|
8
9
|
|
package/src/Outlet.tsrx
CHANGED
|
@@ -19,9 +19,11 @@ import { SafeFragment } from './SafeFragment.tsrx';
|
|
|
19
19
|
|
|
20
20
|
export function Outlet() @{
|
|
21
21
|
const router = useRouter();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
22
|
+
// Outlet only renders inside a match's component, so the context id (and its
|
|
23
|
+
// pooled store) are present — same invariant upstream asserts with invariant().
|
|
24
|
+
const parentId = useContext(matchContext)!;
|
|
25
|
+
const parentStore = router.stores.matchStores.get(parentId)!;
|
|
26
|
+
const parentRouteId = useStore(parentStore, (m) => m?.routeId as string | undefined);
|
|
25
27
|
const globalNotFound = useStore(parentStore, (m) => m?.globalNotFound ?? false);
|
|
26
28
|
const childId = useStore(router.stores.matchesId, (ids) => {
|
|
27
29
|
const i = ids.indexOf(parentId);
|
|
@@ -38,7 +40,7 @@ export function Outlet() @{
|
|
|
38
40
|
DefaultPending ? createElement(DefaultPending, {}) : null;
|
|
39
41
|
|
|
40
42
|
@if (globalNotFound) {
|
|
41
|
-
<RouteNotFound routeId={parentRouteId} />
|
|
43
|
+
<RouteNotFound routeId={parentRouteId!} />
|
|
42
44
|
} @else {
|
|
43
45
|
@if (childId) {
|
|
44
46
|
<RootSuspense fallback={pendingElement}>
|
package/src/RouteNotFound.tsrx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `notFound({ data })` payload arrives as the `data` prop.
|
|
11
11
|
import { useRouter } from './context.ts';
|
|
12
12
|
|
|
13
|
-
export function RouteNotFound(props) @{
|
|
13
|
+
export function RouteNotFound(props: { routeId: string; error?: any }) @{
|
|
14
14
|
const router = useRouter();
|
|
15
15
|
const route = router.routesById[props.routeId];
|
|
16
16
|
const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
|
package/src/RouterProvider.tsrx
CHANGED
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
// instance), wraps in `router.options.Wrap` when configured, and provides the
|
|
6
6
|
// router; `RouterProvider` renders `<Matches/>` inside it.
|
|
7
7
|
import { hasKeys } from '@tanstack/router-core';
|
|
8
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
9
|
+
import type { OctaneNode } from 'octane';
|
|
8
10
|
import { routerContext } from './context.ts';
|
|
9
11
|
import { Matches } from './Matches.tsrx';
|
|
10
12
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
// Upstream's `RouterProps` is `Omit<RouterOptions<…>, 'context'> & { router }` —
|
|
15
|
+
// extra props reconfigure the instance via `router.update()`. The octane binding
|
|
16
|
+
// keeps the router-typed member strict and the option passthrough permissive.
|
|
17
|
+
export type RouterProps = {
|
|
18
|
+
router: AnyRouter;
|
|
19
|
+
context?: Record<string, any>;
|
|
20
|
+
} & Record<string, any>;
|
|
21
|
+
|
|
22
|
+
export function RouterContextProvider(props: RouterProps & { children?: OctaneNode }) @{
|
|
13
23
|
const { router, children, ...rest } = props;
|
|
14
24
|
if (hasKeys(rest)) {
|
|
15
25
|
router.update({
|
|
@@ -28,7 +38,7 @@ export function RouterContextProvider(props) @{
|
|
|
28
38
|
</Wrap>
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
export function RouterProvider(props) @{
|
|
41
|
+
export function RouterProvider(props: RouterProps) @{
|
|
32
42
|
const { router, ...rest } = props;
|
|
33
43
|
|
|
34
44
|
<RouterContextProvider router={router} {...rest}>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Type declaration for the .tsrx component (resolved by relative path).
|
|
2
2
|
import type { AnyRouter } from '@tanstack/router-core';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export type RouterProps = {
|
|
5
|
+
router: AnyRouter;
|
|
6
|
+
context?: Record<string, any>;
|
|
7
|
+
} & Record<string, any>;
|
|
8
|
+
|
|
9
|
+
export declare const RouterProvider: (props: RouterProps) => unknown;
|
|
10
|
+
export declare const RouterContextProvider: (
|
|
11
|
+
props: RouterProps & { children?: unknown },
|
|
12
|
+
) => unknown;
|
package/src/SafeFragment.tsrx
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// — crucially — means a route WITHOUT a pendingComponent/errorComponent does not
|
|
6
6
|
// create a boundary at all, so suspensions and errors bubble to the nearest
|
|
7
7
|
// ancestor that has one (react-router parity).
|
|
8
|
-
|
|
8
|
+
// Props are `any` (upstream parity: react-router's `SafeFragment(props: any)`) —
|
|
9
|
+
// it stands in for Suspense/CatchBoundary/CatchNotFound/shellComponent at dynamic
|
|
10
|
+
// boundary slots, so it must accept any boundary's props and render only children.
|
|
11
|
+
export function SafeFragment(props: any) @{
|
|
9
12
|
<>
|
|
10
13
|
{props.children}
|
|
11
14
|
</>
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,16 @@ export {
|
|
|
54
54
|
RootRoute,
|
|
55
55
|
RouteApi,
|
|
56
56
|
} from './route';
|
|
57
|
+
// Framework-facing component types (react-router parity, on octane renderables).
|
|
58
|
+
// route.ts also narrows router-core's *Extensions interfaces to these via module
|
|
59
|
+
// augmentation — mirroring upstream's route.tsx/router.tsx `declare module`.
|
|
60
|
+
export type {
|
|
61
|
+
SyncRouteComponent,
|
|
62
|
+
AsyncRouteComponent,
|
|
63
|
+
RouteComponent,
|
|
64
|
+
ErrorRouteComponent,
|
|
65
|
+
NotFoundRouteComponent,
|
|
66
|
+
} from './route';
|
|
57
67
|
export { routerContext, getRouterContext, matchContext, useRouter } from './context';
|
|
58
68
|
export { useStore } from './useStore';
|
|
59
69
|
export { useRouterState } from './useRouterState';
|
|
@@ -74,11 +84,13 @@ export {
|
|
|
74
84
|
export { useAwaited } from './useAwaited';
|
|
75
85
|
export { useLinkProps, createLink, linkOptions } from './link';
|
|
76
86
|
export { useBlocker, Block } from './useBlocker.tsrx';
|
|
87
|
+
export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker.tsrx';
|
|
77
88
|
export { useMatchRoute, MatchRoute } from './MatchRoute.tsrx';
|
|
78
89
|
export { useElementScrollRestoration } from './useElementScrollRestoration';
|
|
79
90
|
export { lazyRouteComponent } from './lazyRouteComponent';
|
|
80
91
|
|
|
81
92
|
export { RouterProvider, RouterContextProvider } from './RouterProvider.tsrx';
|
|
93
|
+
export type { RouterProps } from './RouterProvider.tsrx';
|
|
82
94
|
export { Outlet } from './Outlet.tsrx';
|
|
83
95
|
export { Link } from './Link.tsrx';
|
|
84
96
|
export { Navigate } from './Navigate.tsrx';
|
package/src/not-found.tsrx
CHANGED
|
@@ -4,18 +4,28 @@
|
|
|
4
4
|
// up; the reset key is `not-found-${pathname}-${status}` so navigating away (or a
|
|
5
5
|
// new load settling) clears the not-found UI.
|
|
6
6
|
import { isNotFound } from '@tanstack/router-core';
|
|
7
|
+
import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core';
|
|
8
|
+
import type { OctaneNode } from 'octane';
|
|
9
|
+
import type { ErrorInfo } from './route.ts';
|
|
7
10
|
import { useRouter } from './context.ts';
|
|
8
11
|
import { useStore } from './useStore.ts';
|
|
9
12
|
import { CatchBoundary } from './CatchBoundary.tsrx';
|
|
10
13
|
|
|
11
|
-
function NotFoundFallback(props
|
|
14
|
+
function NotFoundFallback(props: {
|
|
15
|
+
error: unknown;
|
|
16
|
+
render?: (error: NotFoundError) => OctaneNode;
|
|
17
|
+
}) {
|
|
12
18
|
if (isNotFound(props.error)) {
|
|
13
19
|
return props.render ? props.render(props.error) : null;
|
|
14
20
|
}
|
|
15
21
|
throw props.error;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
export function CatchNotFound(props
|
|
24
|
+
export function CatchNotFound(props: {
|
|
25
|
+
fallback?: (error: NotFoundError) => OctaneNode;
|
|
26
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
27
|
+
children: OctaneNode;
|
|
28
|
+
}) @{
|
|
19
29
|
const router = useRouter();
|
|
20
30
|
const pathname = useStore(router.stores.location, (l) => l.pathname);
|
|
21
31
|
const status = useStore(router.stores.status, (s) => s);
|
|
@@ -23,14 +33,17 @@ export function CatchNotFound(props) @{
|
|
|
23
33
|
|
|
24
34
|
<CatchBoundary
|
|
25
35
|
getResetKey={() => resetKey}
|
|
26
|
-
onCatch={(error, errorInfo) => {
|
|
36
|
+
onCatch={(error: Error, errorInfo: ErrorInfo) => {
|
|
27
37
|
if (isNotFound(error)) {
|
|
28
38
|
if (props.onCatch) props.onCatch(error, errorInfo);
|
|
29
39
|
} else {
|
|
30
40
|
throw error;
|
|
31
41
|
}
|
|
32
42
|
}}
|
|
33
|
-
errorComponent={(p) => NotFoundFallback({
|
|
43
|
+
errorComponent={(p: ErrorComponentProps) => NotFoundFallback({
|
|
44
|
+
error: p.error,
|
|
45
|
+
render: props.fallback,
|
|
46
|
+
})}
|
|
34
47
|
>{props.children}</CatchBoundary>
|
|
35
48
|
}
|
|
36
49
|
|
package/src/not-found.tsrx.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Type declaration for the .tsrx components (resolved by relative path).
|
|
2
|
+
import type { NotFoundError } from '@tanstack/router-core';
|
|
3
|
+
import type { ErrorInfo } from './route';
|
|
4
|
+
|
|
2
5
|
export declare const CatchNotFound: (props: {
|
|
3
|
-
fallback?: (error:
|
|
4
|
-
onCatch?: (error:
|
|
6
|
+
fallback?: (error: NotFoundError) => unknown;
|
|
7
|
+
onCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
5
8
|
children?: unknown;
|
|
6
9
|
}) => unknown;
|
|
7
10
|
export declare const DefaultGlobalNotFound: () => unknown;
|
package/src/route.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// `withSlot` and passes the call-site symbol as the trailing argument, which
|
|
9
9
|
// each accessor forwards to the hooks it composes.
|
|
10
10
|
import { BaseRoute, BaseRootRoute, BaseRouteApi, notFound } from '@tanstack/router-core';
|
|
11
|
+
import type { ErrorComponentProps, NotFoundRouteProps } from '@tanstack/router-core';
|
|
11
12
|
import { createElement } from 'octane';
|
|
13
|
+
import type { OctaneNode } from 'octane';
|
|
12
14
|
import {
|
|
13
15
|
useMatch,
|
|
14
16
|
useParams,
|
|
@@ -22,6 +24,45 @@ import { useRouter } from './context';
|
|
|
22
24
|
import { splitSlot, subSlot } from './internal';
|
|
23
25
|
import { Link } from './Link.tsrx';
|
|
24
26
|
|
|
27
|
+
// ── Component types (react-router's route.tsx, on octane renderables) ────────
|
|
28
|
+
// Octane analog of React's `ErrorInfo` (octane reports no component stacks).
|
|
29
|
+
export type ErrorInfo = {
|
|
30
|
+
componentStack?: string | null;
|
|
31
|
+
digest?: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type SyncRouteComponent<TProps> = (props: TProps) => OctaneNode;
|
|
35
|
+
export type AsyncRouteComponent<TProps> = SyncRouteComponent<TProps> & {
|
|
36
|
+
preload?: () => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
export type RouteComponent = AsyncRouteComponent<{}>;
|
|
39
|
+
export type ErrorRouteComponent = AsyncRouteComponent<ErrorComponentProps>;
|
|
40
|
+
export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps>;
|
|
41
|
+
|
|
42
|
+
// router-core leaves the framework-facing component options typed `unknown` and
|
|
43
|
+
// each binding narrows them via interface merging — react-router's route.tsx /
|
|
44
|
+
// router.tsx do exactly this with React types; this is the octane equivalent.
|
|
45
|
+
declare module '@tanstack/router-core' {
|
|
46
|
+
interface UpdatableRouteOptionsExtensions {
|
|
47
|
+
component?: RouteComponent;
|
|
48
|
+
errorComponent?: false | null | undefined | ErrorRouteComponent;
|
|
49
|
+
notFoundComponent?: NotFoundRouteComponent;
|
|
50
|
+
pendingComponent?: RouteComponent;
|
|
51
|
+
}
|
|
52
|
+
interface RootRouteOptionsExtensions {
|
|
53
|
+
shellComponent?: (props: { children: OctaneNode }) => OctaneNode;
|
|
54
|
+
}
|
|
55
|
+
interface RouterOptionsExtensions {
|
|
56
|
+
defaultComponent?: RouteComponent;
|
|
57
|
+
defaultErrorComponent?: ErrorRouteComponent;
|
|
58
|
+
defaultPendingComponent?: RouteComponent;
|
|
59
|
+
defaultNotFoundComponent?: NotFoundRouteComponent;
|
|
60
|
+
Wrap?: (props: { children: OctaneNode }) => OctaneNode;
|
|
61
|
+
InnerWrap?: (props: { children: OctaneNode }) => OctaneNode;
|
|
62
|
+
defaultOnCatch?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
// Attach the hook accessors to a route-shaped instance (Route / RootRoute /
|
|
26
67
|
// RouteApi). `strictLoaderHooks: false` is RouteApi's mode — its loader hooks
|
|
27
68
|
// pass `strict: false` upstream (the api may be read from ancestor layouts).
|
package/src/useBlocker.tsrx
CHANGED
|
@@ -7,9 +7,62 @@
|
|
|
7
7
|
// in .tsrx so the compiler slots the internal useState/useEffect and callers'
|
|
8
8
|
// withSlot wrapping keeps per-call-site state independent.
|
|
9
9
|
import { useState, useEffect, isChildrenBlock } from 'octane';
|
|
10
|
+
import type { OctaneNode } from 'octane';
|
|
11
|
+
import type { BlockerFnArgs, HistoryAction, HistoryLocation } from '@tanstack/history';
|
|
10
12
|
import { useRouter } from './context.ts';
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
// Blocker types, ported from react-router's useBlocker.tsx. Upstream derives the
|
|
15
|
+
// location shapes from the registered route tree's generics; the octane binding's
|
|
16
|
+
// route factories are untyped (`createRoute(options: any)`), so the union of
|
|
17
|
+
// matched-route shapes collapses to the loosely-typed equivalent.
|
|
18
|
+
export interface ShouldBlockFnLocation {
|
|
19
|
+
routeId: string;
|
|
20
|
+
fullPath: string;
|
|
21
|
+
pathname: string;
|
|
22
|
+
params: Record<string, string>;
|
|
23
|
+
search: Record<string, any>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type BlockerResolver =
|
|
27
|
+
| {
|
|
28
|
+
status: 'blocked';
|
|
29
|
+
current: ShouldBlockFnLocation;
|
|
30
|
+
next: ShouldBlockFnLocation;
|
|
31
|
+
action: HistoryAction;
|
|
32
|
+
proceed: () => void;
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
status: 'idle';
|
|
37
|
+
current: undefined;
|
|
38
|
+
next: undefined;
|
|
39
|
+
action: undefined;
|
|
40
|
+
proceed: undefined;
|
|
41
|
+
reset: undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ShouldBlockFnArgs = {
|
|
45
|
+
current: ShouldBlockFnLocation;
|
|
46
|
+
next: ShouldBlockFnLocation;
|
|
47
|
+
action: HistoryAction;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
|
|
51
|
+
|
|
52
|
+
export type UseBlockerOpts = {
|
|
53
|
+
shouldBlockFn: ShouldBlockFn;
|
|
54
|
+
enableBeforeUnload?: boolean | (() => boolean);
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
withResolver?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type LegacyBlockerFn = () => Promise<any> | any;
|
|
60
|
+
type LegacyBlockerOpts = {
|
|
61
|
+
blockerFn?: LegacyBlockerFn;
|
|
62
|
+
condition?: boolean | any;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const IDLE_RESOLVER: BlockerResolver = {
|
|
13
66
|
status: 'idle',
|
|
14
67
|
current: undefined,
|
|
15
68
|
next: undefined,
|
|
@@ -18,7 +71,10 @@ const IDLE_RESOLVER = {
|
|
|
18
71
|
reset: undefined,
|
|
19
72
|
};
|
|
20
73
|
|
|
21
|
-
function _resolveBlockerOpts(
|
|
74
|
+
function _resolveBlockerOpts(
|
|
75
|
+
opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
|
|
76
|
+
condition?: boolean | any,
|
|
77
|
+
): UseBlockerOpts {
|
|
22
78
|
if (opts === undefined) {
|
|
23
79
|
return { shouldBlockFn: () => true, withResolver: false };
|
|
24
80
|
}
|
|
@@ -48,18 +104,21 @@ function _resolveBlockerOpts(opts, condition) {
|
|
|
48
104
|
};
|
|
49
105
|
}
|
|
50
106
|
|
|
51
|
-
export function useBlocker(
|
|
107
|
+
export function useBlocker(
|
|
108
|
+
opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
|
|
109
|
+
condition?: boolean | any,
|
|
110
|
+
): BlockerResolver {
|
|
52
111
|
const { shouldBlockFn, enableBeforeUnload = true, disabled = false, withResolver = false } =
|
|
53
112
|
_resolveBlockerOpts(opts, condition);
|
|
54
113
|
|
|
55
114
|
const router = useRouter();
|
|
56
115
|
const { history } = router;
|
|
57
116
|
|
|
58
|
-
const [resolver, setResolver] = useState(IDLE_RESOLVER);
|
|
117
|
+
const [resolver, setResolver] = useState<BlockerResolver>(IDLE_RESOLVER);
|
|
59
118
|
|
|
60
119
|
useEffect(() => {
|
|
61
|
-
const blockerFnComposed = async (blockerFnArgs) => {
|
|
62
|
-
function getLocation(location) {
|
|
120
|
+
const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
|
|
121
|
+
function getLocation(location: HistoryLocation): ShouldBlockFnLocation {
|
|
63
122
|
const parsedLocation = router.parseLocation(location);
|
|
64
123
|
const matchedRoutes = router.getMatchedRoutes(parsedLocation.pathname);
|
|
65
124
|
if (matchedRoutes.foundRoute === undefined) {
|
|
@@ -95,7 +154,7 @@ export function useBlocker(opts, condition) {
|
|
|
95
154
|
if (!withResolver) return shouldBlock;
|
|
96
155
|
if (!shouldBlock) return false;
|
|
97
156
|
|
|
98
|
-
const promise = new Promise((resolve) => {
|
|
157
|
+
const promise = new Promise<boolean>((resolve) => {
|
|
99
158
|
setResolver({
|
|
100
159
|
status: 'blocked',
|
|
101
160
|
current,
|
|
@@ -119,9 +178,15 @@ export function useBlocker(opts, condition) {
|
|
|
119
178
|
return resolver;
|
|
120
179
|
}
|
|
121
180
|
|
|
181
|
+
// Upstream's PromptProps: the blocker options plus optional children (a render
|
|
182
|
+
// prop receiving the resolver, or plain renderables).
|
|
183
|
+
export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
|
|
184
|
+
children?: OctaneNode | ((params: BlockerResolver) => OctaneNode);
|
|
185
|
+
};
|
|
186
|
+
|
|
122
187
|
// Declarative blocker: registers useBlocker and renders children (optionally a
|
|
123
188
|
// render prop receiving the resolver).
|
|
124
|
-
export function Block(props) @{
|
|
189
|
+
export function Block(props: PromptProps) @{
|
|
125
190
|
const { children, ...rest } = props;
|
|
126
191
|
const resolver = useBlocker(rest);
|
|
127
192
|
const out =
|
package/src/useBlocker.tsrx.d.ts
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
1
|
// Type declaration for the .tsrx module (resolved by relative path).
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { HistoryAction } from '@tanstack/history';
|
|
3
|
+
|
|
4
|
+
export interface ShouldBlockFnLocation {
|
|
5
|
+
routeId: string;
|
|
6
|
+
fullPath: string;
|
|
7
|
+
pathname: string;
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
search: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type BlockerResolver =
|
|
13
|
+
| {
|
|
14
|
+
status: 'blocked';
|
|
15
|
+
current: ShouldBlockFnLocation;
|
|
16
|
+
next: ShouldBlockFnLocation;
|
|
17
|
+
action: HistoryAction;
|
|
18
|
+
proceed: () => void;
|
|
19
|
+
reset: () => void;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
status: 'idle';
|
|
23
|
+
current: undefined;
|
|
24
|
+
next: undefined;
|
|
25
|
+
action: undefined;
|
|
26
|
+
proceed: undefined;
|
|
27
|
+
reset: undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ShouldBlockFnArgs = {
|
|
31
|
+
current: ShouldBlockFnLocation;
|
|
32
|
+
next: ShouldBlockFnLocation;
|
|
33
|
+
action: HistoryAction;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
|
|
37
|
+
|
|
38
|
+
export type UseBlockerOpts = {
|
|
39
|
+
shouldBlockFn: ShouldBlockFn;
|
|
40
|
+
enableBeforeUnload?: boolean | (() => boolean);
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
withResolver?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type LegacyBlockerFn = () => Promise<any> | any;
|
|
46
|
+
type LegacyBlockerOpts = {
|
|
47
|
+
blockerFn?: LegacyBlockerFn;
|
|
48
|
+
condition?: boolean | any;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type PromptProps = (UseBlockerOpts | LegacyBlockerOpts) & {
|
|
52
|
+
children?: unknown | ((params: BlockerResolver) => unknown);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export declare const useBlocker: (
|
|
56
|
+
opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
|
|
57
|
+
condition?: boolean | any,
|
|
58
|
+
) => BlockerResolver;
|
|
59
|
+
export declare const Block: (props: PromptProps) => unknown;
|
package/src/useStore.ts
CHANGED
|
@@ -13,11 +13,22 @@ interface Atom<T> {
|
|
|
13
13
|
get: () => T;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Public signature mirrors @tanstack/react-store's `useStore(store, selector,
|
|
17
|
+
// compare)`; the store parameter is the structural `{ get }` shape so router-core's
|
|
18
|
+
// `RouterReadableStore`/`RouterWritableStore` atoms (whose types omit `subscribe`)
|
|
19
|
+
// infer `T` directly. The trailing `slot` is the binding's forwarded call-site slot
|
|
20
|
+
// (see internal.ts) — the implementation splits it off the raw argument list.
|
|
21
|
+
export function useStore<T, S = T>(
|
|
22
|
+
atom: { get: () => T },
|
|
23
|
+
selector?: (state: T) => S,
|
|
24
|
+
compare?: (a: S, b: S) => boolean,
|
|
25
|
+
slot?: symbol,
|
|
26
|
+
): S;
|
|
27
|
+
export function useStore(...args: any[]): any {
|
|
17
28
|
const [user, slot] = splitSlot(args);
|
|
18
|
-
const atom = user[0] as Atom<
|
|
19
|
-
const selector = (user[1] ?? ((s:
|
|
20
|
-
const compare = (user[2] ?? Object.is) as (a:
|
|
29
|
+
const atom = user[0] as Atom<unknown>;
|
|
30
|
+
const selector = (user[1] ?? ((s: unknown) => s)) as (s: unknown) => unknown;
|
|
31
|
+
const compare = (user[2] ?? Object.is) as (a: unknown, b: unknown) => boolean;
|
|
21
32
|
|
|
22
33
|
// Re-subscribe only when the atom identity changes (it's stable across renders).
|
|
23
34
|
const subscribe = useCallback(
|
|
@@ -28,8 +39,8 @@ export function useStore<T, S = T>(...args: any[]): S {
|
|
|
28
39
|
|
|
29
40
|
// Memoize selector output: same store input → same output; structurally-equal
|
|
30
41
|
// output keeps its previous reference (so useSyncExternalStore doesn't loop).
|
|
31
|
-
const cache = useRef<{ in:
|
|
32
|
-
const getSnapshot = ():
|
|
42
|
+
const cache = useRef<{ in: unknown; out: unknown } | null>(null, subSlot(slot, 'us:cache'));
|
|
43
|
+
const getSnapshot = (): unknown => {
|
|
33
44
|
const input = atom.get();
|
|
34
45
|
const prev = cache.current;
|
|
35
46
|
if (prev && Object.is(prev.in, input)) return prev.out;
|