15 KiB
ESLint-плагины и дизайн-токены: детали
Эта часть репозитория подробно описана здесь, а не в корневом AGENTS.md:
рутина-разработка её не требует, детали нужны только при правке самих
линт-правил или конфигов.
Структура
web/eslint-plugins/— локальные ESLint-плагины.design-tokens/— правила для<style>-блоков svelte-компонентов.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.__tests__/— тесты аудита и HCT-плагина; подхватываются обычнымvitest runчерезscripts/**/*.test.mjs, поэтому входят вverify.
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) навешаны на UI-код (src/lib/components/**,src/routes/**). Scoped-пути двигаются вместе с папками: не оставлять устаревшие пути вeslint.config.js. - Базовый
js.configs.recommended+globals.nodeнавешаны на инфраструктуру линтинга:eslint-plugins/**/*.jsиscripts/**/*.mjs. TS/svelte-рекомендации туда не подключаются. - Для
*.svelteвыключенprefer-const(пропсы в Svelte 5 пишутся черезlet). allowDefaultProjectперечисляет test-файлы и фикстуры точечно (glob'ы с**там запрещены tseslint) — при добавлении файлов в__tests__//__fixtures__/дописать их туда же.- design-tokens правила применяются к тем же scoped-путям
(
src/lib/components/**,src/routes/**). i18n/no-hardcoded-user-textприменяется к обоим scoped-путям (src/lib/components/**иsrc/routes/**): хардкод в+page.svelteи+layout.svelte— такой же production-UI. Исключение одно —/kit, витрина с намеренно латинскими подписями; выключается отдельным объектом конфига сfiles: ["**/src/routes/kit/**/*.svelte"], потому что негативные паттерны вfilesне сужают область действия правила. ОпцияallowWords— brand не хардкодится в правиле.
Правила плагина 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/)
i18n/dict-consistency
Кросс-языковой линтер словарей lib/i18n/ (историческая модель и завершённые
фазы: docs/archive/plan-preview-i18n.md). Сравнивает каждый словарь со
всеми остальными локалями (не только с BASE). Список локалей берётся из
LOCALES в lib/i18n/dict.ts — новый словарь подхватывается без правки
правила.
- warn-only, никогда
error— пропущенный перевод не валит сборку. - Диагностика живёт там, где фикс: каждый файл репортит только свои
расхождения:
- паритет ключей: правило собирает union всех ключей из словарей-соседей (с диска) и для текущего файла репортит те, которых в нём нет — ровно одно предупреждение на разрыв, независимо от числа локалей, где ключ есть; пропавшая целая секция репортится один раз (не по каждому потомку);
- пустые значения:
""и строка из одних пробелов; - паритет плейсхолдеров: у одинакового ключа набор
{name}сравнивается с каноническим (мажоритарный по локалям, при равенстве —BASE_LOCALE, затемLOCALES) — ловит потерянную при переводе переменную ровно один раз, даже когда отклоняется одна-единственная локаль.
- Словари-соседи читаются с диска и кэшируются на процесс линта (как
no-undefined-in-svelte); каждый парсится@typescript-eslint/parser(синтакс, без типов). - Опция
allowPaths: string[]— dot-path ключи, исключаемые из всех проверок (осознанные отклонения до достижения zero-warn). baseLocaleFallback: string[]— паттерны ключей, которые допустимо отсутствовать только в базовой локали; в текущей конфигурации это registry metadatatools.*.titleиtools.*.description. Пустые значения и placeholders для присутствующих ключей всё равно проверяются.ignoreMissingPatterns: string[]— паттерны ключей, которые временно не проверяются на missing; в текущей конфигурации это неиспользуемыеtools.*.params. Эта опция не отключает empty/placeholder checks.- Тесты:
__tests__/dict-consistency.test.ts, фикстуры-словари в__fixtures__/src/lib/i18n/(en,ru,de— консистентные, zero-warn) — в тестах подаются модифицированные вариантыen.ts/ru.tsпротив эталонных на диске.
i18n/no-hardcoded-user-text
Запрещает пользовательский текст, зашитый в шаблон: литерал в Svelte-тексте или
в строковом значении пользовательского атрибута молча уедет в RU-локаль. Пилот
report-only (warn), baseline — 0 срабатываний.
- Поверхность: узлы
SvelteText(включая текстовые части смешанного контента) и строковые литералы атрибутов изi18n/lists.js(label,title,placeholder,aria-label,alt,description,emptyText,hint,eyebrow,suffix,unit,error). Технические атрибуты (variant,tone,type,icon,size,href,class) не проверяются, значения в скобках ({expr}) тоже — они считаются в другом месте. - Исключения списком, а не «на глаз»: форматы и единицы (
PNG,px,×), имена файлов и версии (result.png,v0.1.0,v1), размеры (512×512), registry-строки и прочие ALL CAPS идентификаторы (BACKGROUND), а также/kit— витрина компонентов с намеренно латинскими подписями (исключение живёт вeslint.config.js, а не в правиле). - Опция
allowWords: string[]— brand и прочие собственные имена. Название продукта передаётся конфигом, а не зашито в правило. - Содержимое
<style>и<script>пропускается: там код, а не копирайт. - Тесты:
__tests__/no-hardcoded-user-text.test.ts—RuleTesterна svelte-eslint-parser плюс прямые кейсыhasUserWords.
Токены-префиксы (целевой словарь дизайна)
- Цвета:
--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-*запрещены. -
Резолв
var(): каждая ссылкаvar(--x)в app.css должна указывать на токен, определённый в этом же файле.var(--x, fallback)не считается ошибкой — fallback легален без определения. Провал (exit 1), потому что несуществующий токен молча роняет объявление. -
Неиспользуемые токены: выводится варнинг (определены, но нигде не используются) — выход он не меняет. Проверка видит только ссылки
var(--x), поэтому чтение токена из JS (getPropertyValue) она не учтёт; решение оставить проверку неблокирующей зафиксировано вdocs/quality-gates.md. Провалами (exit 1) считаются parity, hct-авторство и резолвvar().
Тесты кастомных линт-правил
Линт-правила покрыты юнит-тестами (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__/helpers.tsсобираетLinterсcwd = __fixtures__—process.cwd()не трогается, реальныеsrc/не читаются.
Запуск: pnpm --dir web test:rules (vitest run eslint-plugins/__tests__).
Полный pnpm --dir web test тоже их гоняет.
Фикстуры живут в __fixtures__/ и сами прогоняются линтом (eslint .), поэтому
добавление/правка стабов — тоже работа с валидным кодом.