pnpm Workspace Configuration

March 6, 2026 · View on GitHub

This Nx monorepo uses pnpm as the package manager with a specific configuration to optimize for workspace management.

Understanding node_modules in Packages

Why do packages have node_modules directories?

The node_modules directories in individual packages (like packages/core/node_modules) are not full dependency installations. They are lightweight symlinks/junctions created by pnpm for proper Node.js module resolution.

What's Actually Inside

# Check what's in a package's node_modules
Get-ChildItem packages/core/node_modules | Select-Object Name, LinkType, Target

# Output shows they are junctions/symlinks:
# Name       LinkType Target
# ----       -------- ------
# tslib      Junction C:\...\node_modules\.pnpm\tslib@2.8.1...
# uuid       Junction C:\...\node_modules\.pnpm\uuid@13.0.0...

These are pointer files that take up minimal disk space (~0-13 KB total per package).

How pnpm Works

  1. All actual packages are stored once in node_modules/.pnpm/ (content-addressable store)
  2. Root node_modules contains symlinks to the store
  3. Package node_modules contains symlinks to the root store for their specific dependencies
  4. This enables:
    • ✅ Fast installations
    • ✅ Disk space savings
    • ✅ Proper dependency isolation
    • ✅ Correct Node.js module resolution

Configuration Files

.npmrc

# Registry configuration
registry=https://registry.npmjs.org/
@acontplus:registry=https://registry.npmjs.org/
access=public
save-exact=true

# Hoist everything to root to minimize package node_modules
shamefully-hoist=true
public-hoist-pattern[]=*

# Workspace package linking
link-workspace-packages=deep
prefer-workspace-packages=true

Key Settings:

  • shamefully-hoist=true: Hoists dependencies to the root node_modules (similar to npm/yarn behavior)
  • public-hoist-pattern[]=*: Hoists all packages to be accessible from root
  • link-workspace-packages=deep: Deep links workspace packages
  • prefer-workspace-packages=true: Prefers workspace versions over registry versions

.gitignore

# Ignore node_modules in all packages and apps
packages/**/node_modules/
apps/**/node_modules/
libs/**/node_modules/
projects/**/node_modules/

These directories are ignored because:

  1. They're just symlinks (not real dependencies)
  2. They're regenerated by pnpm install
  3. They shouldn't be committed to version control

.npmignore (per package)

Each publishable package has a .npmignore file that ensures:

  • node_modules/ is not published to npm
  • Test files and configs are excluded
  • Only production build artifacts are published

Package Structure

Dependencies Configuration

Production Packages use only peerDependencies:

{
  "name": "@acontplus/core",
  "peerDependencies": {
    "@acontplus/utils": "^1.0.10",
    "tslib": "^2.8.1",
    "uuid": "^13.0.0"
  }
}

Why peerDependencies?

  • The consuming application provides the actual dependencies
  • Prevents duplicate installations
  • Ensures version compatibility across the monorepo
  • Standard pattern for library packages

Workspace Configuration

pnpm-workspace.yaml:

packages:
  - 'apps/*'
  - 'packages/*'

This tells pnpm which directories contain workspace packages.

Common Tasks

Install Dependencies

pnpm install

Clean Install

# Remove all node_modules and reinstall
Remove-Item -Recurse -Force node_modules, packages/*/node_modules, apps/*/node_modules
pnpm install

Check Package node_modules

# See which packages have node_modules (they all will, and that's OK)
Get-ChildItem -Path packages -Directory | ForEach-Object {
  $nodeModules = Join-Path $_.FullName "node_modules"
  if (Test-Path $nodeModules) {
    $size = (Get-ChildItem $nodeModules -Recurse -File -ErrorAction SilentlyContinue |
             Measure-Object -Property Length -Sum).Sum / 1KB
    Write-Host "$($_.Name): $([math]::Round($size, 2)) KB"
  }
}
# Check if node_modules contents are symlinks
Get-ChildItem packages/core/node_modules | Where-Object { $_.LinkType }

Troubleshooting

"Why do my packages still have node_modules?"

This is normal and expected. pnpm creates these symlink directories for module resolution. They are:

  • ✅ Lightweight (< 15 KB per package)
  • ✅ Just pointers to the root store
  • ✅ Required for Node.js to resolve dependencies
  • ✅ Automatically ignored by git
  • ✅ Excluded from npm publish

"Can I delete them?"

You can, but they'll come back after pnpm install. They're necessary for the build tools to work properly.

"How is this different from npm/yarn?"

Featurepnpmnpm/yarn
StorageContent-addressable storeFlat node_modules
Disk Usage~50% lessMore duplication
Install SpeedFasterSlower
StrictnessStricter (only listed deps)More lenient
Workspace LinksSymlinks in packagesHoisted to root

Best Practices

  1. Don't commit node_modules - Already configured in .gitignore
  2. Use peerDependencies in libraries - Prevents dependency duplication
  3. Run pnpm install after pulling - Regenerates symlinks
  4. Use nx reset to clear caches - When experiencing build issues
  5. Check actual disk usage - Symlinks take minimal space

Resources