- Добавлен лендинг на 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
186 lines
6.0 KiB
TypeScript
186 lines
6.0 KiB
TypeScript
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();
|