@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.
Files changed (25) hide show
  1. package/dist/cjs/module-federation/native-browser-dev-client.js +320 -0
  2. package/dist/cjs/module-federation/native-browser-dev-refresh.js +226 -0
  3. package/dist/cjs/module-federation/native-browser-dev-runtime-plugin.js +233 -0
  4. package/dist/cjs/module-federation/native-dev-refresh-runtime-plugin.js +267 -0
  5. package/dist/cjs/module-federation/renderer-runtime-plugin.js +5 -2
  6. package/dist/esm/module-federation/native-browser-dev-client.mjs +276 -0
  7. package/dist/esm/module-federation/native-browser-dev-refresh.mjs +182 -0
  8. package/dist/esm/module-federation/native-browser-dev-runtime-plugin.mjs +183 -0
  9. package/dist/esm/module-federation/native-dev-refresh-runtime-plugin.mjs +229 -0
  10. package/dist/esm/module-federation/renderer-runtime-plugin.mjs +5 -2
  11. package/dist/esm-node/module-federation/native-browser-dev-client.mjs +277 -0
  12. package/dist/esm-node/module-federation/native-browser-dev-refresh.mjs +183 -0
  13. package/dist/esm-node/module-federation/native-browser-dev-runtime-plugin.mjs +184 -0
  14. package/dist/esm-node/module-federation/native-dev-refresh-runtime-plugin.mjs +230 -0
  15. package/dist/esm-node/module-federation/renderer-runtime-plugin.mjs +5 -2
  16. package/dist/types/module-federation/native-browser-dev-client.d.ts +41 -0
  17. package/dist/types/module-federation/native-browser-dev-refresh.d.ts +86 -0
  18. package/dist/types/module-federation/native-browser-dev-runtime-plugin.d.ts +40 -0
  19. package/dist/types/module-federation/native-dev-refresh-runtime-plugin.d.ts +90 -0
  20. package/package.json +54 -7
  21. package/src/module-federation/native-browser-dev-client.ts +424 -0
  22. package/src/module-federation/native-browser-dev-refresh.ts +373 -0
  23. package/src/module-federation/native-browser-dev-runtime-plugin.ts +276 -0
  24. package/src/module-federation/native-dev-refresh-runtime-plugin.ts +445 -0
  25. 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
+ }