monorepo-management
Monorepo Management with PNPM
Best practices for managing large PNPM monorepos following Constructive conventions. Covers workspace configuration, dependency management, selective builds, and package organization.
When to Apply
Use this skill when:
- Managing a multi-package repository
- Configuring workspace dependencies
- Setting up selective builds and filtering
- Organizing packages in a large codebase
- Configuring lerna versioning strategies
Workspace Configuration
pnpm-workspace.yaml
Define package locations:
packages:
- 'packages/*'
For larger projects with multiple directories:
packages:
- 'packages/*'
- 'pgpm/*'
- 'graphql/*'
- 'postgres/*'
- 'apps/*'
- 'libs/*'
Root package.json
{
"name": "my-workspace",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "pnpm -r run lint",
"clean": "pnpm -r run clean",
"deps": "pnpm up -r -i -L"
}
}
Key points:
- Root is always
private: true - Scripts use
pnpm -rfor recursive execution depsscript for interactive dependency updates
Workspace Dependencies
workspace:* Protocol
Reference internal packages:
{
"dependencies": {
"my-utils": "workspace:*"
}
}
When published, workspace:* is replaced with the actual version.
Dependency Types
| Protocol | Behavior |
|---|---|
workspace:* |
Latest version in workspace |
workspace:^ |
Compatible version range |
workspace:~ |
Patch version range |
Adding Workspace Dependencies
# Add workspace dependency
pnpm add my-utils --filter my-app --workspace
# Add external dependency to specific package
pnpm add lodash --filter my-app
# Add dev dependency to root
pnpm add -D typescript -w
Filtering and Selective Builds
--filter Flag
Run commands on specific packages:
# Single package
pnpm --filter my-app run build
# Multiple packages
pnpm --filter my-app --filter my-utils run build
# Glob patterns
pnpm --filter "my-*" run build
# Package and its dependencies
pnpm --filter my-app... run build
# Package and its dependents
pnpm --filter ...my-utils run build
# Exclude packages
pnpm --filter "!my-legacy" run build
Dependency-Aware Builds
# Build package and all its dependencies
pnpm --filter my-app... run build
# Build only dependencies (not the package itself)
pnpm --filter "my-app^..." run build
# Build dependents (packages that depend on this)
pnpm --filter "...my-utils" run build
Changed Packages
# Packages changed since main
pnpm --filter "...[origin/main]" run build
# Packages changed in last commit
pnpm --filter "...[HEAD~1]" run build
Package Organization
Directory Structure
Organize by domain or type:
my-workspace/
├── packages/ # Shared utilities
│ ├── utils/
│ ├── types/
│ └── config/
├── apps/ # Applications
│ ├── web/
│ └── api/
├── libs/ # Domain libraries
│ ├── auth/
│ └── database/
├── pgpm/ # PGPM modules (if hybrid)
│ └── migrations/
├── pnpm-workspace.yaml
├── lerna.json
└── package.json
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Scoped packages | @org/package-name |
@myorg/utils |
| Internal packages | package-name |
my-utils |
| Apps | app-name |
web-app |
Lerna Configuration
Independent Versioning
Each package versioned separately:
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "pnpm",
"command": {
"publish": {
"allowBranch": "main",
"conventionalCommits": true
}
}
}
Best for: utility libraries with different release cycles.
Fixed Versioning
All packages share same version:
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "1.0.0",
"npmClient": "pnpm",
"command": {
"publish": {
"allowBranch": "main",
"conventionalCommits": true
}
}
}
Best for: tightly coupled packages that release together.
Versioning Commands
# Version changed packages
pnpm lerna version
# Version with specific bump
pnpm lerna version patch
pnpm lerna version minor
pnpm lerna version major
# Preview without changes
pnpm lerna version --no-git-tag-version --no-push
Dependency Management
Update Dependencies
# Interactive update all packages
pnpm up -r -i -L
# Update specific dependency everywhere
pnpm up lodash -r
# Update to latest
pnpm up lodash@latest -r
Check for Outdated
pnpm outdated -r
Dedupe Dependencies
pnpm dedupe
Build Optimization
Parallel Builds
PNPM runs in parallel by default. Control concurrency:
# Limit concurrent tasks
pnpm -r --workspace-concurrency=4 run build
Topological Order
Dependencies are built first automatically:
# Builds in dependency order
pnpm -r run build
Caching
Use turbo or nx for build caching in large repos:
{
"scripts": {
"build": "turbo run build"
}
}
CI/CD Patterns
Install Dependencies
pnpm install --frozen-lockfile
Build Changed Packages
# Build packages changed since main
pnpm --filter "...[origin/main]" run build
Test Changed Packages
pnpm --filter "...[origin/main]" run test
Publish Workflow
# Version and publish
pnpm lerna version --yes
pnpm lerna publish from-package --yes
Common Commands Reference
| Command | Description |
|---|---|
pnpm install |
Install all dependencies |
pnpm -r run build |
Build all packages |
pnpm -r run test |
Test all packages |
pnpm --filter <pkg> run <cmd> |
Run command in specific package |
pnpm --filter <pkg>... run <cmd> |
Run in package and dependencies |
pnpm add <dep> --filter <pkg> |
Add dependency to package |
pnpm add <dep> -w |
Add dependency to root |
pnpm up -r -i -L |
Interactive dependency update |
pnpm lerna version |
Version packages |
pnpm lerna publish |
Publish packages |
Hybrid Workspaces
Some repos (like Constructive) have both PNPM packages and PGPM modules:
# pnpm-workspace.yaml
packages:
- 'packages/*' # TypeScript packages
- 'pgpm/*' # PGPM CLI and tools
- 'postgres/*' # PostgreSQL utilities
The root may also have a pgpm.json for SQL module configuration.
Troubleshooting
Dependency Resolution Issues
# Clear cache and reinstall
pnpm store prune
rm -rf node_modules
pnpm install
Workspace Link Issues
# Check workspace links
pnpm why <package-name>
Build Order Issues
# Verify dependency graph
pnpm list -r --depth=0
Best Practices
- Keep root private: Never publish the root package
- Use workspace protocol: Always
workspace:*for internal deps - Consistent structure: Same directory layout across packages
- Shared config: Extend root tsconfig.json, eslint.config.js
- Filter in CI: Only build/test changed packages
- Lock file: Always commit pnpm-lock.yaml
- Dedupe regularly: Run
pnpm dedupeperiodically - Document dependencies: Clear README for each package
References
- Related skill:
pnpm-workspacefor workspace setup - Related skill:
pnpm-publishingfor publishing workflow - Related skill:
pgpm-workspacefor SQL module workspaces