@aom-js/visualizer
This content is not available in your language yet.
Пакет строит по дереву контроллеров граф API — маршруты, цепочки звеньев и компонентные схемы с их связями — и рендерит его в одну самодостаточную HTML-страницу. Страница отдаётся обычным endpoint-ом, поэтому карта живёт внутри самого модуля API рядом с /openapi.json.
Читать до: @aom-js/router (мета-модель, курсоры, сборка маршрутов), @aom-js/schemer (компонентные схемы). Читать после: @aom-js/api (как endpoint отдаёт HTML).
Назначение и место в стеке
Заголовок раздела «Назначение и место в стеке»Роутер уже знает всё, что нужно для карты: Router разворачивает дерево бриджей в плоский список AomRoute, у каждого маршрута есть упорядоченная цепочка курсоров, а OpenApi вместе с SchemasCollector сводит компонентные классы в components.schemas с $ref-связями. Пакет ничего из этого не пересчитывает — он собирает данные в один сериализуемый граф (VisualGraph) и рисует его.
Ключевое решение: граф всегда собирается на собственном экземпляре OpenApi. Причина в том, как устроены отложенные декларации. Значения openApi.* (параметры билдеров, ответы, маркеры доступа) материализуются только в конструкторе AomCursor — до сборки маршрутов их в мета-инстансах просто нет (packages/router/src/common/cursor.ts). Обойти дерево через AomApi.instance(...) без построения маршрутов недостаточно: получится структура без деклараций и с пустыми route.markers. При этом сборщик схем у роутера приложения общий на весь модуль, и поддерево из него не выделить. Поэтому buildVisualGraph строит свой Router на свежем OpenApi — это чистая сборка без ввода-вывода и без сервера, а курсоры создаются заново, так что живой роутер приложения не затрагивается.
Отсюда же следует, что карта строится от любого узла дерева, а не только от корня: любой контроллер — валидный аргумент, для него собирается свой Router со своими префиксами.
Зависимости: @aom-js/core (ClassType), @aom-js/router (Router, OpenApi, AomApi, AomRoute) и reflect-metadata. Обратная зависимость одна: @aom-js/api использует пакет в BaseVisualizerController, поэтому в цепочке сборки визуализатор идёт перед api.
Пакет вводит два собственных термина: граф — структура VisualGraph, сериализуемый срез API; звено — элемент цепочки исполнения маршрута (курсор): middleware, nextpoint, маркер, хук инстанса или сам endpoint.
Публичный API
Заголовок раздела «Публичный API»| Экспорт | Сигнатура | Назначение |
|---|---|---|
buildVisualGraph | (source: ClassType | Router, options?: BuildGraphOptions) => VisualGraph | Собрать граф от контроллера или от корня живого роутера |
renderVisualGraph | (graph: VisualGraph, options?: RenderOptions) => string | Отрендерить готовый граф в HTML-страницу |
collectControllers | (target: ClassType) => Map<string, ClassType> | Контроллеры, достижимые по бриджам |
Visualizer | class Visualizer(source: AomRoute | Router | ClassType, options?) | Компонента карты: .graph — данные, .html — страница, .root — класс-корень |
| типы графа | VisualGraph, VisualController, VisualRoute, VisualChainItem, VisualMiddleware, VisualSchema, VisualSchemaProperty, VisualMember, VisualParameter, VisualResponse, VisualRequestBody, VisualBridge, VisualSchemaRef, VisualNodeKind | Контракт данных для собственных потребителей графа |
| опции | BuildGraphOptions, RenderOptions | title, info, skipMarkers; sharedThreshold |
Внутренние модули
Заголовок раздела «Внутренние модули»| Файл | Ответственность | Ключевые символы |
|---|---|---|
src/defines/types.ts | Контракт графа | VisualGraph и сопутствующие интерфейсы |
src/defines/graph.ts | Сборка графа из мета-модели роутера | buildVisualGraph, collectControllers, nodeKind, openApiPath, describeSchema, declaredMembers |
src/defines/render.ts | HTML-документ | renderVisualGraph, embedJson |
src/defines/styles.ts | CSS страницы (строка) | styles |
src/defines/script.ts | Клиентский сценарий (строка) | script |
src/defines/component.ts | Компонента карты | Visualizer |
Как собирается граф
Заголовок раздела «Как собирается граф»new OpenApi(...)+new Router(target, docs). Сокеты роутер в документ не кладёт —buildVisualGraphрегистрирует их сам, иначе их схемы не попадут в сборщик.- Обход бриджей от корня даёт дерево контроллеров: префиксы,
pre/post-middleware, хуки, маркеры, наследование. Повторный вход в уже раскрытый контроллер обрывается и попадает вwarnings— циклический бридж не уводит обход в бесконечность. - Каждый
AomRouteдаётVisualRoute: цепочка — изroute.cursors(id, роль, точка прикрепления, порядковый номер), а параметры, тело, ответы, теги и защита — из уже собранной операции документа. Операция находится по ключуметод:путь, где путь пересчитан той же подстановкой:param→{name}, что делаетOpenApi.registerPath. Схемы маршрута (schemas, ребро «маршрут → схема») — из параметров, тела и ответов; у потока событийdataсобытия — строка с JSON конверта, поэтому схемы событий берутся из свойств конвертаitemSchema.properties.data.contentSchema.properties(packages/visualizer/src/defines/graph.ts(buildVisualGraph)). - Звенья агрегируются по всем маршрутам:
usedBy— список маршрутов, в цепочку которых звено входит. Это и есть метрика «общности», по ней раскрашивается интерфейс. - Схемы разбираются из
components.schemas: свойства с типами и признаками, а$ref(в том числе внутриitems,allOf,anyOf,oneOf) становится ребром ER —refsв одну сторону,referencedByв другую. declaredMembersчитает_.originsузла класса — единственный способ перечислить декорированные методы контроллера, не зная их имён. Так в карту попадают и звенья, объявленные, но никуда не подключённые.
Страница
Заголовок раздела «Страница»Документ самодостаточен: CSS, сценарий и сам граф вшиты внутрь, внешних загрузок нет. Граф лежит в <script type="application/json"> с экранированными угловыми скобками — имя схемы или заголовок не могут закрыть блок.
Раскладка повторяет вложенность. Столбец на полотне — это глубина от корня: слева корневой контроллер, правее те, до кого от него ведёт бридж, и так далее. Одноранговые контроллеры идут в столбце сверху вниз, обход в глубину, поэтому потомки одного родителя оказываются рядом. Методы не вынесены в отдельную колонку — каждый стоит прямо под своим контроллером. Бриджи показаны линиями между столбцами: они и есть каркас картины, поэтому рисуются всегда.
Структуры данных живут внутри методов. Общего списка схем на странице нет: каждая показана там, где используется, отдельным блоком. Шеврон блока раскрывает состав — свойства с типами и признаком обязательности; кнопка «подсветить» находит эту же структуру во всех остальных методах, гасит непричастное и соединяет вхождения пунктиром. Вложенная структура выбирается кликом по строке свойства, которое на неё ссылается. У всех вхождений одной схемы одинаковый data-key — на нём держится подсветка, а уникальный data-occ нужен, чтобы связать их линиями.
В шапке блока две независимые операции, разведённые по элементам: шеврон сворачивает и разворачивает, название выделяет. Клик по шеврону не всплывает, поэтому свернуть блок можно, не сбив текущее выделение, а выделить — не меняя того, что раскрыто. Esc сбрасывает выделение и поиск (из поля поиска первым же нажатием). Поиск фильтрует по пути, классу, звену и имени структуры.
Выделение подсвечивает окружение и гасит остальное: для звена — все методы, где оно встречается; для структуры — её замыкание по связям плюс методы-потребители; для метода — его цепочка и структуры; для контроллера — его методы, звенья и соседи по бриджам. Подсвеченное раскрывается автоматически, чтобы содержимое было видно сразу. Общие звенья окрашены по числу использований и вынесены в боковую панель списком.
Перерисовка идёт на следующем кадре, но с запасным таймером: в скрытой вкладке requestAnimationFrame не вызывается вовсе, и слой связей оставался бы устаревшим до возвращения фокуса.
Как подключить
Заголовок раздела «Как подключить»Пакет не содержит контроллеров и ничего не знает про HTTP — это осознанная граница: визуализатор читает мета-модель, а маршруты объявляет тот слой, которому положено. Есть два способа.
Компонентой — там, где нужно. Создаётся от маршрута, роутера или класса; корнем карты становится корень роутера этого маршрута, то есть модуль, внутри которого компоненту разместили:
@Get("/visualizer")static Page(@Route() route: AomRoute, @Ctx() ctx: HttpCtx): string { // строка по умолчанию уходит как text/plain - тип страницы ставим сами ctx.res.setHeader("Content-Type", "text/html; charset=utf-8"); return new Visualizer(route).html;}.html здесь обязателен: строка уходит телом ответа, а объект — в JSON. Заголовок
Content-Type нужно выставить самому до return: строковый результат Server
по умолчанию отдаёт как text/plain, а не text/html (иначе данные
из запроса, попавшие в строку, браузер исполнил бы как разметку), и свой заголовок
обработчика при этом не перетирается
(packages/api/src/server/http.ts (Server); так же поступает
BaseVisualizerController.Page). Это не случайность контракта — у компоненты есть
и второй режим: toJSON() возвращает граф, поэтому return new Visualizer(route)
из endpoint-а отдаёт данные, а не страницу. Сборка ленивая: пока не спросили graph
или html, ничего не считается.
Готовым контроллером из @aom-js/api. BaseVisualizerController объявляет
/visualizer и /visualizer.json поверх той же компоненты; подключается бриджем
или наследованием, если нужны свои ограничения доступа. Закрывать карту стоит не слабее,
чем /openapi.json: страница раскрывает полную структуру API вместе с цепочками доступа.
@Controller()@Before(AuthBasic.Required)export class VisualizerController extends BaseVisualizerController {}Корень карты
Заголовок раздела «Корень карты»Снаружи корень не параметризуется: ни query-параметра, ни отдельного маршрута для
поддерева нет. Страница получает граф целиком, а выбор узла — операция клиента через
#root=ИмяКонтроллера. Смена корня не порождает обращений к серверу и не перезагружает
страницу; ссылка с hash открывает нужный срез сразу.
Пути в срезе остаются абсолютными — это реальные адреса модуля, а не пересборка дерева от нового корня. Сужается только состав: контроллеры, достижимые по бриджам, их маршруты, звенья этих маршрутов и замыкание задействованных схем. Счётчик общности звена пересчитывается внутри среза.
Программно тот же срез недоступен намеренно: buildVisualGraph(SomeController)
собирает отдельный граф от этого класса, с собственными префиксами — это другая
операция, и её результат не совпадает со срезом.
Ограничения
Заголовок раздела «Ограничения»- Сборка графа перестраивает дерево маршрутов на каждый запрос. Для модуля из ~75 маршрутов это единицы миллисекунд, кеша нет намеренно: карта должна отражать текущее состояние процесса.
- Маркеры маршрутов сериализуются через
JSON.stringifyкак есть. Структура с циклом не роняет страницу — маршрут получает пустые маркеры, а причина попадает вwarnings. - Порядок бриджей в карте обратен порядку объявления: декораторы класса выполняются снизу вверх. Это поведение роутера, не визуализатора.
- Websocket-маршруты роутер в документ OpenAPI не кладёт; пакет регистрирует их сам, но декларации у сокетов обычно беднее, чем у http-маршрутов.
Тесты пакета
Заголовок раздела «Тесты пакета»Из корня ядра: bunx vitest run packages/visualizer.