
Monorepo Management
- 98 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Helps with ai & agent building tasks.
About
monorepo-management is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- monorepo-management
- AI & Agent Building
- AI-coding skill
Monorepo Management by the numbers
- 98 all-time installs (skills.sh)
- Ranked #4,469 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill monorepo-managementAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 98 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Monorepo Management
Monorepo tooling and workspace architecture expert for JavaScript and TypeScript projects. Covers tool selection, task pipeline configuration, dependency graph management, CI optimization, and versioning strategy.
When to Use
Use for:
- Choosing between Turborepo, Nx, Lerna, and Rush for a new or migrating repository
- Configuring
turbo.jsontask pipelines, remote caching, and Docker pruning - Structuring workspace packages: apps, packages, shared configs, internal libraries
- Setting up pnpm or npm workspaces with correct hoisting rules
- Eliminating circular dependencies between workspace packages
- Configuring path-based CI with affected package detection
- Managing package versioning and changelogs with Changesets
NOT for:
- Git submodules or polyrepo coordination → discuss trade-offs separately
- Bazel, Pants, or Buck for non-JavaScript monorepos
- Single-package repository setup → use project-level tooling skills
- Container orchestration of services within the monorepo → use docker-containerization
---
Tool Selection Decision Tree
flowchart TD
A[Monorepo tool needed] --> B{Team size and complexity?}
B -->|Small team, apps-first| C{Need plugin ecosystem?}
B -->|Large org, many teams| D[Rush or Nx]
C -->|No — just fast builds| E[Turborepo]
C -->|Yes — code gen, generators| F[Nx]
D --> G{Microsoft/enterprise patterns?}
G -->|Yes| H[Rush]
G -->|No| I[Nx]
E --> J{Package manager preference?}
J -->|pnpm — recommended| K[pnpm + Turborepo]
J -->|npm or yarn| L[npm/yarn workspaces + Turborepo]
I --> M[Nx Cloud for remote caching]
H --> N[Rush's own cache]
K --> O[Vercel Remote Cache\nor self-hosted]Tool Comparison Summary
| Tool | Best For | Remote Cache | Learning Curve |
|---|---|---|---|
| Turborepo | Apps-first, fast builds, simple config | Vercel / self-hosted | Low |
| Nx | Library-heavy, code generation, plugin ecosystem | Nx Cloud / self-hosted | Medium |
| Rush | Enterprise, Microsoft stack, strict isolation | Custom | High |
| Lerna | Legacy — migrating from | None (use with Turborepo) | Low |
Recommendation for new projects: Start with pnpm workspaces + Turborepo. Migrate to Nx if you need advanced code generation or project graph visualization.
---
Workspace Architecture Decision Tree
flowchart TD
A[New package in monorepo?] --> B{Who consumes it?}
B -->|Internal only, not published| C[Internal package:\nno versioning, workspace: protocol]
B -->|Published to npm| D[Published package:\nChangesets, semver, CHANGELOG]
B -->|Shared config only| E[Config package:\neslint-config-*, tsconfig-*]
C --> F{What type?}
F -->|UI components| G[packages/ui]
F -->|Business logic / utilities| H[packages/utils or packages/core]
F -->|Shared types| I[packages/types]
F -->|API client| J[packages/api-client]
D --> K[packages/publishable-name]
A --> L{Is it an application?}
L -->|Yes| M[apps/ directory:\nnext-app, api, docs-site]---
Dependency Graph and Build Pipeline
graph LR
A[apps/web] --> B[packages/ui]
A --> C[packages/utils]
A --> D[packages/types]
E[apps/api] --> C
E --> D
B --> D
F[packages/ui-icons] --> D
B --> F
style A fill:#4a9d9e,color:#fff
style E fill:#4a9d9e,color:#fff
style B fill:#6b7280,color:#fff
style C fill:#6b7280,color:#fff
style D fill:#9ca3af
style F fill:#9ca3afReading the graph: Build order flows from dependencies to dependents. packages/types (no deps) builds first. packages/ui builds after types. apps/web builds last. Turborepo and Nx compute this graph automatically — you define task dependencies, they order execution.
---
Turborepo Configuration
turbo.json — Task Pipeline
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "package.json", "tsconfig.json"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "**/*.test.ts", "**/*.test.tsx"],
"outputs": ["coverage/**"]
},
"lint": {
"inputs": ["src/**/*.ts", "src/**/*.tsx", ".eslintrc*"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Key concepts:
^buildmeans "build all dependencies first before building this package"inputsdetermines cache keys — changes outside inputs don't invalidate the cacheoutputsare stored in cache and restored on cache hitcache: falsefor long-running tasks (dev servers, watchers)persistent: truefor tasks that don't exit
Running Tasks
# Run build for all packages
turbo build
# Run only for packages affected by changes since main branch
turbo build --filter=...[origin/main]
# Run for a specific app and its dependencies
turbo build --filter=web...
# Run in parallel across packages
turbo lint typecheck --parallel
# Dry run to see what would execute
turbo build --dry-runRemote Caching
# Login to Vercel remote cache (free for open source)
npx turbo login
# Link to team/project
npx turbo link
# CI: pass token via environment
TURBO_TOKEN=$TURBO_TOKEN turbo buildSelf-hosted alternative: turbo-remote-cache package or Turborepo's built-in HTTP cache server in Turborepo 2.x.
Consult references/turborepo-patterns.md for Docker pruning for deployment, scoped filtering, and advanced pipeline patterns.
---
pnpm Workspaces
pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'package.json workspace dependencies
{
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^1.0.0"
}
}Use workspace:* for internal packages that should always match the local version. Use workspace:^ only for internal packages that are also published and where you want semver range resolution.
Hoisting Control
# .npmrc — control hoisting behavior
hoist=true
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
shamefully-hoist=false # never — breaks encapsulationshamefully-hoist=true is a trap: it makes all packages available everywhere but breaks encapsulation. If your tools require it, fix the tool dependency instead.
---
Changesets for Versioning
# Initialize in your monorepo
npx changeset init
# Create a changeset when making a change
npx changeset
# Prompts: which packages changed, major/minor/patch, description
# Preview what versions will be bumped
npx changeset status
# Bump versions and update changelogs (CI or release branch)
npx changeset version
# Publish to npm
npx changeset publish.changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myorg/app-web", "@myorg/app-api"]
}Set access: "public" for open-source packages. ignore lists apps that should not be published to npm.
Consult references/workspace-architecture.md for full CODEOWNERS setup, ESLint config sharing, and shared tsconfig patterns.
---
Path-Based CI (Only Test What Changed)
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for turbo --filter to work correctly
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build affected packages
run: pnpm turbo build --filter=...[origin/main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test affected packages
run: pnpm turbo test --filter=...[origin/main]--filter=...[origin/main] means "run for packages whose files changed compared to main, plus all packages that depend on them (upstream consumers)."
---
Anti-Patterns
Anti-Pattern: No Task Caching (Rebuilding Everything)
Novice: "We run turbo build and it always rebuilds all 40 packages, even when only one changed."
Expert: Turborepo caches by computing a hash of inputs (source files + env + turbo config). If inputs haven't changed, the output is restored from cache in milliseconds. The most common cause of cache misses is missing inputs declarations — Turborepo falls back to hashing the entire package directory, including files like .DS_Store, node_modules, and IDE configs that change constantly.
Detection: Run turbo build --verbosity=2 and look for "MISS" next to packages. Check whether .turbo cache entries exist and if the hash matches between runs.
Fix: Explicitly declare inputs in turbo.json to include only files that affect build output. Explicitly declare outputs so the cache knows what to store. Add non-source files to .gitignore and .turboignore.
LLM mistake: Tutorials often omit inputs and outputs because a working demo doesn't need them. Production repos require explicit declarations or cache hit rates stay near 0%.
---
Anti-Pattern: Circular Dependencies Between Workspace Packages
Novice: "packages/auth imports from packages/api-client and packages/api-client imports from packages/auth — is that a problem?"
Expert: Yes. Circular dependencies make build order impossible to determine. Turborepo will error on cycles. More importantly, circular deps indicate a design flaw: two packages whose concerns are entangled. The fix is to extract the shared types or utilities to a third package that both can import without creating a cycle.
Detection:
# Turborepo detects cycles and refuses to run
turbo build # "Error: Package graph cycle detected"
# Manual detection with madge
npx madge --circular --extensions ts packages/Fix:
Before:
packages/auth → packages/api-client → packages/auth (cycle!)
After:
packages/types (new: shared auth types, no dependencies)
packages/api-client → packages/types
packages/auth → packages/types
packages/auth → packages/api-client (one-way, no cycle)Timeline: This is not a new problem — circular deps have been a JavaScript packaging issue since npm v1 (2010). The reason it appears in monorepos specifically is that workspace packages make it easy to import across package boundaries without thinking about dependency direction.
---
Anti-Pattern: Using shamefully-hoist=true in pnpm
Novice: "My CLI tool can't find its peer dependency. I'll add shamefully-hoist=true to .npmrc to fix it."
Expert: shamefully-hoist makes pnpm behave like npm/yarn classic, putting all packages in a flat node_modules. This "fixes" the immediate issue but breaks pnpm's strict isolation, which is the entire reason to use pnpm. The right fix is to add the missing peer dependency to the package that needs it, or use public-hoist-pattern to hoist only the specific package that requires it.
Detection: Any .npmrc with shamefully-hoist=true in a pnpm workspace.
---
References
references/turborepo-patterns.md— Consult when configuring turbo.json, setting up remote caching, using Docker pruning for deployment, or debugging cache misses.references/workspace-architecture.md— Consult when designing package boundaries, sharing ESLint/TypeScript configs, setting up CODEOWNERS, or planning a migration from single-repo to monorepo.
Turborepo Patterns Reference
turbo.json Configuration Deep Dive
Full Task Configuration Options
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"src/**",
"public/**",
"package.json",
"tsconfig.json",
"next.config.*",
"!**/*.test.*",
"!**/*.spec.*"
],
"outputs": [
".next/**",
"!.next/cache/**",
"dist/**",
"build/**"
],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "**/*.test.*", "**/*.spec.*", "vitest.config.*"],
"outputs": ["coverage/**"],
"env": ["NODE_ENV"]
},
"lint": {
"inputs": ["src/**", ".eslintrc*", "eslint.config.*"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
},
"globalEnv": ["CI", "VERCEL_ENV"],
"globalDependencies": [".env.local", "turbo.json"]
}Input Patterns
src/**— all files under src!**/*.test.*— exclude test files from build cache key (tests don't affect build output)$TURBO_DEFAULT$— expands to the default Turborepo input set (all files tracked by git)
Critical: env in task config adds environment variables to the cache key. If your build reads NEXT_PUBLIC_API_URL, add it here or cache hits in CI won't match local builds.
Output Patterns
.next/**followed by!.next/cache/**— include .next but exclude the Next.js build cache (it's large and managed separately)dist/**— TypeScript compiled output""— empty outputs array, still cache the task completion (useful for lint, typecheck)
---
Task Filtering
# Run for a specific package by name
turbo build --filter=web
turbo build --filter=@myorg/ui
# Run for a package and all its dependencies (packages it imports)
turbo build --filter=web...
# Run for a package and all packages that depend on it (downstream)
turbo build --filter=...web
# Packages changed since branching from main
turbo build --filter=...[origin/main]
# Changed packages and their dependents (full impact)
turbo build --filter=...[origin/main]...
# Exclude a package
turbo build --filter=!docs-site
# Multiple filters
turbo build --filter=web --filter=api
# From a specific directory
turbo build --filter=./apps/web---
Remote Caching
Vercel Remote Cache (Recommended)
# Authenticate (one-time setup)
npx turbo login
npx turbo link
# Environment variables for CI
TURBO_TOKEN=your-token # from Vercel dashboard → Settings → Tokens
TURBO_TEAM=your-org-slug # from Vercel team URL# GitHub Actions
- name: Build
run: turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Self-Hosted Remote Cache
Turborepo 2.x includes a built-in HTTP cache server:
# Start cache server
turbo daemon
# Or use the open-source turbo-remote-cache package
npx turbo-remote-cacheEnvironment variables for custom cache server:
TURBO_API=http://your-cache-server.internal
TURBO_TOKEN=your-token
TURBO_TEAM=your-team---
Docker Pruning for Deployment
Turborepo's prune command creates a minimal workspace with only the packages needed for a specific app. This is essential for Docker builds — without it, you'd copy the entire monorepo into the container.
# Prune to only what 'web' needs
turbo prune web --dockerThis produces:
out/
├── json/ # Only package.json files (for dependency install layer)
│ ├── package.json
│ └── packages/ui/package.json
└── full/ # Full source for packages that 'web' depends on
├── apps/web/
└── packages/ui/Dockerfile Pattern with Turbo Prune
FROM node:20-alpine AS base
RUN corepack enable pnpm
# Stage 1: Prune to only what 'web' needs
FROM base AS pruner
WORKDIR /app
COPY . .
RUN npx turbo prune web --docker
# Stage 2: Install dependencies (cached layer)
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
# Stage 3: Build
FROM installer AS builder
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo build --filter=web...
# Stage 4: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/public ./apps/web/public
EXPOSE 3000
CMD ["node", "apps/web/server.js"]Why this order matters: The out/json/ copy + install step is a separate Docker layer. If package.json files don't change, this layer is cached and pnpm install doesn't re-run. Source code changes only invalidate the build layer.
---
Debugging Cache Misses
# See exactly why each package ran or was cached
turbo build --verbosity=2
# Generate a graph of what would run
turbo build --graph
# Summarize cache status without running
turbo build --dry-run
# Force a run ignoring cache
turbo build --forceCommon cache miss causes:
1. Missing `inputs` field — Turborepo hashes the whole package directory including IDE files 2. Non-deterministic build — Build output differs on each run (timestamps in files, random seeds) 3. Environment variable not in `env` — Build uses an env var that's not in the cache key 4. Global dependency changed — globalDependencies includes a file that changed
Verification:
# Check what files Turborepo is hashing for a package
turbo build --summarize
# Creates .turbo/runs/HASH.json with full hash details---
Pipeline Dependency Patterns
{
"tasks": {
"build": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
},
"deploy": {
"dependsOn": ["build", "test", "^deploy"]
},
"db:migrate": {
"cache": false,
"dependsOn": ["^build"]
}
}
}"^build"— topological: dependencies build first"build"— same-package: this package's build must complete first"^deploy"— deploy all dependencies before deploying this package (useful for infrastructure ordering)
---
Package-Level turbo.json Overrides
Individual packages can override task config (Turborepo 2.x):
// apps/web/turbo.json
{
"extends": ["//"],
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_ANALYTICS_ID"]
}
}
}// refers to the root turbo.json. This is preferred over duplicating root config.
Workspace Architecture Reference
Directory Structure Patterns
Standard Layout
myorg-monorepo/
├── apps/
│ ├── web/ # Next.js consumer application
│ ├── api/ # Express/Hono/Fastify API
│ ├── docs/ # Documentation site (Docusaurus, Nextra)
│ └── admin/ # Internal admin dashboard
├── packages/
│ ├── ui/ # Shared React component library
│ ├── ui-icons/ # Icon components
│ ├── utils/ # Shared utility functions
│ ├── types/ # Shared TypeScript type definitions
│ ├── api-client/ # Generated or hand-written API client
│ └── database/ # Drizzle/Prisma schema and client
├── configs/
│ ├── eslint-config/ # Shared ESLint configuration
│ ├── tsconfig/ # Shared tsconfig bases
│ └── prettier-config/ # Shared Prettier configuration
├── tools/
│ └── scripts/ # Build, deploy, and maintenance scripts
├── pnpm-workspace.yaml
├── turbo.json
└── package.jsonRule: apps/ are deployable end products. packages/ are libraries consumed by apps or other packages. configs/ are configuration-only packages with no runtime code.
---
Shared TypeScript Configuration
configs/tsconfig/
├── package.json
├── base.json
├── nextjs.json
└── node.json// configs/tsconfig/package.json
{
"name": "@myorg/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["*.json"]
}// configs/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}// configs/tsconfig/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"plugins": [{"name": "next"}],
"module": "ESNext",
"incremental": true
}
}Consuming in an app:
// apps/web/tsconfig.json
{
"extends": "@myorg/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}---
Shared ESLint Configuration
configs/eslint-config/
├── package.json
├── index.js # ESLint flat config (ESLint 9+)
├── next.js
└── react-internal.js// configs/eslint-config/index.js
import js from "@eslint/js";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
export default [
js.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
plugins: { "@typescript-eslint": tsPlugin },
languageOptions: { parser: tsParser },
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"no-console": "warn"
}
}
];// configs/eslint-config/react-internal.js — for UI packages
import baseConfig from "./index.js";
import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";
export default [
...baseConfig,
{
plugins: { react: reactPlugin, "react-hooks": hooksPlugin },
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/prop-types": "off"
}
}
];Consuming in a package:
// packages/ui/eslint.config.js
import reactConfig from "@myorg/eslint-config/react-internal";
export default reactConfig;---
Internal Package Structure
Internal packages (not published to npm) have a simpler setup than published ones.
packages/ui/
├── src/
│ ├── index.ts # Public API: re-exports everything consumers need
│ ├── button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ └── index.ts
│ └── input/
│ ├── Input.tsx
│ └── index.ts
├── package.json
├── tsconfig.json
└── eslint.config.js// packages/ui/package.json — internal package
{
"name": "@myorg/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"devDependencies": {
"@myorg/tsconfig": "workspace:*",
"@myorg/eslint-config": "workspace:*",
"typescript": "^5.4.0"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
}Key pattern: Internal packages use ./src/index.ts as their entry point — raw TypeScript, not compiled. The consuming app's bundler (Next.js, Vite) compiles it. This avoids a separate build step for internal packages and gives the bundler full access to source for tree-shaking and fast refresh.
Published packages need a proper build step producing dist/ with compiled JS and .d.ts files.
---
Published Package Structure
// packages/publishable-thing/package.json
{
"name": "@myorg/publishable-thing",
"version": "1.2.3",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
}
}Use tsup for building published packages — it handles ESM + CJS dual output and .d.ts generation with minimal configuration.
---
CODEOWNERS
Place .github/CODEOWNERS at the repository root. GitHub automatically requests review from the listed owners for PRs that touch matching paths.
# .github/CODEOWNERS
# Default: these people are responsible for anything not matched below
* @myorg/platform-team
# Apps
/apps/web/ @myorg/frontend-team
/apps/api/ @myorg/backend-team
/apps/admin/ @myorg/frontend-team @myorg/platform-team
# Shared packages
/packages/ui/ @myorg/design-system-team
/packages/database/ @myorg/backend-team
/packages/types/ @myorg/platform-team
# Infrastructure and configs
/configs/ @myorg/platform-team
/turbo.json @myorg/platform-team
/pnpm-workspace.yaml @myorg/platform-team
/.github/ @myorg/platform-team
# Changesets
/.changeset/ @myorg/release-team---
Dependency Direction Rules
Enforcing a dependency direction policy prevents circular deps from accumulating.
Allowed directions:
apps/* → packages/* (apps consume packages)
packages/* → packages/* (packages consume other packages)
apps/* → configs/* (apps use shared configs)
packages/* → configs/* (packages use shared configs)
Forbidden:
packages/* → apps/* (packages must never import apps)
configs/* → packages/* (configs are config-only)
configs/* → apps/* (configs are config-only)Enforce with ESLint's import/no-restricted-paths or Nx's @nx/enforce-module-boundaries:
// ESLint rule (for projects not using Nx)
{
"import/no-restricted-paths": ["error", {
"zones": [
{
"target": "./packages",
"from": "./apps",
"message": "Packages cannot import from apps."
}
]
}]
}---
Migration: Single Repo to Monorepo
When converting an existing project to a monorepo, the order of operations matters:
1. Set up workspace tooling first — pnpm-workspace.yaml, turbo.json, shared configs 2. Move existing app to `apps/` — mv src apps/web/src, update paths 3. Extract one package — pick the clearest shared utility, create packages/utils/ 4. Wire workspace deps — replace relative imports with @myorg/utils workspace references 5. Configure CI — add --filter=...[origin/main] for affected-only runs 6. Extract more packages — continue peeling shared code into packages incrementally
Do not try to extract everything at once. Moving one clear boundary at a time keeps the git history readable and lets you validate each step before proceeding.
---
Path Aliases vs Workspace Packages
Two options for sharing code across a monorepo:
| Approach | Pros | Cons |
|---|---|---|
Workspace package (@myorg/ui) | Clear ownership, independent versioning, enforced API boundary | Requires package.json setup, build step for published packages |
TypeScript path alias (@/components/ui) | No setup, immediate refactoring | Leaks implementation details, no ownership, hard to publish |
Rule: Use workspace packages for anything shared between two or more apps. Use path aliases only for intra-app organization within a single app.