ADR-012: pnpm Catalogs for Shared Dependencies
Status
Accepted
Date
2026-08-14
Context
The repository contains multiple applications and packages that directly use the same external dependencies.
Examples include:
ESLint
TypeScript
React
React DOM
React type packages
Node.js type packages
clsx
globals
Previously, each consumer repeated a version specifier in its own
package.json.
For example:
{
"devDependencies": {
"eslint": "^9.39.1",
"typescript": "5.9.2"
}
}
This makes dependency ownership explicit, but the version policy is duplicated.
Over time, repeated ranges can drift between workspaces and make repository-wide upgrades harder to reason about.
Moving every shared tool only to the root package would remove version duplication but would also create implicit dependencies.
For example, a workspace script such as:
{
"scripts": {
"lint": "eslint ."
}
}
should not have to rely silently on an undeclared root dependency.
The repository therefore needs both:
explicit dependency ownership
+
centralized shared version policy
Decision
Use pnpm Catalogs for external dependencies that:
- are directly declared by multiple workspace consumers
- are intentionally expected to use one repository-wide version baseline
- do not represent an intentional multi-version compatibility requirement
Each consumer continues to declare the dependency explicitly.
The version itself is defined once in:
pnpm-workspace.yaml
Consumers reference the default catalog with:
catalog:
Current default catalog
The initial shared catalog contains:
@types/node
@types/react
@types/react-dom
clsx
eslint
globals
react
react-dom
typescript
The corresponding repository baselines are defined only in
pnpm-workspace.yaml.
For example:
catalog:
eslint: ^9.39.1
react: ^19.2.0
react-dom: ^19.2.0
typescript: 5.9.2
A consumer declares:
{
"devDependencies": {
"eslint": "catalog:",
"typescript": "catalog:"
}
}
Explicit ownership
Catalogs centralize versions but do not centralize dependency ownership.
If a workspace directly runs or imports a dependency, that workspace should normally continue to declare it.
For example:
root
→ declares eslint because the repository-level Lefthook job executes ESLint
web
→ declares eslint because its lint task executes ESLint
docs
→ declares eslint because its lint task executes ESLint
ui
→ declares eslint because its lint task executes ESLint
All of those declarations reference the same catalog entry.
This keeps package manifests understandable without duplicating the version range.
Internal workspace dependencies
pnpm Catalogs do not replace the workspace protocol.
Repository-owned packages continue to use:
workspace:*
Examples include:
@repo/ui
@repo/eslint-config
@repo/typescript-config
The distinction is:
workspace:*
→ resolve a package implemented inside this monorepo
catalog:
→ resolve the version policy for a shared external package
Catalog selection criteria
Not every dependency belongs in the shared catalog.
A package should normally enter the default catalog when the same external package is directly consumed by at least two repository packages and one shared version is intentional.
A package should normally remain directly versioned when:
- it has only one direct consumer
- it is implementation-specific to one workspace
- version independence is desirable
- the dependency is experimental or intentionally tracks a distribution tag
- centralization would add indirection without reducing meaningful drift
For that reason, repository-local Docusaurus, Storybook, Tailwind CSS, Prettier, Next.js, and similar single-consumer dependencies are not automatically moved into the shared catalog.
Catalogs are a version-consistency mechanism, not a requirement to centralize every version string in the repository.
Catalog mode
The repository uses:
catalogMode: prefer
When adding dependencies, pnpm prefers an existing compatible default-catalog version.
Dependencies that are not part of the catalog can still be added normally.
This provides useful automatic reuse without requiring every external dependency to be catalog-managed.
Strict catalog enforcement can be reconsidered separately when its workflow, dependency-update automation, and pnpm behavior have been validated for this repository.
Multiple versions
A shared default catalog does not prevent the repository from supporting multiple dependency versions in the future.
If a deliberate migration requires separate version lanes, pnpm named catalogs can be introduced.
For example:
catalog:
react: ^19.2.0
react-dom: ^19.2.0
catalogs:
react20:
react: ^20.0.0
react-dom: ^20.0.0
A selected workspace could then use:
{
"dependencies": {
"react": "catalog:react20",
"react-dom": "catalog:react20"
}
}
This makes intentional version divergence explicit rather than allowing accidental drift between package manifests.
Version-range policy
The catalog preserves the repository's intended range policy.
Examples:
eslint
→ compatible minor and patch upgrades within ESLint 9
react
→ compatible minor and patch upgrades within React 19
typescript
→ exact repository compiler baseline
React type packages
→ exact type baseline
Changing those policies must be done in the catalog rather than independently inside consumer manifests.
Dependency upgrades
For a catalog-managed dependency, the catalog entry is the version source of truth.
A normal repository-wide upgrade therefore changes:
pnpm-workspace.yaml
followed by:
pnpm install
pnpm verify
The consuming package manifests normally remain unchanged.
This reduces repetitive edits and makes a shared dependency upgrade easier to review.
Consequences
Positive
- shared dependency versions have one source of truth
- workspace dependency ownership remains explicit
- accidental version drift is reduced
- repository-wide upgrades require fewer manifest changes
- package manifests contain less repeated version policy
- root-level tooling can declare the same shared tools as workspace consumers
- intentional future multi-version migrations can use named catalogs
- internal packages continue to use the semantically correct workspace protocol
Negative
- dependency resolution now requires understanding the
catalog:protocol - the effective version is not visible directly inside every package manifest
- catalog membership must be maintained deliberately
- not every dependency can be upgraded by editing only its consumer manifest
- named catalogs add complexity if intentional multi-version support is later required
Alternatives considered
Install shared tools only at the workspace root
Rejected as the default policy.
This reduces repeated declarations but makes workspace scripts depend on tools that the workspace itself does not declare.
The repository prefers explicit consumer ownership.
Repeat the same semver range in every package
Rejected for intentionally shared dependencies.
This is simple initially but creates multiple version-policy sources of truth.
Use workspace:* for external dependencies
Rejected.
The workspace protocol identifies packages implemented inside the current workspace.
External registry packages such as ESLint and React are not workspace packages.
Put every external dependency in the catalog
Rejected.
Catalogs are used where they reduce real version drift.
Single-consumer and workspace-specific dependencies retain direct version specifiers unless a future architectural reason justifies centralizing them.
Enable strict catalog mode immediately
Deferred.
The repository currently prefers catalog reuse without making all dependency addition and update workflows dependent on strict enforcement behavior.
Strict mode can be evaluated later together with dependency-update automation.
Relationship to other ADRs
ADR-002 remains authoritative for the selection of pnpm as the repository package manager.
ADR-008 remains authoritative for the locked Node.js and pnpm toolchain.
ADR-011 remains authoritative for repository quality commands and task ownership.
This ADR defines shared external dependency version ownership and complements those decisions.
Follow-up work
- keep catalog membership synchronized with actual multi-workspace usage
- evaluate strict catalog enforcement when dependency-update automation is introduced
- use named catalogs only for deliberate multi-version migrations
- continue dependency and security remediation independently of catalog adoption