Ir al contenido
angel.library

pnpm es el gestor con el soporte de workspaces más maduro de los tres: fue el primero en implementar el protocolo workspace: de forma estricta, y es el único con catalogs — versiones compartidas declaradas una sola vez. Si estás empezando un monorepo hoy sin una razón para elegir otro gestor, pnpm es la elección por defecto más común.

mi-monorepo/
├── apps/
│ └── web/
│ └── package.json
├── packages/
│ └── ui/
│ └── package.json
├── package.json
├── pnpm-workspace.yaml
└── pnpm-lock.yaml

apps/ para lo desplegable (sitios, servicios); packages/ para lo compartido (librerías internas, configuración). Es una convención, no una regla de pnpm — los nombres de carpeta son libres, lo que importa es qué declares en pnpm-workspace.yaml.

Este archivo, en la raíz, es lo que convierte una carpeta común en un workspace de pnpm:

pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"

Cada entrada es un patrón glob. apps/* incluye toda carpeta directa dentro de apps/ que tenga un package.json; una carpeta sin package.json se ignora sin error.

package.json
{
"name": "mi-monorepo",
"private": true,
"packageManager": "pnpm@11.19.0"
}

"private": true es obligatorio en la raíz: evita publicar por accidente el paquete raíz (que no es un paquete real, es el contenedor del monorepo) a npm. packageManager fija la versión exacta de pnpm que el proyecto espera — si alguien con otra versión corre un comando, herramientas como Corepack pueden avisar o forzar la versión declarada.

4. Crear un paquete y declarar una dependencia interna

Sección titulada «4. Crear un paquete y declarar una dependencia interna»
packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts"
}
apps/web/package.json
{
"name": "web",
"private": true,
"dependencies": {
"@repo/ui": "workspace:*"
}
}
Ventana de terminal
pnpm install

Tras el install, apps/web/node_modules/@repo/ui es un symlink a packages/ui — no una copia. Cambiar un archivo en packages/ui se ve al instante en apps/web, sin build ni publish intermedio.

Ventana de terminal
# Agregar una dependencia externa a un paquete concreto
pnpm add zod --filter web
# Agregar una dependencia de desarrollo a la raíz (herramientas compartidas: eslint, typescript, etc.)
pnpm add -D -w typescript
# Instalar todo el árbol del monorepo de una
pnpm install

-w (--workspace-root) es explícito a propósito: instalar algo en la raíz sin el flag da un error, para que agregar una dependencia “a secas” sin pensar en qué paquete no sea el comportamiento por defecto.

6. —filter — ejecutar comandos en paquetes concretos

Sección titulada «6. —filter — ejecutar comandos en paquetes concretos»

--filter es el corazón de trabajar en un monorepo con pnpm: casi todo comando puede acotarse a un paquete o a un subconjunto.

Ventana de terminal
# Correr el script "dev" solo en el paquete "web"
pnpm --filter web dev
# Correr "build" en web Y en todo lo que web depende (para que ui esté compilado antes)
pnpm --filter web... build
# Correr "test" en todos los paquetes
pnpm -r test
SintaxisSelecciona
--filter webSolo el paquete web
--filter web...web y todas sus dependencias (útil para build en orden)
--filter ...webweb y todo lo que depende de web
-r / --recursiveTodos los paquetes del workspace

Esto resuelve el problema real de un monorepo sin necesitar Turborepo ni Nx para lo básico: pnpm --filter web... build construye packages/ui antes que apps/web automáticamente, porque pnpm conoce el grafo de dependencias declarado en cada package.json.

Sin catalogs, actualizar React en un monorepo de diez paquetes significa editar diez package.json. Un catalog centraliza esa versión:

pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^19.0.0
react-dom: ^19.0.0
apps/web/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}

"react": "catalog:" se resuelve contra la versión declarada en el catalog. Subir React de 19 a 20 es cambiar una línea en pnpm-workspace.yaml y correr pnpm install — cada package.json sigue diciendo catalog:, sin tocar diez archivos ni generar diez líneas de diff en un PR de actualización.

Para casos donde de verdad necesitas dos versiones convivientes (migrando de React 17 a 18 gradualmente, por ejemplo), existen catalogs nombrados:

pnpm-workspace.yaml
catalogs:
react17:
react: ^17.0.2
react18:
react: ^18.2.0
{
"dependencies": { "react": "catalog:react18" }
}
package.json
{
"scripts": {
"dev": "pnpm --filter web dev",
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint"
}
}

Así pnpm build desde la raíz corre el build de todos los paquetes sin que cada persona del equipo tenga que recordar la sintaxis de --filter para las tareas del día a día.

Para un caso real con dos procesos de dev que no terminan (un backend y un frontend) corriendo a la vez con un solo comando, ver el ejemplo completo de frontend + backend.

Fuentes: pnpm Workspaces, pnpm —filter y pnpm Catalogs.