pnpm 15 📦 Migrating Existing Projects from npm or Yarn to pnpm
Migrating a project from npm or Yarn to pnpm is not a single command. It is a sequence of decisions: how to convert the lockfile, how to handle the strict dependency resolution that pnpm enforces, how to rewrite CI scripts, and how to deal with the inevitable phantom dependency that npm’s hoisting was hiding. The LFCS exam does not test pnpm, but the pnpm ecosystem documentation is explicit about the migration path. The most important distinction is between the lockfile conversion (pnpm import) and the installation (pnpm install). The import creates a pnpm-lock.yaml from the existing package-lock.json or yarn.lock. The install then resolves the dependency tree against that lockfile and fails if a dependency is missing from the manifest .
Key point: pnpm import generates a pnpm-lock.yaml from an existing npm or Yarn lockfile. It does not modify package.json or node_modules. The migration is only complete when pnpm install succeeds and the old lockfile is deleted. In a workspace, pnpm-workspace.yaml must exist before pnpm import runs, or workspace packages are silently skipped .
Why migration requires a deliberate sequence
The lockfile conversion problem. npm and pnpm use different lockfile formats. package-lock.json records the physical node_modules layout, including hoisting decisions. pnpm-lock.yaml records only the logical dependency tree. pnpm import reads the npm lockfile and generates a pnpm lockfile that prefers the versions from the source. The conversion is not one-to-one: if the npm lockfile does not satisfy the package.json manifests, pnpm resolves new versions during the import . The result is a valid lockfile, but the dependency tree may differ slightly from what npm installed.
The phantom dependency problem. npm’s hoisting allows a package to import a dependency it never declared. pnpm’s strict layout blocks this. A project that worked under npm may fail under pnpm with Cannot find module 'lodash' because lodash was hoisted from a transitive dependency and never added to package.json . The fix is to add the missing dependency explicitly with pnpm add lodash. This is not a bug in pnpm; it is a declaration that was missing from the manifest.
The symlink compatibility problem. Some tools do not follow symlinks correctly. A bundler, a test runner, or an IDE plugin that assumes a flat node_modules may fail under pnpm’s default isolated layout. The escape hatch is nodeLinker: hoisted in pnpm-workspace.yaml, which produces an npm-like flat layout. The setting sacrifices pnpm’s disk efficiency and strictness, but it makes legacy tools work .
The configuration migration problem. pnpm 11 stopped reading non-auth settings from .npmrc. Registry URLs and authentication tokens remain in .npmrc. Everything else—nodeLinker, hoist, strictPeerDependencies, autoInstallPeers—moved to pnpm-workspace.yaml with camelCase keys . A project migrating from npm or Yarn that has .npmrc settings for hoisting or peer dependencies must translate those settings to the YAML file.
The trade-off. Migration is not a one-command operation. It requires running the import, attempting the install, fixing the phantom dependencies that the install reveals, and then adjusting the configuration if a tool breaks. The benefit is a strict dependency tree that mirrors production, a lockfile that is reviewable, and disk savings from the content-addressable store.
a. Converting the lockfile and installing
The migration begins with pnpm import. The command reads the existing lockfile (package-lock.json, yarn.lock, or npm-shrinkwrap.json) and generates pnpm-lock.yaml. It does not delete the source lockfile or modify node_modules .
# Install pnpm if not present
npm install -g pnpm
# Remove the existing node_modules
rm -rf node_modules
# Convert the lockfile
pnpm import
# Install dependencies
pnpm install
The pnpm import command fails if no supported lockfile is found. It also fails if the source lockfile cannot be parsed, such as a yarn.lock with merge conflict markers. In a workspace, the pnpm-workspace.yaml file must exist before the import runs. Without it, pnpm does not know which directories are workspaces, and the import skips them .
After the install succeeds, the old lockfile must be deleted. Keeping both package-lock.json and pnpm-lock.yaml creates ambiguity: npm and pnpm resolve dependencies differently, and the lockfiles will diverge over time.
# Remove the npm lockfile
rm package-lock.json
# Commit the pnpm lockfile
git add pnpm-lock.yaml
git commit -m "Migrate from npm to pnpm"
b. Fixing phantom dependencies and tool compatibility
The first pnpm install after migration often fails with a missing module error. The module exists in the dependency tree—it was installed as a transitive dependency of something else—but pnpm’s strict root layout does not expose it to the project’s own code .
Error: Cannot find module 'lodash'
The fix is to declare the dependency explicitly.
# Add the missing dependency to package.json
pnpm add lodash
If the missing module is a peer dependency, pnpm reports it during install. The autoInstallPeers setting (default true in pnpm 8+) installs peer dependencies automatically. If a specific peer dependency is known to be incompatible, the peerDependencyRules.ignoreMissing setting suppresses the warning .
For tools that break on symlinks, the nodeLinker setting in pnpm-workspace.yaml changes the layout.
# pnpm-workspace.yaml
nodeLinker: hoisted
The hoisted layout produces an npm-like node_modules. The publicHoistPattern setting can hoist specific packages without abandoning the isolated layout entirely.
publicHoistPattern:
- '*eslint*'
- '*babel*'
c. Updating CI and scripts
A CI pipeline that runs npm ci must be rewritten to use pnpm. The pnpm/action-setup@v4 action installs pnpm and reads the version from packageManager in package.json. The pnpm install --frozen-lockfile command replaces npm ci .
# Before (npm)
- run: npm ci
# After (pnpm)
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
The packageManager field in package.json pins the pnpm version. Corepack reads this field and ensures every developer and CI runner uses the same version.
{
"packageManager": "pnpm@10.0.0"
}
Scripts in package.json that reference npm run must be updated to pnpm run. The npx command is replaced by pnpm exec or pnpm dlx. In a workspace, npm run build --workspaces becomes pnpm -r run build. A specific workspace script is selected with pnpm --filter <name> run <script> .
For monorepos migrating from npm workspaces, the pnpm-workspace.yaml file replaces the workspaces field in package.json. The internal dependencies use the workspace:* protocol.
# pnpm-workspace.yaml
packages:
- 'packages/*'
{
"dependencies": {
"@myorg/utils": "workspace:*"
}
}
Complete Example Session
# ============================================
# PART 1: INSTALL PNPM
# ============================================
npm install -g pnpm
# pnpm is now available as a command.
# ============================================
# PART 2: REMOVE NODE_MODULES
# ============================================
rm -rf node_modules
# The existing install is discarded.
# ============================================
# PART 3: CONVERT THE LOCKFILE
# ============================================
pnpm import
# pnpm-lock.yaml generated from package-lock.json.
# ============================================
# PART 4: INSTALL
# ============================================
pnpm install
# The install may fail with missing module errors.
# ============================================
# PART 5: FIX PHANTOM DEPENDENCIES
# ============================================
pnpm add lodash
# The missing dependency is added explicitly.
# ============================================
# PART 6: REINSTALL
# ============================================
pnpm install
# The install succeeds.
# ============================================
# PART 7: DELETE THE OLD LOCKFILE
# ============================================
rm package-lock.json
git add pnpm-lock.yaml
git commit -m "Migrate to pnpm"
# ============================================
# PART 8: UPDATE CI
# ============================================
# .github/workflows/ci.yml
# - uses: pnpm/action-setup@v4
# - run: pnpm install --frozen-lockfile
# ============================================
# PART 9: PIN THE PACKAGE MANAGER
# ============================================
# package.json
# "packageManager": "pnpm@10.0.0"
The nine parts trace the migration: installation, cleanup, lockfile conversion, installation attempt, phantom dependency fix, reinstallation, lockfile deletion, CI update, and version pinning.
Quick Reference
Migration Commands
| Command | Purpose |
|---|---|
pnpm import | Convert npm/Yarn lockfile to pnpm-lock.yaml |
pnpm install | Install from the new lockfile |
rm package-lock.json | Delete the old lockfile after successful install |
pnpm add <pkg> | Fix phantom dependencies |
Configuration Split (pnpm 11)
| Setting | Location |
|---|---|
| Registry URLs | .npmrc |
| Authentication tokens | .npmrc or global config |
nodeLinker | pnpm-workspace.yaml |
autoInstallPeers | pnpm-workspace.yaml |
strictPeerDependencies | pnpm-workspace.yaml |
CI Migration
| Before (npm) | After (pnpm) |
|---|---|
npm ci | pnpm install --frozen-lockfile |
npm run build | pnpm run build |
npx <cmd> | pnpm exec <cmd> |
npm install <pkg> | pnpm add <pkg> |
Script Migration (Workspaces)
| Before (npm) | After (pnpm) |
|---|---|
npm run build --workspaces | pnpm -r run build |
npm run build -w <pkg> | pnpm --filter <pkg> run build |
Best Practices
✅ Do This:
pnpm import
<!-- Convert the lockfile before installing --> # ✅
pnpm add <missing-package>
<!-- Declare phantom dependencies explicitly --> # ✅
nodeLinker: hoisted
<!-- Use only when a tool breaks on symlinks --> # ✅
❌ Don’t Do This:
# Delete package-lock.json before pnpm import
<!-- The import requires the source lockfile --> # ❌
# Keep both package-lock.json and pnpm-lock.yaml
<!-- Ambiguity about which is authoritative --> # ❌
# Assume all tools work with symlinks
<!-- Test the build; fall back to hoisted if needed --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
pnpm import fails | No supported lockfile | Verify package-lock.json or yarn.lock exists |
| Missing module after install | Phantom dependency | pnpm add <package> |
| Workspace packages skipped | No pnpm-workspace.yaml before import | Create the file, then import |
| Tool cannot find package | Symlink incompatibility | nodeLinker: hoisted |
| CI fails with frozen lockfile | Lockfile not committed | git add pnpm-lock.yaml |
Real-World Examples
1. Import Lockfile
pnpm import
2. Install After Import
pnpm install
3. Fix Phantom Dependency
pnpm add lodash
4. Fallback to Hoisted Layout
nodeLinker: hoisted
5. Update CI
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
6. Pin pnpm Version
{ "packageManager": "pnpm@10.0.0" }
7. Delete Old Lockfile
rm package-lock.json
8. Workspace Migration
packages:
- 'packages/*'
9. Update Scripts
{ "scripts": { "build": "pnpm run build:wasm && pnpm run build:vite" } }
10. Filtered Execution
pnpm --filter @myorg/app run build
Visual
Migration Flow
┌──────────────────────────────────────────────┐
│ npm or Yarn project │
│ ├── package.json │
│ ├── package-lock.json (or yarn.lock) │
│ └── node_modules/ │
└──────────────────┬───────────────────────────┘
│
│ rm -rf node_modules
▼
┌──────────────────────────────────────────────┐
│ pnpm import │
│ → pnpm-lock.yaml generated │
└──────────────────┬───────────────────────────┘
│
│ pnpm install
▼
┌──────────────────────────────────────────────┐
│ Install succeeds? │
│ ├─ YES → Delete old lockfile │
│ └─ NO → Add missing dependencies │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Commit pnpm-lock.yaml │
│ Update CI to pnpm │
└──────────────────────────────────────────────┘
Phantom Dependency Discovery
┌──────────────────────────────────────────────┐
│ Code: import lodash from 'lodash' │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ npm: lodash hoisted → works │
│ pnpm: lodash not declared → fails │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Fix: pnpm add lodash │
│ → lodash added to package.json │
│ → import succeeds │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Lockfile conversion | pnpm import |
| Supported sources | package-lock.json, yarn.lock, npm-shrinkwrap.json |
| Install after import | pnpm install |
| Phantom dependency fix | pnpm add <package> |
| Symlink fallback | nodeLinker: hoisted |
| CI replacement | pnpm install --frozen-lockfile |
| Version pin | packageManager in package.json |
| Old lockfile | Delete after successful install |
Key takeaways:
pnpm importconverts the lockfile, not the manifest. The command readspackage-lock.jsonoryarn.lockand generatespnpm-lock.yaml. It does not modifypackage.jsonornode_modules.- Phantom dependencies surface immediately. A package that was hoisted under npm and imported without declaration will fail under pnpm. The fix is
pnpm add <package>, which adds the missing declaration . - pnpm 11 split configuration. Registry and auth stay in
.npmrc. Everything else—nodeLinker,autoInstallPeers,strictPeerDependencies—moves topnpm-workspace.yamlwith camelCase keys . - Symlink compatibility requires a fallback. Some tools do not follow symlinks. The
nodeLinker: hoistedsetting produces an npm-like layout for those tools. - CI must use
--frozen-lockfile. Thepnpm/action-setup@v4action installs pnpm and reads the version frompackageManager. Thepnpm install --frozen-lockfilecommand replacesnpm ci.
Remember: Migration is a process, not a command. The import creates the lockfile, the install reveals the missing declarations, and the configuration adjustments handle the tools that cannot follow symlinks. The old lockfile is deleted only after the install succeeds. The CI pipeline is updated to enforce the new lockfile. The project is on pnpm when pnpm install --frozen-lockfile passes in CI.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!