@bleedingdev/modern-js-renderer-octane 3.9.3-ultramodern.7 → 3.9.3-ultramodern.9
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/dist/cjs/client.js +56 -6
- package/dist/cjs/entry-application.js +7 -2
- package/dist/cjs/entry-client.js +3 -0
- package/dist/cjs/entry-server.js +13 -5
- package/dist/cjs/federation.js +108 -6
- package/dist/cjs/i18n/I18nProvider.js +6 -14
- package/dist/cjs/i18n/LocalizedLink.js +24 -33
- package/dist/cjs/i18n/index.js +0 -3
- package/dist/cjs/i18n-external-snapshot.js +645 -0
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/manifest.js +2 -2
- package/dist/cjs/plugin/compiler.js +2 -5
- package/dist/cjs/plugin/index.js +2 -2
- package/dist/cjs/plugin/profile.js +12 -10
- package/dist/cjs/router-client.js +1 -1
- package/dist/cjs/router-external-snapshot.js +263 -0
- package/dist/cjs/router-handler.js +11 -8
- package/dist/cjs/router-server.js +3 -11
- package/dist/cjs/router.js +1 -1
- package/dist/cjs/routes.js +11 -17
- package/dist/cjs/server.js +28 -7
- 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/client.mjs +56 -6
- package/dist/esm/entry-application.mjs +7 -2
- package/dist/esm/entry-client.mjs +3 -0
- package/dist/esm/entry-server.mjs +11 -6
- package/dist/esm/federation.mjs +110 -8
- package/dist/esm/i18n/I18nProvider.mjs +7 -15
- package/dist/esm/i18n/LocalizedLink.mjs +24 -30
- package/dist/esm/i18n/index.mjs +1 -1
- package/dist/esm/i18n-external-snapshot.mjs +576 -0
- package/dist/esm/index.mjs +4 -4
- package/dist/esm/manifest.mjs +2 -2
- package/dist/esm/router-client.mjs +1 -1
- package/dist/esm/router-external-snapshot.mjs +216 -0
- package/dist/esm/router-handler.mjs +11 -8
- package/dist/esm/router-server.mjs +0 -1
- package/dist/esm/router.mjs +2 -2
- package/dist/esm/routes.mjs +11 -17
- package/dist/esm/server.mjs +24 -3
- 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/manifest.mjs +2 -2
- package/dist/esm-node/plugin/compiler.mjs +2 -5
- package/dist/esm-node/plugin/index.mjs +2 -2
- package/dist/esm-node/plugin/profile.mjs +12 -10
- package/dist/types/client.d.ts +5 -1
- package/dist/types/entry-client.d.ts +2 -0
- package/dist/types/entry-server.d.ts +4 -1
- package/dist/types/federation.d.ts +2 -0
- package/dist/types/i18n/LocalizedLink.d.ts +5 -32
- package/dist/types/i18n/context.d.ts +1 -1
- package/dist/types/i18n/index.d.ts +1 -1
- package/dist/types/i18n-external-snapshot.d.ts +43 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/manifest.d.ts +2 -2
- package/dist/types/router-external-snapshot.d.ts +10 -0
- package/dist/types/router-server.d.ts +0 -2
- package/dist/types/server.d.ts +6 -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 +84 -22
- package/src/client.ts +99 -16
- package/src/entry-application.ts +7 -3
- package/src/entry-client.ts +10 -0
- package/src/entry-server.ts +20 -9
- package/src/federation.ts +159 -15
- package/src/i18n/I18nProvider.ts +11 -20
- package/src/i18n/LocalizedLink.ts +46 -72
- package/src/i18n/context.ts +1 -1
- package/src/i18n/index.ts +1 -5
- package/src/i18n-external-snapshot.ts +803 -0
- package/src/index.ts +4 -4
- package/src/manifest.ts +2 -2
- package/src/plugin/compiler-manifest.cjs +27 -1
- package/src/plugin/compiler.ts +6 -14
- package/src/plugin/index.ts +4 -2
- package/src/plugin/profile.ts +14 -10
- package/src/router-client.ts +1 -1
- package/src/router-external-snapshot.ts +306 -0
- package/src/router-handler.ts +11 -8
- package/src/router-server.ts +0 -2
- package/src/router.ts +2 -2
- package/src/routes.ts +28 -26
- package/src/server.ts +45 -5
- package/src/worker-federation-context.ts +51 -0
- package/src/worker-federation-contexts.ts +91 -0
- package/src/worker-federation.ts +120 -0
- package/dist/cjs/router-injection.js +0 -139
- package/dist/esm/router-injection.mjs +0 -101
- package/dist/types/router-injection.d.ts +0 -8
- package/src/router-injection.ts +0 -145
package/src/client.ts
CHANGED
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
FederationApplication,
|
|
25
25
|
type OctaneFederationScope,
|
|
26
26
|
} from './federation-context';
|
|
27
|
+
import {
|
|
28
|
+
type OctaneWorkerFederationFactory,
|
|
29
|
+
type OctaneWorkerFederationScope,
|
|
30
|
+
WorkerFederationApplication,
|
|
31
|
+
} from './worker-federation-context';
|
|
27
32
|
|
|
28
33
|
export type { OctaneDocumentBootstrap } from './bootstrap';
|
|
29
34
|
export { readOctaneDocumentBootstrap } from './bootstrap';
|
|
@@ -50,9 +55,14 @@ export interface OctaneApplicationOptions {
|
|
|
50
55
|
readonly signal?: AbortSignal;
|
|
51
56
|
/** Hydration installs the native signal bridge before invoking this importer. */
|
|
52
57
|
readonly load: () => Promise<OctaneApplicationModule>;
|
|
53
|
-
readonly options?: Omit<RootOptions, 'signalOwner'>;
|
|
58
|
+
readonly options?: Omit<RootOptions, 'signalOwner' | 'externalSnapshots'>;
|
|
54
59
|
/** This browser compilation's native Module Federation runtime. */
|
|
55
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;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export interface OctaneHydrationOptions extends OctaneApplicationOptions {
|
|
@@ -296,6 +306,8 @@ function createHandle(input: {
|
|
|
296
306
|
signal?: AbortSignal;
|
|
297
307
|
onCleanupError?: (error: unknown) => void;
|
|
298
308
|
federation?: OctaneFederationScope;
|
|
309
|
+
workerFederation?: OctaneWorkerFederationScope;
|
|
310
|
+
retireWorkerFederation?: () => void;
|
|
299
311
|
}): OctaneApplicationHandle {
|
|
300
312
|
let releaseModule = input.releaseModule;
|
|
301
313
|
let disposed = false;
|
|
@@ -310,12 +322,18 @@ function createHandle(input: {
|
|
|
310
322
|
const nextRelease = claimModuleResources(next);
|
|
311
323
|
releaseModule = nextRelease;
|
|
312
324
|
try {
|
|
313
|
-
input.
|
|
314
|
-
? input.root.render(
|
|
315
|
-
scope: input.
|
|
325
|
+
input.workerFederation
|
|
326
|
+
? input.root.render(WorkerFederationApplication, {
|
|
327
|
+
scope: input.workerFederation,
|
|
328
|
+
federation: input.federation,
|
|
316
329
|
application: next,
|
|
317
330
|
})
|
|
318
|
-
: input.
|
|
331
|
+
: input.federation
|
|
332
|
+
? input.root.render(FederationApplication, {
|
|
333
|
+
scope: input.federation,
|
|
334
|
+
application: next,
|
|
335
|
+
})
|
|
336
|
+
: input.root.render(next.default, next.props);
|
|
319
337
|
previousRelease();
|
|
320
338
|
} catch (error) {
|
|
321
339
|
cleanupResources([previousRelease, () => handle.dispose()]);
|
|
@@ -327,6 +345,7 @@ function createHandle(input: {
|
|
|
327
345
|
disposed = true;
|
|
328
346
|
input.signal?.removeEventListener('abort', onAbort);
|
|
329
347
|
cleanupResources([
|
|
348
|
+
() => input.retireWorkerFederation?.(),
|
|
330
349
|
() => input.root.unmount(),
|
|
331
350
|
() => releaseModule(),
|
|
332
351
|
() => input.bridge?.dispose(),
|
|
@@ -356,6 +375,21 @@ async function startApplication(
|
|
|
356
375
|
hydration?: { document: Document; documentId: string },
|
|
357
376
|
): Promise<OctaneApplicationHandle> {
|
|
358
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
|
+
]);
|
|
359
393
|
let bridge: StreamedSignalHydration | undefined;
|
|
360
394
|
let root: Root | undefined;
|
|
361
395
|
let releaseModule: (() => void) | undefined;
|
|
@@ -369,6 +403,7 @@ async function startApplication(
|
|
|
369
403
|
if (cleaned) return;
|
|
370
404
|
cleaned = true;
|
|
371
405
|
cleanupResources([
|
|
406
|
+
retireWorkerFederation,
|
|
372
407
|
() => root?.unmount(),
|
|
373
408
|
() => releaseModule?.(),
|
|
374
409
|
() => bridge?.dispose(),
|
|
@@ -376,10 +411,30 @@ async function startApplication(
|
|
|
376
411
|
]);
|
|
377
412
|
};
|
|
378
413
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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;
|
|
383
438
|
if (hydration) {
|
|
384
439
|
const { document, documentId } = hydration;
|
|
385
440
|
bridge = bootstrapStreamedSignalHydration({
|
|
@@ -402,15 +457,41 @@ async function startApplication(
|
|
|
402
457
|
if (bridge) {
|
|
403
458
|
root = hydrateRoot(
|
|
404
459
|
input.container,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
},
|
|
408
475
|
);
|
|
409
476
|
} else {
|
|
410
|
-
root = createRoot(input.container,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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);
|
|
414
495
|
}
|
|
415
496
|
errors.assertInitialRender();
|
|
416
497
|
input.signal?.throwIfAborted();
|
|
@@ -421,6 +502,8 @@ async function startApplication(
|
|
|
421
502
|
release,
|
|
422
503
|
bridge,
|
|
423
504
|
federation,
|
|
505
|
+
workerFederation,
|
|
506
|
+
retireWorkerFederation,
|
|
424
507
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
425
508
|
onCleanupError: error => reportCleanupError(input, error),
|
|
426
509
|
});
|
package/src/entry-application.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { type ComponentBody, createElement } from 'octane';
|
|
11
11
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
12
12
|
import type { I18nInstanceLike } from './i18n/types';
|
|
13
|
+
import { associateOctaneFileRouteSnapshot } from './router-external-snapshot';
|
|
13
14
|
import {
|
|
14
15
|
createFileSystemRouteTree,
|
|
15
16
|
type FileSystemRouteModule,
|
|
@@ -41,8 +42,8 @@ export const octaneRouterFactory: NativeRouterFactory<AnyRoute, AnyRouter> = {
|
|
|
41
42
|
modules as Readonly<Record<string, FileSystemRouteModule>>,
|
|
42
43
|
options,
|
|
43
44
|
),
|
|
44
|
-
router: ({ location, ...options }) =>
|
|
45
|
-
createRouter({
|
|
45
|
+
router: ({ location, ...options }) => {
|
|
46
|
+
const router = createRouter({
|
|
46
47
|
...options,
|
|
47
48
|
...(location
|
|
48
49
|
? {
|
|
@@ -50,7 +51,10 @@ export const octaneRouterFactory: NativeRouterFactory<AnyRoute, AnyRouter> = {
|
|
|
50
51
|
history: createMemoryHistory({ initialEntries: [location.href] }),
|
|
51
52
|
}
|
|
52
53
|
: {}),
|
|
53
|
-
})
|
|
54
|
+
});
|
|
55
|
+
associateOctaneFileRouteSnapshot(router);
|
|
56
|
+
return router;
|
|
57
|
+
},
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
interface LocalizedRouterProps {
|
package/src/entry-client.ts
CHANGED
|
@@ -21,11 +21,18 @@ import {
|
|
|
21
21
|
OctaneRouterRoot,
|
|
22
22
|
prepareOctaneRouterHydration,
|
|
23
23
|
} from './router-client';
|
|
24
|
+
import type {
|
|
25
|
+
OctaneWorkerFederationFactory,
|
|
26
|
+
OctaneWorkerFederationScope,
|
|
27
|
+
} from './worker-federation-context';
|
|
24
28
|
|
|
25
29
|
export interface OctaneClientEntryOptions
|
|
26
30
|
extends NativeClientEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
|
|
27
31
|
/** The native client compilation hash, `__webpack_hash__`. */
|
|
28
32
|
readonly nativeHydrationBuildId: string;
|
|
33
|
+
readonly workerFederation?:
|
|
34
|
+
| OctaneWorkerFederationScope
|
|
35
|
+
| OctaneWorkerFederationFactory;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const LocalizedRouterRoot = localizedRouterRoot(OctaneRouterRoot);
|
|
@@ -38,6 +45,7 @@ export function startNativeClient(options: OctaneClientEntryOptions): void {
|
|
|
38
45
|
readOctaneDocumentBootstrap(document, identity, nativeHydrationBuildId),
|
|
39
46
|
createRouter: (application, routerOptions) =>
|
|
40
47
|
createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
48
|
+
disposeRouter: router => router.history.destroy(),
|
|
41
49
|
async prepareRouter(router, hydrating, signal) {
|
|
42
50
|
// Hydration adopts the server's matches; a fresh mount loads its own.
|
|
43
51
|
if (hydrating) await prepareOctaneRouterHydration(router, { signal });
|
|
@@ -49,6 +57,8 @@ export function startNativeClient(options: OctaneClientEntryOptions): void {
|
|
|
49
57
|
identity,
|
|
50
58
|
nativeHydrationBuildId,
|
|
51
59
|
federation: options.federation,
|
|
60
|
+
workerFederation: options.workerFederation,
|
|
61
|
+
documentId: bootstrap?.documentId,
|
|
52
62
|
signal,
|
|
53
63
|
// Hydration installs the native signal bridge before this importer runs.
|
|
54
64
|
load: async (): Promise<OctaneApplicationModule> => {
|
package/src/entry-server.ts
CHANGED
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
import { validateOctaneModuleManifest } from './manifest';
|
|
24
24
|
import {
|
|
25
25
|
createOctaneRequestHandler,
|
|
26
|
-
createOctaneRouterInjection,
|
|
27
26
|
createSsrStreamResponse,
|
|
27
|
+
getSsrStatus,
|
|
28
28
|
OctaneRouterServer,
|
|
29
29
|
} from './router-server';
|
|
30
30
|
import { renderOctaneApplication, renderOctaneCSRDocument } from './server';
|
|
@@ -52,10 +52,12 @@ export function withOctaneDocumentFields(
|
|
|
52
52
|
: policy;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
export { createNativeWorkerFederationRuntime } from './worker-federation';
|
|
56
|
+
|
|
57
|
+
export interface OctaneServerEntryOptions
|
|
58
|
+
extends NativeServerEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
|
|
59
|
+
readonly workerFederationLocale?: string;
|
|
60
|
+
}
|
|
59
61
|
|
|
60
62
|
const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
|
|
61
63
|
|
|
@@ -77,16 +79,23 @@ export function createNativeServerEntry(
|
|
|
77
79
|
createNativeRouter(application, routerOptions, octaneRouterFactory),
|
|
78
80
|
renderCSR: ({ session }, document) =>
|
|
79
81
|
renderOctaneCSRDocument({ session, document }),
|
|
80
|
-
renderComponent: (
|
|
82
|
+
renderComponent: (
|
|
83
|
+
{ session, workerFederation },
|
|
84
|
+
component,
|
|
85
|
+
document,
|
|
86
|
+
i18n,
|
|
87
|
+
) =>
|
|
81
88
|
renderOctaneApplication({
|
|
82
89
|
session,
|
|
83
90
|
App: componentView(component, i18n) as ServerRenderNode,
|
|
84
91
|
document,
|
|
85
92
|
federation: options.federation,
|
|
93
|
+
workerFederation,
|
|
94
|
+
workerFederationLocale: options.workerFederationLocale,
|
|
86
95
|
}),
|
|
87
96
|
async renderRoutes({
|
|
88
97
|
request,
|
|
89
|
-
context: { session },
|
|
98
|
+
context: { session, workerFederation },
|
|
90
99
|
router,
|
|
91
100
|
outcomes,
|
|
92
101
|
document,
|
|
@@ -100,7 +109,7 @@ export function createNativeServerEntry(
|
|
|
100
109
|
serialization: { forbiddenValues },
|
|
101
110
|
})(async ({ router, responseHeaders }) => {
|
|
102
111
|
const metadata = mergeDataResponseMetadata(outcomes, {
|
|
103
|
-
status: router
|
|
112
|
+
status: getSsrStatus(router),
|
|
104
113
|
});
|
|
105
114
|
// Route headers() take precedence over loader singleton fields; list
|
|
106
115
|
// fields keep both.
|
|
@@ -127,7 +136,9 @@ export function createNativeServerEntry(
|
|
|
127
136
|
}),
|
|
128
137
|
document,
|
|
129
138
|
federation: options.federation,
|
|
130
|
-
|
|
139
|
+
workerFederation,
|
|
140
|
+
workerFederationLocale: options.workerFederationLocale,
|
|
141
|
+
router,
|
|
131
142
|
});
|
|
132
143
|
return createSsrStreamResponse(router, rendered);
|
|
133
144
|
});
|
package/src/federation.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadFederatedModule,
|
|
3
|
-
remoteBrowserAssets,
|
|
4
|
-
} from '@modern-js/renderer-core/federation';
|
|
1
|
+
import { loadFederatedModule } from '@modern-js/renderer-core/federation';
|
|
5
2
|
import {
|
|
6
3
|
type ComponentBody,
|
|
7
4
|
createElement,
|
|
5
|
+
decodeExternalSnapshot,
|
|
6
|
+
externalSnapshotBoundary,
|
|
7
|
+
hookSlots,
|
|
8
8
|
lazy,
|
|
9
9
|
type OctaneNode,
|
|
10
10
|
preinit,
|
|
11
|
+
publisherBoundary,
|
|
11
12
|
Suspense,
|
|
13
|
+
serializeExternalSnapshotRequest,
|
|
12
14
|
useContext,
|
|
15
|
+
useEffect,
|
|
13
16
|
} from 'octane';
|
|
14
17
|
import {
|
|
15
18
|
FederationContext,
|
|
16
19
|
type OctaneFederationScope,
|
|
17
20
|
} from './federation-context';
|
|
18
21
|
|
|
22
|
+
import { I18nContext } from './i18n/context';
|
|
23
|
+
import {
|
|
24
|
+
type OctaneWorkerFederationScope,
|
|
25
|
+
WorkerFederationContext,
|
|
26
|
+
} from './worker-federation-context';
|
|
27
|
+
import { useOctaneWorkerContextKeys } from './worker-federation-contexts';
|
|
28
|
+
|
|
19
29
|
/** The native component shape exposed by a same-renderer Octane remote. */
|
|
20
30
|
export interface FederatedModule<P extends object> {
|
|
21
31
|
readonly default: ComponentBody<P>;
|
|
@@ -26,6 +36,8 @@ export interface FederatedComponentOptions {
|
|
|
26
36
|
readonly fallback?: () => OctaneNode;
|
|
27
37
|
/** A positive load limit; the server defaults to 3000ms. */
|
|
28
38
|
readonly timeout?: number;
|
|
39
|
+
/** Explicit registered server projections required by an application remote. */
|
|
40
|
+
readonly contextKeys?: readonly string[];
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
function withTimeout<T>(
|
|
@@ -56,6 +68,9 @@ function withTimeout<T>(
|
|
|
56
68
|
});
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
// This library wrapper is published as plain JavaScript, outside app compilation.
|
|
72
|
+
const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
|
|
73
|
+
|
|
59
74
|
/**
|
|
60
75
|
* Render a native Octane remote by its `remote/Expose` id or a browser loader.
|
|
61
76
|
* Native Suspense owns server fallbacks, streamed styles and hydration adoption;
|
|
@@ -90,11 +105,33 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
90
105
|
};
|
|
91
106
|
// Native lazy caches its module. Scope the cache to one application/response,
|
|
92
107
|
// so two hosts using the same component definition cannot share a remote.
|
|
93
|
-
const components = new WeakMap<
|
|
108
|
+
const components = new WeakMap<object, ComponentBody<P>>();
|
|
109
|
+
const workerOwners = new WeakMap<
|
|
110
|
+
OctaneFederationScope,
|
|
111
|
+
WeakMap<OctaneWorkerFederationScope, Map<string, object>>
|
|
112
|
+
>();
|
|
94
113
|
|
|
95
114
|
return props => {
|
|
96
115
|
const context = useContext(FederationContext) ?? standalone;
|
|
97
|
-
|
|
116
|
+
const worker = useContext(WorkerFederationContext);
|
|
117
|
+
const language = worker
|
|
118
|
+
? (useContext(I18nContext)?.language ?? worker.locale)
|
|
119
|
+
: undefined;
|
|
120
|
+
const contextKeys = worker
|
|
121
|
+
? useOctaneWorkerContextKeys(options.contextKeys)
|
|
122
|
+
: undefined;
|
|
123
|
+
let owner: object = context;
|
|
124
|
+
if (worker) {
|
|
125
|
+
let scopes = workerOwners.get(context);
|
|
126
|
+
if (!scopes) workerOwners.set(context, (scopes = new WeakMap()));
|
|
127
|
+
let variants = scopes.get(worker);
|
|
128
|
+
if (!variants) scopes.set(worker, (variants = new Map()));
|
|
129
|
+
const key = JSON.stringify([language, contextKeys]);
|
|
130
|
+
let variant = variants.get(key);
|
|
131
|
+
if (!variant) variants.set(key, (variant = {}));
|
|
132
|
+
owner = variant;
|
|
133
|
+
}
|
|
134
|
+
let Remote = components.get(owner);
|
|
98
135
|
if (!Remote) {
|
|
99
136
|
Remote = lazy(async () => {
|
|
100
137
|
try {
|
|
@@ -103,11 +140,101 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
103
140
|
'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
|
|
104
141
|
);
|
|
105
142
|
}
|
|
106
|
-
|
|
143
|
+
if (worker) {
|
|
144
|
+
if (!id || !language) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'Worker federation requires a remote id and the owning entry locale',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const separator = id.indexOf('/');
|
|
150
|
+
const remoteName = id.slice(0, separator);
|
|
151
|
+
const expose = `./${id.slice(separator + 1)}`;
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const signal = worker.signal
|
|
154
|
+
? AbortSignal.any([worker.signal, controller.signal])
|
|
155
|
+
: controller.signal;
|
|
156
|
+
const timeout =
|
|
157
|
+
options.timeout ?? (context.server ? 3000 : undefined);
|
|
158
|
+
let admitted;
|
|
159
|
+
try {
|
|
160
|
+
admitted = await withTimeout(
|
|
161
|
+
worker.transport.admit({
|
|
162
|
+
remote: remoteName,
|
|
163
|
+
expose,
|
|
164
|
+
locale: language,
|
|
165
|
+
signal,
|
|
166
|
+
}),
|
|
167
|
+
timeout,
|
|
168
|
+
id,
|
|
169
|
+
);
|
|
170
|
+
} finally {
|
|
171
|
+
// Release this admission waiter, including after its own deadline.
|
|
172
|
+
controller.abort();
|
|
173
|
+
}
|
|
174
|
+
const BrowserRemote = lazy(async () => {
|
|
175
|
+
const { module, assets } = await loadFederatedModule<
|
|
176
|
+
FederatedModule<P>
|
|
177
|
+
>(context.instance, id);
|
|
178
|
+
if (typeof module?.default !== 'function') {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
'A federated Octane module must default-export a native component',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const component = module.default;
|
|
184
|
+
const Loaded: ComponentBody<P> = (props, scope, extra) => {
|
|
185
|
+
for (const href of assets?.css ?? []) {
|
|
186
|
+
preinit(href, {
|
|
187
|
+
as: 'style',
|
|
188
|
+
precedence: 'federated',
|
|
189
|
+
...(context.nonce === undefined
|
|
190
|
+
? {}
|
|
191
|
+
: { nonce: context.nonce }),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return component(props, scope, extra);
|
|
195
|
+
};
|
|
196
|
+
return { default: Loaded };
|
|
197
|
+
});
|
|
198
|
+
const boundary = externalSnapshotBoundary({
|
|
199
|
+
authority: admitted.coordinate.native,
|
|
200
|
+
component: BrowserRemote,
|
|
201
|
+
contextKeys,
|
|
202
|
+
timeoutMs: timeout,
|
|
203
|
+
async snapshot(request, signal) {
|
|
204
|
+
const response = await worker.transport.request({
|
|
205
|
+
remote: remoteName,
|
|
206
|
+
expose,
|
|
207
|
+
locale: language,
|
|
208
|
+
payload: serializeExternalSnapshotRequest(request),
|
|
209
|
+
signal,
|
|
210
|
+
});
|
|
211
|
+
return decodeExternalSnapshot(response.payload);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const Loaded: ComponentBody<P> = props => {
|
|
215
|
+
// A Worker server does not execute BrowserRemote. Register the
|
|
216
|
+
// exact admitted expose's extracted CSS in this active native
|
|
217
|
+
// render; nested snapshots carry these hints through native head.
|
|
218
|
+
for (const href of admitted.styles) {
|
|
219
|
+
preinit(href, {
|
|
220
|
+
as: 'style',
|
|
221
|
+
precedence: 'federated',
|
|
222
|
+
...(context.nonce === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { nonce: context.nonce }),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return createElement(boundary, props);
|
|
228
|
+
};
|
|
229
|
+
return { default: Loaded };
|
|
230
|
+
}
|
|
231
|
+
const { module, assets } = await withTimeout(
|
|
107
232
|
Promise.resolve().then(() =>
|
|
108
233
|
id
|
|
109
234
|
? loadFederatedModule<FederatedModule<P>>(context.instance, id)
|
|
110
|
-
: (remote as () => Promise<FederatedModule<P>>)()
|
|
235
|
+
: (remote as () => Promise<FederatedModule<P>>)().then(
|
|
236
|
+
module => ({ module, assets: undefined }),
|
|
237
|
+
),
|
|
111
238
|
),
|
|
112
239
|
options.timeout ?? (context.server ? 3000 : undefined),
|
|
113
240
|
id ?? 'loader',
|
|
@@ -117,17 +244,34 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
117
244
|
'A federated Octane module must default-export a native component',
|
|
118
245
|
);
|
|
119
246
|
}
|
|
120
|
-
const assets =
|
|
121
|
-
id && context.instance
|
|
122
|
-
? remoteBrowserAssets(context.instance, id)
|
|
123
|
-
: undefined;
|
|
124
247
|
if (context.server && !assets) {
|
|
125
248
|
throw new Error(
|
|
126
249
|
`Cannot server-render ${id}: the host has no browser assets for this remote`,
|
|
127
250
|
);
|
|
128
251
|
}
|
|
129
|
-
|
|
252
|
+
// Native publisher ownership admits this exact compiled component's
|
|
253
|
+
// signal generations. Keep the boundary in the context-scoped lazy
|
|
254
|
+
// cache so props and compatible hot updates retain its live owner.
|
|
255
|
+
const component = publisherBoundary(module.default, {
|
|
256
|
+
publisherKey: id ?? 'ultramodern:browser-loader',
|
|
257
|
+
});
|
|
130
258
|
const Loaded: ComponentBody<P> = props => {
|
|
259
|
+
// A development runtime installs this hook on the actual owning
|
|
260
|
+
// host. Registration follows the native mounted lifetime; routing
|
|
261
|
+
// alone grants no replacement authority to a remote name.
|
|
262
|
+
useEffect(
|
|
263
|
+
() => {
|
|
264
|
+
if (context.server || !id || !context.instance) return;
|
|
265
|
+
const register = Reflect.get(
|
|
266
|
+
context.instance,
|
|
267
|
+
Symbol.for('ultramodern.federation.browser-dev-boundary.v1'),
|
|
268
|
+
);
|
|
269
|
+
if (typeof register !== 'function') return;
|
|
270
|
+
return register(id, module.default, component);
|
|
271
|
+
},
|
|
272
|
+
[],
|
|
273
|
+
publisherRegistrationEffectSlot,
|
|
274
|
+
);
|
|
131
275
|
// Resource APIs require an active native render. Late styles travel
|
|
132
276
|
// in Octane's resource carrier ahead of the remote's HTML segment.
|
|
133
277
|
for (const href of assets?.css ?? []) {
|
|
@@ -144,11 +288,11 @@ export function federatedComponent<P extends object = Record<string, never>>(
|
|
|
144
288
|
return { default: Loaded };
|
|
145
289
|
} catch (error) {
|
|
146
290
|
// A later mount or ErrorBoundary reset may retry a failed load.
|
|
147
|
-
if (!context.server) components.delete(
|
|
291
|
+
if (!context.server) components.delete(owner);
|
|
148
292
|
throw error;
|
|
149
293
|
}
|
|
150
294
|
});
|
|
151
|
-
components.set(
|
|
295
|
+
components.set(owner, Remote);
|
|
152
296
|
}
|
|
153
297
|
return createElement(Suspense, {
|
|
154
298
|
fallback: options.fallback,
|
package/src/i18n/I18nProvider.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createElement, hookSlots,
|
|
1
|
+
import { createElement, hookSlots, useSyncExternalStore } from 'octane';
|
|
2
2
|
import { I18nContext } from './context';
|
|
3
|
-
import type { I18nContextValue
|
|
3
|
+
import type { I18nContextValue } from './types';
|
|
4
4
|
|
|
5
5
|
export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
|
|
6
6
|
children?: unknown;
|
|
@@ -22,27 +22,18 @@ export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
|
|
|
22
22
|
*/
|
|
23
23
|
// Plain TypeScript is not rewritten by the Octane compiler, so each hook call
|
|
24
24
|
// site carries its own stable slot, like `OctaneRouterRoot`.
|
|
25
|
-
const
|
|
26
|
-
const languageSubscriptionSlot = Symbol(hookSlots(1));
|
|
25
|
+
const languageStoreSlot = Symbol(hookSlots(1));
|
|
27
26
|
|
|
28
27
|
export function I18nProvider(props: I18nProviderProps) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
useEffect(
|
|
35
|
-
() => {
|
|
36
|
-
const instance: I18nInstanceLike = props.instance;
|
|
37
|
-
setLanguage(instance.language);
|
|
38
|
-
const handleLanguageChanged = (lng: string) => setLanguage(lng);
|
|
39
|
-
instance.on?.('languageChanged', handleLanguageChanged);
|
|
40
|
-
return () => {
|
|
41
|
-
instance.off?.('languageChanged', handleLanguageChanged);
|
|
42
|
-
};
|
|
28
|
+
const instance = props.instance;
|
|
29
|
+
const language = useSyncExternalStore(
|
|
30
|
+
notify => {
|
|
31
|
+
instance.on?.('languageChanged', notify);
|
|
32
|
+
return () => instance.off?.('languageChanged', notify);
|
|
43
33
|
},
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
() => instance.language,
|
|
35
|
+
() => instance.language,
|
|
36
|
+
languageStoreSlot,
|
|
46
37
|
);
|
|
47
38
|
|
|
48
39
|
const value: I18nContextValue = {
|