@module-federation/vite 1.15.5 → 1.16.0
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 +27 -0
- package/lib/index.cjs +1120 -870
- package/lib/index.d.cts +22 -6
- package/lib/index.d.mts +22 -6
- package/lib/index.mjs +1060 -793
- package/lib/packageUtils-CbbnJvKu.cjs +358 -0
- package/lib/packageUtils-DOekOsFz.mjs +250 -0
- package/lib/pluginDts-B5pcUmam.mjs +309 -0
- package/lib/pluginDts-BEOKyKQ-.cjs +311 -0
- package/lib/utils/ssrEntryLoader.cjs +301 -0
- package/lib/utils/ssrEntryLoader.d.cts +65 -0
- package/lib/utils/ssrEntryLoader.d.mts +66 -0
- package/lib/utils/ssrEntryLoader.mjs +301 -0
- package/package.json +13 -5
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
//#region src/utils/ssrEntryLoader.ts
|
|
2
|
+
/**
|
|
3
|
+
* MF runtime plugin that intercepts the `loadEntry` lifecycle hook on the
|
|
4
|
+
* server and loads the SSR-compatible remote entry instead of the browser one.
|
|
5
|
+
*
|
|
6
|
+
* This completely replaces the need for any `@module-federation/sdk` patches.
|
|
7
|
+
* The `loadEntry` hook is emitted by `runtime-core` before it falls through to
|
|
8
|
+
* `loadScriptNode` — if the hook returns a value, the runtime uses it directly.
|
|
9
|
+
*
|
|
10
|
+
* Strategy:
|
|
11
|
+
* - In Node (typeof window === 'undefined'), fetch the remote's mf-manifest.json
|
|
12
|
+
* to discover the ssrRemoteEntry URL and its type.
|
|
13
|
+
* - ESM entry: use a dynamic `import()` — the SSR entry has no browser
|
|
14
|
+
* globals and all shared packages are external.
|
|
15
|
+
* - Dev mode (Vite 8+ only): use `ModuleRunner` with an HTTP transport backed
|
|
16
|
+
* by the remote's `/__mf_runner__` endpoint. This fetches fully-transformed
|
|
17
|
+
* module source through Vite's plugin pipeline, avoiding serialisation which
|
|
18
|
+
* cannot faithfully represent React components or closures.
|
|
19
|
+
*
|
|
20
|
+
* Dev mode on Vite < 8 is NOT supported — `ModuleRunner` and
|
|
21
|
+
* `FetchableDevEnvironment` are Vite 8+ APIs. If you need dev-mode SSR on
|
|
22
|
+
* an older Vite version, implement an alternative loader in `loadSSRRemoteEntry`
|
|
23
|
+
* for the `isDevSsrEntry` branch and expose a corresponding server endpoint
|
|
24
|
+
* from `pluginSSRRemoteEntry.configureServer`.
|
|
25
|
+
*
|
|
26
|
+
* Exported as a plain factory function so it can be serialised into the
|
|
27
|
+
* generated runtimePlugins list in virtualRemotes.ts.
|
|
28
|
+
*/
|
|
29
|
+
const importCache = /* @__PURE__ */ new Map();
|
|
30
|
+
async function nodeImport(id) {
|
|
31
|
+
if (!importCache.has(id)) importCache.set(id, import(
|
|
32
|
+
/* @vite-ignore */
|
|
33
|
+
id
|
|
34
|
+
));
|
|
35
|
+
return importCache.get(id);
|
|
36
|
+
}
|
|
37
|
+
const runnerCache = /* @__PURE__ */ new Map();
|
|
38
|
+
/**
|
|
39
|
+
* Import `vite/module-runner` dynamically. Returns null on Vite < 8 where the
|
|
40
|
+
* subpath doesn't exist. Uses a plain dynamic import (not nodeImport) so that
|
|
41
|
+
* Vitest can intercept it with vi.mock in tests.
|
|
42
|
+
*/
|
|
43
|
+
async function getModuleRunnerModule() {
|
|
44
|
+
try {
|
|
45
|
+
return await import("vite/module-runner");
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a ModuleRunner that fetches modules from a remote Vite dev server's
|
|
52
|
+
* `/__mf_runner__` endpoint. Each HTTP POST carries a `fetchModule` invoke
|
|
53
|
+
* payload; the remote responds with the transformed module source as JSON.
|
|
54
|
+
*
|
|
55
|
+
* This is Vite 8+ only — older versions don't expose `vite/module-runner` or
|
|
56
|
+
* the `/__mf_runner__` proxy endpoint.
|
|
57
|
+
*/
|
|
58
|
+
async function getOrCreateRunner(remoteOrigin) {
|
|
59
|
+
if (runnerCache.has(remoteOrigin)) return runnerCache.get(remoteOrigin);
|
|
60
|
+
const promise = (async () => {
|
|
61
|
+
const viteRunner = await getModuleRunnerModule();
|
|
62
|
+
if (!viteRunner) return null;
|
|
63
|
+
const { ModuleRunner, ESModulesEvaluator } = viteRunner;
|
|
64
|
+
const runnerEndpoint = `${remoteOrigin}/__mf_runner__`;
|
|
65
|
+
try {
|
|
66
|
+
return new ModuleRunner({
|
|
67
|
+
hmr: false,
|
|
68
|
+
transport: { async invoke(payload) {
|
|
69
|
+
return await (await fetch(runnerEndpoint, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
name: payload.data.name,
|
|
74
|
+
data: payload.data.data
|
|
75
|
+
})
|
|
76
|
+
})).json();
|
|
77
|
+
} }
|
|
78
|
+
}, new ESModulesEvaluator());
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
runnerCache.set(remoteOrigin, promise);
|
|
84
|
+
return promise;
|
|
85
|
+
}
|
|
86
|
+
const _path = () => nodeImport("path");
|
|
87
|
+
const _fs = () => nodeImport("fs");
|
|
88
|
+
const _crypto = () => nodeImport("crypto");
|
|
89
|
+
const _module = () => nodeImport("module");
|
|
90
|
+
const manifestCache = /* @__PURE__ */ new Map();
|
|
91
|
+
async function fetchManifest(manifestUrl) {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(manifestUrl);
|
|
94
|
+
if (!res.ok) return null;
|
|
95
|
+
return await res.json();
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getManifestUrl(remoteEntryUrl) {
|
|
101
|
+
return remoteEntryUrl.replace(/\/[^/]+$/, "/mf-manifest.json");
|
|
102
|
+
}
|
|
103
|
+
function resolveSSREntryUrl(manifest, manifestUrl) {
|
|
104
|
+
const meta = manifest?.metaData;
|
|
105
|
+
if (!meta?.ssrRemoteEntry?.name) return null;
|
|
106
|
+
const base = manifestUrl.replace(/\/[^/]+$/, "/");
|
|
107
|
+
const entryPath = (meta.ssrRemoteEntry.path || "") + meta.ssrRemoteEntry.name;
|
|
108
|
+
return {
|
|
109
|
+
url: new URL(entryPath, base).href,
|
|
110
|
+
type: meta.ssrRemoteEntry.type || "module"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Derive the SSR entry URL by convention when no manifest is available.
|
|
115
|
+
* remoteEntry.js → remoteEntry.ssr.js
|
|
116
|
+
* remoteEntry.js → /__mf_ssr__/remoteEntry.ssr.js (dev middleware)
|
|
117
|
+
* Returns the first URL that responds with a 200.
|
|
118
|
+
*/
|
|
119
|
+
async function headCheckSsrEntry(candidate) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(candidate.url, { method: "HEAD" });
|
|
122
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
123
|
+
if (res.ok && !ct.includes("text/html")) return candidate;
|
|
124
|
+
} catch {}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
async function getSSREntryByConvention(remoteEntryUrl, options = {}) {
|
|
128
|
+
const base = remoteEntryUrl.replace(/\.[^.]+$/, "");
|
|
129
|
+
const remoteOrigin = remoteEntryUrl.replace(/\/[^/]+$/, "");
|
|
130
|
+
const filename = remoteEntryUrl.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "remoteEntry";
|
|
131
|
+
const candidates = [
|
|
132
|
+
...options.skipServerBuild ? [] : [{
|
|
133
|
+
url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
|
|
134
|
+
type: "module"
|
|
135
|
+
}],
|
|
136
|
+
{
|
|
137
|
+
url: `${base}.ssr.js`,
|
|
138
|
+
type: "module"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
url: `${remoteOrigin}/__mf_ssr__/${filename}.ssr.js`,
|
|
142
|
+
type: "module"
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
for (const candidate of candidates) {
|
|
146
|
+
const hit = await headCheckSsrEntry(candidate);
|
|
147
|
+
if (hit) return hit;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
async function getSSREntry(remoteEntryUrl) {
|
|
152
|
+
const remoteOrigin = remoteEntryUrl.replace(/\/[^/]+$/, "");
|
|
153
|
+
const filename = remoteEntryUrl.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "remoteEntry";
|
|
154
|
+
const manifestUrl = getManifestUrl(remoteEntryUrl);
|
|
155
|
+
if (!manifestCache.has(manifestUrl)) manifestCache.set(manifestUrl, (async () => {
|
|
156
|
+
const fromServerBuild = await headCheckSsrEntry({
|
|
157
|
+
url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
|
|
158
|
+
type: "module"
|
|
159
|
+
});
|
|
160
|
+
if (fromServerBuild) return fromServerBuild;
|
|
161
|
+
const manifest = await fetchManifest(manifestUrl);
|
|
162
|
+
if (manifest) {
|
|
163
|
+
const fromManifest = resolveSSREntryUrl(manifest, manifestUrl);
|
|
164
|
+
if (fromManifest) return fromManifest;
|
|
165
|
+
}
|
|
166
|
+
return getSSREntryByConvention(remoteEntryUrl, { skipServerBuild: true });
|
|
167
|
+
})());
|
|
168
|
+
return manifestCache.get(manifestUrl);
|
|
169
|
+
}
|
|
170
|
+
const tempFileCache = /* @__PURE__ */ new Map();
|
|
171
|
+
let ssrCacheDirPromise;
|
|
172
|
+
async function getSSRCacheDir() {
|
|
173
|
+
if (!ssrCacheDirPromise) ssrCacheDirPromise = (async () => {
|
|
174
|
+
const { join } = await _path();
|
|
175
|
+
const { rmSync } = await _fs();
|
|
176
|
+
const dir = join(process.cwd(), "node_modules", ".ssr-cache");
|
|
177
|
+
process.once("exit", () => {
|
|
178
|
+
try {
|
|
179
|
+
rmSync(dir, {
|
|
180
|
+
recursive: true,
|
|
181
|
+
force: true
|
|
182
|
+
});
|
|
183
|
+
} catch {}
|
|
184
|
+
});
|
|
185
|
+
return dir;
|
|
186
|
+
})();
|
|
187
|
+
return ssrCacheDirPromise;
|
|
188
|
+
}
|
|
189
|
+
function transformSsrCode(code, base, sharedPkgMap) {
|
|
190
|
+
code = code.replace(/((?:from|export\s*\*\s*from)\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
|
|
191
|
+
code = code.replace(/(import\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`]/g, (_m, prefix, _q, specifier) => `${prefix}"${new URL(specifier, base).href}"`);
|
|
192
|
+
code = code.replace(/(import\s*\(\s*)(["'`])(\.\.?\/[^"'`\s][^"'`]*)["'`](\s*\))/g, (_m, prefix, _q, specifier, suffix) => `${prefix}"${new URL(specifier, base).href}"${suffix}`);
|
|
193
|
+
if (sharedPkgMap && sharedPkgMap.size > 0) code = code.replace(/(?:from|import\s*\()\s*(["'`])([^"'`./][^"'`]*)["'`]/g, (m, _q, specifier) => {
|
|
194
|
+
const resolved = sharedPkgMap.get(specifier);
|
|
195
|
+
return resolved ? m.replace(specifier, `file://${resolved}`) : m;
|
|
196
|
+
});
|
|
197
|
+
code = code.replace(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*preload-helper[^"']*["'];?/g, (_m, bindings) => {
|
|
198
|
+
return bindings.split(",").map((b) => {
|
|
199
|
+
const parts = b.trim().split(/\s+as\s+/);
|
|
200
|
+
return (parts[1] ?? parts[0]).trim();
|
|
201
|
+
}).filter(Boolean).map((l) => `const ${l} = (fn) => fn();`).join("\n");
|
|
202
|
+
});
|
|
203
|
+
code = code.replace(/__vite__mapDeps\([^)]+\)/g, "[]");
|
|
204
|
+
code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
|
|
205
|
+
return code;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Fetch an HTTP ESM module, transform it, write it to a temp .mjs file and
|
|
209
|
+
* return the file path. Recursively does the same for HTTP transitive imports
|
|
210
|
+
* so that `import('file:///...temp.mjs')` can resolve them.
|
|
211
|
+
*/
|
|
212
|
+
async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
|
|
213
|
+
if (visited.has(url)) return visited.get(url);
|
|
214
|
+
if (tempFileCache.has(url)) return tempFileCache.get(url);
|
|
215
|
+
const promise = (async () => {
|
|
216
|
+
let code = await (await fetch(url)).text();
|
|
217
|
+
const base = url.replace(/\/[^/]*$/, "/");
|
|
218
|
+
const relImports = [];
|
|
219
|
+
const relRegex = /(?:from|export\s*\*\s*from|import\s*(?:\(|\s))\s*["'`]([^"'`\s]+)["'`]/g;
|
|
220
|
+
let m;
|
|
221
|
+
while ((m = relRegex.exec(code)) !== null) if (m[1].startsWith("./") || m[1].startsWith("../")) relImports.push(new URL(m[1], base).href);
|
|
222
|
+
const subMap = /* @__PURE__ */ new Map();
|
|
223
|
+
await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
|
|
224
|
+
const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap);
|
|
225
|
+
subMap.set(u, `file://${tmpPath}`);
|
|
226
|
+
}));
|
|
227
|
+
code = transformSsrCode(code, base, sharedPkgMap);
|
|
228
|
+
for (const [httpUrl, fileUrl] of subMap) code = code.split(httpUrl).join(fileUrl);
|
|
229
|
+
const { createHash } = await _crypto();
|
|
230
|
+
const { join } = await _path();
|
|
231
|
+
const { writeFileSync } = await _fs();
|
|
232
|
+
const tmpFile = join(tmpDir, `${createHash("sha1").update(url).digest("hex").slice(0, 12)}.mjs`);
|
|
233
|
+
writeFileSync(tmpFile, code, "utf8");
|
|
234
|
+
visited.set(url, tmpFile);
|
|
235
|
+
return tmpFile;
|
|
236
|
+
})();
|
|
237
|
+
tempFileCache.set(url, promise);
|
|
238
|
+
return promise;
|
|
239
|
+
}
|
|
240
|
+
async function importTempModule(filePath) {
|
|
241
|
+
return await import(
|
|
242
|
+
/* @vite-ignore */
|
|
243
|
+
filePath
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
|
|
247
|
+
const { url, type } = ssrEntry;
|
|
248
|
+
if (type === "commonjs-module" || type === "commonjs") {
|
|
249
|
+
const { createRequire } = await _module();
|
|
250
|
+
const req = createRequire(import.meta.url);
|
|
251
|
+
try {
|
|
252
|
+
return req(url);
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
256
|
+
const urlObj = new URL(url);
|
|
257
|
+
if (urlObj.pathname.includes("/__mf_ssr__/")) {
|
|
258
|
+
const remoteOrigin = urlObj.origin;
|
|
259
|
+
const runner = await getOrCreateRunner(remoteOrigin);
|
|
260
|
+
if (!runner) return null;
|
|
261
|
+
try {
|
|
262
|
+
return await runner.import(urlObj.pathname);
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const { mkdirSync } = await _fs();
|
|
268
|
+
const cacheDir = await getSSRCacheDir();
|
|
269
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
270
|
+
const sharedPkgMap = new Map(Object.entries(resolvedShared));
|
|
271
|
+
try {
|
|
272
|
+
return await importTempModule(await fetchEsmToTempFile(url, cacheDir, /* @__PURE__ */ new Map(), sharedPkgMap));
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return await import(
|
|
279
|
+
/* @vite-ignore */
|
|
280
|
+
url
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function ssrEntryLoaderPlugin(options = {}) {
|
|
287
|
+
const resolvedShared = options.resolvedShared ?? {};
|
|
288
|
+
return {
|
|
289
|
+
name: "mf-vite:ssr-entry-loader",
|
|
290
|
+
async loadEntry({ remoteInfo }) {
|
|
291
|
+
if (typeof globalThis.window !== "undefined") return;
|
|
292
|
+
const ssrEntry = await getSSREntry(remoteInfo.entry);
|
|
293
|
+
if (!ssrEntry) return;
|
|
294
|
+
const mod = await loadSSRRemoteEntry(ssrEntry, resolvedShared);
|
|
295
|
+
if (!mod) return;
|
|
296
|
+
return mod;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
export { ssrEntryLoaderPlugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@module-federation/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Vite plugin for Module Federation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.cjs",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"import": "./lib/index.mjs",
|
|
16
16
|
"require": "./lib/index.cjs"
|
|
17
17
|
},
|
|
18
|
+
"./ssrEntryLoader": {
|
|
19
|
+
"types": {
|
|
20
|
+
"import": "./lib/utils/ssrEntryLoader.d.mts",
|
|
21
|
+
"require": "./lib/utils/ssrEntryLoader.d.cts"
|
|
22
|
+
},
|
|
23
|
+
"import": "./lib/utils/ssrEntryLoader.mjs",
|
|
24
|
+
"require": "./lib/utils/ssrEntryLoader.cjs"
|
|
25
|
+
},
|
|
18
26
|
"./package.json": "./package.json"
|
|
19
27
|
},
|
|
20
28
|
"files": [
|
|
@@ -69,9 +77,9 @@
|
|
|
69
77
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
70
78
|
},
|
|
71
79
|
"dependencies": {
|
|
72
|
-
"@module-federation/dts-plugin": "2.
|
|
73
|
-
"@module-federation/runtime": "2.
|
|
74
|
-
"@module-federation/sdk": "2.
|
|
80
|
+
"@module-federation/dts-plugin": "2.5.0",
|
|
81
|
+
"@module-federation/runtime": "2.5.0",
|
|
82
|
+
"@module-federation/sdk": "2.5.0",
|
|
75
83
|
"es-module-lexer": "^2.0.0",
|
|
76
84
|
"estree-walker": "^3.0.3",
|
|
77
85
|
"pathe": "^2.0.3"
|
|
@@ -88,4 +96,4 @@
|
|
|
88
96
|
"vite": "^8.0.10",
|
|
89
97
|
"vitest": "^4.0.18"
|
|
90
98
|
}
|
|
91
|
-
}
|
|
99
|
+
}
|