biomejs-plugin-fsd
v0.1.0
Published
Генератор .grit-плагинов Biome для проверки правил Feature-Sliced Design
Downloads
214
Maintainers
Readme
biomejs-plugin-fsd
Генератор .grit-плагинов Biome для проверки правил
Feature-Sliced Design в проектах на TypeScript/React + Next.js.
GritQL-плагины Biome не имеют собственного механизма конфигурации — имена слоёв,
алиасы и includes-globs кодогенерируются из единого fsd.config. Пакет читает
конфиг, эмитит набор .grit-файлов и обновляет biome.json, после чего обычный
biome check проверяет архитектурные правила FSD. Раскладку папок/сегментов
проверяет отдельная подкоманда check (FS-чекер), которая не требует Biome.
Содержание
- Установка
- Быстрый старт
- Конфигурация
- Правила
- Next.js + FSD
- CLI
- Программный API
- Ограничения
- Разработка
- Релиз и публикация
Установка
pnpm add -D biomejs-plugin-fsdТребуется Node ≥ 22.18 (нативный стриппинг типов для .ts-конфига).
Быстрый старт
1. Создайте конфиг fsd.config.ts в корне проекта:
import { defineConfig } from 'biomejs-plugin-fsd';
export default defineConfig({
root: 'src',
alias: '@/',
});2. Сгенерируйте плагины:
npx biomejs-plugin-fsd generateКоманда записывает .grit-файлы в .fsd/ и прописывает их в biome.json.
3. Запустите проверку:
biome check # AST-правила (слои, public API, Next.js-стык)
biomejs-plugin-fsd check # раскладка папок и сегментовДля CI удобно объединить:
biome check && biomejs-plugin-fsd checkКонфигурация
fsd.config.ts
import { defineConfig } from 'biomejs-plugin-fsd';
export default defineConfig({
// FSD-корень относительно cwd. Дефолт: 'src'
root: 'src',
// Каталог для сгенерированных .grit-файлов. Дефолт: '.fsd'
// Допускает вложенный путь: '.biomejs-rules/fsd'
// CLI-флаг --out-dir имеет приоритет над этим полем.
outDir: '.fsd',
// Path-алиас(ы) для абсолютных импортов. Дефолт: '@/'
// Обязан оканчиваться на '/'. Массив — если несколько алиасов.
alias: '@/',
// Расширения файлов, которые проверяются. Дефолт: ['.ts', '.tsx', '.js', '.jsx']
extensions: ['.ts', '.tsx', '.js', '.jsx'],
// Модель слоёв. Два варианта: карта переименования или полный массив (см. ниже).
layers: { pages: 'views' },
// Конвенциональные сегменты. Дефолт: ['ui', 'api', 'lib', 'model', 'config']
segments: ['ui', 'api', 'lib', 'model', 'config'],
// Базовое имя public-API файла. Дефолт: 'index'
indexFile: 'index',
// Regex числового сорт-префикса папки ('2_entities', '01-shared').
// false — запрещает префиксы. Дефолт: '/^\d+[_-]/'
slicePrefix: /^\d+[_-]/,
// Токен cross-import public API между сущностями. Дефолт: '@x'
crossRefToken: '@x',
// Каталог App Router Next.js относительно cwd. Дефолт: 'app'
nextAppDir: 'app',
// Канонические слои, разрешённые в page.tsx (nextBoundary). Дефолт: ['pages', 'shared']
nextPageAllow: ['pages', 'shared'],
// Severity по правилам (off | warn | error)
rules: {
layerImports: 'error', // дефолт
publicApi: 'error', // дефолт
segmentLayout: 'error', // дефолт
nextBoundary: 'warn', // дефолт
},
});Модель слоёв
Канонические слои FSD (снизу вверх): shared → entities → features → widgets → pages → app.
Sliced (есть слайсы): entities, features, widgets, pages.
Unsliced (без слайсов): shared, app.
Форма A — карта переименования (рекомендуется):
layers: { pages: 'views' } // переименовать один слой, остальные — каноническиеФорма B — полный упорядоченный массив (снизу вверх):
layers: [
{ name: 'shared', canonical: 'shared', sliced: false },
{ name: 'entities', canonical: 'entities', sliced: true },
{ name: 'features', canonical: 'features', sliced: true },
{ name: 'widgets', canonical: 'widgets', sliced: true },
{ name: 'views', canonical: 'pages', sliced: true },
{ name: 'app', canonical: 'app', sliced: false },
]В форме B canonical связывает фактическое имя папки с каноническим смыслом — это
нужно для правил nextBoundary и @x-логики, которые опираются на канон, а не имя.
Без canonical слой считается нестандартным и часть правил его не покрывает.
Правила
layerImports — зависимости между слоями и слайсами
Severity по умолчанию: error.
Два вида нарушений под одним ключом:
1. Импорт вышестоящего слоя (layer-imports):
// src/entities/user/model/store.ts
import { Cart } from '@/features/cart'; // ❌ features выше entitiesСлой может импортировать только нижестоящие слои. Порядок снизу вверх: shared → entities → features → widgets → pages → app.
2. Межслайсовый импорт (cross-slice):
// src/features/auth/ui/Form.tsx
import { useCart } from '@/features/cart/model/store'; // ❌ соседний слайсСлайс не может напрямую импортировать соседний слайс того же слоя.
Исключение — слой entities: там межслайсовый cross-import public API разрешён через токен @x:
// src/entities/user/@x/product.ts
export { ... } // cross-import public API
// src/entities/product/model/store.ts
import { User } from '@/entities/user/@x/product'; // ✅ entities → @xpublicApi — импорты в обход публичного API
Severity по умолчанию: error.
Два вида нарушений под одним ключом:
1. Deep-импорт чужого слайса:
import { userStore } from '@/features/auth/model/store'; // ❌ обход public API
import { userStore } from '@/features/auth'; // ✅ через barrel
import { userStore } from '@/features/auth/index'; // ✅ явный index2. Самоимпорт слайса через алиас (self-import):
// src/features/auth/ui/Form.tsx
import { authModel } from '@/features/auth'; // ❌ свой слайс через alias → цикл через index
import { authModel } from '../model'; // ✅ относительный путьВнутри слайса нужно ходить относительными путями — алиасный самоимпорт создаёт
циклическую зависимость через index.
segmentLayout — раскладка слоёв и сегментов (FS-чекер)
Severity по умолчанию: error.
Запускается командой biomejs-plugin-fsd check (не biome check).
| Код | Нарушение | Пример |
|-----|-----------|--------|
| unknownLayer | Папка в корне FSD не является слоем | src/helpers/ при слоях shared…app |
| segmentlessSlice | Слайс без единого сегмента | src/features/auth/ — только utils.ts, без ui//model//… |
| unknownSegment | Сегмент вне cfg.segments (только sliced-слои) | src/features/auth/helpers/ при дефолтных сегментах |
| strayFile | Code-файл вне сегмента | src/features/auth/store.ts вместо src/features/auth/model/store.ts |
| missingIndex | Слайс без public-API index-файла | src/entities/user/ без index.ts |
Unsliced-слои (shared, app) не требуют index и допускают любые имена сегментов —
кастомные сегменты providers/, styles/ и т.д. там нормальны.
nextBoundary — стык с Next.js App Router
Severity по умолчанию: warn.
Три доктрины для роутинг-файлов в app/:
page.tsx — строгая:
// app/cart/page.tsx
import { CartPage } from '@/views/cart'; // ✅ из слоя страниц
import type { Metadata } from 'next'; // ✅ внешний пакет
import { CartPage } from '@/features/cart/ui/CartPage'; // ❌ бизнес-слой напрямуюРазрешены только слои из nextPageAllow (по умолчанию pages/views + shared).
Настраивается, чтобы ослабить/ужесточить доктрину (см. Next.js + FSD).
layout.tsx / template.tsx / loading.tsx / error.tsx / not-found.tsx / default.tsx — мягкая:
Разрешены: pages/views, widgets, shared, app (провайдеры).
Запрещены: entities, features и неканонические слои — бизнес-логика в layout — архитектурная ошибка.
route.ts (API Route Handler) — зеркальная:
// app/api/cart/route.ts
import { getCart } from '@/features/cart/api/getCart'; // ✅ бизнес-логика
import { CartWidget } from '@/widgets/cart'; // ❌ UI-слой в API-handlerЗапрещены UI-слои (pages/views, widgets). Разрешены features, entities, shared.
Route-handler должен быть тонким: парсинг входа → вызов use-case → сериализация ответа.
Next.js + FSD
Переименование слоя pages → views
В Next.js pages/ — зарезервированная папка (Pages Router). При использовании
App Router коллизия всё равно создаёт путаницу. Решение — переименовать FSD-слой:
export default defineConfig({
layers: { pages: 'views' },
nextAppDir: 'app',
});Правила плагина опираются на canonical (внутренний смысл слоя), поэтому
views ведёт себя как pages — всё работает без изменений в правилах.
Настройка доктрины page.tsx
Поле nextPageAllow задаёт канонические слои, разрешённые в page.tsx:
// Дефолт — страница из слоя страниц + примитивы shared (Metadata, конфиги)
nextPageAllow: ['pages', 'shared']
// Строгий вариант — только слой страниц, shared запрещён
nextPageAllow: ['pages']
// Расширенный — добавить entities для SSR-страниц без лишних слайсов
nextPageAllow: ['pages', 'shared', 'entities']generateMetadata и shared
По умолчанию shared разрешён в page.tsx, чтобы generateMetadata мог
импортировать типы и константы из shared без ошибок плагина.
Структура Next.js App Router + FSD
src/
shared/ # utils, ui-kit, типы
entities/ # доменные сущности
features/ # бизнес-сценарии
widgets/ # композиция виджетов
views/ # страницы (pages → views)
cart/
ui/
CartPage.tsx
index.ts
app/
cart/
page.tsx # import { CartPage } from '@/views/cart'
layout.tsxCLI
biomejs-plugin-fsd <команда> [опции]generate
Генерирует .grit-файлы и идемпотентно обновляет biome.json.
biomejs-plugin-fsd generate [опции]| Опция | Описание |
|-------|----------|
| -c, --config <path> | Путь к конфигу (по умолчанию ищется fsd.config.* в cwd) |
| --cwd <dir> | Рабочий каталог проекта |
| --out-dir <dir> | Каталог вывода .grit (приоритет над fsd.config.outDir) |
| --dry-run | Показать план без записи на диск |
После генерации запустите biome check как обычно — плагины подхватятся автоматически.
Важно: biome.json обновляется идемпотентно — существующие настройки не затираются,
только добавляются/обновляются plugins[] и linter.enabled: true.
check
FS-чекер раскладки слоёв/сегментов. Работает независимо от Biome.
biomejs-plugin-fsd check [опции]| Опция | Описание |
|-------|----------|
| -c, --config <path> | Путь к конфигу |
| --cwd <dir> | Рабочий каталог проекта |
Exit-code: 1 если rules.segmentLayout = 'error' и есть нарушения, иначе 0.
Программный API
import { defineConfig, resolveConfig, buildArtifacts, generate } from 'biomejs-plugin-fsd';
// Нормализация конфига с валидацией
const cfg = resolveConfig({ layers: { pages: 'views' } });
// Чистая сборка артефактов (без I/O) — детерминированная и идемпотентная
const artifacts = buildArtifacts(cfg);
// artifacts.gritFiles — [{path, content}]
// artifacts.biomeConfig — объект для merge в biome.json
// Запись на диск
await generate({ root: 'src' }, { cwd: process.cwd() });
// С явным outDir
await generate({ root: 'src' }, { cwd: process.cwd(), outDir: '.biomejs-rules/fsd' });Публичный контракт пакета (exports['.']): defineConfig, resolveConfig,
buildArtifacts, generate, mergeBiomeConfig, emitLayerImports, emitCrossSlice
и типы (FsdConfig, ResolvedConfig, …).
Ограничения
Per-layer алиасы не поддерживаются. Модель плагина — единый префикс + слой/сегмент
(@/<layer>/<slice>). Алиасы вида @features/, @entities/ не анализируются.
При наличии таких алиасов в проекте потребуется канонизация: codemod + обновление
tsconfig.json/vite.config.ts.
Числовые сорт-префиксы папок обрабатываются с ограничением. Правила publicApi
и crossSlice сравнивают имена групп, а не реальные слайсы внутри группы.
Например, features/01-auth/ui и features/02-billing/ui будут сравниваться как
01-auth vs 02-billing — ложных срабатываний нет, но некоторые нарушения
внутри группы могут быть пропущены (мягкий недо-репорт).
GritQL-плагины требуют включённого линтера. При linter.enabled = false в biome.json
правила молчат. Команда generate автоматически выставляет linter.enabled: true.
Интерполяция переменных в regex GritQL не работает внутри пайплайна плагина. Поэтому сопоставление «свой/чужой слайс» реализовано через унификацию (capture-переменные), а не конкатенацию строк.
biome.json plugins[].includes матчат абсолютный путь — поэтому все glob'ы
в сгенерированных файлах имеют ведущий **/.
