pnpm Workspaces: Managing Multiple Packages Without the Headaches
pnpm workspaces offer a fast, disk-efficient way to manage monorepos. Learn how to set up workspaces, share dependencies, and avoid the common pitfalls that trip up most teams.
Every frontend team eventually outgrows a single package.json. You start extracting shared utilities, then a component library, then a config package — and suddenly you're managing dependencies across five packages with npm or yarn, watching your node_modules consume 3GB of disk space. pnpm workspaces solve this elegantly with a content-addressable store and strict dependency isolation.
Why pnpm Over npm or Yarn Workspaces?
The core advantage is pnpm's storage model. Instead of duplicating packages across workspaces, pnpm uses hard links to a global content-addressable store. If five packages depend on React 19, there's one copy on disk, not five. On a large monorepo, this typically saves 50-70% disk space and makes installs significantly faster.
But the real win is strictness. pnpm creates a non-flat node_modules structure by default, which means packages can only access dependencies they explicitly declare. No more phantom dependencies — if it's not in your package.json, you can't import it.
Setting Up Your Workspace
Create a pnpm-workspace.yaml at the root of your project:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tools/*"
Then structure your monorepo with clear boundaries between apps and shared packages. Each directory listed in the workspace config gets its own package.json and can declare dependencies on other workspace packages using the workspace: protocol.
// apps/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^1.0.0",
"next": "^16.0.0",
"react": "^19.0.0"
}
}
The workspace:* protocol tells pnpm to always resolve to the local version. When you publish, pnpm automatically replaces it with the actual version number. The workspace:^ variant respects semver ranges, which is useful for packages you might eventually publish to npm.
Running Scripts Across Packages
pnpm's --filter flag is your primary tool for targeting specific packages. You can filter by name, by directory, or by what changed in git. Combined with the -r (recursive) flag, you can orchestrate builds across your entire workspace with surgical precision.
For CI pipelines, the git-based filter is particularly powerful. Running pnpm --filter "...[origin/main]" build builds only packages that changed since the main branch — and their dependents. This keeps CI fast as your monorepo grows.
Common Pitfalls
The most frequent issue teams hit is the peer dependency strictness. pnpm enforces peer dependencies by default, which surfaces real problems but can be noisy during migration. Set auto-install-peers=true in your .npmrc for a smoother transition, then tighten it later.
Another gotcha: hoisting. Some tools (notably React Native and certain bundlers) expect a flat node_modules. pnpm lets you selectively hoist packages via public-hoist-pattern in .npmrc, but the default strict behavior is what you want for most web projects.
pnpm + Turborepo: The Winning Combination
pnpm handles package management and dependency resolution. Turborepo handles task orchestration and caching. Together, they form the most popular monorepo stack in the frontend ecosystem. pnpm ensures correct, efficient dependency graphs while Turborepo ensures you never rebuild what hasn't changed.
Start with pnpm workspaces for your next multi-package project. The strict dependency model catches real bugs, the disk savings are substantial, and the migration from npm or yarn is straightforward. Your future self — and your CI bill — will thank you.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!