How to migrate a JavaScript project to pnpm (package manager)

6 min read
How to migrate a JavaScript project to pnpm (package manager) cover image

pnpm claims to be the “performant node package manager”, hence the acronym! 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.

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. It has some sponsorship. 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 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 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). 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 in 2016 called 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,
  2. It paralellizes operations that npm does not, and makes more efficient use of the network,
  3. It may make more efficient use of other system resources (such as RAM) 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 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:

performance benchmarks package managers

Here are the feature sets of the package managers:

FeaturepnpmYarnnpm
Workspace support
Isolated node_modules
- The default
Hoisted node_modules
- The default
Autoinstalling peers
- Via auto-install-peers=true
Plug’n’Play
- The default
Zero-Installs
Patching dependencies
Managing Node.js versions
Has a lockfile
- pnpm-lock.yaml

- yarn.lock

- package-lock.json
Overrides support
- Via resolutions
Content-addressable storage
Dynamic package execution
- Via pnpm dlx

- Via yarn dlx

- Via npx
Side-effects cache

Installation of pnpm

You can follow the installation instructions on the official website.

I used their installation script as below:

 curl -fsSL https://get.pnpm.io/install.sh | sh -

And this was the output:

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.

You should be able to use the pnpm command from the command-line now. Run pnpm --help to check.

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

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 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 file in its root. You will need port the contents of the workspaces field from your package.json.

     packages:
        - "apps/**"
        - 'packages/**'
    
  5. Optional: If you want to ensure that pnpm must be used with the project, add the following script to package.json:

     "scripts": {
       "preinstall": "npx only-allow pnpm", 
       ...
     }
    
  6. Optional: 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.