Монорепозиторий с TypeScript: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Монорепозиторий с TypeScript: организация кода на практике

В современной веб-разработке, особенно при работе с TypeScript, разработчики часто сталкиваются с дилеммой: хранить весь код в одном репозитории (монорепозиторий) или разделять его на множество маленьких репозиториев (полирепозиторий). Для средних и крупных проектов, где важна переиспользуемость кода и единая конфигурация, монорепозиторий становится стандартом де-факто. В этой статье мы разберем, как правильно организовать монорепозиторий на TypeScript, начиная от базовой структуры и заканчивая продвинутыми техниками.

Что такое монорепозиторий и зачем он нужен?

Монорепозиторий — это подход, при котором код нескольких проектов (пакетов, библиотек, приложений) хранится в одном репозитории. В контексте TypeScript это особенно удобно, так как позволяет:

  • Переиспользовать типы и интерфейсы между разными частями системы без публикации в npm.
  • Упростить рефакторинг: изменение в одном пакете автоматически валидируется во всех зависимых.
  • Иметь единую конфигурацию для линтера, форматтера и сборщика.
  • Упростить CI/CD: один репозиторий — один пайплайн.

Среди популярных инструментов для работы с монорепозиториями в TypeScript выделяются Nx, Turborepo и npm/yarn/pnpm workspaces. Мы рассмотрим подход с использованием pnpm workspaces и TypeScript Project References, так как это минимальный и гибкий набор.

Структура монорепозитория: базовая настройка

Давайте создадим типовую структуру монорепозитория для TypeScript-проекта. Предположим, у нас есть три пакета: core (общие типы и утилиты), server (бэкенд) и web (фронтенд).

1. Корневая конфигурация

Начнем с корня проекта. Создадим файл package.json с workspace:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "tsc -b",
    "clean": "tsc -b --clean",
    "lint": "eslint packages/*/src"
  }
}

Затем настроим корневой tsconfig.json. Используем Project References — это ключевая фича TypeScript для монорепозиториев:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": ".",
    "skipLibCheck": true
  },
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/server" },
    { "path": "./packages/web" }
  ]
}

Важно: опция composite: true обязательна для пакетов, на которые ссылаются другие пакеты. Она включает инкрементальную сборку и генерацию .d.ts файлов.

2. Настройка пакета core

Перейдем в packages/core. Создадим package.json:

{
  "name": "@my/core",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc -b"
  }
}

И tsconfig.json для этого пакета:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

Создадим простой модуль src/index.ts:

export interface User {
  id: string;
  name: string;
  email: string;
}

export function greet(user: User): string {
  return `Hello, ${user.name}!`;
}

Теперь пакет @my/core готов к использованию.

Организация зависимостей между пакетами

Самое интересное — это настройка кросс-пакетных зависимостей. Допустим, наш server использует типы из core.

1. Настройка пакета server

Создадим packages/server/package.json:

{
  "name": "@my/server",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "dependencies": {
    "@my/core": "workspace:*"
  },
  "scripts": {
    "build": "tsc -b"
  }
}

Обратите внимание на "workspace:*". В pnpm это означает "использовать локальную версию пакета из workspace". В yarn аналогично "*" или "^1.0.0".

packages/server/tsconfig.json:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../core" }
  ]
}

Теперь в server/src/index.ts мы можем импортировать типы из core:

import { User, greet } from '@my/core';

const user: User = { id: '1', name: 'Alice', email: 'alice@example.com' };
console.log(greet(user));

При запуске pnpm build из корня, TypeScript сначала соберет core, а затем server — порядок гарантирован за счет Project References.

Продвинутые практики: Nx и Turborepo

Для больших проектов ручная настройка может стать обременительной. Здесь на помощь приходят инструменты вроде Nx

Блоги

Рекомендаций по книгам