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:
185
generate.ts
Normal file
185
generate.ts
Normal 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();
|
||||
Reference in New Issue
Block a user