Better Use pNPM

Better Use pNPM

November 6th, 2023 | 4 mins read

Published on dev.to and medium.com

pNPM is a (another one) package manager for Node.

Ok, Ok!! There's already NPM (or better Yarn), so why change!? Well, for performance reasons, obviously πŸ˜ŽπŸ‘Œ According to the metrics, pNPM is 2x faster than NPM πŸš€

Explanation

Like its counterparts, pNPM takes security very seriously by ensuring the integrity of dependencies used by one (or more *) JavaScript project(s), through a "lock" file strategy (pnpm-lock.yaml).

* NB: Just as NPM (and Yarn before it), pNPM supports monolithic projects, or more commonly called: "workspaces".

What we appreciate the most about pNPM is the way it handles node_modules. In fact, the big difference in using this package manager lies in the organization of dependencies.

NPM (and Yarn) have a "flat" node_modules tree; in other words, all libraries are on the same level... pNPM works differently, listing dependencies by hard and symbolic links.

Moreover, pNPM keeps all libraries in a single location on disk: the "store". So, even if you fetch several versions of the same dependency, pNPM takes care of differentiating between them, and then only retrieves the necessary changes.

NB: Remember, with NPM, you fetch all dependencies when installing a project, and that for each project. As a result, each version is stored independently, taking up more memory space... The pNPM store isn't only efficient, it also saves a lot of disk space.

This mode of operation makes pNPM more robust than its rivals, since it is not allowed to use an unsaved dependency in the package.json file.

In fact, with a traditional package manager, it's possible to use libraries implicitly, as long as they are retrieved from another dependency.

Use Case

When creating a new project with the "official" React CLI tool (for now): npx create-react-app my-project; most of the project libraries are embedded in the react-scripts dependency. This makes it possible to use ESLint or Jest implicitly. ⚠️ SPOILER ALERT: this is not good practice!!!

This kind of use can create major problems for future development/application maintenance... For exemple, if react-scripts updates one or other of the libraries, the behavior of your unit tests (i.e. Jest) will be affected. Even worse, what would be the consequence of removing Jest (in favor of another test runner)? That's why, the use of "transitive" dependencies isn't recommended! By the way, this is a part of the left-pad's story...

NB: To reproduce a resilience mechanism (similar to pNPM) for NPM or Yarn, you can use dependency-check to ensure that your project is compliant.

Migration

Convinced by this introduction? Then it's time to get down to business! The first step to migrate from NPM (or Yarn) to pNPM is installing the package manager: npm i -g pnpm (alternative methods exist).

Then, into existing project, which already has a package-lock.json file (or Yarn equivalent), just follow the instruction: pnpm import.

Finally, you can delete your node_modules and retrieve them (again) from pNPM: pnpm install

NB: Overall, pNPM is based on the NPM API, so the pnpm install, pnpm config set and pnpm run your:script commands are fully supported!

Let's take a look at the following package.json, and compare the before/after using the new package manager:

{
  "name": "react-only",
  "version": "0.0.1",
  "description": "React + DOM (Only)",
  "main": "index.js",
  "scripts": {
    "start": "echo \"Β―\\_(ツ)_/Β―\" && exit 0"
  },
  "dependencies": {
    "react": "~18.2.0",
    "react-dom": "~18.2.0"
  },
  "license": "Beerware"
}

When installing the above dependencies with NPM, you'll end up with a "flat" structure for your project:

/
β”œβ”€β”€ node_modules/
β”‚   β”œβ”€β”€ js-token
β”‚   β”œβ”€β”€ loose-envify
β”‚   β”œβ”€β”€ react
β”‚   β”œβ”€β”€ react-dom
β”‚   └── scheduler
β”œβ”€β”€ package.json
└── package-lock.json

File tree with NPM

On the other hand, when using pNPM to install react and react-dom, all dependencies (including indirect dependencies) are retrieved inside the hidden .pnpm folder (and identified by version); only project-specific libraries are present at the root level of node_modules (by a symbolic link). So, it's impossible to call js-token, loose-envify or scheduler directly!

/
β”œβ”€β”€ node_modules/
β”‚   β”œβ”€β”€ .pnpm/
β”‚   β”‚   β”œβ”€β”€ js-token@4.0.0
β”‚   β”‚   β”œβ”€β”€ loose-envify@1.4.0
β”‚   β”‚   β”œβ”€β”€ react@18.2.0
β”‚   β”‚   β”œβ”€β”€ react-dom@18.2.0
β”‚   β”‚   └── scheduler@0.23.0
β”‚   β”œβ”€β”€ react -> .pnpm/react@18.2.0/node_modules/react
β”‚   └── react-dom -> .pnpm/react-dom@18.2.0/node_modules/react-dom
β”œβ”€β”€ package.json
└── pnpm-lock.yaml

File tree with pNPM

After that, the choice of pNPM seems obvious, if only for the readability of node_modules. Don't worry, you won't be the first to migrate to this next-generation package manager, projects such as Prisma, Vue or even SvelteKit have done it, as well as Microsoft. Enjoy πŸ‘