In the ground since Sun Nov 16 2025
Last watered inSun Nov 16 2025
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:
- Disk space waste - Same package installed multiple times
- Slow installs - Copying files repeatedly
- Phantom dependencies - Can import packages not in package.json
- 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:
- One global store - Each package version stored once
- Hard links - Files reference the store (instant, no copies)
- Strict dependencies - Can only import what's in package.json
- Fast - 2x faster than npm, comparable to yarn
How pnpm Symlinks Work
When you run pnpm install:
- Download to store: Package goes to ~/.pnpm-store/v3/files/...
- Create virtual store: In node_modules/.pnpm/package@version/
- Hard link: Virtual store hard-links to global store
- 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:
-
pnpm reads workspace:*
- Looks for @peek-a-boo/core in workspace
- Finds it at packages/core
-
Creates symlink
1packages/react-sdk/node_modules/@peek-a-boo/core →
2 ../../core
-
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:
-
Turborepo reads the graph:
1react-sdk depends on core
2↓
3Must build core first
-
Build order:
1[1/2] Building @peek-a-boo/core...
2[2/2] Building @peek-a-boo/react-sdk...
-
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:
- pnpm finds the package by name (@peek-a-boo/react-sdk)
- Adds vite-plugin-dts to its devDependencies
- Installs only for that package
- 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:
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
- pnpm is fast and efficient - Uses symlinks + hard links to global store
- workspace:* - Protocol for linking local monorepo packages
- pnpm handles linking - Creates symlinks in node_modules
- Turborepo handles builds - Orchestrates build order based on those links
- 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