623 lines
12 KiB
Markdown
623 lines
12 KiB
Markdown
---
|
|
name: monorepo-management
|
|
description: Master monorepo management with Turborepo, Nx, and pnpm workspaces to build efficient, scalable multi-package repositories with optimized builds and dependency management. Use when setting up monorepos, optimizing builds, or managing shared dependencies.
|
|
---
|
|
|
|
# Monorepo Management
|
|
|
|
Build efficient, scalable monorepos that enable code sharing, consistent tooling, and atomic changes across multiple packages and applications.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Setting up new monorepo projects
|
|
- Migrating from multi-repo to monorepo
|
|
- Optimizing build and test performance
|
|
- Managing shared dependencies
|
|
- Implementing code sharing strategies
|
|
- Setting up CI/CD for monorepos
|
|
- Versioning and publishing packages
|
|
- Debugging monorepo-specific issues
|
|
|
|
## Core Concepts
|
|
|
|
### 1. Why Monorepos?
|
|
|
|
**Advantages:**
|
|
- Shared code and dependencies
|
|
- Atomic commits across projects
|
|
- Consistent tooling and standards
|
|
- Easier refactoring
|
|
- Simplified dependency management
|
|
- Better code visibility
|
|
|
|
**Challenges:**
|
|
- Build performance at scale
|
|
- CI/CD complexity
|
|
- Access control
|
|
- Large Git repository
|
|
|
|
### 2. Monorepo Tools
|
|
|
|
**Package Managers:**
|
|
- pnpm workspaces (recommended)
|
|
- npm workspaces
|
|
- Yarn workspaces
|
|
|
|
**Build Systems:**
|
|
- Turborepo (recommended for most)
|
|
- Nx (feature-rich, complex)
|
|
- Lerna (older, maintenance mode)
|
|
|
|
## Turborepo Setup
|
|
|
|
### Initial Setup
|
|
|
|
```bash
|
|
# Create new monorepo
|
|
npx create-turbo@latest my-monorepo
|
|
cd my-monorepo
|
|
|
|
# Structure:
|
|
# apps/
|
|
# web/ - Next.js app
|
|
# docs/ - Documentation site
|
|
# packages/
|
|
# ui/ - Shared UI components
|
|
# config/ - Shared configurations
|
|
# tsconfig/ - Shared TypeScript configs
|
|
# turbo.json - Turborepo configuration
|
|
# package.json - Root package.json
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```json
|
|
// turbo.json
|
|
{
|
|
"$schema": "https://turbo.build/schema.json",
|
|
"globalDependencies": ["**/.env.*local"],
|
|
"pipeline": {
|
|
"build": {
|
|
"dependsOn": ["^build"],
|
|
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
|
},
|
|
"test": {
|
|
"dependsOn": ["build"],
|
|
"outputs": ["coverage/**"]
|
|
},
|
|
"lint": {
|
|
"outputs": []
|
|
},
|
|
"dev": {
|
|
"cache": false,
|
|
"persistent": true
|
|
},
|
|
"type-check": {
|
|
"dependsOn": ["^build"],
|
|
"outputs": []
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```json
|
|
// package.json (root)
|
|
{
|
|
"name": "my-monorepo",
|
|
"private": true,
|
|
"workspaces": [
|
|
"apps/*",
|
|
"packages/*"
|
|
],
|
|
"scripts": {
|
|
"build": "turbo run build",
|
|
"dev": "turbo run dev",
|
|
"test": "turbo run test",
|
|
"lint": "turbo run lint",
|
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
"clean": "turbo run clean && rm -rf node_modules"
|
|
},
|
|
"devDependencies": {
|
|
"turbo": "^1.10.0",
|
|
"prettier": "^3.0.0",
|
|
"typescript": "^5.0.0"
|
|
},
|
|
"packageManager": "pnpm@8.0.0"
|
|
}
|
|
```
|
|
|
|
### Package Structure
|
|
|
|
```json
|
|
// packages/ui/package.json
|
|
{
|
|
"name": "@repo/ui",
|
|
"version": "0.0.0",
|
|
"private": true,
|
|
"main": "./dist/index.js",
|
|
"types": "./dist/index.d.ts",
|
|
"exports": {
|
|
".": {
|
|
"import": "./dist/index.js",
|
|
"types": "./dist/index.d.ts"
|
|
},
|
|
"./button": {
|
|
"import": "./dist/button.js",
|
|
"types": "./dist/button.d.ts"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
"lint": "eslint src/",
|
|
"type-check": "tsc --noEmit"
|
|
},
|
|
"devDependencies": {
|
|
"@repo/tsconfig": "workspace:*",
|
|
"tsup": "^7.0.0",
|
|
"typescript": "^5.0.0"
|
|
},
|
|
"dependencies": {
|
|
"react": "^18.2.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## pnpm Workspaces
|
|
|
|
### Setup
|
|
|
|
```yaml
|
|
# pnpm-workspace.yaml
|
|
packages:
|
|
- 'apps/*'
|
|
- 'packages/*'
|
|
- 'tools/*'
|
|
```
|
|
|
|
```json
|
|
// .npmrc
|
|
# Hoist shared dependencies
|
|
shamefully-hoist=true
|
|
|
|
# Strict peer dependencies
|
|
auto-install-peers=true
|
|
strict-peer-dependencies=true
|
|
|
|
# Performance
|
|
store-dir=~/.pnpm-store
|
|
```
|
|
|
|
### Dependency Management
|
|
|
|
```bash
|
|
# Install dependency in specific package
|
|
pnpm add react --filter @repo/ui
|
|
pnpm add -D typescript --filter @repo/ui
|
|
|
|
# Install workspace dependency
|
|
pnpm add @repo/ui --filter web
|
|
|
|
# Install in all packages
|
|
pnpm add -D eslint -w
|
|
|
|
# Update all dependencies
|
|
pnpm update -r
|
|
|
|
# Remove dependency
|
|
pnpm remove react --filter @repo/ui
|
|
```
|
|
|
|
### Scripts
|
|
|
|
```bash
|
|
# Run script in specific package
|
|
pnpm --filter web dev
|
|
pnpm --filter @repo/ui build
|
|
|
|
# Run in all packages
|
|
pnpm -r build
|
|
pnpm -r test
|
|
|
|
# Run in parallel
|
|
pnpm -r --parallel dev
|
|
|
|
# Filter by pattern
|
|
pnpm --filter "@repo/*" build
|
|
pnpm --filter "...web" build # Build web and dependencies
|
|
```
|
|
|
|
## Nx Monorepo
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
# Create Nx monorepo
|
|
npx create-nx-workspace@latest my-org
|
|
|
|
# Generate applications
|
|
nx generate @nx/react:app my-app
|
|
nx generate @nx/next:app my-next-app
|
|
|
|
# Generate libraries
|
|
nx generate @nx/react:lib ui-components
|
|
nx generate @nx/js:lib utils
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```json
|
|
// nx.json
|
|
{
|
|
"extends": "nx/presets/npm.json",
|
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
|
"targetDefaults": {
|
|
"build": {
|
|
"dependsOn": ["^build"],
|
|
"inputs": ["production", "^production"],
|
|
"cache": true
|
|
},
|
|
"test": {
|
|
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
|
|
"cache": true
|
|
},
|
|
"lint": {
|
|
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
|
|
"cache": true
|
|
}
|
|
},
|
|
"namedInputs": {
|
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
|
"production": [
|
|
"default",
|
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
|
"!{projectRoot}/tsconfig.spec.json"
|
|
],
|
|
"sharedGlobals": []
|
|
}
|
|
}
|
|
```
|
|
|
|
### Running Tasks
|
|
|
|
```bash
|
|
# Run task for specific project
|
|
nx build my-app
|
|
nx test ui-components
|
|
nx lint utils
|
|
|
|
# Run for affected projects
|
|
nx affected:build
|
|
nx affected:test --base=main
|
|
|
|
# Visualize dependencies
|
|
nx graph
|
|
|
|
# Run in parallel
|
|
nx run-many --target=build --all --parallel=3
|
|
```
|
|
|
|
## Shared Configurations
|
|
|
|
### TypeScript Configuration
|
|
|
|
```json
|
|
// packages/tsconfig/base.json
|
|
{
|
|
"compilerOptions": {
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"module": "ESNext",
|
|
"moduleResolution": "bundler",
|
|
"resolveJsonModule": true,
|
|
"isolatedModules": true,
|
|
"incremental": true,
|
|
"declaration": true
|
|
},
|
|
"exclude": ["node_modules"]
|
|
}
|
|
|
|
// packages/tsconfig/react.json
|
|
{
|
|
"extends": "./base.json",
|
|
"compilerOptions": {
|
|
"jsx": "react-jsx",
|
|
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
}
|
|
}
|
|
|
|
// apps/web/tsconfig.json
|
|
{
|
|
"extends": "@repo/tsconfig/react.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src"
|
|
},
|
|
"include": ["src"],
|
|
"exclude": ["node_modules", "dist"]
|
|
}
|
|
```
|
|
|
|
### ESLint Configuration
|
|
|
|
```javascript
|
|
// packages/config/eslint-preset.js
|
|
module.exports = {
|
|
extends: [
|
|
'eslint:recommended',
|
|
'plugin:@typescript-eslint/recommended',
|
|
'plugin:react/recommended',
|
|
'plugin:react-hooks/recommended',
|
|
'prettier',
|
|
],
|
|
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
|
|
parser: '@typescript-eslint/parser',
|
|
parserOptions: {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'module',
|
|
ecmaFeatures: {
|
|
jsx: true,
|
|
},
|
|
},
|
|
settings: {
|
|
react: {
|
|
version: 'detect',
|
|
},
|
|
},
|
|
rules: {
|
|
'@typescript-eslint/no-unused-vars': 'error',
|
|
'react/react-in-jsx-scope': 'off',
|
|
},
|
|
};
|
|
|
|
// apps/web/.eslintrc.js
|
|
module.exports = {
|
|
extends: ['@repo/config/eslint-preset'],
|
|
rules: {
|
|
// App-specific rules
|
|
},
|
|
};
|
|
```
|
|
|
|
## Code Sharing Patterns
|
|
|
|
### Pattern 1: Shared UI Components
|
|
|
|
```typescript
|
|
// packages/ui/src/button.tsx
|
|
import * as React from 'react';
|
|
|
|
export interface ButtonProps {
|
|
variant?: 'primary' | 'secondary';
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={`btn btn-${variant}`}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// packages/ui/src/index.ts
|
|
export { Button, type ButtonProps } from './button';
|
|
export { Input, type InputProps } from './input';
|
|
|
|
// apps/web/src/app.tsx
|
|
import { Button } from '@repo/ui';
|
|
|
|
export function App() {
|
|
return <Button variant="primary">Click me</Button>;
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Shared Utilities
|
|
|
|
```typescript
|
|
// packages/utils/src/string.ts
|
|
export function capitalize(str: string): string {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
export function truncate(str: string, length: number): string {
|
|
return str.length > length ? str.slice(0, length) + '...' : str;
|
|
}
|
|
|
|
// packages/utils/src/index.ts
|
|
export * from './string';
|
|
export * from './array';
|
|
export * from './date';
|
|
|
|
// Usage in apps
|
|
import { capitalize, truncate } from '@repo/utils';
|
|
```
|
|
|
|
### Pattern 3: Shared Types
|
|
|
|
```typescript
|
|
// packages/types/src/user.ts
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
role: 'admin' | 'user';
|
|
}
|
|
|
|
export interface CreateUserInput {
|
|
email: string;
|
|
name: string;
|
|
password: string;
|
|
}
|
|
|
|
// Used in both frontend and backend
|
|
import type { User, CreateUserInput } from '@repo/types';
|
|
```
|
|
|
|
## Build Optimization
|
|
|
|
### Turborepo Caching
|
|
|
|
```json
|
|
// turbo.json
|
|
{
|
|
"pipeline": {
|
|
"build": {
|
|
// Build depends on dependencies being built first
|
|
"dependsOn": ["^build"],
|
|
|
|
// Cache these outputs
|
|
"outputs": ["dist/**", ".next/**"],
|
|
|
|
// Cache based on these inputs (default: all files)
|
|
"inputs": ["src/**/*.tsx", "src/**/*.ts", "package.json"]
|
|
},
|
|
"test": {
|
|
// Run tests in parallel, don't depend on build
|
|
"cache": true,
|
|
"outputs": ["coverage/**"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Remote Caching
|
|
|
|
```bash
|
|
# Turborepo Remote Cache (Vercel)
|
|
npx turbo login
|
|
npx turbo link
|
|
|
|
# Custom remote cache
|
|
# turbo.json
|
|
{
|
|
"remoteCache": {
|
|
"signature": true,
|
|
"enabled": true
|
|
}
|
|
}
|
|
```
|
|
|
|
## CI/CD for Monorepos
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
# .github/workflows/ci.yml
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
with:
|
|
fetch-depth: 0 # For Nx affected commands
|
|
|
|
- uses: pnpm/action-setup@v2
|
|
with:
|
|
version: 8
|
|
|
|
- uses: actions/setup-node@v3
|
|
with:
|
|
node-version: 18
|
|
cache: 'pnpm'
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build
|
|
run: pnpm turbo run build
|
|
|
|
- name: Test
|
|
run: pnpm turbo run test
|
|
|
|
- name: Lint
|
|
run: pnpm turbo run lint
|
|
|
|
- name: Type check
|
|
run: pnpm turbo run type-check
|
|
```
|
|
|
|
### Deploy Affected Only
|
|
|
|
```yaml
|
|
# Deploy only changed apps
|
|
- name: Deploy affected apps
|
|
run: |
|
|
if pnpm nx affected:apps --base=origin/main --head=HEAD | grep -q "web"; then
|
|
echo "Deploying web app"
|
|
pnpm --filter web deploy
|
|
fi
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Consistent Versioning**: Lock dependency versions across workspace
|
|
2. **Shared Configs**: Centralize ESLint, TypeScript, Prettier configs
|
|
3. **Dependency Graph**: Keep it acyclic, avoid circular dependencies
|
|
4. **Cache Effectively**: Configure inputs/outputs correctly
|
|
5. **Type Safety**: Share types between frontend/backend
|
|
6. **Testing Strategy**: Unit tests in packages, E2E in apps
|
|
7. **Documentation**: README in each package
|
|
8. **Release Strategy**: Use changesets for versioning
|
|
|
|
## Common Pitfalls
|
|
|
|
- **Circular Dependencies**: A depends on B, B depends on A
|
|
- **Phantom Dependencies**: Using deps not in package.json
|
|
- **Incorrect Cache Inputs**: Missing files in Turborepo inputs
|
|
- **Over-Sharing**: Sharing code that should be separate
|
|
- **Under-Sharing**: Duplicating code across packages
|
|
- **Large Monorepos**: Without proper tooling, builds slow down
|
|
|
|
## Publishing Packages
|
|
|
|
```bash
|
|
# Using Changesets
|
|
pnpm add -Dw @changesets/cli
|
|
pnpm changeset init
|
|
|
|
# Create changeset
|
|
pnpm changeset
|
|
|
|
# Version packages
|
|
pnpm changeset version
|
|
|
|
# Publish
|
|
pnpm changeset publish
|
|
```
|
|
|
|
```yaml
|
|
# .github/workflows/release.yml
|
|
- name: Create Release Pull Request or Publish
|
|
uses: changesets/action@v1
|
|
with:
|
|
publish: pnpm release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
```
|
|
|
|
## Resources
|
|
|
|
- **references/turborepo-guide.md**: Comprehensive Turborepo documentation
|
|
- **references/nx-guide.md**: Nx monorepo patterns
|
|
- **references/pnpm-workspaces.md**: pnpm workspace features
|
|
- **assets/monorepo-checklist.md**: Setup checklist
|
|
- **assets/migration-guide.md**: Multi-repo to monorepo migration
|
|
- **scripts/dependency-graph.ts**: Visualize package dependencies
|