How to migrate a JavaScript project to pnpm (package manager)
Tags: tooling, javascript, webdev, node
Published: 2022-08-01 15:04:00

[pnpm](https://pnpm.io/) claims to be the "performant node package manager". It saves disk space by only ever saving a single copy of a version of a package, and using hard links to reference the package in a project. Also, this enables it to resolve dependency trees much faster than its peers.

So, should I migrate my existing projects?

## Should I migrate my existing projects?

The performance is noticeable better than npm, and if you use it consistently you will use less disk space.

Personally, I have too many projects on my system to migrate them all. If there is a smart way to automate the process, I would consider doing it in a more wholesale manner. I might selectively migrate some projects to reclaim some disk space in the meantime.

In terms of public projects, should you force users to use pnpm?

I think you should let it up to users where possible. There is some friction to doing this, because you should include a lock file to make installation a more predictable process for a particular package manager. You're not going to maintain multiple lock files.

The command-line interfaces are quite similar, so you won't get a major headache from switching. You will have to learn the differences. For example, to add a package for pnpm is `pnpm add <package>`, whereas for npm it is `npm install <package>`.

The other factor to consider is will pnpm be around in the long-term?

That is hard to predict! It is quite a popular project, with [18.6k stars on GitHub](https://github.com/pnpm/pnpm). It has [some sponsorship](https://opencollective.com/pnpm). I think the future of npm and yarn are more assured because they are well funded. This is a common quandry in JavaScript-land really! I recall a package manager called [tink](https://github.com/npm/tink) that was moth-balled.

## What is the difference between npm and yarn and pnpm?

All of them are package managers, but they vary in their approaches and feature sets. Although, the features seem to be converging over time.

[*npm*](https://github.com/npm/npm) was developed in the early days of Node.js and is the default package manager of Node.js. *npm* was designed around the idea of [Semantic Versioning (semver)](http://semver.org). The dependencies of a project are stored in *package.json*. npm installs dependencies in a non-deterministic way, meaning that two developers could have a different `node_modules` directory resulting into different behaviors.

To resolve those problems and others, [Facebook introduced a new package manager](https://code.fb.com/web/yarn-a-new-package-manager-for-javascript/) in 2016 called  [Yarn](https://github.com/yarnpkg/yarn) . *Yarn* was touted as being faster, more secure, and more reliable than *npm*. *Yarn* consumed the same `package.json` making it more straightforward to migrate. The differences between *npm* and *Yarn* were:

1. You always know you're getting the same thing on every development machine,
1. It paralellizes operations that *npm* does not, and makes more efficient use of the network,
1. It may make more efficient [use of other system resources (such as RAM)](https://stackoverflow.com/a/43987433/114900) as well.

However, *npm* narrowed the gap considerably. Since version 5 *npm* has done the following:

- *npm* now generates a 'lockfile' called `package-lock.json` that fixes your entire dependency tree much the same way the yarn (or any other) locking mechanism does,
- `--save` is now implied for `npm i`,
- It has improved network and cache usage.

The performance of *Yarn* is still superior to *npm*, but not by that much in many scenarios.

The [main motivation behind pnpm](https://pnpm.io/motivation) was performance. It uses a content-addressable store to save disk space and to deliver quicker installation times.

Here are [performance benchmarks from pnpm.io](https://pnpm.io/benchmarks):

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kb9mss4372bqlhimiai.png" alt="performance benchmarks package managers" loading="lazy"/>

Here are the feature sets of the package managers:

| Feature                     | pnpm                                                         | Yarn                | npm                     |
| --------------------------- | ------------------------------------------------------------ | ------------------- | ----------------------- |
| [Workspace support](https://next.yarnpkg.com/features/workspaces)           | ✔️                                                            | ✔️                   | ✔️                       |
| Isolated `node_modules`     | ✔️ <br>- The default                                              | ✔️                   | ❌                       |
| Hoisted `node_modules`      | ✔️                                                            | ✔️                   | ✔️ <br>- The default         |
| Autoinstalling peers        | ✔️ <br>- Via [auto-install-peers=true](https://pnpm.io/npmrc#auto-install-peers) | ❌                   | ✔️                       |
| [Plug'n'Play](https://next.yarnpkg.com/features/pnp)                | ✔️                                                            | ✔️ <br>- The default     | ❌                       |
| [Zero-Installs](https://next.yarnpkg.com/features/zero-installs)              | ❌                                                            | ✔️                   | ❌                       |
| Patching dependencies       | ✔️                                                            | ✔️                   | ❌                       |
| Managing Node.js versions   | ✔️                                                            | ❌                   | ❌                       |
| Has a lockfile              | ✔️ <br>- `pnpm-lock.yaml`                                         | ✔️ <br>- `yarn.lock`     | ✔️ <br>- `package-lock.json` |
| Overrides support           | ✔️                                                            | ✔️ <br>- Via resolutions | ✔️                       |
| Content-addressable storage | ✔️                                                            | ❌                   | ❌                       |
| Dynamic package execution   | ✔️ <br>- Via `pnpm dlx`                                           | ✔️ <br>- Via `yarn dlx`  | ✔️ <br>- Via `npx`           |
| Side-effects cache          | ✔️                                                            | ❌                   | ❌                       |

## Installation of pnpm

You can follow the [installation instructions](https://pnpm.io/installation) on the official website.

I used their installation script as below:

```shell
 curl -fsSL https://get.pnpm.io/install.sh | sh -
==> Extracting pnpm binaries 7.6.0
Copying pnpm CLI from /tmp/tmp.iofxhAyyNc/pnpm to /home/rob/.local/share/pnpm/pnpm
No changes to the environment were made. Everything is already up-to-date.
```

When it was done, I couldn't run `pnpm` as a command on the command-line as expected. There was an issue with the configuration for the Zsh shell. I submitted [an issue for this](https://github.com/pnpm/pnpm/issues/5103) on GitHub.

I fixed this myself by moving the `pnpm` file to *.local/bin*:

```shell
mv /home/rob/.local/share/pnpm/pnpm /home/rob/.local/bin/pnpm
```

## Migrate a project

**Important! You need to keep in mind that `pnpm` doesn’t use dependency hoisting by default:**

> When installing dependencies with npm or Yarn Classic, all packages are hoisted to the root of the modules directory. As a result, source code has access to dependencies that are not added as [direct] dependencies to the project.
>
> By default, pnpm uses symlinks to add only the direct dependencies of the project into the root of the modules directory.
>
> source: [pnpm](https://pnpm.io/motivation#creating-a-non-flat-node_modules-directory)

This means if the *package.json* doesn’t reference a dependency that your code has a `require()` or `import` for, then it will fail to resolve. This is the biggest hurdle in the migration. You can use the  [auto-install-peers](https://pnpm.io/npmrc#auto-install-peers) setting to do this automatically (disabled by default).

1. Run `pnpm import`. This generates a `pnpm-lock.yaml` based on the npm/yarn lockfile in the directory. Supported lock files:
   - *package-lock.json*
   - *npm-shrinkwrap.json*
   - *yarn.lock*
2. Clean up the files:
   - Delete the *node_modules* folder in your project.
   - Delete *package-lock.json* or *yarn.lock*.
3. Run `pnpm install` (alias is `pnpm i`) to install the dependencies into a fresh *node_modules* folder.
4. **If it is a monorepo, a workspace must have a [`pnpm-workspace.yaml`](https://pnpm.io/pnpm-workspace_yaml) file in its root.** You will need port the contents of the *workspaces* field from your *package.json*.
    ```yaml
    packages:
       - "apps/**"
       - 'packages/**'
    ```
5. <u>Optional</u>: If you want to ensure that pnpm *must* be used with the project, add the following script to *package.json*:
    ```jsonc
    "scripts": {
      "preinstall": "npx only-allow pnpm", 
      ...
    }
    ```
6. <u>Optional</u>: You can replace any mention of `npm run` with `pnpm` in the `scripts` section of the *package.json*. pnpm will figure it out if you don't, so this can be skipped.