package.json ownership

rule

each package declares only the dependencies, lifecycle tasks, and lint/format configuration it owns. the root coordinates the Turbo workspace; it is not a convenience-script dumping ground.

why

every field in a workspace has one runtime owner, and only that owner can answer its questions: is this update breaking for me, does removing this package break me, which lifecycle names do i own. when ownership blurs into the root, those questions become coordination problems, update tools act on dependencies they do not own, and the graph turns opaque. invariant: each package is independently installable, buildable, and verifiable at its own boundary.

decide ownership before adding a field

package.json is the smallest executable boundary. before adding a dependency, script, export, or override, name the package that owns the runtime behavior — the field belongs there. the root may coordinate the graph but must not impersonate an application or library.

what blurred ownership costs:

  • root holds a frontend dependency → backend engineers install a build-time Next.js they never use.
  • frontend version updated at root → nobody knows if it is a frontend breaking change or a root choice; a deployer cannot decide to deploy or wait.
  • two packages need the same library at different versions → the root owns neither consumer, both versions stay incompatible in the lockfile.
  • Renovate/Dependabot scans the root → finds dependencies it does not own, giving premature or missed updates.
changeownerreason
a browser-only runtime libraryconsuming applicationpart of that application's deployed runtime.
a generated client used by several applicationsgenerated contract packageone generated surface prevents consumers from inventing different clients.
formatter or ESLint configurationowning package or its local config/ folderformatting/linting must be independently runnable at the package boundary.
a deploy commanddeployable applicationdeployment needs that application's environment and output contract.

required shape

  • private: true unless publication is an explicit product decision.
  • type: module for authored TypeScript ESM packages.
  • expose applicable canonical scripts: dev, generate, build, format, lint, typecheck, and deploy only where the package owns deployment.
  • every package implements its relevant lifecycle names itself; never delegate format or lint to a root script.
  • keep runtime dependencies separate from development tooling.

root contract

the root package.json is a thin Turbo control plane. it may declare: the package manager, workspace metadata, Turbo itself, and the single pipeline entrypoint orchestrating package tasks. it must not acquire package dependencies, shared ESLint/formatter configurations, or aliases such as format:all, lint:all, and package-specific build scripts.

canonical pipeline — resolved install, then the same package lifecycle names through Turbo:

pnpm install && turbo run generate format build lint typecheck

script contract

each script name is a stable lifecycle phase, not a convenience alias:

  • generate creates declared derived artifacts
  • build produces deployable or consumable output
  • typecheck checks the authored and generated type surface
  • dev starts a persistent local process

if a package cannot own the command's inputs, outputs, and failure boundary, it should not expose that script.

do not

  • put a dependency in the root because one app needs it.
  • add root aliases that obscure which package owns an operation.
  • place shared ESLint or formatter behavior in the root as a default for packages that lack their own configuration.
  • hand-edit generated package metadata.

good and bad

bad

{
"scripts": { "deploy:frontend": "pnpm --filter frontend deploy" },
"dependencies": { "next": "latest" }
}

the root owns a frontend runtime dependency and hides the deploy boundary behind an alias: every package install pulls a runtime it never uses, update tools cannot attribute dependencies to a consumer, and no one can tell a frontend breaking change from a root choice. ownership questions become coordination problems — the slowest, most fragile way to resolve them.

good

{
"scripts": { "build": "turbo run build" },
"devDependencies": { "turbo": "^2.5.8" }
}

the root coordinates packages; the frontend package owns its own Next dependency and deploy script. the graph answers questions locally: a dependency update is evaluated by its only consumer, a deploy runs from the package that owns its environment, and the same lifecycle name means the same thing in every owner. each package stays independently installable, buildable, and verifiable — adding or extracting one does not reopen decisions about the others.

verification

  • dependency change: inspect the lockfile edge; would removing the package break this package at runtime or only its tooling?
  • script change: run it through the owning package and through the root graph; the same lifecycle name must not mean conflicting things in two owners.

search pages

go to any page