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

Add support for pnpm #24

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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down
17 changes: 14 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
});

Expand Down
23 changes: 23 additions & 0 deletions test/pnpm/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -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
"
`;
2 changes: 2 additions & 0 deletions test/pnpm/entry.dupe.different-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require("a");
require("c");
2 changes: 2 additions & 0 deletions test/pnpm/entry.dupe.same-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require("a");
require("b");
1 change: 1 addition & 0 deletions test/pnpm/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("a");
31 changes: 31 additions & 0 deletions test/pnpm/index.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Empty file.
4 changes: 4 additions & 0 deletions test/pnpm/node_modules/a/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/pnpm/node_modules/b/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
4 changes: 4 additions & 0 deletions test/pnpm/node_modules/b/node_modules/a/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions test/pnpm/node_modules/b/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/pnpm/node_modules/c/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
4 changes: 4 additions & 0 deletions test/pnpm/node_modules/c/node_modules/a/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions test/pnpm/node_modules/c/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions test/pnpm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "test",
"version": "1.0.0",
"dependencies": {
"a": "*",
"b": "*",
"c": "*"
}
}
15 changes: 15 additions & 0 deletions test/pnpm/webpack.config.js
Original file line number Diff line number Diff line change
@@ -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 })]
};
};