npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

biomejs-plugin-fsd

v0.1.0

Published

Генератор .grit-плагинов Biome для проверки правил Feature-Sliced Design

Downloads

214

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.

Содержание

Установка

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 (снизу вверх): sharedentitiesfeatureswidgetspagesapp.

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 → @x

publicApi — импорты в обход публичного 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';       // ✅ явный index

2. Самоимпорт слайса через алиас (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/ при слоях sharedapp | | 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.tsx

CLI

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'ы в сгенерированных файлах имеют ведущий **/.