@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { StructuralSharingOption, ValidateSelected } from './structuralSharing';
|
|
2
|
+
import type {
|
|
3
|
+
AnyRouter,
|
|
4
|
+
FromPathOption,
|
|
5
|
+
MakeRouteMatch,
|
|
6
|
+
MakeRouteMatchUnion,
|
|
7
|
+
RegisteredRouter,
|
|
8
|
+
ResolveUseLoaderData,
|
|
9
|
+
ResolveUseLoaderDeps,
|
|
10
|
+
ResolveUseParams,
|
|
11
|
+
ResolveUseSearch,
|
|
12
|
+
RouterState,
|
|
13
|
+
StrictOrFrom,
|
|
14
|
+
ThrowConstraint,
|
|
15
|
+
ThrowOrOptional,
|
|
16
|
+
UseLoaderDataResult,
|
|
17
|
+
UseLoaderDepsResult,
|
|
18
|
+
UseNavigateResult,
|
|
19
|
+
UseParamsResult,
|
|
20
|
+
UseRouteContextBaseOptions,
|
|
21
|
+
UseRouteContextOptions,
|
|
22
|
+
UseRouteContextResult,
|
|
23
|
+
UseSearchResult,
|
|
24
|
+
} from '@tanstack/router-core';
|
|
25
|
+
|
|
26
|
+
export interface UseMatchBaseOptions<
|
|
27
|
+
TRouter extends AnyRouter,
|
|
28
|
+
TFrom,
|
|
29
|
+
TStrict extends boolean,
|
|
30
|
+
TThrow extends boolean,
|
|
31
|
+
TSelected,
|
|
32
|
+
TStructuralSharing extends boolean,
|
|
33
|
+
> {
|
|
34
|
+
select?: (
|
|
35
|
+
match: MakeRouteMatch<TRouter['routeTree'], TFrom, TStrict>,
|
|
36
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
37
|
+
shouldThrow?: TThrow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type UseMatchRoute<out TFrom> = <
|
|
41
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
42
|
+
TSelected = unknown,
|
|
43
|
+
TStructuralSharing extends boolean = boolean,
|
|
44
|
+
>(
|
|
45
|
+
opts?: UseMatchBaseOptions<TRouter, TFrom, true, true, TSelected, TStructuralSharing> &
|
|
46
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
|
|
47
|
+
) => UseMatchResult<TRouter, TFrom, true, TSelected>;
|
|
48
|
+
|
|
49
|
+
export type UseMatchOptions<
|
|
50
|
+
TRouter extends AnyRouter,
|
|
51
|
+
TFrom extends string | undefined,
|
|
52
|
+
TStrict extends boolean,
|
|
53
|
+
TThrow extends boolean,
|
|
54
|
+
TSelected,
|
|
55
|
+
TStructuralSharing extends boolean,
|
|
56
|
+
> = StrictOrFrom<TRouter, TFrom, TStrict> &
|
|
57
|
+
UseMatchBaseOptions<TRouter, TFrom, TStrict, TThrow, TSelected, TStructuralSharing> &
|
|
58
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
|
|
59
|
+
|
|
60
|
+
export type UseMatchResult<
|
|
61
|
+
TRouter extends AnyRouter,
|
|
62
|
+
TFrom,
|
|
63
|
+
TStrict extends boolean,
|
|
64
|
+
TSelected,
|
|
65
|
+
> = unknown extends TSelected
|
|
66
|
+
? TStrict extends true
|
|
67
|
+
? MakeRouteMatch<TRouter['routeTree'], TFrom, TStrict>
|
|
68
|
+
: MakeRouteMatchUnion<TRouter>
|
|
69
|
+
: TSelected;
|
|
70
|
+
|
|
71
|
+
export interface UseParamsBaseOptions<
|
|
72
|
+
TRouter extends AnyRouter,
|
|
73
|
+
TFrom,
|
|
74
|
+
TStrict extends boolean,
|
|
75
|
+
TThrow extends boolean,
|
|
76
|
+
TSelected,
|
|
77
|
+
TStructuralSharing,
|
|
78
|
+
> {
|
|
79
|
+
select?: (
|
|
80
|
+
params: ResolveUseParams<TRouter, TFrom, TStrict>,
|
|
81
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
82
|
+
shouldThrow?: TThrow;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type UseParamsRoute<out TFrom> = <
|
|
86
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
87
|
+
TSelected = unknown,
|
|
88
|
+
TStructuralSharing extends boolean = boolean,
|
|
89
|
+
>(
|
|
90
|
+
opts?: UseParamsBaseOptions<TRouter, TFrom, true, true, TSelected, TStructuralSharing> &
|
|
91
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
|
|
92
|
+
) => UseParamsResult<TRouter, TFrom, true, TSelected>;
|
|
93
|
+
|
|
94
|
+
export type UseParamsOptions<
|
|
95
|
+
TRouter extends AnyRouter,
|
|
96
|
+
TFrom extends string | undefined,
|
|
97
|
+
TStrict extends boolean,
|
|
98
|
+
TThrow extends boolean,
|
|
99
|
+
TSelected,
|
|
100
|
+
TStructuralSharing,
|
|
101
|
+
> = StrictOrFrom<TRouter, TFrom, TStrict> &
|
|
102
|
+
UseParamsBaseOptions<TRouter, TFrom, TStrict, TThrow, TSelected, TStructuralSharing> &
|
|
103
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
|
|
104
|
+
|
|
105
|
+
export interface UseSearchBaseOptions<
|
|
106
|
+
TRouter extends AnyRouter,
|
|
107
|
+
TFrom,
|
|
108
|
+
TStrict extends boolean,
|
|
109
|
+
TThrow extends boolean,
|
|
110
|
+
TSelected,
|
|
111
|
+
TStructuralSharing,
|
|
112
|
+
> {
|
|
113
|
+
select?: (
|
|
114
|
+
search: ResolveUseSearch<TRouter, TFrom, TStrict>,
|
|
115
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
116
|
+
shouldThrow?: TThrow;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type UseSearchRoute<out TFrom> = <
|
|
120
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
121
|
+
TSelected = unknown,
|
|
122
|
+
TStructuralSharing extends boolean = boolean,
|
|
123
|
+
>(
|
|
124
|
+
opts?: UseSearchBaseOptions<TRouter, TFrom, true, true, TSelected, TStructuralSharing> &
|
|
125
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
|
|
126
|
+
) => UseSearchResult<TRouter, TFrom, true, TSelected>;
|
|
127
|
+
|
|
128
|
+
export type UseSearchOptions<
|
|
129
|
+
TRouter extends AnyRouter,
|
|
130
|
+
TFrom,
|
|
131
|
+
TStrict extends boolean,
|
|
132
|
+
TThrow extends boolean,
|
|
133
|
+
TSelected,
|
|
134
|
+
TStructuralSharing,
|
|
135
|
+
> = StrictOrFrom<TRouter, TFrom, TStrict> &
|
|
136
|
+
UseSearchBaseOptions<TRouter, TFrom, TStrict, TThrow, TSelected, TStructuralSharing> &
|
|
137
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
|
|
138
|
+
|
|
139
|
+
export interface UseLoaderDataBaseOptions<
|
|
140
|
+
TRouter extends AnyRouter,
|
|
141
|
+
TFrom,
|
|
142
|
+
TStrict extends boolean,
|
|
143
|
+
TSelected,
|
|
144
|
+
TStructuralSharing,
|
|
145
|
+
> {
|
|
146
|
+
select?: (
|
|
147
|
+
data: ResolveUseLoaderData<TRouter, TFrom, TStrict>,
|
|
148
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type UseLoaderDataRoute<out TFrom> = <
|
|
152
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
153
|
+
TSelected = unknown,
|
|
154
|
+
TStructuralSharing extends boolean = boolean,
|
|
155
|
+
>(
|
|
156
|
+
opts?: UseLoaderDataBaseOptions<TRouter, TFrom, true, TSelected, TStructuralSharing> &
|
|
157
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
|
|
158
|
+
) => UseLoaderDataResult<TRouter, TFrom, true, TSelected>;
|
|
159
|
+
|
|
160
|
+
export type UseLoaderDataOptions<
|
|
161
|
+
TRouter extends AnyRouter,
|
|
162
|
+
TFrom extends string | undefined,
|
|
163
|
+
TStrict extends boolean,
|
|
164
|
+
TSelected,
|
|
165
|
+
TStructuralSharing,
|
|
166
|
+
> = StrictOrFrom<TRouter, TFrom, TStrict> &
|
|
167
|
+
UseLoaderDataBaseOptions<TRouter, TFrom, TStrict, TSelected, TStructuralSharing> &
|
|
168
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
|
|
169
|
+
|
|
170
|
+
export interface UseLoaderDepsBaseOptions<
|
|
171
|
+
TRouter extends AnyRouter,
|
|
172
|
+
TFrom,
|
|
173
|
+
TSelected,
|
|
174
|
+
TStructuralSharing,
|
|
175
|
+
> {
|
|
176
|
+
select?: (
|
|
177
|
+
deps: ResolveUseLoaderDeps<TRouter, TFrom>,
|
|
178
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type UseLoaderDepsRoute<out TFrom> = <
|
|
182
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
183
|
+
TSelected = unknown,
|
|
184
|
+
TStructuralSharing extends boolean = boolean,
|
|
185
|
+
>(
|
|
186
|
+
opts?: UseLoaderDepsBaseOptions<TRouter, TFrom, TSelected, TStructuralSharing> &
|
|
187
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
|
|
188
|
+
) => UseLoaderDepsResult<TRouter, TFrom, TSelected>;
|
|
189
|
+
|
|
190
|
+
export type UseLoaderDepsOptions<
|
|
191
|
+
TRouter extends AnyRouter,
|
|
192
|
+
TFrom extends string | undefined,
|
|
193
|
+
TSelected,
|
|
194
|
+
TStructuralSharing,
|
|
195
|
+
> = StrictOrFrom<TRouter, TFrom> &
|
|
196
|
+
UseLoaderDepsBaseOptions<TRouter, TFrom, TSelected, TStructuralSharing> &
|
|
197
|
+
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>;
|
|
198
|
+
|
|
199
|
+
export type UseRouteContextRoute<out TFrom> = <
|
|
200
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
201
|
+
TSelected = unknown,
|
|
202
|
+
>(
|
|
203
|
+
opts?: UseRouteContextBaseOptions<TRouter, TFrom, true, TSelected>,
|
|
204
|
+
) => UseRouteContextResult<TRouter, TFrom, true, TSelected>;
|
|
205
|
+
|
|
206
|
+
export interface UseLocationBaseOptions<
|
|
207
|
+
TRouter extends AnyRouter,
|
|
208
|
+
TSelected,
|
|
209
|
+
TStructuralSharing extends boolean = boolean,
|
|
210
|
+
> {
|
|
211
|
+
select?: (
|
|
212
|
+
location: RouterState<TRouter['routeTree']>['location'],
|
|
213
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type UseLocationResult<TRouter extends AnyRouter, TSelected> = unknown extends TSelected
|
|
217
|
+
? RouterState<TRouter['routeTree']>['location']
|
|
218
|
+
: TSelected;
|
|
219
|
+
|
|
220
|
+
export interface UseMatchesBaseOptions<TRouter extends AnyRouter, TSelected, TStructuralSharing> {
|
|
221
|
+
select?: (
|
|
222
|
+
matches: Array<MakeRouteMatchUnion<TRouter>>,
|
|
223
|
+
) => ValidateSelected<TRouter, TSelected, TStructuralSharing>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type UseMatchesResult<TRouter extends AnyRouter, TSelected> = unknown extends TSelected
|
|
227
|
+
? Array<MakeRouteMatchUnion<TRouter>>
|
|
228
|
+
: TSelected;
|
package/src/router.ts
CHANGED
|
@@ -13,6 +13,13 @@ import {
|
|
|
13
13
|
} from '@tanstack/router-core';
|
|
14
14
|
import { createAtom, batch } from '@tanstack/store';
|
|
15
15
|
import { startTransition } from 'octane';
|
|
16
|
+
import type { RouterHistory } from '@tanstack/history';
|
|
17
|
+
import type {
|
|
18
|
+
AnyRoute,
|
|
19
|
+
CreateRouterFn,
|
|
20
|
+
RouterConstructorOptions,
|
|
21
|
+
TrailingSlashOption,
|
|
22
|
+
} from '@tanstack/router-core';
|
|
16
23
|
|
|
17
24
|
const isServerEnv = typeof document === 'undefined';
|
|
18
25
|
|
|
@@ -52,8 +59,28 @@ const octaneStoreFactory = (opts: { isServer?: boolean }) => {
|
|
|
52
59
|
};
|
|
53
60
|
};
|
|
54
61
|
|
|
55
|
-
export class Router
|
|
56
|
-
|
|
62
|
+
export class Router<
|
|
63
|
+
in out TRouteTree extends AnyRoute,
|
|
64
|
+
in out TTrailingSlashOption extends TrailingSlashOption = 'never',
|
|
65
|
+
in out TDefaultStructuralSharingOption extends boolean = false,
|
|
66
|
+
in out TRouterHistory extends RouterHistory = RouterHistory,
|
|
67
|
+
in out TDehydrated extends Record<string, any> = Record<string, any>,
|
|
68
|
+
> extends RouterCore<
|
|
69
|
+
TRouteTree,
|
|
70
|
+
TTrailingSlashOption,
|
|
71
|
+
TDefaultStructuralSharingOption,
|
|
72
|
+
TRouterHistory,
|
|
73
|
+
TDehydrated
|
|
74
|
+
> {
|
|
75
|
+
constructor(
|
|
76
|
+
options: RouterConstructorOptions<
|
|
77
|
+
TRouteTree,
|
|
78
|
+
TTrailingSlashOption,
|
|
79
|
+
TDefaultStructuralSharingOption,
|
|
80
|
+
TRouterHistory,
|
|
81
|
+
TDehydrated
|
|
82
|
+
>,
|
|
83
|
+
) {
|
|
57
84
|
super(options, octaneStoreFactory);
|
|
58
85
|
|
|
59
86
|
// router-core starts the resolved-match commit through startViewTransition,
|
|
@@ -106,7 +133,7 @@ export class Router extends (RouterCore as any) {
|
|
|
106
133
|
activeLoadScopes.add(viewCommits);
|
|
107
134
|
let hasLoadError = false;
|
|
108
135
|
let loadError: unknown;
|
|
109
|
-
let result:
|
|
136
|
+
let result: void;
|
|
110
137
|
try {
|
|
111
138
|
result = await coreLoad(...args);
|
|
112
139
|
} catch (error) {
|
|
@@ -153,6 +180,4 @@ export class Router extends (RouterCore as any) {
|
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
export
|
|
157
|
-
return new Router(options);
|
|
158
|
-
}
|
|
183
|
+
export const createRouter: CreateRouterFn = (options) => new Router(options);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { deepEqual } from '@tanstack/router-core';
|
|
2
|
+
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
|
+
import { useRouter } from './context';
|
|
4
|
+
import { splitSlot, subSlot } from './internal';
|
|
5
|
+
import { useStore } from './useStore';
|
|
6
|
+
import type { AnyRouteMatch, AnyRouter, RouterManagedTag } from '@tanstack/router-core';
|
|
7
|
+
|
|
8
|
+
function getScripts(router: AnyRouter, matches: Array<AnyRouteMatch>) {
|
|
9
|
+
const nonce = router.options.ssr?.nonce;
|
|
10
|
+
const scripts: Array<RouterManagedTag> = matches
|
|
11
|
+
.flatMap((match) => match.scripts ?? [])
|
|
12
|
+
.filter((script) => script !== undefined)
|
|
13
|
+
.map(({ children, ...attrs }) => ({
|
|
14
|
+
tag: 'script',
|
|
15
|
+
attrs: { ...attrs, nonce },
|
|
16
|
+
children,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const manifest = router.ssr?.manifest;
|
|
20
|
+
if (manifest) {
|
|
21
|
+
for (const match of matches) {
|
|
22
|
+
for (const asset of manifest.routes[match.routeId]?.scripts ?? []) {
|
|
23
|
+
scripts.push({
|
|
24
|
+
tag: 'script',
|
|
25
|
+
attrs: { ...asset.attrs, nonce },
|
|
26
|
+
children: asset.children,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return scripts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useScripts(...args: Array<unknown>): Array<RouterManagedTag> {
|
|
36
|
+
const [, slot] = splitSlot(args);
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
|
|
39
|
+
if (isServer ?? router.isServer) {
|
|
40
|
+
const scripts = getScripts(router, router.stores.matches.get());
|
|
41
|
+
const buffered = router.serverSsr?.takeBufferedScripts();
|
|
42
|
+
if (!buffered || buffered.tag !== 'script') {
|
|
43
|
+
return scripts;
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
tag: 'script',
|
|
48
|
+
attrs: buffered.attrs,
|
|
49
|
+
children:
|
|
50
|
+
typeof buffered.children === 'string'
|
|
51
|
+
? buffered.children.replace(/;document\.currentScript\.remove\(\)$/, '')
|
|
52
|
+
: buffered.children,
|
|
53
|
+
},
|
|
54
|
+
...scripts,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return useStore(
|
|
59
|
+
router.stores.matches,
|
|
60
|
+
(matches: Array<AnyRouteMatch>) => getScripts(router, matches),
|
|
61
|
+
deepEqual,
|
|
62
|
+
subSlot(slot, 'body:scripts'),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getScrollRestorationScriptForRouter,
|
|
3
|
+
} from '@tanstack/router-core/scroll-restoration-script';
|
|
4
|
+
import { useRouter } from './context.ts';
|
|
5
|
+
import { ScriptOnce } from './ScriptOnce.tsrx';
|
|
6
|
+
|
|
7
|
+
export function ScrollRestorationScript() @{
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const script = getScrollRestorationScriptForRouter(router);
|
|
10
|
+
|
|
11
|
+
@if (script) {
|
|
12
|
+
<ScriptOnce children={script} />
|
|
13
|
+
} @else {
|
|
14
|
+
<></>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { use } from 'octane';
|
|
2
|
+
import { hydrate } from '@tanstack/router-core/ssr/client';
|
|
3
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
4
|
+
import { RouterProvider } from '../RouterProvider.tsrx';
|
|
5
|
+
import { toExternalHydrationThenable } from '../externalHydration.ts';
|
|
6
|
+
|
|
7
|
+
const hydrationPromises = new WeakMap<AnyRouter, PromiseLike<void>>();
|
|
8
|
+
const ready = {
|
|
9
|
+
status: 'fulfilled',
|
|
10
|
+
value: undefined,
|
|
11
|
+
then(onfulfilled?: (() => void) | null): Promise<void> {
|
|
12
|
+
return Promise.resolve(onfulfilled?.());
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function getHydrationPromise(router: AnyRouter) {
|
|
17
|
+
let promise = hydrationPromises.get(router);
|
|
18
|
+
if (!promise) {
|
|
19
|
+
promise = router.stores.matchesId.get().length ? ready as PromiseLike<void> : hydrate(router);
|
|
20
|
+
hydrationPromises.set(router, promise);
|
|
21
|
+
}
|
|
22
|
+
return promise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function RouterClientInner(props: { router: AnyRouter }) @{
|
|
26
|
+
use(toExternalHydrationThenable(getHydrationPromise(props.router)));
|
|
27
|
+
<RouterProvider router={props.router} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function RouterClient(props: { router: AnyRouter }) @{
|
|
31
|
+
@try {
|
|
32
|
+
<RouterClientInner router={props.router} />
|
|
33
|
+
} @pending {
|
|
34
|
+
<></>
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineHandlerCallback } from '@tanstack/router-core/ssr/server';
|
|
2
|
+
import { RouterServer } from './RouterServer.tsrx';
|
|
3
|
+
import { renderRouterToString } from './renderRouterToString';
|
|
4
|
+
|
|
5
|
+
export const defaultRenderHandler = defineHandlerCallback(({ router, responseHeaders }) =>
|
|
6
|
+
renderRouterToString({
|
|
7
|
+
router,
|
|
8
|
+
responseHeaders,
|
|
9
|
+
App: RouterServer,
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineHandlerCallback } from '@tanstack/router-core/ssr/server';
|
|
2
|
+
import { RouterServer } from './RouterServer.tsrx';
|
|
3
|
+
import { renderRouterToStream } from './renderRouterToStream';
|
|
4
|
+
|
|
5
|
+
export const defaultStreamHandler = defineHandlerCallback(({ request, router, responseHeaders }) =>
|
|
6
|
+
renderRouterToStream({
|
|
7
|
+
request,
|
|
8
|
+
router,
|
|
9
|
+
responseHeaders,
|
|
10
|
+
App: RouterServer,
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { renderToReadableStream } from 'octane/server';
|
|
2
|
+
import { prerender } from 'octane/static';
|
|
3
|
+
import { isbot } from 'isbot';
|
|
4
|
+
import { createSsrStreamResponse } from '@tanstack/router-core/ssr/server';
|
|
5
|
+
import { finalizeBufferedHtml } from './renderRouterToString';
|
|
6
|
+
import type { ComponentBody } from 'octane';
|
|
7
|
+
import type { StreamInjectionSource } from 'octane/server';
|
|
8
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
9
|
+
|
|
10
|
+
type RouterApp = ComponentBody<{ router: AnyRouter }>;
|
|
11
|
+
type ServerComponent = Parameters<typeof renderToReadableStream>[0];
|
|
12
|
+
|
|
13
|
+
// The router's data stream is merged through octane's native
|
|
14
|
+
// `StreamOptions.injection` (octane >= 0.1.11) instead of router-core's
|
|
15
|
+
// `transformStreamWithRouter` text transform. Octane emits tag-complete
|
|
16
|
+
// chunks and owns the document tail, so the byte-level re-parse
|
|
17
|
+
// (closing-tag scans, leftover buffers, the held-`</body>` tail that also
|
|
18
|
+
// buffered every post-shell suspense segment until stream end) is
|
|
19
|
+
// unnecessary — boundary segments stream out of order for document renders,
|
|
20
|
+
// and the transform's 64 KiB tail cap on segment volume disappears.
|
|
21
|
+
// Octane's document mode also emits `<!DOCTYPE html>` and folds the leading
|
|
22
|
+
// renderer-owned styles into `<head>`, replacing the `prependDoctype` and
|
|
23
|
+
// `relocateLeadingOctaneStylesToHead` transforms this file previously piped
|
|
24
|
+
// the stream through.
|
|
25
|
+
//
|
|
26
|
+
// The serialization timeout is preserved: it arms when octane reports the
|
|
27
|
+
// render finished (`renderComplete`) and fails the stream if serialization
|
|
28
|
+
// never completes. The script barrier lifts when octane subscribes — octane
|
|
29
|
+
// only subscribes after the shell (which carries the barrier anchor) is on
|
|
30
|
+
// the wire, matching the transform's lift-after-marker-flush.
|
|
31
|
+
// (`setRenderFinished` lifts it as a backstop regardless, exactly as
|
|
32
|
+
// before.)
|
|
33
|
+
|
|
34
|
+
const SERIALIZATION_TIMEOUT_MS = 60_000;
|
|
35
|
+
|
|
36
|
+
export async function renderRouterToStream({
|
|
37
|
+
request,
|
|
38
|
+
router,
|
|
39
|
+
responseHeaders,
|
|
40
|
+
App,
|
|
41
|
+
}: {
|
|
42
|
+
request: Request;
|
|
43
|
+
router: AnyRouter;
|
|
44
|
+
responseHeaders: Headers;
|
|
45
|
+
App: RouterApp;
|
|
46
|
+
}) {
|
|
47
|
+
if (isbot(request.headers.get('User-Agent'))) {
|
|
48
|
+
return renderRouterForBot({ request, router, responseHeaders, App });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const serverSsr = router.serverSsr;
|
|
52
|
+
if (!serverSsr) {
|
|
53
|
+
throw new Error('Invariant failed: router.serverSsr is required');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const renderController = new AbortController();
|
|
57
|
+
const onRequestAbort = () => renderController.abort(request.signal.reason);
|
|
58
|
+
if (request.signal.aborted) {
|
|
59
|
+
onRequestAbort();
|
|
60
|
+
} else {
|
|
61
|
+
request.signal.addEventListener('abort', onRequestAbort, { once: true });
|
|
62
|
+
serverSsr.onCleanup(() => {
|
|
63
|
+
request.signal.removeEventListener('abort', onRequestAbort);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let serializationTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
let stopSerializationListener: (() => void) | undefined;
|
|
69
|
+
let settleDone!: () => void;
|
|
70
|
+
let failDone!: (reason: unknown) => void;
|
|
71
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
72
|
+
settleDone = resolve;
|
|
73
|
+
failDone = reject;
|
|
74
|
+
});
|
|
75
|
+
if (serverSsr.isSerializationFinished()) {
|
|
76
|
+
settleDone();
|
|
77
|
+
} else {
|
|
78
|
+
stopSerializationListener = serverSsr.onSerializationFinished(() => settleDone());
|
|
79
|
+
}
|
|
80
|
+
const releaseInjection = () => {
|
|
81
|
+
if (serializationTimeout !== undefined) {
|
|
82
|
+
clearTimeout(serializationTimeout);
|
|
83
|
+
serializationTimeout = undefined;
|
|
84
|
+
}
|
|
85
|
+
stopSerializationListener?.();
|
|
86
|
+
stopSerializationListener = undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const injection: StreamInjectionSource = {
|
|
90
|
+
take: () => serverSsr.takeBufferedHtml() ?? '',
|
|
91
|
+
subscribe(notify) {
|
|
92
|
+
serverSsr.liftScriptBarrier();
|
|
93
|
+
return serverSsr.onInjectedHtml(notify);
|
|
94
|
+
},
|
|
95
|
+
done,
|
|
96
|
+
renderComplete() {
|
|
97
|
+
serverSsr.setRenderFinished();
|
|
98
|
+
if (!serverSsr.isSerializationFinished() && serializationTimeout === undefined) {
|
|
99
|
+
serializationTimeout = setTimeout(() => {
|
|
100
|
+
failDone(new Error('Serialization timeout after app render finished'));
|
|
101
|
+
}, SERIALIZATION_TIMEOUT_MS);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const stream = await renderToReadableStream(
|
|
108
|
+
App as unknown as ServerComponent,
|
|
109
|
+
{ router },
|
|
110
|
+
{
|
|
111
|
+
signal: renderController.signal,
|
|
112
|
+
nonce: router.options.ssr?.nonce,
|
|
113
|
+
injection,
|
|
114
|
+
onError(error) {
|
|
115
|
+
if (!isAbortError(request, error)) {
|
|
116
|
+
console.error('Error in renderToReadableStream:', error);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// The renderer's stream is the response body verbatim. `allReady` settles
|
|
123
|
+
// in every terminal state (close, abort, fatal, consumer cancel) — the
|
|
124
|
+
// single place to release the injection wiring and the router's SSR state.
|
|
125
|
+
stream.allReady.then(
|
|
126
|
+
() => {
|
|
127
|
+
releaseInjection();
|
|
128
|
+
serverSsr.cleanup();
|
|
129
|
+
},
|
|
130
|
+
() => {
|
|
131
|
+
releaseInjection();
|
|
132
|
+
serverSsr.cleanup();
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return createSsrStreamResponse(
|
|
137
|
+
router,
|
|
138
|
+
new Response(stream as unknown as BodyInit, {
|
|
139
|
+
status: router.stores.statusCode.get(),
|
|
140
|
+
headers: responseHeaders,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
renderController.abort(error);
|
|
145
|
+
releaseInjection();
|
|
146
|
+
router.serverSsr?.cleanup();
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function renderRouterForBot({
|
|
152
|
+
request,
|
|
153
|
+
router,
|
|
154
|
+
responseHeaders,
|
|
155
|
+
App,
|
|
156
|
+
}: {
|
|
157
|
+
request: Request;
|
|
158
|
+
router: AnyRouter;
|
|
159
|
+
responseHeaders: Headers;
|
|
160
|
+
App: RouterApp;
|
|
161
|
+
}) {
|
|
162
|
+
try {
|
|
163
|
+
const result = await prerender(
|
|
164
|
+
App as unknown as Parameters<typeof prerender>[0],
|
|
165
|
+
{ router },
|
|
166
|
+
{
|
|
167
|
+
signal: request.signal,
|
|
168
|
+
nonce: router.options.ssr?.nonce,
|
|
169
|
+
onError(error) {
|
|
170
|
+
if (!isAbortError(request, error)) {
|
|
171
|
+
console.error('Error in prerender:', error);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
router.serverSsr!.setRenderFinished();
|
|
177
|
+
|
|
178
|
+
return new Response(
|
|
179
|
+
finalizeBufferedHtml(result.html, result.css, router.serverSsr!.takeBufferedHtml()),
|
|
180
|
+
{
|
|
181
|
+
status: router.stores.statusCode.get(),
|
|
182
|
+
headers: responseHeaders,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
router.serverSsr?.cleanup();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isAbortError(request: Request, error: unknown) {
|
|
191
|
+
return (
|
|
192
|
+
(request.signal.aborted && error === request.signal.reason) ||
|
|
193
|
+
(error instanceof Error && error.name === 'AbortError')
|
|
194
|
+
);
|
|
195
|
+
}
|