← Dev log

devops

pnpm for a monorepo, and the phantom-dependency trap it closes

pnpm for a monorepo, and the phantom-dependency trap it closes

pnpm's defining trait is a content-addressable global store: every package version is stored once on disk, and every project gets a hard link into its local node_modules rather than its own full copy. In a monorepo with three near-identical React + Vite apps plus a backend, all sharing most of the same dependency tree, that's a measurable difference in disk usage and install speed, not a marginal preference.

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

That lets apps/web depend on packages/api via "@app/api": "workspace:*" — a real live symlink to the local package, not a published version. Edit the package, and after its own build step every app depending on it sees the change immediately.

pnpm --filter @app/types build
pnpm --filter web dev

Scopes a command to one workspace package rather than every package in the monorepo — essential once there are several apps.

The strictness is the point

pnpm's node_modules structure is intentionally stricter than npm or yarn's: a package can only import something it explicitly declared as a dependency, not something that merely happens to be hoisted nearby. Flatter hoisting in npm/yarn can let a project accidentally work by depending on a package it never declared, just because a sibling pulled it in — a phantom dependency that works by accident and breaks unpredictably the moment the sibling's tree changes. pnpm surfaces this at install or build time instead of as a mysterious runtime failure discovered later, often in someone else's environment.

pnpm install --frozen-lockfile

Fails the install outright if the lockfile doesn't already match package.json, rather than silently regenerating it. That's what guarantees CI installs the exact versions actually tested and committed.

"packageManager": "[email protected]"

Pins the exact pnpm version via Corepack, so two machines — or a local machine and a CI runner — don't resolve dependencies slightly differently between pnpm versions.