wandres.dev
PNPM CATALOGS · versiones compartidas

Definir catalogs: el default y los catalogs nombrados

Cómo declarar versiones una sola vez en pnpm-workspace.yaml: el catalogo default con el campo singular catalog, los catalogs nombrados con el campo plural catalogs, y cómo referenciarlos desde package.json con el protocolo catalog.

⏱ 14 min

Un catalog es un diccionario de versiones: un lugar donde escribes react: ^18.3.1 una vez y luego, en cada paquete, dices solo react: catalog: para heredar ese rango. pnpm ofrece dos formas de definirlos en pnpm-workspace.yaml —un catálogo default con el campo singular catalog, y catálogos con nombre bajo el campo plural catalogs— y esa dualidad no es un capricho: es lo que permite que un monorepo tenga una versión canónica y, a la vez, versiones alternativas nombradas para las migraciones a medias. Esta lección es la sintaxis exacta.

🎯 Al terminar esta lección sabrás
  • Definir el catálogo default con el campo singular catalog.
  • Definir catálogos nombrados con el campo plural catalogs.
  • Referenciarlos desde package.json con catalog: y catalog:nombre.
  • Saber en qué campos es válido el protocolo catalog:.

Dos campos: catalog y catalogs

Los catalogs viven en pnpm-workspace.yaml, el mismo archivo que declara los packages del workspace. Hay dos claves posibles, y conviene no confundirlas por lo parecido de sus nombres. El campo singular catalog define un único catálogo especial llamado default. El campo plural catalogs define un mapa de catálogos con el nombre que tú elijas. Puedes usar uno, otro, o ambos a la vez.

packages:
  - apps/*
  - packages/*

# campo singular: crea el catalogo llamado default
catalog:
  react: ^18.3.1
  react-dom: ^18.3.1
  redux: ^5.0.1

Cada entrada es un par nombre-de-paquete: rango. El valor es un rango de semver normal —caret, tilde, una versión exacta, lo que quieras—, exactamente lo que habrías escrito en un package.json. La diferencia es que ahora lo escribes una sola vez.

Ese pnpm-workspace.yaml es, además, el mismo archivo donde en pnpm moderno vive casi todo el gobierno del workspace: los packages, los overrides, los ajustes de instalación. El catálogo no es un archivo nuevo que aprender, sino una sección más del centro de mando que ya conoces. Todo lo que gobierna el monorepo —incluidas ahora las versiones canónicas de sus dependencias— converge en un único lugar versionado en git, que es exactamente donde quieres que viva la fuente de verdad.

El catálogo default y el atajo catalog:

Un paquete referencia una entrada del catálogo escribiendo catalog: como si fuera el rango. En el momento de instalar, pnpm sustituye ese marcador por el rango real que encuentre en el catálogo.

{
  "name": "@acme/web",
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:",
    "redux": "catalog:"
  }
}

El catálogo default tiene un privilegio sintáctico: puedes referenciarlo con catalog:default o con el atajo catalog: a secas. Piensa en catalog: como una abreviatura que se expande a catalog:default. Ambas formas son idénticas; la corta es la que verás casi siempre, porque la mayoría de los equipos tienen un solo catálogo canónico y no necesitan nombrarlo.

📝
Singular es default, plural son nombrados

El detalle que más confunde al principio es la cercanía de los nombres. El campo catalog (singular) crea un catálogo, el llamado default. El campo catalogs (plural) crea muchos, con los nombres que elijas. Una letra de diferencia, dos comportamientos distintos: si tu YAML no hace lo que esperabas, empieza por comprobar que no confundiste el singular con el plural.

💡
El catálogo no instala nada por sí solo

Definir react en el catálogo no añade React a ningún paquete. El catálogo es solo el diccionario de versiones; la dependencia sigue existiendo únicamente en los package.json que la declaran con catalog:. Un paquete que no menciona react no lo recibe por estar en el catálogo. Catálogo y grafo de dependencias son cosas distintas: uno dice qué versión, el otro dice quién la usa.

Catálogos nombrados: convivencia de versiones

A veces un monorepo necesita, de forma legítima, dos versiones de la misma dependencia a la vez: durante una migración gradual, la mitad de los paquetes ya usa la nueva y la otra mitad aún no puede. Para eso están los catálogos nombrados, bajo el campo plural catalogs.

# el default, para lo que ya convergio
catalog:
  react: ^18.3.1
  react-dom: ^18.3.1

# catalogos nombrados, para lo que aun convive en dos versiones
catalogs:
  react17:
    react: ^17.0.2
    react-dom: ^17.0.2
  react18:
    react: ^18.3.1
    react-dom: ^18.3.1

Cada catálogo nombrado se referencia con catalog: seguido de su nombre. Así, un paquete heredado apunta a la versión antigua y uno moderno a la nueva, ambos de forma explícita y auditada:

// packages/legacy-admin/package.json
{ "dependencies": { "react": "catalog:react17" } }

// apps/web/package.json
{ "dependencies": { "react": "catalog:react18" } }
flowchart TD
W[pnpm-workspace.yaml] --> DEF[catalog default]
W --> NAM[catalogs nombrados]
NAM --> R17[react17]
NAM --> R18[react18]
DEF --> P1[app-web hereda catalog]
R17 --> P2[legacy-admin usa catalog react17]
R18 --> P3[app-web usa catalog react18]
style W fill:#89b4fa,color:#11111b
style DEF fill:#a6e3a1,color:#11111b
style NAM fill:#cba6f7,color:#11111b

La clave de diseño es que los nombres dan intención. Ver catalog:react17 en un package.json comunica al lector que ese paquete pertenece a propósito a la cohorte que aún no ha migrado, no que alguien se despistó con la versión. La deriva accidental y la divergencia deliberada dejan de ser indistinguibles.

Este patrón —un default para lo consolidado, más nombrados para lo que todavía convive en dos versiones— es la forma canónica de encarar una migración grande por partes. Puedes tener la versión moderna como default mientras react17 sostiene el frente heredado, y mover paquetes de un catálogo al otro de uno en uno, con un diff mínimo por pull request: una sola línea que cambia catalog:react17 por catalog:react18. El catálogo no te obliga a migrar todo de golpe; te da el vocabulario para migrar de forma incremental, visible y auditada, que es la única manera realista de hacerlo en un repo con vida.

Dónde es válido el protocolo catalog:

El protocolo no vale en cualquier sitio; pnpm lo acepta en un conjunto concreto de campos. En package.json puedes usarlo en dependencies, devDependencies, peerDependencies y optionalDependencies. Y en el propio pnpm-workspace.yaml, dentro de overrides, para forzar una versión transitiva desde el mismo catálogo.

Archivo Campo Acepta catalog:
package.json dependencies
package.json devDependencies
package.json peerDependencies
package.json optionalDependencies
pnpm-workspace.yaml overrides

Que funcione en peerDependencies es especialmente elegante: una librería interna puede declarar react: catalog: como peer, garantizando que exige exactamente la misma versión que consume el resto del repo, sin cablear el número a mano en dos sitios que podrían divergir.

El caso de overrides cierra el círculo hacia las dependencias transitivas. Un override fuerza la versión de un paquete que tú no declaras directamente, pero que llega arrastrado por otros más abajo en el árbol; poder escribir ese override como catalog: significa que hasta la versión que impones río abajo bebe de la misma fuente de verdad que el resto del repo. Así la coherencia deja de detenerse en tus dependencias directas y alcanza, a través del catálogo, al árbol entero de lo que se instala.

Fuera de esos campos, catalog: no es válido, y conviene saberlo para no perder el tiempo: no lo usas en el campo version de tu propio paquete, ni en scripts, ni sustituye a otros mecanismos de resolución. El catálogo declara qué versión de terceros usas, no qué versión eres tú. Esa frontera es deliberada y mantiene el concepto afilado: un diccionario de versiones de dependencias, ni más ni menos.

💡
Un catálogo por workspace, no por paquete

Los catalogs son una propiedad del workspace, no de cada paquete: viven en el único pnpm-workspace.yaml de la raíz y son visibles para todos los paquetes a la vez. No hay catálogos locales por carpeta, y es lo correcto: la razón de ser del catálogo es ser el punto común. Tener uno por paquete reintroduciría exactamente la dispersión que el catálogo vino a eliminar.

Un catálogo es una tabla de constantes con nombre para tus versiones

Lo que acabas de aprender es, en esencia, aplicar a las versiones de dependencias la disciplina más vieja de la programación: no repetir un valor mágico, sino declararlo una vez con un nombre y referenciarlo. El campo catalog es tu bloque de constantes por defecto; el campo catalogs es el espacio de nombres para cuando una sola constante no basta. Y la decisión de permitir ambos a la vez —un default más varios nombrados— es lo que convierte al catálogo en una herramienta expresiva y no en un simple candado. Un candado te obligaría a tener una versión de todo, lo cual es imposible durante una migración real. La combinación default más nombrados te deja declarar la verdad tal como es: “la versión canónica de React es la 18, y estos tres paquetes concretos siguen, a propósito y con nombre, en la 17 hasta que terminen de migrar”. El catálogo no solo elimina la deriva; documenta la intención detrás de cada excepción, y convierte lo que antes era una divergencia invisible en una decisión visible y con dueño. Esa es la diferencia entre una convención frágil y una que el propio sistema hace legible.

⚔️ Declara tu primer catálogo
  1. En un pnpm-workspace.yaml, crea un catalog default con tus tres dependencias más compartidas y sus rangos actuales.
  2. Convierte un package.json para que referencie esas tres con catalog: y ejecuta pnpm install; confirma que resuelven al rango del catálogo.
  3. Añade un catálogo nombrado (por ejemplo react17) con una versión alternativa y apúntalo desde un paquete distinto con catalog:react17.
  4. Declara en una librería interna un peerDependencies con catalog: y razona qué garantiza frente a cablear el número a mano.
  5. Verifica el efecto de que el catálogo no instala nada: quita react de un package.json y comprueba que el paquete deja de recibirlo aunque siga en el catálogo.