Оптимизация конфигурации для монорепозиториев

Монорепозиторий представляет собой единое хранилище исходного кода, содержащее несколько приложений, библиотек, сервисов и вспомогательных пакетов. По мере роста такого проекта конфигурация ESLint становится одним из ключевых элементов поддержки качества кода. Неправильно организованная система линтинга приводит к дублированию настроек, снижению производительности проверок и усложнению сопровождения.

Основная задача оптимизации заключается в создании централизованной, масштабируемой и производительной конфигурации, которая может использоваться всеми пакетами монорепозитория без копирования одних и тех же настроек.


Особенности монорепозиториев

Типичный монорепозиторий может содержать следующую структуру:

repo/
├── apps/
│   ├── frontend/
│   └── admin/
├── packages/
│   ├── ui/
│   ├── utils/
│   └── api-client/
├── tools/
└── package.json

Каждый пакет может использовать:

  • разные версии JavaScript;
  • TypeScript;
  • React;
  • Node.js;
  • специализированные библиотеки;
  • собственные правила кодирования.

Без централизованного подхода возникает большое количество отдельных файлов конфигурации:

apps/frontend/.eslintrc.json
apps/admin/.eslintrc.json
packages/ui/.eslintrc.json
packages/utils/.eslintrc.json

Подобная организация быстро становится трудноуправляемой.


Централизованная конфигурация

Наиболее распространённым подходом является размещение базовой конфигурации в корне репозитория.

Пример:

// eslint.config.js

export default [
    {
        rules: {
            semi: ["error", "always"],
            quotes: ["error", "single"]
        }
    }
];

Все подпроекты автоматически наследуют данные настройки.

Преимущества:

  • единый источник конфигурации;
  • отсутствие дублирования;
  • упрощённое обновление правил;
  • одинаковый стиль кода во всех пакетах.

Выделение общих конфигураций в отдельный пакет

В крупных монорепозиториях конфигурацию ESLint часто выносят в отдельный внутренний пакет.

Структура:

packages/
└── eslint-config/
    ├── package.json
    └── index.js

Файл конфигурации:

module.exports = {
    rules: {
        semi: ["error", "always"],
        quotes: ["error", "single"]
    }
};

Использование:

module.exports = {
    extends: ['@company/eslint-config']
};

Преимущества подхода:

  • повторное использование настроек;
  • публикация конфигурации между проектами;
  • централизованное управление стандартами разработки.

Иерархия конфигураций

Монорепозиторий обычно требует нескольких уровней настроек.

Например:

eslint-config-base
        │
        ▼
eslint-config-react
        │
        ▼
apps/frontend

Базовая конфигурация:

module.exports = {
    rules: {
        semi: ['error', 'always']
    }
};

React-конфигурация:

module.exports = {
    extends: ['./base'],
    plugins: ['react']
};

Конфигурация приложения:

module.exports = {
    extends: ['@company/eslint-config-react']
};

Подобная схема обеспечивает гибкое наследование правил без их дублирования.


Использование Flat Config

Начиная с ESLint 9 основной моделью конфигурации становится Flat Config.

Пример:

import js from '@eslint/js';

export default [
    js.configs.recommended,
    {
        rules: {
            semi: ['error', 'always']
        }
    }
];

Преимущества Flat Config:

  • более высокая производительность;
  • предсказуемый порядок загрузки;
  • отсутствие сложного механизма поиска конфигураций;
  • улучшенная работа в больших репозиториях.

Для монорепозиториев Flat Config особенно полезен благодаря явному описанию всех конфигурационных блоков.


Разделение правил по типам пакетов

Часто разные части монорепозитория требуют различных настроек.

Пример:

apps/frontend
packages/server
packages/shared

В Flat Config это реализуется через свойство files.

export default [
    {
        files: ['apps/frontend/**/*.{js,jsx}'],
        rules: {
            'no-alert': 'error'
        }
    },
    {
        files: ['packages/server/**/*.js'],
        rules: {
            'no-process-exit': 'error'
        }
    }
];

Преимущества:

  • отсутствие отдельных файлов конфигурации;
  • централизованное управление;
  • гибкость настройки.

Игнорирование служебных директорий

В монорепозиториях количество файлов может исчисляться сотнями тысяч.

Линтер не должен анализировать:

node_modules/
dist/
build/
coverage/
.next/
storybook-static/

Пример:

export default [
    {
        ignores: [
            '**/node_modules/**',
            '**/dist/**',
            '**/build/**',
            '**/coverage/**'
        ]
    }
];

Грамотное исключение директорий существенно ускоряет работу ESLint.


Оптимизация TypeScript-проектов

TypeScript является наиболее затратной частью линтинга.

Типичная конфигурация:

parserOptions: {
    project: './tsconfig.json'
}

При большом количестве пакетов ESLint начинает многократно создавать экземпляры TypeScript Program.

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

tsconfig.eslint.json

Пример:

{
    "extends": "./tsconfig.json",
    "include": ["src/**/*"]
}

Это позволяет уменьшить количество анализируемых файлов.


Использование Project References

В монорепозиториях TypeScript часто применяются ссылки между проектами.

{
    "references": [
        {
            "path": "../shared"
        }
    ]
}

ESLint способен использовать такую структуру значительно эффективнее, чем полностью независимые проекты.

Преимущества:

  • ускорение анализа типов;
  • уменьшение потребления памяти;
  • улучшение масштабируемости.

Минимизация количества type-aware правил

Наиболее ресурсоёмкими считаются правила, использующие информацию о типах.

Примеры:

@typescript-eslint/no-floating-promises
@typescript-eslint/no-misused-promises
@typescript-eslint/restrict-plus-operands

Если пакет содержит простой код утилит, подобные проверки могут быть отключены:

{
    files: ['packages/utils/**/*.ts'],
    rules: {
        '@typescript-eslint/no-floating-promises': 'off'
    }
}

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


Кэширование результатов

Одним из самых эффективных способов ускорения ESLint является использование кэша.

Запуск:

eslint . --cache

Создаётся файл:

.eslintcache

Повторная проверка анализирует только изменённые файлы.

Для CI-пайплайнов часто используют:

eslint . --cache --cache-location .cache/eslint

Преимущества:

  • многократное сокращение времени выполнения;
  • снижение нагрузки на процессор;
  • ускорение локальной разработки.

Линтинг только изменённых файлов

Полный запуск ESLint на крупном монорепозитории может занимать несколько минут.

Более эффективный подход — проверка изменённых файлов.

Пример через Git:

git diff --name-only HEAD~1

Далее список передаётся в ESLint:

eslint file1.js file2.js

Многие инструменты автоматизации строят линтинг именно по этому принципу.


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

Система управления монорепозиториями Nx предоставляет встроенную поддержку ESLint.

Пример конфигурации цели:

{
    "lint": {
        "executor": "@nx/eslint:lint",
        "options": {
            "lintFilePatterns": [
                "apps/frontend/**/*.ts"
            ]
        }
    }
}

Nx вычисляет граф зависимостей и запускает линтер только для затронутых проектов.

Преимущества:

  • инкрементальные проверки;
  • распределённое кэширование;
  • сокращение времени CI.

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

В Turborepo ESLint обычно выступает отдельной задачей.

Пример:

{
    "pipeline": {
        "lint": {
            "outputs": []
        }
    }
}

Запуск:

turbo run lint

Система автоматически использует локальный и удалённый кэш результатов.

Это особенно полезно при большом количестве пакетов.


Параллельное выполнение

Современные процессоры содержат множество ядер.

ESLint поддерживает распараллеливание через внешние инструменты и системы сборки.

Пример:

turbo run lint

или

nx run-many --target=lint

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


Управление наборами правил

Большое количество правил увеличивает время анализа.

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

Базовые

semi
quotes
eqeqeq

Рекомендованные

no-unused-vars
no-console
prefer-const

Тяжёлые

import/no-cycle
sonarjs/cognitive-complexity
@typescript-eslint/no-floating-promises

Тяжёлые правила нередко запускаются только в CI.


Отдельные конфигурации для CI

Локальная разработка требует высокой скорости.

CI требует максимального контроля качества.

Пример разделения:

export default [
    {
        rules: {
            'no-console': 'warn'
        }
    }
];

Для CI:

export default [
    {
        rules: {
            'no-console': 'error'
        }
    }
];

Такой подход сохраняет комфорт разработки и одновременно обеспечивает строгие проверки перед публикацией изменений.


Контроль версий конфигураций

Внутренние ESLint-конфигурации рекомендуется версионировать так же, как обычные пакеты.

Пример:

{
    "name": "@company/eslint-config",
    "version": "3.2.0"
}

Преимущества:

  • предсказуемость обновлений;
  • контроль совместимости;
  • возможность постепенной миграции пакетов.

Стратегии масштабирования

При росте монорепозитория эффективной считается следующая архитектура:

eslint-config-base
        │
        ├── eslint-config-react
        │
        ├── eslint-config-node
        │
        ├── eslint-config-typescript
        │
        └── eslint-config-testing

Каждый пакет подключает только необходимые наборы правил.

Например:

export default [
    ...typescriptConfig,
    ...reactConfig
];

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


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

Дублирование конфигураций

Проблема:

20 пакетов
20 одинаковых .eslintrc

Последствия:

  • сложность обновления;
  • расхождение правил;
  • рост технического долга.

Избыточное использование TypeScript-проверок

Проблема:

parserOptions: {
    project: '**/tsconfig.json'
}

Последствия:

  • высокое потребление памяти;
  • медленные проверки;
  • нестабильность CI.

Отсутствие кэширования

Проблема:

eslint .

Вместо:

eslint . --cache

Последствия:

  • повторный анализ неизменённых файлов;
  • значительное увеличение времени выполнения.

Проверка всего репозитория при каждом коммите

Проблема:

eslint .

Последствия:

  • медленные pre-commit хуки;
  • снижение продуктивности команды.

Более эффективным решением является анализ только изменённых файлов или затронутых проектов.


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

repo/
├── eslint.config.js
├── packages/
│   ├── eslint-config-base/
│   ├── eslint-config-react/
│   ├── eslint-config-node/
│   └── eslint-config-typescript/
├── apps/
├── packages/
└── .eslintcache

Ключевые принципы такой архитектуры:

  • единая точка управления конфигурацией;
  • использование Flat Config;
  • модульные наборы правил;
  • минимизация type-aware анализа;
  • обязательное кэширование;
  • запуск линтинга только для изменённых проектов;
  • интеграция с инструментами управления монорепозиториями;
  • строгий контроль версий конфигурационных пакетов;
  • разделение быстрых локальных и полных CI-проверок.

Подобный подход обеспечивает высокую производительность ESLint даже в монорепозиториях, содержащих десятки пакетов, множество приложений и сотни тысяч строк кода.