@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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/bin/octane-tsc.mjs +4 -0
  3. package/dist/cjs/bootstrap.js +68 -0
  4. package/dist/cjs/client.js +421 -0
  5. package/dist/cjs/entry-application.js +88 -0
  6. package/dist/cjs/entry-client.js +95 -0
  7. package/dist/cjs/entry-server.js +135 -0
  8. package/dist/cjs/federation-context.js +72 -0
  9. package/dist/cjs/federation.js +205 -0
  10. package/dist/cjs/i18n/I18nProvider.js +59 -0
  11. package/dist/cjs/i18n/LocalizedLink.js +80 -0
  12. package/dist/cjs/i18n/context.js +41 -0
  13. package/dist/cjs/i18n/index.js +83 -0
  14. package/dist/cjs/i18n/languageSync.js +99 -0
  15. package/dist/cjs/i18n/types.js +18 -0
  16. package/dist/cjs/i18n/useI18n.js +110 -0
  17. package/dist/cjs/i18n-external-snapshot.js +645 -0
  18. package/dist/cjs/index.js +46 -0
  19. package/dist/cjs/manifest.js +167 -0
  20. package/dist/cjs/plugin/compiler.js +122 -0
  21. package/dist/cjs/plugin/create.js +266 -0
  22. package/dist/cjs/plugin/index.js +151 -0
  23. package/dist/cjs/plugin/profile.js +91 -0
  24. package/dist/cjs/plugin/source.js +56 -0
  25. package/dist/cjs/router-client.js +139 -0
  26. package/dist/cjs/router-external-snapshot.js +291 -0
  27. package/dist/cjs/router-handler.js +171 -0
  28. package/dist/cjs/router-server-cleanup.js +46 -0
  29. package/dist/cjs/router-server-snapshot.js +405 -0
  30. package/dist/cjs/router-server.js +100 -0
  31. package/dist/cjs/router-snapshot.js +270 -0
  32. package/dist/cjs/router.js +218 -0
  33. package/dist/cjs/routes.js +190 -0
  34. package/dist/cjs/server.js +245 -0
  35. package/dist/cjs/typecheck.js +615 -0
  36. package/dist/cjs/worker-federation-context.js +63 -0
  37. package/dist/cjs/worker-federation-contexts.js +97 -0
  38. package/dist/cjs/worker-federation.js +97 -0
  39. package/dist/esm/bootstrap.mjs +24 -0
  40. package/dist/esm/client.mjs +377 -0
  41. package/dist/esm/entry-application.mjs +44 -0
  42. package/dist/esm/entry-client.mjs +57 -0
  43. package/dist/esm/entry-server.mjs +91 -0
  44. package/dist/esm/federation-context.mjs +25 -0
  45. package/dist/esm/federation.mjs +167 -0
  46. package/dist/esm/i18n/I18nProvider.mjs +21 -0
  47. package/dist/esm/i18n/LocalizedLink.mjs +42 -0
  48. package/dist/esm/i18n/context.mjs +3 -0
  49. package/dist/esm/i18n/index.mjs +8 -0
  50. package/dist/esm/i18n/languageSync.mjs +61 -0
  51. package/dist/esm/i18n/types.mjs +0 -0
  52. package/dist/esm/i18n/useI18n.mjs +72 -0
  53. package/dist/esm/i18n-external-snapshot.mjs +576 -0
  54. package/dist/esm/index.mjs +8 -0
  55. package/dist/esm/manifest.mjs +117 -0
  56. package/dist/esm/router-client.mjs +98 -0
  57. package/dist/esm/router-external-snapshot.mjs +244 -0
  58. package/dist/esm/router-handler.mjs +133 -0
  59. package/dist/esm/router-server-cleanup.mjs +8 -0
  60. package/dist/esm/router-server-snapshot.mjs +367 -0
  61. package/dist/esm/router-server.mjs +11 -0
  62. package/dist/esm/router-snapshot.mjs +199 -0
  63. package/dist/esm/router.mjs +94 -0
  64. package/dist/esm/routes.mjs +134 -0
  65. package/dist/esm/server.mjs +204 -0
  66. package/dist/esm/worker-federation-context.mjs +19 -0
  67. package/dist/esm/worker-federation-contexts.mjs +47 -0
  68. package/dist/esm/worker-federation.mjs +59 -0
  69. package/dist/esm-node/bootstrap.mjs +24 -0
  70. package/dist/esm-node/manifest.mjs +117 -0
  71. package/dist/esm-node/plugin/compiler.mjs +67 -0
  72. package/dist/esm-node/plugin/create.mjs +228 -0
  73. package/dist/esm-node/plugin/index.mjs +103 -0
  74. package/dist/esm-node/plugin/profile.mjs +53 -0
  75. package/dist/esm-node/plugin/source.mjs +7 -0
  76. package/dist/esm-node/typecheck.mjs +560 -0
  77. package/dist/types/bootstrap.d.ts +10 -0
  78. package/dist/types/client.d.ts +40 -0
  79. package/dist/types/entry-application.d.ts +19 -0
  80. package/dist/types/entry-client.d.ts +10 -0
  81. package/dist/types/entry-server.d.ts +14 -0
  82. package/dist/types/federation-context.d.ts +28 -0
  83. package/dist/types/federation.d.ts +20 -0
  84. package/dist/types/i18n/I18nProvider.d.ts +8 -0
  85. package/dist/types/i18n/LocalizedLink.d.ts +14 -0
  86. package/dist/types/i18n/context.d.ts +9 -0
  87. package/dist/types/i18n/index.d.ts +9 -0
  88. package/dist/types/i18n/languageSync.d.ts +26 -0
  89. package/dist/types/i18n/types.d.ts +34 -0
  90. package/dist/types/i18n/useI18n.d.ts +21 -0
  91. package/dist/types/i18n-external-snapshot.d.ts +43 -0
  92. package/dist/types/index.d.ts +9 -0
  93. package/dist/types/manifest.d.ts +34 -0
  94. package/dist/types/plugin/compiler-manifest.d.cts +10 -0
  95. package/dist/types/plugin/compiler.d.ts +5 -0
  96. package/dist/types/plugin/create.d.ts +23 -0
  97. package/dist/types/plugin/index.d.ts +3 -0
  98. package/dist/types/plugin/profile.d.ts +2 -0
  99. package/dist/types/plugin/source.d.ts +1 -0
  100. package/dist/types/plugin/svg-component-template.d.cts +6 -0
  101. package/dist/types/router-client.d.ts +12 -0
  102. package/dist/types/router-external-snapshot.d.ts +10 -0
  103. package/dist/types/router-handler.d.ts +13 -0
  104. package/dist/types/router-server-cleanup.d.ts +3 -0
  105. package/dist/types/router-server-snapshot.d.ts +15 -0
  106. package/dist/types/router-server.d.ts +11 -0
  107. package/dist/types/router-snapshot.d.ts +14 -0
  108. package/dist/types/router.d.ts +30 -0
  109. package/dist/types/routes.d.ts +20 -0
  110. package/dist/types/server.d.ts +47 -0
  111. package/dist/types/typecheck.d.ts +31 -0
  112. package/dist/types/worker-federation-context.d.ts +34 -0
  113. package/dist/types/worker-federation-contexts.d.ts +25 -0
  114. package/dist/types/worker-federation.d.ts +20 -0
  115. package/package.json +278 -4
  116. package/src/bootstrap.ts +60 -0
  117. package/src/client.ts +568 -0
  118. package/src/entry-application.ts +76 -0
  119. package/src/entry-client.ts +90 -0
  120. package/src/entry-server.ts +153 -0
  121. package/src/federation-context.ts +53 -0
  122. package/src/federation.ts +307 -0
  123. package/src/i18n/I18nProvider.ts +47 -0
  124. package/src/i18n/LocalizedLink.ts +86 -0
  125. package/src/i18n/context.ts +12 -0
  126. package/src/i18n/index.ts +31 -0
  127. package/src/i18n/languageSync.ts +132 -0
  128. package/src/i18n/types.ts +36 -0
  129. package/src/i18n/useI18n.ts +119 -0
  130. package/src/i18n-external-snapshot.ts +803 -0
  131. package/src/index.ts +10 -0
  132. package/src/manifest.ts +253 -0
  133. package/src/plugin/compiler-manifest.cjs +204 -0
  134. package/src/plugin/compiler.ts +104 -0
  135. package/src/plugin/create.ts +273 -0
  136. package/src/plugin/index.ts +107 -0
  137. package/src/plugin/profile.ts +50 -0
  138. package/src/plugin/source.ts +16 -0
  139. package/src/plugin/svg-component-template.cjs +17 -0
  140. package/src/router-client.ts +144 -0
  141. package/src/router-external-snapshot.ts +363 -0
  142. package/src/router-handler.ts +185 -0
  143. package/src/router-server-cleanup.ts +11 -0
  144. package/src/router-server-snapshot.ts +564 -0
  145. package/src/router-server.ts +21 -0
  146. package/src/router-snapshot.ts +251 -0
  147. package/src/router.ts +192 -0
  148. package/src/routes.ts +274 -0
  149. package/src/server.ts +340 -0
  150. package/src/typecheck.ts +723 -0
  151. package/src/worker-federation-context.ts +51 -0
  152. package/src/worker-federation-contexts.ts +91 -0
  153. package/src/worker-federation.ts +120 -0
  154. package/README.md +0 -3
@@ -0,0 +1,363 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
2
+ import {
3
+ type AnyRoute,
4
+ type AnyRouter,
5
+ associateRouterRenderProfile,
6
+ captureRouterRenderSnapshot,
7
+ createRouterRenderRouteTree,
8
+ defaultParseSearch,
9
+ defaultStringifySearch,
10
+ disposeRouterRenderSnapshot,
11
+ getRouterRenderProfile,
12
+ matchContext,
13
+ type RouterRenderJsonRecord,
14
+ type RouterRenderSnapshot,
15
+ registerRouterRenderProfile,
16
+ restoreRouterRenderSnapshot,
17
+ routerContext,
18
+ } from '@octanejs/tanstack-router';
19
+ import { hasUnchangedOctaneFileRouteHead } from './routes';
20
+ import {
21
+ getOctaneWorkerRouterRewrite,
22
+ OCTANE_WORKER_CONTEXT_KEYS,
23
+ registerOctaneWorkerContext,
24
+ } from './worker-federation-contexts';
25
+
26
+ export const OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE = Object.freeze({
27
+ key: 'ultramodern.file-routes',
28
+ version: '1',
29
+ });
30
+
31
+ interface GeneratedRouterOrigin {
32
+ readonly publicContext: RouterRenderJsonRecord;
33
+ readonly rewrite: AnyRouter['options']['rewrite'];
34
+ }
35
+ const origins = new WeakMap<AnyRouter, GeneratedRouterOrigin>();
36
+ let profileRegistered = false;
37
+
38
+ function plainRecord(value: unknown): value is Record<string, unknown> {
39
+ return (
40
+ value !== null &&
41
+ typeof value === 'object' &&
42
+ [Object.prototype, null].includes(Object.getPrototypeOf(value))
43
+ );
44
+ }
45
+
46
+ function record(
47
+ value: unknown,
48
+ fields?: readonly string[],
49
+ ): Record<string, unknown> {
50
+ if (!plainRecord(value))
51
+ throw new TypeError('Worker router context requires a plain record');
52
+ for (const key of Reflect.ownKeys(value)) {
53
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
54
+ if (
55
+ typeof key !== 'string' ||
56
+ (fields && !fields.includes(key)) ||
57
+ !descriptor ||
58
+ !descriptor.enumerable ||
59
+ !('value' in descriptor)
60
+ )
61
+ throw new TypeError('Unknown Worker router context field or accessor');
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function nonempty(value: unknown): string {
67
+ if (typeof value !== 'string' || !value.trim() || value.length > 1024)
68
+ throw new TypeError('Invalid Worker router identity');
69
+ return value;
70
+ }
71
+
72
+ /** Only the host entry's public logical identity crosses the Worker boundary. */
73
+ function publicContext(value: unknown): RouterRenderJsonRecord {
74
+ const context = record(value, ['ultramodern']);
75
+ const metadata = record(context.ultramodern, ['rendererIdentity']);
76
+ const identity = record(metadata.rendererIdentity, [
77
+ 'renderer',
78
+ 'appId',
79
+ 'entryName',
80
+ 'protocolVersion',
81
+ 'buildId',
82
+ ]);
83
+ if (identity.renderer !== 'octane' || identity.protocolVersion !== 1)
84
+ throw new TypeError('Unsupported Worker router identity');
85
+ return Object.freeze({
86
+ ultramodern: Object.freeze({
87
+ rendererIdentity: Object.freeze({
88
+ renderer: 'octane',
89
+ appId: nonempty(identity.appId),
90
+ entryName: nonempty(identity.entryName),
91
+ protocolVersion: 1,
92
+ buildId: nonempty(identity.buildId),
93
+ }),
94
+ }),
95
+ });
96
+ }
97
+
98
+ function localizedUrls(value: unknown): LocalisedUrlsOption | undefined {
99
+ if (value === undefined || typeof value === 'boolean') return value;
100
+ const input = record(value);
101
+ const result: Exclude<LocalisedUrlsOption, boolean> = {};
102
+ for (const [path, translations] of Object.entries(input)) {
103
+ const names: Record<string, string> = {};
104
+ for (const [language, target] of Object.entries(record(translations)))
105
+ names[nonempty(language)] = nonempty(target);
106
+ result[path] = names;
107
+ }
108
+ return result;
109
+ }
110
+
111
+ function rewriteDescriptor(value: unknown):
112
+ | {
113
+ readonly language: string;
114
+ readonly languages: readonly string[];
115
+ readonly localisedUrls?: LocalisedUrlsOption;
116
+ }
117
+ | undefined {
118
+ if (value === undefined) return undefined;
119
+ const input = record(value, ['language', 'languages', 'localisedUrls']);
120
+ if (
121
+ !Array.isArray(input.languages) ||
122
+ !input.languages.length ||
123
+ input.languages.length > 64
124
+ )
125
+ throw new TypeError('Invalid Worker router i18n languages');
126
+ const languages = input.languages.map(nonempty);
127
+ const language = nonempty(input.language);
128
+ if (
129
+ new Set(languages).size !== languages.length ||
130
+ !languages.includes(language)
131
+ )
132
+ throw new TypeError('Invalid Worker router i18n language');
133
+ const urls = localizedUrls(input.localisedUrls);
134
+ return {
135
+ language,
136
+ languages,
137
+ ...(urls === undefined ? {} : { localisedUrls: urls }),
138
+ };
139
+ }
140
+
141
+ function nativeRewriteDescriptor(
142
+ router: AnyRouter,
143
+ ): RouterRenderJsonRecord | undefined {
144
+ if (!router.options.rewrite) return undefined;
145
+ const known = getOctaneWorkerRouterRewrite()?.capture(router.options.rewrite);
146
+ if (!known)
147
+ throw new TypeError(
148
+ 'Custom router rewrite requires an exact Worker profile',
149
+ );
150
+ const descriptor = rewriteDescriptor(known);
151
+ if (!descriptor)
152
+ throw new TypeError('Missing native router rewrite provenance');
153
+ return {
154
+ language: descriptor.language,
155
+ languages: [...descriptor.languages],
156
+ ...(descriptor.localisedUrls === undefined
157
+ ? {}
158
+ : { localisedUrls: descriptor.localisedUrls }),
159
+ };
160
+ }
161
+
162
+ function validateDefaultSnapshot(snapshot: RouterRenderSnapshot): void {
163
+ publicContext(snapshot.publicContext);
164
+ rewriteDescriptor(snapshot.i18n);
165
+ }
166
+
167
+ /** File-route heads are host presentation work, like the already settled loaders. */
168
+ function assertFileRouteRenderHost(
169
+ router: AnyRouter,
170
+ allowRewrite: boolean,
171
+ ): void {
172
+ if (
173
+ router.options.parseSearch !== defaultParseSearch ||
174
+ router.options.stringifySearch !== defaultStringifySearch ||
175
+ (!allowRewrite && router.options.rewrite) ||
176
+ router.options.routeMasks?.length
177
+ )
178
+ throw new TypeError(
179
+ 'Router render snapshot: authored router behavior requires an exact local profile',
180
+ );
181
+ const authoredRouting = [
182
+ 'caseSensitive',
183
+ 'validateSearch',
184
+ 'loaderDeps',
185
+ 'beforeLoad',
186
+ 'context',
187
+ 'onEnter',
188
+ 'onStay',
189
+ 'onLeave',
190
+ 'lazyFn',
191
+ 'scripts',
192
+ 'headers',
193
+ 'onError',
194
+ 'parseParams',
195
+ 'stringifyParams',
196
+ ];
197
+ const seen = new Set<AnyRoute>();
198
+ const visit = (route: AnyRoute, depth: number) => {
199
+ if (seen.has(route) || seen.size >= 4096 || depth > 64)
200
+ throw new TypeError(
201
+ 'Router render snapshot: route geometry must be bounded and acyclic',
202
+ );
203
+ seen.add(route);
204
+ if (
205
+ authoredRouting.some(
206
+ key => Reflect.get(route.options, key) !== undefined,
207
+ ) ||
208
+ route.options.params?.parse ||
209
+ route.options.params?.stringify ||
210
+ route.options.search?.middlewares?.length ||
211
+ route.lazyFn ||
212
+ ('path' in route.options && 'id' in route.options) ||
213
+ !hasUnchangedOctaneFileRouteHead(route)
214
+ )
215
+ throw new TypeError(
216
+ 'Router render snapshot: authored route behavior requires an exact local profile',
217
+ );
218
+ for (const child of route.children ?? []) visit(child, depth + 1);
219
+ };
220
+ visit(router.options.routeTree, 0);
221
+ }
222
+
223
+ function ensureDefaultProfile(): void {
224
+ if (profileRegistered) return;
225
+ registerRouterRenderProfile({
226
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
227
+ assertHost: (router, snapshot) => {
228
+ const origin = origins.get(router);
229
+ if (
230
+ !origin ||
231
+ origin.rewrite !== router.options.rewrite ||
232
+ JSON.stringify(publicContext(router.options.context)) !==
233
+ JSON.stringify(origin.publicContext)
234
+ )
235
+ throw new TypeError('Generated Worker router origin changed');
236
+ assertFileRouteRenderHost(router, snapshot.i18n !== undefined);
237
+ validateDefaultSnapshot(snapshot);
238
+ if (
239
+ JSON.stringify(nativeRewriteDescriptor(router)) !==
240
+ JSON.stringify(snapshot.i18n)
241
+ )
242
+ throw new TypeError(
243
+ 'Worker router rewrite differs from its native provenance',
244
+ );
245
+ },
246
+ createRoutingTree: snapshot => {
247
+ validateDefaultSnapshot(snapshot);
248
+ return createRouterRenderRouteTree(snapshot.routes);
249
+ },
250
+ routingOptions: snapshot => {
251
+ const defaults = {
252
+ parseSearch: defaultParseSearch,
253
+ stringifySearch: defaultStringifySearch,
254
+ };
255
+ const i18n = rewriteDescriptor(snapshot.i18n);
256
+ if (!i18n) return defaults;
257
+ const codec = getOctaneWorkerRouterRewrite();
258
+ if (!codec)
259
+ throw new TypeError('Missing owning Worker router i18n codec');
260
+ return { ...defaults, rewrite: codec.restore(i18n) };
261
+ },
262
+ });
263
+ profileRegistered = true;
264
+ }
265
+
266
+ /** Associate the genuine router produced by the generated file-route factory. */
267
+ export function associateOctaneFileRouteSnapshot(router: AnyRouter): void {
268
+ ensureDefaultProfile();
269
+ const context = publicContext(router.options.context);
270
+ origins.set(router, {
271
+ publicContext: context,
272
+ rewrite: router.options.rewrite,
273
+ });
274
+ associateRouterRenderProfile(router, {
275
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
276
+ publicContext: context,
277
+ });
278
+ }
279
+
280
+ export function captureOctaneRouterSnapshot(
281
+ router: AnyRouter,
282
+ ): RouterRenderSnapshot {
283
+ const origin = origins.get(router);
284
+ const declared = getRouterRenderProfile(router);
285
+ if (
286
+ origin &&
287
+ declared?.key === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.key &&
288
+ declared.version === OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE.version
289
+ ) {
290
+ const i18n = nativeRewriteDescriptor(router);
291
+ associateRouterRenderProfile(router, {
292
+ id: OCTANE_FILE_ROUTE_SNAPSHOT_PROFILE,
293
+ publicContext: origin.publicContext,
294
+ ...(i18n === undefined ? {} : { i18n }),
295
+ });
296
+ }
297
+ return captureRouterRenderSnapshot(router);
298
+ }
299
+
300
+ let registrations = 0;
301
+ let releaseContexts: (() => void) | undefined;
302
+
303
+ /** Startup registration uses the actual physical native router/match Contexts. */
304
+ export function registerOctaneRouterWorkerContexts(): () => void {
305
+ ensureDefaultProfile();
306
+ if (!releaseContexts) {
307
+ const releaseRouter = registerOctaneWorkerContext(routerContext, {
308
+ key: OCTANE_WORKER_CONTEXT_KEYS.router,
309
+ encode: value => {
310
+ if (!value) throw new TypeError('Missing native router provider');
311
+ return captureOctaneRouterSnapshot(value);
312
+ },
313
+ decode: (value, signal) => restoreRouterRenderSnapshot(value, { signal }),
314
+ dispose: value => {
315
+ if (value) disposeRouterRenderSnapshot(value);
316
+ },
317
+ isProvided: value => value !== undefined,
318
+ });
319
+ try {
320
+ const releaseMatch = registerOctaneWorkerContext(matchContext, {
321
+ key: OCTANE_WORKER_CONTEXT_KEYS.match,
322
+ encode: nonempty,
323
+ decode: nonempty,
324
+ validate: (value, readContext) => {
325
+ const selected = readContext(routerContext);
326
+ const router = selected.present ? selected.value : undefined;
327
+ // Native Match and useMatch share the active route store key, not
328
+ // the location-specific match identity (which includes parameters).
329
+ const current = router?.state.matches.find(
330
+ match => match.routeId === value,
331
+ );
332
+ if (
333
+ !router ||
334
+ !current ||
335
+ typeof value !== 'string' ||
336
+ router.stores.getMatchStore(value).get() !== current
337
+ )
338
+ throw new TypeError(
339
+ 'Worker match context requires its selected native router and a current match',
340
+ );
341
+ },
342
+ isProvided: value => value !== undefined,
343
+ });
344
+ releaseContexts = () => {
345
+ releaseMatch();
346
+ releaseRouter();
347
+ };
348
+ } catch (error) {
349
+ releaseRouter();
350
+ throw error;
351
+ }
352
+ }
353
+ registrations++;
354
+ let released = false;
355
+ return () => {
356
+ if (released) return;
357
+ released = true;
358
+ if (--registrations === 0) {
359
+ releaseContexts?.();
360
+ releaseContexts = undefined;
361
+ }
362
+ };
363
+ }
@@ -0,0 +1,185 @@
1
+ import type { RequestSession } from '@modern-js/renderer-core/session';
2
+ import { type AnyRouter, createMemoryHistory } from '@octanejs/tanstack-router';
3
+ import {
4
+ attachRouterServerSsrUtils,
5
+ getNormalizedURL,
6
+ getOrigin,
7
+ normalizeSsrResponse,
8
+ type RequestHandler,
9
+ } from '@octanejs/tanstack-router/ssr/server';
10
+ import { cleanupOctaneRouterSSR } from './router-server-cleanup';
11
+ import {
12
+ type OctaneRouterSerializationOptions,
13
+ prepareOctaneRouterSerialization,
14
+ } from './router-server-snapshot';
15
+
16
+ export interface OctaneRequestHandlerOptions<TRouter extends AnyRouter> {
17
+ createRouter(): TRouter;
18
+ request: Request;
19
+ session: RequestSession;
20
+ getRouterManifest?: () =>
21
+ | Parameters<typeof attachRouterServerSsrUtils>[0]['manifest']
22
+ | Promise<Parameters<typeof attachRouterServerSsrUtils>[0]['manifest']>;
23
+ serialization?: OctaneRouterSerializationOptions;
24
+ }
25
+
26
+ function withAbort<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {
27
+ signal.throwIfAborted();
28
+ return new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ const finish = (success: boolean, value: unknown) => {
31
+ if (settled) return;
32
+ settled = true;
33
+ signal.removeEventListener('abort', onAbort);
34
+ if (success) resolve(value as T);
35
+ else reject(value);
36
+ };
37
+ const onAbort = () => finish(false, signal.reason);
38
+ signal.addEventListener('abort', onAbort, { once: true });
39
+ pending.then(
40
+ value => finish(true, value),
41
+ error => finish(false, error),
42
+ );
43
+ if (signal.aborted) onAbort();
44
+ });
45
+ }
46
+
47
+ /** List fields that nested matches add to: CSP policies, links and timing. */
48
+ const CSP_HEADERS = new Set([
49
+ 'content-security-policy',
50
+ 'content-security-policy-report-only',
51
+ 'link',
52
+ 'server-timing',
53
+ ]);
54
+
55
+ /**
56
+ * Match headers are server HTTP policy, never part of the public router
57
+ * snapshot. Nested matches union `Vary` and append CSP fields; other
58
+ * singleton fields stay last-writer-wins.
59
+ */
60
+ function appendHeaders(destination: Headers, value: unknown): void {
61
+ if (value == null) return;
62
+ const source =
63
+ value instanceof Headers ? value : new Headers(value as HeadersInit);
64
+ for (const [key, item] of Headers.prototype.entries.call(source)) {
65
+ if (key === 'set-cookie') continue;
66
+ if (CSP_HEADERS.has(key)) destination.append(key, item);
67
+ else if (key === 'vary') {
68
+ const fields = new Map<string, string>();
69
+ for (const field of `${destination.get('vary') ?? ''},${item}`.split(
70
+ ',',
71
+ )) {
72
+ const name = field.trim();
73
+ if (name && !fields.has(name.toLowerCase()))
74
+ fields.set(name.toLowerCase(), name);
75
+ }
76
+ destination.set('vary', [...fields.values()].join(', '));
77
+ } else destination.set(key, item);
78
+ }
79
+ for (const cookie of Headers.prototype.getSetCookie.call(source)) {
80
+ destination.append('set-cookie', cookie);
81
+ }
82
+ }
83
+
84
+ function requestHeaders(router: AnyRouter): Headers {
85
+ const headers = new Headers({ 'content-type': 'text/html; charset=utf-8' });
86
+ for (const match of router.stores.matches.get()) {
87
+ const field = Object.getOwnPropertyDescriptor(match, 'headers');
88
+ if (field?.get || field?.set)
89
+ throw new TypeError(
90
+ 'Native Octane response headers must be owned values',
91
+ );
92
+ appendHeaders(headers, field?.value);
93
+ }
94
+ const result = router._serverResult;
95
+ if (result?.type === 'redirect')
96
+ appendHeaders(headers, result.redirect.headers);
97
+ return headers;
98
+ }
99
+
100
+ /** Resolve native routes and HTTP disposition before the native document serializer. */
101
+ export function createOctaneRequestHandler<TRouter extends AnyRouter>({
102
+ createRouter,
103
+ request,
104
+ session,
105
+ getRouterManifest,
106
+ serialization,
107
+ }: OctaneRequestHandlerOptions<TRouter>): RequestHandler<TRouter> {
108
+ return async callback => {
109
+ session.signal.throwIfAborted();
110
+ const router = createRouter();
111
+ let responseOwnsCleanup = false;
112
+ try {
113
+ const manifest = getRouterManifest
114
+ ? await withAbort(Promise.resolve(getRouterManifest()), session.signal)
115
+ : undefined;
116
+ attachRouterServerSsrUtils({ router, manifest });
117
+ const guard = prepareOctaneRouterSerialization(router, session, {
118
+ ...serialization,
119
+ forbiddenValues: [request, ...(serialization?.forbiddenValues ?? [])],
120
+ });
121
+
122
+ const { url } = getNormalizedURL(request.url, 'http://localhost');
123
+ const href = url.href.replace(url.origin, '');
124
+ router.update({
125
+ ...router.options,
126
+ history: createMemoryHistory({ initialEntries: [href] }),
127
+ origin: router.options.origin ?? getOrigin(request),
128
+ });
129
+ await withAbort(router.load({ _signal: session.signal }), session.signal);
130
+ guard.assertActive();
131
+
132
+ // A native redirect is a terminal HTTP Response. It must never enter
133
+ // the public serializer as a match error or document payload.
134
+ const loadResult = router._serverResult;
135
+ if (loadResult?.type === 'redirect') {
136
+ const resolved = loadResult.redirect;
137
+ const headers = requestHeaders(router);
138
+ return new Response(null, {
139
+ status: resolved.status,
140
+ statusText: resolved.statusText,
141
+ headers,
142
+ });
143
+ }
144
+
145
+ guard.assertMatches();
146
+ const serverSsr = router.serverSsr;
147
+ if (!serverSsr)
148
+ throw new Error('Native Octane router SSR state is required');
149
+ await withAbort(
150
+ serverSsr.dehydrate({ signal: session.signal }),
151
+ session.signal,
152
+ );
153
+ guard.assertActive();
154
+ const responseHeaders = requestHeaders(router);
155
+ // A callback that ignores cancellation must not hold the router SSR
156
+ // state; a result arriving after abort only has its body released.
157
+ const rendered = Promise.resolve(
158
+ callback({ request, router, responseHeaders }),
159
+ );
160
+ const result = normalizeSsrResponse(
161
+ await withAbort(rendered, session.signal).catch(error => {
162
+ if (session.signal.aborted)
163
+ void rendered.then(
164
+ late => {
165
+ try {
166
+ void normalizeSsrResponse(late)
167
+ .response.body?.cancel()
168
+ .catch(() => {});
169
+ } catch {}
170
+ },
171
+ () => {},
172
+ );
173
+ throw error;
174
+ }),
175
+ );
176
+ responseOwnsCleanup = result.serverSsrCleanup === 'stream';
177
+ return result.response;
178
+ } catch (error) {
179
+ session.fail(error);
180
+ throw error;
181
+ } finally {
182
+ if (!responseOwnsCleanup) cleanupOctaneRouterSSR(router);
183
+ }
184
+ };
185
+ }
@@ -0,0 +1,11 @@
1
+ import type { AnyRouter } from '@octanejs/tanstack-router';
2
+
3
+ const released = new WeakSet<NonNullable<AnyRouter['serverSsr']>>();
4
+
5
+ /** The request handler, serializer and renderer share one native SSR owner. */
6
+ export function cleanupOctaneRouterSSR(router: AnyRouter): void {
7
+ const serverSsr = router.serverSsr;
8
+ if (!serverSsr || released.has(serverSsr)) return;
9
+ released.add(serverSsr);
10
+ serverSsr.cleanup();
11
+ }