@octanejs/tanstack-router 0.1.9 → 0.1.11
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 +24 -11
- package/package.json +22 -7
- package/src/Asset.tsrx +121 -0
- package/src/Asset.tsrx.d.ts +9 -0
- package/src/Await.tsrx +9 -3
- package/src/Await.tsrx.d.ts +8 -6
- package/src/Body.ts +31 -0
- package/src/CatchBoundary.tsrx +21 -4
- package/src/CatchBoundary.tsrx.d.ts +10 -4
- package/src/ClientOnly.tsrx +6 -3
- package/src/Head.ts +22 -0
- package/src/HeadContent.tsrx +41 -0
- package/src/HeadContent.tsrx.d.ts +8 -0
- package/src/Html.ts +19 -0
- package/src/Link.tsrx +19 -4
- package/src/Link.tsrx.d.ts +3 -4
- package/src/Match.tsrx +61 -29
- package/src/MatchRoute.tsrx +6 -3
- package/src/Matches.tsrx +7 -7
- package/src/Navigate.tsrx +2 -1
- package/src/Outlet.tsrx +8 -6
- package/src/RouteNotFound.tsrx +2 -2
- package/src/RouterProvider.tsrx +12 -2
- package/src/RouterProvider.tsrx.d.ts +9 -5
- package/src/SafeFragment.tsrx +4 -1
- package/src/ScriptOnce.tsrx +23 -0
- package/src/ScriptOnce.tsrx.d.ts +3 -0
- package/src/Scripts.tsrx +42 -0
- package/src/Scripts.tsrx.d.ts +3 -0
- package/src/Transitioner.tsrx +15 -13
- package/src/assetKeys.ts +11 -0
- package/src/context.ts +5 -1
- package/src/externalHydration.ts +77 -0
- package/src/fileRoute.ts +277 -0
- package/src/frameworkTypes.ts +42 -0
- package/src/generator-plugin.d.ts +20 -0
- package/src/generator-plugin.js +100 -0
- package/src/headContentUtils.ts +172 -0
- package/src/hooks.ts +151 -0
- package/src/index.ts +95 -3
- package/src/lazyRouteComponent.ts +2 -1
- package/src/link.ts +25 -4
- package/src/linkTypes.ts +96 -0
- package/src/not-found.tsrx +19 -6
- package/src/not-found.tsrx.d.ts +5 -2
- package/src/octane-compiler.d.ts +12 -0
- package/src/route.ts +473 -36
- package/src/routeHookTypes.ts +228 -0
- package/src/router.ts +31 -6
- package/src/scriptContentUtils.ts +64 -0
- package/src/scroll-restoration.tsrx +16 -0
- package/src/scroll-restoration.tsrx.d.ts +3 -0
- package/src/ssr/RouterClient.tsrx +36 -0
- package/src/ssr/RouterClient.tsrx.d.ts +4 -0
- package/src/ssr/RouterServer.tsrx +6 -0
- package/src/ssr/RouterServer.tsrx.d.ts +4 -0
- package/src/ssr/client.ts +4 -0
- package/src/ssr/defaultRenderHandler.ts +11 -0
- package/src/ssr/defaultStreamHandler.ts +12 -0
- package/src/ssr/renderRouterToStream.ts +195 -0
- package/src/ssr/renderRouterToString.ts +58 -0
- package/src/ssr/server.ts +8 -0
- package/src/structuralSharing.ts +41 -0
- package/src/typePrimitives.ts +77 -0
- package/src/useAwaited.ts +7 -2
- package/src/useBlocker.tsrx +73 -8
- package/src/useBlocker.tsrx.d.ts +58 -2
- package/src/useRouterState.ts +20 -0
- package/src/useStore.ts +17 -6
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';
|
|
@@ -31,13 +33,26 @@ import { RouteNotFound } from './RouteNotFound.tsrx';
|
|
|
31
33
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
32
34
|
import { CatchNotFound } from './not-found.tsrx';
|
|
33
35
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
36
|
+
import { ClientOnly } from './ClientOnly.tsrx';
|
|
37
|
+
import { ScrollRestorationScript } from './scroll-restoration.tsrx';
|
|
34
38
|
|
|
35
|
-
export function Match(props) @{
|
|
39
|
+
export function Match(props: { matchId: string }) @{
|
|
36
40
|
const router = useRouter();
|
|
37
|
-
const matchStore = router.stores.matchStores.get(props.matchId)
|
|
38
|
-
const resetKey = useStore(router.stores.loadedAt, (l) => l);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
42
|
+
const resetKey = useStore(router.stores.loadedAt, (l: any) => l as number | string);
|
|
43
|
+
const matchState = useStore(
|
|
44
|
+
matchStore,
|
|
45
|
+
(m: any) => ({
|
|
46
|
+
routeId: m.routeId,
|
|
47
|
+
ssr: m.ssr,
|
|
48
|
+
_displayPending: m._displayPending,
|
|
49
|
+
}),
|
|
50
|
+
(previous, next) =>
|
|
51
|
+
previous.routeId === next.routeId && previous.ssr === next.ssr &&
|
|
52
|
+
previous._displayPending === next._displayPending,
|
|
53
|
+
);
|
|
54
|
+
const routeId = matchState.routeId as string;
|
|
55
|
+
const route = (router.routesById as Record<string, any>)[routeId];
|
|
41
56
|
|
|
42
57
|
const PendingComponent = route.options.pendingComponent ?? router.options.defaultPendingComponent;
|
|
43
58
|
const routeErrorComponent = route.options.errorComponent ?? router.options.defaultErrorComponent;
|
|
@@ -45,17 +60,23 @@ export function Match(props) @{
|
|
|
45
60
|
const routeNotFoundComponent = route.isRoot
|
|
46
61
|
? route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component
|
|
47
62
|
: route.options.notFoundComponent;
|
|
63
|
+
const resolvedNoSsr = matchState.ssr === false || matchState.ssr === 'data-only';
|
|
48
64
|
|
|
49
65
|
// Boundary presence, per upstream MatchView. The root route only gets a
|
|
50
66
|
// Suspense boundary when explicitly opted in via wrapInSuspense.
|
|
67
|
+
const suspenseSignal =
|
|
68
|
+
route.options.wrapInSuspense ?? PendingComponent ??
|
|
69
|
+
(routeErrorComponent as ErrorRouteComponent | undefined)?.preload;
|
|
51
70
|
const SuspenseWrap =
|
|
52
|
-
(!route.isRoot || route.options.wrapInSuspense) &&
|
|
53
|
-
(
|
|
71
|
+
(!route.isRoot || route.options.wrapInSuspense || resolvedNoSsr) &&
|
|
72
|
+
(suspenseSignal || resolvedNoSsr)
|
|
54
73
|
? Suspense
|
|
55
74
|
: SafeFragment;
|
|
56
75
|
const CatchWrap = routeErrorComponent ? CatchBoundary : SafeFragment;
|
|
57
76
|
const NotFoundWrap = routeNotFoundComponent ? CatchNotFound : SafeFragment;
|
|
58
|
-
const ShellComponent = route.isRoot
|
|
77
|
+
const ShellComponent = route.isRoot
|
|
78
|
+
? (route.options as RootRouteOptions).shellComponent ?? SafeFragment
|
|
79
|
+
: SafeFragment;
|
|
59
80
|
|
|
60
81
|
const pendingElement =
|
|
61
82
|
PendingComponent ? createElement(PendingComponent, {}) : null;
|
|
@@ -67,17 +88,17 @@ export function Match(props) @{
|
|
|
67
88
|
<CatchWrap
|
|
68
89
|
getResetKey={() => resetKey}
|
|
69
90
|
errorComponent={routeErrorComponent || ErrorComponent}
|
|
70
|
-
onCatch={(error, errorInfo) => {
|
|
91
|
+
onCatch={(error: Error, errorInfo: ErrorInfo) => {
|
|
71
92
|
if (isNotFound(error)) {
|
|
72
|
-
error.routeId ??= routeId;
|
|
93
|
+
(error as any).routeId ??= routeId;
|
|
73
94
|
throw error;
|
|
74
95
|
}
|
|
75
96
|
if (routeOnCatch) routeOnCatch(error, errorInfo);
|
|
76
97
|
}}
|
|
77
98
|
>
|
|
78
99
|
<NotFoundWrap
|
|
79
|
-
fallback={(error) => {
|
|
80
|
-
error.routeId ??= routeId;
|
|
100
|
+
fallback={(error: NotFoundError) => {
|
|
101
|
+
(error as any).routeId ??= routeId;
|
|
81
102
|
|
|
82
103
|
if (
|
|
83
104
|
!routeNotFoundComponent || error.routeId && error.routeId !== routeId ||
|
|
@@ -85,38 +106,49 @@ export function Match(props) @{
|
|
|
85
106
|
) {
|
|
86
107
|
throw error;
|
|
87
108
|
}
|
|
88
|
-
return createElement(routeNotFoundComponent, error);
|
|
109
|
+
return createElement(routeNotFoundComponent, error as any);
|
|
89
110
|
}}
|
|
90
111
|
>
|
|
91
|
-
|
|
112
|
+
@if (resolvedNoSsr || matchState._displayPending) {
|
|
113
|
+
<ClientOnly fallback={pendingElement}>
|
|
114
|
+
<MatchInner matchId={props.matchId} />
|
|
115
|
+
</ClientOnly>
|
|
116
|
+
} @else {
|
|
117
|
+
<MatchInner matchId={props.matchId} />
|
|
118
|
+
}
|
|
92
119
|
</NotFoundWrap>
|
|
93
120
|
</CatchWrap>
|
|
94
121
|
</SuspenseWrap>
|
|
95
122
|
</matchContext.Provider>
|
|
96
123
|
@if (parentRouteId === rootRouteId) {
|
|
97
|
-
|
|
124
|
+
<>
|
|
125
|
+
<OnRendered />
|
|
126
|
+
@if (router.options.scrollRestoration) {
|
|
127
|
+
<ScrollRestorationScript />
|
|
128
|
+
}
|
|
129
|
+
</>
|
|
98
130
|
}
|
|
99
131
|
</ShellComponent>
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
function MatchInner(props) {
|
|
134
|
+
function MatchInner(props: { matchId: string }) {
|
|
103
135
|
const router = useRouter();
|
|
104
|
-
const matchStore = router.stores.matchStores.get(props.matchId)
|
|
105
|
-
const match = useStore(matchStore, (m) => m);
|
|
106
|
-
const routeId = match.routeId;
|
|
107
|
-
const route = router.routesById[routeId];
|
|
136
|
+
const matchStore = router.stores.matchStores.get(props.matchId)!;
|
|
137
|
+
const match = useStore(matchStore, (m: any) => m);
|
|
138
|
+
const routeId = match.routeId as string;
|
|
139
|
+
const route = (router.routesById as Record<string, any>)[routeId];
|
|
108
140
|
|
|
109
141
|
// The live match in the router (if still mounted there) wins over the
|
|
110
142
|
// snapshot for promise lookups, per upstream getMatchPromise.
|
|
111
|
-
const getMatchPromise = (key
|
|
112
|
-
match._nonReactive[key];
|
|
143
|
+
const getMatchPromise = (key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise') =>
|
|
144
|
+
router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key];
|
|
113
145
|
|
|
114
146
|
if (match._displayPending) {
|
|
115
|
-
use(getMatchPromise('displayPendingPromise'));
|
|
147
|
+
use(getMatchPromise('displayPendingPromise')!);
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
if (match._forcePending) {
|
|
119
|
-
use(getMatchPromise('minPendingPromise'));
|
|
151
|
+
use(getMatchPromise('minPendingPromise')!);
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
if (match.status === 'pending') {
|
|
@@ -125,7 +157,7 @@ function MatchInner(props) {
|
|
|
125
157
|
if (pendingMinMs) {
|
|
126
158
|
const routerMatch = router.getMatch(match.id);
|
|
127
159
|
if (routerMatch && !routerMatch._nonReactive.minPendingPromise) {
|
|
128
|
-
const minPendingPromise = createControlledPromise();
|
|
160
|
+
const minPendingPromise = createControlledPromise<void>();
|
|
129
161
|
routerMatch._nonReactive.minPendingPromise = minPendingPromise;
|
|
130
162
|
setTimeout(() => {
|
|
131
163
|
minPendingPromise.resolve();
|
|
@@ -133,7 +165,7 @@ function MatchInner(props) {
|
|
|
133
165
|
}, pendingMinMs);
|
|
134
166
|
}
|
|
135
167
|
}
|
|
136
|
-
use(getMatchPromise('loadPromise'));
|
|
168
|
+
use(getMatchPromise('loadPromise')!);
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
if (match.status === 'notFound') {
|
|
@@ -143,7 +175,7 @@ function MatchInner(props) {
|
|
|
143
175
|
if (match.status === 'redirected') {
|
|
144
176
|
// Observed mid-transition while a redirect is in flight — suspend on the
|
|
145
177
|
// load so this stale render is abandoned and the redirect completes.
|
|
146
|
-
use(getMatchPromise('loadPromise'));
|
|
178
|
+
use(getMatchPromise('loadPromise')!);
|
|
147
179
|
}
|
|
148
180
|
|
|
149
181
|
if (match.status === 'error') {
|
|
@@ -174,10 +206,10 @@ function MatchInner(props) {
|
|
|
174
206
|
// `resolvedLocation` to the new location.
|
|
175
207
|
function OnRendered() @{
|
|
176
208
|
const router = useRouter();
|
|
177
|
-
const prevResolvedLocationRef = useRef(undefined);
|
|
209
|
+
const prevResolvedLocationRef = useRef<ParsedLocation<any> | undefined>(undefined);
|
|
178
210
|
const renderedLocationKey = useStore(
|
|
179
211
|
router.stores.resolvedLocation,
|
|
180
|
-
(loc) => loc?.state.__TSR_key,
|
|
212
|
+
(loc: ParsedLocation<any> | undefined) => loc?.state.__TSR_key,
|
|
181
213
|
);
|
|
182
214
|
|
|
183
215
|
useLayoutEffect(() => {
|
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/Matches.tsrx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Runs the navigation engine and renders the match tree root — port of react-router's
|
|
2
2
|
// Matches.tsx. Structure mirrors upstream:
|
|
3
3
|
//
|
|
4
|
-
// InnerWrap? > Suspense(root pending) >
|
|
4
|
+
// useTransitioner + InnerWrap? > Suspense(root pending) > MatchesInner
|
|
5
5
|
// MatchesInner: matchContext(firstId) > global CatchBoundary? > Match(firstId)
|
|
6
6
|
//
|
|
7
7
|
// The root Suspense is ALWAYS present on the client (fallback is the root
|
|
@@ -14,14 +14,14 @@ import { useLayoutEffect, createElement, Suspense } from 'octane';
|
|
|
14
14
|
import { setupScrollRestoration, rootRouteId } from '@tanstack/router-core';
|
|
15
15
|
import { useStore } from './useStore.ts';
|
|
16
16
|
import { useRouter, matchContext } from './context.ts';
|
|
17
|
-
import {
|
|
17
|
+
import { useTransitioner } from './Transitioner.tsrx';
|
|
18
18
|
import { Match } from './Match.tsrx';
|
|
19
19
|
import { CatchBoundary, ErrorComponent } from './CatchBoundary.tsrx';
|
|
20
20
|
import { SafeFragment } from './SafeFragment.tsrx';
|
|
21
21
|
|
|
22
22
|
export function Matches() @{
|
|
23
23
|
const router = useRouter();
|
|
24
|
-
const rootRoute = router.routesById[rootRouteId];
|
|
24
|
+
const rootRoute = (router.routesById as any)[rootRouteId];
|
|
25
25
|
const PendingComponent =
|
|
26
26
|
rootRoute.options.pendingComponent ?? router.options.defaultPendingComponent;
|
|
27
27
|
const pendingElement =
|
|
@@ -33,10 +33,10 @@ export function Matches() @{
|
|
|
33
33
|
useLayoutEffect(() => {
|
|
34
34
|
if (router.options.scrollRestoration) setupScrollRestoration(router);
|
|
35
35
|
}, [router]);
|
|
36
|
+
useTransitioner();
|
|
36
37
|
|
|
37
38
|
<InnerWrap>
|
|
38
39
|
<Suspense fallback={pendingElement}>
|
|
39
|
-
<Transitioner />
|
|
40
40
|
<MatchesInner />
|
|
41
41
|
</Suspense>
|
|
42
42
|
</InnerWrap>
|
|
@@ -44,8 +44,8 @@ export function Matches() @{
|
|
|
44
44
|
|
|
45
45
|
function MatchesInner() @{
|
|
46
46
|
const router = useRouter();
|
|
47
|
-
const matchId = useStore(router.stores.firstId, (id) => id);
|
|
48
|
-
const resetKey = useStore(router.stores.loadedAt, (l) => l);
|
|
47
|
+
const matchId = useStore(router.stores.firstId, (id: any) => id as string | undefined);
|
|
48
|
+
const resetKey = useStore(router.stores.loadedAt, (l: any) => l as number | string);
|
|
49
49
|
const CatchWrap = router.options.disableGlobalCatchBoundary ? SafeFragment : CatchBoundary;
|
|
50
50
|
|
|
51
51
|
<matchContext.Provider value={matchId}>
|
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,11 +19,13 @@ import { SafeFragment } from './SafeFragment.tsrx';
|
|
|
19
19
|
|
|
20
20
|
export function Outlet() @{
|
|
21
21
|
const router = useRouter();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
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: any) => m?.routeId as string | undefined);
|
|
27
|
+
const globalNotFound = useStore(parentStore, (m: any) => m?.globalNotFound ?? false as boolean);
|
|
28
|
+
const childId = useStore(router.stores.matchesId, (ids: Array<string>) => {
|
|
27
29
|
const i = ids.indexOf(parentId);
|
|
28
30
|
return i >= 0 ? ids[i + 1] : undefined;
|
|
29
31
|
});
|
|
@@ -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,9 +10,9 @@
|
|
|
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
|
-
const route = router.routesById[props.routeId];
|
|
15
|
+
const route = (router.routesById as any)[props.routeId];
|
|
16
16
|
const NotFound = route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
|
|
17
17
|
|
|
18
18
|
@if (NotFound) {
|
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
|
</>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useState } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import { useRouter } from './context.ts';
|
|
4
|
+
|
|
5
|
+
export function ScriptOnce(props: { children: string }) @{
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
const server = isServer ?? router.isServer;
|
|
8
|
+
const [hydrating, setHydrating] = useState(true);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setHydrating(false);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
@if (server || hydrating) {
|
|
14
|
+
<script
|
|
15
|
+
nonce={router.options.ssr?.nonce}
|
|
16
|
+
dangerouslySetInnerHTML={{
|
|
17
|
+
__html: props.children,
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
} @else {
|
|
21
|
+
<></>
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/Scripts.tsrx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createElement, useEffect, useState } from 'octane';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import type { RouterManagedTag } from '@tanstack/router-core';
|
|
4
|
+
import { getAssetKey } from './assetKeys.ts';
|
|
5
|
+
import { useRouter } from './context.ts';
|
|
6
|
+
import { useScripts } from './scriptContentUtils.ts';
|
|
7
|
+
|
|
8
|
+
export function Scripts() @{
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const scripts = useScripts();
|
|
11
|
+
const [hydrating, setHydrating] = useState(true);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setHydrating(false);
|
|
14
|
+
}, []);
|
|
15
|
+
const renderScripts: Array<RouterManagedTag> =
|
|
16
|
+
!(isServer ?? router.isServer) && hydrating
|
|
17
|
+
? [
|
|
18
|
+
{
|
|
19
|
+
tag: 'script',
|
|
20
|
+
attrs: {
|
|
21
|
+
nonce: router.options.ssr?.nonce,
|
|
22
|
+
className: '$tsr',
|
|
23
|
+
id: '$tsr-stream-barrier',
|
|
24
|
+
suppressHydrationWarning: true,
|
|
25
|
+
},
|
|
26
|
+
children: '',
|
|
27
|
+
},
|
|
28
|
+
...scripts,
|
|
29
|
+
]
|
|
30
|
+
: scripts;
|
|
31
|
+
<>
|
|
32
|
+
{renderScripts.map((script, index) => {
|
|
33
|
+
const assetKey = getAssetKey('body', script, index);
|
|
34
|
+
return createElement('script', {
|
|
35
|
+
...script.attrs,
|
|
36
|
+
key: assetKey,
|
|
37
|
+
'data-tsr-managed-key': assetKey,
|
|
38
|
+
dangerouslySetInnerHTML: { __html: script.children ?? '' },
|
|
39
|
+
});
|
|
40
|
+
})}
|
|
41
|
+
</>
|
|
42
|
+
}
|
package/src/Transitioner.tsrx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// The navigation engine
|
|
1
|
+
// The navigation engine — port of react-router's Transitioner. It runs as a
|
|
2
|
+
// hook because a rendered sibling would put an SSR marker before a root route's
|
|
3
|
+
// document-level <html> node and outside the #__app hydration range.
|
|
2
4
|
// It (1) supplies `router.startTransition` so every navigation state update rides
|
|
3
5
|
// an octane transition (concurrent navigation: the current page holds while the
|
|
4
6
|
// next route suspends), (2) subscribes to history so back/forward and `Link`
|
|
@@ -18,7 +20,7 @@ import { useRouter } from './context.ts';
|
|
|
18
20
|
import { useStore } from './useStore.ts';
|
|
19
21
|
import { usePrevious } from './utils.ts';
|
|
20
22
|
|
|
21
|
-
export function
|
|
23
|
+
export function useTransitioner() {
|
|
22
24
|
const router = useRouter();
|
|
23
25
|
const mountLoadForRouter = useRef({ router, mounted: false });
|
|
24
26
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
@@ -32,7 +34,7 @@ export function Transitioner() @{
|
|
|
32
34
|
const isPagePending = isLoading || hasPending;
|
|
33
35
|
const previousIsPagePending = usePrevious(isPagePending);
|
|
34
36
|
|
|
35
|
-
router.startTransition = (fn) => {
|
|
37
|
+
router.startTransition = (fn: () => void) => {
|
|
36
38
|
setIsTransitioning(true);
|
|
37
39
|
startTransition(() => {
|
|
38
40
|
fn();
|
|
@@ -47,14 +49,16 @@ export function Transitioner() @{
|
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
const unsub = router.history.subscribe(() => router.load());
|
|
49
51
|
|
|
50
|
-
const nextLocation = router.buildLocation(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
const nextLocation = router.buildLocation(
|
|
53
|
+
{
|
|
54
|
+
to: router.latestLocation.pathname,
|
|
55
|
+
search: true,
|
|
56
|
+
params: true,
|
|
57
|
+
hash: true,
|
|
58
|
+
state: true,
|
|
59
|
+
_includeValidateSearch: true,
|
|
60
|
+
} as any,
|
|
61
|
+
);
|
|
58
62
|
if (
|
|
59
63
|
trimPathRight(router.latestLocation.publicHref) !== trimPathRight(nextLocation.publicHref)
|
|
60
64
|
) {
|
|
@@ -121,6 +125,4 @@ export function Transitioner() @{
|
|
|
121
125
|
});
|
|
122
126
|
}
|
|
123
127
|
}, [isAnyPending, previousIsAnyPending, router]);
|
|
124
|
-
|
|
125
|
-
<></>
|
|
126
128
|
}
|
package/src/assetKeys.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RouterManagedTag } from '@tanstack/router-core';
|
|
2
|
+
|
|
3
|
+
export function getAssetKey(scope: 'head' | 'body', asset: RouterManagedTag, index: number) {
|
|
4
|
+
const inlineCss = asset.tag === 'style' && asset.inlineCss;
|
|
5
|
+
return `${scope}:${index}:${JSON.stringify({
|
|
6
|
+
tag: asset.tag,
|
|
7
|
+
attrs: asset.attrs,
|
|
8
|
+
children: inlineCss ? undefined : asset.children,
|
|
9
|
+
inlineCss,
|
|
10
|
+
})}`;
|
|
11
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// match id down the render tree so `Outlet` can find the NEXT match to render —
|
|
4
4
|
// the pull-based chaining that replaces a top-down state diff.
|
|
5
5
|
import { createContext, useContext } from 'octane';
|
|
6
|
-
import type { AnyRouter } from '@tanstack/router-core';
|
|
6
|
+
import type { AnyRouter, RegisteredRouter } from '@tanstack/router-core';
|
|
7
7
|
|
|
8
8
|
export const routerContext = createContext<AnyRouter | undefined>(undefined);
|
|
9
9
|
export const getRouterContext = (): typeof routerContext => routerContext;
|
|
@@ -14,6 +14,10 @@ export const matchContext = createContext<string | undefined>(undefined);
|
|
|
14
14
|
// Resolve the active router: an explicitly-passed one wins, else the context.
|
|
15
15
|
// `useContext` is keyed by context identity (not a per-call-site slot), so it's
|
|
16
16
|
// safe to call from this binding code without a slot.
|
|
17
|
+
export function useRouter<TRouter extends AnyRouter = RegisteredRouter>(opts?: {
|
|
18
|
+
router?: TRouter;
|
|
19
|
+
warn?: boolean;
|
|
20
|
+
}): TRouter;
|
|
17
21
|
export function useRouter(...args: unknown[]): AnyRouter {
|
|
18
22
|
const opts = (args.length && typeof args[0] !== 'symbol' ? args[0] : undefined) as
|
|
19
23
|
| { router?: AnyRouter; warn?: boolean }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const EXTERNAL_HYDRATION_PROMISE = Symbol.for('octane.external-hydration-promise');
|
|
2
|
+
|
|
3
|
+
type ExternalHydrationThenable<T> = PromiseLike<T> & {
|
|
4
|
+
[EXTERNAL_HYDRATION_PROMISE]: true;
|
|
5
|
+
status?: ThenableStatus;
|
|
6
|
+
value?: T;
|
|
7
|
+
reason?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ThenableStatus = 'pending' | 'fulfilled' | 'rejected';
|
|
11
|
+
|
|
12
|
+
const externalHydrationThenables = new WeakMap<object, ExternalHydrationThenable<unknown>>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wrap a router-owned promise so Octane still schedules its suspense boundary,
|
|
16
|
+
* while TanStack's serializer remains the only owner of its hydration value.
|
|
17
|
+
*/
|
|
18
|
+
export function toExternalHydrationThenable<T>(thenable: PromiseLike<T>): PromiseLike<T> {
|
|
19
|
+
const key = thenable as object;
|
|
20
|
+
const existing = externalHydrationThenables.get(key);
|
|
21
|
+
if (existing) {
|
|
22
|
+
return existing as ExternalHydrationThenable<T>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let localStatus: ThenableStatus | undefined;
|
|
26
|
+
let localValue: T | undefined;
|
|
27
|
+
let localReason: unknown;
|
|
28
|
+
let hasLocalValue = false;
|
|
29
|
+
let hasLocalReason = false;
|
|
30
|
+
|
|
31
|
+
const externalThenable: ExternalHydrationThenable<T> = {
|
|
32
|
+
[EXTERNAL_HYDRATION_PROMISE]: true,
|
|
33
|
+
get status() {
|
|
34
|
+
return localStatus ?? readThenableStatus(thenable);
|
|
35
|
+
},
|
|
36
|
+
set status(status) {
|
|
37
|
+
localStatus = status;
|
|
38
|
+
},
|
|
39
|
+
get value(): T | undefined {
|
|
40
|
+
return hasLocalValue ? localValue : readThenableProperty<T>(thenable, 'value');
|
|
41
|
+
},
|
|
42
|
+
set value(value: T | undefined) {
|
|
43
|
+
hasLocalValue = true;
|
|
44
|
+
localValue = value;
|
|
45
|
+
},
|
|
46
|
+
get reason(): unknown {
|
|
47
|
+
return hasLocalReason ? localReason : readThenableProperty<unknown>(thenable, 'reason');
|
|
48
|
+
},
|
|
49
|
+
set reason(reason) {
|
|
50
|
+
hasLocalReason = true;
|
|
51
|
+
localReason = reason;
|
|
52
|
+
},
|
|
53
|
+
then(onfulfilled, onrejected) {
|
|
54
|
+
return thenable.then(onfulfilled, onrejected);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
externalHydrationThenables.set(key, externalThenable);
|
|
58
|
+
return externalThenable;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readThenableStatus(thenable: PromiseLike<unknown>) {
|
|
62
|
+
const status = readThenableProperty(thenable, 'status');
|
|
63
|
+
return status === 'pending' || status === 'fulfilled' || status === 'rejected'
|
|
64
|
+
? status
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readThenableProperty<T>(
|
|
69
|
+
thenable: PromiseLike<unknown>,
|
|
70
|
+
property: 'status' | 'value' | 'reason',
|
|
71
|
+
): T | undefined {
|
|
72
|
+
try {
|
|
73
|
+
return (thenable as PromiseLike<unknown> & Record<string, T>)[property];
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|