Project references y declaration emit en monorepos | Nicolás Garzón
Las project references dividen un repositorio en proyectos TypeScript con límites, dependencias y resultados incrementales explícitos.
Texto
Copiar apps/web
↓
packages/application
↓
packages/domainUn proyecto referenciado necesita:
JSON
Copiar {
"compilerOptions" : {
"composite" : true
}
} Esto activa requisitos para que TypeScript pueda conocer sus entradas y resultados de forma confiable.
Normalmente también produce información incremental y permite declarations.
JSON
Copiar {
"files" : [ ] ,
"references" : [
{ "path" : "./packages/domain" } ,
{ "path" : "./packages/application" } ,
{ "path" : "./apps/web" }
]
} Un tsconfig raíz puede funcionar como solution y no incluir archivos propios.
JSON
Copiar {
"extends" : "../../tsconfig.base.json" ,
"compilerOptions" : {
"composite" : true ,
"rootDir" : "src" ,
"outDir" : "dist" ,
"declaration" : true ,
"declarationMap" : true
} ,
"include" : [ "src" ] ,
"references" : [
{ "path" : "../domain" }
]
} Bash
Copiar npx tsc --build Construye proyectos en orden de dependencias y reutiliza resultados actualizados.
Bash
Copiar npx tsc -b --clean Elimina outputs conocidos por los proyectos referenciados.
Bash
Copiar npx tsc -b --force Reconstruye aunque los metadatos indiquen que está actualizado.
Bash
Copiar npx tsc -b --verbose Explica qué proyecto se reconstruye y por qué.
JSON
Copiar {
"compilerOptions" : {
"incremental" : true ,
"tsBuildInfoFile" : ".cache/tsconfig.tsbuildinfo"
}
} Guarda información para evitar recomprobar trabajo sin cambios. El archivo es un cache del compilador, no un artefacto portable de API.
Cuando un proyecto referencia a otro, el checker puede utilizar sus .d.ts para entender la API pública en vez de incluir todos sus detalles internos como fuente propia.
Reducir trabajo.
Hacer visible la frontera.
Detectar exports públicos incompletos.
declarationMap permite navegar desde declarations hacia source cuando las rutas y archivos están disponibles.
TypeScript
Copiar import { internalHelper } from "../../domain/src/internal.js" ; Rompe la frontera y puede hacer que el consumidor compile source ajeno con otra configuración.
Importa la API del package:
TypeScript
Copiar import { Product } from "@app/domain" ; Cada workspace package debe exponer únicamente entradas soportadas:
JSON
Copiar {
"name" : "@app/domain" ,
"type" : "module" ,
"exports" : {
"." : {
"types" : "./dist/index.d.ts" ,
"import" : "./dist/index.js"
}
}
} paths puede apuntar a source durante desarrollo, pero puede ocultar que el package publicado o runtime no resuelve igual.
En monorepos, workspaces y package exports suelen representar mejor el consumo real. Si utilizas paths, prueba también el output construido.
Opciones antiguas de concatenación y prepend no son el camino habitual en proyectos modernos. Prefiere módulos, outputs por package y bundling explícito.
Para packages donde otra herramienta genera JS:
JSON
Copiar {
"compilerOptions" : {
"composite" : true ,
"declaration" : true ,
"emitDeclarationOnly" : true ,
"isolatedDeclarations" : true
}
} TypeScript produce la API estática y el bundler produce JavaScript.
Exige que cada archivo exportado contenga suficiente información para generar declarations sin análisis global completo.
Aporta para builds paralelas y herramientas rápidas, pero puede requerir más anotaciones en exports públicos.
Texto
Copiar rootDir → estructura fuente esperada
outDir → destino del outputUn archivo fuera de rootDir que deba emitirse produce diagnósticos o una estructura inesperada. No uses rootDir para seleccionar archivos; include/files controlan el programa.
No lo compartas entre configuraciones incompatibles. Define rutas distintas para build, tests y packages cuando utilizan opciones diferentes.
Texto
Copiar package A → package B → package ALas references necesitan un grafo dirigido sin ciclos útiles. Un ciclo suele indicar:
Contratos mal ubicados.
Un package común faltante.
Dependencias de dominio invertidas.
Los tests pueden tener su propio proyecto:
JSON
Copiar {
"extends" : "./tsconfig.json" ,
"compilerOptions" : {
"noEmit" : true ,
"types" : [ "vitest/globals" ]
} ,
"include" : [ "src" , "tests" ]
} Evita contaminar la build de librería con globals y archivos de tests.
Texto
Copiar pack @app/domain
install tarball in fixture
run tsc and testsComprueba que exports, declarations y archivos publicados coincidan.
JSON
Copiar {
"files" : [ ] ,
"references" : [
{ "path" : "./packages/domain" } ,
{ "path" : "./packages/infrastructure" } ,
{ "path" : "./apps/api" }
]
}
Incluir todo el monorepo en un único tsconfig.
Referenciar un proyecto sin composite.
Importar directamente su carpeta src.
Usar paths que solo entiende el checker.
Publicar JS sin declarations alineadas.
Mezclar tests y globals en la build de librería.
Compartir un tsBuildInfo entre configs distintas.
Crear ciclos entre packages.
Asumir que una build exitosa representa el package publicado.
Project references dividen el grafo en proyectos compilables.
composite hace posible esa frontera.
tsc -b construye en orden e incrementalmente.
Declarations representan la API entre proyectos.
No importes source interno de otro package.
package exports deben coincidir con el output.
Tests y entornos pueden usar proyectos separados.
Un fixture consumidor valida el artefacto real.
¿Por qué importar ../package/src/internal.ts debilita una project reference?
Respuesta Porque salta la API declarada del package, acopla al consumidor a detalles internos y puede incluir source bajo una configuración distinta a la del proyecto propietario.
Diseño de APIs tipadas conecta inferencia, modelado y declarations en contratos fáciles de consumir y evolucionar.