{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}Am văzut prea multe monorepo-uri de TS care merg în genunchi doar pentru că cineva a dat copy-paste la un tsconfig.json din primul tutorial găsit pe net. Dacă ai mai mult de 3-4 pachete interne și nu folosești Project References, practic îți pedepsești echipa cu timpi de build uriași și erori de tipuri care apar aleatoriu doar în CI.
La un proiect trecut, cu 14 sub-proiecte (un mix de backend, frontend și pachete de utilitare), build-ul local dura aproape un minut. După ce am configurat corect composite și references, am scăzut timpul de build la sub 12 secunde pentru rulări incrementale. Cum faci asta fără să-ți prinzi urechile în erori de genul "Cannot prepend project..."?
Problema cu simplele paths
Cea mai comună greșeală este să folosești doar proprietatea paths din compilerOptions pentru a rezolva importurile dintre pachete. Pare că merge ușor: scrii @monorepo/shared și IDE-ul e fericit.
Dar aici e capcana. Când rulezi tsc în aplicația principală, compilatorul va re-evalua și va compila din nou toate fișierele din pachetul shared, ca și cum ar fi parte din codul tău curent. Nu există cache, nu există separare reală. Dacă ai 5 aplicații care importă shared, acel cod se compilează de 5 ori. E o risipă stupidă de resurse.
Soluția: composite: true și Project References
Ca să rezolvăm asta, trebuie să tratăm fiecare pachet ca pe o unitate de build independentă. Aici intervin cele trei flag-uri de bază din tsconfig-ul pachetului partajat:
"composite": true: Îi spune lui TS că acest proiect poate fi parte dintr-un build mai mare. Activează automat generarea de fișiere.d.tsși build-ul incremental."declarationMap": true: Esențial pentru DX. Îți permite să dai "Go to Definition" direct în codul.tsdin shared, nu în fișierele compilate de tipuri.
Apoi, în aplicația ta principală (să zicem apps/api), în loc să te bazezi doar pe alias-uri, adaugi o referință directă către folderul pachetului dependent. TypeScript va ști acum să verifice dacă pachetul shared s-a schimbat înainte de a compila aplicația. Dacă nu s-a schimbat nimic în el, folosește direct output-ul deja compilat din cache (fișierele .tsbuildinfo).
Trade-off-ul sincer: de ce doare configurația asta?
Sună ideal, dar există și probleme de care m-am lovit des în producție:
- Poluarea cu
.tsbuildinfoși.d.ts: Folderele tale de cod vor fi pline de fișiere de build intermediare. Trebuie neapărat să le adaugi în.gitignoreca să nu poluezi repository-ul. - Tooling-ul terț: Nu toate bundlere-urile (Vite, Webpack, Esbuild) înțeleg nativ Project References direct din cutie. De multe ori ai nevoie de plugin-uri suplimentare ca să nu fii obligat să rulezi
tsc --buildmanual înainte de a porni serverul de dev. - Restart-ul serverului de TS: În VS Code, uneori tipurile nu se propagă instant între pachete după o modificare majoră în shared. Te trezești că trebuie să rulezi comanda "Restart TS Server" destul de des.
La final de zi, merită efortul? Pentru orice monorepo care depășește 10k linii de cod, răspunsul este un "da" categoric. Economisești timp de CI și nervi la build-uri locale.
Voi cum gestionați asta? Mergeți pe Project References sau ați lăsat totul în seama unui build tool mai deștept ca Turborepo sau Nx care face cache la nivel de fișiere?