Mario Brusarosco

pnpm

In the ground since Sun Nov 16 2025

Last watered inSun Nov 16 2025

Related Topics

Referece Links

Recap

pnpm (performant npm) is a fast, disk space efficient package manager that uses a content-addressable store and symlinks to manage dependencies. In monorepo contexts, it provides workspace protocol features that allow packages to reference each other locally without publishing to npm.

Key Achievement: Understanding how pnpm's workspace protocol enables local package linking in monorepos, and how this integrates with Turborepo's build orchestration.

What is pnpm?

pnpm is an alternative to npm and yarn that solves several problems with traditional Node.js package managers:

The Traditional npm Problem

npm and yarn (classic):

1node_modules/
2├── project-a/
3│ └── node_modules/
4│ └── lodash/ (copy 1)
5└── project-b/
6 └── node_modules/
7 └── lodash/ (copy 2)

Problems:

  1. Disk space waste - Same package installed multiple times
  2. Slow installs - Copying files repeatedly
  3. Phantom dependencies - Can import packages not in package.json
  4. Hoisting issues - Flattening creates version conflicts

The pnpm Solution

pnpm uses a content-addressable store:

1~/.pnpm-store/
2└── v3/
3 └── files/
4 └── ab/
5 └── 1234.../lodash@4.17.21/ (single copy)
6
7project/
8└── node_modules/
9 └── .pnpm/
10 └── lodash@4.17.21/
11 └── node_modules/
12 └── lodash/ → symlink to ~/.pnpm-store

Benefits:

  1. One global store - Each package version stored once
  2. Hard links - Files reference the store (instant, no copies)
  3. Strict dependencies - Can only import what's in package.json
  4. Fast - 2x faster than npm, comparable to yarn

How pnpm Symlinks Work

When you run pnpm install:

  1. Download to store: Package goes to ~/.pnpm-store/v3/files/...
  2. Create virtual store: In node_modules/.pnpm/package@version/
  3. Hard link: Virtual store hard-links to global store
  4. Symlink: node_modules/package/ symlinks to virtual store

Example:

1# What you import
2import lodash from 'lodash';
3
4# Actual path resolution
5node_modules/lodash
6 node_modules/.pnpm/lodash@4.17.21/node_modules/lodash
7 ~/.pnpm-store/v3/files/.../lodash

Why this matters:

  • Installing same package in 10 projects = 1 copy on disk
  • Changing project dependencies = just update symlinks (fast!)
  • No phantom dependencies - strict resolution

Link Local Package into Turborepo

When building a monorepo package that depends on another local package, you need to tell pnpm to link them together instead of fetching from npm.

The Problem

We're building @peek-a-boo/react-sdk which needs types from @peek-a-boo/core:

1// src/client/types.ts
2import type { FeatureFlag, Environment } from '@peek-a-boo/core';
3// ^^^^^^^^^^^^^^^^^^
4// How does this resolve?

Without workspace linking:

  • TypeScript error: "Cannot find module '@peek-a-boo/core'"
  • pnpm would try to fetch from npm registry (doesn't exist yet)

The Solution: workspace:* Protocol

Add the local package as a dependency using pnpm's workspace protocol:

1// packages/react-sdk/package.json
2{
3 "name": "@peek-a-boo/react-sdk",
4 "dependencies": {
5 "@peek-a-boo/core": "workspace:*"
6 // ^^^^^^^^^^^^
7 // pnpm workspace protocol
8 }
9}

What workspace:* Means

workspace: - Protocol telling pnpm "this is a local package in the monorepo"

* - Version range meaning "use whatever version the local package defines"

Alternatives:

1"@peek-a-boo/core": "workspace:*" // Any version (most flexible)
2"@peek-a-boo/core": "workspace:^" // Compatible version
3"@peek-a-boo/core": "workspace:~" // Patch version
4"@peek-a-boo/core": "workspace:1.0.0" // Exact version

Recommendation: Use workspace:* for monorepo packages - lets you change versions freely.

Step-by-Step: What I Did

Step 1: Identify the Missing Dependency

Created src/client/types.ts with import:

1import type { FeatureFlag, Environment } from '@peek-a-boo/core';

Ran type check:

1pnpm run type:check
2# Error: Cannot find module '@peek-a-boo/core'

Step 2: Add to package.json

Edited packages/react-sdk/package.json:

1{
2 "name": "@peek-a-boo/react-sdk",
3 "version": "0.0.1",
4
5 // Added this section
6 "dependencies": {
7 "@peek-a-boo/core": "workspace:*"
8 },
9
10 "peerDependencies": {
11 "react": ">=16.8.0"
12 },
13 // ...
14}

Why in dependencies, not devDependencies?

  • @peek-a-boo/core types are needed at runtime (TypeScript consumers need them)
  • devDependencies = only needed during development
  • dependencies = bundled or needed by consumers

Step 3: Run pnpm install

1cd packages/react-sdk
2pnpm install

What happened:

  1. pnpm reads workspace:*

    • Looks for @peek-a-boo/core in workspace
    • Finds it at packages/core
  2. Creates symlink

    1packages/react-sdk/node_modules/@peek-a-boo/core →
    2 ../../core
  3. Outputs

    1Scope: all 5 workspace projects ← pnpm scans entire workspace
    2+10 -117 ← Added 10 new, removed 117 unused

Step 4: Verify It Works

1pnpm run type:check
2# ✅ No errors - TypeScript can now resolve @peek-a-boo/core

Check the actual symlink:

1ls -la node_modules/@peek-a-boo/
2# core -> ../../core ← Symlink created by pnpm

How This Integrates with Turborepo

pnpm handles linking, Turborepo handles build order.

The Build Dependency Graph

Once pnpm creates the symlinks, Turborepo reads package.json dependencies to build a graph:

1turbo.json:
2{
3 "pipeline": {
4 "build": {
5 "dependsOn": ["^build"] ← "^" means "dependencies first"
6 }
7 }
8}

When you run pnpm build:

  1. Turborepo reads the graph:

    1react-sdk depends on core
    2
    3Must build core first
  2. Build order:

    1[1/2] Building @peek-a-boo/core...
    2[2/2] Building @peek-a-boo/react-sdk...
  3. Caching:

    • If core hasn't changed, Turborepo uses cached output
    • Only rebuilds what changed

The Complete Flow

11. Developer adds dependency:
2 "dependencies": { "@peek-a-boo/core": "workspace:*" }
3
42. pnpm install:
5 node_modules/@peek-a-boo/core → ../../core (symlink)
6
73. TypeScript compiles:
8 import from '@peek-a-boo/core' → resolves via symlink
9
104. Turborepo builds:
11 Reads dependency → builds core → builds react-sdk
12
135. Runtime (published package):
14 workspace:* → replaced with actual version (e.g., "^1.0.0")

Common Patterns

Pattern 1: Shared Types (Our Use Case)

1// packages/react-sdk/package.json
2{
3 "dependencies": {
4 "@peek-a-boo/core": "workspace:*" // Import shared types
5 }
6}

Pattern 2: Shared Utilities

1// apps/dashboard/package.json
2{
3 "dependencies": {
4 "@peek-a-boo/core": "workspace:*", // Database access
5 "@peek-a-boo/ui": "workspace:*" // Shared UI components
6 }
7}

Pattern 3: Development Tools

1// packages/eslint-config/package.json
2{
3 "devDependencies": {
4 "@peek-a-boo/typescript-config": "workspace:*" // Shared TS config
5 }
6}

Adding Packages in a Monorepo

One common question: Where do I run pnpm add? At the root or in the specific package?

The answer depends on who needs the package.

Option 1: Add to Specific Package (Most Common)

When to use: Package is only needed by ONE workspace package.

Example: Adding vite-plugin-dts to react-sdk (TypeScript declaration generator for Vite).

Method A: Using --filter (Recommended)

Run from anywhere in the monorepo:

1# From root or any directory
2pnpm add -D vite-plugin-dts --filter=@peek-a-boo/react-sdk
3# ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4# devDependency Target package

What happens:

  1. pnpm finds the package by name (@peek-a-boo/react-sdk)
  2. Adds vite-plugin-dts to its devDependencies
  3. Installs only for that package
  4. Updates package.json automatically

Result:

1// packages/react-sdk/package.json
2{
3 "devDependencies": {
4 "vite-plugin-dts": "^0.10.0" // ← Added here
5 }
6}

Method B: Change Directory First

1# Navigate to package first
2cd packages/react-sdk
3
4# Then add normally
5pnpm add -D vite-plugin-dts

When to use Method B:

  • You're already working in that directory
  • Running multiple package-specific commands
  • Simpler for quick local development

When to use Method A:

  • Running from root (common with scripts)
  • Want to stay in current directory
  • Adding to multiple packages in sequence

Option 2: Add to Root (Shared Dependencies)

When to use: Package is needed by MULTIPLE workspace packages, or for workspace-wide tooling.

Example: Shared Dev Tools

1# From root directory
2pnpm add -D -w typescript
3# ^^
4# --workspace-root flag

What the -w flag does:

  • -w or --workspace-root tells pnpm "install at workspace root"
  • Without it, pnpm refuses (safety measure - prevents accidental root installs)

When to install at root:

Good use cases:

1# Linting/formatting (used by all packages)
2pnpm add -D -w eslint prettier
3
4# TypeScript (workspace-wide config)
5pnpm add -D -w typescript
6
7# Turborepo itself
8pnpm add -D -w turbo
9
10# Testing tools (if shared across packages)
11pnpm add -D -w vitest @vitest/ui

Bad use cases:

1# React (only some packages use it)
2pnpm add -w react # ❌ Should be in specific packages
3
4# Vite (only build packages need it)
5pnpm add -D -w vite # ❌ Should be in packages that use Vite

Rule of thumb: If it's in the root package.json, it should be used by ALL or MOST packages.

Real-World Example: Adding vite-plugin-dts

Let's add vite-plugin-dts to the react-sdk (needed for TypeScript declaration generation).

Step 1: Decide Where It Goes

Question: Who needs vite-plugin-dts?

  • Answer: Only @peek-a-boo/react-sdk (it's a Vite plugin for our library build)

Decision: Add to react-sdk package, not root.

Step 2: Choose Method

Using --filter (from root):

1pnpm add -D vite-plugin-dts --filter=@peek-a-boo/react-sdk

Or cd first:

1cd packages/react-sdk
2pnpm add -D vite-plugin-dts

Step 3: Verify Installation

1# Check package.json was updated
2cat packages/react-sdk/package.json | grep vite-plugin-dts
3
4# Output:
5# "vite-plugin-dts": "^0.10.0"

Step 4: Use in Code

1// packages/react-sdk/vite.config.ts
2import { defineConfig } from 'vitest/config';
3import dts from 'vite-plugin-dts'; // ← Now available
4
5export default defineConfig({
6 plugins: [
7 dts({
8 insertTypesEntry: true,
9 rollupTypes: true
10 })
11 ],
12 // ...
13});

Quick Reference

| Command | Where it installs | When to use | |---------|-------------------|-------------| | pnpm add pkg --filter=@scope/name | Specific package (from anywhere) | Most common - package-specific deps | | cd path/to/pkg && pnpm add pkg | Specific package (after cd) | When already in package directory | | pnpm add -D -w pkg | Workspace root | Shared tooling (ESLint, TypeScript, etc.) | | pnpm add -D pkg (from root) | ❌ Error | Safety - use -w flag explicitly |

Common Patterns

Adding Multiple Packages

1# Add multiple to same package
2pnpm add -D prettier eslint --filter=@peek-a-boo/react-sdk
3
4# Add to multiple packages at once
5pnpm add -D vitest --filter=@peek-a-boo/react-sdk --filter=@peek-a-boo/core

Check What's Installed Where

1# List all dependencies in a specific package
2pnpm list --filter=@peek-a-boo/react-sdk
3
4# Show dependency tree
5pnpm list --filter=@peek-a-boo/react-sdk --depth=1
6
7# Find where a package is installed
8pnpm list -r vite-plugin-dts
9# -r = recursive (searches all workspace packages)

Remove Packages

1# Remove from specific package
2pnpm remove vite-plugin-dts --filter=@peek-a-boo/react-sdk
3
4# Remove from root
5pnpm remove -w typescript

Troubleshooting

Error: "Running this command will add the dependency to the workspace root"

1pnpm add eslint
2# Error: Use -w flag for workspace root

Fix: Add -w flag if you really want root install:

1pnpm add -D -w eslint

Package not found after install:

1# Clean and reinstall
2pnpm install
3
4# If still broken, clean everything
5rm -rf node_modules
6pnpm install

Wrong package.json was updated:

1# Always verify with --filter
2pnpm add -D pkg --filter=correct-package-name
3
4# Check where it went
5git diff # Shows what changed

Key Takeaways

  1. pnpm is fast and efficient - Uses symlinks + hard links to global store
  2. workspace:* - Protocol for linking local monorepo packages
  3. pnpm handles linking - Creates symlinks in node_modules
  4. Turborepo handles builds - Orchestrates build order based on those links
  5. Single source of truth - No duplicate types, shared code lives in one place

Used in Projects

  • Peek-a-boo monorepo - All packages use workspace:* for internal dependencies
  • Pattern applicable to any pnpm workspace + Turborepo setup

Debugging Tips

Check if symlink exists:

1ls -la packages/react-sdk/node_modules/@peek-a-boo/
2# Should show: core -> ../../core

Verify workspace resolution:

1pnpm list @peek-a-boo/core
2# Should show: @peek-a-boo/core 0.1.0 → link:../../core

Force reinstall:

1rm -rf node_modules
2pnpm install

Check Turborepo graph:

1pnpm dlx turbo run build --graph
2# Opens visualization of build dependencies