У монорепозиторіях із десятками внутрішніх бібліотек та застосунків наївний підхід до налаштування TypeScript через єдиний кореневий tsconfig.json призводить до архітектурного глухого кута:
ts.Program).JavaScript heap out of memory).Для вирішення проблеми горизонтального масштабування в TypeScript розроблена технологія Project References (Посилання на проєкти).
Project References базується на трьох фундаментальних принципах:
"composite": true, що гарантує сувору ізоляцію меж модуля та обов'язкову генерацію декларацій типів.tsc --build (tsc -b): Компілятор самостійно будує граф залежностей проєктів і компілює їх у правильному порядку, пропускаючи вже зібрані модулі..tsbuildinfo: Якщо зміна у файлі пакета не вплинула на його публічний API (публічний файл .d.ts залишився ідентичним), TypeScript не перекомпільовує залежні вищі пакети.В архітектурі Project References кожен пакет компілюється ізольовано, надаючи вищим пакетам лише свої скомпільовані інтерфейси типів (.d.ts), а не сирі вихідні файли:
+-------------------------------------------------------------------------+
| ТОПОЛОГІЧНИЙ ГРАФ ЗБІРКИ МОНОРЕПОЗИТОРІЮ |
+-------------------------------------------------------------------------+
| ROOT tsconfig.json |
| "references": [ { "path": "apps/web" }, { "path": "packages/ui" } ] |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| apps/web/tsconfig.json |
| "composite": true |
| "references": [ { "path": "../../packages/ui" } ] |
| (Читає ТІЛЬКИ скомпільовані .d.ts із packages/ui/dist) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| packages/ui/tsconfig.json |
| "composite": true |
| "references": [ { "path": "../core" } ] |
| (Генерує dist/index.d.ts та записує кеш .tsbuildinfo) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| packages/core/tsconfig.json (Базовий листок графа) |
| "composite": true |
| "references": [] |
+-------------------------------------------------------------------------+Коли розробник змінює реалізацію приватної функції в packages/core без зміни сигнатури експорту:
tsc -b компілює packages/core.packages/core/dist/index.d.ts залишається незмінним.packages/core/.tsbuildinfo і пропускає повторну перевірку типів для packages/ui та apps/web.tsc --build скорочує час повної перевірки типів у CI з 15 хвилин до 90 секунд завдяки повторному використанню файлів .tsbuildinfo.@company/ui миттєво віддає скомпільовані .d.ts в усі застосунки без необхідності запускати важкі watcher-процеси для кожного залежного пакета.composite: true#Увімкнення прапорця composite: true повідомляє компілятору, що цей проєкт є частиною більшого графа залежностей.
Обов'язкові вимоги для композитного проєкту:
declaration: true: проєкт зобов'язаний генерувати файли типів, оскільки залежні проєкти читатимуть саме їх.rootDir: має бути строго зафіксований (зазвичай ./src). Заборонено імпортувати файли за межами цієї директорії без посилань на проєкти.include або явно перераховані у files.{
"extends": "../../packages/tsconfig/base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}Вищий пакет оголошує посилання на підпроєкти в секції 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/**/*"]
}У корені монорепозиторію створюється керівний tsconfig.json без власних вихідних файлів (files: []), який агрегує всі вузли графа посилань:
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" },
{ "path": "./apps/docs" }
]
}Запуск інкрементальної збірки всього монорепозиторію виконується єдиною командою:
pnpm exec tsc --build --verboseКомпілятор будує топологічний порядок і виводить журнал виконання:
[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'...Зчитування секцій references та побудова ациклічного графа залежностей пакетів.
Порівняння контрольних сум вихідних файлів та хешів d.ts із записами у файлах .tsbuildinfo.
Компіляція змінених пакетів із генерацією свіжих .d.ts та карт декларацій .d.ts.map.
Якщо сигнатура d.ts не змінилася, компіляція залежних пакетів миттєво пропускається.
У монорепозиторіях поширеною проблемою є розсинхронізація: розробник додає залежність у package.json, але забуває додати посилання в tsconfig.json references. Нижче наведено production-скрипт валідації та автосинхронізації:
import fs from 'node:fs';
import path from 'node:path';
import { globSync } from 'glob';
console.log('🔍 Перевірка консистентності Project References у монорепозиторії...');
// Пошук усіх пакетів та застосунків
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(`⚠️ У пакеті ${pkgName} відсутні tsconfig references:`, missingRefs);
hasDiscrepancy = true;
// Автоматичне виправлення
tsconfig.references = [
...(tsconfig.references || []),
...missingRefs.map((refPath) => ({ path: refPath })),
];
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n');
console.log(`✅ Додано відсутні посилання в ${tsconfigPath}`);
}
}
if (hasDiscrepancy) {
console.log('🔄 Файли tsconfig.json успішно синхронізовано.');
} else {
console.log('✨ Усі Project References ідеально синхронізовані з package.json.');
}Налаштування співставлення шляхів "paths": { "@repo/ui": ["../../packages/ui/src"] } змушує компілятор включати вихідний код стороннього пакета в програму поточного застосунку. У результаті пакет компілюється стільки разів, скільки застосунків його імпортують, що знищує кеш і призводить до вибухового споживання пам'яті.
// ❌ Антипатерн: пряме накладання шляхів на чужі вихідні файли
{
"compilerOptions": {
"paths": {
"@repo/ui": ["../../packages/ui/src/index.ts"]
}
}
}
// ✅ Правильне рішення: посилання на проєкти та імпорт через скомпільовані .d.ts
{
"references": [
{ "path": "../../packages/ui" }
]
}Якщо проєкт A посилається на проєкт B, а проєкт B прямо чи опосередковано посилається на проєкт A, компілятор tsc --build аварійно завершиться з помилкою: Project references may not form a circular graph.
Рішення: винести спільні інтерфейси в незалежний низькорівневий пакет packages/contracts або packages/types, що розміщується в основі графа.
Якщо вручну видалити каталог dist із файлами .d.ts, але залишити файл .tsbuildinfo (або навпаки), tsc -b вважатиме проєкт актуальним на основі наявного кешу і пропустить генерацію типів, що призведе до помилок Cannot find module у споживачів.
Рішення: очищати артефакти строго через команду tsc --build --clean.
Увімкнути 'composite': true, 'declaration': true та налаштувати outDir у tsconfig кожного пакета packages/*.
Видалити відносні шляхи до чужих src із секції paths, замінивши їх стандартним резолвінгом пакетів.
У кожному залежному пакеті додати масив references із відносними шляхами до залежностей.
У кореневому tsconfig.json вказати files: [] та перелічити всі пакети в references.
Запустити pnpm exec tsc --build --verbose і переконатися в коректній топологічній збірці графа.