wandres.dev
PNPM WORKSPACES · el monorepo básico

Recursivo (-r), scripts y organización a escala

Ejecutar tareas en todo el workspace con -r, orquestar scripts en orden topológico o en paralelo, correr en la raíz con -w, y los patrones que sostienen un monorepo grande: catalogs, tooling y politica de version unica.

⏱ 15 min

Un paquete se maneja con un comando; cincuenta necesitan un telar. pnpm -r es ese telar: ejecuta una tarea a través de todo el workspace, respetando el orden topológico del grafo o disparándolo todo en paralelo cuando el orden no importa. Sumado a los scripts de la raíz, a los catalogs y a una topología disciplinada, es lo que mantiene coherente un monorepo que crece a decenas de paquetes y varias personas.

🎯 Al terminar esta lección sabrás
  • Ejecutar tareas en todo el workspace con pnpm -r (recursivo).
  • Controlar concurrencia, paralelismo y salida en tareas masivas.
  • Distinguir correr en la raíz (-w) de correr en los paquetes.
  • Aplicar patrones de escala: catalogs, tooling y política de versión única.

Comandos recursivos: -r

La bandera -r (o --recursive) hace que el comando se ejecute en cada paquete del workspace. Por defecto respeta el orden topológico: las dependencias primero, sus consumidores después.

pnpm -r run build      # build en cada paquete, en orden topologico
pnpm -r test           # atajo: los scripts conocidos no necesitan run
pnpm -r exec -- rm -rf dist   # ejecuta un binario o comando en cada uno

Tres comandos recursivos cubren casi todo: run para scripts del package.json, exec para lanzar un binario instalado —un linter, un generador de código— en cada paquete, y test o build como atajos de run. Para incluir también la raíz en la iteración usas --include-workspace-root:

pnpm -r exec -- eslint .                          # lint en cada paquete
pnpm -r --include-workspace-root exec -- node -v  # incluye la raiz

Un detalle que sorprende: pnpm -r excluye el proyecto raíz por defecto. La raíz es un orquestador, no un paquete que se construya. Para correr algo en la raíz usas -w (lo vemos abajo). Y para tareas que no existen en todos los paquetes, --if-present evita que pnpm aborte por un script ausente en lugar de saltarlo.

Paralelismo, concurrencia y salida

El orden topológico es correcto para builds, pero equivocado para procesos que no terminan. Si haces pnpm -r run dev, pnpm intentaría esperar a que el primer dev server “acabe” antes de arrancar el siguiente, y nunca acaba. Para eso está --parallel, que ignora el orden y la concurrencia y lo lanza todo a la vez:

pnpm -r --parallel run dev              # todos los dev servers a la vez
pnpm -r --workspace-concurrency=8 build # hasta 8 builds simultaneos
pnpm -r --aggregate-output run test     # agrupa la salida por paquete
🔀

--parallel

Ignora orden topológico y límite de concurrencia. Para tareas de larga duración: dev servers, watchers. Nunca para builds con dependencias.

🎚️

--workspace-concurrency

Cuántos paquetes procesar a la vez respetando el grafo. Por defecto un número modesto; súbelo en CI con muchos núcleos.

📑

--aggregate-output

Almacena la salida de cada paquete y la imprime junta al terminar. En CI produce logs legibles en vez de líneas entrelazadas.

🌊

--stream

Lo contrario: emite en tiempo real prefijando cada línea con el nombre del paquete. Para ver el progreso mientras ocurre.

Un detalle de diseño que confunde al principio: --parallel y --stream resuelven problemas distintos. --parallel afecta al scheduling —qué corre a la vez, ignorando el grafo—; --stream y --aggregate-output afectan solo a cómo se presenta la salida. Se combinan según el contexto: en local, --parallel --stream para levantar todos los dev servers y verlos a la vez; en CI, orden topológico con --aggregate-output para logs deterministas que se puedan leer cuando algo falla.

La raíz y el reparto de scripts

Hay dos lugares donde vive un script: en un paquete o en la raíz. pnpm -w run <script> (o --workspace-root) fuerza la ejecución en el package.json raíz, donde suelen vivir los scripts orquestadores. Un patrón habitual reparte responsabilidades así:

{
  "scripts": {
    "build": "pnpm -r run build",
    "dev": "pnpm -r --parallel run dev",
    "test": "pnpm -r run test --if-present",
    "lint": "pnpm -r run lint"
  }
}

Así pnpm build en la raíz dispara el build de todo el grafo en orden, y pnpm --filter @acme/web build construye solo una rama. La combinación de -r, --filter y --parallel cubre casi cualquier necesidad de orquestación.

Un antipatrón a evitar: scripts que hacen cd a otro paquete y encadenan comandos con rutas relativas escritas a mano. Se rompen en cuanto cambia la topología y no respetan el orden del grafo. Todo movimiento entre paquetes debe pasar por --filter, que conoce las fronteras y las dependencias, y nunca por rutas frágiles cosidas en un script.

⚠️
pnpm ordena, pero no cachea

pnpm -r garantiza el orden correcto, pero no memoriza resultados: si nada cambió, vuelve a construir igual. Ahí termina su competencia y empieza la de Turborepo o Nx, que hashean las entradas de cada tarea y reutilizan la salida cacheada. El patrón de 2026 es claro: pnpm provee el grafo, el enlace y la selección; la capa de caché va encima. No esperes de pnpm lo que es trabajo del orquestador de tareas.

Patrones de organización a escala

A partir de cierto tamaño, la coherencia importa más que la velocidad. Tres patrones sostienen un monorepo grande:

  • Catalogs: política de versión única. En pnpm-workspace.yaml defines una versión central y cada paquete la referencia con catalog:, no con un número. Así React es la misma versión en los cuarenta paquetes, y actualizarla es editar una línea.
# pnpm-workspace.yaml
catalog:
  react: ^19.0.0
  zod: ^3.24.0
{ "dependencies": { "react": "catalog:", "zod": "catalog:" } }

Cuando necesitas convivencia de versiones —por ejemplo, una migración gradual de React 18 a 19— usas catalogs con nombre:

# pnpm-workspace.yaml
catalog:
  react: ^19.0.0
catalogs:
  react18:
    react: ^18.3.0
{ "dependencies": { "react": "catalog:react18" } }

El catálogo por defecto cubre el caso mayoritario; los nombrados aíslan las excepciones sin renunciar a la centralización. Al publicar, catalog: se reescribe a la versión concreta, igual que hace workspace:.

  • Paquetes de tooling. El eslint-config, el tsconfig base, la config de Tailwind viven como paquetes internos en tooling/ y se consumen con workspace:*. Un cambio de regla se propaga a todo el repo sin copiar y pegar.
  • packageManager y corepack. El campo "packageManager": "pnpm@10.x" clava la versión de pnpm para todo el equipo, y corepack la aplica. Reproducibilidad no solo de dependencias, sino del propio gestor; junto a engines, cierra la puerta al clásico “en mi máquina funciona” por desajuste de runtime.
  • Convención de nombres y private. Todo paquete lleva scope (@acme/*); las apps se marcan "private": true para que un pnpm publish -r accidental nunca las suba al registro.

Y una pieza de despliegue: pnpm deploy genera una copia autónoma de un paquete con sus dependencias ya resueltas y sin symlinks, ideal para meter una sola app del monorepo en una imagen de contenedor sin arrastrar el repo entero.

En conjunto, estos patrones convergen en una sola idea: a escala, el reto del monorepo no es ejecutar tareas más rápido, sino garantizar que todo el repo comparta una única versión de cada cosa —dependencias, configuración, gestor— y que cualquiera pueda clonar, instalar y construir con resultados idénticos. La velocidad es consecuencia; la coherencia es el objetivo.

📝
Un solo lockfile, una sola verdad

El pnpm-lock.yaml de la raíz es innegociable: commitéalo siempre, no lo edites a mano y en CI usa --frozen-lockfile para que la instalación falle si el lockfile no cuadra con los package.json. Un lockfile que se regenera en silencio durante el pipeline es la puerta de entrada a builds no reproducibles.

El monorepo es una máquina de coherencia

Es tentador ver un monorepo como una optimización de velocidad —construir rápido, cachear, no repetir trabajo—, pero su valor más profundo es la coherencia. Cuando todo vive en un grafo con un solo lockfile, un cambio atómico puede cruzar fronteras de paquete: renombras una función en @acme/utils y actualizas sus diez consumidores en el mismo commit, con el CI validando el conjunto de una vez. Los catalogs llevan esa idea a las versiones: una política de versión única elimina de raíz la clase de bugs en que dos paquetes cargan dos Reacts distintos. Los paquetes de tooling la llevan a la configuración: una sola fuente de verdad para el lint y los tipos. pnpm -r es el ejecutor de esa coherencia, recorriendo el grafo en el orden que el propio grafo dicta. La madurez consiste en entender la división del trabajo del ecosistema de 2026: pnpm resuelve, enlaza, ordena y selecciona; un orquestador como Turborepo memoiza; Changesets versiona y publica. Cada capa hace una cosa y la hace bien, y encajarlas es diseñar cómo cientos de commits al día conviven sin pisarse. Un monorepo bien montado no es una carpeta grande: es una disciplina de que el repositorio entero sea, en todo momento, internamente consistente.

⚔️ Orquesta el workspace entero
  1. Escribe en el package.json raíz los scripts build, dev, test y lint usando pnpm -r, con --parallel solo en dev.
  2. Ejecuta pnpm -r --stream run build y observa el orden topológico en los prefijos de cada línea.
  3. Mueve una dependencia común (por ejemplo zod) a un catalog: en pnpm-workspace.yaml y sustitúyela en dos paquetes; reinstala y confirma la versión única.
  4. Crea un paquete tooling/tsconfig y haz que dos paquetes extiendan de él con workspace:*.
  5. Investiga: corre pnpm deploy para una app y examina la carpeta generada; comprueba que no contiene enlaces simbólicos al resto del monorepo.