В монорепозиториях с десятками внутренних библиотек и приложений наивный подход к настройке 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.Включить 'composite': true, 'declaration': true и настроить outDir в tsconfig каждого пакета packages/*.
Удалить относительные пути к чужим src из секции paths, заменив их на стандартный резолвинг пакетов.
В каждом зависимом пакете добавить массив references с относительными путями к зависимостям.
В корневом tsconfig.json указать files: [] и перечислить все пакеты в references.
Запустить pnpm exec tsc --build --verbose и убедиться в корректной топологической сборке графа.
Как механизм Project References и кэш .tsbuildinfo предотвращают каскадную перекомпиляцию всего монорепозитория при изменении кода в базовом пакете?
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.');
}Почему связывание внутренних пакетов монорепозитория через compilerOptions.paths является критическим антипаттерном?
Настройка сопоставления путей "paths": { "@repo/ui": ["../../packages/ui/src"] } заставляет компилятор встраивать исходный код чужого пакета в программу текущего приложения. В результате пакет компилируется столько раз, сколько приложений его импортируют, что уничтожает кэш и приводит к взрывному росту потребления RAM.
// ❌ Антипаттерн: прямое наложение путей на чужие исходники
{
"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.