pnpm 05 📦 Managing Dependencies: Add, Update, and Remove Operations
The daily rhythm of package management is a three-command cycle: add what you need, update what has changed, and remove what is no longer used. pnpm’s commands for these operations follow a consistent pattern: they modify package.json, resolve the change into pnpm-lock.yaml, and update the virtual store and symlinks in node_modules. The critical difference from npm and Yarn lies in the flags that control where a dependency is placed and how the lockfile is written, particularly in the pnpm 10 era of strict dependency trees and default security blocking.
Key point: pnpm add installs and saves to dependencies by default. pnpm update respects the ranges in package.json unless --latest is passed. pnpm remove deletes from every dependency field unless a --save-* flag targets a specific field. All three commands update the lockfile and the symlink graph in tandem.
Why dependency operations differ in pnpm
The strict tree consequence. In npm or Yarn Classic, adding a dependency often causes hoisting to rearrange node_modules unpredictably. In pnpm, adding a package creates a new symlink at the root and a new hard-linked folder inside .pnpm. The rest of the tree remains untouched. This isolation means that adding a package cannot break a transitive dependency that was already working, because those dependencies were never hoisted to the root in the first place.
The lifecycle script block. pnpm 10 changed the default behavior of preinstall and postinstall scripts. Packages that rely on these scripts to build native code (like esbuild or sharp) will not run them automatically. The --allow-build flag must be used to explicitly permit a package to execute its install scripts, which also records the permission in pnpm-workspace.yaml for future installs . This is a supply-chain security measure that changes the workflow for native dependencies.
The workspace root check. Adding a dependency to the root of a workspace requires the -w flag (--ignore-workspace-root-check). Without it, pnpm refuses to modify the root package.json . This prevents accidental installation of tools into the root that should belong to a specific workspace package.
The update semantics. pnpm update without arguments respects the semver ranges in package.json. It will update within the range but will not cross a major version boundary. The --latest flag ignores the ranges and installs the latest version available, which may be a major version bump. This distinction is critical for CI environments where unexpected major updates can break builds .
The trade-off. pnpm’s strictness means that a dependency you add is visible only to the package that declared it. If you add lodash to a workspace package, other workspaces cannot import it. This forces explicit declaration but adds friction in monorepos where shared utilities are common.
a. Adding dependencies
pnpm add installs a package and saves it to dependencies by default. The destination field is controlled by flags: -D for devDependencies, -O for optionalDependencies, and -P for explicit production dependencies .
# Add to dependencies
pnpm add express
# Add to devDependencies
pnpm add -D typescript
# Add an exact version (no caret)
pnpm add -E lodash@4.17.21
# Add a peer dependency (also installs as dev)
pnpm add --save-peer react
The --save-exact (-E) flag writes the exact version to package.json, bypassing the default caret range. This is useful for tools where minor updates are known to introduce breaking changes .
For packages from non-npm sources, pnpm supports protocols. A JSR package uses jsr:, a workspace package uses workspace:, and a local tarball uses a file path. Aliases allow installing a package under a different name:
# Install @babel/core under the name babel-core
pnpm add babel-core@npm:@babel/core@8.0.6
This writes "babel-core": "npm:@babel/core@8.0.6" to package.json. The npm: prefix is the alias protocol, not a registry selector .
The --allow-build flag is required for packages with install scripts. Without it, the scripts are silently skipped, and native modules may fail at runtime.
pnpm --allow-build=esbuild add my-bundler
This runs esbuild‘s postinstall script and records the permission in onlyBuiltDependencies within pnpm-workspace.yaml, so future installs also allow it .
b. Updating dependencies
pnpm update (aliases: up, upgrade) updates packages to their latest versions within the ranges declared in package.json .
# Update all dependencies within declared ranges
pnpm update
# Update a specific package
pnpm update express
# Update beyond declared ranges (may cross major versions)
pnpm update --latest
# Interactive update
pnpm update --interactive
Patterns allow scoped updates. pnpm update "@babel/*" updates all Babel packages. The negation pattern pnpm update "\!webpack" updates everything except webpack .
The --recursive (-r) flag runs the update in every workspace package concurrently. The --workspace flag restricts updates to workspace packages only, failing if a specified package is not found in the workspace .
A critical caveat for pnpm 11+: pnpm update <name>@<version> fails if the package is not a direct dependency of any selected project. Transitive dependencies must be updated via overrides instead .
c. Removing dependencies
pnpm remove (aliases: rm, uninstall, un) removes a package from node_modules and from package.json .
# Remove from all dependency fields
pnpm remove lodash
# Remove only from devDependencies
pnpm remove -D typescript
Without a --save-* flag, the package is removed from every field it appears in. The --save-dev, --save-optional, and --save-prod flags target specific fields, failing silently if the package is not in that field .
The --recursive (-r) flag removes the package from every workspace package. This is useful for cleaning up a dependency that was added to the wrong scope or is no longer needed anywhere .
The --lockfile-only flag updates package.json and pnpm-lock.yaml without touching node_modules. This is useful for scripted changes where the actual installation is deferred .
Complete Example Session
# ============================================
# PART 1: ADD A PRODUCTION DEPENDENCY
# ============================================
pnpm add express
# package.json: "express": "^5.1.0"
# node_modules/express -> .pnpm/express@.../node_modules/express
# ============================================
# PART 2: ADD A DEV DEPENDENCY
# ============================================
pnpm add -D typescript
# package.json: "typescript": "^5.8.0" (devDependencies)
# ============================================
# PART 3: ADD WITH EXACT VERSION
# ============================================
pnpm add -E lodash@4.17.21
# package.json: "lodash": "4.17.21" (no caret)
# ============================================
# PART 4: ADD A NATIVE PACKAGE WITH SCRIPT PERMISSION
# ============================================
pnpm --allow-build=esbuild add esbuild
# esbuild postinstall runs.
# Permission recorded in pnpm-workspace.yaml.
# ============================================
# PART 5: UPDATE WITHIN RANGES
# ============================================
pnpm update express
# Updates to latest 5.x.x within ^5.1.0 range.
# Lockfile updated.
# ============================================
# PART 6: UPDATE BEYOND RANGES
# ============================================
pnpm update --latest
# All dependencies updated to latest.
# Major version bumps may occur.
# ============================================
# PART 7: REMOVE A DEPENDENCY
# ============================================
pnpm remove lodash
# package.json: lodash removed from dependencies.
# node_modules: lodash symlink removed.
# ============================================
# PART 8: REMOVE FROM ALL WORKSPACES
# ============================================
pnpm remove -r lodash
# lodash removed from every workspace package.
# ============================================
# PART 9: VERIFY THE GRAPH
# ============================================
ls -la node_modules/express
# symlink to .pnpm/express@5.1.0/node_modules/express
The nine parts trace the lifecycle: adding production and dev dependencies, pinning exact versions, permitting native build scripts, updating within and beyond ranges, removing from a single package, removing recursively from a workspace, and verifying the resulting symlink graph.
Quick Reference
Add Commands
| Command | Destination |
|---|---|
pnpm add <pkg> | dependencies |
pnpm add -D <pkg> | devDependencies |
pnpm add -O <pkg> | optionalDependencies |
pnpm add -E <pkg> | Exact version |
pnpm add --save-peer <pkg> | peerDependencies + dev |
pnpm add --allow-build=<pkg> <pkg> | Allow install scripts |
Update Commands
| Command | Effect |
|---|---|
pnpm update | Update within ranges |
pnpm update --latest | Update to latest, ignoring ranges |
pnpm update --interactive | Choose updates interactively |
pnpm update -r | Update all workspaces |
pnpm update "@scope/*" | Update by pattern |
Remove Commands
| Command | Effect |
|---|---|
pnpm remove <pkg> | Remove from all fields |
pnpm remove -D <pkg> | Remove from devDependencies |
pnpm remove -r <pkg> | Remove from all workspaces |
pnpm remove --lockfile-only <pkg> | Update manifests without node_modules |
Best Practices
✅ Do This:
pnpm add -D typescript
<!-- Build tools belong in devDependencies --> # ✅
pnpm update --interactive
<!-- Review each update before applying --> # ✅
pnpm --allow-build=esbuild add esbuild
<!-- Explicitly permit native build scripts --> # ✅
❌ Don’t Do This:
# Add a dependency without -D to a package that only runs in tests
<!-- Pollutes production dependencies --> # ❌
# Run pnpm update --latest without reviewing
<!-- Major version bumps can break APIs --> # ❌
# Assume install scripts run automatically
<!-- pnpm 10 blocks them by default --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Native module fails | Install script blocked by default | --allow-build=<pkg> |
| Cannot add to workspace root | Missing -w flag | pnpm add <pkg> -w |
| Update does not cross major | Ranges constrain update | pnpm update --latest |
| Remove fails on workspace package | Not declared in that workspace | pnpm remove -r <pkg> |
| Lockfile out of sync | Manual package.json edit | Run pnpm install to reconcile |
Real-World Examples
1. Add Production Dependency
pnpm add express
2. Add Dev Dependency
pnpm add -D vitest
3. Add Exact Version
pnpm add -E lodash@4.17.21
4. Add Peer Dependency
pnpm add --save-peer react
5. Add with Build Permission
pnpm --allow-build=esbuild add esbuild
6. Update Within Range
pnpm update express
7. Update to Latest
pnpm update --latest
8. Interactive Update
pnpm update -i
9. Remove from All Fields
pnpm remove lodash
10. Remove from All Workspaces
pnpm remove -r lodash
Visual
Add Operation Flow
┌──────────────────────────────────────────────┐
│ pnpm add express │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. Resolve version (^5.1.0) │
│ 2. Check global store for express@5.1.0 │
│ 3. Hard-link into .pnpm/express@5.1.0/ │
│ 4. Create symlink at node_modules/express │
│ 5. Write to package.json and lockfile │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Dependency available │
└──────────────────────────────────────────────┘
Update Decision
┌──────────────────────────────────────────────┐
│ pnpm update │
│ (without --latest) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Check package.json range: ^5.1.0 │
│ Find latest version satisfying ^5.1.0 │
│ → Update lockfile if newer exists │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ pnpm update --latest │
│ → Ignore range, use latest tag │
│ → May cross major version boundary │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Add default | dependencies |
| Add dev flag | -D |
| Add exact flag | -E |
| Install script permission | --allow-build=<pkg> |
| Update default | Within ranges |
| Update latest flag | --latest |
| Remove default | All fields |
| Remove workspace flag | -r |
| Root workspace add | -w |
Key takeaways:
pnpm adddefaults todependencies. Use-Dfor build tools and test runners,-Efor exact versions, and--save-peerfor plugins that expect a shared runtime .- Install scripts are blocked by default in pnpm 10. Native packages require
--allow-build=<pkg>to run their postinstall scripts. The permission is recorded for future installs . pnpm updaterespects ranges;--latestignores them. The default behavior prevents accidental major version bumps. Use--latestonly when you intend to cross major versions .pnpm removecleans every field by default. The--save-*flags target specific fields. Recursive removal (-r) applies to all workspace packages .- Workspace root additions require
-w. This prevents accidental installation of tools that should belong to a specific workspace package .
Remember: The three commands are the daily cycle. add grows the tree, update refreshes it, and remove prunes it. Each command modifies package.json, pnpm-lock.yaml, and the symlink graph in .pnpm together. The flags control where the dependency lands and whether native scripts are permitted to run. pnpm’s strictness means that every addition is isolated, every removal is clean, and every update is constrained by the ranges you declared.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!