wandres.dev
PNPM WORKSPACES · el monorepo básico

--filter: ejecutar tareas en un subconjunto

El bisturi del monorepo: seleccionar paquetes por nombre, por directorio y por cambios de git, y recorrer el grafo con el operador ... para incluir dependencias o dependientes.

⏱ 15 min

En un monorepo de cincuenta paquetes casi nunca quieres actuar sobre los cincuenta. Quieres testear solo lo que tocaste, construir solo la app que despliegas, o rehacer un paquete y todo lo que dependa de él. --filter es el bisturi que selecciona ese subconjunto, y su verdadero poder no está en filtrar por nombre, sino en razonar sobre el grafo de dependencias con un operador de tres puntos.

🎯 Al terminar esta lección sabrás
  • Seleccionar paquetes por nombre, patrón y negación con --filter.
  • Filtrar por ubicación en disco y por cambios respecto a una rama de git.
  • Dominar el operador ... para incluir dependencias o dependientes.
  • Combinar selectores para expresar exactamente qué debe recompilarse en CI.

Filtrar por nombre y por patrón

La forma más simple selecciona por el name del package.json:

pnpm --filter @acme/web run build       # solo un paquete
pnpm --filter "@acme/*" run test         # todos los del scope acme
pnpm --filter "!@acme/legacy" run lint   # todos menos uno (negacion)

Los patrones son globs sobre el nombre, y la negación con ! resta del conjunto ya seleccionado. Puedes encadenar varios --filter: se unen. Si un filtro no casa con nada, pnpm lo avisa; con --fail-if-no-match conviertes ese aviso en un error, útil en CI para que un typo no pase inadvertido como “cero paquetes, cero trabajo”.

--filter tiene el alias corto -F y se acumula, de modo que combinar inclusión y exclusión es idiomático:

pnpm -F "@acme/*" -F "!@acme/legacy" run build   # todo acme menos legacy
pnpm -F @acme/web build                          # en scripts conocidos, sin run

Filtrar por directorio y por cambios de git

A veces piensas en términos de ubicación, no de nombre. Los selectores entre llaves filtran por la posición en el disco:

pnpm --filter "./apps/**" run build      # todo lo que cuelga de apps/
pnpm --filter "{packages/ui}" run test   # el paquete en esa ruta

Pero el selector que cambia el juego en CI es el de cambios de git: entre corchetes indicas un punto de comparación y pnpm selecciona los paquetes cuyos archivos cambiaron respecto a esa referencia.

pnpm --filter "[origin/main]" run test    # paquetes tocados vs main
pnpm --filter "[HEAD~1]" run build        # tocados en el ultimo commit

pnpm calcula el cambio contra el merge-base con esa referencia, no contra su punta: la selección refleja “lo que introduce mi rama” y no el ruido de commits ajenos que también entraron en main. Puedes afinar qué cuenta como cambio con --changed-files-ignore-pattern, para que tocar un README.md o un snapshot de test no dispare un rebuild de producción:

pnpm --filter "[origin/main]" \
  --changed-files-ignore-pattern "**/*.md" run build

Y el selector de git se combina con los demás: --filter "{packages/**}[origin/main]" limita la comparación a los paquetes bajo packages/, y anteponer ... extiende el resultado a sus dependientes.

Esto es lo que permite que un pipeline no reconstruya el mundo en cada push, sino solo lo que de verdad cambió. Y se combina con lo anterior: --filter "{packages/ui}[origin/main]" selecciona el paquete de UI solo si fue modificado respecto a main.

El operador …: subir y bajar por el grafo

Aquí vive la potencia real. Tres puntos, a un lado u otro del selector, extienden la selección a lo largo de las aristas del grafo de dependencias:

⬇️

pkg...

--filter "@acme/ui..." incluye @acme/ui y sus dependencias (aguas abajo). Útil para construir un paquete junto con todo lo que necesita.

⬆️

...pkg

--filter "...@acme/ui" incluye @acme/ui y sus dependientes (aguas arriba). Útil para testear todo lo que podrías haber roto al cambiar la UI.

🔽

pkg^...

--filter "@acme/ui^..." incluye solo las dependencias de @acme/ui, sin el paquete en sí. El caret excluye el propio nodo.

🔼

...^pkg

--filter "...^@acme/ui" incluye solo los dependientes, sin @acme/ui. Para reconstruir lo de arriba tras un cambio ya construido.

La combinación estrella para CI une git y grafo: --filter "...[origin/main]" selecciona todos los paquetes que cambiaron respecto a main más todos sus dependientes. Es decir: lo que tocaste y todo lo que ese cambio podría haber roto aguas arriba. Ese único selector expresa la pregunta correcta de un pipeline —“¿qué necesito volver a validar?”— sin hardcodear nada.

Un error frecuente es confundir las dos direcciones. Regla mnemotécnica: los puntos “caen” hacia lo que apuntan. pkg... cae hacia sus dependencias, aguas abajo; ...pkg llega desde sus dependientes, aguas arriba. Si dudas, pnpm --filter <expr> exec pwd imprime sin ejecutar la tarea qué paquetes entran en la selección.

flowchart BT
utils[acme utils] --> ui[acme ui]
utils --> api[acme api]
ui --> web[acme web]
api --> web
ui --> docs[acme docs]
style ui fill:#a6e3a1,color:#11111b
style web fill:#89b4fa,color:#11111b
style docs fill:#89b4fa,color:#11111b

En ese grafo, --filter "...@acme/ui" selecciona ui, web y docs (verde y azul): el paquete cambiado y sus dependientes. --filter "@acme/ui^..." seleccionaria en cambio a utils: solo aquello de lo que la UI depende. Leído así, ... es el operador que responde “¿y qué más?”: hacia los dependientes, qué se ve afectado por mi cambio; hacia las dependencias, qué necesito tener listo antes de construirme.

Prod, dev y afinar el alcance

Dos matices que se pagan a escala. --filter-prod aplica el mismo selector pero ignorando devDependencies al calcular el grafo: en un build de producción no quieres arrastrar dependientes que solo te consumen como herramienta de test. Y para tareas recursivas que quizá no existan en todos los paquetes seleccionados, --if-present evita que pnpm falle por un script ausente:

pnpm --filter "...[origin/main]" run build --if-present

En CI el patrón canónico encadena selección y ejecución. Un job que solo valida lo afectado por la rama luce así:

# instala TODO el workspace: se necesita el grafo completo
pnpm install --frozen-lockfile
# testea solo lo cambiado respecto a main y sus dependientes
pnpm --filter "...[origin/main]" run test --if-present
# construye lo mismo, con salida agrupada por paquete
pnpm --filter "...[origin/main]" run build --aggregate-output

Conviene tener a mano el repertorio completo de selectores, que se combinan encadenando varios --filter:

  • @acme/ui — por nombre exacto.
  • "@acme/*" — por patrón de nombre (glob).
  • "!@acme/legacy" — negación, resta del conjunto ya elegido.
  • "./apps/**" o "{packages/ui}" — por ubicación en el disco.
  • "[origin/main]" — por cambios respecto a una referencia de git.
  • pkg... y ...pkg — más sus dependencias, más sus dependientes.
  • pkg^... y ...^pkg — solo dependencias, solo dependientes.

Todo eso son piezas de un pequeño lenguaje de consulta sobre el grafo; combinarlas es lo que separa lanzar tareas a ciegas de recomputar exactamente lo necesario, ni un paquete más.

💡
Instala completo, ejecuta filtrado

Un error típico en CI es intentar instalar solo el subconjunto filtrado. No lo hagas: instala el workspace entero con --frozen-lockfile para que pnpm conozca el grafo completo, y reserva --filter para el momento de ejecutar las tareas. La selección decide qué trabajo corre, no qué se instala; sin el grafo completo, el operador ... no puede calcular bien los dependientes.

Filtrar es razonar sobre el grafo, no sobre carpetas

El salto conceptual de --filter es dejar de pensar en “qué carpetas” y empezar a pensar en “qué región del grafo”. Un monorepo no es una lista de proyectos: es un grafo dirigido acíclico donde las aristas son las dependencias workspace:. El operador ... es, literalmente, un operador de alcance transitivo sobre ese grafo —clausura hacia las dependencias o hacia los dependientes— y combinarlo con el selector de git te da algo profundo: la capacidad de calcular, a partir de un diff, el conjunto mínimo y suficiente de trabajo que hay que rehacer. --filter "...[origin/main]" no es un comando, es una proposición: “todo lo afectado por lo que cambié”. Esa es exactamente la información que un sistema de caché como Turborepo o Nx necesita para decidir qué recomputar y qué reutilizar; de hecho, pnpm te da la selección y esas herramientas le añaden encima la memoización. Interiorizar que cada invocación de --filter es una consulta sobre la topología del repo —y no un atajo para no escribir rutas— es lo que convierte el monorepo de una molestia lenta en una máquina que solo hace el trabajo estrictamente necesario. A cien paquetes, la diferencia entre filtrar bien y filtrar mal es la diferencia entre un CI de treinta segundos y uno de treinta minutos.

⚔️ Piensa el grafo con filtros
  1. En un workspace con al menos ui, utils, web y docs, dibuja en papel el grafo de dependencias workspace:.
  2. Predice qué selecciona --filter "...@acme/utils" y verifícalo con pnpm --filter "...@acme/utils" exec pwd.
  3. Compara --filter "@acme/web..." (web y sus dependencias) con --filter "...@acme/web" (web y sus dependientes) y explica por qué uno de los dos casi solo devuelve web.
  4. Cambia un archivo en packages/ui, commitea, y ejecuta pnpm --filter "...[HEAD~1]" run build --if-present: observa que se reconstruye la UI y sus consumidores.
  5. Investiga: añade --fail-if-no-match a un filtro con un typo y confirma que CI lo rechazaría.