@module-federation/manifest
Version:
Provide manifest/stats for webpack/rspack MF project .
1,225 lines (1,215 loc) • 47.3 kB
JavaScript
'use strict';
var sdk = require('@module-federation/sdk');
var chalk = require('chalk');
var path = require('path');
var core = require('@module-federation/dts-plugin/core');
var fs = require('fs');
var managers = require('@module-federation/managers');
const PLUGIN_IDENTIFIER = 'Module Federation Manifest Plugin';
const HOT_UPDATE_SUFFIX = '.hot-update';
const createBundlerLogger = typeof sdk.createInfrastructureLogger === 'function'
? sdk.createInfrastructureLogger
: sdk.createLogger;
const logger = createBundlerLogger(chalk.cyan(`[ ${PLUGIN_IDENTIFIER} ]`));
function isHotFile(file) {
return file.includes(HOT_UPDATE_SUFFIX);
}
const collectAssets = (assets, jsTargetSet, cssTargetSet) => {
assets.forEach((file) => {
if (file.endsWith('.css')) {
cssTargetSet.add(file);
}
else {
if (isDev()) {
if (!isHotFile(file)) {
jsTargetSet.add(file);
}
}
else {
jsTargetSet.add(file);
}
}
});
};
function getSharedModuleName(name) {
const [_type, _shared, _module, _shareScope, sharedInfo] = name.split(' ');
return sharedInfo.split('@').slice(0, -1).join('@');
}
function getAssetsByChunkIDs(compilation, chunkIDMap) {
const arrayChunks = Array.from(compilation.chunks);
const assetMap = {};
Object.keys(chunkIDMap).forEach((key) => {
const chunkIDs = Array.from(chunkIDMap[key]);
if (!assetMap[key]) {
assetMap[key] = {
css: new Set(),
js: new Set(),
};
}
chunkIDs.forEach((chunkID) => {
const chunk = arrayChunks.find((item) => item.id === chunkID);
if (chunk) {
collectAssets([...chunk.files], assetMap[key].js, assetMap[key].css);
}
});
});
const assets = {};
Object.keys(assetMap).map((key) => {
assets[key] = {
js: Array.from(assetMap[key].js),
css: Array.from(assetMap[key].css),
};
});
return assets;
}
function findChunk(id, chunks) {
for (const chunk of chunks) {
if (id === chunk.id) {
return chunk;
}
}
}
function getSharedModules(stats, sharedModules) {
// 获取入口文件就是实际内容的 module
const entryContentModuleNames = [];
let effectiveSharedModules = stats.modules?.reduce((sum, module) => {
for (const sharedModule of sharedModules) {
if (sharedModule.name === module.issuerName) {
entryContentModuleNames.push(sharedModule.name);
sum.push([getSharedModuleName(module.issuerName), module]);
return sum;
}
}
return sum;
}, []) || [];
// 获取入口文件仅作为 Re Export 的 module
const entryReExportModules = sharedModules.filter((sharedModule) => !entryContentModuleNames.includes(sharedModule.name));
if (entryReExportModules.length) {
effectiveSharedModules = effectiveSharedModules.concat(stats.modules.reduce((sum, module) => {
let flag = false;
for (const entryReExportModule of entryReExportModules) {
if (flag) {
break;
}
if (module.reasons) {
for (const issueModule of module.reasons) {
if (issueModule.moduleName === entryReExportModule.name) {
sum.push([
getSharedModuleName(entryReExportModule.name),
module,
]);
flag = true;
break;
}
}
}
}
return sum;
}, []));
}
return effectiveSharedModules;
}
function getAssetsByChunk(chunk, entryPointNames) {
const assesSet = {
js: {
sync: new Set(),
async: new Set(),
},
css: {
sync: new Set(),
async: new Set(),
},
};
const collectChunkFiles = (targetChunk, type) => {
[...targetChunk.groupsIterable].forEach((chunkGroup) => {
if (chunkGroup.name && !entryPointNames.includes(chunkGroup.name)) {
collectAssets(chunkGroup.getFiles(), assesSet.js[type], assesSet.css[type]);
}
});
};
collectChunkFiles(chunk, 'sync');
[...chunk.getAllAsyncChunks()].forEach((asyncChunk) => {
collectAssets([...asyncChunk.files], assesSet.js['async'], assesSet.css['async']);
collectChunkFiles(asyncChunk, 'async');
});
const assets = {
js: {
sync: Array.from(assesSet.js.sync),
async: Array.from(assesSet.js.async),
},
css: {
sync: Array.from(assesSet.css.sync),
async: Array.from(assesSet.css.async),
},
};
return assets;
}
function assert(condition, msg) {
if (!condition) {
error(msg);
}
}
function error(msg) {
throw new Error(`[ ${PLUGIN_IDENTIFIER} ]: ${msg}`);
}
function isDev() {
return process.env['NODE_ENV'] === 'development';
}
function getFileNameWithOutExt(str) {
return str.replace(path.extname(str), '');
}
function getFileName(manifestOptions) {
if (!manifestOptions) {
return {
statsFileName: sdk.StatsFileName,
manifestFileName: sdk.ManifestFileName,
};
}
let filePath = typeof manifestOptions === 'boolean' ? '' : manifestOptions.filePath || '';
let fileName = typeof manifestOptions === 'boolean' ? '' : manifestOptions.fileName || '';
const JSON_EXT = '.json';
const addExt = (name) => {
if (name.endsWith(JSON_EXT)) {
return name;
}
return `${name}${JSON_EXT}`;
};
const insertSuffix = (name, suffix) => {
return name.replace(JSON_EXT, `${suffix}${JSON_EXT}`);
};
const manifestFileName = fileName ? addExt(fileName) : sdk.ManifestFileName;
const statsFileName = fileName
? insertSuffix(manifestFileName, '-stats')
: sdk.StatsFileName;
return {
statsFileName: sdk.simpleJoinRemoteEntry(filePath, statsFileName),
manifestFileName: sdk.simpleJoinRemoteEntry(filePath, manifestFileName),
};
}
function getTypesMetaInfo(pluginOptions, context) {
const defaultRemoteOptions = {
generateAPITypes: true,
compileInChildProcess: true,
};
const defaultTypesMetaInfo = {
path: '',
name: '',
zip: '',
api: '',
};
try {
const normalizedDtsOptions = sdk.normalizeOptions(core.isTSProject(pluginOptions.dts, context), {
generateTypes: defaultRemoteOptions,
consumeTypes: {},
}, 'mfOptions.dts')(pluginOptions.dts);
if (normalizedDtsOptions === false) {
return defaultTypesMetaInfo;
}
const normalizedRemote = sdk.normalizeOptions(true, defaultRemoteOptions, 'mfOptions.dts.generateTypes')(normalizedDtsOptions.generateTypes);
if (normalizedRemote === false) {
return defaultTypesMetaInfo;
}
const { apiFileName, zipName, zipPrefix } = core.retrieveTypesAssetsInfo({
...normalizedRemote,
context,
moduleFederationConfig: pluginOptions,
});
const zip = path.join(zipPrefix, zipName);
const api = path.join(zipPrefix, apiFileName);
return {
path: '',
name: '',
zip,
api,
};
}
catch (err) {
logger.warn(`getTypesMetaInfo failed, it will use the default types meta info, and the errors as belows: ${err}`);
return defaultTypesMetaInfo;
}
}
class ManifestManager {
constructor() {
this._options = {};
}
get manifest() {
return this._manifest;
}
init(options) {
this._options = options;
}
get fileName() {
return getFileName(this._options.manifest).manifestFileName;
}
async generateManifest(options, extraOptions = {}) {
const { compilation, publicPath, stats, compiler, bundler, additionalData, } = options;
const { disableEmit } = extraOptions;
// Initialize manifest with required properties from stats
const { id, name, metaData } = stats;
const manifest = {
id,
name,
metaData,
shared: [],
remotes: [],
exposes: [],
};
manifest.exposes = stats.exposes.reduce((sum, cur) => {
const expose = {
id: cur.id,
name: cur.name,
assets: cur.assets,
path: cur.path,
};
sum.push(expose);
return sum;
}, []);
manifest.shared = stats.shared.reduce((sum, cur) => {
const shared = {
id: cur.id,
name: cur.name,
version: cur.version,
singleton: cur.singleton,
requiredVersion: cur.requiredVersion,
hash: cur.hash,
assets: cur.assets,
};
sum.push(shared);
return sum;
}, []);
manifest.remotes = stats.remotes.reduce((sum, cur) => {
// @ts-ignore version/entry will be added as follow
const remote = {
federationContainerName: cur.federationContainerName,
moduleName: cur.moduleName,
alias: cur.alias,
};
if ('entry' in cur) {
// @ts-ignore
remote.entry = cur.entry;
}
else if ('version' in cur) {
// @ts-ignore
remote.entry = cur.version;
}
sum.push(remote);
return sum;
}, []);
this._manifest = manifest;
const manifestFileName = this.fileName;
if (additionalData) {
const ret = await additionalData({
manifest: this._manifest,
stats,
pluginOptions: this._options,
compiler,
compilation,
bundler,
});
this._manifest = ret || this._manifest;
}
if (!disableEmit) {
compilation.emitAsset(manifestFileName, new compiler.webpack.sources.RawSource(JSON.stringify(this._manifest, null, 2)));
}
if (isDev() &&
(process.env['MF_SSR_PRJ']
? compiler.options.target !== 'async-node'
: true)) {
logger.info(`Manifest Link: ${chalk.cyan(`${publicPath === 'auto' ? '{auto}/' : publicPath}${manifestFileName}`)} `);
}
return {
manifest: this._manifest,
filename: manifestFileName,
};
}
}
const isNonEmptyString = (value) => {
return typeof value === 'string' && value.trim().length > 0;
};
const normalizeExposeValue = (exposeValue) => {
if (!exposeValue) {
return undefined;
}
const toImportArray = (value) => {
if (isNonEmptyString(value)) {
return [value];
}
if (Array.isArray(value)) {
const normalized = value.filter(isNonEmptyString);
return normalized.length ? normalized : undefined;
}
return undefined;
};
if (typeof exposeValue === 'object') {
if ('import' in exposeValue) {
const { import: rawImport, name } = exposeValue;
const normalizedImport = toImportArray(rawImport);
if (!normalizedImport?.length) {
return undefined;
}
return {
import: normalizedImport,
...(isNonEmptyString(name) ? { name } : {}),
};
}
return undefined;
}
const normalizedImport = toImportArray(exposeValue);
if (!normalizedImport?.length) {
return undefined;
}
return { import: normalizedImport };
};
const parseContainerExposeEntries = (identifier) => {
const startIndex = identifier.indexOf('[');
if (startIndex < 0) {
return undefined;
}
let depth = 0;
let inString = false;
let isEscaped = false;
for (let cursor = startIndex; cursor < identifier.length; cursor++) {
const char = identifier[cursor];
if (isEscaped) {
isEscaped = false;
continue;
}
if (char === '\\') {
isEscaped = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (inString) {
continue;
}
if (char === '[') {
depth++;
}
else if (char === ']') {
depth--;
if (depth === 0) {
const serialized = identifier.slice(startIndex, cursor + 1);
try {
return JSON.parse(serialized);
}
catch {
return undefined;
}
}
}
}
return undefined;
};
const getExposeName = (exposeKey) => {
return exposeKey.replace('./', '');
};
function getExposeItem({ exposeKey, name, file, }) {
const exposeModuleName = getExposeName(exposeKey);
return {
path: exposeKey,
id: sdk.composeKeyWithSeparator(name, exposeModuleName),
name: exposeModuleName,
// @ts-ignore to deduplicate
requires: [],
file: path.relative(process.cwd(), file.import[0]),
assets: {
js: {
async: [],
sync: [],
},
css: {
async: [],
sync: [],
},
},
};
}
class ModuleHandler {
constructor(options, modules, { bundler }) {
this._bundler = 'webpack';
this._remoteManager = new managers.RemoteManager();
this._sharedManager = new managers.SharedManager();
this._options = options;
this._modules = modules;
this._bundler = bundler;
this._containerManager = new managers.ContainerManager();
this._containerManager.init(options);
this._remoteManager = new managers.RemoteManager();
this._remoteManager.init(options);
this._sharedManager = new managers.SharedManager();
this._sharedManager.init(options);
}
get isRspack() {
return this._bundler === 'rspack';
}
_handleSharedModule(mod, sharedMap, exposesMap) {
const { identifier, moduleType } = mod;
if (!identifier) {
return;
}
const sharedManagerNormalizedOptions = this._sharedManager.normalizedOptions;
const initShared = (pkgName, pkgVersion) => {
if (sharedMap[pkgName]) {
return;
}
sharedMap[pkgName] = {
...sharedManagerNormalizedOptions[pkgName],
id: `${this._options.name}:${pkgName}`,
requiredVersion: sharedManagerNormalizedOptions[pkgName]?.requiredVersion ||
`^${pkgVersion}`,
name: pkgName,
version: pkgVersion,
assets: {
js: {
async: [],
sync: [],
},
css: {
async: [],
sync: [],
},
},
// @ts-ignore to deduplicate
usedIn: new Set(),
};
};
const collectRelationshipMap = (mod, pkgName) => {
const { issuerName, reasons } = mod;
if (issuerName) {
if (exposesMap[getFileNameWithOutExt(issuerName)]) {
const expose = exposesMap[getFileNameWithOutExt(issuerName)];
// @ts-ignore use Set to deduplicate
expose.requires.push(pkgName);
// @ts-ignore use Set to deduplicate
sharedMap[pkgName].usedIn.add(expose.path);
}
}
if (reasons) {
reasons.forEach(({ resolvedModule, moduleName }) => {
let exposeModName = this.isRspack ? moduleName : resolvedModule;
// filters out entrypoints
if (exposeModName) {
if (exposesMap[getFileNameWithOutExt(exposeModName)]) {
const expose = exposesMap[getFileNameWithOutExt(exposeModName)];
// @ts-ignore to deduplicate
expose.requires.push(pkgName);
// @ts-ignore to deduplicate
sharedMap[pkgName].usedIn.add(expose.path);
}
}
});
}
};
const parseResolvedIdentifier = (nameAndVersion) => {
let name = '';
let version = '';
if (nameAndVersion.startsWith('@')) {
const splitInfo = nameAndVersion.split('@');
splitInfo[0] = '@';
name = splitInfo[0] + splitInfo[1];
version = splitInfo[2];
}
else if (nameAndVersion.includes('@')) {
[name, version] = nameAndVersion.split('@');
version = version.replace(/[\^~>|>=]/g, '');
}
return {
name,
version,
};
};
if (moduleType === 'provide-module') {
// identifier(rspack) = provide shared module (default) react@18.2.0 = /temp/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js
// identifier(webpack) = provide module (default) react@18.2.0 = /temp/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js
const data = identifier.split(' ');
const nameAndVersion = this.isRspack ? data[4] : data[3];
const { name, version } = parseResolvedIdentifier(nameAndVersion);
if (name && version) {
initShared(name, version);
collectRelationshipMap(mod, name);
}
}
if (moduleType === 'consume-shared-module') {
// identifier(rspack) = consume shared module (default) lodash/get@^4.17.21 (strict) (fallback: /temp/node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/get.js)
// identifier(webpack) = consume-shared-module|default|react-dom|!=1.8...2...0|false|/temp/node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js|true|false
const SEPARATOR = this.isRspack ? ' ' : '|';
const data = identifier.split(SEPARATOR);
let pkgName = '';
let pkgVersion = '';
if (this.isRspack) {
const nameAndVersion = data[4];
const res = parseResolvedIdentifier(nameAndVersion);
pkgName = res.name;
pkgVersion = res.version;
}
else {
pkgName = data[2];
const pkgVersionRange = data[3];
pkgVersion = '';
if (pkgVersionRange.startsWith('=')) {
pkgVersion = data[3].replace('=', '');
}
else {
if (sharedManagerNormalizedOptions[pkgName]) {
pkgVersion = sharedManagerNormalizedOptions[pkgName].version;
}
else {
const fullPkgName = pkgName.split('/').slice(0, -1).join('/');
// pkgName: react-dom/
if (sharedManagerNormalizedOptions[`${fullPkgName}/`]) {
if (sharedManagerNormalizedOptions[fullPkgName]) {
pkgVersion =
sharedManagerNormalizedOptions[fullPkgName].version;
}
else {
pkgVersion =
sharedManagerNormalizedOptions[`${fullPkgName}/`].version;
}
}
}
}
}
if (pkgName && pkgVersion) {
initShared(pkgName, pkgVersion);
collectRelationshipMap(mod, pkgName);
}
}
}
_handleRemoteModule(mod, remotes, remotesConsumerMap) {
const { identifier, reasons, nameForCondition } = mod;
if (!identifier) {
return;
}
const remoteManagerNormalizedOptions = this._remoteManager.normalizedOptions;
// identifier = remote (default) webpack/container/reference/app2 ./Button
const data = identifier.split(' ');
if (data.length === 4) {
const moduleName = data[3].replace('./', '');
const remoteAlias = data[2].replace('webpack/container/reference/', '');
const normalizedRemote = remoteManagerNormalizedOptions[remoteAlias];
const basicRemote = {
alias: normalizedRemote.alias,
consumingFederationContainerName: this._options.name || '',
federationContainerName: remoteManagerNormalizedOptions[remoteAlias].name,
moduleName,
// @ts-ignore to deduplicate
usedIn: new Set(),
};
if (!nameForCondition) {
return;
}
let remote;
if ('version' in normalizedRemote) {
remote = {
...basicRemote,
version: normalizedRemote.version,
};
}
else {
remote = {
...basicRemote,
entry: normalizedRemote.entry,
};
}
remotes.push(remote);
remotesConsumerMap[nameForCondition] = remote;
}
if (reasons) {
reasons.forEach(({ userRequest, resolvedModule, moduleName }) => {
let exposeModName = this.isRspack ? moduleName : resolvedModule;
if (userRequest && exposeModName && remotesConsumerMap[userRequest]) {
// @ts-ignore to deduplicate
remotesConsumerMap[userRequest].usedIn.add(exposeModName.replace('./', ''));
}
});
}
}
_handleContainerModule(mod, exposesMap) {
const { identifier } = mod;
if (!identifier) {
return;
}
// identifier: container entry (default) [[".",{"import":["./src/routes/page.tsx"],"name":"__federation_expose_default_export"}]]'
const entries = parseContainerExposeEntries(identifier) ??
this._getContainerExposeEntriesFromOptions();
if (!entries) {
return;
}
entries.forEach(([prefixedName, file]) => {
// TODO: support multiple import
exposesMap[getFileNameWithOutExt(file.import[0])] = getExposeItem({
exposeKey: prefixedName,
name: this._options.name,
file,
});
});
}
_getContainerExposeEntriesFromOptions() {
const exposes = this._containerManager.containerPluginExposesOptions;
const normalizedEntries = Object.entries(exposes).reduce((acc, [exposeKey, exposeOptions]) => {
const normalizedExpose = normalizeExposeValue(exposeOptions);
if (!normalizedExpose?.import.length) {
return acc;
}
acc.push([exposeKey, normalizedExpose]);
return acc;
}, []);
if (normalizedEntries.length) {
return normalizedEntries;
}
const rawExposes = this._options.exposes;
if (!rawExposes || Array.isArray(rawExposes)) {
return undefined;
}
const normalizedFromOptions = Object.entries(rawExposes).reduce((acc, [exposeKey, exposeOptions]) => {
const normalizedExpose = normalizeExposeValue(exposeOptions);
if (!normalizedExpose?.import.length) {
return acc;
}
acc.push([exposeKey, normalizedExpose]);
return acc;
}, []);
return normalizedFromOptions.length ? normalizedFromOptions : undefined;
}
_initializeExposesFromOptions(exposesMap) {
if (!this._options.name || !this._containerManager.enable) {
return;
}
const exposes = this._containerManager.containerPluginExposesOptions;
Object.entries(exposes).forEach(([exposeKey, exposeOptions]) => {
if (!exposeOptions.import?.length) {
return;
}
const [exposeImport] = exposeOptions.import;
if (!exposeImport) {
return;
}
const exposeMapKey = getFileNameWithOutExt(exposeImport);
if (!exposesMap[exposeMapKey]) {
exposesMap[exposeMapKey] = getExposeItem({
exposeKey,
name: this._options.name,
file: exposeOptions,
});
}
});
}
collect() {
const remotes = [];
const remotesConsumerMap = {};
const exposesMap = {};
const sharedMap = {};
this._initializeExposesFromOptions(exposesMap);
const isSharedModule = (moduleType) => {
return Boolean(moduleType &&
['provide-module', 'consume-shared-module'].includes(moduleType));
};
const isContainerModule = (identifier) => {
return identifier.startsWith('container entry');
};
const isRemoteModule = (identifier) => {
return identifier.startsWith('remote ');
};
// handle remote/expose
this._modules.forEach((mod) => {
const { identifier, reasons, nameForCondition, moduleType } = mod;
if (!identifier) {
return;
}
if (isSharedModule(moduleType)) {
this._handleSharedModule(mod, sharedMap, exposesMap);
}
if (isRemoteModule(identifier)) {
this._handleRemoteModule(mod, remotes, remotesConsumerMap);
}
else if (!this._containerManager.enable &&
isContainerModule(identifier)) {
this._handleContainerModule(mod, exposesMap);
}
});
return {
remotes,
exposesMap,
sharedMap,
};
}
}
/* eslint-disable max-lines-per-function */
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable max-depth */
class StatsManager {
constructor() {
this._options = {};
this._bundler = 'webpack';
this._containerManager = new managers.ContainerManager();
this._remoteManager = new managers.RemoteManager();
this._sharedManager = new managers.SharedManager();
this._pkgJsonManager = new managers.PKGJsonManager();
}
getBuildInfo(context) {
const rootPath = context || process.cwd();
const pkg = this._pkgJsonManager.readPKGJson(rootPath);
return {
buildVersion: managers.utils.getBuildVersion(rootPath),
buildName: managers.utils.getBuildName() || pkg['name'],
};
}
get fileName() {
return getFileName(this._options.manifest).statsFileName;
}
_getMetaData(compiler, compilation, extraOptions) {
const { context } = compiler.options;
const { _options: { name }, } = this;
const buildInfo = this.getBuildInfo(context);
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?.library?.type ||
'global',
},
types: getTypesMetaInfo(this._options, compiler.context),
globalName: globalName,
pluginVersion: this._pluginVersion,
};
let prefetchInterface = false;
const prefetchFilePath = path.resolve(compiler.options.context || process.cwd(), `node_modules/.mf/${sdk.encodeName(name)}/${sdk.MFPrefetchCommon.fileName}`);
const existPrefetch = fs.existsSync(prefetchFilePath);
if (existPrefetch) {
const content = fs.readFileSync(prefetchFilePath).toString();
if (content) {
prefetchInterface = true;
}
}
metaData.prefetchInterface = prefetchInterface;
if (this._options.getPublicPath) {
if ('publicPath' in metaData) {
delete metaData.publicPath;
}
return {
...metaData,
getPublicPath: this._options.getPublicPath,
};
}
return {
...metaData,
publicPath: this.getPublicPath(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' &&
exposeFileNameImportMap[chunk.name]) {
// TODO: support multiple import
const exposeKey = exposeFileNameImportMap[chunk.name][0];
assets[getFileNameWithOutExt(exposeKey)] = getAssetsByChunk(chunk, entryPointNames);
}
});
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,
},
});
});
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: sdk.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 managers.ContainerManager();
this._containerManager.init(options);
this._remoteManager = new managers.RemoteManager();
this._remoteManager.init(options);
this._sharedManager = new managers.SharedManager();
this._sharedManager.init(options);
}
async generateStats(compiler, compilation, extraOptions = {}) {
try {
const { disableEmit } = extraOptions;
const existedStats = compilation.getAsset(this.fileName);
if (existedStats && !isDev()) {
return {
stats: JSON.parse(existedStats.source.source().toString()),
filename: this.fileName,
};
}
const { manifest: manifestOptions = {} } = this._options;
let stats = await this._generateStats(compiler, compilation);
if (typeof manifestOptions === 'object' &&
manifestOptions.additionalData) {
const ret = await manifestOptions.additionalData({
stats,
pluginOptions: this._options,
compiler,
compilation,
bundler: this._bundler,
});
stats = ret || stats;
}
if (!disableEmit) {
compilation.emitAsset(this.fileName, new compiler.webpack.sources.RawSource(JSON.stringify(stats, null, 2)));
}
return {
stats,
filename: this.fileName,
};
}
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;
}
}
class StatsPlugin {
constructor(options, { pluginVersion, bundler, }) {
this.name = 'StatsPlugin';
this._options = {};
this._statsManager = new StatsManager();
this._manifestManager = new ManifestManager();
this._enable = true;
this._bundler = 'webpack';
try {
this._options = options;
this._bundler = bundler;
this.disableEmit = Boolean(process.env['MF_DISABLE_EMIT_STATS']);
this._statsManager.init(this._options, { pluginVersion, bundler });
this._manifestManager.init(this._options);
}
catch (err) {
if (err instanceof Error) {
err.message = `[ ${PLUGIN_IDENTIFIER} ]: Manifest will not generate, because: ${err.message}`;
}
logger.error(err);
this._enable = false;
}
}
apply(compiler) {
sdk.bindLoggerToCompiler(logger, compiler, PLUGIN_IDENTIFIER);
if (!this._enable) {
return;
}
const res = this._statsManager.validate(compiler);
if (!res) {
return;
}
compiler.hooks.thisCompilation.tap('generateStats', (compilation) => {
compilation.hooks.processAssets.tapPromise({
name: 'generateStats',
// @ts-ignore use runtime variable in case peer dep not installed
stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER,
}, async () => {
if (this._options.manifest !== false) {
this.statsInfo = await this._statsManager.generateStats(compiler, compilation, {
disableEmit: this.disableEmit,
});
this.manifestInfo = await this._manifestManager.generateManifest({
compilation,
stats: this.statsInfo.stats,
publicPath: this._statsManager.getPublicPath(compiler),
compiler,
bundler: this._bundler,
additionalData: typeof this._options.manifest === 'object'
? this._options.manifest.additionalData
: undefined,
}, {
disableEmit: this.disableEmit,
});
}
});
});
}
get resourceInfo() {
return {
stats: this.statsInfo,
manifest: this.manifestInfo,
};
}
}
exports.ManifestManager = ManifestManager;
exports.StatsManager = StatsManager;
exports.StatsPlugin = StatsPlugin;
//# sourceMappingURL=index.cjs.js.map