Overview

Migrating from npm to pnpm saves disk space, enforces strict dependency isolation, and speeds up installs. The migration is low-risk: pnpm reads package.json and the npm registry unchanged. The main work is fixing phantom dependencies that npm’s hoisting hid, and updating CI scripts. See yarn-vs-pnpm for when to use pnpm versus Yarn.

Prerequisites

  • Node 22 on the path.
  • A project using package-lock.json. Yarn migrations are similar but start from yarn.lock.
  • CI access to update install steps. For GitHub Actions, see set-up-github-actions-cache.
  • For monorepos: a list of all workspace packages.

Steps

1. Install pnpm

npm install -g pnpm
pnpm --version

Or use Corepack, which ships with Node 22:

corepack enable
corepack prepare pnpm@latest --activate

2. Import the lock file

pnpm import reads package-lock.json and generates a pnpm-lock.yaml with equivalent resolved versions. This preserves the exact versions npm resolved, avoiding unexpected upgrades.

pnpm import

Commit pnpm-lock.yaml before continuing. Do not delete package-lock.json yet.

3. Install with pnpm and surface phantom dependency errors

rm -rf node_modules
pnpm install

pnpm’s strict linker does not hoist all packages. If the project uses a package that is a transitive dependency but not declared in package.json, the build will fail with a resolution error.

Fix each phantom dependency by adding it explicitly:

pnpm add package-name

These are real bugs npm’s hoisting was hiding. Every package you add is a dependency the project actually needs.

4. Update scripts in package.json

Replace npm run calls inside scripts with pnpm run. Most scripts work unchanged; the main difference is lifecycle hooks.

{
  "scripts": {
    "build": "vite build",
    "test": "vitest run",
    "postinstall": "pnpm run generate"
  }
}

5. Configure monorepo workspaces (if applicable)

npm workspaces use a workspaces field in the root package.json. pnpm uses a separate pnpm-workspace.yaml file.

Create pnpm-workspace.yaml at the root:

packages:
  - "apps/*"
  - "packages/*"

Remove the workspaces field from the root package.json (pnpm ignores it in favor of the YAML file).

6. Update CI

Replace npm ci with pnpm install --frozen-lockfile. For GitHub Actions:

- uses: pnpm/action-setup@v4
  with:
    version: 9
 
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm
 
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm test

The cache: pnpm option in actions/setup-node caches the pnpm store between runs. See set-up-github-actions-cache for caching strategy details.

7. Delete the npm lock file

After CI passes with pnpm on a test branch, delete package-lock.json and commit.

rm package-lock.json
git add pnpm-lock.yaml pnpm-workspace.yaml
git rm package-lock.json
git commit -m "feat: migrate from npm to pnpm"

Verify it worked

# Clean install from lock file
rm -rf node_modules
pnpm install --frozen-lockfile
 
# Build and test
pnpm run build
pnpm test

Both commands should exit 0. If they do, the migration is complete.

Common errors

  • ERR_PNPM_MISMATCHED_LOCKFILE: pnpm-lock.yaml is out of sync with package.json. Run pnpm install to regenerate, then commit.
  • Package not found at import time: a phantom dependency. Add it explicitly with pnpm add package-name.
  • Postinstall script failing: some packages run npm in postinstall scripts. Set pnpm.overridePackageManager = false in package.json or patch the script.
  • Monorepo cross-package imports fail: ensure pnpm-workspace.yaml lists all package globs correctly. Run pnpm ls -r to verify all packages are discovered.
  • CI cache miss every run: confirm the cache: pnpm setup in actions/setup-node uses pnpm-lock.yaml as the cache key. The default behavior does this automatically.