npm's flat node_modules lets code import any transitive dependency (phantom deps), and its default posture runs postinstall scripts from every package without restriction. pnpm fixes both structurally: strict node_modules isolation prevents phantom dependency access, and since v10/v11 it blocks lifecycle scripts by default, enforces minimum release age (1 day cooldown on new versions), and blocks exotic subdependencies (git/tarball refs in transitive deps).
What needs to change
Lock files:
- Delete
frontend/package-lock.json and frontend/yarn.lock
- Generate
frontend/pnpm-lock.yaml via pnpm import or pnpm install
Dockerfile (root):
- Install pnpm in the build stage (
corepack enable pnpm or npm i -g pnpm)
npm ci -> pnpm install --frozen-lockfile
npm run build -> pnpm run build
npm start -> pnpm start
- Remove
npm from apt-get install line in runtime stage
frontend/Dockerfile:
- Same npm -> pnpm swaps as above
CI (.github/workflows/e2e.yml):
- Add
pnpm/action-setup@v4 step
npm ci -> pnpm install --frozen-lockfile
npx playwright -> pnpm exec playwright
Makefile:
npm run dev -> pnpm run dev
npx -> pnpm exec
Docs (README.md, CLAUDE.md):
npm install -> pnpm install
Claude command (.claude/commands/screenshots.md):
npm install --no-save playwright -> pnpm equivalent
pnpm config to set
In frontend/.npmrc (pnpm reads this):
strict-peer-dependencies=true
auto-install-peers=true
Consider also enabling in frontend/package.json or .npmrc:
minimum-release-age=1440
block-exotic-subdeps=true
strict-dep-builds=true
These are defaults in pnpm 11+ but being explicit is self-documenting.
Verification
pnpm install completes cleanly
pnpm run build produces working frontend
pnpm run dev starts dev server
pnpm exec vitest run passes all tests
pnpm exec playwright test passes e2e
- Docker build succeeds
- No phantom dependency imports (build would fail if any exist)
npm's flat node_modules lets code import any transitive dependency (phantom deps), and its default posture runs postinstall scripts from every package without restriction. pnpm fixes both structurally: strict node_modules isolation prevents phantom dependency access, and since v10/v11 it blocks lifecycle scripts by default, enforces minimum release age (1 day cooldown on new versions), and blocks exotic subdependencies (git/tarball refs in transitive deps).
What needs to change
Lock files:
frontend/package-lock.jsonandfrontend/yarn.lockfrontend/pnpm-lock.yamlviapnpm importorpnpm installDockerfile (root):
corepack enable pnpmornpm i -g pnpm)npm ci->pnpm install --frozen-lockfilenpm run build->pnpm run buildnpm start->pnpm startnpmfrom apt-get install line in runtime stagefrontend/Dockerfile:
CI (.github/workflows/e2e.yml):
pnpm/action-setup@v4stepnpm ci->pnpm install --frozen-lockfilenpx playwright->pnpm exec playwrightMakefile:
npm run dev->pnpm run devnpx->pnpm execDocs (README.md, CLAUDE.md):
npm install->pnpm installClaude command (.claude/commands/screenshots.md):
npm install --no-save playwright-> pnpm equivalentpnpm config to set
In
frontend/.npmrc(pnpm reads this):Consider also enabling in
frontend/package.jsonor.npmrc:These are defaults in pnpm 11+ but being explicit is self-documenting.
Verification
pnpm installcompletes cleanlypnpm run buildproduces working frontendpnpm run devstarts dev serverpnpm exec vitest runpasses all testspnpm exec playwright testpasses e2e