517 lines
12 KiB
Markdown
517 lines
12 KiB
Markdown
# monorepo-navigator reference
|
|
|
|
## Turborepo
|
|
|
|
### turbo.json pipeline config
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://turbo.build/schema.json",
|
|
"globalEnv": ["NODE_ENV", "DATABASE_URL"],
|
|
"pipeline": {
|
|
"build": {
|
|
"dependsOn": ["^build"], // build deps first (topological order)
|
|
"outputs": [".next/**", "dist/**", "build/**"],
|
|
"env": ["NEXT_PUBLIC_API_URL"]
|
|
},
|
|
"test": {
|
|
"dependsOn": ["^build"], // need built deps to test
|
|
"outputs": ["coverage/**"],
|
|
"cache": true
|
|
},
|
|
"lint": {
|
|
"outputs": [],
|
|
"cache": true
|
|
},
|
|
"dev": {
|
|
"cache": false, // never cache dev servers
|
|
"persistent": true // long-running process
|
|
},
|
|
"type-check": {
|
|
"dependsOn": ["^build"],
|
|
"outputs": []
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Key commands
|
|
|
|
```bash
|
|
# Build everything (respects dependency order)
|
|
turbo run build
|
|
|
|
# Build only affected packages (requires --filter)
|
|
turbo run build --filter=...[HEAD^1] # changed since last commit
|
|
turbo run build --filter=...[main] # changed vs main branch
|
|
|
|
# Test only affected
|
|
turbo run test --filter=...[HEAD^1]
|
|
|
|
# Run for a specific app and all its dependencies
|
|
turbo run build --filter=@myorg/web...
|
|
|
|
# Run for a specific package only (no dependencies)
|
|
turbo run build --filter=@myorg/ui
|
|
|
|
# Dry-run — see what would run without executing
|
|
turbo run build --dry-run
|
|
|
|
# Enable remote caching (Vercel Remote Cache)
|
|
turbo login
|
|
turbo link
|
|
```
|
|
|
|
### Remote caching setup
|
|
|
|
```bash
|
|
# .turbo/config.json (auto-created by turbo link)
|
|
{
|
|
"teamid": "team_xxxx",
|
|
"apiurl": "https://vercel.com"
|
|
}
|
|
|
|
# Self-hosted cache server (open-source alternative)
|
|
# Run ducktape/turborepo-remote-cache or Turborepo's official server
|
|
TURBO_API=http://your-cache-server.internal \
|
|
TURBO_TOKEN=your-token \
|
|
TURBO_TEAM=your-team \
|
|
turbo run build
|
|
```
|
|
|
|
---
|
|
|
|
## Nx
|
|
|
|
### Project graph and affected commands
|
|
|
|
```bash
|
|
# Install
|
|
npx create-nx-workspace@latest my-monorepo
|
|
|
|
# Visualize the project graph (opens browser)
|
|
nx graph
|
|
|
|
# Show affected packages for the current branch
|
|
nx affected:graph
|
|
|
|
# Run only affected tests
|
|
nx affected --target=test
|
|
|
|
# Run only affected builds
|
|
nx affected --target=build
|
|
|
|
# Run affected with base/head (for CI)
|
|
nx affected --target=test --base=main --head=HEAD
|
|
```
|
|
|
|
### nx.json configuration
|
|
|
|
```json
|
|
{
|
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
|
"targetDefaults": {
|
|
"build": {
|
|
"dependsOn": ["^build"],
|
|
"cache": true
|
|
},
|
|
"test": {
|
|
"cache": true,
|
|
"inputs": ["default", "^production"]
|
|
}
|
|
},
|
|
"namedInputs": {
|
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
|
"production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/jest.config.*"],
|
|
"sharedGlobals": []
|
|
},
|
|
"parallel": 4,
|
|
"cacheDirectory": "/tmp/nx-cache"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## pnpm Workspaces
|
|
|
|
### pnpm-workspace.yaml
|
|
|
|
```yaml
|
|
packages:
|
|
- 'apps/*'
|
|
- 'packages/*'
|
|
- 'tools/*'
|
|
```
|
|
|
|
### workspace:* protocol for local packages
|
|
|
|
```json
|
|
// apps/web/package.json
|
|
{
|
|
"name": "@myorg/web",
|
|
"dependencies": {
|
|
"@myorg/ui": "workspace:*", // always use local version
|
|
"@myorg/utils": "workspace:^", // local, but respect semver on publish
|
|
"@myorg/types": "workspace:~"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Useful pnpm workspace commands
|
|
|
|
```bash
|
|
# Install all packages across workspace
|
|
pnpm install
|
|
|
|
# Run script in a specific package
|
|
pnpm --filter @myorg/web dev
|
|
|
|
# Run script in all packages
|
|
pnpm --filter "*" build
|
|
|
|
# Run script in a package and all its dependencies
|
|
pnpm --filter @myorg/web... build
|
|
|
|
# Add a dependency to a specific package
|
|
pnpm --filter @myorg/web add react
|
|
|
|
# Add a shared dev dependency to root
|
|
pnpm add -D typescript -w
|
|
|
|
# List workspace packages
|
|
pnpm ls --depth -1 -r
|
|
```
|
|
|
|
---
|
|
|
|
## Cross-Package Impact Analysis
|
|
|
|
When a shared package changes, determine what's affected before you ship.
|
|
|
|
```bash
|
|
# Using Turborepo — show affected packages
|
|
turbo run build --filter=...[HEAD^1] --dry-run 2>&1 | grep "Tasks to run"
|
|
|
|
# Using Nx
|
|
nx affected:apps --base=main --head=HEAD # which apps are affected
|
|
nx affected:libs --base=main --head=HEAD # which libs are affected
|
|
|
|
# Manual analysis with pnpm
|
|
# Find all packages that depend on @myorg/utils:
|
|
grep -r '"@myorg/utils"' packages/*/package.json apps/*/package.json
|
|
|
|
# Using jq for structured output
|
|
for pkg in packages/*/package.json apps/*/package.json; do
|
|
name=$(jq -r '.name' "$pkg")
|
|
if jq -e '.dependencies["@myorg/utils"] // .devDependencies["@myorg/utils"]' "$pkg" > /dev/null 2>&1; then
|
|
echo "$name depends on @myorg/utils"
|
|
fi
|
|
done
|
|
```
|
|
|
|
---
|
|
|
|
## Dependency Graph Visualization
|
|
|
|
Generate a Mermaid diagram from your workspace:
|
|
|
|
```bash
|
|
# Generate dependency graph as Mermaid
|
|
cat > scripts/gen-dep-graph.js << 'EOF'
|
|
const { execSync } = require('child_process');
|
|
const fs = require('fs');
|
|
|
|
// Parse pnpm workspace packages
|
|
const packages = JSON.parse(
|
|
execSync('pnpm ls --depth -1 -r --json').toString()
|
|
);
|
|
|
|
let mermaid = 'graph TD\n';
|
|
packages.forEach(pkg => {
|
|
const deps = Object.keys(pkg.dependencies || {})
|
|
.filter(d => d.startsWith('@myorg/'));
|
|
deps.forEach(dep => {
|
|
const from = pkg.name.replace('@myorg/', '');
|
|
const to = dep.replace('@myorg/', '');
|
|
mermaid += ` ${from} --> ${to}\n`;
|
|
});
|
|
});
|
|
|
|
fs.writeFileSync('docs/dep-graph.md', '```mermaid\n' + mermaid + '```\n');
|
|
console.log('Written to docs/dep-graph.md');
|
|
EOF
|
|
node scripts/gen-dep-graph.js
|
|
```
|
|
|
|
**Example output:**
|
|
|
|
```mermaid
|
|
graph TD
|
|
web --> ui
|
|
web --> utils
|
|
web --> types
|
|
mobile --> ui
|
|
mobile --> utils
|
|
mobile --> types
|
|
admin --> ui
|
|
admin --> utils
|
|
api --> types
|
|
ui --> utils
|
|
```
|
|
|
|
---
|
|
|
|
## Claude Code Configuration (Workspace-Aware CLAUDE.md)
|
|
|
|
Place a root CLAUDE.md + per-package CLAUDE.md files:
|
|
|
|
```markdown
|
|
# /CLAUDE.md — Root (applies to all packages)
|
|
|
|
## Monorepo Structure
|
|
- apps/web — Next.js customer-facing app
|
|
- apps/admin — Next.js internal admin
|
|
- apps/api — Express REST API
|
|
- packages/ui — Shared React component library
|
|
- packages/utils — Shared utilities (pure functions only)
|
|
- packages/types — Shared TypeScript types (no runtime code)
|
|
|
|
## Build System
|
|
- pnpm workspaces + Turborepo
|
|
- Always use `pnpm --filter <package>` to scope commands
|
|
- Never run `npm install` or `yarn` — pnpm only
|
|
- Run `turbo run build --filter=...[HEAD^1]` before committing
|
|
|
|
## Task Scoping Rules
|
|
- When modifying packages/ui: also run tests for apps/web and apps/admin (they depend on it)
|
|
- When modifying packages/types: run type-check across ALL packages
|
|
- When modifying apps/api: only need to test apps/api
|
|
|
|
## Package Manager
|
|
pnpm — version pinned in packageManager field of root package.json
|
|
```
|
|
|
|
```markdown
|
|
# /packages/ui/CLAUDE.md — Package-specific
|
|
|
|
## This Package
|
|
Shared React component library. Zero business logic. Pure UI only.
|
|
|
|
## Rules
|
|
- All components must be exported from src/index.ts
|
|
- No direct API calls in components — accept data via props
|
|
- Every component needs a Storybook story in src/stories/
|
|
- Use Tailwind for styling — no CSS modules or styled-components
|
|
|
|
## Testing
|
|
- Component tests: `pnpm --filter @myorg/ui test`
|
|
- Visual regression: `pnpm --filter @myorg/ui test:storybook`
|
|
|
|
## Publishing
|
|
- Version bumps via changesets only — never edit package.json version manually
|
|
- Run `pnpm changeset` from repo root after changes
|
|
```
|
|
|
|
---
|
|
|
|
## Migration: Multi-Repo → Monorepo
|
|
|
|
```bash
|
|
# Step 1: Create monorepo scaffold
|
|
mkdir my-monorepo && cd my-monorepo
|
|
pnpm init
|
|
echo "packages:\n - 'apps/*'\n - 'packages/*'" > pnpm-workspace.yaml
|
|
|
|
# Step 2: Move repos with git history preserved
|
|
mkdir -p apps packages
|
|
|
|
# For each existing repo:
|
|
git clone https://github.com/myorg/web-app
|
|
cd web-app
|
|
git filter-repo --to-subdirectory-filter apps/web # rewrites history into subdir
|
|
cd ..
|
|
git remote add web-app ./web-app
|
|
git fetch web-app --tags
|
|
git merge web-app/main --allow-unrelated-histories
|
|
|
|
# Step 3: Update package names to scoped
|
|
# In each package.json, change "name": "web" to "name": "@myorg/web"
|
|
|
|
# Step 4: Replace cross-repo npm deps with workspace:*
|
|
# apps/web/package.json: "@myorg/ui": "1.2.3" → "@myorg/ui": "workspace:*"
|
|
|
|
# Step 5: Add shared configs to root
|
|
cp apps/web/.eslintrc.js .eslintrc.base.js
|
|
# Update each package's config to extend root:
|
|
# { "extends": ["../../.eslintrc.base.js"] }
|
|
|
|
# Step 6: Add Turborepo
|
|
pnpm add -D turbo -w
|
|
# Create turbo.json (see above)
|
|
|
|
# Step 7: Unified CI (see CI section below)
|
|
# Step 8: Test everything
|
|
turbo run build test lint
|
|
```
|
|
|
|
---
|
|
|
|
## CI Patterns
|
|
|
|
### GitHub Actions — Affected Only
|
|
|
|
```yaml
|
|
# .github/workflows/ci.yml
|
|
name: "ci"
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
jobs:
|
|
affected:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0 # full history needed for affected detection
|
|
|
|
- uses: pnpm/action-setup@v3
|
|
with:
|
|
version: 9
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: pnpm
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
# Turborepo remote cache
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: .turbo
|
|
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
|
restore-keys: ${{ runner.os }}-turbo-
|
|
|
|
# Only test/build affected packages
|
|
- name: "build-affected"
|
|
run: turbo run build --filter=...[origin/main]
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
|
|
- name: "test-affected"
|
|
run: turbo run test --filter=...[origin/main]
|
|
|
|
- name: "lint-affected"
|
|
run: turbo run lint --filter=...[origin/main]
|
|
```
|
|
|
|
### GitLab CI — Parallel Stages
|
|
|
|
```yaml
|
|
# .gitlab-ci.yml
|
|
stages: [install, build, test, publish]
|
|
|
|
variables:
|
|
PNPM_CACHE_FOLDER: .pnpm-store
|
|
|
|
cache:
|
|
key: pnpm-$CI_COMMIT_REF_SLUG
|
|
paths: [.pnpm-store/, .turbo/]
|
|
|
|
install:
|
|
stage: install
|
|
script:
|
|
- pnpm install --frozen-lockfile
|
|
artifacts:
|
|
paths: [node_modules/, packages/*/node_modules/, apps/*/node_modules/]
|
|
expire_in: 1h
|
|
|
|
build:affected:
|
|
stage: build
|
|
needs: [install]
|
|
script:
|
|
- turbo run build --filter=...[origin/main]
|
|
artifacts:
|
|
paths: [apps/*/dist/, apps/*/.next/, packages/*/dist/]
|
|
|
|
test:affected:
|
|
stage: test
|
|
needs: [build:affected]
|
|
script:
|
|
- turbo run test --filter=...[origin/main]
|
|
coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
|
|
artifacts:
|
|
reports:
|
|
coverage_report:
|
|
coverage_format: cobertura
|
|
path: "**/coverage/cobertura-coverage.xml"
|
|
```
|
|
|
|
---
|
|
|
|
## Publishing with Changesets
|
|
|
|
```bash
|
|
# Install changesets
|
|
pnpm add -D @changesets/cli -w
|
|
pnpm changeset init
|
|
|
|
# After making changes, create a changeset
|
|
pnpm changeset
|
|
# Interactive: select packages, choose semver bump, write changelog entry
|
|
|
|
# In CI — version packages + update changelogs
|
|
pnpm changeset version
|
|
|
|
# Publish all changed packages
|
|
pnpm changeset publish
|
|
|
|
# Pre-release channel (for alpha/beta)
|
|
pnpm changeset pre enter beta
|
|
pnpm changeset
|
|
pnpm changeset version # produces 1.2.0-beta.0
|
|
pnpm changeset publish --tag beta
|
|
pnpm changeset pre exit # back to stable releases
|
|
```
|
|
|
|
### Automated publish workflow (GitHub Actions)
|
|
|
|
```yaml
|
|
# .github/workflows/release.yml
|
|
name: "release"
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: pnpm/action-setup@v3
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
registry-url: https://registry.npmjs.org
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: "create-release-pr-or-publish"
|
|
uses: changesets/action@v1
|
|
with:
|
|
publish: pnpm changeset publish
|
|
version: pnpm changeset version
|
|
commit: "chore: release packages"
|
|
title: "chore: release packages"
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
```
|
|
|
|
---
|