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 DOCS_PUBLIC_DIR = path.join(SRC_DIR, "public"); const DOC_ROUTE_PREFIX = "/docs"; const PUBLIC_ARCHITECTURE_FILE = "ARCHITECTURE.md"; const PUBLIC_ARCHITECTURE_NOTICE = `> Локальная копия канонической спецификации SLM Design. > Источник: https://slm-design.gromlab.ru/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 linkToFileRel(link: string): string { const rel = link.replace(/^\//, ""); if (rel === "" || rel.endsWith("/")) return `${rel}index.md`; return `${rel}.md`; } function fileRelToRoute(file: string): string { const route = file.endsWith("/index.md") ? file.replace(/index\.md$/, "") : file.replace(/\.md$/, ""); return `${DOC_ROUTE_PREFIX}/${route}`; } function fileRelToMdUrl(file: string): string { return `${DOC_ROUTE_PREFIX}/${file}`; } const ARCHITECTURE_LINK_RE = /\]\((\/architecture(?:\/[^)\s#]*)?)(#[^)\s]*)?\)/g; function architectureRouteToFileRel(route: string): string { if (route.replace(/\/$/, "") === "/architecture") return "architecture/index.md"; return linkToFileRel(route); } function transformArchitectureLinks( content: string, toHref: (route: string, hash: string) => string, ): string { return content.replace(ARCHITECTURE_LINK_RE, (_match, route: string, hash = "") => { return `](${toHref(route, hash)})`; }); } function transformArchiveLinks(content: string): string { return transformArchitectureLinks(content, (route, hash) => { const fileName = path.basename(architectureRouteToFileRel(route)); return `./${fileName}${hash}`; }); } function transformSiteMarkdownLinks(content: string): string { return transformArchitectureLinks(content, (route, hash) => { return `${fileRelToMdUrl(architectureRouteToFileRel(route))}${hash}`; }); } function getAllFiles(): string[] { return SIDEBAR.flatMap((g) => g.items.map((item) => linkToFileRel(item.link))); } const stripFrontmatter = (content: string) => content.replace(/^---[\s\S]*?---\n*/m, ""); const stripRulesLink = (content: string) => content.replace(/[\s\S]*?\n*/g, ""); function slugifyHeading(heading: string): string { return heading .trim() .replace(/[`*_~[\]()]/g, "") .toLowerCase() .replace(/[^\p{L}\p{N}\s-]/gu, "") .trim() .replace(/\s+/g, "-"); } function fileRelToSingleFileAnchor(file: string): string { const filePath = path.join(SRC_DIR, file); if (!fs.existsSync(filePath)) return slugifyHeading(path.basename(file, ".md")); const raw = stripFrontmatter(fs.readFileSync(filePath, "utf8")); const title = raw.match(/^#\s+(.+)$/m)?.[1]; return slugifyHeading(title ?? path.basename(file, ".md")); } function transformSingleFileLinks(content: string): string { return transformArchitectureLinks(content, (route, hash) => { if (hash) return hash; return `#${fileRelToSingleFileAnchor(architectureRouteToFileRel(route))}`; }); } function transformReadmeLinks(content: string): string { return transformArchitectureLinks(content, (route, hash) => { return `docs/${architectureRouteToFileRel(route)}${hash}`; }); } 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 + fileRelToRoute(file).replace(DOC_ROUTE_PREFIX, ""); const shifted = file.endsWith("index.md") ? content : shiftHeadings(content); const processed = transformSingleFileLinks(shifted); parts.push(`\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 fileRel = linkToFileRel(item.link); 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 = fileRelToMdUrl(fileRel); 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 copyMarkdownFiles() { fs.rmSync(DOCS_PUBLIC_DIR, { recursive: true, force: true }); let copied = 0; for (const file of getAllFiles()) { const src = path.join(SRC_DIR, file); if (!fs.existsSync(src)) continue; const content = transformSiteMarkdownLinks(fs.readFileSync(src, "utf8")); const dest = path.join(DOCS_PUBLIC_DIR, file); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, content, "utf8"); copied++; } console.log(`скопировано ${copied} .md-файлов в ${DOCS_PUBLIC_DIR}`); } function buildPublicArchitecture() { const outPath = path.join(PUBLIC_DIR, PUBLIC_ARCHITECTURE_FILE); const content = `${PUBLIC_ARCHITECTURE_NOTICE}\n\n${buildArchitectureMarkdown("/docs")}`; fs.mkdirSync(PUBLIC_DIR, { recursive: true }); fs.writeFileSync(outPath, content, "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(); content = transformArchiveLinks(content); 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(/[\s\S]*?\n*/g, ""); content = transformReadmeLinks(content); fs.writeFileSync("./README.md", content, "utf8"); console.log("README.md создан"); } buildLlms(); buildLlmsFull(); copyMarkdownFiles(); buildPublicArchitecture(); buildZip(); buildReadme();