@bleedingdev/modern-js-federation-runtime 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (35) hide show
  1. package/dist/cjs/module-federation/consume-surface.js +6 -16
  2. package/dist/cjs/module-federation/distributed-ssr.js +6 -1
  3. package/dist/cjs/module-federation/index.js +13 -239
  4. package/dist/cjs/module-federation/manifest-recovery-runtime-plugin.js +1 -1
  5. package/dist/cjs/module-federation/renderer-contract.js +159 -0
  6. package/dist/cjs/module-federation/renderer-runtime-plugin.js +265 -0
  7. package/dist/cjs/module-federation/telemetry.js +214 -0
  8. package/dist/esm/module-federation/consume-surface.mjs +3 -13
  9. package/dist/esm/module-federation/distributed-ssr.mjs +6 -1
  10. package/dist/esm/module-federation/index.mjs +1 -111
  11. package/dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs +1 -1
  12. package/dist/esm/module-federation/renderer-contract.mjs +103 -0
  13. package/dist/esm/module-federation/renderer-runtime-plugin.mjs +222 -0
  14. package/dist/esm/module-federation/telemetry.mjs +142 -0
  15. package/dist/esm-node/module-federation/consume-surface.mjs +3 -13
  16. package/dist/esm-node/module-federation/distributed-ssr.mjs +6 -1
  17. package/dist/esm-node/module-federation/index.mjs +1 -111
  18. package/dist/esm-node/module-federation/manifest-recovery-runtime-plugin.mjs +1 -1
  19. package/dist/esm-node/module-federation/renderer-contract.mjs +104 -0
  20. package/dist/esm-node/module-federation/renderer-runtime-plugin.mjs +223 -0
  21. package/dist/esm-node/module-federation/telemetry.mjs +143 -0
  22. package/dist/types/module-federation/consume-surface.d.ts +5 -5
  23. package/dist/types/module-federation/distributed-ssr.d.ts +18 -1
  24. package/dist/types/module-federation/index.d.ts +1 -67
  25. package/dist/types/module-federation/renderer-contract.d.ts +20 -0
  26. package/dist/types/module-federation/renderer-runtime-plugin.d.ts +98 -0
  27. package/dist/types/module-federation/telemetry.d.ts +71 -0
  28. package/package.json +54 -18
  29. package/src/module-federation/consume-surface.ts +18 -33
  30. package/src/module-federation/distributed-ssr.tsx +27 -2
  31. package/src/module-federation/index.ts +1 -267
  32. package/src/module-federation/manifest-recovery-runtime-plugin.ts +5 -1
  33. package/src/module-federation/renderer-contract.ts +176 -0
  34. package/src/module-federation/renderer-runtime-plugin.ts +396 -0
  35. package/src/module-federation/telemetry.ts +297 -0
@@ -1,274 +1,8 @@
1
1
  // @effect-diagnostics asyncFunction:off extendsNativeError:off globalFetch:off strictBooleanExpressions:off
2
2
 
3
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
4
  export * from './last-known-good';
10
5
  export * from './manifest-recovery-runtime-plugin';
11
6
  export * from './surface-resolution-types';
12
7
 
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
- }
8
+ export * from './telemetry';
@@ -63,8 +63,12 @@ const getErrorStatus = (error: unknown) => {
63
63
  };
64
64
 
65
65
  const isRetryableError = (error: unknown) => {
66
+ // Module Federation parses the manifest body without checking the HTTP
67
+ // status, so a 503 error page reaches this hook as a JSON SyntaxError.
68
+ // Recovery fetches again and reads the real status: a retryable status
69
+ // retries, and a successful response with malformed JSON stops there.
66
70
  if (error instanceof SyntaxError) {
67
- return false;
71
+ return true;
68
72
  }
69
73
 
70
74
  const message = getErrorMessage(error);
@@ -0,0 +1,176 @@
1
+ import {
2
+ assertRendererProfileCompatibility,
3
+ formatBackendFederationValidationErrors,
4
+ type RendererIdentity,
5
+ type RendererPackageIdentity,
6
+ type RendererProfile,
7
+ validateRendererIdentity,
8
+ validateRendererProfile,
9
+ } from '@modern-js/backend-federation-contracts';
10
+
11
+ export const RENDERER_FEDERATION_METADATA_KEY = 'ultramodernRenderer';
12
+ export const RENDERER_FEDERATION_SCHEMA = 'ultramodern.renderer-federation';
13
+ export const RENDERER_FEDERATION_SCHEMA_VERSION = 1;
14
+
15
+ /** The consuming renderer tuple; application and source identities are independent. */
16
+ export interface RendererFederationCompatibility {
17
+ readonly profile: RendererProfile;
18
+ readonly runtime: RendererPackageIdentity;
19
+ readonly bootstrap: RendererPackageIdentity;
20
+ }
21
+
22
+ /** Finalized native build authority for both browser and SSR remote entries. */
23
+ export interface RendererFederationContract
24
+ extends RendererFederationCompatibility {
25
+ readonly schema: typeof RENDERER_FEDERATION_SCHEMA;
26
+ readonly schemaVersion: typeof RENDERER_FEDERATION_SCHEMA_VERSION;
27
+ readonly identities: Readonly<Record<string, RendererIdentity>>;
28
+ }
29
+
30
+ export const rendererFederationError = (message: string): Error =>
31
+ new Error(`Renderer federation manifest contract: ${message}`);
32
+
33
+ const record = (value: unknown): value is Record<string, unknown> =>
34
+ value !== null && typeof value === 'object' && !Array.isArray(value);
35
+
36
+ const exactFields = (
37
+ value: Record<string, unknown>,
38
+ fields: readonly string[],
39
+ label: string,
40
+ ): void => {
41
+ const unknown = Object.keys(value).filter(key => !fields.includes(key));
42
+ const missing = fields.filter(key => !Object.hasOwn(value, key));
43
+ if (unknown.length || missing.length)
44
+ throw rendererFederationError(
45
+ `${label} fields differ (unknown=${unknown.join(',')}; missing=${missing.join(',')}).`,
46
+ );
47
+ };
48
+
49
+ const assertProfile = (value: unknown): RendererProfile => {
50
+ const result = validateRendererProfile(value, 'rendererProfile');
51
+ if (!result.ok)
52
+ throw rendererFederationError(
53
+ formatBackendFederationValidationErrors(result.errors),
54
+ );
55
+ const profile = value as RendererProfile;
56
+ return Object.freeze({
57
+ ...profile,
58
+ compiler: Object.freeze({ ...profile.compiler }),
59
+ hydration: Object.freeze({ ...profile.hydration }),
60
+ router: Object.freeze({ ...profile.router }),
61
+ });
62
+ };
63
+
64
+ // The shared contract's compiler package validator also owns exact package
65
+ // names/versions for runtime and bootstrap; do not maintain another semver ABI.
66
+ const assertPackage = (
67
+ value: unknown,
68
+ profile: RendererProfile,
69
+ label: string,
70
+ ): RendererPackageIdentity => {
71
+ const result = validateRendererProfile(
72
+ { ...profile, compiler: value },
73
+ label,
74
+ );
75
+ if (!result.ok)
76
+ throw rendererFederationError(
77
+ formatBackendFederationValidationErrors(result.errors),
78
+ );
79
+ return Object.freeze({ ...(value as RendererPackageIdentity) });
80
+ };
81
+
82
+ export function readRendererFederationCompatibility(
83
+ value: unknown,
84
+ ): RendererFederationCompatibility {
85
+ if (!record(value))
86
+ throw rendererFederationError('the consuming renderer tuple is absent.');
87
+ exactFields(value, ['profile', 'runtime', 'bootstrap'], 'compatibility');
88
+ const profile = assertProfile(value.profile);
89
+ return Object.freeze({
90
+ profile,
91
+ runtime: assertPackage(value.runtime, profile, 'runtime'),
92
+ bootstrap: assertPackage(value.bootstrap, profile, 'bootstrap'),
93
+ });
94
+ }
95
+
96
+ export function readRendererFederationContract(
97
+ value: unknown,
98
+ ): RendererFederationContract {
99
+ if (!record(value))
100
+ throw rendererFederationError('renderer publication metadata is absent.');
101
+ exactFields(
102
+ value,
103
+ [
104
+ 'schema',
105
+ 'schemaVersion',
106
+ 'profile',
107
+ 'runtime',
108
+ 'bootstrap',
109
+ 'identities',
110
+ ],
111
+ 'publication',
112
+ );
113
+ if (
114
+ value.schema !== RENDERER_FEDERATION_SCHEMA ||
115
+ value.schemaVersion !== RENDERER_FEDERATION_SCHEMA_VERSION
116
+ )
117
+ throw rendererFederationError('unsupported renderer publication schema.');
118
+ const compatibility = readRendererFederationCompatibility({
119
+ profile: value.profile,
120
+ runtime: value.runtime,
121
+ bootstrap: value.bootstrap,
122
+ });
123
+ if (!record(value.identities) || !Object.keys(value.identities).length)
124
+ throw rendererFederationError(
125
+ 'finalized application entry identities are absent.',
126
+ );
127
+ const identities: Record<string, RendererIdentity> = Object.create(null);
128
+ let appId: string | undefined;
129
+ for (const [entryName, valueIdentity] of Object.entries(value.identities)) {
130
+ const result = validateRendererIdentity(
131
+ valueIdentity,
132
+ `identities.${entryName}`,
133
+ );
134
+ if (!result.ok)
135
+ throw rendererFederationError(
136
+ formatBackendFederationValidationErrors(result.errors),
137
+ );
138
+ const identity = valueIdentity as RendererIdentity;
139
+ if (
140
+ identity.entryName !== entryName ||
141
+ identity.renderer !== compatibility.profile.renderer ||
142
+ identity.protocolVersion !== compatibility.profile.protocolVersion ||
143
+ (appId !== undefined && identity.appId !== appId)
144
+ )
145
+ throw rendererFederationError(
146
+ `incoherent publication identity for ${entryName}.`,
147
+ );
148
+ appId = identity.appId;
149
+ identities[entryName] = Object.freeze({ ...identity });
150
+ }
151
+ return Object.freeze({
152
+ schema: RENDERER_FEDERATION_SCHEMA,
153
+ schemaVersion: RENDERER_FEDERATION_SCHEMA_VERSION,
154
+ ...compatibility,
155
+ identities: Object.freeze(identities),
156
+ });
157
+ }
158
+
159
+ export function assertRendererFederationCompatibility(
160
+ expected: RendererFederationCompatibility,
161
+ actual: RendererFederationCompatibility,
162
+ ): void {
163
+ try {
164
+ assertRendererProfileCompatibility(expected.profile, actual.profile);
165
+ } catch (error) {
166
+ throw rendererFederationError(
167
+ error instanceof Error ? error.message : String(error),
168
+ );
169
+ }
170
+ for (const field of ['runtime', 'bootstrap'] as const)
171
+ for (const key of ['name', 'version'] as const)
172
+ if (expected[field][key] !== actual[field][key])
173
+ throw rendererFederationError(
174
+ `${field}.${key} must match the consuming renderer tuple.`,
175
+ );
176
+ }