Files
remnawave-minishop/docs-site/scripts/sync-docs.mjs
T
2026-05-26 16:04:04 +03:00

97 lines
4.2 KiB
JavaScript

import { copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const siteRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
const repoRoot = path.resolve(siteRoot, '..');
const sourceDir = path.join(repoRoot, 'docs');
const outputDir = path.join(siteRoot, 'src', 'content', 'docs', 'reference');
const descriptions = {
'admin.md': 'Возможности админ-панели, управление пользователями, настройками, тарифами и поддержкой.',
'architecture.md': 'Краткая архитектура backend, frontend, worker и инфраструктурных сервисов.',
'configuration.md': 'Минимальный .env, bootstrap-секреты и настройка через Web App админку.',
'deployment.md': 'Docker Compose, reverse proxy, TLS, образы, обновления и резервные копии.',
'env-vars.md': 'Полный справочник переменных окружения Remnawave Minishop.',
'migration-to-minishop.md': 'Перенос данных со старого remnawave-tg-shop на split-архитектуру Minishop.',
'support.md': 'Пользовательские тикеты, админский inbox, уведомления и лимиты поддержки.',
'tariffs.md': 'Каталог тарифов, period/traffic-модели, premium-сквады и HWID-устройства.',
'webapp.md': 'Telegram Mini App, авторизация, публичные инструкции и проксирование.',
'webapp-themes.md': 'Кастомные темы, CSS-токены, ассеты и пайплайн создания темы.',
};
const imageExtensions = new Set(['.avif', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp']);
function yamlString(value) {
return JSON.stringify(value);
}
function slugFor(fileName) {
return fileName.replace(/\.md$/i, '');
}
function extractTitle(fileName, content) {
const match = content.match(/^#\s+(.+?)\s*$/m);
return match?.[1] ?? slugFor(fileName);
}
function stripFirstHeading(content) {
return content.replace(/^#\s+.+?\s*\r?\n+/, '');
}
function rewriteMarkdownLinks(markdown) {
return markdown.replace(/\]\((?!https?:\/\/|mailto:|tel:|\/|#)([^)\s]+\.md)(#[^)]+)?\)/g, (match, target, hash = '') => {
const slug = slugFor(path.posix.basename(target));
return `](/reference/${slug}/${hash})`;
});
}
function normalizeCodeFences(markdown) {
return markdown
.replace(/^```env\s*$/gim, '```ini')
.replace(/^```caddyfile\s*$/gim, '```txt');
}
function frontmatter({ title, description, fileName }) {
const editUrl = `https://gitlab.com/3252a8/remnawave-minshop/-/edit/main/docs/${encodeURIComponent(fileName)}`;
return [
'---',
`title: ${yamlString(title)}`,
`description: ${yamlString(description)}`,
`editUrl: ${yamlString(editUrl)}`,
'---',
'',
].join('\n');
}
async function syncMarkdown(entries) {
for (const entry of entries.filter((item) => item.name.endsWith('.md'))) {
const sourcePath = path.join(sourceDir, entry.name);
const content = await readFile(sourcePath, 'utf8');
const title = extractTitle(entry.name, content);
const body = normalizeCodeFences(rewriteMarkdownLinks(stripFirstHeading(content).trimStart()));
const output = frontmatter({
title,
description: descriptions[entry.name] ?? title,
fileName: entry.name,
});
await writeFile(path.join(outputDir, entry.name), `${output}${body}\n`, 'utf8');
}
}
async function syncImages(entries) {
for (const entry of entries.filter((item) => imageExtensions.has(path.extname(item.name).toLowerCase()))) {
await copyFile(path.join(sourceDir, entry.name), path.join(outputDir, entry.name));
}
}
await rm(outputDir, { recursive: true, force: true });
await mkdir(outputDir, { recursive: true });
const entries = await readdir(sourceDir, { withFileTypes: true });
await syncMarkdown(entries);
await syncImages(entries);
console.log(`Synced documentation from ${path.relative(repoRoot, sourceDir)} to ${path.relative(repoRoot, outputDir)}`);