pnpm
pnpm package manager, workspaces, strict dependency resolution, monorepo support, and performance optimization
SKILL.md.claude/skills/pnpm/SKILL.md
GitHubpnpm
Purpose
Manage JavaScript/TypeScript dependencies efficiently using pnpm. Covers the content-addressable store, workspace monorepo management, strict dependency resolution, catalogs for shared versions, patching packages, CI optimization, and common workflows for day-to-day development.
Core Patterns
Essential Commands
# Install all dependencies
pnpm install # Install from lockfile
pnpm install --frozen-lockfile # CI mode — fail if lockfile needs update
# Add/remove packages
pnpm add zod drizzle-orm # Add production dependencies
pnpm add -D vitest @types/node # Add dev dependencies
pnpm add -g tsx # Add global package
pnpm remove lodash # Remove a package
# Run scripts
pnpm dev # Run "dev" script from package.json
pnpm run build # Run "build" script
pnpm exec vitest # Run a binary from node_modules/.bin
pnpm dlx create-next-app@latest # Run a package without installing (like npx)
# Update dependencies
pnpm update # Update all within semver ranges
pnpm update --latest # Update to latest versions (ignoring ranges)
pnpm update zod # Update specific package
pnpm outdated # Check for outdated packages
# Inspect
pnpm list # List installed packages
pnpm list --depth 0 # Top-level only
pnpm why react # Show why a package is installed
pnpm audit # Check for vulnerabilities
Workspace Setup (Monorepo)
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
my-monorepo/
pnpm-workspace.yaml
package.json # Root package.json
.npmrc # pnpm settings
apps/
web/
package.json # "name": "@myorg/web"
api/
package.json # "name": "@myorg/api"
packages/
ui/
package.json # "name": "@myorg/ui"
shared/
package.json # "name": "@myorg/shared"
tooling/
eslint-config/
package.json # "name": "@myorg/eslint-config"
tsconfig/
package.json # "name": "@myorg/tsconfig"
Workspace Dependencies
// apps/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*", // Always latest from workspace
"@myorg/shared": "workspace:^", // Semver-compatible from workspace
"next": "^15.0.0"
}
}
# Workspace-aware commands
pnpm add zod --filter @myorg/web # Add to specific package
pnpm add zod --filter @myorg/web... # Add to package and its dependencies
pnpm --filter @myorg/web dev # Run dev in specific package
pnpm --filter "./apps/*" build # Run build in all apps
pnpm -r build # Run build in all packages (recursive)
pnpm -r --parallel build # Run build in parallel across packages
pnpm --filter @myorg/web... build # Build package and all its workspace deps
pnpm --filter "...@myorg/web" test # Test package and everything that depends on it
Catalogs (Shared Version Management)
# pnpm-workspace.yaml — centralize dependency versions
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.7.0
zod: ^3.24.0
catalogs:
react18:
react: ^18.3.0
react-dom: ^18.3.0
// apps/web/package.json — reference catalog versions
{
"dependencies": {
"react": "catalog:", // Uses ^19.0.0 from default catalog
"react-dom": "catalog:",
"zod": "catalog:"
}
}
// packages/legacy-widget/package.json
{
"dependencies": {
"react": "catalog:react18", // Uses ^18.3.0 from named catalog
"react-dom": "catalog:react18"
}
}
.npmrc Configuration
# .npmrc — project-level pnpm settings
# Strictness
strict-peer-dependencies=false
auto-install-peers=true
# Hoisting (needed for some tools like Next.js)
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
shamefully-hoist=false # Keep strict by default
# Performance
store-dir=~/.pnpm-store # Default content-addressable store location
prefer-frozen-lockfile=true
# Registry
registry=https://registry.npmjs.org/
# Scoped registries
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Patching Packages
# Create a patch for a broken dependency
pnpm patch express@4.21.0 # Opens package in temp dir for editing
# ... make your edits ...
pnpm patch-commit /tmp/abc123 # Commits the patch
# Patches stored in package.json:
# "pnpm": { "patchedDependencies": { "express@4.21.0": "patches/express@4.21.0.patch" } }
Overrides (Dependency Resolution)
// package.json — force specific versions across the tree
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21", // Force version everywhere
"foo>bar": "^2.0.0", // Override only within foo's deps
"react": "$react" // Use the version from root package.json
},
"peerDependencyRules": {
"ignoreMissing": ["@babel/*"], // Suppress peer dep warnings
"allowedVersions": {
"react": "19" // Allow React 19 even if peer asks for 18
}
}
}
}
CI Optimization
# GitHub Actions — optimized pnpm setup
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm # Caches the pnpm store automatically
- run: pnpm install --frozen-lockfile
# For monorepos — only install affected packages
- run: pnpm install --frozen-lockfile --filter "...[origin/main]"
# Dockerfile — multi-stage with pnpm
FROM node:22-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prod
FROM base AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM base AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
Best Practices
- Always commit
pnpm-lock.yaml— ensures reproducible installs across environments - Use
--frozen-lockfilein CI — prevents accidental lockfile drift in pipelines - Use
workspace:*for internal deps — keeps workspace packages linked automatically - Use catalogs for shared versions — eliminates version drift across workspace packages
- Avoid
shamefully-hoist— it defeats pnpm's strict isolation; fix the root cause instead - Use
pnpm dlxinstead ofnpx— respects pnpm's store and is more predictable - Enable
corepackfor CI —corepack enablepins the pnpm version frompackageManagerfield - Use filter patterns for monorepo commands —
--filteris more precise than-r - Set
packageManagerin rootpackage.json—"packageManager": "pnpm@9.15.0"for corepack
Anti-Patterns
| Anti-Pattern | Why It's Bad | Do Instead |
|---|---|---|
shamefully-hoist=true globally |
Breaks pnpm's isolation model | Hoist specific packages with public-hoist-pattern |
| Not committing lockfile | Non-reproducible installs | Always commit pnpm-lock.yaml |
pnpm install in CI without --frozen-lockfile |
Can silently update lockfile | Always use --frozen-lockfile |
Using npm: or yarn: in scripts |
Mixes package managers | Stick to pnpm exclusively |
| Manual version sync in monorepo | Versions drift between packages | Use catalog: in pnpm-workspace.yaml |
| Installing everything globally | Pollutes system, version conflicts | Use pnpm dlx for one-off tools |
| Ignoring peer dependency warnings | Hidden runtime compatibility bugs | Fix peers or explicitly allow versions |
Running pnpm -r build without filter |
Builds everything, slow CI | Use --filter to target affected packages |
Decision Guide
| Scenario | Recommendation |
|---|---|
| New project, single package | pnpm init + standard workflow |
| Monorepo with shared packages | pnpm-workspace.yaml + workspace:* |
| Monorepo with version alignment | Use catalogs in pnpm-workspace.yaml |
| CI pipeline caching | pnpm/action-setup + actions/setup-node with cache: pnpm |
| Docker builds | Multi-stage with --mount=type=cache for the store |
| Need to patch a broken dep | pnpm patch + pnpm patch-commit |
| Force a transitive dep version | Use pnpm.overrides in root package.json |
| Deploying monorepo app | pnpm deploy --filter @myorg/web for isolated output |