Phantom Dependencies

The node_modules/.pnpm Layout and Phantom Dependencies

Node.js 2,131 still resolves modules by walking node_modules, so pnpm 69,400 builds that tree in two layers, visible in project a from the previous listing.

Inspecting pnpm's two-layer node_modulesShell
cd ~/v5-ch1/store-demo/a
ls -A node_modules | xargs && readlink node_modules/pg
ls node_modules/.pnpm | xargs -n 4
ls node_modules/.pnpm/pg@8.23.0/node_modules | xargs
node -e 'require("pg"); console.log("pg ok")'
node -e 'require("pg-types")' 2>&1 | grep -m1 Error
Output
.modules.yaml .pnpm .pnpm-workspace-state-v1.json pg
.pnpm/pg@8.23.0/node_modules/pg
lock.yaml node_modules pg-cloudflare@1.4.0 pg-connection-string@2.14.0
pg-int8@1.0.1 pg-pool@3.14.0_pg@8.23.0 pg-protocol@1.16.0 pg-types@2.2.0
pg@8.23.0 pgpass@1.0.5 postgres-array@2.0.0 postgres-bytea@1.0.1
postgres-date@1.0.7 postgres-interval@1.2.0 split2@4.2.0 xtend@4.0.2
pg pg-cloudflare pg-connection-string pg-pool pg-protocol pg-types pgpass
pg ok
Error: Cannot find module 'pg-types'

The top level holds only what package.json declares: one symlink, pg. Everything else lives in the virtual store, node_modules/.pnpm, one folder per version. Inside pg@8.23.0/node_modules/ sit the real pg (hard links into the store) and symlinks to pg's own dependencies, so pg finds pg-types as a sibling while your code finds nothing. The suffix in pg-pool@3.14.0_pg@8.23.0 records a peer dependency: pg-pool is wired to the exact pg it was installed with.

That failed require is the phantom dependency check. Under npm 2,036 's flat layout it would succeed, tying your code to whatever version pg pulled in (Hoisting and Deduplication). For older packages that relied on hoisting, pnpm hoists everything into the hidden node_modules/.pnpm/node_modules, which dependencies can reach but your project cannot, and public-hoist-pattern can expose a package at the top level when a tool insists.