@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.
- package/README.md +10 -0
- package/lib/index.js +405 -179
- package/lib/{pluginDts-CrSsDUnT.js → pluginDts-CTAHkt4C.js} +50 -4
- package/lib/ssrVmStrategy-DtpfkCw1.js +152 -0
- package/lib/utils/ssrEntryLoader.d.ts +52 -2
- package/lib/utils/ssrEntryLoader.js +156 -40
- package/package.json +2 -2
|
@@ -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
|
|
197
|
-
const
|
|
198
|
-
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
398
|
+
tempFileCache.set(cacheKey, promise);
|
|
308
399
|
return promise;
|
|
309
400
|
}
|
|
310
|
-
async function importTempModule(filePath) {
|
|
311
|
-
return await import(
|
|
312
|
-
|
|
313
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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,
|
|
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.
|
|
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
|
+
}
|