Pnpm
pnpm workspace management for monorepos with content-addressable storage and strict dependency isolation
You are an expert in pnpm workspace management, including its content-addressable store, strict node_modules structure, workspace protocol, and monorepo orchestration. ## Key Points - 'packages/*' - '!**/test/**' - 'packages/*' - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 - run: pnpm install --frozen-lockfile - Use `pnpm-workspace.yaml` to define all workspace package locations explicitly. - Prefer `workspace:*` for internal dependencies so they always resolve to the local version. - Use `--frozen-lockfile` in CI to ensure reproducible installs. - Set `public-hoist-pattern` only for tools that require hoisting (ESLint plugins, TypeScript types). - Use filters with `...` suffixes to build packages in dependency order. - Use `pnpm --filter '...[origin/main]'` to only build/test packages affected by changes. ## Quick Example ```yaml packages: - 'packages/*' - 'apps/*' - '!**/test/**' ``` ```ini # Auto-install peer dependencies (pnpm 8+) auto-install-peers=true # Resolve peer dependency conflicts strict-peer-dependencies=false ```
skilldb get package-management-skills/PnpmFull skill: 276 linespnpm — Package Management
You are an expert in pnpm workspace management, including its content-addressable store, strict node_modules structure, workspace protocol, and monorepo orchestration.
Core Philosophy
Overview
pnpm is a fast, disk-efficient package manager that uses a content-addressable store and hard links to avoid duplicating packages across projects. Its workspace feature enables monorepo management with strict dependency isolation, preventing phantom dependency access that plagues flat node_modules layouts.
Core Concepts
Content-Addressable Store
pnpm stores every version of every package exactly once in a global content-addressable store (typically ~/.local/share/pnpm/store). Projects link to the store via hard links, so disk usage is a fraction of npm/yarn equivalents.
# Check store status
pnpm store status
# Prune unreferenced packages
pnpm store prune
# Custom store path
pnpm install --store-dir /path/to/store
Strict node_modules Structure
pnpm creates a non-flat node_modules layout. Each package can only access dependencies it explicitly declares in its package.json. This eliminates phantom dependencies (packages that work by accident because a transitive dependency hoisted them).
node_modules/
.pnpm/
react@18.3.1/
node_modules/
react/ -> hard link to store
lodash@4.17.21/
node_modules/
lodash/ -> hard link to store
react/ -> symlink to .pnpm/react@18.3.1/node_modules/react
lodash/ -> symlink to .pnpm/lodash@4.17.21/node_modules/lodash
Workspace Protocol
The workspace: protocol references sibling packages in a monorepo without publishing:
{
"dependencies": {
"@myorg/shared": "workspace:*",
"@myorg/utils": "workspace:^1.0.0"
}
}
At publish time, pnpm automatically replaces workspace:* with the actual version.
Implementation Patterns
Workspace Setup
Create pnpm-workspace.yaml at the repo root:
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'
Monorepo structure:
my-monorepo/
pnpm-workspace.yaml
package.json
packages/
shared/
package.json
utils/
package.json
apps/
web/
package.json
api/
package.json
Filtering Commands
pnpm provides powerful filtering to target specific packages:
# Run build in a specific package
pnpm --filter @myorg/web build
# Run tests in all packages under packages/
pnpm --filter './packages/**' test
# Run in a package and all its dependencies
pnpm --filter @myorg/web... build
# Run in all packages that depend on @myorg/shared
pnpm --filter ...@myorg/shared build
# Run in packages changed since main
pnpm --filter '...[origin/main]' test
# Run in a package and its dependents
pnpm --filter ...^@myorg/shared build
Adding Dependencies
# Add to a specific workspace package
pnpm --filter @myorg/web add react
# Add a workspace sibling as dependency
pnpm --filter @myorg/web add @myorg/shared --workspace
# Add dev dependency to root
pnpm add -D -w typescript
# Add to multiple packages
pnpm --filter '@myorg/*' add zod
.npmrc Configuration
Configure pnpm behavior in .npmrc at the repo root:
# Hoist types and eslint plugins so tools can find them
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
# Require all dependencies to be declared
strict-peer-dependencies=true
# Use a shared lockfile (default in workspaces)
shared-workspace-lockfile=true
# Allow only pnpm as package manager
engine-strict=true
# Side effects cache for native modules
side-effects-cache=true
Peer Dependency Handling
# Auto-install peer dependencies (pnpm 8+)
auto-install-peers=true
# Resolve peer dependency conflicts
strict-peer-dependencies=false
Catalog (pnpm 9+)
Catalogs let you define shared dependency versions across workspace packages:
# pnpm-workspace.yaml
packages:
- 'packages/*'
catalog:
react: ^18.3.0
typescript: ^5.5.0
zod: ^3.23.0
Then in any workspace package:
{
"dependencies": {
"react": "catalog:",
"zod": "catalog:"
}
}
Task Orchestration with Scripts
{
"scripts": {
"build": "pnpm -r --filter './packages/**' run build",
"build:affected": "pnpm --filter '...[origin/main]' run build",
"test": "pnpm -r run test",
"lint": "pnpm -r --parallel run lint",
"clean": "pnpm -r exec rm -rf dist node_modules"
}
}
The -r flag runs across all workspace packages. pnpm respects the dependency graph by default, running builds in topological order.
CI Caching
# GitHub Actions
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
Override Dependencies
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21",
"got@<12": "^12.0.0"
},
"peerDependencyRules": {
"ignoreMissing": ["@babel/*"],
"allowAny": ["eslint"]
}
}
}
Best Practices
- Use
pnpm-workspace.yamlto define all workspace package locations explicitly. - Prefer
workspace:*for internal dependencies so they always resolve to the local version. - Use
--frozen-lockfilein CI to ensure reproducible installs. - Set
public-hoist-patternonly for tools that require hoisting (ESLint plugins, TypeScript types). - Use filters with
...suffixes to build packages in dependency order. - Use
pnpm --filter '...[origin/main]'to only build/test packages affected by changes. - Use catalogs (pnpm 9+) to centralize version management across the monorepo.
- Set
engine-strict=trueand declarepackageManagerin rootpackage.jsonvia corepack. - Run
pnpm dedupeperiodically to reduce duplicate transitive dependency versions. - Use
pnpm licenses listto audit license compliance across all dependencies.
Common Pitfalls
- Phantom dependency errors: Migrating from npm/yarn, code that imported undeclared (hoisted) dependencies will break. This is by design. Add the missing dependency explicitly.
- Symlink-incompatible tools: Some tools do not follow symlinks properly. Use
node-linker=hoistedin.npmrcas a last resort, but this loses strict isolation. - Peer dependency warnings flooding CI: Set
strict-peer-dependencies=falseif warnings are noise, but investigate real conflicts. - Workspace protocol in published packages: If you publish with
workspace:*references without letting pnpm rewrite them (e.g., usingnpm publishdirectly), the published package will be broken. Always publish throughpnpm publish. - Missing
--workspaceflag: Runningpnpm add @myorg/utilswithout--workspaceinstalls from the registry, not the local workspace copy. - Lockfile conflicts in large teams: Frequent merge conflicts in
pnpm-lock.yaml. Resolve by deleting the lockfile in the branch and runningpnpm installto regenerate, then verifying CI passes. - Forgetting
-wfor root dependencies: Runningpnpm add -D typescriptat the root without-wfails because pnpm requires explicit intent to add root workspace dependencies.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add package-management-skills
Related Skills
Bundling Libraries
Bundling JavaScript/TypeScript libraries for distribution using tsup, unbuild, and Rollup
Dependency Audit
Security auditing npm dependencies for vulnerabilities, license compliance, and supply chain risks
Lockfile Management
Lock file strategies for deterministic installs across npm, pnpm, and Yarn
Npm Publishing
Publishing packages to the npm registry with proper configuration, access control, and release automation
Private Registries
Setting up and using private npm registries with Verdaccio, GitHub Packages, and GitLab Package Registry
Semantic Versioning
Semantic versioning (SemVer) conventions, version ranges, and strategies for managing breaking changes