{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["src/**/*"],
"references": [
{ "path": "../shared-utils" }
]
}Cei mai mulți developeri sar peste Project References și trântesc doar trei aliasuri în paths. Merge brici local în VS Code, dar pe CI ajungi să recompilezi tot repo-ul la fiecare mic commit.
Am pățit-o acum doi ani pe un proiect cu 14 pachete interne și vreo 60k linii de cod. Făceai o modificare într-un helper de date și așteptai 4 minute pe GitHub Actions doar pentru typecheck.
Capcana clasică: doar paths în root
Când pui paths: { "@app/ui": ["packages/ui/src"] } în root tsconfig, îi spui compilatorului unde găsește fișierele sursă pentru autocompletion. Atât.
Problema? TypeScript tratează tot repo-ul ca pe un singur program monolitic uriaș. Dacă app-web depinde de ui, iar ui depinde de utils, compilatorul parsează AST-ul pentru toate de fiecare dată când rulezi tsc. Nu există caching între pachete, nu există limite de domeniu, iar consumul de RAM sare lejer de 2GB la proiecte medii.
Soluția curată: composite și Project References
Pentru a împărți monorepo-ul în bucăți independente, fiecare pachet trebuie să devină un sub-proiect de sine stătător. Asta înseamnă să adaugi "composite": true în tsconfig.json-ul fiecărui package.
Ce face composite de fapt sub capotă:
- Forțează
declaration: true— generează fișiere.d.tspentru tipuri. - Activează automat
incremental: trueși creează fișierul.tsbuildinfo. - Obligă TypeScript să valideze că pachetul respectiv poate fi compilat independent, fără să cunoască restul codului din monorepo.
Apoi, în pachetul care consumă (de exemplu apps/web), nu mai folosești paths spre .ts direct. Adaugi pachetul în array-ul references:
{
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/utils" }
]
}
Când rulezi tsc --build (sau prescurtat tsc -b), TypeScript știe ordinea topologică a dependențelor. Dacă utils nu s-a schimbat deloc de la ultimul build, folosește direct fișierele .d.ts și .tsbuildinfo generate anterior și trece instant mai departe.
Caching inteligent cu incremental pe CI
Dacă vrei viteze maxime pe CI, salvează fișierele .tsbuildinfo în cache-ul runner-ului (GitHub Actions cache, Nx Cloud sau Turbo cache).
Specifică mereu o cale clară pentru ele:
"tsBuildInfoFile": "./dist/.tsbuildinfo"
La al doilea build pe același branch, dacă ai schimbat un singur fișier din apps/web, tsc -b verifică doar fișierul respectiv. La noi, timpul pe CI a scăzut de la 4 minute și 10 secunde la sub 35 de secunde.
Trade-off-uri: unde doare treaba asta?
Nu e totul lapte și miere, altfel toată lumea ar fi folosit asta din prima zi:
- Configurare migăloasă: Fiecare pachet are nevoie de un
tsconfig.jsonextins dintr-o bază comună, plus untsconfig.build.jsonuneori, dacă ai teste. - Erori stricte de export:
compositete obligă să nu uiți tipuri neexportate. Dacă o funcție publică returnează un tip intern privat,tscva urla direct. - Fără dependențe circulare: Dacă package A importă din B și B din A,
tsc -bva refuza să pornească. Ceea ce tehnic e un lucru bun, dar când migrezi un codebase vechi, o să ai de refactorizat serios.
Voi cum aveți împărțit monorepo-ul? Rulați tsc -b la nivel de root sau lăsați Turborepo/Nx să execute comenzi tsc izolate în paralel pentru fiecare pachet?