@bleedingdev/modern-js-renderer-octane 0.0.0-bootstrap → 3.9.3-ultramodern.1

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