Disciplines · Decisions (ADRs)

ADR-0002: Package Manager Unification with pnpm

The Oshun monorepo consolidation requires a unified package manager strategy.

Accepted · 2026-01-10
11sections8 minread

On this page

Status: Accepted Date: 2026-01-10 Authors: Development Team Reviewers: Architecture Team Supersedes: N/A Superseded by: N/A

Context and Problem Statement#

The Oshun monorepo consolidation requires a unified package manager strategy. Currently, the two primary codebases use different package managers:

Lilith (Current State):

  • Uses Yarn workspaces (yarn.lock present)
  • Also has package-lock.json (npm artifacts)
  • Workspace configuration via workspaces array in package.json
  • No pnpm configuration

Yemaya (Current State):

  • Uses pnpm 10.25.0 (specified via packageManager field)
  • Has mature pnpm-workspace.yaml with dependency catalog
  • Well-configured .npmrc with hoisting rules
  • Uses Turbo for build orchestration (being replaced by Nx)

We need to select a single package manager for the unified Oshun monorepo that:

  • Supports efficient monorepo workflows at scale
  • Integrates well with Nx workspace tooling
  • Provides consistent dependency resolution across all packages
  • Minimizes disk space usage and install times
  • Offers strong security and dependency isolation

Decision Drivers#

  • Monorepo Optimization: Native workspace support with efficient linking and dependency hoisting
  • Disk Efficiency: Storage optimization for large repositories with many packages
  • Install Performance: Fast dependency resolution and installation
  • Dependency Strictness: Prevention of phantom dependencies and undeclared imports
  • Nx Integration: Seamless integration with Nx workspace features
  • Developer Experience: Familiar commands, good error messages, IDE support
  • Existing Investment: Leverage existing pnpm configuration from Yemaya
  • Lock File Stability: Deterministic builds with minimal lockfile churn
  • Security: Supply chain security features and audit capabilities

Considered Options#

Description: Performant npm - a fast, disk space efficient package manager with strict dependency isolation and native workspace support.

Pros:

  • Disk Efficiency: Content-addressable storage shares packages globally; ~60-70% disk savings vs npm
  • Strict by Default: Prevents phantom dependencies - packages can only import declared dependencies
  • Native Workspaces: First-class monorepo support with pnpm-workspace.yaml
  • Dependency Catalog: Centralized version management via catalog: prefix
  • Fast Installs: Typically 2-3x faster than npm for cold installs
  • Existing Configuration: Yemaya already has mature pnpm setup to build upon
  • Excellent Nx Integration: Nx has first-class pnpm support
  • Symlink Strategy: Proper node_modules structure with symlinks for correct resolution
  • Lockfile Stability: Minimal lockfile changes when adding/removing packages
  • Active Development: Rapid feature development, responsive maintainers

Cons:

  • Learning Curve: Developers unfamiliar with pnpm need onboarding
  • Compatibility Issues: Some packages with hardcoded node_modules assumptions may break
  • Migration Effort: Lilith packages need conversion from Yarn workspaces
  • Symlink Complexity: Debugging can be harder due to symlinked structure

Cost: Low - pnpm is free, migration is straightforward Risk: Low - mature, widely adopted, well-documented Effort: 2-3 days for full migration and verification

Option 2: Yarn (Classic or Berry)#

Description: Facebook's package manager with workspaces support. Yarn Berry (v3+) introduces Plug'n'Play (PnP) for zero-install workflows.

Pros:

  • Lilith Familiarity: Lilith already uses Yarn workspaces
  • Zero-Install (Berry): Commit dependencies to repo for instant installs
  • Stable and Mature: Long track record, large community
  • Good Workspace Support: Native workspaces with selective versioning

Cons:

  • Disk Inefficiency (Classic): Each project has full node_modules copy
  • PnP Compatibility Issues (Berry): Many packages incompatible with Plug'n'Play
  • No Dependency Catalog: Manual version synchronization across packages
  • Slower than pnpm: Install times 30-50% slower for large monorepos
  • Version Fragmentation: Classic vs Berry creates confusion
  • Would Discard Yemaya Config: Existing pnpm setup would be abandoned

Cost: Low - Yarn is free Risk: Medium - PnP compatibility issues if using Berry Effort: 3-4 days (need to convert Yemaya from pnpm)

Option 3: npm Workspaces#

Description: Native package manager bundled with Node.js with workspace support added in npm v7+.

Pros:

  • Universal Availability: Bundled with Node.js, no additional installation
  • No Migration: Works with existing package-lock.json from Lilith
  • Familiar Commands: Most developers know npm commands
  • Improving Performance: npm v10+ significantly faster than earlier versions

Cons:

  • Disk Inefficient: Full node_modules copy for each package, high disk usage
  • Slower Installs: 2-3x slower than pnpm for monorepo installs
  • No Dependency Catalog: Manual version management across packages
  • Phantom Dependencies: Allows importing undeclared packages (hoisting side effect)
  • Lockfile Churn: More frequent unnecessary changes to package-lock.json
  • Would Discard Yemaya Config: Existing pnpm setup would be abandoned
  • Limited Monorepo Features: Workspaces are basic compared to pnpm/Yarn

Cost: Free Risk: Medium - performance and phantom dependency issues at scale Effort: 3-4 days (need to convert Yemaya from pnpm)

Option 4: Bun#

Description: All-in-one JavaScript runtime with built-in package manager, bundler, and test runner.

Pros:

  • Extremely Fast: 10-100x faster installs than npm in benchmarks
  • All-in-One: Package manager, runtime, bundler, test runner
  • npm Compatible: Uses existing package.json and node_modules structure
  • Native Workspaces: Supports workspace protocol

Cons:

  • Immature: Version 1.0 released recently, still stabilizing
  • Compatibility Issues: Not all Node.js packages work correctly
  • Limited Monorepo Features: No dependency catalog, basic workspace support
  • Nx Integration Uncertain: Not officially supported by Nx
  • Production Risk: Too new for critical production workloads
  • Team Unfamiliarity: No existing experience with Bun

Cost: Free Risk: High - immature ecosystem, compatibility issues Effort: 5+ days (extensive testing required)

Decision Outcome#

Chosen option: Option 1 - pnpm

Justification:

pnpm is the optimal choice for the Oshun monorepo for the following reasons:

  1. Existing Foundation: Yemaya already has a mature, well-configured pnpm setup including:

    • Comprehensive pnpm-workspace.yaml with dependency catalog
    • Optimized .npmrc configuration with proper hoisting rules
    • Established patterns for workspace-internal dependencies
  2. Monorepo Excellence: pnpm's design philosophy aligns perfectly with Nx monorepo patterns:

    • Content-addressable storage dramatically reduces disk usage
    • Strict dependency isolation prevents phantom dependency bugs
    • Dependency catalog feature enables centralized version management
    • Fast installs improve CI/CD pipeline performance
  3. Nx Integration: Nx has excellent pnpm support:

    • nx affected works correctly with pnpm workspace structure
    • pnpm's deterministic installs complement Nx computation caching
    • Shared configuration patterns between Nx and pnpm
  4. Performance at Scale: For a monorepo with 50+ packages:

    • Install times: pnpm ~30s vs npm ~90s vs Yarn ~60s (typical)
    • Disk usage: pnpm ~500MB vs npm ~1.5GB (typical)
    • CI/CD: Faster pipelines due to efficient caching
  5. Developer Safety: pnpm's strictness catches errors early:

    • Cannot import packages not declared in package.json
    • Clear error messages for missing dependencies
    • Reproducible builds across environments

Trade-offs Accepted:

  • Lilith developers need brief pnpm onboarding
  • Some packages may need compatibility fixes for pnpm's strict mode
  • Minor workflow adjustments for developers used to npm/yarn

Implementation Plan:

  1. Phase 1: Configuration Setup (Day 1)

    • Create root pnpm-workspace.yaml incorporating all workspace paths
    • Create root .npmrc with optimized settings
    • Define comprehensive dependency catalog for shared versions
  2. Phase 2: Lilith Migration (Day 1-2)

    • Remove yarn.lock and package-lock.json
    • Update package.json workspace configuration
    • Convert workspace:* protocol usage
    • Fix any hardcoded node_modules paths
  3. Phase 3: Unified Install (Day 2)

    • Remove all node_modules directories
    • Run pnpm install at root
    • Resolve any peer dependency warnings
    • Verify all packages resolve correctly
  4. Phase 4: Verification (Day 2-3)

    • Run builds for all packages
    • Run test suites
    • Verify Nx commands work correctly
    • Update CI/CD pipelines to use pnpm

Success Metrics:

  • All packages install successfully with pnpm install
  • No phantom dependency errors during build
  • CI/CD install time < 60 seconds (with cache)
  • Disk usage < 1GB for full node_modules
  • All existing tests pass

Review Schedule: 30 days post-migration

Implementation Details#

Technical Specifications#

Root pnpm-workspace.yaml Structure:

yaml
packages:
  # Oshun-level shared packages
  - 'libs/**/*'
  - 'apps/**/*'
  - 'tools/*'

  # Legacy Lilith packages (during migration)
  - 'lilith/packages/*'
  - 'lilith/services/*'
  - 'lilith/sdk/typescript'

  # Legacy Yemaya packages (during migration)
  - 'yemaya/packages/*'
  - 'yemaya/apps/*'
  - 'yemaya/configs/*'

catalog:
  # TypeScript toolchain
  typescript: '^6.0.3'
  tsup: '^8.0.2'
  tsx: '^4.7.1'

  # Testing
  vitest: '^2.0.0'
  '@vitest/coverage-v8': '^2.0.0'
  playwright: '^1.42.0'

  # Linting & Formatting
  eslint: '^9.0.0'
  prettier: '^3.2.5'

  # React ecosystem
  react: '^18.3.0'
  react-dom: '^18.3.0'

  # Node.js types
  '@types/node': '^20.12.0'

  # Validation
  zod: '^3.22.4'

  # Build tools
  vite: '^6.0.0'

  # Backend
  fastify: '^4.26.0'
  bullmq: '^5.34.0'
  ioredis: '^5.4.0'

  # Database
  prisma: '^5.10.0'
  '@prisma/client': '^5.10.0'

Root .npmrc Configuration:

ini
# Dependency Resolution
strict-peer-dependencies=true
auto-install-peers=true
resolve-peers-from-workspace-root=true

# Hoisting Configuration
shamefully-hoist=true
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=vite
public-hoist-pattern[]=vitest
public-hoist-pattern[]=@nx/*

# Performance
store-dir=~/.pnpm-store
prefer-offline=true

# Scripts
enable-pre-post-scripts=true

# Registry
registry=https://registry.npmjs.org/

# Git
git-checks=true

# Lockfile
use-lockfile-v6=false

Root package.json Structure:

json
{
  "name": "oshun",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "packageManager": "pnpm@10.25.0",
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=9.0.0"
  },
  "scripts": {
    "build": "nx run-many -t build",
    "test": "nx run-many -t test",
    "lint": "nx run-many -t lint",
    "typecheck": "nx run-many -t typecheck",
    "clean": "nx reset && rimraf node_modules",
    "prepare": "husky"
  }
}

Migration Strategy#

From:

  • Lilith: Yarn workspaces + npm hybrid
  • Yemaya: pnpm standalone

To:

  • Oshun: Unified pnpm workspace at root

Migration Commands:

bash
# Step 1: Backup existing lock files
cp lilith/yarn.lock ~/lilith-yarn.lock.backup
cp lilith/package-lock.json ~/lilith-package-lock.backup
cp yemaya/pnpm-lock.yaml ~/yemaya-pnpm-lock.backup

# Step 2: Remove old package manager artifacts
rm -f lilith/yarn.lock
rm -f lilith/package-lock.json
rm -f yemaya/pnpm-lock.yaml

# Step 3: Remove all node_modules
find . -name "node_modules" -type d -prune -exec rm -rf {} +

# Step 4: Create root configuration
# (Create pnpm-workspace.yaml, .npmrc, package.json as specified above)

# Step 5: Install all dependencies
pnpm install

# Step 6: Verify installation
pnpm ls --depth 0
pnpm why <package-name>  # Debug specific packages

Rollback Plan: If critical issues discovered:

  1. Remove root pnpm-lock.yaml and node_modules
  2. Restore original lock files from backup
  3. Reinstall with original package managers
  4. Investigate issues before retrying

Testing Strategy#

Pre-Migration Verification:

  • Capture current npm ls or yarn list output
  • Record all package versions
  • Run full test suite, record results

Post-Migration Verification:

  • Compare package versions match pre-migration
  • Run full test suite, compare results
  • Verify build outputs are identical
  • Test hot reload and development workflows
  • Verify CI/CD pipeline runs successfully

Consequences#

Positive Consequences#

  • Unified Tooling: Single package manager across entire monorepo
  • Disk Savings: ~60% reduction in node_modules disk usage
  • Faster CI/CD: Reduced install times improve pipeline efficiency
  • Dependency Safety: Strict mode prevents phantom dependency bugs
  • Version Consistency: Catalog ensures consistent versions across packages
  • Better Debugging: Clear dependency graph aids troubleshooting
  • Nx Synergy: Optimized integration with Nx workspace features

Negative Consequences#

  • Migration Effort: One-time effort to convert Lilith packages
  • Learning Curve: Developers need pnpm familiarity
  • Compatibility Work: Some packages may need fixes for strict mode
  • Documentation Updates: Contribution guides need updating

Risks and Mitigation#

Risk Probability Impact Mitigation Strategy
Package compatibility issues Medium Medium Test thoroughly, use shamefully-hoist for problematic packages
Developer productivity dip Low Low Provide pnpm cheat sheet, office hours for questions
CI/CD pipeline failures Medium Medium Test pipeline changes in separate branch first
Build output differences Low High Compare build artifacts before/after migration

Compliance and Security#

Security Implications#

  • Lockfile Integrity: pnpm-lock.yaml provides deterministic installs
  • Audit Support: pnpm audit scans for vulnerabilities
  • Supply Chain: Strict mode reduces attack surface from phantom dependencies
  • Reproducibility: Content-addressable storage ensures identical packages

Compliance Requirements#

  • Deterministic Builds: pnpm lockfile ensures reproducible installations
  • Audit Trail: Package installation is fully logged
  • License Compliance: pnpm licenses reports all dependency licenses

Monitoring and Observability#

Metrics to Track#

  • Install Time: Track pnpm install duration in CI/CD
  • Disk Usage: Monitor ~/.pnpm-store and node_modules sizes
  • Cache Hit Rate: pnpm store cache effectiveness
  • Dependency Count: Total dependencies across workspace

Alerting Strategy#

  • Install Failures: Alert on pnpm install errors in CI
  • Security Vulnerabilities: Weekly pnpm audit runs
  • Disk Growth: Alert if store exceeds 5GB

Upstream Dependencies#

  • ADR-0001: Git Repository Consolidation (single repo enables unified package management)
  • Nx Adoption: Nx workspace requires consistent package manager

Downstream Impacts#

  • CI/CD Configuration: Pipelines must use pnpm commands
  • Docker Builds: Dockerfiles updated to use pnpm
  • Developer Setup: Onboarding docs updated for pnpm
  • IDE Configuration: VS Code/WebStorm settings for pnpm

References#

External Resources#

Internal Resources#

  • ADR-0001: Git Repository Consolidation
  • Yemaya pnpm configuration — yemaya/pnpm-workspace.yaml (pre-consolidation source repo)
  • Yemaya .npmrcyemaya/.npmrc (pre-consolidation source repo)

Revision History#

Version Date Author Changes
1.0 2026-01-10 Development Team Initial version