Better Use pNPM
Tags: javascript, node, npm
Published: 2023-11-06 07:03:42

[pNPM](https://pnpm.io) 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](https://pnpm.io/benchmarks), 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](https://dev.to/dmnchzl/migrate-a-webapp-from-cra-to-vite-ebe)): `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](https://www.theregister.com/2016/03/23/npm_left_pad_chaos)'s story...

> **NB**: To reproduce a resilience mechanism (similar to pNPM) for NPM or Yarn, you can use [dependency-check](https://www.npmjs.com/package/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](https://pnpm.io/installation) 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:

```json
{
  "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": "WTFPL"
}
```

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
```

<figcaption>File tree with NPM</figcaption>

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
```

<figcaption>File tree with pNPM</figcaption>

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 👍