@bleedingdev/modern-js-federation-runtime 0.0.0 → 3.9.0-ultramodern.5

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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/module-federation/consume-surface.js +199 -0
  3. package/dist/cjs/module-federation/distributed-ssr.js +135 -0
  4. package/dist/cjs/module-federation/index.js +316 -0
  5. package/dist/cjs/module-federation/last-known-good.js +157 -0
  6. package/dist/cjs/module-federation/manifest-recovery-runtime-plugin.js +130 -0
  7. package/dist/cjs/module-federation/surface-resolution-types.js +61 -0
  8. package/dist/esm/module-federation/consume-surface.mjs +158 -0
  9. package/dist/esm/module-federation/distributed-ssr.mjs +82 -0
  10. package/dist/esm/module-federation/index.mjs +115 -0
  11. package/dist/esm/module-federation/last-known-good.mjs +113 -0
  12. package/dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs +90 -0
  13. package/dist/esm/module-federation/surface-resolution-types.mjs +8 -0
  14. package/dist/esm-node/module-federation/consume-surface.mjs +159 -0
  15. package/dist/esm-node/module-federation/distributed-ssr.mjs +83 -0
  16. package/dist/esm-node/module-federation/index.mjs +116 -0
  17. package/dist/esm-node/module-federation/last-known-good.mjs +114 -0
  18. package/dist/esm-node/module-federation/manifest-recovery-runtime-plugin.mjs +91 -0
  19. package/dist/esm-node/module-federation/surface-resolution-types.mjs +9 -0
  20. package/dist/types/module-federation/consume-surface.d.ts +104 -0
  21. package/dist/types/module-federation/distributed-ssr.d.ts +61 -0
  22. package/dist/types/module-federation/index.d.ts +71 -0
  23. package/dist/types/module-federation/last-known-good.d.ts +52 -0
  24. package/dist/types/module-federation/manifest-recovery-runtime-plugin.d.ts +25 -0
  25. package/dist/types/module-federation/surface-resolution-types.d.ts +23 -0
  26. package/package.json +86 -4
  27. package/src/module-federation/consume-surface.ts +369 -0
  28. package/src/module-federation/distributed-ssr.tsx +240 -0
  29. package/src/module-federation/index.ts +274 -0
  30. package/src/module-federation/last-known-good.ts +292 -0
  31. package/src/module-federation/manifest-recovery-runtime-plugin.ts +215 -0
  32. package/src/module-federation/surface-resolution-types.ts +60 -0
  33. package/README.md +0 -3
@@ -0,0 +1,240 @@
1
+ import { useRuntimeContext } from '@modern-js/runtime';
2
+ import type { ComponentType, ReactNode } from 'react';
3
+
4
+ export const DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY =
5
+ '__modernDistributedSsrFragments';
6
+ export const DISTRIBUTED_SSR_FRAGMENT_REQUEST_LOCALS_KEY =
7
+ '__modernDistributedSsrFragmentRequest';
8
+
9
+ export type DistributedSsrFragment = {
10
+ boundaryId: string;
11
+ buildMarker?: string;
12
+ digest?: string;
13
+ expose: string;
14
+ html: string;
15
+ remote: string;
16
+ status: 'ready';
17
+ };
18
+
19
+ export type DistributedSsrFragmentFailure = {
20
+ boundaryId: string;
21
+ expose: string;
22
+ reason: string;
23
+ remote: string;
24
+ status: 'degraded';
25
+ };
26
+
27
+ type DistributedSsrFragmentResult =
28
+ | DistributedSsrFragment
29
+ | DistributedSsrFragmentFailure;
30
+
31
+ export type DistributedSsrFragmentContext = {
32
+ fragments?: Record<
33
+ string,
34
+ DistributedSsrFragment | DistributedSsrFragmentFailure
35
+ >;
36
+ required: true;
37
+ resolve?: (
38
+ remote: string,
39
+ expose: string,
40
+ props: Record<string, unknown>,
41
+ ) => DistributedSsrFragmentResult | PromiseLike<DistributedSsrFragmentResult>;
42
+ };
43
+
44
+ export type DistributedSsrBoundaryProps<
45
+ Props extends object = Record<string, unknown>,
46
+ > = {
47
+ children: ReactNode;
48
+ expose: string;
49
+ fallback: ReactNode;
50
+ fragmentProps?: Props;
51
+ remote: string;
52
+ };
53
+
54
+ export type CreateDistributedSsrComponentOptions<Props extends object> = {
55
+ createComponent: () => ComponentType<Props>;
56
+ expose: string;
57
+ fallback: ReactNode;
58
+ remote: string;
59
+ };
60
+
61
+ export function distributedSsrFragmentKey(remote: string, expose: string) {
62
+ return `${remote}::${expose}`;
63
+ }
64
+
65
+ function getDistributedSsrFragmentContext(
66
+ response: { locals?: unknown } | undefined,
67
+ ): DistributedSsrFragmentContext | undefined {
68
+ const locals = response?.locals;
69
+ if (typeof locals !== 'object' || locals === null) {
70
+ return undefined;
71
+ }
72
+
73
+ const context =
74
+ DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY in locals
75
+ ? locals[DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY]
76
+ : undefined;
77
+
78
+ if (typeof context !== 'object' || context === null) {
79
+ return undefined;
80
+ }
81
+
82
+ return context as DistributedSsrFragmentContext;
83
+ }
84
+
85
+ export function useDistributedSsrFragmentProps<Props extends object>({
86
+ boundaryId,
87
+ expose,
88
+ }: {
89
+ boundaryId: string;
90
+ expose: string;
91
+ }): Props {
92
+ const runtimeContext = useRuntimeContext();
93
+ const locals = runtimeContext.requestContext?.response?.locals;
94
+ const request =
95
+ typeof locals === 'object' &&
96
+ locals !== null &&
97
+ DISTRIBUTED_SSR_FRAGMENT_REQUEST_LOCALS_KEY in locals
98
+ ? locals[DISTRIBUTED_SSR_FRAGMENT_REQUEST_LOCALS_KEY]
99
+ : undefined;
100
+
101
+ if (
102
+ typeof request !== 'object' ||
103
+ request === null ||
104
+ !('boundaryId' in request) ||
105
+ request.boundaryId !== boundaryId ||
106
+ !('expose' in request) ||
107
+ request.expose !== expose
108
+ ) {
109
+ throw new Error(
110
+ `Distributed SSR fragment request contract mismatch for ${boundaryId} ${expose}.`,
111
+ );
112
+ }
113
+
114
+ const props = 'props' in request ? request.props : undefined;
115
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) {
116
+ throw new Error(
117
+ `Distributed SSR fragment request props must be an object for ${boundaryId} ${expose}.`,
118
+ );
119
+ }
120
+
121
+ return props as Props;
122
+ }
123
+
124
+ function isThenable(
125
+ value:
126
+ | DistributedSsrFragmentResult
127
+ | PromiseLike<DistributedSsrFragmentResult>
128
+ | undefined,
129
+ ): value is PromiseLike<DistributedSsrFragmentResult> {
130
+ return (
131
+ value !== undefined && 'then' in value && typeof value.then === 'function'
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Keeps the browser on the native Module Federation component while allowing
137
+ * servers that cannot execute remote code (notably workerd) to render a
138
+ * trusted fragment produced by the remote Worker itself.
139
+ *
140
+ * Node SSR has no distributed-fragment context and therefore renders the
141
+ * child normally, preserving Module Federation's in-process SSR path.
142
+ */
143
+ export function DistributedSsrBoundary<
144
+ Props extends object = Record<string, unknown>,
145
+ >({
146
+ children,
147
+ expose,
148
+ fallback,
149
+ fragmentProps,
150
+ remote,
151
+ }: DistributedSsrBoundaryProps<Props>) {
152
+ const runtimeContext = useRuntimeContext();
153
+ const key = distributedSsrFragmentKey(remote, expose);
154
+ const attributes = {
155
+ 'data-modern-distributed-ssr-boundary': key,
156
+ };
157
+
158
+ if (runtimeContext.isBrowser) {
159
+ return <div {...attributes}>{children}</div>;
160
+ }
161
+
162
+ const fragmentContext = getDistributedSsrFragmentContext(
163
+ runtimeContext.requestContext?.response,
164
+ );
165
+
166
+ // Node can execute the Module Federation server runtime. Only workerd's
167
+ // request adapter supplies a required distributed-fragment context.
168
+ if (fragmentContext === undefined) {
169
+ return <div {...attributes}>{children}</div>;
170
+ }
171
+
172
+ const resolverFragmentProps =
173
+ fragmentProps === undefined
174
+ ? {}
175
+ : Object.fromEntries(Object.entries(fragmentProps));
176
+ const fragmentOrPromise =
177
+ fragmentContext.resolve?.(remote, expose, resolverFragmentProps) ??
178
+ fragmentContext.fragments?.[key];
179
+ if (isThenable(fragmentOrPromise)) {
180
+ throw fragmentOrPromise;
181
+ }
182
+ const fragment = fragmentOrPromise;
183
+ if (fragment?.status === 'ready') {
184
+ return (
185
+ <div
186
+ {...attributes}
187
+ data-modern-distributed-ssr-build={fragment.buildMarker}
188
+ data-modern-distributed-ssr-digest={fragment.digest}
189
+ data-modern-distributed-ssr-status="ready"
190
+ // The fragment is accepted only after the Worker adapter verifies its
191
+ // declared boundary id and expose marker.
192
+ dangerouslySetInnerHTML={{ __html: fragment.html }}
193
+ />
194
+ );
195
+ }
196
+
197
+ return (
198
+ <div
199
+ {...attributes}
200
+ data-modern-distributed-ssr-reason={fragment?.reason ?? 'missing'}
201
+ data-modern-distributed-ssr-status="degraded"
202
+ >
203
+ {fallback}
204
+ </div>
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Defers creation of the native Module Federation component until React
210
+ * actually renders that child. A workerd request with a verified distributed
211
+ * fragment never constructs the native remote, so its module cannot perform
212
+ * forbidden global-scope network I/O. Browser and Node rendering keep using
213
+ * the native Module Federation component unchanged.
214
+ */
215
+ export function createDistributedSsrComponent<Props extends object>({
216
+ createComponent,
217
+ expose,
218
+ fallback,
219
+ remote,
220
+ }: CreateDistributedSsrComponentOptions<Props>) {
221
+ let RemoteComponent: ComponentType<Props> | undefined;
222
+
223
+ function DeferredRemoteComponent(props: Props) {
224
+ RemoteComponent ??= createComponent();
225
+ return <RemoteComponent {...props} />;
226
+ }
227
+
228
+ return function DistributedSsrComponent(props: Props) {
229
+ return (
230
+ <DistributedSsrBoundary
231
+ expose={expose}
232
+ fallback={fallback}
233
+ fragmentProps={props}
234
+ remote={remote}
235
+ >
236
+ <DeferredRemoteComponent {...props} />
237
+ </DistributedSsrBoundary>
238
+ );
239
+ };
240
+ }
@@ -0,0 +1,274 @@
1
+ // @effect-diagnostics asyncFunction:off extendsNativeError:off globalFetch:off strictBooleanExpressions:off
2
+
3
+ export * from './consume-surface';
4
+ export * from './distributed-ssr';
5
+ export {
6
+ DISTRIBUTED_SSR_FRAGMENT_REQUEST_LOCALS_KEY,
7
+ useDistributedSsrFragmentProps,
8
+ } from './distributed-ssr';
9
+ export * from './last-known-good';
10
+ export * from './manifest-recovery-runtime-plugin';
11
+ export * from './surface-resolution-types';
12
+
13
+ export const MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT =
14
+ 'modernjs:mf-runtime-fallback';
15
+
16
+ export const MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT =
17
+ 'modernjs:mf-runtime-recovery';
18
+
19
+ export const DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT =
20
+ '/_modern/contract-gates/runtime-fallback';
21
+
22
+ export const DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER =
23
+ 'x-modernjs-runtime-signal-token';
24
+
25
+ export type ModuleFederationFallbackClassification =
26
+ | 'timeout'
27
+ | 'network'
28
+ | 'contract'
29
+ | 'version-skew'
30
+ | 'remote-unavailable';
31
+
32
+ export type ModuleFederationFallbackStatus =
33
+ | 'degraded'
34
+ | 'failed'
35
+ | 'recovered';
36
+
37
+ export type ModuleFederationFallbackTelemetryInput = {
38
+ appName: string;
39
+ classification: ModuleFederationFallbackClassification;
40
+ entry?: string;
41
+ error?: unknown;
42
+ eventName?: string;
43
+ exportName?: string;
44
+ metadata?: Record<string, unknown>;
45
+ phase: 'discovery' | 'load' | 'mount' | 'hydrate' | 'recover';
46
+ remote: string;
47
+ runtimeDigest?: string;
48
+ status?: ModuleFederationFallbackStatus;
49
+ };
50
+
51
+ export type ModuleFederationFallbackTelemetryPayload = {
52
+ appName: string;
53
+ entry?: string;
54
+ eventName: string;
55
+ phase: ModuleFederationFallbackTelemetryInput['phase'];
56
+ reason: ModuleFederationFallbackClassification;
57
+ runtimeDigest?: string;
58
+ schemaVersion: 1;
59
+ metadata: {
60
+ classification: ModuleFederationFallbackClassification;
61
+ errorMessage?: string;
62
+ errorName?: string;
63
+ exportName?: string;
64
+ remote: string;
65
+ runtimeDigest?: string;
66
+ status: ModuleFederationFallbackStatus;
67
+ } & Record<string, unknown>;
68
+ };
69
+
70
+ export type ModuleFederationFallbackTelemetryEmitOptions = {
71
+ authHeaderName?: string;
72
+ authToken?: string;
73
+ endpoint?: string;
74
+ fetchImpl?: typeof fetch;
75
+ postSignal?: boolean;
76
+ };
77
+
78
+ export type ModuleFederationFallbackTelemetryEmitResult = {
79
+ dispatched: boolean;
80
+ posted: boolean;
81
+ postStatus?: number;
82
+ };
83
+
84
+ export class ModuleFederationRemoteLoadTimeoutError extends Error {
85
+ constructor(remote: string, timeoutMs: number) {
86
+ super(`Loading remote "${remote}" timed out after ${timeoutMs}ms`);
87
+ this.name = 'ModuleFederationRemoteLoadTimeoutError';
88
+ }
89
+ }
90
+
91
+ export class ModuleFederationRemoteLoadError extends Error {
92
+ readonly remote: string;
93
+ readonly attempts: number;
94
+ readonly causeError: Error;
95
+
96
+ constructor(remote: string, attempts: number, causeError: Error) {
97
+ super(
98
+ `Unable to load remote "${remote}" after ${attempts} attempt${
99
+ attempts > 1 ? 's' : ''
100
+ }: ${causeError.message}`,
101
+ );
102
+ this.name = 'ModuleFederationRemoteLoadError';
103
+ this.remote = remote;
104
+ this.attempts = attempts;
105
+ this.causeError = causeError;
106
+ }
107
+ }
108
+
109
+ export class ModuleFederationRemoteComponentContractError extends Error {
110
+ constructor(remote: string, exportName: string) {
111
+ super(
112
+ `Remote "${remote}" export "${exportName}" is not a valid React component`,
113
+ );
114
+ this.name = 'ModuleFederationRemoteComponentContractError';
115
+ }
116
+ }
117
+
118
+ function toError(error: unknown) {
119
+ if (error instanceof Error) {
120
+ return error;
121
+ }
122
+
123
+ return new Error(
124
+ typeof error === 'string' ? error : 'Unknown remote load error',
125
+ );
126
+ }
127
+
128
+ export function classifyModuleFederationFallback(
129
+ error: unknown,
130
+ ): ModuleFederationFallbackClassification {
131
+ const normalizedError = toError(error);
132
+ if (normalizedError instanceof ModuleFederationRemoteLoadError) {
133
+ return classifyModuleFederationFallback(normalizedError.causeError);
134
+ }
135
+ if (normalizedError instanceof ModuleFederationRemoteLoadTimeoutError) {
136
+ return 'timeout';
137
+ }
138
+ if (normalizedError instanceof ModuleFederationRemoteComponentContractError) {
139
+ return 'contract';
140
+ }
141
+
142
+ const message = normalizedError.message;
143
+ if (
144
+ /version|requiredVersion|singleton|share scope|shared module/i.test(message)
145
+ ) {
146
+ return 'version-skew';
147
+ }
148
+ if (/network|fetch|script|timeout|chunk|loading/i.test(message)) {
149
+ return 'network';
150
+ }
151
+
152
+ return 'remote-unavailable';
153
+ }
154
+
155
+ export function createModuleFederationFallbackTelemetry(
156
+ input: ModuleFederationFallbackTelemetryInput,
157
+ ): ModuleFederationFallbackTelemetryPayload {
158
+ const error = input.error !== undefined ? toError(input.error) : undefined;
159
+ const status = input.status ?? 'degraded';
160
+ const eventName =
161
+ input.eventName ??
162
+ (status === 'recovered'
163
+ ? MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT
164
+ : MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT);
165
+ const metadata: ModuleFederationFallbackTelemetryPayload['metadata'] = {
166
+ ...(input.metadata ?? {}),
167
+ classification: input.classification,
168
+ remote: input.remote,
169
+ status,
170
+ };
171
+
172
+ if (input.exportName !== undefined) {
173
+ metadata.exportName = input.exportName;
174
+ }
175
+ if (input.runtimeDigest !== undefined) {
176
+ metadata.runtimeDigest = input.runtimeDigest;
177
+ }
178
+ if (error !== undefined) {
179
+ metadata.errorName = error.name;
180
+ metadata.errorMessage = error.message;
181
+ }
182
+
183
+ const payload: ModuleFederationFallbackTelemetryPayload = {
184
+ appName: input.appName,
185
+ eventName,
186
+ metadata,
187
+ phase: input.phase,
188
+ reason: input.classification,
189
+ schemaVersion: 1,
190
+ };
191
+
192
+ if (input.entry !== undefined) {
193
+ payload.entry = input.entry;
194
+ }
195
+ if (input.runtimeDigest !== undefined) {
196
+ payload.runtimeDigest = input.runtimeDigest;
197
+ }
198
+
199
+ return payload;
200
+ }
201
+
202
+ export function toModuleFederationFallbackAttributes(
203
+ payload: ModuleFederationFallbackTelemetryPayload,
204
+ ): Record<string, string> {
205
+ return {
206
+ 'data-mf-fallback-app': payload.appName,
207
+ 'data-mf-fallback-classification': payload.reason,
208
+ 'data-mf-fallback-phase': payload.phase,
209
+ 'data-mf-fallback-remote': String(payload.metadata.remote),
210
+ 'data-mf-fallback-status': String(payload.metadata.status),
211
+ 'data-mf-telemetry-event': payload.eventName,
212
+ };
213
+ }
214
+
215
+ export async function emitModuleFederationFallbackTelemetry(
216
+ input: ModuleFederationFallbackTelemetryInput,
217
+ options: ModuleFederationFallbackTelemetryEmitOptions = {},
218
+ ): Promise<ModuleFederationFallbackTelemetryEmitResult> {
219
+ const payload = createModuleFederationFallbackTelemetry(input);
220
+
221
+ if (
222
+ typeof window !== 'undefined' &&
223
+ typeof window.dispatchEvent === 'function' &&
224
+ typeof CustomEvent !== 'undefined'
225
+ ) {
226
+ window.dispatchEvent(
227
+ new CustomEvent(payload.eventName, {
228
+ detail: payload,
229
+ }),
230
+ );
231
+ }
232
+
233
+ const shouldPost = options.postSignal === true || Boolean(options.endpoint);
234
+ if (!shouldPost) {
235
+ return {
236
+ dispatched: true,
237
+ posted: false,
238
+ };
239
+ }
240
+
241
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
242
+ if (typeof fetchImpl !== 'function') {
243
+ return {
244
+ dispatched: true,
245
+ posted: false,
246
+ };
247
+ }
248
+
249
+ const headers = new Headers({
250
+ 'content-type': 'application/json',
251
+ });
252
+ if (options.authToken !== undefined && options.authToken.length > 0) {
253
+ headers.set(
254
+ options.authHeaderName ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER,
255
+ options.authToken,
256
+ );
257
+ }
258
+
259
+ const response = await fetchImpl(
260
+ options.endpoint ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT,
261
+ {
262
+ body: JSON.stringify(payload),
263
+ headers,
264
+ method: 'POST',
265
+ keepalive: true,
266
+ },
267
+ );
268
+
269
+ return {
270
+ dispatched: true,
271
+ posted: true,
272
+ postStatus: response.status,
273
+ };
274
+ }