Монорепозиторий с TypeScript: организация кода на практике
Современная фронтенд- и бэкенд-разработка всё чаще сталкивается с проблемой управления несколькими взаимосвязанными проектами. Когда количество пакетов, библиотек и приложений растёт, традиционный подход с отдельными репозиториями (multi-repo) начинает тормозить разработку: дублирование кода, сложности с синхронизацией версий и долгие циклы ревью. Решение — монорепозиторий (monorepo), особенно в связке с TypeScript. В этой статье мы разберём практические аспекты организации кода в монорепозитории на TypeScript: от выбора инструментов до конкретных примеров структуры и настройки.
Что такое монорепозиторий и зачем он нужен TypeScript-проектам?
Монорепозиторий — это подход, при котором код нескольких проектов (пакетов, библиотек, сервисов) хранится в одном Git-репозитории. Для TypeScript это особенно актуально, так как язык строгой типизации позволяет легко переиспользовать общие типы и интерфейсы между разными частями системы.
Основные преимущества монорепозитория с TypeScript:
- Единая версия зависимостей — исключается ситуация, когда в разных проектах используются разные версии одной библиотеки.
- Переиспользование кода — общие типы, утилиты и конфигурации легко доступны всем пакетам.
- Атомарные коммиты — изменения, затрагивающие несколько пакетов, можно коммитить вместе.
- Упрощение CI/CD — один пайплайн для сборки и тестирования всех частей.
Однако монорепозиторий требует дисциплины в организации кода. Без правильной структуры он превращается в "свалку". TypeScript помогает навести порядок благодаря строгой типизации и модульной системе.
Выбор инструмента для управления монорепозиторием
На рынке есть несколько популярных инструментов для работы с монорепозиториями. Рассмотрим два самых распространённых для TypeScript: Nx и Turborepo.
Nx — мощная система с поддержкой монорепозиториев, генерацией кода и продвинутым кэшированием. Она отлично подходит для больших проектов с множеством пакетов.
Turborepo — более лёгкий и быстрый инструмент от Vercel, ориентированный на простоту. Он идеален для средних проектов и команд, которые хотят минимальную конфигурацию.
Пример базовой настройки с Turborepo:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}
Для TypeScript обязательна настройка tsconfig.json с поддержкой project references. Это позволяет компилятору понимать зависимости между пакетами.
// tsconfig.base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Практическая организация структуры монорепозитория
Ключевой принцип — разделение кода на логические слои. Рассмотрим типовую структуру для TypeScript-монорепозитория с веб-приложением и бэкендом:
monorepo/
├── apps/
│ ├── web/ # Frontend приложение (React, Vue, etc.)
│ └── api/ # Backend сервис (Express, NestJS)
├── packages/
│ ├── shared/ # Общие типы и интерфейсы
│ ├── utils/ # Утилитарные функции
│ ├── ui/ # UI-компоненты (если используется)
│ └── config/ # Общие конфигурации (eslint, tsconfig)
├── tools/ # Скрипты и инструменты для CI/CD
├── turbo.json
├── package.json
└── tsconfig.base.json
Каждый пакет в packages/ должен иметь свой package.json и tsconfig.json, наследующий от базового. Например, для пакета shared:
// packages/shared/package.json
{
"name": "@monorepo/shared",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"typescript": "^5.0.0"
}
}
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Пример общих типов в пакете shared:
// packages/shared/src/types/user.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
}
export type CreateUserDto = Omit<User, 'id'>;
Теперь эти типы можно импортировать в любом приложении:
// apps/api/src/routes/user.ts
import { User, CreateUserDto } from '@monorepo/shared';
export function createUser(data: CreateUserDto): User {
// логика создания пользователя
return { id: generateId(), ...data };
}
Управление зависимостями и сборка
В монорепозитории TypeScript важно правильно настроить зависимости между пакетами. Используйте workspace-ссылки (workspace protocol) в package.json корневого проекта:
// package.json (корневой)
{
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
Для локальной разработки используйте npm link или workspace-зависимости. Например, в apps/web/package.json:
{
"