iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦁

Features of pnpm

に公開

pnpm is a JavaScript package manager alongside npm and yarn. The name pnpm originates from "performant npm".

https://pnpm.io/

It is compatible with npm and has the following features:

  • Up to 2x faster compared to other tools
  • Efficient disk space usage
  • Strict package management, ensuring you can only access packages listed in package.json

Getting Started with pnpm

pnpm can be installed using the following commands:

# macOS or Linux
$ curl -fsSL https://get.pnpm.io/install.sh | sh -
# Windows (PowerShell) 
$ iwr https://get.pnpm.io/install.ps1 -useb | iex

Alternatively, you can install it using npm.

$ npm install -g pnpm

Once the pnpm installation is complete, let's try installing express as a test. To install a package, execute the pnpm install <package-name> command.

$ pnpm install express
Packages: +57
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: ~/Library/pnpm/store/v3
  Virtual store is at:             node_modules/.pnpm
Progress: resolved 61, reused 0, downloaded 57, added 57, done

dependencies:
+ express 4.18.1

Disk Space Efficient node_modules

As indicated by the message "Packages are hard linked from the content-addressable store to the virtual store.", all actual package files are placed in a globally managed Content-addressable store. On macOS, ~/Library/pnpm/store/v3 is the default storage location.

Every file contained in every package within node_modules is a hard link to the content store.

Once hard links are created in node_modules for all packages, symbolic links are created to reflect the nested dependency graph structure.

In fact, the node_modules directory looks like this:

node_modules/
└── .pnpm/
    └── body-parser@1.20.0
      └── node_modules
          ├── body-parser
          │   ├── index.js
          │   └── package.json
          ├── bytes -> ../../bytes@3.1.2/node_modules/bytes
          ├── content-type -> ../../content-type@1.0.4/node_modules/content-
          └── unpipe -> ../../unpipe@1.0.0/node_modules/unpipe
    └── express@4.18.1
      └── node_modules
          ├── body-parser -> ../../body-parser@1.20.0/node_modules/body-parser
          └── express/
├── express -> .pnpm/express@4.18.1/node_modules/express
└── .modules.yaml

In this way, unlike npm or yarn, all packages are stored in a content-addressable store, allowing packages of the same version to be shared across different projects. This achieves both disk space savings and faster installations.

Strict Package Management

Unlike npm, which you usually use, have you noticed that the structure under node_modules is quite unusual? pnpm does not adopt the flat node_modules structure used by npm and yarn. For reference, here is the node_modules structure generated by npm:

node_modules/
├── body-parser
│   ├── index.js
│   └── package.json
├── express
│   ├── index.js
│   └── package.json

As shown above, npm's node_modules places all packages directly at the root.

On the other hand, with pnpm, only the symbolic link for express is placed directly under node_modules. This is because only express is listed in the dependencies of package.json. This structure means that express is the only package that the application code can access directly.

This is what makes pnpm "strict".

Let's perform an experiment to prove pnpm's strictness. First, create the following code in a project where express was installed using npm.

// npm-repo/index.mjs
import bodyParser from "body-parser";
import assert from "node:assert/strict";

assert.ok(bodyParser);

Even though body-parser is not a package you installed directly, you can successfully import it and run this code without any issues. With a flat node_modules structure, you can access packages that express depends on, even if you haven't installed them directly yourself.

However, this behavior can cause problems in the following cases:

  • body-parser underwent a major version update during a patch update of express.
    • For example, suppose the version of body-parser you are currently using is 1.x, your code is written based on that assumption, and body-parser version 2.x is released. It won't be a problem for a while, but let's say express is updated in a patch release to use body-parser 2.x. Since it's a patch update, there should be no breaking changes in express itself, but from the next npm install onwards, body-parser 2.0 will be placed in node_modules. This could potentially break code that depends on body-parser even though you haven't changed anything.
  • express no longer depends on body-parser after a patch update.
    • Similarly to the previous example, if express no longer depends on body-parser, body-parser will not be placed in node_modules. In this case, code depending on body-parser will break again.

The only way to prevent such issues is to explicitly install and use body-parser as well.

Now, what about the case with pnpm? Let's run the exact same code.

// pnpm-repo/index.mjs
import bodyParser from "body-parser";
import assert from "node:assert/strict";

assert.ok(bodyParser);

As a result, an error is displayed stating that the module does not exist. This is because body-parser is not listed in the dependencies of package.json, so it is not placed directly under node_modules, but exists under node_modules/.pnpm instead.

$ node index.mjs 
node:internal/errors:465
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'body-parser' imported from /work/pnpm-test/pnpm-repo/index.mjs
    at new NodeError (node:internal/errors:372:5)
    at packageResolve (node:internal/modules/esm/resolve:954:9)
    at moduleResolve (node:internal/modules/esm/resolve:1003:20)
    at defaultResolve (node:internal/modules/esm/resolve:1218:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:80:40)
    at link (node:internal/modules/esm/module_job:78:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

The correct way to resolve this is to simply install body-parser.

$ pnpm add body-parser

Performance

There are benchmarks for pnpm compared to npm, yarn, and Yarn PnP. Looking at these results, it's clear that pnpm can indeed install packages very quickly.

action cache lockfile node_modules npm pnpm Yarn Yarn PnP
install 35.3s 15.7s 16.7s 22.9s
install 1.8s 1.1s 2.1s n/a
install 10.3s 4.1s 6.5s 1.42
install 14.9s 7.2s 11.1s 6.1s
install 16.8s 12.6s 11.5s 17.2s
install 2.4s 2.7s 6.9s n/a
install 1.8s 1.2s 7.1s n/a
install 2.3s 7.8s 11.7s n/a
update n/a n/a n/a 1.9s 9s 15.4s 28.3s

https://pnpm.io/benchmarks

As for why pnpm performs better than other package managers, it is stated that pnpm does not cause blocking between installation stages.

To install a package, there are three stages: "Resolving", "Fetching", and "Writing". As shown in the image below, traditional package managers require each package to wait for the current stage to complete before starting the next stage.

In pnpm, stages are executed individually for each package, so there is no time wasted waiting for other packages to start their next stage.

You can also check the benchmarks published by Yarn. pnpm shows good results there as well.

Screenshot 2022-07-24 17.25.42

https://p.datadoghq.eu/sb/d2wdprp9uki7gfks-c562c42f4dfd0ade4885690fa719c818?tpl_var_npm=*&tpl_var_pnpm=*&tpl_var_yarn-classic=*&tpl_var_yarn-modern=*&tpl_var_yarn-nm=*&tpl_var_yarn-pnpm=no&from_ts=1658478221525&to_ts=1658651021525&live=true

Impressions

A key characteristic of pnpm is that its node_modules structure is significantly different from other package managers. The fact that a package manager can provide such strict management is an interesting point.

References

GitHubで編集を提案

Discussion