Adopting pnpm Workspaces with a Single Lockfile and Filtered CI Builds
Adopt pnpm workspaces with a single root lockfile and filtered CI builds to get deterministic installs, prevent phantom dependencies, and speed up CI. This guide explains the minimal design, trust boundaries, operational checks, failure modes, and when to redesign.
21 Apr 2026, 22:40 UTC

Problem & Takeaway
In a JavaScript/TypeScript monorepo you often have dozens of packages that ship independently but share common utilities. Reproducible installs, fast CI, and clear dependency boundaries are hard to maintain with npm or yarn when you want to avoid unnecessary rebuilds. pnpm’s isolated node_modules layout, a single root lockfile, and its powerful --filter syntax provide a clean architecture that satisfies these needs.
Key takeaway: Keep one pnpm-lock.yaml at the repo root, enable workspaces=true, and use pnpm --filter in CI to rebuild only affected packages and their dependents. This gives deterministic installs, prevents phantom dependencies, and keeps CI fast.
Requirements
- Multiple deployable packages that share internal libraries.
- Reproducible installs across local and CI environments.
- CI should skip unchanged packages to reduce build time.
- No accidental reliance on hoisted transitive dependencies.
Minimal Design
The smallest viable repository layout is:
repo-root/
├─ pnpm-workspace.yaml
├─ pnpm-lock.yaml
├─ .npmrc
├─ packages/
│ ├─ core-lib/
│ │ └─ package.json
│ ├─ ui-lib/
│ │ └─ package.json
│ └─ app-
│ └─ package.json
Key files:
pnpm-workspace.yamllists all packages.packages: - "packages/*".npmrcenables workspaces and sets the store path.shamefully-hoist=false store-dir=~/.pnpm-store- Each
package.jsondeclares only its direct dependencies. Transitive dependencies are never hoisted.
Trust & Data Boundaries
- Internal code – All workspace packages are trusted source code. They can import each other freely.
- External tarballs – npm registry packages are untrusted. pnpm downloads them into a content-addressable store and verifies their checksum.
- Node_modules layout – pnpm creates a
node_modules/.pnpmdirectory where each dependency is a symlink to the store. Only declared dependencies appear; any missing import results inMODULE_NOT_FOUND.
Operational Checks
- Reproducible install
# Run on a clean machine or CI agent pnpm install --frozen-lockfileExits non‑zero if
pnpm-lock.yamlis out of sync. This guarantees that the lockfile is the source of truth. - CI filter selection
# In CI, determine changed packages relative to base ref CHANGED=$(git diff --name-only $BASE_REF..HEAD | grep '^packages/' | cut -d'/' -f2 | uniq) # Run tests only for affected packages and their dependents pnpm --filter "${CHANGED}" testEnsure CI fetches at least the base commit and the full history of the
packages/directory. A shallow clone can silently skip tests. - Audit and why
pnpm audit pnpm whyUse these tools to verify that no undeclared dependency is being used.
- Store inspection
pnpm store pathPrints the absolute path to the local store. Useful for debugging disk‑usage or checksum errors.
Failure Modes
| Scenario | Cause | Detection |
|---|---|---|
| Phantom dependency error | Package imports a transitive dependency that is not declared. | Runtime error MODULE_NOT_FOUND when running node or tests. |
| Store corruption | Checksum mismatch when reusing a store across incompatible pnpm versions. | pnpm install fails with checksum mismatch or ENOENT. |
| CI filter skips tests | Shallow clone missing base ref; --filter resolves to empty set. | CI job reports “No packages matched filter” or missing tests. |
| Duplicate installs | Different workspace protocol versions in the same repo. | Multiple copies of the same dependency appear in node_modules/.pnpm. |
When to Redesign
- Need npm or yarn compatibility for a third‑party tool that only understands their layout.
- Legacy native tooling that requires hoisted
node_modulesto resolvenode-gyppaths. - Store size exceeds local disk capacity and you cannot share a remote store.
- Individual packages need independent release trains; a single lockfile forces a joint release cadence.
In such cases consider:
- Switching to
npm workspacesoryarn workspaceswith--hoistenabled. - Using
pnpm --lockfile-onlyper package and merging them manually. - Configuring a remote pnpm store (e.g.,
pnpm store add --global) to offload disk usage.
Practical Verification Checklist
- [ ]
pnpm install --frozen-lockfilepasses on a fresh clone. - [ ]
pnpm --filter @mypkg/test testruns only the intended package. - [ ]
pnpm whyshows direct dependency chain. - [ ]
pnpm store pathpoints to a non‑empty directory anddu -sh $(pnpm store path)is within limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.