Skip to main content

ADR-0007: pnpm as Package Manager for Monorepo

Status

Accepted - 2025-01-26


Context

TVL Platform uses a monorepo structure with multiple packages (database, API, workers, frontend, shared libraries). We need a package manager that:

Business Requirements

  • Fast installation times (developer productivity)
  • Reliable dependency resolution (no "works on my machine")
  • Cost-effective disk usage (CI/CD minutes cost money)

Technical Requirements

  • Workspace support for monorepo
  • Strict dependency isolation (no phantom dependencies)
  • Lock file for reproducible builds
  • Compatible with Node.js 20 and TypeScript
  • Fast CI/CD builds

Constraints

  • Monorepo with 10-20 packages eventually
  • Must work with Docker (Alpine Linux)
  • Team familiar with npm/yarn
  • GitHub Actions for CI/CD

Decision

pnpm 10.x as the package manager for the entire monorepo.

Configuration

# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'services/*'
- 'apps/*'
- 'integrations/*'
// package.json (root)
{
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=8.0.0"
}
}

Rationale

  1. Speed: 2-3x faster than npm/yarn (symlinks instead of copying)
  2. Disk Efficiency: Uses content-addressable storage (saves 50-70% disk space)
  3. Strict: Prevents phantom dependencies (packages must declare all dependencies)
  4. Workspace Support: First-class monorepo support
  5. Lock File: Deterministic, mergeable pnpm-lock.yaml

Alternatives Considered

Alternative 1: npm

Rejected

Pros:

  • Built into Node.js (no extra install)
  • Most familiar to developers
  • Official package manager

Cons:

  • Slower than pnpm (3x slower installs)
  • Wastes disk space (duplicates packages)
  • Allows phantom dependencies (not strict)
  • Workspaces added late (npm 7, less mature)

Decision: Performance and strictness benefits outweigh familiarity.


Alternative 2: Yarn Classic (v1)

Rejected

Pros:

  • Faster than npm
  • Lock file (yarn.lock)
  • Workspaces support

Cons:

  • Deprecated (no longer maintained)
  • Slower than pnpm
  • Phantom dependencies allowed
  • No longer recommended by Yarn team

Decision: Yarn team recommends Yarn Berry or pnpm instead.


Alternative 3: Yarn Berry (v3+)

Rejected

Pros:

  • Plug'n'Play (PnP) mode very fast
  • Modern architecture
  • Good workspace support

Cons:

  • PnP mode incompatible with many tools (TypeScript, ESLint had issues)
  • Steep learning curve (PnP requires mental model shift)
  • Smaller community than pnpm
  • .pnp.cjs file adds complexity

Decision: pnpm offers speed without PnP complexity.


Alternative 4: Turbo + npm

Rejected

Pros:

  • Turbo provides caching (fast rebuilds)
  • npm is familiar

Cons:

  • Turbo doesn't replace package manager (still need npm/pnpm/yarn)
  • npm still slow for installs
  • We chose Nx for build orchestration (includes caching)

Decision: pnpm + Nx provides both fast installs and fast builds.


Consequences

Positive

  1. Performance

    • Install time: ~30 seconds (vs. 2-3 minutes with npm)
    • CI/CD builds 2x faster
    • Local development faster (quick installs)
  2. Disk Efficiency

    • Content-addressable store: Shared packages stored once
    • Saves 50-70% disk space vs. npm
    • Example: 10 projects with React → 1 copy instead of 10
  3. Correctness

    • Strict mode prevents phantom dependencies
    • Forces packages to declare all dependencies
    • Catches missing dependencies in package.json
  4. Monorepo Support

    • First-class workspace support
    • pnpm --filter to run commands in specific packages
    • Recursive commands: pnpm -r test (run tests in all packages)
  5. Lock File

    • pnpm-lock.yaml is deterministic
    • Merge-friendly (YAML format)
    • Ensures same versions across all environments

Negative

  1. Learning Curve

    • Developers must learn pnpm CLI
    • Different from npm (e.g., pnpm add vs. npm install)
    • Mitigation: CLI is similar to npm, team adapts quickly
  2. Symlink Complexity

    • pnpm uses symlinks (can confuse debugging)
    • Some tools don't follow symlinks correctly
    • Mitigation: Modern tools handle symlinks well
  3. Ecosystem Support

    • Smaller community than npm
    • Some tools assume npm/yarn
    • Mitigation: pnpm compatible with npm ecosystem (uses node_modules)
  4. CI/CD Setup

    • Must install pnpm in CI
    • Mitigation: Simple setup with GitHub Actions

pnpm Features Used

1. Workspaces

# Install dependencies for all packages
pnpm install

# Add dependency to specific package
pnpm --filter @tvl/database add drizzle-orm

# Run tests in all packages
pnpm -r test

# Run command in specific package
pnpm --filter @tvl/api dev

2. Strict Mode

// .npmrc
auto-install-peers=false
strict-peer-dependencies=true

Effect: Prevents phantom dependencies (packages must declare all dependencies).

3. Shared Scripts

// package.json (root)
{
"scripts": {
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"build": "pnpm -r build",
"db:migrate": "pnpm --filter @tvl/database migrate"
}
}

4. Content-Addressable Store

~/.pnpm-store/
v3/
files/
00/
abc123def456... → react@18.2.0

Effect: All packages symlink to central store (saves disk space).


Implementation Guidelines

1. Installation

# Install pnpm globally (one-time)
npm install -g pnpm@10.19.0

# Or use Node.js Corepack (recommended)
corepack enable
corepack prepare pnpm@10.19.0 --activate

2. Dockerfile Setup

FROM node:20-alpine

# Enable pnpm via Corepack
RUN corepack enable pnpm

WORKDIR /app

# Copy dependency files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/*/package.json ./packages/

# Install dependencies
RUN pnpm install --frozen-lockfile --prod

# Copy source code
COPY . .

CMD ["node", "dist/server.js"]

3. GitHub Actions Setup

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v2
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- run: pnpm install --frozen-lockfile
- run: pnpm test

4. Common Commands

# Install all dependencies
pnpm install

# Add dependency to workspace root
pnpm add -w typescript

# Add dependency to specific package
pnpm --filter @tvl/api add fastify

# Remove dependency
pnpm remove axios

# Update dependencies
pnpm update

# Run script in all packages
pnpm -r build

# Run script in specific package
pnpm --filter @tvl/database test

Migration from npm

Step 1: Install pnpm

corepack enable
corepack prepare pnpm@10.19.0 --activate

Step 2: Import from package-lock.json

# Convert npm lockfile to pnpm
pnpm import

Step 3: Create Workspace File

# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'services/*'
- 'apps/*'

Step 4: Update Scripts

{
"scripts": {
"test": "pnpm -r test"
}
}

Step 5: Delete node_modules

rm -rf node_modules package-lock.json
pnpm install

Validation Checklist

  • pnpm-workspace.yaml configured
  • package.json has packageManager field
  • .npmrc configured for strict mode
  • pnpm-lock.yaml committed to git
  • GitHub Actions uses pnpm
  • Dockerfile uses pnpm
  • All team members have pnpm installed
  • No phantom dependencies (strict mode enforced)

References