Skip to content

Instantly share code, notes, and snippets.

@jcreamer898
Created November 18, 2022 18:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcreamer898/9f676dbce22ddccb361258b3f133c045 to your computer and use it in GitHub Desktop.
Save jcreamer898/9f676dbce22ddccb361258b3f133c045 to your computer and use it in GitHub Desktop.
import path from "path";
import findRoot from "find-root";
import chalk from "chalk";
import type { Compiler, Plugin, compilation } from "webpack";
interface PluginOptions {
verbose?: boolean;
showHelp?: boolean;
emitError?: boolean;
exclude?: string[] | undefined | null;
strict?: boolean;
}
interface PackageJson {
name: string;
version: string;
}
const defaults = {
verbose: false,
showHelp: true,
emitError: true,
exclude: null,
strict: true,
};
export class DuplicatePathCheckerWebpackPlugin implements Plugin {
options: PluginOptions;
constructor(options: Partial<PluginOptions> = {}) {
this.options = { ...defaults, ...options };
}
onEmit(compilation: compilation.Compilation, callback: () => void) {
let emitError = this.options.emitError;
let exclude = this.options.exclude;
let context = compilation.compiler.context;
function cleanPathRelativeToContext(modulePath: string) {
let cleanedPath = cleanPath(modulePath);
// Make relative to compilation context
if (cleanedPath.indexOf(context) === 0) {
cleanedPath = "." + cleanedPath.replace(context, "");
}
return cleanedPath;
}
/**
* Keep track of pkg@version to a set of paths
* @type {Map<string, Set<string>>}
*/
let modulePaths = new Map();
/**
* @type {Map<string, string>}
*/
let modulePathIssuer = new Map();
compilation.modules.forEach((module) => {
if (!module.resource) {
return;
}
let pkg;
let packagePath;
let closestPackage = getClosestPackage(module.resource);
// Skip module if no closest package is found
if (!closestPackage) {
return;
}
pkg = closestPackage.package;
packagePath = closestPackage.path;
let modulePath = cleanPathRelativeToContext(packagePath);
if (exclude && exclude.includes(pkg.name)) {
return;
}
const pkgNameVersion = `${pkg.name}@${pkg.version}`;
if (!modulePaths.has(pkgNameVersion)) {
modulePaths.set(pkgNameVersion, new Set([modulePath]));
} else {
const pathsForPkg = modulePaths.get(pkgNameVersion);
pathsForPkg.add(modulePath);
}
if (module.issuer && module.issuer.resource) {
modulePathIssuer.set(
modulePath,
cleanPathRelativeToContext(module.issuer.resource)
);
}
});
let array = emitError ? compilation.errors : compilation.warnings;
let message = ["\n"];
for (const [key, value] of modulePaths.entries()) {
if (value.size > 1) {
message.push(
`${chalk.yellowBright(
`Duplicate Package Path Found for`
)} ${chalk.cyanBright(key)}`
);
for (const path of value) {
message.push(`${chalk.cyanBright("Path")} ${path}`);
if (modulePathIssuer.has(path)) {
message.push(
`${chalk.cyanBright("Issuer")} ${modulePathIssuer.get(path)}`
);
}
}
message.push("");
array.push(message.join("\n"));
}
}
callback();
}
apply(compiler: Compiler) {
compiler.hooks.emit.tapAsync(
"DuplicatePathCheckerWebpackPlugin",
this.onEmit.bind(this)
);
}
}
function cleanPath(path: string) {
return path.split(/[\/\\]node_modules[\/\\]/).join("/~/");
}
// Get closest package definition from path
function getClosestPackage(
modulePath: string
): { package: PackageJson; path: string } | null {
let root;
let pkg;
// Catch findRoot or require errors
try {
root = findRoot(modulePath);
/* eslint-disable security/detect-non-literal-require */
pkg = require(path.join(root, "package.json"));
} catch (e) {
return null;
}
// If the package.json does not have a name property, try again from
// one level higher.
// https://github.com/jsdnxx/find-root/issues/2
// https://github.com/date-fns/date-fns/issues/264#issuecomment-265128399
if (!pkg.name) {
return getClosestPackage(path.resolve(root, ".."));
}
return {
package: pkg,
path: root,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment