@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.
Files changed (69) hide show
  1. package/README.md +24 -11
  2. package/package.json +22 -7
  3. package/src/Asset.tsrx +121 -0
  4. package/src/Asset.tsrx.d.ts +9 -0
  5. package/src/Await.tsrx +9 -3
  6. package/src/Await.tsrx.d.ts +8 -6
  7. package/src/Body.ts +31 -0
  8. package/src/CatchBoundary.tsrx +21 -4
  9. package/src/CatchBoundary.tsrx.d.ts +10 -4
  10. package/src/ClientOnly.tsrx +6 -3
  11. package/src/Head.ts +22 -0
  12. package/src/HeadContent.tsrx +41 -0
  13. package/src/HeadContent.tsrx.d.ts +8 -0
  14. package/src/Html.ts +19 -0
  15. package/src/Link.tsrx +19 -4
  16. package/src/Link.tsrx.d.ts +3 -4
  17. package/src/Match.tsrx +61 -29
  18. package/src/MatchRoute.tsrx +6 -3
  19. package/src/Matches.tsrx +7 -7
  20. package/src/Navigate.tsrx +2 -1
  21. package/src/Outlet.tsrx +8 -6
  22. package/src/RouteNotFound.tsrx +2 -2
  23. package/src/RouterProvider.tsrx +12 -2
  24. package/src/RouterProvider.tsrx.d.ts +9 -5
  25. package/src/SafeFragment.tsrx +4 -1
  26. package/src/ScriptOnce.tsrx +23 -0
  27. package/src/ScriptOnce.tsrx.d.ts +3 -0
  28. package/src/Scripts.tsrx +42 -0
  29. package/src/Scripts.tsrx.d.ts +3 -0
  30. package/src/Transitioner.tsrx +15 -13
  31. package/src/assetKeys.ts +11 -0
  32. package/src/context.ts +5 -1
  33. package/src/externalHydration.ts +77 -0
  34. package/src/fileRoute.ts +277 -0
  35. package/src/frameworkTypes.ts +42 -0
  36. package/src/generator-plugin.d.ts +20 -0
  37. package/src/generator-plugin.js +100 -0
  38. package/src/headContentUtils.ts +172 -0
  39. package/src/hooks.ts +151 -0
  40. package/src/index.ts +95 -3
  41. package/src/lazyRouteComponent.ts +2 -1
  42. package/src/link.ts +25 -4
  43. package/src/linkTypes.ts +96 -0
  44. package/src/not-found.tsrx +19 -6
  45. package/src/not-found.tsrx.d.ts +5 -2
  46. package/src/octane-compiler.d.ts +12 -0
  47. package/src/route.ts +473 -36
  48. package/src/routeHookTypes.ts +228 -0
  49. package/src/router.ts +31 -6
  50. package/src/scriptContentUtils.ts +64 -0
  51. package/src/scroll-restoration.tsrx +16 -0
  52. package/src/scroll-restoration.tsrx.d.ts +3 -0
  53. package/src/ssr/RouterClient.tsrx +36 -0
  54. package/src/ssr/RouterClient.tsrx.d.ts +4 -0
  55. package/src/ssr/RouterServer.tsrx +6 -0
  56. package/src/ssr/RouterServer.tsrx.d.ts +4 -0
  57. package/src/ssr/client.ts +4 -0
  58. package/src/ssr/defaultRenderHandler.ts +11 -0
  59. package/src/ssr/defaultStreamHandler.ts +12 -0
  60. package/src/ssr/renderRouterToStream.ts +195 -0
  61. package/src/ssr/renderRouterToString.ts +58 -0
  62. package/src/ssr/server.ts +8 -0
  63. package/src/structuralSharing.ts +41 -0
  64. package/src/typePrimitives.ts +77 -0
  65. package/src/useAwaited.ts +7 -2
  66. package/src/useBlocker.tsrx +73 -8
  67. package/src/useBlocker.tsrx.d.ts +58 -2
  68. package/src/useRouterState.ts +20 -0
  69. 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 extends (RouterCore as any) {
56
- constructor(options: any) {
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: unknown;
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 function createRouter(options: any): any {
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,3 @@
1
+ import type { ComponentBody } from 'octane';
2
+
3
+ export declare const ScrollRestorationScript: ComponentBody<Record<never, never>>;
@@ -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,4 @@
1
+ import type { ComponentBody } from 'octane';
2
+ import type { AnyRouter } from '@tanstack/router-core';
3
+
4
+ export declare const RouterClient: ComponentBody<{ router: AnyRouter }>;
@@ -0,0 +1,6 @@
1
+ import { RouterProvider } from '../RouterProvider.tsrx';
2
+ import type { AnyRouter } from '@tanstack/router-core';
3
+
4
+ export function RouterServer(props: { router: AnyRouter }) @{
5
+ <RouterProvider router={props.router} />
6
+ }
@@ -0,0 +1,4 @@
1
+ import type { ComponentBody } from 'octane';
2
+ import type { AnyRouter } from '@tanstack/router-core';
3
+
4
+ export declare const RouterServer: ComponentBody<{ router: AnyRouter }>;
@@ -0,0 +1,4 @@
1
+ import '../frameworkTypes';
2
+
3
+ export { RouterClient } from './RouterClient.tsrx';
4
+ export * from '@tanstack/router-core/ssr/client';
@@ -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
+ }