@bleedingdev/modern-js-federation-runtime 3.9.3-ultramodern.7 → 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 +60 -13
  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,445 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ interface ManifestRemote {
4
+ readonly name: string;
5
+ readonly alias: string;
6
+ readonly entry: string;
7
+ }
8
+
9
+ interface RemoteInfo {
10
+ readonly name: string;
11
+ readonly entry: string;
12
+ }
13
+
14
+ interface NativeHost {
15
+ readonly options?: unknown;
16
+ readonly shareScopeMap?: Record<
17
+ string,
18
+ Record<
19
+ string,
20
+ Record<string, { loaded?: boolean; loading?: unknown; lib?: unknown }>
21
+ >
22
+ >;
23
+ loadShare?(pkgName: string): Promise<unknown>;
24
+ loadRemote<T>(id: string, options?: unknown): Promise<T | null>;
25
+ registerRemotes(remotes: ManifestRemote[], options: { force: true }): void;
26
+ readonly loaderHook: {
27
+ readonly lifecycle: {
28
+ readonly fetch: {
29
+ emit(
30
+ url: string,
31
+ init: RequestInit,
32
+ remoteInfo: RemoteInfo,
33
+ context: {
34
+ initiator: 'loadRemote';
35
+ id: string;
36
+ resourceType: 'remoteEntry';
37
+ url: string;
38
+ expose?: string;
39
+ },
40
+ ): Promise<Response | false | void>;
41
+ };
42
+ };
43
+ };
44
+ [LOAD_WITH_CAPTURE]?: <T, Captured>(
45
+ id: string,
46
+ capture: (module: T | null, resolution?: LoadResolution) => Captured,
47
+ ) => Promise<{ module: T | null; captured: Captured | undefined }>;
48
+ }
49
+
50
+ const LOAD_WITH_CAPTURE = Symbol.for(
51
+ 'ultramodern.federation.load-with-capture',
52
+ );
53
+
54
+ interface RemoteLane {
55
+ readonly remote: ManifestRemote;
56
+ readonly active: Set<Promise<unknown>>;
57
+ info?: RemoteInfo;
58
+ revision?: string | null;
59
+ candidate?: string | null;
60
+ transition?: Promise<unknown>;
61
+ probe?: Promise<string | null>;
62
+ snapshot?: unknown;
63
+ }
64
+
65
+ interface LoadLease {
66
+ readonly host: NativeHost;
67
+ readonly parent?: LoadLease;
68
+ lane?: RemoteLane;
69
+ admitted?: boolean;
70
+ active?: boolean;
71
+ snapshot?: unknown;
72
+ resolution?: LoadResolution;
73
+ release?: (success: boolean) => void;
74
+ }
75
+
76
+ interface LoadResolution {
77
+ readonly remoteSnapshot: unknown;
78
+ readonly expose: string;
79
+ }
80
+
81
+ export interface NativeDevRefreshOptions {
82
+ readonly remotes: readonly ManifestRemote[];
83
+ readonly revisionHeader: string;
84
+ readonly requestTimeout: number;
85
+ /** Exact and prefix singleton keys owned by the selected native adapter. */
86
+ readonly shared?: readonly string[];
87
+ }
88
+
89
+ /**
90
+ * Retain the host and its shared factories while refreshing one completed
91
+ * development remote. Production remotes without a revision keep native MF's
92
+ * ordinary cache. The compiler registers this plugin only on Node dev hosts.
93
+ */
94
+ export default function nativeDevRefreshRuntimePlugin(
95
+ options: NativeDevRefreshOptions,
96
+ ) {
97
+ if (
98
+ !options.revisionHeader ||
99
+ !Number.isSafeInteger(options.requestTimeout) ||
100
+ options.requestTimeout <= 0
101
+ )
102
+ throw new TypeError(
103
+ 'Native federation dev refresh requires a bounded revision probe.',
104
+ );
105
+ const hosts = new WeakMap<NativeHost, Map<string, RemoteLane>>();
106
+ const calls = new AsyncLocalStorage<LoadLease>();
107
+
108
+ const probe = (
109
+ host: NativeHost,
110
+ lane: RemoteLane,
111
+ expose?: string,
112
+ ): Promise<string | null> => {
113
+ if (lane.probe) return lane.probe;
114
+ const info = lane.info;
115
+ if (!info)
116
+ throw new Error('Native federation dev remote has no admitted entry.');
117
+ const pending = (async () => {
118
+ const init: RequestInit = {
119
+ method: 'HEAD',
120
+ cache: 'no-store',
121
+ signal: AbortSignal.timeout(options.requestTimeout),
122
+ };
123
+ // Use the same native fetch lifecycle as entry loading, including any
124
+ // authored authentication hooks and the shared bounded Node transport.
125
+ let clearDeadline!: () => void;
126
+ const signal = init.signal!;
127
+ const deadline = new Promise<never>((_resolve, reject) => {
128
+ const abort = () => reject(signal.reason);
129
+ signal.addEventListener('abort', abort, { once: true });
130
+ clearDeadline = () => signal.removeEventListener('abort', abort);
131
+ });
132
+ const request = (async () => {
133
+ const hooked = await host.loaderHook.lifecycle.fetch.emit(
134
+ info.entry,
135
+ init,
136
+ info,
137
+ {
138
+ initiator: 'loadRemote',
139
+ id:
140
+ expose && expose !== '.'
141
+ ? `${info.name}/${expose.replace(/^\.\//u, '')}`
142
+ : info.name,
143
+ resourceType: 'remoteEntry',
144
+ url: info.entry,
145
+ expose,
146
+ },
147
+ );
148
+ const response =
149
+ hooked instanceof Response
150
+ ? hooked
151
+ : await globalThis.fetch(info.entry, init);
152
+ if (signal.aborted) {
153
+ void response.body?.cancel().catch(() => {});
154
+ throw signal.reason;
155
+ }
156
+ return response;
157
+ })();
158
+ let response: Response;
159
+ try {
160
+ // Authored hooks must receive the native transport context, but a
161
+ // hook ignoring the signal must not hold this remote's gate forever.
162
+ response = await Promise.race([request, deadline]);
163
+ } finally {
164
+ clearDeadline();
165
+ }
166
+ void response.body?.cancel().catch(() => {});
167
+ if (!response.ok)
168
+ throw Object.assign(
169
+ new Error(
170
+ `Native federation dev revision request failed: ${response.status} ${info.entry}`,
171
+ ),
172
+ {
173
+ status: response.status,
174
+ },
175
+ );
176
+ const revision = response.headers.get(options.revisionHeader);
177
+ if (revision !== null && !/^[a-f0-9]{64}$/u.test(revision))
178
+ throw new Error(
179
+ 'Native federation dev remote returned an invalid graph revision.',
180
+ );
181
+ if (revision === null && typeof lane.revision === 'string')
182
+ throw new Error(
183
+ 'Native federation dev remote lost its completed graph revision.',
184
+ );
185
+ return revision;
186
+ })();
187
+ lane.probe = pending;
188
+ const clear = () => {
189
+ if (lane.probe === pending) lane.probe = undefined;
190
+ };
191
+ void pending.then(clear, clear);
192
+ return pending;
193
+ };
194
+
195
+ return {
196
+ name: 'ultramodern-native-federation-dev-refresh',
197
+ beforeInit<T extends { origin: NativeHost }>(args: T): T {
198
+ const host = args.origin;
199
+ if (hosts.has(host)) return args;
200
+ const lanes = new Map<string, RemoteLane>(
201
+ options.remotes.map(remote => [
202
+ remote.name,
203
+ {
204
+ remote: { ...remote },
205
+ active: new Set<Promise<unknown>>(),
206
+ } satisfies RemoteLane,
207
+ ]),
208
+ );
209
+ hosts.set(host, lanes);
210
+ const load = host.loadRemote.bind(host);
211
+ const loadWithCapture = async <Module, Captured>(
212
+ id: string,
213
+ loadOptions?: unknown,
214
+ capture?: (
215
+ module: Module | null,
216
+ resolution?: LoadResolution,
217
+ ) => Captured,
218
+ ): Promise<{ module: Module | null; captured: Captured | undefined }> => {
219
+ const parent = calls.getStore();
220
+ const lease: LoadLease = {
221
+ host,
222
+ ...(parent?.host === host ? { parent } : {}),
223
+ };
224
+ return calls.run(lease, async () => {
225
+ let success = false;
226
+ try {
227
+ const module = await load<Module>(id, loadOptions);
228
+ // Capture this container's admitted assets before releasing its
229
+ // lease, even when native hooks rewrite the requested module ID.
230
+ const captured = capture?.(module, lease.resolution);
231
+ success = module !== null && lease.admitted === true;
232
+ return { module, captured };
233
+ } finally {
234
+ lease.active = false;
235
+ lease.release?.(success);
236
+ }
237
+ });
238
+ };
239
+ host.loadRemote = async <Module>(id: string, loadOptions?: unknown) =>
240
+ (await loadWithCapture<Module, never>(id, loadOptions)).module;
241
+ host[LOAD_WITH_CAPTURE] = <Module, Captured>(
242
+ id: string,
243
+ capture: (
244
+ module: Module | null,
245
+ resolution?: LoadResolution,
246
+ ) => Captured,
247
+ ) => loadWithCapture(id, undefined, capture);
248
+ return args;
249
+ },
250
+ async beforeRequest<T extends { origin: NativeHost; id: string }>(
251
+ args: T,
252
+ ): Promise<T> {
253
+ const lease = calls.getStore();
254
+ if (!lease || lease.host !== args.origin) return args;
255
+ // Authored beforeRequest hooks have already rewritten the ID. Discover
256
+ // its actual remote here without executing those hooks a second time.
257
+ const lane = [...(hosts.get(args.origin)?.values() ?? [])].find(
258
+ ({ remote }) =>
259
+ [remote.alias, remote.name].some(
260
+ name => args.id === name || args.id.startsWith(`${name}/`),
261
+ ),
262
+ );
263
+ if (!lane || lease.lane) return args;
264
+ lease.lane = lane;
265
+ const prefix = [lane.remote.alias, lane.remote.name].find(
266
+ name => args.id === name || args.id.startsWith(`${name}/`),
267
+ )!;
268
+ const expose =
269
+ args.id === prefix ? '.' : `./${args.id.slice(prefix.length + 1)}`;
270
+ for (let ancestor = lease.parent; ancestor; ancestor = ancestor.parent) {
271
+ if (ancestor.lane === lane && ancestor.active) {
272
+ // An authored native hook may await another expose on this host.
273
+ // Its ancestor already fences this remote's generation; borrowing
274
+ // that lease avoids waiting for or draining the current call itself.
275
+ // Track the child as well: a hook may start it without awaiting it,
276
+ // and a later refresh must still wait for that older native load.
277
+ let release!: () => void;
278
+ const finished = new Promise<void>(resolve => {
279
+ release = resolve;
280
+ });
281
+ lane.active.add(finished);
282
+ lease.active = true;
283
+ lease.snapshot = ancestor.snapshot;
284
+ lease.release = () => {
285
+ lane.active.delete(finished);
286
+ release();
287
+ };
288
+ return args;
289
+ }
290
+ }
291
+ for (;;) {
292
+ if (lane.transition) {
293
+ await lane.transition;
294
+ continue;
295
+ }
296
+ const revision =
297
+ lane.revision === undefined
298
+ ? undefined
299
+ : lane.revision === null
300
+ ? null
301
+ : await probe(args.origin, lane, expose);
302
+ if (lane.transition) {
303
+ await lane.transition;
304
+ continue;
305
+ }
306
+ let finish!: () => void;
307
+ const finished = new Promise<void>(resolve => {
308
+ finish = resolve;
309
+ });
310
+ if (lane.revision === undefined || revision !== lane.revision) {
311
+ lane.transition = finished;
312
+ lease.release = success => {
313
+ if (success) lane.revision = lane.candidate ?? null;
314
+ if (lane.transition === finished) lane.transition = undefined;
315
+ finish();
316
+ };
317
+ // Finish older native loads, including their asset capture, before
318
+ // evicting this remote. New calls wait for this admission to finish.
319
+ while (lane.active.size) await Promise.allSettled([...lane.active]);
320
+ lane.candidate = revision;
321
+ if (lane.revision !== undefined) {
322
+ // Authored share hooks may load another expose while we retain
323
+ // providers. Let those calls borrow the still-current generation,
324
+ // then drain any child they started before evicting its container.
325
+ lease.active = true;
326
+ lease.snapshot = lane.snapshot;
327
+ // Native force-registration removes remote-origin providers that
328
+ // only the remote explicitly consumed. The host can still retain
329
+ // their private state through another shared renderer export.
330
+ // Record those loaded providers as host uses through the ordinary
331
+ // share lifecycle before native eviction; do not reload unused
332
+ // providers or mutate native factory/cache records ourselves.
333
+ const retained = new Set<string>();
334
+ for (const scope of Object.values(args.origin.shareScopeMap ?? {}))
335
+ for (const [pkgName, versions] of Object.entries(scope))
336
+ if (
337
+ options.shared?.some(
338
+ key =>
339
+ key === pkgName ||
340
+ (key.endsWith('/') && pkgName.startsWith(key)),
341
+ ) &&
342
+ Object.values(versions).some(
343
+ shared => shared.loaded || shared.loading || shared.lib,
344
+ )
345
+ )
346
+ retained.add(pkgName);
347
+ for (const pkgName of retained) {
348
+ if (!args.origin.loadShare)
349
+ throw new TypeError(
350
+ 'Native federation dev refresh requires the host share lifecycle.',
351
+ );
352
+ await args.origin.loadShare(pkgName);
353
+ }
354
+ while (lane.active.size) await Promise.allSettled([...lane.active]);
355
+ args.origin.registerRemotes([{ ...lane.remote }], { force: true });
356
+ lease.snapshot = undefined;
357
+ }
358
+ } else {
359
+ lane.active.add(finished);
360
+ lease.release = () => {
361
+ lane.active.delete(finished);
362
+ finish();
363
+ };
364
+ }
365
+ lease.active = true;
366
+ return args;
367
+ }
368
+ },
369
+ loadSnapshot<
370
+ T extends {
371
+ options: unknown;
372
+ moduleInfo: { name: string };
373
+ remoteSnapshot?: unknown;
374
+ },
375
+ >(args: T): T {
376
+ const lease = calls.getStore();
377
+ if (
378
+ !lease ||
379
+ args.options !== lease.host.options ||
380
+ args.moduleInfo.name !== lease.lane?.remote.name
381
+ )
382
+ return args;
383
+ // A borrowed load uses its active ancestor's generation, even when a
384
+ // different host has replaced the process-global snapshot meanwhile.
385
+ // Native snapshot and renderer admission still validate this selection.
386
+ if (lease.snapshot !== undefined)
387
+ return { ...args, remoteSnapshot: lease.snapshot };
388
+ lease.snapshot = args.remoteSnapshot;
389
+ return args;
390
+ },
391
+ afterLoadSnapshot<
392
+ T extends {
393
+ host: NativeHost;
394
+ moduleInfo: { name: string };
395
+ remoteSnapshot: unknown;
396
+ },
397
+ >(args: T): T {
398
+ const lease = calls.getStore();
399
+ if (
400
+ lease?.host === args.host &&
401
+ args.moduleInfo.name === lease.lane?.remote.name
402
+ )
403
+ // Initial manifest loads have no snapshot at loadSnapshot. Retain the
404
+ // resolved snapshot before authored afterResolve hooks can nest loads.
405
+ lease.snapshot = args.remoteSnapshot;
406
+ return args;
407
+ },
408
+ async afterResolve<
409
+ T extends {
410
+ origin: NativeHost;
411
+ remote: { name: string };
412
+ remoteInfo: RemoteInfo;
413
+ remoteSnapshot: unknown;
414
+ expose: string;
415
+ },
416
+ >(args: T): Promise<T> {
417
+ const lane = hosts.get(args.origin)?.get(args.remote.name);
418
+ const lease = calls.getStore();
419
+ if (
420
+ !lane ||
421
+ lease?.host !== args.origin ||
422
+ lease.lane !== lane ||
423
+ !lease.active
424
+ )
425
+ return args;
426
+ // Preloads and direct remote-handler resolutions have no load lease.
427
+ // They must not change the entry or candidate a guarded refresh commits.
428
+ if (lane.info?.entry !== args.remoteInfo.entry)
429
+ lane.candidate = undefined;
430
+ lane.info = args.remoteInfo;
431
+ if (lane.candidate === undefined)
432
+ lane.candidate = await probe(args.origin, lane, args.expose);
433
+ lease.admitted = true;
434
+ lane.snapshot = args.remoteSnapshot;
435
+ // Native force-registration evicts a process-global snapshot. Retain
436
+ // the snapshot this particular host/load admitted, so another host's
437
+ // refresh cannot change its later CSS and hydration asset capture.
438
+ lease.resolution = {
439
+ remoteSnapshot: args.remoteSnapshot,
440
+ expose: args.expose,
441
+ };
442
+ return args;
443
+ },
444
+ };
445
+ }
@@ -155,6 +155,11 @@ export function createRendererFederationRuntimePlugin(
155
155
  const observedEntries = new WeakMap<object, string>();
156
156
  const acceptedEntries = new Set<string>();
157
157
  const versions = rendererShareVersions(expected);
158
+ const sharedVersion = (name: string): string | undefined =>
159
+ versions.get(name) ??
160
+ (expected.profile.renderer === 'react'
161
+ ? undefined
162
+ : [...versions].find(([owner]) => name.startsWith(`${owner}/`))?.[1]);
158
163
  return {
159
164
  name: 'ultramodern-renderer-federation-contract',
160
165
  beforeRegisterRemote(args: { remote: unknown }) {
@@ -332,7 +337,7 @@ export function createRendererFederationRuntimePlugin(
332
337
  return args;
333
338
  },
334
339
  resolveShare(args: ResolveShareArgs) {
335
- const version = versions.get(args.pkgName);
340
+ const version = sharedVersion(args.pkgName);
336
341
  if (version === undefined) return args;
337
342
  const resolve = args.resolver;
338
343
  return {
@@ -378,7 +383,7 @@ export function createRendererFederationRuntimePlugin(
378
383
  // Native loading fills lib after resolveShare. Observe that completed
379
384
  // factory identity here; rejection remains in the pre-factory resolver
380
385
  // because native afterLoadShare deliberately swallows thrown errors.
381
- if (versions.has(args.pkgName) && args.selectedShared) {
386
+ if (sharedVersion(args.pkgName) !== undefined && args.selectedShared) {
382
387
  const shared =
383
388
  record(args.selectedShared.treeShaking) &&
384
389
  args.selectedShared.treeShaking.loaded