@module-federation/vite 1.16.11 → 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.
@@ -193,10 +193,56 @@ function getPackageNameFromNodeModulePath(source) {
193
193
  if (parts[0].startsWith("@")) return parts[1] ? `${parts[0]}/${parts[1]}` : void 0;
194
194
  return parts[0];
195
195
  }
196
- function getSharedCacheKey(pkg, shareItem) {
197
- const prefix = `${shareItem.scope || "default"}:`;
198
- return shareItem.shareConfig.singleton || !shareItem.version ? `${prefix}${pkg}` : `${prefix}${pkg}@${shareItem.version}`;
196
+ function getSharedCacheKeyParts(input) {
197
+ const scope = (Array.isArray(input.scope) ? input.scope[0] : input.scope) || "default";
198
+ const id = input.singleton || !input.version ? input.pkg : `${input.pkg}@${input.version}`;
199
+ return {
200
+ scope,
201
+ id,
202
+ key: `${scope}:${id}`
203
+ };
204
+ }
205
+ function getSharedCacheDescriptor(pkg, shareItem) {
206
+ const parts = getSharedCacheKeyParts({
207
+ pkg,
208
+ singleton: shareItem.shareConfig.singleton,
209
+ version: shareItem.version,
210
+ scope: shareItem.scope
211
+ });
212
+ return {
213
+ canonical: parts.key,
214
+ ...parts.scope === "default" ? { aliases: [parts.id] } : {}
215
+ };
199
216
  }
217
+ const sharedCacheHelperCode = `const __mfGetSharedCacheDescriptor = (pkg, singleton, version, scope) => {
218
+ const normalizedScope = Array.isArray(scope) ? scope[0] : scope;
219
+ const scopeName = normalizedScope || "default";
220
+ const id = singleton || !version ? pkg : pkg + "@" + version;
221
+ const descriptor = { canonical: scopeName + ":" + id };
222
+ if (scopeName === "default") descriptor.aliases = [id];
223
+ return descriptor;
224
+ };
225
+ const __mfReadSharedCache = (cache, descriptor) => {
226
+ const value = cache[descriptor.canonical];
227
+ if (value !== undefined) return value;
228
+ const aliases = descriptor.aliases || [];
229
+ for (const alias of aliases) {
230
+ const aliasValue = cache[alias];
231
+ if (aliasValue !== undefined) {
232
+ cache[descriptor.canonical] = aliasValue;
233
+ return aliasValue;
234
+ }
235
+ }
236
+ return undefined;
237
+ };
238
+ const __mfWriteSharedCache = (cache, descriptor, value) => {
239
+ cache[descriptor.canonical] = value;
240
+ const aliases = descriptor.aliases || [];
241
+ for (const alias of aliases) {
242
+ if (cache[alias] === undefined) cache[alias] = value;
243
+ }
244
+ return value;
245
+ };`;
200
246
  function getInstalledPackageJson(pkg, opts) {
201
247
  const cwd = opts?.cwd || getPackageDetectionCwd();
202
248
  const packageName = opts?.packageName || getPackageName(pkg);
@@ -630,4 +676,4 @@ function pluginDts(options) {
630
676
  }];
631
677
  }
632
678
  //#endregion
633
- export { mfWarn as _, getIsRolldown as a, getPackageNameFromNodeModulePath as c, isNuxtProjectRoot as d, packageNameDecode as f, createModuleFederationError as g, setPackageDetectionCwd as h, getInstalledPackageJson as i, getSharedCacheKey as l, resolveImportPath as m, pluginDts_exports as n, getPackageDetectionCwd as o, packageNameEncode as p, getInstalledPackageEntry as r, getPackageName as s, DEFAULT_PUBLIC_TYPES_FOLDER as t, hasPackageDependency as u, normalizePathForImport as v, rebaseImport as y };
679
+ export { createModuleFederationError as _, getIsRolldown as a, rebaseImport as b, getPackageNameFromNodeModulePath as c, isNuxtProjectRoot as d, packageNameDecode as f, sharedCacheHelperCode as g, setPackageDetectionCwd as h, getInstalledPackageJson as i, getSharedCacheDescriptor as l, resolveImportPath as m, pluginDts_exports as n, getPackageDetectionCwd as o, packageNameEncode as p, getInstalledPackageEntry as r, getPackageName as s, DEFAULT_PUBLIC_TYPES_FOLDER as t, hasPackageDependency as u, mfWarn as v, normalizePathForImport as y };
@@ -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 };
@@ -8,7 +8,7 @@
8
8
  * `loadScriptNode` — if the hook returns a value, the runtime uses it directly.
9
9
  *
10
10
  * Strategy:
11
- * - In Node (typeof window === 'undefined'), fetch the remote's mf-manifest.json
11
+ * - In Node (detected through process.versions.node), fetch the remote's mf-manifest.json
12
12
  * to discover the ssrRemoteEntry URL and its type.
13
13
  * - ESM entry: use a dynamic `import()` — the SSR entry has no browser
14
14
  * globals and all shared packages are external.
@@ -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 };
@@ -8,7 +8,7 @@
8
8
  * `loadScriptNode` — if the hook returns a value, the runtime uses it directly.
9
9
  *
10
10
  * Strategy:
11
- * - In Node (typeof window === 'undefined'), fetch the remote's mf-manifest.json
11
+ * - In Node (detected through process.versions.node), fetch the remote's mf-manifest.json
12
12
  * to discover the ssrRemoteEntry URL and its type.
13
13
  * - ESM entry: use a dynamic `import()` — the SSR entry has no browser
14
14
  * globals and all shared packages are external.
@@ -34,6 +34,7 @@ async function nodeImport(id) {
34
34
  ));
35
35
  return importCache.get(id);
36
36
  }
37
+ const isNodeServer = () => typeof globalThis.process?.versions?.node === "string";
37
38
  const runnerCache = /* @__PURE__ */ new Map();
38
39
  /**
39
40
  * Import `vite/module-runner` dynamically. Returns null on Vite < 8 where the
@@ -69,10 +70,7 @@ async function getOrCreateRunner(remoteOrigin) {
69
70
  return await (await fetch(runnerEndpoint, {
70
71
  method: "POST",
71
72
  headers: { "Content-Type": "application/json" },
72
- body: JSON.stringify({
73
- name: payload.data.name,
74
- data: payload.data.data
75
- })
73
+ body: JSON.stringify(payload)
76
74
  })).json();
77
75
  } }
78
76
  }, new ESModulesEvaluator());
@@ -87,6 +85,27 @@ const _path = () => nodeImport("path");
87
85
  const _fs = () => nodeImport("fs");
88
86
  const _crypto = () => nodeImport("crypto");
89
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
+ }
90
109
  const ssrEntryCache = /* @__PURE__ */ new Map();
91
110
  const manifestFetchCache = /* @__PURE__ */ new Map();
92
111
  var SsrEntryHttpError = class extends Error {
@@ -148,7 +167,8 @@ function resolveSSREntryUrl(manifest, manifestUrl) {
148
167
  const entryPath = (meta.ssrRemoteEntry.path || "") + meta.ssrRemoteEntry.name;
149
168
  return {
150
169
  url: new URL(entryPath, base).href,
151
- type: meta.ssrRemoteEntry.type || "module"
170
+ type: meta.ssrRemoteEntry.type || "module",
171
+ versionKey: computeManifestVersionKey(manifest)
152
172
  };
153
173
  }
154
174
  /**
@@ -190,14 +210,17 @@ function buildSsrEntryCandidates(ctx, options = {}) {
190
210
  const candidates = [];
191
211
  if (!options.skipServerBuild) candidates.push({
192
212
  url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
193
- type: "module"
213
+ type: "module",
214
+ versionKey: UNVERSIONED
194
215
  });
195
216
  candidates.push({
196
217
  url: `${base}.ssr.js`,
197
- type: "module"
218
+ type: "module",
219
+ versionKey: UNVERSIONED
198
220
  }, {
199
221
  url: `${remoteOrigin}/__mf_ssr__/${filename}.ssr.js`,
200
- type: "module"
222
+ type: "module",
223
+ versionKey: UNVERSIONED
201
224
  });
202
225
  return candidates;
203
226
  }
@@ -211,13 +234,15 @@ async function resolveFirstReachableCandidate(candidates) {
211
234
  async function resolveSSREntryImpl(remoteEntryUrl) {
212
235
  if (isSsrEntry(remoteEntryUrl)) return {
213
236
  url: remoteEntryUrl,
214
- type: "module"
237
+ type: "module",
238
+ versionKey: UNVERSIONED
215
239
  };
216
240
  if (!isManifestEntry(remoteEntryUrl)) {
217
241
  const filename = getEntryFilename(remoteEntryUrl);
218
242
  const fromServerBuild = await headCheckSsrEntry({
219
243
  url: `${remoteEntryUrl.replace(/\/[^/]+$/, "")}/__mf_server__/${filename}.ssr.js`,
220
- type: "module"
244
+ type: "module",
245
+ versionKey: UNVERSIONED
221
246
  });
222
247
  if (fromServerBuild) return fromServerBuild;
223
248
  }
@@ -228,9 +253,63 @@ async function resolveSSREntryImpl(remoteEntryUrl) {
228
253
  }
229
254
  return resolveFirstReachableCandidate(buildSsrEntryCandidates(ctx, { skipServerBuild: !isManifestEntry(remoteEntryUrl) }));
230
255
  }
231
- async function getSSREntry(remoteEntryUrl) {
232
- if (!ssrEntryCache.has(remoteEntryUrl)) ssrEntryCache.set(remoteEntryUrl, resolveSSREntryImpl(remoteEntryUrl));
233
- 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 {}
234
313
  }
235
314
  const tempFileCache = /* @__PURE__ */ new Map();
236
315
  let ssrCacheDirPromise;
@@ -251,14 +330,11 @@ async function getSSRCacheDir() {
251
330
  })();
252
331
  return ssrCacheDirPromise;
253
332
  }
254
- function transformSsrCode(code, base, sharedPkgMap) {
255
- code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
256
- code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
257
- code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
258
- if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
259
- const resolved = sharedPkgMap.get(specifier);
260
- return resolved ? m.replace(specifier, `file://${resolved}`) : m;
261
- });
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) {
262
338
  code = code.replace(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*preload-helper[^"']*["'];?/g, (_m, bindings) => {
263
339
  return bindings.split(",").map((b) => {
264
340
  const parts = b.trim().split(/\s+as\s+/);
@@ -269,6 +345,16 @@ function transformSsrCode(code, base, sharedPkgMap) {
269
345
  code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
270
346
  return code;
271
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
+ }
272
358
  function isVitePreloadHelperSpecifier(specifier) {
273
359
  return specifier.includes("preload-helper");
274
360
  }
@@ -276,10 +362,15 @@ function isVitePreloadHelperSpecifier(specifier) {
276
362
  * Fetch an HTTP ESM module, transform it, write it to a temp .js file and
277
363
  * return the file path. Recursively does the same for HTTP transitive imports
278
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.
279
369
  */
280
- async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
370
+ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap, versionKey = UNVERSIONED) {
371
+ const cacheKey = `${versionKey}::${url}`;
281
372
  if (visited.has(url)) return visited.get(url);
282
- if (tempFileCache.has(url)) return tempFileCache.get(url);
373
+ if (tempFileCache.has(cacheKey)) return tempFileCache.get(cacheKey);
283
374
  const promise = (async () => {
284
375
  const res = await fetch(url);
285
376
  let code = await res.text();
@@ -291,7 +382,7 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
291
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);
292
383
  const subMap = /* @__PURE__ */ new Map();
293
384
  await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
294
- const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap);
385
+ const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap, versionKey);
295
386
  subMap.set(u, `file://${tmpPath}`);
296
387
  }));
297
388
  code = transformSsrCode(code, base, sharedPkgMap);
@@ -299,22 +390,36 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
299
390
  const { createHash } = await _crypto();
300
391
  const { join } = await _path();
301
392
  const { writeFileSync } = await _fs();
302
- 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`);
303
394
  writeFileSync(tmpFile, code, "utf8");
304
395
  visited.set(url, tmpFile);
305
396
  return tmpFile;
306
397
  })();
307
- tempFileCache.set(url, promise);
398
+ tempFileCache.set(cacheKey, promise);
308
399
  return promise;
309
400
  }
310
- async function importTempModule(filePath) {
311
- return await import(
312
- /* @vite-ignore */
313
- filePath
314
- );
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
+ });
315
419
  }
316
- async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
317
- const { url, type } = ssrEntry;
420
+ async function loadSSRRemoteEntry(ssrEntry, options) {
421
+ const { url, type, versionKey } = ssrEntry;
422
+ const { resolvedShared } = options;
318
423
  if (type === "commonjs-module" || type === "commonjs") {
319
424
  const { createRequire } = await _module();
320
425
  const req = createRequire(import.meta.url);
@@ -337,12 +442,18 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
337
442
  if (process.env.NODE_ENV !== "production") return null;
338
443
  }
339
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
+ }
340
451
  const { mkdirSync } = await _fs();
341
452
  const cacheDir = await getSSRCacheDir();
342
453
  mkdirSync(cacheDir, { recursive: true });
343
454
  const sharedPkgMap = new Map(Object.entries(resolvedShared));
344
455
  try {
345
- 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);
346
457
  } catch (error) {
347
458
  if (isSsrEntryHttpError(error)) throw error;
348
459
  return null;
@@ -358,18 +469,23 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
358
469
  }
359
470
  }
360
471
  function ssrEntryLoaderPlugin(options = {}) {
361
- 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
+ };
362
478
  return {
363
479
  name: "mf-vite:ssr-entry-loader",
364
480
  async loadEntry({ remoteInfo }) {
365
- if (typeof globalThis.window !== "undefined") return;
366
- const ssrEntry = await getSSREntry(remoteInfo.entry);
481
+ if (!isNodeServer()) return;
482
+ const ssrEntry = await getSSREntry(remoteInfo.entry, resolved.maxAgeMs);
367
483
  if (!ssrEntry) return;
368
- const mod = await loadSSRRemoteEntry(ssrEntry, resolvedShared);
484
+ const mod = await loadSSRRemoteEntry(ssrEntry, resolved);
369
485
  if (!mod) return;
370
486
  return mod;
371
487
  }
372
488
  };
373
489
  }
374
490
  //#endregion
375
- 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.11",
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
+ }