@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.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/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +421 -0
- package/dist/cjs/entry-application.js +88 -0
- package/dist/cjs/entry-client.js +95 -0
- package/dist/cjs/entry-server.js +135 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +205 -0
- package/dist/cjs/i18n/I18nProvider.js +59 -0
- package/dist/cjs/i18n/LocalizedLink.js +80 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +83 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +122 -0
- package/dist/cjs/plugin/create.js +266 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +91 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-external-snapshot.js +291 -0
- package/dist/cjs/router-handler.js +171 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +100 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +190 -0
- package/dist/cjs/server.js +245 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/cjs/worker-federation-context.js +63 -0
- package/dist/cjs/worker-federation-contexts.js +97 -0
- package/dist/cjs/worker-federation.js +97 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +377 -0
- package/dist/esm/entry-application.mjs +44 -0
- package/dist/esm/entry-client.mjs +57 -0
- package/dist/esm/entry-server.mjs +91 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +167 -0
- package/dist/esm/i18n/I18nProvider.mjs +21 -0
- package/dist/esm/i18n/LocalizedLink.mjs +42 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-external-snapshot.mjs +244 -0
- package/dist/esm/router-handler.mjs +133 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +11 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +204 -0
- package/dist/esm/worker-federation-context.mjs +19 -0
- package/dist/esm/worker-federation-contexts.mjs +47 -0
- package/dist/esm/worker-federation.mjs +59 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +67 -0
- package/dist/esm-node/plugin/create.mjs +228 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +53 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +40 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +10 -0
- package/dist/types/entry-server.d.ts +14 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +20 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +14 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +11 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +20 -0
- package/dist/types/server.d.ts +47 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/dist/types/worker-federation-context.d.ts +34 -0
- package/dist/types/worker-federation-contexts.d.ts +25 -0
- package/dist/types/worker-federation.d.ts +20 -0
- package/package.json +278 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +568 -0
- package/src/entry-application.ts +76 -0
- package/src/entry-client.ts +90 -0
- package/src/entry-server.ts +153 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +307 -0
- package/src/i18n/I18nProvider.ts +47 -0
- package/src/i18n/LocalizedLink.ts +86 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +31 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +204 -0
- package/src/plugin/compiler.ts +104 -0
- package/src/plugin/create.ts +273 -0
- package/src/plugin/index.ts +107 -0
- package/src/plugin/profile.ts +50 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-external-snapshot.ts +363 -0
- package/src/router-handler.ts +185 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +21 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +274 -0
- package/src/server.ts +340 -0
- package/src/typecheck.ts +723 -0
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/README.md +0 -3
package/src/routes.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataOutcome,
|
|
3
|
+
FileSystemRouteIR,
|
|
4
|
+
PublicDataOutcome,
|
|
5
|
+
} from '@modern-js/renderer-core/data';
|
|
6
|
+
import {
|
|
7
|
+
type FileSystemRouteOptions,
|
|
8
|
+
loadFileSystemRoute,
|
|
9
|
+
type NativeLoaderMatch,
|
|
10
|
+
nativeRoutePath,
|
|
11
|
+
resolveRouteData as resolveNativeRouteData,
|
|
12
|
+
splitFileSystemRoutes,
|
|
13
|
+
} from '@modern-js/renderer-core/router';
|
|
14
|
+
import {
|
|
15
|
+
type AnyRoute,
|
|
16
|
+
type AnyRouteMatch,
|
|
17
|
+
type AnyRouter,
|
|
18
|
+
createRootRoute,
|
|
19
|
+
createRoute,
|
|
20
|
+
ErrorComponent,
|
|
21
|
+
type ErrorRouteComponent,
|
|
22
|
+
HeadContent,
|
|
23
|
+
type NotFoundRouteComponent,
|
|
24
|
+
notFound,
|
|
25
|
+
Outlet,
|
|
26
|
+
type RouteComponent,
|
|
27
|
+
redirect,
|
|
28
|
+
Scripts,
|
|
29
|
+
} from '@octanejs/tanstack-router';
|
|
30
|
+
import { createElement, Fragment } from 'octane';
|
|
31
|
+
|
|
32
|
+
const fileRouteHeads = new WeakMap<AnyRoute, AnyRoute['options']['head']>();
|
|
33
|
+
|
|
34
|
+
/** Head callbacks execute on the host; only the factory's original binding is admitted. */
|
|
35
|
+
export function hasUnchangedOctaneFileRouteHead(route: AnyRoute): boolean {
|
|
36
|
+
return fileRouteHeads.get(route) === route.options.head;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type {
|
|
40
|
+
FileSystemDataModule,
|
|
41
|
+
FileSystemRouteOptions,
|
|
42
|
+
} from '@modern-js/renderer-core/router';
|
|
43
|
+
export {
|
|
44
|
+
matchApplicationRouteIds,
|
|
45
|
+
matchApplicationRoutes,
|
|
46
|
+
RouteDataError,
|
|
47
|
+
selectApplicationDataRoute,
|
|
48
|
+
} from '@modern-js/renderer-core/router';
|
|
49
|
+
|
|
50
|
+
/** Generated imports contain native view bindings, keyed by the structural id. */
|
|
51
|
+
export interface FileSystemRouteModule {
|
|
52
|
+
component?: RouteComponent;
|
|
53
|
+
pendingComponent?: RouteComponent;
|
|
54
|
+
errorComponent?: ErrorRouteComponent;
|
|
55
|
+
notFoundComponent?: NotFoundRouteComponent;
|
|
56
|
+
validateSearch?: (search: Record<string, unknown>) => Record<string, unknown>;
|
|
57
|
+
head?: AnyRoute['options']['head'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Native route boundaries decide how redirects, missing data and errors render. */
|
|
61
|
+
export function resolveRouteData(
|
|
62
|
+
routeId: string,
|
|
63
|
+
outcome: DataOutcome | PublicDataOutcome,
|
|
64
|
+
): unknown {
|
|
65
|
+
return resolveNativeRouteData({ redirect, notFound }, routeId, outcome);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ReactiveNativeMatch {
|
|
69
|
+
get(): AnyRouteMatch | undefined;
|
|
70
|
+
subscribe(callback: () => void): { unsubscribe(): void };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isReactiveNativeMatch(value: unknown): value is ReactiveNativeMatch {
|
|
74
|
+
return (
|
|
75
|
+
typeof value === 'object' &&
|
|
76
|
+
value !== null &&
|
|
77
|
+
typeof Reflect.get(value, 'get') === 'function' &&
|
|
78
|
+
typeof Reflect.get(value, 'subscribe') === 'function'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A transport failure belongs to the native generation that fetched it. */
|
|
83
|
+
function observeRouteCompletion(
|
|
84
|
+
router: AnyRouter,
|
|
85
|
+
routeId: string,
|
|
86
|
+
controller: AbortController,
|
|
87
|
+
signal: AbortSignal,
|
|
88
|
+
completion: Promise<void>,
|
|
89
|
+
) {
|
|
90
|
+
void completion.catch(error => {
|
|
91
|
+
if (signal.aborted) return;
|
|
92
|
+
const route = Object.values(router.routesById).find(
|
|
93
|
+
candidate => candidate.options.staticData?.ultramodernRouteId === routeId,
|
|
94
|
+
);
|
|
95
|
+
if (!route) return;
|
|
96
|
+
const store = router.stores.getMatchStore(route.id);
|
|
97
|
+
let subscription: { unsubscribe(): void } | undefined;
|
|
98
|
+
let settled = false;
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
subscription?.unsubscribe();
|
|
101
|
+
signal.removeEventListener('abort', onAbort);
|
|
102
|
+
};
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
settled = true;
|
|
105
|
+
cleanup();
|
|
106
|
+
};
|
|
107
|
+
const publish = async () => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
const current = store.get();
|
|
110
|
+
if (!current) return;
|
|
111
|
+
if (
|
|
112
|
+
current.abortController !== controller ||
|
|
113
|
+
signal.aborted ||
|
|
114
|
+
(current.status !== 'pending' && current.status !== 'success')
|
|
115
|
+
) {
|
|
116
|
+
onAbort();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// The initial loader can still be pending after its critical value returns.
|
|
120
|
+
if (current.status !== 'success' || current.isFetching) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
cleanup();
|
|
123
|
+
const route = router.routesById[current.routeId];
|
|
124
|
+
let failure = error;
|
|
125
|
+
try {
|
|
126
|
+
await route?.options.errorComponent?.preload?.();
|
|
127
|
+
if (signal.aborted || store.get()?.abortController !== controller)
|
|
128
|
+
return;
|
|
129
|
+
route?.options.onError?.(failure);
|
|
130
|
+
} catch (onErrorFailure) {
|
|
131
|
+
failure = onErrorFailure;
|
|
132
|
+
}
|
|
133
|
+
// The error component is ready before publishing a late transport failure.
|
|
134
|
+
if (signal.aborted) return;
|
|
135
|
+
router.stores.setMatches(
|
|
136
|
+
router.stores.matches.get().map(previous =>
|
|
137
|
+
previous.routeId === route.id &&
|
|
138
|
+
previous.abortController === controller &&
|
|
139
|
+
previous.status === 'success' &&
|
|
140
|
+
!previous.isFetching
|
|
141
|
+
? {
|
|
142
|
+
...previous,
|
|
143
|
+
error: failure,
|
|
144
|
+
status: 'error',
|
|
145
|
+
isFetching: false,
|
|
146
|
+
}
|
|
147
|
+
: previous,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
152
|
+
if (isReactiveNativeMatch(store)) subscription = store.subscribe(publish);
|
|
153
|
+
if (settled) subscription?.unsubscribe();
|
|
154
|
+
else publish();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Pass filesystem structure to native factories without matching any URL. */
|
|
159
|
+
export function createFileSystemRouteTree<Context = unknown>(
|
|
160
|
+
routes: readonly FileSystemRouteIR[],
|
|
161
|
+
modules: Readonly<Record<string, FileSystemRouteModule>>,
|
|
162
|
+
options: FileSystemRouteOptions<Context, AnyRouter> = {},
|
|
163
|
+
): AnyRoute {
|
|
164
|
+
const { root: rootDescriptor, children } = splitFileSystemRoutes(routes);
|
|
165
|
+
|
|
166
|
+
function bindings(
|
|
167
|
+
route: FileSystemRouteIR,
|
|
168
|
+
errorComponent?: ErrorRouteComponent,
|
|
169
|
+
) {
|
|
170
|
+
const needsData = Boolean(route.modules?.data || route.modules?.clientData);
|
|
171
|
+
if (needsData && !options.loadRoute) {
|
|
172
|
+
throw new Error(`An Octane data route requires its loader: ${route.id}`);
|
|
173
|
+
}
|
|
174
|
+
const loadRoute = options.loadRoute;
|
|
175
|
+
const { head, ...module } = modules[route.id] ?? {};
|
|
176
|
+
return {
|
|
177
|
+
...module,
|
|
178
|
+
...(head !== undefined ? { head } : {}),
|
|
179
|
+
// Octane's server renders an uncaught match error through the root
|
|
180
|
+
// outlet's Suspense, which defers it to a client retry that then
|
|
181
|
+
// replaces the whole layout. Give every child match the nearest
|
|
182
|
+
// error.tsx (or the native default) so the error renders in place.
|
|
183
|
+
...(errorComponent ? { errorComponent } : {}),
|
|
184
|
+
component: module.component ?? Outlet,
|
|
185
|
+
staticData: { ultramodernRouteId: route.id },
|
|
186
|
+
...(needsData && loadRoute
|
|
187
|
+
? {
|
|
188
|
+
loader: async (match: NativeLoaderMatch) => {
|
|
189
|
+
const { outcome, signal } = await loadFileSystemRoute(
|
|
190
|
+
route,
|
|
191
|
+
loadRoute,
|
|
192
|
+
options,
|
|
193
|
+
match,
|
|
194
|
+
);
|
|
195
|
+
if ('completion' in outcome && outcome.completion) {
|
|
196
|
+
const router = options.getRouter?.();
|
|
197
|
+
if (!router) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
'An Octane streamed data route requires its native router',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
let failed = false;
|
|
203
|
+
let failure: unknown;
|
|
204
|
+
void outcome.completion.catch(error => {
|
|
205
|
+
failed = true;
|
|
206
|
+
failure = error;
|
|
207
|
+
});
|
|
208
|
+
// Detect a terminal failure already received with the first frame.
|
|
209
|
+
await Promise.resolve();
|
|
210
|
+
signal.throwIfAborted();
|
|
211
|
+
if (failed) throw failure;
|
|
212
|
+
observeRouteCompletion(
|
|
213
|
+
router,
|
|
214
|
+
route.id,
|
|
215
|
+
match.abortController,
|
|
216
|
+
signal,
|
|
217
|
+
outcome.completion,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
options.onOutcome?.(route.id, outcome);
|
|
221
|
+
return resolveRouteData(route.id, outcome);
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rootBindings = rootDescriptor
|
|
229
|
+
? bindings(rootDescriptor)
|
|
230
|
+
: { component: Outlet };
|
|
231
|
+
const authoredRoot: RouteComponent = rootBindings.component;
|
|
232
|
+
function OctaneDocumentRoot(props: Parameters<RouteComponent>[0]) {
|
|
233
|
+
return createElement(
|
|
234
|
+
Fragment,
|
|
235
|
+
null,
|
|
236
|
+
createElement(HeadContent),
|
|
237
|
+
createElement(authoredRoot, props),
|
|
238
|
+
createElement(Scripts),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (authoredRoot.preload !== undefined) {
|
|
242
|
+
OctaneDocumentRoot.preload = authoredRoot.preload;
|
|
243
|
+
}
|
|
244
|
+
const rootErrorComponent =
|
|
245
|
+
(rootDescriptor && modules[rootDescriptor.id]?.errorComponent) ||
|
|
246
|
+
ErrorComponent;
|
|
247
|
+
const root = createRootRoute({
|
|
248
|
+
...rootBindings,
|
|
249
|
+
component: OctaneDocumentRoot,
|
|
250
|
+
});
|
|
251
|
+
fileRouteHeads.set(root, root.options.head);
|
|
252
|
+
|
|
253
|
+
function bind(
|
|
254
|
+
route: FileSystemRouteIR,
|
|
255
|
+
parent: AnyRoute,
|
|
256
|
+
inheritedErrorComponent: ErrorRouteComponent,
|
|
257
|
+
): AnyRoute {
|
|
258
|
+
const errorComponent =
|
|
259
|
+
modules[route.id]?.errorComponent ?? inheritedErrorComponent;
|
|
260
|
+
const native = createRoute({
|
|
261
|
+
getParentRoute: () => parent,
|
|
262
|
+
...nativeRoutePath(route),
|
|
263
|
+
...bindings(route, errorComponent),
|
|
264
|
+
});
|
|
265
|
+
fileRouteHeads.set(native, native.options.head);
|
|
266
|
+
return native.addChildren(
|
|
267
|
+
route.children.map(child => bind(child, native, errorComponent)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return root.addChildren(
|
|
272
|
+
children.map(route => bind(route, root, rootErrorComponent)),
|
|
273
|
+
);
|
|
274
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DocumentAsset,
|
|
3
|
+
type DocumentInlineData,
|
|
4
|
+
prepareDocument,
|
|
5
|
+
} from '@modern-js/renderer-core/document';
|
|
6
|
+
import type { NativeFederationBinding } from '@modern-js/renderer-core/federation';
|
|
7
|
+
import {
|
|
8
|
+
type RequestSession,
|
|
9
|
+
type ResponsePolicy,
|
|
10
|
+
untilAborted,
|
|
11
|
+
} from '@modern-js/renderer-core/session';
|
|
12
|
+
import type { WorkerFederationTransport } from '@modern-js/renderer-core/worker-federation';
|
|
13
|
+
import type { AnyRouter } from '@octanejs/tanstack-router';
|
|
14
|
+
import { transformReadableStreamWithRouter } from '@octanejs/tanstack-router/ssr/server';
|
|
15
|
+
import {
|
|
16
|
+
createElement,
|
|
17
|
+
earlySignalBootstrapScript,
|
|
18
|
+
renderToReadableStream,
|
|
19
|
+
type ServerRenderNode,
|
|
20
|
+
type StreamOptions,
|
|
21
|
+
} from 'octane/server';
|
|
22
|
+
import {
|
|
23
|
+
assertNativeHydrationBuildId,
|
|
24
|
+
assertOctaneIdentity,
|
|
25
|
+
} from './bootstrap';
|
|
26
|
+
import { createFederationScope, FederationRoot } from './federation-context';
|
|
27
|
+
import { WorkerFederationRoot } from './worker-federation-context';
|
|
28
|
+
|
|
29
|
+
export interface OctaneDocumentOptions {
|
|
30
|
+
/** One identity for this response, shared with the client's signal receiver. */
|
|
31
|
+
readonly documentId: string;
|
|
32
|
+
/** Actual native client compilation hash, separate from application identity. */
|
|
33
|
+
readonly nativeHydrationBuildId: string;
|
|
34
|
+
readonly rootId?: string;
|
|
35
|
+
readonly lang?: string;
|
|
36
|
+
readonly nonce?: string;
|
|
37
|
+
readonly assets?: readonly DocumentAsset[];
|
|
38
|
+
/** Placed after the renderer bootstrap and before the entry scripts. */
|
|
39
|
+
readonly inlineData?: readonly DocumentInlineData[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type { DocumentInlineData as OctaneDocumentInlineData };
|
|
43
|
+
|
|
44
|
+
export interface OctaneDocumentResponseOptions<
|
|
45
|
+
Bindings extends object = object,
|
|
46
|
+
> {
|
|
47
|
+
readonly session: RequestSession<Bindings>;
|
|
48
|
+
readonly document: OctaneDocumentOptions;
|
|
49
|
+
readonly responsePolicy?: ResponsePolicy;
|
|
50
|
+
/** Native route loading finishes its blocking HTTP work before rendering. */
|
|
51
|
+
readonly resolveResponse?: (
|
|
52
|
+
session: RequestSession<Bindings>,
|
|
53
|
+
) => ResponsePolicy | Promise<ResponsePolicy>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RenderOctaneApplicationOptions<
|
|
57
|
+
Bindings extends object = object,
|
|
58
|
+
> extends OctaneDocumentResponseOptions<Bindings> {
|
|
59
|
+
readonly App: ServerRenderNode;
|
|
60
|
+
readonly props?: unknown;
|
|
61
|
+
/** Native router serialization, owned by this request's router SSR context. */
|
|
62
|
+
readonly injection?: StreamOptions['injection'];
|
|
63
|
+
readonly router?: AnyRouter;
|
|
64
|
+
readonly routerSerializationTimeoutMs?: number;
|
|
65
|
+
/** This server compilation's native Module Federation runtime. */
|
|
66
|
+
readonly federation?: NativeFederationBinding;
|
|
67
|
+
readonly workerFederation?: WorkerFederationTransport;
|
|
68
|
+
readonly workerFederationLocale?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function prepareOctaneDocument<Bindings extends object>(
|
|
72
|
+
session: RequestSession<Bindings>,
|
|
73
|
+
document: OctaneDocumentOptions,
|
|
74
|
+
hydrating: boolean,
|
|
75
|
+
) {
|
|
76
|
+
assertOctaneIdentity(session.identity);
|
|
77
|
+
if (session.platform.kind !== 'node' && session.platform.kind !== 'worker') {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'An Octane document requires a Node or worker request platform.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
assertNativeHydrationBuildId(document.nativeHydrationBuildId);
|
|
83
|
+
const nonce = document.nonce;
|
|
84
|
+
return prepareDocument(
|
|
85
|
+
{
|
|
86
|
+
identity: session.identity,
|
|
87
|
+
documentId: document.documentId,
|
|
88
|
+
hydrating,
|
|
89
|
+
nativeHydrationBuildId: document.nativeHydrationBuildId,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
rootId: document.rootId,
|
|
93
|
+
lang: document.lang,
|
|
94
|
+
nonce: { script: nonce, style: nonce },
|
|
95
|
+
assets: document.assets,
|
|
96
|
+
inlineData: document.inlineData,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveDocumentResponse<Bindings extends object>(
|
|
102
|
+
input: OctaneDocumentResponseOptions<Bindings>,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const { session } = input;
|
|
105
|
+
if (input.responsePolicy && input.resolveResponse) {
|
|
106
|
+
throw new Error('Choose one blocking Octane response policy resolver.');
|
|
107
|
+
}
|
|
108
|
+
if (input.resolveResponse) {
|
|
109
|
+
// A resolver that ignores cancellation must not hold the request.
|
|
110
|
+
session.resolveResponse(
|
|
111
|
+
await untilAborted(input.resolveResponse(session), session.signal),
|
|
112
|
+
);
|
|
113
|
+
} else if (input.responsePolicy) {
|
|
114
|
+
session.resolveResponse(input.responsePolicy);
|
|
115
|
+
} else if (!session.responsePolicy) {
|
|
116
|
+
session.resolveResponse({
|
|
117
|
+
kind: 'document',
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: [['content-type', 'text/html; charset=utf-8']],
|
|
120
|
+
cache: { mode: 'no-store' },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Deliver the CSR document; native browser mounting owns the empty root. */
|
|
126
|
+
export async function renderOctaneCSRDocument<Bindings extends object>(
|
|
127
|
+
input: OctaneDocumentResponseOptions<Bindings>,
|
|
128
|
+
): Promise<Response> {
|
|
129
|
+
const { session } = input;
|
|
130
|
+
try {
|
|
131
|
+
const { rootId, lang, bootstrap, headAssets, entryScripts } =
|
|
132
|
+
prepareOctaneDocument(session, input.document, false);
|
|
133
|
+
await resolveDocumentResponse(input);
|
|
134
|
+
if (session.responsePolicy?.kind === 'terminal')
|
|
135
|
+
return session.respond(null);
|
|
136
|
+
session.startRendering();
|
|
137
|
+
const bytes = new TextEncoder().encode(
|
|
138
|
+
`<!doctype html><html lang="${lang}"><head>${headAssets}</head><body><div id="${rootId}"></div>${bootstrap}${entryScripts}</body></html>`,
|
|
139
|
+
);
|
|
140
|
+
return session.respond(
|
|
141
|
+
new ReadableStream<Uint8Array>({
|
|
142
|
+
start(controller) {
|
|
143
|
+
controller.enqueue(bytes);
|
|
144
|
+
controller.close();
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
await session.fail(error);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render a native Octane application into one request-owned HTML document.
|
|
156
|
+
* The dispatcher owns the session; this adapter owns native stream consumption
|
|
157
|
+
* and document placement. A late native error fails the body without changing
|
|
158
|
+
* the status and headers already sent to the client.
|
|
159
|
+
*/
|
|
160
|
+
export async function renderOctaneApplication<Bindings extends object>(
|
|
161
|
+
input: RenderOctaneApplicationOptions<Bindings>,
|
|
162
|
+
): Promise<Response> {
|
|
163
|
+
const { session, document } = input;
|
|
164
|
+
try {
|
|
165
|
+
const { rootId, lang, bootstrap, headAssets, entryScripts } =
|
|
166
|
+
prepareOctaneDocument(session, document, true);
|
|
167
|
+
const earlyBootstrap = earlySignalBootstrapScript(
|
|
168
|
+
document.nonce === undefined ? {} : { nonce: document.nonce },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
await resolveDocumentResponse(input);
|
|
172
|
+
if (session.responsePolicy?.kind === 'terminal')
|
|
173
|
+
return session.respond(null);
|
|
174
|
+
session.startRendering();
|
|
175
|
+
|
|
176
|
+
let head = '';
|
|
177
|
+
// Worker SSR and browser activation must own the same provider ranges,
|
|
178
|
+
// even when only the browser needs a Module Federation loader.
|
|
179
|
+
const federation =
|
|
180
|
+
input.federation || input.workerFederation
|
|
181
|
+
? createFederationScope(input.federation, true, document.nonce)
|
|
182
|
+
: undefined;
|
|
183
|
+
const App = federation
|
|
184
|
+
? () =>
|
|
185
|
+
createElement(FederationRoot, {
|
|
186
|
+
scope: federation,
|
|
187
|
+
children:
|
|
188
|
+
typeof input.App === 'function'
|
|
189
|
+
? createElement(input.App, input.props)
|
|
190
|
+
: input.App,
|
|
191
|
+
})
|
|
192
|
+
: input.App;
|
|
193
|
+
// Native SSR retries the application while lazy boundaries settle. Keep
|
|
194
|
+
// their request-owned context identity stable across those render passes.
|
|
195
|
+
const workerFederation = input.workerFederation
|
|
196
|
+
? {
|
|
197
|
+
transport: input.workerFederation,
|
|
198
|
+
locale: input.workerFederationLocale ?? 'default',
|
|
199
|
+
signal: session.signal,
|
|
200
|
+
}
|
|
201
|
+
: undefined;
|
|
202
|
+
const NativeApp = workerFederation
|
|
203
|
+
? () =>
|
|
204
|
+
createElement(WorkerFederationRoot, {
|
|
205
|
+
scope: workerFederation,
|
|
206
|
+
children:
|
|
207
|
+
typeof App === 'function' ? createElement(App, input.props) : App,
|
|
208
|
+
})
|
|
209
|
+
: App;
|
|
210
|
+
const nativeStream = await renderToReadableStream(NativeApp, input.props, {
|
|
211
|
+
signal: session.signal,
|
|
212
|
+
...(document.nonce === undefined ? {} : { nonce: document.nonce }),
|
|
213
|
+
earlySignalBootstrap: 'external',
|
|
214
|
+
...(input.injection === undefined ? {} : { injection: input.injection }),
|
|
215
|
+
streamedSignals: {
|
|
216
|
+
buildId: document.nativeHydrationBuildId,
|
|
217
|
+
documentId: document.documentId,
|
|
218
|
+
},
|
|
219
|
+
externalSnapshots: { documentId: document.documentId },
|
|
220
|
+
headChannel: 'separate',
|
|
221
|
+
onHeadReady(value) {
|
|
222
|
+
head = value;
|
|
223
|
+
},
|
|
224
|
+
onError(error) {
|
|
225
|
+
// Octane reports recoverable Suspense-boundary errors here as well as
|
|
226
|
+
// fatal ones. A recoverable error streams the boundary's client
|
|
227
|
+
// fallback into an otherwise valid document, so it must not terminate
|
|
228
|
+
// the request. Fatal errors reject the shell or allReady instead.
|
|
229
|
+
if (session.signal.aborted) return;
|
|
230
|
+
// A document carrying an errored boundary must not enter a shared cache.
|
|
231
|
+
session.markFallback();
|
|
232
|
+
console.error('Octane render error:', error);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
// Observe rejection immediately, but consume concurrently: native allReady
|
|
236
|
+
// requires downstream demand and cannot be awaited before reading the body.
|
|
237
|
+
const ready = nativeStream.allReady.then(
|
|
238
|
+
() => ({ ok: true as const }),
|
|
239
|
+
error => ({ ok: false as const, error }),
|
|
240
|
+
);
|
|
241
|
+
const reader = nativeStream.getReader();
|
|
242
|
+
const encoder = new TextEncoder();
|
|
243
|
+
const prefix = encoder.encode(
|
|
244
|
+
`<!doctype html><html lang="${lang}"><head>${head}${headAssets}</head><body>${earlyBootstrap}<div id="${rootId}">`,
|
|
245
|
+
);
|
|
246
|
+
const shellBootstrap = encoder.encode(`</div>${bootstrap}${entryScripts}`);
|
|
247
|
+
const suffix = encoder.encode('</body></html>');
|
|
248
|
+
let phase: 'prefix' | 'shell' | 'bootstrap' | 'native' | 'done' = 'prefix';
|
|
249
|
+
let released = false;
|
|
250
|
+
const release = () => {
|
|
251
|
+
if (released) return;
|
|
252
|
+
released = true;
|
|
253
|
+
reader.releaseLock();
|
|
254
|
+
};
|
|
255
|
+
let cancellation: Promise<void> | undefined;
|
|
256
|
+
const cancelNative = (reason: unknown): Promise<void> => {
|
|
257
|
+
if (cancellation) return cancellation;
|
|
258
|
+
if (released) return Promise.resolve();
|
|
259
|
+
cancellation = reader.cancel(reason).finally(release);
|
|
260
|
+
return cancellation;
|
|
261
|
+
};
|
|
262
|
+
// Cleanup starts native cancellation without waiting for it: a native
|
|
263
|
+
// stream that never finishes cancelling must not hold the session.
|
|
264
|
+
const releaseNative = (reason: unknown) => {
|
|
265
|
+
void cancelNative(reason).catch(() => {});
|
|
266
|
+
};
|
|
267
|
+
try {
|
|
268
|
+
session.registerCleanup(() => releaseNative(session.signal.reason));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// The request can abort between native shell readiness and registration.
|
|
271
|
+
releaseNative(error);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const body = new ReadableStream<Uint8Array>(
|
|
275
|
+
{
|
|
276
|
+
async pull(controller) {
|
|
277
|
+
try {
|
|
278
|
+
if (phase === 'prefix') {
|
|
279
|
+
phase = 'shell';
|
|
280
|
+
controller.enqueue(prefix);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (phase === 'bootstrap') {
|
|
284
|
+
phase = 'native';
|
|
285
|
+
controller.enqueue(shellBootstrap);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const next = await reader.read();
|
|
289
|
+
if (!next.done) {
|
|
290
|
+
// Octane 0.7.1 writes its complete initial shell as the first
|
|
291
|
+
// chunk, including initial signal selections. Close only our
|
|
292
|
+
// root after it, then start the browser before deferred segments.
|
|
293
|
+
// Later native carriers find their targets in the whole document.
|
|
294
|
+
if (phase === 'shell') phase = 'bootstrap';
|
|
295
|
+
controller.enqueue(next.value);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (phase === 'shell') {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'Octane ended before publishing its initial shell.',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const completion = await ready;
|
|
304
|
+
release();
|
|
305
|
+
if (!completion.ok) throw completion.error;
|
|
306
|
+
phase = 'done';
|
|
307
|
+
controller.enqueue(suffix);
|
|
308
|
+
controller.close();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
phase = 'done';
|
|
311
|
+
// An errored ReadableStream does not invoke its cancel callback.
|
|
312
|
+
// Release the native owner here as well as on consumer cancellation.
|
|
313
|
+
releaseNative(error);
|
|
314
|
+
controller.error(error);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
cancel(reason) {
|
|
318
|
+
phase = 'done';
|
|
319
|
+
releaseNative(reason);
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{ highWaterMark: 0 },
|
|
323
|
+
);
|
|
324
|
+
return session.respond(
|
|
325
|
+
input.router
|
|
326
|
+
? transformReadableStreamWithRouter(input.router, body, {
|
|
327
|
+
signal: session.signal,
|
|
328
|
+
rendererSafePoint: 'script-close',
|
|
329
|
+
timeoutMs: input.routerSerializationTimeoutMs,
|
|
330
|
+
onAbort: reason => {
|
|
331
|
+
void session.fail(reason);
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
: body,
|
|
335
|
+
);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
await session.fail(error);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|