Compare commits
4 Commits
main
...
70f0e20646
| Author | SHA1 | Date | |
|---|---|---|---|
| 70f0e20646 | |||
| ced6a07398 | |||
| 1af27795ed | |||
| d69fca16fe |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -135,7 +135,6 @@ dist
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
docs/.vitepress
|
||||
docs/public/
|
||||
|
||||
# Generated artifacts
|
||||
public/docs/
|
||||
@@ -150,3 +149,4 @@ dist
|
||||
# Рабочие заметки
|
||||
notes
|
||||
|
||||
|
||||
|
||||
@@ -14,18 +14,10 @@ const sidebar = [
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'docs',
|
||||
srcExclude: ['public/**'],
|
||||
outDir: 'public/docs',
|
||||
title: 'SLM Design',
|
||||
description: 'Правила и стандарты архитектуры проекта',
|
||||
base: '/docs/',
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
['meta', { name: 'llms', content: '/llms.txt' }],
|
||||
['link', { rel: 'alternate llms', type: 'text/plain', href: '/llms.txt', title: 'llms.txt' }],
|
||||
['link', { rel: 'alternate', type: 'text/plain', href: '/llms-full.txt', title: 'llms-full.txt' }],
|
||||
['link', { rel: 'alternate', type: 'text/markdown', href: '/ARCHITECTURE.md', title: 'ARCHITECTURE.md' }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
sidebar,
|
||||
|
||||
41
Caddyfile
41
Caddyfile
@@ -1,40 +1,11 @@
|
||||
:8082 {
|
||||
root * /srv
|
||||
|
||||
# Устаревшие пути llms.txt в подпапках ведём к корневым артефактам.
|
||||
redir /docs/llms.txt /llms.txt 301
|
||||
redir /docs/llms-full.txt /llms-full.txt 301
|
||||
|
||||
# Чистые URL: запросы вида `/docs/foo.html` редиректим на `/docs/foo`.
|
||||
@legacyHtml {
|
||||
path_regexp legacyHtml ^(/.+)\.html$
|
||||
not path /index.html
|
||||
}
|
||||
redir @legacyHtml {re.legacyHtml.1} 301
|
||||
|
||||
header Link "</llms.txt>; rel=\"llms\""
|
||||
|
||||
@existingText {
|
||||
path *.txt
|
||||
file
|
||||
}
|
||||
header @existingText Content-Type "text/plain; charset=utf-8"
|
||||
|
||||
@existingMarkdown {
|
||||
path *.md
|
||||
file
|
||||
}
|
||||
header @existingMarkdown Content-Type "text/markdown; charset=utf-8"
|
||||
|
||||
@architecture path /ARCHITECTURE.md
|
||||
header @architecture Cache-Control "no-cache, no-store, must-revalidate"
|
||||
|
||||
@missingText {
|
||||
path *.txt *.md
|
||||
not file
|
||||
}
|
||||
respond @missingText 404
|
||||
|
||||
@plainText path /llms.txt /llms-full.txt
|
||||
header @plainText Content-Type "text/plain; charset=utf-8"
|
||||
@markdown path /ARCHITECTURE.md
|
||||
header @markdown Content-Type "text/markdown; charset=utf-8"
|
||||
header @markdown Cache-Control "no-cache, no-store, must-revalidate"
|
||||
file_server
|
||||
header Link "</llms.txt>; rel=\"llms\""
|
||||
try_files {path} {path}.html {path}/index.html /index.html
|
||||
}
|
||||
|
||||
10
README.md
10
README.md
@@ -1,16 +1,6 @@
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](docs/architecture/layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](docs/architecture/modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](docs/architecture/segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
@@ -6,16 +6,6 @@ description: Назначение архитектуры, ключевые пр
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
127
generate.ts
127
generate.ts
@@ -5,12 +5,7 @@ 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;
|
||||
@@ -48,54 +43,15 @@ function parseSidebar(): SidebarGroup[] {
|
||||
|
||||
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)));
|
||||
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) =>
|
||||
@@ -104,38 +60,6 @@ const stripFrontmatter = (content: string) =>
|
||||
const stripRulesLink = (content: string) =>
|
||||
content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\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;
|
||||
@@ -160,9 +84,8 @@ const buildArchitectureMarkdown = (routePrefix: string) => {
|
||||
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);
|
||||
const route = routePrefix + "/" + file.replace(/\.md$/, "");
|
||||
const processed = file.endsWith("index.md") ? content : shiftHeadings(content);
|
||||
parts.push(`<!-- ${route} -->\n${processed}`);
|
||||
}
|
||||
|
||||
@@ -176,7 +99,9 @@ function buildLlms() {
|
||||
for (const group of SIDEBAR) {
|
||||
parts.push(`## ${group.text}`);
|
||||
for (const item of group.items) {
|
||||
const fileRel = linkToFileRel(item.link);
|
||||
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)) {
|
||||
@@ -184,7 +109,7 @@ function buildLlms() {
|
||||
const fm = raw.match(/^---[\s\S]*?---\n*/m);
|
||||
desc = fm ? fm[0].match(/description:\s*(.+)/)?.[1] || "" : "";
|
||||
}
|
||||
const route = fileRelToMdUrl(fileRel);
|
||||
const route = "/docs" + item.link;
|
||||
const line = desc
|
||||
? `- [${item.text}](${route}): ${desc}`
|
||||
: `- [${item.text}](${route})`;
|
||||
@@ -205,29 +130,10 @@ function buildLlmsFull() {
|
||||
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");
|
||||
fs.writeFileSync(outPath, buildArchitectureMarkdown("/docs"), "utf8");
|
||||
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
|
||||
}
|
||||
|
||||
@@ -242,7 +148,6 @@ function buildZip() {
|
||||
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");
|
||||
}
|
||||
@@ -269,14 +174,12 @@ function buildReadme() {
|
||||
|
||||
let content = stripFrontmatter(fs.readFileSync(indexPath, "utf8"));
|
||||
content = content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
|
||||
content = transformReadmeLinks(content);
|
||||
fs.writeFileSync("./README.md", content, "utf8");
|
||||
console.log("README.md создан");
|
||||
}
|
||||
|
||||
buildLlms();
|
||||
buildLlmsFull();
|
||||
copyMarkdownFiles();
|
||||
buildPublicArchitecture();
|
||||
buildZip();
|
||||
buildReadme();
|
||||
|
||||
20
index.html
20
index.html
@@ -5,28 +5,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SLM Design</title>
|
||||
<meta name="description" content="Scoped Layered Module Design — модульная архитектура фронтенд-приложений" />
|
||||
<meta name="llms" content="/llms.txt" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="alternate llms" type="text/plain" href="/llms.txt" title="llms.txt" />
|
||||
<link rel="alternate" type="text/plain" href="/llms-full.txt" title="llms-full.txt" />
|
||||
<link rel="alternate" type="text/markdown" href="/ARCHITECTURE.md" title="ARCHITECTURE.md" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<main>
|
||||
<h1>SLM Design</h1>
|
||||
<p>Scoped Layered Module Design — модульная архитектура фронтенд-приложений.</p>
|
||||
<nav aria-label="Карта сайта и AI-артефакты">
|
||||
<ul>
|
||||
<li><a href="/docs/">Документация</a></li>
|
||||
<li><a href="/llms.txt" rel="alternate" type="text/plain">llms.txt</a></li>
|
||||
<li><a href="/llms-full.txt" rel="alternate" type="text/plain">llms-full.txt</a></li>
|
||||
<li><a href="/ARCHITECTURE.md" rel="alternate" type="text/markdown">ARCHITECTURE.md</a></li>
|
||||
<li><a href="/slm-design.zip" download>slm-design.zip</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
> Локальная копия канонической спецификации SLM Design.
|
||||
> Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md
|
||||
> Не редактировать вручную в этом проекте.
|
||||
|
||||
<!-- /docs/architecture/ -->
|
||||
<!-- /docs/architecture//index -->
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
|
||||
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||
|
||||
- [Слои](#слои) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](#модули) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](#сегменты) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
@@ -508,7 +494,7 @@ backend-api/
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание сегментов — в разделе [Сегменты](#сегменты).
|
||||
Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
|
||||
|
||||
### Публичный API
|
||||
|
||||
@@ -685,7 +671,7 @@ export const HomeScreen = () => {
|
||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||
- Не содержит бизнес-логику или сценарную логику.
|
||||
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](#компонент).
|
||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
|
||||
|
||||
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||
|
||||
|
||||
@@ -8,19 +8,10 @@ export const homeCards = [
|
||||
cta: 'Открыть →',
|
||||
},
|
||||
{
|
||||
title: 'Скачать',
|
||||
description: 'Локальная копия спецификации и архив документации.',
|
||||
actions: [
|
||||
{
|
||||
href: '/ARCHITECTURE.md',
|
||||
label: 'ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
href: '/slm-design.zip',
|
||||
label: 'slm-design.zip',
|
||||
download: true,
|
||||
},
|
||||
],
|
||||
title: 'ARCHITECTURE.md',
|
||||
description: 'Полная версия архитектуры в одном файле',
|
||||
href: '/ARCHITECTURE.md',
|
||||
cta: 'Открыть →',
|
||||
},
|
||||
{
|
||||
title: 'Ассистенту',
|
||||
|
||||
@@ -48,12 +48,7 @@ export function HomeScreen() {
|
||||
<p>{card.description}</p>
|
||||
<div className={styles.cardActions}>
|
||||
{card.actions.map((action) => (
|
||||
<a
|
||||
className={styles.cardAction}
|
||||
download={'download' in action ? action.download : undefined}
|
||||
href={action.href}
|
||||
key={action.href}
|
||||
>
|
||||
<a className={styles.cardAction} href={action.href} key={action.href}>
|
||||
{action.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user