Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Workflow · pnpm 11 · Turborepo 2.10 · TypeScript 6 · Node 24

Monorepo

Shared packages, task caching and clean boundaries — a monorepo that scales.

monorepoturborepopnpmworkspacestooling

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You maintain a TypeScript monorepo built on pnpm workspaces and Turborepo. Good means: one lockfile, no phantom dependencies, packages imported by name through explicit exports, tasks cached deterministically by dependsOn + outputs, releases driven by changesets, and every change touching only the affected packages.

Stack

  • Node 24 LTS (Active LTS). Pin with "engines": { "node": ">=24" } and an .nvmrc/.node-version of 24. Node 22 is Maintenance-only; do not target it for new apps.
  • pnpm 11.9 as the sole package manager. Pin exactly: "packageManager": "pnpm@11.9.0" in the root package.json (Corepack reads this). pnpm 11 ships as pure ESM and enables supply-chain defaults.
  • Turborepo 2.10 (turbo@2.10.x). Config key is tasks (renamed from pipeline in 2.0). Use turbo boundaries for import-boundary checks and turbo prune for Docker.
  • TypeScript 6.0 (last JS-based compiler). Use "type": "module" everywhere. For very large repos you may add the TS 7 native preview (@typescript/native-preview, tsgo) for fast typecheck, but keep tsc 6.0 as the source of truth until 7.0 is GA.
  • ESLint 10.6 flat config only (eslint.config.ts). .eslintrc* was removed in v10 — do not create one. Lint TypeScript with typescript-eslint 8.x (tseslint.config(...)).
  • Vitest 4.1 for tests, using the root test.projects array (the workspace field is deprecated since 3.2). Requires Vite >= 6.
  • Changesets @changesets/cli 2.31 for versioning and publishing.
  • Formatting: Prettier 3 (or Biome 2 if the repo standardizes on it) — pick one, run it through Turbo, never both.

Project conventions

  • Root holds only orchestration: package.json (with "private": true), pnpm-workspace.yaml, turbo.json, tsconfig.json (references only), eslint.config.ts, .changeset/. No application source at the root.
  • Two workspace roots: apps/* (deployable units — Next.js, API, worker) and packages/* (shared libraries and config). Optionally tooling/* for build/lint/tsconfig config packages.
  • Every package name is scoped to an internal namespace, e.g. @repo/ui, @repo/eslint-config, @acme/api. Choose one scope and use it for all internal packages so --filter=@repo/* works.
  • One responsibility per package. If a package has two unrelated export surfaces, split it.
  • Folder = kebab-case, package name = scope + kebab-case, matching the folder. TypeScript files kebab-case; React components PascalCase file names are fine if that is the app's convention — be consistent per app.
  • Every package sets "type": "module", an "exports" map, and its own tsconfig.json extending a shared base. Apps additionally set "private": true.
  • Imports: absolute by package name (import { Button } from "@repo/ui"), or path aliases within a package (~/); never deep-relative across packages (../../packages/ui/src/button).

Structure

apps/
  web/            # Next.js app, private
  api/            # service, private
packages/
  ui/             # @repo/ui — shared React components
  core/           # @repo/core — framework-agnostic domain logic
tooling/
  tsconfig/       # @repo/tsconfig — base tsconfig files
  eslint-config/  # @repo/eslint-config — flat config presets
  • Apps depend on packages; packages never depend on apps. Enforce with turbo boundaries and a dependency-cycle check.
  • Prefer internal (unbundled) packages: a package's exports point at .ts source and consumers compile it as part of their own build. This is the fastest default for internal-only code and keeps HMR working. Only add a build step (compiled package, exportsdist/) when the package is published to npm or consumed by a runtime that cannot transpile TS.
  • Keep leaf packages small and single-purpose so Turbo can cache and parallelize them independently.

pnpm workspaces

  • Declare members and the catalog in pnpm-workspace.yaml (in pnpm 11 the packages field lives here, not in root package.json):
packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"
catalog:
  react: ^19.2.0
  typescript: ^6.0.0
  zod: ^4.0.0
  • Reference workspace packages with workspace:* in dependencies: "@repo/ui": "workspace:*". pnpm rewrites this to a real version on publish. Never hand-write a version range for an internal package.
  • Use the catalog: protocol for third-party versions shared across packages: "react": "catalog:". This guarantees a single version repo-wide and moves upgrades to one file, killing merge conflicts in package.json. Set catalogMode: strict in pnpm-workspace.yaml so pnpm add refuses out-of-catalog versions.
  • No phantom dependencies. Every import must correspond to a dependency declared in that package's own package.json. pnpm's non-flat node_modules will fail these at runtime — treat that as correct, and add the missing dep to the right package (not the root).
  • One lockfile at the root: pnpm-lock.yaml, committed. CI installs with pnpm install --frozen-lockfile. Never delete or regenerate the lockfile to "fix" an install.
  • Run pnpm dedupe after dependency changes; commit the result. Use pnpm.overrides (root package.json) only to force a transitive security fix, with a comment linking the advisory.
  • Add dependencies to a specific package: pnpm --filter @repo/ui add clsx. Add a dev tool to the root only when it is genuinely repo-wide: pnpm add -Dw turbo.
  • Build scripts are blocked by default (pnpm 10+). Approve only the packages you trust via onlyBuiltDependencies in pnpm-workspace.yaml; audit each entry.

Shared config as packages

  • @repo/tsconfig exports base files (base.json, react-library.json, nextjs.json). Each package's tsconfig.json does "extends": "@repo/tsconfig/base.json" and sets only compilerOptions.outDir/rootDir and include. Base sets "strict": true, "moduleResolution": "bundler" (or "nodenext" for pure Node services), "verbatimModuleSyntax": true, "isolatedModules": true, and "isolatedDeclarations": true when packages emit .d.ts (enables parallel declaration emit).
  • @repo/eslint-config exports flat-config arrays (base, react, next). Consumers spread them: export default tseslint.config(...base, ...react). Include eslint-plugin-import-x or the turbo plugin to flag undeclared/phantom deps.
  • @repo/ui and other shared runtime packages are consumed by name only; their exports map defines the public surface.
  • These config packages are devDependencies of consumers and use workspace:*. They are private (not published).

Turbo pipeline

  • Define tasks in turbo.json under tasks. Every task that produces files must declare outputs or it will not be cached:
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NODE_ENV"]
    },
    "lint": { "dependsOn": ["^build"] },
    "typecheck": { "dependsOn": ["^build"], "outputs": [] },
    "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
    "dev": { "cache": false, "persistent": true }
  }
}
  • dependsOn: ["^build"] means "build all internal dependencies first" — the ^ is topological. Use a bare task name ("build") for same-package ordering.
  • Mark long-running tasks (dev, start) "persistent": true and "cache": false. Turbo refuses to let another task dependsOn a persistent one unless you use with.
  • Run only affected packages. Locally: turbo run test --filter=@repo/ui. In CI on a PR: turbo run build lint typecheck test --affected (compares against the merge base) or --filter=...[origin/main]. Never run the whole graph when a scope suffices.
  • Make caching honest: list every env var a task reads in env (task-level) or globalEnv (affects all tasks). Unlisted env vars are not part of the hash, so a changed value silently reuses a stale cache. Turbo 2.x defaults to strict env mode — tasks only see declared env vars; add runtime-only vars to passThroughEnv.
  • Enable Remote Cache (Vercel or self-hosted) so CI and teammates share artifacts. Add .turbo to .gitignore.
  • For Docker, use turbo prune --docker @acme/api to emit a minimal pruned workspace + lockfile, then install and build in the image. Never COPY the whole monorepo.

Boundaries

  • Import across packages only by package name, resolved through the target's exports map. Deep relative imports (../../packages/...) and importing unexported internals are forbidden — they break caching, pruning, and refactors.
  • Control each package's public API with exports; do not add a "." wildcard that leaks the whole src:
{
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx"
  }
}
  • Enforce with turbo boundaries in CI (flags imports of undeclared packages and reaching into another package's non-exported files).
  • No circular dependencies between packages. Add a cycle check (turbo boundaries or madge --circular --extensions ts,tsx .) to CI. Break cycles by extracting the shared piece into a lower-level package.
  • The dependency graph is a DAG pointing down: apps → feature packages → core/util packages → config packages. A util package importing an app or feature package is a design error.

Versioning and release with changesets

  • Every PR that changes a publishable package must include a changeset: pnpm changeset. It records the bump type (patch/minor/major) per package and a changelog line. CI fails PRs missing one (changeset status --since=origin/main).
  • Configure .changeset/config.json: "access": "public" for public packages, "updateInternalDependencies": "patch", and "linked"/"fixed" groups only if packages must move in lockstep.
  • Release flow: changeset version bumps versions, rewrites workspace:*/catalog: to concrete ranges in the published output, and writes CHANGELOG.md; then pnpm -r publish (or changeset publish). Automate with the changesets/action GitHub Action, which opens a "Version Packages" PR and publishes on merge.
  • Publish with npm provenance (--provenance, or the action's provenance option) so consumers can verify the build. Never publish from a laptop for production packages — publish from CI with an npm automation token.
  • Private apps ("private": true) are ignored by publish; still add changesets for them if you want changelog history, or set them in the changesets config ignore list.

Environments and secrets

  • Never put a root .env that leaks into every workspace. Each app owns its own .env/.env.local next to its code; packages read config passed in, not process.env directly.
  • Declare which env vars each task depends on in turbo.json (env/globalEnv), so cache keys reflect config. Runtime-only secrets that must not bust the cache go in passThroughEnv.
  • Keep secrets out of git and out of the build hash. Load them at runtime from the platform (Vercel/Doppler/1Password/SSM), not baked into cached dist.
  • Validate env at the app boundary with a schema (zod + @t3-oss/env-core) so a missing var fails fast at boot, not deep in a request.
  • .env* files are in .gitignore; commit a .env.example per app listing required keys with dummy values.

Testing

  • Vitest 4 with a single root vitest.config.ts using test.projects (not the deprecated workspace file). Point projects at packages/* and apps/* globs; share common settings via an imported vitest.shared.ts (project configs cannot extends the root config).
  • Coverage, reporters, and snapshot-path resolution are root-only in project mode — configure coverage (v8 provider) once at the root, not per project.
  • Run tests through Turbo so results cache: turbo run test. A package whose inputs are unchanged replays cached results instead of re-running.
  • Test the package's public API (its exports), not private internals. Colocate tests as *.test.ts beside source.
  • Use browser mode (stable in Vitest 4) or a jsdom environment for @repo/ui component tests; keep pure logic packages in the default node environment for speed.
  • CI runs turbo run typecheck lint test --affected — typecheck and lint are first-class gates, not afterthoughts.

Security

  • Keep pnpm 11's minimumReleaseAge on (default ~1 day): newly published versions are quarantined, blunting the window for compromised-package supply-chain attacks. Do not lower it to 0.
  • Keep build scripts disabled by default; only packages listed in onlyBuiltDependencies may run postinstall. Review each addition — arbitrary install scripts are the top supply-chain vector.
  • Commit pnpm-lock.yaml; CI uses --frozen-lockfile. A drifting or regenerated lockfile in a PR is a red flag.
  • Run pnpm audit (and an overrides pin) in CI; fail on high/critical. Pin transitive fixes via root pnpm.overrides with a linked advisory.
  • Publish with provenance and npm 2FA/automation tokens scoped per package; never a personal token with global publish rights.
  • No secrets in the repo, in turbo.json env values, or in cached outputs. Treat the remote cache as a shared artifact store — nothing sensitive may land in outputs.
  • Use a single catalog version per dependency so a security bump lands everywhere at once instead of leaving one package on a vulnerable pin.

Do

  • Add each import's package to that package's own package.json; fix "phantom dep" errors by declaring, not hoisting to root.
  • Use workspace:* for internal deps and catalog: for shared third-party versions.
  • Give every file-producing Turbo task an outputs array and every env-reading task an env list.
  • Scope commands: pnpm --filter <pkg> and turbo run <task> --affected/--filter.
  • Extend shared @repo/tsconfig and @repo/eslint-config; keep per-package config to overrides only.
  • Ship a changeset with every user-visible package change; let CI publish from the "Version Packages" PR.
  • Keep packages single-purpose and depend downward only; run turbo boundaries and a cycle check in CI.

Avoid

  • Do not run npm/yarn here, or commit their lockfiles. One tool, one pnpm-lock.yaml.
  • Do not use turbo.json's old pipeline key — it is tasks since Turbo 2.0.
  • Do not write .eslintrc* — ESLint 10 is flat-config only; use eslint.config.ts.
  • Do not deep-import across packages (../../packages/ui/src/...); import @repo/ui and expand its exports if you need more surface.
  • Do not hand-bump internal dependency versions or edit CHANGELOG.md by hand — changesets owns both.
  • Do not add a dependency to the root package.json to satisfy a leaf package's import; add it to the leaf.
  • Do not run -w/--recursive builds of the whole repo in CI when --affected covers the change.
  • Do not use pnpm install --no-frozen-lockfile in CI, disable minimumReleaseAge, or blanket-allow build scripts.
  • Do not omit outputs on a build task (breaks caching) or read an undeclared env var in a task (produces stale cache hits).
  • Do not create circular package dependencies or import an app from a library.

When you code

  • Make small, single-package diffs. If a change spans packages, do it as one coherent PR but keep each package's edit minimal and explain the boundary crossing.
  • After editing, run the affected gates locally: pnpm install (if deps changed) → turbo run typecheck lint test --filter=<changed>.... Fix, don't suppress.
  • When you add or move a dependency, update the owning package.json, run pnpm install + pnpm dedupe, and add a changeset if a publishable package's public behavior changed.
  • When you touch a task's inputs/outputs/env, update turbo.json in the same commit so caching stays correct.
  • Ask before: introducing a new top-level package or app, adding a compiled/bundled build step to a currently-unbundled package, changing the release/versioning strategy, upgrading a major version of pnpm/Turbo/TypeScript/ESLint, or editing root config that affects every workspace.
  • Prefer extending existing shared config packages over adding new tooling. Do not introduce a second formatter, test runner, or bundler alongside the established one.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on pnpm 11 · Turborepo 2.10 · TypeScript 6 · Node 24.

Back to top ↑