@module-federation/vite 1.23.0 → 1.23.2

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.
@@ -1,4 +1,4 @@
1
- import { t as EXTERNAL_URL_RE } from "./buildPaths-BoaQTkxt.js";
1
+ import { a as CodeRewriter, i as createCodePositionMap, r as mfWarn, s as EXTERNAL_URL_RE } from "./logger-DOwfTlw5.js";
2
2
  //#region src/utils/fetchWithTimeout.ts
3
3
  const DEFAULT_SSR_FETCH_TIMEOUT_MS = 1e4;
4
4
  const DEFAULT_SSR_FETCH_MAX_BYTES = 10 * 1024 * 1024;
@@ -98,6 +98,15 @@ async function readResponseTextBounded(res, maxBytes = DEFAULT_SSR_FETCH_MAX_BYT
98
98
  return new TextDecoder().decode(merged);
99
99
  }
100
100
  //#endregion
101
+ //#region src/utils/url.ts
102
+ function getUrlOrigin(value) {
103
+ try {
104
+ return new URL(value).origin;
105
+ } catch {
106
+ return;
107
+ }
108
+ }
109
+ //#endregion
101
110
  //#region src/utils/ssrEntryLoader.ts
102
111
  /**
103
112
  * MF runtime plugin that intercepts the `loadEntry` lifecycle hook on the
@@ -470,35 +479,55 @@ async function getSSREntry(remoteEntryUrl, maxAgeMs, fetchTimeoutMs, fetchMaxByt
470
479
  manifestFetchCache.delete(makeUrlCacheKey(getManifestUrl(remoteEntryUrl), fetchTimeoutMs, fetchMaxBytes));
471
480
  const record = setSsrEntryCache(remoteEntryUrl, fetchTimeoutMs, fetchMaxBytes);
472
481
  const next = await record.promise.catch(() => null);
473
- if (previous && next && previous.versionKey !== next.versionKey) {
474
- dropRemoteCaches(remoteEntryUrl);
475
- await clearRunnerCaches(remoteEntryUrl);
476
- }
482
+ if (previous && next && previous.versionKey !== next.versionKey) await invalidateRemoteCaches(remoteEntryUrl);
477
483
  return record.promise;
478
484
  }
479
485
  /**
480
- * Drop per-remote caches after a version change so old artifacts stop being
481
- * reused. Temp-file cache keys hold SSR entry/chunk URLs (not the browser
482
- * entry URL), so scope the invalidation by origin.
486
+ * Drop a remote graph's temp-file caches after a version change so old
487
+ * artifacts stop being reused. A generated module can be shared by multiple
488
+ * remote graphs, so cleanup is scoped by graph owner rather than URL origin.
483
489
  */
484
- function dropRemoteCaches(remoteEntryUrl) {
485
- let origin;
486
- try {
487
- origin = new URL(remoteEntryUrl).origin;
488
- } catch {
489
- return;
490
- }
491
- for (const [key] of tempFileCache) if (JSON.parse(key).find((part) => typeof part === "string" && /^https?:\/\//.test(part))?.startsWith(origin)) {
490
+ function dropRemoteTempFileCaches(remoteEntryUrl) {
491
+ const staleRecords = [];
492
+ for (const [key, record] of tempFileRecords) {
493
+ if (!record.owners.delete(remoteEntryUrl) || record.owners.size > 0) continue;
492
494
  tempFileCache.delete(key);
493
495
  tempFilePathCache.delete(key);
496
+ staleRecords.push(record);
494
497
  }
498
+ scheduleTempFileCleanup(staleRecords);
495
499
  }
500
+ /**
501
+ * Invalidate all SSR loader caches owned by a remote. The runtime loading map
502
+ * is cleared synchronously so `revalidate()` can safely clear module caches
503
+ * immediately after invoking this function.
504
+ */
505
+ function invalidateRemoteCaches(remoteEntryUrl) {
506
+ invalidateRuntimeRemoteEntry(remoteEntryUrl);
507
+ dropRemoteTempFileCaches(remoteEntryUrl);
508
+ scheduleVmCacheCleanup(remoteEntryUrl);
509
+ return clearRunnerCaches(remoteEntryUrl);
510
+ }
511
+ /**
512
+ * The VM strategy owns separate in-memory module-graph caches. Keep their
513
+ * invalidation aligned with the temp-file and ModuleRunner caches without a
514
+ * static import cycle between the two SSR strategies.
515
+ */
516
+ function scheduleVmCacheCleanup(remoteEntryUrl) {
517
+ if (!vmStrategyModulePromise) return;
518
+ vmStrategyModulePromise.then(({ clearVmStrategyCaches }) => clearVmStrategyCaches(remoteEntryUrl)).catch(() => {});
519
+ }
520
+ /**
521
+ * A dev-mode ModuleRunner is created per origin (one Vite dev server) and is
522
+ * shared by every remote that server hosts, so its module cache can only be
523
+ * cleared as a whole. Origin scoping is therefore the finest granularity here;
524
+ * clearing only costs a re-evaluation, no generated files are removed.
525
+ */
496
526
  async function clearRunnerCaches(remoteEntryUrl) {
497
527
  let remoteOrigin;
498
- if (remoteEntryUrl) try {
499
- remoteOrigin = new URL(remoteEntryUrl).origin;
500
- } catch {
501
- return;
528
+ if (remoteEntryUrl) {
529
+ remoteOrigin = getUrlOrigin(remoteEntryUrl);
530
+ if (!remoteOrigin) return;
502
531
  }
503
532
  await Promise.all([...runnerCache.values()].filter((cached) => !remoteOrigin || cached.remoteOrigin === remoteOrigin).map(async (cached) => {
504
533
  try {
@@ -506,6 +535,28 @@ async function clearRunnerCaches(remoteEntryUrl) {
506
535
  } catch {}
507
536
  }));
508
537
  }
538
+ /** Invalidate all SSR loader caches across every remote. */
539
+ function invalidateAllRemoteCaches() {
540
+ invalidateRuntimeRemoteEntry();
541
+ const staleRecords = [...tempFileRecords.values()];
542
+ for (const record of staleRecords) record.owners.clear();
543
+ tempFileCache.clear();
544
+ tempFilePathCache.clear();
545
+ scheduleTempFileCleanup(staleRecords);
546
+ scheduleVmCacheCleanup();
547
+ clearRunnerCaches();
548
+ }
549
+ /**
550
+ * runtime-core deduplicates remote entry loads in a process-global map. The
551
+ * SSR loader's module cache invalidation must evict the matching runtime entry
552
+ * too, otherwise the next loadRemote() returns the pre-revalidation container
553
+ * without invoking this loader again.
554
+ */
555
+ function invalidateRuntimeRemoteEntry(remoteEntryUrl) {
556
+ const globalLoading = globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__;
557
+ if (!globalLoading) return;
558
+ for (const key of Object.keys(globalLoading)) if (remoteEntryUrl === void 0 || key === remoteEntryUrl || key.endsWith(`:${remoteEntryUrl}`)) delete globalLoading[key];
559
+ }
509
560
  /**
510
561
  * Drop the loader's caches so the next `loadEntry` re-resolves and re-fetches
511
562
  * remote SSR entries. Pass a remote entry URL to scope the invalidation to one
@@ -522,15 +573,13 @@ function revalidate(remoteEntryUrl) {
522
573
  for (const key of ssrEntryCache.keys()) if (key.endsWith(`::${remoteEntryUrl}`)) ssrEntryCache.delete(key);
523
574
  const manifestUrl = getManifestUrl(remoteEntryUrl);
524
575
  for (const key of manifestFetchCache.keys()) if (key.endsWith(`::${manifestUrl}`)) manifestFetchCache.delete(key);
525
- dropRemoteCaches(remoteEntryUrl);
576
+ invalidateRemoteCaches(remoteEntryUrl);
526
577
  } else {
527
578
  unversionedGlobalGeneration += 1;
528
579
  ssrEntryCache.clear();
529
580
  manifestFetchCache.clear();
530
- tempFileCache.clear();
531
- tempFilePathCache.clear();
581
+ invalidateAllRemoteCaches();
532
582
  }
533
- clearRunnerCaches(remoteEntryUrl);
534
583
  const federation = globalThis.__FEDERATION__;
535
584
  for (const instance of federation?.__INSTANCES__ ?? []) try {
536
585
  instance?.moduleCache?.clear?.();
@@ -538,6 +587,74 @@ function revalidate(remoteEntryUrl) {
538
587
  }
539
588
  const tempFileCache = /* @__PURE__ */ new Map();
540
589
  const tempFilePathCache = /* @__PURE__ */ new Map();
590
+ const TEMP_FILE_CLEANUP_DELAY_MS = 3e4;
591
+ const tempFileRecords = /* @__PURE__ */ new Map();
592
+ const scheduledTempFileCleanup = /* @__PURE__ */ new Set();
593
+ function scheduleTempFileCleanup(records) {
594
+ for (const record of records) {
595
+ if (scheduledTempFileCleanup.has(record.cacheKey)) continue;
596
+ scheduledTempFileCleanup.add(record.cacheKey);
597
+ const timer = setTimeout(() => {
598
+ cleanupTempFileRecord(record);
599
+ }, TEMP_FILE_CLEANUP_DELAY_MS);
600
+ if (typeof timer === "object" && timer && "unref" in timer) timer.unref?.();
601
+ }
602
+ }
603
+ async function cleanupTempFileRecord(record) {
604
+ try {
605
+ await record.promise.catch(() => void 0);
606
+ if (record.owners.size > 0) return;
607
+ if (tempFileRecords.get(record.cacheKey) !== record) return;
608
+ const filePath = await record.filePathPromise;
609
+ const { rmSync } = await _fs();
610
+ rmSync(filePath, { force: true });
611
+ } catch {} finally {
612
+ if (tempFileRecords.get(record.cacheKey) === record) tempFileRecords.delete(record.cacheKey);
613
+ scheduledTempFileCleanup.delete(record.cacheKey);
614
+ }
615
+ }
616
+ const requireShimCache = /* @__PURE__ */ new Map();
617
+ /**
618
+ * `resolvedShared` rewrites import/export specifiers only. Bundled CommonJS in a
619
+ * remote requires shared packages through `createRequire(import.meta.url)` —
620
+ * Rolldown's `__require("react")` — and Node resolves those from the temp dir,
621
+ * which can reach another copy than the pinned one. Remote modules importing
622
+ * `node:module` get this shim instead: its `createRequire` resolves pinned
623
+ * specifiers to the same files the rewritten imports load.
624
+ */
625
+ function createRequireShimSource(sharedPkgMap) {
626
+ return [
627
+ "import * as nodeModule from 'node:module';",
628
+ "export * from 'node:module';",
629
+ "export default nodeModule.default;",
630
+ `const pinned = new Map(${JSON.stringify([...sharedPkgMap])});`,
631
+ "const pin = (id) => (typeof id === 'string' && pinned.get(id)) || id;",
632
+ "export function createRequire(filename) {",
633
+ " const require = nodeModule.createRequire(filename);",
634
+ " const resolve = (id, options) => require.resolve(pin(id), options);",
635
+ " resolve.paths = require.resolve.paths;",
636
+ " return Object.assign((id) => require(pin(id)), require, { resolve });",
637
+ "}",
638
+ ""
639
+ ].join("\n");
640
+ }
641
+ function getRequireShimUrl(tmpDir, sharedPkgMap, contextKey) {
642
+ const cacheKey = JSON.stringify([tmpDir, contextKey]);
643
+ const cached = requireShimCache.get(cacheKey);
644
+ if (cached) return cached;
645
+ const promise = (async () => {
646
+ const { join } = await _path();
647
+ const { writeFileSync } = await _fs();
648
+ const file = join(tmpDir, `node-module-${hashString(contextKey)}.mjs`);
649
+ writeFileSync(file, createRequireShimSource(sharedPkgMap), "utf8");
650
+ return `file://${file}`;
651
+ })();
652
+ requireShimCache.set(cacheKey, promise);
653
+ promise.catch(() => {
654
+ if (requireShimCache.get(cacheKey) === promise) requireShimCache.delete(cacheKey);
655
+ });
656
+ return promise;
657
+ }
541
658
  function getSsrTransformContextKey(resolvedShared, shareScopeName) {
542
659
  return JSON.stringify([shareScopeName, getSortedRecordEntries(resolvedShared)]);
543
660
  }
@@ -574,15 +691,57 @@ function neutralizeBrowserPreloadHelpers(code) {
574
691
  code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
575
692
  return code;
576
693
  }
577
- function transformSsrCode(code, base, sharedPkgMap) {
578
- code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
579
- code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
580
- code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
581
- if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
582
- const resolved = sharedPkgMap.get(specifier);
583
- return resolved ? m.replace(specifier, `file://${resolved}`) : m;
694
+ const SSR_MODULE_SPECIFIER_RE = /(?:from|export\s*\*\s*from|import\s*\(?\s*)\s*(["'`])([^"'`\s]+)\1/g;
695
+ function isRelativeSpecifier(specifier) {
696
+ return specifier.startsWith("./") || specifier.startsWith("../");
697
+ }
698
+ /**
699
+ * Module specifiers used by the import/export syntax of `code`. Import-like
700
+ * text inside comments and string literals (JSDoc such as
701
+ * `@type {import('./x')}`, docs, error messages) is skipped, and a comment
702
+ * between `import(` and the specifier (webpack-style magic comments) does not
703
+ * hide a real dynamic import. Discovery and rewriting both read this list, so
704
+ * they always agree on which specifiers exist.
705
+ */
706
+ function findSsrModuleSpecifiers(code) {
707
+ let blanked = "";
708
+ let blankedUntil = 0;
709
+ const isCode = createCodePositionMap(code, (start, end) => {
710
+ blanked += code.slice(blankedUntil, start) + " ".repeat(end - start);
711
+ blankedUntil = end;
584
712
  });
585
- return neutralizeBrowserPreloadHelpers(code);
713
+ blanked += code.slice(blankedUntil);
714
+ const specifiers = [];
715
+ for (const match of blanked.matchAll(SSR_MODULE_SPECIFIER_RE)) {
716
+ if (!isCode[match.index]) continue;
717
+ const end = match.index + match[0].length;
718
+ const value = match[2];
719
+ specifiers.push({
720
+ start: end - value.length - 2,
721
+ end,
722
+ value
723
+ });
724
+ }
725
+ return specifiers;
726
+ }
727
+ function isNodeModuleSpecifier(specifier) {
728
+ return specifier === "node:module" || specifier === "module";
729
+ }
730
+ function transformSsrCode(code, base, specifiers, sharedPkgMap, requireShimUrl, tempFileUrlMap) {
731
+ const rewriter = new CodeRewriter(code);
732
+ for (const { start, end, value } of specifiers) {
733
+ if (requireShimUrl && isNodeModuleSpecifier(value)) {
734
+ rewriter.overwrite(start, end, `"${requireShimUrl}"`);
735
+ continue;
736
+ }
737
+ const resolvedShared = sharedPkgMap?.get(value);
738
+ const relativeUrl = isRelativeSpecifier(value) ? new URL(value, base).href : void 0;
739
+ const absoluteHttpUrl = value.startsWith("http://") || value.startsWith("https://") ? value : void 0;
740
+ const httpUrl = relativeUrl ?? absoluteHttpUrl;
741
+ const replacement = (httpUrl ? tempFileUrlMap?.get(httpUrl) : void 0) ?? relativeUrl ?? (resolvedShared && `file://${resolvedShared}`);
742
+ if (replacement) rewriter.overwrite(start, end, `"${replacement}"`);
743
+ }
744
+ return neutralizeBrowserPreloadHelpers(rewriter.toString());
586
745
  }
587
746
  function isVitePreloadHelperSpecifier(specifier) {
588
747
  return specifier.includes("preload-helper");
@@ -591,25 +750,27 @@ function getTempFileImportUrl(filePath, versionKey) {
591
750
  return `file://${filePath}?v=${encodeURIComponent(versionKey)}`;
592
751
  }
593
752
  /**
594
- * Fetch an HTTP ESM module, transform it, write it to a temp .js file and
753
+ * Fetch an HTTP ESM module, transform it, write it to a temp file and
595
754
  * return the file path. Recursively does the same for HTTP transitive imports
596
- * so that `import('file:///...temp.js')` can resolve them.
755
+ * so that `import('file:///...temp.mjs')` can resolve them.
597
756
  *
598
757
  * `versionKey` participates in both the cache key and the temp file name, so
599
758
  * a remote redeploy (new manifest → new key) produces new files and bypasses
600
759
  * Node's ESM module cache instead of serving the stale build.
601
760
  */
602
- async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES) {
761
+ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES, extension = ".mjs", owner) {
603
762
  const cacheKey = JSON.stringify([
604
763
  fetchTimeoutMs,
605
764
  fetchMaxBytes,
606
765
  versionKey,
607
766
  url,
608
- contextKey
767
+ contextKey,
768
+ extension
609
769
  ]);
610
770
  if (visited.has(url)) return visited.get(url);
611
771
  const cached = tempFileCache.get(cacheKey);
612
772
  if (cached) {
773
+ if (owner) tempFileRecords.get(cacheKey)?.owners.add(owner);
613
774
  pending.add(cached);
614
775
  const reserved = tempFilePathCache.get(cacheKey);
615
776
  const tmpFile = reserved ? await reserved : await cached;
@@ -619,7 +780,7 @@ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, v
619
780
  const tmpFilePromise = (async () => {
620
781
  const { createHash } = await _crypto();
621
782
  const { join } = await _path();
622
- return join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}.js`);
783
+ return join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}${extension}`);
623
784
  })();
624
785
  tempFilePathCache.set(cacheKey, tmpFilePromise);
625
786
  const promise = (async () => {
@@ -629,32 +790,38 @@ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, v
629
790
  let code = await readResponseTextBounded(res, fetchMaxBytes, url);
630
791
  if (!res.ok) throw new SsrEntryHttpError(url, res.status, res.statusText, getBodyPreview(code));
631
792
  const base = url.replace(/\/[^/]*$/, "/");
632
- const relImports = [];
633
- const relRegex = /(?:from|export\s*\*\s*from|import\s*\(?\s*)\s*["'`]([^"'`\s]+)["'`]/g;
634
- let m;
635
- while ((m = relRegex.exec(code)) !== null) if ((m[1].startsWith("./") || m[1].startsWith("../")) && !isVitePreloadHelperSpecifier(m[1])) relImports.push(new URL(m[1], base).href);
793
+ const specifiers = findSsrModuleSpecifiers(code);
794
+ const relImports = specifiers.map(({ value }) => value).filter((value) => isRelativeSpecifier(value) && !isVitePreloadHelperSpecifier(value)).map((value) => new URL(value, base).href);
636
795
  const subMap = /* @__PURE__ */ new Map();
637
796
  await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
638
- const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes);
797
+ const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes, extension, owner);
639
798
  subMap.set(u, getTempFileImportUrl(tmpPath, versionKey));
640
799
  }));
641
- code = transformSsrCode(code, base, sharedPkgMap);
642
- for (const [httpUrl, fileUrl] of subMap) code = code.split(httpUrl).join(fileUrl);
800
+ const requireShimUrl = sharedPkgMap?.size && specifiers.some(({ value }) => isNodeModuleSpecifier(value)) ? await getRequireShimUrl(tmpDir, sharedPkgMap, contextKey) : void 0;
801
+ code = transformSsrCode(code, base, specifiers, sharedPkgMap, requireShimUrl, subMap);
643
802
  const { writeFileSync } = await _fs();
644
803
  writeFileSync(tmpFile, code, "utf8");
645
804
  return tmpFile;
646
805
  })();
806
+ const record = {
807
+ cacheKey,
808
+ filePathPromise: tmpFilePromise,
809
+ promise,
810
+ owners: owner ? /* @__PURE__ */ new Set([owner]) : /* @__PURE__ */ new Set()
811
+ };
812
+ tempFileRecords.set(cacheKey, record);
647
813
  tempFileCache.set(cacheKey, promise);
648
814
  pending.add(promise);
649
815
  promise.catch(() => {
650
816
  if (tempFileCache.get(cacheKey) === promise) tempFileCache.delete(cacheKey);
651
817
  if (tempFilePathCache.get(cacheKey) === tmpFilePromise) tempFilePathCache.delete(cacheKey);
818
+ if (tempFileRecords.get(cacheKey) === record && !scheduledTempFileCleanup.has(cacheKey)) tempFileRecords.delete(cacheKey);
652
819
  });
653
820
  return promise;
654
821
  }
655
- async function fetchEsmGraphToTempFile(url, tmpDir, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES) {
822
+ async function fetchEsmGraphToTempFile(url, tmpDir, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES, extension = ".mjs", owner) {
656
823
  const pending = /* @__PURE__ */ new Set();
657
- const rootFile = await fetchEsmToTempFile(url, tmpDir, /* @__PURE__ */ new Map(), pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes);
824
+ const rootFile = await fetchEsmToTempFile(url, tmpDir, /* @__PURE__ */ new Map(), pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes, extension, owner);
658
825
  await Promise.all(pending);
659
826
  return rootFile;
660
827
  }
@@ -665,12 +832,25 @@ async function importTempModule(filePath, versionKey) {
665
832
  );
666
833
  }
667
834
  let warnedVmUnavailable = false;
668
- async function tryVmStrategy(ssrEntry, options) {
669
- const { loadViaVmStrategy, isVmStrategyAvailable } = await import("./ssrVmStrategy-CDbcwMnf.js");
835
+ let vmStrategyModulePromise;
836
+ function getVmStrategyModule() {
837
+ if (!vmStrategyModulePromise) vmStrategyModulePromise = import("./ssrVmStrategy-COMHOCN2.js");
838
+ return vmStrategyModulePromise;
839
+ }
840
+ /**
841
+ * Non-HTTP failures keep falling back (to temp-file after vm, to the federation
842
+ * runtime's own Node loader after temp-file), but whatever the fallback throws
843
+ * then hides the real cause, so report it first.
844
+ */
845
+ function warnStrategyFallback(strategy, url, fallback, error) {
846
+ mfWarn(`SSR entry loader: strategy "${strategy}" failed to load ${url}; falling back to ${fallback}.`, error);
847
+ }
848
+ async function tryVmStrategy(ssrEntry, options, rootEntryUrl) {
849
+ const { loadViaVmStrategy, isVmStrategyAvailable } = await getVmStrategyModule();
670
850
  if (!await isVmStrategyAvailable()) {
671
851
  if (!warnedVmUnavailable) {
672
852
  warnedVmUnavailable = true;
673
- 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.");
853
+ mfWarn("SSR entry loader: strategy \"vm\" requires vm.SourceTextModule (run Node with --experimental-vm-modules); falling back to the temp-file strategy.");
674
854
  }
675
855
  return null;
676
856
  }
@@ -681,10 +861,11 @@ async function tryVmStrategy(ssrEntry, options) {
681
861
  fetchTimeoutMs: options.fetchTimeoutMs,
682
862
  fetchMaxBytes: options.fetchMaxBytes,
683
863
  cacheContext: options.cacheContext,
684
- federationInstance: options.federationInstance
864
+ federationInstance: options.federationInstance,
865
+ rootEntryUrl
685
866
  });
686
867
  }
687
- async function loadSSRRemoteEntry(ssrEntry, options) {
868
+ async function loadSSRRemoteEntry(ssrEntry, options, rootEntryUrl) {
688
869
  const { url, type, versionKey } = ssrEntry;
689
870
  const { resolvedShared } = options;
690
871
  if (type === "commonjs-module" || type === "commonjs") {
@@ -711,19 +892,22 @@ async function loadSSRRemoteEntry(ssrEntry, options) {
711
892
  }
712
893
  }
713
894
  if (options.strategy === "vm") try {
714
- const fromVm = await tryVmStrategy(ssrEntry, options);
895
+ const fromVm = await tryVmStrategy(ssrEntry, options, rootEntryUrl);
715
896
  if (fromVm) return fromVm;
716
897
  } catch (error) {
717
898
  if (isSsrEntryHttpError(error) || isSsrFetchBodyTooLargeError(error)) throw error;
899
+ warnStrategyFallback("vm", url, "the temp-file strategy", error);
718
900
  }
719
901
  const { mkdirSync } = await _fs();
720
902
  const cacheDir = await getSSRCacheDir();
721
903
  mkdirSync(cacheDir, { recursive: true });
722
904
  const sharedPkgMap = new Map(Object.entries(resolvedShared));
905
+ const extension = type === "commonjs-module" || type === "commonjs" ? ".js" : ".mjs";
723
906
  try {
724
- return await importTempModule(await fetchEsmGraphToTempFile(url, cacheDir, sharedPkgMap, versionKey, options.fetchTimeoutMs, getSsrTransformContextKey(resolvedShared, options.shareScopeName), options.fetchMaxBytes), versionKey);
907
+ return await importTempModule(await fetchEsmGraphToTempFile(url, cacheDir, sharedPkgMap, versionKey, options.fetchTimeoutMs, getSsrTransformContextKey(resolvedShared, options.shareScopeName), options.fetchMaxBytes, extension, rootEntryUrl), versionKey);
725
908
  } catch (error) {
726
909
  if (isSsrEntryHttpError(error) || isSsrFetchBodyTooLargeError(error)) throw error;
910
+ warnStrategyFallback("temp-file", url, "the federation runtime's loader", error);
727
911
  return null;
728
912
  }
729
913
  }
@@ -757,7 +941,7 @@ function ssrEntryLoaderPlugin(options = {}) {
757
941
  } : resolved;
758
942
  const ssrEntry = await getSSREntry(remoteInfo.entry, loadOptions.maxAgeMs, loadOptions.fetchTimeoutMs, loadOptions.fetchMaxBytes);
759
943
  if (!ssrEntry) return;
760
- const mod = await loadSSRRemoteEntry(ssrEntry, loadOptions);
944
+ const mod = await loadSSRRemoteEntry(ssrEntry, loadOptions, remoteInfo.entry);
761
945
  if (!mod) return;
762
946
  return mod;
763
947
  }
@@ -1,5 +1,5 @@
1
1
  import { t as findSharedKey } from "./sharedKeyMatcher-Od_nYxJA.js";
2
- import { c as readResponseTextBounded, n as neutralizeBrowserPreloadHelpers, s as fetchWithTimeout, t as SsrEntryHttpError } from "./ssrEntryLoader-BvAkpCyi.js";
2
+ import { c as readResponseTextBounded, n as neutralizeBrowserPreloadHelpers, s as fetchWithTimeout, t as SsrEntryHttpError } from "./ssrEntryLoader-DEH3eV1e.js";
3
3
  //#region src/utils/ssrVmStrategy.ts
4
4
  /**
5
5
  * vm.SourceTextModule strategy for loading remote SSR entries.
@@ -73,10 +73,38 @@ async function loadBareModule(specifier, options) {
73
73
  /* @vite-ignore */
74
74
  `file://${resolvedPath}`
75
75
  );
76
- return import(
76
+ const namespace = await import(
77
77
  /* @vite-ignore */
78
78
  specifier
79
79
  );
80
+ return specifier === "module" || specifier === "node:module" ? withRemoteCreateRequire(namespace, options.resolvedShared) : namespace;
81
+ }
82
+ /**
83
+ * Remote modules keep their http(s) URL as `import.meta.url`, which Node's
84
+ * `createRequire` rejects. Rolldown's runtime for Node-targeted output calls
85
+ * `createRequire(import.meta.url)` as soon as a bundled CommonJS module
86
+ * requires an external, so hand remote modules a `createRequire` that resolves
87
+ * those URLs from the app root — the base the temp-file strategy's temp files
88
+ * resolve from. Its `require` also pins `resolvedShared` specifiers to the
89
+ * same files the linker loads for the matching imports, so a bundled
90
+ * `__require("react")` cannot reach a second copy of a shared package.
91
+ */
92
+ async function withRemoteCreateRequire(nodeModule, resolvedShared) {
93
+ const { pathToFileURL } = await import(
94
+ /* @vite-ignore */
95
+ "url"
96
+ );
97
+ const appRoot = pathToFileURL(`${process.cwd()}/`);
98
+ const pin = (id) => resolvedShared[id] ?? id;
99
+ return {
100
+ ...nodeModule,
101
+ createRequire: (filename) => {
102
+ const require = nodeModule.createRequire(isHttpUrl(String(filename)) ? appRoot : filename);
103
+ const resolve = ((id, options) => require.resolve(pin(id), options));
104
+ resolve.paths = require.resolve.paths;
105
+ return Object.assign((id) => require(pin(id)), require, { resolve });
106
+ }
107
+ };
80
108
  }
81
109
  function createSyntheticModule(vm, specifier, namespace) {
82
110
  const source = namespace && typeof namespace === "object" ? namespace : { default: namespace };
@@ -90,6 +118,8 @@ function createSyntheticModule(vm, specifier, namespace) {
90
118
  }
91
119
  const httpModuleCache = /* @__PURE__ */ new Map();
92
120
  const namespaceCache = /* @__PURE__ */ new Map();
121
+ const httpModuleCacheOwners = /* @__PURE__ */ new Map();
122
+ const namespaceCacheOwners = /* @__PURE__ */ new Map();
93
123
  const linkQueues = /* @__PURE__ */ new WeakMap();
94
124
  const contextIds = /* @__PURE__ */ new WeakMap();
95
125
  let nextContextId = 1;
@@ -108,6 +138,31 @@ function getVmCacheContextKey(options) {
108
138
  Object.entries(options.resolvedShared).sort(([left], [right]) => left.localeCompare(right))
109
139
  ]);
110
140
  }
141
+ function addVmCacheOwner(ownersByCacheKey, cacheKey, rootEntryUrl) {
142
+ if (!rootEntryUrl) return;
143
+ const owners = ownersByCacheKey.get(cacheKey) ?? /* @__PURE__ */ new Set();
144
+ owners.add(rootEntryUrl);
145
+ ownersByCacheKey.set(cacheKey, owners);
146
+ }
147
+ function clearOwnedVmCache(cache, ownersByCacheKey, remoteEntryUrl) {
148
+ for (const [cacheKey, owners] of ownersByCacheKey) {
149
+ if (remoteEntryUrl !== void 0 && !owners.delete(remoteEntryUrl)) continue;
150
+ if (remoteEntryUrl !== void 0 && owners.size > 0) continue;
151
+ cache.delete(cacheKey);
152
+ ownersByCacheKey.delete(cacheKey);
153
+ }
154
+ }
155
+ /**
156
+ * Drop cached VM module graphs after a remote revalidation. A module graph can
157
+ * contain URLs from another origin, and one origin can serve several remotes,
158
+ * so ownership is tracked by the owning remote entry URL rather than inferred
159
+ * from each module URL. A module shared by several graphs stays cached while
160
+ * another remote still owns it.
161
+ */
162
+ function clearVmStrategyCaches(remoteEntryUrl) {
163
+ clearOwnedVmCache(httpModuleCache, httpModuleCacheOwners, remoteEntryUrl);
164
+ clearOwnedVmCache(namespaceCache, namespaceCacheOwners, remoteEntryUrl);
165
+ }
111
166
  function getBodyPreview(body) {
112
167
  return body.slice(0, 240).replace(/\s+/g, " ").trim();
113
168
  }
@@ -134,6 +189,7 @@ function getHttpModule(vm, url, options) {
134
189
  options.versionKey,
135
190
  url
136
191
  ]);
192
+ addVmCacheOwner(httpModuleCacheOwners, cacheKey, options.rootEntryUrl);
137
193
  if (!httpModuleCache.has(cacheKey)) httpModuleCache.set(cacheKey, (async () => {
138
194
  const code = await fetchModuleSource(url, options.fetchTimeoutMs, options.fetchMaxBytes);
139
195
  return new vm.SourceTextModule(code, {
@@ -145,6 +201,7 @@ function getHttpModule(vm, url, options) {
145
201
  });
146
202
  })().catch((error) => {
147
203
  httpModuleCache.delete(cacheKey);
204
+ httpModuleCacheOwners.delete(cacheKey);
148
205
  throw error;
149
206
  }));
150
207
  return httpModuleCache.get(cacheKey);
@@ -180,18 +237,25 @@ async function importDynamically(vm, specifier, referencingModule, options) {
180
237
  async function loadViaVmStrategy(entryUrl, options) {
181
238
  const vm = await getVmApi();
182
239
  if (!vm) return null;
240
+ const rootEntryUrl = options.rootEntryUrl ?? entryUrl;
241
+ const graphOptions = {
242
+ ...options,
243
+ rootEntryUrl
244
+ };
183
245
  const cacheKey = `${getVmCacheContextKey(options)}::${options.versionKey}::${entryUrl}`;
246
+ addVmCacheOwner(namespaceCacheOwners, cacheKey, rootEntryUrl);
184
247
  if (!namespaceCache.has(cacheKey)) namespaceCache.set(cacheKey, (async () => {
185
- const entryModule = await getHttpModule(vm, entryUrl, options);
186
- const linker = (specifier, referencingModule) => linkModule(vm, specifier, referencingModule, options);
248
+ const entryModule = await getHttpModule(vm, entryUrl, graphOptions);
249
+ const linker = (specifier, referencingModule) => linkModule(vm, specifier, referencingModule, graphOptions);
187
250
  await linkModuleGraph(entryModule, linker, options.cacheContext);
188
251
  if (entryModule.status === "linked") await entryModule.evaluate();
189
252
  return entryModule.namespace;
190
253
  })().catch((error) => {
191
254
  namespaceCache.delete(cacheKey);
255
+ namespaceCacheOwners.delete(cacheKey);
192
256
  throw error;
193
257
  }));
194
258
  return namespaceCache.get(cacheKey);
195
259
  }
196
260
  //#endregion
197
- export { isVmStrategyAvailable, loadViaVmStrategy };
261
+ export { clearVmStrategyCaches, findVmSharedKey, isVmStrategyAvailable, loadViaVmStrategy };
@@ -1,2 +1,2 @@
1
- import { i as ssrEntryLoaderPlugin, n as neutralizeBrowserPreloadHelpers, r as revalidate, t as SsrEntryHttpError } from "../ssrEntryLoader-BvAkpCyi.js";
1
+ import { i as ssrEntryLoaderPlugin, n as neutralizeBrowserPreloadHelpers, r as revalidate, t as SsrEntryHttpError } from "../ssrEntryLoader-DEH3eV1e.js";
2
2
  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.23.0",
3
+ "version": "1.23.2",
4
4
  "description": "Vite plugin for Module Federation",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -77,9 +77,9 @@
77
77
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
78
78
  },
79
79
  "dependencies": {
80
- "@module-federation/dts-plugin": "2.9.1",
81
- "@module-federation/runtime": "2.9.1",
82
- "@module-federation/sdk": "2.9.1"
80
+ "@module-federation/dts-plugin": "2.9.2",
81
+ "@module-federation/runtime": "2.9.2",
82
+ "@module-federation/sdk": "2.9.2"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@playwright/test": "1.62.0",