mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
Compare commits
10
Commits
0073d3d4cc
...
fcf4d8156b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf4d8156b | ||
|
|
b59a434c65 | ||
|
|
7bca9f3a3a | ||
|
|
6a44a3d018 | ||
|
|
67fdef4363 | ||
|
|
bd941d5ac5 | ||
|
|
8801c3dd91 | ||
|
|
e2bd786d46 | ||
|
|
73bcbcc26a | ||
|
|
ad01ae92d4 |
@@ -1,6 +1,9 @@
|
|||||||
name: Quality
|
name: Quality
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ refs/public/placeholder-logo.png
|
|||||||
refs/public/placeholder.svg
|
refs/public/placeholder.svg
|
||||||
web/audit/cdp-audit*.txt
|
web/audit/cdp-audit*.txt
|
||||||
css-lint.txt
|
css-lint.txt
|
||||||
|
web/coverage
|
||||||
|
|
||||||
# Локальные планы с приватной информацией (выбранное имя домена) — не коммитить
|
# Локальные планы с приватной информацией (выбранное имя домена) — не коммитить
|
||||||
docs/plan-domain.local.md
|
docs/plan-domain.local.md
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`). Не использовать
|
корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`). Не использовать
|
||||||
npm.
|
npm.
|
||||||
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web check`, тесты:
|
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web check`, тесты:
|
||||||
`pnpm --dir web test` (Vitest), тесты lint-правил:
|
`pnpm --dir web test` (Vitest), тесты с coverage-гейтом:
|
||||||
|
`pnpm --dir web test:coverage`, тесты lint-правил:
|
||||||
`pnpm --dir web test:rules`, e2e: `pnpm --dir web test:e2e` (Playwright,
|
`pnpm --dir web test:rules`, e2e: `pnpm --dir web test:e2e` (Playwright,
|
||||||
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт
|
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт
|
||||||
4173).
|
4173).
|
||||||
@@ -18,12 +19,11 @@
|
|||||||
(docs) + `pnpm --dir web exec prettier --check .` (код).
|
(docs) + `pnpm --dir web exec prettier --check .` (код).
|
||||||
- Полный быстрый gate: `pnpm verify`; подробная матрица команд и CI —
|
- Полный быстрый gate: `pnpm verify`; подробная матрица команд и CI —
|
||||||
`docs/quality-gates.md`.
|
`docs/quality-gates.md`.
|
||||||
- После пуша результат CI проверяется явно: смотреть и blocking `verify`, и
|
- После пуша результат CI проверяется явно: смотреть статусы отдельных job, а не
|
||||||
non-blocking `e2e` — если тот job вообще запускался. `Quality` workflow живёт
|
общий conclusion прогона. У `e2e` стоит `continue-on-error`, поэтому прогон с
|
||||||
на `pull_request` и `workflow_dispatch`, поэтому при прямом пуше в `main`
|
упавшим e2e помечается как `success`. `Quality` workflow запускается на каждом
|
||||||
выполняется только `Deploy to GitHub Pages` со своим `verify`, а browser
|
пуше в `main` и на `pull_request`, поэтому browser matrix проверяется сама;
|
||||||
matrix проверяется на PR или ручным запуском. Красный `e2e` не блокирует PR,
|
ручной запуск нужен только для старого коммита.
|
||||||
но остаётся падением — сообщать его фактический статус.
|
|
||||||
- Линтинг: `pnpm --dir web lint` (ESLint). Полный прогон дизайн-проверок:
|
- Линтинг: `pnpm --dir web lint` (ESLint). Полный прогон дизайн-проверок:
|
||||||
`pnpm --dir web lint:all` (ESLint + stylelint + токен-аудит). Каждый шаг
|
`pnpm --dir web lint:all` (ESLint + stylelint + токен-аудит). Каждый шаг
|
||||||
выполняется, даже если предыдущий упал. Новые изменения не должны добавлять
|
выполняется, даже если предыдущий упал. Новые изменения не должны добавлять
|
||||||
@@ -61,10 +61,46 @@
|
|||||||
нескольких местах и запутывает. (Синтаксис самих определений — под линтером:
|
нескольких местах и запутывает. (Синтаксис самих определений — под линтером:
|
||||||
строка `conventions/no-string-union-alias` и т.д.)
|
строка `conventions/no-string-union-alias` и т.д.)
|
||||||
|
|
||||||
|
### Границы изменений
|
||||||
|
|
||||||
|
- Production — только текущие маршруты и `web/src/lib/components/**`. Старый
|
||||||
|
UI (`/v1/**`) удалён и не возвращается: без redirects и совместимого режима;
|
||||||
|
исторические упоминания живят только в `docs/archive/`. Будущий API-контракт
|
||||||
|
`POST /v1/process` из `docs/plan-platform.md` не связан с удалённым UI-v1.
|
||||||
|
- `core/` и `theme/` — общие модули: их изменение проверяется unit-тестами и
|
||||||
|
smoke-сценариями production-потребителей.
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
|
||||||
|
- Новый пользовательский текст — только через словари и `t()`. Исключения:
|
||||||
|
бренд, форматы и имена файлов, технические значения, registry source strings и
|
||||||
|
`/kit`. Проверяется warn-правилом `i18n/no-hardcoded-user-text`, детали и
|
||||||
|
список исключений — в `web/eslint-plugins/README.md`.
|
||||||
|
- Новые ключи добавляются во **все** локали сразу: паритет ключей и
|
||||||
|
плейсхолдеров проверяет `i18n/dict-consistency`.
|
||||||
|
|
||||||
|
### Комментарии
|
||||||
|
|
||||||
|
- Комментарий объясняет «почему», а не пересказывает код.
|
||||||
|
- `TODO`/`FIXME` — только зарегистрированные задачи: ссылка на `docs/backlog.md`
|
||||||
|
или issue и критерий закрытия. В коде незарегистрированных пометок не
|
||||||
|
оставляем.
|
||||||
|
|
||||||
|
### Новые lint-правила
|
||||||
|
|
||||||
|
Новое правило сначала появляется в report-only/warn-only режиме, получает
|
||||||
|
fixtures и тесты (`test:rules`), и только после очистки baseline может стать
|
||||||
|
blocking. Учитывай: `lint:all` гоняет ESLint с `--max-warnings=0`, поэтому
|
||||||
|
warn-правило обязано приземлиться без срабатываний на текущем коде.
|
||||||
|
|
||||||
|
|
||||||
### Дизайн
|
### Дизайн
|
||||||
|
|
||||||
Описание нового визуального языка — в `docs/plan-redesign.md`. Общие правила,
|
Источник правды по визуальному языку — сам код: `web/src/app.css` (дизайн-токены
|
||||||
которые линтер не проверяет:
|
и глобальные стили), `web/src/lib/components/**` (компоненты и локальные стили)
|
||||||
|
и витрина `/kit`. Приёмка — `docs/checklist-manual-testing.md`, ограничения
|
||||||
|
линтера — `web/eslint-plugins/README.md`. Общие правила, которые линтер не
|
||||||
|
проверяет:
|
||||||
|
|
||||||
- Новый дизайн живёт в `web/src/app.css` и применяется ко всем
|
- Новый дизайн живёт в `web/src/app.css` и применяется ко всем
|
||||||
production-маршрутам.
|
production-маршрутам.
|
||||||
@@ -72,14 +108,17 @@
|
|||||||
`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 только потому, что
|
||||||
|
проверка оставлена предупреждением.
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- Здесь держать только обязательные инварианты, команды и ссылки на подробные
|
- Здесь держать только обязательные инварианты, команды и ссылки на подробные
|
||||||
документы; не превращать `AGENTS.md` в полный справочник.
|
документы; не превращать `AGENTS.md` в полный справочник.
|
||||||
- Индекс активных документов: `docs/README.md`; подробный workflow:
|
- Индекс активных документов: `docs/README.md`; подробный workflow:
|
||||||
`docs/agent-workflow.md`; quality gates: `docs/quality-gates.md`; текущий
|
`docs/agent-workflow.md`; quality gates: `docs/quality-gates.md`; тестовая
|
||||||
план: `docs/plan-codebase-quality.md`.
|
архитектура: `docs/testing-strategy.md`.
|
||||||
- Новые подробные правила, процедуры и таблицы хранить в `docs/`, а детали
|
- Новые подробные правила, процедуры и таблицы хранить в `docs/`, а детали
|
||||||
конкретных lint-правил — в `web/eslint-plugins/README.md`.
|
конкретных lint-правил — в `web/eslint-plugins/README.md`.
|
||||||
- Завершённые или устаревшие документы переносить в `docs/archive/`, указывать
|
- Завершённые или устаревшие документы переносить в `docs/archive/`, указывать
|
||||||
|
|||||||
+24
-14
@@ -8,23 +8,19 @@
|
|||||||
|
|
||||||
1. `AGENTS.md` — команды и обязательные инварианты.
|
1. `AGENTS.md` — команды и обязательные инварианты.
|
||||||
2. Этот файл — выбрать актуальный документ по теме.
|
2. Этот файл — выбрать актуальный документ по теме.
|
||||||
3. `docs/plan-codebase-quality.md` — текущий план приведения качества в порядок
|
3. Соответствующий план или checklist темы.
|
||||||
и последовательность следующих шагов.
|
4. Исходники, конфигурация и тесты рядом с изменяемым кодом.
|
||||||
4. Соответствующий план или checklist темы.
|
|
||||||
5. Исходники, конфигурация и тесты рядом с изменяемым кодом.
|
|
||||||
|
|
||||||
## Активные документы
|
## Активные документы
|
||||||
|
|
||||||
| Документ | Роль | Статус |
|
| Документ | Роль | Статус |
|
||||||
| ---------------------------------- | ---------------------------------------------------- | ------ |
|
| ---------------------------------- | ---------------------------------------------------- | ------ |
|
||||||
| `AGENTS.md` | Краткий обязательный контракт для агента | active |
|
| `AGENTS.md` | Краткий обязательный контракт для агента | active |
|
||||||
| `docs/plan-codebase-quality.md` | План quality gates, тестов и lint-правил | draft |
|
|
||||||
| `docs/agent-workflow.md` | Подробный порядок исследования, изменения и проверки | active |
|
| `docs/agent-workflow.md` | Подробный порядок исследования, изменения и проверки | active |
|
||||||
| `docs/testing-strategy.md` | Locator, fixture, i18n и fixme-policy для e2e | active |
|
| `docs/testing-strategy.md` | Locator, fixture, i18n и fixme-policy для e2e | active |
|
||||||
| `docs/quality-gates.md` | Команды проверки, warnings и CI | active |
|
| `docs/quality-gates.md` | Команды проверки, warnings и CI | active |
|
||||||
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
|
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
|
||||||
| `docs/roadmap.md` | Общие направления развития | active |
|
| `docs/roadmap.md` | Общие направления развития | active |
|
||||||
| `docs/plan-redesign.md` | Новый дизайн production UI | active |
|
|
||||||
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
||||||
| `docs/plan-platform.md` | PWA/API/WASM-план | active |
|
| `docs/plan-platform.md` | PWA/API/WASM-план | active |
|
||||||
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
||||||
@@ -47,26 +43,40 @@
|
|||||||
`web/eslint-plugins/README.md` и исходниках правил.
|
`web/eslint-plugins/README.md` и исходниках правил.
|
||||||
- Продуктовые решения и ручная приёмка — в backlog, плане и checklist.
|
- Продуктовые решения и ручная приёмка — в backlog, плане и checklist.
|
||||||
|
|
||||||
|
У документа должен быть один источник истины: остальные файлы ссылаются на него,
|
||||||
|
а не пересказывают. Подробные материалы хранятся в `docs/`, чтобы не засорять
|
||||||
|
корень репозитория. Не создавать отдельные `docs/commands.md`,
|
||||||
|
`docs/known-issues.md` или копию backlog: команды принадлежат `quality-gates`,
|
||||||
|
известные проблемы — backlog и checklist. `README.md` в корне остаётся кратким
|
||||||
|
обзором продукта, а не вторым руководством агента.
|
||||||
|
|
||||||
|
Новый документ создаётся только когда темы нет в существующем файле; иначе
|
||||||
|
обновляется источник истины. Активный документ имеет статус (`draft`, `active`,
|
||||||
|
`completed`) и понятную область применения.
|
||||||
|
|
||||||
Подробные материалы хранятся в `docs/`, а не в корне. Новый файл создаётся
|
Подробные материалы хранятся в `docs/`, а не в корне. Новый файл создаётся
|
||||||
только если темы нет в существующем документе; иначе обновляется источник
|
только если темы нет в существующем документе; иначе обновляется источник
|
||||||
истины.
|
истины.
|
||||||
|
|
||||||
## Текущий baseline Q6f.3
|
## Текущий baseline
|
||||||
|
|
||||||
На момент фиксации Q6f.3:
|
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
- production registry содержит 122 инструмента;
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
||||||
- `pnpm --dir web test` — 43 test files / 465 tests;
|
- `pnpm --dir web test` — 46 test files / 534 tests, включая тесты token audit,
|
||||||
- `pnpm --dir web test:rules` — 4 test files / 74 tests;
|
PostCSS HCT-плагина и правил линтинга;
|
||||||
- `pnpm --dir web lint:all` — 0 errors и 0 i18n warnings; остаются 4 unused
|
- `pnpm --dir web test:rules` — 5 test files / 104 tests;
|
||||||
design tokens от token audit;
|
- `pnpm --dir web lint:all` — 0 errors, 0 i18n warnings и 0 unused design tokens
|
||||||
|
от token audit;
|
||||||
|
- coverage `src/lib` — statements 91.77%, branches 83.3%, functions 74.31%,
|
||||||
|
lines 92.53% при порогах 90 / 82 / 72 / 91;
|
||||||
- `pnpm verify` проходит;
|
- `pnpm verify` проходит;
|
||||||
- `pnpm --dir web test:e2e` — 276 тестов в четырёх проектах: 276 passed, 0
|
- `pnpm --dir web test:e2e` — 276 тестов в четырёх проектах: 276 passed, 0
|
||||||
skipped, 0 failed. Отказ декодера битого PNG зафиксирован per-engine в
|
skipped, 0 failed. Отказ декодера битого PNG зафиксирован per-engine в
|
||||||
`web/e2e/png-fixtures.spec.ts`.
|
`web/e2e/png-fixtures.spec.ts`.
|
||||||
|
|
||||||
Baseline и следующий шаг поддерживаются в `docs/plan-codebase-quality.md`.
|
Состав проверок и их семантика описаны в `docs/quality-gates.md`, тестовая
|
||||||
|
архитектура — в `docs/testing-strategy.md`. Baseline обновляется здесь при
|
||||||
|
изменении любого из гейтов.
|
||||||
|
|
||||||
## Правила актуальности
|
## Правила актуальности
|
||||||
|
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ E2E запускается отдельно от быстрых проверок
|
|||||||
|
|
||||||
Коммит и пуш делает разработчик. После пуша агент проверяет результат CI и
|
Коммит и пуш делает разработчик. После пуша агент проверяет результат CI и
|
||||||
сообщает его вместе с локальными проверками, включая случай, когда нужный job не
|
сообщает его вместе с локальными проверками, включая случай, когда нужный job не
|
||||||
запускался: `Quality` workflow с browser matrix живёт на `pull_request` и
|
запускался: статусы смотрятся по отдельным job, потому что общий conclusion
|
||||||
`workflow_dispatch`, поэтому прямой пуш в `main` проверяет только быстрый gate.
|
прогона с `continue-on-error` у `e2e` остаётся зелёным. Процедура описана в
|
||||||
Процедура описана в `docs/quality-gates.md`.
|
`docs/quality-gates.md`.
|
||||||
|
|
||||||
Следующий шаг начинается только после ревью текущего изменения.
|
Следующий шаг начинается только после ревью текущего изменения.
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
# План: линтеры, тесты и качество кодовой базы
|
# План: линтеры, тесты и качество кодовой базы
|
||||||
|
|
||||||
> Статус: **draft — Q0–Q5 и Q6a–Q6f.3 готовы к ревью; Q6f.4 — следующий этап**.
|
> Статус: **archived** Архивирован: 2026-09-26 Удалить после: 2026-10-26
|
||||||
|
> Причина: все этапы Q0–Q8 выполнены или закрыты решениями; рабочие части
|
||||||
|
> перенесены в `docs/quality-gates.md`, `docs/testing-strategy.md`,
|
||||||
|
> `docs/agent-workflow.md`, `AGENTS.md`, `web/eslint-plugins/README.md` и
|
||||||
|
> `docs/backlog.md`. Использование: только историческая справка
|
||||||
|
>
|
||||||
|
> Выполнены Q0–Q5, Q6a–Q6f.5, Q7a–Q7e и Q8.1–Q8.3; открытых шагов не осталось.
|
||||||
>
|
>
|
||||||
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
||||||
> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6f.3 выполнены; Q6f.4–Q8 выполняются
|
> инфраструктурных скриптов. Q0–Q5, Q6a–Q6f.5, Q7a–Q7e и Q8.1–Q8.3 выполнены.
|
||||||
> отдельными атомарными шагами.
|
> Решения, важные за пределами плана, живут в `docs/quality-gates.md`,
|
||||||
|
> `docs/testing-strategy.md` и `AGENTS.md`.
|
||||||
>
|
>
|
||||||
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
||||||
> проверок, стабилизировать тестовые контракты и только после этого расширять
|
> проверок, стабилизировать тестовые контракты и только после этого расширять
|
||||||
@@ -43,8 +50,8 @@
|
|||||||
|
|
||||||
- `pnpm verify` объединяет typecheck, unit, rule-тесты, lint, форматирование,
|
- `pnpm verify` объединяет typecheck, unit, rule-тесты, lint, форматирование,
|
||||||
docs и production build; deploy выполняет его перед сборкой.
|
docs и production build; deploy выполняет его перед сборкой.
|
||||||
- `i18n/dict-consistency` даёт 0 warnings на текущем baseline; отдельный debt —
|
- `i18n/dict-consistency` даёт 0 warnings на текущем baseline; после Q7 вычищены
|
||||||
4 unused design tokens от token audit.
|
4 неиспользуемых design token, token audit тоже чист.
|
||||||
- E2E-контракт использует semantic locators и общий source-aware schema
|
- E2E-контракт использует semantic locators и общий source-aware schema
|
||||||
resolver; активных known-issue `test.fixme` нет.
|
resolver; активных known-issue `test.fixme` нет.
|
||||||
- `ToolError` хранится как key/vars и переводится только при выводе; смена
|
- `ToolError` хранится как key/vars и переводится только при выводе; смена
|
||||||
@@ -112,7 +119,8 @@ pnpm --dir web test:e2e
|
|||||||
- Добавить отдельный workflow для pull request: typecheck, unit, rule-тесты,
|
- Добавить отдельный workflow для pull request: typecheck, unit, rule-тесты,
|
||||||
lint, format и build.
|
lint, format и build.
|
||||||
- E2E сначала запускать отдельным job; сделать его blocking после закрытия
|
- E2E сначала запускать отдельным job; сделать его blocking после закрытия
|
||||||
известных ошибок и стабилизации baseline.
|
известных ошибок и стабилизации baseline. Условия перехода и текущая политика
|
||||||
|
описаны в `docs/quality-gates.md`, раздел «Политика e2e в CI».
|
||||||
|
|
||||||
**Готово, когда:** одна команда воспроизводит все быстрые проверки, а deploy не
|
**Готово, когда:** одна команда воспроизводит все быстрые проверки, а deploy не
|
||||||
может пройти при type/unit/lint/build-ошибке.
|
может пройти при type/unit/lint/build-ошибке.
|
||||||
@@ -132,8 +140,8 @@ pnpm --dir web test:e2e
|
|||||||
option-группы из `ru.ts`.
|
option-группы из `ru.ts`.
|
||||||
- Исправлен tie-break placeholder parity: при равенстве голосов выбирается
|
- Исправлен tie-break placeholder parity: при равенстве голосов выбирается
|
||||||
`BASE_LOCALE`.
|
`BASE_LOCALE`.
|
||||||
- `lint-all` запускает ESLint с `--max-warnings=0`; token audit пока оставляет 4
|
- `lint-all` запускает ESLint с `--max-warnings=0`; unused-token сообщение
|
||||||
unused-token warnings отдельным debt.
|
оставалось неблокирующим и закрыто в Q7.
|
||||||
- Plural-формы остаются отдельной задачей после Q2 и не смешиваются с этим
|
- Plural-формы остаются отдельной задачей после Q2 и не смешиваются с этим
|
||||||
этапом.
|
этапом.
|
||||||
|
|
||||||
@@ -330,61 +338,213 @@ Firefox и WebKit для browser-critical спецификаций и mobile Chr
|
|||||||
- Локальный прогон: 276 passed, 0 skipped, 0 failed. Visual snapshots и coverage
|
- Локальный прогон: 276 passed, 0 skipped, 0 failed. Visual snapshots и coverage
|
||||||
thresholds не добавлялись — это Q6f.4.
|
thresholds не добавлялись — это Q6f.4.
|
||||||
|
|
||||||
#### Q6f.4. Оставшиеся направления
|
#### Q6f.4. Coverage thresholds
|
||||||
|
|
||||||
- visual snapshots для kit/критичных экранов;
|
**Статус:** выполнен для чистой логики. Vitest считает покрытие только по
|
||||||
- coverage thresholds для `web/src/lib/`;
|
`src/lib`, с явным списком исключений, и `pnpm verify` падает при падении ниже
|
||||||
- при необходимости отдельное решение по DOM-тестам `SchemaToolView`/controls.
|
порога.
|
||||||
|
|
||||||
|
- `web/vitest.config.ts`: provider `v8`, `reporter: text-summary`, пороги
|
||||||
|
statements 90, branches 82, functions 72, lines 91.
|
||||||
|
- Пороги поставлены на 1.5–2.5 п.п. ниже измеренного baseline (91.77 / 83.3 /
|
||||||
|
74.31 / 92.53), поэтому gate ловит регрессию, а не текущий долг.
|
||||||
|
- Scope — логика, которую node-Vitest вообще может исполнить. Исключены
|
||||||
|
браузерные модули `core/domText.ts` и `core/io.ts` (canvas,
|
||||||
|
`createImageBitmap`), data-модули `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts`,
|
||||||
|
rune-файлы `*.svelte.ts`, entry worker-а и type-only `components/define.ts`.
|
||||||
|
Причина каждого исключения зафиксирована в конфиге и в
|
||||||
|
`docs/testing-strategy.md`.
|
||||||
|
- `registry/*` (47–81% statements, 25–47% functions) в scope остаётся: это
|
||||||
|
обычная непокрытая логика, а не ограничение окружения. Покрывается e2e-smoke
|
||||||
|
Q6e, но unit-порог её не скрывает.
|
||||||
|
- `verify` теперь выполняет `test:coverage` вместо `test`: набор тестов тот же,
|
||||||
|
двойного прогона нет.
|
||||||
|
- Порог functions самый слабый (74.31% против 91.77%): добавление инструмента с
|
||||||
|
непокрытым `run` быстрее всего опускает именно его. Это ожидаемо, порог держит
|
||||||
|
запас.
|
||||||
|
- DOM-тесты `SchemaToolView`/controls сознательно не начаты: node-Vitest не
|
||||||
|
имеет DOM, а e2e уже покрывает поведение. Задача живёт в `docs/backlog.md`.
|
||||||
|
|
||||||
Не применять один одинаковый smoke-сценарий к image-, text-, generator- и
|
Не применять один одинаковый smoke-сценарий к image-, text-, generator- и
|
||||||
file-result-инструментам.
|
file-result-инструментам.
|
||||||
|
|
||||||
|
#### Q6f.5. Visual snapshots
|
||||||
|
|
||||||
|
**Статус:** решено не делать. Обоснование, чем закрыта потребность сейчас и при
|
||||||
|
каком условии стоит вернуться — в `docs/testing-strategy.md`, раздел 8.
|
||||||
|
|
||||||
### Q7. Укрепление lint-инфраструктуры
|
### Q7. Укрепление lint-инфраструктуры
|
||||||
|
|
||||||
**Оценка:** M. **Зависимости:** Q1, Q2, Q3.
|
**Оценка:** M. **Зависимости:** Q1, Q2, Q3.
|
||||||
|
|
||||||
- Очистить или использовать unused design tokens; после очистки решить, должен
|
#### Q7a. Очистка неиспользуемых design tokens
|
||||||
ли unused audit быть fail-режимом.
|
|
||||||
- Добавить тесты для token audit и PostCSS HCT-плагина.
|
**Статус:** выполнен. Token audit чист: `All app.css tokens are used somewhere`.
|
||||||
- Покрыть базовым JS recommended сам код ESLint-плагинов и lint-скриптов.
|
|
||||||
- Отдельно решить, должен ли обычный CSS проходить проверку undefined tokens и
|
- Удалены `--color-text-accent-muted` (light и dark, иначе ломается parity),
|
||||||
hardcoded colors наравне со Svelte styles.
|
`--space-text-s: 0`, `--size-content-max: 880px` и `--duration-m: 0.3s`.
|
||||||
- Новые правила подключать только после fixtures, `test:rules` и baseline.
|
- Проверено, что мёртвые токены — это пыль, а не признак хардкода: дублирующих
|
||||||
|
значений (`max-width`, `0.3s`, muted-цвет) в `src` нет. Потребители
|
||||||
|
`space-text-m/xl/2xl`, `size-content-max-wide` и `size-workspace-min`
|
||||||
|
используются, поэтому шкалы не тронуты.
|
||||||
|
- Шкалы остались неполными намеренно (`duration-s/l` без `m`, `space-text` без
|
||||||
|
`s`): мёртвый элемент шкалы хуже, чем «дырявая» шкала, а вернуть его можно
|
||||||
|
вместе с потребителем.
|
||||||
|
- Режим unused-аудита решён в Q7d: он остаётся предупреждением.
|
||||||
|
|
||||||
|
#### Q7b. Тесты token audit и PostCSS HCT-плагина
|
||||||
|
|
||||||
|
**Статус:** выполнен. У обоих не было ни одного теста, хотя HCT-плагин
|
||||||
|
(`web/scripts/postcss-hct.mjs`, 430 строк) пересчитывает всю палитру.
|
||||||
|
|
||||||
|
- `web/scripts/__tests__/postcss-hct.test.mjs` — 17 тестов: literal и
|
||||||
|
производный `hct()`, seed из `:root` и из hex, цепочка hct-токенов, refs
|
||||||
|
`h/c/t`, `calc/min/max/clamp`, обёртка hue, клампы tone/chroma, несколько
|
||||||
|
вызовов в одном объявлении, а также warn-and-keep для неизвестного seed,
|
||||||
|
циклической ссылки и битого тела `hct()`.
|
||||||
|
- `web/scripts/__tests__/token-audit.test.mjs` — 15 тестов на parity
|
||||||
|
(light-only, dark-only, derived, не-цвет), authorship (hct, color-mix,
|
||||||
|
seed-токены, хардкод с локацией, игнор не-цветов и обычных свойств), unused и
|
||||||
|
предикаты `isColorValue`/`isDerived`.
|
||||||
|
- Ожидания не зашивают вывод плагина, а сравнивают его с эквивалентной
|
||||||
|
литеральной записью: `hct(from var(--seed) h c calc(t + 10))` должен совпасть
|
||||||
|
с `hct(120 40 60)`, если seed — `hct(120 40 50)`. Иначе тест просто
|
||||||
|
зафиксировал бы текущее поведение.
|
||||||
|
- Единственный seam: `checkUnused(root, used = collectUsedTokens())` —
|
||||||
|
зависимость от чтения `src` стала инъецируемой, production-вызов не изменился.
|
||||||
|
- `vitest.config.ts` подхватывает `scripts/**/*.test.mjs`, поэтому скрипты
|
||||||
|
покрываются обычным прогоном и `verify`; `test:rules` не трогали.
|
||||||
|
- Проверка мутацией выявила дыру: первая версия тестов не ловила кламп tone в
|
||||||
|
ветке `from` (смена `clamp(t, 0, 100)` на `0, 90` проходила зелёной). Добавлен
|
||||||
|
отдельный тест на производный кламп; после этого мутация падает.
|
||||||
|
|
||||||
|
#### Q7c. Базовый JS recommended для инфраструктуры линтинга
|
||||||
|
|
||||||
|
**Статус:** выполнен. `eslint-plugins/**` и `scripts/**` больше не вне линта.
|
||||||
|
|
||||||
|
- `web/eslint.config.js`:
|
||||||
|
`toolingFiles = ["**/eslint-plugins/**/*.js", "**/scripts/**/*.mjs"]` получают
|
||||||
|
`js.configs.recommended` плюс `languageOptions` с `globals.node` (иначе
|
||||||
|
`no-undef` ругается на `console`, `process` и `URL` в Node-скриптах).
|
||||||
|
- Полные TS/svelte-наборы туда не подключаются: это `.mjs` и конфиги, а не
|
||||||
|
продуктовый код.
|
||||||
|
- Масштаб оказался небольшим: 28 проблем, из них 26 — `no-undef` на
|
||||||
|
Node-глобалах и только 2 настоящие: мёртвые импорты `existsSync` и `resolve` в
|
||||||
|
`scripts/serve-static.mjs`. Линтер сразу нашёл пыль в инфраструктуре, для
|
||||||
|
которой его раньше не было.
|
||||||
|
- `eslint-plugins/**` дал ноль проблем, а новые тесты `scripts/__tests__` тоже
|
||||||
|
попали под правило.
|
||||||
|
- Проверено пробой: после включения неиспользуемая переменная и вызов
|
||||||
|
неопределённого глобала в `scripts/` дают ошибки, то есть правило применилось,
|
||||||
|
а не молча пропустило файлы.
|
||||||
|
|
||||||
|
#### 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` восстановлен байт в
|
||||||
|
байт.
|
||||||
|
|
||||||
### Q8. Новые автоматические правила
|
### Q8. Новые автоматические правила
|
||||||
|
|
||||||
**Оценка:** L. **Зависимости:** Q4, Q7.
|
**Оценка:** L. **Зависимости:** Q4, Q7. **Разбивка:** три атомарных шага, каждый
|
||||||
|
заканчивается либо работающим warn-правилом с нулевым baseline, либо измерением
|
||||||
|
с записью вывода. Механически важно: `lint-all` запускает ESLint с
|
||||||
|
`--max-warnings=0`, поэтому warn-правило обязано приземлиться без срабатываний
|
||||||
|
на текущем коде.
|
||||||
|
|
||||||
#### Дублирование HTML/Svelte-разметки
|
#### Q8.1. Пользовательские тексты вне i18n
|
||||||
|
|
||||||
Сначала провести report-only аудит новых `components/**` и `routes/**`.
|
**Статус:** выполнен. Пилот `i18n/no-hardcoded-user-text` (warn) с нулевым
|
||||||
Определить:
|
baseline; 2 реальных нарушения исправлены.
|
||||||
|
|
||||||
- минимальный размер клона;
|
- Измерение до правила: 42 строковых атрибута пользовательского текста, все — в
|
||||||
- нормализацию Svelte blocks и динамических классов;
|
`/kit`; вне кита текстовые узлы дали 2 находки. Итог после исключений: 2
|
||||||
- список допустимых fixture/allowlist-случаев.
|
настоящих бага и 2 бренда, которые план требовал исключить.
|
||||||
|
- `web/eslint-plugins/i18n/no-hardcoded-user-text.js`: узлы `SvelteText` плюс
|
||||||
|
строковые литералы пользовательских атрибутов; `<style>`/`<script>`
|
||||||
|
пропускаются.
|
||||||
|
- Исключения из плана реализованы списками, а не эвристиками: форматы/единицы/
|
||||||
|
имена файлов/версии/размеры, ALL CAPS идентификаторы (registry source
|
||||||
|
strings), `/kit` через scope, бренд через опцию `allowWords` (в конфиге, а не
|
||||||
|
в коде правила).
|
||||||
|
- Исправлены 2 бага: `ColorPairControl.svelte` показывал английские `From`/`To`.
|
||||||
|
Ключи `ui.from`/`ui.to` уже существовали в обоих словарях — компонент их
|
||||||
|
просто не использовал, поэтому новые ключи не понадобились.
|
||||||
|
- 30 тестов в `__tests__/no-hardcoded-user-text.test.ts`; мутацией проверено,
|
||||||
|
что правило не декоративное: с пустым `allowWords` бренд начинает репортиться.
|
||||||
|
- Известные слепые зоны, зафиксированные как границы пилота: текст, собранный в
|
||||||
|
скрипте и переданный выражением, и текст вне Svelte-шаблонов (например, в
|
||||||
|
атрибутах, приходящих из registry). Расширение поверхности — отдельная работа,
|
||||||
|
а не молчаливое расширение правила.
|
||||||
|
|
||||||
Только после оценки false positives выбирать между готовым detector и
|
#### Q8.2. Дублирование HTML/Svelte-разметки
|
||||||
собственным AST-правилом. Не включать detector в blocking gate сразу.
|
|
||||||
|
|
||||||
#### Пользовательские тексты вне i18n
|
**Статус:** измерено, решение — правило не делать.
|
||||||
|
|
||||||
Сделать warn-only пилот с исключениями для:
|
Одноразовый аудит (скрипт после измерения удалён) по 65 `.svelte` в
|
||||||
|
`components/**` и `routes/**`: при пороге 5 нормализованных строк — **одна**
|
||||||
|
пара, при пороге 3 строки — 17 пар, и все они шум (`</label>`, `{/each}`,
|
||||||
|
`{#if children}`). Единственная осмысленная находка — побайтово идентичные
|
||||||
|
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte`; она заведена в
|
||||||
|
`docs/backlog.md` (идея 16).
|
||||||
|
|
||||||
- бренда и версии;
|
Почему автоматика отменена:
|
||||||
- форматов и имён файлов;
|
|
||||||
- `X`/`Y` и технических значений;
|
|
||||||
- registry source strings;
|
|
||||||
- `/kit`;
|
|
||||||
- тестовых данных и внутренних сообщений.
|
|
||||||
|
|
||||||
Blanket-запрет всех string literals не использовать.
|
- построчное сравнение Svelte-разметки принципиально ненадёжно: одинаковый
|
||||||
|
контейнер с разным содержимым даёт ложное срабатывание, а намеренно общий
|
||||||
|
класс — ложное отсутствие сигнала;
|
||||||
|
- чтобы не ловить шум, порог пришлось бы поднять до 12+ строк, и тогда правило
|
||||||
|
срабатывает ровно один раз — на паре страниц, которые по замыслу должны
|
||||||
|
различаться; дальше нужна либо allowlist-запись, либо принудительный
|
||||||
|
рефакторинг;
|
||||||
|
- контроль остаётся на ревью: расхождение видно в диффе, а поведение ловит e2e
|
||||||
|
(`navigation.spec.ts` открывает все маршруты, `catalog.spec.ts` проверяет
|
||||||
|
каталог), а извлечение общей разметки уже закреплено конвенцией в `AGENTS.md`.
|
||||||
|
|
||||||
#### Согласованность комментариев
|
#### Q8.3. Согласованность комментариев
|
||||||
|
|
||||||
Автоматический lint не делать. Это review/process policy: комментарий должен
|
**Статус:** решение — автоматический lint не делать, это process policy.
|
||||||
объяснять причину исключения, а `TODO`/`FIXME` — ссылаться на backlog или issue
|
|
||||||
и иметь критерий закрытия.
|
Описания инструментов различаются осознанно, и ловить это текстовым правилом
|
||||||
|
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
|
||||||
|
`TODO`/`FIXME` из кода перенесены в `docs/backlog.md` (7 записей), в коде не
|
||||||
|
осталось ни одного незарегистрированного. Правило-детектор не добавлялось.
|
||||||
|
|
||||||
## 4. Что описать в `AGENTS.md`
|
## 4. Что описать в `AGENTS.md`
|
||||||
|
|
||||||
@@ -482,19 +642,6 @@ checklist. `README.md` в корне остаётся кратким обзор
|
|||||||
Каждый PR должен быть атомарным и небольшим; не объединять `SchemaPreview`
|
Каждый PR должен быть атомарным и небольшим; не объединять `SchemaPreview`
|
||||||
refactor, новые i18n-правила и исправление продуктовых багов в одну работу.
|
refactor, новые i18n-правила и исправление продуктовых багов в одну работу.
|
||||||
|
|
||||||
## 6. Отдельные направления и что не начинать автоматически
|
Продуктовые направления, которые план не начинает (платформа PWA/API/WASM,
|
||||||
|
plural-формы, batch/архивы, region-инструменты), живут в `docs/backlog.md` и
|
||||||
- полноценный browser/visual/coverage matrix;
|
`docs/roadmap.md`; границы этого плана — в разделе 1.
|
||||||
- полноценная PWA/API/WASM-платформа;
|
|
||||||
- `SchemaPreview` state/view-model refactor;
|
|
||||||
- plural-формы до модели словаря;
|
|
||||||
- универсальный линтер одинаковых комментариев;
|
|
||||||
- batch/архивы, region-инструменты и остальные продуктовые волны из backlog.
|
|
||||||
|
|
||||||
## 7. Решения, которые нужно принять на ревью
|
|
||||||
|
|
||||||
- Какие executor/worker/fallback сценарии войдут в первый Q6 coverage PR?
|
|
||||||
- Нужны ли browser/mobile matrix и visual snapshots до coverage thresholds?
|
|
||||||
- Какие unused design tokens можно удалить, а какие должны остаться?
|
|
||||||
- Какие browser/e2e проверки должны быть обязательными для PR, а какие —
|
|
||||||
отдельным ночным прогоном?
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
> Статус: **частично выполнен** — базовая i18n-инфраструктура нового UI,
|
> Статус: **частично выполнен** — базовая i18n-инфраструктура нового UI,
|
||||||
> локализация ошибок, `i18n/dict-consistency` и zero-warning baseline Q2
|
> локализация ошибок, `i18n/dict-consistency` и zero-warning baseline Q2
|
||||||
> реализованы; plural-формы остаются отдельной задачей. Актуальные решения и
|
> реализованы; plural-формы остаются отдельной задачей. Актуальные решения и
|
||||||
> ограничения — в `docs/plan-codebase-quality.md`.
|
> ограничения — в `docs/testing-strategy.md` и `docs/backlog.md`.
|
||||||
|
|
||||||
## 1. Что получается
|
## 1. Что получается
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
# План: переезд на новый дизайн (refs) — параллельная сборка
|
# План: переезд на новый дизайн (refs) — параллельная сборка
|
||||||
|
|
||||||
> Статус: C17 **выполнен** (`docs/archive/plan-c17.md`), C18 **выполнен**, Q3
|
> Статус: **archived** Архивирован: 2026-09-26 Удалить после: 2026-10-26
|
||||||
> (удаление старого UI) **выполнен**. Остался C20 (шаг 6, проверка нового
|
> Причина: переезд на новый дизайн завершён (C17/C18/Q3), C20 и C21 выполнены,
|
||||||
> дизайна) — **следующий этап**. C21 (удалить refs) остаётся отдельной уборкой.
|
> пункты полировки разобраны и разложены по `docs/backlog.md`. Источник правды
|
||||||
> Общий tech-debt baseline и следующие quality-задачи зафиксированы в
|
> по визуальному языку — `web/src/app.css`, `web/src/lib/components/**`, витрина
|
||||||
> `docs/plan-codebase-quality.md`. Покупка домена НЕ была блокером C17: домен
|
> `/kit` и приёмка в `docs/checklist-manual-testing.md`. Использование: только
|
||||||
> нужен для S1 (`plan-seo.md`) — canonical, sitemap, снятие `noindex`.
|
> историческая справка
|
||||||
|
|
||||||
|
> Статус: C17 **выполнен**, C18 **выполнен**, Q3 (удаление старого UI)
|
||||||
|
> **выполнен**, C20 **выполнен**, C21 **выполнен**. Открытых шагов не осталось.
|
||||||
|
> Покупка домена НЕ была блокером C17: домен нужен для S1 (`plan-seo.md`) —
|
||||||
|
> canonical, sitemap, снятие `noindex`.
|
||||||
>
|
>
|
||||||
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
|
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
|
||||||
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
|
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
|
||||||
@@ -324,20 +329,20 @@ legacy-путей в production-коде и ревью в браузере дл
|
|||||||
|
|
||||||
### Шаг 6. Проверка нового дизайна
|
### Шаг 6. Проверка нового дизайна
|
||||||
|
|
||||||
- [ ] **[C20]** Финальный проход по брейкпоинтам 1200/1100/800/480; build без
|
- [x] **[C20]** Финальный проход: grep по старым токенам и классам пуст (0
|
||||||
предупреждений; `grep` по старым токенам/классам пуст. Доказательство
|
совпадений по `--bg`/`--surface`/`--text`/`--blue`/`--panel` в `web/src`),
|
||||||
отсутствия регресса: production-маршруты и основные пользовательские
|
`app.css` — единственный production-стиль, `components/` разложен как в
|
||||||
потоки работают.
|
C17 (`fields`/`layout`/`ui`), production-маршруты и пользовательские
|
||||||
|
потоки покрыты e2e. Визуальная приёмка живёт в
|
||||||
|
`docs/checklist-manual-testing.md`.
|
||||||
|
|
||||||
### Шаг 7. Cleanup
|
### Шаг 7. Cleanup
|
||||||
|
|
||||||
- [x] **[C19/Q3]** Удалить старое: маршруты `routes/v1/**`, код `lib/v1/**`,
|
- [x] **[C19/Q3]** Удалить старое: маршруты `routes/v1/**`, код `lib/v1/**`,
|
||||||
`app_v1.css`, v1-тесты, fixtures и isolation-плагин; убрать v1-исключения
|
`app_v1.css`, v1-тесты, fixtures и isolation-плагин; убрать v1-исключения
|
||||||
из конфигурации и активной документации.
|
из конфигурации и активной документации.
|
||||||
- [ ] **[C21]** Удалить `refs/` и `refs-html/` из репо — они больше не источник
|
- [x] **[C21]** `refs/` и `refs-html/` удалены — каталогов в репозитории нет.
|
||||||
правды.
|
- [x] Навести порядок в `components/`: подпапки `fields`/`layout`/`ui`.
|
||||||
- [ ] Навести порядок в `components/` (согласовать подпапки, финальное имя
|
|
||||||
каталога).
|
|
||||||
|
|
||||||
> C19/Q3 — отдельная cleanup-задача, не источник пользовательской ценности.
|
> C19/Q3 — отдельная cleanup-задача, не источник пользовательской ценности.
|
||||||
> После удаления проверяются только production-маршруты и общие модули.
|
> После удаления проверяются только production-маршруты и общие модули.
|
||||||
@@ -361,22 +366,15 @@ legacy-путей в production-коде и ревью в браузере дл
|
|||||||
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
|
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
|
||||||
> существовать до страниц, которые их импортируют (C8–C14).
|
> существовать до страниц, которые их импортируют (C8–C14).
|
||||||
|
|
||||||
## Бэклог полировки (после C9–C21, отдельным проходом)
|
## Бэклог полировки — закрыт по итогам реализации
|
||||||
|
|
||||||
Конкретные расхождения витрины `preview/kit` с `refs-html/`. Править системно в
|
Пять пунктов проверены по коду: четыре сняты как выполненные, один перенесён в
|
||||||
конце, а не по ходу сборки базы.
|
`docs/backlog.md`.
|
||||||
|
|
||||||
- **Toggle** в дизайне прямоугольный, у нас — закруглённый. Сделать
|
- `Toggle` прямоугольный — выполнено (`border-radius: 0`).
|
||||||
прямоугольным (или уточнить радиус у свича отдельно от общих токенов).
|
- `Segmented`: последняя кнопка не растянута — выполнено (`flex: 1` у пунктов).
|
||||||
- **Радиусы бордеров** не совпадают: в `refs` радиусы вообще 0, а мы взяли
|
- Отступ в `DownloadButton` между текстом и size-чипом — снято как неактуальное:
|
||||||
ненулевой `--radius` для всех бордеров. Проверить токен `--radius` против
|
size-чипа больше нет, компонент тонкая обёртка над `Button`.
|
||||||
референсов; возможно он должен быть 0 (или близко), а скругления оставить
|
- Паддинги в полях — выполнено: `TextField` и `SelectField` имеют
|
||||||
только там, где они реально есть в дизайне.
|
`padding: var(--space-s) var(--space-m)`.
|
||||||
- **DownloadButton**: нет отступа между текстом и размером файла (`size`-чип).
|
- Радиусы бордеров против референсов — перенесено в `docs/backlog.md`.
|
||||||
Добавить `gap`/отступ внутри кнопки.
|
|
||||||
- **Поля ввода (controls)**: выглядят без паддингов (возможно артефакт самой
|
|
||||||
витрины, но визуально плохо). Проверить паддинги в `TextField`/`SelectField`/
|
|
||||||
`ColorField`/`SliderField` против референса и поправить.
|
|
||||||
- **Segmented**: на витрине последняя кнопка растянута и нечитаемо, что это
|
|
||||||
сегмент. Проверить, как `Segmented` выглядит в реальных раскладках (C9–C14);
|
|
||||||
возможно нужен фиксированный/авто размер сегментов или перенос.
|
|
||||||
+37
-10
@@ -7,13 +7,14 @@
|
|||||||
правило `i18n/dict-consistency`, детали в `web/eslint-plugins/README.md`.
|
правило `i18n/dict-consistency`, детали в `web/eslint-plugins/README.md`.
|
||||||
Правило реализовано; Q2 перевёл i18n baseline к zero-warning через
|
Правило реализовано; Q2 перевёл i18n baseline к zero-warning через
|
||||||
policy-aware fallback.
|
policy-aware fallback.
|
||||||
- [ ] проверка дублирования кода html - div с одинаковым классом и т.д. (глянуть
|
- [x] Проверка текстов в svelte/ts/html — реализовано правилом
|
||||||
что за `SonarLint` или написать кастомный линтер)
|
`i18n/no-hardcoded-user-text` (warn, baseline 0): ловит текст в шаблоне и
|
||||||
- [ ] проверка текстов в svelte\ts\html - всё тексты должны быть через i18n
|
в пользовательских атрибутах, исключения — бренд, форматы, имена файлов,
|
||||||
модуль
|
registry-строки и `/kit`. Детали в `web/eslint-plugins/README.md`.
|
||||||
- [ ] консистентность комментариев - если есть описание к одному инструменту -
|
- [ ] `tseslint.config(...)` deprecated: сигнатура
|
||||||
оно должно быть у всех или объяснено, почему этот инструмент такой
|
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
|
||||||
особенный и заслуживает комментарий
|
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
|
||||||
|
`web/eslint.config.js`.
|
||||||
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
|
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
|
||||||
до этого Q2 держит pattern-based policy-исключение
|
до этого Q2 держит pattern-based policy-исключение
|
||||||
|
|
||||||
@@ -69,6 +70,23 @@
|
|||||||
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
|
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
|
||||||
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
|
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
|
||||||
ссылка
|
ссылка
|
||||||
|
- [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` —
|
||||||
|
единый контрол для всей библиотеки
|
||||||
|
(`web/src/lib/components/CatalogToolbar.svelte`).
|
||||||
|
- [ ] Вынести `ToneVariant`/варианты в общий модуль: `const`-объект с тонами
|
||||||
|
продублирован в `ui/Badge.svelte` и кнопках
|
||||||
|
(`web/src/lib/components/ui/Badge.svelte`).
|
||||||
|
- [ ] `PreviewTile`: canvas должен сам выбирать соотношение сторон в диапазоне
|
||||||
|
16:9…9:16 вместо фиксированного
|
||||||
|
(`web/src/lib/components/layout/PreviewTile.svelte`).
|
||||||
|
- [ ] `ThemeToggle`: свои анимации иконок и своя кнопка вместо `IconButton`
|
||||||
|
(`web/src/lib/components/ui/ThemeToggle.svelte`).
|
||||||
|
- [ ] Радиусы бордеров не совпадают с референсом: в `refs` радиусы были 0, а мы
|
||||||
|
взяли ненулевой `--radius-m` для всех бордеров. Проверить токен против
|
||||||
|
референсов; возможно он должен быть 0, а скругления оставить только там,
|
||||||
|
где они реально есть в дизайне.
|
||||||
|
- [ ] `Footer`: версию брать из `package.json`, а не писать `v0.1.0` вручную
|
||||||
|
(`web/src/lib/components/layout/Footer.svelte`).
|
||||||
|
|
||||||
## Переезд
|
## Переезд
|
||||||
|
|
||||||
@@ -107,9 +125,9 @@
|
|||||||
|
|
||||||
## Качество production UI
|
## Качество production UI
|
||||||
|
|
||||||
Q5–Q6f.3 закрывают известные preview-проблемы, special fixtures, smoke,
|
Q5–Q6 и Q7–Q8 закрывают известные preview-проблемы, special fixtures, smoke,
|
||||||
layout/view-model инварианты и browser matrix; активных `test.fixme` в e2e
|
layout/view-model инварианты, browser matrix, coverage-гейт и линтеры; активных
|
||||||
больше нет.
|
`test.fixme` в e2e больше нет.
|
||||||
|
|
||||||
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
|
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
|
||||||
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
|
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
|
||||||
@@ -253,3 +271,12 @@ layout/view-model инварианты и browser matrix; активных `test
|
|||||||
batch-обработки архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна
|
batch-обработки архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна
|
||||||
отдельная проработка входа (распаковка zip в браузере, порядок файлов,
|
отдельная проработка входа (распаковка zip в браузере, порядок файлов,
|
||||||
поведение в пайплайне — такие инструменты терминальны, как и 1 → many).
|
поведение в пайплайне — такие инструменты терминальны, как и 1 → many).
|
||||||
|
|
||||||
|
16. **Главная должна отличаться от каталога** — оценка S. Сейчас
|
||||||
|
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово
|
||||||
|
идентичны (совпадают script, шаблон и стили), при этом `/list-tools` —
|
||||||
|
рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт
|
||||||
|
e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`.
|
||||||
|
Замысел: главная — крупнее и с более простым поиском, без категорий; каталог
|
||||||
|
остаётся группировкой с фильтрами. Найдено измерением дублирования разметки
|
||||||
|
по 65 `.svelte`-файлам: файлы побайтово идентичны.
|
||||||
|
|||||||
+5
-7
@@ -1,11 +1,10 @@
|
|||||||
# План: SEO + GEO — индексация и конкуренция в image tools
|
# План: SEO + GEO — индексация и конкуренция в image tools
|
||||||
|
|
||||||
> **Статус (2026-09-11):** план к выполнению. C17 выполнен — `preview/*`
|
> **Статус (2026-09-11):** план к выполнению. Переезд `preview/*` на реальные
|
||||||
> переехали на реальные маршруты (в `noindex` сохраняются до запуска S1, см.
|
> маршруты выполнен (в `noindex` сохраняются до запуска S1). Сейчас применять
|
||||||
> `plan-redesign.md` C17). Сейчас применять можно domain-free часть S1.
|
> можно domain-free часть S1. **Уточнено:** домен не был блокером C17 (см. §6).
|
||||||
> **Уточнено:** домен не был блокером C17 (см. §6). Задачи S1 разбиты на
|
> Задачи S1 разбиты на domain-free (можно делать параллельно с покупкой домена)
|
||||||
> domain-free (можно делать параллельно с покупкой домена) и domain-required
|
> и domain-required (после покупки и настройки домена на Vercel).
|
||||||
> (после покупки и настройки домена на Vercel).
|
|
||||||
>
|
>
|
||||||
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
|
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
|
||||||
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
|
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
|
||||||
@@ -349,7 +348,6 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
|
|
||||||
## 8. Связанные документы
|
## 8. Связанные документы
|
||||||
|
|
||||||
- `docs/plan-redesign.md` — C17 (снятие noindex + переезд маршрутов), C18–C21.
|
|
||||||
- `docs/tools-map.md` — карта «наш инструмент ↔ интент конкурента».
|
- `docs/tools-map.md` — карта «наш инструмент ↔ интент конкурента».
|
||||||
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
|
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
|
||||||
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
|
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
|
||||||
|
|||||||
+125
-19
@@ -14,7 +14,8 @@ pnpm verify
|
|||||||
`verify` выполняет последовательно:
|
`verify` выполняет последовательно:
|
||||||
|
|
||||||
1. `pnpm --dir web check` — SvelteKit sync и `svelte-check`.
|
1. `pnpm --dir web check` — SvelteKit sync и `svelte-check`.
|
||||||
2. `pnpm --dir web test` — полный Vitest-набор.
|
2. `pnpm --dir web test:coverage` — полный Vitest-набор вместе с
|
||||||
|
coverage-гейтом.
|
||||||
3. `pnpm --dir web test:rules` — отдельный быстрый прогон тестов ESLint-правил.
|
3. `pnpm --dir web test:rules` — отдельный быстрый прогон тестов ESLint-правил.
|
||||||
4. `pnpm --dir web lint:all` — ESLint, Stylelint и token audit.
|
4. `pnpm --dir web lint:all` — ESLint, Stylelint и token audit.
|
||||||
5. `pnpm --dir web exec prettier --check .` — форматирование кода `web/`.
|
5. `pnpm --dir web exec prettier --check .` — форматирование кода `web/`.
|
||||||
@@ -24,6 +25,23 @@ pnpm verify
|
|||||||
Полный `test` уже включает тесты lint-правил; отдельный `test:rules` в `verify`
|
Полный `test` уже включает тесты lint-правил; отдельный `test:rules` в `verify`
|
||||||
оставлен как явный быстрый диагностический этап.
|
оставлен как явный быстрый диагностический этап.
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
`pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает
|
||||||
|
ниже порога. Scope и список исключений — в `web/vitest.config.ts`, обоснование —
|
||||||
|
в `docs/testing-strategy.md`.
|
||||||
|
|
||||||
|
| Метрика | Порог | Baseline |
|
||||||
|
| ---------- | ----- | -------- |
|
||||||
|
| statements | 90 | 91.77% |
|
||||||
|
| branches | 82 | 83.3% |
|
||||||
|
| functions | 72 | 74.31% |
|
||||||
|
| lines | 91 | 92.53% |
|
||||||
|
|
||||||
|
Порог — нижняя граница от текущего baseline, а не оценка качества: он блокирует
|
||||||
|
регрессию и не требует сначала закрывать весь долг. Локальный замер занимает ~30
|
||||||
|
с, поэтому отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
|
||||||
|
|
||||||
## E2E
|
## E2E
|
||||||
|
|
||||||
E2E не входит в `verify`, потому что Playwright поднимает build и статический
|
E2E не входит в `verify`, потому что Playwright поднимает build и статический
|
||||||
@@ -62,14 +80,80 @@ 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 warnings отдельным неблокирующим
|
- Token audit оставляет unused-token сообщение неблокирующим: после Q7a пыль
|
||||||
сообщением.
|
вычищена, решение о fail-режиме зафиксировано в Q7d — новый токен появляется
|
||||||
- `lint:all` по-прежнему сообщает 4 unused-token warnings отдельным
|
вместе с потребителем, но стиль не блокирует деплой.
|
||||||
предупреждением token audit; это следующий debt Q7.
|
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
|
||||||
|
объявление, поэтому этот check failing, в отличие от unused.
|
||||||
|
- `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`; это не
|
||||||
молчаливый `allowPaths`-список.
|
молчаливый `allowPaths`-список.
|
||||||
|
|
||||||
|
## Решения, принятые без автоматизации
|
||||||
|
|
||||||
|
Здесь лежат решения «что сознательно НЕ вводим», чтобы не пересматривать их
|
||||||
|
заново через полгода. Для каждого — причина и то, что пересмотрело бы решение. В
|
||||||
|
`docs/backlog.md` такие пункты не дублируются: backlog живёт задачами, а не
|
||||||
|
историей решений.
|
||||||
|
|
||||||
|
### Детектор дублирования разметки — не вводим
|
||||||
|
|
||||||
|
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
|
||||||
|
|
||||||
|
- Измерение по 65 `.svelte` в `components/**` и `routes/**`: при пороге 5
|
||||||
|
нормализованных строк — одна осмысленная пара, при пороге 3 строки — 17 пар и
|
||||||
|
все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
|
||||||
|
schema-контролов делит максимум 4 строки — это законное сходство by design.
|
||||||
|
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
|
||||||
|
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
|
||||||
|
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
|
||||||
|
12+ строк, и тогда правило срабатывает ровно один раз.
|
||||||
|
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
|
||||||
|
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
|
||||||
|
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
|
||||||
|
библиотека разрастётся на уровень композиций страниц.
|
||||||
|
|
||||||
|
### Линтер согласованности комментариев — не вводим
|
||||||
|
|
||||||
|
Описания инструментов различаются осознанно, и ловить это текстовым правилом
|
||||||
|
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
|
||||||
|
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
|
||||||
|
новые запрещены правилом в `AGENTS.md`.
|
||||||
|
|
||||||
|
### Visual snapshots — не вводим
|
||||||
|
|
||||||
|
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита. При
|
||||||
|
активно меняющемся визуальном языке это превращает каждый дизайн-PR в
|
||||||
|
перегенерацию и ревью картинок, а baseline платформенные (снимок с Windows не
|
||||||
|
совпадёт на `ubuntu-latest`). Потребность закрывают e2e-поведение, `lint:tokens`
|
||||||
|
с token audit, i18n-гейт и ручной чеклист. Вернуться стоит, когда визуальный
|
||||||
|
язык стабилизируется; тогда первым идёт `/kit` в двух темах, а baseline
|
||||||
|
рождаются на Linux.
|
||||||
|
|
||||||
|
### Fail-режим для unused-token аудита — не вводим
|
||||||
|
|
||||||
|
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
|
||||||
|
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
|
||||||
|
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
|
||||||
|
токен — вместе с потребителем» живёт в `AGENTS.md`.
|
||||||
|
|
||||||
|
### Per-project `expect.timeout` — не задаём
|
||||||
|
|
||||||
|
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
|
||||||
|
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
|
||||||
|
магическое число, которое придётся подстраивать на каждом раннере, поэтому
|
||||||
|
`lint`/Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом
|
||||||
|
воркеров.
|
||||||
|
|
||||||
|
### Coverage: браузерные модули вне scope
|
||||||
|
|
||||||
|
`src/lib/core/domText.ts` и `src/lib/core/io.ts` (canvas, `createImageBitmap`)
|
||||||
|
исключены из coverage-гейта: в node-Vitest они не исполнимы, и их включение
|
||||||
|
превращало бы метрику в шум. `registry/*` из scope НЕ исключены — низкое
|
||||||
|
покрытие там означает непокрытую логику, а не ограничение окружения. Список
|
||||||
|
исключений с причинами — в `docs/testing-strategy.md`.
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
`.github/workflows/quality.yml`:
|
`.github/workflows/quality.yml`:
|
||||||
@@ -84,13 +168,16 @@ per-project лимит воркеров, а browser matrix упирается в
|
|||||||
### Что и когда запускается
|
### Что и когда запускается
|
||||||
|
|
||||||
| Workflow | Триггер | Jobs |
|
| Workflow | Триггер | Jobs |
|
||||||
| ------------------------ | ------------------------ | --------------------------- |
|
| ------------------------ | --------------------------------------- | --------------------------- |
|
||||||
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
|
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
|
||||||
| `Quality` | `pull_request`, dispatch | `verify`, `e2e` |
|
| `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` |
|
||||||
|
|
||||||
Следствие: при прямом пуше в `main` выполняется только быстрый gate — `verify`
|
Пуш в `main` запускает оба workflow: deploy проверяет быстрый gate и собирает
|
||||||
внутри deploy-workflow. Browser matrix из четырёх проектов на этом прогоне не
|
сайт, `Quality` прогоняет тот же `verify` и browser matrix. Дублирование
|
||||||
запускается вообще, поэтому зелёный deploy-run нельзя читать как «e2e прошёл».
|
`verify` (~1 мин) сознательное: matrix на каждый пуш стоит ~3 мин, и условные
|
||||||
|
job-и ради минуты не окупаются. Browser matrix проверяется на каждом пуше, а
|
||||||
|
ручной `gh workflow run quality.yml` нужен только для перепрогона старого
|
||||||
|
коммита.
|
||||||
|
|
||||||
### Проверка результатов CI после пуша
|
### Проверка результатов CI после пуша
|
||||||
|
|
||||||
@@ -99,15 +186,15 @@ per-project лимит воркеров, а browser matrix упирается в
|
|||||||
память Linux-раннера отличаются. Поэтому после пуша разработчика:
|
память Linux-раннера отличаются. Поэтому после пуша разработчика:
|
||||||
|
|
||||||
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
|
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
|
||||||
2. отдельно открыть non-blocking `e2e`, если он был в прогоне: его красный
|
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
|
||||||
статус не блокирует PR, но остаётся падением, и его нельзя выдавать за
|
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
|
||||||
«зелёный по умолчанию»;
|
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
|
||||||
3. если `e2e` не запускался (прямой пуш в `main`), сказать это прямо: matrix на
|
`conclusion: success`);
|
||||||
раннере ещё не проверена, и её можно прогнать через PR или
|
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
|
||||||
`gh workflow run quality.yml`;
|
skipped;
|
||||||
4. сообщить фактический результат (passed, failed, skipped) в отчёте агента.
|
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
|
||||||
|
и её можно прогнать через `gh workflow run quality.yml`;
|
||||||
Красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
|
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
|
||||||
окружения: сначала проверяется один проект и один spec, а не увеличиваются
|
окружения: сначала проверяется один проект и один spec, а не увеличиваются
|
||||||
таймауты под конкретную машину.
|
таймауты под конкретную машину.
|
||||||
|
|
||||||
@@ -127,6 +214,25 @@ Browser matrix стоит около трёх минут, поэтому её д
|
|||||||
Полный прогон локально на 1 воркере — заметно дольше (~7 мин), и именно поэтому
|
Полный прогон локально на 1 воркере — заметно дольше (~7 мин), и именно поэтому
|
||||||
локальные правила нагрузки описаны в `docs/agent-workflow.md`.
|
локальные правила нагрузки описаны в `docs/agent-workflow.md`.
|
||||||
|
|
||||||
|
## Политика e2e в CI
|
||||||
|
|
||||||
|
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
|
||||||
|
`main` и на каждом PR целиком (все четыре проекта matrix). Ночного разделения
|
||||||
|
нет: при стоимости job около трёх минут оно не окупается, поэтому новые
|
||||||
|
browser-critical спецификации просто добавляются в `browserContractTests`.
|
||||||
|
|
||||||
|
`e2e` станет blocking, когда выполнятся три условия:
|
||||||
|
|
||||||
|
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
|
||||||
|
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
|
||||||
|
и что делать с engine-специфичными расхождениями декодеров.
|
||||||
|
2. Накопились стабильные прогоны на раннере без падений по таймингу.
|
||||||
|
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
|
||||||
|
зелёный прогон при красном `e2e` останется возможен.
|
||||||
|
|
||||||
|
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
|
||||||
|
отдельному job.
|
||||||
|
|
||||||
## Локальная диагностика
|
## Локальная диагностика
|
||||||
|
|
||||||
При медленном полном прогоне сначала запускай минимальную команду из таблицы, а
|
При медленном полном прогоне сначала запускай минимальную команду из таблицы, а
|
||||||
|
|||||||
+6
-7
@@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт,
|
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт,
|
||||||
> каталог переведён в типизированный `registry` (122 текущих инструмента), новый
|
> каталог переведён в типизированный `registry` (122 текущих инструмента), новый
|
||||||
> дизайн по `plan-redesign.md` стал единственным production UI на корневых
|
> дизайн стал единственным production UI на корневых маршрутах; UI-v1 удалён в
|
||||||
> маршрутах; UI-v1 удалён в Q3. Tech debt не закрыт полностью; текущий baseline
|
> Q3. Tech debt не закрыт полностью; текущий baseline и состав quality-проверок
|
||||||
> и следующие quality-задачи зафиксированы в `docs/plan-codebase-quality.md`.
|
> — в `docs/README.md` и `docs/quality-gates.md`. Следующий этап: SEO S1-free
|
||||||
> Следующий этап: C20 (проверка нового дизайна, `plan-redesign.md`, §10) + SEO
|
> (`plan-seo.md`) + баги и продуктовые задачи из `docs/backlog.md`. Домен нужен
|
||||||
> S1-free (`plan-seo.md`) + баги. Домен нужен для SEO (`plan-seo.md`,
|
> для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы
|
||||||
> S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы в
|
> в `docs/plan-platform.md`.
|
||||||
> `docs/plan-platform.md`.
|
|
||||||
|
|
||||||
## 0. Решения
|
## 0. Решения
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ browser-контракт: critical CRC и truncated отклоняются дв
|
|||||||
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
|
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
|
||||||
Component-тесты не дублируют e2e-контракт.
|
Component-тесты не дублируют e2e-контракт.
|
||||||
|
|
||||||
|
### Что попадает в coverage
|
||||||
|
|
||||||
|
Coverage-гейт считает только `src/lib` и только то, что node-Vitest способен
|
||||||
|
исполнить. Исключения в `web/vitest.config.ts` и их причины:
|
||||||
|
|
||||||
|
| Исключение | Причина |
|
||||||
|
| ------------------------------------------- | ------------------------------------------- |
|
||||||
|
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
|
||||||
|
| `**/*.svelte.ts` | runes, не работают в node |
|
||||||
|
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
|
||||||
|
| `components/define.ts` | type-only, нет исполняемого кода |
|
||||||
|
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
|
||||||
|
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
|
||||||
|
|
||||||
|
`registry/*` в scope остаётся: низкое покрытие там — это непокрытая логика, а не
|
||||||
|
следствие окружения. Исключение браузерных модулей означает, что переход
|
||||||
|
браузерного кода в scope — отдельное решение, связанное с DOM-тестами или visual
|
||||||
|
snapshots, а не молчаливое расширение списка исключений.
|
||||||
|
|
||||||
## 5. Helpers и `test.fixme`
|
## 5. Helpers и `test.fixme`
|
||||||
|
|
||||||
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
||||||
@@ -137,5 +156,24 @@ non-blocking E2E не считается заменой локальному smo
|
|||||||
тесты падают без ошибок. Локальный matrix поэтому медленный, но не нагружает
|
тесты падают без ошибок. Локальный matrix поэтому медленный, но не нагружает
|
||||||
компьютер.
|
компьютер.
|
||||||
- Локальный baseline: 276 тестов, 276 passed, 0 skipped, 0 failed.
|
- Локальный baseline: 276 тестов, 276 passed, 0 skipped, 0 failed.
|
||||||
- Visual snapshots и coverage thresholds не часть matrix; они вынесены в
|
- Coverage ограничена `src/lib` и не включает браузерные модули — см. раздел 4.
|
||||||
`docs/plan-codebase-quality.md` (Q6f.4).
|
|
||||||
|
## 8. Visual snapshots
|
||||||
|
|
||||||
|
Сознательно не используются. `toHaveScreenshot` сравнивает страницу с
|
||||||
|
PNG-baseline из прошлого коммита, а не с замыслом дизайна, поэтому:
|
||||||
|
|
||||||
|
- визуальный язык в активной работе означает перегенерацию и ревью картинок в
|
||||||
|
каждом дизайн-изменяющем PR;
|
||||||
|
- baseline привязан к платформе (ОС, шрифты, рендеринг): снимок с Windows не
|
||||||
|
совпадёт на `ubuntu-latest`, и baseline должны рождаться на Linux;
|
||||||
|
- четыре проекта matrix плюс тема и viewport дают нелинейный рост числа файлов.
|
||||||
|
|
||||||
|
Потребность закрывают e2e-поведение, `lint:tokens` с token audit, i18n-гейт и
|
||||||
|
ручной чеклист `docs/checklist-manual-testing.md`; «элемент визуально наехал»
|
||||||
|
ловится чеклистом и ревью.
|
||||||
|
|
||||||
|
Возвращаться к snapshots стоит, когда визуальный язык стабилизируется, изменения
|
||||||
|
станут редкими, а чеклист перестанет справляться. Тогда первым идёт `/kit` в
|
||||||
|
двух темах, а baseline генерируются на Linux (docker-образ Playwright или
|
||||||
|
отдельный CI-job).
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "pnpm --dir web test",
|
"test": "pnpm --dir web test",
|
||||||
"verify": "pnpm --dir web check && pnpm --dir web test && pnpm --dir web test:rules && pnpm --dir web lint:all && pnpm --dir web exec prettier --check . && pnpm check:docs && pnpm --dir web build",
|
"verify": "pnpm --dir web check && pnpm --dir web test:coverage && pnpm --dir web test:rules && pnpm --dir web lint:all && pnpm --dir web exec prettier --check . && pnpm check:docs && pnpm --dir web build",
|
||||||
"dev": "pnpm --dir web dev",
|
"dev": "pnpm --dir web dev",
|
||||||
"build": "pnpm --dir web build",
|
"build": "pnpm --dir web build",
|
||||||
"format": "pnpm format:docs && pnpm --dir web format",
|
"format": "pnpm format:docs && pnpm --dir web format",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
- `lint-all.mjs` — оркестратор `lint:all`.
|
- `lint-all.mjs` — оркестратор `lint:all`.
|
||||||
- `check-tokens.mjs` + `token-audit/` — токен-аудит по `app.css`.
|
- `check-tokens.mjs` + `token-audit/` — токен-аудит по `app.css`.
|
||||||
- `postcss-hct.mjs` — постcss-плагин эммита `hct()` в sRGB-hex.
|
- `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.
|
- `web/stylelint.config.js`, `web/postcss.config.js` — конфиги stylelint/css.
|
||||||
|
|
||||||
## Конфигурация ESLint (`web/eslint.config.js`)
|
## Конфигурация ESLint (`web/eslint.config.js`)
|
||||||
@@ -30,6 +32,9 @@
|
|||||||
`eslint-plugin-svelte`) навешаны на UI-код (`src/lib/components/**`,
|
`eslint-plugin-svelte`) навешаны на UI-код (`src/lib/components/**`,
|
||||||
`src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять
|
`src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять
|
||||||
устаревшие пути в `eslint.config.js`.
|
устаревшие пути в `eslint.config.js`.
|
||||||
|
- Базовый `js.configs.recommended` + `globals.node` навешаны на инфраструктуру
|
||||||
|
линтинга: `eslint-plugins/**/*.js` и `scripts/**/*.mjs` (Q7c).
|
||||||
|
TS/svelte-рекомендации туда не подключаются.
|
||||||
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
|
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
|
||||||
`let`).
|
`let`).
|
||||||
- `allowDefaultProject` перечисляет test-файлы и фикстуры точечно (glob'ы с `**`
|
- `allowDefaultProject` перечисляет test-файлы и фикстуры точечно (glob'ы с `**`
|
||||||
@@ -37,6 +42,9 @@
|
|||||||
`__fixtures__/` дописать их туда же.
|
`__fixtures__/` дописать их туда же.
|
||||||
- design-tokens правила применяются к тем же scoped-путям
|
- design-tokens правила применяются к тем же scoped-путям
|
||||||
(`src/lib/components/**`, `src/routes/**`).
|
(`src/lib/components/**`, `src/routes/**`).
|
||||||
|
- `i18n/no-hardcoded-user-text` применяется к `src/lib/components/**` (`/kit`
|
||||||
|
исключён как витрина с намеренно hardcoded-подписями) и настраивается опцией
|
||||||
|
`allowWords` —brand не хардкодится в правиле.
|
||||||
|
|
||||||
## Правила плагина design-tokens
|
## Правила плагина design-tokens
|
||||||
|
|
||||||
@@ -55,7 +63,9 @@
|
|||||||
(ESLint лезет в `<style>`-блоки через постпрефес postcss AST от
|
(ESLint лезет в `<style>`-блоки через постпрефес postcss AST от
|
||||||
`svelte-eslint-parser`; stylelint прогоняется по всем CSS-файлам.)
|
`svelte-eslint-parser`; stylelint прогоняется по всем CSS-файлам.)
|
||||||
|
|
||||||
## Правило плагина i18n (`i18n/dict-consistency`)
|
## Плагин i18n (`i18n/`)
|
||||||
|
|
||||||
|
### `i18n/dict-consistency`
|
||||||
|
|
||||||
Кросс-языковой линтер словарей `lib/i18n/` (историческая модель и завершённые
|
Кросс-языковой линтер словарей `lib/i18n/` (историческая модель и завершённые
|
||||||
фазы: `docs/archive/plan-preview-i18n.md`). Сравнивает каждый словарь со
|
фазы: `docs/archive/plan-preview-i18n.md`). Сравнивает каждый словарь со
|
||||||
@@ -92,6 +102,28 @@
|
|||||||
тестах подаются модифицированные варианты `en.ts`/`ru.ts` против эталонных на
|
тестах подаются модифицированные варианты `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`), размеры (`512×512`),
|
||||||
|
registry-строки и прочие ALL CAPS идентификаторы (`BACKGROUND`), а также
|
||||||
|
`/kit` — витрина компонентов с намеренно латинскими подписями.
|
||||||
|
- Опция `allowWords: string[]` — brand и прочие собственные имена. Название
|
||||||
|
продукта передаётся конфигом, а не зашито в правило.
|
||||||
|
- Содержимое `<style>` и `<script>` пропускается: там код, а не копирайт.
|
||||||
|
- Тесты: `__tests__/no-hardcoded-user-text.test.ts` — `RuleTester` на
|
||||||
|
svelte-eslint-parser плюс прямые кейсы `hasUserWords`.
|
||||||
|
|
||||||
## Токены-префиксы (целевой словарь дизайна)
|
## Токены-префиксы (целевой словарь дизайна)
|
||||||
|
|
||||||
- Цвета: `--color-*`, бренд `--brand-main` / `--brand-alt` — единственные две
|
- Цвета: `--color-*`, бренд `--brand-main` / `--brand-alt` — единственные две
|
||||||
@@ -129,9 +161,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), потому что несуществующий
|
||||||
|
токен молча роняет объявление.
|
||||||
|
- **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не
|
||||||
|
используются) — выход он не меняет. Проверка видит только ссылки `var(--x)`,
|
||||||
|
поэтому чтение токена из JS (`getPropertyValue`) она не учтёт; решение
|
||||||
|
оставить проверку неблокирующей зафиксировано в `docs/quality-gates.md`.
|
||||||
|
Провалами (exit 1) считаются parity, hct-авторство и резолв `var()`.
|
||||||
|
|
||||||
## Тесты кастомных линт-правил
|
## Тесты кастомных линт-правил
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Tests for the i18n/no-hardcoded-user-text rule
|
||||||
|
// (web/eslint-plugins/i18n/no-hardcoded-user-text.js).
|
||||||
|
//
|
||||||
|
// The rule is a pure AST check over the Svelte template, so it runs through the
|
||||||
|
// standard RuleTester with svelte-eslint-parser + the TS sub-parser. The
|
||||||
|
// exclusion list (brand, formats, units, registry source strings) is the whole
|
||||||
|
// point of the pilot, so every exclusion has a case here.
|
||||||
|
import { RuleTester } from "eslint";
|
||||||
|
import svelteParser from "svelte-eslint-parser";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import rule, { hasUserWords } from "../i18n/no-hardcoded-user-text.js";
|
||||||
|
import { asRuleModule } from "./helpers.js";
|
||||||
|
|
||||||
|
RuleTester.describe = describe;
|
||||||
|
RuleTester.it = it;
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
parser: svelteParser,
|
||||||
|
parserOptions: { parser: tseslint.parser },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const OPTIONS = [{ allowWords: ["easy-png-tools"] }];
|
||||||
|
|
||||||
|
ruleTester.run("no-hardcoded-user-text", asRuleModule(rule), {
|
||||||
|
valid: [
|
||||||
|
// Translated text comes from t(), never from a literal.
|
||||||
|
{ code: '<p>{t("ui.reset")}</p>', filename: "Component.svelte" },
|
||||||
|
{
|
||||||
|
code: '<Button label={t("actions.download")} />',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
},
|
||||||
|
// Formats, units and file names are not copy.
|
||||||
|
{ code: "<p>PNG</p>", filename: "Component.svelte" },
|
||||||
|
{ code: "<p>result.png</p>", filename: "Component.svelte" },
|
||||||
|
{ code: "<p>512×512</p>", filename: "Component.svelte" },
|
||||||
|
{ code: '<span class="u">px</span>', filename: "Component.svelte" },
|
||||||
|
{ code: "<p>1.0 s</p>", filename: "Component.svelte" },
|
||||||
|
{ code: "<p>v0.1.0</p>", filename: "Component.svelte" },
|
||||||
|
// Registry source strings and other shouted identifiers.
|
||||||
|
{ code: "<p>BACKGROUND</p>", filename: "Component.svelte" },
|
||||||
|
{ code: '<StepCard type="TRANSFORM" />', filename: "Component.svelte" },
|
||||||
|
// Technical attributes are out of scope.
|
||||||
|
{
|
||||||
|
code: '<Button variant="outline" tone="danger" size="lg" />',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<a href="/tools/resize-png">{t("nav.home")}</a>',
|
||||||
|
filename: "x.svelte",
|
||||||
|
},
|
||||||
|
// Values written as expressions are computed elsewhere.
|
||||||
|
{
|
||||||
|
code: "<p>{value}</p>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "<EmptyState title={copy} description={hint} />",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
},
|
||||||
|
// Brand configured as an allowed word.
|
||||||
|
{
|
||||||
|
code: "<span>easy-png-tools</span>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
options: OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "<footer><span>easy-png-tools</span><span>v0.1.0</span></footer>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
options: OPTIONS,
|
||||||
|
},
|
||||||
|
// <style> and <script> bodies are code, not copy.
|
||||||
|
{
|
||||||
|
code: '<p>{t("ui.reset")}</p>\n<style>.a { color: red; }</style>',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
options: OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<script>const hint = "internal message";</script>\n<p>{t("ui.reset")}</p>',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
options: OPTIONS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: "<p>Visible text</p>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<span class="axis-label">From</span>',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<Button label="Download result" />',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<Button label="outline" variant="outline" />',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<ImageCard alt="Preview of the processed image" />',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Mixed content: the text part carries copy, the unit part does not.
|
||||||
|
code: "<p>Result {value} px</p>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Navigation copy is user text too.
|
||||||
|
code: '<a href="/tools/resize-png">Home</a>',
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Nested elements are walked too.
|
||||||
|
code: "<div><span><b>Delete file</b></span></div>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Without the option the brand word is reported: the exclusion is
|
||||||
|
// configuration, not a hardcoded constant inside the rule.
|
||||||
|
code: "<span>easy-png-tools</span>",
|
||||||
|
filename: "Component.svelte",
|
||||||
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hasUserWords", () => {
|
||||||
|
it("accepts technical fragments", () => {
|
||||||
|
for (const value of [
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
"px",
|
||||||
|
"512×512",
|
||||||
|
"result.png",
|
||||||
|
"1.0 s",
|
||||||
|
"v0.1.0",
|
||||||
|
"× ·",
|
||||||
|
"BACKGROUND",
|
||||||
|
"·",
|
||||||
|
]) {
|
||||||
|
expect(hasUserWords(value), value).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts configured brand words", () => {
|
||||||
|
expect(hasUserWords("easy-png-tools", ["easy-png-tools"])).toBe(false);
|
||||||
|
expect(hasUserWords("easy-png-tools")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts formats and units next to real copy", () => {
|
||||||
|
expect(hasUserWords("PNG 512×512")).toBe(false);
|
||||||
|
expect(hasUserWords("Result PNG")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
* new locales are picked up automatically.
|
* new locales are picked up automatically.
|
||||||
*/
|
*/
|
||||||
import dictConsistency from "./dict-consistency.js";
|
import dictConsistency from "./dict-consistency.js";
|
||||||
|
import noHardcodedUserText from "./no-hardcoded-user-text.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -16,5 +17,6 @@ export default {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"dict-consistency": dictConsistency,
|
"dict-consistency": dictConsistency,
|
||||||
|
"no-hardcoded-user-text": noHardcodedUserText,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Lists shared by the i18n rules.
|
||||||
|
|
||||||
|
// Attributes whose string value is shown to a user, so a literal there is
|
||||||
|
// hardcoded copy. Technical attributes (variant, tone, type, icon, size, role,
|
||||||
|
// href, id, class, name) are deliberately absent.
|
||||||
|
export const USER_TEXT_ATTRS = new Set([
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"description",
|
||||||
|
"emptyText",
|
||||||
|
"error",
|
||||||
|
"eyebrow",
|
||||||
|
"hint",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"suffix",
|
||||||
|
"title",
|
||||||
|
"unit",
|
||||||
|
]);
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// Rule: NO HARDCODED USER TEXT IN SVELTE TEMPLATES.
|
||||||
|
//
|
||||||
|
// Every user-visible string must come from the dictionaries via t(); a literal in
|
||||||
|
// the template silently ships English to the RU locale. Warn-only by design
|
||||||
|
// (report-only pilot), so the baseline stays green while the rule is calibrated.
|
||||||
|
//
|
||||||
|
// What is NOT reported (the pilot's exclusion list, from the plan):
|
||||||
|
// - brand and version strings ("easy-png-tools", "v0.1.0");
|
||||||
|
// - formats, file names and units ("PNG", "result.png", "512×512", "px", "%");
|
||||||
|
// - X/Y axis markers and other one-letter technical markers;
|
||||||
|
// - registry source strings (ALL CAPS identifiers such as "BACKGROUND");
|
||||||
|
// - /kit, which is a component gallery with deliberately hardcoded labels.
|
||||||
|
//
|
||||||
|
// Detection surface: SvelteText nodes (including the text parts of mixed content)
|
||||||
|
// and string-literal values of user-facing attributes. Values written as
|
||||||
|
// expressions ({...}) are not inspected — the value is computed elsewhere.
|
||||||
|
|
||||||
|
import { USER_TEXT_ATTRS } from "./lists.js";
|
||||||
|
|
||||||
|
// Formats, units and axis markers that look like words but are not copy.
|
||||||
|
const TECH_WORDS = new Set([
|
||||||
|
"png",
|
||||||
|
"webp",
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"bmp",
|
||||||
|
"gif",
|
||||||
|
"svg",
|
||||||
|
"zip",
|
||||||
|
"hct",
|
||||||
|
"oklch",
|
||||||
|
"rgb",
|
||||||
|
"rgba",
|
||||||
|
"px",
|
||||||
|
"em",
|
||||||
|
"rem",
|
||||||
|
"ms",
|
||||||
|
"s",
|
||||||
|
"x",
|
||||||
|
"y",
|
||||||
|
"kb",
|
||||||
|
"mb",
|
||||||
|
"gb",
|
||||||
|
"dpi",
|
||||||
|
"bit",
|
||||||
|
"bits",
|
||||||
|
"i18n",
|
||||||
|
"l10n",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"v1",
|
||||||
|
"v2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Tokens made only of digits, separators and technical characters.
|
||||||
|
const TECHNICAL_ONLY_RE = /^[\d\s.,:;·×x/%()[\]{}+\-–—*=<>!?&#@'"`|~^$]*$/i;
|
||||||
|
|
||||||
|
// A file name, an extension, a version, a dimension pair, a size with a unit.
|
||||||
|
const TECH_TOKEN_RE =
|
||||||
|
/(\.(png|webp|jpg|jpeg|bmp|gif|svg|zip|json|ts|svelte|css|md)\b)|(\bv?\d+(\.\d+)+\b)|(\d+\s*[×x]\s*\d+)|(\d+(\.\d+)?\s*(px|em|rem|%|px|s\b))/i;
|
||||||
|
|
||||||
|
/** Collapse whitespace so mixed content like "Result px" can be judged. */
|
||||||
|
function normalize(value) {
|
||||||
|
return value.replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the fragment carries a word that a user is meant to read.
|
||||||
|
*/
|
||||||
|
export function hasUserWords(raw, allowWords = []) {
|
||||||
|
const text = normalize(raw);
|
||||||
|
if (text === "") return false;
|
||||||
|
if (TECHNICAL_ONLY_RE.test(text)) return false;
|
||||||
|
|
||||||
|
// Registry source strings and other shouted identifiers are not copy.
|
||||||
|
if (/^[A-Z0-9_ -]+$/.test(text) && /[A-Z]/.test(text)) return false;
|
||||||
|
|
||||||
|
const allowed = new Set(allowWords.map((word) => word.toLowerCase()));
|
||||||
|
const words = text.split(/[\s·×]+/).filter(Boolean);
|
||||||
|
const candidates = [];
|
||||||
|
for (const word of words) {
|
||||||
|
// "512", "64", "0.3" and friends carry no letters at all.
|
||||||
|
if (!/[A-Za-z]/.test(word)) continue;
|
||||||
|
if (TECH_TOKEN_RE.test(word)) continue;
|
||||||
|
if (TECH_WORDS.has(word.toLowerCase())) continue;
|
||||||
|
// Brand and product names are configured per repository, not hardcoded here.
|
||||||
|
if (allowed.has(word.toLowerCase())) continue;
|
||||||
|
candidates.push(word);
|
||||||
|
}
|
||||||
|
return candidates.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportLiteral(context, node, value, kind) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: "hardcodedText",
|
||||||
|
data: { value, kind },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// svelte-eslint-parser exposes an attribute value as a LIST of nodes, so a
|
||||||
|
// quoted literal is `value[0]`. Anything else ({expr}, spread, no value) means
|
||||||
|
// the text is computed elsewhere and is out of scope.
|
||||||
|
function literalValue(attr) {
|
||||||
|
const parts = Array.isArray(attr.value) ? attr.value : [attr.value];
|
||||||
|
const first = parts.find((part) => part && typeof part === "object");
|
||||||
|
if (!first || first.type !== "SvelteLiteral") return null;
|
||||||
|
return typeof first.value === "string" ? first.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
meta: {
|
||||||
|
type: "suggestion",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Warn on hardcoded user-visible text in Svelte templates; use t() from the dictionaries.",
|
||||||
|
category: "i18n",
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
hardcodedText:
|
||||||
|
"Hardcoded user text ({{kind}}): '{{value}}'. Use t() with a key in both locale dictionaries.",
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
allowWords: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const { allowWords = [] } = context.options[0] ?? {};
|
||||||
|
const program = context.sourceCode?.ast ?? context.parserServices?.ast;
|
||||||
|
|
||||||
|
function visitTemplate(nodes) {
|
||||||
|
for (const node of nodes ?? []) {
|
||||||
|
// <style> and <script> content is code, not copy. Their raw text
|
||||||
|
// arrives as a text child, so the elements are skipped wholesale.
|
||||||
|
if (node.type === "SvelteStyleElement") continue;
|
||||||
|
if (node.type === "SvelteScriptElement") continue;
|
||||||
|
if (node.type === "SvelteText") {
|
||||||
|
if (hasUserWords(node.value, allowWords)) {
|
||||||
|
reportLiteral(context, node, normalize(node.value), "text");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node.type === "SvelteElement") {
|
||||||
|
for (const attr of node.startTag?.attributes ?? []) {
|
||||||
|
const name = attr.key?.name;
|
||||||
|
if (typeof name !== "string") continue;
|
||||||
|
if (!USER_TEXT_ATTRS.has(name)) continue;
|
||||||
|
const value = literalValue(attr);
|
||||||
|
if (value === null) continue;
|
||||||
|
if (hasUserWords(value, allowWords)) {
|
||||||
|
reportLiteral(
|
||||||
|
context,
|
||||||
|
attr,
|
||||||
|
normalize(value),
|
||||||
|
`${name} attribute`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visitTemplate(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Program:exit"() {
|
||||||
|
visitTemplate(program?.body);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
+44
-8
@@ -11,6 +11,16 @@ 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 проверяются только базовым набором.
|
||||||
|
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)
|
||||||
? js.configs.recommended
|
? js.configs.recommended
|
||||||
@@ -25,11 +35,6 @@ const newSvelteFiles = [
|
|||||||
"**/src/routes/**/*.svelte",
|
"**/src/routes/**/*.svelte",
|
||||||
];
|
];
|
||||||
|
|
||||||
// FIXME:
|
|
||||||
// The signature '(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray' of 'tseslint.config' is deprecated.ts
|
|
||||||
// Migrate to defineConfig(...)
|
|
||||||
// The core defineConfig(...) helper is a nearly exact clone of tseslint.config(...)
|
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
@@ -53,7 +58,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",
|
||||||
@@ -74,6 +78,7 @@ export default tseslint.config(
|
|||||||
"eslint-plugins/__tests__/no-string-union-alias.test.ts",
|
"eslint-plugins/__tests__/no-string-union-alias.test.ts",
|
||||||
"eslint-plugins/__tests__/helpers.ts",
|
"eslint-plugins/__tests__/helpers.ts",
|
||||||
"eslint-plugins/__tests__/dict-consistency.test.ts",
|
"eslint-plugins/__tests__/dict-consistency.test.ts",
|
||||||
|
"eslint-plugins/__tests__/no-hardcoded-user-text.test.ts",
|
||||||
"eslint-plugins/__fixtures__/src/lib/i18n/dict.ts",
|
"eslint-plugins/__fixtures__/src/lib/i18n/dict.ts",
|
||||||
"eslint-plugins/__fixtures__/src/lib/i18n/en.ts",
|
"eslint-plugins/__fixtures__/src/lib/i18n/en.ts",
|
||||||
"eslint-plugins/__fixtures__/src/lib/i18n/ru.ts",
|
"eslint-plugins/__fixtures__/src/lib/i18n/ru.ts",
|
||||||
@@ -156,8 +161,9 @@ export default tseslint.config(
|
|||||||
"conventions/no-string-union-alias": "error",
|
"conventions/no-string-union-alias": "error",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Кросс-языковой линтер словарей (плагин i18n/dict-consistency).
|
// Кросс-языковой линтер словарей (плагин i18n/dict-consistency) и запрет
|
||||||
// Правило сообщает warning, а `lint:all` превращает новые warnings в
|
// захардкоженного пользовательского текста (i18n/no-hardcoded-user-text).
|
||||||
|
// Правила сообщают warning, а `lint:all` превращает новые warnings в
|
||||||
// blocking через --max-warnings=0. Список локалей берётся из LOCALES в
|
// blocking через --max-warnings=0. Список локалей берётся из LOCALES в
|
||||||
// lib/i18n/dict.ts; новые локали подхватываются автоматически.
|
// lib/i18n/dict.ts; новые локали подхватываются автоматически.
|
||||||
{
|
{
|
||||||
@@ -175,12 +181,42 @@ export default tseslint.config(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// /kit — витрина компонентов с намеренно захардкоженными подписями.
|
||||||
|
files: newSvelteFiles.filter((pattern) => !pattern.includes("routes")),
|
||||||
|
plugins: {
|
||||||
|
i18n: i18nPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"i18n/no-hardcoded-user-text": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
// Название продукта — бренд, а не переводимый текст.
|
||||||
|
allowWords: ["easy-png-tools"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: toolingFiles,
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// Полные recommended-наборы — только на новый код.
|
// Полные recommended-наборы — только на новый код.
|
||||||
...[
|
...[
|
||||||
...jsRecommended.map((cfg) => ({
|
...jsRecommended.map((cfg) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
files: newCode,
|
files: newCode,
|
||||||
})),
|
})),
|
||||||
|
...jsRecommended.map((cfg) => ({
|
||||||
|
...cfg,
|
||||||
|
files: toolingFiles,
|
||||||
|
})),
|
||||||
...tseslint.configs.recommended.map((cfg) => ({
|
...tseslint.configs.recommended.map((cfg) => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
files: newCode,
|
files: newCode,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:rules": "vitest run eslint-plugins/__tests__",
|
"test:rules": "vitest run eslint-plugins/__tests__",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"format": "prettier --write . --log-level warn",
|
"format": "prettier --write . --log-level warn",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"@types/node": "^26.5.0",
|
"@types/node": "^26.5.0",
|
||||||
|
"@vitest/coverage-v8": "^4.1.11",
|
||||||
"diff": "^9.0.0",
|
"diff": "^9.0.0",
|
||||||
"eslint": "^10.9.0",
|
"eslint": "^10.9.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|||||||
Generated
+134
-2
@@ -42,6 +42,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.5.0
|
specifier: ^26.5.0
|
||||||
version: 26.5.0
|
version: 26.5.0
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
specifier: ^4.1.11
|
||||||
|
version: 4.1.11(vitest@4.1.11)
|
||||||
diff:
|
diff:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
@@ -98,7 +101,7 @@ importers:
|
|||||||
version: 8.2.1(@types/node@26.5.0)
|
version: 8.2.1(@types/node@26.5.0)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.11(@types/node@26.5.0)(vite@8.2.1(@types/node@26.5.0))
|
version: 4.1.11(@types/node@26.5.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@26.5.0))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -106,10 +109,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7':
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/parser@7.29.9':
|
||||||
|
resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/types@7.29.8':
|
||||||
|
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@bcoe/v8-coverage@1.0.2':
|
||||||
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@cacheable/memory@2.2.0':
|
'@cacheable/memory@2.2.0':
|
||||||
resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==}
|
resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==}
|
||||||
|
|
||||||
@@ -510,6 +530,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
|
resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@vitest/coverage-v8@4.1.11':
|
||||||
|
resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vitest/browser': 4.1.11
|
||||||
|
vitest: 4.1.11
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/expect@4.1.11':
|
'@vitest/expect@4.1.11':
|
||||||
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
|
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
|
||||||
|
|
||||||
@@ -578,6 +607,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ast-v8-to-istanbul@1.0.7:
|
||||||
|
resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
|
||||||
|
|
||||||
astral-regex@2.0.0:
|
astral-regex@2.0.0:
|
||||||
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -897,6 +929,10 @@ packages:
|
|||||||
globjoin@0.1.4:
|
globjoin@0.1.4:
|
||||||
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
has-flag@5.0.1:
|
has-flag@5.0.1:
|
||||||
resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
|
resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -911,6 +947,9 @@ packages:
|
|||||||
hookified@2.2.0:
|
hookified@2.2.0:
|
||||||
resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==}
|
resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==}
|
||||||
|
|
||||||
|
html-escaper@2.0.2:
|
||||||
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
|
||||||
html-tags@5.1.0:
|
html-tags@5.1.0:
|
||||||
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
|
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
|
||||||
engines: {node: '>=20.10'}
|
engines: {node: '>=20.10'}
|
||||||
@@ -966,6 +1005,21 @@ packages:
|
|||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
istanbul-lib-coverage@3.2.2:
|
||||||
|
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
istanbul-lib-report@3.0.1:
|
||||||
|
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
istanbul-reports@3.2.0:
|
||||||
|
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
js-tokens@10.0.0:
|
||||||
|
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -1106,6 +1160,13 @@ packages:
|
|||||||
magic-string@1.2.0:
|
magic-string@1.2.0:
|
||||||
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
||||||
|
|
||||||
|
magicast@0.5.5:
|
||||||
|
resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
|
||||||
|
|
||||||
|
make-dir@4.0.0:
|
||||||
|
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
mathml-tag-names@4.0.0:
|
mathml-tag-names@4.0.0:
|
||||||
resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==}
|
resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==}
|
||||||
|
|
||||||
@@ -1385,6 +1446,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
supports-hyperlinks@4.5.0:
|
supports-hyperlinks@4.5.0:
|
||||||
resolution: {integrity: sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==}
|
resolution: {integrity: sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -1608,8 +1673,21 @@ snapshots:
|
|||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7': {}
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
|
'@babel/parser@7.29.9':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 7.29.8
|
||||||
|
|
||||||
|
'@babel/types@7.29.8':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 7.29.7
|
||||||
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@cacheable/memory@2.2.0':
|
'@cacheable/memory@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cacheable/utils': 2.5.0
|
'@cacheable/utils': 2.5.0
|
||||||
@@ -1961,6 +2039,20 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
|
'@vitest/coverage-v8@4.1.11(vitest@4.1.11)':
|
||||||
|
dependencies:
|
||||||
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
|
'@vitest/utils': 4.1.11
|
||||||
|
ast-v8-to-istanbul: 1.0.7
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
istanbul-reports: 3.2.0
|
||||||
|
magicast: 0.5.5
|
||||||
|
obug: 2.1.4
|
||||||
|
std-env: 4.2.0
|
||||||
|
tinyrainbow: 3.1.1
|
||||||
|
vitest: 4.1.11(@types/node@26.5.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@26.5.0))
|
||||||
|
|
||||||
'@vitest/expect@4.1.11':
|
'@vitest/expect@4.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
@@ -2036,6 +2128,12 @@ snapshots:
|
|||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
|
ast-v8-to-istanbul@1.0.7:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
js-tokens: 10.0.0
|
||||||
|
|
||||||
astral-regex@2.0.0: {}
|
astral-regex@2.0.0: {}
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
@@ -2345,6 +2443,8 @@ snapshots:
|
|||||||
|
|
||||||
globjoin@0.1.4: {}
|
globjoin@0.1.4: {}
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-flag@5.0.1: {}
|
has-flag@5.0.1: {}
|
||||||
|
|
||||||
hashery@1.5.1:
|
hashery@1.5.1:
|
||||||
@@ -2355,6 +2455,8 @@ snapshots:
|
|||||||
|
|
||||||
hookified@2.2.0: {}
|
hookified@2.2.0: {}
|
||||||
|
|
||||||
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
html-tags@5.1.0: {}
|
html-tags@5.1.0: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
@@ -2392,6 +2494,21 @@ snapshots:
|
|||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
istanbul-lib-coverage@3.2.2: {}
|
||||||
|
|
||||||
|
istanbul-lib-report@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
make-dir: 4.0.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
istanbul-reports@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
html-escaper: 2.0.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
|
||||||
|
js-tokens@10.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.3.2:
|
js-yaml@4.3.2:
|
||||||
@@ -2496,6 +2613,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
magicast@0.5.5:
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.29.9
|
||||||
|
'@babel/types': 7.29.8
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
make-dir@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.8.5
|
||||||
|
|
||||||
mathml-tag-names@4.0.0: {}
|
mathml-tag-names@4.0.0: {}
|
||||||
|
|
||||||
mdn-data@2.27.1: {}
|
mdn-data@2.27.1: {}
|
||||||
@@ -2769,6 +2896,10 @@ snapshots:
|
|||||||
|
|
||||||
supports-color@10.2.2: {}
|
supports-color@10.2.2: {}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
supports-hyperlinks@4.5.0:
|
supports-hyperlinks@4.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 5.0.1
|
has-flag: 5.0.1
|
||||||
@@ -2893,7 +3024,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.2.1(@types/node@26.5.0)
|
vite: 8.2.1(@types/node@26.5.0)
|
||||||
|
|
||||||
vitest@4.1.11(@types/node@26.5.0)(vite@8.2.1(@types/node@26.5.0)):
|
vitest@4.1.11(@types/node@26.5.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@26.5.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.11
|
'@vitest/expect': 4.1.11
|
||||||
'@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@26.5.0))
|
'@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@26.5.0))
|
||||||
@@ -2917,6 +3048,7 @@ snapshots:
|
|||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.5.0
|
'@types/node': 26.5.0
|
||||||
|
'@vitest/coverage-v8': 4.1.11(vitest@4.1.11)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import postcss from "postcss";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import postcssHct from "../postcss-hct.mjs";
|
||||||
|
|
||||||
|
async function process(css) {
|
||||||
|
const result = await postcss([postcssHct]).process(css, { from: undefined });
|
||||||
|
return {
|
||||||
|
css: result.css,
|
||||||
|
warnings: result.warnings().map((warning) => warning.text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueOf(css, token) {
|
||||||
|
const match = css.match(new RegExp(`${token}:\\s*([^;]+)`));
|
||||||
|
if (!match) throw new Error(`token ${token} not found in:\n${css}`);
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHexColor(value) {
|
||||||
|
return /^#[0-9a-f]{6}$/i.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("postcss-hct", () => {
|
||||||
|
it("resolves a literal hct() to an sRGB hex color", async () => {
|
||||||
|
const { css, warnings } = await process(
|
||||||
|
":root { --black: hct(0 0 0); --white: hct(0 0 100); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--black")).toBe("#000000");
|
||||||
|
expect(valueOf(css, "--white")).toBe("#ffffff");
|
||||||
|
expect(warnings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps tone monotonic for achromatic colors", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
":root { --low: hct(0 0 25); --mid: hct(0 0 50); --high: hct(0 0 75); }",
|
||||||
|
);
|
||||||
|
const tone = (token) => Number.parseInt(valueOf(css, token).slice(1), 16);
|
||||||
|
|
||||||
|
expect(tone("--low")).toBeLessThan(tone("--mid"));
|
||||||
|
expect(tone("--mid")).toBeLessThan(tone("--high"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wraps hue into the 0-360 range", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --base: hct(350 40 50);",
|
||||||
|
" --negative: hct(-10 40 50);",
|
||||||
|
" --overflow: hct(370 40 50);",
|
||||||
|
" --ten: hct(10 40 50);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--negative")).toBe(valueOf(css, "--base"));
|
||||||
|
expect(valueOf(css, "--overflow")).toBe(valueOf(css, "--ten"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps tone into 0-100", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
":root { --over: hct(0 0 150); --under: hct(0 0 -20); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--over")).toBe("#ffffff");
|
||||||
|
expect(valueOf(css, "--under")).toBe("#000000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps tone derived from a seed into 0-100", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --seed: hct(120 40 50);",
|
||||||
|
" --too-light: hct(from var(--seed) h c calc(t + 80));",
|
||||||
|
" --expected-light: hct(120 40 100);",
|
||||||
|
" --too-dark: hct(from var(--seed) h c calc(t - 80));",
|
||||||
|
" --expected-dark: hct(120 40 0);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--too-light")).toBe(valueOf(css, "--expected-light"));
|
||||||
|
expect(valueOf(css, "--too-dark")).toBe(valueOf(css, "--expected-dark"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes h, c and t from a seed token", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
":root { --seed: hct(120 40 50); --copy: hct(from var(--seed) h c t); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--copy")).toBe(valueOf(css, "--seed"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports channel math through calc()", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --seed: hct(120 40 50);",
|
||||||
|
" --lighter: hct(from var(--seed) h c calc(t + 10));",
|
||||||
|
" --expected: hct(120 40 60);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--lighter")).toBe(valueOf(css, "--expected"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports min, max and clamp over channels", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --seed: hct(120 40 50);",
|
||||||
|
" --capped: hct(from var(--seed) h c clamp(10, t, 45));",
|
||||||
|
" --expected: hct(120 40 45);",
|
||||||
|
" --smallest: hct(from var(--seed) h c min(t, 20));",
|
||||||
|
" --expected-smallest: hct(120 40 20);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--capped")).toBe(valueOf(css, "--expected"));
|
||||||
|
expect(valueOf(css, "--smallest")).toBe(
|
||||||
|
valueOf(css, "--expected-smallest"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves a hex seed through a variable and inline alike", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --seed: #336699;",
|
||||||
|
" --via-var: hct(from var(--seed) h c t);",
|
||||||
|
" --inline: hct(from #336699 h c t);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isHexColor(valueOf(css, "--via-var"))).toBe(true);
|
||||||
|
expect(valueOf(css, "--via-var")).toBe(valueOf(css, "--inline"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves seeds that are themselves hct() tokens", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --first: hct(200 30 60);",
|
||||||
|
" --second: hct(from var(--first) h c t);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--second")).toBe(valueOf(css, "--first"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses :root seeds inside other selectors", async () => {
|
||||||
|
const { css } = await process(
|
||||||
|
[
|
||||||
|
":root { --seed: hct(90 50 40); }",
|
||||||
|
".dark { --from-root: hct(from var(--seed) h c t); }",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--from-root")).toBe(valueOf(css, "--seed"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves every hct() call inside one declaration", async () => {
|
||||||
|
const { css } = await process(":root { --pair: hct(0 0 10) hct(0 0 90); }");
|
||||||
|
|
||||||
|
expect(valueOf(css, "--pair")).toBe(
|
||||||
|
`${valueOf(css, "--pair").split(" ")[0]} ${valueOf(css, "--pair").split(" ")[1]}`,
|
||||||
|
);
|
||||||
|
expect(valueOf(css, "--pair").split(" ")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves values without hct() untouched", async () => {
|
||||||
|
const source = ":root { --size: 12px; --name: red; --ref: var(--size); }";
|
||||||
|
const { css } = await process(source);
|
||||||
|
|
||||||
|
expect(css).toContain("--size: 12px");
|
||||||
|
expect(css).toContain("--name: red");
|
||||||
|
expect(css).toContain("--ref: var(--size)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns and keeps the value when the seed is unknown", async () => {
|
||||||
|
const { css, warnings } = await process(
|
||||||
|
":root { --orphan: hct(from var(--missing) h c t); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--orphan")).toBe("hct(from var(--missing) h c t)");
|
||||||
|
expect(warnings).toHaveLength(1);
|
||||||
|
expect(warnings[0]).toContain("could not resolve");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns and keeps the value on a circular reference", async () => {
|
||||||
|
const { css, warnings } = await process(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --ping: hct(from var(--pong) h c t);",
|
||||||
|
" --pong: hct(from var(--ping) h c t);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(valueOf(css, "--ping")).toBe("hct(from var(--pong) h c t)");
|
||||||
|
expect(warnings).toHaveLength(2);
|
||||||
|
expect(warnings.every((text) => text.includes("could not resolve"))).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns and keeps the value for a malformed hct() body", async () => {
|
||||||
|
const { css, warnings } = await process(":root { --bad: hct(1 2); }");
|
||||||
|
|
||||||
|
expect(valueOf(css, "--bad")).toBe("hct(1 2)");
|
||||||
|
expect(warnings).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import postcss from "postcss";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { checkColorAuthorship } from "../token-audit/colors.mjs";
|
||||||
|
import {
|
||||||
|
collectTokens,
|
||||||
|
isColorValue,
|
||||||
|
isDerived,
|
||||||
|
} from "../token-audit/helpers.mjs";
|
||||||
|
import { checkParity } from "../token-audit/parity.mjs";
|
||||||
|
import { checkUnresolvedRefs } from "../token-audit/refs.mjs";
|
||||||
|
import { checkUnused } from "../token-audit/unused.mjs";
|
||||||
|
|
||||||
|
function themes(css) {
|
||||||
|
const root = postcss.parse(css);
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
light: collectTokens(root, ":root"),
|
||||||
|
dark: collectTokens(root, '[data-theme="dark"]'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("checkParity", () => {
|
||||||
|
it("accepts a color token that has both themes", () => {
|
||||||
|
const { light, dark } = themes(
|
||||||
|
':root { --color-text: #111; } [data-theme="dark"] { --color-text: #eee; }',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkParity({ light, dark })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a light-only color token", () => {
|
||||||
|
const { light, dark } = themes(":root { --color-text: #111; }");
|
||||||
|
|
||||||
|
expect(checkParity({ light, dark })).toEqual([
|
||||||
|
' --color-text in :root has no [data-theme="dark"] override',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a dark-only color token as orphaned", () => {
|
||||||
|
const { light, dark } = themes(
|
||||||
|
'[data-theme="dark"] { --color-text: #eee; }',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkParity({ light, dark })).toEqual([
|
||||||
|
' --color-text in [data-theme="dark"] is missing in :root',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exempts derived tokens because they follow the theme", () => {
|
||||||
|
const { light, dark } = themes(
|
||||||
|
":root { --color-text: hct(from var(--brand-main) h c t); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkParity({ light, dark })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exempts non-color tokens", () => {
|
||||||
|
const { light, dark } = themes(":root { --space-page-top: 48px; }");
|
||||||
|
|
||||||
|
expect(checkParity({ light, dark })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkColorAuthorship", () => {
|
||||||
|
it("accepts hct() and color-mix()", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
[
|
||||||
|
":root {",
|
||||||
|
" --color-main: hct(160 80 48);",
|
||||||
|
" --color-mixed: color-mix(in oklch, var(--color-main), #fff);",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkColorAuthorship(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts seed tokens in any format", () => {
|
||||||
|
const { root } = themes(":root { --brand-main: #ff8800; }");
|
||||||
|
|
||||||
|
expect(checkColorAuthorship(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a hardcoded hex color with its location", () => {
|
||||||
|
const { root } = themes(":root { --color-text: #112233; }");
|
||||||
|
|
||||||
|
expect(checkColorAuthorship(root)).toEqual([
|
||||||
|
" --color-text (:root): #112233 — colors must be hct(); only --brand-main/--brand-alt may use other formats",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an rgb() color outside hct()", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
'[data-theme="dark"] { --color-accent: rgb(1 2 3); }',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkColorAuthorship(root)[0]).toContain("--color-accent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores non-color values and non-custom properties", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
":root { --font-size-m: 14px; } .x { color: #ff0000; }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkColorAuthorship(root)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkUnused", () => {
|
||||||
|
it("reports a token that is never referenced", () => {
|
||||||
|
const { root } = themes(":root { --color-dust: #111; }");
|
||||||
|
|
||||||
|
expect(checkUnused(root, new Set())).toEqual(["--color-dust"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a token that is referenced somewhere", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
":root { --color-text: #111; } .x { color: var(--color-text); }",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnused(root, new Set(["--color-text"]))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collects definitions across both themes", () => {
|
||||||
|
const { root } = themes(
|
||||||
|
[
|
||||||
|
":root { --color-text: #111; }",
|
||||||
|
'[data-theme="dark"] { --color-text: #eee; --color-dust: #222; }',
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkUnused(root, new Set(["--color-text"]))).toEqual([
|
||||||
|
"--color-dust",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
it("detects color values in every supported notation", () => {
|
||||||
|
for (const value of [
|
||||||
|
"#abc",
|
||||||
|
"#aabbcc",
|
||||||
|
"rgb(1 2 3)",
|
||||||
|
"hsl(1 2% 3%)",
|
||||||
|
"oklch(0.5 0.1 30)",
|
||||||
|
"hct(1 2 3)",
|
||||||
|
"color-mix(in oklch, #fff, #000)",
|
||||||
|
]) {
|
||||||
|
expect(isColorValue(value), value).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat sizes and durations as colors", () => {
|
||||||
|
for (const value of ["12px", "0.3s", "1fr", "bold", "var(--size-m)"]) {
|
||||||
|
expect(isColorValue(value), value).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats values referencing tokens as derived", () => {
|
||||||
|
expect(isDerived("hct(from var(--brand-main) h c t)")).toBe(true);
|
||||||
|
expect(isDerived("hct(160 80 48)")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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):");
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// (adapter-static): /foo -> foo.html | foo/index.html, MIME по расширению.
|
// (adapter-static): /foo -> foo.html | foo/index.html, MIME по расширению.
|
||||||
// Запуск: pnpm build && node scripts/serve-static.mjs --port 4173
|
// Запуск: pnpm build && node scripts/serve-static.mjs --port 4173
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { readFileSync, existsSync, statSync } from "node:fs";
|
import { readFileSync, statSync } from "node:fs";
|
||||||
import { extname, join, resolve } from "node:path";
|
import { extname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const portIndex = process.argv.indexOf("--port");
|
const portIndex = process.argv.indexOf("--port");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
// Unused tokens: defined in app.css but never referenced via var()
|
// Unused tokens: defined in app.css but never referenced via var()
|
||||||
// anywhere in src. A dead token is not the single source of truth — it's dust.
|
// anywhere in src. A dead token is not the single source of truth — it's dust.
|
||||||
// Reported as a warning; it does not fail the run.
|
// Reported as a warning; it does not fail the run. The `used` set is injectable
|
||||||
|
// so tests can pass a fixture instead of scanning src.
|
||||||
|
|
||||||
import { collectUsedTokens } from "./helpers.mjs";
|
import { collectUsedTokens } from "./helpers.mjs";
|
||||||
|
|
||||||
export function checkUnused(root) {
|
export function checkUnused(root, used = collectUsedTokens()) {
|
||||||
const defined = new Set();
|
const defined = new Set();
|
||||||
root.walkDecls((decl) => {
|
root.walkDecls((decl) => {
|
||||||
if (decl.prop.startsWith("--")) defined.add(decl.prop);
|
if (decl.prop.startsWith("--")) defined.add(decl.prop);
|
||||||
});
|
});
|
||||||
const used = collectUsedTokens();
|
|
||||||
return [...defined].filter((token) => !used.has(token));
|
return [...defined].filter((token) => !used.has(token));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
--color-text: hct(from var(--brand-main) h 5.9 1.7);
|
--color-text: hct(from var(--brand-main) h 5.9 1.7);
|
||||||
--color-text-muted: hct(from var(--brand-main) h 13.4 19.2);
|
--color-text-muted: hct(from var(--brand-main) h 13.4 19.2);
|
||||||
--color-text-accent: hct(from var(--brand-alt) h 5.9 1.7);
|
--color-text-accent: hct(from var(--brand-alt) h 5.9 1.7);
|
||||||
--color-text-accent-muted: hct(from var(--brand-alt) h 13.4 19.2);
|
|
||||||
|
|
||||||
--color-main: hct(from var(--brand-main) h 80 48);
|
--color-main: hct(from var(--brand-main) h 80 48);
|
||||||
--color-accent: hct(from var(--brand-alt) h 80 48);
|
--color-accent: hct(from var(--brand-alt) h 80 48);
|
||||||
@@ -141,7 +140,6 @@
|
|||||||
--space-page-top: 48px;
|
--space-page-top: 48px;
|
||||||
--space-page-bottom: 72px;
|
--space-page-bottom: 72px;
|
||||||
|
|
||||||
--space-text-s: 0;
|
|
||||||
--space-text-m: 0.04em;
|
--space-text-m: 0.04em;
|
||||||
--space-text-l: 0.08em;
|
--space-text-l: 0.08em;
|
||||||
--space-text-xl: 0.12em;
|
--space-text-xl: 0.12em;
|
||||||
@@ -164,7 +162,6 @@
|
|||||||
--size-card-min-narrow: 200px;
|
--size-card-min-narrow: 200px;
|
||||||
--size-parts-grid-min: 120px;
|
--size-parts-grid-min: 120px;
|
||||||
--size-workspace-min: 260px;
|
--size-workspace-min: 260px;
|
||||||
--size-content-max: 880px;
|
|
||||||
--size-content-max-wide: 1680px;
|
--size-content-max-wide: 1680px;
|
||||||
|
|
||||||
--font-size-s: 12px;
|
--font-size-s: 12px;
|
||||||
@@ -178,7 +175,6 @@
|
|||||||
--radius-m: 4px;
|
--radius-m: 4px;
|
||||||
|
|
||||||
--duration-s: 0.15s;
|
--duration-s: 0.15s;
|
||||||
--duration-m: 0.3s;
|
|
||||||
--duration-l: 0.8s;
|
--duration-l: 0.8s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +193,6 @@
|
|||||||
--color-text: hct(from var(--brand-main) h 25 90);
|
--color-text: hct(from var(--brand-main) h 25 90);
|
||||||
--color-text-muted: hct(from var(--brand-main) h 22 70);
|
--color-text-muted: hct(from var(--brand-main) h 22 70);
|
||||||
--color-text-accent: hct(from var(--brand-alt) h 25 90);
|
--color-text-accent: hct(from var(--brand-alt) h 25 90);
|
||||||
--color-text-accent-muted: hct(from var(--brand-alt) h 22 70);
|
|
||||||
|
|
||||||
--color-main: hct(from var(--brand-main) h 60 56);
|
--color-main: hct(from var(--brand-main) h 60 56);
|
||||||
--color-accent: hct(from var(--brand-alt) h 60 56);
|
--color-accent: hct(from var(--brand-alt) h 60 56);
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
{ value: "analyze", label: t("categories.analyze") },
|
{ value: "analyze", label: t("categories.analyze") },
|
||||||
{ value: "generate", label: t("categories.generate") },
|
{ value: "generate", label: t("categories.generate") },
|
||||||
];
|
];
|
||||||
|
|
||||||
// TODO: Button component
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="catalog-toolbar">
|
<div class="catalog-toolbar">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n/t";
|
||||||
import type {
|
import type {
|
||||||
ColorPair,
|
ColorPair,
|
||||||
ColorPairSpec,
|
ColorPairSpec,
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
value={current.from}
|
value={current.from}
|
||||||
oninput={(e) => setColor("from", (e.target as HTMLInputElement).value)}
|
oninput={(e) => setColor("from", (e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
<span class="axis-label">From</span>
|
<span class="axis-label">{t("ui.from")}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="pair-axis">
|
<label class="pair-axis">
|
||||||
<span class="swatch" style="background:{current.to}"></span>
|
<span class="swatch" style="background:{current.to}"></span>
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
value={current.to}
|
value={current.to}
|
||||||
oninput={(e) => setColor("to", (e.target as HTMLInputElement).value)}
|
oninput={(e) => setColor("to", (e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
<span class="axis-label">To</span>
|
<span class="axis-label">{t("ui.to")}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
<script lang="ts">
|
|
||||||
// TODO: insert version from package.json
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer class="preview-footer">
|
<footer class="preview-footer">
|
||||||
<span>easy-png-tools <b class="version">v0.1.0</b></span>
|
<span>easy-png-tools <b class="version">v0.1.0</b></span>
|
||||||
<span>© 2026</span>
|
<span>© 2026</span>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
loading = false,
|
loading = false,
|
||||||
parts = undefined,
|
parts = undefined,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// TODO: make canvas aspect ratio auto, from 16:9 to 9:16 max
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<figure class="tile">
|
<figure class="tile">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check } from "@lucide/svelte";
|
import { Check } from "@lucide/svelte";
|
||||||
|
|
||||||
// TODO: сделать эти типы общими для кнопок \ бейджей \ етц???
|
|
||||||
const ToneVariantDefine = {
|
const ToneVariantDefine = {
|
||||||
DEFAULT: "default",
|
DEFAULT: "default",
|
||||||
ACCENT: "accent",
|
ACCENT: "accent",
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { theme, ontoggle }: Props = $props();
|
let { theme, ontoggle }: Props = $props();
|
||||||
|
|
||||||
// TODO: change icon animations - custom button instead of IconButton
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
),
|
),
|
||||||
})).filter((g) => g.tools.length > 0),
|
})).filter((g) => g.tools.length > 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: make main page different from catalog - larger and simpler search, no categories
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="catalog-page">
|
<div class="catalog-page">
|
||||||
|
|||||||
+29
-1
@@ -4,7 +4,35 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts", "eslint-plugins/**/*.test.ts"],
|
include: [
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"eslint-plugins/**/*.test.ts",
|
||||||
|
"scripts/**/*.test.mjs",
|
||||||
|
],
|
||||||
environment: "node",
|
environment: "node",
|
||||||
|
coverage: {
|
||||||
|
provider: "v8",
|
||||||
|
reporter: ["text-summary"],
|
||||||
|
reportsDirectory: "./coverage",
|
||||||
|
include: ["src/lib/**/*.ts"],
|
||||||
|
exclude: [
|
||||||
|
"src/lib/**/*.test.ts",
|
||||||
|
"src/lib/**/test-helpers.ts",
|
||||||
|
"src/lib/**/*.svelte.ts",
|
||||||
|
"src/lib/executor/executor.worker.ts",
|
||||||
|
"src/lib/components/define.ts",
|
||||||
|
"src/lib/core/domText.ts",
|
||||||
|
"src/lib/core/io.ts",
|
||||||
|
"src/lib/i18n/en.ts",
|
||||||
|
"src/lib/i18n/ru.ts",
|
||||||
|
"src/lib/tool-icons.ts",
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
statements: 90,
|
||||||
|
branches: 82,
|
||||||
|
functions: 72,
|
||||||
|
lines: 91,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user