@bleedingdev/modern-js-federation-runtime 3.9.0-ultramodern.9 → 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.
- package/dist/cjs/module-federation/consume-surface.js +6 -16
- package/dist/cjs/module-federation/distributed-ssr.js +6 -1
- package/dist/cjs/module-federation/index.js +13 -239
- package/dist/cjs/module-federation/manifest-recovery-runtime-plugin.js +1 -1
- package/dist/cjs/module-federation/native-browser-dev-client.js +320 -0
- package/dist/cjs/module-federation/native-browser-dev-refresh.js +226 -0
- package/dist/cjs/module-federation/native-browser-dev-runtime-plugin.js +233 -0
- package/dist/cjs/module-federation/native-dev-refresh-runtime-plugin.js +267 -0
- package/dist/cjs/module-federation/renderer-contract.js +159 -0
- package/dist/cjs/module-federation/renderer-runtime-plugin.js +268 -0
- package/dist/cjs/module-federation/telemetry.js +214 -0
- package/dist/esm/module-federation/consume-surface.mjs +3 -13
- package/dist/esm/module-federation/distributed-ssr.mjs +6 -1
- package/dist/esm/module-federation/index.mjs +1 -111
- package/dist/esm/module-federation/manifest-recovery-runtime-plugin.mjs +1 -1
- package/dist/esm/module-federation/native-browser-dev-client.mjs +276 -0
- package/dist/esm/module-federation/native-browser-dev-refresh.mjs +182 -0
- package/dist/esm/module-federation/native-browser-dev-runtime-plugin.mjs +183 -0
- package/dist/esm/module-federation/native-dev-refresh-runtime-plugin.mjs +229 -0
- package/dist/esm/module-federation/renderer-contract.mjs +103 -0
- package/dist/esm/module-federation/renderer-runtime-plugin.mjs +225 -0
- package/dist/esm/module-federation/telemetry.mjs +142 -0
- package/dist/esm-node/module-federation/consume-surface.mjs +3 -13
- package/dist/esm-node/module-federation/distributed-ssr.mjs +6 -1
- package/dist/esm-node/module-federation/index.mjs +1 -111
- package/dist/esm-node/module-federation/manifest-recovery-runtime-plugin.mjs +1 -1
- package/dist/esm-node/module-federation/native-browser-dev-client.mjs +277 -0
- package/dist/esm-node/module-federation/native-browser-dev-refresh.mjs +183 -0
- package/dist/esm-node/module-federation/native-browser-dev-runtime-plugin.mjs +184 -0
- package/dist/esm-node/module-federation/native-dev-refresh-runtime-plugin.mjs +230 -0
- package/dist/esm-node/module-federation/renderer-contract.mjs +104 -0
- package/dist/esm-node/module-federation/renderer-runtime-plugin.mjs +226 -0
- package/dist/esm-node/module-federation/telemetry.mjs +143 -0
- package/dist/types/module-federation/consume-surface.d.ts +5 -5
- package/dist/types/module-federation/distributed-ssr.d.ts +18 -1
- package/dist/types/module-federation/index.d.ts +1 -67
- package/dist/types/module-federation/native-browser-dev-client.d.ts +41 -0
- package/dist/types/module-federation/native-browser-dev-refresh.d.ts +86 -0
- package/dist/types/module-federation/native-browser-dev-runtime-plugin.d.ts +40 -0
- package/dist/types/module-federation/native-dev-refresh-runtime-plugin.d.ts +90 -0
- package/dist/types/module-federation/renderer-contract.d.ts +20 -0
- package/dist/types/module-federation/renderer-runtime-plugin.d.ts +98 -0
- package/dist/types/module-federation/telemetry.d.ts +71 -0
- package/package.json +102 -19
- package/src/module-federation/consume-surface.ts +18 -33
- package/src/module-federation/distributed-ssr.tsx +27 -2
- package/src/module-federation/index.ts +1 -267
- package/src/module-federation/manifest-recovery-runtime-plugin.ts +5 -1
- package/src/module-federation/native-browser-dev-client.ts +424 -0
- package/src/module-federation/native-browser-dev-refresh.ts +373 -0
- package/src/module-federation/native-browser-dev-runtime-plugin.ts +276 -0
- package/src/module-federation/native-dev-refresh-runtime-plugin.ts +445 -0
- package/src/module-federation/renderer-contract.ts +176 -0
- package/src/module-federation/renderer-runtime-plugin.ts +401 -0
- package/src/module-federation/telemetry.ts +297 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { assertRendererProfileCompatibility, formatBackendFederationValidationErrors, validateRendererIdentity, validateRendererProfile } from "@modern-js/backend-federation-contracts";
|
|
3
|
+
const RENDERER_FEDERATION_METADATA_KEY = 'ultramodernRenderer';
|
|
4
|
+
const RENDERER_FEDERATION_SCHEMA = 'ultramodern.renderer-federation';
|
|
5
|
+
const RENDERER_FEDERATION_SCHEMA_VERSION = 1;
|
|
6
|
+
const rendererFederationError = (message)=>new Error(`Renderer federation manifest contract: ${message}`);
|
|
7
|
+
const record = (value)=>null !== value && 'object' == typeof value && !Array.isArray(value);
|
|
8
|
+
const exactFields = (value, fields, label)=>{
|
|
9
|
+
const unknown = Object.keys(value).filter((key)=>!fields.includes(key));
|
|
10
|
+
const missing = fields.filter((key)=>!Object.hasOwn(value, key));
|
|
11
|
+
if (unknown.length || missing.length) throw rendererFederationError(`${label} fields differ (unknown=${unknown.join(',')}; missing=${missing.join(',')}).`);
|
|
12
|
+
};
|
|
13
|
+
const assertProfile = (value)=>{
|
|
14
|
+
const result = validateRendererProfile(value, 'rendererProfile');
|
|
15
|
+
if (!result.ok) throw rendererFederationError(formatBackendFederationValidationErrors(result.errors));
|
|
16
|
+
const profile = value;
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
...profile,
|
|
19
|
+
compiler: Object.freeze({
|
|
20
|
+
...profile.compiler
|
|
21
|
+
}),
|
|
22
|
+
hydration: Object.freeze({
|
|
23
|
+
...profile.hydration
|
|
24
|
+
}),
|
|
25
|
+
router: Object.freeze({
|
|
26
|
+
...profile.router
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const assertPackage = (value, profile, label)=>{
|
|
31
|
+
const result = validateRendererProfile({
|
|
32
|
+
...profile,
|
|
33
|
+
compiler: value
|
|
34
|
+
}, label);
|
|
35
|
+
if (!result.ok) throw rendererFederationError(formatBackendFederationValidationErrors(result.errors));
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
...value
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
function readRendererFederationCompatibility(value) {
|
|
41
|
+
if (!record(value)) throw rendererFederationError('the consuming renderer tuple is absent.');
|
|
42
|
+
exactFields(value, [
|
|
43
|
+
'profile',
|
|
44
|
+
'runtime',
|
|
45
|
+
'bootstrap'
|
|
46
|
+
], 'compatibility');
|
|
47
|
+
const profile = assertProfile(value.profile);
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
profile,
|
|
50
|
+
runtime: assertPackage(value.runtime, profile, 'runtime'),
|
|
51
|
+
bootstrap: assertPackage(value.bootstrap, profile, 'bootstrap')
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function readRendererFederationContract(value) {
|
|
55
|
+
if (!record(value)) throw rendererFederationError('renderer publication metadata is absent.');
|
|
56
|
+
exactFields(value, [
|
|
57
|
+
'schema',
|
|
58
|
+
'schemaVersion',
|
|
59
|
+
'profile',
|
|
60
|
+
'runtime',
|
|
61
|
+
'bootstrap',
|
|
62
|
+
'identities'
|
|
63
|
+
], 'publication');
|
|
64
|
+
if (value.schema !== RENDERER_FEDERATION_SCHEMA || value.schemaVersion !== RENDERER_FEDERATION_SCHEMA_VERSION) throw rendererFederationError('unsupported renderer publication schema.');
|
|
65
|
+
const compatibility = readRendererFederationCompatibility({
|
|
66
|
+
profile: value.profile,
|
|
67
|
+
runtime: value.runtime,
|
|
68
|
+
bootstrap: value.bootstrap
|
|
69
|
+
});
|
|
70
|
+
if (!record(value.identities) || !Object.keys(value.identities).length) throw rendererFederationError('finalized application entry identities are absent.');
|
|
71
|
+
const identities = Object.create(null);
|
|
72
|
+
let appId;
|
|
73
|
+
for (const [entryName, valueIdentity] of Object.entries(value.identities)){
|
|
74
|
+
const result = validateRendererIdentity(valueIdentity, `identities.${entryName}`);
|
|
75
|
+
if (!result.ok) throw rendererFederationError(formatBackendFederationValidationErrors(result.errors));
|
|
76
|
+
const identity = valueIdentity;
|
|
77
|
+
if (identity.entryName !== entryName || identity.renderer !== compatibility.profile.renderer || identity.protocolVersion !== compatibility.profile.protocolVersion || void 0 !== appId && identity.appId !== appId) throw rendererFederationError(`incoherent publication identity for ${entryName}.`);
|
|
78
|
+
appId = identity.appId;
|
|
79
|
+
identities[entryName] = Object.freeze({
|
|
80
|
+
...identity
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
schema: RENDERER_FEDERATION_SCHEMA,
|
|
85
|
+
schemaVersion: RENDERER_FEDERATION_SCHEMA_VERSION,
|
|
86
|
+
...compatibility,
|
|
87
|
+
identities: Object.freeze(identities)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function assertRendererFederationCompatibility(expected, actual) {
|
|
91
|
+
try {
|
|
92
|
+
assertRendererProfileCompatibility(expected.profile, actual.profile);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw rendererFederationError(error instanceof Error ? error.message : String(error));
|
|
95
|
+
}
|
|
96
|
+
for (const field of [
|
|
97
|
+
'runtime',
|
|
98
|
+
'bootstrap'
|
|
99
|
+
])for (const key of [
|
|
100
|
+
'name',
|
|
101
|
+
'version'
|
|
102
|
+
])if (expected[field][key] !== actual[field][key]) throw rendererFederationError(`${field}.${key} must match the consuming renderer tuple.`);
|
|
103
|
+
}
|
|
104
|
+
export { RENDERER_FEDERATION_METADATA_KEY, RENDERER_FEDERATION_SCHEMA, RENDERER_FEDERATION_SCHEMA_VERSION, assertRendererFederationCompatibility, readRendererFederationCompatibility, readRendererFederationContract, rendererFederationError };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { getResourceUrl, isBrowserEnvValue, isReactNativeEnv } from "@module-federation/sdk";
|
|
3
|
+
import { RENDERER_FEDERATION_METADATA_KEY, assertRendererFederationCompatibility, readRendererFederationCompatibility, readRendererFederationContract, rendererFederationError } from "./renderer-contract.mjs";
|
|
4
|
+
const ATTESTATION = Symbol.for('ultramodern.renderer-federation.attestation.v1');
|
|
5
|
+
const record = (value)=>null !== value && 'object' == typeof value && !Array.isArray(value);
|
|
6
|
+
const coordinates = (snapshot)=>JSON.stringify([
|
|
7
|
+
'globalName',
|
|
8
|
+
'buildVersion',
|
|
9
|
+
'remoteEntry',
|
|
10
|
+
'remoteEntryType',
|
|
11
|
+
'ssrRemoteEntry',
|
|
12
|
+
'ssrRemoteEntryType',
|
|
13
|
+
'publicPath',
|
|
14
|
+
'ssrPublicPath',
|
|
15
|
+
'getPublicPath'
|
|
16
|
+
].map((key)=>[
|
|
17
|
+
key,
|
|
18
|
+
snapshot[key]
|
|
19
|
+
]));
|
|
20
|
+
function attestation(snapshot) {
|
|
21
|
+
const descriptor = Object.getOwnPropertyDescriptor(snapshot, ATTESTATION);
|
|
22
|
+
const value = descriptor?.value;
|
|
23
|
+
if (!descriptor || descriptor.writable || descriptor.configurable || !record(value) || 'string' != typeof value.coordinates || value.coordinates !== coordinates(snapshot)) throw rendererFederationError('cached remote snapshot is missing or has changed its attestation.');
|
|
24
|
+
return {
|
|
25
|
+
contract: readRendererFederationContract(value.contract),
|
|
26
|
+
coordinates: value.coordinates
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const assertManifestRemote = (remote)=>{
|
|
30
|
+
if (!record(remote)) throw rendererFederationError('remote registration is absent.');
|
|
31
|
+
if ('entry' in remote && ('string' != typeof remote.entry || !remote.entry.includes('.json'))) throw rendererFederationError('renderer components require a native JSON manifest remote.');
|
|
32
|
+
};
|
|
33
|
+
function sameFactory(left, right) {
|
|
34
|
+
if (left === right) return true;
|
|
35
|
+
if ('function' == typeof left.lib && 'function' == typeof right.lib) return left.lib === right.lib;
|
|
36
|
+
return 'function' == typeof left.get && left.get === right.get;
|
|
37
|
+
}
|
|
38
|
+
const containerCoordinates = (info)=>JSON.stringify([
|
|
39
|
+
'name',
|
|
40
|
+
'entry',
|
|
41
|
+
'type',
|
|
42
|
+
'entryGlobalName',
|
|
43
|
+
'buildVersion'
|
|
44
|
+
].map((key)=>[
|
|
45
|
+
key,
|
|
46
|
+
info[key]
|
|
47
|
+
]));
|
|
48
|
+
const entryCoordinates = (info)=>JSON.stringify([
|
|
49
|
+
'name',
|
|
50
|
+
'entry',
|
|
51
|
+
'type',
|
|
52
|
+
'entryGlobalName'
|
|
53
|
+
].map((key)=>[
|
|
54
|
+
key,
|
|
55
|
+
info[key]
|
|
56
|
+
]));
|
|
57
|
+
function rendererShareVersions(expected) {
|
|
58
|
+
const { profile, runtime, bootstrap } = expected;
|
|
59
|
+
if ('react' === profile.renderer) return new Map([
|
|
60
|
+
[
|
|
61
|
+
'react',
|
|
62
|
+
runtime.version
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'react-dom',
|
|
66
|
+
profile.hydration.version
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'react-dom/client',
|
|
70
|
+
profile.hydration.version
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'@modern-js/runtime',
|
|
74
|
+
bootstrap.version
|
|
75
|
+
]
|
|
76
|
+
]);
|
|
77
|
+
const versions = new Map();
|
|
78
|
+
for (const [name, version] of [
|
|
79
|
+
[
|
|
80
|
+
runtime.name,
|
|
81
|
+
runtime.version
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
profile.hydration.name,
|
|
85
|
+
profile.hydration.version
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
bootstrap.name,
|
|
89
|
+
bootstrap.version
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
profile.router.name,
|
|
93
|
+
profile.router.version
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
profile.router.coreName,
|
|
97
|
+
profile.router.coreVersion
|
|
98
|
+
]
|
|
99
|
+
]){
|
|
100
|
+
const prior = versions.get(name);
|
|
101
|
+
if (void 0 !== prior && prior !== version) throw rendererFederationError(`renderer tuple names ${name} with conflicting versions.`);
|
|
102
|
+
versions.set(name, version);
|
|
103
|
+
}
|
|
104
|
+
return versions;
|
|
105
|
+
}
|
|
106
|
+
function createRendererFederationRuntimePlugin(options) {
|
|
107
|
+
const expected = readRendererFederationCompatibility(options);
|
|
108
|
+
const selected = new Map();
|
|
109
|
+
const approvedRemoteInfos = new WeakMap();
|
|
110
|
+
const observedEntries = new WeakMap();
|
|
111
|
+
const acceptedEntries = new Set();
|
|
112
|
+
const versions = rendererShareVersions(expected);
|
|
113
|
+
const sharedVersion = (name)=>versions.get(name) ?? ('react' === expected.profile.renderer ? void 0 : [
|
|
114
|
+
...versions
|
|
115
|
+
].find(([owner])=>name.startsWith(`${owner}/`))?.[1]);
|
|
116
|
+
return {
|
|
117
|
+
name: 'ultramodern-renderer-federation-contract',
|
|
118
|
+
beforeRegisterRemote (args) {
|
|
119
|
+
assertManifestRemote(args.remote);
|
|
120
|
+
return args;
|
|
121
|
+
},
|
|
122
|
+
async afterMatchRemote (args) {
|
|
123
|
+
if (!args.error) assertManifestRemote(args.remote);
|
|
124
|
+
return args;
|
|
125
|
+
},
|
|
126
|
+
async loadRemoteSnapshot (args) {
|
|
127
|
+
const snapshot = args.remoteSnapshot;
|
|
128
|
+
if ('manifest' === args.from) {
|
|
129
|
+
const manifest = args.manifestJson;
|
|
130
|
+
const metadata = record(manifest) && record(manifest.metaData) ? manifest.metaData[RENDERER_FEDERATION_METADATA_KEY] : void 0;
|
|
131
|
+
const contract = readRendererFederationContract(metadata);
|
|
132
|
+
if (!record(snapshot) || 'string' != typeof snapshot.remoteEntry) throw rendererFederationError('native remote entry coordinates are absent.');
|
|
133
|
+
const existing = Object.getOwnPropertyDescriptor(snapshot, ATTESTATION);
|
|
134
|
+
if (existing) {
|
|
135
|
+
const prior = attestation(snapshot);
|
|
136
|
+
if (JSON.stringify(prior.contract) !== JSON.stringify(contract)) throw rendererFederationError('remote snapshot has conflicting publication ownership.');
|
|
137
|
+
} else Object.defineProperty(snapshot, ATTESTATION, {
|
|
138
|
+
value: Object.freeze({
|
|
139
|
+
contract,
|
|
140
|
+
coordinates: coordinates(snapshot)
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
} else assertRendererFederationCompatibility(expected, attestation(snapshot).contract);
|
|
144
|
+
return args;
|
|
145
|
+
},
|
|
146
|
+
async afterLoadSnapshot (args) {
|
|
147
|
+
assertRendererFederationCompatibility(expected, attestation(args.remoteSnapshot).contract);
|
|
148
|
+
const snapshot = args.remoteSnapshot;
|
|
149
|
+
const browser = isBrowserEnvValue || isReactNativeEnv() || !('ssrRemoteEntry' in snapshot);
|
|
150
|
+
const entry = browser ? snapshot.remoteEntry : snapshot.ssrRemoteEntry;
|
|
151
|
+
if ('string' != typeof entry || !entry) throw rendererFederationError('native projected remote entry is absent.');
|
|
152
|
+
let entryUrl = getResourceUrl(snapshot, entry);
|
|
153
|
+
if (!isBrowserEnvValue && !entryUrl.startsWith('http')) entryUrl = `https:${entryUrl}`;
|
|
154
|
+
acceptedEntries.add(entryCoordinates({
|
|
155
|
+
name: args.moduleInfo?.name ?? snapshot.globalName,
|
|
156
|
+
entry: entryUrl,
|
|
157
|
+
type: browser ? snapshot.remoteEntryType : snapshot.ssrRemoteEntryType || 'global',
|
|
158
|
+
entryGlobalName: snapshot.globalName
|
|
159
|
+
}));
|
|
160
|
+
return args;
|
|
161
|
+
},
|
|
162
|
+
async afterResolve (args) {
|
|
163
|
+
if (!args.remoteSnapshot) throw rendererFederationError('native remote resolution has no attested snapshot.');
|
|
164
|
+
assertRendererFederationCompatibility(expected, attestation(args.remoteSnapshot).contract);
|
|
165
|
+
const cached = args.origin.moduleCache.get(args.remote.name);
|
|
166
|
+
if (cached && (approvedRemoteInfos.get(cached.remoteInfo) !== containerCoordinates(cached.remoteInfo) || containerCoordinates(cached.remoteInfo) !== containerCoordinates(args.remoteInfo))) throw rendererFederationError('cached remote container has unattested or conflicting ownership.');
|
|
167
|
+
const priorCoordinates = approvedRemoteInfos.get(args.remoteInfo);
|
|
168
|
+
if (void 0 !== priorCoordinates && priorCoordinates !== containerCoordinates(args.remoteInfo)) throw rendererFederationError('native remote container coordinates changed after attestation.');
|
|
169
|
+
approvedRemoteInfos.set(args.remoteInfo, containerCoordinates(args.remoteInfo));
|
|
170
|
+
return args;
|
|
171
|
+
},
|
|
172
|
+
loadEntry (args) {
|
|
173
|
+
if (!acceptedEntries.has(entryCoordinates(args.remoteInfo))) throw rendererFederationError('remote entry coordinates differ from the attested native snapshot.');
|
|
174
|
+
},
|
|
175
|
+
async afterLoadEntry (args) {
|
|
176
|
+
if (args.error && !args.recovered) return args;
|
|
177
|
+
const binding = containerCoordinates(args.remoteInfo);
|
|
178
|
+
const entryBinding = entryCoordinates(args.remoteInfo);
|
|
179
|
+
if (args.cached && approvedRemoteInfos.get(args.remoteInfo) !== binding) throw rendererFederationError('remote entry has no attested native container resolution.');
|
|
180
|
+
if (!acceptedEntries.has(entryBinding)) throw rendererFederationError('remote entry coordinates differ from the attested native snapshot.');
|
|
181
|
+
const entry = args.remoteEntryExports;
|
|
182
|
+
if (('object' != typeof entry || null === entry) && 'function' != typeof entry) throw rendererFederationError('native container exports are absent.');
|
|
183
|
+
if (args.cached && observedEntries.get(entry) !== entryBinding) throw rendererFederationError('cached native container exports have unattested ownership.');
|
|
184
|
+
approvedRemoteInfos.set(args.remoteInfo, binding);
|
|
185
|
+
observedEntries.set(entry, entryBinding);
|
|
186
|
+
return args;
|
|
187
|
+
},
|
|
188
|
+
resolveShare (args) {
|
|
189
|
+
const version = sharedVersion(args.pkgName);
|
|
190
|
+
if (void 0 === version) return args;
|
|
191
|
+
const resolve = args.resolver;
|
|
192
|
+
return {
|
|
193
|
+
...args,
|
|
194
|
+
resolver () {
|
|
195
|
+
const result = resolve();
|
|
196
|
+
if (!result) return result;
|
|
197
|
+
const shared = result.useTreesShaking && record(result.shared.treeShaking) ? result.shared.treeShaking : result.shared;
|
|
198
|
+
if (result.shared.version !== version) throw rendererFederationError(`shared ${args.pkgName} version must be ${version}.`);
|
|
199
|
+
const prior = selected.get(args.pkgName);
|
|
200
|
+
if (prior && !sameFactory(prior, shared)) throw rendererFederationError(`shared ${args.pkgName} has conflicting runtime ownership.`);
|
|
201
|
+
for (const scope of Object.values(args.shareScopeMap))for (const candidate of Object.values(scope[args.pkgName] ?? {})){
|
|
202
|
+
const active = record(candidate.treeShaking) && candidate.treeShaking.loaded ? candidate.treeShaking : candidate;
|
|
203
|
+
if ((active.loaded || active.lib) && !sameFactory(active, shared)) throw rendererFederationError(`shared ${args.pkgName} has duplicate loaded runtime identities.`);
|
|
204
|
+
}
|
|
205
|
+
selected.set(args.pkgName, Object.freeze({
|
|
206
|
+
lib: shared.lib,
|
|
207
|
+
get: shared.get
|
|
208
|
+
}));
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
afterLoadShare (args) {
|
|
214
|
+
if (void 0 !== sharedVersion(args.pkgName) && args.selectedShared) {
|
|
215
|
+
const shared = record(args.selectedShared.treeShaking) && args.selectedShared.treeShaking.loaded ? args.selectedShared.treeShaking : args.selectedShared;
|
|
216
|
+
selected.set(args.pkgName, Object.freeze({
|
|
217
|
+
lib: shared.lib,
|
|
218
|
+
get: shared.get
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const renderer_runtime_plugin = createRendererFederationRuntimePlugin;
|
|
225
|
+
export default renderer_runtime_plugin;
|
|
226
|
+
export { createRendererFederationRuntimePlugin, rendererShareVersions };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT = 'modernjs:mf-runtime-fallback';
|
|
3
|
+
const MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT = 'modernjs:mf-runtime-recovery';
|
|
4
|
+
const DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT = '/_modern/contract-gates/runtime-fallback';
|
|
5
|
+
const DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER = 'x-modernjs-runtime-signal-token';
|
|
6
|
+
class ModuleFederationRemoteLoadTimeoutError extends Error {
|
|
7
|
+
constructor(remote, timeoutMs){
|
|
8
|
+
super(`Loading remote "${remote}" timed out after ${timeoutMs}ms`);
|
|
9
|
+
this.name = 'ModuleFederationRemoteLoadTimeoutError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
class ModuleFederationRemoteLoadError extends Error {
|
|
13
|
+
constructor(remote, attempts, causeError){
|
|
14
|
+
super(`Unable to load remote "${remote}" after ${attempts} attempt${attempts > 1 ? 's' : ''}: ${causeError.message}`);
|
|
15
|
+
this.name = 'ModuleFederationRemoteLoadError';
|
|
16
|
+
this.remote = remote;
|
|
17
|
+
this.attempts = attempts;
|
|
18
|
+
this.causeError = causeError;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class ModuleFederationRemoteComponentContractError extends Error {
|
|
22
|
+
constructor(remote, exportName){
|
|
23
|
+
super(`Remote "${remote}" export "${exportName}" is not a valid React component`);
|
|
24
|
+
this.name = 'ModuleFederationRemoteComponentContractError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function toError(error) {
|
|
28
|
+
if (error instanceof Error) return error;
|
|
29
|
+
return new Error('string' == typeof error ? error : 'Unknown remote load error');
|
|
30
|
+
}
|
|
31
|
+
function classifyModuleFederationFallback(error) {
|
|
32
|
+
const normalizedError = toError(error);
|
|
33
|
+
if (normalizedError instanceof ModuleFederationRemoteLoadError) return classifyModuleFederationFallback(normalizedError.causeError);
|
|
34
|
+
if (normalizedError instanceof ModuleFederationRemoteLoadTimeoutError) return 'timeout';
|
|
35
|
+
if (normalizedError instanceof ModuleFederationRemoteComponentContractError) return 'contract';
|
|
36
|
+
const message = normalizedError.message;
|
|
37
|
+
if (/version|requiredVersion|singleton|share scope|shared module/i.test(message)) return 'version-skew';
|
|
38
|
+
if (/network|fetch|script|timeout|chunk|loading/i.test(message)) return 'network';
|
|
39
|
+
return 'remote-unavailable';
|
|
40
|
+
}
|
|
41
|
+
function createModuleFederationFallbackTelemetry(input) {
|
|
42
|
+
const error = void 0 !== input.error ? toError(input.error) : void 0;
|
|
43
|
+
const status = input.status ?? 'degraded';
|
|
44
|
+
const eventName = input.eventName ?? ('recovered' === status ? MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT : MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT);
|
|
45
|
+
const metadata = {
|
|
46
|
+
...input.metadata ?? {},
|
|
47
|
+
classification: input.classification,
|
|
48
|
+
remote: input.remote,
|
|
49
|
+
status
|
|
50
|
+
};
|
|
51
|
+
if (void 0 !== input.exportName) metadata.exportName = input.exportName;
|
|
52
|
+
if (void 0 !== input.runtimeDigest) metadata.runtimeDigest = input.runtimeDigest;
|
|
53
|
+
if (void 0 !== error) {
|
|
54
|
+
metadata.errorName = error.name;
|
|
55
|
+
metadata.errorMessage = error.message;
|
|
56
|
+
}
|
|
57
|
+
const payload = {
|
|
58
|
+
appName: input.appName,
|
|
59
|
+
eventName,
|
|
60
|
+
metadata,
|
|
61
|
+
phase: input.phase,
|
|
62
|
+
reason: input.classification,
|
|
63
|
+
schemaVersion: 1
|
|
64
|
+
};
|
|
65
|
+
if (void 0 !== input.entry) payload.entry = input.entry;
|
|
66
|
+
if (void 0 !== input.runtimeDigest) payload.runtimeDigest = input.runtimeDigest;
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
function toModuleFederationFallbackAttributes(payload) {
|
|
70
|
+
return {
|
|
71
|
+
'data-mf-fallback-app': payload.appName,
|
|
72
|
+
'data-mf-fallback-classification': payload.reason,
|
|
73
|
+
'data-mf-fallback-phase': payload.phase,
|
|
74
|
+
'data-mf-fallback-remote': String(payload.metadata.remote),
|
|
75
|
+
'data-mf-fallback-status': String(payload.metadata.status),
|
|
76
|
+
'data-mf-telemetry-event': payload.eventName
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function emitModuleFederationFallbackTelemetry(input, options = {}) {
|
|
80
|
+
return emitModuleFederationFallbackPayload(createModuleFederationFallbackTelemetry(input), options);
|
|
81
|
+
}
|
|
82
|
+
function emitModuleFederationFallbackPayload(payload, options = {}) {
|
|
83
|
+
const delivery = deliver(payload, options);
|
|
84
|
+
try {
|
|
85
|
+
options.observe?.(payload, delivery);
|
|
86
|
+
} catch {}
|
|
87
|
+
return delivery;
|
|
88
|
+
}
|
|
89
|
+
async function deliver(payload, options) {
|
|
90
|
+
let dispatched = false;
|
|
91
|
+
let timer;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
try {
|
|
94
|
+
if ("u" > typeof window && 'function' == typeof window.dispatchEvent && "u" > typeof CustomEvent) {
|
|
95
|
+
window.dispatchEvent(new CustomEvent(payload.eventName, {
|
|
96
|
+
detail: payload
|
|
97
|
+
}));
|
|
98
|
+
dispatched = true;
|
|
99
|
+
}
|
|
100
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
101
|
+
if (!(true === options.postSignal || options.endpoint) || 'function' != typeof fetchImpl) return {
|
|
102
|
+
dispatched,
|
|
103
|
+
posted: false
|
|
104
|
+
};
|
|
105
|
+
const headers = new Headers({
|
|
106
|
+
'content-type': 'application/json'
|
|
107
|
+
});
|
|
108
|
+
if (options.authToken) headers.set(options.authHeaderName ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER, options.authToken);
|
|
109
|
+
const timeoutMs = Number.isSafeInteger(options.timeoutMs) && (options.timeoutMs ?? 0) > 0 ? options.timeoutMs : 1000;
|
|
110
|
+
const expired = new Promise((resolve)=>{
|
|
111
|
+
timer = setTimeout(()=>{
|
|
112
|
+
controller.abort();
|
|
113
|
+
resolve(void 0);
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
});
|
|
116
|
+
const response = await Promise.race([
|
|
117
|
+
fetchImpl(options.endpoint ?? DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT, {
|
|
118
|
+
body: JSON.stringify(payload),
|
|
119
|
+
headers,
|
|
120
|
+
method: 'POST',
|
|
121
|
+
keepalive: true,
|
|
122
|
+
signal: controller.signal
|
|
123
|
+
}),
|
|
124
|
+
expired
|
|
125
|
+
]);
|
|
126
|
+
return response ? {
|
|
127
|
+
dispatched,
|
|
128
|
+
posted: true,
|
|
129
|
+
postStatus: response.status
|
|
130
|
+
} : {
|
|
131
|
+
dispatched,
|
|
132
|
+
posted: false
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return {
|
|
136
|
+
dispatched,
|
|
137
|
+
posted: false
|
|
138
|
+
};
|
|
139
|
+
} finally{
|
|
140
|
+
if (void 0 !== timer) clearTimeout(timer);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export { DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER, DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT, MODULE_FEDERATION_FALLBACK_SIGNAL_EVENT, MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT, ModuleFederationRemoteComponentContractError, ModuleFederationRemoteLoadError, ModuleFederationRemoteLoadTimeoutError, classifyModuleFederationFallback, createModuleFederationFallbackTelemetry, emitModuleFederationFallbackPayload, emitModuleFederationFallbackTelemetry, toModuleFederationFallbackAttributes };
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* consumption during a rollout window should be driven by expand/contract, not
|
|
23
23
|
* by silent degrade.
|
|
24
24
|
*/
|
|
25
|
-
import { type ModuleFederationFallbackClassification, type ModuleFederationFallbackTelemetryEmitOptions, type ModuleFederationFallbackTelemetryPayload } from './index.js';
|
|
26
25
|
import { type DiscoveryError, type EnvironmentId, type ParsedSurfaceRef, type ResolvedDeliveryUnit, type SurfaceResolutionProvider } from './surface-resolution-types.js';
|
|
26
|
+
import { type ModuleFederationFallbackClassification, type ModuleFederationFallbackTelemetryEmitOptions, type ModuleFederationFallbackTelemetryPayload } from './telemetry.js';
|
|
27
27
|
/** Where in the consumption lifecycle a failure was raised. */
|
|
28
28
|
export type SurfaceConsumptionPhase = 'discovery' | 'load' | 'mount';
|
|
29
29
|
/**
|
|
@@ -76,7 +76,7 @@ export type SurfaceConsumerOptions<T> = {
|
|
|
76
76
|
* omitted.
|
|
77
77
|
*
|
|
78
78
|
* - `'noncritical'`: the degraded handler's result is returned and the
|
|
79
|
-
* failure is swallowed —
|
|
79
|
+
* failure is swallowed — a failed degraded handler resolves undefined.
|
|
80
80
|
* - `'critical'`: the degraded handler still runs (telemetry + fallback-UI
|
|
81
81
|
* obligations hold), but the returned promise then REJECTS with the typed
|
|
82
82
|
* failure so callers / rollout machinery observe it. Prefer expand/contract
|
|
@@ -91,14 +91,14 @@ export type SurfaceConsumerOptions<T> = {
|
|
|
91
91
|
* load failure the required degraded handler runs (telemetry + fallback UI).
|
|
92
92
|
*
|
|
93
93
|
* For a `noncritical` consumption (see {@link SurfaceConsumerOptions.classification})
|
|
94
|
-
* this resolves to the degraded value
|
|
94
|
+
* this resolves to the degraded value (or undefined if the handler fails), so a failing
|
|
95
95
|
* consumption is isolated from any sibling. For a `critical` consumption (the
|
|
96
96
|
* default) it rejects with the typed failure AFTER the degraded handler has
|
|
97
97
|
* run, so callers / rollout machinery observe the failure.
|
|
98
98
|
*/
|
|
99
|
-
export declare function consumeSurface<T>(options: SurfaceConsumerOptions<T>): Promise<T>;
|
|
99
|
+
export declare function consumeSurface<T>(options: SurfaceConsumerOptions<T>): Promise<T | undefined>;
|
|
100
100
|
/**
|
|
101
101
|
* Build a reusable consumer bound to a provider/env/app, still requiring a
|
|
102
102
|
* degraded handler per call. Per-call options override the bound base.
|
|
103
103
|
*/
|
|
104
|
-
export declare function createSurfaceConsumer(base: Pick<SurfaceConsumerOptions<unknown>, 'provider' | 'env' | 'appName' | 'telemetry'>): <T>(options: Omit<SurfaceConsumerOptions<T>, keyof typeof base> & Partial<Pick<SurfaceConsumerOptions<T>, keyof typeof base>>) => Promise<T>;
|
|
104
|
+
export declare function createSurfaceConsumer(base: Pick<SurfaceConsumerOptions<unknown>, 'provider' | 'env' | 'appName' | 'telemetry'>): <T>(options: Omit<SurfaceConsumerOptions<T>, keyof typeof base> & Partial<Pick<SurfaceConsumerOptions<T>, keyof typeof base>>) => Promise<T | undefined>;
|
|
@@ -30,8 +30,25 @@ export type DistributedSsrBoundaryProps<Props extends object = Record<string, un
|
|
|
30
30
|
fragmentProps?: Props;
|
|
31
31
|
remote: string;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The Module Federation `createLazyComponent` options the distributed boundary
|
|
35
|
+
* owns. `injectLink` is always false: the boundary owns remote CSS (the
|
|
36
|
+
* fragment CSS inventory in workerd, the MF manifest in the browser), so the
|
|
37
|
+
* bridge must not render a second `<link>` in SSR HTML.
|
|
38
|
+
*/
|
|
39
|
+
export type DistributedSsrLazyComponentOptions = {
|
|
40
|
+
export: 'default';
|
|
41
|
+
fallback: ReactNode;
|
|
42
|
+
injectLink: false;
|
|
43
|
+
loading: null;
|
|
44
|
+
};
|
|
33
45
|
export type CreateDistributedSsrComponentOptions<Props extends object> = {
|
|
34
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Creates the native remote on first render. Spread `options` into
|
|
48
|
+
* Module Federation's `createLazyComponent` and add the instance and
|
|
49
|
+
* loader.
|
|
50
|
+
*/
|
|
51
|
+
createComponent: (options: DistributedSsrLazyComponentOptions) => ComponentType<Props>;
|
|
35
52
|
expose: string;
|
|
36
53
|
fallback: ReactNode;
|
|
37
54
|
remote: string;
|
|
@@ -1,71 +1,5 @@
|
|
|
1
1
|
export * from './consume-surface.js';
|
|
2
|
-
export * from './distributed-ssr.js';
|
|
3
|
-
export { DISTRIBUTED_SSR_FRAGMENT_REQUEST_LOCALS_KEY, useDistributedSsrFragmentProps, } from './distributed-ssr.js';
|
|
4
2
|
export * from './last-known-good.js';
|
|
5
3
|
export * from './manifest-recovery-runtime-plugin.js';
|
|
6
4
|
export * from './surface-resolution-types.js';
|
|
7
|
-
export
|
|
8
|
-
export declare const MODULE_FEDERATION_RECOVERY_SIGNAL_EVENT = "modernjs:mf-runtime-recovery";
|
|
9
|
-
export declare const DEFAULT_RUNTIME_FALLBACK_SIGNAL_ENDPOINT = "/_modern/contract-gates/runtime-fallback";
|
|
10
|
-
export declare const DEFAULT_RUNTIME_FALLBACK_SIGNAL_AUTH_HEADER = "x-modernjs-runtime-signal-token";
|
|
11
|
-
export type ModuleFederationFallbackClassification = 'timeout' | 'network' | 'contract' | 'version-skew' | 'remote-unavailable';
|
|
12
|
-
export type ModuleFederationFallbackStatus = 'degraded' | 'failed' | 'recovered';
|
|
13
|
-
export type ModuleFederationFallbackTelemetryInput = {
|
|
14
|
-
appName: string;
|
|
15
|
-
classification: ModuleFederationFallbackClassification;
|
|
16
|
-
entry?: string;
|
|
17
|
-
error?: unknown;
|
|
18
|
-
eventName?: string;
|
|
19
|
-
exportName?: string;
|
|
20
|
-
metadata?: Record<string, unknown>;
|
|
21
|
-
phase: 'discovery' | 'load' | 'mount' | 'hydrate' | 'recover';
|
|
22
|
-
remote: string;
|
|
23
|
-
runtimeDigest?: string;
|
|
24
|
-
status?: ModuleFederationFallbackStatus;
|
|
25
|
-
};
|
|
26
|
-
export type ModuleFederationFallbackTelemetryPayload = {
|
|
27
|
-
appName: string;
|
|
28
|
-
entry?: string;
|
|
29
|
-
eventName: string;
|
|
30
|
-
phase: ModuleFederationFallbackTelemetryInput['phase'];
|
|
31
|
-
reason: ModuleFederationFallbackClassification;
|
|
32
|
-
runtimeDigest?: string;
|
|
33
|
-
schemaVersion: 1;
|
|
34
|
-
metadata: {
|
|
35
|
-
classification: ModuleFederationFallbackClassification;
|
|
36
|
-
errorMessage?: string;
|
|
37
|
-
errorName?: string;
|
|
38
|
-
exportName?: string;
|
|
39
|
-
remote: string;
|
|
40
|
-
runtimeDigest?: string;
|
|
41
|
-
status: ModuleFederationFallbackStatus;
|
|
42
|
-
} & Record<string, unknown>;
|
|
43
|
-
};
|
|
44
|
-
export type ModuleFederationFallbackTelemetryEmitOptions = {
|
|
45
|
-
authHeaderName?: string;
|
|
46
|
-
authToken?: string;
|
|
47
|
-
endpoint?: string;
|
|
48
|
-
fetchImpl?: typeof fetch;
|
|
49
|
-
postSignal?: boolean;
|
|
50
|
-
};
|
|
51
|
-
export type ModuleFederationFallbackTelemetryEmitResult = {
|
|
52
|
-
dispatched: boolean;
|
|
53
|
-
posted: boolean;
|
|
54
|
-
postStatus?: number;
|
|
55
|
-
};
|
|
56
|
-
export declare class ModuleFederationRemoteLoadTimeoutError extends Error {
|
|
57
|
-
constructor(remote: string, timeoutMs: number);
|
|
58
|
-
}
|
|
59
|
-
export declare class ModuleFederationRemoteLoadError extends Error {
|
|
60
|
-
readonly remote: string;
|
|
61
|
-
readonly attempts: number;
|
|
62
|
-
readonly causeError: Error;
|
|
63
|
-
constructor(remote: string, attempts: number, causeError: Error);
|
|
64
|
-
}
|
|
65
|
-
export declare class ModuleFederationRemoteComponentContractError extends Error {
|
|
66
|
-
constructor(remote: string, exportName: string);
|
|
67
|
-
}
|
|
68
|
-
export declare function classifyModuleFederationFallback(error: unknown): ModuleFederationFallbackClassification;
|
|
69
|
-
export declare function createModuleFederationFallbackTelemetry(input: ModuleFederationFallbackTelemetryInput): ModuleFederationFallbackTelemetryPayload;
|
|
70
|
-
export declare function toModuleFederationFallbackAttributes(payload: ModuleFederationFallbackTelemetryPayload): Record<string, string>;
|
|
71
|
-
export declare function emitModuleFederationFallbackTelemetry(input: ModuleFederationFallbackTelemetryInput, options?: ModuleFederationFallbackTelemetryEmitOptions): Promise<ModuleFederationFallbackTelemetryEmitResult>;
|
|
5
|
+
export * from './telemetry.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface HotRuntime {
|
|
2
|
+
status(): string;
|
|
3
|
+
check(apply: true): Promise<readonly unknown[] | null>;
|
|
4
|
+
}
|
|
5
|
+
interface Overlay {
|
|
6
|
+
create(title: string, body: string): void;
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
9
|
+
/** Rearm only a loaded standby runtime before its exposed factory executes. */
|
|
10
|
+
export declare function prepareNativeBrowserDevClient(remoteName: string, manifest: string): () => void;
|
|
11
|
+
/** Release attempt-owned activity without evicting any working shared owner. */
|
|
12
|
+
export declare function releaseNativeBrowserDevClient(remoteName: string, manifest: string): void;
|
|
13
|
+
/** Rsbuild's ordinary socket/check protocol, with an explicit native handoff. */
|
|
14
|
+
export declare function startNativeBrowserDevClient(options: {
|
|
15
|
+
readonly remoteName: string;
|
|
16
|
+
readonly buildId: string;
|
|
17
|
+
readonly revisionHeader: string;
|
|
18
|
+
readonly token: string;
|
|
19
|
+
readonly config: {
|
|
20
|
+
readonly host?: string;
|
|
21
|
+
readonly port?: number | string;
|
|
22
|
+
readonly protocol?: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly reconnect: number;
|
|
25
|
+
readonly overlay?: boolean | {
|
|
26
|
+
readonly errors?: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
readonly serverHost: string;
|
|
30
|
+
readonly serverPort: number;
|
|
31
|
+
readonly serverBase: string;
|
|
32
|
+
readonly liveReload: boolean;
|
|
33
|
+
readonly browserLogs: boolean;
|
|
34
|
+
readonly logLevel: string;
|
|
35
|
+
readonly resolveWebSocketUrl?: (url: string) => string;
|
|
36
|
+
readonly hot: HotRuntime | undefined;
|
|
37
|
+
readonly hash: () => string;
|
|
38
|
+
readonly overlay: () => Overlay | undefined;
|
|
39
|
+
readonly installCustomListeners: (listeners: Map<string, ((value: unknown) => void)[]>) => () => void;
|
|
40
|
+
}): () => void;
|
|
41
|
+
export {};
|