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

View File

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

View File

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

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`. Основное хранилище данных программного продукта реализуется на `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 Каталог продукции ### 10.3.2 Каталог продукции
@@ -81,7 +81,7 @@
Схематичный прототип: Схематичный прототип:
<NamedMermaidDiagram name="catalog-grid" /> ![Прототип каталога продукции](/prototypes/catalog-grid.svg)
### 10.3.3 Карточка товара ### 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 Карточка заявки или заказа ### 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 Менеджерские экранные формы ## 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 Настройки каталога ### 10.4.5 Настройки каталога

View File

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

View File

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

884
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff