UNPKG

inspectpack

Version:

An inspection tool for Webpack frontend JavaScript bundles.

291 lines (289 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DuplicatesPlugin = exports._getDuplicatesVersionsData = void 0; const chalk = require("chalk"); const semverCompare = require("semver-compare"); const lib_1 = require("../lib"); const versions_1 = require("../lib/actions/versions"); const strings_1 = require("../lib/util/strings"); // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- const identical = (val) => chalk `{bold.magenta ${val}}`; const similar = (val) => chalk `{bold.blue ${val}}`; const warning = (val) => chalk `{bold.yellow ${val}}`; const error = (val) => chalk `{bold.red ${val}}`; // `~/different-foo/~/foo` + highlight last component. const shortPath = (filePath, pkgName) => { let short = filePath.replace(/node_modules/g, "~"); // Color last part of package name. const lastPkgIdx = short.lastIndexOf(pkgName); if (lastPkgIdx > -1) { short = chalk `${short.substring(0, lastPkgIdx)}{cyan ${pkgName}}`; } return short; }; // `duplicates-cjs@1.2.3 -> different-foo@1.1.1 -> foo@3.3.3` const pkgNamePath = (pkgParts) => pkgParts.reduce((m, part) => `${m}${m ? " -> " : ""}${part.name}@${part.range}`, ""); // Organize duplicates by package name. const getDuplicatesByFile = (files) => { const dupsByFile = {}; Object.keys(files).forEach((fileName) => { files[fileName].sources.forEach((source) => { source.modules.forEach((mod) => { dupsByFile[mod.fileName] = { baseName: mod.baseName || mod.fileName, bytes: mod.size.full, isIdentical: source.meta.extraSources.num > 1, }; }); }); }); return dupsByFile; }; // Return object of asset names keyed to sets of package names with duplicates. const getDuplicatesPackageNames = (data) => { const names = {}; Object.keys(data.assets).forEach((assetName) => { // Convert to package names. const pkgNames = Object.keys(data.assets[assetName].files).map(versions_1._packageName); // Unique names. const uniqPkgNames = new Set(pkgNames); names[assetName] = uniqPkgNames; }); return names; }; // Return a new versions object with _only_ duplicates packages included. const _getDuplicatesVersionsData = (dupData, pkgDataOrig, addWarning) => { // Start with a clone of the data. const pkgData = JSON.parse(JSON.stringify(pkgDataOrig)); const assetsToDupPkgs = getDuplicatesPackageNames(dupData); // Iterate the data and mutate meta _and_ resultant entries. Object.keys(pkgData.assets).forEach((assetName) => { const dupPkgs = assetsToDupPkgs[assetName] || new Set(); const { meta, packages } = pkgData.assets[assetName]; Object.keys(packages) // Identify the packages that are not duplicates. .filter((pkgName) => !dupPkgs.has(pkgName)) // Mutate packages and meta. // Basically, unwind exactly everything from `versions.ts`. .forEach((pkgName) => { const pkgVersions = Object.keys(packages[pkgName]); // Unwind stats. meta.packages.num -= 1; meta.resolved.num -= pkgVersions.length; pkgData.meta.packages.num -= 1; pkgData.meta.resolved.num -= pkgVersions.length; pkgVersions.forEach((version) => { const pkgVers = packages[pkgName][version]; Object.keys(pkgVers).forEach((filePath) => { meta.files.num -= pkgVers[filePath].modules.length; meta.depended.num -= pkgVers[filePath].skews.length; meta.installed.num -= 1; pkgData.meta.files.num -= pkgVers[filePath].modules.length; pkgData.meta.depended.num -= pkgVers[filePath].skews.length; pkgData.meta.installed.num -= 1; }); }); // Remove package. delete packages[pkgName]; }); }); // Validate mutated package data by checking we have matching number of // sources (identical or not). const extraSources = dupData.meta.extraSources.num; const foundFilesMap = {}; Object.keys(pkgData.assets).forEach((assetName) => { const pkgs = pkgData.assets[assetName].packages; Object.keys(pkgs).forEach((pkgName) => { Object.keys(pkgs[pkgName]).forEach((pkgVers) => { const pkgInstalls = pkgs[pkgName][pkgVers]; Object.keys(pkgInstalls).forEach((installPath) => { pkgInstalls[installPath].modules.forEach((mod) => { if (!mod.baseName) { return; } foundFilesMap[mod.baseName] = (foundFilesMap[mod.baseName] || 0) + 1; }); }); }); }); }); const foundDupFilesMap = {}; Object.keys(foundFilesMap).forEach((baseName) => { if (foundFilesMap[baseName] >= 2) { foundDupFilesMap[baseName] = foundFilesMap[baseName]; } }); const foundSources = Object.keys(foundDupFilesMap) .reduce((memo, baseName) => { return memo + foundDupFilesMap[baseName]; }, 0); if (extraSources !== foundSources) { addWarning(error(`Missing sources: Expected ${strings_1.numF(extraSources)}, found ${strings_1.numF(foundSources)}.\n` + chalk `{white Found map:} {gray ${JSON.stringify(foundDupFilesMap)}}\n`)); } return pkgData; }; exports._getDuplicatesVersionsData = _getDuplicatesVersionsData; // ---------------------------------------------------------------------------- // Plugin // ---------------------------------------------------------------------------- class DuplicatesPlugin { constructor({ verbose, emitErrors, emitHandler, ignoredPackages } = {}) { this.opts = { emitErrors: emitErrors === true, emitHandler: typeof emitHandler === "function" ? emitHandler : undefined, ignoredPackages: Array.isArray(ignoredPackages) ? ignoredPackages : undefined, verbose: verbose === true, }; } apply(compiler) { if (compiler.hooks) { // Webpack4+ integration compiler.hooks.emit.tapPromise("inspectpack-duplicates-plugin", this.analyze.bind(this)); } else if (compiler.plugin) { // Webpack1-3 integration compiler.plugin("emit", this.analyze.bind(this)); } else { throw new Error("Unrecognized compiler format"); } } analyze(compilation, callback) { const { errors, warnings } = compilation; const stats = compilation .getStats() .toJson({ source: true // Needed for webpack5+ }); const { emitErrors, emitHandler, ignoredPackages, verbose } = this.opts; // Stash messages for output to console (success) or compilation warnings // or errors arrays on duplicates found. const msgs = []; const addMsg = (msg) => msgs.push(msg); return Promise.all([ lib_1.actions("duplicates", { stats, ignoredPackages }).then((a) => a.getData()), lib_1.actions("versions", { stats, ignoredPackages }).then((a) => a.getData()), ]) .then((datas) => { const [dupData, pkgDataOrig] = datas; const header = chalk `{bold.underline Duplicate Sources / Packages}`; // No duplicates. if (dupData.meta.extraFiles.num === 0) { // tslint:disable no-console console.log(chalk `\n${header} - {green No duplicates found. 🚀}\n`); return; } // Filter versions/packages data to _just_ duplicates. const pkgData = exports._getDuplicatesVersionsData(dupData, pkgDataOrig, addMsg); // Choose output format. const fmt = emitErrors ? error : warning; // Have duplicates. Report summary. // tslint:disable max-line-length addMsg(chalk `${header} - ${fmt("Duplicates found! ⚠️")} * {yellow.bold.underline Duplicates}: Found ${strings_1.numF(dupData.meta.extraFiles.num)} ${similar("similar")} files across ${strings_1.numF(dupData.meta.extraSources.num)} code sources (both ${identical("identical")} + similar) accounting for ${strings_1.numF(dupData.meta.extraSources.bytes)} bundled bytes. * {yellow.bold.underline Packages}: Found ${strings_1.numF(pkgData.meta.packages.num)} packages with ${strings_1.numF(pkgData.meta.resolved.num)} {underline resolved}, ${strings_1.numF(pkgData.meta.installed.num)} {underline installed}, and ${strings_1.numF(pkgData.meta.depended.num)} {underline depended} versions. `); // tslint:enable max-line-length Object.keys(pkgData.assets).forEach((dupAssetName) => { const pkgAsset = pkgData.assets[dupAssetName]; let dupsByFile = {}; if (dupData.assets[dupAssetName] && dupData.assets[dupAssetName].files) { dupsByFile = getDuplicatesByFile(dupData.assets[dupAssetName].files); } const { packages } = pkgAsset; const pkgNames = Object.keys(packages); // Only add asset name when duplicates. if (pkgNames.length) { addMsg(chalk `{gray ## ${dupAssetName}}`); } pkgNames.forEach((pkgName) => { // Calculate stats / info during maps. let latestVersion; let numPkgInstalled = 0; const numPkgResolved = Object.keys(packages[pkgName]).length; let numPkgDepended = 0; const versions = Object.keys(packages[pkgName]) .sort(semverCompare) .map((version) => { // Capture latestVersion = version; // Latest should be correct bc of `semverCompare` numPkgInstalled += Object.keys(packages[pkgName][version]).length; let installs = Object.keys(packages[pkgName][version]).map((installed) => { const skews = packages[pkgName][version][installed].skews .map((pkgParts) => pkgParts.map((part, i) => Object.assign({}, part, { name: chalk[i < pkgParts.length - 1 ? "gray" : "cyan"](part.name), }))) .map(pkgNamePath) .sort(strings_1.sort); numPkgDepended += skews.length; if (!verbose) { return chalk ` {green ${version}} {gray ${shortPath(installed, pkgName)}} ${skews.join("\n ")}`; } const duplicates = packages[pkgName][version][installed].modules .map((mod) => dupsByFile[mod.fileName]) .filter(Boolean) .map((mod) => { const note = mod.isIdentical ? identical("I") : similar("S"); return chalk `{gray ${mod.baseName}} (${note}, ${strings_1.numF(mod.bytes)})`; }); return chalk ` {gray ${shortPath(installed, pkgName)}} {white * Dependency graph} ${skews.join("\n ")} {white * Duplicated files in }{gray ${dupAssetName}} ${duplicates.join("\n ")} `; }); if (verbose) { installs = [chalk ` {green ${version}}`].concat(installs); } return installs; }) .reduce((m, a) => m.concat(a), []); // flatten. // tslint:disable-next-line max-line-length addMsg(chalk `{cyan ${pkgName}} (Found ${strings_1.numF(numPkgResolved)} {underline resolved}, ${strings_1.numF(numPkgInstalled)} {underline installed}, ${strings_1.numF(numPkgDepended)} {underline depended}. Latest {green ${latestVersion || "NONE"}}.)`); versions.forEach(addMsg); if (!verbose) { addMsg(""); // extra newline in terse mode. } }); }); // tslint:disable max-line-length addMsg(chalk ` * {gray.bold.underline Understanding the report}: Need help with the details? See: https://github.com/FormidableLabs/inspectpack/#diagnosing-duplicates * {gray.bold.underline Fixing bundle duplicates}: An introductory guide: https://github.com/FormidableLabs/inspectpack/#fixing-bundle-duplicates `.trimLeft()); // tslint:enable max-line-length // Drain messages into custom handler or warnings/errors. const report = msgs.join("\n"); if (emitHandler) { emitHandler(report); } else { const output = emitErrors ? errors : warnings; output.push(new Error(report)); } }) // Handle old plugin API callback. .then(() => { if (callback) { return void callback(); } }) .catch((err) => { // Ignore error from old webpack. if (callback) { return void callback(); } throw err; }); } } exports.DuplicatesPlugin = DuplicatesPlugin;