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.json task 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.json targets; inputs / outputs at target level.
  • Good for multi-stack repos that need migration tooling and codegen guardrails.

Turborepo

  • tasks pipeline in turbo.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 .gitignore so 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.

Orchestrator

              

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.

Back to skills More skills