Вот готовое практическое руководство по настройке ESLint v9+ (Flat Config) для стека React + TypeScript и созданию структуры папок по методологии Feature-Sliced Design (FSD).
1. Настройка ESLint (для React + TS)
Современный ESLint (начиная с версии 9) использует новый формат конфигурации eslint.config.js (Flat Config).
Шаг 1: Установка зависимостей
Выполните команду в терминале проекта:
npm install -D eslint @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh
Шаг 2: Создание файла eslint.config.js
Создайте этот файл в корневом каталоге проекта:
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
// Игнорируемые папки (замена старого .eslintignore)
{ ignores: ['dist', 'node_modules', 'build'] },
// Базовые настройки для JavaScript и TypeScript
js.configs.recommended,
...tseslint.configs.recommended,
// Настройки для React-компонентов
{
files: ['**/*.{ts,tsx}'],
plugins: {
'react': reactPlugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
settings: {
react: { version: 'detect' }, // Автоопределение версии React
},
rules: {
// Правила React Hooks
...reactHooks.configs.recommended.rules,
// Специфичные правила для React
'react/react-in-jsx-scope': 'off', // Отключено для React 17+
'react/jsx-no-target-blank': 'warn',
// Правила для React Fast Refresh (полезно для Vite)
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// Кастомные правила TypeScript
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
},
}
);
2. Структура папок по стандарту Feature-Sliced Design (FSD)
Методология FSD делит проект на слои (Layers), внутри которых находятся слайсы (Slices), разбитые на сегменты (Segments). Главное правило: верхние слои могут импортировать код только из нижних, но не наоборот.
Вот как выглядит правильная структура каталога src для типичного React-приложения:
src/
├── 1_app/ # Слои (Layers) пишутся строчными буквами, цифры — для визуального порядка
│ ├── providers/ # Провайдеры (Redux Store, RouterProvider, ThemeProvider)
│ ├── styles/ # Глобальные стили (index.css, variables.scss)
│ └── App.tsx # Инициализация приложения
│
├── 2_pages/ # Страницы приложения
│ ├── home/ # Слайс страницы (Slice)
│ │ ├── ui/ # Сегменты (Segments): UI-компоненты страницы
│ │ └── index.ts # Публичное API (только отсюда можно делать импорт наружу!)
│ └── profile/
│ ├── ui/
│ └── index.ts
│
├── 3_widgets/ # Крупные самостоятельные блоки (из фич и сущностей)
│ ├── header/
│ │ ├── ui/ # Header.tsx
│ │ └── index.ts
│ └── sidebar/
│
├── 4_features/ # Действия пользователя, несущие бизнес-ценность
│ ├── auth-by-username/ # Авторизация
│ │ ├── model/ # Стейт, экшены, селекторы (Redux/Zustand)
│ │ ├── ui/ # Форма логина, кнопка
│ │ └── index.ts
│ └── add-to-cart/ # Добавление в корзину
│
├── 5_entities/ # Бизнес-сущности (без привязки к конкретным действиям)
│ ├── user/
│ │ ├── model/ # Типы пользователя, стейт авторизации
│ │ └── index.ts
│ └── product/
│ ├── ui/ # Карточка продукта (ProductCard)
│ └── index.ts
│
└── 6_shared/ # Переиспользуемый код (инфраструктура, утилиты)
├── api/ # Базовые запросы (axios, fetch)
├── assets/ # Иконки, картинки, шрифты
├── lib/ # Хелперы, кастомные хуки (useDebounce и др.)
└── ui/ # Атомарные UI-компоненты (Button, Input, Modal)
├── button/
│ ├── Button.tsx
│ └── index.ts
└── input/
Важные правила FSD для вашей команды:
- Индексные файлы (index.ts) — это «ворота» слайса. Извне разрешено импортировать код только через них (например: import { Button } from '@/shared/ui/button'). Импорты вида .../button/Button.tsx строго запрещены.
- Иерархия импортов — widgets может импортировать из features, entities и shared. Но shared не может импортировать ничего из слоев выше.
Настроить специальный плагин для ESLint, который запретит некорректные импорты между слоями.
Для автоматического контроля архитектуры Feature-Sliced Design (FSD) в ESLint используется официальный плагин eslint-plugin-boundaries или специализированный eslint-plugin-fsd (также известный как eslint-plugin-feature-sliced).
Наиболее гибким, поддерживаемым и совместимым с ESLint v9+ (Flat Config) является eslint-plugin-boundaries. Он позволяет жестко заблокировать импорты снизу вверх (например, из shared в features).
Вот пошаговое руководство по его интеграции.
Шаг 1: Установка плагина
Установите плагин контроля границ в dev-зависимости:
npm install -D eslint-plugin-boundaries
Шаг 2: Обновление конфигурации eslint.config.js
Вам нужно импортировать плагин, разметить папки вашего проекта на «типы» (слои) и задать правила их взаимодействия.
Добавьте этот код в ваш существующий файл eslint.config.js:
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
// 1. Импортируем плагин границ
import boundaries from 'eslint-plugin-boundaries';
export default tseslint.config(
{ ignores: ['dist', 'node_modules', 'build'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx,js,jsx}'],
plugins: {
'react': reactPlugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
// 2. Регистрируем плагин
'boundaries': boundaries,
},
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
settings: {
react: { version: 'detect' },
// 3. Настраиваем распознавание путей (поддерживает относительные импорты и алиасы вроде @/*)
'boundaries/elements': [
{ type: 'app', pattern: 'src/1_app/**/*' },
{ type: 'pages', pattern: 'src/2_pages/**/*' },
{ type: 'widgets', pattern: 'src/3_widgets/**/*' },
{ type: 'features', pattern: 'src/4_features/**/*' },
{ type: 'entities', pattern: 'src/5_entities/**/*' },
{ type: 'shared', pattern: 'src/6_shared/**/*' },
],
},
rules: {
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
// 4. Включаем правила FSD
'boundaries/entry-point': 'error', // Запрещает глубокие импорты в обход index.ts
'boundaries/element-types': [
'error',
{
default: 'disallow', // По умолчанию всё запрещено, кроме явных разрешений ниже
message: 'Архитектурная ошибка FSD: импорт из слоев выше или чужих слайсов запрещен (${file.type} <- ${dependency.type})',
rules: [
// Слою App доступно всё
{ from: 'app', allow: ['pages', 'widgets', 'features', 'entities', 'shared'] },
// Слою Pages доступны все нижележащие слои
{ from: 'pages', allow: ['widgets', 'features', 'entities', 'shared'] },
// Слою Widgets доступны фичи, сущности и shared
{ from: 'widgets', allow: ['features', 'entities', 'shared'] },
// Слою Features доступны сущности и shared
{ from: 'features', allow: ['entities', 'shared'] },
// Слою Entities доступен только shared
{ from: 'entities', allow: ['shared'] },
// Слою Shared запрещено импортировать из любых других слоев FSD
{ from: 'shared', allow: [] },
],
},
],
},
}
);
Как это работает на практике (Примеры)
Если линтер настроен правильно, вы сразу получите сообщения об ошибках в редакторе (VS Code / WebStorm) или при сборке проекта:
- Ошибка ❌: Попытка импортировать хук авторизации из features внутрь кнопки в shared:
// Внутри src/6_shared/ui/button/Button.tsx import { useAuth } from '@/features/auth-by-username'; // ESLint выведет: "Архитектурная ошибка FSD: импорт из слоев выше или чужих слайсов запрещен (shared <- features)" - Ошибка ❌: Нарушение принципа Public API (глубокий импорт в обход index.ts слайса):
// Внутри src/2_pages/home/ui/HomePage.tsx import { LoginForm } from '@/features/auth-by-username/ui/LoginForm'; // ESLint выдаст ошибку правила boundaries/entry-point. Нужно импортировать строго из '@/features/auth-by-username' - **Корректно Запрос из виджета в фичу через Public API:
// Внутри src/3_widgets/header/ui/Header.tsx import { LoginButton } from '@/features/auth-by-username';
Настройка алиасов путей (Path Aliases) в tsconfig.json и Vite/Webpack, чтобы вместо длинных относительных путей ../../ использовать красивый префикс @/
Настройка Path Aliases (алиасов путей) сделает ваши импорты чистыми (вместо ../../../../shared/ui вы будете писать @/shared/ui), а также поможет плагину eslint-plugin-boundaries работать без сбоев.
Поскольку вы используете React, проект, скорее всего, собран на Vite. Нам нужно настроить алиасы в трех местах: в TypeScript (для подсказок в IDE), в Vite (для сборщика) и в ESLint (для резолва путей плагином границ).
Шаг 1: Настройка TypeScript (tsconfig.json)
Откройте файл tsconfig.json (или tsconfig.app.json, если у вас проект на свежем Vite) и добавьте секцию paths внутрь compilerOptions:
{
"compilerOptions": {
// ... ваши текущие настройки
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Что это делает: Говорит TypeScript, что символ @/ указывает на папку src/.
Шаг 2: Настройка Vite (vite.config.ts)
Чтобы Vite понимал эти пути при сборке, установим плагин vite-tsconfig-paths. Он автоматически синхронизирует настройки из tsconfig.json в Vite, избавляя от ручного дублирования.
- Установите плагин:
npm install -D vite-tsconfig-paths
- Обновите vite.config.ts:
import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // 1. Импортируем плагин import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ // 2. Добавляем в массив плагинов plugins: [react(), tsconfigPaths()], });
Шаг 3: Корректировка ESLint для плагина границ (eslint.config.js)
Чтобы плагин eslint-plugin-boundaries правильно читал пути с префиксом @/, нам нужно добавить специальный плагин-резолвер для ESLint.
- Установите парсер путей TypeScript для ESLint:
npm install -D eslint-import-resolver-typescript
- Обновите секцию settings в вашем eslint.config.js:
// ... ваши импорты
export default tseslint.config(
{ ignores: ['dist', 'node_modules', 'build'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx,js,jsx}'],
plugins: {
'react': reactPlugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'boundaries': boundaries,
},
// ОБНОВЛЕННАЯ СЕКЦИЯ SETTINGS
settings: {
react: { version: 'detect' },
// Настройка для распознавания алиаса @/* парсером ESLint
'import/resolver': {
typescript: {},
},
// Шаблоны теперь учитывают как относительные пути, так и алиас @/
'boundaries/elements': [
{ type: 'app', pattern: ['src/1_app/**/*', '@/1_app/**/*'] },
{ type: 'pages', pattern: ['src/2_pages/**/*', '@/2_pages/**/*'] },
{ type: 'widgets', pattern: ['src/3_widgets/**/*', '@/3_widgets/**/*'] },
{ type: 'features', pattern: ['src/4_features/**/*', '@/4_features/**/*'] },
{ type: 'entities', pattern: ['src/5_entities/**/*', '@/5_entities/**/*'] },
{ type: 'shared', pattern: ['src/6_shared/**/*', '@/6_shared/**/*'] },
],
},
rules: {
// ... правила, которые мы настроили на прошлом шаге
}
}
);
Проверка результата 🚀
После перезапуска IDE (иногда VS Code нужно перезапустить через Ctrl+Shift+P -> Developer: Reload Window), вы сможете писать импорты в стиле FSD:
// Внутри src/2_pages/home/ui/HomePage.tsx
// Отлично: импорт виджета через красивый алиас
import { Header } from '@/3_widgets/header';
// Отлично: импорт кнопки из shared
import { Button } from '@/6_shared/ui/button';
// ESLint выдаст ошибку ❌ (импорт сверху вниз):
// import { ProfilePage } from '@/2_pages/profile';
Если вы планируете использовать менеджер состояния (например, Zustand или Redux Toolkit), я могу показать, как правильно разложить стейт по слоям features и entities, чтобы не нарушить правила линтера. Рассказать об этом подробнее?