Compare commits
9 Commits
1af27795ed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a1de7cad4 | |||
| 07542330b5 | |||
| 245dbbe302 | |||
| ee5b947fb0 | |||
| b3d501c920 | |||
| 5431651814 | |||
| 54b4060b6f | |||
|
|
004a73a869 | ||
| 7ee243389d |
@@ -2,12 +2,37 @@ name: CI/CD Pipeline
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docs:
|
build:
|
||||||
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:
|
||||||
@@ -16,11 +41,6 @@ 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: |
|
||||||
@@ -28,23 +48,6 @@ 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 }}
|
||||||
@@ -58,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: docs
|
needs: version
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -92,7 +95,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.docs.outputs.version }}
|
type=raw,value=${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -104,7 +107,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.docs.outputs.version }}
|
VERSION_TAG=${{ needs.version.outputs.version }}
|
||||||
provenance: false
|
provenance: false
|
||||||
sbom: false
|
sbom: false
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -135,6 +135,7 @@ dist
|
|||||||
.vitepress/cache
|
.vitepress/cache
|
||||||
.vitepress/dist
|
.vitepress/dist
|
||||||
docs/.vitepress
|
docs/.vitepress
|
||||||
|
docs/public/
|
||||||
|
|
||||||
# Generated artifacts
|
# Generated artifacts
|
||||||
public/docs/
|
public/docs/
|
||||||
@@ -149,4 +150,3 @@ dist
|
|||||||
# Рабочие заметки
|
# Рабочие заметки
|
||||||
notes
|
notes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,18 @@ 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,
|
||||||
|
|||||||
198
CONTRIBUTING.md
198
CONTRIBUTING.md
@@ -4,182 +4,79 @@
|
|||||||
|
|
||||||
## О проекте
|
## О проекте
|
||||||
|
|
||||||
Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
|
Сайт-документация архитектуры SLM Design с лендингом.
|
||||||
|
|
||||||
- Движок: VitePress
|
- Лендинг: React + Vite
|
||||||
- Языки: русский (основной), английский
|
- Документация: VitePress
|
||||||
- Русская версия: `docs/ru/`
|
- Язык: русский
|
||||||
- Английская версия: `docs/en/`
|
- Документация: `docs/architecture/`
|
||||||
|
|
||||||
## Команды
|
## Команды
|
||||||
|
|
||||||
| Команда | Что делает |
|
| Команда | Что делает |
|
||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| `npm run dev` | Локальный сервер разработки |
|
| `npm run dev` | Локальный сервер лендинга |
|
||||||
| `npm run build` | Сборка статического сайта |
|
| `npm run build` | Сборка лендинга |
|
||||||
| `npm run docs` | Генерация `generated/{lang}/RULES.md` — единый файл для AI-ассистентов |
|
| `npm run docs:dev` | Локальный сервер документации |
|
||||||
|
| `npm run docs:build` | Сборка документации |
|
||||||
|
| `npm run generate` | Генерация артефактов (llms.txt, llms-full.txt, ARCHITECTURE.md, ZIP, README) |
|
||||||
|
|
||||||
## Структура файлов
|
## Структура файлов
|
||||||
|
|
||||||
```
|
```
|
||||||
docs/
|
docs/
|
||||||
├── ru/ # Русская версия (основная)
|
├── index.md # Страница навигации по документации
|
||||||
│ ├── index.md # Главная страница
|
└── architecture/ # Разделы архитектуры
|
||||||
│ ├── basics/ # Базовые правила
|
├── index.md # Обзор SLM Design
|
||||||
│ │ ├── tech-stack.md
|
├── layers.md # Слои
|
||||||
│ │ ├── architecture.md
|
├── modules.md # Модули
|
||||||
│ │ ├── code-style.md
|
└── segments.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, сайдбар
|
||||||
generated/
|
public/
|
||||||
├── ru/RULES.md # Сгенерированный единый файл (ru)
|
├── llms.txt # Карта документации для LLM
|
||||||
└── en/RULES.md # Сгенерированный единый файл (en)
|
├── llms-full.txt # Полная документация в одном файле
|
||||||
concat-md.js # Скрипт генерации RULES.md
|
└── ARCHITECTURE.md # Единый файл архитектуры
|
||||||
|
generate.ts # Скрипт генерации артефактов
|
||||||
|
src/ # Исходники лендинга (React + Vite)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Добавление нового раздела
|
### Добавление нового раздела
|
||||||
|
|
||||||
1. Создать `.md`-файл в нужной папке (`basics/` или `applied/`).
|
1. Создать `.md`-файл в `docs/architecture/`.
|
||||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод).
|
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||||
3. Добавить файл в массив `fileOrder` — `concat-md.js` (для генерации RULES.md).
|
3. Добавить `description` в frontmatter файла — используется для `llms.txt`.
|
||||||
|
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. **Описание раздела** — 1–2 строки сразу после заголовка. Говорит, что это за раздел, какую информацию он описывает и что читатель в нём получит.
|
||||||
|
3. **Определение** (`## Определение`) — для справочных страниц, посвящённых одному термину. Короткая формулировка жирным: что это за сущность и какую роль она играет.
|
||||||
|
4. **Контент** — остальные `h2`-подразделы.
|
||||||
|
|
||||||
|
## Конвенции оформления
|
||||||
|
|
||||||
### Заголовки
|
### Заголовки
|
||||||
|
|
||||||
- Один `h1` на файл — совпадает с `title` из frontmatter.
|
- Один `h1` на файл — совпадает с `title` из frontmatter.
|
||||||
- Сразу после `h1` — вводный абзац (одно-два предложения).
|
- Сразу после `h1` — описание раздела (1–2 предложения).
|
||||||
- Основные секции — `h2`.
|
- Основные секции — `h2`.
|
||||||
- Подсекции внутри `h2` — `h3`.
|
- Подсекции внутри `h2` — `h3`.
|
||||||
- `h4` не используется.
|
- `h4` не используется.
|
||||||
@@ -194,8 +91,6 @@ title: Название раздела
|
|||||||
|
|
||||||
Используются для контрастного сравнения правильного и неправильного подхода.
|
Используются для контрастного сравнения правильного и неправильного подхода.
|
||||||
|
|
||||||
Формат:
|
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
**Хорошо:**
|
**Хорошо:**
|
||||||
|
|
||||||
@@ -217,11 +112,10 @@ title: Название раздела
|
|||||||
|
|
||||||
### Ссылки между разделами
|
### Ссылки между разделами
|
||||||
|
|
||||||
Прикладной раздел может ссылаться на другие разделы, но не дублирует их содержимое.
|
Раздел может ссылаться на другие разделы, но не дублирует их содержимое.
|
||||||
|
|
||||||
## Принципы
|
## Принципы
|
||||||
|
|
||||||
1. **Не дублировать.** Одна мысль живёт в одном месте. Остальные ссылаются.
|
1. **Не дублировать.** Одна мысль живёт в одном месте. Остальные ссылаются.
|
||||||
2. **Базовое vs прикладное.** Если правило применимо ко всему коду — оно базовое. Если только к одной области — прикладное.
|
2. **Пустые секции не создавать.** Если для раздела нет специфики — секция не создаётся.
|
||||||
3. **Пустые секции не создавать.** Если для раздела нет специфики по именованию — секции «Именование» в нём нет.
|
3. **Примеры обязательны.** Раздел без примеров кода — незавершён.
|
||||||
4. **Примеры обязательны.** Прикладной раздел без примеров кода — незавершён.
|
|
||||||
|
|||||||
40
Caddyfile
40
Caddyfile
@@ -1,10 +1,40 @@
|
|||||||
:8082 {
|
:8082 {
|
||||||
root * /srv
|
root * /srv
|
||||||
@plainText path /llms.txt /llms-full.txt
|
|
||||||
header @plainText Content-Type "text/plain; charset=utf-8"
|
# Устаревшие пути llms.txt в подпапках ведём к корневым артефактам.
|
||||||
@markdown path /ARCHITECTURE.md
|
redir /docs/llms.txt /llms.txt 301
|
||||||
header @markdown Content-Type "text/markdown; charset=utf-8"
|
redir /docs/llms-full.txt /llms-full.txt 301
|
||||||
file_server
|
|
||||||
|
# Чистые 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\""
|
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
|
||||||
try_files {path} {path}.html {path}/index.html /index.html
|
try_files {path} {path}.html {path}/index.html /index.html
|
||||||
}
|
}
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,6 +1,16 @@
|
|||||||
# 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/` и другие) и правила размещения файлов.
|
||||||
|
|
||||||
|
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||||
|
|
||||||
## Преимущества
|
## Преимущества
|
||||||
|
|
||||||
### Вертикальная организация домена
|
### Вертикальная организация домена
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ 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/` и другие) и правила размещения файлов.
|
||||||
|
|
||||||
|
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||||
|
|
||||||
## Преимущества
|
## Преимущества
|
||||||
|
|
||||||
### Вертикальная организация домена
|
### Вертикальная организация домена
|
||||||
|
|||||||
127
generate.ts
127
generate.ts
@@ -5,7 +5,12 @@ 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;
|
||||||
@@ -43,15 +48,54 @@ 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) =>
|
return SIDEBAR.flatMap((g) => g.items.map((item) => linkToFileRel(item.link)));
|
||||||
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) =>
|
||||||
@@ -60,6 +104,38 @@ 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;
|
||||||
@@ -84,8 +160,9 @@ 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 + "/" + file.replace(/\.md$/, "");
|
const route = routePrefix + fileRelToRoute(file).replace(DOC_ROUTE_PREFIX, "");
|
||||||
const processed = file.endsWith("index.md") ? content : shiftHeadings(content);
|
const shifted = file.endsWith("index.md") ? content : shiftHeadings(content);
|
||||||
|
const processed = transformSingleFileLinks(shifted);
|
||||||
parts.push(`<!-- ${route} -->\n${processed}`);
|
parts.push(`<!-- ${route} -->\n${processed}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +176,7 @@ 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 rel = item.link.replace(/^\//, "") + ".md";
|
const fileRel = linkToFileRel(item.link);
|
||||||
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)) {
|
||||||
@@ -109,7 +184,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 = "/docs" + item.link;
|
const route = fileRelToMdUrl(fileRel);
|
||||||
const line = desc
|
const line = desc
|
||||||
? `- [${item.text}](${route}): ${desc}`
|
? `- [${item.text}](${route}): ${desc}`
|
||||||
: `- [${item.text}](${route})`;
|
: `- [${item.text}](${route})`;
|
||||||
@@ -130,10 +205,29 @@ 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, buildArchitectureMarkdown("/docs"), "utf8");
|
fs.writeFileSync(outPath, content, "utf8");
|
||||||
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
|
console.log(`${PUBLIC_ARCHITECTURE_FILE} создан: ${outPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +242,7 @@ 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");
|
||||||
}
|
}
|
||||||
@@ -174,12 +269,14 @@ 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();
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -5,10 +5,28 @@
|
|||||||
<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>
|
<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>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
<!-- /docs/architecture//index -->
|
> Локальная копия канонической спецификации SLM Design.
|
||||||
|
> Источник: 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/` и другие) и правила размещения файлов.
|
||||||
|
|
||||||
|
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||||
|
|
||||||
## Преимущества
|
## Преимущества
|
||||||
|
|
||||||
### Вертикальная организация домена
|
### Вертикальная организация домена
|
||||||
@@ -494,7 +508,7 @@ backend-api/
|
|||||||
└── index.ts # публичный API
|
└── index.ts # публичный API
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
|
Подробное описание сегментов — в разделе [Сегменты](#сегменты).
|
||||||
|
|
||||||
### Публичный API
|
### Публичный API
|
||||||
|
|
||||||
@@ -671,7 +685,7 @@ export const HomeScreen = () => {
|
|||||||
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||||
- Не содержит бизнес-логику или сценарную логику.
|
- Не содержит бизнес-логику или сценарную логику.
|
||||||
|
|
||||||
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
|
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](#компонент).
|
||||||
|
|
||||||
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
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 = [
|
||||||
@@ -10,10 +8,19 @@ export const homeCards = [
|
|||||||
cta: 'Открыть →',
|
cta: 'Открыть →',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ARCHITECTURE.md',
|
title: 'Скачать',
|
||||||
description: 'Полная версия архитектуры в одном файле',
|
description: 'Локальная копия спецификации и архив документации.',
|
||||||
href: `/ARCHITECTURE.md?v=${version}`,
|
actions: [
|
||||||
cta: 'Открыть →',
|
{
|
||||||
|
href: '/ARCHITECTURE.md',
|
||||||
|
label: 'ARCHITECTURE.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/slm-design.zip',
|
||||||
|
label: 'slm-design.zip',
|
||||||
|
download: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Ассистенту',
|
title: 'Ассистенту',
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ 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 className={styles.cardAction} href={action.href} key={action.href}>
|
<a
|
||||||
|
className={styles.cardAction}
|
||||||
|
download={'download' in action ? action.download : undefined}
|
||||||
|
href={action.href}
|
||||||
|
key={action.href}
|
||||||
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user