inspectpack
Version:
An inspection tool for Webpack frontend JavaScript bundles.
291 lines (289 loc) • 14.1 kB
JavaScript
;
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;