mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
253 lines
21 KiB
Markdown
253 lines
21 KiB
Markdown
# Архитектура
|
||
|
||
> Статус: **active**. Описание текущего кода: границы модулей, поток данных от
|
||
> реестра до скачивания, точки расширения. Канонический источник истины — сам
|
||
> код; этот документ объясняет, **куда смотреть и что с чем связано**, и не
|
||
> дублирует сигнатуры.
|
||
|
||
## 1. Слои
|
||
|
||
```txt
|
||
web/src/lib/
|
||
registry/ # источник истины: инструменты и страницы
|
||
registry/tools/ # Tool: реализации (id), без адресов и текстов
|
||
registry/pages/ # Page: адрес, тексты, категория, список шагов
|
||
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
|
||
catalog.ts # группировка страниц и счётчики для /list-tools
|
||
categories.ts # id/названия категорий
|
||
tool-icons.ts # slug страницы → иконка
|
||
core/ # операции над ImageData; без DOM, кроме io.ts/domText.ts
|
||
executor/ # единственная точка исполнения инструментов
|
||
i18n/ # словари ru/en, t(), состояние локали
|
||
components/ # UI (fields/, layout/, ui/, schema-компоненты)
|
||
zip.ts # сборка мультифайлового результата (1 → many)
|
||
theme.svelte.ts # light/dark + localStorage
|
||
web/src/routes/
|
||
+page.svelte # главная
|
||
list-tools/ # каталог
|
||
tools/[slug]/ # страница инструмента (адрес = slug страницы)
|
||
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
|
||
```
|
||
|
||
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
|
||
`executor/**` и `core/**`; `core/**` не знает ни о ком; `registry/**` описывает
|
||
данные и знает только `core/**` и `registry-schema.ts`. Нарушение этого порядка
|
||
— повод не заводить импорт, а инвертировать зависимость.
|
||
|
||
Исключение из «core без DOM» — `core/io.ts` (декодирование файла, кодирование,
|
||
скачивание) и `core/domText.ts` (canvas-рендер текста и эмодзи). Они трогают
|
||
`document`, `createImageBitmap` и `HTMLCanvasElement`, поэтому не работают в
|
||
node-Vitest и исключены из coverage-гейта (`docs/quality-gates.md`, раздел
|
||
Coverage). Всё остальное в `core/**` — чистые функции над `ImageData`.
|
||
|
||
### Чего в коде нет
|
||
|
||
Страница исполняет ровно один шаг: загрузил файл → применила инструмент →
|
||
скачала результат. Отсутствуют:
|
||
|
||
- **Пайплайн / цепочки шагов** — модуля нет, состояния нет, localStorage для
|
||
него нет. Модель страницы уже хранит список `steps`, но исполняет первый шаг,
|
||
а `registry.test.ts` требует, чтобы шаг был ровно один: композиция формы из
|
||
полей двух инструментов — отдельная работа, описанная как фаза 11
|
||
`docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte`
|
||
(сейчас импортируется только витриной `/kit`) и ключи `chain.*`,
|
||
`errors.badPipelineShape`, `errors.pipelineVersion`, `errors.noSteps`.
|
||
- **Разделения страниц, которые делят один инструмент** — сводит все
|
||
convert-страницы на один инструмент; задача в `docs/backlog.md`. Пока
|
||
`registry.test.ts` не требует, чтобы на инструмент ссылались две страницы, а
|
||
на практике не ссылается ни одна.
|
||
|
||
Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по
|
||
пайплайну — `docs/roadmap.md`.
|
||
|
||
## 2. Поток данных
|
||
|
||
```txt
|
||
registry/{tools,pages}/*.ts
|
||
│
|
||
├─► catalog.ts ──► routes/list-tools (каталог страниц, поиск, фильтры)
|
||
│
|
||
└─► routes/tools/[slug]/+page.svelte
|
||
│ getPageBySlug(slug) → Page, getTool(steps[0].id) → Tool
|
||
▼
|
||
SchemaToolView.svelte оркестратор страницы
|
||
│ schema = tool.schema, тексты и имя файла — от page
|
||
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
|
||
├─► SchemaSourceTile/SchemaTextSource.svelte вход
|
||
└─► SchemaPreview + schema-preview-model.ts вид превью
|
||
│ buildSchemaPreviewModel(input) → { sourceValue, resultValue, formatValue, hasResult }
|
||
▼
|
||
executor.execute(ctx) единственная точка исполнения
|
||
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
|
||
▼
|
||
core/* пиксели
|
||
│
|
||
▼
|
||
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
|
||
│ кодирование по Tool.output (mime/ext/quality)
|
||
└─► zip.ts, если result === "files"
|
||
```
|
||
|
||
Ключевые решения:
|
||
|
||
- **Реестр — единственный источник истины.** Страница и форма строятся из
|
||
записей; новый инструмент = новая запись `Tool` + функция `run` и новая
|
||
страница `Page` на него.
|
||
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
|
||
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
|
||
вызывающего кода.
|
||
- **Форма параметров не знает про инструмент.** Она рендерится из
|
||
`registry-schema.ts`, поэтому добавление вида поля не трогает страницы.
|
||
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
|
||
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
|
||
|
||
## 3. Инструмент и страница
|
||
|
||
Реестр двухслойный: реализация и то, что видит пользователь, лежат в разных
|
||
файлах. `Tool` (`web/src/lib/registry/types.ts`, записи в
|
||
`registry/tools/<group>.ts`):
|
||
|
||
| Поле | Смысл |
|
||
| ---------- | ------------------------------------------------------------------------------ |
|
||
| `id` | внутреннее имя инструмента, уникально; ключ `tools.<id>.*` и `toolId` в worker |
|
||
| `schema` | `toolSchema({ fields, layout?, meta? })` |
|
||
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
|
||
| `result`? | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
|
||
| `domOnly`? | инструмент требует DOM (canvas); превью-executor запускает его напрямую |
|
||
| `output`? | mime/ext/quality для скачивания (не у всех) |
|
||
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
|
||
|
||
`Page` (записи в `registry/pages/<group>.ts`):
|
||
|
||
| Поле | Смысл |
|
||
| ------------- | ---------------------------------------------------------------------- |
|
||
| `slug` | уникальный адрес `/tools/<slug>`, имя скачиваемого файла и ключ иконки |
|
||
| `title` | EN-строка страницы, переводится через `pages.<slug>.title` |
|
||
| `description` | EN-строка страницы, переводится через `pages.<slug>.description` |
|
||
| `category` | id из `categories.ts`; по нему каталог группирует страницы |
|
||
| `steps` | упорядоченный список шагов `{ id, params? }` — ссылки на инструменты |
|
||
|
||
Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по
|
||
каталогу; `id` — ключ `tools.<id>.*` (подписи полей, `options`, `results`),
|
||
`toolId` worker-протокола и запись пайплайна. Модель допускает, что один
|
||
инструмент обслуживает несколько страниц, а страница состоит из нескольких
|
||
инструментов; на практике пока каждая страница ссылается на свой инструмент, а
|
||
исполняется первый (и единственный) шаг.
|
||
|
||
Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт
|
||
только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`.
|
||
Правило имени `id`: снять суффикс `-png` и в середине имени (`png-to-base64` →
|
||
`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`). Правило
|
||
относится только к `id`: у `slug` своя история и SEO-значение, поэтому
|
||
переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя
|
||
файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Слаги, сложившиеся
|
||
до разделения реестра, правилу не соответствуют (`convert-png-to-jpg`,
|
||
`change-png-opacity`, `png-is-grayscale`, `verify-is-png` и другие) — это не
|
||
ошибка, менять их без SEO-решения нельзя. Форму slug проверяет
|
||
`registry.test.ts` (только форма, не смысл).
|
||
|
||
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
|
||
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
|
||
и `PAGES` собираются в `registry/index.ts` из восьми групп; `getTool(id)` и
|
||
`getPageBySlug(slug)` — способы достать запись, `registry.test.ts` проверяет
|
||
уникальность обоих имён, существование инструмента под каждым шагом и то, что
|
||
шаг ровно один.
|
||
|
||
## 4. Схема параметров
|
||
|
||
`registry-schema.ts` — единственное место, где описаны виды полей.
|
||
|
||
- **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`,
|
||
`checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`,
|
||
`font-style`, `plate`, `gradient`.
|
||
- **Фабрика `field.*`** создаёт `Field<T>`; UI-контрол подбирается в
|
||
`SchemaFields.svelte` по `spec.kind` через карту `FIELDS`.
|
||
- **`schema.layout`** группирует поля (`resolveLayoutGroups`); раскладка
|
||
разделов — единственное место, где живёт порядок и группировка.
|
||
- **Значения по умолчанию** даёт `defaultSchemaParams`; `applySourceDefaults`
|
||
подставляет размеры текущего source; `sanitizeSchemaParams` приводит мусор к
|
||
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
|
||
|
||
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
|
||
контрол (`components/fields/schema/`, а для `dimension` —
|
||
`fields/DimensionField.svelte`) → ключ в `FIELDS` → дефолт и санитизация →
|
||
i18n-подписи → тест в `registry-schema.test.ts`.
|
||
|
||
## 5. Исполнение
|
||
|
||
`executor/executor.ts` — фабрика `createExecutor({ workerFactory? })` плюс
|
||
готовый экспорт `execute`. Контракт асинхронный (`Promise`), параметры
|
||
сериализуются по `executor/protocol.ts`, исполнение уходит в
|
||
`executor.worker.ts`; `worker-handler.ts` разбирает сообщение и зовёт `run`.
|
||
Если worker недоступен — прямой вызов. UI импортирует `execute` из
|
||
`executor/index.ts`.
|
||
|
||
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
|
||
исполнителя, а не правку компонентов.
|
||
|
||
## 6. i18n
|
||
|
||
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
|
||
`LOCALES`/`BASE_LOCALE`, `t.ts` с `t()`, `locale.svelte.ts` с состоянием
|
||
локали). Пользовательский текст живёт только в словарях и читается через `t()`.
|
||
Списки ключей и допустимых атрибутов для i18n-правил линтера лежат не здесь, а в
|
||
`web/eslint-plugins/i18n/lists.js`. Секции разделены по слоям:
|
||
`pages.<slug>.title` и `pages.<slug>.description` — тексты страницы (в базовой
|
||
локали секция пустая, EN берётся из реестра),
|
||
`tools.<id>.options/params/results` — подписи полей и тексты вердиктов
|
||
инструмента, `fields.*`/`groups.*` — общие подписи схемы. Инварианты паритета
|
||
ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали —
|
||
`web/eslint-plugins/README.md`.
|
||
|
||
## 7. Точки расширения
|
||
|
||
| Задача | Где править |
|
||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| Новый инструмент | `registry/tools/<group>.ts` + страница в `registry/pages/<group>.ts` + i18n + тесты (рецепт ниже) |
|
||
| Новая страница на готовом инструменте | запись `Page` со своим `slug` и текстами; новый код не нужен |
|
||
| Смена `id` | `registry/tools/*` + ключи `tools.*` в словарях + хардкод id в тестах; адреса, имена файлов, иконки и карта не меняются |
|
||
| Смена `slug` | `registry/pages/*` + `pages.*` в словарях + `tool-icons.ts` + `docs/tools-map.md` + хардкод slug в тестах и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи |
|
||
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
|
||
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
|
||
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
|
||
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
|
||
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
|
||
| Мультифайловый результат | `result: "files"` + `zip.ts` |
|
||
|
||
## 8. Рецепт: добавить инструмент
|
||
|
||
1. **Найти соседей.** Поиск по ключу в `registry/tools/<group>.ts` и по всему
|
||
`web/src/` — не заводить дублирующую константу, тип или утилиту.
|
||
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
|
||
`generate`/`text`/`analyze`) и положить инструмент в её экспорт; при новой
|
||
категории — сначала `categories.ts`.
|
||
3. **Задать `id`** по правилу раздела 3: без png-интента (`crop`, `to-jpg`,
|
||
`from-base64`, `is-grayscale`).
|
||
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
|
||
сгруппировать. Качество кодирования (`quality`) — не поле страницы, а
|
||
настройка скачивания (`docs/plan-seo.md` §4, S1f).
|
||
5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
|
||
`core/`, без DOM.
|
||
6. **Добавить страницу** в `registry/pages/<group>.ts`: `slug` с адресным
|
||
png-интентом (`crop-png`), тексты EN, `category`, `steps: [{ id }]`.
|
||
7. **Проверить обязательные контракты**: инструмент попадает в `TOOLS`, страница
|
||
в `PAGES`, `/tools/<slug>` prerender-ится, `input`/`result` согласованы с
|
||
тем, что делает `run`.
|
||
8. **Добавить i18n-ключи во все локали** сразу: `pages.<slug>.title` и
|
||
`pages.<slug>.description`, при наличии опций и вердиктов — `tools.<id>`.
|
||
9. **Тесты:** unit — в `registry/registry.test.ts` или рядом с `core`-функцией;
|
||
e2e-smoke — существующий spec по типу `input`/`result` (правила —
|
||
`docs/testing-strategy.md`).
|
||
10. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе
|
||
«Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug`
|
||
страниц** из `registry/pages/`, то есть адреса. Проверяет
|
||
`web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница
|
||
без записи в карте роняют тест; разделы «Можно добавить» и «Идеи»
|
||
проверяются в обратную сторону — реализованный slug в плане тоже роняет
|
||
тест, поэтому при переезде инструмента из плана в реестр его вынимают
|
||
оттуда. Обоснование и границы автоматизации — `docs/decisions.md`, раздел 4.
|
||
|
||
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
|
||
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
|
||
`docs/quality-gates.md`).
|