78 lines
3.5 KiB
JavaScript
78 lines
3.5 KiB
JavaScript
import js from '@eslint/js';
|
||
import prettier from 'eslint-config-prettier';
|
||
import reactHooks from 'eslint-plugin-react-hooks';
|
||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||
import globals from 'globals';
|
||
import tseslint from 'typescript-eslint';
|
||
|
||
// Гейт «одно место для цвета и размера», половина в TSX (STACK_DECISIONS §3).
|
||
// CSS-половина — в stylelint.config.js.
|
||
const colorLiteral =
|
||
'Цвет живёт только в src/tokens/tokens.css. В коде — var(--color-...) из .module.css.';
|
||
const inlineStyle =
|
||
'Инлайновый стиль запрещён: размеры и цвета берутся из .module.css на токенах. ' +
|
||
"Единственное исключение — передача CSS-переменной: style={{ '--progress': value }}.";
|
||
|
||
const tokenGate = [
|
||
{ selector: 'Literal[value=/#[0-9a-fA-F]{3}\\b/]', message: colorLiteral },
|
||
{ selector: 'Literal[value=/#[0-9a-fA-F]{6}\\b/]', message: colorLiteral },
|
||
{ selector: 'Literal[value=/#[0-9a-fA-F]{8}\\b/]', message: colorLiteral },
|
||
{ selector: 'Literal[value=/\\b(rgba?|hsla?|oklch|oklab|lab|lch)\\(/]', message: colorLiteral },
|
||
{ selector: 'TemplateElement[value.raw=/#[0-9a-fA-F]{3,8}\\b/]', message: colorLiteral },
|
||
// Ключ-идентификатор — обычное свойство (color, width); CSS-переменная синтаксически
|
||
// обязана быть строковым ключом, поэтому проходит только она.
|
||
{
|
||
selector: "JSXAttribute[name.name='style'] ObjectExpression > Property[key.type='Identifier']",
|
||
message: inlineStyle,
|
||
},
|
||
{
|
||
selector:
|
||
"JSXAttribute[name.name='style'] ObjectExpression > Property[key.type='Literal'][key.value!=/^--/]",
|
||
message: inlineStyle,
|
||
},
|
||
{
|
||
selector: "JSXAttribute[name.name='style'] > JSXExpressionContainer > Identifier",
|
||
message: inlineStyle,
|
||
},
|
||
];
|
||
|
||
export default tseslint.config(
|
||
{ ignores: ['dist/**', '.shots/**', '.tooling/**'] },
|
||
js.configs.recommended,
|
||
tseslint.configs.recommended,
|
||
// .flat — именно flat-вариант: одноимённый ключ верхнего уровня остался в старом формате.
|
||
reactHooks.configs.flat['recommended-latest'],
|
||
reactRefresh.configs.vite,
|
||
{
|
||
languageOptions: {
|
||
globals: { ...globals.browser, ...globals.node },
|
||
},
|
||
rules: {
|
||
'no-restricted-syntax': ['error', ...tokenGate],
|
||
// Глобальных стилей ровно два файла; всё остальное — CSS Modules, они скоупятся сами.
|
||
'no-restricted-imports': [
|
||
'error',
|
||
{
|
||
patterns: [
|
||
{
|
||
group: ['**/*.css', '!**/*.module.css'],
|
||
message:
|
||
'Глобальный CSS импортируется только в src/main.tsx. Экранам и примитивам — *.module.css.',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
// Точка сборки глобальных стилей: сброс, токены и шрифты подключаются здесь и больше нигде.
|
||
files: ['src/main.tsx'],
|
||
rules: { 'no-restricted-imports': 'off' },
|
||
},
|
||
{
|
||
// Контракт-тест обязан называть замеренные цвета в лицо — иначе ему нечего сверять.
|
||
files: ['src/tokens/*.test.ts'],
|
||
rules: { 'no-restricted-syntax': 'off' },
|
||
},
|
||
prettier,
|
||
);
|