@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
package/src/client.ts ADDED
@@ -0,0 +1,568 @@
1
+ import type { NativeFederationBinding } from '@modern-js/renderer-core/federation';
2
+ import {
3
+ assertRendererIdentity,
4
+ type RendererIdentity,
5
+ } from '@modern-js/renderer-core/identity';
6
+ import {
7
+ type ComponentBody,
8
+ createRoot,
9
+ hydrateRoot,
10
+ type Root,
11
+ type RootContainer,
12
+ type RootOptions,
13
+ } from 'octane';
14
+ import {
15
+ bootstrapStreamedSignalHydration,
16
+ type StreamedSignalHydration,
17
+ } from 'octane/hydration/streamed-signals';
18
+ import {
19
+ assertNativeHydrationBuildId,
20
+ assertOctaneIdentity,
21
+ } from './bootstrap';
22
+ import {
23
+ createFederationScope,
24
+ FederationApplication,
25
+ type OctaneFederationScope,
26
+ } from './federation-context';
27
+ import {
28
+ type OctaneWorkerFederationFactory,
29
+ type OctaneWorkerFederationScope,
30
+ WorkerFederationApplication,
31
+ } from './worker-federation-context';
32
+
33
+ export type { OctaneDocumentBootstrap } from './bootstrap';
34
+ export { readOctaneDocumentBootstrap } from './bootstrap';
35
+
36
+ export interface OctaneApplicationModule {
37
+ readonly default: ComponentBody;
38
+ readonly props?: Record<string, unknown>;
39
+ /** Request-independent resources owned by this browser application module. */
40
+ readonly dispose?: () => void;
41
+ }
42
+
43
+ export interface OctaneApplicationHandle {
44
+ readonly root: Root;
45
+ readonly identity: Readonly<RendererIdentity>;
46
+ update(application: OctaneApplicationModule): void;
47
+ dispose(): void;
48
+ }
49
+
50
+ export interface OctaneApplicationOptions {
51
+ readonly container: Exclude<RootContainer, Document>;
52
+ readonly identity: RendererIdentity;
53
+ readonly nativeHydrationBuildId: string;
54
+ /** Cancels startup ownership and retires an active application. */
55
+ readonly signal?: AbortSignal;
56
+ /** Hydration installs the native signal bridge before invoking this importer. */
57
+ readonly load: () => Promise<OctaneApplicationModule>;
58
+ readonly options?: Omit<RootOptions, 'signalOwner' | 'externalSnapshots'>;
59
+ /** This browser compilation's native Module Federation runtime. */
60
+ readonly federation?: NativeFederationBinding;
61
+ readonly workerFederation?:
62
+ | OctaneWorkerFederationScope
63
+ | OctaneWorkerFederationFactory;
64
+ /** Trusted document bootstrap identity, also present for a CSR document. */
65
+ readonly documentId?: string;
66
+ }
67
+
68
+ export interface OctaneHydrationOptions extends OctaneApplicationOptions {
69
+ readonly documentIdentity: RendererIdentity;
70
+ readonly documentNativeHydrationBuildId: string;
71
+ readonly documentId: string;
72
+ }
73
+
74
+ const roots = new WeakMap<RootContainer, symbol>();
75
+ const documents = new WeakMap<Document, symbol>();
76
+ const moduleResources = new WeakMap<
77
+ () => void,
78
+ { users: number; retired: boolean }
79
+ >();
80
+
81
+ function claimModuleResources(
82
+ application: OctaneApplicationModule,
83
+ ): () => void {
84
+ const dispose = application.dispose;
85
+ if (!dispose) return () => {};
86
+ let resources = moduleResources.get(dispose);
87
+ if (resources?.retired) {
88
+ throw new Error(
89
+ 'A retired Octane application requires a fresh module resource instance.',
90
+ );
91
+ }
92
+ if (!resources) {
93
+ resources = { users: 0, retired: false };
94
+ moduleResources.set(dispose, resources);
95
+ }
96
+ resources.users++;
97
+ let released = false;
98
+ return () => {
99
+ if (released) return;
100
+ released = true;
101
+ resources.users--;
102
+ if (resources.users === 0 && !resources.retired) {
103
+ resources.retired = true;
104
+ dispose();
105
+ }
106
+ };
107
+ }
108
+
109
+ function retireAbandonedModule(application: OctaneApplicationModule): void {
110
+ if (application.dispose && moduleResources.get(application.dispose)?.users)
111
+ return;
112
+ if (application.dispose && moduleResources.get(application.dispose)?.retired)
113
+ return;
114
+ claimModuleResources(application)();
115
+ }
116
+
117
+ function observeRootErrors(options: OctaneApplicationOptions['options']) {
118
+ let failed = false;
119
+ let failure: unknown;
120
+ let handle: OctaneApplicationHandle | undefined;
121
+ return {
122
+ options: {
123
+ ...options,
124
+ onUncaughtError(error: unknown) {
125
+ if (failed) return;
126
+ failed = true;
127
+ failure = error;
128
+ if (handle) {
129
+ // Native reporting can occur inside a flush. Retire the application
130
+ // after that flush so unmount does not reenter the native renderer.
131
+ const failedHandle = handle;
132
+ queueMicrotask(() => {
133
+ try {
134
+ failedHandle.dispose();
135
+ } catch (cleanupError) {
136
+ failure = new AggregateError(
137
+ [error, cleanupError],
138
+ 'Octane rendering and cleanup failed.',
139
+ );
140
+ }
141
+ if (options?.onUncaughtError) options.onUncaughtError(failure);
142
+ else throw failure;
143
+ });
144
+ }
145
+ },
146
+ },
147
+ attach(next: OctaneApplicationHandle) {
148
+ handle = next;
149
+ },
150
+ assertInitialRender() {
151
+ if (failed) {
152
+ options?.onUncaughtError?.(failure);
153
+ throw failure;
154
+ }
155
+ },
156
+ };
157
+ }
158
+
159
+ function claimRoot(container: RootContainer): () => void {
160
+ if (container.nodeType === 9) {
161
+ throw new Error(
162
+ 'An Octane application requires a subtree root in its framework-owned document.',
163
+ );
164
+ }
165
+ if (roots.has(container)) {
166
+ throw new Error('An Octane application already owns this root container.');
167
+ }
168
+ const owner = Symbol('Octane root owner');
169
+ roots.set(container, owner);
170
+ return () => {
171
+ if (roots.get(container) === owner) roots.delete(container);
172
+ };
173
+ }
174
+
175
+ function cleanupResources(cleanups: (() => void)[]): void {
176
+ const errors: unknown[] = [];
177
+ for (const cleanup of cleanups) {
178
+ try {
179
+ cleanup();
180
+ } catch (error) {
181
+ errors.push(error);
182
+ }
183
+ }
184
+ if (errors.length) {
185
+ throw new AggregateError(errors, 'Octane application cleanup failed.');
186
+ }
187
+ }
188
+
189
+ function reportCleanupError(
190
+ input: OctaneApplicationOptions,
191
+ error: unknown,
192
+ ): void {
193
+ if (input.options?.onUncaughtError) input.options.onUncaughtError(error);
194
+ else
195
+ queueMicrotask(() => {
196
+ throw error;
197
+ });
198
+ }
199
+
200
+ /** An import cannot be aborted, but its root and bridge leases can be. */
201
+ function loadApplication(
202
+ input: OctaneApplicationOptions,
203
+ abandon: () => void,
204
+ ): Promise<{
205
+ application: OctaneApplicationModule;
206
+ releaseModule: () => void;
207
+ }> {
208
+ const acquire = (application: OctaneApplicationModule) => {
209
+ try {
210
+ validateModule(application);
211
+ } catch (error) {
212
+ if (application && typeof application.dispose === 'function')
213
+ retireAbandonedModule(application);
214
+ throw error;
215
+ }
216
+ return { application, releaseModule: claimModuleResources(application) };
217
+ };
218
+ const { signal } = input;
219
+ if (!signal) return input.load().then(acquire);
220
+ return new Promise((resolve, reject) => {
221
+ let settled = false;
222
+ let abandoned = false;
223
+ const onAbort = () => {
224
+ if (settled) return;
225
+ settled = true;
226
+ abandoned = true;
227
+ signal.removeEventListener('abort', onAbort);
228
+ try {
229
+ abandon();
230
+ reject(signal.reason);
231
+ } catch (cleanupError) {
232
+ reject(
233
+ new AggregateError(
234
+ [signal.reason, cleanupError],
235
+ 'Octane startup cancellation failed.',
236
+ ),
237
+ );
238
+ }
239
+ };
240
+ signal.addEventListener('abort', onAbort, { once: true });
241
+ if (signal.aborted) {
242
+ onAbort();
243
+ return;
244
+ }
245
+ let loading: Promise<OctaneApplicationModule>;
246
+ try {
247
+ loading = input.load();
248
+ } catch (error) {
249
+ settled = true;
250
+ signal.removeEventListener('abort', onAbort);
251
+ reject(error);
252
+ return;
253
+ }
254
+ loading.then(
255
+ application => {
256
+ if (abandoned) {
257
+ // Other consumers of the same import reserve resources in their
258
+ // resolution callbacks before an abandoned consumer can retire them.
259
+ queueMicrotask(() => {
260
+ try {
261
+ retireAbandonedModule(application);
262
+ } catch (error) {
263
+ reportCleanupError(input, error);
264
+ }
265
+ });
266
+ return;
267
+ }
268
+ settled = true;
269
+ signal.removeEventListener('abort', onAbort);
270
+ try {
271
+ resolve(acquire(application));
272
+ } catch (error) {
273
+ reject(error);
274
+ }
275
+ },
276
+ error => {
277
+ if (settled) return;
278
+ settled = true;
279
+ signal.removeEventListener('abort', onAbort);
280
+ reject(error);
281
+ },
282
+ );
283
+ });
284
+ }
285
+
286
+ function validateModule(application: OctaneApplicationModule): void {
287
+ if (!application || typeof application.default !== 'function') {
288
+ throw new TypeError(
289
+ 'An Octane application module must default-export a native component.',
290
+ );
291
+ }
292
+ if (
293
+ application.dispose !== undefined &&
294
+ typeof application.dispose !== 'function'
295
+ ) {
296
+ throw new TypeError('An Octane application disposer must be a function.');
297
+ }
298
+ }
299
+
300
+ function createHandle(input: {
301
+ root: Root;
302
+ identity: RendererIdentity;
303
+ releaseModule: () => void;
304
+ release: () => void;
305
+ bridge?: StreamedSignalHydration;
306
+ signal?: AbortSignal;
307
+ onCleanupError?: (error: unknown) => void;
308
+ federation?: OctaneFederationScope;
309
+ workerFederation?: OctaneWorkerFederationScope;
310
+ retireWorkerFederation?: () => void;
311
+ }): OctaneApplicationHandle {
312
+ let releaseModule = input.releaseModule;
313
+ let disposed = false;
314
+ const handle: OctaneApplicationHandle = {
315
+ root: input.root,
316
+ identity: Object.freeze({ ...input.identity }),
317
+ update(next) {
318
+ if (disposed)
319
+ throw new Error('Cannot update a disposed Octane application.');
320
+ validateModule(next);
321
+ const previousRelease = releaseModule;
322
+ const nextRelease = claimModuleResources(next);
323
+ releaseModule = nextRelease;
324
+ try {
325
+ input.workerFederation
326
+ ? input.root.render(WorkerFederationApplication, {
327
+ scope: input.workerFederation,
328
+ federation: input.federation,
329
+ application: next,
330
+ })
331
+ : input.federation
332
+ ? input.root.render(FederationApplication, {
333
+ scope: input.federation,
334
+ application: next,
335
+ })
336
+ : input.root.render(next.default, next.props);
337
+ previousRelease();
338
+ } catch (error) {
339
+ cleanupResources([previousRelease, () => handle.dispose()]);
340
+ throw error;
341
+ }
342
+ },
343
+ dispose() {
344
+ if (disposed) return;
345
+ disposed = true;
346
+ input.signal?.removeEventListener('abort', onAbort);
347
+ cleanupResources([
348
+ () => input.retireWorkerFederation?.(),
349
+ () => input.root.unmount(),
350
+ () => releaseModule(),
351
+ () => input.bridge?.dispose(),
352
+ input.release,
353
+ ]);
354
+ },
355
+ };
356
+ function onAbort() {
357
+ // An abort can originate in a native render/layout effect. Retire after
358
+ // the current flush, matching the native error retirement path.
359
+ queueMicrotask(() => {
360
+ try {
361
+ handle.dispose();
362
+ } catch (error) {
363
+ input.onCleanupError?.(error);
364
+ }
365
+ });
366
+ }
367
+ // Callers check the signal synchronously before constructing the handle.
368
+ input.signal?.addEventListener('abort', onAbort, { once: true });
369
+ return handle;
370
+ }
371
+
372
+ async function startApplication(
373
+ input: OctaneApplicationOptions,
374
+ release: () => void,
375
+ hydration?: { document: Document; documentId: string },
376
+ ): Promise<OctaneApplicationHandle> {
377
+ const errors = observeRootErrors(input.options);
378
+ const workerController = input.workerFederation
379
+ ? new AbortController()
380
+ : undefined;
381
+ let workerFederation: OctaneWorkerFederationScope | undefined;
382
+ const retireWorkerFederation = () =>
383
+ cleanupResources([
384
+ () =>
385
+ workerController?.abort(
386
+ new DOMException(
387
+ 'The Worker federation document was disposed',
388
+ 'AbortError',
389
+ ),
390
+ ),
391
+ () => workerFederation?.transport.dispose(),
392
+ ]);
393
+ let bridge: StreamedSignalHydration | undefined;
394
+ let root: Root | undefined;
395
+ let releaseModule: (() => void) | undefined;
396
+ let handle: OctaneApplicationHandle | undefined;
397
+ let cleaned = false;
398
+ const cleanup = () => {
399
+ if (handle) {
400
+ handle.dispose();
401
+ return;
402
+ }
403
+ if (cleaned) return;
404
+ cleaned = true;
405
+ cleanupResources([
406
+ retireWorkerFederation,
407
+ () => root?.unmount(),
408
+ () => releaseModule?.(),
409
+ () => bridge?.dispose(),
410
+ release,
411
+ ]);
412
+ };
413
+ try {
414
+ if (input.workerFederation && workerController) {
415
+ const signal = AbortSignal.any([
416
+ workerController.signal,
417
+ ...(input.signal ? [input.signal] : []),
418
+ ]);
419
+ const scope =
420
+ typeof input.workerFederation === 'function'
421
+ ? input.workerFederation(signal)
422
+ : input.workerFederation;
423
+ workerFederation = {
424
+ ...scope,
425
+ signal: scope.signal ? AbortSignal.any([signal, scope.signal]) : signal,
426
+ };
427
+ }
428
+ if (workerFederation && !hydration && !input.documentId)
429
+ throw new Error(
430
+ 'Worker federation requires the trusted document identity before application loading',
431
+ );
432
+ // Capture the host before imports and retain Worker SSR's provider ranges
433
+ // even when no regular Module Federation binding is configured.
434
+ const federation =
435
+ input.federation || workerFederation
436
+ ? createFederationScope(input.federation, false)
437
+ : undefined;
438
+ if (hydration) {
439
+ const { document, documentId } = hydration;
440
+ bridge = bootstrapStreamedSignalHydration({
441
+ buildId: input.nativeHydrationBuildId,
442
+ documentId,
443
+ ...(document.defaultView === null
444
+ ? {}
445
+ : {
446
+ target: document.defaultView as unknown as Record<
447
+ string,
448
+ unknown
449
+ >,
450
+ }),
451
+ });
452
+ }
453
+ const loaded = await loadApplication(input, cleanup);
454
+ const application = loaded.application;
455
+ releaseModule = loaded.releaseModule;
456
+ input.signal?.throwIfAborted();
457
+ if (bridge) {
458
+ root = hydrateRoot(
459
+ input.container,
460
+ workerFederation
461
+ ? WorkerFederationApplication
462
+ : federation
463
+ ? FederationApplication
464
+ : application.default,
465
+ workerFederation
466
+ ? { scope: workerFederation, federation, application }
467
+ : federation
468
+ ? { scope: federation, application }
469
+ : application.props,
470
+ {
471
+ ...errors.options,
472
+ signalOwner: bridge.signalOwner,
473
+ externalSnapshots: { documentId: hydration!.documentId },
474
+ },
475
+ );
476
+ } else {
477
+ root = createRoot(input.container, {
478
+ ...errors.options,
479
+ ...(input.documentId
480
+ ? { externalSnapshots: { documentId: input.documentId } }
481
+ : {}),
482
+ });
483
+ workerFederation
484
+ ? root.render(WorkerFederationApplication, {
485
+ scope: workerFederation,
486
+ federation,
487
+ application,
488
+ })
489
+ : federation
490
+ ? root.render(FederationApplication, {
491
+ scope: federation,
492
+ application,
493
+ })
494
+ : root.render(application.default, application.props);
495
+ }
496
+ errors.assertInitialRender();
497
+ input.signal?.throwIfAborted();
498
+ handle = createHandle({
499
+ root,
500
+ identity: input.identity,
501
+ releaseModule,
502
+ release,
503
+ bridge,
504
+ federation,
505
+ workerFederation,
506
+ retireWorkerFederation,
507
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
508
+ onCleanupError: error => reportCleanupError(input, error),
509
+ });
510
+ errors.attach(handle);
511
+ return handle;
512
+ } catch (error) {
513
+ cleanup();
514
+ throw error;
515
+ }
516
+ }
517
+
518
+ export async function mountOctaneApplication(
519
+ input: OctaneApplicationOptions,
520
+ ): Promise<OctaneApplicationHandle> {
521
+ assertOctaneIdentity(input.identity);
522
+ assertNativeHydrationBuildId(input.nativeHydrationBuildId);
523
+ input.signal?.throwIfAborted();
524
+ return startApplication(input, claimRoot(input.container));
525
+ }
526
+
527
+ export async function hydrateOctaneApplication(
528
+ input: OctaneHydrationOptions,
529
+ ): Promise<OctaneApplicationHandle> {
530
+ assertOctaneIdentity(input.identity);
531
+ assertNativeHydrationBuildId(input.nativeHydrationBuildId);
532
+ assertNativeHydrationBuildId(input.documentNativeHydrationBuildId);
533
+ if (input.nativeHydrationBuildId !== input.documentNativeHydrationBuildId) {
534
+ throw new Error(
535
+ 'Octane hydration bytes belong to a different native client compilation.',
536
+ );
537
+ }
538
+ assertRendererIdentity(input.documentIdentity, input.identity);
539
+ input.signal?.throwIfAborted();
540
+ if (
541
+ typeof input.documentId !== 'string' ||
542
+ input.documentId.trim().length === 0
543
+ ) {
544
+ throw new Error('Octane hydration requires a nonempty document identity.');
545
+ }
546
+ const document = input.container.ownerDocument;
547
+ if (!document)
548
+ throw new Error('An Octane hydration root requires a document.');
549
+ if (document !== globalThis.document) {
550
+ throw new Error(
551
+ 'Octane hydration requires a root in the active browser document.',
552
+ );
553
+ }
554
+ if (documents.has(document)) {
555
+ throw new Error('An Octane signal bridge already owns this document.');
556
+ }
557
+ const releaseRoot = claimRoot(input.container);
558
+ const documentOwner = Symbol(input.documentId);
559
+ documents.set(document, documentOwner);
560
+ const release = () => {
561
+ releaseRoot();
562
+ if (documents.get(document) === documentOwner) documents.delete(document);
563
+ };
564
+ return startApplication(input, release, {
565
+ document,
566
+ documentId: input.documentId,
567
+ });
568
+ }
@@ -0,0 +1,76 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
2
+ import type { NativeI18nView } from '@modern-js/renderer-core/entry-client';
3
+ import type { NativeRouterFactory } from '@modern-js/renderer-core/router';
4
+ import {
5
+ type AnyRoute,
6
+ type AnyRouter,
7
+ createMemoryHistory,
8
+ createRouter,
9
+ } from '@octanejs/tanstack-router';
10
+ import { type ComponentBody, createElement } from 'octane';
11
+ import { I18nProvider } from './i18n/I18nProvider';
12
+ import type { I18nInstanceLike } from './i18n/types';
13
+ import { associateOctaneFileRouteSnapshot } from './router-external-snapshot';
14
+ import {
15
+ createFileSystemRouteTree,
16
+ type FileSystemRouteModule,
17
+ } from './routes';
18
+
19
+ export type OctaneI18nView = NativeI18nView<unknown, LocalisedUrlsOption>;
20
+
21
+ /** Wrap a component entry in its request's or document's i18n provider. */
22
+ export function componentView(
23
+ component: unknown,
24
+ i18n?: OctaneI18nView,
25
+ ): ComponentBody {
26
+ const App = component as ComponentBody;
27
+ if (!i18n) return App;
28
+ return () =>
29
+ createElement(I18nProvider, {
30
+ instance: i18n.instance as I18nInstanceLike,
31
+ languages: i18n.languages,
32
+ localisedUrls: i18n.localisedUrls,
33
+ children: createElement(App),
34
+ });
35
+ }
36
+
37
+ /** The Octane route tree and router of a generated entry. */
38
+ export const octaneRouterFactory: NativeRouterFactory<AnyRoute, AnyRouter> = {
39
+ routeTree: (routes, modules, options) =>
40
+ createFileSystemRouteTree(
41
+ routes,
42
+ modules as Readonly<Record<string, FileSystemRouteModule>>,
43
+ options,
44
+ ),
45
+ router: ({ location, ...options }) => {
46
+ const router = createRouter({
47
+ ...options,
48
+ ...(location
49
+ ? {
50
+ origin: location.origin,
51
+ history: createMemoryHistory({ initialEntries: [location.href] }),
52
+ }
53
+ : {}),
54
+ });
55
+ associateOctaneFileRouteSnapshot(router);
56
+ return router;
57
+ },
58
+ };
59
+
60
+ interface LocalizedRouterProps {
61
+ readonly router: AnyRouter;
62
+ readonly i18n: OctaneI18nView;
63
+ }
64
+
65
+ /** Wrap a router root in the request's or document's i18n provider. */
66
+ export function localizedRouterRoot(
67
+ Root: ComponentBody<{ router: AnyRouter }>,
68
+ ): ComponentBody<LocalizedRouterProps> {
69
+ return ({ router, i18n }) =>
70
+ createElement(I18nProvider, {
71
+ instance: i18n.instance as I18nInstanceLike,
72
+ languages: i18n.languages,
73
+ localisedUrls: i18n.localisedUrls,
74
+ children: createElement(Root, { router }),
75
+ });
76
+ }