En monorrepositorios con decenas de librerías y aplicaciones internas, un planteamiento ingenuo basado en un único tsconfig.json raíz conduce a un callejón sin salida arquitectónico:
ts.Program).JavaScript heap out of memory).Para escalar la verificación estática de tipos de manera horizontal, TypeScript proporciona la tecnología de Project References (Referencias de Proyectos).
Project References descansa sobre tres principios esenciales:
"composite": true, imponiendo fronteras modulares estrictas y la emisión obligatoria de declaraciones de tipos.tsc --build (tsc -b): El compilador elabora un grafo acíclico dirigido (DAG) de dependencias y compila los proyectos en estricto orden topológico, ignorando módulos actualizados..tsbuildinfo: Si una alteración interna en un paquete no altera su API pública (su archivo .d.ts resultante permanece idéntico), TypeScript descarta recompilar los proyectos dependientes aguas arriba.Bajo la arquitectura de Project References, cada paquete se compila de forma aislada, suministrando a los proyectos superiores contratos de tipos compactos (.d.ts) en lugar de árboles de código fuente sin procesar:
+-------------------------------------------------------------------------+
| GRAFO TOPOLÓGICO DE BUILD DEL MONORREPOSITORIO |
+-------------------------------------------------------------------------+
| ROOT tsconfig.json |
| "references": [ { "path": "apps/web" }, { "path": "packages/ui" } ] |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| apps/web/tsconfig.json |
| "composite": true |
| "references": [ { "path": "../../packages/ui" } ] |
| (Lee ÚNICAMENTE archivos .d.ts generados en packages/ui/dist) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| packages/ui/tsconfig.json |
| "composite": true |
| "references": [ { "path": "../core" } ] |
| (Emite dist/index.d.ts y escribe la caché .tsbuildinfo) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| packages/core/tsconfig.json (Nodo Hoja Base del Grafo) |
| "composite": true |
| "references": [] |
+-------------------------------------------------------------------------+Cuando un desarrollador modifica una función privada dentro de packages/core sin alterar las firmas públicas exportadas:
tsc -b compila packages/core.packages/core/dist/index.d.ts permanece intacto.packages/core/.tsbuildinfo y omite la verificación de tipos para packages/ui y apps/web.tsc --build reduce el tiempo de validación de tipos en CI de 15 minutos a 90 segundos al reaprovechar cachés .tsbuildinfo.@company/ui expone al instante sus archivos .d.ts compilados a todas las aplicaciones sin requerir watchers pesados por cada paquete dependiente.composite: true#Activar composite: true indica al compilador que el proyecto forma parte de un grafo de dependencias superior.
Requisitos indispensables en proyectos compuestos:
declaration: true: El proyecto debe emitir archivos de declaración porque los subproyectos dependientes los consumen directamente.rootDir: Debe fijarse explícitamente (habitualmente ./src). Importar archivos situados fuera de esta frontera sin una referencia formal queda vetado.include o enumerarse en files.{
"extends": "../../packages/tsconfig/base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}Un paquete dependiente formaliza sus dependencias aguas arriba en el array references:
{
"extends": "../../packages/tsconfig/base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"declaration": true,
"declarationMap": true
},
"references": [
{ "path": "../core" }
],
"include": ["src/**/*"]
}En la raíz del repositorio se ubica un tsconfig.json coordinador sin archivos fuente propios (files: []) que centraliza todos los nodos del grafo:
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" },
{ "path": "./apps/docs" }
]
}Ejecutar la compilación incremental de todo el monorrepositorio se efectúa mediante una sola orden:
pnpm exec tsc --build --verboseEl compilador evalúa el orden de ejecución topológico y muestra la traza de compilación:
[14:20:01] Projects in this build:
* packages/core/tsconfig.json
* packages/ui/tsconfig.json
* apps/web/tsconfig.json
* tsconfig.json
[14:20:02] Project 'packages/core' is up to date because newest input 'src/index.ts' is older than output 'dist/index.d.ts'
[14:20:02] Building project 'packages/ui/tsconfig.json'...
[14:20:04] Updating output timestamps of project 'apps/web/tsconfig.json'...Lectura de los bloques references y conformación del grafo acíclico de paquetes.
Contraste de checksums de fuentes y hashes de .d.ts frente a registros en .tsbuildinfo.
Compilación de paquetes obsoletos emitiendo declaraciones .d.ts y sourcemaps .d.ts.map.
Si las firmas .d.ts no han variado, la compilación de consumidores se descarta de inmediato.
Un problema recurrente en monorrepositorios enterprise es la desincronización: se agrega una dependencia en package.json pero se omite la referencia en tsconfig.json references. A continuación se provee un script para auditar y corregir automáticamente estas referencias:
import fs from 'node:fs';
import path from 'node:path';
import { globSync } from 'glob';
console.log('🔍 Comprobando consistencia de Project References en el monorrepositorio...');
// Localizar todos los paquetes y aplicaciones
const packageJsonFiles = globSync('{packages,apps}/*/package.json');
const packageMap = new Map();
for (const pkgPath of packageJsonFiles) {
const content = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const dir = path.dirname(pkgPath);
packageMap.set(content.name, dir);
}
let hasDiscrepancy = false;
for (const [pkgName, pkgDir] of packageMap.entries()) {
const tsconfigPath = path.join(pkgDir, 'tsconfig.json');
const pkgJsonPath = path.join(pkgDir, 'package.json');
if (!fs.existsSync(tsconfigPath)) continue;
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
const internalDeps = [];
const allDeps = {
...pkgJson.dependencies,
...pkgJson.devDependencies,
};
for (const depName of Object.keys(allDeps)) {
if (packageMap.has(depName)) {
const targetDir = packageMap.get(depName);
const relativePath = path.relative(pkgDir, targetDir).replace(/\\/g, '/');
internalDeps.push(relativePath);
}
}
const currentRefs = (tsconfig.references || []).map((ref) =>
ref.path.replace(/\\/g, '/')
);
const missingRefs = internalDeps.filter((dep) => !currentRefs.includes(dep));
if (missingRefs.length > 0) {
console.warn(`⚠️ El paquete ${pkgName} carece de referencias en tsconfig:`, missingRefs);
hasDiscrepancy = true;
// Sincronización automática
tsconfig.references = [
...(tsconfig.references || []),
...missingRefs.map((refPath) => ({ path: refPath })),
];
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n');
console.log(`✅ Referencias añadidas en ${tsconfigPath}`);
}
}
if (hasDiscrepancy) {
console.log('🔄 Archivos tsconfig.json sincronizados exitosamente.');
} else {
console.log('✨ Todas las Project References concuerdan a la perfección con package.json.');
}Configurar alias de rutas "paths": { "@repo/ui": ["../../packages/ui/src"] } hace que el compilador incorpore el código fuente de otro paquete en el programa de la aplicación actual. Como consecuencia, el paquete se recompila por cada aplicación que lo importa, destruyendo la incrementalidad y disparando el uso de RAM.
// ❌ Antipatrón: mapeo directo a fuentes de otros paquetes
{
"compilerOptions": {
"paths": {
"@repo/ui": ["../../packages/ui/src/index.ts"]
}
}
}
// ✅ Práctica recomendada: referencias de proyecto e importación vía .d.ts compilados
{
"references": [
{ "path": "../../packages/ui" }
]
}Si el proyecto A referencia al proyecto B, y el proyecto B referencia directa o indirectamente al proyecto A, tsc --build se interrumpe emitiendo: Project references may not form a circular graph.
Solución: aislar contratos y tipos compartidos en un paquete hoja (packages/contracts o packages/types) situado en la base del árbol.
Eliminar los directorios dist conservando los archivos .tsbuildinfo hace que tsc -b interprete que el build sigue actualizado según su registro en caché, omitiendo la emisión de tipos y provocando errores Cannot find module en los consumidores.
Solución: limpiar los builds siempre mediante el comando nativo: tsc --build --clean.
Activar 'composite': true, 'declaration': true y definir outDir en cada tsconfig de packages/*.
Suprimir rutas relativas hacia carpetas src externas en la sección paths de compilerOptions.
Incorporar el bloque references con las rutas relativas correspondientes a las dependencias directas.
Definir un tsconfig.json raíz con files: [] y registrar todos los subproyectos en su bloque references.
Ejecutar pnpm exec tsc --build --verbose y constatar la correcta ejecución topológica del DAG.