Render documentation diagrams as static Mermaid assets

This commit is contained in:
Ruslan Bakiev
2026-05-01 15:09:02 +07:00
parent b7a5018c6e
commit 542ad1b648
23 changed files with 1105 additions and 134 deletions

View File

@@ -6,7 +6,7 @@ const props = defineProps<{
chart: string;
}>();
const svg = ref('');
const container = ref<HTMLElement | null>(null);
const error = ref('');
let initialized = false;
@@ -20,7 +20,7 @@ function initMermaid() {
mermaid.initialize({
startOnLoad: false,
theme: 'neutral',
securityLevel: 'strict',
securityLevel: 'loose',
fontFamily: 'Inter, Arial, sans-serif',
flowchart: {
useMaxWidth: true,
@@ -34,7 +34,7 @@ function initMermaid() {
const source = computed(() => props.chart.trim());
async function renderDiagram() {
if (!import.meta.client) {
if (!import.meta.client || !container.value) {
return;
}
@@ -42,10 +42,13 @@ async function renderDiagram() {
initMermaid();
error.value = '';
diagramId += 1;
const { svg: result } = await mermaid.render(`mermaid-diagram-${diagramId}`, source.value);
svg.value = result;
container.value.removeAttribute('data-processed');
container.value.id = `mermaid-diagram-${diagramId}`;
container.value.textContent = source.value;
await mermaid.run({
nodes: [container.value],
});
} catch (cause) {
svg.value = '';
error.value = cause instanceof Error ? cause.message : 'Failed to render Mermaid diagram.';
}
}
@@ -62,7 +65,7 @@ watch(source, () => {
<template>
<div class="mermaid-diagram">
<div v-if="error" class="mermaid-diagram__error">{{ error }}</div>
<div v-else class="mermaid-diagram__canvas" v-html="svg" />
<pre v-else ref="container" class="mermaid-diagram__canvas mermaid" />
</div>
</template>
@@ -77,6 +80,8 @@ watch(source, () => {
.mermaid-diagram__canvas {
padding: 1rem;
margin: 0;
white-space: pre-wrap;
}
.mermaid-diagram__canvas :deep(svg) {

View File

@@ -1,15 +1,15 @@
export const diagramSources: Record<string, string> = {
'architecture-overview': `flowchart LR
Browser[Браузер клиента]
Frontend[web-frontend<br/>Nuxt 4 + Vue 3]
Proxy[/api/graphql proxy]
Backend[apollo-backend<br/>Apollo Server + Express]
Prisma[Prisma ORM]
Db[(PostgreSQL)]
Vault[Vault]
OneC[1С]
Bots[Telegram / MAX / email]
Dokploy[Dokploy]
Browser["Браузер клиента"]
Frontend["web-frontend<br/>Nuxt 4 + Vue 3"]
Proxy["/api/graphql proxy"]
Backend["apollo-backend<br/>Apollo Server + Express"]
Prisma["Prisma ORM"]
Db[("PostgreSQL")]
Vault["Vault"]
OneC["1С"]
Bots["Telegram / MAX / email"]
Dokploy["Dokploy"]
Browser --> Frontend
Frontend --> Proxy
@@ -22,32 +22,32 @@ export const diagramSources: Record<string, string> = {
Frontend -.deploy.-> Dokploy
Backend -.deploy.-> Dokploy`,
'component-map': `flowchart LR
subgraph UI[UI и маршруты]
Pages[app/pages]
Components[app/components]
Composables[app/composables]
Middleware[middleware / plugins]
subgraph UI["UI и маршруты"]
Pages["app/pages"]
Components["app/components"]
Composables["app/composables"]
Middleware["middleware / plugins"]
end
subgraph Transport[Контракт и transport]
Ops[graphql/operations]
Generated[generated.ts]
Apollo[Apollo client]
Api[server/api/graphql]
subgraph Transport["Контракт и transport"]
Ops["graphql/operations"]
Generated["generated.ts"]
Apollo["Apollo client"]
Api["server/api/graphql"]
end
subgraph Server[Backend logic]
Schema[schema.graphql]
Resolvers[resolvers.js]
Auth[auth.js / access.js]
Context[context.js]
Messenger[messenger / telegram / max]
subgraph Server["Backend logic"]
Schema["schema.graphql"]
Resolvers["resolvers.js"]
Auth["auth.js / access.js"]
Context["context.js"]
Messenger["messenger / telegram / max"]
end
subgraph Data[Data layer]
Prisma[prisma-client.js]
PrismaSchema[schema.prisma]
Db[(PostgreSQL)]
subgraph Data["Data layer"]
Prisma["prisma-client.js"]
PrismaSchema["schema.prisma"]
Db[("PostgreSQL")]
end
Pages --> Components
@@ -67,33 +67,33 @@ export const diagramSources: Record<string, string> = {
Prisma --> PrismaSchema
Prisma --> Db`,
'infrastructure-topology': `flowchart TB
subgraph Repo[Репозиторий fregat]
FrontendRepo[web-frontend]
BackendRepo[apollo-backend]
TgRepo[tg-bot]
MaxRepo[max-bot]
BonusRepo[bonus-bot]
WorkerRepo[hatchet-worker]
VaultRepo[vault]
subgraph Repo["Репозиторий fregat"]
FrontendRepo["web-frontend"]
BackendRepo["apollo-backend"]
TgRepo["tg-bot"]
MaxRepo["max-bot"]
BonusRepo["bonus-bot"]
WorkerRepo["hatchet-worker"]
VaultRepo["vault"]
end
subgraph Dokploy[Dokploy]
FrontendSvc[web-frontend service]
BackendSvc[apollo-backend service]
TgSvc[tg-bot service]
MaxSvc[max-bot service]
BonusSvc[bonus-bot service]
WorkerSvc[hatchet-worker service]
VaultSvc[vault service]
subgraph Dokploy["Dokploy"]
FrontendSvc["web-frontend service"]
BackendSvc["apollo-backend service"]
TgSvc["tg-bot service"]
MaxSvc["max-bot service"]
BonusSvc["bonus-bot service"]
WorkerSvc["hatchet-worker service"]
VaultSvc["vault service"]
end
subgraph Infra[Инфраструктурный контур]
VaultData[Vault raft storage /vault/data]
BackendDb[(PostgreSQL for app)]
HatchetEngine[Hatchet engine]
HatchetPg[(PostgreSQL for Hatchet)]
OneC[1С]
Tailscale[Tailscale SSH / diagnostics]
subgraph Infra["Инфраструктурный контур"]
VaultData["Vault raft storage /vault/data"]
BackendDb[("PostgreSQL for app")]
HatchetEngine["Hatchet engine"]
HatchetPg[("PostgreSQL for Hatchet")]
OneC["1С"]
Tailscale["Tailscale SSH / diagnostics"]
end
FrontendRepo --> FrontendSvc
@@ -160,78 +160,78 @@ export const diagramSources: Record<string, string> = {
User "1" --> "*" ReferralLink : referrals
CatalogProductTypeSetting --> Product : productType`,
dashboard: `flowchart TB
subgraph Page[Главная страница клиента]
Header[Header / навигация]
Quick[Быстрые действия]
Active[Актуальные заказы и заявки]
Notes[Последние уведомления]
Bonus[Бонусный блок]
subgraph Page["Главная страница клиента"]
Header["Header / навигация"]
Quick["Быстрые действия"]
Active["Актуальные заказы и заявки"]
Notes["Последние уведомления"]
Bonus["Бонусный блок"]
end
Header --> Quick --> Active --> Notes --> Bonus`,
'catalog-grid': `flowchart TB
subgraph Catalog[Каталог продукции]
Title[Заголовок раздела]
Search[Поиск]
Grid[Сетка карточек товарных направлений]
Cards[Упаковочный скотч | Алюминиевый скотч | Крепп | Вспененный | Двусторонний ПП | Двусторонний PVC]
subgraph Catalog["Каталог продукции"]
Title["Заголовок раздела"]
Search["Поиск"]
Grid["Сетка карточек товарных направлений"]
Cards["Упаковочный скотч | Алюминиевый скотч | Крепп | Вспененный | Двусторонний ПП | Двусторонний PVC"]
end
Title --> Search --> Grid --> Cards`,
'product-card': `flowchart TB
subgraph ProductPage[Карточка товара]
Title[Заголовок товара и навигация]
subgraph TopRow[Верхний блок]
Image[Изображение товара]
Params[Параметры выбора]
Action[SKU / действие В корзину]
subgraph ProductPage["Карточка товара"]
Title["Заголовок товара и навигация"]
subgraph TopRow["Верхний блок"]
Image["Изображение товара"]
Params["Параметры выбора"]
Action["SKU / действие В корзину"]
end
Help[Пояснения по параметрам]
Table[Таблица доступных вариантов]
Help["Пояснения по параметрам"]
Table["Таблица доступных вариантов"]
end
Title --> TopRow --> Help --> Table`,
cart: `flowchart TB
subgraph CartPage[Корзина]
Items[Список выбранных позиций]
Delivery[Адрес доставки]
Comment[Комментарий клиента]
Submit[Отправить заявку]
subgraph CartPage["Корзина"]
Items["Список выбранных позиций"]
Delivery["Адрес доставки"]
Comment["Комментарий клиента"]
Submit["Отправить заявку"]
end
Items --> Delivery --> Comment --> Submit`,
'client-order': `flowchart TB
subgraph ClientOrder[Карточка заявки / заказа]
Summary[Номер документа и статус]
Composition[Состав позиций]
Terms[Стоимость и условия поставки]
History[История изменений]
subgraph ClientOrder["Карточка заявки / заказа"]
Summary["Номер документа и статус"]
Composition["Состав позиций"]
Terms["Стоимость и условия поставки"]
History["История изменений"]
end
Summary --> Composition --> Terms --> History`,
'bonus-cabinet': `flowchart TB
subgraph BonusPage[Бонусный кабинет]
Balance[Текущий бонусный баланс]
History[История операций]
Referrals[Реферальные связи]
Action[Подача заявки на использование или вывод]
subgraph BonusPage["Бонусный кабинет"]
Balance["Текущий бонусный баланс"]
History["История операций"]
Referrals["Реферальные связи"]
Action["Подача заявки на использование или вывод"]
end
Balance --> History --> Referrals --> Action`,
'manager-order': `flowchart TB
subgraph ManagerOrder[Карточка обработки заявки]
Summary[Клиент / контрагент / менеджер]
Payload[Состав заявки или расчетный payload]
Terms[Стоимость и условия поставки]
Actions[Опубликовать условия / перевести в работу / отменить]
subgraph ManagerOrder["Карточка обработки заявки"]
Summary["Клиент / контрагент / менеджер"]
Payload["Состав заявки или расчетный payload"]
Terms["Стоимость и условия поставки"]
Actions["Опубликовать условия / перевести в работу / отменить"]
end
Summary --> Payload --> Terms --> Actions`,
'manager-orders': `flowchart TB
subgraph ManagerOrders[Список заказов менеджера]
Header[Заголовок раздела]
Filters[Фильтры: статус / клиент / период]
Table[Таблица заказов]
subgraph ManagerOrders["Список заказов менеджера"]
Header["Заголовок раздела"]
Filters["Фильтры: статус / клиент / период"]
Table["Таблица заказов"]
end
Header --> Filters --> Table`,

View File

@@ -1,15 +1,10 @@
import DefaultTheme from 'vitepress/theme';
import type { Theme } from 'vitepress';
import MermaidDiagram from './components/MermaidDiagram.vue';
import NamedMermaidDiagram from './components/NamedMermaidDiagram.vue';
const theme: Theme = {
...DefaultTheme,
enhanceApp({ app }) {
DefaultTheme.enhanceApp?.({ app });
app.component('MermaidDiagram', MermaidDiagram);
app.component('NamedMermaidDiagram', NamedMermaidDiagram);
enhanceApp(ctx) {
DefaultTheme.enhanceApp?.(ctx);
},
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,9 @@
{
"theme": "neutral",
"securityLevel": "loose",
"flowchart": {
"useMaxWidth": true,
"htmlLabels": true
},
"fontFamily": "Inter, Arial, sans-serif"
}

View File

@@ -0,0 +1,9 @@
{
"headless": true,
"executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage"
]
}

View File

@@ -0,0 +1,80 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import { diagramSources } from '../.vitepress/theme/components/diagramSources.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const docsRoot = path.resolve(__dirname, '..');
const publicRoot = path.join(docsRoot, 'public');
const diagramsDir = path.join(publicRoot, 'diagrams');
const prototypesDir = path.join(publicRoot, 'prototypes');
const puppeteerConfig = path.join(__dirname, 'puppeteer-config.json');
const mermaidConfig = path.join(__dirname, 'mermaid-config.json');
const prototypeNames = new Set([
'dashboard',
'catalog-grid',
'product-card',
'cart',
'client-order',
'bonus-cabinet',
'manager-order',
'manager-orders',
]);
function runCommand(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: docsRoot,
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`));
});
child.on('error', reject);
});
}
async function renderDiagram(name, source) {
const targetDir = prototypeNames.has(name) ? prototypesDir : diagramsDir;
const tmpDir = await mkdtemp(path.join(os.tmpdir(), `mermaid-${name}-`));
const inputFile = path.join(tmpDir, `${name}.mmd`);
const outputFile = path.join(targetDir, `${name}.svg`);
try {
await writeFile(inputFile, source, 'utf8');
await runCommand('pnpm', [
'exec',
'mmdc',
'-i',
inputFile,
'-o',
outputFile,
'-b',
'transparent',
'-c',
mermaidConfig,
'-p',
puppeteerConfig,
]);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
}
await mkdir(diagramsDir, { recursive: true });
await mkdir(prototypesDir, { recursive: true });
for (const [name, source] of Object.entries(diagramSources)) {
await renderDiagram(name, source);
}

View File

@@ -4,7 +4,7 @@
Основное хранилище данных программного продукта реализуется на `PostgreSQL`. Прикладной доступ к данным осуществляется через `Prisma ORM`.
<NamedMermaidDiagram name="database-model" />
![Укрупненная модель базы данных](/diagrams/database-model.svg)
Модель данных должна обеспечивать хранение:

View File

@@ -62,7 +62,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="dashboard" />
![Прототип главной страницы клиента](/prototypes/dashboard.svg)
### 10.3.2 Каталог продукции
@@ -81,7 +81,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="catalog-grid" />
![Прототип каталога продукции](/prototypes/catalog-grid.svg)
### 10.3.3 Карточка товара
@@ -105,7 +105,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="product-card" />
![Прототип карточки товара](/prototypes/product-card.svg)
Состав блока выбора параметров:
@@ -144,7 +144,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="cart" />
![Прототип корзины](/prototypes/cart.svg)
### 10.3.5 Карточка заявки или заказа
@@ -157,7 +157,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="client-order" />
![Прототип карточки заявки или заказа](/prototypes/client-order.svg)
Состав страницы:
@@ -221,7 +221,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="bonus-cabinet" />
![Прототип бонусного кабинета](/prototypes/bonus-cabinet.svg)
## 10.4 Менеджерские экранные формы
@@ -266,7 +266,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="manager-order" />
![Прототип карточки обработки заявки](/prototypes/manager-order.svg)
Состав страницы:
@@ -288,7 +288,7 @@
Схематичный прототип:
<NamedMermaidDiagram name="manager-orders" />
![Прототип списка заказов менеджера](/prototypes/manager-orders.svg)
### 10.4.5 Настройки каталога

View File

@@ -4,7 +4,7 @@
Программный продукт реализуется по клиент-серверной модели и включает веб-клиент, сервер бизнес-логики, базу данных, модуль интеграции и вспомогательные сервисы уведомлений.
<NamedMermaidDiagram name="architecture-overview" />
![Общая архитектурная схема](/diagrams/architecture-overview.svg)
## 7.2 Состав прикладных сервисов
@@ -66,7 +66,7 @@
## 7.6 Карта слоев и компонентов
<NamedMermaidDiagram name="component-map" />
![Карта слоев и компонентов](/diagrams/component-map.svg)
## 7.7 Архитектура серверной части
@@ -130,7 +130,7 @@
Текущая инфраструктурная схема проекта включает прикладные сервисы, сервис секретов, мессенджерные сервисы и вспомогательный worker-контур.
<NamedMermaidDiagram name="infrastructure-topology" />
![Инфраструктура, деплой и эксплуатационный контур](/diagrams/infrastructure-topology.svg)
Сервисы проекта и способ их развёртывания:

View File

@@ -38,6 +38,7 @@
"@graphql-codegen/typescript": "^5.0.9",
"@graphql-codegen/typescript-operations": "^5.0.9",
"@graphql-codegen/typescript-vue-apollo": "^5.0.0",
"@mermaid-js/mermaid-cli": "^11.14.0",
"@storybook/addon-essentials": "8.6.14",
"@storybook/vue3-vite": "^8.6.14",
"storybook": "^8.6.14",

884
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff