Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Experimental Feature: Add versions plugin #171

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} Rendered result
*/
export const render = (
{ action, format, stats, ignoredPackages }: IRenderOptions,
{ action, format, stats, ignoredPackages, duplicatesOnly }: IRenderOptions,
): Promise<string> => Promise.resolve()
.then(() => actions(action, { stats, ignoredPackages }))
.then(() => actions(action, { stats, ignoredPackages, duplicatesOnly }))
.then((instance: IAction) => instance.template.render(format));
9 changes: 8 additions & 1 deletion src/lib/actions/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { sort } from "../util/strings";
export interface IActionConstructor {
stats: IWebpackStats;
ignoredPackages?: (string | RegExp)[];
duplicatesOnly?: boolean;
}

export interface IModulesByAsset {
Expand Down Expand Up @@ -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<IAction> {
Expand Down Expand Up @@ -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<boolean> {
return Promise.resolve(false);
Expand Down
21 changes: 12 additions & 9 deletions src/lib/actions/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -272,7 +265,7 @@ interface IVersionsAsset {
packages: IVersionsPackages;
}

interface IVersionsDataAssets {
export interface IVersionsDataAssets {
[asset: string]: IVersionsAsset;
}

Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/plugin/common.ts
Original file line number Diff line number Diff line change
@@ -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}`,
"",
);
22 changes: 1 addition & 21 deletions src/plugin/duplicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -70,12 +56,6 @@ const shortPath = (filePath: string, pkgName: string) => {
return short;
};

// `[email protected] -> [email protected] -> [email protected]`
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 = {};
Expand Down
1 change: 1 addition & 0 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { DuplicatesPlugin } from "./duplicates";
export { VersionsPlugin } from "./versions";
149 changes: 149 additions & 0 deletions src/plugin/versions.ts
Original file line number Diff line number Diff line change
@@ -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<IVersionsData>)
.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;
});
}
}