Unifying a pnpm Monorepo's Dependency Versions with Catalog
Using pnpm catalog to turn two workspace packages with divergent TypeScript and ESLint versions into a single source of truth. Covers a Remotion peer-dep fix and a minimumReleaseAge interaction.
Two package.json files. Two different TypeScript and ESLint versions. Two apps pinned independently, Renovate bumped them on separate schedules, and a transitive peer-dep conflict kept one of them on months-old releases. I moved them to pnpm catalog.
Version drift
The monorepo has two frontend workspaces: one is a Vite + React app, the other is a Remotion video project. Between them, TypeScript ranged from 5.7.3 to 6.0.3 and ESLint from 9.39.4 to 10.5.0.
Each package.json pinned its own versions independently with saveExact: true in pnpm-workspace.yaml. Renovate managed upgrades with rangeStrategy: pin. The config grouped devDependencies updates and major bumps, but the group ran per-package and per-update-type. The Vite app received its TypeScript 6 and ESLint 10 bumps. The Remotion app did not, because @remotion/eslint-config-flat transitively pinned typescript-eslint@8.21.0, whose peer dependencies rejected both TypeScript 6 and ESLint 10. The pnpm install failed under strictPeerDependencies: true, Renovate couldn’t regenerate the lockfile, and the PR sat red.
I wanted a single place to bump TypeScript, ESLint, React, and shared type definitions. pnpm catalog provides that. Each piece of the migration needed its own adjustment.
Catalog migration
pnpm catalog: one version, two packages
pnpm catalog adds a shared version map to pnpm-workspace.yaml. Workspace packages reference "catalog:" instead of literal versions. Bump the catalog, everything follows.
# pnpm-workspace.yaml
catalog:
typescript: "6.0.3"
eslint: "10.5.0"
react: "19.2.7"
react-dom: "19.2.7"
"@types/react": "19.2.17"
"@types/react-dom": "19.2.3"
Each workspace package swapped literal versions for the protocol:
// apps/vite-app/package.json — before
"dependencies": {
"react": "19.2.7",
"react-dom": "19.2.7"
},
"devDependencies": {
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"eslint": "10.5.0",
"typescript": "6.0.3"
}
// after
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
The Remotion workspace followed the same pattern. The catalog covers six packages across two package.json files. Renovate understands catalogs natively, so rangeStrategy: pin and minimumReleaseAge apply to catalog entries the same way they applied to literal pins.
The Remotion peer-dep blocker
The Remotion workspace’s eslint.config.mjs is one line:
import { config } from "@remotion/eslint-config-flat";
export default config;
@remotion/eslint-config-flat@4.0.477 depends on typescript-eslint@8.21.0 as a hard dependency. typescript-eslint@8.21.0 peers typescript <5.8.0 and eslint ^8.57.0 || ^9.0.0. TypeScript 6.0.3 and ESLint 10.5.0 violate both.
Bumping @remotion/eslint-config-flat does not help. 4.0.477 is the latest release. Controlling the transitive version requires a pnpm override:
# pnpm-workspace.yaml
overrides:
"typescript-eslint": "8.61.0"
typescript-eslint@8.61.0 widened its peers to typescript <6.1.0 and eslint ^8.57.0 || ^9.0.0 || ^10.0.0. That covers both target versions.
The override version itself needs to pass the supply chain gate. I first tried 8.61.1, the most recent release. pnpm rejected it with ERR_PNPM_NO_MATURE_MATCHING_VERSION: 8.61.1 was published 29 hours earlier, and minimumReleaseAge: 1440 (1 day) requires 24 hours. 8.61.0, published a few days earlier, resolved fine. The supply chain config worked as intended.
This took a moment to debug. The error message lists every @typescript-eslint/* sub-package independently, so the output is noisy. The underlying cause is simple: a transitive dep pins an old umbrella package, an override replaces it with a mature version whose peers accept the target TypeScript and ESLint.
TypeScript 6 removed implicit DOM types
The Remotion project type-checked on TypeScript 5.7.3. On 6.0.3, tsc --noEmit produced six errors:
error TS2304: Cannot find name 'fetch'.
error TS2304: Cannot find name 'FontFace'.
error TS2584: Cannot find name 'document'.
error TS2584: Cannot find name 'console'.
tsconfig.json had "lib": ["es2015"] and no DOM entries. TypeScript 5 was lenient; TypeScript 6 is not. Remotion renders in a headless browser context that uses fetch, FontFace, and the DOM. The fix is adding the DOM libraries:
- "lib": ["es2015"],
+ "lib": ["es2015", "DOM", "DOM.Iterable"],
Verification
The catalog covers six packages across two package.json files. A single version in pnpm-workspace.yaml is the source of truth. Bump once, Renovate keeps it current, both workspaces follow.
| Check | Result |
|---|---|
pnpm install (strict peer deps, min release age) | passed |
tsc --noEmit (both apps, TS 6) | passed |
eslint (both apps, ESLint 10) | passed |
vitest (Vite app) | 10/10 |
Caveats
I’d check which transitive dependencies of each workspace’s ESLint configs impose peer constraints on TypeScript and ESLint before bumping the catalog versions. The Remotion peer issue was discoverable by inspecting the transitive typescript-eslint version, but I found it through a failed pnpm install instead. A pnpm why typescript-eslint in the Remotion workspace would have shown the pinned version and its peer range before the catalog edit.
References
- pnpm catalog. Shared version groups for workspace packages
- pnpm overrides. Resolve conflicting transitive dependencies
- pnpm
minimumReleaseAge. The release-age policy that triggered the maturity rejection - typescript-eslint@8.61.0 release. The version whose peers allow TypeScript 6.0.x and ESLint 10
- Supply Chain Security with mise and pnpm. The config that caught the 8.61.1 age violation
- Dagger’s TypeScript SDK doesn’t understand pnpm catalog:. Companion TIL on the Dagger CI interaction
This post was written with AI assistance.