diff --git a/README.md b/README.md index 15482fb..10741d6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ new DuplicatePackageCheckerPlugin({ showHelp: false, // Warn also if major versions differ (default: true) strict: false, + // Pnpm mode: warn if a package is included from two different paths (default: false) + pnpm: false, /** * Exclude instances of packages from the results. * If all instances of a package are excluded, or all instances except one, @@ -69,6 +71,18 @@ Packages with different major versions introduce backward incompatible changes a It is suggested that strict mode is kept enabled since this improves visibility into your bundle and can help in solving and identifying potential issues. +## Pnpm mode + +Consider this common scenario. `A` depends on both `B` and `C`. Further, `B` depends on `C`. There are two separate packages pulling in `C`. Sometimes the versions of `C` will be different. + +Npm and Yarn arrange their packages in a single folder. When a conflict like this comes up, they pick one instance of `C` to keep, and throw out the rest. Not always easy (or correct) when multiple versions of `C` are in play, but its the way things work and people understand the mechanism. + +Pnpm isolates package dependencies to avoid bugs that come from this npm/yarn design choice. Pnpm keeps both instances of `C` (even if they are the same version). Each instance is scoped to the package which depends on it. **That means each instance has a unique path on the file system.** + +When webpack puts the bundle together, it sees both paths to `C`. It identifies them as different packages and includes them both. Now you have two of everthing, including globals, which is a Bad Thing™. + +Pnpm mode detects this condition and warns you when it happens. Remember, the versions don't matter so much. They may be the same. It's the paths which are important. + ## Resolving duplicate packages in your bundle There are multiple ways you can go about resolving duplicate packages in your bundle, the right solution mostly depends on what tools you're using and on each particular case. diff --git a/src/index.js b/src/index.js index 9066934..3da154f 100644 --- a/src/index.js +++ b/src/index.js @@ -16,8 +16,10 @@ function DuplicatePackageCheckerPlugin(options) { this.options = _.extend({}, defaults, options); } -function cleanPath(path) { - return path.split(/[\/\\]node_modules[\/\\]/).join("/~/"); +function cleanPath(p) { + // normalize paths since they are compared when pnpm mode is active + const normPath = path.normalize(p); + return normPath.split(/[\/\\]node_modules[\/\\]/).join("/~/"); } // Get closest package definition from path @@ -53,12 +55,14 @@ DuplicatePackageCheckerPlugin.prototype.apply = function(compiler) { let emitError = this.options.emitError; let exclude = this.options.exclude; let strict = this.options.strict; + let pnpm = this.options.pnpm; compiler.hooks.emit.tapAsync("DuplicatePackageCheckerPlugin", function( compilation, callback ) { - let context = compilation.compiler.context; + // normalize paths since they are compared when pnpm mode is active + let context = path.normalize(compilation.compiler.context); let modules = {}; function cleanPathRelativeToContext(modulePath) { @@ -97,6 +101,13 @@ DuplicatePackageCheckerPlugin.prototype.apply = function(compiler) { modules[pkg.name] = modules[pkg.name] || []; let isSeen = _.find(modules[pkg.name], module => { + if (pnpm) { + // pnpm isolates dependencies hierarchically. it is possible for + // the same package version to be referenced from two different + // locations. webpack treats these as different packages and + // includes them both. + return module.path === modulePath; + } return module.version === version; }); diff --git a/test/pnpm/__snapshots__/index.test.js.snap b/test/pnpm/__snapshots__/index.test.js.snap new file mode 100644 index 0000000..d3b377d --- /dev/null +++ b/test/pnpm/__snapshots__/index.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pnpm dependency tree should output a warning when package 'a' is duplicated with a different version 1`] = ` +"a + Multiple versions of a found: + 1.0.0 ./~/a + 1.1.0 ./~/c/~/a + +Check how you can resolve duplicate packages:  +https://github.com/darrenscerri/duplicate-package-checker-webpack-plugin#resolving-duplicate-packages-in-your-bundle +" +`; + +exports[`Pnpm dependency tree should output a warning when package 'a' is duplicated with the same version 1`] = ` +"a + Multiple versions of a found: + 1.0.0 ./~/b/~/a + 1.0.0 ./~/a + +Check how you can resolve duplicate packages:  +https://github.com/darrenscerri/duplicate-package-checker-webpack-plugin#resolving-duplicate-packages-in-your-bundle +" +`; diff --git a/test/pnpm/entry.dupe.different-version.js b/test/pnpm/entry.dupe.different-version.js new file mode 100644 index 0000000..50003ee --- /dev/null +++ b/test/pnpm/entry.dupe.different-version.js @@ -0,0 +1,2 @@ +require("a"); +require("c"); diff --git a/test/pnpm/entry.dupe.same-version.js b/test/pnpm/entry.dupe.same-version.js new file mode 100644 index 0000000..a90a425 --- /dev/null +++ b/test/pnpm/entry.dupe.same-version.js @@ -0,0 +1,2 @@ +require("a"); +require("b"); diff --git a/test/pnpm/entry.js b/test/pnpm/entry.js new file mode 100644 index 0000000..6a6cce6 --- /dev/null +++ b/test/pnpm/entry.js @@ -0,0 +1 @@ +require("a"); diff --git a/test/pnpm/index.test.js b/test/pnpm/index.test.js new file mode 100644 index 0000000..1eea5d9 --- /dev/null +++ b/test/pnpm/index.test.js @@ -0,0 +1,31 @@ +var webpack = require("webpack"); +var assert = require("assert"); +var MakeConfig = require("./webpack.config"); + +describe("Pnpm dependency tree", function() { + it("should not output warnings when no duplicates exist", function(done) { + webpack(MakeConfig("entry.js"), function(err, stats) { + assert(stats.compilation.warnings.length === 0); + done(); + }); + }); + + it("should output a warning when package 'a' is duplicated with the same version", function(done) { + webpack(MakeConfig("entry.dupe.same-version.js"), function(err, stats) { + assert(stats.compilation.warnings.length === 1); + expect(stats.compilation.warnings[0].message).toMatchSnapshot(); + done(); + }); + }); + + it("should output a warning when package 'a' is duplicated with a different version", function(done) { + webpack(MakeConfig("entry.dupe.different-version.js"), function( + err, + stats + ) { + assert(stats.compilation.warnings.length === 1); + expect(stats.compilation.warnings[0].message).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/test/pnpm/node_modules/a/index.js b/test/pnpm/node_modules/a/index.js new file mode 100644 index 0000000..e69de29 diff --git a/test/pnpm/node_modules/a/package.json b/test/pnpm/node_modules/a/package.json new file mode 100644 index 0000000..9113c25 --- /dev/null +++ b/test/pnpm/node_modules/a/package.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "1.0.0" +} diff --git a/test/pnpm/node_modules/b/index.js b/test/pnpm/node_modules/b/index.js new file mode 100644 index 0000000..4c6f107 --- /dev/null +++ b/test/pnpm/node_modules/b/index.js @@ -0,0 +1 @@ +require('a'); diff --git a/test/pnpm/node_modules/b/node_modules/a/index.js b/test/pnpm/node_modules/b/node_modules/a/index.js new file mode 100644 index 0000000..e69de29 diff --git a/test/pnpm/node_modules/b/node_modules/a/package.json b/test/pnpm/node_modules/b/node_modules/a/package.json new file mode 100644 index 0000000..9113c25 --- /dev/null +++ b/test/pnpm/node_modules/b/node_modules/a/package.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "1.0.0" +} diff --git a/test/pnpm/node_modules/b/package.json b/test/pnpm/node_modules/b/package.json new file mode 100644 index 0000000..426d23c --- /dev/null +++ b/test/pnpm/node_modules/b/package.json @@ -0,0 +1,7 @@ +{ + "name": "b", + "version": "1.0.0", + "dependencies": { + "a" : "~1.0.0" + } +} diff --git a/test/pnpm/node_modules/c/index.js b/test/pnpm/node_modules/c/index.js new file mode 100644 index 0000000..4c6f107 --- /dev/null +++ b/test/pnpm/node_modules/c/index.js @@ -0,0 +1 @@ +require('a'); diff --git a/test/pnpm/node_modules/c/node_modules/a/index.js b/test/pnpm/node_modules/c/node_modules/a/index.js new file mode 100644 index 0000000..e69de29 diff --git a/test/pnpm/node_modules/c/node_modules/a/package.json b/test/pnpm/node_modules/c/node_modules/a/package.json new file mode 100644 index 0000000..7746231 --- /dev/null +++ b/test/pnpm/node_modules/c/node_modules/a/package.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "1.1.0" +} diff --git a/test/pnpm/node_modules/c/package.json b/test/pnpm/node_modules/c/package.json new file mode 100644 index 0000000..519d04e --- /dev/null +++ b/test/pnpm/node_modules/c/package.json @@ -0,0 +1,7 @@ +{ + "name": "c", + "version": "1.0.0", + "dependencies": { + "a" : "~1.0.0" + } +} diff --git a/test/pnpm/package.json b/test/pnpm/package.json new file mode 100644 index 0000000..d032fc4 --- /dev/null +++ b/test/pnpm/package.json @@ -0,0 +1,9 @@ +{ + "name": "test", + "version": "1.0.0", + "dependencies": { + "a": "*", + "b": "*", + "c": "*" + } +} diff --git a/test/pnpm/webpack.config.js b/test/pnpm/webpack.config.js new file mode 100644 index 0000000..34408b9 --- /dev/null +++ b/test/pnpm/webpack.config.js @@ -0,0 +1,15 @@ +const path = require("path"); +var DuplicatePackageCheckerPlugin = require("../../src"); + +module.exports = function(entryFile) { + return { + entry: "./" + entryFile, + mode: "development", + context: __dirname, + output: { + path: path.resolve(__dirname, "dist"), + filename: entryFile + "-bundle.js" + }, + plugins: [new DuplicatePackageCheckerPlugin({ pnpm: true })] + }; +};