Skip to main content
Technology & EngineeringMonorepo224 lines

Pnpm Workspaces

Managing monorepo packages with pnpm workspaces including linking, filtering, and dependency hoisting

Quick Summary18 lines
You are an expert in pnpm workspaces for managing monorepo package structure, dependency resolution, and cross-package linking.

## Key Points

- 'packages/*'
- 'tools/*'
- '!**/test-fixtures/**'
- `workspace:*` — Any version; resolves to the current version on publish.
- `workspace:^` — Resolves to `^x.y.z` on publish.
- `workspace:~` — Resolves to `~x.y.z` on publish.
- 'packages/*'
1. **Always use `workspace:` protocol** — Ensures internal packages link correctly and resolve properly on publish.
2. **Set `packageManager` field** — Use Corepack to enforce the exact pnpm version across the team.
3. **Keep `shamefully-hoist=false`** — Default strict mode catches missing dependencies that would break in production.
4. **Use `public-hoist-pattern` selectively** — Only hoist packages that require it (like ESLint plugins) instead of using `shamefully-hoist`.
5. **Use catalogs for shared versions** — Centralize common dependency versions to avoid drift across packages.
skilldb get monorepo-skills/Pnpm WorkspacesFull skill: 224 lines
Paste into your CLAUDE.md or agent config

pnpm Workspaces — Monorepo Management

You are an expert in pnpm workspaces for managing monorepo package structure, dependency resolution, and cross-package linking.

Core Philosophy

Overview

pnpm workspaces provide native monorepo support through a content-addressable store, strict dependency isolation, and workspace protocol linking. pnpm's approach is faster and more disk-efficient than npm or Yarn, and its strictness catches phantom dependency issues that other package managers miss.

Setup & Configuration

Workspace Definition

Create pnpm-workspace.yaml at the repo root:

packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'
  # Exclude test fixtures
  - '!**/test-fixtures/**'

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "clean": "pnpm -r exec rm -rf dist node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=9"
  },
  "packageManager": "pnpm@9.1.0"
}

.npmrc Configuration

# Hoist specific packages needed by tools that don't support strict mode
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

# Prevent phantom dependencies
shamefully-hoist=false

# Strict peer dependencies
strict-peer-dependencies=true

# Ensure lockfile is up to date in CI
frozen-lockfile=true

Core Patterns

Workspace Protocol

Reference internal packages with the workspace: protocol:

{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^",
    "@myorg/config": "workspace:~"
  }
}
  • workspace:* — Any version; resolves to the current version on publish.
  • workspace:^ — Resolves to ^x.y.z on publish.
  • workspace:~ — Resolves to ~x.y.z on publish.

Filtering Commands

# Run in a specific package
pnpm --filter @myorg/web dev

# Run in a package and all its dependencies
pnpm --filter @myorg/web... build

# Run in dependents of a package (reverse dependencies)
pnpm --filter ...@myorg/ui build

# Run in packages changed since main
pnpm --filter "...[main]" test

# Run in all packages under apps/
pnpm --filter "./apps/**" lint

# Exclude a package
pnpm --filter "!@myorg/docs" build

Adding Dependencies

# Add to a specific workspace package
pnpm --filter @myorg/web add react

# Add a dev dependency to root
pnpm add -D typescript -w

# Add an internal package as dependency
pnpm --filter @myorg/web add @myorg/ui --workspace

# Add to multiple packages
pnpm --filter "@myorg/web" --filter "@myorg/api" add zod

Recursive Commands

# Run a script in all packages that have it
pnpm -r run build

# Run in all packages, topological order
pnpm -r --sort run build

# Run with concurrency limit
pnpm -r --workspace-concurrency=4 run test

# Execute a command in all packages
pnpm -r exec -- rm -rf dist

Catalogs (pnpm 9+)

Centralize dependency versions across the monorepo in pnpm-workspace.yaml:

packages:
  - 'apps/*'
  - 'packages/*'

catalog:
  react: ^18.3.0
  react-dom: ^18.3.0
  typescript: ^5.5.0
  vitest: ^2.0.0

Then in any package:

{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  }
}

Publishing Configuration

{
  "name": "@myorg/ui",
  "version": "1.2.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

Best Practices

  1. Always use workspace: protocol — Ensures internal packages link correctly and resolve properly on publish.
  2. Set packageManager field — Use Corepack to enforce the exact pnpm version across the team.
  3. Keep shamefully-hoist=false — Default strict mode catches missing dependencies that would break in production.
  4. Use public-hoist-pattern selectively — Only hoist packages that require it (like ESLint plugins) instead of using shamefully-hoist.
  5. Use catalogs for shared versions — Centralize common dependency versions to avoid drift across packages.
  6. Use frozen-lockfile in CI — Prevents accidental lockfile changes during CI runs.
  7. Scope packages with @org/ — Namespaced packages avoid conflicts and make internal vs. external deps clear.

Common Pitfalls

  • Phantom dependencies — pnpm's strict isolation means you must declare every dependency. Code that works with npm/Yarn may fail with pnpm because undeclared transitive deps are not accessible.
  • Peer dependency warnings — pnpm is strict about peers by default. Use pnpm config set auto-install-peers true or resolve them explicitly.
  • Forgetting --workspace flag — Running pnpm add @myorg/ui without --workspace fetches from the registry instead of linking locally.
  • Not using -w for root depspnpm add typescript fails at the root; you must pass -w (workspace root) explicitly.
  • Lockfile conflictspnpm-lock.yaml conflicts are common in monorepos. Use pnpm install --no-frozen-lockfile to regenerate, then commit the resolved lockfile.

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 monorepo-skills

Get CLI access →