@module-federation/vite 1.23.0 → 1.23.1

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--uKHOVjR.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,44 @@ 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 per-remote temp-file caches after a version change so old artifacts stop
487
+ * being reused. Temp-file cache keys hold SSR entry/chunk URLs (not the
488
+ * browser entry URL), so scope the invalidation by origin.
483
489
  */
484
- function dropRemoteCaches(remoteEntryUrl) {
485
- let origin;
486
- try {
487
- origin = new URL(remoteEntryUrl).origin;
488
- } catch {
489
- return;
490
- }
490
+ function dropRemoteTempFileCaches(remoteEntryUrl) {
491
+ const origin = getUrlOrigin(remoteEntryUrl);
492
+ if (!origin) return;
493
+ const staleRecords = [...tempFileRecords.values()].filter((record) => record.remoteOrigin === origin);
491
494
  for (const [key] of tempFileCache) if (JSON.parse(key).find((part) => typeof part === "string" && /^https?:\/\//.test(part))?.startsWith(origin)) {
492
495
  tempFileCache.delete(key);
493
496
  tempFilePathCache.delete(key);
494
497
  }
498
+ scheduleTempFileCleanup(staleRecords);
499
+ }
500
+ /** Invalidate all SSR loader caches owned by a remote. */
501
+ async function invalidateRemoteCaches(remoteEntryUrl) {
502
+ dropRemoteTempFileCaches(remoteEntryUrl);
503
+ scheduleVmCacheCleanup(remoteEntryUrl);
504
+ await clearRunnerCaches(remoteEntryUrl);
505
+ }
506
+ /**
507
+ * The VM strategy owns separate in-memory module-graph caches. Keep their
508
+ * invalidation aligned with the temp-file and ModuleRunner caches without a
509
+ * static import cycle between the two SSR strategies.
510
+ */
511
+ function scheduleVmCacheCleanup(remoteEntryUrl) {
512
+ if (!vmStrategyModulePromise) return;
513
+ vmStrategyModulePromise.then(({ clearVmStrategyCaches }) => clearVmStrategyCaches(remoteEntryUrl)).catch(() => {});
495
514
  }
496
515
  async function clearRunnerCaches(remoteEntryUrl) {
497
516
  let remoteOrigin;
498
- if (remoteEntryUrl) try {
499
- remoteOrigin = new URL(remoteEntryUrl).origin;
500
- } catch {
501
- return;
517
+ if (remoteEntryUrl) {
518
+ remoteOrigin = getUrlOrigin(remoteEntryUrl);
519
+ if (!remoteOrigin) return;
502
520
  }
503
521
  await Promise.all([...runnerCache.values()].filter((cached) => !remoteOrigin || cached.remoteOrigin === remoteOrigin).map(async (cached) => {
504
522
  try {
@@ -506,6 +524,15 @@ async function clearRunnerCaches(remoteEntryUrl) {
506
524
  } catch {}
507
525
  }));
508
526
  }
527
+ /** Invalidate all SSR loader caches across every remote. */
528
+ function invalidateAllRemoteCaches() {
529
+ const staleRecords = [...tempFileRecords.values()];
530
+ tempFileCache.clear();
531
+ tempFilePathCache.clear();
532
+ scheduleTempFileCleanup(staleRecords);
533
+ scheduleVmCacheCleanup();
534
+ clearRunnerCaches();
535
+ }
509
536
  /**
510
537
  * Drop the loader's caches so the next `loadEntry` re-resolves and re-fetches
511
538
  * remote SSR entries. Pass a remote entry URL to scope the invalidation to one
@@ -522,15 +549,13 @@ function revalidate(remoteEntryUrl) {
522
549
  for (const key of ssrEntryCache.keys()) if (key.endsWith(`::${remoteEntryUrl}`)) ssrEntryCache.delete(key);
523
550
  const manifestUrl = getManifestUrl(remoteEntryUrl);
524
551
  for (const key of manifestFetchCache.keys()) if (key.endsWith(`::${manifestUrl}`)) manifestFetchCache.delete(key);
525
- dropRemoteCaches(remoteEntryUrl);
552
+ invalidateRemoteCaches(remoteEntryUrl);
526
553
  } else {
527
554
  unversionedGlobalGeneration += 1;
528
555
  ssrEntryCache.clear();
529
556
  manifestFetchCache.clear();
530
- tempFileCache.clear();
531
- tempFilePathCache.clear();
557
+ invalidateAllRemoteCaches();
532
558
  }
533
- clearRunnerCaches(remoteEntryUrl);
534
559
  const federation = globalThis.__FEDERATION__;
535
560
  for (const instance of federation?.__INSTANCES__ ?? []) try {
536
561
  instance?.moduleCache?.clear?.();
@@ -538,6 +563,73 @@ function revalidate(remoteEntryUrl) {
538
563
  }
539
564
  const tempFileCache = /* @__PURE__ */ new Map();
540
565
  const tempFilePathCache = /* @__PURE__ */ new Map();
566
+ const TEMP_FILE_CLEANUP_DELAY_MS = 3e4;
567
+ const tempFileRecords = /* @__PURE__ */ new Map();
568
+ const scheduledTempFileCleanup = /* @__PURE__ */ new Set();
569
+ function scheduleTempFileCleanup(records) {
570
+ for (const record of records) {
571
+ if (scheduledTempFileCleanup.has(record.cacheKey)) continue;
572
+ scheduledTempFileCleanup.add(record.cacheKey);
573
+ const timer = setTimeout(() => {
574
+ cleanupTempFileRecord(record);
575
+ }, TEMP_FILE_CLEANUP_DELAY_MS);
576
+ if (typeof timer === "object" && timer && "unref" in timer) timer.unref?.();
577
+ }
578
+ }
579
+ async function cleanupTempFileRecord(record) {
580
+ try {
581
+ await record.promise.catch(() => void 0);
582
+ if (tempFileRecords.get(record.cacheKey) !== record) return;
583
+ const filePath = await record.filePathPromise;
584
+ const { rmSync } = await _fs();
585
+ rmSync(filePath, { force: true });
586
+ } catch {} finally {
587
+ if (tempFileRecords.get(record.cacheKey) === record) tempFileRecords.delete(record.cacheKey);
588
+ scheduledTempFileCleanup.delete(record.cacheKey);
589
+ }
590
+ }
591
+ const requireShimCache = /* @__PURE__ */ new Map();
592
+ /**
593
+ * `resolvedShared` rewrites import/export specifiers only. Bundled CommonJS in a
594
+ * remote requires shared packages through `createRequire(import.meta.url)` —
595
+ * Rolldown's `__require("react")` — and Node resolves those from the temp dir,
596
+ * which can reach another copy than the pinned one. Remote modules importing
597
+ * `node:module` get this shim instead: its `createRequire` resolves pinned
598
+ * specifiers to the same files the rewritten imports load.
599
+ */
600
+ function createRequireShimSource(sharedPkgMap) {
601
+ return [
602
+ "import * as nodeModule from 'node:module';",
603
+ "export * from 'node:module';",
604
+ "export default nodeModule.default;",
605
+ `const pinned = new Map(${JSON.stringify([...sharedPkgMap])});`,
606
+ "const pin = (id) => (typeof id === 'string' && pinned.get(id)) || id;",
607
+ "export function createRequire(filename) {",
608
+ " const require = nodeModule.createRequire(filename);",
609
+ " const resolve = (id, options) => require.resolve(pin(id), options);",
610
+ " resolve.paths = require.resolve.paths;",
611
+ " return Object.assign((id) => require(pin(id)), require, { resolve });",
612
+ "}",
613
+ ""
614
+ ].join("\n");
615
+ }
616
+ function getRequireShimUrl(tmpDir, sharedPkgMap, contextKey) {
617
+ const cacheKey = JSON.stringify([tmpDir, contextKey]);
618
+ const cached = requireShimCache.get(cacheKey);
619
+ if (cached) return cached;
620
+ const promise = (async () => {
621
+ const { join } = await _path();
622
+ const { writeFileSync } = await _fs();
623
+ const file = join(tmpDir, `node-module-${hashString(contextKey)}.mjs`);
624
+ writeFileSync(file, createRequireShimSource(sharedPkgMap), "utf8");
625
+ return `file://${file}`;
626
+ })();
627
+ requireShimCache.set(cacheKey, promise);
628
+ promise.catch(() => {
629
+ if (requireShimCache.get(cacheKey) === promise) requireShimCache.delete(cacheKey);
630
+ });
631
+ return promise;
632
+ }
541
633
  function getSsrTransformContextKey(resolvedShared, shareScopeName) {
542
634
  return JSON.stringify([shareScopeName, getSortedRecordEntries(resolvedShared)]);
543
635
  }
@@ -574,15 +666,57 @@ function neutralizeBrowserPreloadHelpers(code) {
574
666
  code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
575
667
  return code;
576
668
  }
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;
669
+ const SSR_MODULE_SPECIFIER_RE = /(?:from|export\s*\*\s*from|import\s*\(?\s*)\s*(["'`])([^"'`\s]+)\1/g;
670
+ function isRelativeSpecifier(specifier) {
671
+ return specifier.startsWith("./") || specifier.startsWith("../");
672
+ }
673
+ /**
674
+ * Module specifiers used by the import/export syntax of `code`. Import-like
675
+ * text inside comments and string literals (JSDoc such as
676
+ * `@type {import('./x')}`, docs, error messages) is skipped, and a comment
677
+ * between `import(` and the specifier (webpack-style magic comments) does not
678
+ * hide a real dynamic import. Discovery and rewriting both read this list, so
679
+ * they always agree on which specifiers exist.
680
+ */
681
+ function findSsrModuleSpecifiers(code) {
682
+ let blanked = "";
683
+ let blankedUntil = 0;
684
+ const isCode = createCodePositionMap(code, (start, end) => {
685
+ blanked += code.slice(blankedUntil, start) + " ".repeat(end - start);
686
+ blankedUntil = end;
584
687
  });
585
- return neutralizeBrowserPreloadHelpers(code);
688
+ blanked += code.slice(blankedUntil);
689
+ const specifiers = [];
690
+ for (const match of blanked.matchAll(SSR_MODULE_SPECIFIER_RE)) {
691
+ if (!isCode[match.index]) continue;
692
+ const end = match.index + match[0].length;
693
+ const value = match[2];
694
+ specifiers.push({
695
+ start: end - value.length - 2,
696
+ end,
697
+ value
698
+ });
699
+ }
700
+ return specifiers;
701
+ }
702
+ function isNodeModuleSpecifier(specifier) {
703
+ return specifier === "node:module" || specifier === "module";
704
+ }
705
+ function transformSsrCode(code, base, specifiers, sharedPkgMap, requireShimUrl, tempFileUrlMap) {
706
+ const rewriter = new CodeRewriter(code);
707
+ for (const { start, end, value } of specifiers) {
708
+ if (requireShimUrl && isNodeModuleSpecifier(value)) {
709
+ rewriter.overwrite(start, end, `"${requireShimUrl}"`);
710
+ continue;
711
+ }
712
+ const resolvedShared = sharedPkgMap?.get(value);
713
+ const relativeUrl = isRelativeSpecifier(value) ? new URL(value, base).href : void 0;
714
+ const absoluteHttpUrl = value.startsWith("http://") || value.startsWith("https://") ? value : void 0;
715
+ const httpUrl = relativeUrl ?? absoluteHttpUrl;
716
+ const replacement = (httpUrl ? tempFileUrlMap?.get(httpUrl) : void 0) ?? relativeUrl ?? (resolvedShared && `file://${resolvedShared}`);
717
+ if (replacement) rewriter.overwrite(start, end, `"${replacement}"`);
718
+ }
719
+ return neutralizeBrowserPreloadHelpers(rewriter.toString());
586
720
  }
587
721
  function isVitePreloadHelperSpecifier(specifier) {
588
722
  return specifier.includes("preload-helper");
@@ -591,21 +725,22 @@ function getTempFileImportUrl(filePath, versionKey) {
591
725
  return `file://${filePath}?v=${encodeURIComponent(versionKey)}`;
592
726
  }
593
727
  /**
594
- * Fetch an HTTP ESM module, transform it, write it to a temp .js file and
728
+ * Fetch an HTTP ESM module, transform it, write it to a temp file and
595
729
  * return the file path. Recursively does the same for HTTP transitive imports
596
- * so that `import('file:///...temp.js')` can resolve them.
730
+ * so that `import('file:///...temp.mjs')` can resolve them.
597
731
  *
598
732
  * `versionKey` participates in both the cache key and the temp file name, so
599
733
  * a remote redeploy (new manifest → new key) produces new files and bypasses
600
734
  * Node's ESM module cache instead of serving the stale build.
601
735
  */
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) {
736
+ 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") {
603
737
  const cacheKey = JSON.stringify([
604
738
  fetchTimeoutMs,
605
739
  fetchMaxBytes,
606
740
  versionKey,
607
741
  url,
608
- contextKey
742
+ contextKey,
743
+ extension
609
744
  ]);
610
745
  if (visited.has(url)) return visited.get(url);
611
746
  const cached = tempFileCache.get(cacheKey);
@@ -619,7 +754,7 @@ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, v
619
754
  const tmpFilePromise = (async () => {
620
755
  const { createHash } = await _crypto();
621
756
  const { join } = await _path();
622
- return join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}.js`);
757
+ return join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}${extension}`);
623
758
  })();
624
759
  tempFilePathCache.set(cacheKey, tmpFilePromise);
625
760
  const promise = (async () => {
@@ -629,32 +764,38 @@ async function fetchEsmToTempFile(url, tmpDir, visited, pending, sharedPkgMap, v
629
764
  let code = await readResponseTextBounded(res, fetchMaxBytes, url);
630
765
  if (!res.ok) throw new SsrEntryHttpError(url, res.status, res.statusText, getBodyPreview(code));
631
766
  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);
767
+ const specifiers = findSsrModuleSpecifiers(code);
768
+ const relImports = specifiers.map(({ value }) => value).filter((value) => isRelativeSpecifier(value) && !isVitePreloadHelperSpecifier(value)).map((value) => new URL(value, base).href);
636
769
  const subMap = /* @__PURE__ */ new Map();
637
770
  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);
771
+ const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes, extension);
639
772
  subMap.set(u, getTempFileImportUrl(tmpPath, versionKey));
640
773
  }));
641
- code = transformSsrCode(code, base, sharedPkgMap);
642
- for (const [httpUrl, fileUrl] of subMap) code = code.split(httpUrl).join(fileUrl);
774
+ const requireShimUrl = sharedPkgMap?.size && specifiers.some(({ value }) => isNodeModuleSpecifier(value)) ? await getRequireShimUrl(tmpDir, sharedPkgMap, contextKey) : void 0;
775
+ code = transformSsrCode(code, base, specifiers, sharedPkgMap, requireShimUrl, subMap);
643
776
  const { writeFileSync } = await _fs();
644
777
  writeFileSync(tmpFile, code, "utf8");
645
778
  return tmpFile;
646
779
  })();
780
+ const record = {
781
+ cacheKey,
782
+ filePathPromise: tmpFilePromise,
783
+ promise,
784
+ remoteOrigin: getUrlOrigin(url)
785
+ };
786
+ tempFileRecords.set(cacheKey, record);
647
787
  tempFileCache.set(cacheKey, promise);
648
788
  pending.add(promise);
649
789
  promise.catch(() => {
650
790
  if (tempFileCache.get(cacheKey) === promise) tempFileCache.delete(cacheKey);
651
791
  if (tempFilePathCache.get(cacheKey) === tmpFilePromise) tempFilePathCache.delete(cacheKey);
792
+ if (tempFileRecords.get(cacheKey) === record && !scheduledTempFileCleanup.has(cacheKey)) tempFileRecords.delete(cacheKey);
652
793
  });
653
794
  return promise;
654
795
  }
655
- async function fetchEsmGraphToTempFile(url, tmpDir, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES) {
796
+ async function fetchEsmGraphToTempFile(url, tmpDir, sharedPkgMap, versionKey = UNVERSIONED, fetchTimeoutMs = DEFAULT_SSR_FETCH_TIMEOUT_MS, contextKey = "default", fetchMaxBytes = DEFAULT_SSR_FETCH_MAX_BYTES, extension = ".mjs") {
656
797
  const pending = /* @__PURE__ */ new Set();
657
- const rootFile = await fetchEsmToTempFile(url, tmpDir, /* @__PURE__ */ new Map(), pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes);
798
+ const rootFile = await fetchEsmToTempFile(url, tmpDir, /* @__PURE__ */ new Map(), pending, sharedPkgMap, versionKey, fetchTimeoutMs, contextKey, fetchMaxBytes, extension);
658
799
  await Promise.all(pending);
659
800
  return rootFile;
660
801
  }
@@ -665,12 +806,25 @@ async function importTempModule(filePath, versionKey) {
665
806
  );
666
807
  }
667
808
  let warnedVmUnavailable = false;
809
+ let vmStrategyModulePromise;
810
+ function getVmStrategyModule() {
811
+ if (!vmStrategyModulePromise) vmStrategyModulePromise = import("./ssrVmStrategy-B_e3yqa2.js");
812
+ return vmStrategyModulePromise;
813
+ }
814
+ /**
815
+ * Non-HTTP failures keep falling back (to temp-file after vm, to the federation
816
+ * runtime's own Node loader after temp-file), but whatever the fallback throws
817
+ * then hides the real cause, so report it first.
818
+ */
819
+ function warnStrategyFallback(strategy, url, fallback, error) {
820
+ mfWarn(`SSR entry loader: strategy "${strategy}" failed to load ${url}; falling back to ${fallback}.`, error);
821
+ }
668
822
  async function tryVmStrategy(ssrEntry, options) {
669
- const { loadViaVmStrategy, isVmStrategyAvailable } = await import("./ssrVmStrategy-CDbcwMnf.js");
823
+ const { loadViaVmStrategy, isVmStrategyAvailable } = await getVmStrategyModule();
670
824
  if (!await isVmStrategyAvailable()) {
671
825
  if (!warnedVmUnavailable) {
672
826
  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.");
827
+ mfWarn("SSR entry loader: strategy \"vm\" requires vm.SourceTextModule (run Node with --experimental-vm-modules); falling back to the temp-file strategy.");
674
828
  }
675
829
  return null;
676
830
  }
@@ -715,15 +869,18 @@ async function loadSSRRemoteEntry(ssrEntry, options) {
715
869
  if (fromVm) return fromVm;
716
870
  } catch (error) {
717
871
  if (isSsrEntryHttpError(error) || isSsrFetchBodyTooLargeError(error)) throw error;
872
+ warnStrategyFallback("vm", url, "the temp-file strategy", error);
718
873
  }
719
874
  const { mkdirSync } = await _fs();
720
875
  const cacheDir = await getSSRCacheDir();
721
876
  mkdirSync(cacheDir, { recursive: true });
722
877
  const sharedPkgMap = new Map(Object.entries(resolvedShared));
878
+ const extension = type === "commonjs-module" || type === "commonjs" ? ".js" : ".mjs";
723
879
  try {
724
- return await importTempModule(await fetchEsmGraphToTempFile(url, cacheDir, sharedPkgMap, versionKey, options.fetchTimeoutMs, getSsrTransformContextKey(resolvedShared, options.shareScopeName), options.fetchMaxBytes), versionKey);
880
+ return await importTempModule(await fetchEsmGraphToTempFile(url, cacheDir, sharedPkgMap, versionKey, options.fetchTimeoutMs, getSsrTransformContextKey(resolvedShared, options.shareScopeName), options.fetchMaxBytes, extension), versionKey);
725
881
  } catch (error) {
726
882
  if (isSsrEntryHttpError(error) || isSsrFetchBodyTooLargeError(error)) throw error;
883
+ warnStrategyFallback("temp-file", url, "the federation runtime's loader", error);
727
884
  return null;
728
885
  }
729
886
  }
@@ -764,4 +921,4 @@ function ssrEntryLoaderPlugin(options = {}) {
764
921
  };
765
922
  }
766
923
  //#endregion
767
- export { DEFAULT_SSR_FETCH_MAX_BYTES as a, readResponseTextBounded as c, ssrEntryLoaderPlugin as i, neutralizeBrowserPreloadHelpers as n, DEFAULT_SSR_FETCH_TIMEOUT_MS as o, revalidate as r, fetchWithTimeout as s, SsrEntryHttpError as t };
924
+ export { getUrlOrigin as a, fetchWithTimeout as c, ssrEntryLoaderPlugin as i, readResponseTextBounded as l, neutralizeBrowserPreloadHelpers as n, DEFAULT_SSR_FETCH_MAX_BYTES as o, revalidate as r, DEFAULT_SSR_FETCH_TIMEOUT_MS as s, SsrEntryHttpError as t };
@@ -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 { a as getUrlOrigin, c as fetchWithTimeout, l as readResponseTextBounded, n as neutralizeBrowserPreloadHelpers, t as SsrEntryHttpError } from "./ssrEntryLoader-DmqLXUmm.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,37 @@ 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, remoteOrigin) {
148
+ for (const [cacheKey, owners] of ownersByCacheKey) {
149
+ if (!remoteOrigin) {
150
+ cache.delete(cacheKey);
151
+ ownersByCacheKey.delete(cacheKey);
152
+ continue;
153
+ }
154
+ for (const owner of owners) if (getUrlOrigin(owner) === remoteOrigin) owners.delete(owner);
155
+ if (owners.size === 0) {
156
+ cache.delete(cacheKey);
157
+ ownersByCacheKey.delete(cacheKey);
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * Drop cached VM module graphs after a remote revalidation. A module graph can
163
+ * contain URLs from another origin, so ownership is tracked by the root entry
164
+ * rather than inferred from each module URL.
165
+ */
166
+ function clearVmStrategyCaches(remoteEntryUrl) {
167
+ const remoteOrigin = remoteEntryUrl === void 0 ? void 0 : getUrlOrigin(remoteEntryUrl);
168
+ if (remoteEntryUrl !== void 0 && !remoteOrigin) return;
169
+ clearOwnedVmCache(httpModuleCache, httpModuleCacheOwners, remoteOrigin);
170
+ clearOwnedVmCache(namespaceCache, namespaceCacheOwners, remoteOrigin);
171
+ }
111
172
  function getBodyPreview(body) {
112
173
  return body.slice(0, 240).replace(/\s+/g, " ").trim();
113
174
  }
@@ -134,6 +195,7 @@ function getHttpModule(vm, url, options) {
134
195
  options.versionKey,
135
196
  url
136
197
  ]);
198
+ addVmCacheOwner(httpModuleCacheOwners, cacheKey, options.rootEntryUrl);
137
199
  if (!httpModuleCache.has(cacheKey)) httpModuleCache.set(cacheKey, (async () => {
138
200
  const code = await fetchModuleSource(url, options.fetchTimeoutMs, options.fetchMaxBytes);
139
201
  return new vm.SourceTextModule(code, {
@@ -145,6 +207,7 @@ function getHttpModule(vm, url, options) {
145
207
  });
146
208
  })().catch((error) => {
147
209
  httpModuleCache.delete(cacheKey);
210
+ httpModuleCacheOwners.delete(cacheKey);
148
211
  throw error;
149
212
  }));
150
213
  return httpModuleCache.get(cacheKey);
@@ -180,18 +243,24 @@ async function importDynamically(vm, specifier, referencingModule, options) {
180
243
  async function loadViaVmStrategy(entryUrl, options) {
181
244
  const vm = await getVmApi();
182
245
  if (!vm) return null;
246
+ const graphOptions = {
247
+ ...options,
248
+ rootEntryUrl: entryUrl
249
+ };
183
250
  const cacheKey = `${getVmCacheContextKey(options)}::${options.versionKey}::${entryUrl}`;
251
+ addVmCacheOwner(namespaceCacheOwners, cacheKey, entryUrl);
184
252
  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);
253
+ const entryModule = await getHttpModule(vm, entryUrl, graphOptions);
254
+ const linker = (specifier, referencingModule) => linkModule(vm, specifier, referencingModule, graphOptions);
187
255
  await linkModuleGraph(entryModule, linker, options.cacheContext);
188
256
  if (entryModule.status === "linked") await entryModule.evaluate();
189
257
  return entryModule.namespace;
190
258
  })().catch((error) => {
191
259
  namespaceCache.delete(cacheKey);
260
+ namespaceCacheOwners.delete(cacheKey);
192
261
  throw error;
193
262
  }));
194
263
  return namespaceCache.get(cacheKey);
195
264
  }
196
265
  //#endregion
197
- export { isVmStrategyAvailable, loadViaVmStrategy };
266
+ 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-DmqLXUmm.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.1",
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",
@@ -1,66 +0,0 @@
1
- //#region src/utils/buildPaths.ts
2
- /**
3
- * Rebase an import path for a bootstrap file that moved from root into `dir`.
4
- *
5
- * When entryFileNames places entries in a subdirectory (e.g. `static/js/`),
6
- * the bootstrap file moves there too. Paths that resolved from the HTML root
7
- * must resolve from the new directory instead.
8
- *
9
- * Cases: `/static/js/hostInit.js` → `./hostInit.js` (strip dir prefix)
10
- * `./src/main.tsx` → `../../src/main.tsx` (climb back up for each dir level)
11
- * `https://cdn.example.com` → unchanged (absolute URL)
12
- */
13
- function rebaseImport(importSrc, dir) {
14
- if (!dir) return importSrc;
15
- if (isAbsoluteUrl(importSrc)) return importSrc;
16
- const normalizedDir = dir.replace(/^\/+|\/+$/g, "");
17
- if (!normalizedDir) return importSrc;
18
- const stripDirPrefix = (src, prefix) => {
19
- if (src === prefix) return "";
20
- if (src.startsWith(prefix + "/")) return src.slice(prefix.length);
21
- };
22
- const absoluteRemainder = stripDirPrefix(importSrc, "/" + normalizedDir);
23
- if (absoluteRemainder !== void 0) {
24
- const remainder = absoluteRemainder.replace(/^\/+/, "");
25
- return remainder ? "./" + remainder : "./";
26
- }
27
- const relativeRemainder = stripDirPrefix(importSrc, normalizedDir);
28
- if (relativeRemainder !== void 0) {
29
- const remainder = relativeRemainder.replace(/^\/+/, "");
30
- return remainder ? "./" + remainder : "./";
31
- }
32
- const upLevels = normalizedDir.split("/").filter(Boolean).length;
33
- const prefix = upLevels > 0 ? "../".repeat(upLevels) : "./";
34
- if (importSrc.startsWith("./")) return prefix + importSrc.slice(2);
35
- if (importSrc.startsWith("/")) return prefix + importSrc.slice(1);
36
- return prefix + importSrc;
37
- }
38
- function normalizePathForImport(path) {
39
- return path.replace(/\\/g, "/");
40
- }
41
- const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
42
- function isAbsoluteUrl(src) {
43
- if (/^[a-z]:[\\/]/i.test(src)) return false;
44
- return EXTERNAL_URL_RE.test(src);
45
- }
46
- const HASH_PLACEHOLDER_RE = /(?:[._-]?\[hash(?::\d+)?\])/g;
47
- function hasFileExtension(fileName) {
48
- return fileName.slice(Math.max(fileName.lastIndexOf("/"), fileName.lastIndexOf("\\")) + 1).lastIndexOf(".") > 0;
49
- }
50
- /**
51
- * Resolve a bundler `filename` template that still contains `[hash]` placeholders
52
- * into the concrete, stable file name Module Federation serves.
53
- *
54
- * The federation entries are emitted by us rather than hashed by the bundler, so
55
- * the placeholder is dropped instead of being substituted. When stripping it also
56
- * removes the extension (`mf-[hash:8]` → `mf`), `.js` is appended so the result
57
- * stays a loadable module. The extension check deliberately looks at the basename
58
- * only — a dotted directory (`assets/v1.2/entry`) must not be mistaken for one.
59
- */
60
- function resolveHashPlaceholderFileName(fileName) {
61
- if (!fileName.includes("[hash")) return fileName;
62
- const normalized = fileName.replace(HASH_PLACEHOLDER_RE, "");
63
- return hasFileExtension(normalized) ? normalized : `${normalized}.js`;
64
- }
65
- //#endregion
66
- export { resolveHashPlaceholderFileName as i, normalizePathForImport as n, rebaseImport as r, EXTERNAL_URL_RE as t };