En proyectos de gran escala, desarrollar componentes UI, utilidades compartidas y configuraciones en polirrepositorios aislados (un repositorio Git independiente por cada librería) genera parálisis operativa:
npm link, lo que provoca desajustes de versiones.La arquitectura de monorrepositorio (Monorepo) basada en pnpm workspaces y el orquestador de tareas Turborepo elimina estas fricciones.
La combinación de pnpm y Turborepo define el estándar industrial contemporáneo:
content-addressable store) de pnpm erradican por completo las dependencias fantasma (Phantom Dependencies) habituales en npm y yarn clásico.En un monorrepositorio, el código se divide de forma modular entre aplicaciones ejecutables (apps/*) y paquetes internos compartidos (packages/*):
┌──────────────────────────────────────────────────────────────────────────┐
│ ROOT MONOREPO │
│ pnpm-workspace.yaml | turbo.json | package.json │
└─────────────────────┬──────────────────────────────┬─────────────────────┘
│ │
┌─────────────▼────────────┐ ┌─────────────▼────────────┐
│ apps/web │ │ apps/docs │
│ (Next.js 15+) │ │ (Storybook) │
└─────────────┬────────────┘ └─────────────┬────────────┘
│ depends on: │ depends on:
│ - @repo/ui │ - @repo/ui
│ - @repo/tailwind-config │ - @repo/tailwind-config
│ │
┌─────────────┴──────────────────────────────┴────────────┐
│ SHARED PACKAGES │
│ │
│ ┌───────────────────┐ ┌────────────────────────────┐ │
│ │ @repo/ui │ │ @repo/tailwind-config │ │
│ │ (React 19 Kit) │ │ (Tailwind v4 Presets) │ │
│ └─────────┬─────────┘ └────────────────────────────┘ │
│ │ depends on: │
│ ┌─────────▼─────────┐ ┌────────────────────────────┐ │
│ │ @repo/tsconfig │ │ @repo/eslint-config │ │
│ │ (Strict TS) │ │ (Shared Linting Rules) │ │
│ └───────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Cuando un desarrollador ejecuta pnpm build, Turborepo:
inputs), variables de entorno y dependencias del paquete.dist/ en aproximadamente 50 ms (>>> FULL TURBO), omitiendo la compilación de TypeScript.Cree el directorio packages/analytics e inicialice package.json con el nombre único @repo/analytics.
Añada @repo/tsconfig y @repo/eslint-config a devDependencies usando el protocolo workspace:*.
Configure los puntos de entrada en package.json y declare react y react-dom en peerDependencies.
En apps/web/package.json, añada '@repo/analytics': 'workspace:*' a dependencies.
Ejecute pnpm turbo run build --filter=@repo/analytics... para verificar el grafo DAG.
¿De qué manera la arquitectura de pnpm workspaces protege el proyecto frente a dependencias fantasma (Phantom Dependencies) comunes en npm clásico?
@company/ui se consume simultáneamente desde una aplicación web en Next.js, un cliente móvil en React Native y un panel administrativo en Vite.source en package.json, evitando compilar en modo watch cada librería interna.--filter=...[HEAD^1] de Turborepo ejecuta pruebas y compilaciones exclusivamente para dicho componente y sus aplicaciones dependientes.pnpm update -r react@latest, impidiendo la coexistencia de versiones conflictivas.packages:
- 'apps/*'
- 'packages/*'El archivo turbo.json define la topología del flujo de trabajo:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}dependsOn: ["^build"]: El símbolo ^ define una dependencia topológica en el DAG. Antes de compilar apps/web, Turborepo debe compilar obligatoriamente todos los paquetes internos de packages/* de los que depende.cache: false, persistent: true: Configuración para servidores de desarrollo de larga duración.El paquete @repo/ui nunca debe empaquetar su propia versión de React. En su lugar, declara react y react-dom dentro de peerDependencies:
{
"name": "@repo/ui",
"version": "1.4.0",
"private": false,
"type": "module",
"exports": {
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.js"
},
"./card": {
"types": "./dist/card.d.ts",
"import": "./dist/card.js"
},
"./styles.css": "./dist/index.css"
},
"scripts": {
"build": "tsup src/index.ts src/button.tsx src/card.tsx --dts --format esm",
"dev": "tsup src/index.ts --watch --dts --format esm",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2"
}
}El desarrollador edita @repo/ui, ejecuta pnpm changeset y selecciona el tipo de incremento (patch, minor, major).
GitHub Actions ejecuta turbo run test build estrictamente sobre los paquetes modificados.
Un bot abre el PR 'Version Packages', actualizando package.json y consolidando CHANGELOG.md.
Al aprobarse el PR de versión, los paquetes se publican en npm/GitHub Packages y se crean tags en Git.
¿Qué función cumple la directiva dependsOn: ['^build'] dentro de la tarea 'build' de turbo.json para apps/web?
A continuación se muestra una configuración optimizada de tsup para un paquete de componentes de diseño, generando módulos ESM con soporte para tree-shaking, tipos .d.ts e inyección de la directiva 'use client' para React 19:
import { defineConfig, type Options } from 'tsup';
export default defineConfig((options: Options) => ({
// Múltiples puntos de entrada para permitir tree-shaking granular
entry: [
'src/index.ts',
'src/components/Button/index.ts',
'src/components/Card/index.ts',
'src/components/Modal/index.ts',
],
format: ['esm'],
dts: true,
clean: !options.watch,
sourcemap: true,
minify: !options.watch,
target: 'es2022',
external: ['react', 'react-dom'],
// Inyección de la directiva 'use client' para límites de cliente en Next.js App Router
banner: {
js: "'use client';",
},
esbuildOptions(esbuildOptions) {
esbuildOptions.jsx = 'automatic';
},
}));En entornos planos, apps/web puede importar librerías (como lodash) omitidas en su package.json pero instaladas en packages/ui. Al construir la imagen en un contenedor Docker aislado, la compilación fallará de inmediato con Module not found.
// ❌ Antipatrón: falta de dependencia explícita en apps/web/package.json
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*"
// lodash no está declarado, pero funciona en local por hoisting accidental
}
}
// ✅ Patrón de producción: declarar explícitamente todas las dependencias directas
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*",
"lodash": "^4.17.21"
}
}Si en turbo.json no se define con precisión el array inputs, Turborepo invalidará la caché de compilación ante cualquier cambio en el repositorio (como un archivo README.md o documentación).
// ❌ Antipatrón: cualquier commit en markdown invalida la caché de compilación
"build": {
"outputs": ["dist/**"]
}
// ✅ Patrón de producción: definición estricta de archivos de código fuente
"build": {
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}Si @repo/auth depende de @repo/ui y @repo/ui importa interfaces o hooks de @repo/auth, Turborepo no podrá construir el DAG y generará un error Circular dependency detected.
Solución: Extraer contratos y tipos comunes hacia un paquete desacoplado de bajo nivel, como @repo/types o @repo/contracts.