inspectpack
Version:
An inspection tool for Webpack frontend JavaScript bundles.
206 lines (203 loc) • 8.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.create = void 0;
const chalk = require("chalk");
const modules_1 = require("../interfaces/modules");
const strings_1 = require("../util/strings");
const base_1 = require("./base");
/**
* Create map of `basename` -> `source` -> `IModule`.
*
* @param mods {IModule[]} array of module objects.
* @returns {IModulesByBaseNameBySource} map
*/
const modulesByBaseNameBySource = (mods) => {
// Mutable, empty object to group base names with.
const modsMap = {};
// Iterate node_modules modules and add to keyed object.
mods.forEach((mod) => {
if (!mod.isNodeModules) {
return;
}
// First level -- base name
if (mod.baseName === null) { // Programming error.
throw new Error(`Encountered non-node_modules null baseName: ${JSON.stringify(mod)}`);
}
const base = modsMap[mod.baseName] = modsMap[mod.baseName] || {};
// Second level -- source.
// Use token placeholder if synthetic.
const source = mod.isSynthetic ? modules_1.SYNTHETIC_SOURCE_TOKEN : mod.source;
if (source === null) { // Programming error.
throw new Error(`Encountered null source in non-synthetic module: ${JSON.stringify(mod)}`);
}
base[source] = (base[source] || []).concat(mod);
});
// Now, remove any single item keys (no duplicates).
Object.keys(modsMap).forEach((baseName) => {
const keys = Object.keys(modsMap[baseName]);
if (keys.length === 1 && modsMap[baseName][keys[0]].length === 1) {
delete modsMap[baseName];
}
});
return modsMap;
};
// Helper
const createEmptySummary = () => ({
extraFiles: {
num: 0,
},
extraSources: {
bytes: 0,
num: 0,
},
});
class Duplicates extends base_1.Action {
shouldBail() {
return this.getData().then((data) => data.meta.extraFiles.num !== 0);
}
_getData() {
return Promise.resolve()
.then(() => {
const { assets } = this;
const assetNames = Object.keys(assets).sort(strings_1.sort);
// Get asset duplicates
const assetDups = {};
assetNames.forEach((name) => {
const modsMap = modulesByBaseNameBySource(assets[name].mods);
const files = {};
Object.keys(modsMap).forEach((baseName) => {
files[baseName] = {
meta: createEmptySummary(),
sources: Object
.keys(modsMap[baseName])
.sort(strings_1.sort)
.map((source) => ({
meta: createEmptySummary(),
modules: modsMap[baseName][source].map((mod) => ({
baseName: mod.baseName,
fileName: mod.identifier,
size: {
full: mod.size,
},
})),
})),
};
});
assetDups[name] = {
files,
meta: createEmptySummary(),
};
});
// Create real data object.
// Start without any summaries. Just raw object structure.
const data = {
assets: assetDups,
meta: createEmptySummary(),
};
assetNames.forEach((name) => {
const assetDup = data.assets[name];
Object.keys(assetDup.files).forEach((baseName) => {
const { sources, meta } = assetDup.files[baseName];
sources.forEach((sourceGroup) => {
// Then, replace per source group meta
sourceGroup.meta = {
extraFiles: {
num: 1,
},
extraSources: {
bytes: sourceGroup.modules.reduce((bytes, mod) => bytes + mod.size.full, 0),
num: sourceGroup.modules.length,
},
};
// Then, update per file group meta
meta.extraFiles.num += 1;
meta.extraSources.bytes += sourceGroup.meta.extraSources.bytes;
meta.extraSources.num += sourceGroup.meta.extraSources.num;
// Then, update asset meta
assetDup.meta.extraFiles.num += 1;
assetDup.meta.extraSources.bytes += sourceGroup.meta.extraSources.bytes;
assetDup.meta.extraSources.num += sourceGroup.meta.extraSources.num;
// Then, update total meta
data.meta.extraFiles.num += 1;
data.meta.extraSources.bytes += sourceGroup.meta.extraSources.bytes;
data.meta.extraSources.num += sourceGroup.meta.extraSources.num;
});
});
});
return data;
});
}
_createTemplate() {
return new DuplicatesTemplate({ action: this });
}
}
class DuplicatesTemplate extends base_1.Template {
text() {
return Promise.resolve()
.then(() => this.action.getData())
.then(({ meta, assets }) => {
const dupAsset = (name) => chalk `{gray ## \`${name}\`}`;
const dupFiles = (name) => Object.keys(assets[name].files)
.map((baseName) => {
const { files } = assets[name];
const base = files[baseName];
const inlineMeta = (obj) => `Files ${strings_1.numF(obj.extraFiles.num)}, ` +
`Sources ${strings_1.numF(obj.extraSources.num)}, ` +
`Bytes ${strings_1.numF(obj.extraSources.bytes)}`;
const sources = files[baseName].sources
.map((sourceGroup, i) => this.trim(`
${i}. (${inlineMeta(sourceGroup.meta)})
${sourceGroup.modules
.map((mod) => `(${mod.size.full}) ${chalk.gray(mod.fileName)}`)
.join("\n ")}
`, 14))
.join("\n ");
return this.trim(chalk `
* {green ${baseName}}
* Meta: ${inlineMeta(base.meta)}
${sources}
`, 14);
})
.join("\n");
const duplicates = (name) => `${dupAsset(name)}\n${dupFiles(name)}\n`;
const report = this.trim(chalk `
{cyan inspectpack --action=duplicates}
{gray ===============================}
{gray ## Summary}
* Extra Files (unique): ${strings_1.numF(meta.extraFiles.num)}
* Extra Sources (non-unique): ${strings_1.numF(meta.extraSources.num)}
* Extra Bytes (non-unique): ${strings_1.numF(meta.extraSources.bytes)}
${Object.keys(assets)
.filter((name) => Object.keys(assets[name].files).length)
.map(duplicates).join("\n")}
`, 10);
return report;
});
}
tsv() {
return Promise.resolve()
.then(() => this.action.getData())
.then(({ assets }) => ["Asset\tFull Name\tShort Name\tGroup Index\tSize"]
.concat(Object.keys(assets)
.filter((name) => Object.keys(assets[name].files).length)
.map((name) => Object.keys(assets[name].files)
.map((baseName) => assets[name].files[baseName].sources
.map((sourceGroup, i) => sourceGroup.modules
.map((mod) => [
name,
mod.fileName,
mod.baseName,
i,
mod.size.full,
].join("\t"))
.join("\n"))
.join("\n"))
.join("\n"))
.join("\n"))
.join("\n"));
}
}
const create = (opts) => {
return new Duplicates(opts);
};
exports.create = create;