Monorepo tooling
Helps agents choose among Nx, Turborepo, and package-manager workspaces: declare the task graph, local and remote cache keys, and how CI splits affected vs full runs so lint / test / build stay incremental and reproducible.
A SKILL should state package boundaries and workspace: protocol dependencies; once you pick an internal versioning strategy (pinning, changesets, single version line), do not invent a third ad hoc in PRs—avoid phantom deps and implicit hoisting.
Task graphs must declare inputs, outputs, and cache keys; call out generated code, environment variables, and secret-exclusion rules so remote cache is not wrong. Align with CI matrix: sharded tests, affected subsets, and periodic full runs on main belong in the skill body.
One-page summary
- Fix workspace and publish model first, then pick orchestration: Nx leans on project graph and plugins; Turborepo leans on a light pipeline and
turbo.jsontask declarations. - Cache correctness beats hit rate: inputs/outputs, env allowlists, and generated paths must match between SKILL and repo config.
- PR branches run affected (or equivalent filters); main runs periodic fulls; releases and Docker multi-stage builds must explain how monorepo context is trimmed.
Local-to-CI task flow (skill-flow-block)
[ clone / fetch ]
│
▼
┌─────────────│ Workspace install: pnpm/npm/yarn + lockfile check
│ Resolve │──── Constraint: no implicit deps; internal pkgs use workspace
│ deps │
└─────────────│
│
▼
┌─────────────│ Nx graph / turbo run: declare task edges + cache scope
│ Task graph │──── Local: .cache / remote; record env exclusion table
└─────────────│
│
▼
┌─────────────│ PR: affected + shards; main: scheduled full + artifact promo
│ CI matrix │──── Remote cache: RW tokens per env; fork PRs read-only or no write
└─────────────┘
When agents change config, first verify task names match root scripts / projects, then touch cache keys—otherwise you get local green with CI false hits or the reverse.
Nx and Turborepo
Both support task caching and parallelism; differences are mostly default mental models and extension paths. Name the one your team actually uses in the SKILL with official task/cache field names to avoid mixed terminology.
Nx
- Project graph,
affected, and plugins (Vite, Cypress, etc.) in one stack. project.json/package.jsontargets;inputs/outputsat target level.- Good for multi-stack repos that need migration tooling and codegen guardrails.
Turborepo
taskspipeline inturbo.json; fits package-manager workspaces naturally.- Simple remote-cache protocol; often paired with pnpm
workspace:*. - Good for package-centric front/back monorepos with minimal boilerplate.
// Nx project.json — full target configuration example
// packages/payment-ui/project.json
{
"name": "payment-ui",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/payment-ui/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": { "outputPath": "dist/packages/payment-ui" },
"dependsOn": ["^build"] // all upstream packages must finish build first
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{workspaceRoot}/coverage/packages/payment-ui"],
"inputs": [
"default",
"^default",
{ "externalDependencies": ["vitest"] }
],
"options": { "coverage": true }
},
"lint": {
"executor": "@nx/eslint:lint",
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"outputs": ["{options.outputFile}"]
}
}
}
// Turborepo turbo.json — pipeline configuration example
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // ^ means wait for dependency packages' build first
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**", ".next/**"],
"env": ["NODE_ENV", "API_URL"] // these env vars participate in the cache key
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "**/*.test.ts", "vitest.config.ts"],
"outputs": ["coverage/**"],
"env": ["CI"]
},
"lint": {
"inputs": ["src/**", ".eslintrc*", "../../.eslintrc.json"],
"outputs": [] // no file output; caches exit code
},
"dev": {
"cache": false, // dev server is never cached
"persistent": true
}
},
"remoteCache": {
"signature": true // verify remote cache signatures
}
}
# pnpm workspace configuration
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
- "tools/*"
# Internal references use workspace protocol (prevents phantom deps)
# packages/checkout/package.json
# {
# "dependencies": {
# "@acme/payment-ui": "workspace:*",
# "@acme/shared-utils": "workspace:^"
# }
# }
# Run only affected packages (affected)
# Nx: run tests only for packages affected by current changes
npx nx affected --target=test --base=origin/main --head=HEAD
# Turborepo: filter affected packages with --filter
# (requires turbo run ... --filter=[HEAD^1])
turbo run build test --filter="...[origin/main]"
# Remote cache setup
# Nx Cloud: configure in nx.json (after running npx nx connect)
# Turborepo: set TURBO_TOKEN and TURBO_TEAM environment variables
# export TURBO_TOKEN="your-token"
# export TURBO_TEAM="your-team"
# turbo run build # automatically uses remote cache
Workspace and package boundaries
- Filtering: run subset commands by path or project graph (
nx affected,turbo run --filter, etc.). - Constraints: eslint/tsconfig inheritance and root override policy; shared config packages versioned or referenced via workspace—document which.
- Publishing: artifact promotion and trimming monorepo context in Docker multi-stage builds (copy only needed packages and lockfiles).
// ESLint package boundary rules: enforce cross-package import restrictions (@nx/enforce-module-boundaries)
// .eslintrc.json (root)
{
"plugins": ["@nx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:checkout",
"onlyDependOnLibsWithTags": ["scope:checkout", "scope:shared"]
},
{
"sourceTag": "scope:payment",
"onlyDependOnLibsWithTags": ["scope:payment", "scope:shared"]
},
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:lib", "type:feature"]
},
{
"sourceTag": "type:lib",
"onlyDependOnLibsWithTags": ["type:lib"]
// lib layer cannot import from feature or app layers
}
]
}
]
}
}
// In packages/payment-ui/project.json add tags:
// "tags": ["scope:payment", "type:lib"]
Task graph and cache inputs
Declare inputs, outputs, and cache keys; list generated code, env vars, and secret exclusions so bad cache hits do not slip through. Treat paths that change outputs but are missing from inputs as bugs—not a permanent --force workaround.
- inputs: source globs, config files, dependency lockfile, shared toolchain version files.
- outputs: dist, type declarations, coverage dirs, etc.; align with
.gitignoreso dirty cache is not uploaded. - env: only allowlisted vars hash; CI secrets and feature-flag default changes must be explicitly included or excluded and documented.
CI: affected and matrix
Align with CI matrix: document sharded tests, affected subsets, and main-branch full runs. Fork and dependabot-style PRs need their own remote-cache write policy (often no write or isolated bucket).
- PR: default affected + required gates; large changes may opt into full (label or manual workflow).
- main: nightly or post-merge full regression by size; release pipelines stay consistent with read-only cache where needed.
- Sharding: when splitting by project or test file, declare random seed and retry policy to avoid order dependence across shards.
# .github/workflows/ci.yml — Nx affected + remote cache
name: CI
on:
push:
branches: [main]
pull_request:
env:
NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_AUTH_TOKEN }}
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # affected needs full git history
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install --frozen-lockfile
# PR: run only lint + test + build for affected packages
- name: Affected (PR)
if: github.event_name == 'pull_request'
run: |
npx nx affected \
--target=lint,test,build \
--base=origin/${{ github.base_ref }} \
--head=HEAD \
--parallel=3
# main: full run (ensures overall health after each merge)
- name: Full run (main)
if: github.ref == 'refs/heads/main'
run: |
npx nx run-many \
--target=lint,test,build \
--all \
--parallel=3
# Fork PRs: block remote cache writes (read-only mode)
# Nx Cloud detects forks automatically and downgrades to read-only;
# for Turborepo, do this manually:
# if: github.event.pull_request.head.repo.fork == false
# env: { TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} }
SKILL front-matter example
---
name: monorepo-tooling
description: Review or generate Nx/Turborepo task graph, cache configuration, and CI affected strategy
---
# Tool selection
Nx: multi-stack / plugin ecosystem / codegen guardrails → project.json targets
Turborepo: package-centric / lightweight pipeline → turbo.json tasks
pnpm workspace:* protocol prevents phantom deps; internal packages not published to npm
# Pipeline configuration
dependsOn: ["^build"] — upstream packages build first (^ means topological dependency)
inputs: source globs + config files + lockfile + shared toolchain
outputs: dist/** / coverage/** aligned with .gitignore
env: only allowlisted vars hash; CI secrets explicitly excluded
# Remote cache
Nx Cloud: nx.json config + NX_CLOUD_AUTH_TOKEN secret
Turborepo: TURBO_TOKEN + TURBO_TEAM environment variables
Fork PRs: remote cache writes disabled (read-only or isolated bucket)
# Package boundaries
@nx/enforce-module-boundaries: sourceTag → onlyDependOnLibsWithTags
tags: scope:{domain} + type:{app|lib|feature}
# CI strategy
PR: nx affected --base=origin/main --head=HEAD (affected packages only)
main: nx run-many --all (full run for safety)
Sharding: --parallel=3; fixed random seed; avoid cross-shard order dependence
Task inputs draft lab
Generate pasteable review text or SKILL appendix from the selected tool (not executable config); still verify against official schema and real repo paths before merge.
Lab output is for communication only; real turbo.json / project.json must stay consistent with other tasks—dependsOn and persistent output conventions in the repo.