From cba6f8ea6258d432324b26b2636053d7620341f4 Mon Sep 17 00:00:00 2001 From: step2yeung Date: Tue, 8 Jun 2021 17:20:12 -0700 Subject: [PATCH] Adding versions plugin --- src/lib/actions.ts | 6 +- src/lib/actions/base.ts | 9 ++- src/lib/actions/versions.ts | 21 ++--- src/plugin/common.ts | 28 +++++++ src/plugin/duplicates.ts | 22 +----- src/plugin/index.ts | 1 + src/plugin/versions.ts | 149 ++++++++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 src/plugin/common.ts create mode 100644 src/plugin/versions.ts diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 69264d87..bb3edde8 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -52,10 +52,12 @@ export const actions = ( * @param opts.action {String} name of action * @param opts.format {Object} output format * @param opts.stats {Object} webpack stats object + * @param opts.ignoredPackages {(string | RegExp)[]} packages to ignore + * @param opts.duplicatesOnly {boolean} only report on duplicate * @returns {Promise} Rendered result */ export const render = ( - { action, format, stats, ignoredPackages }: IRenderOptions, + { action, format, stats, ignoredPackages, duplicatesOnly }: IRenderOptions, ): Promise => Promise.resolve() - .then(() => actions(action, { stats, ignoredPackages })) + .then(() => actions(action, { stats, ignoredPackages, duplicatesOnly })) .then((instance: IAction) => instance.template.render(format)); diff --git a/src/lib/actions/base.ts b/src/lib/actions/base.ts index 42adfaad..ce20de3e 100644 --- a/src/lib/actions/base.ts +++ b/src/lib/actions/base.ts @@ -25,6 +25,7 @@ import { sort } from "../util/strings"; export interface IActionConstructor { stats: IWebpackStats; ignoredPackages?: (string | RegExp)[]; + duplicatesOnly?: boolean; } export interface IModulesByAsset { @@ -145,11 +146,13 @@ export abstract class Action { private _assets?: IModulesByAsset; private _template?: ITemplate; private _ignoredPackages: (string | RegExp)[]; + private _duplicatesOnly: boolean; - constructor({ stats, ignoredPackages }: IActionConstructor) { + constructor({ stats, ignoredPackages, duplicatesOnly }: IActionConstructor) { this.stats = stats; this._ignoredPackages = (ignoredPackages || []) .map((pattern) => typeof pattern === "string" ? `${pattern}/` : pattern); + this._duplicatesOnly = duplicatesOnly !== false; } public validate(): Promise { @@ -180,6 +183,10 @@ export abstract class Action { return this._modules = this._modules || this.getSourceMods(this.stats.modules); } + public get duplicatesOnly(): boolean { + return this._duplicatesOnly; + } + // Whether or not we consider the data to indicate we should bail with error. public shouldBail(): Promise { return Promise.resolve(false); diff --git a/src/lib/actions/versions.ts b/src/lib/actions/versions.ts index f0bcbeb6..5be44a6c 100644 --- a/src/lib/actions/versions.ts +++ b/src/lib/actions/versions.ts @@ -214,13 +214,6 @@ const modulesByPackageNameByPackagePath = ( pkgMap[pkgPath] = (pkgMap[pkgPath] || []).concat(mod); }); - // Now, remove any single item keys (no duplicates). - Object.keys(modsMap).forEach((pkgName) => { - if (Object.keys(modsMap[pkgName]).length === 1) { - delete modsMap[pkgName]; - } - }); - return modsMap; }; @@ -272,7 +265,7 @@ interface IVersionsAsset { packages: IVersionsPackages; } -interface IVersionsDataAssets { +export interface IVersionsDataAssets { [asset: string]: IVersionsAsset; } @@ -336,11 +329,21 @@ const getAssetData = ( commonRoot: string, allDeps: (IDependencies | null)[], mods: IModule[], + duplicatesOnly: boolean, ): IVersionsAsset => { // Start assembling and merging in deps for each package root. const data = createEmptyAsset(); const modsMap = modulesByPackageNameByPackagePath(mods); + if (duplicatesOnly) { + // Now, remove any single item keys (no duplicates). + Object.keys(modsMap).forEach((pkgName) => { + if (Object.keys(modsMap[pkgName]).length === 1) { + delete modsMap[pkgName]; + } + }); + } + allDeps.forEach((deps) => { // Skip nulls. if (deps === null) { return; } @@ -476,7 +479,7 @@ class Versions extends Action { // Create root data without meta summary. const assetsData: IVersionsDataAssets = {}; assetNames.forEach((assetName) => { - assetsData[assetName] = getAssetData(commonRoot, allDeps, assets[assetName].mods); + assetsData[assetName] = getAssetData(commonRoot, allDeps, assets[assetName].mods, this.duplicatesOnly); }); const data: IVersionsData = Object.assign(createEmptyData(), { assets: assetsData, diff --git a/src/plugin/common.ts b/src/plugin/common.ts new file mode 100644 index 00000000..8da069e9 --- /dev/null +++ b/src/plugin/common.ts @@ -0,0 +1,28 @@ +import { IWebpackStats } from "../lib/interfaces/webpack-stats"; +import { INpmPackageBase } from "../lib/util/dependencies"; + +// ---------------------------------------------------------------------------- +// Interfaces +// ---------------------------------------------------------------------------- + +export interface ICompiler { + hooks?: any; + plugin?: (name: string, callback: () => void) => void; +} + +export interface ICompilation { + errors: Error[]; + warnings: Error[]; + getStats: () => { + toJson: (opts: object) => IWebpackStats; + }; +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- +// `~/different-foo/~/foo` +export const pkgNamePath = (pkgParts: INpmPackageBase[]) => pkgParts.reduce( + (m, part) => `${m}${m ? " -> " : ""}${part.name}@${part.range}`, + "", +); \ No newline at end of file diff --git a/src/plugin/duplicates.ts b/src/plugin/duplicates.ts index cdc7fe32..fcaaccfd 100644 --- a/src/plugin/duplicates.ts +++ b/src/plugin/duplicates.ts @@ -3,9 +3,8 @@ import semverCompare = require("semver-compare"); import { actions } from "../lib"; import { IDuplicatesData, IDuplicatesFiles } from "../lib/actions/duplicates"; import { _packageName, IVersionsData } from "../lib/actions/versions"; -import { IWebpackStats } from "../lib/interfaces/webpack-stats"; -import { INpmPackageBase } from "../lib/util/dependencies"; import { numF, sort } from "../lib/util/strings"; +import { pkgNamePath, ICompiler, ICompilation } from "./common"; // ---------------------------------------------------------------------------- // Interfaces @@ -15,19 +14,6 @@ import { numF, sort } from "../lib/util/strings"; // See, e.g. https://github.com/TypeStrong/ts-loader/blob/master/src/interfaces.ts // Permissive compiler type that spans webpack v1 - current. -interface ICompiler { - hooks?: any; - plugin?: (name: string, callback: () => void) => void; -} - -export interface ICompilation { - errors: Error[]; - warnings: Error[]; - getStats: () => { - toJson: (opts: object) => IWebpackStats; - }; -} - interface IDuplicatesByFileModule { baseName: string; bytes: number; @@ -70,12 +56,6 @@ const shortPath = (filePath: string, pkgName: string) => { return short; }; -// `duplicates-cjs@1.2.3 -> different-foo@1.1.1 -> foo@3.3.3` -const pkgNamePath = (pkgParts: INpmPackageBase[]) => pkgParts.reduce( - (m, part) => `${m}${m ? " -> " : ""}${part.name}@${part.range}`, - "", -); - // Organize duplicates by package name. const getDuplicatesByFile = (files: IDuplicatesFiles) => { const dupsByFile: IDuplicatesByFile = {}; diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 43746707..c206b4b7 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1 +1,2 @@ export { DuplicatesPlugin } from "./duplicates"; +export { VersionsPlugin } from "./versions"; \ No newline at end of file diff --git a/src/plugin/versions.ts b/src/plugin/versions.ts new file mode 100644 index 00000000..19f648e3 --- /dev/null +++ b/src/plugin/versions.ts @@ -0,0 +1,149 @@ +import * as chalk from "chalk"; +import semverCompare = require("semver-compare"); +import { actions } from "../lib"; +import { _packageName, IVersionsData, IVersionsDataAssets } from "../lib/actions/versions"; +import { numF, sort } from "../lib/util/strings"; +import { pkgNamePath, ICompiler, ICompilation } from "./common"; + +// ---------------------------------------------------------------------------- +// Interfaces +// ---------------------------------------------------------------------------- +interface IVersionsPluginConstructor { + duplicatesOnly?:boolean; + verbose?: boolean; + emitErrors?: boolean; + emitHandler?: (report: string) => {}; + ignoredPackages?: (string | RegExp)[]; +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- +// return version messages of pkgName +const getVersions = (assetName: string, pkgName:string, assets: IVersionsDataAssets): string[] => { + const data: string[] = []; + data.push(chalk`* {cyan ${pkgName}}`); + Object.keys(assets[assetName].packages[pkgName]) + .sort(semverCompare) + .forEach((version) => { + data.push(chalk` * {gray ${version}}`); + Object.keys(assets[assetName].packages[pkgName][version]) + .sort(sort) + .forEach((filePath) => { + const { + skews, + modules, + } = assets[assetName].packages[pkgName][version][filePath]; + + data.push(chalk` * Num deps: ${numF(skews.length)}, files: ${numF(modules.length)}`); + skews.map((pkgParts) => pkgParts.map((part, i) => Object.assign({}, part, { + name: chalk[i < pkgParts.length - 1 ? "gray" : "cyan"](part.name), + }))) + .map(pkgNamePath) + .sort(sort) + .forEach((pkgStr) => data.push(` * ${pkgStr}`)); + }); + }); + return data; +} + +// ---------------------------------------------------------------------------- +// Plugin +// ---------------------------------------------------------------------------- +export class VersionsPlugin { + private opts: IVersionsPluginConstructor; + + constructor({duplicatesOnly, verbose, emitErrors, emitHandler, ignoredPackages}: IVersionsPluginConstructor = {}) { + this.opts = { + emitErrors: emitErrors === true, // default `false` + emitHandler: typeof emitHandler === "function" ? emitHandler : undefined, + ignoredPackages: Array.isArray(ignoredPackages) ? ignoredPackages : undefined, + verbose: verbose === true, // default `false` + duplicatesOnly: duplicatesOnly !== false, // default `true` + }; + } + + public apply(compiler: ICompiler) { + if (compiler.hooks) { + // Webpack4+ integration + compiler.hooks.emit.tapPromise("inspectpack-versions-plugin", this.analyze.bind(this)); + } else if (compiler.plugin) { + // Webpack1-3 integration + compiler.plugin("emit", this.analyze.bind(this) as any); + } else { + throw new Error("Unrecognized compiler format"); + } + } + + public analyze(compilation: ICompilation, callback?: () => void) { + const { errors, warnings } = compilation; + const stats = compilation + .getStats() + .toJson({ + source: true // Needed for webpack5+ + }); + + const { emitErrors, emitHandler, ignoredPackages, duplicatesOnly } = this.opts; + + // Stash messages for output to console (success) or compilation warnings + // or errors arrays on duplicates found. + const msgs: string[] = []; + const dupPkgMsgs: string[] = []; + const singlePkgMsgs: string[] = []; + + return Promise.resolve() + .then(() => actions("versions", { stats, ignoredPackages, duplicatesOnly })) + .then((a) => a.getData() as Promise) + .then((data) => { + const { assets } = data; + Object.keys(assets) + .filter((assetName) => Object.keys(assets[assetName].packages).length) + .forEach((assetName) => { + // For each asset, report duplicated packages and single version packages seperately + const dupPkgForAsset: string[] = []; + const singlePkgForAsset: string[] = []; + + Object.keys(assets[assetName].packages) + .sort(sort) + .forEach((pkgName) => { + if (Object.keys(assets[assetName].packages[pkgName]).length > 1) { + dupPkgForAsset.push(...getVersions(assetName, pkgName, assets)); + } else { + singlePkgForAsset.push(...getVersions(assetName, pkgName, assets)); + } + }); + + if (dupPkgForAsset.length > 1) { + dupPkgMsgs.push(chalk `{gray ## \`${assetName}\`}`, ...dupPkgForAsset, ''); + } + if (singlePkgForAsset.length > 1) { + singlePkgMsgs.push(chalk `{gray ## \`${assetName}\`}`, ...singlePkgForAsset, ''); + } + }); + + msgs.push(chalk`{bold.underline Versions info} - {bold.yellow ${duplicatesOnly ? 'Duplicates Only': 'All Packages'}}\n`); + msgs.push(chalk`{underline Single version packages}`); + (singlePkgMsgs.length > 1) ? msgs.push(...singlePkgMsgs) : msgs.push(chalk` {red n/a}\n`); + msgs.push(chalk`{underline Duplicate version packages}`); + (dupPkgMsgs.length > 1) ? msgs.push(...dupPkgMsgs) : msgs.push(chalk` {green n/a}\n`); + + // 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; + }); + } +} \ No newline at end of file