Monorepos con Astro: pnpm workspaces y Turborepo
Cuando un producto deja de ser un sitio y pasa a ser varios que comparten identidad, el monorepo ordena la casa: pnpm workspaces reúne apps y paquetes en un solo repositorio con el protocolo workspace, los paquetes internos comparten UI, configuración y tipos sin publicarlos en npm, y Turborepo orquesta las tareas respetando el grafo de dependencias y cacheando lo que no cambió.
Un producto rara vez se queda en un solo sitio. Primero es la web de marketing; luego llega la documentación, después el panel de la aplicación, quizá un blog aparte. Los cuatro comparten el mismo botón, la misma paleta, la misma configuración de TypeScript y el mismo cliente de datos, y mantenerlos sincronizados a base de copiar y pegar entre cuatro repositorios es una forma lenta de perder la cabeza. El monorepo es la respuesta a ese problema: un único repositorio que aloja varias aplicaciones y las librerías internas que comparten, con herramientas que entienden cómo se relacionan. Este capítulo monta esa casa con las dos piezas que hoy la sostienen —pnpm para el espacio de trabajo y Turborepo para orquestar las tareas— y muestra cómo un componente Astro viaja de una app a otra sin publicarse en ningún registro.
- Reunir apps y paquetes en un solo repositorio con pnpm workspaces.
- Enlazar dependencias internas con el protocolo
workspace:*. - Compartir UI, configuración y tipos como paquetes internos sin publicarlos.
- Orquestar y cachear las tareas del grafo con Turborepo.
Un solo repo, muchos paquetes
La base del monorepo es el espacio de trabajo, y pnpm lo declara en un fichero en la raíz. pnpm-workspace.yaml enumera qué carpetas contienen paquetes; la convención más extendida separa apps/ —las cosas que se despliegan, como cada sitio Astro— de packages/ —las librerías internas que esas apps consumen—.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Con eso, pnpm trata cada subcarpeta con su package.json como un paquete del mismo universo. Instala las dependencias una sola vez en un almacén compartido y las enlaza donde hagan falta, de modo que dos apps que usan Astro no guardan dos copias sino una, referenciada desde ambas. Esa deduplicación no es un detalle de disco: es lo que mantiene el árbol de dependencias coherente y evita que dos apps convivan sin querer con versiones distintas de la misma librería.
La pieza que teje el monorepo por dentro es el protocolo workspace:*. Cuando la app web declara que depende de @repo/ui, no pone un número de versión de npm sino workspace:*, y pnpm entiende que ha de resolver esa dependencia desde el propio repositorio, enlazándola a la carpeta local. El paquete interno se comporta como cualquier dependencia —lo importas por su nombre—, pero su código vive a unos metros, en packages/ui, y cualquier cambio se refleja al instante sin publicar ni reinstalar.
// apps/web/package.json
{
"name": "web",
"type": "module",
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/config": "workspace:*",
"astro": "^7.0.0"
}
}
Compartir UI, config y tipos sin publicarlos
El corazón del monorepo son los paquetes internos, y con Astro hay una decisión de fondo que tomar: ¿el paquete de UI se compila antes de usarse, o se consume en crudo? La opción que mejor encaja con Astro son los llamados paquetes just-in-time: no tienen paso de build propio, sino que exponen sus ficheros .astro tal cual y dejan que sea el pipeline de Vite de la app que los importa quien los compile. Un componente Astro no es JavaScript que haya que transpilar por separado; es una plantilla que el compilador de la app entiende de forma nativa, así que interponer un build intermedio solo añade latencia y complejidad.
El paquete declara qué expone mediante el campo exports de su package.json, el mapa que decide qué rutas de importación son públicas y a qué fichero apuntan. Todo lo que no esté ahí queda como detalle interno, invisible para quien lo consume.
// packages/ui/package.json
{
"name": "@repo/ui",
"type": "module",
"exports": {
"./Boton.astro": "./src/Boton.astro",
"./styles.css": "./src/styles.css"
}
}
Desde cualquier app, ese componente se importa como si viniera de npm —import Boton from '@repo/ui/Boton.astro'— y Vite lo compila junto al resto. La misma técnica sirve para compartir mucho más que UI: un @repo/config con la configuración común de Astro o Vite, un @repo/tsconfig con la base de TypeScript que cada app extiende, un @repo/lib con el cliente de datos y los tipos del dominio. Cada preocupación transversal se convierte en un paquete con una fachada clara, y las apps se vuelven delgadas: casi todo lo sustancial vive en packages/, y cada sitio es sobre todo su enrutado y su despliegue.
Un peligro sutil del monorepo es la deriva de versiones: que la app web quede en Astro 7.0 y la de docs salte a la 7.2 sin que nadie lo decida, y que un componente compartido se compile distinto según quién lo use. pnpm lo ataja con los catálogos: defines en pnpm-workspace.yaml un rango único para cada dependencia clave, y las apps la referencian como catalog: en vez de fijar su propio número. Así Astro, Vite y las librerías críticas suben para todos a la vez, con un solo cambio, y el monorepo mantiene un frente común de versiones en lugar de un archipiélago de variantes.
La regla que evita dolores: los paquetes que exportan componentes .astro o de framework se sirven mejor sin compilar, porque el bundler de cada app ya sabe procesarlos y así comparten un solo pipeline. Los paquetes de lógica pura —un cliente de API, utilidades de fechas, tipos— pueden compilarse a JavaScript y tipos si quieres publicarlos algún día fuera del monorepo, pero dentro de él muchas veces basta con apuntar exports a los fuentes de TypeScript y dejar que cada app los transpile. Menos pasos de build, menos artefactos que sincronizar, menos cosas que se rompen.
Orquestar las tareas con Turborepo
pnpm resuelve dónde vive el código; falta resolver cómo se ejecutan las tareas cuando hay diez paquetes y cada uno tiene su build, su check, su dev. Lanzarlas a mano, en el orden correcto, es inviable: si @repo/ui ha de construirse antes que la app que lo usa, alguien tiene que saberlo. Turborepo lee el grafo de dependencias del workspace y ejecuta las tareas respetándolo, en paralelo cuando puede y en serie cuando debe.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"check": { "dependsOn": ["^build"] },
"dev": { "cache": false, "persistent": true }
}
}
La clave está en dependsOn y en ese acento circunflejo. "^build" significa antes de construirme yo, construye el build de mis dependencias: Turborepo remonta el grafo y garantiza que @repo/ui esté listo antes de tocar la app. Pero su golpe maestro es la caché: declara en outputs qué produce cada tarea, calcula una huella de las entradas —ficheros, dependencias, variables— y, si esa huella no ha cambiado desde la última vez, no reejecuta nada; restaura el resultado cacheado en un instante. En un monorepo donde solo tocaste el blog, reconstruir el blog y saltarse las otras tres apps intactas es la diferencia entre una CI de treinta segundos y una de diez minutos.
pnpm --filter web dev # arranca solo la app web
pnpm --filter web add @repo/ui # enlaza el paquete interno en web
turbo run build # construye todo el grafo en orden, con cache
Esa caché puede además compartirse. Con la caché remota, la huella y el resultado de cada tarea viajan a un almacén común, de modo que si un compañero —o la CI— ya construyó exactamente ese estado, tu máquina lo descarga en vez de recalcularlo. El trabajo se hace una vez en todo el equipo y se reutiliza en todas partes; es la economía del monorepo llevada también al tiempo de cómputo.
flowchart TD WEB[app web] --> UI[repo ui] WEB --> CFG[repo config] DOCS[app docs] --> UI DOCS --> CFG UI --> TS[repo tsconfig] CFG --> TS TURBO[turbo orquesta y cachea] -.-> WEB TURBO -.-> DOCS style WEB fill:#89b4fa,color:#11111b style DOCS fill:#89b4fa,color:#11111b style TURBO fill:#f9e2af,color:#11111b style TS fill:#a6e3a1,color:#11111b
Reunir todo en un repositorio tienta a acoplarlo todo. Si la app de docs importa una función interna de la app web porque estaba a mano, has creado una dependencia que ningún grafo declara y que romperá en silencio. La disciplina del monorepo es la misma que la del capítulo anterior, subida un nivel: las apps no se importan entre sí, jamás; lo que dos apps comparten se sube a un paquete de packages/ con su fachada pública, y solo por ahí se consume. El monorepo acerca el código, y esa cercanía es su virtud y su trampa: facilita compartir bien y, con la misma facilidad, acoplar mal.
pnpm workspaces
Un pnpm-workspace.yaml reune apps y paquetes; el protocolo workspace enlaza las dependencias internas al codigo local.
Paquetes just-in-time
Los componentes astro se exportan en crudo y los compila el Vite de cada app. Sin build intermedio que sincronizar.
Turborepo
Lee el grafo, respeta dependsOn y ejecuta en el orden correcto, en paralelo donde puede.
Cache por huella
Si las entradas no cambiaron, restaura el resultado en vez de recalcularlo; la cache remota lo comparte con todo el equipo.
La pregunta que subyace a todo monorepo no es de herramientas sino de topología: ¿cuál es la unidad natural de tu sistema, el repositorio o el paquete? Durante años la respuesta por defecto fue un repositorio por cosa desplegable, y con ella llegó una ficción cómoda: la de que cada sitio era una isla independiente. Pero esa independencia casi nunca era real. La web y el panel siempre compartieron el botón, la paleta, el cliente de datos; lo que cambiaba entre el mundo de muchos repos y el de uno solo no era si compartían código, sino si ese hecho estaba declarado o quedaba oculto en cuatro copias que se desincronizaban en silencio. El monorepo no crea el acoplamiento entre tus proyectos: lo hace visible. Convierte una relación que existía de facto —“cuando cambio el botón, cambian los cuatro sitios”— en una arista explícita del grafo, con una dirección, una versión y una herramienta que la respeta. Y una vez que el grafo es visible, se vuelve gobernable: puedes prohibir las aristas que no quieres, ordenar las tareas según las que sí, y cachear con precisión porque sabes exactamente qué depende de qué. Ahí está la idea profunda, la que trasciende a pnpm y a Turborepo: las herramientas de un monorepo no son más que máquinas para razonar sobre un grafo de dependencias que siempre estuvo ahí, y su valor es proporcional a cuánto de ese grafo consigues sacar de la sombra de la costumbre y ponerlo a la luz donde una máquina pueda verificarlo. Un monorepo bien llevado no es un repositorio grande; es un grafo honesto. Cuando dejes de preguntarte “¿cuántos repos tengo?” y empieces a preguntarte “¿qué depende de qué, y quién lo vigila?”, habrás entendido para qué sirve de verdad meterlo todo en el mismo sitio.
- Crea la estructura
apps/web,apps/docsypackages/ui, y declara el espacio de trabajo enpnpm-workspace.yamlconapps/*ypackages/*. - Exporta un componente
.astrodesdepackages/uimediante el campoexportsy consúmelo en ambas apps con una dependenciaworkspace:*. - Cambia el componente una vez y confirma que las dos apps reflejan el cambio sin publicar ni reinstalar nada.
- Añade un
turbo.jsoncon una tareabuildque dependa de^build, ejecútala dos veces seguidas y observa cómo la segunda restaura desde la caché en vez de reconstruir.