Files
easy-png-tools2/docs/architecture.md
T

16 KiB
Raw Blame History

Архитектура

Статус: active. Описание текущего кода: границы модулей, поток данных от реестра до скачивания, точки расширения. Канонический источник истины — сам код; этот документ объясняет, куда смотреть и что с чем связано, и не дублирует сигнатуры.

1. Слои

web/src/lib/
  registry/            # источник истины: записи ToolEntry
  registry-schema.ts   # декларативная схема параметров + санитизация дефолтов
  catalog.ts           # группировка и счётчики для /list-tools
  categories.ts        # id/названия категорий
  tool-icons.ts        # id → иконка
  core/                # чистый TS: операции над ImageData, без DOM
  executor/            # единственная точка исполнения инструментов
  i18n/                # словари ru/en, t(), списки для i18n-правил линтера
  components/          # UI (fields/, layout/, ui/, schema-компоненты)
  zip.ts               # сборка мультифайлового результата (1 → many)
  theme.svelte.ts      # light/dark + localStorage
web/src/routes/
  +page.svelte         # главная
  list-tools/          # каталог
  tools/[id]/          # страница инструмента (адрес = id записи)
  kit/                 # витрина компонентов (production, но исключена из i18n-гейта)

Правило зависимостей: components/** и routes/** знают про registry/**, executor/** и core/**; core/** не знает ни о ком и не зависит от DOM; registry/** описывает данные и знает только core/** и registry-schema.ts. Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.

Чего в коде нет

Страница инструмента работает ровно с одним инструментом: загрузил файл → применил одну запись реестра → скачал результат. Отсутствуют:

  • Пайплайн / цепочки шагов — модуля нет, состояния нет, localStorage для него нет. docs/roadmap.md описывает его как отдельную фазу, а не как поставленное. Под будущую фазу зарезервированы components/StepCard.svelte (сейчас импортируется только витриной /kit) и ключи chain.*, errors.badPipelineShape, errors.noSteps.
  • Разделения id и slug — раздел 3.

Задачи по обоим пунктам — в docs/backlog.md; архитектурные решения по пайплайну — docs/roadmap.md.

2. Поток данных

registry/*.ts  ──(TOOLS: ToolEntry[])
        │
        ├─► catalog.ts ──► routes/list-tools  (каталог, поиск, фильтры)
        │
        └─► routes/tools/[id]/+page.svelte
                 │  getTool(id) → запись реестра
                 ▼
             SchemaToolView.svelte          оркестратор страницы
                 │  schema = tool.schema
                 ├─► SchemaFields.svelte     поля из schema.fields (FIELDS: kind → контрол)
                 ├─► SchemaSourceTile/SchemaTextSource.svelte   вход
                 └─► SchemaPreview + schema-preview-model.ts    вид превью
                 │  buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
                 ▼
             executor.execute(ctx)          единственная точка исполнения
                 │  сериализация параметров → worker → worker-handler → tool.run(ctx)
                 ▼
             core/*                         пиксели
                 │
                 ▼
             SchemaResultTile / SchemaTextResult  + SchemaActions (download/copy)
                 │  кодирование по ToolEntry.output (mime/ext/quality)
                 └─► zip.ts, если result === "files"

Ключевые решения:

  • Реестр — единственный источник истины. Страница и форма строятся из записи; новый инструмент = новая запись + функция run.
  • Исполнение идёт только через executor.execute. Компоненты не зовут tool.run напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки вызывающего кода.
  • Форма параметров не знает про инструмент. Она рендерится из registry-schema.ts, поэтому добавление вида поля не трогает страницы.
  • Модель превью отделена от вёрстки. schema-preview-model.ts — чистая функция, покрыта unit-тестами; SchemaPreview.svelte только рисует её вывод.

3. Запись реестра

ToolEntry (web/src/lib/registry/types.ts):

Поле Смысл
id идентичность инструмента и публичный адрес /tools/<id> и база имени скачиваемого файла
title EN-строка реестра, переводится через tools.*.title
description EN-строка реестра, переводится через tools.*.description
category id из categories.ts
schema toolSchema({ fields, layout?, meta? })
input "image" | "text" | "none" — что ждёт инструмент на входе
result "image" | "text" | "verdict" | "files" — что отдаёт
domOnly инструмент требует DOM (canvas); превью-executor запускает его напрямую, минуя worker
output mime/ext/quality для download (не у всех инструментов)
icon id иконки из tool-icons.ts
run(ctx) единственный метод: ctx → ToolResult

Отдельного поля slug нет: адрес страницы, ключ i18n (tools.<id>.*) и имя файла скачивания — это один и тот же id. Инструмент работает с любым поддержанным форматом, но страница держит png-интент, поэтому -png живёт внутри id (id: "crop-png" → /tools/crop-png → crop-png.png). Следствие: переименование id меняет адрес страницы, ключи tools.* и имя файла.

Хелперы genTool / imgTool / textGen из types.ts задают run для типовых случаев (приведение типа результата), чтобы запись не повторяла обвязку. TOOLS собирается в registry/index.ts из восьми групп; getTool(id) — единственный способ достать запись, registry.test.ts проверяет уникальность id.

Разделение id (внутреннее имя) и slug (адрес) — решённое, но не реализованное решение: задача и порядок волн в docs/backlog.md, раздел «Мелочи всякие». До её выполнения этот документ описывает текущее состояние, а id и slug совпадают.

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(factories?) плюс готовый экспорт execute. Контракт асинхронный (Promise), параметры сериализуются, исполнение уходит в executor.worker.ts; worker-handler.ts разбирает сообщение и зовёт run. Если worker недоступен — прямой вызов.

Отсюда следует: подключение wasm позже означает замену реализации внутри исполнителя, а не правку компонентов.

6. i18n

Словари — web/src/lib/i18n/ (en.ts, ru.ts, общий dict.ts с LOCALES/BASE_LOCALE). Пользовательский текст живёт только в словарях и читается через t(); строки реестра (title/description) — источник для EN-ключей tools.* и словарь-переводчик. Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали — web/eslint-plugins/README.md.

7. Точки расширения

Задача Где править
Новый инструмент запись в registry/<group>.ts + i18n-ключи + тесты (рецепт ниже)
Смена id запись реестра + i18n-ключи tools.* + tool-icons.ts + хардкод id в тестах, /kit и 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/<group>.ts и по всему web/src/ — не заводить дублирующую константу, тип или утилиту.
  2. Выбрать группу (geometry/alpha/color/convert/filters/ generate/text/analyze) и положить запись в её экспорт; при новой категории — сначала categories.ts.
  3. Задать id. Он же адрес страницы, он же ключ i18n, он же база имени скачиваемого файла. Страница держит png-интент, поэтому суффикс -png входит в id (crop-png, resize-png) — это осознанно, отдельного поля slug в реестре нет (раздел 3).
  4. Описать схему через field.*, задать layout, если поля нужно сгруппировать.
  5. Написать run поверх core/*; пиксельные операции — чистые функции в core/, без DOM.
  6. Проверить обязательные контракты для новой записи: она попадает в TOOLS, страница /tools/<id> prerender-ится, input/result согласованы с тем, что делает run.
  7. Добавить i18n-ключи во все локали сразу (ключ — по id).
  8. Тесты: unit — в registry/*.test.ts или рядом с core-функцией; e2e-smoke — существующий spec по типу input/result (правила — docs/testing-strategy.md).
  9. Обновить docs/tools-map.md: новая страница обязательна в разделе «Реализовано», формат строки — в шапке раздела. Карта перечисляет id реестра, которые одновременно являются адресами страниц. Проверяет web/src/lib/registry/tools-map-doc.test.ts: id без инструмента и инструмент без записи в карте роняют тест. Обоснование и границы автоматизации — docs/decisions.md, раздел 4.

Ожидаемый минимум проверок: pnpm --dir web check и pnpm --dir web test; для инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 docs/quality-gates.md).