Настройка Vite в монорепозитории

Монорепозиторий строится вокруг идеи единого хранилища для нескольких приложений и пакетов, где общие зависимости и код переиспользуются без дублирования. В экосистеме JavaScript это особенно актуально для проектов с несколькими фронтенд-приложениями, библиотеками компонентов и общими утилитами.

Vite в такой архитектуре выполняет роль инструмента разработки и сборки, который должен корректно работать как с локальными пакетами, так и с внешними зависимостями, не теряя скорости dev-сервера и не ломая резолв модулей.

Ключевая сложность монорепозитория — корректное связывание workspace-пакетов, поддержка алиасов и предотвращение дублирования зависимостей, которые могут приводить к конфликтам React/Vue или другим runtime-ошибкам.


Базовая структура монорепозитория

Типичная структура с Vite выглядит следующим образом:

repo/
  apps/
    admin/
    web/
  packages/
    ui/
    utils/
    config/
  package.json
  pnpm-workspace.yaml

Разделение по слоям

  • apps — конечные приложения, которые собираются и деплоятся
  • packages — переиспользуемые модули
  • config — общие конфигурации (eslint, tsconfig, vite config)

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


Выбор менеджера пакетов

Монорепозиторий с Vite требует workspace-поддержки. На практике используются три варианта:

pnpm

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

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Особенность: отсутствует классический hoisting, зависимости строго изолированы.

yarn workspaces

Подходит для проектов, где важен привычный npm-опыт:

{
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

npm workspaces

Минимальный набор возможностей, но достаточный для простых монореп.


Установка Vite в приложении

Каждое приложение в apps/* содержит собственный Vite-проект:

apps/web/package.json
apps/admin/package.json

Пример зависимости:

{
  "name": "web",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "vite": "^5.0.0"
  }
}

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


Работа с workspace-пакетами

Внутренние пакеты подключаются через workspace-ссылки:

{
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}

Vite по умолчанию не всегда корректно резолвит такие зависимости, поэтому требуется настройка.


Настройка resolve.alias

В каждом приложении Vite необходимо явно указать алиасы:

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@repo/ui': path.resolve(__dirname, '../. ./packages/ui/src'),
      '@repo/utils': path.resolve(__dirname, '../. ./packages/utils/src')
    }
  }
})

Это позволяет:

  • ускорить HMR
  • избежать повторной сборки пакетов
  • упростить отладку

Поддержка TypeScript в монорепозитории

Ключевой файл — общий tsconfig.base.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@repo/ui": ["packages/ui/src"],
      "@repo/utils": ["packages/utils/src"]
    }
  }
}

Каждое приложение наследует конфигурацию:

{
  "extends": "../. ./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist"
  }
}

Важно синхронизировать paths в TypeScript и alias в Vite, иначе возможны расхождения между dev и build.


Особенности резолва зависимостей Vite

Vite использует ESM-ориентированный резолвер, поэтому в монорепозитории возникают типичные проблемы:

1. Дублирование React/Vue

Если react установлен в каждом пакете отдельно, возможны ошибки хуков.

Решение:

  • вынос React в root зависимости
  • использование peerDependencies в библиотеках
{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

2. Оптимизация зависимостей (optimizeDeps)

Vite пред-бандлит зависимости через esbuild:

export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom']
  }
})

В монорепозитории часто требуется явно указывать зависимости, иначе dev-сервер будет медленным или нестабильным.


3. Исключение workspace-пакетов из prebundle

export default defineConfig({
  optimizeDeps: {
    exclude: ['@repo/ui', '@repo/utils']
  }
})

Настройка build для библиотек

Пакеты в packages/* часто собираются отдельно через Vite library mode:

import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es']
    },
    rollupOptions: {
      external: ['react', 'vue']
    }
  }
})

Важно:

  • external предотвращает включение зависимостей в бандл
  • формат es предпочтителен для монорепы

Изоляция стилей и ассетов

В монорепозитории часто используются UI-библиотеки, которые импортируют CSS:

Подход 1: явный импорт

import '@repo/ui/styles.css'

Подход 2: CSS-in-JS или scoped CSS

Подходит для уменьшения глобальных конфликтов.

Подход 3: сборка стилей в пакете

build: {
  cssCodeSplit: true
}

Интеграция с Turborepo или Nx

Для ускорения сборок используется task runner:

turbo.json

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false
    }
  }
}

Vite в этом случае отвечает за сборку конкретных пакетов, а orchestrator управляет зависимостями.


Dev-сервер в монорепозитории

При запуске нескольких приложений одновременно возникают вопросы:

Порты

server: {
  port: 3000
}

Каждое приложение должно иметь уникальный порт.

HMR между пакетами

Чтобы изменения в packages/ui обновляли приложения:

  • использовать прямые пути (alias на src)
  • избегать сборки пакета в watch-режиме отдельно

Общая конфигурация Vite

В больших монорепозиториях часто выносят базовый конфиг:

packages/config/vite.base.ts
import { defineConfig } from 'vite'

export const baseViteConfig = defineConfig({
  resolve: {
    dedupe: ['react', 'vue']
  }
})

И используют в приложениях:

import { baseViteConfig } from '@repo/config/vite.base'

export default defineConfig({
  ...baseViteConfig
})

Частые проблемы и их решения

Проблема: HMR не обновляет изменения в пакетах

Причина: пакет подключён как готовый build, а не исходники.

Решение: alias на src.


Проблема: двойной React

Причина: разные копии react в node_modules.

Решение:

  • pnpm strict mode
  • peerDependencies
  • dedupe в Vite

Проблема: медленный dev server

Причина: слишком много зависимостей в optimizeDeps.

Решение:

  • явное include/exclude
  • уменьшение workspace-границ

Проблема: несинхронность TS и Vite

Причина: разные alias/path конфигурации.

Решение: единый tsconfig.base.json.


Масштабирование архитектуры

При росте монорепозитория Vite остаётся эффективным при соблюдении нескольких принципов:

  • минимизация cross-dependency между пакетами
  • явное разделение app и lib слоёв
  • единая конфигурация резолва
  • контроль peerDependencies
  • использование library mode для пакетов

Монорепозиторий с Vite становится предсказуемым только при строгом контроле зависимостей и единых правилах импорта между пакетами.