Skip to main content
Technology & EngineeringPackage Management276 lines

Pnpm

pnpm workspace management for monorepos with content-addressable storage and strict dependency isolation

Quick Summary35 lines
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 lines
Paste into your CLAUDE.md or agent config

pnpm — 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.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.
  • Use catalogs (pnpm 9+) to centralize version management across the monorepo.
  • Set engine-strict=true and declare packageManager in root package.json via corepack.
  • Run pnpm dedupe periodically to reduce duplicate transitive dependency versions.
  • Use pnpm licenses list to 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=hoisted in .npmrc as a last resort, but this loses strict isolation.
  • Peer dependency warnings flooding CI: Set strict-peer-dependencies=false if 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., using npm publish directly), the published package will be broken. Always publish through pnpm publish.
  • Missing --workspace flag: Running pnpm add @myorg/utils without --workspace installs 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 running pnpm install to regenerate, then verifying CI passes.
  • Forgetting -w for root dependencies: Running pnpm add -D typescript at the root without -w fails 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

Get CLI access →