pnpm
pnpm (Performant npm) is a package manager for Node.js that saves disk space by storing all packages in a global content-addressable store and using hard links to project node_modules. This means packages are never duplicated on disk, installs are faster, and all projects share the same cached packages.
Installation
# npm (recommended for bootstrapping)
npm install -g pnpm
# Corepack (built into Node.js 16.9+)
corepack enable
corepack prepare pnpm@latest --activate
# macOS (Homebrew)
brew install pnpm
# Windows (Scoop)
scoop install nodejs-lts
corepack enable
# Linux standalone install
curl -fsSL https://get.pnpm.io/install.sh | sh -
# Upgrade pnpm
pnpm self-update
# Check version
pnpm --version
# Set Node.js version to use (pnpm manages Node versions too)
pnpm env use --global lts
pnpm env use --global 20
Corepack Setup (Recommended)
# Enable corepack (ships with Node.js)
corepack enable pnpm
# Pin pnpm version per project in package.json
# Add to package.json:
# "packageManager": "pnpm@9.1.0"
Configuration
pnpm uses .npmrc for configuration and pnpm-workspace.yaml for monorepos:
# .npmrc
# Store location (default: ~/.pnpm-store)
store-dir=~/.pnpm-store
# Use shamefully-hoist to fix compatibility with tools that expect flat node_modules
# (only use if a package explicitly requires it)
shamefully-hoist=false
# Strict mode — fail on peer dependency issues
strict-peer-dependencies=true
# Auto-install peer dependencies
auto-install-peers=true
# Save exact versions (no ^ or ~)
save-exact=false
# Custom registry
registry=https://registry.npmjs.org/
# Scoped registry
@myorg:registry=https://npm.myorg.com/
# Link workspace packages by default
link-workspace-packages=true
# Prefer frozen lockfile in CI (set via env var instead)
# PNPM_FROZEN_LOCKFILE=true
Monorepo Workspace Config
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
- "!**/__tests__/**"
package.json Overrides
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21",
"minimist@<1.2.6": "^1.2.6"
},
"peerDependencyRules": {
"ignoreMissing": ["react"],
"allowAny": ["typescript"]
},
"packageExtensions": {
"some-lib@*": {
"peerDependencies": {
"react": "*"
}
}
}
}
}
Core Commands
| Command | Description |
|---|---|
pnpm install | Install all dependencies |
pnpm install --frozen-lockfile | Install without updating lockfile (CI) |
pnpm add package | Add a dependency |
pnpm add -D package | Add a dev dependency |
pnpm add -g package | Install globally |
pnpm add -O package | Add as optional dependency |
pnpm add package@2.0.0 | Add specific version |
pnpm remove package | Remove a dependency |
pnpm update | Update packages per semver range |
pnpm update --latest | Update to latest versions |
pnpm update package | Update a specific package |
pnpm outdated | Check for outdated packages |
pnpm run script | Run a script from package.json |
pnpm run -r script | Run script in all workspace packages |
pnpm exec command | Run a command from node_modules/.bin |
pnpm dlx package | Execute package without installing (npx equivalent) |
pnpm list | List installed packages |
pnpm list --depth 0 | List top-level packages only |
pnpm why package | Show why a package is installed |
pnpm store status | Check if store is up to date |
pnpm store prune | Remove unused packages from store |
pnpm audit | Audit packages for vulnerabilities |
pnpm patch package | Patch a dependency |
pnpm deploy ./deploy-dir | Create a production-ready copy |
pnpm init | Initialize a new package.json |
pnpm publish | Publish package to npm registry |
Advanced Usage
Workspace Filtering
# Run build in a specific package
pnpm --filter my-app build
# Run tests in all packages matching pattern
pnpm --filter "*-lib" test
# Run in all packages that have changed since main
pnpm --filter "[main]" build
# Run in packages that depend on my-lib (dependents)
pnpm --filter "...my-lib" build
# Run in my-lib and all its dependencies
pnpm --filter "my-lib..." build
# Combine filters
pnpm --filter "my-app" --filter "my-lib" build
# Run in all workspace packages
pnpm -r build
pnpm -r --parallel build # run in parallel
Dependency Patching
# Start patching a dependency
pnpm patch lodash@4.17.21
# After editing files in the temp directory printed above:
pnpm patch-commit /tmp/patch-dir
# This creates patches/lodash@4.17.21.patch and adds to package.json:
# "pnpm": { "patchedDependencies": { "lodash@4.17.21": "patches/lodash@4.17.21.patch" } }
Deploy Command (for Monorepos)
# Create a self-contained deployment directory
# Only includes the app and its workspace dependencies
pnpm --filter my-app deploy --prod ./deploy
# The deploy directory has its own node_modules with no symlinks
# Safe to copy to production server or Docker image
Catalog (pnpm 9+)
# pnpm-workspace.yaml — define shared version catalog
packages:
- "packages/*"
catalog:
react: "^18.3.0"
typescript: "^5.4.0"
vitest: "^1.6.0"
// packages/my-app/package.json
{
"dependencies": {
"react": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
Content-Addressable Store
# View store location
pnpm store path
# Check store integrity
pnpm store status
# Remove unused packages (safe to run periodically)
pnpm store prune
# Add packages to store without linking (warm up cache)
pnpm fetch
Setting Up pnpm in Docker
FROM node:20-alpine
# Enable corepack and install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
# Copy lockfile and manifests first for layer caching
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/my-app/package.json ./packages/my-app/
COPY packages/my-lib/package.json ./packages/my-lib/
# Install deps (uses frozen lockfile)
RUN pnpm install --frozen-lockfile
# Copy source
COPY . .
RUN pnpm --filter my-app build
Common Workflows
Setting Up a Monorepo
# Create root
mkdir my-monorepo && cd my-monorepo
pnpm init
# Create workspace config
cat > pnpm-workspace.yaml << 'EOF'
packages:
- "packages/*"
- "apps/*"
EOF
# Create packages
mkdir -p packages/ui apps/web
cd packages/ui && pnpm init && cd ../..
cd apps/web && pnpm init && cd ../..
# Add shared root devDependencies
pnpm add -D -w typescript vitest
# Add workspace package as dependency
pnpm --filter web add @myorg/ui --workspace
Adding Overrides to Fix Security Vulnerabilities
# Check what's vulnerable
pnpm audit
# Override the vulnerable version
# Add to package.json "pnpm.overrides":
# "vulnerable-pkg@<2.0.0": "^2.0.0"
pnpm install # applies override
pnpm audit # verify fix
Publishing Monorepo Packages
# Build all packages
pnpm -r build
# Publish all public packages
pnpm -r publish --access public
# Dry run to preview
pnpm -r publish --dry-run
Tips and Best Practices
Commit pnpm-lock.yaml — Always commit the lockfile. Use pnpm install --frozen-lockfile in CI to ensure deterministic installs. Set CI=true to enable this automatically.
Use pnpm dlx instead of npx — pnpm dlx is the pnpm equivalent of npx and is faster because it leverages the pnpm store for caching.
Avoid shamefully-hoist — The default strict node_modules structure catches phantom dependencies (packages you use but didn’t declare). Only enable hoisting if a specific package explicitly requires it.
Use pnpm why to audit dependencies — Before adding a package, run pnpm why package to understand if it’s already a transitive dependency you could use directly.
Prune the store periodically — Run pnpm store prune monthly to remove packages that no longer appear in any lockfile. This keeps disk usage in check.
Pin pnpm version with packageManager — Add "packageManager": "pnpm@9.x.x" to your root package.json. With corepack enabled, this ensures all contributors use the same version.
{
"packageManager": "pnpm@9.1.0+sha256...."
}
Use the catalog for version consistency — In pnpm 9+, the workspace catalog ensures every package in a monorepo uses exactly the same version of shared tools like TypeScript, Vitest, and React — with a single place to update them.
Run scripts in parallel carefully — pnpm -r --parallel run build runs all builds simultaneously regardless of dependencies. Use --stream to see output in real time. For topology-aware ordering, use Turborepo or Nx on top of pnpm.