ESLint-плагины и дизайн-токены: детали
Эта часть репозитория подробно описана здесь, а не в корневом AGENTS.md:
рутина-разработка её не требует, детали нужны только при правке самих
линт-правил или конфигов.
Структура
web/eslint-plugins/— локальные ESLint-плагины.design-tokens/— правила для<style>-блоков svelte-компонентов.isolation/— правило изоляции old/new UI.conventions/— конвенции кода (интерфейс пропсов, запрет union-алиасов).i18n/— кросс-языковой линтер словарей (dict-consistency).__tests__/— юнит-тесты (Vitest +RuleTester/Linter).__fixtures__/— фикстуры для тестов: миниатюрныйsrc/app.css(словарь токенов), файлы для изоляционных тестов и мини-словариlib/i18n/.
web/scripts/:lint-all.mjs— оркестраторlint:all.check-tokens.mjs+token-audit/— токен-аудит поapp.css.postcss-hct.mjs— постcss-плагин эммитаhct()в sRGB-hex.
web/stylelint.config.js,web/postcss.config.js— конфиги stylelint/css.
Конфигурация ESLint (web/eslint.config.js)
Устроена инкрементально:
- На весь код — парсинг TS/Svelte + правило
@typescript-eslint/consistent-type-imports(запрет инлайн-тип-импортов,prefer: 'type-imports'). - Полные
recommended-наборы (eslint+typescript-eslint+eslint-plugin-svelte) навешаны только на новый код (src/lib/components/**,src/routes/**), чтобы старый код не засыпался предсуществующими ошибками. Старый код (C17) —src/lib/v1/**,src/routes/v1/**. Когда старый дизайн удалён (C19), scoped-блок убирается иrecommendedвключается на весь код. - Scoped-пути двигаются вместе с папками (см.
plan-redesign.md§10): не оставлять устаревшие пути вeslint.config.js(C17 уже двигалroutes/preview/**→routes/**иcomponents/kit/**→components/**; на C19 при наведении порядка вcomponents/paths останутсяcomponents/**+routes/**). - Для
*.svelteвыключенprefer-const(пропсы в Svelte 5 пишутся черезlet). allowDefaultProjectперечисляет test-файлы и фикстуры точечно (glob'ы с**там запрещены tseslint) — при добавлении файлов в__tests__//__fixtures__/дописать их туда же.- design-tokens правила применяются к тем же scoped-путям
(
src/lib/components/**,src/routes/**). - isolation-правило включено на весь
**/*.{ts,svelte}.
Правила плагина design-tokens
design-tokens/no-hardcoded-in-svelte— запрет прямых цветов/размеров/ длительностей/z-index иcolor-mix()(результат токенизировать в CSS). Исключения размеров:0,0px,1px, проценты и unitless (line-height).design-tokens/no-category-mismatch— size-свойство не может использовать color-токен (--color-*/--brand-*) и наоборот.design-tokens/no-token-definition-in-svelte— определение--x:в компоненте не может содержать примитив (hex/rgb/oklch/px/rem); допустимы производные от токенов (var(),calc(), unitless-числа).design-tokens/no-undefined-in-svelte—var(--x)в<style>должен быть определён вsrc/app.css(словарь токенов) или локально в компоненте. Файл читается один раз и кэшируется (неglob-зависим).
(ESLint лезет в <style>-блоки через постпрефес postcss AST от
svelte-eslint-parser; stylelint прогоняется по всем CSS-файлам.)
Правило плагина i18n (i18n/dict-consistency)
Кросс-языковой линтер словарей lib/i18n/ (план: docs/plan-preview-i18n.md
§Фаза 8). Сравнивает каждый словарь со всеми остальными локалями (не только
с BASE). Список локалей берётся из LOCALES в lib/i18n/dict.ts — новый
словарь подхватывается без правки правила.
- warn-only, никогда
error— пропущенный перевод не валит сборку. - Диагностика живёт там, где фикс: каждый файл репортит только свои
расхождения:
- паритет ключей: правило собирает union всех ключей из словарей-соседей (с диска) и для текущего файла репортит те, которых в нём нет — ровно одно предупреждение на разрыв, независимо от числа локалей, где ключ есть; пропавшая целая секция репортится один раз (не по каждому потомку);
- пустые значения:
""и строка из одних пробелов; - паритет плейсхолдеров: у одинакового ключа набор
{name}сравнивается с каноническим (мажоритарный по локалям, при равенстве — первый поLOCALES) — ловит потерянную при переводе переменную ровно один раз, даже когда отклоняется одна-единственная локаль.
- Словари-соседи читаются с диска и кэшируются на процесс линта (как
no-undefined-in-svelte); каждый парсится@typescript-eslint/parser(синтакс, без типов). - Опция
allowPaths: string[]— dot-path ключи, исключаемые из всех проверок (осознанные отклонения до достижения zero-warn). - Тесты:
__tests__/dict-consistency.test.ts, фикстуры-словари в__fixtures__/src/lib/i18n/(en,ru,de— консистентные, zero-warn) — в тестах подаются модифицированные вариантыen.ts/ru.tsпротив эталонных на диске.
Токены-префиксы (целевой словарь дизайна)
- Цвета:
--color-*, бренд--brand-main/--brand-alt— единственные две переменные, которым разрешено быть hex/rgb, остальные цвета — толькоhct()и только в app.css. - Размеры:
--space-*,--text-*(font-size),--radius-*,--size-*. - Брейкпоинты:
@custom-media --bp-mobile (max-width: 640px)/--bp-tablet (800px)/--bp-desktop (1100px)— объявляются в app.css, используются как@media (--bp-*). CSS-переменные в@mediaне работают, поэтому отдельных--bp-*токенов нет; раскрытие делает postcss-плагинpostcss-custom-media(конфигweb/postcss.config.js, определения подтягиваются через@csstools/postcss-global-data). - z-index:
--z-*; длительности/анимации:--duration-*,--ease-*.
Нюансы stylelint:
- Правило
custom-property-patternтестирует паттерн без--(--x→x), аdeclaration-property-value-disallowed-list— целиком с--. custom-property-empty-line-beforeперенастроен (after-custom-propertyвignore, не вexcept): пустые строки между подряд идущими токенами свободны и--fixих не удаляет — можно группировать цветовые и размерные токены в app.css отдельными блоками.
Токен-аудит (check-tokens.mjs)
Лёгкий оркестратор поверх web/scripts/token-audit/*. Проверяет:
- Parity: каждый цветовой токен из
:rootобязан иметь пару в[data-theme="dark"]и наоборот. Производные токены (значение содержитvar(), напр.hct(from var(--...))) из пары исключены — они наследуют тему автоматически. - hct-only для цветов: ЛЮБОЕ цветовое значение в app.css обязано быть
hct(...)— и литерал, и производноеhct(from var(...) h c t). Исключения: только--brand-main/--brand-alt(seed-токены, любая форма) иcolor-mix(...)(единственный легальный способ смешать два токена).oklch()/rgb()/#hexв--color-*запрещены.
Выводит варнинг о неиспользуемых токенах app.css (определены, но нигде не используются) — выход он не меняет. Провалом (exit 1) считаются только parity и hct-авторство.
Тесты кастомных линт-правил
Линт-правила покрыты юнит-тестами (Vitest, test:rules):
__tests__/design-tokens.test.ts— три чистых правила (no-hardcoded-in-svelte,no-category-mismatch,no-token-definition-in-svelte) черезRuleTesterсо строковыми кейсами;no-undefined-in-svelte— черезLinterAPI, т.к. читает словарь токенов из__fixtures__/src/app.css(не из реальногоsrc/app.css).__tests__/no-mixed-imports.test.ts— isolation-правило черезLinterAPI сcwdна__fixtures__: правило резолвит импорты по реальным файлам, поэтому цели импортов обязаны существовать на диске.- Хелпер
__tests__/helpers.tsсобираетLinterсcwd = __fixtures__—process.cwd()не трогается, реальныеsrc/не читаются.
Запуск: pnpm --dir web test:rules (vitest run eslint-plugins/__tests__).
Полный pnpm --dir web test тоже их гоняет.
Фикстуры живут в __fixtures__/ и сами прогоняются линтом (eslint .), поэтому
добавление/правка стабов — тоже работа с валидным кодом.
Isolation-правило (isolation/no-mixed-imports)
Полная взаимная изоляция старого (v1) и нового (корень) UI. В отличие от
no-restricted-imports, правило резолвит каждый импорт (и $lib/..., и
относительные .//../) до реального файла и классифицирует стороны по
фактическому пути, поэтому относительным импортом правило не обойти.
- Старое:
routes/v1/**,lib/v1/**. - Новое:
routes/**безv1/**; корневойlibцеликом (lib/**) безlib/v1/**и без общих пакетов. - Общее (разрешено обоим) — только служебные пакеты:
core/,theme. v1 не высовывается: изlib/v1/**наружу разрешены импорты лишь в общие пакеты и в собственный форк i18n (lib/v1/i18n/**). - Плагин конфигурируем (опции
old/new+root/aliasвeslint.config.js): перенос старых файлов в папкиold/— это правка glob-паттернов в настройке, а не код правила. - Изоляция уже достигнута: старый
lib/v1/registry.ts/registry-helpers.tsне тянутregistry-schema(пилоты add-border/add-stroke работают черезparams),registry.tsне импортируетToolSchema; новыйlist-toolsиспользует$lib/tool-icons.ts, а не старыеlib/v1/tools/tool-icons.ts.
Линтер только показывает ошибки, старый код (непрефиксованные токены, hex в
app_v1.css) — известный техдолг, его НЕ чинить и не игнорировать правилами.