22 KiB
Архитектура
Статус: active. Описание текущего кода: границы модулей, поток данных от реестра до скачивания, точки расширения. Канонический источник истины — сам код; этот документ объясняет, куда смотреть и что с чем связано, и не дублирует сигнатуры.
1. Слои
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 по слоям: ui/ примитивы, layout/ каркас,
# catalog/ каталог, schema/ view инструмента,
# fields/ поля (+ fields/schema/ контролы схемы),
# display/ типографика и карточки, feedback/ состояния
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.
Чего в коде нет
Страница исполняет ровно один шаг: загрузил файл → применила инструмент → скачала результат. Отсутствуют:
- Пайплайн / цепочки шагов — реализованы: модуль шагов
web/src/lib/pipeline.svelte.ts(createChain/parseChain/serializeSteps), именованные цепочки —web/src/lib/chains.svelte.ts(localStoragechains:named). Страница инструмента всегда открывается чистой; при первом структурном изменении (добавлен шаг, заменён первый инструмент) цепочка автоматически становится именованной (имя — дата/время) и маршрут бесшовно меняется на/pipeline?id=<id>(передача исходника через handoff-стор, replaceState)./chains— список цепочек (переименовать/удалить/открыть/ создать). Легаси-автосейвpipeline:<slug>снят без миграции (ключи просто перестают читаться). Экспортный JSON-формат не устоялся и может меняться до фазы 11docs/roadmap.md. - Разделения страниц, которые делят один инструмент — сводит все
convert-страницы на один инструмент; задача в
docs/backlog.md. Покаregistry.test.tsне требует, чтобы на инструмент ссылались две страницы, а на практике не ссылается ни одна.
Задачи по обоим пунктам — в docs/backlog.md; архитектурные решения по
пайплайну — docs/roadmap.md.
2. Поток данных
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/) → ключ в 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. Рецепт: добавить инструмент
- Найти соседей. Поиск по ключу в
registry/tools/<group>.tsи по всемуweb/src/— не заводить дублирующую константу, тип или утилиту. - Выбрать группу (
geometry/alpha/color/convert/filters/generate/text/analyze) и положить инструмент в её экспорт; при новой категории — сначалаcategories.ts. - Задать
idпо правилу раздела 3: без png-интента (crop,to-jpg,from-base64,is-grayscale). - Описать схему через
field.*, задатьlayout, если поля нужно сгруппировать. Качество кодирования (quality) — не поле страницы, а настройка скачивания (docs/plan-seo.md§4, S1f). - Написать
runповерхcore/*; пиксельные операции — чистые функции вcore/, без DOM. - Добавить страницу в
registry/pages/<group>.ts:slugс адресным png-интентом (crop-png), тексты EN,category,steps: [{ id }]. - Проверить обязательные контракты: инструмент попадает в
TOOLS, страница вPAGES,/tools/<slug>prerender-ится,input/resultсогласованы с тем, что делаетrun. - Добавить i18n-ключи во все локали сразу:
pages.<slug>.titleиpages.<slug>.description, при наличии опций и вердиктов —tools.<id>. - Тесты: unit — в
registry/registry.test.tsили рядом сcore-функцией; e2e-smoke — существующий spec по типуinput/result(правила —docs/testing-strategy.md). - Обновить
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).