feat: добавить лендинг, переписать документацию и унифицировать генерацию

- Добавлен лендинг на React + Vite с темой и карточками навигации
- Добавлен модуль темы (src/infra/theme) с поддержкой system/light/dark
- Документация переписана: разделы «Модули», «Сегменты», «Компонент»
- Добавлена страница навигации docs/index.md
- Генерация llms.txt переведена на парсинг сайдбара VitePress
- Описания для llms.txt вынесены в frontmatter (поле description)
- Удалена директория generated/, архив ZIP убран с лендинга
- Удалены английская документация, README_RU, concat-md.js
- Добавлен vite-плагин для UTF-8 заголовков текстовых артефактов
- Caddyfile обновлён: charset=utf-8 для llms.txt и ARCHITECTURE.md
This commit is contained in:
2026-05-01 21:00:25 +03:00
parent 004a73a869
commit 54b4060b6f
43 changed files with 3877 additions and 1282 deletions

185
generate.ts Normal file
View File

@@ -0,0 +1,185 @@
import path from "path";
import fs from "fs";
import os from "os";
import { execFileSync } from "child_process";
const SRC_DIR = "./docs";
const PUBLIC_DIR = "./public";
const PUBLIC_ARCHITECTURE_FILE = "ARCHITECTURE.md";
interface SidebarItem {
text: string;
link: string;
}
interface SidebarGroup {
text: string;
items: SidebarItem[];
}
function parseSidebar(): SidebarGroup[] {
const configPath = path.join(".vitepress", "config.ts");
const raw = fs.readFileSync(configPath, "utf8");
const start = raw.indexOf("const sidebar = [");
const end = raw.indexOf("];", start) + 2;
const section = raw.substring(start, end);
const groups: SidebarGroup[] = [];
const groupParts = section.split(/\n\s*\}\s*,?\s*\n/).filter(Boolean);
for (const part of groupParts) {
const textMatch = part.match(/text:\s*'([^']*)'/);
if (!textMatch) continue;
const items: SidebarItem[] = [];
const itemRe = /\{\s*text:\s*'([^']*)'\s*,\s*link:\s*'([^']*)'\s*\}/g;
let im: RegExpExecArray | null;
while ((im = itemRe.exec(part)) !== null) {
items.push({ text: im[1], link: im[2] });
}
if (items.length > 0) groups.push({ text: textMatch[1], items });
}
return groups;
}
const SIDEBAR = parseSidebar();
function getAllFiles(): string[] {
return SIDEBAR.flatMap((g) =>
g.items.map((item) => {
const rel = item.link.replace(/^\//, "") + ".md";
const indexPath = rel.replace(/\.md$/, "/index.md");
const filePath = path.join(SRC_DIR, indexPath);
return fs.existsSync(filePath) ? indexPath : rel;
})
);
}
const stripFrontmatter = (content: string) =>
content.replace(/^---[\s\S]*?---\n*/m, "");
const stripRulesLink = (content: string) =>
content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
const shiftHeadings = (content: string) => {
const lines = content.split("\n");
let inCodeBlock = false;
return lines
.map((line) => {
if (line.startsWith("```")) inCodeBlock = !inCodeBlock;
if (inCodeBlock) return line;
if (/^#{1,5}\s/.test(line)) return "#" + line;
return line;
})
.join("\n");
};
const buildArchitectureMarkdown = (routePrefix: string) => {
const files = getAllFiles();
const parts: string[] = [];
for (const file of files) {
const filePath = path.join(SRC_DIR, file);
if (!fs.existsSync(filePath)) continue;
const raw = fs.readFileSync(filePath, "utf8");
const content = stripRulesLink(stripFrontmatter(raw)).trim();
if (!content) continue;
const route = routePrefix + "/" + file.replace(/\.md$/, "");
const processed = file.endsWith("index.md") ? content : shiftHeadings(content);
parts.push(`<!-- ${route} -->\n${processed}`);
}
return parts.join("\n\n");
};
function buildLlms() {
const parts: string[] = [`# SLM Design\n`];
parts.push(`> Scoped Layered Module Design — модульная архитектура фронтенд-приложений\n`);
for (const group of SIDEBAR) {
parts.push(`## ${group.text}`);
for (const item of group.items) {
const rel = item.link.replace(/^\//, "") + ".md";
const indexPath = rel.replace(/\.md$/, "/index.md");
const fileRel = fs.existsSync(path.join(SRC_DIR, indexPath)) ? indexPath : rel;
const filePath = path.join(SRC_DIR, fileRel);
let desc = "";
if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, "utf8");
const fm = raw.match(/^---[\s\S]*?---\n*/m);
desc = fm ? fm[0].match(/description:\s*(.+)/)?.[1] || "" : "";
}
const route = "/docs" + item.link;
const line = desc
? `- [${item.text}](${route}): ${desc}`
: `- [${item.text}](${route})`;
parts.push(line);
}
parts.push("");
}
const outPath = path.join(PUBLIC_DIR, "llms.txt");
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(outPath, parts.join("\n"), "utf8");
console.log(`llms.txt создан: ${outPath}`);
}
function buildLlmsFull() {
const outPath = path.join(PUBLIC_DIR, "llms-full.txt");
fs.writeFileSync(outPath, buildArchitectureMarkdown("/docs"), "utf8");
console.log(`llms-full.txt создан: ${outPath}`);
}
function buildPublicArchitecture() {
const outPath = path.join(PUBLIC_DIR, PUBLIC_ARCHITECTURE_FILE);
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(outPath, buildArchitectureMarkdown("/docs"), "utf8");
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
}
function buildZip() {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "slm-"));
const tmpDir = path.join(tmpRoot, "slm-design");
fs.mkdirSync(tmpDir, { recursive: true });
const files = getAllFiles();
for (const file of files) {
const src = path.join(SRC_DIR, file);
if (!fs.existsSync(src)) continue;
let content = fs.readFileSync(src, "utf8");
content = stripRulesLink(stripFrontmatter(content)).trim();
const destName = path.basename(file);
fs.writeFileSync(path.join(tmpDir, destName), content, "utf8");
}
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
const version = `v${pkg.version}`;
fs.writeFileSync(path.join(tmpDir, "VERSION"), `${version}\n${new Date().toISOString()}\n`, "utf8");
const outPath = path.resolve(PUBLIC_DIR, "slm-design.zip");
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
execFileSync("zip", ["-rq", outPath, "slm-design"], { cwd: tmpRoot });
fs.rmSync(tmpRoot, { recursive: true });
console.log(`slm-design.zip создан: ${outPath}`);
}
function buildReadme() {
const indexPath = path.join(SRC_DIR, "architecture/index.md");
if (!fs.existsSync(indexPath)) {
console.log("Пропуск README: index.md не найден");
return;
}
let content = stripFrontmatter(fs.readFileSync(indexPath, "utf8"));
content = content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
fs.writeFileSync("./README.md", content, "utf8");
console.log("README.md создан");
}
buildLlms();
buildLlmsFull();
buildPublicArchitecture();
buildZip();
buildReadme();