Why pnpm Workspaces Need Strict-Mode Rules
pnpm workspaces have a fundamentally different node_modules structure than npm or yarn. pnpm uses a content-addressable store with symlinks — packages can only access dependencies they explicitly declare. This prevents phantom dependencies (importing a package you did not declare, which works with npm hoisting but breaks in production). AI trained on npm/yarn generates code with phantom dependencies that passes on npm and crashes on pnpm.
The second difference: pnpm uses workspace: protocol for internal package references. AI generates "@repo/ui": "^1.0.0" (tries to resolve from npm registry) instead of "@repo/ui": "workspace:*" (resolves to the local workspace package). This breaks builds and installs.
These rules target pnpm 9+ with workspaces. They cover the strict isolation model, workspace protocol, catalog for version alignment, filtering, and lockfile management.
Rule 1: workspace: Protocol for All Internal Packages
The rule: 'Reference internal workspace packages with workspace: protocol: "@repo/ui": "workspace:*" (any version, resolves to local), "@repo/config": "workspace:^" (latest compatible, resolves to local). Never use version numbers for internal packages — they try to resolve from the npm registry. pnpm-workspace.yaml defines which directories contain workspace packages: packages: ["apps/*", "packages/*"].'
For publishing: 'When publishing, pnpm replaces workspace:* with the actual version number. workspace:^ becomes ^actual.version. This means internal development uses local resolution, but published packages have proper version dependencies. Never manually set version numbers for internal references.'
AI generates "@repo/shared": "^1.0.0" or "@repo/shared": "file:../shared" — both wrong. workspace:* is the correct protocol: resolved locally during development, converted to real versions during publishing.
- workspace:* for any version — workspace:^ for compatible — workspace:~ for patch
- pnpm-workspace.yaml: packages: ['apps/*', 'packages/*']
- Never version numbers for internal: '1.0.0' tries npm registry
- Never file: protocol — workspace: is the correct local resolution
- Publishing converts workspace:* to actual version automatically
AI generates '@repo/ui': '^1.0.0' — pnpm tries the npm registry and fails. workspace:* resolves to the local package. This is the single most common pnpm workspace error AI generates.
Rule 2: Strict Node_Modules Isolation
The rule: 'pnpm strict mode (default) means packages can only import what they declare in their package.json. If package A depends on lodash but package B does not, B cannot import lodash — even though lodash exists in the store. This catches missing dependencies before production. Never set shamefully-hoist=true (it recreates npm flat hoisting, defeating pnpm strict mode).'
For the benefit: 'Strict isolation prevents: phantom dependencies (importing undeclared packages that happen to be hoisted), version conflicts (two packages using different versions of the same dep — pnpm supports this natively), and production crashes (a dependency removed by another package breaks yours). If your code imports X, X must be in your package.json.'
AI generates code that imports packages not declared in the local package.json — it works with npm (hoisting makes everything available) and crashes with pnpm (strict isolation). Run pnpm install after AI generates code — if it fails to resolve, the dependency is missing from package.json.
pnpm strict mode prevents phantom dependencies: importing packages not in your package.json. npm hoisting hides these — they work locally, crash in production. Strict mode catches them at install time. Never disable with shamefully-hoist.
Rule 3: Catalog for Version Alignment
The rule: 'Use pnpm catalog (9.5+) for version alignment across the workspace: pnpm catalog set react 19.0.0, then in package.json: "react": "catalog:". All packages that use catalog: get the same version — one command to update React across the entire monorepo. Use for: framework versions, shared dependencies, and any package that should be the same version everywhere.'
For without catalog: 'Before catalog, use syncpack or manypkg for version alignment. Or define versions in a root .npmrc: public-hoist-pattern[]=react. But catalog is the native pnpm solution — type-safe, workspace-aware, and integrated into pnpm install/update.'
AI hardcodes version numbers in every package.json — "react": "19.0.0" in 10 files. Updating means editing 10 files. catalog: centralizes versions: one command updates all packages. One source of truth for version alignment.
- pnpm catalog set react 19.0.0 — centralized version definition
- "react": "catalog:" in package.json — resolves to catalog version
- One command updates all packages — no grepping version strings
- Use for: frameworks, shared deps, anything that must align across packages
- pnpm 9.5+ feature — the native version alignment solution
catalog: centralizes version numbers: pnpm catalog set react 19.0.0, then 'react': 'catalog:' in every package.json. One command updates React across the entire monorepo. No grepping, no drift, no mismatches.
Rule 4: Filtering and Workspace Commands
The rule: 'Use pnpm filtering for targeted commands: pnpm --filter @repo/web build (one package), pnpm --filter @repo/web... build (package + its dependencies), pnpm --filter ...@repo/ui build (package + its dependents), pnpm -r build (all packages). Use filtering in CI to build only what you need — not the entire monorepo.'
For common commands: 'pnpm add lodash --filter @repo/web (add to one package). pnpm add -Dw typescript (add dev dep to root workspace). pnpm -r exec -- rm -rf dist (run in all packages). pnpm -r --parallel run dev (parallel dev servers). Use --filter with build systems (Turborepo, Nx) for even smarter filtering.'
For scripts: 'Define workspace-level scripts in root package.json: "build": "pnpm -r build", "test": "pnpm -r test", "dev": "pnpm --filter @repo/web dev". Use turbo run or nx run if you need dependency-aware task scheduling — pnpm -r runs tasks in topological order but without caching.'
Rule 5: Lockfile and Reproducibility
The rule: 'Commit pnpm-lock.yaml to git — it ensures reproducible installs. Use pnpm install --frozen-lockfile in CI (fails if lockfile is outdated). Never use pnpm install without --frozen-lockfile in CI — it can modify the lockfile, creating inconsistency. Update lockfile locally: pnpm install (updates lockfile), then commit the changes.'
For .npmrc: 'Configure pnpm behavior in .npmrc: strict-peer-dependencies=true (fail on peer dep mismatches), auto-install-peers=true (install peer deps automatically), link-workspace-packages=true (resolve workspace packages by default). Commit .npmrc — it affects install behavior for the entire team.'
For node version: 'Pin the Node.js version: engines: { "node": ">=22" } in root package.json. Use .nvmrc or .node-version for version managers. pnpm respects engines — it warns or fails on wrong Node version. Pin pnpm version too: packageManager: "pnpm@9.15.0" in root package.json (corepack enforces it).'
- Commit pnpm-lock.yaml — --frozen-lockfile in CI — never update in CI
- .npmrc: strict-peer-dependencies, auto-install-peers, link-workspace-packages
- engines for Node version — packageManager for pnpm version (corepack)
- .nvmrc or .node-version for version managers — pinned, consistent
- pnpm install locally updates lock — commit lock — CI uses --frozen-lockfile
Complete pnpm Workspaces Rules Template
Consolidated rules for pnpm workspace projects.
- workspace: protocol for all internal packages — never version numbers or file:
- pnpm-workspace.yaml defines workspace packages — apps/* and packages/*
- Strict isolation: import only declared deps — never shamefully-hoist=true
- catalog: for version alignment — one command updates all packages
- --filter for targeted commands — @repo/web... for package + deps
- pnpm-lock.yaml committed — --frozen-lockfile in CI — never modify in CI
- .npmrc: strict-peer-dependencies, auto-install-peers — committed to git
- packageManager field for pnpm version — engines for Node version — corepack