UNPKG

@module-federation/manifest

Version:

Provide manifest/stats for webpack/rspack MF project .

478 lines (463 loc) • 20.4 kB
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 };