2 Commits

Author SHA1 Message Date
1af27795ed fix: добавить cache-busting для ARCHITECTURE.md через query-параметр версии 2026-05-01 21:06:34 +03:00
d69fca16fe 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
2026-05-01 21:00:25 +03:00
12 changed files with 211 additions and 307 deletions

View File

@@ -2,37 +2,12 @@ name: CI/CD Pipeline
on: on:
push: push:
branches: [main, dev] branches: [main]
jobs: jobs:
build: docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')" if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Установка зависимостей
run: npm ci
- name: Генерация артефактов
run: npm run generate
- name: Сборка документации
run: npm run docs:build
- name: Сборка лендинга
run: npm run build
version:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
outputs: outputs:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
steps: steps:
@@ -41,6 +16,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Версия из package.json - name: Версия из package.json
id: version id: version
run: | run: |
@@ -48,6 +28,23 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Версия: $VERSION" echo "Версия: $VERSION"
- name: Генерация docs
run: |
npm ci
npm run docs
- name: Коммит generated/
run: |
git config user.name "CI Bot"
git config user.email "ci@gromlab.ru"
git add generated/ README_RU.md
if git diff --cached --quiet; then
echo "Нет изменений, пропуск"
else
git commit -m "docs: обновить generated (${{ steps.version.outputs.version }}) [skip ci]"
git push origin main
fi
- name: Создать тег - name: Создать тег
run: | run: |
VERSION=${{ steps.version.outputs.version }} VERSION=${{ steps.version.outputs.version }}
@@ -61,7 +58,7 @@ jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: version needs: docs
if: "!contains(github.event.head_commit.message, '[skip ci]')" if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps: steps:
- name: Checkout - name: Checkout
@@ -95,7 +92,7 @@ jobs:
type=ref,event=branch type=ref,event=branch
type=sha,prefix= type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ needs.version.outputs.version }} type=raw,value=${{ needs.docs.outputs.version }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -107,7 +104,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
VERSION_TAG=${{ needs.version.outputs.version }} VERSION_TAG=${{ needs.docs.outputs.version }}
provenance: false provenance: false
sbom: false sbom: false

2
.gitignore vendored
View File

@@ -135,7 +135,6 @@ dist
.vitepress/cache .vitepress/cache
.vitepress/dist .vitepress/dist
docs/.vitepress docs/.vitepress
docs/public/
# Generated artifacts # Generated artifacts
public/docs/ public/docs/
@@ -150,3 +149,4 @@ dist
# Рабочие заметки # Рабочие заметки
notes notes

View File

@@ -14,18 +14,10 @@ const sidebar = [
export default defineConfig({ export default defineConfig({
srcDir: 'docs', srcDir: 'docs',
srcExclude: ['public/**'],
outDir: 'public/docs', outDir: 'public/docs',
title: 'SLM Design', title: 'SLM Design',
description: 'Правила и стандарты архитектуры проекта', description: 'Правила и стандарты архитектуры проекта',
base: '/docs/', 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: { themeConfig: {
sidebar, sidebar,

View File

@@ -4,79 +4,182 @@
## О проекте ## О проекте
Сайт-документация архитектуры SLM Design с лендингом. Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
- Лендинг: React + Vite - Движок: VitePress
- Документация: VitePress - Языки: русский (основной), английский
- Язык: русский - Русская версия: `docs/ru/`
- Документация: `docs/architecture/` - Английская версия: `docs/en/`
## Команды ## Команды
| Команда | Что делает | | Команда | Что делает |
|---------|-----------| |---------|-----------|
| `npm run dev` | Локальный сервер лендинга | | `npm run dev` | Локальный сервер разработки |
| `npm run build` | Сборка лендинга | | `npm run build` | Сборка статического сайта |
| `npm run docs:dev` | Локальный сервер документации | | `npm run docs` | Генерация `generated/{lang}/RULES.md` — единый файл для AI-ассистентов |
| `npm run docs:build` | Сборка документации |
| `npm run generate` | Генерация артефактов (llms.txt, llms-full.txt, ARCHITECTURE.md, ZIP, README) |
## Структура файлов ## Структура файлов
``` ```
docs/ docs/
├── index.md # Страница навигации по документации ├── ru/ # Русская версия (основная)
└── architecture/ # Разделы архитектуры │ ├── index.md # Главная страница
├── index.md # Обзор SLM Design ├── basics/ # Базовые правила
├── layers.md # Слои │ │ ├── tech-stack.md
├── modules.md # Модули │ │ ├── architecture.md
└── segments.md # Сегменты │ ├── code-style.md
│ │ ├── naming.md
│ │ ├── documentation.md
│ │ └── typing.md
│ └── applied/ # Прикладные разделы
│ ├── vscode.md
│ ├── project-structure.md
│ ├── components.md
│ ├── page-level.md
│ ├── templates-generation.md
│ ├── styles.md
│ ├── images-sprites.md
│ ├── svg-sprites.md
│ ├── video.md
│ ├── api.md
│ ├── stores.md
│ ├── hooks.md
│ ├── fonts.md
│ └── localization.md
├── en/ # Английская версия (зеркало ru/)
.vitepress/ .vitepress/
├── config.ts # Конфигурация VitePress, сайдбар ├── config.ts # Конфигурация VitePress, сайдбары, локали
public/ generated/
├── llms.txt # Карта документации для LLM ├── ru/RULES.md # Сгенерированный единый файл (ru)
── llms-full.txt # Полная документация в одном файле ── en/RULES.md # Сгенерированный единый файл (en)
└── ARCHITECTURE.md # Единый файл архитектуры concat-md.js # Скрипт генерации RULES.md
generate.ts # Скрипт генерации артефактов
src/ # Исходники лендинга (React + Vite)
``` ```
### Добавление нового раздела ### Добавление нового раздела
1. Создать `.md`-файл в `docs/architecture/`. 1. Создать `.md`-файл в нужной папке (`basics/` или `applied/`).
2. Добавить пункт в сайдбар — `.vitepress/config.ts`. 2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод).
3. Добавить `description` в frontmatter файла — используется для `llms.txt`. 3. Добавить файл в массив `fileOrder``concat-md.js` (для генерации RULES.md).
4. Запустить `npm run generate` для обновления артефактов.
## Frontmatter ## Два типа документации
### Базовые правила
**Отвечает на вопрос:** «Каким должен быть любой код?»
Универсальные стандарты, **не привязанные к конкретной области**.
Правило базовое, если оно применимо ко всему коду одинаково: именование переменных, оформление импортов, когда использовать `type` vs `interface`.
Примеры в базовых правилах допускаются, но служат иллюстрацией принципа, а не инструкцией по конкретной области.
**Граница:** если правило касается только одной области (только стили, только компоненты, только API) — оно живёт в прикладном разделе, не в базовых.
### Прикладные разделы
**Отвечает на вопрос:** «Как работать с X?»
Полное описание конкретной области: структура файлов, правила, именование, типизация, примеры.
**Граница:** прикладной раздел не дублирует базовые правила.
Если правило уже описано в базовых — прикладной раздел ссылается на него, но не повторяет.
## Структура прикладного раздела
Шаблон ниже описывает все допустимые секции. Раздел включает только те секции, которые для него релевантны — пустые секции не создаются.
```markdown
# {Название}
Краткое описание: о чём раздел и какие аспекты работы с областью он охватывает.
## Что нужно знать
Неочевидная информация, которую читатель должен знать перед чтением раздела.
Если для раздела нет такой вводной — секция не создаётся.
## Структура
Файловая организация: какие файлы создавать и куда класть.
Обязательно — дерево файлов через code-block.
## Правила
Конкретные требования, специфичные для области. Делятся на две подсекции:
### Реализация
Как написан код внутри файла: синтаксис, паттерны, API.
Отвечает на вопрос: «Как писать код?»
Примеры: объявление через `const`, деструктуризация пропсов, формат вызова `cl()`, способ подключения стилей, структура хука.
### Организация
Как компонент/модуль встроен в проект: файловые границы, зоны ответственности, экспорт.
Отвечает на вопрос: «Где что лежит и за что отвечает?»
Примеры: один компонент — один файл, вложенные компоненты в `ui/`, логика выносится в `model/`.
Формат обеих подсекций — маркированный список.
Для неочевидных случаев — блоки «Хорошо / Плохо».
Если в области нет правил одной из категорий — подсекция не создаётся.
## Именование
Соглашения по именам, специфичные для этой области.
Только то, что НЕ покрыто в базовом разделе «Именование».
## Типизация
Правила типизации, специфичные для этой области.
Только то, что НЕ покрыто в базовом разделе «Типизация».
## Документирование
Что и как документировать в этой области.
Только то, что НЕ покрыто в базовом разделе «Документирование».
## Примеры
Полноценные примеры кода.
Каждый пример с путём к файлу и пояснениями.
```
### Порядок секций
Порядок фиксированный: контекст → структура → правила → специализации базовых правил → примеры.
Логика: читатель сначала понимает «что это», потом «где это лежит», потом «как это делать», и в конце видит полный пример.
### Секции-расширения базовых правил
«Именование», «Типизация», «Документирование» в прикладном разделе — это **точки расширения** базовых правил.
- В базовых описано общее: `camelCase` для переменных, `type` vs `interface`, формат JSDoc.
- В прикладном разделе описано специфичное: как именовать CSS-классы (стили), как типизировать пропсы компонентов (компоненты), как документировать хуки (хуки).
Если для области нет специфики по именованию, типизации или документированию — секция не создаётся.
## Конвенции оформления
### Frontmatter
Каждый `.md`-файл начинается с YAML frontmatter: Каждый `.md`-файл начинается с YAML frontmatter:
```yaml ```yaml
--- ---
title: Название раздела title: Название раздела
description: Краткое описание для llms.txt
--- ---
``` ```
- `title` совпадает с `h1`-заголовком в файле. Значение `title` совпадает с текстом `h1`-заголовка в файле.
- `description` — кратное описание содержимого страницы, используется при генерации `llms.txt`.
## Структура страницы документации
Каждая страница начинается одинаково:
1. **Заголовок** (`h1`) — совпадает с `title` из frontmatter.
2. **Описание раздела** — 12 строки сразу после заголовка. Говорит, что это за раздел, какую информацию он описывает и что читатель в нём получит.
3. **Определение** (`## Определение`) — для справочных страниц, посвящённых одному термину. Короткая формулировка жирным: что это за сущность и какую роль она играет.
4. **Контент** — остальные `h2`-подразделы.
## Конвенции оформления
### Заголовки ### Заголовки
- Один `h1` на файл — совпадает с `title` из frontmatter. - Один `h1` на файл — совпадает с `title` из frontmatter.
- Сразу после `h1`описание раздела (12 предложения). - Сразу после `h1`вводный абзац (одно-два предложения).
- Основные секции — `h2`. - Основные секции — `h2`.
- Подсекции внутри `h2``h3`. - Подсекции внутри `h2``h3`.
- `h4` не используется. - `h4` не используется.
@@ -91,6 +194,8 @@ description: Краткое описание для llms.txt
Используются для контрастного сравнения правильного и неправильного подхода. Используются для контрастного сравнения правильного и неправильного подхода.
Формат:
```markdown ```markdown
**Хорошо:** **Хорошо:**
@@ -112,10 +217,11 @@ description: Краткое описание для llms.txt
### Ссылки между разделами ### Ссылки между разделами
Раздел может ссылаться на другие разделы, но не дублирует их содержимое. Прикладной раздел может ссылаться на другие разделы, но не дублирует их содержимое.
## Принципы ## Принципы
1. **Не дублировать.** Одна мысль живёт в одном месте. Остальные ссылаются. 1. **Не дублировать.** Одна мысль живёт в одном месте. Остальные ссылаются.
2. **Пустые секции не создавать.** Если для раздела нет специфики — секция не создаётся. 2. **Базовое vs прикладное.** Если правило применимо ко всему коду — оно базовое. Если только к одной области — прикладное.
3. **Примеры обязательны.** Раздел без примеров кода — незавершён. 3. **Пустые секции не создавать.** Если для раздела нет специфики по именованию — секции «Именование» в нём нет.
4. **Примеры обязательны.** Прикладной раздел без примеров кода — незавершён.

View File

@@ -1,40 +1,10 @@
:8082 { :8082 {
root * /srv root * /srv
@plainText path /llms.txt /llms-full.txt
# Устаревшие пути llms.txt в подпапках ведём к корневым артефактам. header @plainText Content-Type "text/plain; charset=utf-8"
redir /docs/llms.txt /llms.txt 301 @markdown path /ARCHITECTURE.md
redir /docs/llms-full.txt /llms-full.txt 301 header @markdown Content-Type "text/markdown; charset=utf-8"
# Чистые 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
file_server file_server
header Link "</llms.txt>; rel=\"llms\""
try_files {path} {path}.html {path}/index.html /index.html try_files {path} {path}.html {path}/index.html /index.html
} }

View File

@@ -1,16 +1,6 @@
# SLM Design # SLM Design
Scoped Layered Module 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/` и другие) и правила размещения файлов.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
## Преимущества ## Преимущества
### Вертикальная организация домена ### Вертикальная организация домена

View File

@@ -6,16 +6,6 @@ description: Назначение архитектуры, ключевые пр
# SLM Design # SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
## Преимущества ## Преимущества
### Вертикальная организация домена ### Вертикальная организация домена

View File

@@ -5,12 +5,7 @@ import { execFileSync } from "child_process";
const SRC_DIR = "./docs"; const SRC_DIR = "./docs";
const PUBLIC_DIR = "./public"; 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_FILE = "ARCHITECTURE.md";
const PUBLIC_ARCHITECTURE_NOTICE = `> Локальная копия канонической спецификации SLM Design.
> Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md
> Не редактировать вручную в этом проекте.`;
interface SidebarItem { interface SidebarItem {
text: string; text: string;
@@ -48,54 +43,15 @@ function parseSidebar(): SidebarGroup[] {
const SIDEBAR = parseSidebar(); 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[] { 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) => const stripFrontmatter = (content: string) =>
@@ -104,38 +60,6 @@ const stripFrontmatter = (content: string) =>
const stripRulesLink = (content: string) => const stripRulesLink = (content: string) =>
content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, ""); 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 shiftHeadings = (content: string) => {
const lines = content.split("\n"); const lines = content.split("\n");
let inCodeBlock = false; let inCodeBlock = false;
@@ -160,9 +84,8 @@ const buildArchitectureMarkdown = (routePrefix: string) => {
const content = stripRulesLink(stripFrontmatter(raw)).trim(); const content = stripRulesLink(stripFrontmatter(raw)).trim();
if (!content) continue; if (!content) continue;
const route = routePrefix + fileRelToRoute(file).replace(DOC_ROUTE_PREFIX, ""); const route = routePrefix + "/" + file.replace(/\.md$/, "");
const shifted = file.endsWith("index.md") ? content : shiftHeadings(content); const processed = file.endsWith("index.md") ? content : shiftHeadings(content);
const processed = transformSingleFileLinks(shifted);
parts.push(`<!-- ${route} -->\n${processed}`); parts.push(`<!-- ${route} -->\n${processed}`);
} }
@@ -176,7 +99,9 @@ function buildLlms() {
for (const group of SIDEBAR) { for (const group of SIDEBAR) {
parts.push(`## ${group.text}`); parts.push(`## ${group.text}`);
for (const item of group.items) { 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); const filePath = path.join(SRC_DIR, fileRel);
let desc = ""; let desc = "";
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
@@ -184,7 +109,7 @@ function buildLlms() {
const fm = raw.match(/^---[\s\S]*?---\n*/m); const fm = raw.match(/^---[\s\S]*?---\n*/m);
desc = fm ? fm[0].match(/description:\s*(.+)/)?.[1] || "" : ""; desc = fm ? fm[0].match(/description:\s*(.+)/)?.[1] || "" : "";
} }
const route = fileRelToMdUrl(fileRel); const route = "/docs" + item.link;
const line = desc const line = desc
? `- [${item.text}](${route}): ${desc}` ? `- [${item.text}](${route}): ${desc}`
: `- [${item.text}](${route})`; : `- [${item.text}](${route})`;
@@ -205,29 +130,10 @@ function buildLlmsFull() {
console.log(`llms-full.txt создан: ${outPath}`); 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() { function buildPublicArchitecture() {
const outPath = path.join(PUBLIC_DIR, PUBLIC_ARCHITECTURE_FILE); 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.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(outPath, content, "utf8"); fs.writeFileSync(outPath, buildArchitectureMarkdown("/docs"), "utf8");
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`); console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
} }
@@ -242,7 +148,6 @@ function buildZip() {
if (!fs.existsSync(src)) continue; if (!fs.existsSync(src)) continue;
let content = fs.readFileSync(src, "utf8"); let content = fs.readFileSync(src, "utf8");
content = stripRulesLink(stripFrontmatter(content)).trim(); content = stripRulesLink(stripFrontmatter(content)).trim();
content = transformArchiveLinks(content);
const destName = path.basename(file); const destName = path.basename(file);
fs.writeFileSync(path.join(tmpDir, destName), content, "utf8"); fs.writeFileSync(path.join(tmpDir, destName), content, "utf8");
} }
@@ -269,14 +174,12 @@ function buildReadme() {
let content = stripFrontmatter(fs.readFileSync(indexPath, "utf8")); let content = stripFrontmatter(fs.readFileSync(indexPath, "utf8"));
content = content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, ""); content = content.replace(/<!-- rules-link -->[\s\S]*?<!-- \/rules-link -->\n*/g, "");
content = transformReadmeLinks(content);
fs.writeFileSync("./README.md", content, "utf8"); fs.writeFileSync("./README.md", content, "utf8");
console.log("README.md создан"); console.log("README.md создан");
} }
buildLlms(); buildLlms();
buildLlmsFull(); buildLlmsFull();
copyMarkdownFiles();
buildPublicArchitecture(); buildPublicArchitecture();
buildZip(); buildZip();
buildReadme(); buildReadme();

View File

@@ -5,28 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SLM Design</title> <title>SLM Design</title>
<meta name="description" content="Scoped Layered Module Design — модульная архитектура фронтенд-приложений" /> <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="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> </head>
<body> <body>
<div id="root"> <div id="root"></div>
<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>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,21 +1,7 @@
> Локальная копия канонической спецификации SLM Design. <!-- /docs/architecture//index -->
> Источник: https://slm-design.gromlab.ru/ARCHITECTURE.md
> Не редактировать вручную в этом проекте.
<!-- /docs/architecture/ -->
# SLM Design # SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](#слои) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](#модули) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](#сегменты) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
## Преимущества ## Преимущества
### Вертикальная организация домена ### Вертикальная организация домена
@@ -508,7 +494,7 @@ backend-api/
└── index.ts # публичный API └── index.ts # публичный API
``` ```
Подробное описание сегментов — в разделе [Сегменты](#сегменты). Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
### Публичный API ### Публичный API
@@ -685,7 +671,7 @@ export const HomeScreen = () => {
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. - Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику. - Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](#компонент). Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.

View File

@@ -1,3 +1,5 @@
const version = __BUILD_VERSION__
export const repositoryUrl = 'https://gromlab.ru/gromov/slm-design' export const repositoryUrl = 'https://gromlab.ru/gromov/slm-design'
export const homeCards = [ export const homeCards = [
@@ -8,19 +10,10 @@ export const homeCards = [
cta: 'Открыть →', cta: 'Открыть →',
}, },
{ {
title: 'Скачать', title: 'ARCHITECTURE.md',
description: 'Локальная копия спецификации и архив документации.', description: 'Полная версия архитектуры в одном файле',
actions: [ href: `/ARCHITECTURE.md?v=${version}`,
{ cta: 'Открыть →',
href: '/ARCHITECTURE.md',
label: 'ARCHITECTURE.md',
},
{
href: '/slm-design.zip',
label: 'slm-design.zip',
download: true,
},
],
}, },
{ {
title: 'Ассистенту', title: 'Ассистенту',

View File

@@ -48,12 +48,7 @@ export function HomeScreen() {
<p>{card.description}</p> <p>{card.description}</p>
<div className={styles.cardActions}> <div className={styles.cardActions}>
{card.actions.map((action) => ( {card.actions.map((action) => (
<a <a className={styles.cardAction} href={action.href} key={action.href}>
className={styles.cardAction}
download={'download' in action ? action.download : undefined}
href={action.href}
key={action.href}
>
{action.label} {action.label}
</a> </a>
))} ))}