UNPKG

@module-federation/manifest

Version:

Provide manifest/stats for webpack/rspack MF project .

447 lines (435 loc) • 17 kB
import { composeKeyWithSeparator } from "@module-federation/sdk"; import path from "path"; import { ContainerManager, RemoteManager, SharedManager } from "@module-federation/managers"; import { getFileNameWithOutExt } from "./utils.mjs"; ;// CONCATENATED MODULE: external "@module-federation/sdk" ;// CONCATENATED MODULE: external "path" ;// CONCATENATED MODULE: external "@module-federation/managers" ;// CONCATENATED MODULE: external "./utils.mjs" ;// CONCATENATED MODULE: ./src/ModuleHandler.ts 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 === null || normalizedImport === void 0 ? void 0 : normalizedImport.length)) { return undefined; } return { import: normalizedImport, ...isNonEmptyString(name) ? { name } : {} }; } return undefined; } const normalizedImport = toImportArray(exposeValue); if (!(normalizedImport === null || normalizedImport === void 0 ? void 0 : 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: 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: [] } } }; } const getShareItem = ({ pkgName, normalizedShareOptions, pkgVersion, hostName })=>{ return { ...normalizedShareOptions, id: `${hostName}:${pkgName}`, requiredVersion: (normalizedShareOptions === null || normalizedShareOptions === void 0 ? void 0 : normalizedShareOptions.requiredVersion) || `^${pkgVersion}`, name: pkgName, version: pkgVersion, assets: { js: { async: [], sync: [] }, css: { async: [], sync: [] } }, // @ts-ignore to deduplicate usedIn: new Set(), usedExports: [], fallback: '' }; }; class ModuleHandler { 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] = getShareItem({ pkgName, pkgVersion, normalizedShareOptions: sharedManagerNormalizedOptions[pkgName], hostName: this._options.name }); }; 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 === null || normalizedExpose === void 0 ? void 0 : 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 === null || normalizedExpose === void 0 ? void 0 : 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])=>{ var _exposeOptions_import; if (!((_exposeOptions_import = exposeOptions.import) === null || _exposeOptions_import === void 0 ? void 0 : _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 (isContainerModule(identifier)) { this._handleContainerModule(mod, exposesMap); } }); return { remotes, exposesMap, sharedMap }; } constructor(options, modules, { bundler }){ this._bundler = 'webpack'; this._remoteManager = new RemoteManager(); this._sharedManager = new SharedManager(); this._options = options; this._modules = modules; 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); } } export { ModuleHandler, getExposeItem, getExposeName, getShareItem };