@rollipop/plugin-module-federation 1.0.0-alpha.28 → 1.0.0-alpha.30

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/README.md CHANGED
@@ -93,7 +93,7 @@ export default defineConfig({
93
93
  });
94
94
  ```
95
95
 
96
- The bundle is emitted as IIFE. Each shared dep import (e.g. `import RN from 'react-native'`) is replaced with a stub that reads from the host's `globalThis.__rollipop_shared__` registry and throws if the dep is not registered. Rollipop's prelude / polyfills are skipped because the host has already initialized the React Native runtime.
96
+ The bundle is emitted as IIFE. Each shared dep import (e.g. `import RN from 'react-native'`) is replaced with a stub that reads from the host's `globalThis.__rollipop_shared__` registry and throws if the dep is not registered. Rollipop's prelude and polyfills are skipped because the host has already initialized the React Native runtime.
97
97
 
98
98
  ## Script loader contract
99
99
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Plugin } from "rollipop";
2
-
3
2
  //#region src/types.d.ts
4
3
  /**
5
4
  * Module Federation plugin configuration.
package/dist/index.js CHANGED
@@ -15,8 +15,6 @@ const VIRTUAL_REMOTE_PROXY_PREFIX = `${VIRTUAL_PREFIX}remote:`;
15
15
  const SCRIPT_LOADER_GLOBAL = "__rollipop_script_loader__";
16
16
  const SHARED_REGISTRY_GLOBAL = "__rollipop_shared__";
17
17
  const REMOTE_CACHE_GLOBAL = "__rollipop_module_federation_cache__";
18
- const HMR_HOT_PATH = "/hot";
19
- const HMR_EVENT = "mf:remote-update";
20
18
  //#endregion
21
19
  //#region src/virtual/_dedent.ts
22
20
  const dedent = baseDedent.withOptions({ escapeSpecialCharacters: false });
@@ -64,18 +62,10 @@ function generateHostInitCode(config) {
64
62
  shareStrategy: ${JSON.stringify(config.shareStrategy)},
65
63
  });
66
64
 
67
- const remoteList = ${JSON.stringify(remotesArray)};
68
-
69
65
  if (globalThis.${REMOTE_CACHE_GLOBAL} == null) {
70
66
  const cache = {
71
67
  modules: Object.create(null),
72
68
  pending: Object.create(null),
73
- // Per-id invalidation: only ids actually present in the cache when
74
- // HMR fired need to bypass the federation runtime. Cleared after
75
- // each successful bypass load — new ids fall back to the normal
76
- // loadRemote flow with full shared-module negotiation.
77
- invalidatedIds: new Set(),
78
- subscribers: new Set(),
79
69
  load(id) {
80
70
  if (this.modules[id] !== undefined) {
81
71
  return Promise.resolve(this.modules[id]);
@@ -83,126 +73,17 @@ function generateHostInitCode(config) {
83
73
  if (this.pending[id]) {
84
74
  return this.pending[id];
85
75
  }
86
- const isInvalidated = this.invalidatedIds.has(id);
87
- let fetcher;
88
- if (isInvalidated) {
89
- const slash = id.indexOf('/');
90
- const remoteName = slash === -1 ? id : id.slice(0, slash);
91
- const exposePath = slash === -1 ? '.' : './' + id.slice(slash + 1);
92
- fetcher = Promise.resolve().then(() => {
93
- const container = globalThis[remoteName];
94
- if (container == null) {
95
- throw new Error('[rollipop:module-federation] container ${Q}' + remoteName + '${Q} not registered');
96
- }
97
- return container.get(exposePath).then((factory) => {
98
- return typeof factory === 'function' ? factory() : factory;
99
- });
100
- });
101
- } else {
102
- fetcher = instance.loadRemote(id);
103
- }
76
+ const fetcher = instance.loadRemote(id);
104
77
  this.pending[id] = fetcher.then((mod) => {
105
78
  this.modules[id] = mod;
106
79
  delete this.pending[id];
107
- if (isInvalidated) {
108
- this.invalidatedIds.delete(id);
109
- }
110
80
  return mod;
111
81
  });
112
82
  return this.pending[id];
113
83
  },
114
- invalidate(remoteName) {
115
- for (const key of Object.keys(this.modules)) {
116
- if (key === remoteName || key.startsWith(remoteName + '/')) {
117
- this.invalidatedIds.add(key);
118
- delete this.modules[key];
119
- }
120
- }
121
- for (const cb of this.subscribers) {
122
- try {
123
- cb(remoteName);
124
- } catch (_e) {}
125
- }
126
- },
127
84
  };
128
85
  globalThis.${REMOTE_CACHE_GLOBAL} = cache;
129
86
  }
130
-
131
- const __cache = globalThis.${REMOTE_CACHE_GLOBAL};
132
-
133
- async function applyRemoteUpdate(remote) {
134
- const generation = (remote.__mfHmrGen = (remote.__mfHmrGen || 0) + 1);
135
- const cacheBust = (remote.entry.indexOf('?') === -1 ? '?' : '&') + '_mf_hmr=' + generation;
136
- try {
137
- await globalThis.${SCRIPT_LOADER_GLOBAL}.loadScript({
138
- scriptId: remote.name + '@' + generation,
139
- url: remote.entry + cacheBust,
140
- });
141
- } catch (error) {
142
- console.error(
143
- '[rollipop:module-federation] failed to reload remote ${Q}' + remote.name + '${Q} during HMR. App is running stale code until next successful update.',
144
- error,
145
- );
146
- return;
147
- }
148
- __cache.invalidate(remote.name);
149
- }
150
-
151
- function subscribeRemoteHmr(remote) {
152
- let wsUrl;
153
- try {
154
- const url = new URL(remote.entry);
155
- const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
156
- wsUrl = wsProtocol + '//' + url.host + ${JSON.stringify(HMR_HOT_PATH)};
157
- } catch (_e) {
158
- return;
159
- }
160
-
161
- const connect = () => {
162
- let socket;
163
- try {
164
- socket = new WebSocket(wsUrl);
165
- } catch (_e) {
166
- setTimeout(connect, 1000);
167
- return;
168
- }
169
- socket.onmessage = (event) => {
170
- let parsed;
171
- try {
172
- parsed = JSON.parse(typeof event.data === 'string' ? event.data : '');
173
- } catch (_e) {
174
- return;
175
- }
176
- if (parsed == null || parsed.type !== ${JSON.stringify(HMR_EVENT)}) {
177
- return;
178
- }
179
- if (parsed.payload != null && parsed.payload.name !== remote.name) {
180
- return;
181
- }
182
- applyRemoteUpdate(remote);
183
- };
184
- socket.onclose = () => setTimeout(connect, 1000);
185
- socket.onerror = () => {
186
- try {
187
- socket.close();
188
- } catch (_e) {}
189
- };
190
- };
191
- connect();
192
- }
193
-
194
- // Defer to the next tick so React Native's InitializeCore has set up
195
- // WebSocket / setTimeout before the subscription starts.
196
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
197
- Promise.resolve().then(() => {
198
- if (typeof WebSocket === 'undefined') {
199
- return;
200
- }
201
- for (const r of remoteList) {
202
- subscribeRemoteHmr(r);
203
- }
204
- });
205
- }
206
87
  `;
207
88
  }
208
89
  //#endregion
@@ -339,11 +220,13 @@ function validateConfig(config) {
339
220
  //#region src/virtual/remote-entry.ts
340
221
  function generateRemoteEntryCode(options) {
341
222
  const exposeEntries = Object.entries(options.exposes);
223
+ const importLines = exposeEntries.map(([, filePath], index) => `import * as __expose_${index} from ${JSON.stringify(filePath)};`).join("\n");
224
+ const moduleMapLines = exposeEntries.map(([key], index) => ` ${JSON.stringify(key)}: () => __expose_${index},`).join("\n");
342
225
  return dedent`
343
- ${exposeEntries.map(([, filePath], index) => `import * as __expose_${index} from ${JSON.stringify(filePath)};`).join("\n")}
226
+ ${importLines}
344
227
 
345
228
  const moduleMap = {
346
- ${exposeEntries.map(([key], index) => ` ${JSON.stringify(key)}: () => __expose_${index},`).join("\n")}
229
+ ${moduleMapLines}
347
230
  };
348
231
 
349
232
  const container = {
@@ -363,68 +246,11 @@ function generateRemoteEntryCode(options) {
363
246
  }
364
247
  //#endregion
365
248
  //#region src/virtual/remote-proxy.ts
366
- function generateRemoteProxyCode({ remoteId, reactAware }) {
249
+ function generateRemoteProxyCode({ remoteId }) {
367
250
  const idLiteral = JSON.stringify(remoteId);
368
- if (reactAware) return dedent`
369
- import * as __mfReact from 'react';
370
-
371
- const __cache = globalThis.${REMOTE_CACHE_GLOBAL};
372
- const __id = ${idLiteral};
373
-
374
- function __ensureLoaded() {
375
- if (__cache.modules[__id] !== undefined) {
376
- return null;
377
- }
378
- return __cache.load(__id);
379
- }
380
-
381
- function __getMod() {
382
- return __cache.modules[__id];
383
- }
384
-
385
- function __FederatedProxy(props) {
386
- const [, setVersion] = __mfReact.useState(0);
387
- __mfReact.useEffect(() => {
388
- const listener = () => setVersion((v) => v + 1);
389
- __cache.subscribers.add(listener);
390
- return () => {
391
- __cache.subscribers.delete(listener);
392
- };
393
- }, []);
394
-
395
- const mod = __getMod();
396
- if (mod === undefined) {
397
- throw __ensureLoaded();
398
- }
399
- const fn = mod.default ?? mod;
400
- return __mfReact.createElement(fn, props);
401
- }
402
-
403
- const __proxy = new Proxy(__FederatedProxy, {
404
- get(target, prop) {
405
- if (prop === '__esModule') {
406
- return true;
407
- }
408
- if (prop === 'then') {
409
- return undefined;
410
- }
411
- const mod = __getMod();
412
- if (mod === undefined) {
413
- throw __ensureLoaded();
414
- }
415
- if (prop in mod) {
416
- return mod[prop];
417
- }
418
- if (mod.default != null && prop in mod.default) {
419
- return mod.default[prop];
420
- }
421
- return target[prop];
422
- },
423
- });
424
-
425
- export default __proxy;
426
- `;
427
251
  return dedent`
252
+ import * as __mfReact from 'react';
253
+
428
254
  const __cache = globalThis.${REMOTE_CACHE_GLOBAL};
429
255
  const __id = ${idLiteral};
430
256
 
@@ -435,13 +261,12 @@ function generateRemoteProxyCode({ remoteId, reactAware }) {
435
261
  throw __cache.load(__id);
436
262
  }
437
263
 
438
- function __invoke(...args) {
264
+ function __FederatedProxy(props) {
439
265
  const mod = __getMod();
440
- const fn = mod.default ?? mod;
441
- return fn.apply(this, args);
266
+ return __mfReact.createElement(mod.default ?? mod, props);
442
267
  }
443
268
 
444
- const __proxy = new Proxy(__invoke, {
269
+ const __proxy = new Proxy(__FederatedProxy, {
445
270
  get(target, prop) {
446
271
  if (prop === '__esModule') {
447
272
  return true;
@@ -466,8 +291,9 @@ function generateRemoteProxyCode({ remoteId, reactAware }) {
466
291
  //#endregion
467
292
  //#region src/virtual/shared-shim.ts
468
293
  function generateSharedShimCode(sharedName) {
294
+ const key = JSON.stringify(sharedName);
469
295
  return dedent`
470
- const __mod = globalThis.${SHARED_REGISTRY_GLOBAL} && globalThis.${SHARED_REGISTRY_GLOBAL}[${JSON.stringify(sharedName)}];
296
+ const __mod = globalThis.${SHARED_REGISTRY_GLOBAL} && globalThis.${SHARED_REGISTRY_GLOBAL}[${key}];
471
297
  if (__mod == null) {
472
298
  throw new Error('[${PLUGIN_NAME}] shared module ${Q}${sharedName}${Q} is not registered on the host. Add it to the host config${Q}s ${Q}shared${Q} field.');
473
299
  }
@@ -483,26 +309,23 @@ function moduleFederationPlugin(config) {
483
309
  if (hasRemotes && hasExposes) throw new Error(`[${PLUGIN_NAME}] A single config cannot define both 'remotes' and 'exposes'. Split into two configs (one host, one remote) and run them as separate Rollipop processes.`);
484
310
  let resolvedConfig = null;
485
311
  let normalized = null;
486
- let broadcast = null;
487
312
  const exposesAbsolute = {};
488
313
  const sharedRoots = new Set(collectSharedNames(federationConfig));
489
- let debounceTimer = null;
490
314
  return {
491
315
  name: PLUGIN_NAME,
492
316
  config(pluginConfig) {
493
- const serializer = pluginConfig.serializer ??= {};
494
317
  if (hasExposes) {
495
- serializer.prelude = [];
496
- serializer.polyfills = [];
497
- return { dangerously_overrideRolldownOptions: (opts) => ({
318
+ pluginConfig.prelude = [];
319
+ pluginConfig.polyfills = [];
320
+ return { rolldownOptions: (opts, context) => ({
498
321
  input: opts.input,
499
322
  output: {
500
323
  ...opts.output,
501
- format: "iife"
324
+ format: context.buildType === "serve" ? "rollipop" : "iife"
502
325
  }
503
326
  }) };
504
327
  }
505
- if (hasRemotes && config.runtime?.implement != null) (serializer.polyfills ??= []).push({
328
+ if (hasRemotes && config.runtime?.implement != null) (pluginConfig.polyfills ??= []).push({
506
329
  type: "iife",
507
330
  code: config.runtime.implement
508
331
  });
@@ -530,10 +353,7 @@ function moduleFederationPlugin(config) {
530
353
  moduleType: "js"
531
354
  };
532
355
  if (id.startsWith(VIRTUAL_REMOTE_PROXY_PREFIX)) return {
533
- code: generateRemoteProxyCode({
534
- remoteId: id.slice(VIRTUAL_REMOTE_PROXY_PREFIX.length),
535
- reactAware: resolvedConfig?.mode !== "production"
536
- }),
356
+ code: generateRemoteProxyCode({ remoteId: id.slice(VIRTUAL_REMOTE_PROXY_PREFIX.length) }),
537
357
  moduleType: "js"
538
358
  };
539
359
  if (normalized == null) return null;
@@ -565,21 +385,7 @@ function moduleFederationPlugin(config) {
565
385
  return { code: magicString };
566
386
  }
567
387
  return null;
568
- } },
569
- watchChange() {
570
- if (!hasExposes || broadcast == null) return;
571
- if (debounceTimer != null) clearTimeout(debounceTimer);
572
- debounceTimer = setTimeout(() => {
573
- debounceTimer = null;
574
- broadcast?.();
575
- }, 100);
576
- },
577
- configureServer(server) {
578
- if (!hasExposes) return;
579
- broadcast = () => {
580
- server.hot.sendAll(HMR_EVENT, { name: federationConfig.name });
581
- };
582
- }
388
+ } }
583
389
  };
584
390
  }
585
391
  function collectSharedNames(config) {
package/dist/runtime.d.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  //#region src/runtime.d.ts
2
- interface ModuleFederationScriptLoader {
2
+ export interface ModuleFederationScriptLoader {
3
3
  loadScript(args: {
4
4
  scriptId: string;
5
5
  url: string;
6
6
  parentUrl?: string;
7
7
  }): Promise<void>;
8
8
  }
9
- interface RemoteEntryExports {
9
+ export interface RemoteEntryExports {
10
10
  init(shareScope: unknown, initScope?: unknown[]): void | Promise<void>;
11
11
  get(modulePath: string): () => Promise<unknown>;
12
12
  }
13
13
  declare global {
14
14
  var __rollipop_script_loader__: ModuleFederationScriptLoader | undefined;
15
15
  }
16
- //#endregion
17
- export { ModuleFederationScriptLoader, RemoteEntryExports };
16
+ //#endregion
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@rollipop/plugin-module-federation",
3
- "version": "1.0.0-alpha.28",
4
- "stableVersion": "1.0.0-alpha.23",
3
+ "version": "1.0.0-alpha.30",
5
4
  "homepage": "https://github.com/rollipop-dev/rollipop#readme",
6
5
  "bugs": {
7
6
  "url": "https://github.com/rollipop-dev/rollipop/issues"
@@ -37,17 +36,23 @@
37
36
  "build": "vp pack"
38
37
  },
39
38
  "dependencies": {
40
- "@module-federation/runtime": "2.5.1",
41
- "@module-federation/sdk": "2.5.1",
39
+ "@module-federation/runtime": "2.9.1",
42
40
  "dedent": "1.7.2",
43
- "es-toolkit": "1.47.0"
41
+ "es-toolkit": "1.52.0"
44
42
  },
45
43
  "devDependencies": {
46
- "rollipop": "1.0.0-alpha.28",
47
- "typescript": "6.0.3",
48
- "vite-plus": "0.1.24"
44
+ "@types/react": "19.2.14",
45
+ "@vitest/coverage-v8": "4.1.11",
46
+ "react": "19.2.3",
47
+ "rollipop": "1.0.0-alpha.30",
48
+ "typescript": "7.0.2",
49
+ "vite-plus": "0.3.3",
50
+ "vitest": "4.1.11"
49
51
  },
50
52
  "peerDependencies": {
51
- "rollipop": "1.0.0-alpha.28"
53
+ "rollipop": "1.0.0-alpha.30"
54
+ },
55
+ "installConfig": {
56
+ "hoistingLimits": "none"
52
57
  }
53
58
  }