@bleedingdev/modern-js-federation-runtime 3.9.3-ultramodern.8 → 3.9.3-ultramodern.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/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-runtime-plugin.js +5 -2
- 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-runtime-plugin.mjs +5 -2
- 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-runtime-plugin.mjs +5 -2
- 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/package.json +54 -7
- 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-runtime-plugin.ts +7 -2
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { isNativeBrowserDevCommitError } from './native-browser-dev-refresh';
|
|
2
|
+
import {
|
|
3
|
+
hasNativeBrowserDevPublisher,
|
|
4
|
+
type NativeBrowserDevPublication,
|
|
5
|
+
readNativeBrowserDevPublication,
|
|
6
|
+
replaceNativeBrowserDevRemote,
|
|
7
|
+
} from './native-browser-dev-runtime-plugin';
|
|
8
|
+
|
|
9
|
+
interface HotRuntime {
|
|
10
|
+
status(): string;
|
|
11
|
+
check(apply: true): Promise<readonly unknown[] | null>;
|
|
12
|
+
}
|
|
13
|
+
interface Overlay {
|
|
14
|
+
create(title: string, body: string): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
interface Client {
|
|
18
|
+
arm(): void;
|
|
19
|
+
lease(): () => void;
|
|
20
|
+
releaseStandby(): void;
|
|
21
|
+
start(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|
|
24
|
+
interface Clients {
|
|
25
|
+
active?: Client;
|
|
26
|
+
failure?: { readonly runtimeHash: string; readonly error: unknown };
|
|
27
|
+
readonly candidates: Map<string, WeakRef<Client>>;
|
|
28
|
+
}
|
|
29
|
+
const CLIENTS = Symbol.for('ultramodern.federation.browser-dev-clients.v1');
|
|
30
|
+
const clients = (): Map<string, Clients> => {
|
|
31
|
+
const root = globalThis as typeof globalThis & {
|
|
32
|
+
[CLIENTS]?: Map<string, Clients>;
|
|
33
|
+
};
|
|
34
|
+
if (!root[CLIENTS])
|
|
35
|
+
Object.defineProperty(root, CLIENTS, { value: new Map() });
|
|
36
|
+
return root[CLIENTS]!;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Called only after the native synchronous factory transaction has accepted. */
|
|
40
|
+
function commitClient(
|
|
41
|
+
remoteName: string,
|
|
42
|
+
graph: NativeBrowserDevPublication,
|
|
43
|
+
): void {
|
|
44
|
+
const state = clients().get(remoteName);
|
|
45
|
+
const next = state?.candidates.get(graph.runtimeHash)?.deref();
|
|
46
|
+
if (!state || !next)
|
|
47
|
+
throw new Error('Accepted native graph has no matching fresh HMR runtime.');
|
|
48
|
+
if (state.active === next) return;
|
|
49
|
+
state.active?.stop();
|
|
50
|
+
state.active = next;
|
|
51
|
+
next.start();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Rearm only a loaded standby runtime before its exposed factory executes. */
|
|
55
|
+
export function prepareNativeBrowserDevClient(
|
|
56
|
+
remoteName: string,
|
|
57
|
+
manifest: string,
|
|
58
|
+
): () => void {
|
|
59
|
+
const hash = /\/__ultramodern_mf__\/([a-f0-9]+)\//u.exec(
|
|
60
|
+
new URL(manifest).pathname,
|
|
61
|
+
)?.[1];
|
|
62
|
+
const candidate =
|
|
63
|
+
hash && clients().get(remoteName)?.candidates.get(hash)?.deref();
|
|
64
|
+
if (!candidate)
|
|
65
|
+
throw new Error('Native graph has no loaded standby HMR runtime.');
|
|
66
|
+
return candidate.lease();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Release attempt-owned activity without evicting any working shared owner. */
|
|
70
|
+
export function releaseNativeBrowserDevClient(
|
|
71
|
+
remoteName: string,
|
|
72
|
+
manifest: string,
|
|
73
|
+
): void {
|
|
74
|
+
const hash = /\/__ultramodern_mf__\/([a-f0-9]+)\//u.exec(
|
|
75
|
+
new URL(manifest).pathname,
|
|
76
|
+
)?.[1];
|
|
77
|
+
if (!hash) return;
|
|
78
|
+
const state = clients().get(remoteName);
|
|
79
|
+
const candidate = state?.candidates.get(hash)?.deref();
|
|
80
|
+
if (!state || !candidate) return;
|
|
81
|
+
candidate.releaseStandby();
|
|
82
|
+
// A failed asynchronous chunk load can retry the same unexecuted factory.
|
|
83
|
+
// Weak routing retains no transport resources and permits its cached runtime
|
|
84
|
+
// to start after a later successful native transaction.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Rsbuild's ordinary socket/check protocol, with an explicit native handoff. */
|
|
88
|
+
export function startNativeBrowserDevClient(options: {
|
|
89
|
+
readonly remoteName: string;
|
|
90
|
+
readonly buildId: string;
|
|
91
|
+
readonly revisionHeader: string;
|
|
92
|
+
readonly token: string;
|
|
93
|
+
readonly config: {
|
|
94
|
+
readonly host?: string;
|
|
95
|
+
readonly port?: number | string;
|
|
96
|
+
readonly protocol?: string;
|
|
97
|
+
readonly path: string;
|
|
98
|
+
readonly reconnect: number;
|
|
99
|
+
readonly overlay?: boolean | { readonly errors?: boolean };
|
|
100
|
+
};
|
|
101
|
+
readonly serverHost: string;
|
|
102
|
+
readonly serverPort: number;
|
|
103
|
+
readonly serverBase: string;
|
|
104
|
+
readonly liveReload: boolean;
|
|
105
|
+
readonly browserLogs: boolean;
|
|
106
|
+
readonly logLevel: string;
|
|
107
|
+
readonly resolveWebSocketUrl?: (url: string) => string;
|
|
108
|
+
readonly hot: HotRuntime | undefined;
|
|
109
|
+
readonly hash: () => string;
|
|
110
|
+
readonly overlay: () => Overlay | undefined;
|
|
111
|
+
readonly installCustomListeners: (
|
|
112
|
+
listeners: Map<string, ((value: unknown) => void)[]>,
|
|
113
|
+
) => () => void;
|
|
114
|
+
}): () => void {
|
|
115
|
+
const state: Clients = clients().get(options.remoteName) ?? {
|
|
116
|
+
candidates: new Map(),
|
|
117
|
+
};
|
|
118
|
+
clients().set(options.remoteName, state);
|
|
119
|
+
const initialHash = options.hash();
|
|
120
|
+
let socket: WebSocket | undefined;
|
|
121
|
+
let ping: ReturnType<typeof setInterval> | undefined;
|
|
122
|
+
let reconnect: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
let reconnectCount = 0;
|
|
124
|
+
let lastHash: string | undefined;
|
|
125
|
+
let running = false;
|
|
126
|
+
let applying: Promise<void> | undefined;
|
|
127
|
+
let lifetime: AbortController | undefined;
|
|
128
|
+
let removeCustom: (() => void) | undefined;
|
|
129
|
+
const custom = new Map<string, ((value: unknown) => void)[]>();
|
|
130
|
+
const queued: unknown[] = [];
|
|
131
|
+
const leases = new Set<object>();
|
|
132
|
+
const base = options.serverBase.endsWith('/')
|
|
133
|
+
? options.serverBase
|
|
134
|
+
: `${options.serverBase}/`;
|
|
135
|
+
const report = (error: unknown) => {
|
|
136
|
+
console.error(
|
|
137
|
+
'[ultramodern] Native federation development update failed:',
|
|
138
|
+
error,
|
|
139
|
+
);
|
|
140
|
+
if (
|
|
141
|
+
options.config.overlay !== false &&
|
|
142
|
+
(typeof options.config.overlay !== 'object' ||
|
|
143
|
+
options.config.overlay.errors !== false)
|
|
144
|
+
)
|
|
145
|
+
options
|
|
146
|
+
.overlay()
|
|
147
|
+
?.create('Native federation update failed', String(error));
|
|
148
|
+
};
|
|
149
|
+
const send = (message: unknown) => {
|
|
150
|
+
if (socket?.readyState === WebSocket.OPEN)
|
|
151
|
+
socket.send(JSON.stringify(message));
|
|
152
|
+
else queued.push(message);
|
|
153
|
+
};
|
|
154
|
+
const errorMessage = (error: unknown): string =>
|
|
155
|
+
error instanceof Error ? error.message : String(error);
|
|
156
|
+
const onError = (event: ErrorEvent) =>
|
|
157
|
+
send({
|
|
158
|
+
type: 'client-error',
|
|
159
|
+
id: crypto.randomUUID(),
|
|
160
|
+
message: event.message,
|
|
161
|
+
name: event.error?.name,
|
|
162
|
+
stack: event.error?.stack,
|
|
163
|
+
});
|
|
164
|
+
const onRejected = (event: PromiseRejectionEvent) =>
|
|
165
|
+
send({
|
|
166
|
+
type: 'client-error',
|
|
167
|
+
id: crypto.randomUUID(),
|
|
168
|
+
message: `Uncaught (in promise) ${errorMessage(event.reason)}`,
|
|
169
|
+
name: event.reason?.name,
|
|
170
|
+
stack: event.reason?.stack,
|
|
171
|
+
});
|
|
172
|
+
const fullReload = (data?: { path?: string }) => {
|
|
173
|
+
if (!options.liveReload) return;
|
|
174
|
+
if (data?.path?.endsWith('.html')) {
|
|
175
|
+
const pathname = decodeURI(location.pathname);
|
|
176
|
+
const target = base + data.path.slice(1);
|
|
177
|
+
if (
|
|
178
|
+
pathname !== target &&
|
|
179
|
+
pathname !== target.slice(0, -5) &&
|
|
180
|
+
!(pathname.endsWith('/') && `${pathname}index.html` === target)
|
|
181
|
+
)
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
location.reload();
|
|
185
|
+
};
|
|
186
|
+
const apply = (): void => {
|
|
187
|
+
if (!running || applying || !lastHash || lastHash === options.hash())
|
|
188
|
+
return;
|
|
189
|
+
const signal = lifetime!.signal;
|
|
190
|
+
const targetHash = lastHash;
|
|
191
|
+
applying = (async () => {
|
|
192
|
+
if (hasNativeBrowserDevPublisher(options.remoteName)) {
|
|
193
|
+
const graph = await readNativeBrowserDevPublication(
|
|
194
|
+
options.remoteName,
|
|
195
|
+
{
|
|
196
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3000)]),
|
|
197
|
+
revisionHeader: options.revisionHeader,
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
if (signal.aborted) throw signal.reason;
|
|
201
|
+
if (graph.buildId !== options.buildId) {
|
|
202
|
+
// Never ask an obsolete process's runtime for a missing hot-update
|
|
203
|
+
// chain. The normal manifest admission and native capability must
|
|
204
|
+
// accept the fresh factory before its socket can replace this one.
|
|
205
|
+
try {
|
|
206
|
+
await replaceNativeBrowserDevRemote(graph);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (
|
|
209
|
+
running &&
|
|
210
|
+
!signal.aborted &&
|
|
211
|
+
hasNativeBrowserDevPublisher(options.remoteName) &&
|
|
212
|
+
isNativeBrowserDevCommitError(error, graph.revision)
|
|
213
|
+
) {
|
|
214
|
+
// Moving the socket aborts this old client's lifetime. Report
|
|
215
|
+
// first so that abort cannot hide the native callback failure.
|
|
216
|
+
state.failure = { runtimeHash: graph.runtimeHash, error };
|
|
217
|
+
report(error);
|
|
218
|
+
// The native owner already began an irreversible transfer. Keep
|
|
219
|
+
// its fresh runtime available for recovery, then surface failure.
|
|
220
|
+
commitClient(options.remoteName, graph);
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
!running ||
|
|
226
|
+
signal.aborted ||
|
|
227
|
+
!hasNativeBrowserDevPublisher(options.remoteName)
|
|
228
|
+
)
|
|
229
|
+
return;
|
|
230
|
+
commitClient(options.remoteName, graph);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!options.hot) return fullReload();
|
|
235
|
+
if (options.hot.status() !== 'idle') return;
|
|
236
|
+
try {
|
|
237
|
+
const updated = await options.hot.check(true);
|
|
238
|
+
if (!updated) fullReload();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(
|
|
241
|
+
'[rsbuild] HMR update failed, performing full reload:',
|
|
242
|
+
error,
|
|
243
|
+
);
|
|
244
|
+
fullReload();
|
|
245
|
+
}
|
|
246
|
+
})()
|
|
247
|
+
.catch(error => {
|
|
248
|
+
// No failed graph acknowledges a completed update. Precommit failures
|
|
249
|
+
// retain the original owner; native commit failures keep recovery owners.
|
|
250
|
+
// Surface the failure without aborting an obsolete runtime's hot check.
|
|
251
|
+
if (!signal.aborted) report(error);
|
|
252
|
+
})
|
|
253
|
+
.finally(() => {
|
|
254
|
+
applying = undefined;
|
|
255
|
+
if (running && !signal.aborted && lastHash !== targetHash) apply();
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
const clearOverlayForBuild = () => {
|
|
259
|
+
if (state.failure) {
|
|
260
|
+
// A fresh runtime's initial hash/ok handshake acknowledges the same
|
|
261
|
+
// failed graph, not a repaired native owner. Keep that failure visible.
|
|
262
|
+
if (!lastHash || lastHash === state.failure.runtimeHash) return;
|
|
263
|
+
state.failure = undefined;
|
|
264
|
+
}
|
|
265
|
+
options.overlay()?.clear();
|
|
266
|
+
};
|
|
267
|
+
const message = (event: MessageEvent<string>) => {
|
|
268
|
+
let value: { type: string; data?: any };
|
|
269
|
+
try {
|
|
270
|
+
value = JSON.parse(event.data);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
report(error);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
switch (value.type) {
|
|
276
|
+
case 'hash':
|
|
277
|
+
lastHash = value.data;
|
|
278
|
+
break;
|
|
279
|
+
case 'ok':
|
|
280
|
+
clearOverlayForBuild();
|
|
281
|
+
apply();
|
|
282
|
+
break;
|
|
283
|
+
case 'warnings':
|
|
284
|
+
for (const warning of value.data?.text ?? []) console.warn(warning);
|
|
285
|
+
clearOverlayForBuild();
|
|
286
|
+
apply();
|
|
287
|
+
break;
|
|
288
|
+
case 'errors':
|
|
289
|
+
for (const error of value.data?.text ?? []) console.error(error);
|
|
290
|
+
if (value.data?.html)
|
|
291
|
+
options.overlay()?.create('Build failed', value.data.html);
|
|
292
|
+
break;
|
|
293
|
+
case 'full-reload':
|
|
294
|
+
fullReload(value.data);
|
|
295
|
+
break;
|
|
296
|
+
case 'static-changed':
|
|
297
|
+
fullReload();
|
|
298
|
+
break;
|
|
299
|
+
case 'custom':
|
|
300
|
+
for (const listener of custom.get(value.data?.event) ?? [])
|
|
301
|
+
listener(value.data.data);
|
|
302
|
+
break;
|
|
303
|
+
case 'resolved-client-error':
|
|
304
|
+
options
|
|
305
|
+
.overlay()
|
|
306
|
+
?.create('Runtime errors', String(value.data?.message ?? ''));
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const socketUrl = (fallback: boolean) => {
|
|
311
|
+
const url = new URL('http://localhost');
|
|
312
|
+
url.hostname =
|
|
313
|
+
(fallback ? options.serverHost : options.config.host) ||
|
|
314
|
+
location.hostname;
|
|
315
|
+
url.port = String(
|
|
316
|
+
(fallback ? options.serverPort : options.config.port) || location.port,
|
|
317
|
+
);
|
|
318
|
+
url.protocol =
|
|
319
|
+
options.config.protocol ||
|
|
320
|
+
(location.protocol === 'https:' ? 'wss:' : 'ws:');
|
|
321
|
+
url.pathname = options.config.path;
|
|
322
|
+
url.searchParams.set('token', options.token);
|
|
323
|
+
return options.resolveWebSocketUrl?.(url.href) ?? url.href;
|
|
324
|
+
};
|
|
325
|
+
const detachSocket = () => {
|
|
326
|
+
clearInterval(ping);
|
|
327
|
+
ping = undefined;
|
|
328
|
+
const previous = socket;
|
|
329
|
+
socket = undefined;
|
|
330
|
+
if (!previous) return;
|
|
331
|
+
previous.onopen =
|
|
332
|
+
previous.onclose =
|
|
333
|
+
previous.onmessage =
|
|
334
|
+
previous.onerror =
|
|
335
|
+
null;
|
|
336
|
+
previous.close();
|
|
337
|
+
};
|
|
338
|
+
const connect = (fallback = false) => {
|
|
339
|
+
if (!running) return;
|
|
340
|
+
socket = new WebSocket(socketUrl(fallback));
|
|
341
|
+
socket.onopen = () => {
|
|
342
|
+
reconnectCount = 0;
|
|
343
|
+
ping = setInterval(() => send({ type: 'ping' }), 30000);
|
|
344
|
+
for (const item of queued.splice(0)) send(item);
|
|
345
|
+
};
|
|
346
|
+
socket.onmessage = message;
|
|
347
|
+
socket.onclose = () => {
|
|
348
|
+
detachSocket();
|
|
349
|
+
if (!running || reconnectCount >= options.config.reconnect) return;
|
|
350
|
+
reconnectCount++;
|
|
351
|
+
reconnect = setTimeout(() => connect(), 1000 * 1.5 ** reconnectCount);
|
|
352
|
+
};
|
|
353
|
+
socket.onerror = () => {
|
|
354
|
+
if (
|
|
355
|
+
options.resolveWebSocketUrl ||
|
|
356
|
+
fallback ||
|
|
357
|
+
socketUrl(false) === socketUrl(true)
|
|
358
|
+
)
|
|
359
|
+
return;
|
|
360
|
+
detachSocket();
|
|
361
|
+
connect(true);
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
const client: Client = {
|
|
365
|
+
arm() {
|
|
366
|
+
removeCustom ??= options.installCustomListeners(custom);
|
|
367
|
+
},
|
|
368
|
+
lease() {
|
|
369
|
+
const lease = {};
|
|
370
|
+
leases.add(lease);
|
|
371
|
+
client.arm();
|
|
372
|
+
return () => {
|
|
373
|
+
if (!leases.delete(lease)) return;
|
|
374
|
+
client.releaseStandby();
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
releaseStandby() {
|
|
378
|
+
// A live runtime remains the ordinary remote HMR owner even when its
|
|
379
|
+
// final publisher unmounts. Another expose can also hold this candidate.
|
|
380
|
+
if (state.active !== client && !leases.size) client.stop();
|
|
381
|
+
},
|
|
382
|
+
start() {
|
|
383
|
+
if (running) return;
|
|
384
|
+
running = true;
|
|
385
|
+
lifetime = new AbortController();
|
|
386
|
+
client.arm();
|
|
387
|
+
if (options.browserLogs) {
|
|
388
|
+
window.addEventListener('error', onError);
|
|
389
|
+
window.addEventListener('unhandledrejection', onRejected);
|
|
390
|
+
}
|
|
391
|
+
connect();
|
|
392
|
+
},
|
|
393
|
+
stop() {
|
|
394
|
+
if (!running && !removeCustom) return;
|
|
395
|
+
running = false;
|
|
396
|
+
lifetime?.abort(new Error('Native browser HMR runtime retired.'));
|
|
397
|
+
clearTimeout(reconnect);
|
|
398
|
+
reconnect = undefined;
|
|
399
|
+
detachSocket();
|
|
400
|
+
window.removeEventListener('error', onError);
|
|
401
|
+
window.removeEventListener('unhandledrejection', onRejected);
|
|
402
|
+
removeCustom?.();
|
|
403
|
+
removeCustom = undefined;
|
|
404
|
+
custom.clear();
|
|
405
|
+
queued.length = 0;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
// Standby runtimes need custom hot listeners before their exposed modules
|
|
409
|
+
// execute, but have no socket/global error listeners until native acceptance.
|
|
410
|
+
client.arm();
|
|
411
|
+
state.candidates.set(initialHash, new WeakRef(client));
|
|
412
|
+
if (!state.active) {
|
|
413
|
+
state.active = client;
|
|
414
|
+
client.start();
|
|
415
|
+
}
|
|
416
|
+
return () => {
|
|
417
|
+
client.stop();
|
|
418
|
+
if (state.active === client) state.active = undefined;
|
|
419
|
+
if (state.candidates.get(initialHash)?.deref() === client)
|
|
420
|
+
state.candidates.delete(initialHash);
|
|
421
|
+
if (!state.active && !state.candidates.size)
|
|
422
|
+
clients().delete(options.remoteName);
|
|
423
|
+
};
|
|
424
|
+
}
|