@module-federation/vite 1.16.12 → 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 +9 -16
- package/lib/index.js +198 -81
- package/lib/ssrVmStrategy-DtpfkCw1.js +152 -0
- package/lib/utils/ssrEntryLoader.d.ts +51 -1
- package/lib/utils/ssrEntryLoader.js +153 -38
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,22 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
[Read the announcement](https://www.linkedin.com/posts/voidzero_github-module-federationvite-vite-plugin-activity-7449452398202241024-JyAL).
|
|
8
8
|
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<img src="https://github.com/thecoder93.png?size=96" alt="thecoder93" width="64" height="64" />
|
|
19
|
-
</a>
|
|
20
|
-
<a href="https://github.com/stephanelgrg">
|
|
21
|
-
<img src="https://github.com/stephanelgrg.png?size=96" alt="stephanelgrg" width="64" height="64" />
|
|
22
|
-
</a>
|
|
23
|
-
</p>
|
|
24
|
-
</div>
|
|
9
|
+
<br />
|
|
10
|
+
|
|
11
|
+
<a href="https://github.com/sponsors/gioboa">
|
|
12
|
+
<img src="./docs/sponsors.png" alt="Sponsors" />
|
|
13
|
+
</a>
|
|
14
|
+
|
|
15
|
+
## Become a sponsor
|
|
16
|
+
|
|
17
|
+
[Support this project on GitHub Sponsors](https://github.com/sponsors/gioboa)
|
|
25
18
|
|
|
26
19
|
## Reason why 🤔
|
|
27
20
|
|
package/lib/index.js
CHANGED
|
@@ -650,7 +650,7 @@ function getExposesCssMapPlaceholder() {
|
|
|
650
650
|
function getVirtualExposesId(options) {
|
|
651
651
|
return `virtual:mf-exposes:${`${options.internalName}__${options.filename}`.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
652
652
|
}
|
|
653
|
-
function generateExposes(options) {
|
|
653
|
+
function generateExposes(options, remoteDependencyMap = {}, command = "build") {
|
|
654
654
|
return `
|
|
655
655
|
const cssAssetMap = ${JSON.stringify(options.bundleAllCSS ? EXPOSES_CSS_MAP_PLACEHOLDER : {})};
|
|
656
656
|
const injectedCssHrefs = new Set();
|
|
@@ -683,8 +683,12 @@ function generateExposes(options) {
|
|
|
683
683
|
}
|
|
684
684
|
injectedCssHrefs.add(href);
|
|
685
685
|
|
|
686
|
+
// Check for any existing stylesheet with the same href, not just
|
|
687
|
+
// MF-injected ones. This prevents duplicate <link> tags when Vite's
|
|
688
|
+
// own CSS module injection or MF runtime's createLink has already
|
|
689
|
+
// created a <link rel="stylesheet"> for the same URL.
|
|
686
690
|
const existingLink = document.querySelector(
|
|
687
|
-
\`link[rel="stylesheet"][
|
|
691
|
+
\`link[rel="stylesheet"][href="\${href}"]\`
|
|
688
692
|
);
|
|
689
693
|
if (existingLink) {
|
|
690
694
|
return Promise.resolve();
|
|
@@ -694,7 +698,6 @@ function generateExposes(options) {
|
|
|
694
698
|
const link = document.createElement("link");
|
|
695
699
|
link.rel = "stylesheet";
|
|
696
700
|
link.href = href;
|
|
697
|
-
link.setAttribute("data-mf-href", href);
|
|
698
701
|
link.onload = () => resolve();
|
|
699
702
|
link.onerror = () => reject(new Error(\`[Module Federation] Failed to load CSS asset: \${href}\`));
|
|
700
703
|
document.head.appendChild(link);
|
|
@@ -705,12 +708,22 @@ function generateExposes(options) {
|
|
|
705
708
|
|
|
706
709
|
export default {
|
|
707
710
|
${Object.keys(options.exposes).map((key) => {
|
|
711
|
+
const remoteDependencyPreloads = (remoteDependencyMap[key] ?? []).map((remoteId) => {
|
|
712
|
+
const virtualRemote = getRemoteVirtualModule(remoteId, command);
|
|
713
|
+
return `import(${JSON.stringify(virtualRemote.getImportId())})
|
|
714
|
+
.then((mod) => mod.__mf_remote_pending)`;
|
|
715
|
+
}).join(",");
|
|
708
716
|
return `
|
|
709
717
|
${JSON.stringify(key)}: async () => {
|
|
710
718
|
await injectCssAssets(${JSON.stringify(key)})
|
|
719
|
+
await Promise.all([${remoteDependencyPreloads}])
|
|
711
720
|
const importModule = await importExposedModule(
|
|
712
721
|
() => import(${JSON.stringify(options.exposes[key].import)})
|
|
713
722
|
)
|
|
723
|
+
const dependencyPending = importModule && importModule.__mf_remote_dependency_pending;
|
|
724
|
+
if (dependencyPending && typeof dependencyPending.then === "function") {
|
|
725
|
+
await dependencyPending;
|
|
726
|
+
}
|
|
714
727
|
const exportModule = {}
|
|
715
728
|
Object.assign(exportModule, importModule)
|
|
716
729
|
Object.defineProperty(exportModule, "__esModule", {
|
|
@@ -1245,7 +1258,7 @@ function writePreBuildLibPath(pkg, shareItem) {
|
|
|
1245
1258
|
const __mfPrebuildExports = __mfPrebuildNamespace;
|
|
1246
1259
|
${declarations}
|
|
1247
1260
|
${namedExportLine}
|
|
1248
|
-
export default
|
|
1261
|
+
export default __mfPrebuildNamespace.default ?? __mfPrebuildNamespace;
|
|
1249
1262
|
`, true);
|
|
1250
1263
|
return;
|
|
1251
1264
|
}
|
|
@@ -1334,13 +1347,13 @@ function generateLazyWorkspaceSingletonExports(namedExports, importSource, cache
|
|
|
1334
1347
|
${eagerLocalFallback ? applyLocalFallback : `if (import.meta.env.SSR) {
|
|
1335
1348
|
${applyLocalFallback}
|
|
1336
1349
|
} else {
|
|
1337
|
-
initPromise.then(() =>
|
|
1350
|
+
(__mfModuleCache.pendingShareLoads ||= []).push(initPromise.then(() =>
|
|
1338
1351
|
import(${escapeGeneratedStringLiteral(importSource)}).then((mod) => {
|
|
1339
1352
|
exportModule = __mfNormalizeShareModule(mod);
|
|
1340
1353
|
__mfWriteSharedCache(__mfModuleCache.share, ${cacheDescriptor}, exportModule);
|
|
1341
1354
|
__mfApplyLazyShareExports(exportModule);
|
|
1342
1355
|
})
|
|
1343
|
-
);
|
|
1356
|
+
));
|
|
1344
1357
|
}`}
|
|
1345
1358
|
} else {
|
|
1346
1359
|
__mfApplyLazyShareExports(exportModule);
|
|
@@ -1367,13 +1380,13 @@ function generateDeferredHostProvidedExports(namedExports, pkg, cacheDescriptor)
|
|
|
1367
1380
|
};
|
|
1368
1381
|
let exportModule = __mfReadSharedCache(__mfModuleCache.share, ${cacheDescriptor});
|
|
1369
1382
|
if (exportModule === undefined) {
|
|
1370
|
-
initPromise.then(() => {
|
|
1383
|
+
(__mfModuleCache.pendingShareLoads ||= []).push(initPromise.then(() => {
|
|
1371
1384
|
exportModule = __mfReadSharedCache(__mfModuleCache.share, ${cacheDescriptor});
|
|
1372
1385
|
if (exportModule === undefined) {
|
|
1373
1386
|
throw new Error("[Module Federation] Shared module ${pkg} was imported before federation bootstrap finished.");
|
|
1374
1387
|
}
|
|
1375
1388
|
__mfApplyHostProvidedExports(exportModule);
|
|
1376
|
-
});
|
|
1389
|
+
}));
|
|
1377
1390
|
} else {
|
|
1378
1391
|
__mfApplyHostProvidedExports(exportModule);
|
|
1379
1392
|
}
|
|
@@ -1890,7 +1903,6 @@ function generateRemoteEntry(options, virtualExposesId = getVirtualExposesId(opt
|
|
|
1890
1903
|
if (initScope.indexOf(initToken) >= 0) return;
|
|
1891
1904
|
initScope.push(initToken);
|
|
1892
1905
|
initRes.initShareScopeMap('${options.shareScope}', shared);
|
|
1893
|
-
initResolve(initRes)
|
|
1894
1906
|
try {
|
|
1895
1907
|
await retrySharedInit(async () => {
|
|
1896
1908
|
await Promise.all(await initRes.initializeSharing('${options.shareScope}', {
|
|
@@ -1914,6 +1926,7 @@ function generateRemoteEntry(options, virtualExposesId = getVirtualExposesId(opt
|
|
|
1914
1926
|
const resolved = await Promise.resolve(mod);
|
|
1915
1927
|
__mfWriteSharedCache(__mfModuleCache.share, cacheDescriptor, __mfNormalizeRuntimeShare(resolved));
|
|
1916
1928
|
}
|
|
1929
|
+
initResolve(initRes)
|
|
1917
1930
|
return initRes
|
|
1918
1931
|
}
|
|
1919
1932
|
|
|
@@ -2272,10 +2285,10 @@ function injectHostInitPreloads(html, bundle, resolvePath) {
|
|
|
2272
2285
|
function getFirstHtmlEntryFile(entryFiles) {
|
|
2273
2286
|
return entryFiles.find((file) => file.endsWith(".html"));
|
|
2274
2287
|
}
|
|
2275
|
-
function stripQueryAndHash(file) {
|
|
2288
|
+
function stripQueryAndHash$1(file) {
|
|
2276
2289
|
return file.split(/[?#]/)[0];
|
|
2277
2290
|
}
|
|
2278
|
-
function resolveDevHashEntryFileName(fileName) {
|
|
2291
|
+
function resolveDevHashEntryFileName$1(fileName) {
|
|
2279
2292
|
if (!fileName.includes("[hash")) return fileName;
|
|
2280
2293
|
const normalized = fileName.replace(/(?:[._-]?\[hash(?::\d+)?\])/g, "");
|
|
2281
2294
|
const baseName = path$1.basename(normalized);
|
|
@@ -2419,6 +2432,9 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
|
|
|
2419
2432
|
await __mfHostInit.__tla;
|
|
2420
2433
|
const { initHost } = __mfHostInit;
|
|
2421
2434
|
${preloadBlock}
|
|
2435
|
+
if (__mfModuleCache.pendingShareLoads) {
|
|
2436
|
+
await Promise.all(__mfModuleCache.pendingShareLoads);
|
|
2437
|
+
}
|
|
2422
2438
|
})().then(() => ${importExpression(entrySrc)});
|
|
2423
2439
|
`;
|
|
2424
2440
|
return [
|
|
@@ -2461,7 +2477,7 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
|
|
|
2461
2477
|
const scriptRegex = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>/gi;
|
|
2462
2478
|
let match;
|
|
2463
2479
|
while ((match = scriptRegex.exec(htmlContent)) !== null) {
|
|
2464
|
-
const scriptSrc = stripQueryAndHash(match[1]);
|
|
2480
|
+
const scriptSrc = stripQueryAndHash$1(match[1]);
|
|
2465
2481
|
if (/^(?:[a-z]+:)?\/\//i.test(scriptSrc)) continue;
|
|
2466
2482
|
addEntryFile(scriptSrc);
|
|
2467
2483
|
addEntryFile(scriptSrc.startsWith("/") ? path$1.resolve(viteConfig.root, scriptSrc.slice(1)) : path$1.resolve(path$1.dirname(htmlPath), scriptSrc));
|
|
@@ -2504,7 +2520,7 @@ const addEntry = ({ entryName, entryPath, fileName, inject = "entry", forceClien
|
|
|
2504
2520
|
next();
|
|
2505
2521
|
return;
|
|
2506
2522
|
}
|
|
2507
|
-
const devFileName = resolveDevHashEntryFileName(fileName);
|
|
2523
|
+
const devFileName = resolveDevHashEntryFileName$1(fileName);
|
|
2508
2524
|
if (devFileName !== fileName && req.url?.startsWith((viteConfig.base + devFileName).replace(/^\/?/, "/"))) req.url = req.url.replace(devFileName, fileName);
|
|
2509
2525
|
if (req.url && req.url.startsWith((viteConfig.base + fileName).replace(/^\/?/, "/"))) req.url = devEntryPath;
|
|
2510
2526
|
next();
|
|
@@ -4088,8 +4104,39 @@ function pluginModuleParseEnd_default(excludeFn, options) {
|
|
|
4088
4104
|
}
|
|
4089
4105
|
//#endregion
|
|
4090
4106
|
//#region src/plugins/pluginProxyRemoteEntry.ts
|
|
4107
|
+
function resolveDevHashEntryFileName(fileName) {
|
|
4108
|
+
if (!fileName.includes("[hash")) return fileName;
|
|
4109
|
+
const normalized = fileName.replace(/(?:[._-]?\[hash(?::\d+)?\])/g, "");
|
|
4110
|
+
const baseName = path$1.basename(normalized);
|
|
4111
|
+
return path$1.extname(baseName) ? normalized : `${normalized}.js`;
|
|
4112
|
+
}
|
|
4091
4113
|
function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposesId }) {
|
|
4092
4114
|
let viteConfig, _command, root;
|
|
4115
|
+
let exposeRemoteDependencies = {};
|
|
4116
|
+
function isRemoteImport(source) {
|
|
4117
|
+
return Object.keys(options.remotes).some((name) => source === name || source.startsWith(name + "/"));
|
|
4118
|
+
}
|
|
4119
|
+
function collectRemoteDependencies(code) {
|
|
4120
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
4121
|
+
for (const match of code.matchAll(/(?:^|[;\n\r])\s*import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)/g)) {
|
|
4122
|
+
const source = match[1] || match[2];
|
|
4123
|
+
if (source && isRemoteImport(source)) dependencies.add(source);
|
|
4124
|
+
}
|
|
4125
|
+
return Array.from(dependencies).sort();
|
|
4126
|
+
}
|
|
4127
|
+
async function refreshExposeRemoteDependencies(ctx) {
|
|
4128
|
+
const next = {};
|
|
4129
|
+
for (const [exposeKey, expose] of Object.entries(options.exposes)) {
|
|
4130
|
+
const resolved = await ctx.resolve(expose.import);
|
|
4131
|
+
if (!resolved?.id || resolved.id.includes("\0")) continue;
|
|
4132
|
+
try {
|
|
4133
|
+
next[exposeKey] = collectRemoteDependencies(readFileSync$1(resolved.id, "utf8"));
|
|
4134
|
+
} catch {
|
|
4135
|
+
next[exposeKey] = [];
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
exposeRemoteDependencies = next;
|
|
4139
|
+
}
|
|
4093
4140
|
return {
|
|
4094
4141
|
name: "proxyRemoteEntry",
|
|
4095
4142
|
enforce: "post",
|
|
@@ -4101,6 +4148,7 @@ function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposes
|
|
|
4101
4148
|
_command = command;
|
|
4102
4149
|
},
|
|
4103
4150
|
async buildStart() {
|
|
4151
|
+
await refreshExposeRemoteDependencies(this);
|
|
4104
4152
|
if (_command !== "build") return;
|
|
4105
4153
|
for (const expose of Object.values(options.exposes)) {
|
|
4106
4154
|
const resolved = await this.resolve(expose.import);
|
|
@@ -4122,19 +4170,19 @@ function pluginProxyRemoteEntry_default({ options, remoteEntryId, virtualExposes
|
|
|
4122
4170
|
},
|
|
4123
4171
|
load(id) {
|
|
4124
4172
|
if (id === remoteEntryId) return parsePromise.then((_) => generateRemoteEntry(options, virtualExposesId, _command));
|
|
4125
|
-
if (id === virtualExposesId) return generateExposes(options);
|
|
4173
|
+
if (id === virtualExposesId) return generateExposes(options, exposeRemoteDependencies, _command);
|
|
4126
4174
|
if (_command === "serve" && id.includes(getHostAutoInitPath())) return id;
|
|
4127
4175
|
},
|
|
4128
4176
|
transform(code, id) {
|
|
4129
4177
|
return mapCodeToCodeWithSourcemap((() => {
|
|
4130
4178
|
if (!filterId(id)) return;
|
|
4131
4179
|
if (id.includes(remoteEntryId)) return parsePromise.then((_) => generateRemoteEntry(options, virtualExposesId, _command));
|
|
4132
|
-
if (id === virtualExposesId) return generateExposes(options);
|
|
4180
|
+
if (id === virtualExposesId) return generateExposes(options, exposeRemoteDependencies, _command);
|
|
4133
4181
|
if (id.includes(getHostAutoInitPath())) {
|
|
4134
4182
|
if (_command === "serve") {
|
|
4135
4183
|
const host = typeof viteConfig.server?.host === "string" && viteConfig.server.host !== "0.0.0.0" ? viteConfig.server.host : "localhost";
|
|
4136
4184
|
const resolvedPublicPath = resolvePublicPath(options, viteConfig.base);
|
|
4137
|
-
const publicPath = JSON.stringify((resolvedPublicPath === "auto" ? "/" : resolvedPublicPath) + options.filename);
|
|
4185
|
+
const publicPath = JSON.stringify((resolvedPublicPath === "auto" ? "/" : resolvedPublicPath) + resolveDevHashEntryFileName(options.filename));
|
|
4138
4186
|
const fallbackOrigin = `//${host}:${viteConfig.server?.port}`;
|
|
4139
4187
|
const ssrRemoteEntry = "data:text/javascript," + encodeURIComponent("export async function init(){return {loadRemote:async()=>({}),loadShare:async()=>({})}}");
|
|
4140
4188
|
return `
|
|
@@ -4573,26 +4621,7 @@ function applyRewrites(code, imports, id) {
|
|
|
4573
4621
|
const ms = new CodeRewriter(code);
|
|
4574
4622
|
let changed = false;
|
|
4575
4623
|
let counter = 0;
|
|
4576
|
-
let namedProxyHelperDeclared = false;
|
|
4577
4624
|
const dependencyPendingIds = [];
|
|
4578
|
-
const namedProxyHelper = `function __mfCreateNamedRemoteProxy(ns, key) {
|
|
4579
|
-
const target = function (...args) {
|
|
4580
|
-
const value = ns[key];
|
|
4581
|
-
return typeof value === "function" ? value.apply(this, args) : value;
|
|
4582
|
-
};
|
|
4583
|
-
return new Proxy(target, {
|
|
4584
|
-
get(_target, prop) {
|
|
4585
|
-
if (prop === "then") return undefined;
|
|
4586
|
-
const value = ns[key];
|
|
4587
|
-
if (prop === Symbol.toPrimitive) return () => value;
|
|
4588
|
-
const item = value == null ? undefined : value[prop];
|
|
4589
|
-
return typeof item === "function" ? item.bind(value) : item;
|
|
4590
|
-
},
|
|
4591
|
-
apply(target, thisArg, args) {
|
|
4592
|
-
return target.apply(thisArg, args);
|
|
4593
|
-
}
|
|
4594
|
-
});
|
|
4595
|
-
}`;
|
|
4596
4625
|
for (const imp of imports) switch (imp.kind) {
|
|
4597
4626
|
case "static": {
|
|
4598
4627
|
const src = JSON.stringify(imp.source);
|
|
@@ -4610,20 +4639,8 @@ function applyRewrites(code, imports, id) {
|
|
|
4610
4639
|
importParts.push(`__mf_remote_pending as ${pendingId}`);
|
|
4611
4640
|
let rewrite = `import { ${importParts.join(", ")} } from ${src};`;
|
|
4612
4641
|
if (imp.named.length > 0) {
|
|
4613
|
-
const
|
|
4614
|
-
|
|
4615
|
-
const destructParts = imp.named.map((s, index) => `${s.imported}: ${tempNames[index]}`);
|
|
4616
|
-
const bindingLines = imp.named.map((s, index) => {
|
|
4617
|
-
const temp = tempNames[index];
|
|
4618
|
-
return `const ${s.local} = ${isProxyId} ? __mfCreateNamedRemoteProxy(${nsId}, ${JSON.stringify(s.imported)}) : ${temp};`;
|
|
4619
|
-
});
|
|
4620
|
-
if (!namedProxyHelperDeclared) {
|
|
4621
|
-
rewrite += `\n${namedProxyHelper}`;
|
|
4622
|
-
namedProxyHelperDeclared = true;
|
|
4623
|
-
}
|
|
4624
|
-
rewrite += `\nconst ${isProxyId} = ${nsId} && ${nsId}.__mf_is_remote_proxy;`;
|
|
4625
|
-
rewrite += `\nconst { ${destructParts.join(", ")} } = ${isProxyId} ? {} : ${nsId};`;
|
|
4626
|
-
rewrite += `\n${bindingLines.join("\n")}`;
|
|
4642
|
+
const destructParts = imp.named.map((s) => `${s.imported}: ${s.local}`);
|
|
4643
|
+
rewrite += `\nconst { ${destructParts.join(", ")} } = ${nsId};`;
|
|
4627
4644
|
}
|
|
4628
4645
|
ms.overwrite(imp.start, imp.end, rewrite);
|
|
4629
4646
|
}
|
|
@@ -4880,6 +4897,104 @@ function pluginRemoteNamedExports(options) {
|
|
|
4880
4897
|
}
|
|
4881
4898
|
//#endregion
|
|
4882
4899
|
//#region src/plugins/pluginSSRRemoteEntry.ts
|
|
4900
|
+
const MAX_RUNNER_BODY_BYTES = 1024 * 1024;
|
|
4901
|
+
const ALLOWED_RUNNER_INVOKE_NAMES = new Set(["fetchModule", "getBuiltins"]);
|
|
4902
|
+
const VITE_FS_PREFIX = "/@fs/";
|
|
4903
|
+
function isPlainObject(value) {
|
|
4904
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4905
|
+
}
|
|
4906
|
+
function stripQueryAndHash(id) {
|
|
4907
|
+
const queryIndex = id.indexOf("?");
|
|
4908
|
+
const hashIndex = id.indexOf("#");
|
|
4909
|
+
const endIndex = queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
4910
|
+
return endIndex === -1 ? id : id.slice(0, endIndex);
|
|
4911
|
+
}
|
|
4912
|
+
function decodeRunnerFilePath(filePath) {
|
|
4913
|
+
try {
|
|
4914
|
+
return decodeURIComponent(filePath);
|
|
4915
|
+
} catch {
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
function hasRelativeTraversal(id) {
|
|
4920
|
+
return id.split(/[\\/]+/).includes("..");
|
|
4921
|
+
}
|
|
4922
|
+
function getRealPathIfExists(filePath) {
|
|
4923
|
+
try {
|
|
4924
|
+
return fs$1.realpathSync.native(filePath);
|
|
4925
|
+
} catch {
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
function isPathWithinDirectory(filePath, directory) {
|
|
4930
|
+
const realFilePath = getRealPathIfExists(filePath) ?? path$1.resolve(filePath);
|
|
4931
|
+
const realDirectory = getRealPathIfExists(directory) ?? path$1.resolve(directory);
|
|
4932
|
+
const relative = path$1.relative(realDirectory, realFilePath);
|
|
4933
|
+
return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
|
|
4934
|
+
}
|
|
4935
|
+
function getRunnerAllowedDirectories(config) {
|
|
4936
|
+
return [config.root, ...config.server?.fs?.allow ?? []].map((directory) => path$1.resolve(directory));
|
|
4937
|
+
}
|
|
4938
|
+
function isPathWithinAllowedDirectories(filePath, allowedDirectories) {
|
|
4939
|
+
return allowedDirectories.some((directory) => isPathWithinDirectory(filePath, directory));
|
|
4940
|
+
}
|
|
4941
|
+
function isSafeRunnerFetchModuleId(id, config) {
|
|
4942
|
+
if (typeof id !== "string" || !id || id.includes("\0")) return false;
|
|
4943
|
+
const decoded = decodeViteId(id).replace(/^\0+/, "");
|
|
4944
|
+
if (!decoded || decoded.startsWith("virtual:")) return !!decoded;
|
|
4945
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(decoded) || decoded.startsWith("//")) return false;
|
|
4946
|
+
const cleanId = decodeRunnerFilePath(stripQueryAndHash(decoded));
|
|
4947
|
+
if (!cleanId || hasRelativeTraversal(cleanId)) return false;
|
|
4948
|
+
const allowedDirectories = getRunnerAllowedDirectories(config);
|
|
4949
|
+
if (cleanId.startsWith(VITE_FS_PREFIX)) {
|
|
4950
|
+
const fsPath = cleanId.slice(5);
|
|
4951
|
+
return path$1.isAbsolute(fsPath) && isPathWithinAllowedDirectories(fsPath, allowedDirectories);
|
|
4952
|
+
}
|
|
4953
|
+
if (path$1.isAbsolute(cleanId)) {
|
|
4954
|
+
if (isPathWithinAllowedDirectories(cleanId, allowedDirectories)) return true;
|
|
4955
|
+
return !fs$1.existsSync(cleanId);
|
|
4956
|
+
}
|
|
4957
|
+
return true;
|
|
4958
|
+
}
|
|
4959
|
+
function isRunnerInvokePayload(payload, config) {
|
|
4960
|
+
if (!payload || typeof payload !== "object") return false;
|
|
4961
|
+
if (payload.type !== "custom" || payload.event !== "vite:invoke") return false;
|
|
4962
|
+
const data = payload.data;
|
|
4963
|
+
if (!data || typeof data !== "object") return false;
|
|
4964
|
+
const name = data.name;
|
|
4965
|
+
const args = data.data;
|
|
4966
|
+
if (typeof name !== "string" || !ALLOWED_RUNNER_INVOKE_NAMES.has(name) || !Array.isArray(args)) return false;
|
|
4967
|
+
if (name === "getBuiltins") return args.length === 0;
|
|
4968
|
+
if (args.length < 1 || args.length > 3) return false;
|
|
4969
|
+
const [id, importer, opts] = args;
|
|
4970
|
+
return isSafeRunnerFetchModuleId(id, config) && (importer === void 0 || importer === null || isSafeRunnerFetchModuleId(importer, config)) && (opts === void 0 || isPlainObject(opts));
|
|
4971
|
+
}
|
|
4972
|
+
function readBoundedRunnerBody(req, res) {
|
|
4973
|
+
return new Promise((resolve) => {
|
|
4974
|
+
const chunks = [];
|
|
4975
|
+
let size = 0;
|
|
4976
|
+
let done = false;
|
|
4977
|
+
const fail = (statusCode, message) => {
|
|
4978
|
+
if (done) return;
|
|
4979
|
+
done = true;
|
|
4980
|
+
res.statusCode = statusCode;
|
|
4981
|
+
res.end(message);
|
|
4982
|
+
resolve(void 0);
|
|
4983
|
+
};
|
|
4984
|
+
req.on("data", (chunk) => {
|
|
4985
|
+
if (done) return;
|
|
4986
|
+
size += chunk.length;
|
|
4987
|
+
if (size > MAX_RUNNER_BODY_BYTES) return fail(413, "Payload too large");
|
|
4988
|
+
chunks.push(chunk);
|
|
4989
|
+
});
|
|
4990
|
+
req.on("end", () => {
|
|
4991
|
+
if (done) return;
|
|
4992
|
+
done = true;
|
|
4993
|
+
resolve(Buffer.concat(chunks));
|
|
4994
|
+
});
|
|
4995
|
+
req.on("error", () => fail(400, "Bad request"));
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
4883
4998
|
/**
|
|
4884
4999
|
* Emits a Node-compatible SSR remote entry alongside the browser entry.
|
|
4885
5000
|
*
|
|
@@ -4966,7 +5081,8 @@ function pluginSSRRemoteEntry(options) {
|
|
|
4966
5081
|
});
|
|
4967
5082
|
const ssrEnv = server.environments?.ssr;
|
|
4968
5083
|
const clientEnv = server.environments?.client;
|
|
4969
|
-
|
|
5084
|
+
const runnerEnv = typeof ssrEnv?.hot?.handleInvoke === "function" ? ssrEnv : typeof clientEnv?.hot?.handleInvoke === "function" ? clientEnv : void 0;
|
|
5085
|
+
if (typeof (ssrEnv?.fetchModule ?? clientEnv?.fetchModule) === "function" && runnerEnv) server.middlewares.use("/__mf_runner__", async (req, res) => {
|
|
4970
5086
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4971
5087
|
if (req.method === "OPTIONS") {
|
|
4972
5088
|
res.setHeader("Access-Control-Allow-Methods", "POST");
|
|
@@ -4981,46 +5097,37 @@ function pluginSSRRemoteEntry(options) {
|
|
|
4981
5097
|
return;
|
|
4982
5098
|
}
|
|
4983
5099
|
try {
|
|
4984
|
-
const
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
}
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
const builtins = (clientEnv ?? ssrEnv)?.config?.resolve?.builtins ?? [];
|
|
4993
|
-
res.setHeader("Content-Type", "application/json");
|
|
4994
|
-
res.end(JSON.stringify({ result: builtins }));
|
|
5100
|
+
const rawBody = await readBoundedRunnerBody(req, res);
|
|
5101
|
+
if (!rawBody) return;
|
|
5102
|
+
let body;
|
|
5103
|
+
try {
|
|
5104
|
+
body = JSON.parse(rawBody.toString("utf8"));
|
|
5105
|
+
} catch {
|
|
5106
|
+
res.statusCode = 400;
|
|
5107
|
+
res.end(JSON.stringify({ error: { message: "Invalid JSON" } }));
|
|
4995
5108
|
return;
|
|
4996
5109
|
}
|
|
4997
|
-
if (body.
|
|
5110
|
+
if (!isRunnerInvokePayload(body, server.config)) {
|
|
4998
5111
|
res.statusCode = 400;
|
|
4999
|
-
res.end(JSON.stringify({ error: { message:
|
|
5112
|
+
res.end(JSON.stringify({ error: { message: "Invalid runner invoke" } }));
|
|
5000
5113
|
return;
|
|
5001
5114
|
}
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
result = await fetchFn(id, importer, opts);
|
|
5008
|
-
} catch (fetchErr) {
|
|
5009
|
-
const bareId = decodeViteId(id);
|
|
5010
|
-
try {
|
|
5115
|
+
let result = await runnerEnv.hot.handleInvoke(body);
|
|
5116
|
+
if ("error" in result && body.data.name === "fetchModule") {
|
|
5117
|
+
const id = body.data.data[0];
|
|
5118
|
+
const bareId = typeof id === "string" ? decodeViteId(id).replace(/^\0/, "") : "";
|
|
5119
|
+
if (bareId && !bareId.startsWith(".") && !bareId.startsWith("/") && !bareId.startsWith("file:") && !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(bareId)) try {
|
|
5011
5120
|
const { createRequire } = await import("module");
|
|
5012
5121
|
const path = await import("path");
|
|
5013
5122
|
const { pathToFileURL } = await import("url");
|
|
5014
|
-
result = {
|
|
5015
|
-
externalize: pathToFileURL(createRequire(pathToFileURL(path.join(server.config.root, "package.json"))).resolve(bareId
|
|
5123
|
+
result = { result: {
|
|
5124
|
+
externalize: pathToFileURL(createRequire(pathToFileURL(path.join(server.config.root, "package.json"))).resolve(bareId)).href,
|
|
5016
5125
|
type: "module"
|
|
5017
|
-
};
|
|
5018
|
-
} catch {
|
|
5019
|
-
throw fetchErr;
|
|
5020
|
-
}
|
|
5126
|
+
} };
|
|
5127
|
+
} catch {}
|
|
5021
5128
|
}
|
|
5022
5129
|
res.setHeader("Content-Type", "application/json");
|
|
5023
|
-
res.end(JSON.stringify(
|
|
5130
|
+
res.end(JSON.stringify(result));
|
|
5024
5131
|
} catch (e) {
|
|
5025
5132
|
res.setHeader("Content-Type", "application/json");
|
|
5026
5133
|
res.end(JSON.stringify({ error: { message: String(e instanceof Error ? e.message : e) } }));
|
|
@@ -5864,7 +5971,9 @@ function federation(mfUserOptions) {
|
|
|
5864
5971
|
},
|
|
5865
5972
|
load(id) {
|
|
5866
5973
|
if (id.includes("__loadShare__") || id.includes("__loadRemote__")) {
|
|
5867
|
-
|
|
5974
|
+
const virtualModule = VirtualModule.findById(id);
|
|
5975
|
+
if (!virtualModule?.code) return null;
|
|
5976
|
+
let code = virtualModule.code;
|
|
5868
5977
|
const environmentName = this.environment?.name;
|
|
5869
5978
|
if (environmentName && environmentName !== "client" || !environmentName && isSsrBuild) code = prependWorkspaceSingletonSsrImport(code);
|
|
5870
5979
|
code = code.replace(/import\s+["'][^"']*__prebuild__[^"']*["']\s*;?/g, "");
|
|
@@ -5948,7 +6057,15 @@ function federation(mfUserOptions) {
|
|
|
5948
6057
|
options.runtimePlugins.forEach((p) => {
|
|
5949
6058
|
const pluginPath = typeof p === "string" ? p : p[0];
|
|
5950
6059
|
if (SSR_ONLY_PLUGINS.has(pluginPath)) return;
|
|
5951
|
-
if (pluginPath && !pluginPath.startsWith(".") && !pluginPath.startsWith("/") && !pluginPath.startsWith("\0") && !pluginPath.startsWith("virtual:"))
|
|
6060
|
+
if (pluginPath && !pluginPath.startsWith(".") && !pluginPath.startsWith("/") && !pluginPath.startsWith("\0") && !pluginPath.startsWith("virtual:")) {
|
|
6061
|
+
let optimizeDep = pluginPath;
|
|
6062
|
+
if (pluginPath === "@module-federation/dts-plugin/dynamic-remote-type-hints-plugin") try {
|
|
6063
|
+
optimizeDep = normalizePathForImport(resolveImportPath(pluginPath));
|
|
6064
|
+
} catch {
|
|
6065
|
+
optimizeDep = pluginPath;
|
|
6066
|
+
}
|
|
6067
|
+
config.optimizeDeps.include.push(optimizeDep);
|
|
6068
|
+
}
|
|
5952
6069
|
});
|
|
5953
6070
|
if (isRolldown) {
|
|
5954
6071
|
config.build ??= {};
|
|
@@ -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 };
|
|
@@ -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 };
|
|
@@ -70,10 +70,7 @@ async function getOrCreateRunner(remoteOrigin) {
|
|
|
70
70
|
return await (await fetch(runnerEndpoint, {
|
|
71
71
|
method: "POST",
|
|
72
72
|
headers: { "Content-Type": "application/json" },
|
|
73
|
-
body: JSON.stringify(
|
|
74
|
-
name: payload.data.name,
|
|
75
|
-
data: payload.data.data
|
|
76
|
-
})
|
|
73
|
+
body: JSON.stringify(payload)
|
|
77
74
|
})).json();
|
|
78
75
|
} }
|
|
79
76
|
}, new ESModulesEvaluator());
|
|
@@ -88,6 +85,27 @@ const _path = () => nodeImport("path");
|
|
|
88
85
|
const _fs = () => nodeImport("fs");
|
|
89
86
|
const _crypto = () => nodeImport("crypto");
|
|
90
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
|
+
}
|
|
91
109
|
const ssrEntryCache = /* @__PURE__ */ new Map();
|
|
92
110
|
const manifestFetchCache = /* @__PURE__ */ new Map();
|
|
93
111
|
var SsrEntryHttpError = class extends Error {
|
|
@@ -149,7 +167,8 @@ function resolveSSREntryUrl(manifest, manifestUrl) {
|
|
|
149
167
|
const entryPath = (meta.ssrRemoteEntry.path || "") + meta.ssrRemoteEntry.name;
|
|
150
168
|
return {
|
|
151
169
|
url: new URL(entryPath, base).href,
|
|
152
|
-
type: meta.ssrRemoteEntry.type || "module"
|
|
170
|
+
type: meta.ssrRemoteEntry.type || "module",
|
|
171
|
+
versionKey: computeManifestVersionKey(manifest)
|
|
153
172
|
};
|
|
154
173
|
}
|
|
155
174
|
/**
|
|
@@ -191,14 +210,17 @@ function buildSsrEntryCandidates(ctx, options = {}) {
|
|
|
191
210
|
const candidates = [];
|
|
192
211
|
if (!options.skipServerBuild) candidates.push({
|
|
193
212
|
url: `${remoteOrigin}/__mf_server__/${filename}.ssr.js`,
|
|
194
|
-
type: "module"
|
|
213
|
+
type: "module",
|
|
214
|
+
versionKey: UNVERSIONED
|
|
195
215
|
});
|
|
196
216
|
candidates.push({
|
|
197
217
|
url: `${base}.ssr.js`,
|
|
198
|
-
type: "module"
|
|
218
|
+
type: "module",
|
|
219
|
+
versionKey: UNVERSIONED
|
|
199
220
|
}, {
|
|
200
221
|
url: `${remoteOrigin}/__mf_ssr__/${filename}.ssr.js`,
|
|
201
|
-
type: "module"
|
|
222
|
+
type: "module",
|
|
223
|
+
versionKey: UNVERSIONED
|
|
202
224
|
});
|
|
203
225
|
return candidates;
|
|
204
226
|
}
|
|
@@ -212,13 +234,15 @@ async function resolveFirstReachableCandidate(candidates) {
|
|
|
212
234
|
async function resolveSSREntryImpl(remoteEntryUrl) {
|
|
213
235
|
if (isSsrEntry(remoteEntryUrl)) return {
|
|
214
236
|
url: remoteEntryUrl,
|
|
215
|
-
type: "module"
|
|
237
|
+
type: "module",
|
|
238
|
+
versionKey: UNVERSIONED
|
|
216
239
|
};
|
|
217
240
|
if (!isManifestEntry(remoteEntryUrl)) {
|
|
218
241
|
const filename = getEntryFilename(remoteEntryUrl);
|
|
219
242
|
const fromServerBuild = await headCheckSsrEntry({
|
|
220
243
|
url: `${remoteEntryUrl.replace(/\/[^/]+$/, "")}/__mf_server__/${filename}.ssr.js`,
|
|
221
|
-
type: "module"
|
|
244
|
+
type: "module",
|
|
245
|
+
versionKey: UNVERSIONED
|
|
222
246
|
});
|
|
223
247
|
if (fromServerBuild) return fromServerBuild;
|
|
224
248
|
}
|
|
@@ -229,9 +253,63 @@ async function resolveSSREntryImpl(remoteEntryUrl) {
|
|
|
229
253
|
}
|
|
230
254
|
return resolveFirstReachableCandidate(buildSsrEntryCandidates(ctx, { skipServerBuild: !isManifestEntry(remoteEntryUrl) }));
|
|
231
255
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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 {}
|
|
235
313
|
}
|
|
236
314
|
const tempFileCache = /* @__PURE__ */ new Map();
|
|
237
315
|
let ssrCacheDirPromise;
|
|
@@ -252,14 +330,11 @@ async function getSSRCacheDir() {
|
|
|
252
330
|
})();
|
|
253
331
|
return ssrCacheDirPromise;
|
|
254
332
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const resolved = sharedPkgMap.get(specifier);
|
|
261
|
-
return resolved ? m.replace(specifier, `file://${resolved}`) : m;
|
|
262
|
-
});
|
|
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) {
|
|
263
338
|
code = code.replace(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*preload-helper[^"']*["'];?/g, (_m, bindings) => {
|
|
264
339
|
return bindings.split(",").map((b) => {
|
|
265
340
|
const parts = b.trim().split(/\s+as\s+/);
|
|
@@ -270,6 +345,16 @@ function transformSsrCode(code, base, sharedPkgMap) {
|
|
|
270
345
|
code = code.replace(/\b([A-Za-z_$][\w$]*)\s*\(\s*\(\s*\)\s*=>\s*import\(([^)]*)\)\s*,\s*\[\]\s*\)/g, "import($2)");
|
|
271
346
|
return code;
|
|
272
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
|
+
}
|
|
273
358
|
function isVitePreloadHelperSpecifier(specifier) {
|
|
274
359
|
return specifier.includes("preload-helper");
|
|
275
360
|
}
|
|
@@ -277,10 +362,15 @@ function isVitePreloadHelperSpecifier(specifier) {
|
|
|
277
362
|
* Fetch an HTTP ESM module, transform it, write it to a temp .js file and
|
|
278
363
|
* return the file path. Recursively does the same for HTTP transitive imports
|
|
279
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.
|
|
280
369
|
*/
|
|
281
|
-
async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
|
|
370
|
+
async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap, versionKey = UNVERSIONED) {
|
|
371
|
+
const cacheKey = `${versionKey}::${url}`;
|
|
282
372
|
if (visited.has(url)) return visited.get(url);
|
|
283
|
-
if (tempFileCache.has(
|
|
373
|
+
if (tempFileCache.has(cacheKey)) return tempFileCache.get(cacheKey);
|
|
284
374
|
const promise = (async () => {
|
|
285
375
|
const res = await fetch(url);
|
|
286
376
|
let code = await res.text();
|
|
@@ -292,7 +382,7 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
|
|
|
292
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);
|
|
293
383
|
const subMap = /* @__PURE__ */ new Map();
|
|
294
384
|
await Promise.all([...new Set(relImports)].filter((u) => u.startsWith("http://") || u.startsWith("https://")).map(async (u) => {
|
|
295
|
-
const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap);
|
|
385
|
+
const tmpPath = await fetchEsmToTempFile(u, tmpDir, visited, sharedPkgMap, versionKey);
|
|
296
386
|
subMap.set(u, `file://${tmpPath}`);
|
|
297
387
|
}));
|
|
298
388
|
code = transformSsrCode(code, base, sharedPkgMap);
|
|
@@ -300,22 +390,36 @@ async function fetchEsmToTempFile(url, tmpDir, visited, sharedPkgMap) {
|
|
|
300
390
|
const { createHash } = await _crypto();
|
|
301
391
|
const { join } = await _path();
|
|
302
392
|
const { writeFileSync } = await _fs();
|
|
303
|
-
const tmpFile = join(tmpDir, `${createHash("sha1").update(
|
|
393
|
+
const tmpFile = join(tmpDir, `${createHash("sha1").update(cacheKey).digest("hex").slice(0, 12)}.js`);
|
|
304
394
|
writeFileSync(tmpFile, code, "utf8");
|
|
305
395
|
visited.set(url, tmpFile);
|
|
306
396
|
return tmpFile;
|
|
307
397
|
})();
|
|
308
|
-
tempFileCache.set(
|
|
398
|
+
tempFileCache.set(cacheKey, promise);
|
|
309
399
|
return promise;
|
|
310
400
|
}
|
|
311
|
-
async function importTempModule(filePath) {
|
|
312
|
-
return await import(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
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
|
+
});
|
|
316
419
|
}
|
|
317
|
-
async function loadSSRRemoteEntry(ssrEntry,
|
|
318
|
-
const { url, type } = ssrEntry;
|
|
420
|
+
async function loadSSRRemoteEntry(ssrEntry, options) {
|
|
421
|
+
const { url, type, versionKey } = ssrEntry;
|
|
422
|
+
const { resolvedShared } = options;
|
|
319
423
|
if (type === "commonjs-module" || type === "commonjs") {
|
|
320
424
|
const { createRequire } = await _module();
|
|
321
425
|
const req = createRequire(import.meta.url);
|
|
@@ -338,12 +442,18 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
|
|
|
338
442
|
if (process.env.NODE_ENV !== "production") return null;
|
|
339
443
|
}
|
|
340
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
|
+
}
|
|
341
451
|
const { mkdirSync } = await _fs();
|
|
342
452
|
const cacheDir = await getSSRCacheDir();
|
|
343
453
|
mkdirSync(cacheDir, { recursive: true });
|
|
344
454
|
const sharedPkgMap = new Map(Object.entries(resolvedShared));
|
|
345
455
|
try {
|
|
346
|
-
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);
|
|
347
457
|
} catch (error) {
|
|
348
458
|
if (isSsrEntryHttpError(error)) throw error;
|
|
349
459
|
return null;
|
|
@@ -359,18 +469,23 @@ async function loadSSRRemoteEntry(ssrEntry, resolvedShared = {}) {
|
|
|
359
469
|
}
|
|
360
470
|
}
|
|
361
471
|
function ssrEntryLoaderPlugin(options = {}) {
|
|
362
|
-
const
|
|
472
|
+
const resolved = {
|
|
473
|
+
resolvedShared: options.resolvedShared ?? {},
|
|
474
|
+
strategy: options.strategy ?? "temp-file",
|
|
475
|
+
shareScopeName: options.shareScopeName ?? "default",
|
|
476
|
+
maxAgeMs: options.maxAgeMs
|
|
477
|
+
};
|
|
363
478
|
return {
|
|
364
479
|
name: "mf-vite:ssr-entry-loader",
|
|
365
480
|
async loadEntry({ remoteInfo }) {
|
|
366
481
|
if (!isNodeServer()) return;
|
|
367
|
-
const ssrEntry = await getSSREntry(remoteInfo.entry);
|
|
482
|
+
const ssrEntry = await getSSREntry(remoteInfo.entry, resolved.maxAgeMs);
|
|
368
483
|
if (!ssrEntry) return;
|
|
369
|
-
const mod = await loadSSRRemoteEntry(ssrEntry,
|
|
484
|
+
const mod = await loadSSRRemoteEntry(ssrEntry, resolved);
|
|
370
485
|
if (!mod) return;
|
|
371
486
|
return mod;
|
|
372
487
|
}
|
|
373
488
|
};
|
|
374
489
|
}
|
|
375
490
|
//#endregion
|
|
376
|
-
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
|
+
}
|