Full-stack pnpm + Turborepo monorepo skill, independent of the separate backend and frontend monorepo skills it draws on. Repository shape is split four ways at the top level: applications-backend, applications-frontend, packages-backend, packages-frontend — so a path states which runtime a file ends up in. Defaults: NestJS for backend services, React with IBM Carbon Design for browser applications; the project's stated architecture overrides both. Covers workspace globs, the Turborepo pipeline, filtered validation, blast-radius checks, Module Federation, turbo prune image builds, and Helm. Documents one sanctioned cross-boundary dependency: API contract types owned by the backend, imported type-only by the frontend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.6 KiB
Workspace and Tooling
Root-level configuration: workspace globs, the Turborepo pipeline and its caching, root scripts, Prettier, dependency overrides, and lockfile discipline.
Read this when adding a workspace, changing turbo.json or root scripts, debugging a task
that does not run or a cache that never hits, or touching pnpm.overrides.
1. Workspace globs
# pnpm-workspace.yaml
packages:
- "applications-backend/*"
- "applications-frontend/*"
- "packages-backend/*"
- "packages-frontend/*"
Exactly one level of nesting under each root. A directory that no glob matches is invisible to pnpm and to Turborepo — it will not install dependencies, will not build, and will not fail loudly. The symptom is usually "my new package isn't found by its consumer".
After adding a workspace, run pnpm install so the workspace links are created.
2. Root package.json
{
"packageManager": "pnpm@10.17.1",
"engines": { "node": ">=22", "pnpm": ">=10" },
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"check-types": "turbo run check-types",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,scss}\"",
"publish-packages": "turbo run build:ci lint && changeset version && changeset publish",
"prepush": "turbo build --filter=...[HEAD^]"
}
}
packageManagerpins the exact pnpm version; corepack reads it, so every contributor and CI runner resolves dependencies identically.- Root scripts always delegate to Turborepo, never to a single workspace — calling a workspace script directly bypasses the dependency graph and builds stale upstream code.
- Root dependencies are tooling only: turbo, prettier, husky, changesets, typescript.
3. Turborepo pipeline
| Task | Depends on | Cached outputs |
|---|---|---|
build |
^build, check-types, lint |
dist/**, .next/** |
build:ci |
^build:ci |
dist/**, dist-internal/** |
lint |
^lint |
— |
check-types |
^check-types |
— |
dev |
— | — (persistent, never cached) |
^task means the task must complete in every upstream workspace dependency first. That is
what makes --filter safe: filtering to one workspace still builds what it depends on.
Two consequences worth internalising:
builddoes not run untilcheck-typesandlintpass. A type or lint error is a build failure for every downstream workspace, not a warning in one.- Every output directory a workspace produces must be listed in
outputs. An unlisted directory is not restored from cache, so a "cache hit" silently yields a missing build artifact — which surfaces later as a container image built from nothing.
Task names in a workspace's package.json must match the task names in turbo.json. A
mismatch is not an error; Turborepo just skips that workspace, and the task appears to pass.
Cache inputs include $TURBO_DEFAULT$ and .env*.
4. Dependency rules
- Add a dependency to the smallest workspace that needs it.
- Inter-workspace dependencies use
workspace:*. - Run
pnpm installafter everypackage.jsonedit; commit thepnpm-lock.yamlchange together with the edit that caused it. A lockfile committed separately is unreviewable. - Never hand-edit
pnpm-lock.yaml.
pnpm.overrides — security pins
{ "pnpm": { "overrides": { "undici": "^6.21.1", "ws": "^8.17.1" } } }
Each entry exists because a transitive dependency shipped a vulnerability its direct parent had not yet picked up. Removing an entry silently reinstates the vulnerable version and nothing in the build complains — no error, no warning, a green pipeline.
Remove one only after confirming the direct dependency now resolves to a fixed version, and record that verification in the change.
5. Formatting
Root .prettierrc is the single authority for the whole repository. Do not add per-package
Prettier config: two configs mean every file formatted from one side produces churn diffs when
touched from the other.
pnpm format
6. Taskfile.yml
The root Taskfile.yml is a thin delegator — it only includes: module Taskfiles under
.scripts/. Never add inline commands to it. New automation becomes a .scripts/<module>/
with its own Taskfile, which is then included.
7. Diagnosing common failures
| Symptom | Likely cause |
|---|---|
| New workspace not found by its consumer | no matching glob, or pnpm install not re-run |
| Task appears to pass but does nothing | script name does not match a turbo.json task |
| Cache hit but missing build artifact | output directory not listed in outputs |
| Lockfile churn on every install | packageManager not pinned, or pnpm version drift |
| Build fails only in CI | --frozen-lockfile exposing an uncommitted lockfile change |
| Vulnerability reappears after an install | an overrides entry was removed |
pnpm list --depth 0
pnpm why <package>
turbo run build --dry-run # what would run, and in what order
turbo run build --filter=...[HEAD^]
8. Mistakes to avoid
- Adding a workspace outside the four roots, or without updating the globs.
- Calling a workspace script directly instead of going through Turborepo.
- Adding a root dependency for a need local to one workspace.
- Omitting an output directory from a task's
outputs. - A workspace script name that does not match its
turbo.jsontask. - Removing a
pnpm.overridesentry without a security review. - Per-package Prettier configuration.
- Inline commands in the root
Taskfile.yml. - Committing a lockfile change apart from the
package.jsonedit that caused it.