Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Монорепозитории на Turborepo: как организовать шаринг UI-библиотеки и TypeScript-типов

Тимофей Ищенко

Коротко: Разбираем архитектуру монорепозитория на Turborepo: настройка Just-in-Time пакетов, шаринг UI-компонентов, TypeScript-типов и конфигураций без лишнего оверхеда.

Когда кодовая база разрастается до нескольких взаимосвязанных фронтенд-приложений (например, публичный портал на Next.js, панель администратора на Vite и мобильное веб-приложение), разработчики неизбежно сталкиваются с дублированием логики. Начинается ручное копирование интерфейсов DTO, контрактов API, валидаторов и типовых UI-компонентов.

Попытка вынести общий код в отдельные внешние npm-пакеты часто оборачивается инфраструктурным оверхедом: любое мелкое изменение в кнопке или схеме данных требует сборки пакета, публикации новой версии в реестр, обновления зависимостей в целевых репозиториях и разбора конфликтующих версий.

Решением этой проблемы становится монорепозиторий. Инструмент Turborepo в связке с механизмом рабочих пространств (workspaces) пакетных менеджеров объединяет приложения и общие модули в одном репозитории, обеспечивая прозрачный шаринг кода и высокую скорость работы за счет интеллектуального кэширования задач.


Базовая анатомия Turborepo-монорепозитория

Turborepo не заменяет пакетный менеджер, а выполняет роль легковесного оркестратора задач: строит граф зависимостей между пакетами, распараллеливает выполнение скриптов и кэширует артефакты сборки.

Роль пакетного менеджера и Workspaces

Управление зависимостями и создание символических ссылок (symlinks) между локальными пакетами делегируется пакетному менеджеру (pnpm, Yarn, npm или Bun).

Обязательный фундамент архитектуры:

  • Единый lockfile (pnpm-lock.yaml, yarn.lock или package-lock.json) — фиксирует дерево внешних зависимостей и обеспечивает детерминированность сборок.
  • Корневой package.json — определяет глобальные dev-зависимости (включая CLI turbo) и общие скрипты запуска.
  • Конфигурационный файл turbo.json — задает пайплайны выполнения задач и условия кэширования.
  • Манифест рабочих пространств (например, pnpm-workspace.yaml).

Структура директорий

Классический подход к организации директорий разделяет конечные сервисы и переиспользуемые модули:

├── apps/
│   ├── web/               # Приложение Next.js (SSR / App Router)
│   └── admin/             # Панель управления на Vite / React SPA
├── packages/
│   ├── ui/                # Внутренняя UI-библиотека
│   ├── types/             # Общие интерфейсы, DTO и схемы API
│   ├── typescript-config/ # Базовые конфигурации tsconfig
│   └── eslint-config/     # Стандартизированные правила линтинга
├── pnpm-workspace.yaml
├── package.json
└── turbo.json

В файле pnpm-workspace.yaml шаблоны задаются без избыточной вложенности:

packages:
  - "apps/*"
  - "packages/*"

Turborepo рассчитан на плоскую структуру пакетов. Попытка организовать вложенные структуры вида packages/** (например, размещение отдельного package.json внутри packages/ui/button/) не поддерживается и ломает построение топологического графа.

Нейминг внутренних пакетов

Чтобы избежать коллизий с публичными пакетами из реестра npm, для всех внутренних пакетов монорепозитория обязательно задается единый namespace: префикс @repo/* либо имя компании/продукта (@acme/*).

Примеры корректных названий:

  • @repo/ui
  • @repo/types
  • @repo/typescript-config

Шаринг TypeScript-типов и контрактов

Выделение общих типов в автономный пакет создает единый источник правды для серверных ответов, моделей данных и клиентских состояний.

Паттерн Just-in-Time (JIT) для пакетов типов

Для модуля с типами не требуется отдельная стадия компиляции через tsc или бандлеры. Оптимальным решением является паттерн Just-in-Time (JIT) Package, когда пакет напрямую экспортирует исходные .ts-файлы, а компилятор приложения-потребителя разбирает их на лету.

Структура пакета @repo/types:

packages/types/
├── src/
│   ├── api.ts
│   ├── user.ts
│   └── index.ts
├── package.json
└── tsconfig.json

Настройка package.json и subpath exports

Поле exports в манифесте packages/types/package.json задает явные точки входа:

{
  "name": "@repo/types",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts",
    "./api": "./src/api.ts",
    "./user": "./src/user.ts"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*"
  }
}

Такая структура позволяет приложениям импортировать изолированные контексты без загрузки всей кодовой базы типов:

import type { UserProfile, UserRole } from "@repo/types/user";
import type { ApiResponse } from "@repo/types/api";

Базовые конфигурации TypeScript

Чтобы исключить рассинхронизацию настроек компилятора, параметры выносятся в пакет @repo/typescript-config. В нем создаются базовые пресеты: base.json, nextjs.json, react-library.json.

Пример packages/typescript-config/base.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Конфигурация packages/types/tsconfig.json упрощается до наследования:

{
  "extends": "@repo/typescript-config/base.json",
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Создание и переиспользование внутренней UI-библиотеки

Организация UI-кита внутри монорепозитория требует выбора правильного формата поставки: компиляция перед запуском или потребление исходников.

JIT-пакеты против Compiled-пакетов

Параметр Just-in-Time (JIT) Compiled Packages
Формат экспорта Исходники .tsx / .ts Скомпилированный JS (dist/) и типы .d.ts
Сборка пакета Не требуется Требуется (tsup, esbuild, tsc)
DX / HMR Мгновенный Hot Reload в приложении Требуется watch-режим на сборку UI-пакета
Применение Однородный стек (например, Next.js + React) Разнородный стек, сборка под стороннюю публикацию

Для Next.js-приложений стандартом является подход JIT. В файле next.config.js целевого приложения достаточно указать:

module.exports = {
  transpilePackages: ["@repo/ui"],
};

Настройка пакета @repo/ui через subpath exports

Использование единого index.ts (barrel file), агрегирующего все компоненты дизайн-системы, замедляет холодный старт и ухудшает tree-shaking. Рекомендуется использовать точечные экспорты.

Манифест packages/ui/package.json:

{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./button": "./src/components/button.tsx",
    "./card": "./src/components/card.tsx",
    "./styles.css": "./src/styles.css"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "react": "^18.3.0",
    "typescript": "^5.4.0"
  }
}

Изоляция React через peerDependencies

Чтобы избежать рассинхронизации контекста хуков React (Invalid Hook Call Warning), React и React DOM внутри UI-библиотеки должны быть строго определены как peerDependencies. Это гарантирует, что компонент будет использовать тот же экземпляр React runtime, который установлен в приложении-потребителе.


Оркестрация сборки и кэширование в turbo.json

Turborepo строит направленный ациклический граф (DAG) для всех задач монорепозитория. Правильная конфигурация кэша исключает повторное выполнение неизмененных этапов.

Пример корневого turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "type-check": {
      "dependsOn": ["^type-check"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Ключевые директивы:

  • dependsOn: ["^build"] — префикс ^ задает топологический порядок: задача build в приложении начнется только после успешной сборки всех пакетов, от которых оно зависит.
  • inputs — массив путей к файлам, формирующим хэш задачи. Если исходники пакета и его окружение не менялись, Turborepo мгновенно восстанавливает результат из локального или удаленного кэша (FULL TURBO).
  • outputs — перечень каталогов с артефактами сборки, подлежащими архивации.
  • cache: false — директива для долгоживущих процессов (dev-серверы), исключающая их из механизма кэширования.

Типичные ошибки при проектировании монорепозитория

  1. Циклические зависимости между внутренними пакетами. Если @repo/ui обращается к утилитам из @repo/utils, а @repo/utils импортирует константы из @repo/ui, топологическая сортировка графа становится невозможной. Вся общая низкоуровневая логика должна спускаться на уровень ниже — например, в @repo/primitives или @repo/tokens.
  2. Относительные импорты между пакетами. Конструкции вида import { Button } from '../../packages/ui/src/button' нарушают модульные границы и приводят к падению production-сборщиков. Вся коммуникация внутри монорепозитория должна осуществляться исключительно через имена пакетов: import { Button } from '@repo/ui/button'.
  3. Рассинхронизация версий React. Если одно приложение использует React 18, а другое — React 19, это может приводить к конфликтам типов JSX и хуков. Для фиксации версий зависимостей по всему дереву проекта используйте секцию pnpm.overrides (для pnpm), resolutions (для Yarn) или overrides (для npm).

Часто задаваемые вопросы (FAQ)

Чем подход Just-in-Time (JIT) пакетов отличается от Compiled пакетов?

JIT-пакеты отдают необработанные исходные файлы TypeScript (.ts / .tsx). Компиляция и минификация выполняются непосредственно сборщиком конечного приложения. Compiled-пакеты перед использованием собираются во внутреннюю директорию dist/ с генерацией .js и деклараций .d.ts.

Как избежать конфликта нескольких копий React в рантайме?

Необходимо размещать react и react-dom в секции peerDependencies пакета @repo/ui, а для этапа разработки и автодополнения подключать их в devDependencies. Это предотвращает дублирование runtime-бандла React внутри node_modules UI-пакета.

Зачем использовать subpath exports вместо одного файла index.ts?

Экспорт через явные подпути (@repo/ui/button, @repo/ui/modal) избавляет приложение от необходимости парсить громоздкие barrel-файлы. Это снижает объем используемой памяти компилятором TypeScript, предотвращает попадание неиспользуемого кода в бандл и ускоряет Fast Refresh.

Можно ли запускать Turborepo без pnpm?

Да, Turborepo поддерживает npm, Yarn и Bun. Однако pnpm остается популярным выбором для монорепозиториев благодаря жесткой изоляции зависимостей (symlinked node_modules), исключающей использование необъявленных пакетов (phantom dependencies), и экономии дискового пространства за счет глобального контентно-адресуемого хранилища.

Как настроить переиспользование Tailwind CSS в монорепозитории?

Оптимальный паттерн — создание пакета @repo/tailwind-config, экспортирующего базовый конфигурационный объект. В tailwind.config.js приложений и UI-пакета этот конфиг подключается через поле presets. Для корректного сканирования классов в content приложения добавляется путь к исходникам UI-компонентов: ../../packages/ui/src/**/*.{ts,tsx}.


Заключение

Использование Turborepo обеспечивает структурированный и производительный подход к совместному использованию кода во фронтенд-системах. Разделение архитектуры на изолированные модули с пространством имен @repo/*, отказ от громоздких barrel-файлов в пользу subpath exports и корректная изоляция React в peerDependencies позволяют масштабировать кодовую базу без деградации скорости сборки и качества разработки.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также