When this skill is activated, always start your first response with the 🧢 emoji.
Monorepo Management
A monorepo is a single version-controlled repository that houses multiple packages or applications sharing common tooling, dependencies, and build infrastructure. Done well, a monorepo eliminates dependency drift between packages, enables atomic cross-package changes, and lets you run only the builds and tests affected by a given change. This skill covers workspace managers (pnpm, npm, yarn), task orchestrators (Turborepo, Nx), enterprise build systems (Bazel), internal package patterns, and shared tooling config.
When to use this skill
Trigger this skill when the user:
- Wants to set up a new monorepo or migrate from a multi-repo setup
- Asks how to configure Turborepo pipelines, caching, or remote caching
- Asks how to use Nx projects, affected commands, or the Nx task graph
- Needs to share TypeScript, ESLint, or Prettier configs across packages
- Asks about pnpm/npm/yarn workspace protocols and dependency hoisting
- Wants to implement internal packages with proper bundling and type exports
- Needs to choose between Turborepo, Nx, Bazel, or Lerna
- Asks about build caching, cache invalidation, or remote cache setup
Do NOT trigger this skill for:
- Single-package repository build tooling (Vite, webpack, esbuild) - use the frontend or backend skill
- Docker/container orchestration even when containers come from a monorepo
Key principles
- Single source of truth - Each config (TypeScript base, ESLint rules,
Prettier) lives in exactly one package and is extended everywhere else. Duplication is the root cause of config drift.
- Explicit dependencies - Every package declares its workspace
dependencies with workspace:*. Never rely on hoisting to make an undeclared dependency available at runtime.
- Cache everything - Every deterministic task should be cached. Define
precise inputs and outputs so the cache is never stale and never over-broad. Remote caching multiplies this benefit across CI and team.
- Affected-only builds - On CI, build and test only the packages that
changed (directly or transitively). Running the full build on every PR does not scale past ~20 packages.
- Consistent tooling - Use the same package manager, Node version, and
task runner across all packages. Mixed tooling creates invisible resolution differences and breaks cache hits.
Core concepts
Workspace protocols
| Protocol | Package manager | Meaning | |----------|----------------|---------| | workspace: | pnpm | Any version from workspace, keep in lockfile | | workspace:^ | pnpm | Resolve range but pin a semver range | | * | yarn berry | Any version, resolved from workspace | | file:../pkg | npm | Path reference (no lockfile version management) |
Task graph
Turborepo and Nx model tasks as a DAG. A build task with dependsOn: ["^build"] means all dependency packages must complete their build before the current package starts. This replaces manual ordering scripts.
Remote caching
Remote caches (Vercel, Nx Cloud, S3/GCS) store task outputs keyed by a hash of inputs. Any machine with the same inputs gets a cache hit and downloads outputs instead of recomputing. This can reduce CI time by 80-90%.
Affected analysis
Given a diff from a base branch, affected analysis walks the dependency graph in reverse to find every package that transitively depends on a changed package. Turborepo: --filter=...[HEAD^1]. Nx: nx affected -t build.
Dependency topology
Packages form a partial order: leaf packages (utils, tokens) have no internal deps; feature packages depend on leaves; apps depend on features. Circular dependencies break the DAG and must be detected early.
Common tasks
1. Set up pnpm workspaces
pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
Root package.json:
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@9.4.0",
"engines": { "node": ">=20.0.0", "pnpm": ">=9.0.0" },
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": { "turbo": "^2.0.0" }
}
Referencing an internal package:
{ "dependencies": { "@myorg/tokens": "workspace:*" } }
2. Configure Turborepo
turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"typecheck": { "dependsOn": ["^build"], "inputs": ["src/**", "tsconfig.json"] },
"lint": { "inputs": ["src/**", "eslint.config.js"] },
"test": { "dependsOn": ["^build"], "inputs": ["src/**", "tests/**"], "outputs": ["coverage/**"] },
"dev": { "cache": false, "persistent": true }
}
}
Environment variable inputs (invalidate cache on env change):
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"],
"outputs": ["dist/**"]
}
}
}
Remote caching (Vercel) + affected CI runs:
npx turbo login && npx turbo link # set up once
turbo run build --filter=...[origin/main] # CI affected builds
3. Configure Nx
nx.json:
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"defaultBase": "main",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/*.spec.*"],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
},
"targetDefaults": {
"build": { "dependsOn": ["^build"], "inputs": ["production", "^production"], "cache": true },
"test": { "inputs": ["default", "^production"], "cache": true },
"lint": { "inputs": ["default"], "cache": true }
},
"nxCloudAccessToken": "YOUR_NX_CLOUD_TOKEN"
}
Affected commands:
nx show projects --affected --base=main # show affected projects
nx affected -t build # build only affected
nx affected -t test --parallel=4 # test in parallel
nx graph # visualize dependency graph
4. Share TypeScript configs across packages
tooling/tsconfig/base.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Individual package tsconfig.json:
{
"extends": "@myorg/tsconfig/base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
5. Set up shared ESLint/Prettier configs
tooling/eslint-config/index.js (flat config, ESLint 9+):
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
export const base = [
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
prettierConfig,
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/consistent-type-imports": "error",
},
},
];
tooling/prettier-config/index.js:
/** @type {import("prettier").Config} */
export default { semi: true, singleQuote: false, trailingComma: "all", printWidth: 100 };
6. Implement internal packages (tsup)
packages/utils/package.json:
{
"name": "@myorg/utils",
"version": "0.0.0