TheSkillz

Monorepo Boundary Guard

Package boundaries, version drift and a task cache that actually hits

TheSkillz Team TheSkillz Team No reviews yet0 installsv1.0.0
Scan passed · 100/100Human reviewedOfficial · TheSkillz
☆ Star 0

Separates deployable apps from imported packages, keeps internal packages private and unbuilt where the bundler allows, catches version drift with syncpack and the pnpm catalog, and fixes the task-graph mistake behind most cache misses: declaring dependencies a task does not have, such as gating typecheck on upstream builds. Covers project references and why paths aliases break outside the type checker.

SKILL.md

Monorepo Boundary Guard

A monorepo is a bet that shared code is cheaper than shared nothing. The bet pays off only if the packages have real boundaries; without them you have a single application with extra package.json files, a build that touches everything on every change, and a cache that never hits.

Apps and packages are different things

  • apps/* — deployable. Never imported by anything. An app importing another app means one of them is a package.
  • packages/* — imported, never deployed. Each has one clear responsibility and a name that says it.
  • No packages/common, packages/shared or packages/utils. Same rule as inside a single repo, with higher stakes: everything depends on it, so every change to it invalidates every cache entry in the workspace.

Keep internal packages private and unbuilt where your bundler allows — "main": "./src/index.ts" with the app compiling the TypeScript directly. It removes a build step, a watch process and a whole class of "stale dist" bugs. Add a build step only when you publish externally or hit a real constraint.

Version drift is a real bug

Two packages on different versions of the same dependency ship both to the browser, or fail at runtime in ways the type checker cannot see — two copies of React, two of a validation library whose instanceof checks now fail.

  • Run syncpack list-mismatches in CI and fail on drift.
  • Use the pnpm catalog so a version is declared once and referenced as catalog: everywhere.
  • Internal dependencies use workspace:*, never a version number.
  • Peer dependencies for framework packages, so the app owns the single copy.

Make the task graph tell the truth

Most cache disappointment comes from one mistake: declaring dependencies the task does not have.

  • dependsOn: ["^build"] is correct for build. It is usually wrong for typecheck and lint, which can run against source and need no upstream artifacts. Declaring it serialises the graph and throws away most of your parallelism.
  • Declare inputs narrowly. A task listing the whole package directory is invalidated by a README edit. List the source globs and the config files that genuinely affect the output.
  • Declare outputs accurately, or the cache stores nothing and silently replays a no-op.
  • Mark tasks with side effects — deploys, migrations — as cache: false. A cached deploy is a very bad afternoon.

Verify rather than assume: run a task twice and confirm the second run reports a cache hit. If it does not, the inputs are wrong.

TypeScript across packages

  • A shared tsconfig.base.json extended by each package.
  • Project references if packages are built; each references its dependencies and gets incremental builds. Skip them entirely if packages are consumed as source — they add ceremony for nothing.
  • Use workspace resolution, not paths aliases. paths are a compiler fiction the bundler and Node do not share, so they work until something runs outside the type checker.
  • Never reach across a package boundary with a relative path. ../../packages/ui/src/Button bypasses the boundary, the cache and the public surface. Lint against ../.. crossing a package root.

Judgement calls

  • When is a package a package? When two apps use it, or when it has a genuinely different release cadence. Extracting a package for one consumer buys ceremony and no isolation.
  • Turborepo or Nx? Turborepo for task running and caching over an existing structure. Nx when you want generators, an enforced dependency graph and plugin-driven conventions.
  • How granular? Fewer, larger packages beat many tiny ones. Every boundary costs a build edge, a version to keep in step and a place for a cycle to hide.

Verification gate

  • syncpack list-mismatches is clean and runs in CI.
  • A task run twice reports a cache hit on the second run — paste the summary showing it.
  • typecheck is not needlessly gated on ^build; confirm by reading the resolved task config.
  • No app imports another app; no relative import crosses a package root.
  • A clean clone installs and builds from scratch with the documented command.
  • Paste the command output rather than describing it.

Reviews

Sign in to leave a review.

  • Be the first to review this skill.

More in devops