@module-federation/manifest
Version:
Provide manifest/stats for webpack/rspack MF project .
478 lines (463 loc) • 20.4 kB
JavaScript
import { composeKeyWithSeparator, getManifestFileName } from "@module-federation/sdk";
import { assert, findChunk, getAssetsByChunk, getAssetsByChunkIDs, getFileNameWithOutExt, getSharedModules, getTypesMetaInfo } from "./utils.mjs";
import logger from "./logger.mjs";
import { ContainerManager, PKGJsonManager, RemoteManager, SharedManager, UNKNOWN_MODULE_NAME, utils } from "@module-federation/managers";
import { HOT_UPDATE_SUFFIX } from "./constants.mjs";
import { ModuleHandler, getExposeItem, getExposeName, getShareItem } from "./ModuleHandler.mjs";
;// CONCATENATED MODULE: external "@module-federation/sdk"
;// CONCATENATED MODULE: external "./utils.mjs"
;// CONCATENATED MODULE: external "./logger.mjs"
;// CONCATENATED MODULE: external "@module-federation/managers"
;// CONCATENATED MODULE: external "./constants.mjs"
;// CONCATENATED MODULE: external "./ModuleHandler.mjs"
;// CONCATENATED MODULE: ./src/StatsManager.ts
/* eslint-disable max-lines-per-function */ /* eslint-disable @typescript-eslint/member-ordering */ /* eslint-disable max-depth */
class StatsManager {
getBuildInfo(context, target) {
const rootPath = context || process.cwd();
const pkg = this._pkgJsonManager.readPKGJson(rootPath);
const statsBuildInfo = {
buildVersion: utils.getBuildVersion(rootPath),
buildName: utils.getBuildName() || pkg['name']
};
if (this._sharedManager.enableTreeShaking) {
statsBuildInfo.target = target ? Array.isArray(target) ? target : [
target
] : [];
statsBuildInfo.plugins = this._options.treeShakingSharedPlugins || [];
statsBuildInfo.excludePlugins = this._options.treeShakingSharedExcludePlugins || [];
}
return statsBuildInfo;
}
get fileName() {
return getManifestFileName(this._options.manifest).statsFileName;
}
setMetaDataPublicPath(metaData, compiler) {
if (this._options.getPublicPath) {
if ('publicPath' in metaData) {
// @ts-ignore
delete metaData.publicPath;
}
metaData.getPublicPath = this._options.getPublicPath;
} else {
metaData.publicPath = this.getPublicPath(compiler);
}
return metaData;
}
_getMetaData(compiler, compilation, extraOptions) {
var _this__options_library, _this__options;
const { context } = compiler.options;
const { _options: { name } } = this;
const buildInfo = this.getBuildInfo(context, compilation.options.target || '');
const type = this._pkgJsonManager.getExposeGarfishModuleType(context || process.cwd());
const getRemoteEntryName = ()=>{
if (!this._containerManager.enable) {
return '';
}
assert(name, 'name is required');
const remoteEntryPoint = compilation.entrypoints.get(name);
assert(remoteEntryPoint, 'Can not get remoteEntry entryPoint!');
const remoteEntryNameChunk = compilation.namedChunks.get(name);
assert(remoteEntryNameChunk, 'Can not get remoteEntry chunk!');
const files = Array.from(remoteEntryNameChunk.files).filter((f)=>!f.includes(HOT_UPDATE_SUFFIX) && !f.endsWith('.css'));
assert(files.length > 0, 'no files found for remoteEntry chunk');
assert(files.length === 1, `remoteEntry chunk should not have multiple files!, current files: ${files.join(',')}`);
const remoteEntryName = files[0];
return remoteEntryName;
};
const globalName = this._containerManager.globalEntryName;
assert(globalName, 'Can not get library.name, please ensure you have set library.name and the type is "string" !');
assert(this._pluginVersion, 'Can not get pluginVersion, please ensure you have set pluginVersion !');
const metaData = {
name: name,
type,
buildInfo,
remoteEntry: {
name: getRemoteEntryName(),
path: '',
// same as the types supported by runtime, currently only global/var/script is supported
type: ((_this__options = this._options) === null || _this__options === void 0 ? void 0 : (_this__options_library = _this__options.library) === null || _this__options_library === void 0 ? void 0 : _this__options_library.type) || 'global'
},
types: getTypesMetaInfo(this._options, compiler.context),
globalName: globalName,
pluginVersion: this._pluginVersion
};
return this.setMetaDataPublicPath(metaData, compiler);
}
_getFilteredModules(stats) {
const filteredModules = stats.modules.filter((module)=>{
if (!module || !module.name) {
return false;
}
const array = [
module.name.includes('container entry'),
module.name.includes('remote '),
module.name.includes('shared module '),
module.name.includes('provide module ')
];
return array.some((item)=>item);
});
return filteredModules;
}
_getModuleAssets(compilation, entryPointNames) {
const { chunks } = compilation;
const { exposeFileNameImportMap } = this._containerManager;
const assets = {};
chunks.forEach((chunk)=>{
if (typeof chunk.name !== 'string') return;
// Support split chunks caused by splitChunks.maxSize:
// A chunk named "__federation_expose_Foo" may be split into
// "__federation_expose_Foo-<hash>" chunks, so we match both exact
// and prefix+dash patterns.
const matchedKey = exposeFileNameImportMap[chunk.name] !== undefined ? chunk.name : Object.keys(exposeFileNameImportMap).find((key)=>chunk.name.startsWith(key + '-'));
if (!matchedKey) return;
// TODO: support multiple import
const exposeKey = exposeFileNameImportMap[matchedKey][0];
const assetKey = getFileNameWithOutExt(exposeKey);
const chunkAssets = getAssetsByChunk(chunk, entryPointNames);
if (!assets[assetKey]) {
assets[assetKey] = chunkAssets;
} else {
// Merge split chunk assets, deduplicating with Set
assets[assetKey] = {
js: {
sync: [
...new Set([
...assets[assetKey].js.sync,
...chunkAssets.js.sync
])
],
async: [
...new Set([
...assets[assetKey].js.async,
...chunkAssets.js.async
])
]
},
css: {
sync: [
...new Set([
...assets[assetKey].css.sync,
...chunkAssets.css.sync
])
],
async: [
...new Set([
...assets[assetKey].css.async,
...chunkAssets.css.async
])
]
}
};
}
});
return assets;
}
_getProvideSharedAssets(compilation, stats, entryPointNames) {
const sharedModules = stats.modules.filter((module)=>{
if (!module || !module.name) {
return false;
}
const array = [
module.name.includes('consume shared module ')
];
return array.some((item)=>item);
});
const manifestOverrideChunkIDMap = {};
const effectiveSharedModules = getSharedModules(stats, sharedModules);
effectiveSharedModules.forEach((item)=>{
const [sharedModuleName, sharedModule] = item;
if (!manifestOverrideChunkIDMap[sharedModuleName]) {
manifestOverrideChunkIDMap[sharedModuleName] = {
async: new Set(),
sync: new Set()
};
}
sharedModule.chunks.forEach((chunkID)=>{
const chunk = findChunk(chunkID, compilation.chunks);
manifestOverrideChunkIDMap[sharedModuleName].sync.add(chunkID);
if (!chunk) {
return;
}
[
...chunk.groupsIterable
].forEach((group)=>{
if (group.name && !entryPointNames.includes(group.name)) {
manifestOverrideChunkIDMap[sharedModuleName].sync.add(group.id);
}
});
});
});
const assets = {
js: {
async: [],
sync: []
},
css: {
async: [],
sync: []
}
};
Object.keys(manifestOverrideChunkIDMap).forEach((override)=>{
const asyncAssets = getAssetsByChunkIDs(compilation, {
[override]: manifestOverrideChunkIDMap[override].async
});
const syncAssets = getAssetsByChunkIDs(compilation, {
[override]: manifestOverrideChunkIDMap[override].sync
});
assets[override] = {
js: {
async: asyncAssets[override].js,
sync: syncAssets[override].js
},
css: {
async: asyncAssets[override].css,
sync: syncAssets[override].css
}
};
});
return assets;
}
async _generateStats(compiler, compilation, extraOptions) {
try {
const { name, manifest: manifestOptions = {}, exposes = {} } = this._options;
const metaData = this._getMetaData(compiler, compilation, extraOptions);
const stats = {
id: name,
name: name,
metaData,
shared: [],
remotes: [],
exposes: []
};
if (typeof manifestOptions === 'object' && manifestOptions.disableAssetsAnalyze) {
const remotes = this._remoteManager.statsRemoteWithEmptyUsedIn;
stats.remotes = remotes;
stats.exposes = Object.keys(exposes).map((exposeKey)=>{
return getExposeItem({
exposeKey,
name: name,
file: {
import: exposes[exposeKey].import
}
});
});
stats.shared = Object.entries(this._sharedManager.normalizedOptions).reduce((sum, cur)=>{
const [pkgName, normalizedShareOptions] = cur;
sum.push(getShareItem({
pkgName,
normalizedShareOptions,
pkgVersion: normalizedShareOptions.version || UNKNOWN_MODULE_NAME,
hostName: name
}));
return sum;
}, []);
return stats;
}
const liveStats = compilation.getStats();
const statsOptions = {
all: false,
modules: true,
builtAt: true,
hash: true,
ids: true,
version: true,
entrypoints: true,
assets: false,
chunks: false,
reasons: true
};
if (this._bundler === 'webpack') {
statsOptions['cached'] = true;
}
statsOptions['cachedModules'] = true;
const webpackStats = liveStats.toJson(statsOptions);
const filteredModules = this._getFilteredModules(webpackStats);
const moduleHandler = new ModuleHandler(this._options, filteredModules, {
bundler: this._bundler
});
const { remotes, exposesMap, sharedMap } = moduleHandler.collect();
const entryPointNames = [
...compilation.entrypoints.values()
].map((e)=>e.name).filter((v)=>!!v);
await Promise.all([
new Promise((resolve)=>{
const sharedAssets = this._getProvideSharedAssets(compilation, webpackStats, entryPointNames);
Object.keys(sharedMap).forEach((sharedKey)=>{
const assets = sharedAssets[sharedKey];
if (assets) {
sharedMap[sharedKey].assets = assets;
}
});
resolve();
}),
new Promise((resolve)=>{
const moduleAssets = this._getModuleAssets(compilation, entryPointNames);
Object.keys(exposesMap).forEach((exposeKey)=>{
const assets = moduleAssets[exposeKey];
if (assets) {
exposesMap[exposeKey].assets = assets;
}
exposesMap[exposeKey].requires = Array.from(new Set(exposesMap[exposeKey].requires));
});
resolve();
})
]);
await Promise.all([
new Promise((resolve)=>{
const remoteMemo = new Set();
stats.remotes = remotes.map((remote)=>{
remoteMemo.add(remote.federationContainerName);
return {
...remote,
usedIn: Array.from(remote.usedIn.values())
};
});
const statsRemoteWithEmptyUsedIn = this._remoteManager.statsRemoteWithEmptyUsedIn;
statsRemoteWithEmptyUsedIn.forEach((remoteInfo)=>{
if (!remoteMemo.has(remoteInfo.federationContainerName)) {
stats.remotes.push(remoteInfo);
}
});
resolve();
}),
new Promise((resolve)=>{
stats.shared = Object.values(sharedMap).map((shared)=>({
...shared,
usedIn: Array.from(shared.usedIn)
}));
resolve();
})
]);
await new Promise((resolve)=>{
const sharedAssets = stats.shared.reduce((sum, shared)=>{
const { js, css } = shared.assets;
[
...js.sync,
...js.async,
...css.async,
css.sync
].forEach((asset)=>{
sum.add(asset);
});
return sum;
}, new Set());
const { fileExposeKeyMap } = this._containerManager;
stats.exposes = [];
Object.entries(fileExposeKeyMap).forEach(([exposeFileWithoutExt, exposeKeySet])=>{
const expose = exposesMap[exposeFileWithoutExt] || {
assets: {
js: {
sync: [],
async: []
},
css: {
sync: [],
async: []
}
}
};
exposeKeySet.forEach((exposeKey)=>{
const { js, css } = expose.assets;
const exposeModuleName = getExposeName(exposeKey);
stats.exposes.push({
...expose,
path: exposeKey,
id: composeKeyWithSeparator(this._options.name, exposeModuleName),
name: exposeModuleName,
assets: {
js: {
sync: js.sync.filter((asset)=>!sharedAssets.has(asset)),
async: js.async.filter((asset)=>!sharedAssets.has(asset))
},
css: {
sync: css.sync.filter((asset)=>!sharedAssets.has(asset)),
async: css.async.filter((asset)=>!sharedAssets.has(asset))
}
}
});
});
});
Object.values(exposesMap).map((expose)=>{
const { js, css } = expose.assets;
return {
...expose,
assets: {
js: {
sync: js.sync.filter((asset)=>!sharedAssets.has(asset)),
async: js.async.filter((asset)=>!sharedAssets.has(asset))
},
css: {
sync: css.sync.filter((asset)=>!sharedAssets.has(asset)),
async: css.async.filter((asset)=>!sharedAssets.has(asset))
}
}
};
});
resolve();
});
return stats;
} catch (err) {
throw err;
}
}
getPublicPath(compiler) {
if (this._publicPath) {
return this._publicPath;
}
const { output: { publicPath: originalPublicPath } } = compiler.options;
let publicPath = originalPublicPath;
this._publicPath = publicPath;
return publicPath;
}
init(options, { pluginVersion, bundler }) {
this._options = options;
this._pluginVersion = pluginVersion;
this._bundler = bundler;
this._containerManager = new ContainerManager();
this._containerManager.init(options);
this._remoteManager = new RemoteManager();
this._remoteManager.init(options);
this._sharedManager = new SharedManager();
this._sharedManager.init(options);
}
updateStats(stats, compiler) {
const { metaData } = stats;
if (!metaData.types) {
metaData.types = getTypesMetaInfo(this._options, compiler.context);
}
if (!metaData.pluginVersion) {
metaData.pluginVersion = this._pluginVersion;
}
this.setMetaDataPublicPath(metaData, compiler);
return stats;
}
async generateStats(compiler, compilation) {
try {
const stats = await this._generateStats(compiler, compilation);
return stats;
} catch (err) {
throw err;
}
}
validate(compiler) {
const { output: { publicPath } } = compiler.options;
if (typeof publicPath !== 'string') {
logger.warn(`Manifest will not generate, because publicPath can only be string, but got '${publicPath}'`);
return false;
} else if (publicPath === 'auto') {
logger.warn(`Manifest will use absolute path resolution via its host at runtime, reason: publicPath='${publicPath}'`);
return true;
}
return true;
}
constructor(){
this._options = {};
this._bundler = 'webpack';
this._containerManager = new ContainerManager();
this._remoteManager = new RemoteManager();
this._sharedManager = new SharedManager();
this._pkgJsonManager = new PKGJsonManager();
}
}
export { StatsManager };