Migrate from npm to pnpm
Introduction
Recently my computer is running out of disk space. Looking at every frontend project with a node_modules folder of several hundred MB, I decided to look into migrating from npm to pnpm. The reason is that pnpm uses a global local store to host dependencies, similar to Go Modules and Maven, and then manages each project's node_modules folder through a combination of symlinks and hard links. This can save a lot of disk space by eliminating duplicate dependencies.
Beyond this motivation, pnpm has many other benefits: (1) faster – though I feel the main reason for slowness is Windows Defender; (2) used by many well-known open source projects, such as Vue, which proves its reliability; (3) more secure, e.g. blocking post-install scripts by default, which are vulnerable to supply chain attacks.
After reading the pnpm documentation once, I started the migration, which is the main content of this blog post. I migrated a total of 6 projects, including Vue, React, a mini-program, and my personal blog. The overall migration process went smoothly – after all, I'm using well-known open source projects.
Installing and configuring pnpm
Installation
Install using npm:
1 | npm install -g pnpm@latest-10 |
Once pnpm is installed, we'll rarely use the npm and npx commands again. So on Windows, we can modify the PowerShell initialization script pointed to by $PROFILE and add the following commands:
1 | function npm { |
Use these two functions to replace the original commands to prevent accidentally running the wrong one.
Configuration
The most important configuration is setting the path of the local store:
1 | pnpm config set store-dir <path> |
If not set (the default), pnpm will create a .pnpm-store folder under the root directory of the drive where the project resides and use that path.
Besides using pnpm config set|get to configure individual settings, we can also get the path to the global config file with pnpm config get globalconfig and edit the file directly.
Other important configurations may be security-related. Readers can refer to the official guide on mitigating supply chain attacks.
Migration
Overall process
1️⃣ Navigate into an npm project.
2️⃣ Delete the node_modules folder.
3️⃣ Run pnpm import. This command reads package-lock.json and generates the corresponding pnpm-lock.yaml, completing the migration.
4️⃣ Delete package-lock.json.
5️⃣ Run pnpm install to install project dependencies.
If after installation we see a message "Ignored build scripts", follow the prompt to run pnpm approve-builds and select which dependencies are allowed to run builds (this actually modifies the allowBuilds configuration in pnpm-workspace.yaml). Then decide whether to build. If we skip building for now, we can manually run pnpm rebuild later.
6️⃣ Replace all npm in scripts with pnpm, and replace npx with pnpx or pnpm exec.
7️⃣ Modify GitHub Actions workflows, including triggers and specific commands – see below for details.
8️⃣ Modify the Dockerfile.
9️⃣ Run dependabot once – see below for details.
GitHub Actions
For CI workflows, add pnpm-lock.yaml to the path‑triggering conditions.
For the actual workflow content, we need to install pnpm.
1 | steps: |
According to the actions/setup-node documentation, if we enable the cache feature, we must install pnpm before installing Node. The cache-dependency-path defaults to looking for a lock file in the root directory; if not found, an error is thrown, so sometimes we need to specify it manually based on our project structure.
Also, the command to install dependencies must be changed:
1 | pnpm install --frozen-lockfile |
In CI environments, it's always recommended to use the --frozen-lockfile flag. This makes pnpm error out (rather than modifying the lock file) when package.json and the lock file are inconsistent, ensuring dependency consistency.
The Dockerfile modifications are similar: (1) install pnpm, (2) change the dependency installation command.
dependabot
The dependabot.yml file itself does not need to be changed, because package-ecosystem: "npm" recognizes pnpm. However, existing PRs are still about package-lock.json, so we need to run dependabot once again.
If some PRs are still not updated after rerunning, manually comment @dependabot recreate.
Postscript
- I migrated 6 projects in total and deleted the
node_modulesfolders of several older projects, but in the end I only saved about 1 GB – not really achieving my initial goal. - pnpm's documentation is well written and easy to read.