Project references: tsc --build, composite и инкрементальный граф сборки
Project references разбивают монорепо на `tsconfig`-проекты, которые `tsc --build` компилирует по графу зависимостей. `composite` (форсирует `declaration`) включает ссылки и кэш `.tsbuildinfo`. Ловушки: устаревший buildinfo, циклические ссылки, ошибка «must have composite».
Монорепо на 200k строк проверяло типы 90 секунд на каждом сохранении, потому что один большой tsconfig перепроверял все пакеты при любом изменении файла. Кто-то разбил его на проекты со ссылками и добавил composite: true — и однострочная правка в пакете web теперь перепроверяет только web, переиспользуя кэшированный вывод core и ui. Первая сборка записала .tsbuildinfo на каждый проект; последующие сборки читают его и пропускают неизменённые проекты. Но миграция началась со стены ошибок error TS6306: Referenced project must have setting "composite": true.
Форма: проект, указывающий на свои зависимости
У каждого пакета свой tsconfig.json. Пакет, зависящий от другого, перечисляет его в references, и проект, на который ссылаются, должен быть composite:
// packages/web/tsconfig.json
{
"compilerOptions": { "composite": true, "outDir": "dist" },
"references": [
{ "path": "../core" }, // web зависит от core
{ "path": "../ui" } // и от ui
]
}// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true, // ОБЯЗАТЕЛЬНО, чтобы на него ссылались
"declaration": true, // composite это подразумевает; типы — это межпроектный контракт
"outDir": "dist"
}
}composite: true включает три вещи: форсирует declaration: true (чтобы проект генерировал .d.ts — интерфейс, который другие проекты потребляют вместо исходника), форсирует инкрементальное поведение с .tsbuildinfo и требует, чтобы каждый входной файл был покрыт include/files (чтобы сборка надёжно хешировала входы).
▸Почему это работает
Почему composite требует declaration? Потому что весь смысл проекта-со-ссылкой в том, что нижестоящие проекты не перечитывают его исходник — они читают его сгенерированный .d.ts. .d.ts — это межпроектный контракт типов. Без declaration emit web не на что было бы проверять типы, кроме сырых .ts из core, что разрушает изоляцию (и скорость). Поэтому composite и declaration неразделимы: проект-со-ссылкой обязан публиковать свои типы.
tsc --build обходит граф ссылок
Обычная сборка tsc проекта со ссылками либо упадёт, либо проигнорирует их. Нужно использовать режим сборки: tsc --build (или tsc -b). Режим сборки читает граф ссылок, топологически сортирует его и собирает зависимости перед зависящими — и пропускает любой проект, чьи входы не изменились с момента его .tsbuildinfo:
tsc --build packages/web # собирает core, потом ui, потом web — в порядке зависимостей
tsc --build --verbose # печатает, какие проекты актуальны, а какие пересобраны
tsc --build --clean # удаляет все выводы + .tsbuildinfo (лекарство от устаревшего кэша)
tsc --build --force # игнорирует .tsbuildinfo, пересобирает всёИнкрементальность и .tsbuildinfo
Прежде чем смириться с 90-секундной проверкой типов на каждом сохранении, спроси себя: знает ли твой tsconfig, какие файлы принадлежат каждому пакету? Если нет, tsc каждый раз хеширует всё заново.
.tsbuildinfo — это кэш хешей файлов и сигнатур. При пересборке tsc -b сравнивает текущие входы с кэшированным состоянием; если входы проекта (и .d.ts его зависимостей) не изменились, он актуален и пропускается целиком. Важно: проекту нужно пересобирать зависящего, только когда его публичный .d.ts реально изменился — внутренняя правка, не меняющая генерируемые типы, оставляет зависящих в кэше (оптимизация по «сигнатуре»).
Именно поэтому редакторы шустрее: каждый проект-со-ссылкой проверяется изолированно, так что языковой сервер не переанализирует всё монорепо на каждом нажатии клавиши.
Ловушки
error TS6306: Referenced project must have setting "composite": true — ты сослался на проект, который не composite. Добавь composite: true в тот, на который ссылаются (частая путаница: его добавляют ссылающемуся).
Устаревший .tsbuildinfo — если изменить что-то, что кэш не отслеживает (апгрейд компилятора с особенностью, ручная правка dist), сборка может ошибочно счесть проект актуальным. Лекарство: tsc -b --clean, затем пересборка, или --force.
Циклические ссылки — A ссылается на B, а B на A — отвергается (TS6202); DAG сборки должен быть ацикличным. Разорви цикл, вынеся общие типы в третий лист-проект, от которого зависят оба.
Path mapping между проектами — алиасы paths по-прежнему не переписываются при emit (урок 02), поэтому межпроектные импорты должны идти через настоящий entry/exports пакета, а не через алиас paths, который понимает только проверщик типов.
Все четыре ловушки объединяет паттерн: ошибка указывает на проект, на который ссылаются, а не на ссылающийся; кэш — это файл, который всегда можно удалить; циклы требуют нового листа для разрыва; алиасы — фикция проверщика типов, которая не должна просачиваться в emit.
Расставь, что делает `tsc --build packages/web` для графа web → ui → core и ui → core:
- 1 Читает references у web, затем транзитивно собирает ui и core в граф сборки
- 2 Топологически сортирует: у core нет зависимостей, значит он собирается первым
- 3 Для каждого проекта хеширует входы против .tsbuildinfo; пересобирает только при изменении, иначе пропускает
- 4 Собирает зависящих после их зависимостей: core → ui → web, генерируя .js + .d.ts
Ты запускаешь `tsc -b` и получаешь TS6306: Referenced project 'packages/core' must have setting 'composite': true. Куда добавить composite?
- 01Что делает `composite: true` и почему он форсирует `declaration`?
- 02Что делает `tsc --build`, чего не делает обычный `tsc`, и как он использует граф ссылок?
- 03Назови три классических провала project references и их фиксы.
Теперь ты умеешь структурировать монорепо для быстрых изолированных сборок: references объявляют граф, composite (форсирующий declaration) делает проект потребляемым, tsc -b упорядочивает и кэширует сборку через .tsbuildinfo, и ты знаешь, как читать TS6306, чистить устаревшие кэши и разрывать циклы. Дальше — build tooling: ссылки организуют собственную работу tsc, но большинство проектов отдают emit в esbuild/swc/babel; мы увидим, почему эти транспиляторы не проверяют типы, почему тогда isolatedModules обязателен и как tsc --noEmit становится CI-гейтом типов рядом с бандлером. Теперь, когда видишь монорепо с единственным tsconfig, перепроверяющим 200k строк на каждом сохранении, ты знаешь фикс: разбить на проекты-со-ссылками, добавить composite: true каждому листу и запускать tsc -b вместо tsc.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.