42. Project References
TypeScript: Проектные Ссылки (Project References)
Заголовок раздела «TypeScript: Проектные Ссылки (Project References)»Привет, кодер! Сегодня мы погрузимся в мир больших проектов и узнаем, как TypeScript помогает нам управлять ими, как настоящий дирижер оркестром. Мы поговорим о “Проектных Ссылках” (Project References) — фиче, которая становится незаменимой, когда твой код разрастается до размеров целого города, а не просто уютного домика.
Представь, что у тебя не один, а несколько связанных проектов: общая библиотека компонентов UI, API-клиент, написанный на TypeScript, и несколько фронтенд-приложений, которые все это используют. Раньше, чтобы все это работало вместе, приходилось мудрить с npm link или постоянно пересобирать зависимости вручную. TypeScript Project References — это твой личный менеджер по сборке в таких сценариях. Он позволяет TypeScript-компилятору понять, как разные части твоего монорепозитория связаны между собой, и автоматически пересобирать только то, что нужно.
✨ Что такое Project References и зачем они нужны?
Заголовок раздела «✨ Что такое Project References и зачем они нужны?»Project References позволяют разбить один большой TypeScript-проект на множество маленьких, взаимосвязанных подпроектов. Каждый подпроект имеет свой собственный tsconfig.json. Когда ты указываешь один проект как “ссылку” для другого, TypeScript получает информацию о зависимостях между ними.
Основные преимущества:
- Ускорение сборки:
tsc -b(Build Mode) пересобирает только те проекты, которые изменились или от которых зависят изменившиеся проекты. Это значительно сокращает время сборки в больших кодовых базах. - Улучшенная изоляция: Каждый подпроект компилируется независимо, что помогает избежать проблем с глобальными типами и улучшает изоляцию модулей.
- Навигация в IDE: Редакторы кода (VS Code, WebStorm) лучше понимают связи между проектами, предоставляя более точную автодополнение, навигацию и рефакторинг между границами проектов.
- Монорепозитории: Идеально подходит для монорепозиториев, где множество пакетов живут в одном репозитории и зависят друг от друга.
Чтобы проект мог быть целью ссылки (то есть, на него ссылаются другие), он должен быть composite. Это как фундамент, на котором будут строиться другие здания.
🏗️ Основы: composite и references
Заголовок раздела «🏗️ Основы: composite и references»Давай представим, что у нас есть монорепозиторий со структурой:
my-monorepo/├── packages/│ ├── utils/ <-- Общие утилиты (composite)│ │ ├── src/│ │ │ └── index.ts│ │ └── tsconfig.json│ ├── api-client/ <-- Клиент для API, использует 'utils' (composite)│ │ ├── src/│ │ │ └── index.ts│ │ └── tsconfig.json│ └── web-app/ <-- Фронтенд-приложение, использует 'api-client'│ ├── src/│ │ └── main.ts│ └── tsconfig.json└── tsconfig.json <-- Корневой tsconfig для сборки всех проектовpackages/utils/tsconfig.json
Заголовок раздела «packages/utils/tsconfig.json»Это наш “фундамент”. Он должен быть composite.
{ "compilerOptions": { "composite": true, // Обязательно для проекта, на который ссылаются "outDir": "./dist", "rootDir": "./src", "declaration": true, // Генерируем .d.ts файлы для публичного API "strict": true, "module": "esnext", "target": "es2019", "esModuleInterop": true, "moduleResolution": "node" }, "include": ["src"]}export function formatString(text: string): string { return text.toUpperCase(); // Простая утилита для форматирования строки}
export function generateId(): string { return Math.random().toString(36).substring(2, 9); // Генерируем случайный ID}packages/api-client/tsconfig.json
Заголовок раздела «packages/api-client/tsconfig.json»Этот проект ссылается на utils. Он тоже будет composite, так как на него может ссылаться web-app.
{ "compilerOptions": { "composite": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "strict": true, "module": "esnext", "target": "es2019", "esModuleInterop": true, "moduleResolution": "node", // Очень важно: baseUrl и paths для корректного разрешения модулей "baseUrl": ".", "paths": { "@my-monorepo/utils": ["../utils/src"] // Ссылка на исходники utils } }, "include": ["src"], "references": [ // Указываем, что этот проект зависит от 'utils' { "path": "../utils" } ]}import { formatString, generateId } from '@my-monorepo/utils'; // Импорт из utils через paths
export interface User { id: string; name: string; email: string;}
export class ApiClient { private apiUrl: string;
constructor(apiUrl: string) { this.apiUrl = apiUrl; }
fetchUser(userId: string): User { console.log(`Fetching user ${formatString(userId)} from ${this.apiUrl}`); return { id: generateId(), name: `User ${userId}`, email: `${userId}@example.com`, }; }}Обрати внимание на paths в api-client/tsconfig.json. Это позволяет нам импортировать модули из utils с использованием не относительных путей, а красивых алиасов, как если бы это был npm-пакет.
packages/web-app/tsconfig.json
Заголовок раздела «packages/web-app/tsconfig.json»Этот проект использует api-client и не является composite, так как от него никто не зависит.
{ "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "strict": true, "module": "esnext", "target": "es2019", "esModuleInterop": true, "moduleResolution": "node", "jsx": "react-jsx", // Если используем React "baseUrl": ".", "paths": { "@my-monorepo/api-client": ["../api-client/src"], // Ссылка на исходники api-client "@my-monorepo/utils": ["../utils/src"] // Также можем напрямую сослаться на utils } }, "include": ["src"], "references": [ // Указываем, что web-app зависит от api-client { "path": "../api-client" } // Можно также добавить ссылку на utils, если бы web-app его напрямую использовал // { "path": "../utils" } ]}import { ApiClient, User } from '@my-monorepo/api-client';import { formatString } from '@my-monorepo/utils'; // Прямое использование utils
const client = new ApiClient("https://api.example.com");
function displayUser(user: User): void { console.log(`Displaying user: ${formatString(user.name)} (${user.email})`);}
const fetchedUser = client.fetchUser("john-doe-123");displayUser(fetchedUser);
// Пример, демонстрирующий, что можно импортировать и из utils напрямуюconsole.log(`Formatted message: ${formatString("hello from web app")}`);Корневой tsconfig.json
Заголовок раздела «Корневой tsconfig.json»Этот файл нужен для того, чтобы запустить сборку всех проектов из корня монорепозитория с помощью tsc -b.
{ "files": [], // Не компилируем файлы напрямую "references": [ // Указываем все проекты, которые нужно собрать { "path": "packages/utils" }, { "path": "packages/api-client" }, { "path": "packages/web-app" } ]}🚀 Запуск сборки
Заголовок раздела «🚀 Запуск сборки»Теперь, чтобы собрать весь монорепозиторий, достаточно запустить одну команду из корня:
tsc -bTypeScript проанализирует my-monorepo/tsconfig.json, найдет все ссылки, определит порядок зависимостей (utils -> api-client -> web-app) и соберет их. Если ты изменишь файл в packages/utils, а затем снова запустишь tsc -b, TypeScript пересоберет только utils, затем api-client (потому что utils изменился) и, наконец, web-app (потому что api-client изменился). Это магия!
❌ Типичные ошибки и их решения
Заголовок раздела «❌ Типичные ошибки и их решения»-
Забыли
composite: true:- Ошибка:
Error: Project 'path/to/project/tsconfig.json' cannot be referenced without setting 'composite: true'. - Решение: Добавь
"composite": trueвcompilerOptionsкаждого проекта, на который ссылаются другие проекты.
- Ошибка:
-
Неправильные
pathsилиbaseUrl:- Ошибка:
Module not foundпри импорте из ссылочного проекта. - Решение: Убедись, что
baseUrlустановлен корректно (обычно.для текущегоtsconfig.json) иpathsуказывают на исходные файлы (чаще всегоsrc) ссылочного проекта, а не на егоdist. Пути вpathsдолжны быть относительныbaseUrl.
- Ошибка:
-
Не используешь
tsc -b:- Ошибка: Изменения в зависимостях не отражаются в конечном приложении, или
tscвыдает ошибки, что не может найти модули. - Решение: Всегда используй
tsc -bдля сборки проектов с Project References. Обычныйtscне будет учитывать связи между проектами.
- Ошибка: Изменения в зависимостях не отражаются в конечном приложении, или
-
Циклические зависимости:
- Ошибка: TypeScript выдаст ошибку, если обнаружит циклическую зависимость между проектами (например,
Aссылается наB, аBссылается наA). - Решение: Рефакторинг. Разбей проекты так, чтобы зависимости были однонаправленными. Возможно, выдели общую часть в третий, независимый проект.
- Ошибка: TypeScript выдаст ошибку, если обнаружит циклическую зависимость между проектами (например,
### 🎯 Практика
Заголовок раздела «### 🎯 Практика»Создай следующий монорепозиторий и настрой Project References:
my-advanced-project/├── packages/│ ├── logger/ <-- Проект для логирования (composite)│ │ ├── src/│ │ │ └── index.ts│ │ └── tsconfig.json│ ├── data-validator/ <-- Проект для валидации данных (composite), использует 'logger'│ │ ├── src/│ │ │ └── index.ts│ │ └── tsconfig.json│ └── backend-service/ <-- Бэкенд-сервис, использует 'logger' и 'data-validator'│ ├── src/│ │ └── server.ts│ └── tsconfig.json└── tsconfig.json <-- Корневой tsconfigЗадания:
-
Настрой
tsconfig.jsonдляpackages/logger:- Сделай его
composite. - В
src/index.tsсоздай простую функциюlog(message: string, level: 'info' | 'warn' | 'error').
- Сделай его
-
Настрой
tsconfig.jsonдляpackages/data-validator:- Сделай его
composite. - Добавь ссылку на
packages/logger. - Настрой
paths, чтобы импортироватьlogизloggerкак@my-project/logger. - В
src/index.tsсоздай функциюisValidEmail(email: string): boolean, которая используетlogдля вывода информации о валидации.
- Сделай его
-
Настрой
tsconfig.jsonдляpackages/backend-service:- Добавь ссылки на
packages/loggerиpackages/data-validator. - Настрой
pathsдля обоих проектов (например,@my-project/loggerи@my-project/data-validator). - В
src/server.tsиспользуй функцииlogиisValidEmailдля обработки какого-либо “запроса”.
- Добавь ссылки на
-
Создай корневой
tsconfig.json:- Включи в него ссылки на все три пакета.
-
Проверь сборку:
- Запусти
tsc -bиз корня проекта. - Измени что-нибудь в
packages/logger/src/index.tsи снова запустиtsc -b. Убедись, что пересобираются только изменившиеся проекты и их зависимости.
- Запусти
💡 Совет
Заголовок раздела «💡 Совет»Всегда стремись к тому, чтобы твои composite проекты экспортировали только тот код, который действительно является частью их публичного API. Используй declaration: true для генерации .d.ts файлов, которые будут использоваться ссылающимися проектами. Это помогает сохранить чистоту зависимостей и предотвратить случайное использование внутренних реализаций. Имя алиасов в paths (например, @my-monorepo/utils) лучше всего делать уникальными и соответствующими именам пакетов, чтобы избежать конфликтов и улучшить читаемость.