Monorepo (pnpm + Turborepo)

Pathrule2 Rules • 2 Memories • 1 Skill

A pattern bundle for JavaScript and TypeScript monorepos built on pnpm workspaces and Turborepo. It codifies the rules that keep task pipelines deterministic and cacheable, package boundaries explicit, and dependency versions unified. Use it so AI agents and humans both extend the repo without breaking the cache or leaking cross-package imports.

Suggested path map

Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.

/ workspace root
monorepo-pnpm-turborepo-review
turbo.json/
Every task declares outputs, inputs, and env
packages/
Import packages only through their public entry
config/
Share base configs as workspace packages
pnpm-workspace.yaml/
Pin shared dependency versions with pnpm catalogs

Rules

2
Every task declares outputs, inputs, and env/turbo.jsonA task that omits outputs, inputs, or env hashing will cache wrong results or never hit cache.
1Turborepo only caches and replays a task correctly when its hash is complete. Configure each task in `turbo.json` so the hash reflects exactly what the task reads and writes.
2 
3- Set `outputs` to every artifact the task produces (for example `[".next/**", "!.next/cache/**"]` or `["dist/**"]`); a missing `outputs` means nothing is restored from cache.
4- Add build-time `env` and `globalEnv` entries for any variable that changes output, and run with `envMode: "strict"` so undeclared vars cannot silently leak into a build.
5- Use `dependsOn: ["^build"]` to build upstream packages first, and mark `dev`/`watch` tasks `persistent: true` with `cache: false`.
6- Narrow `inputs` only when you understand the default; the safe default already hashes all tracked files in the package.
Import packages only through their public entry/packagesReaching into another package's src or dist by relative or deep path breaks boundaries and caching.
1Internal packages are consumed by their name and `exports` map, never by reaching across folders. This keeps Turborepo Boundaries valid and the dependency graph honest.
2 
3- Import a workspace package as `@repo/ui` (resolved via its `package.json` `exports`), never as `../../packages/ui/src/...`.
4- Declare every workspace package you use in that package's `package.json` with `"@repo/ui": "workspace:*"`; an undeclared import is an implicit dependency Turborepo cannot track.
5- Keep each package's public surface in its `exports` field and avoid deep subpath imports unless they are explicitly exported.
6- Run `turbo boundaries` in CI to catch cross-package imports and undeclared dependencies before merge.

Memories

2
Pin shared dependency versions with pnpm catalogs/pnpm-workspace.yamlDefine one version per shared dependency in pnpm-workspace.yaml and reference it with catalog:.
1This repo uses pnpm workspaces with catalogs as the single source of truth for shared dependency versions, so every package stays on the same React, TypeScript, and tooling versions.
2 
3- `pnpm-workspace.yaml` lists workspace globs under `packages:` (for example `apps/*` and `packages/*`) plus a `catalog:` block and optional named `catalogs:` for version sets.
4- Packages reference shared deps as `"react": "catalog:"` (default catalog) or `"catalog:react19"` for a named one, instead of hard-coding a range.
5- Bump a version in one place in the catalog and run `pnpm install`; pnpm rewrites the resolved lockfile entries across all packages.
6- The content-addressable store hard-links shared deps, so a single committed `pnpm-lock.yaml` keeps installs fast and deterministic.
Share base configs as workspace packages/packages/configTypeScript, ESLint, and Tailwind base configs live in packages and are extended per package.
1Tooling configuration is published as internal config packages (for example `@repo/typescript-config`, `@repo/eslint-config`) rather than duplicated or pushed to the repo root. Each app and package extends the base it needs.
2 
3- There is intentionally no root `tsconfig.json` for source; each package has its own `tsconfig.json` that does `"extends": "@repo/typescript-config/base.json"`.
4- ESLint flat config and Tailwind/PostCSS presets are exported from config packages and imported, so a rule change ships once.
5- List the config package as a `devDependency` with `workspace:*` so Turborepo treats a config change as an input and busts the cache.
6- Keep the root `package.json` for workspace scripts and `turbo` only; per-package concerns stay in their own package.

Skills

1
monorepo-pnpm-turborepo-review/rootChecklist to review a pnpm + Turborepo monorepo change before merge.
1---
2name: monorepo-pnpm-turborepo-review
3description: Review checklist for changes in a pnpm workspaces plus Turborepo monorepo, covering task hashing, caching, package boundaries, catalog-pinned versions, and shared configs. Use before merging any change that touches turbo.json, pnpm-workspace.yaml, package.json files, or cross-package imports.
4---
5 
6# Monorepo (pnpm + Turborepo) review
7 
8- [ ] Every new or changed `turbo.json` task sets `outputs` covering all produced artifacts (and excludes cache dirs like `!.next/cache/**`).
9- [ ] Build-affecting env vars are declared in the task `env` or `globalEnv`, and `envMode` is `strict`.
10- [ ] `dependsOn` uses `^build` for upstream packages; `dev`/`watch` tasks are `persistent: true` and `cache: false`.
11- [ ] New cross-package usage imports by package name (`@repo/*` via `exports`), never by relative or deep `src`/`dist` paths.
12- [ ] Each used workspace package is declared with `workspace:*` in the consuming package's `package.json`.
13- [ ] `turbo boundaries` and the build pass with a clean dependency graph; no implicit deps.
14- [ ] Shared dependency versions use `catalog:` (or a named catalog) instead of hard-coded ranges.
15- [ ] `pnpm-lock.yaml` is committed and reflects the install; only one version of each shared dep is resolved.
16- [ ] TypeScript, ESLint, and Tailwind extend the shared `@repo/*-config` packages rather than duplicating config.
17- [ ] `packageManager` is pinned in the root `package.json` and matches the pnpm version used in CI.

Why this pattern

Monorepo task pipelines silently lose cache hits and packages drift across boundaries as the repo grows.

Built for Teams running a JavaScript or TypeScript monorepo on pnpm workspaces and Turborepo..

Keeps your assistant from:

  • Cache misses from unset outputs or env vars left out of a task hash
  • Cross-package imports that bypass the package's public entry and break boundaries
  • Duplicate dependency versions because packages pin their own ranges instead of using the catalog
License
Apache-2.0
Version
1.0.0
Updated
2026-06-09
View source