@module-federation/vite 1.16.12 → 1.16.13

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
@@ -6,22 +6,15 @@
6
6
 
7
7
  [Read the announcement](https://www.linkedin.com/posts/voidzero_github-module-federationvite-vite-plugin-activity-7449452398202241024-JyAL).
8
8
 
9
- <div align="center">
10
- <h2>Gold Sponsor</h2>
11
- <a href="https://www.zephyr-cloud.io/">
12
- <img src="./docs/sponsors/zephyr-logo.png" alt="Zephyr Cloud" width="360" />
13
- </a>
14
-
15
- <h3>Sponsors</h3>
16
- <p>
17
- <a href="https://github.com/thecoder93">
18
- <img src="https://github.com/thecoder93.png?size=96" alt="thecoder93" width="64" height="64" />
19
- </a>
20
- <a href="https://github.com/stephanelgrg">
21
- <img src="https://github.com/stephanelgrg.png?size=96" alt="stephanelgrg" width="64" height="64" />
22
- </a>
23
- </p>
24
- </div>
9
+ <br />
10
+
11
+ <a href="https://github.com/sponsors/gioboa">
12
+ <img src="./docs/sponsors.png" alt="Sponsors" />
13
+ </a>
14
+
15
+ ## Become a sponsor
16
+
17
+ [Support this project on GitHub Sponsors](https://github.com/sponsors/gioboa)
25
18
 
26
19
  ## Reason why 🤔
27
20
 
package/lib/index.js CHANGED
@@ -650,7 +650,7 @@ function getExposesCssMapPlaceholder() {
650
650
  function getVirtualExposesId(options) {
651
651
  return `virtual:mf-exposes:${`${options.internalName}__${options.filename}`.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
652
652
  }
653
- function generateExposes(options) {
653
+ function generateExposes(options, remoteDependencyMap = {}, command = "build") {
654
654
  return `
655
655
  const cssAssetMap = ${JSON.stringify(options.bundleAllCSS ? EXPOSES_CSS_MAP_PLACEHOLDER : {})};
656
656
  const injectedCssHrefs = new Set();
@@ -683,8 +683,12 @@ function generateExposes(options) {
683
683
  }
684
684
  injectedCssHrefs.add(href);
685
685
 
686
+ // Check for any existing stylesheet with the same href, not just
687
+ // MF-injected ones. This prevents duplicate <link> tags when Vite's
688
+ // own CSS module injection or MF runtime's createLink has already
689
+ // created a <link rel="stylesheet"> for the same URL.
686
690
  const existingLink = document.querySelector(
687
- \`link[rel="stylesheet"][data-mf-href="\${href}"]\`
691
+ \`link[rel="stylesheet"][href="\${href}"]\`
688
692
  );
689
693
  if (existingLink) {
690
694
  return Promise.resolve();
@@ -694,7 +698,6 @@ function generateExposes(options) {
694
698
  const link = document.createElement("link");
695
699
  link.rel = "stylesheet";
696
700
  link.href = href;
697
- link.setAttribute("data-mf-href", href);
698
701
  link.onload = () => resolve();
699
702
  link.onerror = () => reject(new Error(\`[Module Federation] Failed to load CSS asset: \${href}\`));
700
703
  document.head.appendChild(link);
@@ -705,12 +708,22 @@ function generateExposes(options) {
705
708
 
706
709
  export default {
707
710
  ${Object.keys(options.exposes).map((key) => {
711
+ const remoteDependencyPreloads = (remoteDependencyMap[key] ?? []).map((remoteId) => {
712
+ const virtualRemote = getRemoteVirtualModule(remoteId, command);
713
+ return `import(${JSON.stringify(virtualRemote.getImportId())})
714
+ .then((mod) => mod.__mf_remote_pending)`;
715
+ }).join(",");
708
716
  return `
709
717
  ${JSON.stringify(key)}: async () => {
710
718
  await injectCssAssets(${JSON.stringify(key)})
719
+ await Promise.all([${remoteDependencyPreloads}])
711
720
  const importModule = await importExposedModule(
712
721
  () => import(${JSON.stringify(options.exposes[key].import)})
713
722
  )
723
+ const dependencyPending = importModule && importModule.__mf_remote_dependency_pending;
724
+ if (dependencyPending && typeof dependencyPending.then === "function") {
725
+ await dependencyPending;
726
+ }
714
727
  const exportModule = {}
715
728
  Object.assign(exportModule, importModule)
716
729
  Object.defineProperty(exportModule, "__esModule", {
@@ -1245,7 +1258,7 @@ function writePreBuildLibPath(pkg, shareItem) {
1245
1258
  const __mfPrebuildExports = __mfPrebuildNamespace;
1246
1259
  ${declarations}
1247
1260
  ${namedExportLine}
1248
- export default __mfPrebuildExports;
1261
+ export default __mfPrebuildNamespace.default ?? __mfPrebuildNamespace;
1249
1262
  `, true);
1250
1263
  return;
1251
1264
  }
@@ -1334,13 +1347,13 @@ function generateLazyWorkspaceSingletonExports(namedExports, importSource, cache
1334
1347
  ${eagerLocalFallback ? applyLocalFallback : `if (import.meta.env.SSR) {
1335
1348
  ${applyLocalFallback}
1336
1349
  } else {
1337
- initPromise.then(() =>
1350
+ (__mfModuleCache.pendingShareLoads ||= []).push(initPromise.then(() =>
1338
1351
  import(${escapeGeneratedStringLiteral(importSource)}).then((mod) => {
1339
1352
  exportModule = __mfNormalizeShareModule(mod);
1340
1353
  __mfWriteSharedCache(__mfModuleCache.share, ${cacheDescriptor}, exportModule);
1341
1354
  __mfApplyLazyShareExports(exportModule);
1342
1355
  })
1343
- );
1356
+ ));
1344
1357
  }`}
1345
1358
  } else {
1346
1359
  __mfApplyLazyShareExports(exportModule);
@@ -1367,13 +1380,13 @@ function generateDeferredHostProvidedExports(namedExports, pkg, cacheDescriptor)
1367
1380
  };
1368
1381
  let exportModule = __mfReadSharedCache(__mfModuleCache.share, ${cacheDescriptor});
1369
1382
  if (exportModule === undefined) {
1370
- initPromise.then(() => {
1383
+ (__mfModuleCache.pendingShareLoads ||= []).push(initPromise.then(() => {
1371
1384
  exportModule = __mfReadSharedCache(__mfModuleCache.share, ${cacheDescriptor});
1372
1385
  if (exportModule === undefined) {
1373
1386
  throw new Error("[Module Federation] Shared module ${pkg} was imported before federation bootstrap finished.");
1374
1387
  }
1375
1388
  __mfApplyHostProvidedExports(exportModule);
1376
- });
1389
+ }));
1377
1390
  } else {
1378
1391
  __mfApplyHostProvidedExports(exportModule);
1379
1392
  }
@@ -1890,7 +1903,6 @@ function generateRemoteEntry(options, virtualExposesId = getVirtualExposesId(opt
1890
1903
  if (initScope.indexOf(initToken) >= 0) return;
1891
1904
  initScope.push(initToken);
1892
1905
  initRes.initShareScopeMap('${options.shareScope}', shared);
1893
- initResolve(initRes)
1894
1906
  try {
1895
1907
  await retrySharedInit(async () => {
1896
1908
  await Promise.all(await initRes.initializeSharing('${options.shareScope}', {
@@ -1914,6 +1926,7 @@ function generateRemoteEntry(options, virtualExposesId = getVirtualExposesId(opt
1914
1926
  const resolved = await Promise.resolve(mod);
1915
1927
  __mfWriteSharedCache(__mfModuleCache.share, cacheDescriptor, __mfNormalizeRuntimeShare(resolved));
1916
1928
  }
1929
+ initResolve(initRes)
1917
1930
  return initRes
1918
1931
  }
1919
1932
 
@@ -2272,10 +2285,10 @@ function injectHostInitPreloads(html, bundle, resolvePath) {
2272
2285
  function getFirstHtmlEntryFile(entryFiles) {
2273
2286
  return entryFiles.find((file) => file.endsWith(".html"));
2274
2287
  }
2275
- function stripQueryAndHash(file) {
2288
+ function stripQueryAndHash$1(file) {
2276
2289
  return file.split(/[?#]/)[0];
2277
2290
  }
2278
- function resolveDevHashEntryFileName(fileName) {
2291
+ function resolveDevHashEntryFileName$1(fileName) {
2279
2292
  if (!fileName.includes("[hash")) return fileName;
2280
2293
  const normalized = fileName.replace(/(?:[._-]?\[hash(?::\d+)?\])/g, "");
2281
2294
  const baseName = path$1.basename(normalized);
@@ -2419,6 +2432,9 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
2419
2432
  await __mfHostInit.__tla;
2420
2433
  const { initHost } = __mfHostInit;
2421
2434
  ${preloadBlock}
2435
+ if (__mfModuleCache.pendingShareLoads) {
2436
+ await Promise.all(__mfModuleCache.pendingShareLoads);
2437
+ }
2422
2438
  })().then(() => ${importExpression(entrySrc)});
2423
2439
  `;
2424
2440
  return [
@@ -2461,7 +2477,7 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
2461
2477
  const scriptRegex = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>/gi;
2462
2478
  let match;
2463
2479
  while ((match = scriptRegex.exec(htmlContent)) !== null) {
2464
- const scriptSrc = stripQueryAndHash(match[1]);
2480
+ const scriptSrc = stripQueryAndHash$1(match[1]);
2465
2481
  if (/^(?:[a-z]+:)?\/\//i.test(scriptSrc)) continue;
2466
2482
  addEntryFile(scriptSrc);
2467
2483
  addEntryFile(scriptSrc.startsWith("/") ? path$1.resolve(viteConfig.root, scriptSrc.slice(1)) : path$1.resolve(path$1.dirname(htmlPath), scriptSrc));
@@ -2504,7 +2520,7 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
2504
2520
  next();
2505
2521
  return;
2506
2522
  }
2507
- const devFileName = resolveDevHashEntryFileName(fileName);
2523
+ const devFileName = resolveDevHashEntryFileName$1(fileName);
2508
2524
  if (devFileName !== fileName && req.url?.startsWith((viteConfig.base + devFileName).replace(/^\/?/, "/"))) req.url = req.url.replace(devFileName, fileName);
2509
2525
  if (req.url && req.url.startsWith((viteConfig.base + fileName).replace(/^\/?/, "/"))) req.url = devEntryPath;
2510
2526
  next();
@@ -4088,8 +4104,39 @@ function pluginModuleParseEnd_default(excludeFn, options) {
4088
4104
  }
4089
4105
  //#endregion
4090
4106
  //#region src/plugins/pluginProxyRemoteEntry.ts
4107
+ function resolveDevHashEntryFileName(fileName) {
4108
+ if (!fileName.includes("[hash")) return fileName;
4109
+ const normalized = fileName.replace(/(?:[._-]?\[hash(?::\d+)?\])/g, "");
4110
+ const baseName = path$1.basename(normalized);
4111
+ return path$1.extname(baseName) ? normalized : `${normalized}.js`;
4112
+ }
4091
4113
  function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposesId }) {
4092
4114
  let viteConfig, _command, root;
4115
+ let exposeRemoteDependencies = {};
4116
+ function isRemoteImport(source) {
4117
+ return Object.keys(options.remotes).some((name) => source === name || source.startsWith(name + "/"));
4118
+ }
4119
+ function collectRemoteDependencies(code) {
4120
+ const dependencies = /* @__PURE__ */ new Set();
4121
+ for (const match of code.matchAll(/(?:^|[;\n\r])\s*import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)/g)) {
4122
+ const source = match[1] || match[2];
4123
+ if (source && isRemoteImport(source)) dependencies.add(source);
4124
+ }
4125
+ return Array.from(dependencies).sort();
4126
+ }
4127
+ async function refreshExposeRemoteDependencies(ctx) {
4128
+ const next = {};
4129
+ for (const [exposeKey, expose] of Object.entries(options.exposes)) {
4130
+ const resolved = await ctx.resolve(expose.import);
4131
+ if (!resolved?.id || resolved.id.includes("\0")) continue;
4132
+ try {
4133
+ next[exposeKey] = collectRemoteDependencies(readFileSync$1(resolved.id, "utf8"));
4134
+ } catch {
4135
+ next[exposeKey] = [];
4136
+ }
4137
+ }
4138
+ exposeRemoteDependencies = next;
4139
+ }
4093
4140
  return {
4094
4141
  name: "proxyRemoteEntry",
4095
4142
  enforce: "post",
@@ -4101,6 +4148,7 @@ function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposes
4101
4148
  _command = command;
4102
4149
  },
4103
4150
  async buildStart() {
4151
+ await refreshExposeRemoteDependencies(this);
4104
4152
  if (_command !== "build") return;
4105
4153
  for (const expose of Object.values(options.exposes)) {
4106
4154
  const resolved = await this.resolve(expose.import);
@@ -4122,19 +4170,19 @@ function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposes
4122
4170
  },
4123
4171
  load(id) {
4124
4172
  if (id === remoteEntryId) return parsePromise.then((_) => generateRemoteEntry(options, virtualExposesId, _command));
4125
- if (id === virtualExposesId) return generateExposes(options);
4173
+ if (id === virtualExposesId) return generateExposes(options, exposeRemoteDependencies, _command);
4126
4174
  if (_command === "serve" && id.includes(getHostAutoInitPath())) return id;
4127
4175
  },
4128
4176
  transform(code, id) {
4129
4177
  return mapCodeToCodeWithSourcemap((() => {
4130
4178
  if (!filterId(id)) return;
4131
4179
  if (id.includes(remoteEntryId)) return parsePromise.then((_) => generateRemoteEntry(options, virtualExposesId, _command));
4132
- if (id === virtualExposesId) return generateExposes(options);
4180
+ if (id === virtualExposesId) return generateExposes(options, exposeRemoteDependencies, _command);
4133
4181
  if (id.includes(getHostAutoInitPath())) {
4134
4182
  if (_command === "serve") {
4135
4183
  const host = typeof viteConfig.server?.host === "string" && viteConfig.server.host !== "0.0.0.0" ? viteConfig.server.host : "localhost";
4136
4184
  const resolvedPublicPath = resolvePublicPath(options, viteConfig.base);
4137
- const publicPath = JSON.stringify((resolvedPublicPath === "auto" ? "/" : resolvedPublicPath) + options.filename);
4185
+ const publicPath = JSON.stringify((resolvedPublicPath === "auto" ? "/" : resolvedPublicPath) + resolveDevHashEntryFileName(options.filename));
4138
4186
  const fallbackOrigin = `//${host}:${viteConfig.server?.port}`;
4139
4187
  const ssrRemoteEntry = "data:text/javascript," + encodeURIComponent("export async function init(){return {loadRemote:async()=>({}),loadShare:async()=>({})}}");
4140
4188
  return `
@@ -4573,26 +4621,7 @@ function applyRewrites(code, imports, id) {
4573
4621
  const ms = new CodeRewriter(code);
4574
4622
  let changed = false;
4575
4623
  let counter = 0;
4576
- let namedProxyHelperDeclared = false;
4577
4624
  const dependencyPendingIds = [];
4578
- const namedProxyHelper = `function __mfCreateNamedRemoteProxy(ns, key) {
4579
- const target = function (...args) {
4580
- const value = ns[key];
4581
- return typeof value === "function" ? value.apply(this, args) : value;
4582
- };
4583
- return new Proxy(target, {
4584
- get(_target, prop) {
4585
- if (prop === "then") return undefined;
4586
- const value = ns[key];
4587
- if (prop === Symbol.toPrimitive) return () => value;
4588
- const item = value == null ? undefined : value[prop];
4589
- return typeof item === "function" ? item.bind(value) : item;
4590
- },
4591
- apply(target, thisArg, args) {
4592
- return target.apply(thisArg, args);
4593
- }
4594
- });
4595
- }`;
4596
4625
  for (const imp of imports) switch (imp.kind) {
4597
4626
  case "static": {
4598
4627
  const src = JSON.stringify(imp.source);
@@ -4610,20 +4639,8 @@ function applyRewrites(code, imports, id) {
4610
4639
  importParts.push(`__mf_remote_pending as ${pendingId}`);
4611
4640
  let rewrite = `import { ${importParts.join(", ")} } from ${src};`;
4612
4641
  if (imp.named.length > 0) {
4613
- const isProxyId = `__mf_is_proxy_${counter++}`;
4614
- const tempNames = imp.named.map((_s) => `__mf_named_${counter++}`);
4615
- const destructParts = imp.named.map((s, index) => `${s.imported}: ${tempNames[index]}`);
4616
- const bindingLines = imp.named.map((s, index) => {
4617
- const temp = tempNames[index];
4618
- return `const ${s.local} = ${isProxyId} ? __mfCreateNamedRemoteProxy(${nsId}, ${JSON.stringify(s.imported)}) : ${temp};`;
4619
- });
4620
- if (!namedProxyHelperDeclared) {
4621
- rewrite += `\n${namedProxyHelper}`;
4622
- namedProxyHelperDeclared = true;
4623
- }
4624
- rewrite += `\nconst ${isProxyId} = ${nsId} && ${nsId}.__mf_is_remote_proxy;`;
4625
- rewrite += `\nconst { ${destructParts.join(", ")} } = ${isProxyId} ? {} : ${nsId};`;
4626
- rewrite += `\n${bindingLines.join("\n")}`;
4642
+ const destructParts = imp.named.map((s) => `${s.imported}: ${s.local}`);
4643
+ rewrite += `\nconst { ${destructParts.join(", ")} } = ${nsId};`;
4627
4644
  }
4628
4645
  ms.overwrite(imp.start, imp.end, rewrite);
4629
4646
  }
@@ -4880,6 +4897,104 @@ function pluginRemoteNamedExports(options) {
4880
4897
  }
4881
4898
  //#endregion
4882
4899
  //#region src/plugins/pluginSSRRemoteEntry.ts
4900
+ const MAX_RUNNER_BODY_BYTES = 1024 * 1024;
4901
+ const ALLOWED_RUNNER_INVOKE_NAMES = new Set(["fetchModule", "getBuiltins"]);
4902
+ const VITE_FS_PREFIX = "/@fs/";
4903
+ function isPlainObject(value) {
4904
+ return !!value && typeof value === "object" && !Array.isArray(value);
4905
+ }
4906
+ function stripQueryAndHash(id) {
4907
+ const queryIndex = id.indexOf("?");
4908
+ const hashIndex = id.indexOf("#");
4909
+ const endIndex = queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
4910
+ return endIndex === -1 ? id : id.slice(0, endIndex);
4911
+ }
4912
+ function decodeRunnerFilePath(filePath) {
4913
+ try {
4914
+ return decodeURIComponent(filePath);
4915
+ } catch {
4916
+ return;
4917
+ }
4918
+ }
4919
+ function hasRelativeTraversal(id) {
4920
+ return id.split(/[\\/]+/).includes("..");
4921
+ }
4922
+ function getRealPathIfExists(filePath) {
4923
+ try {
4924
+ return fs$1.realpathSync.native(filePath);
4925
+ } catch {
4926
+ return;
4927
+ }
4928
+ }
4929
+ function isPathWithinDirectory(filePath, directory) {
4930
+ const realFilePath = getRealPathIfExists(filePath) ?? path$1.resolve(filePath);
4931
+ const realDirectory = getRealPathIfExists(directory) ?? path$1.resolve(directory);
4932
+ const relative = path$1.relative(realDirectory, realFilePath);
4933
+ return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
4934
+ }
4935
+ function getRunnerAllowedDirectories(config) {
4936
+ return [config.root, ...config.server?.fs?.allow ?? []].map((directory) => path$1.resolve(directory));
4937
+ }
4938
+ function isPathWithinAllowedDirectories(filePath, allowedDirectories) {
4939
+ return allowedDirectories.some((directory) => isPathWithinDirectory(filePath, directory));
4940
+ }
4941
+ function isSafeRunnerFetchModuleId(id, config) {
4942
+ if (typeof id !== "string" || !id || id.includes("\0")) return false;
4943
+ const decoded = decodeViteId(id).replace(/^\0+/, "");
4944
+ if (!decoded || decoded.startsWith("virtual:")) return !!decoded;
4945
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(decoded) || decoded.startsWith("//")) return false;
4946
+ const cleanId = decodeRunnerFilePath(stripQueryAndHash(decoded));
4947
+ if (!cleanId || hasRelativeTraversal(cleanId)) return false;
4948
+ const allowedDirectories = getRunnerAllowedDirectories(config);
4949
+ if (cleanId.startsWith(VITE_FS_PREFIX)) {
4950
+ const fsPath = cleanId.slice(5);
4951
+ return path$1.isAbsolute(fsPath) && isPathWithinAllowedDirectories(fsPath, allowedDirectories);
4952
+ }
4953
+ if (path$1.isAbsolute(cleanId)) {
4954
+ if (isPathWithinAllowedDirectories(cleanId, allowedDirectories)) return true;
4955
+ return !fs$1.existsSync(cleanId);
4956
+ }
4957
+ return true;
4958
+ }
4959
+ function isRunnerInvokePayload(payload, config) {
4960
+ if (!payload || typeof payload !== "object") return false;
4961
+ if (payload.type !== "custom" || payload.event !== "vite:invoke") return false;
4962
+ const data = payload.data;
4963
+ if (!data || typeof data !== "object") return false;
4964
+ const name = data.name;
4965
+ const args = data.data;
4966
+ if (typeof name !== "string" || !ALLOWED_RUNNER_INVOKE_NAMES.has(name) || !Array.isArray(args)) return false;
4967
+ if (name === "getBuiltins") return args.length === 0;
4968
+ if (args.length < 1 || args.length > 3) return false;
4969
+ const [id, importer, opts] = args;
4970
+ return isSafeRunnerFetchModuleId(id, config) && (importer === void 0 || importer === null || isSafeRunnerFetchModuleId(importer, config)) && (opts === void 0 || isPlainObject(opts));
4971
+ }
4972
+ function readBoundedRunnerBody(req, res) {
4973
+ return new Promise((resolve) => {
4974
+ const chunks = [];
4975
+ let size = 0;
4976
+ let done = false;
4977
+ const fail = (statusCode, message) => {
4978
+ if (done) return;
4979
+ done = true;
4980
+ res.statusCode = statusCode;
4981
+ res.end(message);
4982
+ resolve(void 0);
4983
+ };
4984
+ req.on("data", (chunk) => {
4985
+ if (done) return;
4986
+ size += chunk.length;
4987
+ if (size > MAX_RUNNER_BODY_BYTES) return fail(413, "Payload too large");
4988
+ chunks.push(chunk);
4989
+ });
4990
+ req.on("end", () => {
4991
+ if (done) return;
4992
+ done = true;
4993
+ resolve(Buffer.concat(chunks));
4994
+ });
4995
+ req.on("error", () => fail(400, "Bad request"));
4996
+ });
4997
+ }
4883
4998
  /**
4884
4999
  * Emits a Node-compatible SSR remote entry alongside the browser entry.
4885
5000
  *
@@ -4966,7 +5081,8 @@ function pluginSSRRemoteEntry(options) {
4966
5081
  });
4967
5082
  const ssrEnv = server.environments?.ssr;
4968
5083
  const clientEnv = server.environments?.client;
4969
- if (typeof (ssrEnv?.fetchModule ?? clientEnv?.fetchModule) === "function") server.middlewares.use("/__mf_runner__", async (req, res) => {
5084
+ const runnerEnv = typeof ssrEnv?.hot?.handleInvoke === "function" ? ssrEnv : typeof clientEnv?.hot?.handleInvoke === "function" ? clientEnv : void 0;
5085
+ if (typeof (ssrEnv?.fetchModule ?? clientEnv?.fetchModule) === "function" && runnerEnv) server.middlewares.use("/__mf_runner__", async (req, res) => {
4970
5086
  res.setHeader("Access-Control-Allow-Origin", "*");
4971
5087
  if (req.method === "OPTIONS") {
4972
5088
  res.setHeader("Access-Control-Allow-Methods", "POST");
@@ -4981,46 +5097,37 @@ function pluginSSRRemoteEntry(options) {
4981
5097
  return;
4982
5098
  }
4983
5099
  try {
4984
- const chunks = [];
4985
- await new Promise((resolve, reject) => {
4986
- req.on("data", (chunk) => chunks.push(chunk));
4987
- req.on("end", resolve);
4988
- req.on("error", reject);
4989
- });
4990
- const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
4991
- if (body.name === "getBuiltins") {
4992
- const builtins = (clientEnv ?? ssrEnv)?.config?.resolve?.builtins ?? [];
4993
- res.setHeader("Content-Type", "application/json");
4994
- res.end(JSON.stringify({ result: builtins }));
5100
+ const rawBody = await readBoundedRunnerBody(req, res);
5101
+ if (!rawBody) return;
5102
+ let body;
5103
+ try {
5104
+ body = JSON.parse(rawBody.toString("utf8"));
5105
+ } catch {
5106
+ res.statusCode = 400;
5107
+ res.end(JSON.stringify({ error: { message: "Invalid JSON" } }));
4995
5108
  return;
4996
5109
  }
4997
- if (body.name !== "fetchModule") {
5110
+ if (!isRunnerInvokePayload(body, server.config)) {
4998
5111
  res.statusCode = 400;
4999
- res.end(JSON.stringify({ error: { message: `Unsupported invoke: ${body.name}` } }));
5112
+ res.end(JSON.stringify({ error: { message: "Invalid runner invoke" } }));
5000
5113
  return;
5001
5114
  }
5002
- const [id, importer, opts] = body.data;
5003
- const fetchEnv = ssrEnv ?? clientEnv;
5004
- const fetchFn = fetchEnv.fetchModule.bind(fetchEnv);
5005
- let result;
5006
- try {
5007
- result = await fetchFn(id, importer, opts);
5008
- } catch (fetchErr) {
5009
- const bareId = decodeViteId(id);
5010
- try {
5115
+ let result = await runnerEnv.hot.handleInvoke(body);
5116
+ if ("error" in result && body.data.name === "fetchModule") {
5117
+ const id = body.data.data[0];
5118
+ const bareId = typeof id === "string" ? decodeViteId(id).replace(/^\0/, "") : "";
5119
+ if (bareId && !bareId.startsWith(".") && !bareId.startsWith("/") && !bareId.startsWith("file:") && !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(bareId)) try {
5011
5120
  const { createRequire } = await import("module");
5012
5121
  const path = await import("path");
5013
5122
  const { pathToFileURL } = await import("url");
5014
- result = {
5015
- externalize: pathToFileURL(createRequire(pathToFileURL(path.join(server.config.root, "package.json"))).resolve(bareId.replace(/^\0/, ""))).href,
5123
+ result = { result: {
5124
+ externalize: pathToFileURL(createRequire(pathToFileURL(path.join(server.config.root, "package.json"))).resolve(bareId)).href,
5016
5125
  type: "module"
5017
- };
5018
- } catch {
5019
- throw fetchErr;
5020
- }
5126
+ } };
5127
+ } catch {}
5021
5128
  }
5022
5129
  res.setHeader("Content-Type", "application/json");
5023
- res.end(JSON.stringify({ result }));
5130
+ res.end(JSON.stringify(result));
5024
5131
  } catch (e) {
5025
5132
  res.setHeader("Content-Type", "application/json");
5026
5133
  res.end(JSON.stringify({ error: { message: String(e instanceof Error ? e.message : e) } }));
@@ -5864,7 +5971,9 @@ function federation(mfUserOptions) {
5864
5971
  },
5865
5972
  load(id) {
5866
5973
  if (id.includes("__loadShare__") || id.includes("__loadRemote__")) {
5867
- let code = VirtualModule.findById(id)?.code ?? readFileSync(id, "utf-8");
5974
+ const virtualModule = VirtualModule.findById(id);
5975
+ if (!virtualModule?.code) return null;
5976
+ let code = virtualModule.code;
5868
5977
  const environmentName = this.environment?.name;
5869
5978
  if (environmentName && environmentName !== "client" || !environmentName && isSsrBuild) code = prependWorkspaceSingletonSsrImport(code);
5870
5979
  code = code.replace(/import\s+["'][^"']*__prebuild__[^"']*["']\s*;?/g, "");
@@ -5948,7 +6057,15 @@ function federation(mfUserOptions) {
5948
6057
  options.runtimePlugins.forEach((p) => {
5949
6058
  const pluginPath = typeof p === "string" ? p : p[0];
5950
6059
  if (SSR_ONLY_PLUGINS.has(pluginPath)) return;
5951
- if (pluginPath && !pluginPath.startsWith(".") && !pluginPath.startsWith("/") && !pluginPath.startsWith("\0") && !pluginPath.startsWith("virtual:")) config.optimizeDeps.include.push(pluginPath);
6060
+ if (pluginPath && !pluginPath.startsWith(".") && !pluginPath.startsWith("/") && !pluginPath.startsWith("\0") && !pluginPath.startsWith("virtual:")) {
6061
+ let optimizeDep = pluginPath;
6062
+ if (pluginPath === "@module-federation/dts-plugin/dynamic-remote-type-hints-plugin") try {
6063
+ optimizeDep = normalizePathForImport(resolveImportPath(pluginPath));
6064
+ } catch {
6065
+ optimizeDep = pluginPath;
6066
+ }
6067
+ config.optimizeDeps.include.push(optimizeDep);
6068
+ }
5952
6069
  });
5953
6070
  if (isRolldown) {
5954
6071
  config.build ??= {};
@@ -0,0 +1,152 @@
1
+ import { SsrEntryHttpError, neutralizeBrowserPreloadHelpers } from "./utils/ssrEntryLoader.js";
2
+ //#region src/utils/ssrVmStrategy.ts
3
+ /**
4
+ * vm.SourceTextModule strategy for loading remote SSR entries.
5
+ *
6
+ * Unlike the temp-file strategy (which rewrites bare shared imports to
7
+ * host-resolved file:// paths at fetch time), this strategy evaluates the
8
+ * remote's ESM graph with `vm.SourceTextModule` in the current context and
9
+ * resolves bare imports through a linker, in order:
10
+ *
11
+ * 1. The host's federation share scope — `instance.loadShare(name)` on the
12
+ * global `__FEDERATION__` instances. This restores real share-scope
13
+ * semantics (version negotiation, loaded-first reuse) on the server.
14
+ * 2. The build-time `resolvedShared` file map (same source as the temp-file
15
+ * strategy) as a fallback when no instance shares the package.
16
+ * 3. Plain host `import(specifier)` for everything else (node builtins,
17
+ * packages the remote expects the host to provide).
18
+ *
19
+ * Requires Node with `--experimental-vm-modules`; callers must check
20
+ * `isVmStrategyAvailable()` and fall back to the temp-file strategy when the
21
+ * API is missing.
22
+ */
23
+ let vmApiPromise;
24
+ async function getVmApi() {
25
+ if (!vmApiPromise) vmApiPromise = (async () => {
26
+ try {
27
+ const vm = await import(
28
+ /* @vite-ignore */
29
+ "vm"
30
+ );
31
+ if (typeof vm.SourceTextModule !== "function" || typeof vm.SyntheticModule !== "function") return null;
32
+ return vm;
33
+ } catch {
34
+ return null;
35
+ }
36
+ })();
37
+ return vmApiPromise;
38
+ }
39
+ async function isVmStrategyAvailable() {
40
+ return await getVmApi() !== null;
41
+ }
42
+ function getFederationInstances() {
43
+ return globalThis.__FEDERATION__?.__INSTANCES__ ?? [];
44
+ }
45
+ /**
46
+ * Resolve a bare specifier to a module namespace: share scope first, then the
47
+ * build-time resolvedShared file map, then plain host import.
48
+ */
49
+ async function loadBareModule(specifier, options) {
50
+ for (const instance of getFederationInstances()) {
51
+ if (typeof instance?.loadShare !== "function") continue;
52
+ if (!instance.options?.shared || !(specifier in instance.options.shared)) continue;
53
+ try {
54
+ const factory = await instance.loadShare(specifier);
55
+ if (typeof factory === "function") {
56
+ const shared = factory();
57
+ if (shared) return shared;
58
+ }
59
+ } catch {}
60
+ }
61
+ const resolvedPath = options.resolvedShared[specifier];
62
+ if (resolvedPath) return import(
63
+ /* @vite-ignore */
64
+ `file://${resolvedPath}`
65
+ );
66
+ return import(
67
+ /* @vite-ignore */
68
+ specifier
69
+ );
70
+ }
71
+ function createSyntheticModule(vm, specifier, namespace) {
72
+ const source = namespace && typeof namespace === "object" ? namespace : { default: namespace };
73
+ const exportNames = new Set(Object.keys(source));
74
+ exportNames.add("default");
75
+ const syntheticModule = new vm.SyntheticModule([...exportNames], () => {
76
+ for (const exportName of exportNames) if (exportName === "default") syntheticModule.setExport("default", source.default !== void 0 ? source.default : namespace);
77
+ else syntheticModule.setExport(exportName, source[exportName]);
78
+ }, { identifier: `mf-shared:${specifier}` });
79
+ return syntheticModule;
80
+ }
81
+ const httpModuleCache = /* @__PURE__ */ new Map();
82
+ const namespaceCache = /* @__PURE__ */ new Map();
83
+ function getBodyPreview(body) {
84
+ return body.slice(0, 240).replace(/\s+/g, " ").trim();
85
+ }
86
+ async function fetchModuleSource(url) {
87
+ const res = await fetch(url);
88
+ const text = await res.text();
89
+ if (!res.ok) throw new SsrEntryHttpError(url, res.status, res.statusText, getBodyPreview(text));
90
+ return neutralizeBrowserPreloadHelpers(text);
91
+ }
92
+ function isHttpUrl(value) {
93
+ return value.startsWith("http://") || value.startsWith("https://");
94
+ }
95
+ /** Resolve a specifier against the referencing module's URL; null for bare specifiers. */
96
+ function resolveSpecifierUrl(specifier, referencerUrl) {
97
+ if (isHttpUrl(specifier)) return specifier;
98
+ if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) return new URL(specifier, referencerUrl).href;
99
+ return null;
100
+ }
101
+ function getHttpModule(vm, url, options) {
102
+ const cacheKey = `${options.versionKey}::${url}`;
103
+ if (!httpModuleCache.has(cacheKey)) httpModuleCache.set(cacheKey, (async () => {
104
+ const code = await fetchModuleSource(url);
105
+ return new vm.SourceTextModule(code, {
106
+ identifier: url,
107
+ initializeImportMeta(meta) {
108
+ meta.url = url;
109
+ },
110
+ importModuleDynamically: (specifier, referencingModule) => importDynamically(vm, specifier, referencingModule, options)
111
+ });
112
+ })().catch((error) => {
113
+ httpModuleCache.delete(cacheKey);
114
+ throw error;
115
+ }));
116
+ return httpModuleCache.get(cacheKey);
117
+ }
118
+ async function linkModule(vm, specifier, referencingModule, options) {
119
+ const url = resolveSpecifierUrl(specifier, referencingModule.identifier);
120
+ if (url) return getHttpModule(vm, url, options);
121
+ return createSyntheticModule(vm, specifier, await loadBareModule(specifier, options));
122
+ }
123
+ async function importDynamically(vm, specifier, referencingModule, options) {
124
+ const linker = (spec, referencer) => linkModule(vm, spec, referencer, options);
125
+ const module = await linker(specifier, referencingModule);
126
+ if (module.status === "unlinked") await module.link(linker);
127
+ if (module.status === "linked") await module.evaluate();
128
+ return module;
129
+ }
130
+ /**
131
+ * Load and evaluate a remote SSR entry as a `vm.SourceTextModule` graph and
132
+ * return its namespace (the federation container with `init`/`get`).
133
+ * Returns null when the vm module APIs are unavailable.
134
+ */
135
+ async function loadViaVmStrategy(entryUrl, options) {
136
+ const vm = await getVmApi();
137
+ if (!vm) return null;
138
+ const cacheKey = `${options.versionKey}::${entryUrl}`;
139
+ if (!namespaceCache.has(cacheKey)) namespaceCache.set(cacheKey, (async () => {
140
+ const entryModule = await getHttpModule(vm, entryUrl, options);
141
+ const linker = (specifier, referencingModule) => linkModule(vm, specifier, referencingModule, options);
142
+ if (entryModule.status === "unlinked") await entryModule.link(linker);
143
+ if (entryModule.status === "linked") await entryModule.evaluate();
144
+ return entryModule.namespace;
145
+ })().catch((error) => {
146
+ namespaceCache.delete(cacheKey);
147
+ throw error;
148
+ }));
149
+ return namespaceCache.get(cacheKey);
150
+ }
151
+ //#endregion
152
+ export { isVmStrategyAvailable, loadViaVmStrategy };
@@ -32,6 +32,29 @@ interface RemoteInfo {
32
32
  type?: string;
33
33
  entryGlobalName?: string;
34
34
  }
35
+ declare class SsrEntryHttpError extends Error {
36
+ readonly url: string;
37
+ readonly status: number;
38
+ readonly statusText: string;
39
+ readonly bodyPreview: string;
40
+ constructor(url: string, status: number, statusText: string, bodyPreview: string);
41
+ }
42
+ /**
43
+ * Drop the loader's caches so the next `loadEntry` re-resolves and re-fetches
44
+ * remote SSR entries. Pass a remote entry URL to scope the invalidation to one
45
+ * remote; call with no arguments to invalidate everything.
46
+ *
47
+ * Note: the MF runtime keeps its own container/module caches per federation
48
+ * instance. This function best-effort clears the module caches of all global
49
+ * federation instances so re-renders load fresh remote modules, but hosts that
50
+ * hold direct references to previously loaded modules keep those references.
51
+ */
52
+ declare function revalidate(remoteEntryUrl?: string): void;
53
+ /**
54
+ * Neutralize browser-only preload machinery in Vite/Rolldown output so the
55
+ * code can evaluate in Node. Shared by the temp-file and vm strategies.
56
+ */
57
+ declare function neutralizeBrowserPreloadHelpers(code: string): string;
35
58
  /**
36
59
  * MF runtime plugin factory.
37
60
  *
@@ -50,6 +73,33 @@ interface SsrEntryLoaderOptions {
50
73
  * in remote SSR entry temp files — no runtime createRequire walk-up needed.
51
74
  */
52
75
  resolvedShared?: Record<string, string>;
76
+ /**
77
+ * How to evaluate remote SSR entries on the server.
78
+ *
79
+ * - `'temp-file'` (default): fetch the ESM graph, rewrite specifiers, write
80
+ * temp files and `import()` them. Works on stock Node; shared packages are
81
+ * pinned to the host's copies via `resolvedShared` (no version negotiation).
82
+ * - `'vm'`: evaluate the graph with `vm.SourceTextModule` and link bare
83
+ * shared imports through the host's federation share scope (`loadShare`),
84
+ * restoring version negotiation. Requires `--experimental-vm-modules`;
85
+ * falls back to `'temp-file'` when unavailable.
86
+ */
87
+ strategy?: 'temp-file' | 'vm';
88
+ /**
89
+ * Share scope consulted by the `'vm'` strategy when linking bare imports.
90
+ * Defaults to `'default'`.
91
+ */
92
+ shareScopeName?: string;
93
+ /**
94
+ * Re-check each remote's manifest when the cached SSR entry resolution is
95
+ * older than this many milliseconds. When the manifest's version changes
96
+ * (remote redeployed at the same URL), the loader drops its caches for that
97
+ * remote so subsequent loads use the new build. Omit to cache until process
98
+ * exit or an explicit `revalidate()` call. Only manifest-resolved entries
99
+ * can be revalidated this way — convention-resolved entries have no version
100
+ * source.
101
+ */
102
+ maxAgeMs?: number;
53
103
  }
54
104
  declare function ssrEntryLoaderPlugin(options?: SsrEntryLoaderOptions): {
55
105
  name: string;
@@ -63,4 +113,4 @@ declare function ssrEntryLoaderPlugin(options?: SsrEntryLoaderOptions): {
63
113
  } | undefined>;
64
114
  };
65
115
  //#endregion
66
- export { ssrEntryLoaderPlugin as default };
116
+ export { SsrEntryHttpError, ssrEntryLoaderPlugin as default, neutralizeBrowserPreloadHelpers, revalidate };
@@ -70,10 +70,7 @@ async function getOrCreateRunner(remoteOrigin) {
70
70
  return await (await fetch(runnerEndpoint, {
71
71
  method: "POST",
72
72
  headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({
74
- name: payload.data.name,
75
- data: payload.data.data
76
- })
73
+ body: JSON.stringify(payload)
77
74
  })).json();
78
75
  } }
79
76
  }, new ESModulesEvaluator());
@@ -88,6 +85,27 @@ const _path = () => nodeImport("path");
88
85
  const _fs = () => nodeImport("fs");
89
86
  const _crypto = () => nodeImport("crypto");
90
87
  const _module = () => nodeImport("module");
88
+ /**
89
+ * Version key for a resolved SSR entry. Derived from the remote's manifest
90
+ * content so a redeploy at the same URL produces a different key, which in
91
+ * turn produces different temp-file names — busting both our caches and
92
+ * Node's ESM module cache. Convention-resolved entries (no manifest) get a
93
+ * stable placeholder key and cannot be revalidated automatically.
94
+ */
95
+ const UNVERSIONED = "unversioned";
96
+ function hashString(value) {
97
+ let hash = 2166136261;
98
+ for (let i = 0; i < value.length; i++) {
99
+ hash ^= value.charCodeAt(i);
100
+ hash = Math.imul(hash, 16777619);
101
+ }
102
+ return (hash >>> 0).toString(16).padStart(8, "0");
103
+ }
104
+ function computeManifestVersionKey(manifest) {
105
+ const buildVersion = manifest.metaData?.buildInfo?.buildVersion;
106
+ const contentHash = hashString(JSON.stringify(manifest));
107
+ return buildVersion ? `${buildVersion}-${contentHash}` : contentHash;
108
+ }
91
109
  const ssrEntryCache = /* @__PURE__ */ new Map();
92
110
  const manifestFetchCache = /* @__PURE__ */ new Map();
93
111
  var SsrEntryHttpError = class extends Error {
@@ -149,7 +167,8 @@ function resolveSSREntryUrl(manifest, manifestUrl) {
149
167
  const entryPath = (meta.ssrRemoteEntry.path || "") + meta.ssrRemoteEntry.name;
150
168
  return {
151
169
  url: new URL(entryPath, base).href,
152
- type: meta.ssrRemoteEntry.type || "module"
170
+ type: meta.ssrRemoteEntry.type || "module",
171
+ versionKey: computeManifestVersionKey(manifest)
153
172
  };
154
173
  }
155
174
  /**
@@ -191,14 +210,17 @@ function buildSsrEntryCandidates(ctx, options = {}) {
191
210
  const candidates = [];
192
211
  if (!options.skipServerBuild) candidates.push({
193
212
  url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
194
- type: "module"
213
+ type: "module",
214
+ versionKey: UNVERSIONED
195
215
  });
196
216
  candidates.push({
197
217
  url: `${base}.ssr.js`,
198
- type: "module"
218
+ type: "module",
219
+ versionKey: UNVERSIONED
199
220
  }, {
200
221
  url: `${remoteOrigin}/__mf_ssr__/${filename}.ssr.js`,
201
- type: "module"
222
+ type: "module",
223
+ versionKey: UNVERSIONED
202
224
  });
203
225
  return candidates;
204
226
  }
@@ -212,13 +234,15 @@ async function resolveFirstReachableCandidate(candidates) {
212
234
  async function resolveSSREntryImpl(remoteEntryUrl) {
213
235
  if (isSsrEntry(remoteEntryUrl)) return {
214
236
  url: remoteEntryUrl,
215
- type: "module"
237
+ type: "module",
238
+ versionKey: UNVERSIONED
216
239
  };
217
240
  if (!isManifestEntry(remoteEntryUrl)) {
218
241
  const filename = getEntryFilename(remoteEntryUrl);
219
242
  const fromServerBuild = await headCheckSsrEntry({
220
243
  url: `${remoteEntryUrl.replace(/\/[^/]+$/, "")}/__mf_server__/${filename}.ssr.js`,
221
- type: "module"
244
+ type: "module",
245
+ versionKey: UNVERSIONED
222
246
  });
223
247
  if (fromServerBuild) return fromServerBuild;
224
248
  }
@@ -229,9 +253,63 @@ async function resolveSSREntryImpl(remoteEntryUrl) {
229
253
  }
230
254
  return resolveFirstReachableCandidate(buildSsrEntryCandidates(ctx, { skipServerBuild: !isManifestEntry(remoteEntryUrl) }));
231
255
  }
232
- async function getSSREntry(remoteEntryUrl) {
233
- if (!ssrEntryCache.has(remoteEntryUrl)) ssrEntryCache.set(remoteEntryUrl, resolveSSREntryImpl(remoteEntryUrl));
234
- return ssrEntryCache.get(remoteEntryUrl);
256
+ function setSsrEntryCache(remoteEntryUrl) {
257
+ const record = {
258
+ promise: resolveSSREntryImpl(remoteEntryUrl),
259
+ resolvedAt: Date.now()
260
+ };
261
+ ssrEntryCache.set(remoteEntryUrl, record);
262
+ return record;
263
+ }
264
+ async function getSSREntry(remoteEntryUrl, maxAgeMs) {
265
+ const cached = ssrEntryCache.get(remoteEntryUrl);
266
+ if (!cached) return setSsrEntryCache(remoteEntryUrl).promise;
267
+ if (!(typeof maxAgeMs === "number" && maxAgeMs >= 0 && Date.now() - cached.resolvedAt >= maxAgeMs)) return cached.promise;
268
+ const previous = await cached.promise.catch(() => null);
269
+ manifestFetchCache.delete(getManifestUrl(remoteEntryUrl));
270
+ const record = setSsrEntryCache(remoteEntryUrl);
271
+ const next = await record.promise.catch(() => null);
272
+ if (previous && next && previous.versionKey !== next.versionKey) dropRemoteCaches(remoteEntryUrl);
273
+ return record.promise;
274
+ }
275
+ /**
276
+ * Drop per-remote caches after a version change so old artifacts stop being
277
+ * reused. Temp-file cache keys hold SSR entry/chunk URLs (not the browser
278
+ * entry URL), so scope the invalidation by origin.
279
+ */
280
+ function dropRemoteCaches(remoteEntryUrl) {
281
+ let origin;
282
+ try {
283
+ origin = new URL(remoteEntryUrl).origin;
284
+ } catch {
285
+ return;
286
+ }
287
+ for (const key of tempFileCache.keys()) if (key.slice(key.indexOf("::") + 2).startsWith(origin)) tempFileCache.delete(key);
288
+ }
289
+ /**
290
+ * Drop the loader's caches so the next `loadEntry` re-resolves and re-fetches
291
+ * remote SSR entries. Pass a remote entry URL to scope the invalidation to one
292
+ * remote; call with no arguments to invalidate everything.
293
+ *
294
+ * Note: the MF runtime keeps its own container/module caches per federation
295
+ * instance. This function best-effort clears the module caches of all global
296
+ * federation instances so re-renders load fresh remote modules, but hosts that
297
+ * hold direct references to previously loaded modules keep those references.
298
+ */
299
+ function revalidate(remoteEntryUrl) {
300
+ if (remoteEntryUrl) {
301
+ ssrEntryCache.delete(remoteEntryUrl);
302
+ manifestFetchCache.delete(getManifestUrl(remoteEntryUrl));
303
+ dropRemoteCaches(remoteEntryUrl);
304
+ } else {
305
+ ssrEntryCache.clear();
306
+ manifestFetchCache.clear();
307
+ tempFileCache.clear();
308
+ }
309
+ const federation = globalThis.__FEDERATION__;
310
+ for (const instance of federation?.__INSTANCES__ ?? []) try {
311
+ instance?.moduleCache?.clear?.();
312
+ } catch {}
235
313
  }
236
314
  const tempFileCache = /* @__PURE__ */ new Map();
237
315
  let ssrCacheDirPromise;
@@ -252,14 +330,11 @@ async function getSSRCacheDir() {
252
330
  })();
253
331
  return ssrCacheDirPromise;
254
332
  }
255
- function transformSsrCode(code, base, sharedPkgMap) {
256
- code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
257
- code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
258
- code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
259
- if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
260
- const resolved = sharedPkgMap.get(specifier);
261
- return resolved ? m.replace(specifier, `file://${resolved}`) : m;
262
- });
333
+ /**
334
+ * Neutralize browser-only preload machinery in Vite/Rolldown output so the
335
+ * code can evaluate in Node. Shared by the temp-file and vm strategies.
336
+ */
337
+ function neutralizeBrowserPreloadHelpers(code) {
263
338
  code = code.replace(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*preload-helper[^"']*["'];?/g, (_m, bindings) => {
264
339
  return bindings.split(",").map((b) => {
265
340
  const parts = b.trim().split(/\s+as\s+/);
@@ -270,6 +345,16 @@ function transformSsrCode(code, base, sharedPkgMap) {
270
345
  code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
271
346
  return code;
272
347
  }
348
+ function transformSsrCode(code, base, sharedPkgMap) {
349
+ code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
350
+ code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
351
+ code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
352
+ if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
353
+ const resolved = sharedPkgMap.get(specifier);
354
+ return resolved ? m.replace(specifier, `file://${resolved}`) : m;
355
+ });
356
+ return neutralizeBrowserPreloadHelpers(code);
357
+ }
273
358
  function isVitePreloadHelperSpecifier(specifier) {
274
359
  return specifier.includes("preload-helper");
275
360
  }
@@ -277,10 +362,15 @@ function isVitePreloadHelperSpecifier(specifier) {
277
362
  * Fetch an HTTP ESM module, transform it, write it to a temp .js file and
278
363
  * return the file path. Recursively does the same for HTTP transitive imports
279
364
  * so that `import('file:///...temp.js')` can resolve them.
365
+ *
366
+ * `versionKey` participates in both the cache key and the temp file name, so
367
+ * a remote redeploy (new manifest → new key) produces new files and bypasses
368
+ * Node's ESM module cache instead of serving the stale build.
280
369
  */
281
- async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
370
+ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap, versionKey = UNVERSIONED) {
371
+ const cacheKey = `${versionKey}::${url}`;
282
372
  if (visited.has(url)) return visited.get(url);
283
- if (tempFileCache.has(url)) return tempFileCache.get(url);
373
+ if (tempFileCache.has(cacheKey)) return tempFileCache.get(cacheKey);
284
374
  const promise = (async () => {
285
375
  const res = await fetch(url);
286
376
  let code = await res.text();
@@ -292,7 +382,7 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
292
382
  while ((m = relRegex.exec(code)) !== null) if ((m[1].startsWith("./") || m[1].startsWith("../")) && !isVitePreloadHelperSpecifier(m[1])) relImports.push(new URL(m[1], base).href);
293
383
  const subMap = /* @__PURE__ */ new Map();
294
384
  await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
295
- const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap);
385
+ const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap, versionKey);
296
386
  subMap.set(u, `file://${tmpPath}`);
297
387
  }));
298
388
  code = transformSsrCode(code, base, sharedPkgMap);
@@ -300,22 +390,36 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
300
390
  const { createHash } = await _crypto();
301
391
  const { join } = await _path();
302
392
  const { writeFileSync } = await _fs();
303
- const tmpFile = join(tmpDir, `${createHash("sha1").update(url).digest("hex").slice(0, 12)}.js`);
393
+ const tmpFile = join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}.js`);
304
394
  writeFileSync(tmpFile, code, "utf8");
305
395
  visited.set(url, tmpFile);
306
396
  return tmpFile;
307
397
  })();
308
- tempFileCache.set(url, promise);
398
+ tempFileCache.set(cacheKey, promise);
309
399
  return promise;
310
400
  }
311
- async function importTempModule(filePath) {
312
- return await import(
313
- /* @vite-ignore */
314
- filePath
315
- );
401
+ async function importTempModule(filePath, versionKey) {
402
+ return await import(`${filePath}?v=${encodeURIComponent(versionKey)}`);
403
+ }
404
+ let warnedVmUnavailable = false;
405
+ async function tryVmStrategy(ssrEntry, options) {
406
+ const { loadViaVmStrategy, isVmStrategyAvailable } = await import("../ssrVmStrategy-DtpfkCw1.js");
407
+ if (!await isVmStrategyAvailable()) {
408
+ if (!warnedVmUnavailable) {
409
+ warnedVmUnavailable = true;
410
+ console.warn("[mf-vite:ssr-entry-loader] strategy \"vm\" requires vm.SourceTextModule (run Node with --experimental-vm-modules); falling back to the temp-file strategy.");
411
+ }
412
+ return null;
413
+ }
414
+ return await loadViaVmStrategy(ssrEntry.url, {
415
+ resolvedShared: options.resolvedShared,
416
+ shareScopeName: options.shareScopeName,
417
+ versionKey: ssrEntry.versionKey
418
+ });
316
419
  }
317
- async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
318
- const { url, type } = ssrEntry;
420
+ async function loadSSRRemoteEntry(ssrEntry, options) {
421
+ const { url, type, versionKey } = ssrEntry;
422
+ const { resolvedShared } = options;
319
423
  if (type === "commonjs-module" || type === "commonjs") {
320
424
  const { createRequire } = await _module();
321
425
  const req = createRequire(import.meta.url);
@@ -338,12 +442,18 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
338
442
  if (process.env.NODE_ENV !== "production") return null;
339
443
  }
340
444
  }
445
+ if (options.strategy === "vm") try {
446
+ const fromVm = await tryVmStrategy(ssrEntry, options);
447
+ if (fromVm) return fromVm;
448
+ } catch (error) {
449
+ if (isSsrEntryHttpError(error)) throw error;
450
+ }
341
451
  const { mkdirSync } = await _fs();
342
452
  const cacheDir = await getSSRCacheDir();
343
453
  mkdirSync(cacheDir, { recursive: true });
344
454
  const sharedPkgMap = new Map(Object.entries(resolvedShared));
345
455
  try {
346
- return await importTempModule(await fetchEsmToTempFile(url, cacheDir, /* @__PURE__ */ new Map(), sharedPkgMap));
456
+ return await importTempModule(await fetchEsmToTempFile(url, cacheDir, /* @__PURE__ */ new Map(), sharedPkgMap, versionKey), versionKey);
347
457
  } catch (error) {
348
458
  if (isSsrEntryHttpError(error)) throw error;
349
459
  return null;
@@ -359,18 +469,23 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
359
469
  }
360
470
  }
361
471
  function ssrEntryLoaderPlugin(options = {}) {
362
- const resolvedShared = options.resolvedShared ?? {};
472
+ const resolved = {
473
+ resolvedShared: options.resolvedShared ?? {},
474
+ strategy: options.strategy ?? "temp-file",
475
+ shareScopeName: options.shareScopeName ?? "default",
476
+ maxAgeMs: options.maxAgeMs
477
+ };
363
478
  return {
364
479
  name: "mf-vite:ssr-entry-loader",
365
480
  async loadEntry({ remoteInfo }) {
366
481
  if (!isNodeServer()) return;
367
- const ssrEntry = await getSSREntry(remoteInfo.entry);
482
+ const ssrEntry = await getSSREntry(remoteInfo.entry, resolved.maxAgeMs);
368
483
  if (!ssrEntry) return;
369
- const mod = await loadSSRRemoteEntry(ssrEntry, resolvedShared);
484
+ const mod = await loadSSRRemoteEntry(ssrEntry, resolved);
370
485
  if (!mod) return;
371
486
  return mod;
372
487
  }
373
488
  };
374
489
  }
375
490
  //#endregion
376
- export { ssrEntryLoaderPlugin as default };
491
+ export { SsrEntryHttpError, ssrEntryLoaderPlugin as default, neutralizeBrowserPreloadHelpers, revalidate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@module-federation/vite",
3
- "version": "1.16.12",
3
+ "version": "1.16.13",
4
4
  "description": "Vite plugin for Module Federation",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -86,4 +86,4 @@
86
86
  "vite": "8.1.0",
87
87
  "vitest": "4.0.18"
88
88
  }
89
- }
89
+ }