|$ curl https://forge-ai.dev/api/markdown?path=docs/npm/pnpm
$cat docs/pnpm-deep-dive.md
updated Today·24-32 min read·published

pnpm Deep Dive

pnpmMonorepoStoreIntermediate🎯Free Tools
Introduction

pnpm (performant npm) stores every package version once in a content-addressable global store and hard-links files into project node_modules. The on-disk layout is a non-flat, symlink-based structure that prevents accessing undeclared dependencies — the opposite of npm's permissive hoist.

That combination — disk efficiency + strictness + excellent workspace filtering — is why pnpm wins most large monorepos. See the comparison matrix on npm vs Yarn vs pnpm vs Bun.

📝

note

Install pnpm via Corepack and pin "packageManager": "pnpm@…" so every machine uses the same major.
How / Why / Requirements
QuestionAnswer
Why pnpm?Shared store, hard links, strict deps, fastest practical monorepo workflows, catalogs
How?Corepack enable → set packageManager → pnpm install → commit pnpm-lock.yaml
RequirementsFilesystem that supports hard links (local disk; some network FS break); Node + Corepack
When not?Broken tools that need flat hoist and you cannot use shamefully-hoist / public-hoist-pattern
Content-Addressable Store

Each package tarball is unpacked into the store keyed by content hash. Projects never duplicate package bytes for the same version — they hard-link into node_modules/.pnpm and symlink package names into place.

store.sh
Bash
1pnpm store path # where the global store lives
2pnpm store status # verify integrity / modified packages
3pnpm store prune # remove unreferenced packages
4
5# Optional: relocate store (CI cache mounts)
6pnpm config set store-dir /var/cache/pnpm-store

info

In CI, cache the store directory keyed by lockfile hash. Combined with --frozen-lockfile, installs become mostly hard-link operations.
Peer Dependency Strictness

pnpm is strict about peers. Missing or mismatched peer dependencies produce errors (not quiet hoisting). That forces correct package.json declarations — valuable in monorepos.

.npmrc
INI
1# Default is strict; relax only when migrating legacy trees
2auto-install-peers=true
3strict-peer-dependencies=true
4# Temporary escape during migration:
5# strict-peer-dependencies=false

warning

Turning off strict peers hides real bugs. Prefer adding peers / using pnpm.peerDependencyRules for known false positives.
shamefully-hoist & Hoist Patterns

Some tools (older ESLint plugin resolvers, frameworks expecting flat trees) break under isolation. pnpm offers controlled hoisting escapes.

.npmrc
INI
1# Nuclear option — hoist everything (loses isolation)
2shamefully-hoist=true
3
4# Prefer targeted patterns:
5public-hoist-pattern[]=*types*
6public-hoist-pattern[]=*eslint*
7public-hoist-pattern[]=*prettier*
8hoist-pattern[]=*
SettingWhenCost
shamefully-hoistLegacy toolchain demands flat treePhantom deps return
public-hoist-patternHoist specific packages to rootLimited leakage
packageExtensions / depsMissing peer/dep declarationsBest long-term fix

best practice

Start strict. Add hoist patterns only for packages that fail. Revisit after major upgrades — many tools no longer need shameful hoisting.
.npmrc for pnpm

pnpm reads .npmrc (and pnpm-workspace.yaml for workspace + catalogs). Commit project-level settings; keep auth tokens in user-level or CI secrets.

.npmrc
INI
1engine-strict=true
2auto-install-peers=true
3strict-peer-dependencies=true
4shamefully-hoist=false
5# registry overrides for scopes
6@acme:registry=https://npm.pkg.github.com
7//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
pnpm-workspace.yaml
pnpm-workspace.yaml
YAML
1packages:
2 - "apps/*"
3 - "packages/*"
4 - "!**/test/**"
5 - "!packages/deprecated"
filter.sh
Bash
1pnpm --filter @acme/web build
2pnpm --filter "./packages/**" test
3pnpm --filter "...@acme/web" build # web + dependencies
4pnpm --filter "@acme/web..." test # web + dependents
5pnpm -r run lint # recursive all packages
6pnpm --filter @acme/web add zod
🔥

pro tip

Filter syntax with ... (dependents/dependencies) is the killer feature for large monorepos — run only the affected subgraph in CI.
Catalogs

Catalogs centralize dependency versions for the whole workspace. Packages reference catalog: instead of repeating version strings.

pnpm-workspace.yaml
YAML
1packages:
2 - "packages/*"
3catalog:
4 react: ^19.0.0
5 typescript: ^5.7.0
6 zod: ^3.24.0
packages/ui/package.json
JSON
1{
2 "name": "@acme/ui",
3 "dependencies": {
4 "react": "catalog:",
5 "zod": "catalog:"
6 },
7 "devDependencies": {
8 "typescript": "catalog:"
9 }
10}

info

Catalogs replace many uses of Yarn constraints for "one React version" policies with a simpler shared version table.
Overrides

Force a transitive dependency version for security patches or broken ranges. In pnpm this lives under pnpm.overrides in the root package.json.

package.json
JSON
1{
2 "pnpm": {
3 "overrides": {
4 "lodash": "4.17.21",
5 "foo>bar": "2.0.0"
6 },
7 "peerDependencyRules": {
8 "allowedVersions": {
9 "react": "19"
10 },
11 "ignoreMissing": ["@types/react"]
12 }
13 }
14}
pnpm patch
patch.sh
Bash
1pnpm patch some-pkg@1.2.3
2# edit the printed temporary folder
3pnpm patch-commit /tmp/path-to-edit
4# records patch under patches/ and package.json pnpm.patchedDependencies
📝

note

Prefer upstream PRs. Keep patches tiny and re-verify on every upgrade.
pnpm dlx & exec
dlx.sh
Bash
1pnpm dlx create-vite@latest
2pnpm dlx cowsay "strict by default"
3pnpm exec tsc --noEmit # local binary
4pnpm create vite my-app # create-* shortcut
Why pnpm Wins Monorepos
AdvantageDetail
DiskOne store for all packages and all checkouts
StrictnessNo accidental cross-package phantom imports
FilteringPowerful --filter graph queries for CI
CatalogsCentral versions without custom constraint DSLs
CompatibilityStill provides node_modules (unlike default PnP)
ci-monorepo.sh
Bash
1pnpm install --frozen-lockfile
2pnpm --filter "...[origin/main]" run test
3# run tests for packages changed since main, plus dependents
Migrating from npm
migrate.sh
Bash
1corepack enable
2corepack prepare pnpm@9.15.0 --activate
3pnpm import # convert package-lock.json
4rm package-lock.json
5pnpm install
6# fix missing deps that npm hoisting hid
7pnpm why <pkg>
8# add undeclared imports as real dependencies
9npm pkg set packageManager=pnpm@9.15.0
10npm pkg set scripts.preinstall="npx only-allow pnpm"
Symptom after migrateCauseFix
Cannot find module XPhantom deppnpm add X in the right package
ESLint plugin resolve failNeeds hoistpublic-hoist-pattern for eslint*
Peer dependency errorsStrict peersInstall peers or peerDependencyRules
Essential Commands
CommandPurpose
pnpm install --frozen-lockfileCI install; fail on drift
pnpm add / add -DAdd runtime / dev deps
pnpm update --latestBump within policy
pnpm outdatedList available updates
pnpm auditSecurity advisories
pnpm deploy <dir>Deployable subset from workspace
Adoption Checklist
  • Pin pnpm with Corepack packageManager
  • Commit pnpm-lock.yaml and workspace config
  • Use --frozen-lockfile in CI
  • Cache the store directory
  • Introduce catalogs for shared libs
  • Prefer filters over running everything always
  • Document hoist escapes in README
  • Block other managers with only-allow

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.