Adopting pnpm Workspaces for Strict Dependency Isolation in a JavaScript Monorepo
A task guide for migrating a JavaScript/TypeScript monorepo to pnpm workspaces with strict dependency isolation, content-addressable store reuse, and deterministic installs using the workspace:* protocol and isolated node-linker mode.
10 Jan 2026, 16:05 UTC

Desired Outcome
A monorepo where each workspace package installs only its declared dependencies, shares a single content-addressable store, and resolves inter-package references via the workspace:* protocol without duplication or phantom dependencies. This eliminates the "works on my machine" class of errors caused by hoisted dependencies that aren't explicitly declared.
Prerequisites
- Node.js 18 LTS or 20 LTS installed
- pnpm 8.x or 9.x installed globally (
corepack enable && corepack prepare pnpm@latest --activate) - A root
package.jsonwith aworkspacesfield listing package globs (e.g.,packages/*) - Each workspace package has its own
package.jsonwith a uniquenamefield - A root
.npmrcto control hoisting and peer dependency behavior
Procedure
1. Initialize the Workspace Root
Create or update the root package.json with the workspaces configuration:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"install:all": "pnpm install --frozen-lockfile",
"build": "pnpm -r run build"
}
}2. Configure .npmrc for Isolation
Place this at the repository root. The node-linker=isolated setting (pnpm 8+) prevents hoisting entirely; use hoisted only if you have legacy packages that cannot declare all dependencies.
# .npmrc
node-linker=isolated
auto-install-peers=true
shamefully-hoist=false
strict-peer-dependencies=true
Run this at the repository root with write permission to the working directory. No elevated privileges required.
3. Declare Workspace Dependencies
In each package under packages/, reference sibling packages using the workspace:* protocol. This tells pnpm to link the local package instead of fetching from the registry.
# packages/ui/package.json
{
"name": "@myorg/ui",
"version": "1.0.0",
"dependencies": {
"@myorg/utils": "workspace:*"
}
}The * resolves to the version declared in the target package's package.json. Mismatches cause resolution errors at install time.
4. Install and Generate Lockfile
Run from the repository root:
pnpm installThis populates the content-addressable store (~/.pnpm-store by default), creates symlinks under node_modules/.pnpm, and writes pnpm-lock.yaml. Commit the lockfile.
5. Verify Symlink Structure
Inspect the generated layout:
ls -la node_modules/.pnpm/
# Should show content-addressed directories like:
# @myorg+utils@1.0.0/node_modules/@myorg/utils -> ../../../packages/utilsExpected Checks
- Lockfile committed:
pnpm-lock.yamlexists at root and is tracked by git. - Workspace listing: Run
pnpm list -r --depth=0at root; each workspace package appears with its declared version. - No phantom dependencies: Run
pnpm whyin any workspace; every dependency traces to an explicit declaration in that package'spackage.jsonor a transitive peer dependency. - Reproducible CI install: In a clean environment,
pnpm install --frozen-lockfilecompletes without modifyingpnpm-lock.yaml.
Recovery Options
Full Clean Reinstall
Removes all generated artifacts and repopulates from the lockfile:
# Run at repository root
rm -rf node_modules
rm -rf packages/*/node_modules
pnpm store prune
pnpm install --frozen-lockfileRisk: pnpm store prune removes unreferenced packages from the global store; other projects on the same machine using pnpm will re-download on next install.
Lockfile Realignment
If pnpm-lock.yaml drifts (e.g., after manual edits or version upgrades):
pnpm install --forceThis recalculates the dependency graph and rewrites the lockfile. Review the diff before committing.
Diagnose Missing or Phantom Peers
When a build fails with "peer dependency not found":
pnpm why --workspace=@myorg/ui
pnpm list --workspace=@myorg/ui --depth=10Adjust .npmrc hoist settings only as a last resort. Prefer adding the missing peer to the package's peerDependencies and devDependencies.
Limitations and Version Sensitivity
node-linker=isolatedrequires pnpm 8+. Earlier versions usehoistedby default and cannot fully prevent phantom dependencies.- Strict peer dependency enforcement (
strict-peer-dependencies=true) surfaces incompatibilities that hoisting previously masked. Expect build or type-check failures in workspaces that relied on implicit peer resolution. - The
workspace:*protocol does not auto-sync versions. If@myorg/utilsbumps to 2.0.0, dependent packages must be updated manually or via a changeset tool. - Symlink-based resolution can break tools that don't follow symlinks (e.g., some older Webpack configs, certain IDE indexing). Set
resolve.symlinks: truein Webpack or usenode-linker=hoistedfor affected workspaces only.
Verification Checklist
| Check | Command | Pass Criteria |
|---|---|---|
| pnpm version recognized | pnpm --version | Outputs 8.x or 9.x |
| Workspaces detected | pnpm list -r --depth=0 | Lists all packages under packages/ |
| Deterministic install | rm -rf node_modules && pnpm install --frozen-lockfile | Lockfile unchanged after install |
| Content-addressable store used | ls node_modules/.pnpm/ | head -5 | Shows hash-prefixed directories, not flat packages |
| Workspace symlinks correct | readlink node_modules/.pnpm/@myorg+utils@1.0.0/node_modules/@myorg/utils | Points to ../../../packages/utils |
Run these checks after initial setup, after any lockfile change, and in CI before build steps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.