mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
chore: update lint rules
This commit is contained in:
@@ -72,6 +72,9 @@
|
|||||||
`web/src/lib/components/`, даже «просто div с двумя стилями».
|
`web/src/lib/components/`, даже «просто div с двумя стилями».
|
||||||
- Дизайн-токены (цвета, размеры, длительности, z-index) — только из
|
- Дизайн-токены (цвета, размеры, длительности, z-index) — только из
|
||||||
CSS-переменных; детали ограничений — в `web/eslint-plugins/README.md`.
|
CSS-переменных; детали ограничений — в `web/eslint-plugins/README.md`.
|
||||||
|
- Новый токен появляется вместе с потребителем: `pnpm --dir web lint:tokens`
|
||||||
|
сообщает о неиспользуемых токенах, и это не блокирует CI только потому, что
|
||||||
|
проверка оставлена предупреждением.
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
|
|||||||
@@ -452,15 +452,53 @@ file-result-инструментам.
|
|||||||
неопределённого глобала в `scripts/` дают ошибки, то есть правило применилось,
|
неопределённого глобала в `scripts/` дают ошибки, то есть правило применилось,
|
||||||
а не молча пропустило файлы.
|
а не молча пропустило файлы.
|
||||||
|
|
||||||
#### Q7d. Оставшиеся направления
|
#### Q7d. Решения по оставшимся вопросам
|
||||||
|
|
||||||
|
**Статус:** закрыт тремя решениями.
|
||||||
|
|
||||||
|
1. **Обычный CSS под те же токен-правила, что Svelte styles — узко.** В `src`
|
||||||
|
всего один CSS-файл, и он же словарь токенов, поэтому переносить туда
|
||||||
|
`no-hardcoded-in-svelte` и подобные нечего: stylelint уже проверяет
|
||||||
|
`src/**/*.css` (префиксы, запрет `!important`, цвет только у brand-токенов),
|
||||||
|
а `check-tokens.mjs` дублирует проверку цвета. Реальный пробел был один —
|
||||||
|
`var()` может ссылаться на несуществующий токен, — его закрыл новый модуль
|
||||||
|
`web/scripts/token-audit/refs.mjs` (см. Q7e). Широкую унификацию механизмов
|
||||||
|
решено не делать: это лишний слой поверх дублирующихся проверок.
|
||||||
|
2. **Конфиги под рекомендации — только JS.** `*.config.js`
|
||||||
|
(`eslint`/`postcss`/`stylelint`) добавлены в `toolingFiles`; по измерению они
|
||||||
|
дают 0 проблем, поэтому это бесплатно. TS-конфиги (`vite`, `vitest`,
|
||||||
|
`playwright`) не подключены: им потребовался бы `tseslint`-парсер и запись
|
||||||
|
`vite.config.ts` в `allowDefaultProject`, а выигрыш близок к нулю. Попутно
|
||||||
|
удалена мёртвая запись `svelte.config.js` в `allowDefaultProject` — такого
|
||||||
|
файла нет.
|
||||||
|
3. **Unused-аудит остаётся предупреждением.** Правило «новый токен — вместе с
|
||||||
|
потребителем» закреплено в `AGENTS.md`. Причина: `verify` входит в
|
||||||
|
deploy-workflow, поэтому стилевой долг блокировал бы деплой сайта; кроме
|
||||||
|
того, чтение токена из JS (`getPropertyValue`) аудит не видит и дал бы ложное
|
||||||
|
срабатывание. Baseline чистый, пыль вычищена в Q7a.
|
||||||
|
|
||||||
|
#### Q7e. Проверка резолва `var()` в `app.css`
|
||||||
|
|
||||||
|
**Статус:** выполнен. Аудит падает, если `var(--x)` в `app.css` указывает на
|
||||||
|
токен, которого нет в этом же файле.
|
||||||
|
|
||||||
|
- `web/scripts/token-audit/refs.mjs` + вызов в `check-tokens.mjs`; режим —
|
||||||
|
ошибка, а не предупреждение, потому что это дефект (объявление молча
|
||||||
|
отбрасывается), а не стилистический долг.
|
||||||
|
- `var(--x, fallback)` не считается ошибкой: fallback легален без определения
|
||||||
|
токена.
|
||||||
|
- 7 тестов в `scripts/__tests__/token-audit.test.mjs`: резолв в обеих темах,
|
||||||
|
опечатка с локацией, fallback, дедуп повторов, ссылка внутри определения
|
||||||
|
токена, `var()` в обычном свойстве.
|
||||||
|
- Проверено на живом файле: временная опечатка `var(--color-bordr)` дала
|
||||||
|
`Unresolved var() references` и код выхода 1; `app.css` восстановлен байт в
|
||||||
|
байт.
|
||||||
|
|
||||||
|
#### Q7f. Оставшиеся направления
|
||||||
|
|
||||||
- Отдельно решить, должен ли обычный CSS проходить проверку undefined tokens и
|
|
||||||
hardcoded colors наравне со Svelte styles.
|
|
||||||
- Решить, должны ли сами конфиги (`eslint.config.js`, `postcss.config.js`,
|
|
||||||
`stylelint.config.js`, `svelte.config.js`, `vite.config.ts`) тоже попасть под
|
|
||||||
рекомендации: сейчас вне `toolingFiles`.
|
|
||||||
- Решить, должен ли unused audit стать fail-режимом (см. Q7a).
|
|
||||||
- Новые правила подключать только после fixtures, `test:rules` и baseline.
|
- Новые правила подключать только после fixtures, `test:rules` и baseline.
|
||||||
|
- Если в коде появится чтение токенов из JS, `collectUsedTokens` придётся
|
||||||
|
расширить — иначе unused-аудит начнёт считать используемые токены пылью.
|
||||||
|
|
||||||
### Q8. Новые автоматические правила
|
### Q8. Новые автоматические правила
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,11 @@ per-project лимит воркеров, а browser matrix упирается в
|
|||||||
- `errors` завершают соответствующий инструмент ненулевым кодом.
|
- `errors` завершают соответствующий инструмент ненулевым кодом.
|
||||||
- После Q2 ESLint запускается с `--max-warnings=0`: новые ESLint warnings
|
- После Q2 ESLint запускается с `--max-warnings=0`: новые ESLint warnings
|
||||||
блокируют gate.
|
блокируют gate.
|
||||||
- Token audit оставляет unused-token сообщение неблокирующим: после Q7 пыль
|
- Token audit оставляет unused-token сообщение неблокирующим: после Q7a пыль
|
||||||
вычищена, но решение о fail-режиме остаётся открытым.
|
вычищена, решение о fail-режиме зафиксировано в Q7d — новый токен появляется
|
||||||
|
вместе с потребителем, но стиль не блокирует деплой.
|
||||||
|
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
|
||||||
|
объявление, поэтому этот check failing, в отличие от unused.
|
||||||
- `lint:all` сейчас чист: 0 errors, 0 i18n warnings, 0 unused tokens.
|
- `lint:all` сейчас чист: 0 errors, 0 i18n warnings, 0 unused tokens.
|
||||||
- `dict-consistency` использует base-locale fallback для registry metadata и
|
- `dict-consistency` использует base-locale fallback для registry metadata и
|
||||||
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
|
pattern-based policy для временно неиспользуемых `tools.*.params`; это не
|
||||||
|
|||||||
@@ -134,9 +134,15 @@
|
|||||||
`color-mix(...)` (единственный легальный способ смешать два токена).
|
`color-mix(...)` (единственный легальный способ смешать два токена).
|
||||||
`oklch()/rgb()/#hex` в `--color-*` запрещены.
|
`oklch()/rgb()/#hex` в `--color-*` запрещены.
|
||||||
|
|
||||||
Выводит **варнинг** о неиспользуемых токенах app.css (определены, но нигде не
|
- **Резолв `var()`**: каждая ссылка `var(--x)` в app.css должна указывать на
|
||||||
используются) — выход он не меняет. Провалом (exit 1) считаются только parity и
|
токен, определённый в этом же файле. `var(--x, fallback)` не считается ошибкой
|
||||||
hct-авторство.
|
— fallback легален без определения. Провал (exit 1), потому что несуществующий
|
||||||
|
токен молча роняет объявление.
|
||||||
|
- **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не
|
||||||
|
используются) — выход он не меняет. Решение оставить это неблокирующим
|
||||||
|
зафиксировано в `docs/plan-codebase-quality.md` (Q7d): правило «новый токен —
|
||||||
|
вместе с потребителем» живёт в `AGENTS.md`. Провалами (exit 1) считаются
|
||||||
|
parity, hct-авторство и резолв `var()`.
|
||||||
|
|
||||||
## Тесты кастомных линт-правил
|
## Тесты кастомных линт-правил
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ import i18nPlugin from "./eslint-plugins/i18n/index.js";
|
|||||||
// Новый код редизайна: к нему применяем полные recommended-наборы уже сейчас.
|
// Новый код редизайна: к нему применяем полные recommended-наборы уже сейчас.
|
||||||
const newCode = ["**/src/lib/components/**", "**/src/routes/**"];
|
const newCode = ["**/src/lib/components/**", "**/src/routes/**"];
|
||||||
|
|
||||||
// Инфраструктура линтинга: сами плагины и скрипты. Это не продуктовый код, но
|
// Инфраструктура линтинга: сами плагины, скрипты и конфиги. Это не
|
||||||
// он влияет на все гейты, поэтому к нему тоже применяется базовый JS
|
// продуктовый код, но всё это влияет на гейты, поэтому к нему тоже
|
||||||
// recommended. Полные TS/svelte-наборы здесь не подключаются: файлы .mjs и
|
// применяется базовый JS recommended. Полные TS/svelte-наборы здесь не
|
||||||
// конфиги проверяются только базовым набором.
|
// подключаются: файлы .mjs и .js проверяются только базовым набором.
|
||||||
const toolingFiles = ["**/eslint-plugins/**/*.js", "**/scripts/**/*.mjs"];
|
const toolingFiles = [
|
||||||
|
"**/eslint-plugins/**/*.js",
|
||||||
|
"**/scripts/**/*.mjs",
|
||||||
|
"*.config.js",
|
||||||
|
];
|
||||||
|
|
||||||
// Полные recommended-наборы — только на новый код (см. ниже, блок перед prettier).
|
// Полные recommended-наборы — только на новый код (см. ниже, блок перед prettier).
|
||||||
const jsRecommended = Array.isArray(js.configs.recommended)
|
const jsRecommended = Array.isArray(js.configs.recommended)
|
||||||
@@ -59,7 +63,6 @@ export default tseslint.config(
|
|||||||
allowDefaultProject: [
|
allowDefaultProject: [
|
||||||
"vitest.config.ts",
|
"vitest.config.ts",
|
||||||
"eslint.config.js",
|
"eslint.config.js",
|
||||||
"svelte.config.js",
|
|
||||||
"playwright.config.ts",
|
"playwright.config.ts",
|
||||||
"e2e/helpers/fixtures.ts",
|
"e2e/helpers/fixtures.ts",
|
||||||
"e2e/helpers/page.ts",
|
"e2e/helpers/page.ts",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
isDerived,
|
isDerived,
|
||||||
} from "../token-audit/helpers.mjs";
|
} from "../token-audit/helpers.mjs";
|
||||||
import { checkParity } from "../token-audit/parity.mjs";
|
import { checkParity } from "../token-audit/parity.mjs";
|
||||||
|
import { checkUnresolvedRefs } from "../token-audit/refs.mjs";
|
||||||
import { checkUnused } from "../token-audit/unused.mjs";
|
import { checkUnused } from "../token-audit/unused.mjs";
|
||||||
|
|
||||||
function themes(css) {
|
function themes(css) {
|
||||||
@@ -134,6 +135,75 @@ describe("checkUnused", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("checkUnresolvedRefs", () => {
|
||||||
|
it("accepts references to tokens defined in the same file", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
":root { --color-text: #111; .x { color: var(--color-text); } }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts references defined in the other theme block", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
[
|
||||||
|
":root { --color-text: #111; }",
|
||||||
|
'[data-theme="dark"] { --color-text: #eee; }',
|
||||||
|
".x { color: var(--color-text); }",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a typo in a token name with its location", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
":root { --color-text: #111; .x { border-color: var(--color-bordr); } }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([
|
||||||
|
" --color-bordr used in .x (border-color) is not defined in app.css",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exempts a reference that provides a fallback value", () => {
|
||||||
|
const { root } = themes(".x { color: var(--color-missing, #fff); }");
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an undefined reference without a fallback even once", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
[
|
||||||
|
":root { --a: 1px; --b: 1px; }",
|
||||||
|
".x { border-color: var(--ghost); outline-color: var(--ghost); }",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([
|
||||||
|
" --ghost used in .x (border-color) is not defined in app.css",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks references inside token definitions themselves", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
":root { --color-text: hct(from var(--brand-typo) h c t); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([
|
||||||
|
" --brand-typo used in :root (--color-text) is not defined in app.css",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores non-custom properties that use var()", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
".x { --local: 1px; width: calc(var(--local) * 2); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnresolvedRefs(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("token predicates", () => {
|
describe("token predicates", () => {
|
||||||
it("detects color values in every supported notation", () => {
|
it("detects color values in every supported notation", () => {
|
||||||
for (const value of [
|
for (const value of [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { FILE, parsePreview } from "./token-audit/helpers.mjs";
|
import { FILE, parsePreview } from "./token-audit/helpers.mjs";
|
||||||
import { checkParity } from "./token-audit/parity.mjs";
|
import { checkParity } from "./token-audit/parity.mjs";
|
||||||
import { checkColorAuthorship } from "./token-audit/colors.mjs";
|
import { checkColorAuthorship } from "./token-audit/colors.mjs";
|
||||||
|
import { checkUnresolvedRefs } from "./token-audit/refs.mjs";
|
||||||
import { checkUnused } from "./token-audit/unused.mjs";
|
import { checkUnused } from "./token-audit/unused.mjs";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -34,6 +35,16 @@ async function main() {
|
|||||||
console.log("All colors are authored as hct().");
|
console.log("All colors are authored as hct().");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unresolved = checkUnresolvedRefs(root);
|
||||||
|
if (unresolved.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`Unresolved var() references in ${FILE}:\n${unresolved.join("\n")}`,
|
||||||
|
);
|
||||||
|
failed = true;
|
||||||
|
} else {
|
||||||
|
console.log("All var() references in app.css resolve.");
|
||||||
|
}
|
||||||
|
|
||||||
const unused = checkUnused(root);
|
const unused = checkUnused(root);
|
||||||
if (unused.length > 0) {
|
if (unused.length > 0) {
|
||||||
console.log("\nUnused tokens (defined in app.css, never used):");
|
console.log("\nUnused tokens (defined in app.css, never used):");
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Unresolved references: every var(--x) in app.css must point at a token the
|
||||||
|
// same file defines. A typo here silently drops the declaration, so unlike the
|
||||||
|
// unused-token dust it is a defect, not a style choice.
|
||||||
|
//
|
||||||
|
// `var(--x, fallback)` is legal without --x (CSS custom property fallback), so
|
||||||
|
// those references are exempt.
|
||||||
|
|
||||||
|
const VAR_REF_RE = /var\(\s*(--[\w-]+)\s*([,)])/g;
|
||||||
|
|
||||||
|
export function checkUnresolvedRefs(root) {
|
||||||
|
const defined = new Set();
|
||||||
|
root.walkDecls((decl) => {
|
||||||
|
if (decl.prop.startsWith("--")) defined.add(decl.prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const seen = new Set();
|
||||||
|
root.walkDecls((decl) => {
|
||||||
|
for (const match of decl.value.matchAll(VAR_REF_RE)) {
|
||||||
|
const [, token, terminator] = match;
|
||||||
|
// terminator ")" after whitespace means no fallback argument
|
||||||
|
if (defined.has(token)) continue;
|
||||||
|
if (terminator === ",") continue;
|
||||||
|
if (seen.has(token)) continue;
|
||||||
|
seen.add(token);
|
||||||
|
const where = decl.parent?.selector || decl.parent?.name || "";
|
||||||
|
errors.push(
|
||||||
|
` ${token} used in ${where} (${decl.prop}) is not defined in app.css`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user