@module-federation/vite 1.13.4 → 1.13.5

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
@@ -23,6 +23,8 @@ This plugin makes Module Federation work together with [Vite](https://vitejs.dev
23
23
 
24
24
  ### [More examples here](https://github.com/module-federation/vite/tree/main/examples)<br>
25
25
 
26
+ Includes a pure runtime host example in [`examples/vite-runtime-register`](./examples/vite-runtime-register).
27
+
26
28
  ## Try this crazy example with all these bundlers together
27
29
 
28
30
  <img src="./docs/multi-example.png"/>
@@ -168,6 +170,16 @@ const RemoteMFE = defineAsyncComponent( 👈
168
170
  </template>
169
171
  ```
170
172
 
173
+ ## ⚠️ `codeSplitting: false` is not supported
174
+
175
+ Do not set `build.rolldownOptions.output.codeSplitting` to `false` with this plugin — it will be **automatically ignored**.
176
+ Module federation requires chunk splitting to isolate shared dependencies and remote entries into separate chunks.
177
+
178
+ ## ⚠️ `manualChunks` is not supported
179
+
180
+ Do not use `build.rollupOptions.output.manualChunks` with this plugin — it will be **automatically ignored**.
181
+ Module federation transforms shared dependency imports with top-level `await`, and grouping these transformed modules into a single chunk creates circular async dependencies that cause the application to silently hang.
182
+
171
183
  ### So far so good 🎉
172
184
 
173
185
  Now you are ready to use Module Federation in Vite!
package/lib/index.cjs CHANGED
@@ -36,6 +36,7 @@ let _module_federation_dts_plugin = require("@module-federation/dts-plugin");
36
36
  let _module_federation_dts_plugin_core = require("@module-federation/dts-plugin/core");
37
37
  let node_module = require("node:module");
38
38
  let url = require("url");
39
+ let es_module_lexer = require("es-module-lexer");
39
40
  //#region src/utils/mapCodeToCodeWithSourcemap.ts
40
41
  async function mapCodeToCodeWithSourcemap(code) {
41
42
  const resolvedCode = await code;
@@ -151,7 +152,7 @@ function removePathFromNpmPackage(packageString) {
151
152
  return match ? match[0] : packageString;
152
153
  }
153
154
  /**
154
- * Detect whether the current bundler is Rolldown (Vite 8+) by checking
155
+ * Detect whether the current runtime is Vite 8+ (with rolldown internally) by checking
155
156
  * for `meta.rolldownVersion` on the plugin hook context.
156
157
  */
157
158
  function getIsRolldown(ctx) {
@@ -380,15 +381,17 @@ function checkAliasConflicts(options) {
380
381
  };
381
382
  }
382
383
  //#endregion
383
- //#region src/plugins/pluginDevProxyModuleTopLevelAwait.ts
384
- /**
385
- * Solve the problem that dev mode dependency prebunding does not support top-level await syntax
386
- */
384
+ //#region src/utils/loadWalk.ts
387
385
  let walkPromise = null;
388
386
  function loadWalk() {
389
387
  walkPromise ||= import("estree-walker").then(({ walk }) => walk);
390
388
  return walkPromise;
391
389
  }
390
+ //#endregion
391
+ //#region src/plugins/pluginDevProxyModuleTopLevelAwait.ts
392
+ /**
393
+ * Solve the problem that dev mode dependency prebunding does not support top-level await syntax
394
+ */
392
395
  function PluginDevProxyModuleTopLevelAwait() {
393
396
  const filterFunction = (0, _rollup_pluginutils.createFilter)();
394
397
  const processedFlag = "/* already-processed-by-dev-proxy-module-top-level-await */";
@@ -1197,7 +1200,7 @@ function generateRemotes(id, command, isRolldown) {
1197
1200
  const importLine = command === "build" ? getRuntimeInitPromiseBootstrapCode() : useESM ? `${getRuntimeInitBootstrapCode()}
1198
1201
  const { initPromise } = globalThis[globalKey];` : `const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")`;
1199
1202
  const awaitOrPlaceholder = useESM ? "await " : "/*mf top-level-await placeholder replacement mf*/";
1200
- const exportLine = command === "serve" && useESM ? "export default exportModule.default ?? exportModule" : useESM ? "export default exportModule" : "module.exports = exportModule";
1203
+ const exportLine = command === "serve" && useESM ? "export const __moduleExports = exportModule;\nexport default exportModule.default ?? exportModule" : useESM ? "export default exportModule" : "module.exports = exportModule";
1201
1204
  return `
1202
1205
  ${importLine}
1203
1206
  const res = initPromise.then(runtime => runtime.loadRemote(${JSON.stringify(id)}))
@@ -2233,6 +2236,265 @@ function pluginProxyRemotes_default(options) {
2233
2236
  };
2234
2237
  }
2235
2238
  //#endregion
2239
+ //#region src/plugins/pluginRemoteNamedExports.ts
2240
+ /**
2241
+ * Transforms consumer-side imports of remote modules so that named exports
2242
+ * are accessible even when the bundler does not support syntheticNamedExports
2243
+ * (Rolldown / Vite 8+).
2244
+ *
2245
+ * The remote proxy module exports:
2246
+ * export const __moduleExports = exportModule; // full namespace
2247
+ * export default exportModule.default ?? exportModule; // unwrapped default
2248
+ *
2249
+ * This plugin rewrites consumer code:
2250
+ * import { foo } from "remote/xxx"
2251
+ * → import { __moduleExports as __mf_ns_0 } from "remote/xxx"; const { foo } = __mf_ns_0;
2252
+ *
2253
+ * import("remote/xxx")
2254
+ * → import("remote/xxx").then(…) // spreads __moduleExports into namespace
2255
+ *
2256
+ * NOTE: `export * from "remote/xxx"` is not supported — Rolldown cannot
2257
+ * statically resolve the set of exported names from a federated remote at
2258
+ * build time. Use explicit named re-exports instead.
2259
+ */
2260
+ const JS_EXTENSIONS_RE = /\.(?:[mc]?[jt]sx?|vue|svelte)(?:\?|$)/;
2261
+ function wrapDynamicImport(original) {
2262
+ return `${original}.then(function(__mf_m__) {\n if (!__mf_m__ || !__mf_m__.__moduleExports) return __mf_m__;\n var __mf_ns__ = Object.create(null);\n Object.defineProperty(__mf_ns__, Symbol.toStringTag, { value: "Module" });\n var __mf_e__ = __mf_m__.__moduleExports;\n Object.keys(__mf_e__).forEach(function(k) { if (k !== "__esModule") __mf_ns__[k] = __mf_e__[k] });\n if ("default" in __mf_m__) __mf_ns__.default = __mf_m__.default;\n return __mf_ns__;\n})`;
2263
+ }
2264
+ function applyRewrites(code, imports, id) {
2265
+ if (imports.length === 0) return;
2266
+ const ms = new magic_string.default(code);
2267
+ let changed = false;
2268
+ let counter = 0;
2269
+ for (const imp of imports) switch (imp.kind) {
2270
+ case "static": {
2271
+ const src = JSON.stringify(imp.source);
2272
+ if (imp.namespaceLocal && !imp.defaultLocal && imp.named.length === 0) ms.overwrite(imp.start, imp.end, `import { __moduleExports as ${imp.namespaceLocal} } from ${src};`);
2273
+ else {
2274
+ const nsId = `__mf_ns_${counter++}`;
2275
+ const importParts = [];
2276
+ if (imp.defaultLocal) importParts.push(`default as ${imp.defaultLocal}`);
2277
+ importParts.push(`__moduleExports as ${nsId}`);
2278
+ const destructParts = imp.named.map((s) => s.imported === s.local ? s.local : `${s.imported}: ${s.local}`);
2279
+ let rewrite = `import { ${importParts.join(", ")} } from ${src};`;
2280
+ if (destructParts.length > 0) rewrite += `\nconst { ${destructParts.join(", ")} } = ${nsId};`;
2281
+ ms.overwrite(imp.start, imp.end, rewrite);
2282
+ }
2283
+ changed = true;
2284
+ break;
2285
+ }
2286
+ case "reexport": {
2287
+ const src = JSON.stringify(imp.source);
2288
+ const nsId = `__mf_ns_${counter++}`;
2289
+ const vars = imp.specifiers.map((s) => {
2290
+ const tmp = `__mf_re_${counter++}`;
2291
+ return {
2292
+ ...s,
2293
+ tmp
2294
+ };
2295
+ });
2296
+ const importLine = `import { __moduleExports as ${nsId} } from ${src};`;
2297
+ const varLines = vars.map((v) => `const ${v.tmp} = ${nsId}[${JSON.stringify(v.local)}];`).join("\n");
2298
+ const exportLine = `export { ${vars.map((v) => `${v.tmp} as ${v.exported}`).join(", ")} };`;
2299
+ ms.overwrite(imp.start, imp.end, `${importLine}\n${varLines}\n${exportLine}`);
2300
+ changed = true;
2301
+ break;
2302
+ }
2303
+ case "export-all":
2304
+ console.warn(`[module-federation] "export * from '${imp.source}'" is not supported with Rolldown — use explicit named re-exports instead. (${id})`);
2305
+ break;
2306
+ case "dynamic":
2307
+ ms.overwrite(imp.start, imp.end, wrapDynamicImport(imp.originalText));
2308
+ changed = true;
2309
+ break;
2310
+ }
2311
+ if (!changed) return;
2312
+ return {
2313
+ code: ms.toString(),
2314
+ map: ms.generateMap({ hires: true })
2315
+ };
2316
+ }
2317
+ async function collectFromAST(ast, code, isRemoteImport) {
2318
+ const walk = await loadWalk();
2319
+ const result = [];
2320
+ walk(ast, { enter(node) {
2321
+ if (node.type === "ImportDeclaration" && node.source?.value) {
2322
+ if (!isRemoteImport(node.source.value)) return;
2323
+ const specifiers = node.specifiers || [];
2324
+ const named = specifiers.filter((s) => s.type === "ImportSpecifier" && s.importKind !== "type").map((s) => ({
2325
+ imported: s.imported.name ?? s.imported.value,
2326
+ local: s.local.name
2327
+ }));
2328
+ const defaultSpec = specifiers.find((s) => s.type === "ImportDefaultSpecifier");
2329
+ const nsSpec = specifiers.find((s) => s.type === "ImportNamespaceSpecifier");
2330
+ if (named.length === 0 && !nsSpec) return;
2331
+ result.push({
2332
+ kind: "static",
2333
+ source: node.source.value,
2334
+ start: node.start,
2335
+ end: node.end,
2336
+ named,
2337
+ defaultLocal: defaultSpec?.local.name,
2338
+ namespaceLocal: nsSpec?.local.name
2339
+ });
2340
+ }
2341
+ if (node.type === "ExportNamedDeclaration" && node.source?.value && isRemoteImport(node.source.value)) {
2342
+ const specifiers = (node.specifiers || []).filter((s) => s.exportKind !== "type").map((s) => ({
2343
+ local: s.local.name ?? s.local.value,
2344
+ exported: s.exported.name ?? s.exported.value
2345
+ }));
2346
+ if (specifiers.length === 0) return;
2347
+ result.push({
2348
+ kind: "reexport",
2349
+ source: node.source.value,
2350
+ start: node.start,
2351
+ end: node.end,
2352
+ specifiers
2353
+ });
2354
+ }
2355
+ if (node.type === "ExportAllDeclaration" && node.source?.value && isRemoteImport(node.source.value)) {
2356
+ this.skip();
2357
+ result.push({
2358
+ kind: "export-all",
2359
+ source: node.source.value,
2360
+ start: node.start,
2361
+ end: node.end
2362
+ });
2363
+ }
2364
+ if (node.type === "ImportExpression") {
2365
+ const source = node.source;
2366
+ if (source.type !== "Literal" && source.type !== "StringLiteral" && source.type !== "TemplateLiteral") return;
2367
+ const value = source.type === "TemplateLiteral" ? source.quasis?.length === 1 ? source.quasis[0].value?.cooked : void 0 : source.value;
2368
+ if (!value || !isRemoteImport(value)) return;
2369
+ result.push({
2370
+ kind: "dynamic",
2371
+ start: node.start,
2372
+ end: node.end,
2373
+ originalText: code.slice(node.start, node.end)
2374
+ });
2375
+ }
2376
+ } });
2377
+ return result;
2378
+ }
2379
+ async function collectFromEsLexer(code, isRemoteImport) {
2380
+ await es_module_lexer.init;
2381
+ let imports;
2382
+ try {
2383
+ [imports] = (0, es_module_lexer.parse)(code);
2384
+ } catch {
2385
+ return;
2386
+ }
2387
+ const result = [];
2388
+ for (const imp of imports) {
2389
+ if (imp.d === -2) continue;
2390
+ if (!imp.n || !isRemoteImport(imp.n)) continue;
2391
+ const stmtText = code.slice(imp.ss, imp.se);
2392
+ if (imp.d >= 0) {
2393
+ result.push({
2394
+ kind: "dynamic",
2395
+ start: imp.ss,
2396
+ end: imp.se,
2397
+ originalText: stmtText
2398
+ });
2399
+ continue;
2400
+ }
2401
+ if (/^\s*export\s*\*\s/.test(stmtText)) {
2402
+ result.push({
2403
+ kind: "export-all",
2404
+ source: imp.n,
2405
+ start: imp.ss,
2406
+ end: imp.se
2407
+ });
2408
+ continue;
2409
+ }
2410
+ if (/^\s*export\s/.test(stmtText)) {
2411
+ const braceMatch = stmtText.match(/\{([^}]*)\}/);
2412
+ if (!braceMatch) continue;
2413
+ const specs = braceMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2414
+ if (specs.length === 0) continue;
2415
+ const specifiers = specs.map((s) => {
2416
+ const asMatch = s.match(/^(\w+)\s+as\s+(\w+)$/);
2417
+ return {
2418
+ local: asMatch ? asMatch[1] : s,
2419
+ exported: asMatch ? asMatch[2] : s
2420
+ };
2421
+ });
2422
+ result.push({
2423
+ kind: "reexport",
2424
+ source: imp.n,
2425
+ start: imp.ss,
2426
+ end: imp.se,
2427
+ specifiers
2428
+ });
2429
+ continue;
2430
+ }
2431
+ const importMatch = stmtText.match(/^import\s+([\s\S]*?)\s+from\s/);
2432
+ if (!importMatch) continue;
2433
+ const specifiersPart = importMatch[1].trim();
2434
+ if (/^type\s/.test(specifiersPart)) continue;
2435
+ const nsMatch = specifiersPart.match(/^\*\s+as\s+(\w+)$/);
2436
+ if (nsMatch) {
2437
+ result.push({
2438
+ kind: "static",
2439
+ source: imp.n,
2440
+ start: imp.ss,
2441
+ end: imp.se,
2442
+ named: [],
2443
+ namespaceLocal: nsMatch[1]
2444
+ });
2445
+ continue;
2446
+ }
2447
+ const braceMatch = specifiersPart.match(/\{([^}]*)\}/);
2448
+ if (!braceMatch) continue;
2449
+ const namedSpecifiers = braceMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("type "));
2450
+ if (namedSpecifiers.length === 0) continue;
2451
+ const defaultMatch = specifiersPart.match(/^(\w+)\s*,/);
2452
+ const named = namedSpecifiers.map((s) => {
2453
+ const asMatch = s.match(/^(\w+)\s+as\s+(\w+)$/);
2454
+ return {
2455
+ imported: asMatch ? asMatch[1] : s,
2456
+ local: asMatch ? asMatch[2] : s
2457
+ };
2458
+ });
2459
+ result.push({
2460
+ kind: "static",
2461
+ source: imp.n,
2462
+ start: imp.ss,
2463
+ end: imp.se,
2464
+ named,
2465
+ defaultLocal: defaultMatch?.[1]
2466
+ });
2467
+ }
2468
+ return result;
2469
+ }
2470
+ function pluginRemoteNamedExports(options) {
2471
+ const remoteNames = Object.keys(options.remotes);
2472
+ let rolldown;
2473
+ function isRemoteImport(source) {
2474
+ return remoteNames.some((name) => source === name || source.startsWith(name + "/"));
2475
+ }
2476
+ return {
2477
+ name: "module-federation-remote-named-exports",
2478
+ enforce: "pre",
2479
+ async transform(code, id) {
2480
+ rolldown ??= getIsRolldown(this);
2481
+ if (!rolldown) return;
2482
+ if (remoteNames.length === 0) return;
2483
+ if (id.includes("__loadRemote__") || id.includes("__loadShare__")) return;
2484
+ if (!JS_EXTENSIONS_RE.test(id)) return;
2485
+ if (!remoteNames.some((name) => code.includes(name))) return;
2486
+ let imports;
2487
+ try {
2488
+ imports = await collectFromAST(this.parse(code), code, isRemoteImport);
2489
+ } catch {
2490
+ imports = await collectFromEsLexer(code, isRemoteImport);
2491
+ }
2492
+ if (!imports) return;
2493
+ return applyRewrites(code, imports, id);
2494
+ }
2495
+ };
2496
+ }
2497
+ //#endregion
2236
2498
  //#region src/utils/PromiseStore.ts
2237
2499
  /**
2238
2500
  * example:
@@ -2339,7 +2601,7 @@ function proxySharedModule(options) {
2339
2601
  },
2340
2602
  configResolved(config) {
2341
2603
  _config = config;
2342
- const isRolldown = !!config.experimental?.rolldownDev;
2604
+ const isRolldown = getIsRolldown(this);
2343
2605
  Object.keys(shared).forEach((key) => {
2344
2606
  if (key.endsWith("/")) return;
2345
2607
  if (isVinext && key === "react") {
@@ -2650,6 +2912,7 @@ function federation(mfUserOptions) {
2650
2912
  virtualExposesId
2651
2913
  }),
2652
2914
  pluginProxyRemotes_default(options),
2915
+ pluginRemoteNamedExports(options),
2653
2916
  ...pluginModuleParseEnd_default((id) => {
2654
2917
  return id.includes(getHostAutoInitImportId()) || id.includes(remoteEntryId) || id.includes(virtualExposesId) || id.includes(getLocalSharedImportMapPath());
2655
2918
  }, {
@@ -2685,19 +2948,19 @@ function federation(mfUserOptions) {
2685
2948
  warnedAboutCodeSplitting = true;
2686
2949
  mfWarn("Ignoring `build.rolldownOptions.output.codeSplitting = false` because module federation requires chunk splitting.");
2687
2950
  };
2951
+ let warnedAboutManualChunks = false;
2688
2952
  const applyManualChunks = (output) => {
2689
2953
  ensureCodeSplitting(output);
2690
- const existingManualChunks = output.manualChunks;
2954
+ if (output.manualChunks && !warnedAboutManualChunks) {
2955
+ warnedAboutManualChunks = true;
2956
+ mfWarn("Ignoring `build.rollupOptions.output.manualChunks` because it conflicts with module federation. Module federation transforms shared dependency imports with top-level await, and grouping these transformed modules into a single chunk creates circular async dependencies that cause the application to silently hang.");
2957
+ }
2691
2958
  output.manualChunks = function(id) {
2692
2959
  if (id.includes(runtimeInitId)) return "runtimeInit";
2693
2960
  if (id.includes("__loadShare__")) {
2694
2961
  const match = id.match(/([^/\\]+__loadShare__[^/\\]+)/);
2695
2962
  return match ? match[1] : "loadShare";
2696
2963
  }
2697
- if (typeof existingManualChunks === "function") return existingManualChunks.apply(this, arguments);
2698
- if (existingManualChunks && typeof existingManualChunks === "object") {
2699
- for (const [key, ids] of Object.entries(existingManualChunks)) if (Array.isArray(ids) && ids.some((v) => id.includes(v))) return key;
2700
- }
2701
2964
  };
2702
2965
  };
2703
2966
  config.build.rollupOptions = config.build.rollupOptions || {};
@@ -2729,6 +2992,7 @@ function federation(mfUserOptions) {
2729
2992
  * @see https://rollupjs.org/plugin-development/#synthetic-named-exports
2730
2993
  */
2731
2994
  code = code.replace("export default exportModule", "export const __moduleExports = exportModule;\nexport default exportModule.__esModule ? exportModule.default : exportModule");
2995
+ if (getIsRolldown(this)) return { code };
2732
2996
  return {
2733
2997
  code,
2734
2998
  syntheticNamedExports: "__moduleExports"
package/lib/index.mjs CHANGED
@@ -11,6 +11,7 @@ import { normalizeOptions } from "@module-federation/sdk";
11
11
  import { consumeTypesAPI, generateTypesAPI, isTSProject, normalizeConsumeTypesOptions, normalizeDtsOptions, normalizeGenerateTypesOptions } from "@module-federation/dts-plugin";
12
12
  import { rpc } from "@module-federation/dts-plugin/core";
13
13
  import { fileURLToPath } from "url";
14
+ import { init, parse as parse$1 } from "es-module-lexer";
14
15
  //#region \0rolldown/runtime.js
15
16
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
16
17
  //#endregion
@@ -129,7 +130,7 @@ function removePathFromNpmPackage(packageString) {
129
130
  return match ? match[0] : packageString;
130
131
  }
131
132
  /**
132
- * Detect whether the current bundler is Rolldown (Vite 8+) by checking
133
+ * Detect whether the current runtime is Vite 8+ (with rolldown internally) by checking
133
134
  * for `meta.rolldownVersion` on the plugin hook context.
134
135
  */
135
136
  function getIsRolldown(ctx) {
@@ -358,15 +359,17 @@ function checkAliasConflicts(options) {
358
359
  };
359
360
  }
360
361
  //#endregion
361
- //#region src/plugins/pluginDevProxyModuleTopLevelAwait.ts
362
- /**
363
- * Solve the problem that dev mode dependency prebunding does not support top-level await syntax
364
- */
362
+ //#region src/utils/loadWalk.ts
365
363
  let walkPromise = null;
366
364
  function loadWalk() {
367
365
  walkPromise ||= import("estree-walker").then(({ walk }) => walk);
368
366
  return walkPromise;
369
367
  }
368
+ //#endregion
369
+ //#region src/plugins/pluginDevProxyModuleTopLevelAwait.ts
370
+ /**
371
+ * Solve the problem that dev mode dependency prebunding does not support top-level await syntax
372
+ */
370
373
  function PluginDevProxyModuleTopLevelAwait() {
371
374
  const filterFunction = createFilter();
372
375
  const processedFlag = "/* already-processed-by-dev-proxy-module-top-level-await */";
@@ -1174,7 +1177,7 @@ function generateRemotes(id, command, isRolldown) {
1174
1177
  const importLine = command === "build" ? getRuntimeInitPromiseBootstrapCode() : useESM ? `${getRuntimeInitBootstrapCode()}
1175
1178
  const { initPromise } = globalThis[globalKey];` : `const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")`;
1176
1179
  const awaitOrPlaceholder = useESM ? "await " : "/*mf top-level-await placeholder replacement mf*/";
1177
- const exportLine = command === "serve" && useESM ? "export default exportModule.default ?? exportModule" : useESM ? "export default exportModule" : "module.exports = exportModule";
1180
+ const exportLine = command === "serve" && useESM ? "export const __moduleExports = exportModule;\nexport default exportModule.default ?? exportModule" : useESM ? "export default exportModule" : "module.exports = exportModule";
1178
1181
  return `
1179
1182
  ${importLine}
1180
1183
  const res = initPromise.then(runtime => runtime.loadRemote(${JSON.stringify(id)}))
@@ -2210,6 +2213,265 @@ function pluginProxyRemotes_default(options) {
2210
2213
  };
2211
2214
  }
2212
2215
  //#endregion
2216
+ //#region src/plugins/pluginRemoteNamedExports.ts
2217
+ /**
2218
+ * Transforms consumer-side imports of remote modules so that named exports
2219
+ * are accessible even when the bundler does not support syntheticNamedExports
2220
+ * (Rolldown / Vite 8+).
2221
+ *
2222
+ * The remote proxy module exports:
2223
+ * export const __moduleExports = exportModule; // full namespace
2224
+ * export default exportModule.default ?? exportModule; // unwrapped default
2225
+ *
2226
+ * This plugin rewrites consumer code:
2227
+ * import { foo } from "remote/xxx"
2228
+ * → import { __moduleExports as __mf_ns_0 } from "remote/xxx"; const { foo } = __mf_ns_0;
2229
+ *
2230
+ * import("remote/xxx")
2231
+ * → import("remote/xxx").then(…) // spreads __moduleExports into namespace
2232
+ *
2233
+ * NOTE: `export * from "remote/xxx"` is not supported — Rolldown cannot
2234
+ * statically resolve the set of exported names from a federated remote at
2235
+ * build time. Use explicit named re-exports instead.
2236
+ */
2237
+ const JS_EXTENSIONS_RE = /\.(?:[mc]?[jt]sx?|vue|svelte)(?:\?|$)/;
2238
+ function wrapDynamicImport(original) {
2239
+ return `${original}.then(function(__mf_m__) {\n if (!__mf_m__ || !__mf_m__.__moduleExports) return __mf_m__;\n var __mf_ns__ = Object.create(null);\n Object.defineProperty(__mf_ns__, Symbol.toStringTag, { value: "Module" });\n var __mf_e__ = __mf_m__.__moduleExports;\n Object.keys(__mf_e__).forEach(function(k) { if (k !== "__esModule") __mf_ns__[k] = __mf_e__[k] });\n if ("default" in __mf_m__) __mf_ns__.default = __mf_m__.default;\n return __mf_ns__;\n})`;
2240
+ }
2241
+ function applyRewrites(code, imports, id) {
2242
+ if (imports.length === 0) return;
2243
+ const ms = new MagicString(code);
2244
+ let changed = false;
2245
+ let counter = 0;
2246
+ for (const imp of imports) switch (imp.kind) {
2247
+ case "static": {
2248
+ const src = JSON.stringify(imp.source);
2249
+ if (imp.namespaceLocal && !imp.defaultLocal && imp.named.length === 0) ms.overwrite(imp.start, imp.end, `import { __moduleExports as ${imp.namespaceLocal} } from ${src};`);
2250
+ else {
2251
+ const nsId = `__mf_ns_${counter++}`;
2252
+ const importParts = [];
2253
+ if (imp.defaultLocal) importParts.push(`default as ${imp.defaultLocal}`);
2254
+ importParts.push(`__moduleExports as ${nsId}`);
2255
+ const destructParts = imp.named.map((s) => s.imported === s.local ? s.local : `${s.imported}: ${s.local}`);
2256
+ let rewrite = `import { ${importParts.join(", ")} } from ${src};`;
2257
+ if (destructParts.length > 0) rewrite += `\nconst { ${destructParts.join(", ")} } = ${nsId};`;
2258
+ ms.overwrite(imp.start, imp.end, rewrite);
2259
+ }
2260
+ changed = true;
2261
+ break;
2262
+ }
2263
+ case "reexport": {
2264
+ const src = JSON.stringify(imp.source);
2265
+ const nsId = `__mf_ns_${counter++}`;
2266
+ const vars = imp.specifiers.map((s) => {
2267
+ const tmp = `__mf_re_${counter++}`;
2268
+ return {
2269
+ ...s,
2270
+ tmp
2271
+ };
2272
+ });
2273
+ const importLine = `import { __moduleExports as ${nsId} } from ${src};`;
2274
+ const varLines = vars.map((v) => `const ${v.tmp} = ${nsId}[${JSON.stringify(v.local)}];`).join("\n");
2275
+ const exportLine = `export { ${vars.map((v) => `${v.tmp} as ${v.exported}`).join(", ")} };`;
2276
+ ms.overwrite(imp.start, imp.end, `${importLine}\n${varLines}\n${exportLine}`);
2277
+ changed = true;
2278
+ break;
2279
+ }
2280
+ case "export-all":
2281
+ console.warn(`[module-federation] "export * from '${imp.source}'" is not supported with Rolldown — use explicit named re-exports instead. (${id})`);
2282
+ break;
2283
+ case "dynamic":
2284
+ ms.overwrite(imp.start, imp.end, wrapDynamicImport(imp.originalText));
2285
+ changed = true;
2286
+ break;
2287
+ }
2288
+ if (!changed) return;
2289
+ return {
2290
+ code: ms.toString(),
2291
+ map: ms.generateMap({ hires: true })
2292
+ };
2293
+ }
2294
+ async function collectFromAST(ast, code, isRemoteImport) {
2295
+ const walk = await loadWalk();
2296
+ const result = [];
2297
+ walk(ast, { enter(node) {
2298
+ if (node.type === "ImportDeclaration" && node.source?.value) {
2299
+ if (!isRemoteImport(node.source.value)) return;
2300
+ const specifiers = node.specifiers || [];
2301
+ const named = specifiers.filter((s) => s.type === "ImportSpecifier" && s.importKind !== "type").map((s) => ({
2302
+ imported: s.imported.name ?? s.imported.value,
2303
+ local: s.local.name
2304
+ }));
2305
+ const defaultSpec = specifiers.find((s) => s.type === "ImportDefaultSpecifier");
2306
+ const nsSpec = specifiers.find((s) => s.type === "ImportNamespaceSpecifier");
2307
+ if (named.length === 0 && !nsSpec) return;
2308
+ result.push({
2309
+ kind: "static",
2310
+ source: node.source.value,
2311
+ start: node.start,
2312
+ end: node.end,
2313
+ named,
2314
+ defaultLocal: defaultSpec?.local.name,
2315
+ namespaceLocal: nsSpec?.local.name
2316
+ });
2317
+ }
2318
+ if (node.type === "ExportNamedDeclaration" && node.source?.value && isRemoteImport(node.source.value)) {
2319
+ const specifiers = (node.specifiers || []).filter((s) => s.exportKind !== "type").map((s) => ({
2320
+ local: s.local.name ?? s.local.value,
2321
+ exported: s.exported.name ?? s.exported.value
2322
+ }));
2323
+ if (specifiers.length === 0) return;
2324
+ result.push({
2325
+ kind: "reexport",
2326
+ source: node.source.value,
2327
+ start: node.start,
2328
+ end: node.end,
2329
+ specifiers
2330
+ });
2331
+ }
2332
+ if (node.type === "ExportAllDeclaration" && node.source?.value && isRemoteImport(node.source.value)) {
2333
+ this.skip();
2334
+ result.push({
2335
+ kind: "export-all",
2336
+ source: node.source.value,
2337
+ start: node.start,
2338
+ end: node.end
2339
+ });
2340
+ }
2341
+ if (node.type === "ImportExpression") {
2342
+ const source = node.source;
2343
+ if (source.type !== "Literal" && source.type !== "StringLiteral" && source.type !== "TemplateLiteral") return;
2344
+ const value = source.type === "TemplateLiteral" ? source.quasis?.length === 1 ? source.quasis[0].value?.cooked : void 0 : source.value;
2345
+ if (!value || !isRemoteImport(value)) return;
2346
+ result.push({
2347
+ kind: "dynamic",
2348
+ start: node.start,
2349
+ end: node.end,
2350
+ originalText: code.slice(node.start, node.end)
2351
+ });
2352
+ }
2353
+ } });
2354
+ return result;
2355
+ }
2356
+ async function collectFromEsLexer(code, isRemoteImport) {
2357
+ await init;
2358
+ let imports;
2359
+ try {
2360
+ [imports] = parse$1(code);
2361
+ } catch {
2362
+ return;
2363
+ }
2364
+ const result = [];
2365
+ for (const imp of imports) {
2366
+ if (imp.d === -2) continue;
2367
+ if (!imp.n || !isRemoteImport(imp.n)) continue;
2368
+ const stmtText = code.slice(imp.ss, imp.se);
2369
+ if (imp.d >= 0) {
2370
+ result.push({
2371
+ kind: "dynamic",
2372
+ start: imp.ss,
2373
+ end: imp.se,
2374
+ originalText: stmtText
2375
+ });
2376
+ continue;
2377
+ }
2378
+ if (/^\s*export\s*\*\s/.test(stmtText)) {
2379
+ result.push({
2380
+ kind: "export-all",
2381
+ source: imp.n,
2382
+ start: imp.ss,
2383
+ end: imp.se
2384
+ });
2385
+ continue;
2386
+ }
2387
+ if (/^\s*export\s/.test(stmtText)) {
2388
+ const braceMatch = stmtText.match(/\{([^}]*)\}/);
2389
+ if (!braceMatch) continue;
2390
+ const specs = braceMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2391
+ if (specs.length === 0) continue;
2392
+ const specifiers = specs.map((s) => {
2393
+ const asMatch = s.match(/^(\w+)\s+as\s+(\w+)$/);
2394
+ return {
2395
+ local: asMatch ? asMatch[1] : s,
2396
+ exported: asMatch ? asMatch[2] : s
2397
+ };
2398
+ });
2399
+ result.push({
2400
+ kind: "reexport",
2401
+ source: imp.n,
2402
+ start: imp.ss,
2403
+ end: imp.se,
2404
+ specifiers
2405
+ });
2406
+ continue;
2407
+ }
2408
+ const importMatch = stmtText.match(/^import\s+([\s\S]*?)\s+from\s/);
2409
+ if (!importMatch) continue;
2410
+ const specifiersPart = importMatch[1].trim();
2411
+ if (/^type\s/.test(specifiersPart)) continue;
2412
+ const nsMatch = specifiersPart.match(/^\*\s+as\s+(\w+)$/);
2413
+ if (nsMatch) {
2414
+ result.push({
2415
+ kind: "static",
2416
+ source: imp.n,
2417
+ start: imp.ss,
2418
+ end: imp.se,
2419
+ named: [],
2420
+ namespaceLocal: nsMatch[1]
2421
+ });
2422
+ continue;
2423
+ }
2424
+ const braceMatch = specifiersPart.match(/\{([^}]*)\}/);
2425
+ if (!braceMatch) continue;
2426
+ const namedSpecifiers = braceMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("type "));
2427
+ if (namedSpecifiers.length === 0) continue;
2428
+ const defaultMatch = specifiersPart.match(/^(\w+)\s*,/);
2429
+ const named = namedSpecifiers.map((s) => {
2430
+ const asMatch = s.match(/^(\w+)\s+as\s+(\w+)$/);
2431
+ return {
2432
+ imported: asMatch ? asMatch[1] : s,
2433
+ local: asMatch ? asMatch[2] : s
2434
+ };
2435
+ });
2436
+ result.push({
2437
+ kind: "static",
2438
+ source: imp.n,
2439
+ start: imp.ss,
2440
+ end: imp.se,
2441
+ named,
2442
+ defaultLocal: defaultMatch?.[1]
2443
+ });
2444
+ }
2445
+ return result;
2446
+ }
2447
+ function pluginRemoteNamedExports(options) {
2448
+ const remoteNames = Object.keys(options.remotes);
2449
+ let rolldown;
2450
+ function isRemoteImport(source) {
2451
+ return remoteNames.some((name) => source === name || source.startsWith(name + "/"));
2452
+ }
2453
+ return {
2454
+ name: "module-federation-remote-named-exports",
2455
+ enforce: "pre",
2456
+ async transform(code, id) {
2457
+ rolldown ??= getIsRolldown(this);
2458
+ if (!rolldown) return;
2459
+ if (remoteNames.length === 0) return;
2460
+ if (id.includes("__loadRemote__") || id.includes("__loadShare__")) return;
2461
+ if (!JS_EXTENSIONS_RE.test(id)) return;
2462
+ if (!remoteNames.some((name) => code.includes(name))) return;
2463
+ let imports;
2464
+ try {
2465
+ imports = await collectFromAST(this.parse(code), code, isRemoteImport);
2466
+ } catch {
2467
+ imports = await collectFromEsLexer(code, isRemoteImport);
2468
+ }
2469
+ if (!imports) return;
2470
+ return applyRewrites(code, imports, id);
2471
+ }
2472
+ };
2473
+ }
2474
+ //#endregion
2213
2475
  //#region src/utils/PromiseStore.ts
2214
2476
  /**
2215
2477
  * example:
@@ -2316,7 +2578,7 @@ function proxySharedModule(options) {
2316
2578
  },
2317
2579
  configResolved(config) {
2318
2580
  _config = config;
2319
- const isRolldown = !!config.experimental?.rolldownDev;
2581
+ const isRolldown = getIsRolldown(this);
2320
2582
  Object.keys(shared).forEach((key) => {
2321
2583
  if (key.endsWith("/")) return;
2322
2584
  if (isVinext && key === "react") {
@@ -2627,6 +2889,7 @@ function federation(mfUserOptions) {
2627
2889
  virtualExposesId
2628
2890
  }),
2629
2891
  pluginProxyRemotes_default(options),
2892
+ pluginRemoteNamedExports(options),
2630
2893
  ...pluginModuleParseEnd_default((id) => {
2631
2894
  return id.includes(getHostAutoInitImportId()) || id.includes(remoteEntryId) || id.includes(virtualExposesId) || id.includes(getLocalSharedImportMapPath());
2632
2895
  }, {
@@ -2662,19 +2925,19 @@ function federation(mfUserOptions) {
2662
2925
  warnedAboutCodeSplitting = true;
2663
2926
  mfWarn("Ignoring `build.rolldownOptions.output.codeSplitting = false` because module federation requires chunk splitting.");
2664
2927
  };
2928
+ let warnedAboutManualChunks = false;
2665
2929
  const applyManualChunks = (output) => {
2666
2930
  ensureCodeSplitting(output);
2667
- const existingManualChunks = output.manualChunks;
2931
+ if (output.manualChunks && !warnedAboutManualChunks) {
2932
+ warnedAboutManualChunks = true;
2933
+ mfWarn("Ignoring `build.rollupOptions.output.manualChunks` because it conflicts with module federation. Module federation transforms shared dependency imports with top-level await, and grouping these transformed modules into a single chunk creates circular async dependencies that cause the application to silently hang.");
2934
+ }
2668
2935
  output.manualChunks = function(id) {
2669
2936
  if (id.includes(runtimeInitId)) return "runtimeInit";
2670
2937
  if (id.includes("__loadShare__")) {
2671
2938
  const match = id.match(/([^/\\]+__loadShare__[^/\\]+)/);
2672
2939
  return match ? match[1] : "loadShare";
2673
2940
  }
2674
- if (typeof existingManualChunks === "function") return existingManualChunks.apply(this, arguments);
2675
- if (existingManualChunks && typeof existingManualChunks === "object") {
2676
- for (const [key, ids] of Object.entries(existingManualChunks)) if (Array.isArray(ids) && ids.some((v) => id.includes(v))) return key;
2677
- }
2678
2941
  };
2679
2942
  };
2680
2943
  config.build.rollupOptions = config.build.rollupOptions || {};
@@ -2706,6 +2969,7 @@ function federation(mfUserOptions) {
2706
2969
  * @see https://rollupjs.org/plugin-development/#synthetic-named-exports
2707
2970
  */
2708
2971
  code = code.replace("export default exportModule", "export const __moduleExports = exportModule;\nexport default exportModule.__esModule ? exportModule.default : exportModule");
2972
+ if (getIsRolldown(this)) return { code };
2709
2973
  return {
2710
2974
  code,
2711
2975
  syntheticNamedExports: "__moduleExports"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@module-federation/vite",
3
- "version": "1.13.4",
3
+ "version": "1.13.5",
4
4
  "description": "Vite plugin for Module Federation",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",
@@ -30,10 +30,13 @@
30
30
  "dev-rv": "pnpm clean && pnpm -filter 'examples-rust-vite*' run dev",
31
31
  "preview-rv": "pnpm clean && pnpm -filter 'examples-rust-vite*' run preview",
32
32
  "dev-vv": "pnpm clean && pnpm -filter 'examples-vite-vite*' run dev",
33
- "preview-vv": "pnpm clean && pnpm -filter 'examples-vite-vite*' --parallel run preview",
33
+ "preview-vv": "pnpm clean && pnpm run build:shared-lib && pnpm -filter 'examples-vite-vite*' --parallel run preview",
34
+ "preview-vv:ci": "pnpm run build:shared-lib && pnpm -filter 'examples-vite-vite*' --parallel run preview",
35
+ "build:shared-lib": "pnpm --filter @vite-vite/shared-lib run build",
36
+ "multi-example:ci": "pnpm -filter 'multi-example-*' --parallel run start",
34
37
  "mixed-vv:1": "pnpm clean && pnpm -filter 'examples-vite-vite*' run mixed:1",
35
38
  "mixed-vv:2": "pnpm clean && pnpm -filter 'examples-vite-vite*' run mixed:2",
36
- "multi-example": "pnpm clean && pnpm --filter \"multi-example-*\" --parallel run start",
39
+ "multi-example": "pnpm clean && pnpm --filter 'multi-example-*' --parallel run start",
37
40
  "test": "vitest run --dir src",
38
41
  "test:integration": "vitest run integration",
39
42
  "e2e": "playwright test",
@@ -72,7 +75,7 @@
72
75
  "@module-federation/sdk": "2.2.3",
73
76
  "@rollup/pluginutils": "^5.3.0",
74
77
  "defu": "^6.1.4",
75
- "es-module-lexer": "^1.7.0",
78
+ "es-module-lexer": "^2.0.0",
76
79
  "estree-walker": "^3.0.3",
77
80
  "magic-string": "^0.30.21",
78
81
  "pathe": "^2.0.3"