@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,90 @@
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
+ import type {
25
+ OctaneWorkerFederationFactory,
26
+ OctaneWorkerFederationScope,
27
+ } from './worker-federation-context';
28
+
29
+ export interface OctaneClientEntryOptions
30
+ extends NativeClientEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
31
+ /** The native client compilation hash, `__webpack_hash__`. */
32
+ readonly nativeHydrationBuildId: string;
33
+ readonly workerFederation?:
34
+ | OctaneWorkerFederationScope
35
+ | OctaneWorkerFederationFactory;
36
+ }
37
+
38
+ const LocalizedRouterRoot = localizedRouterRoot(OctaneRouterRoot);
39
+
40
+ /** Start a generated Octane browser entry: mount, or hydrate the server document. */
41
+ export function startNativeClient(options: OctaneClientEntryOptions): void {
42
+ const { nativeHydrationBuildId } = options;
43
+ startNativeClientEntry(options, {
44
+ readBootstrap: (document, identity) =>
45
+ readOctaneDocumentBootstrap(document, identity, nativeHydrationBuildId),
46
+ createRouter: (application, routerOptions) =>
47
+ createNativeRouter(application, routerOptions, octaneRouterFactory),
48
+ disposeRouter: router => router.history.destroy(),
49
+ async prepareRouter(router, hydrating, signal) {
50
+ // Hydration adopts the server's matches; a fresh mount loads its own.
51
+ if (hydrating) await prepareOctaneRouterHydration(router, { signal });
52
+ else await router.load();
53
+ },
54
+ async start({ root, identity, bootstrap, signal, load }) {
55
+ const application = {
56
+ container: root,
57
+ identity,
58
+ nativeHydrationBuildId,
59
+ federation: options.federation,
60
+ workerFederation: options.workerFederation,
61
+ documentId: bootstrap?.documentId,
62
+ signal,
63
+ // Hydration installs the native signal bridge before this importer runs.
64
+ load: async (): Promise<OctaneApplicationModule> => {
65
+ const view = await load();
66
+ if (view.kind === 'component')
67
+ return { default: componentView(view.component, view.i18n) };
68
+ return view.i18n
69
+ ? {
70
+ default: LocalizedRouterRoot as ComponentBody,
71
+ props: { router: view.router, i18n: view.i18n },
72
+ }
73
+ : {
74
+ default: OctaneRouterRoot as ComponentBody,
75
+ props: { router: view.router },
76
+ };
77
+ },
78
+ };
79
+ const handle = bootstrap?.hydrating
80
+ ? await hydrateOctaneApplication({
81
+ ...application,
82
+ documentIdentity: bootstrap.identity,
83
+ documentId: bootstrap.documentId,
84
+ documentNativeHydrationBuildId: bootstrap.nativeHydrationBuildId,
85
+ })
86
+ : await mountOctaneApplication(application);
87
+ return () => handle.dispose();
88
+ },
89
+ });
90
+ }
@@ -0,0 +1,153 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions/paths';
2
+ import {
3
+ collectDataHeaders,
4
+ dataMetadataToDocumentPolicy,
5
+ mergeDataResponseIntoResponse,
6
+ mergeDataResponseMetadata,
7
+ mergeHeaderFields,
8
+ } from '@modern-js/renderer-core/data';
9
+ import {
10
+ createNativeServerEntry as createServerEntry,
11
+ type NativeI18nInstance,
12
+ type NativeServerEntry,
13
+ type NativeServerEntryOptions,
14
+ } from '@modern-js/renderer-core/entry-server';
15
+ import { createNativeRouter } from '@modern-js/renderer-core/router';
16
+ import type { ResponsePolicy } from '@modern-js/renderer-core/session';
17
+ import type { ServerRenderNode } from 'octane/server';
18
+ import {
19
+ componentView,
20
+ localizedRouterRoot,
21
+ octaneRouterFactory,
22
+ } from './entry-application';
23
+ import { validateOctaneModuleManifest } from './manifest';
24
+ import {
25
+ createOctaneRequestHandler,
26
+ createSsrStreamResponse,
27
+ getSsrStatus,
28
+ OctaneRouterServer,
29
+ } from './router-server';
30
+ import { renderOctaneApplication, renderOctaneCSRDocument } from './server';
31
+
32
+ /** Document-describing fields an Octane route's `headers()` may set. */
33
+ const OCTANE_DOCUMENT_FIELDS = new Set([
34
+ 'content-language',
35
+ 'content-location',
36
+ 'content-disposition',
37
+ ]);
38
+
39
+ /**
40
+ * The loader projection strips representation fields; the ones an Octane route
41
+ * declares for the document itself are kept, as Solid document headers are.
42
+ */
43
+ export function withOctaneDocumentFields(
44
+ policy: ResponsePolicy,
45
+ responseHeaders: Headers,
46
+ ): ResponsePolicy {
47
+ const fields = collectDataHeaders(responseHeaders).filter(([name]) =>
48
+ OCTANE_DOCUMENT_FIELDS.has(name.toLowerCase()),
49
+ );
50
+ return fields.length
51
+ ? { ...policy, headers: [...policy.headers, ...fields] }
52
+ : policy;
53
+ }
54
+
55
+ export { createNativeWorkerFederationRuntime } from './worker-federation';
56
+
57
+ export interface OctaneServerEntryOptions
58
+ extends NativeServerEntryOptions<NativeI18nInstance, LocalisedUrlsOption> {
59
+ readonly workerFederationLocale?: string;
60
+ }
61
+
62
+ const LocalizedRouterServer = localizedRouterRoot(OctaneRouterServer);
63
+
64
+ /** The native transport handlers of a generated Octane server entry. */
65
+ export function createNativeServerEntry(
66
+ options: OctaneServerEntryOptions,
67
+ ): NativeServerEntry {
68
+ return createServerEntry(options, {
69
+ document: (document, nativeManifest, identity) => ({
70
+ ...document,
71
+ nativeHydrationBuildId: validateOctaneModuleManifest(
72
+ nativeManifest,
73
+ identity,
74
+ ).nativeHydrationBuildId,
75
+ }),
76
+ run: (_session, callback) => callback(),
77
+ respond: (_session, response) => response,
78
+ createRouter: (application, routerOptions) =>
79
+ createNativeRouter(application, routerOptions, octaneRouterFactory),
80
+ renderCSR: ({ session }, document) =>
81
+ renderOctaneCSRDocument({ session, document }),
82
+ renderComponent: (
83
+ { session, workerFederation },
84
+ component,
85
+ document,
86
+ i18n,
87
+ ) =>
88
+ renderOctaneApplication({
89
+ session,
90
+ App: componentView(component, i18n) as ServerRenderNode,
91
+ document,
92
+ federation: options.federation,
93
+ workerFederation,
94
+ workerFederationLocale: options.workerFederationLocale,
95
+ }),
96
+ async renderRoutes({
97
+ request,
98
+ context: { session, workerFederation },
99
+ router,
100
+ outcomes,
101
+ document,
102
+ forbiddenValues,
103
+ i18n,
104
+ }) {
105
+ const response = await createOctaneRequestHandler({
106
+ request,
107
+ session,
108
+ createRouter: () => router,
109
+ serialization: { forbiddenValues },
110
+ })(async ({ router, responseHeaders }) => {
111
+ const metadata = mergeDataResponseMetadata(outcomes, {
112
+ status: getSsrStatus(router),
113
+ });
114
+ // Route headers() take precedence over loader singleton fields; list
115
+ // fields keep both.
116
+ const policy = dataMetadataToDocumentPolicy({
117
+ ...metadata,
118
+ headers: mergeHeaderFields(
119
+ collectDataHeaders(responseHeaders),
120
+ metadata.headers,
121
+ ),
122
+ });
123
+ session.resolveResponse(
124
+ withOctaneDocumentFields(policy, responseHeaders),
125
+ );
126
+ const rendered = await renderOctaneApplication({
127
+ session,
128
+ ...(i18n
129
+ ? {
130
+ App: LocalizedRouterServer as ServerRenderNode,
131
+ props: { router, i18n },
132
+ }
133
+ : {
134
+ App: OctaneRouterServer as ServerRenderNode,
135
+ props: { router },
136
+ }),
137
+ document,
138
+ federation: options.federation,
139
+ workerFederation,
140
+ workerFederationLocale: options.workerFederationLocale,
141
+ router,
142
+ });
143
+ return createSsrStreamResponse(router, rendered);
144
+ });
145
+ return session.committedPolicy
146
+ ? response
147
+ : mergeDataResponseIntoResponse(
148
+ response,
149
+ mergeDataResponseMetadata(outcomes, { status: response.status }),
150
+ );
151
+ },
152
+ });
153
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ type FederationInstance,
3
+ getFederationHost,
4
+ type NativeFederationBinding,
5
+ } from '@modern-js/renderer-core/federation';
6
+ import { type ComponentBody, createContext, createElement } from 'octane';
7
+
8
+ /** One application owns its host runtime; each response owns its render scope. */
9
+ export interface OctaneFederationScope {
10
+ readonly instance: FederationInstance | undefined;
11
+ readonly server: boolean;
12
+ readonly nonce?: string;
13
+ }
14
+
15
+ export const FederationContext = createContext<OctaneFederationScope | null>(
16
+ null,
17
+ );
18
+
19
+ export function createFederationScope(
20
+ binding: NativeFederationBinding | undefined,
21
+ server: boolean,
22
+ nonce?: string,
23
+ ): OctaneFederationScope {
24
+ return {
25
+ instance: getFederationHost(binding),
26
+ server,
27
+ ...(nonce === undefined ? {} : { nonce }),
28
+ };
29
+ }
30
+
31
+ export function FederationRoot(props: {
32
+ readonly scope: OctaneFederationScope;
33
+ readonly children?: unknown;
34
+ }) {
35
+ return createElement(FederationContext, {
36
+ value: props.scope,
37
+ children: props.children,
38
+ });
39
+ }
40
+
41
+ /** A stable root component retains native state when its application updates. */
42
+ export function FederationApplication(props: {
43
+ readonly scope: OctaneFederationScope;
44
+ readonly application: {
45
+ readonly default: ComponentBody;
46
+ readonly props?: unknown;
47
+ };
48
+ }) {
49
+ return createElement(FederationRoot, {
50
+ scope: props.scope,
51
+ children: createElement(props.application.default, props.application.props),
52
+ });
53
+ }
@@ -0,0 +1,307 @@
1
+ import { loadFederatedModule } from '@modern-js/renderer-core/federation';
2
+ import {
3
+ type ComponentBody,
4
+ createElement,
5
+ decodeExternalSnapshot,
6
+ externalSnapshotBoundary,
7
+ hookSlots,
8
+ lazy,
9
+ type OctaneNode,
10
+ preinit,
11
+ publisherBoundary,
12
+ Suspense,
13
+ serializeExternalSnapshotRequest,
14
+ useContext,
15
+ useEffect,
16
+ } from 'octane';
17
+ import {
18
+ FederationContext,
19
+ type OctaneFederationScope,
20
+ } from './federation-context';
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
+
29
+ /** The native component shape exposed by a same-renderer Octane remote. */
30
+ export interface FederatedModule<P extends object> {
31
+ readonly default: ComponentBody<P>;
32
+ }
33
+
34
+ export interface FederatedComponentOptions {
35
+ /** Native content shown while loading, or after a failed server load. */
36
+ readonly fallback?: () => OctaneNode;
37
+ /** A positive load limit; the server defaults to 3000ms. */
38
+ readonly timeout?: number;
39
+ /** Explicit registered server projections required by an application remote. */
40
+ readonly contextKeys?: readonly string[];
41
+ }
42
+
43
+ function withTimeout<T>(
44
+ promise: Promise<T>,
45
+ timeout: number | undefined,
46
+ label: string,
47
+ ): Promise<T> {
48
+ if (timeout === undefined) return promise;
49
+ return new Promise<T>((resolve, reject) => {
50
+ const timer = setTimeout(
51
+ () =>
52
+ reject(
53
+ new Error(`Remote module ${label} did not load within ${timeout}ms`),
54
+ ),
55
+ timeout,
56
+ );
57
+ (timer as { unref?: () => void }).unref?.();
58
+ promise.then(
59
+ value => {
60
+ clearTimeout(timer);
61
+ resolve(value);
62
+ },
63
+ error => {
64
+ clearTimeout(timer);
65
+ reject(error);
66
+ },
67
+ );
68
+ });
69
+ }
70
+
71
+ // This library wrapper is published as plain JavaScript, outside app compilation.
72
+ const publisherRegistrationEffectSlot = Symbol(hookSlots(1));
73
+
74
+ /**
75
+ * Render a native Octane remote by its `remote/Expose` id or a browser loader.
76
+ * Native Suspense owns server fallbacks, streamed styles and hydration adoption;
77
+ * browser failures reach the nearest native ErrorBoundary. A loader function is
78
+ * browser-only; use an id to render through the host's server federation runtime.
79
+ */
80
+ export function federatedComponent<P extends object = Record<string, never>>(
81
+ remote: string | (() => Promise<FederatedModule<P>>),
82
+ options: FederatedComponentOptions = {},
83
+ ): ComponentBody<P> {
84
+ if (
85
+ typeof remote === 'string'
86
+ ? !/^[^/]+\/.+/u.test(remote)
87
+ : typeof remote !== 'function'
88
+ ) {
89
+ throw new TypeError(
90
+ "federatedComponent requires a remote id such as 'remote/Widget' or a loader",
91
+ );
92
+ }
93
+ if (
94
+ options.timeout !== undefined &&
95
+ (!Number.isFinite(options.timeout) || options.timeout <= 0)
96
+ ) {
97
+ throw new TypeError(
98
+ 'federatedComponent timeout must be a positive number of milliseconds',
99
+ );
100
+ }
101
+ const id = typeof remote === 'string' ? remote : undefined;
102
+ const standalone: OctaneFederationScope = {
103
+ instance: undefined,
104
+ server: typeof document === 'undefined',
105
+ };
106
+ // Native lazy caches its module. Scope the cache to one application/response,
107
+ // so two hosts using the same component definition cannot share a remote.
108
+ const components = new WeakMap<object, ComponentBody<P>>();
109
+ const workerOwners = new WeakMap<
110
+ OctaneFederationScope,
111
+ WeakMap<OctaneWorkerFederationScope, Map<string, object>>
112
+ >();
113
+
114
+ return props => {
115
+ const context = useContext(FederationContext) ?? standalone;
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
+ // Server snapshots are locale-specific. The hydrated browser publisher
130
+ // keeps its native owner and receives language changes through context.
131
+ const key = JSON.stringify([
132
+ context.server ? language : undefined,
133
+ contextKeys,
134
+ ]);
135
+ let variant = variants.get(key);
136
+ if (!variant) variants.set(key, (variant = {}));
137
+ owner = variant;
138
+ }
139
+ let Remote = components.get(owner);
140
+ if (!Remote) {
141
+ Remote = lazy(async () => {
142
+ try {
143
+ if (context.server && !id) {
144
+ throw new Error(
145
+ 'A federatedComponent loader renders in the browser only; use a remote id to server-render it',
146
+ );
147
+ }
148
+ if (worker) {
149
+ if (!id || !language) {
150
+ throw new Error(
151
+ 'Worker federation requires a remote id and the owning entry locale',
152
+ );
153
+ }
154
+ const separator = id.indexOf('/');
155
+ const remoteName = id.slice(0, separator);
156
+ const expose = `./${id.slice(separator + 1)}`;
157
+ const controller = new AbortController();
158
+ const signal = worker.signal
159
+ ? AbortSignal.any([worker.signal, controller.signal])
160
+ : controller.signal;
161
+ const timeout =
162
+ options.timeout ?? (context.server ? 3000 : undefined);
163
+ let admitted;
164
+ try {
165
+ admitted = await withTimeout(
166
+ worker.transport.admit({
167
+ remote: remoteName,
168
+ expose,
169
+ locale: language,
170
+ signal,
171
+ }),
172
+ timeout,
173
+ id,
174
+ );
175
+ } finally {
176
+ // Release this admission waiter, including after its own deadline.
177
+ controller.abort();
178
+ }
179
+ const BrowserRemote = lazy(async () => {
180
+ const { module, assets } = await loadFederatedModule<
181
+ FederatedModule<P>
182
+ >(context.instance, id);
183
+ if (typeof module?.default !== 'function') {
184
+ throw new TypeError(
185
+ 'A federated Octane module must default-export a native component',
186
+ );
187
+ }
188
+ const component = module.default;
189
+ const Loaded: ComponentBody<P> = (props, scope, extra) => {
190
+ for (const href of assets?.css ?? []) {
191
+ preinit(href, {
192
+ as: 'style',
193
+ precedence: 'federated',
194
+ ...(context.nonce === undefined
195
+ ? {}
196
+ : { nonce: context.nonce }),
197
+ });
198
+ }
199
+ return component(props, scope, extra);
200
+ };
201
+ return { default: Loaded };
202
+ });
203
+ const boundary = externalSnapshotBoundary({
204
+ authority: admitted.coordinate.native,
205
+ component: BrowserRemote,
206
+ contextKeys,
207
+ timeoutMs: timeout,
208
+ async snapshot(request, signal) {
209
+ const response = await worker.transport.request({
210
+ remote: remoteName,
211
+ expose,
212
+ locale: language,
213
+ payload: serializeExternalSnapshotRequest(request),
214
+ signal,
215
+ });
216
+ return decodeExternalSnapshot(response.payload);
217
+ },
218
+ });
219
+ const Loaded: ComponentBody<P> = props => {
220
+ // A Worker server does not execute BrowserRemote. Register the
221
+ // exact admitted expose's extracted CSS in this active native
222
+ // render; nested snapshots carry these hints through native head.
223
+ for (const href of admitted.styles) {
224
+ preinit(href, {
225
+ as: 'style',
226
+ precedence: 'federated',
227
+ ...(context.nonce === undefined
228
+ ? {}
229
+ : { nonce: context.nonce }),
230
+ });
231
+ }
232
+ return createElement(boundary, props);
233
+ };
234
+ return { default: Loaded };
235
+ }
236
+ const { module, assets } = await withTimeout(
237
+ Promise.resolve().then(() =>
238
+ id
239
+ ? loadFederatedModule<FederatedModule<P>>(context.instance, id)
240
+ : (remote as () => Promise<FederatedModule<P>>)().then(
241
+ module => ({ module, assets: undefined }),
242
+ ),
243
+ ),
244
+ options.timeout ?? (context.server ? 3000 : undefined),
245
+ id ?? 'loader',
246
+ );
247
+ if (typeof module?.default !== 'function') {
248
+ throw new TypeError(
249
+ 'A federated Octane module must default-export a native component',
250
+ );
251
+ }
252
+ if (context.server && !assets) {
253
+ throw new Error(
254
+ `Cannot server-render ${id}: the host has no browser assets for this remote`,
255
+ );
256
+ }
257
+ // Native publisher ownership admits this exact compiled component's
258
+ // signal generations. Keep the boundary in the context-scoped lazy
259
+ // cache so props and compatible hot updates retain its live owner.
260
+ const component = publisherBoundary(module.default, {
261
+ publisherKey: id ?? 'ultramodern:browser-loader',
262
+ });
263
+ const Loaded: ComponentBody<P> = props => {
264
+ // A development runtime installs this hook on the actual owning
265
+ // host. Registration follows the native mounted lifetime; routing
266
+ // alone grants no replacement authority to a remote name.
267
+ useEffect(
268
+ () => {
269
+ if (context.server || !id || !context.instance) return;
270
+ const register = Reflect.get(
271
+ context.instance,
272
+ Symbol.for('ultramodern.federation.browser-dev-boundary.v1'),
273
+ );
274
+ if (typeof register !== 'function') return;
275
+ return register(id, module.default, component);
276
+ },
277
+ [],
278
+ publisherRegistrationEffectSlot,
279
+ );
280
+ // Resource APIs require an active native render. Late styles travel
281
+ // in Octane's resource carrier ahead of the remote's HTML segment.
282
+ for (const href of assets?.css ?? []) {
283
+ preinit(href, {
284
+ as: 'style',
285
+ precedence: 'federated',
286
+ ...(context.nonce === undefined
287
+ ? {}
288
+ : { nonce: context.nonce }),
289
+ });
290
+ }
291
+ return createElement(component, props);
292
+ };
293
+ return { default: Loaded };
294
+ } catch (error) {
295
+ // A later mount or ErrorBoundary reset may retry a failed load.
296
+ if (!context.server) components.delete(owner);
297
+ throw error;
298
+ }
299
+ });
300
+ components.set(owner, Remote);
301
+ }
302
+ return createElement(Suspense, {
303
+ fallback: options.fallback,
304
+ children: createElement(Remote, props),
305
+ });
306
+ };
307
+ }
@@ -0,0 +1,47 @@
1
+ import { createElement, hookSlots, useSyncExternalStore } from 'octane';
2
+ import { I18nContext } from './context';
3
+ import type { I18nContextValue } from './types';
4
+
5
+ export interface I18nProviderProps extends Omit<I18nContextValue, 'language'> {
6
+ children?: unknown;
7
+ }
8
+
9
+ /**
10
+ * Provides a per-request i18next-shaped instance to the Octane tree. The
11
+ * instance must come from the caller (created with `cloneInstance` per
12
+ * request on the server, and the same instance reused on hydration) — this
13
+ * provider holds no module-level state of its own beyond the one reactive
14
+ * `language` state it derives from `instance` and shares with every
15
+ * consumer through context, matching `@modern-js/plugin-i18n`'s React
16
+ * `ModernI18nProvider` shape (one subscription, pushed through context,
17
+ * rather than every consumer subscribing to `languageChanged` itself).
18
+ *
19
+ * No JSX: built with `createElement`, matching this package's existing
20
+ * style (see `src/routes.ts`), and avoiding any dependency on Octane's
21
+ * JSX/compiler toolchain for a file that needs none of its features.
22
+ */
23
+ // Plain TypeScript is not rewritten by the Octane compiler, so each hook call
24
+ // site carries its own stable slot, like `OctaneRouterRoot`.
25
+ const languageStoreSlot = Symbol(hookSlots(1));
26
+
27
+ export function I18nProvider(props: I18nProviderProps) {
28
+ const instance = props.instance;
29
+ const language = useSyncExternalStore(
30
+ notify => {
31
+ instance.on?.('languageChanged', notify);
32
+ return () => instance.off?.('languageChanged', notify);
33
+ },
34
+ () => instance.language,
35
+ () => instance.language,
36
+ languageStoreSlot,
37
+ );
38
+
39
+ const value: I18nContextValue = {
40
+ instance: props.instance,
41
+ languages: props.languages,
42
+ localisedUrls: props.localisedUrls,
43
+ language,
44
+ };
45
+
46
+ return createElement(I18nContext, { value, children: props.children });
47
+ }