Diagnosing Phantom Dependency Failures and Store Linking Issues in pnpm Monorepos
A diagnostic guide for pnpm phantom dependency errors, store linking failures, and workspace issues in monorepos with ordered checks, safe fixes, and escalation criteria.
11 Nov 2025, 15:46 UTC

After switching a monorepo to pnpm, builds break with Cannot find module for packages that worked under npm or Yarn. pnpm uses a content-addressable store with hardlinks and a nested node_modules built from symlinks, so only explicitly declared dependencies are resolvable. The failure is usually strict isolation working correctly, a store-on-different-partition fallback, or a legacy tool that cannot follow symlinks.
Recognizable conditions
| Symptom | Likely cause | Impact |
|---|---|---|
| Module not found for a package present under npm | Phantom dependency: code imports a transitive dependency not listed in package.json | Build fails in pnpm only |
| IDE red squiggles, lint passes in CI | Symlink resolution not followed by IDE plugin | Developer friction |
| Installs slow, disk usage grows with each project | Store and project on different partitions, hardlink fallback to copy | Performance and space regression |
| Multiple versions of a peer dependency loaded at runtime | Peer conflict in workspace, multiple instances linked from virtual store | Runtime bugs, hook mismatches |
Check 1: Verify strict dependency isolation
Confirm pnpm is enforcing declared dependencies before changing configuration.
Run in the affected package directory, no elevated permissions required:
node -e "require('some-undeclared-sub-dependency')"
Expected check: pnpm should throw Error: Cannot find module. If it throws, isolation is working and the package must be declared.
Fix tied to finding: add the missing dependency explicitly.
pnpm add some-undeclared-sub-dependency --filter ./apps/web
Risk: adding transitive deps increases maintenance surface. Prefer fixing imports over blanket hoisting.
Check 2: Verify store and project share a filesystem
pnpm hardlinks files from the content-addressable store into node_modules. Hardlinks fail across partitions and pnpm falls back to copying.
Run at repository root:
pnpm store path
Then compare mount points:
df . && df $(pnpm store path)
Expected check: both paths report the same device. Different devices mean copies instead of hardlinks.
Fix tied to finding: move the store onto the same filesystem as the project.
# .npmrc in repo root
store-dir=./.pnpm-store
Rollback: remove store-dir to revert to default ~/.pnpm-store. Limitation: local store duplicates data per clone.
Check 3: Verify workspace linking
Local packages must be declared in pnpm-workspace.yaml and referenced by name and version.
Check file exists at repo root:
cat pnpm-workspace.yaml
Example content:
packages:
- 'apps/*'
- 'packages/*'
Verify link is live:
pnpm list --filter pkg-a --recursive
Expected check: consumer shows pkg-a@workspace:* and changes in packages/pkg-a are visible in apps/app-b without reinstall.
Fix tied to finding: ensure dependency uses workspace protocol in package.json:
"dependencies": {
"@org/pkg-a": "workspace:*"
}
Risk: using pnpm link manually creates extra symlinks that conflict with workspace linking.
Check 4: Handle legacy tools that need a flatter tree
Some older bundlers expect a package at the root of node_modules.
Preferred fix: selective hoisting via .npmrc, keeping strictness for the rest of the tree.
# .npmrc
public-hoist-pattern[]=*webpack*
public-hoist-pattern[]=*react-native*
Expected check: hoisted packages appear in root node_modules, others remain nested. Verify isolation still holds for non-hoisted packages using Check 1.
Last resort fix: global hoisting.
# .npmrc
shamefully-hoist=true
Limitation: this reintroduces phantom dependencies and should be treated as temporary.
Peer dependency duplication
When multiple packages require different versions of a shared peer, pnpm links separate instances from the virtual store.
Diagnostic:
pnpm why react
Fix tied to finding: align peer versions or use overrides in root package.json:
"pnpm": {
"overrides": {
"react": "18.2.0"
}
}
Escalation criteria
Escalate when:
- Selective hoisting does not resolve a build error and shamefully-hoist is the only workaround
- Disk usage remains high after confirming same filesystem and store-dir
- Peer conflicts persist after aligning versions and overrides
Practical verification after fixes: re-run the isolation test from Check 1, confirm store path device matches project, and run a clean install in a temporary directory to ensure workspace symlinks resolve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.