Compare commits
17 Commits
e5e4ace91a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bf1781f143 | |||
| e835210d6d | |||
| f4e78e3227 | |||
| c8d52e9128 | |||
| 9f1bc0cc32 | |||
| a6cd14585b | |||
| 1195c7b75d | |||
| 028a69f3ac | |||
| a9ea898220 | |||
| 7965ec2a96 | |||
| 7c224fed99 | |||
| 74cbd43a23 | |||
| ef58a02609 | |||
| e265799c26 | |||
| 781efc52f1 | |||
| ec01ae2e1c | |||
| 3d93efd90a |
@@ -6,7 +6,7 @@ const sidebar = [
|
|||||||
link: '/docs/',
|
link: '/docs/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Workflow',
|
text: 'Подсказки',
|
||||||
link: '/docs/workflow',
|
link: '/docs/workflow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -19,9 +19,9 @@ const sidebar = [
|
|||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Обзор', link: '/docs/basics/architecture/' },
|
{ text: 'Обзор', link: '/docs/basics/architecture/' },
|
||||||
{ text: 'Слои', link: '/docs/basics/architecture/reference/layers' },
|
{ text: 'Слои', link: '/docs/basics/architecture/layers' },
|
||||||
{ text: 'Модули', link: '/docs/basics/architecture/reference/modules' },
|
{ text: 'Модули', link: '/docs/basics/architecture/modules' },
|
||||||
{ text: 'Сегменты', link: '/docs/basics/architecture/reference/segments' },
|
{ text: 'Сегменты', link: '/docs/basics/architecture/segments' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ text: 'Стиль кода', link: '/docs/basics/code-style' },
|
{ text: 'Стиль кода', link: '/docs/basics/code-style' },
|
||||||
@@ -29,39 +29,96 @@ const sidebar = [
|
|||||||
{ text: 'Типизация', link: '/docs/basics/typing' },
|
{ text: 'Типизация', link: '/docs/basics/typing' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Создание проекта',
|
||||||
|
items: [
|
||||||
|
{ text: 'Из шаблона', link: '/docs/creating-project/from-template' },
|
||||||
|
{ text: 'По гайду вручную', link: '/docs/creating-project/manual' },
|
||||||
|
{ text: 'Чистый Next.js', link: '/docs/creating-project/nextjs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Работа с данными',
|
||||||
|
// collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Введение', link: '/docs/data/' },
|
||||||
|
{
|
||||||
|
text: 'REST',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Обзор', link: '/docs/data/rest/' },
|
||||||
|
{
|
||||||
|
text: 'Создание клиента',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Обзор', link: '/docs/data/rest/clients/' },
|
||||||
|
{ text: 'Автогенерация из OpenAPI', link: '/docs/data/rest/clients/auto' },
|
||||||
|
{ text: 'Ручное создание', link: '/docs/data/rest/clients/manual' },
|
||||||
|
{ text: 'GET-хуки REST-клиента', link: '/docs/data/rest/clients/hooks' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Использование',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Стратегии получения данных', link: '/docs/data/rest/strategies/' },
|
||||||
|
{ text: 'Серверный await', link: '/docs/data/rest/strategies/server-await' },
|
||||||
|
{ text: 'Параллельные серверные запросы', link: '/docs/data/rest/strategies/parallel-server-requests' },
|
||||||
|
{ text: 'Передача промиса ниже', link: '/docs/data/rest/strategies/pass-promise-down' },
|
||||||
|
{ text: 'Начальные данные для клиентских хуков', link: '/docs/data/rest/strategies/client-hooks-initial-data' },
|
||||||
|
{ text: 'Клиентский GET-хук', link: '/docs/data/rest/strategies/client-get-hook' },
|
||||||
|
{ text: 'Business-композиция', link: '/docs/data/rest/strategies/business-composition' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ text: 'Realtime', link: '/docs/data/realtime' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Прикладные разделы',
|
text: 'Прикладные разделы',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Структура проекта', link: '/docs/applied/project-structure' },
|
{ text: 'Структура проекта', link: '/docs/applied/project-structure' },
|
||||||
{ text: 'Алиасы', link: '/docs/applied/aliases' },
|
{ text: 'Страницы', link: '/docs/applied/page-level' },
|
||||||
{ text: 'Компоненты', link: '/docs/applied/components' },
|
{ text: 'Компонент', link: '/docs/applied/component' },
|
||||||
{ text: 'Страницы (App Router)', link: '/docs/applied/page-level' },
|
{ text: 'Модуль', link: '/docs/applied/module' },
|
||||||
{ text: 'Шаблоны и генерация кода', link: '/docs/applied/templates-generation' },
|
|
||||||
{
|
{
|
||||||
text: 'Стили',
|
text: 'Стили',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'PostCSS', link: '/docs/applied/styles/postcss' },
|
{ text: 'Настройка', link: '/docs/applied/styles/styles-setup' },
|
||||||
{ text: 'Использование', link: '/docs/applied/styles/usage' },
|
{ text: 'Использование', link: '/docs/applied/styles/styles-usage' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ text: 'Изображения', link: '/docs/applied/images-sprites' },
|
|
||||||
{
|
{
|
||||||
text: 'SVG-спрайты',
|
text: 'SVG-спрайты',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Установка и настройка', link: '/docs/applied/svg-sprites/setup' },
|
{ text: 'Введение', link: '/docs/applied/svg-sprites/svg-sprites-intro' },
|
||||||
{ text: 'Использование', link: '/docs/applied/svg-sprites/usage' },
|
{ text: 'Настройка', link: '/docs/applied/svg-sprites/svg-sprites-setup' },
|
||||||
|
{ text: 'Использование', link: '/docs/applied/svg-sprites/svg-sprites-usage' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ text: 'Видео', link: '/docs/applied/video' },
|
{ text: 'Изображения', link: '/docs/applied/images' },
|
||||||
{ text: 'API', link: '/docs/applied/api' },
|
|
||||||
{ text: 'Stores', link: '/docs/applied/stores' },
|
|
||||||
{ text: 'Хуки', link: '/docs/applied/hooks' },
|
|
||||||
{ text: 'Шрифты', link: '/docs/applied/fonts' },
|
{ text: 'Шрифты', link: '/docs/applied/fonts' },
|
||||||
{ text: 'Локализация', link: '/docs/applied/localization' },
|
{ text: 'Алиасы импортов', link: '/docs/applied/aliases' },
|
||||||
|
{
|
||||||
|
text: 'Шаблоны генерации',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Введение', link: '/docs/applied/templates/templates-intro' },
|
||||||
|
{ text: 'Настройка', link: '/docs/applied/templates/templates-setup' },
|
||||||
|
{ text: 'Создание шаблонов', link: '/docs/applied/templates/templates-create' },
|
||||||
|
{ text: 'Использование', link: '/docs/applied/templates/templates-usage' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ text: 'Biome', link: '/docs/applied/biome' },
|
{ text: 'Biome', link: '/docs/applied/biome' },
|
||||||
{ text: 'Настройка VS Code', link: '/docs/applied/vscode' },
|
{ text: 'PostCSS', link: '/docs/applied/postcss' },
|
||||||
|
{ text: 'VS Code', link: '/docs/applied/vscode' },
|
||||||
|
{ text: 'Локализация', link: '/docs/applied/localization' },
|
||||||
|
// Неактивные разделы: страницы существуют, но пока пустые.
|
||||||
|
// Оставляем в sidebar без `link`, чтобы видеть план, но без перехода.
|
||||||
|
{ text: 'Stores · в разработке' }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -88,11 +145,26 @@ export default defineConfig({
|
|||||||
// `docs/public/` содержит сгенерированные `.md`-копии и `llms.txt` для LLM
|
// `docs/public/` содержит сгенерированные `.md`-копии и `llms.txt` для LLM
|
||||||
// (попадают в корень `dist/` как статика). Исключаем из сканирования
|
// (попадают в корень `dist/` как статика). Исключаем из сканирования
|
||||||
// страниц, иначе VitePress рендерит их как HTML-страницы.
|
// страниц, иначе VitePress рендерит их как HTML-страницы.
|
||||||
srcExclude: ['public/**'],
|
//
|
||||||
|
// `DEVELOP.md` и `MAP.md` — файлы архива (точка входа и карта).
|
||||||
|
// Содержат относительные ссылки, на сайте им делать нечего —
|
||||||
|
// эту роль выполняют сайдбар и `llms.txt`.
|
||||||
|
srcExclude: ['public/**', '**/DEVELOP.md', '**/MAP.md'],
|
||||||
lang: 'ru-RU',
|
lang: 'ru-RU',
|
||||||
title: 'NextJS Style Guide',
|
title: 'NextJS Style Guide',
|
||||||
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
|
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
|
||||||
|
|
||||||
|
// Чистые URL без `.html` — канон для индексации.
|
||||||
|
// Серверная поддержка реализована в Caddyfile (try_files + редирект).
|
||||||
|
cleanUrls: true,
|
||||||
|
|
||||||
|
// Дублируем указатель на llms.txt в <head> — для агентов,
|
||||||
|
// которые читают HTML, но не парсят полный DOM/href.
|
||||||
|
head: [
|
||||||
|
['link', { rel: 'alternate', type: 'text/plain', href: '/llms.txt', title: 'llms.txt' }],
|
||||||
|
['link', { rel: 'alternate', type: 'text/plain', href: '/llms-full.txt', title: 'llms-full.txt' }],
|
||||||
|
],
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [utf8TextPlugin],
|
plugins: [utf8TextPlugin],
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
253
CONTRIBUTING.md
253
CONTRIBUTING.md
@@ -25,30 +25,47 @@ docs/
|
|||||||
├── index.md # Лендинг (URL `/`)
|
├── index.md # Лендинг (URL `/`)
|
||||||
└── docs/ # Контент документации (URL `/docs/...`)
|
└── docs/ # Контент документации (URL `/docs/...`)
|
||||||
├── index.md # Главная страница
|
├── index.md # Главная страница
|
||||||
├── workflow.md
|
├── workflow.md # Подсказки
|
||||||
├── workflow/ # Процессы разработки
|
├── basics/ # Базовые правила: каким должен быть код
|
||||||
├── basics/ # Базовые правила
|
|
||||||
│ ├── tech-stack.md
|
│ ├── tech-stack.md
|
||||||
│ ├── architecture/
|
│ ├── architecture/
|
||||||
│ ├── code-style.md
|
│ ├── code-style.md
|
||||||
│ ├── naming.md
|
│ ├── naming.md
|
||||||
│ ├── documentation.md
|
│ ├── documentation.md
|
||||||
│ └── typing.md
|
│ └── typing.md
|
||||||
└── applied/ # Прикладные разделы
|
├── creating-project/ # Создание проекта: как поднять новый проект
|
||||||
├── vscode.md
|
│ ├── from-template.md
|
||||||
|
│ ├── manual.md
|
||||||
|
│ └── nextjs.md
|
||||||
|
├── data/ # Работа с данными
|
||||||
|
│ ├── index.md
|
||||||
|
│ ├── realtime.md
|
||||||
|
│ └── rest/
|
||||||
|
└── applied/ # Прикладные разделы: настройка и использование
|
||||||
├── project-structure.md
|
├── project-structure.md
|
||||||
├── components.md
|
|
||||||
├── page-level.md
|
├── page-level.md
|
||||||
├── templates-generation.md
|
├── component.md
|
||||||
├── styles.md
|
├── module.md
|
||||||
├── images-sprites.md
|
├── styles/ # Стили: настройка + использование
|
||||||
├── svg-sprites.md
|
│ ├── styles-setup.md
|
||||||
├── video.md
|
│ └── styles-usage.md
|
||||||
├── api.md
|
├── svg-sprites/ # SVG-спрайты: введение + настройка + использование
|
||||||
├── stores.md
|
│ ├── svg-sprites-intro.md
|
||||||
├── hooks.md
|
│ ├── svg-sprites-setup.md
|
||||||
|
│ └── svg-sprites-usage.md
|
||||||
|
├── images.md
|
||||||
├── fonts.md
|
├── fonts.md
|
||||||
└── localization.md
|
├── aliases.md
|
||||||
|
├── templates/ # Шаблоны генерации: введение + настройка + использование
|
||||||
|
│ ├── templates-intro.md
|
||||||
|
│ ├── templates-setup.md
|
||||||
|
│ ├── templates-create.md
|
||||||
|
│ └── templates-usage.md
|
||||||
|
├── biome.md
|
||||||
|
├── postcss.md
|
||||||
|
├── vscode.md
|
||||||
|
├── localization.md
|
||||||
|
└── stores.md
|
||||||
.vitepress/
|
.vitepress/
|
||||||
└── config.ts # Конфигурация VitePress, сайдбар
|
└── config.ts # Конфигурация VitePress, сайдбар
|
||||||
generate-llms.ts # Скрипт генерации llms.txt и README
|
generate-llms.ts # Скрипт генерации llms.txt и README
|
||||||
@@ -59,41 +76,78 @@ generate-llms.ts # Скрипт генерации llms.txt и R
|
|||||||
|
|
||||||
### Добавление нового раздела
|
### Добавление нового раздела
|
||||||
|
|
||||||
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/` или `docs/docs/applied/`).
|
1. Создать `.md`-файл в нужной папке: `basics/`, `creating-project/`,
|
||||||
|
или `applied/`.
|
||||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||||
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
||||||
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
||||||
|
|
||||||
## Два типа документации
|
## Типы разделов
|
||||||
|
|
||||||
### Базовые правила
|
Документация разделена на четыре группы. Каждая отвечает на свой вопрос
|
||||||
|
и имеет свою природу — это влияет на содержимое и структуру страницы.
|
||||||
|
|
||||||
|
### Базовые правила (`basics/`)
|
||||||
|
|
||||||
**Отвечает на вопрос:** «Каким должен быть любой код?»
|
**Отвечает на вопрос:** «Каким должен быть любой код?»
|
||||||
|
|
||||||
Универсальные стандарты, **не привязанные к конкретной области**.
|
Универсальные стандарты, **не привязанные к конкретной области**.
|
||||||
Правило базовое, если оно применимо ко всему коду одинаково: именование переменных, оформление импортов, когда использовать `type` vs `interface`.
|
Правило базовое, если оно применимо ко всему коду одинаково: именование
|
||||||
|
переменных, оформление импортов, когда использовать `type` vs `interface`.
|
||||||
|
|
||||||
Примеры в базовых правилах допускаются, но служат иллюстрацией принципа, а не инструкцией по конкретной области.
|
Примеры в базовых правилах допускаются, но служат иллюстрацией принципа,
|
||||||
|
а не инструкцией по конкретной области.
|
||||||
|
|
||||||
**Граница:** если правило касается только одной области (только стили, только компоненты, только API) — оно живёт в прикладном разделе, не в базовых.
|
**Граница:** если правило касается только одной области (только стили,
|
||||||
|
только компоненты, только API) — оно живёт в прикладном разделе, не в базовых.
|
||||||
|
|
||||||
### Прикладные разделы
|
### Создание проекта (`creating-project/`)
|
||||||
|
|
||||||
**Отвечает на вопрос:** «Как работать с X?»
|
**Отвечает на вопрос:** «Как поднять новый проект?»
|
||||||
|
|
||||||
Полное описание конкретной области: структура файлов, правила, именование, типизация, примеры.
|
Сценарии запуска нового проекта целиком: из шаблона, вручную, чистая
|
||||||
|
установка фреймворка. Раздел описывает порядок шагов на уровне всего
|
||||||
|
проекта; детали отдельных инструментов лежат в `applied/`.
|
||||||
|
|
||||||
**Граница:** прикладной раздел не дублирует базовые правила.
|
**Граница:** не дублирует разделы `applied/`. Ссылается на них как на
|
||||||
Если правило уже описано в базовых — прикладной раздел ссылается на него, но не повторяет.
|
шаги в общем сценарии.
|
||||||
|
|
||||||
|
### Прикладные разделы (`applied/`)
|
||||||
|
|
||||||
|
**Отвечает на вопрос:** «Как поставить инструмент и как им пользоваться?»
|
||||||
|
|
||||||
|
Прикладные разделы объединяют настройку и использование инструментов
|
||||||
|
и подсистем. Каждый раздел — самостоятельная предметная область.
|
||||||
|
|
||||||
|
Разделы делятся на два типа:
|
||||||
|
|
||||||
|
1. **Только настройка** — разовая установка инструмента (линтер,
|
||||||
|
CSS-процессор, алиасы). Файл без суффикса: `biome.md`, `postcss.md`.
|
||||||
|
|
||||||
|
2. **Настройка + использование** — область, требующая и установки,
|
||||||
|
и повседневных правил. Два файла с суффиксами: `styles-setup.md`
|
||||||
|
(настройка) и `styles-usage.md` (использование). В сайдбаре
|
||||||
|
оборачиваются в collapsed-группу.
|
||||||
|
|
||||||
|
**Граница:** прикладной раздел не дублирует базовые правила. Если правило
|
||||||
|
уже описано в `basics/` — прикладной раздел ссылается на него, но не
|
||||||
|
повторяет.
|
||||||
|
|
||||||
## Структура прикладного раздела
|
## Структура прикладного раздела
|
||||||
|
|
||||||
Шаблон ниже описывает все допустимые секции. Раздел включает только те секции, которые для него релевантны — пустые секции не создаются.
|
Шаблон ниже относится к usage-страницам прикладных разделов (`applied/*-usage.md`).
|
||||||
|
Setup-страницы (`applied/*-setup.md`) и `creating-project/` имеют другую
|
||||||
|
структуру — ориентированную на пошаговую установку (требования → установка →
|
||||||
|
проверка).
|
||||||
|
|
||||||
|
Шаблон описывает все допустимые секции. Раздел включает только те,
|
||||||
|
которые для него релевантны — пустые секции не создаются.
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# {Название}
|
# {Название}
|
||||||
|
|
||||||
Краткое описание: о чём раздел и какие аспекты работы с областью он охватывает.
|
{Одно-два предложения, по которым читатель за секунду решает, нужен ли ему раздел.
|
||||||
|
Правила оформления — секция «Заголовок и описание» ниже.}
|
||||||
|
|
||||||
## Что нужно знать
|
## Что нужно знать
|
||||||
|
|
||||||
@@ -173,63 +227,152 @@ generate-llms.ts # Скрипт генерации llms.txt и R
|
|||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
title: Название раздела
|
title: Название раздела
|
||||||
|
description: Описание раздела одним предложением.
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
Значение `title` совпадает с текстом `h1`-заголовка в файле.
|
- `title` совпадает с текстом `h1`-заголовка в файле.
|
||||||
|
- `description` совпадает с абзацем-описанием сразу под `h1`.
|
||||||
|
|
||||||
### Заголовки
|
Подробнее о требованиях к самому заголовку и описанию — секция
|
||||||
|
«Заголовок и описание» ниже.
|
||||||
|
|
||||||
- Один `h1` на файл — совпадает с `title` из frontmatter.
|
### Заголовок и описание
|
||||||
- Сразу после `h1` — вводный абзац (одно-два предложения).
|
|
||||||
|
Каждая страница начинается с `h1`-заголовка и абзаца-описания сразу под ним.
|
||||||
|
Эта пара — **навигационный маркер**: попадает в сайдбар, `llms.txt`,
|
||||||
|
`MAP.md` архива и должна за секунду давать читателю или LLM понять,
|
||||||
|
**когда сюда нужно идти**.
|
||||||
|
|
||||||
|
#### Структура заголовков
|
||||||
|
|
||||||
|
- Один `h1` на файл, совпадает с `title` во frontmatter.
|
||||||
|
- Сразу после `h1` — описание (одно-два предложения, см. ниже).
|
||||||
- Основные секции — `h2`.
|
- Основные секции — `h2`.
|
||||||
- Подсекции внутри `h2` — `h3`.
|
- Подсекции внутри `h2` — `h3`.
|
||||||
- `h4` не используется.
|
- `h4` не используется.
|
||||||
|
|
||||||
### Вводный абзац
|
#### Имя `h1` (заголовок страницы)
|
||||||
|
|
||||||
Абзац сразу после `h1` отвечает на вопрос «о чём этот раздел?».
|
- Называет предметную область, а не реализацию.
|
||||||
Он попадает в `llms.txt` и `README.md` архива как краткое описание,
|
- Без имён пакетов, опций, конфигов и путей.
|
||||||
поэтому должен быть плотным и без воды.
|
- Самодостаточен — читается без контекста сайдбара.
|
||||||
|
- Исключение: имя инструмента допустимо, если оно — единственное
|
||||||
|
устойчивое имя самой области (`PostCSS`, `Biome`, `VS Code`).
|
||||||
|
- Если страница вложена в семантическую группу
|
||||||
|
(`Архитектура → Слои`, `Данные → REST → Серверные компоненты`)
|
||||||
|
и короткое имя теряет смысл при прямой ссылке — `h1` поднимает
|
||||||
|
имя родителя в заголовок: `Слои SLM`, `Сегменты SLM`. В сайдбаре
|
||||||
|
допустимо оставить короткий вариант (`Слои`, `Сегменты`) — там
|
||||||
|
путь группы виден через дерево.
|
||||||
|
- Подъём в заголовок применяется только когда читается грамматически
|
||||||
|
естественно (`Слои SLM`, `Автогенерация REST-клиента`). Если
|
||||||
|
получается натянутая конструкция (`REST в серверных компонентах`) —
|
||||||
|
заголовок остаётся коротким, а контекст полностью переносится
|
||||||
|
в описание. Заголовок и описание — пара: если один не несёт
|
||||||
|
контекст, его обязательно несёт второй.
|
||||||
|
|
||||||
**Правила:**
|
**Хорошо:** «Алиасы импортов», «Структура проекта», «SVG-спрайты»,
|
||||||
|
«Слои SLM», «Автогенерация REST-клиента».
|
||||||
|
|
||||||
- Не начинать с «Раздел описывает», «Этот раздел», «В этом разделе»,
|
**Плохо:** «Установка и настройка» (что устанавливаем?),
|
||||||
«Здесь рассмотрено», «В этом документе».
|
«Использование» (что используем?), «Введение» (во что?),
|
||||||
- Начинать с подлежащего — самой темы (`Слои SLM:`, `Соглашения об именовании:`).
|
«Сегменты» (чего сегменты?), «REST в серверных компонентах»
|
||||||
- Двоеточие или тире для перечисления **категорий и областей**, а не
|
(грамматически натянуто — лучше короткий h1 + контекст в описании).
|
||||||
конкретных значений из содержимого.
|
|
||||||
- Не дублировать содержимое: если внутри раздела 12 правил —
|
|
||||||
не перечислять их во вводном абзаце.
|
|
||||||
- Не аргументировать («единые правила делают код предсказуемым»).
|
|
||||||
- 1–2 предложения.
|
|
||||||
|
|
||||||
**Проверка:** если при добавлении нового правила/инструмента/раздела
|
#### Описание
|
||||||
вводный абзац придётся править — он слишком конкретный.
|
|
||||||
|
Описание — короткий ответ на вопрос «у меня задача X, мне сюда?».
|
||||||
|
Не аннотация. Не оглавление.
|
||||||
|
|
||||||
|
**Запреты:**
|
||||||
|
|
||||||
|
- Не упоминать конкретные пакеты, библиотеки, инструменты
|
||||||
|
(`@gromlab/svg-sprites`, `Mantine`, `Zustand`).
|
||||||
|
- Не упоминать конкретные файлы и пути
|
||||||
|
(`postcss.config.mjs`, `.templates/`, `biome.json`).
|
||||||
|
- Не упоминать конкретные опции, ключи API, имена функций
|
||||||
|
(`baseUrl`, `cl()`, `useStore`).
|
||||||
|
- Не начинать с «Раздел описывает», «Этот раздел»,
|
||||||
|
«В этом разделе», «Здесь рассмотрено».
|
||||||
|
- Не использовать дежурные префиксы как шаблон
|
||||||
|
(«Правила работы с...», «Правила написания...») — само то,
|
||||||
|
что раздел про правила, и так понятно из секции и заголовка.
|
||||||
|
- Не пересказывать содержимое перечислением подсекций
|
||||||
|
(«организация, реализация, делегирование, метаданные»).
|
||||||
|
- Не аргументировать пользу
|
||||||
|
(«обеспечит единообразие», «упростит поддержку»).
|
||||||
|
|
||||||
|
**Требования:**
|
||||||
|
|
||||||
|
- 1 предложение, обычно 5–12 слов.
|
||||||
|
- Звучит как ответ человека другу, а не как техспек.
|
||||||
|
- Описание читается **самостоятельно**, без контекста сайдбара.
|
||||||
|
- Если страница вложена в семантическую группу
|
||||||
|
(например, `Данные → REST → Клиенты → ...`) и её заголовок
|
||||||
|
без этой группы теряет смысл — описание явно содержит имя
|
||||||
|
родительской области, чтобы читалось без сайдбара.
|
||||||
|
|
||||||
|
**Подходящие формы:**
|
||||||
|
|
||||||
|
- «Как X.»
|
||||||
|
- «Что такое X.»
|
||||||
|
- «Из чего состоит X.»
|
||||||
|
- «Установка X.»
|
||||||
|
- «Какие X есть и как ими пользоваться.»
|
||||||
|
|
||||||
|
Перечисление аспектов через двоеточие — только если без него читатель
|
||||||
|
не сможет различить раздел от соседнего.
|
||||||
|
|
||||||
|
**Тест навигации.** Читатель видит описание — за секунду должен понять
|
||||||
|
«мне сюда» или «нет, не сюда». Если приходится перечитывать —
|
||||||
|
описание слишком длинное.
|
||||||
|
|
||||||
|
**Тест на изменение.** Если в разделе сменится пакет, переименуется
|
||||||
|
файл или добавится правило — придётся ли править описание?
|
||||||
|
Если да — оно слишком конкретное.
|
||||||
|
|
||||||
**Хорошо:**
|
**Хорошо:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
Слои SLM: назначение, классификация, направление зависимостей, правила.
|
Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||||
```
|
```
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
Базовый стек проекта по областям: UI, архитектура, данные, состояние,
|
Установка и настройка линтера-форматтера в новом проекте.
|
||||||
локализация, тестирование, стили, генерация кода.
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Из чего состоит проект и где что лежит.
|
||||||
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Получение REST-данных в серверных компонентах.
|
||||||
```
|
```
|
||||||
|
|
||||||
**Плохо:**
|
**Плохо:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
Раздел описывает слои SLM: что такое слой, какие бывают, как между
|
Раздел описывает, какие алиасы используются в проекте: их полный список,
|
||||||
ними направлены зависимости и какие правила действуют на каждом.
|
где они объявлены и как ими пользоваться между модулями и внутри модуля.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
_Начинается с «Раздел описывает», пересказывает содержимое._
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
Этот раздел описывает базовый стек технологий и библиотек, принятый в
|
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов,
|
||||||
проекте. React, TypeScript, Next.js, SWR, Zustand, i18next.
|
конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
_Упомянут конкретный файл, перечисление аспектов превратилось в оглавление._
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Правила работы с React-компонентами: файловая структура,
|
||||||
|
типизация пропсов, документирование, реализация.
|
||||||
|
```
|
||||||
|
|
||||||
|
_Дежурный префикс «Правила работы с...» плюс оглавление подсекций._
|
||||||
|
|
||||||
### Примеры кода
|
### Примеры кода
|
||||||
|
|
||||||
- Блоки кода с указанием языка: ` ```tsx `, ` ```css `, ` ```bash `, ` ```text `.
|
- Блоки кода с указанием языка: ` ```tsx `, ` ```css `, ` ```bash `, ` ```text `.
|
||||||
|
|||||||
42
Caddyfile
42
Caddyfile
@@ -1,10 +1,44 @@
|
|||||||
:8080 {
|
:8080 {
|
||||||
root * /srv
|
root * /srv
|
||||||
|
|
||||||
# Кириллица в .txt и .md ломается без явного charset
|
# Устаревшие пути llms.txt в подпапках → корень.
|
||||||
@text path *.txt *.md
|
# Без этого опечатка `/docs/llms.txt` уходит в SPA-фолбэк и
|
||||||
header @text Content-Type "text/plain; charset=utf-8"
|
# отдаёт HTML под видом text/plain — агент верит, что получил llms.txt.
|
||||||
|
redir /docs/llms.txt /llms.txt 301
|
||||||
|
redir /docs/llms-full.txt /llms-full.txt 301
|
||||||
|
|
||||||
|
# Чистые URL: запросы вида `/docs/foo.html` редиректим на `/docs/foo`.
|
||||||
|
# Канон сайта — без `.html` (cleanUrls в VitePress).
|
||||||
|
# Не трогаем index.html в корне — он не имеет смысловой пары без расширения.
|
||||||
|
@legacyHtml {
|
||||||
|
path_regexp legacyHtml ^(/.+)\.html$
|
||||||
|
not path /index.html
|
||||||
|
}
|
||||||
|
redir @legacyHtml {re.legacyHtml.1} 301
|
||||||
|
|
||||||
|
# Подсказка агентам, где лежит карта документации (RFC 8288).
|
||||||
|
# Позволяет найти llms.txt без парсинга DOM — по HTTP-заголовку.
|
||||||
|
header Link "</llms.txt>; rel=\"llms\""
|
||||||
|
|
||||||
|
# Кириллица в .txt/.md ломается без явного charset.
|
||||||
|
# Применяем заголовок только к РЕАЛЬНО существующим файлам,
|
||||||
|
# иначе SPA-фолбэк (HTML) уезжает с Content-Type: text/plain.
|
||||||
|
@existingText {
|
||||||
|
path *.txt *.md
|
||||||
|
file
|
||||||
|
}
|
||||||
|
header @existingText Content-Type "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
# Несуществующие .txt/.md → 404, не HTML-фолбэк.
|
||||||
|
# Это критично для llms.txt: агент должен получить честный 404,
|
||||||
|
# а не валидный «как бы текст» с лендингом внутри.
|
||||||
|
@missingText {
|
||||||
|
path *.txt *.md
|
||||||
|
not file
|
||||||
|
}
|
||||||
|
respond @missingText 404
|
||||||
|
|
||||||
file_server
|
file_server
|
||||||
try_files {path} /index.html
|
# cleanUrls: пробуем точное совпадение → +.html → каталог → SPA-фолбэк.
|
||||||
|
try_files {path} {path}.html {path}/ /index.html
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# Ассистент
|
|
||||||
|
|
||||||
## Для ассистента
|
|
||||||
|
|
||||||
- Всегда используй Русский язык для общения и генерации документации/комментариев/коммитов.
|
|
||||||
- Всегда следуй этим правилам при генерации кода и ответах.
|
|
||||||
- Всегда пиши план действий перед генерацией кода.
|
|
||||||
- Всегда спрашивай разрешения у пользователя перед генерацией кода.
|
|
||||||
- Всегда проверяй, что код соответствует линтингу и форматированию.
|
|
||||||
- Всегда сверяйся с чек-листом при генерации кода.
|
|
||||||
- Не предлагай решения, которые противоречат этим правилам этого файла.
|
|
||||||
- Если не уверен — уточни у пользователя, не гадай, не придумывай.
|
|
||||||
|
|
||||||
## Обязательность чек-листов
|
|
||||||
|
|
||||||
- Все чек-листы, приведённые в правилах, обязательны к исполнению.
|
|
||||||
- Ассистент обязан сверяться с чек-листом при выполнении любой задачи, связанной с кодом.
|
|
||||||
- Нельзя сокращать, игнорировать или опускать пункты чек-листа — каждый пункт должен быть выполнен или явно отмечен как невыполнимый с объяснением причины.
|
|
||||||
- В каждом ответе, связанном с генерацией или изменением кода, ассистент обязан ссылаться на соответствующий чек-лист и подтверждать его выполнение.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
title: Stores
|
|
||||||
---
|
|
||||||
|
|
||||||
# Stores
|
|
||||||
|
|
||||||
## Сторы (Stores)
|
|
||||||
|
|
||||||
> В этом разделе собраны основные правила и рекомендации по созданию и оформлению сторов. Следуйте этим принципам, чтобы обеспечить чистую архитектуру, удобство поддержки и единый стиль работы с состоянием в проекте.
|
|
||||||
> В проекте для организации состояния используется только библиотека Zustand.
|
|
||||||
|
|
||||||
### Структура
|
|
||||||
- Store размещается в файле `<store-name>.store.ts` в папке `stores/` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- Интерфейс состояния описывается в этом же файле с суффиксом `State` (PascalCase).
|
|
||||||
- Для каждого store создаётся отдельный хук доступа (например, `useTodoStore`).
|
|
||||||
- Для глобальных сторов используйте только `shared/store`.
|
|
||||||
|
|
||||||
### Именование
|
|
||||||
- Соблюдайте [правила именования файлов и папок](#правила-именования-файлов-и-папок):
|
|
||||||
- Файл store — `<store-name>.store.ts` (kebab-case).
|
|
||||||
- Имя интерфейса состояния — PascalCase с суффиксом `State`.
|
|
||||||
- Имя хука — camelCase с префиксом `use`.
|
|
||||||
|
|
||||||
### Требования
|
|
||||||
- В store допускается только хранение состояния и методы управления им, без бизнес-логики, асинхронных операций и side-effects (см. раздел "Правила организации и использовалья Store").
|
|
||||||
- Для методов, изменяющих состояние через set, если используется функция — тело функции в фигурных скобках, return с новой строки после стрелки.
|
|
||||||
- Не дублируйте логику между сторами.
|
|
||||||
|
|
||||||
### Типизация
|
|
||||||
- Всегда указывайте типы для всех полей состояния и методов.
|
|
||||||
- Не используйте неявное приведение типов и не полагайтесь на автоматический вывод, если это может снизить читаемость или безопасность.
|
|
||||||
|
|
||||||
### Документирование
|
|
||||||
- Документируйте только назначение store и смысл полей, строго по [правилам документирования кода](#правило-для-документирования-кода).
|
|
||||||
|
|
||||||
### Экспорт
|
|
||||||
- Экспортируйте хук доступа к store и интерфейс состояния через `index.ts` слоя/компонента.
|
|
||||||
|
|
||||||
### Примеры
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { create } from 'zustand';
|
|
||||||
import { TodoItem } from './types/todo-item.interface';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Состояние хранилища задач.
|
|
||||||
*/
|
|
||||||
export interface TodoStoreState {
|
|
||||||
/** Массив задач. */
|
|
||||||
items: TodoItem[];
|
|
||||||
/** Добавить задачу. */
|
|
||||||
addTodo: (item: TodoItem) => void;
|
|
||||||
/** Удалить задачу. */
|
|
||||||
removeTodo: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Хук для доступа к хранилищу задач.
|
|
||||||
*/
|
|
||||||
export const useTodoStore = create<TodoStoreState>((set) => ({
|
|
||||||
items: [],
|
|
||||||
addTodo: (item) => set((state) => {
|
|
||||||
return {
|
|
||||||
items: [...state.items, item],
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
removeTodo: (id) => set((state) => {
|
|
||||||
return {
|
|
||||||
items: state.items.filter((t) => t.id !== id),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Чек-лист
|
|
||||||
|
|
||||||
- [ ] Store размещён в `stores/<store-name>.store.ts` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- [ ] Именование файлов и сущностей соответствует [правилам именования файлов и папок](#правила-именования-файлов-и-папок).
|
|
||||||
- [ ] Все поля и методы строго типизированы (см. [общие правила типизации](#общие-правила-типизации)).
|
|
||||||
- [ ] В store только состояние и методы управления им, без бизнес-логики и side-effects.
|
|
||||||
- [ ] Для методов, изменяющих состояние через set, используется функция с return с новой строки.
|
|
||||||
- [ ] Документировано только назначение store и смысл полей (см. [правила документирования кода](#правило-для-документирования-кода)).
|
|
||||||
- [ ] Нет неиспользуемого или невалидного кода.
|
|
||||||
- [ ] Экспорт через индексный файл.
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
---
|
|
||||||
title: CSS
|
|
||||||
---
|
|
||||||
|
|
||||||
# CSS
|
|
||||||
|
|
||||||
## Правила оформления и стилизации CSS-кода
|
|
||||||
|
|
||||||
- **Препроцессоры**
|
|
||||||
Используй PostCSS и модули для стилизации.
|
|
||||||
|
|
||||||
- **Архитектура написания стилей**
|
|
||||||
Используй подход **Mobile First**
|
|
||||||
Используй CSS переменные для стилизации.
|
|
||||||
Используй Custom Media Queries для адаптивных стилей.
|
|
||||||
Используй BEM для именования классов.
|
|
||||||
Между каждым CSS-правилом (селектором) должен быть один пустой сброс строки, пример:
|
|
||||||
```css
|
|
||||||
.todo-list {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: var(--space-3);
|
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-list__text {
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Запрещено писать правила подряд без пустой строки:
|
|
||||||
```css
|
|
||||||
/* Так делать нельзя! */
|
|
||||||
.todo-list { ... }
|
|
||||||
.todo-list__text { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Методология именования классов**
|
|
||||||
Использовать методологию **BEM** для именования классов.
|
|
||||||
- Блок: kebab-case, пример `.user-bar { }`
|
|
||||||
- Елемент: kebab-case, соеденный с блоком двойным нижним подчеркиванием, пример `.user-bar__slide { }`
|
|
||||||
- Модификатор: kebab-case, отдельный самостоятельный класс, **не соединяется** с блоком/елементом, имя модификатора всегда начинается с нижнего подчеркивания, пример: `._red { }`
|
|
||||||
|
|
||||||
- **Единицы измерения**
|
|
||||||
Используй `px` как основная единица измирения, так-же допускается использовать остальные единицы измерения если того требует реализуемый дизайн.
|
|
||||||
|
|
||||||
- **Импорт стилей**
|
|
||||||
Стили компонента должны импортироваться только внутри соответствующего компонента.
|
|
||||||
Запрещено импортировать стили одного компонента в другой.
|
|
||||||
Запрещено импортировать `css переменные` в файлы стилей, они доступны глобально.
|
|
||||||
Запрещено импортировать `custom media` в файлы стилей, они доступны глобально.
|
|
||||||
|
|
||||||
- **Переменные**
|
|
||||||
Все значения переменных нужно писать в `/shared/styles` или в Mantine ThemeProvider.
|
|
||||||
Все что не является цветами, брекпоинтами, отступами, скруглением допускаются использоваться в компонентах.
|
|
||||||
Обязательное создавай CSS перменные для:
|
|
||||||
- "Цветов", пример: `--color-danger: red;`.
|
|
||||||
- "Брекпоинты", описываем в (Сustom media) пример: `@custom-media --md (min-width: 62em);`.
|
|
||||||
- "Отспупы (--space)", , пример: `--space-1: 4px;`, `--space-2: 8px;`, `--space-3: 12px;` итд..
|
|
||||||
- "Скругление углов (--radius)", пример: `--radius-1: 4px;`,`--radius-2: 8px;`,`--radius-3: 12px;` итд..
|
|
||||||
|
|
||||||
- **Вложенность селекторов**
|
|
||||||
Запрещено использовать вложенность селекторов.
|
|
||||||
Разрешено использовать вложенность только для:
|
|
||||||
- Псевдо-классов `:hover`, `:active` итд..
|
|
||||||
- Псевдо-елементов `::before`, `::after`
|
|
||||||
- Медиа запросов `@media`
|
|
||||||
- Классы **модификаторы** по методологии BEM
|
|
||||||
Каждый вложенный селектор отделяется 1 пустой строкой.
|
|
||||||
|
|
||||||
- **Медиа запросы**
|
|
||||||
Строго запрещено использовать `@media` без вложения в селектор.
|
|
||||||
Строго запрещено использовать в теле `@media` любые селекторы.
|
|
||||||
Разрешено использовать только Custom Media Queries (например, `@media (--md) {}`).
|
|
||||||
Запрещено использовать любые произвольные значения breakpoints (например, max-width: 768px).
|
|
||||||
**Пример как правильно писать @media**
|
|
||||||
```css
|
|
||||||
.todo-list {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 24px;
|
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-list__text {
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
**Пример как неправильно писать @media**
|
|
||||||
```css
|
|
||||||
// Медиа запрос не вложен в селектор
|
|
||||||
@media (--md) {
|
|
||||||
.todo-list {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.todo-list__text {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Используется стандартный `min-width: 992px` вмето Custom Media Queries
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
// Внутри @media запроса используются селекторы
|
|
||||||
.todo-list {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.todo-list__text {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Глобальные стили и сбросы**
|
|
||||||
Все глобальные стили (например, сбросы) должны располагаться в отдельном файле, например, `src/app/styles/global.css`.
|
|
||||||
|
|
||||||
- **Использование Mantine и PostCSS**
|
|
||||||
Для стандартных визуальных компонентов (кнопки, инпуты, layout, grid, notifications и т.д.) использовать только Mantine и его ThemeProvider.
|
|
||||||
Запрещено использовать в Mantine компонентах его props/styling, вмето этого нужно добавлять кастомные стили PostCSS.
|
|
||||||
Кастомные стили допускаются только в случае, если требуемый дизайн невозможно реализовать средствами Mantine.
|
|
||||||
При написании кастомных стилей стараться использовать переменные и токены Mantine, если это возможно.
|
|
||||||
|
|
||||||
- **Порядок CSS-свойств**
|
|
||||||
В стилях рекомендуется придерживаться логического порядка свойств:
|
|
||||||
1. Позиционирование (position, top, left, z-index и т.д.)
|
|
||||||
2. Блочная модель (display, width, height, margin, padding и т.д.)
|
|
||||||
3. Оформление (background, border, box-shadow и т.д.)
|
|
||||||
4. Текст (font, color, text-align и т.д.)
|
|
||||||
5. Прочее (transition, animation и т.д.)
|
|
||||||
|
|
||||||
- **Комментарии**
|
|
||||||
В стилях запрещено использовать комментарии.
|
|
||||||
|
|
||||||
- **Дублирования**
|
|
||||||
Не дублировать стили между компонентами. Общие стили выносить в shared/styles или использовать переменные.
|
|
||||||
|
|
||||||
- **Примеры кода стилей**
|
|
||||||
Пример как хорошо:
|
|
||||||
```css
|
|
||||||
/* Блок BEM */
|
|
||||||
.user-bar {
|
|
||||||
display: none;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
/* Медиа запрос custom media и отделяется 1 пустой строкой */
|
|
||||||
@media (--md) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Елемент BEM отделяется 1 пустой строкой*/
|
|
||||||
.user-bar__button-next {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
|
|
||||||
/* Псевдо-класс отделяется 1 пустой строкой*/
|
|
||||||
&:hover {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Модификатор BEM отделяется 1 пустой строкой*/
|
|
||||||
&._blue {
|
|
||||||
background-color: #2b2bbe;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Модификатор BEM отделяется 1 пустой строкой*/
|
|
||||||
&._green {
|
|
||||||
background-color: #29c53d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Пример как плохо писать:
|
|
||||||
```css
|
|
||||||
.user-bar {
|
|
||||||
display: none;
|
|
||||||
color: black;
|
|
||||||
&__button {
|
|
||||||
&_next {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
&:hover {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
&._blue {
|
|
||||||
background-color: #2b2bbe;
|
|
||||||
}
|
|
||||||
&._green {
|
|
||||||
background-color: #29c53d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.user-bar {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Чек лист для проверки стилизации.**
|
|
||||||
- [ ] Используется PostCSS и CSS-модули для стилизации.
|
|
||||||
- [ ] Применён подход Mobile First.
|
|
||||||
- [ ] Именование классов строго по BEM:
|
|
||||||
- [ ] Модификатор — отдельный класс, начинается с нижнего подчёркивания (например, `._red`, `._active`)
|
|
||||||
- [ ] Все CSS-переменные (цвета, брейкпоинты, отступы, скругления) определены только в `/shared/styles` или через Mantine ThemeProvider.
|
|
||||||
- [ ] Для медиа-запросов используются только custom media переменные из `/shared/styles/media.css`.
|
|
||||||
- [ ] Соблюдается правила вложености селекторов.
|
|
||||||
- [ ] Соблюдается правила отступов селекторов.
|
|
||||||
- [ ] Глобальные стили (reset) вынесены в отдельный файл, остальные стили — модульные.
|
|
||||||
- [ ] Для стандартных UI-элементов используются только компоненты Mantine, кастомные стили — только при необходимости.
|
|
||||||
- [ ] В Mantine-компонентах не используются props/styling для стилизации, только PostCSS.
|
|
||||||
- [ ] Кастомные стили используют переменные и токены Mantine, если это возможно.
|
|
||||||
- [ ] В стилях нет комментариев.
|
|
||||||
- [ ] Стили компонента импортируются только внутри соответствующего компонента.
|
|
||||||
- [ ] Нет импорта стилей одного компонента в другой.
|
|
||||||
- [ ] Нет импорта файлов переменных и custom media — они доступны глобально.
|
|
||||||
- [ ] Нет дублирования стилей между компонентами, общие стили вынесены в shared/styles или используются переменные.
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
---
|
|
||||||
title: Компоненты
|
|
||||||
---
|
|
||||||
|
|
||||||
# Компоненты
|
|
||||||
|
|
||||||
## Правила создания и работы с компонентами.
|
|
||||||
|
|
||||||
### 1. Структура компонента
|
|
||||||
Ассистент при создании/рефакторинге компонента должен **строго** придерживаться следующей структуры файлов и папок:
|
|
||||||
|
|
||||||
```
|
|
||||||
component-name/
|
|
||||||
index.ts
|
|
||||||
component-name.tsx
|
|
||||||
styles/
|
|
||||||
component-name.module.css
|
|
||||||
locales/
|
|
||||||
ru.json
|
|
||||||
en.json
|
|
||||||
types/
|
|
||||||
component-name.interface.ts
|
|
||||||
component-name.type.ts
|
|
||||||
component-name.enum.ts
|
|
||||||
schemas/
|
|
||||||
schema-name.schema.ts
|
|
||||||
utils/
|
|
||||||
util-name.util.ts
|
|
||||||
hooks/
|
|
||||||
use-hook-name.hook.ts
|
|
||||||
stores/
|
|
||||||
store-name.store.ts
|
|
||||||
ui/
|
|
||||||
... # вложенные компоненты для component-name
|
|
||||||
```
|
|
||||||
|
|
||||||
Пояснения к структуре компонента:
|
|
||||||
**Обязательные файлы** обязательны для всех компонентов, даже если они пустые.
|
|
||||||
- component-name/: Папка компонента корень для всего компонента.
|
|
||||||
- index.ts: экспортирует главный компонент, интерфейс и всё, что может быть переиспользовано.
|
|
||||||
- component-name.tsx: главный компонент.
|
|
||||||
- styles/component-name.module.css: стили компонента.
|
|
||||||
- locales/ru.json: локализация на русском языке.
|
|
||||||
- locales/en.json: локализация на английском языке.
|
|
||||||
- types/component-name.interface.ts: интерфейс пропсов компонента.
|
|
||||||
**Не обязательные файлы** добавляются только при необходимости
|
|
||||||
- types/component-name.type.ts: типы компонента.
|
|
||||||
- types/component-name.enum.ts: enum компонента.
|
|
||||||
- schemas/schema-name.schema.ts: схемы валидации.
|
|
||||||
- utils/util-name.util.ts: утилиты компонента.
|
|
||||||
- hooks/use-hook-name.hook.ts: хуки компонента.
|
|
||||||
- stores/store-name.store.ts: хранилища состояния компонента.
|
|
||||||
- ui/: Папка для вложенных компонентов.
|
|
||||||
|
|
||||||
### Требования к компоненту
|
|
||||||
- Использовать `memo()` для всех компонентов, которые принимают пропсы.
|
|
||||||
- Использовать `useMemo` для всех вычислений, которые передаются в пропсы других компонентов.
|
|
||||||
- Использовать `useCallback` для всех функций/методов, которые передаются в пропсы других компонентов.
|
|
||||||
|
|
||||||
### Требования к вложенным компонентам
|
|
||||||
- Вложенный компонент — это полноценный компонент, который обязан полностью соблюдать все правила, описанные для компонентов (структура, именование, документация, типизация, стилизация и т.д.).
|
|
||||||
- Все вложенные компоненты размещаются только в папке ui/ основного компонента.
|
|
||||||
|
|
||||||
**Пояснение**
|
|
||||||
Нет необходимости повторять структуру и требования — вложенный компонент подчиняется тем же правилам, что и любой другой компонент, только располагается в папке ui/ родительского компонента.
|
|
||||||
|
|
||||||
### Требования к локализации
|
|
||||||
- Все добавленные локализации обязательно подключать в экземпляр `app/i18n` (чтобы новые namespace были доступны для i18next).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Чек-лист для создания нового компонента
|
|
||||||
- [ ] Главный компонент размещён в корне и назван по правилу PascalCase.
|
|
||||||
- [ ] Создан файл стилей в папке `styles/`, имя в kebab-case, используется BEM.
|
|
||||||
- [ ] Все классы применяются через `className={styles['component-name']}`.
|
|
||||||
- [ ] Создана папка `locales/` с файлами `ru.json` и `en.json`.
|
|
||||||
- [ ] Создан файл интерфейса пропсов в папке `types/`, даже если интерфейс пустой.
|
|
||||||
- [ ] Создан файл `index.ts` с экспортом главного компонента и интерфейса.
|
|
||||||
- [ ] Внутренние компоненты (если есть) размещены в папке `ui/`.
|
|
||||||
- [ ] Все важные части кода документированы по TSDoc (см. раздел 16).
|
|
||||||
- [ ] Остальные файлы (schemas, дополнительные типы, enum) добавлены только при необходимости.
|
|
||||||
- [ ] Именование файлов и папок соответствует правилам (см. выше).
|
|
||||||
- [ ] Нет неиспользуемого или невалидного кода.
|
|
||||||
- [ ] Для компонентов с пропсами используется `React.memo`.
|
|
||||||
- [ ] Для вычислений, передаваемых в пропсы, используется `useMemo`.
|
|
||||||
- [ ] Для функций, передаваемых в пропсы, используется `useCallback`.
|
|
||||||
- [ ] Все тексты вынесены в локализационные файлы и используются через i18n.
|
|
||||||
- [ ] Новые namespace подключены в экземпляр i18n.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Хуки (React Hooks)
|
|
||||||
|
|
||||||
> В проекте для создания пользовательских хуков используется только React (функциональные компоненты и хуки).
|
|
||||||
> В этом разделе собраны основные правила и рекомендации по созданию и оформлению хуков. Следуйте этим принципам, чтобы обеспечить чистую архитектуру, переиспользуемость и единый стиль работы с хуками в проекте.
|
|
||||||
|
|
||||||
## Рекомендации по использованию сторонних хуков
|
|
||||||
- Если есть возможность, используйте хуки Mantine в компонентах и кастомных хуках для работы с состоянием, темизацией, медиа-запросами и другими возможностями библиотеки.
|
|
||||||
- Не дублируйте функциональность, уже реализованную в Mantine.
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
- Каждый хук размещается в отдельном файле с именем `use-<hook-name>.hook.ts` в папке `hooks/` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- Имя хука — в стиле camelCase с префиксом `use` (например, `useTodoFilter`).
|
|
||||||
- Для сложных возвращаемых структур использовать отдельные типы или интерфейсы, размещая их в папке `types/` на своём уровне абстракции.
|
|
||||||
|
|
||||||
## Именование
|
|
||||||
- Соблюдайте [правила именования файлов и папок](#правила-именования-файлов-и-папок):
|
|
||||||
- Файл хука — `use-<hook-name>.hook.ts` (kebab-case).
|
|
||||||
- Имя хука — camelCase с префиксом `use`.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
- Хук должен быть строго типизирован: все параметры, возвращаемые значения и внутренние переменные должны иметь явные типы.
|
|
||||||
- Не хранить бизнес-логику, связанную с несколькими слоями — хук должен быть изолирован в рамках своего слоя/feature.
|
|
||||||
- Не дублировать логику между хуками — общие части выносить в shared.
|
|
||||||
- Не использовать side-effects вне useEffect/useLayoutEffect.
|
|
||||||
- Для мемоизации возвращаемых значений и функций использовать useMemo и useCallback.
|
|
||||||
- Не использовать устаревшие или неразрешённые паттерны React.
|
|
||||||
|
|
||||||
## Типизация
|
|
||||||
- Всегда явно указывать типы для всех параметров, возвращаемых значений и состояния внутри хука.
|
|
||||||
- Не используйте неявное приведение типов и не полагайтесь на автоматический вывод, если это может снизить читаемость или безопасность.
|
|
||||||
|
|
||||||
## Документирование
|
|
||||||
- Документируйте только назначение хука (описание), строго по [правилам документирования кода](#правило-для-документирования-кода).
|
|
||||||
|
|
||||||
## Экспорт
|
|
||||||
- Экспортируйте хук только именованным экспортом через `index.ts` слоя/компонента.
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { TodoItem } from '../types/todo-item.interface';
|
|
||||||
import { TodoStatus } from '../types/todo-status.enum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Хук фильтрации задач по статусу.
|
|
||||||
*/
|
|
||||||
export const useTodoFilter = (items: TodoItem[], filter: TodoStatus): TodoItem[] => {
|
|
||||||
return useMemo(() => {
|
|
||||||
if (filter === TodoStatus.ALL) return items;
|
|
||||||
if (filter === TodoStatus.ACTIVE) return items.filter((t) => !t.completed);
|
|
||||||
return items.filter((t) => t.completed);
|
|
||||||
}, [items, filter]);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Чек-лист
|
|
||||||
|
|
||||||
- [ ] Хук размещён в файле `use-<hook-name>.hook.ts` в папке `hooks/` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- [ ] Именование файлов и сущностей соответствует [правилам именования файлов и папок](#правила-именования-файлов-и-папок).
|
|
||||||
- [ ] Все параметры, возвращаемые значения и внутренние переменные строго типизированы.
|
|
||||||
- [ ] Вся бизнес-логика изолирована в рамках слоя/feature.
|
|
||||||
- [ ] Нет дублирования логики между хуками.
|
|
||||||
- [ ] Для мемоизации используется useMemo/useCallback.
|
|
||||||
- [ ] Не используются side-effects вне useEffect/useLayoutEffect.
|
|
||||||
- [ ] Документировано только назначение хука (см. [правила документирования кода](#правило-для-документирования-кода)).
|
|
||||||
- [ ] Нет неиспользуемого или невалидного кода.
|
|
||||||
- [ ] Экспорт только именованный через индексный файл.
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# Хуки API (React Hooks)
|
|
||||||
|
|
||||||
> В проекте для работы с API-хуками используется только React и библиотека SWR для получения данных (GET-запросы).
|
|
||||||
> В этом разделе собраны основные правила и рекомендации по созданию и оформлению хуков для работы с API. Следуйте этим принципам, чтобы обеспечить чистую архитектуру, переиспользуемость и единый стиль работы с API-хуками в проекте.
|
|
||||||
|
|
||||||
## Описание и назначение API-хуков
|
|
||||||
|
|
||||||
API-хуки предназначены для получения данных с сервера (GET-запросы) и используются в компонентах или других хуках.
|
|
||||||
В проекте для этого применяется библиотека SWR, которая обеспечивает кэширование, автоматическое обновление и удобную работу с асинхронными запросами.
|
|
||||||
|
|
||||||
**Fetcher** — это функция, которую использует SWR для выполнения запроса к API. В проекте fetcher обычно экспортируется из файла клиента (например, `backendFetcher` из `shared/api/backend/client.ts`) и инкапсулирует логику обращения к конкретному API-клиенту.
|
|
||||||
|
|
||||||
**API-клиент** — это отдельный модуль (папка), отвечающий за взаимодействие с конкретным внешним или внутренним API.
|
|
||||||
API-клиент включает:
|
|
||||||
- инициализацию экземпляра HTTP-клиента (например, Axios),
|
|
||||||
- настройку базового URL, интерцепторов и общих обработчиков ошибок,
|
|
||||||
- организацию всех сущностей и методов для работы с этим API (например, users, auth, orders и т.д.),
|
|
||||||
- экспорт всех функций, типов и fetcher через индексные файлы.
|
|
||||||
|
|
||||||
Каждый API-клиент размещается в папке `src/shared/api/<client-name>/` и имеет собственную структуру согласно архитектуре проекта.
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
- Каждый API-хук размещается в отдельном файле с именем `use-<method-name>.hook-api.ts` в папке `hooks/api/<client-name>/` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- Имя хука — в стиле camelCase с префиксом `use` (например, `useGetUser`).
|
|
||||||
- Для сложных возвращаемых структур используйте отдельные типы или интерфейсы, размещая их в папке `types/` на своём уровне абстракции.
|
|
||||||
|
|
||||||
## Именование
|
|
||||||
- Соблюдайте [правила именования файлов и папок](#правила-именования-файлов-и-папок):
|
|
||||||
- Файл хука — `use-<method-name>.hook-api.ts` (kebab-case).
|
|
||||||
- Имя хука — camelCase с префиксом `use`.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
- Хук должен быть строго типизирован: все параметры, возвращаемые значения и внутренние переменные должны иметь явные типы.
|
|
||||||
- Для получения данных используйте только SWR.
|
|
||||||
- Не дублируйте логику между хуками — общие части выносите в shared.
|
|
||||||
- Не используйте side-effects вне useEffect/useLayoutEffect.
|
|
||||||
|
|
||||||
## Типизация
|
|
||||||
- Всегда явно указывайте типы для всех параметров, возвращаемых значений и состояния внутри хука.
|
|
||||||
- Придерживайтесь [общих правил типизации проекта](#общие-правила-типизации).
|
|
||||||
- Не используйте неявное приведение типов и не полагайтесь на автоматический вывод, если это может снизить читаемость или безопасность.
|
|
||||||
|
|
||||||
## Документирование
|
|
||||||
- Документируйте только назначение хука (описание), строго по [правилам документирования кода](#правило-для-документирования-кода).
|
|
||||||
|
|
||||||
## Экспорт
|
|
||||||
- Экспортируйте хук только именованным экспортом через `index.ts` слоя/компонента.
|
|
||||||
|
|
||||||
## Пример API хука
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// use-get-me.hook-api.ts
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { backendFetcher } from 'shared/api/backend/client';
|
|
||||||
import { UserDto } from 'shared/api/backend/entities/users/get-me.api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Хук для получения информации о текущем пользователе.
|
|
||||||
*/
|
|
||||||
export const useGetMe = () => {
|
|
||||||
const { data, error, isLoading } = useSWR<UserDto>('/users/me', backendFetcher);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример использования хука в компоненте
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import React from 'react';
|
|
||||||
import { useGetMe } from 'shared/hooks/api/backend/use-get-me.hook-api';
|
|
||||||
|
|
||||||
export const UserInfo: React.FC = () => {
|
|
||||||
const { data, error, isLoading } = useGetMe();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Загрузка...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div>Ошибка загрузки данных</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <div>Нет данных о пользователе</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>Имя: {data.name}</div>
|
|
||||||
<div>Email: {data.email}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Чек-лист для создания API-хука
|
|
||||||
|
|
||||||
- [ ] Для каждого GET-запроса создан отдельный хук.
|
|
||||||
- [ ] Хук размещён в `hooks/api/<client-name>/use-<method-name>.hook-api.ts` на своём уровне абстракции согласно архитектуре проекта.
|
|
||||||
- [ ] Именование файлов и сущностей соответствует [правилам именования файлов и папок](#правила-именования-файлов-и-папок).
|
|
||||||
- [ ] Используется SWR для получения данных.
|
|
||||||
- [ ] Все параметры, возвращаемые значения и внутренние переменные строго типизированы.
|
|
||||||
- [ ] Нет дублирования логики между хуками.
|
|
||||||
- [ ] Не используются side-effects вне useEffect/useLayoutEffect.
|
|
||||||
- [ ] Документировано только назначение хука (см. [правила документирования кода](#правило-для-документирования-кода)).
|
|
||||||
- [ ] Нет неиспользуемого или невалидного кода.
|
|
||||||
- [ ] Экспорт только именованный через индексный файл.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Чек-лист для использования API-хука
|
|
||||||
|
|
||||||
- [ ] Импортируется только нужный хук через публичные экспорты (`index.ts`).
|
|
||||||
- [ ] Использование хука строго по назначению (только для получения данных).
|
|
||||||
- [ ] Если требуется получить данные через GET-запрос в компоненте — обязательно используется соответствующий API-хук.
|
|
||||||
**Запрещено вызывать GET-методы API напрямую в компонентах, только через хуки.**
|
|
||||||
- [ ] Обработка состояний загрузки, ошибки и данных реализована корректно.
|
|
||||||
- [ ] Не происходит дублирования логики, связанной с получением данных.
|
|
||||||
- [ ] Нет неиспользуемого или невалидного кода.
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
# API
|
|
||||||
|
|
||||||
> В этом разделе собраны основные правила и рекомендации по созданию, оформлению и использованию API-клиентов и функций для работы с сервером. Следуйте этим принципам, чтобы обеспечить единый стиль, безопасность и удобство поддержки API-слоя в проекте.
|
|
||||||
|
|
||||||
## Описание и назначение API-клиента
|
|
||||||
|
|
||||||
API-клиент — это модуль (папка), отвечающий за взаимодействие с конкретным внешним или внутренним API.
|
|
||||||
В проекте для HTTP-запросов используется только Axios.
|
|
||||||
API-клиент инкапсулирует:
|
|
||||||
- инициализацию экземпляра Axios,
|
|
||||||
- настройку базового URL, интерцепторов, обработчиков ошибок,
|
|
||||||
- организацию всех сущностей и методов для работы с этим API (например, users, auth, orders и т.д.),
|
|
||||||
- экспорт всех функций, типов и fetcher через индексные файлы.
|
|
||||||
|
|
||||||
Каждый API-клиент размещается в папке `src/shared/api/<client-name>/` и имеет собственную структуру согласно архитектуре проекта.
|
|
||||||
|
|
||||||
|
|
||||||
## Использование методов API
|
|
||||||
|
|
||||||
- Все методы API должны использоваться строго внутри блока `try...catch`.
|
|
||||||
- При вызове методов API всегда используйте полный путь, например:
|
|
||||||
`await api.backend.createUser({ email, password });`
|
|
||||||
- Запрещено вызывать методы API вне блока `try...catch` даже в тестах, утилитах и других вспомогательных функциях.
|
|
||||||
|
|
||||||
|
|
||||||
## Структура клиента
|
|
||||||
```text
|
|
||||||
src/shared/api/backend/
|
|
||||||
│
|
|
||||||
├── client.ts
|
|
||||||
├── index.ts
|
|
||||||
└── entities/
|
|
||||||
├── users/
|
|
||||||
│ ├── get-me.api.ts
|
|
||||||
│ ├── create-user.api.ts
|
|
||||||
│ ├── update-user.api.ts
|
|
||||||
│ └── index.ts
|
|
||||||
├── auth/
|
|
||||||
│ ├── login.api.ts
|
|
||||||
│ ├── register.api.ts
|
|
||||||
│ └── index.ts
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Описание ключевых элементов
|
|
||||||
|
|
||||||
- **client.ts**
|
|
||||||
Экземпляр Axios с настройками, интерцепторами, экспортом fetcher для SWR.
|
|
||||||
|
|
||||||
- **index.ts**
|
|
||||||
Главная точка экспорта: экспортирует client, fetcher, все сущности и их методы.
|
|
||||||
|
|
||||||
- **entities/**
|
|
||||||
Папка для бизнес-сущностей (например, users, auth, orders и т.д.).
|
|
||||||
|
|
||||||
- **`<entity>/`**
|
|
||||||
Папка для отдельной сущности. Имя — в kebab-case, отражает бизнес-область (например, users, auth).
|
|
||||||
|
|
||||||
- **`<operation>.api.ts`**
|
|
||||||
Файл для каждой операции (CRUD, спец. действия).
|
|
||||||
Внутри:
|
|
||||||
- DTO (интерфейсы запроса/ответа)
|
|
||||||
- Функция, реализующая запрос через client
|
|
||||||
|
|
||||||
- **index.ts (внутри `<entity>`/)**
|
|
||||||
Экспортирует все методы и типы этой сущности.
|
|
||||||
|
|
||||||
- **index.ts (внутри entities/)**
|
|
||||||
Экспортирует все сущности (users, auth и т.д.).
|
|
||||||
|
|
||||||
|
|
||||||
## Именование
|
|
||||||
|
|
||||||
- Соблюдайте [правила именования файлов и папок](#правила-именования-файлов-и-папок):
|
|
||||||
- Файл клиента — `client.ts`.
|
|
||||||
- Файл функции — `<operation>-<entity>.api.ts` (например, `create-user.api.ts`).
|
|
||||||
- DTO — в папке `dto/` (например, `create-user.dto.ts`).
|
|
||||||
- Все функции и типы экспортируются через индексные файлы на каждом уровне (сущность, entities, клиент).
|
|
||||||
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- Для каждого действия (CRUD, спец. действия) — отдельная функция и файл.
|
|
||||||
- Все функции используют общий экземпляр Axios из `client.ts`.
|
|
||||||
- Все функции строго типизированы (используются DTO).
|
|
||||||
- DTO объявляется в отдельном файле в папке `dto/` перед функцией, которая его использует.
|
|
||||||
- Для каждого GET метода обязательно должен быть создан API-хук.
|
|
||||||
- Все API-хуки должны создаваться строго по [документации раздела "Хуки для API"](#хуки-для-api-api-hooks).
|
|
||||||
|
|
||||||
|
|
||||||
## Типизация
|
|
||||||
|
|
||||||
- Все функции и DTO строго типизированы.
|
|
||||||
- Все интерфейсы, типы и enum размещены в папке `types/` на своём уровне абстракции.
|
|
||||||
- Все DTO размещены в папке `dto/` на своём уровне абстракции.
|
|
||||||
- Придерживайтесь [общих правил типизации проекта](#общие-правила-типизации).
|
|
||||||
|
|
||||||
|
|
||||||
## Документирование
|
|
||||||
|
|
||||||
- Документируйте только назначение функций и DTO.
|
|
||||||
- В описании указывается только смысл функции/типа.
|
|
||||||
|
|
||||||
|
|
||||||
## Экспорт
|
|
||||||
|
|
||||||
- Все функции и типы экспортируются через индексные файлы на каждом уровне (сущность, entities, клиент).
|
|
||||||
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
### Пример клиента API
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// client.ts
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
|
||||||
export { AxiosError, isAxiosError } from 'axios';
|
|
||||||
export type { AxiosResponse } from 'axios';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Экземпляр HTTP-клиента для работы с backend API.
|
|
||||||
*/
|
|
||||||
export const backendHttpClient: AxiosInstance = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Интерцептор запроса
|
|
||||||
backendHttpClient.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
// Здесь можно добавить авторизационные заголовки или другую логику
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Интерцептор ответа
|
|
||||||
backendHttpClient.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
// Здесь можно обработать ошибки (например, показать уведомление)
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример DTO
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// dto/create-user.dto.ts
|
|
||||||
/**
|
|
||||||
* DTO для создания пользователя.
|
|
||||||
*/
|
|
||||||
export interface CreateUserDto {
|
|
||||||
/** Email пользователя. */
|
|
||||||
email: string;
|
|
||||||
/** Пароль пользователя. */
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример API-функции
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// create-user.api.ts
|
|
||||||
import { backendHttpClient } from '../client';
|
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать пользователя.
|
|
||||||
*/
|
|
||||||
export const createUser = (data: CreateUserDto) => backendHttpClient.post('/users', data);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример index.ts (в папке сущности)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export * from './create-user.api';
|
|
||||||
export * from './get-user.api';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример использования API-функции в компоненте
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { api } from 'shared/api';
|
|
||||||
|
|
||||||
export const CreateUserForm: React.FC = () => {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await api.backend.createUser({ email, password });
|
|
||||||
console.log('Пользователь создан!');
|
|
||||||
} catch {
|
|
||||||
console.log('Ошибка создания пользователя');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
placeholder="Email"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
placeholder="Пароль"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button type="submit">Создать пользователя</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Чек-лист для создания клиента
|
|
||||||
- [ ] Новый клиент размещён в `src/shared/api/<client-name>/`.
|
|
||||||
- [ ] В корне клиента есть client.ts (экземпляр Axios) и index.ts (главный экспорт).
|
|
||||||
- [ ] Все бизнес-сущности размещены в entities/, каждая — в отдельной папке.
|
|
||||||
- [ ] Для каждой операции создан отдельный файл `<operation>`.api.ts с DTO и функцией.
|
|
||||||
- [ ] DTO объявлен непосредственно перед функцией.
|
|
||||||
- [ ] В каждой папке сущности есть свой index.ts для экспорта методов и типов.
|
|
||||||
- [ ] В папке entities/ есть общий index.ts для экспорта всех сущностей.
|
|
||||||
- [ ] Все экспорты организованы через индексные файлы.
|
|
||||||
- [ ] Для каждого GET-метода создан отдельный SWR-хук (см. правила API-хуков).
|
|
||||||
- [ ] Нет дублирования кода и неиспользуемых файлов.
|
|
||||||
|
|
||||||
## Чек-лист для использования API
|
|
||||||
- [ ] Импортируется только нужный метод через публичные экспорты (index.ts).
|
|
||||||
- [ ] Все вызовы API обёрнуты в try...catch.
|
|
||||||
- [ ] Используются только строго типизированные методы.
|
|
||||||
- [ ] Не происходит обращения к Axios напрямую — только через client.
|
|
||||||
- [ ] Нет дублирования логики и неиспользуемого кода.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
title: Общие принципы
|
|
||||||
---
|
|
||||||
|
|
||||||
# Общие принципы
|
|
||||||
|
|
||||||
## Стек технологий и библиотеки
|
|
||||||
- Использовать **TypeScript** для всех файлов логики и компонентов.
|
|
||||||
- Использовать **FSD (Feature-Sliced Design)**: разделять код на features, entities, processes, widgets, shared.
|
|
||||||
- Использовать **React** (функциональные компоненты, хуки).
|
|
||||||
- Использовать **Mantine UI** для UI-компонентов.
|
|
||||||
- Использовать **Axios** в качестве клиента для работы с API.
|
|
||||||
- Использовать **SWR** для data fetching (GET-запросы).
|
|
||||||
- Использовать **Zustand** для глобального состояния.
|
|
||||||
- Использовать **i18n** для локализации.
|
|
||||||
- Использовать **Vitest** для тестирования.
|
|
||||||
- Использовать **PostCSS модули** для стилизации.
|
|
||||||
- Использовать **BEM** для именований классов в стилях
|
|
||||||
- Использовать **Mobile First** подход для написания стилей.
|
|
||||||
- Использовать **Context7** примеров использования библиотек.
|
|
||||||
- Использовать **i18n** (i18next) для локализации всех пользовательских текстов.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: Архитектура
|
|
||||||
---
|
|
||||||
|
|
||||||
# Архитектура
|
|
||||||
|
|
||||||
## Архитектура проекта
|
|
||||||
В проекте используется FSD (Feature-Sliced Design) архитектура.
|
|
||||||
|
|
||||||
- **FSD-границы**
|
|
||||||
- Не нарушать границы слоёв (например, feature не может импортировать из widgets).
|
|
||||||
- Бизнес-логика должна быть вынесена в хуки или сервисы.
|
|
||||||
- **Импорты**
|
|
||||||
- Внутри слоя — относительные импорты.
|
|
||||||
- Между слоями — абсолютные импорты.
|
|
||||||
- **Требования**
|
|
||||||
- Не смешивать логику разных слоёв.
|
|
||||||
- Не хранить бизнес-логику в UI-компонентах.
|
|
||||||
- **Именование**
|
|
||||||
- Файлы и папки kebab-case.
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
title: Стиль кода
|
|
||||||
---
|
|
||||||
|
|
||||||
# Стиль кода
|
|
||||||
|
|
||||||
## Отступы
|
|
||||||
|
|
||||||
Используем 2 пробела для отступов во всём проекте. Не используем табы.
|
|
||||||
|
|
||||||
|
|
||||||
## Кавычки
|
|
||||||
|
|
||||||
Используем **одинарные кавычки** для строк в JavaScript/TypeScript, и **двойные кавычки** для атрибутов в JSX/TSX.
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// JavaScript/TypeScript
|
|
||||||
const message = 'Привет, мир!';
|
|
||||||
const name = 'ProjectName';
|
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// JSX/TSX
|
|
||||||
<input type="text" placeholder="Введите имя" />
|
|
||||||
<button title="Сохранить">Сохранить</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Строгая типизация
|
|
||||||
|
|
||||||
всегда указывать типы для пропсов, возвращаемых значений, параметров функций.
|
|
||||||
|
|
||||||
## Ранние возвраты
|
|
||||||
|
|
||||||
(early return) для повышения читаемости.
|
|
||||||
|
|
||||||
## Мемоизация
|
|
||||||
|
|
||||||
Старайся оптимизировать код если это возможно.
|
|
||||||
|
|
||||||
## Документирование
|
|
||||||
|
|
||||||
Документируем ТОЛЬКО ОПИСАНИЕ (функций, компонентов, типов и их полей).
|
|
||||||
|
|
||||||
## any, unknown
|
|
||||||
|
|
||||||
запрещено использовать без крайней необходимости.
|
|
||||||
|
|
||||||
## Классы в TSX
|
|
||||||
|
|
||||||
Для стилизации компонентов используем CSS-модули и методологию BEM. Классы подключаются через объект стилей, импортированный из соответствующего `.module.css` файла.
|
|
||||||
|
|
||||||
> Объект стилей всегда импортируется с именем `s` (сокращённо от style), а не `styles`.
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import s from './my-component.module.css';
|
|
||||||
|
|
||||||
export const MyComponent = () => (
|
|
||||||
<div className={s['my-component']}>
|
|
||||||
<button className={s['my-component__button']}>Кнопка</button>
|
|
||||||
<span className={s['my-component__text']}>Текст</span>
|
|
||||||
<button className={s['my-component__button'] + ' ' + s._active}>
|
|
||||||
Активная кнопка
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
- Имя класса всегда берётся из объекта `s`.
|
|
||||||
- Для модификаторов используется отдельный класс с нижним подчёркиванием (например, `s._active`).
|
|
||||||
- Не используйте строковые литералы с классами напрямую — только через объект `s`.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
title: Именование
|
|
||||||
---
|
|
||||||
|
|
||||||
# Именование
|
|
||||||
|
|
||||||
## Именование файлов и папок
|
|
||||||
- Папка компонента: kebab-case, совпадает с названием компонента, пример: `component-name`.
|
|
||||||
- React-компонент: kebab-case, совпадает с названием компонента, пример: `component-name.tsx`.
|
|
||||||
- Стили: kebab-case, шаблон: `<style-name>.module.css`, пример: `style-name.module.css`.
|
|
||||||
- Интерфейсы: kebab-case, шаблон: `<interface-name>.interface.ts`, пример: `interface-name.interface.ts`.
|
|
||||||
- Типы: kebab-case, шаблон: `<type-name>.type.ts`, пример: `type-name.type.ts`.
|
|
||||||
- Enum: kebab-case, шаблон: `<enum-name>.enum.ts`, пример: `enum-name.enum.ts`.
|
|
||||||
- Схемы: kebab-case, шаблон: `<schema-name>.schema.ts`, пример: `schema-name.schema.ts`.
|
|
||||||
- Локализация: kebab-case, пример: `ru.json`, `en.json`.
|
|
||||||
- Утилиты: kebab-case, шаблон: `<util-name>.util.ts`, пример: `util-name.util.ts`
|
|
||||||
- React Hooks: kebab-case, шаблон: `use-<hook-name>.hook.ts`, пример: `use-hook-name.hook.ts`
|
|
||||||
- Хранилища состояния компонента: kebab-case, шаблон: `<store-name>.store.ts`, пример: `store-name.store.ts`
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
title: Документирование
|
|
||||||
---
|
|
||||||
|
|
||||||
# Документирование
|
|
||||||
|
|
||||||
## Правило для документирования кода
|
|
||||||
|
|
||||||
- Документировать разрешено только описание (назначение) функций, компонентов, типов, интерфейсов, enum и их полей.
|
|
||||||
- Строго запрещено документировать параметры, возвращаемые значения, типы пропсов, аргументы, возвращаемые значения функций, компоненты, хуки и т.д.
|
|
||||||
- В интерфейсах, типах и enum разрешено документировать только смысл (описание) каждого поля или значения.
|
|
||||||
- В React-компонентах, функциях, хранилищах, схемах, утилитах разрешено документировать только назначение (описание), без детализации параметров и возвращаемых значений.
|
|
||||||
- Описание должно быть кратким, информативным и реально помогать понять структуру и бизнес-логику.
|
|
||||||
- Не допускается избыточная или дублирующая очевидное документация.
|
|
||||||
- В конце описания всегда ставить точку.
|
|
||||||
|
|
||||||
**Примеры правильного документирования**
|
|
||||||
```tsx
|
|
||||||
/**
|
|
||||||
* Список задач пользователя.
|
|
||||||
*/
|
|
||||||
export const TodoList = memo(() => { ... });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Интерфейс задачи.
|
|
||||||
*/
|
|
||||||
export interface TodoItem {
|
|
||||||
/** Уникальный идентификатор задачи. */
|
|
||||||
id: string;
|
|
||||||
/** Текст задачи. */
|
|
||||||
text: string;
|
|
||||||
/** Статус выполнения задачи. */
|
|
||||||
completed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление фильтров задач.
|
|
||||||
*/
|
|
||||||
export enum TodoFilter {
|
|
||||||
/** Все задачи. */
|
|
||||||
All = 'all',
|
|
||||||
/** Только активные задачи. */
|
|
||||||
Active = 'active',
|
|
||||||
/** Только выполненные задачи. */
|
|
||||||
Completed = 'completed',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Примеры неправильного документирования**
|
|
||||||
```ts
|
|
||||||
// ❌ Не нужно:/
|
|
||||||
/**
|
|
||||||
* @param id - идентификатор задачи
|
|
||||||
* @returns объект задачи
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ❌ Не нужно:/
|
|
||||||
/**
|
|
||||||
* @param props - пропсы компонента
|
|
||||||
* @returns JSX.Element
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ❌ Не нужно:/
|
|
||||||
/**
|
|
||||||
* id — идентификатор задачи
|
|
||||||
* text — текст задачи
|
|
||||||
* completed — статус выполнения
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
---
|
|
||||||
title: Типизация
|
|
||||||
---
|
|
||||||
|
|
||||||
# Типизация
|
|
||||||
|
|
||||||
## Общие правила типизации
|
|
||||||
|
|
||||||
> Данный раздел определяет единые требования к типизации для всего проекта. Соблюдение этих правил обеспечивает читаемость, предсказуемость и безопасность кода.
|
|
||||||
|
|
||||||
- Использовать только строгую типизацию TypeScript для всех файлов логики, компонентов, хуков, API, сторов и утилит.
|
|
||||||
- Всегда явно указывать типы для:
|
|
||||||
- Пропсов компонентов
|
|
||||||
- Параметров функций и методов
|
|
||||||
- Возвращаемых значений функций и методов
|
|
||||||
- Всех переменных состояния (в том числе в store)
|
|
||||||
- Всех значимых переменных и констант, если их тип не очевиден из присваивания
|
|
||||||
- Не использовать `any` и `unknown` без крайней необходимости. Если использование неизбежно — обязательно добавить комментарий с обоснованием.
|
|
||||||
- Все интерфейсы, типы и enum всегда размещать в папке `types/` на своём уровне абстракции (например, `features/todo/types/`).
|
|
||||||
- Для DTO всегда использовать отдельную папку `dto/` на уровне сущности или слоя.
|
|
||||||
- Для сложных структур использовать отдельные интерфейсы или типы, размещая их в соответствующих файлах в папке `types/`.
|
|
||||||
- Для DTO, enum, схем и других сущностей — всегда создавать отдельные типы/интерфейсы с осмысленными именами.
|
|
||||||
- Ключи enum всегда писать ЗАГЛАВНЫМИ_БУКВАМИ (SCREAMING_SNAKE_CASE).
|
|
||||||
- Не использовать неявное приведение типов и не полагаться на автоматический вывод, если это может снизить читаемость или безопасность.
|
|
||||||
- Для массивов и объектов всегда указывать тип элементов/ключей.
|
|
||||||
- Для возвращаемых значений асинхронных функций всегда указывать тип Promise.
|
|
||||||
- Типизацию коллбеков и функций, передаваемых в пропсы, указывать инлайн, не выносить в отдельные типы.
|
|
||||||
- Для типизации внешних библиотек использовать официальные типы или создавать собственные декларации при необходимости.
|
|
||||||
- Не использовать устаревшие или не рекомендуемые паттерны типизации (например, `Function`, `Object`, `{}`).
|
|
||||||
---
|
|
||||||
|
|
||||||
### Примеры
|
|
||||||
|
|
||||||
#### Интерфейс и типы для сущностей (всегда в папке types/)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// features/todo/types/todo-item.interface.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Интерфейс задачи.
|
|
||||||
*/
|
|
||||||
export interface TodoItem {
|
|
||||||
/** Уникальный идентификатор задачи. */
|
|
||||||
id: string;
|
|
||||||
/** Текст задачи. */
|
|
||||||
text: string;
|
|
||||||
/** Статус выполнения задачи. */
|
|
||||||
completed: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация enum (всегда в папке types/)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// features/todo/types/todo-status.enum.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Перечисление статусов задачи.
|
|
||||||
*/
|
|
||||||
export enum TodoStatus {
|
|
||||||
/** Активная задача. */
|
|
||||||
ACTIVE = 'active',
|
|
||||||
/** Выполненная задача. */
|
|
||||||
COMPLETED = 'completed',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация пропсов компонента
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { FC, memo } from 'react';
|
|
||||||
import { TodoItem } from './types/todo-item.interface';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Список задач.
|
|
||||||
*/
|
|
||||||
export interface TodoListProps {
|
|
||||||
/** Массив задач. */
|
|
||||||
items: TodoItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TodoList: FC<TodoListProps> = memo(({ items }) => (
|
|
||||||
<ul>
|
|
||||||
{items.map((item) => (
|
|
||||||
<li key={item.id}>{item.text}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
));
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация функций и коллбеков (инлайн)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
/**
|
|
||||||
* Функция фильтрации задач.
|
|
||||||
*/
|
|
||||||
export const getCompletedTodos = (items: TodoItem[]): TodoItem[] => {
|
|
||||||
return items.filter((t) => t.completed);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Колбэк для обработки клика (инлайн).
|
|
||||||
*/
|
|
||||||
const handleClick = (id: string): void => {
|
|
||||||
console.log('Clicked:', id);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация асинхронных функций
|
|
||||||
|
|
||||||
```ts
|
|
||||||
/**
|
|
||||||
* Получить задачи с сервера.
|
|
||||||
*/
|
|
||||||
export const fetchTodos = async (): Promise<TodoItem[]> => {
|
|
||||||
const response = await fetch('/api/todos');
|
|
||||||
return response.json();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация состояния в store (интерфейс в types/)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// features/todo/types/todo-store.interface.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Состояние хранилища задач.
|
|
||||||
*/
|
|
||||||
export interface TodoStoreState {
|
|
||||||
/** Массив задач. */
|
|
||||||
items: TodoItem[];
|
|
||||||
/** Добавить задачу. */
|
|
||||||
addTodo: (item: TodoItem) => void;
|
|
||||||
/** Удалить задачу. */
|
|
||||||
removeTodo: (id: string) => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация DTO (всегда в папке dto/)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// features/todo/dto/create-todo.dto.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO для создания задачи.
|
|
||||||
*/
|
|
||||||
export interface CreateTodoDto {
|
|
||||||
/** Текст задачи. */
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// features/todo/dto/todo-response.dto.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO ответа сервера.
|
|
||||||
*/
|
|
||||||
export interface TodoResponseDto {
|
|
||||||
/** Созданная задача. */
|
|
||||||
todo: TodoItem;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Типизация внешних библиотек
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { AxiosResponse } from 'axios';
|
|
||||||
|
|
||||||
export const getData = async (): Promise<AxiosResponse<TodoItem[]>> => {
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
|
||||||
### Чек-лист проверки типизации
|
|
||||||
|
|
||||||
- [ ] Все пропсы компонентов явно типизированы через интерфейс или тип в папке `types/`.
|
|
||||||
- [ ] Все параметры и возвращаемые значения функций и методов явно типизированы.
|
|
||||||
- [ ] Все переменные состояния (в том числе в store) имеют явные типы.
|
|
||||||
- [ ] Все интерфейсы, типы и enum размещены в папке `types/` на своём уровне абстракции.
|
|
||||||
- [ ] Ключи всех enum написаны ЗАГЛАВНЫМИ_БУКВАМИ (SCREAMING_SNAKE_CASE).
|
|
||||||
- [ ] Все DTO размещены в папке `dto/` на своём уровне абстракции.
|
|
||||||
- [ ] Не используется `any` и `unknown` без крайней необходимости и поясняющего комментария.
|
|
||||||
- [ ] Для сложных структур используются отдельные интерфейсы или типы.
|
|
||||||
- [ ] Для массивов и объектов указан тип элементов/ключей.
|
|
||||||
- [ ] Для асинхронных функций указан тип Promise с конкретным типом результата.
|
|
||||||
- [ ] Типы коллбеков и функций, передаваемых в пропсы, указаны инлайн.
|
|
||||||
- [ ] Не используются устаревшие типы (`Function`, `Object`, `{}`).
|
|
||||||
- [ ] Для внешних библиотек используются официальные типы или собственные декларации.
|
|
||||||
- [ ] Нет неявного приведения типов, все типы читаемы и прозрачны.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
title: Локализация
|
|
||||||
---
|
|
||||||
|
|
||||||
# Локализация
|
|
||||||
|
|
||||||
## Правила использования локализации
|
|
||||||
|
|
||||||
- Все пользовательские тексты должны быть вынесены в локализационные файлы.
|
|
||||||
- Для каждого компонента создавать папку `locales/` с файлами `ru.json`, `en.json` и т.д.
|
|
||||||
- Новые namespace обязательно регистрировать в экземпляре i18n (см. `app/i18n.ts`).
|
|
||||||
- В коде использовать только функцию перевода из i18n, не использовать "жёстко" прописанные строки.
|
|
||||||
79
README.md
79
README.md
@@ -1,35 +1,25 @@
|
|||||||
# NextJS Style Guide
|
# NextJS Style Guide
|
||||||
|
|
||||||
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
|
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||||
|
|
||||||
|
Сайт: https://nextjs-style-guide.gromlab.ru
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
**Для AI-агентов:**
|
**Для AI-агентов:**
|
||||||
|
|
||||||
- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
|
- [llms.txt](https://nextjs-style-guide.gromlab.ru/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
|
||||||
- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом.
|
- [llms-full.txt](https://nextjs-style-guide.gromlab.ru/llms-full.txt) — Вся документация одним файлом.
|
||||||
|
|
||||||
**Для проекта:**
|
**Для проекта:**
|
||||||
|
|
||||||
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
- [nextjs-style-guide.zip](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
||||||
|
|
||||||
## Структура документации
|
## Структура документации
|
||||||
|
|
||||||
### Workflow
|
### Подсказки
|
||||||
|
|
||||||
**Что делать и в каком порядке** — пошаговые инструкции.
|
[Подсказки](docs/docs/workflow.md) — короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
|
|
||||||
| Раздел | Отвечает на вопрос |
|
|
||||||
|--------|-------------------|
|
|
||||||
| Начало работы | Что нужно знать перед началом разработки? |
|
|
||||||
| Создание проекта | Как начать новый проект? |
|
|
||||||
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
|
|
||||||
| Добавление страницы | Как добавить новую страницу в проект? |
|
|
||||||
| Добавление UI-модуля | Как создать компонент, бизнес-модуль, виджет или layout? |
|
|
||||||
| Стилизация | Как стилизовать компоненты в проекте? |
|
|
||||||
| Получение данных | Как получать данные с сервера? |
|
|
||||||
| Управление состоянием | Как работать с состоянием? |
|
|
||||||
| Локализация | Как добавлять переводы и подключать локализацию? |
|
|
||||||
|
|
||||||
### Базовые правила
|
### Базовые правила
|
||||||
|
|
||||||
@@ -38,32 +28,67 @@
|
|||||||
| Раздел | Отвечает на вопрос |
|
| Раздел | Отвечает на вопрос |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Технологии и библиотеки | Какой стек используем? |
|
| Технологии и библиотеки | Какой стек используем? |
|
||||||
| Архитектура | Как устроены слои SLM, зависимости, публичный API? |
|
|
||||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
|
||||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||||
|
| SLM Design | Что такое SLM и зачем она нужна? |
|
||||||
|
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
|
||||||
|
| Архитектура: Модули | Что такое модуль и как он устроен? |
|
||||||
|
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
|
||||||
|
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Прикладные разделы
|
### Создание проекта
|
||||||
|
|
||||||
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
|
**Как начать новый проект** — варианты установки и эталонный набор инструментов.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
|
||||||
|
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
|
||||||
|
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
|
||||||
|
|
||||||
|
### Настройка
|
||||||
|
|
||||||
|
**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Алиасы импортов | Как настроить алиасы импортов? |
|
||||||
|
| Biome | Как настроить линтер и форматтер? |
|
||||||
|
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
|
||||||
|
| Стили | Как подключить базовые стили и токены? |
|
||||||
|
| SVG-спрайты | Как подключить генерацию SVG-спрайтов? |
|
||||||
|
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
|
||||||
|
| VS Code | Как настроить редактор для проекта? |
|
||||||
|
|
||||||
|
### Использование
|
||||||
|
|
||||||
|
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
|
||||||
|
|
||||||
| Раздел | Отвечает на вопрос |
|
| Раздел | Отвечает на вопрос |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
|
||||||
| Структура проекта | Как организованы папки и файлы по SLM? |
|
| Структура проекта | Как организованы папки и файлы по SLM? |
|
||||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
| Файлы роутинга | Как описывать layout, page, loading, error, not-found? |
|
||||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
| Стили | Как писать CSS: вложенность, медиа, токены? |
|
||||||
|
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
|
||||||
| Изображения | _(не заполнен)_ |
|
| Изображения | _(не заполнен)_ |
|
||||||
| SVG-спрайты | _(не заполнен)_ |
|
|
||||||
| Видео | _(не заполнен)_ |
|
| Видео | _(не заполнен)_ |
|
||||||
| API | _(не заполнен)_ |
|
|
||||||
| Stores | _(не заполнен)_ |
|
| Stores | _(не заполнен)_ |
|
||||||
| Хуки | _(не заполнен)_ |
|
| Хуки | _(не заполнен)_ |
|
||||||
| Шрифты | _(не заполнен)_ |
|
| Шрифты | _(не заполнен)_ |
|
||||||
| Локализация | _(не заполнен)_ |
|
| Локализация | _(не заполнен)_ |
|
||||||
|
|
||||||
|
### Данные
|
||||||
|
|
||||||
|
**Как работать с источниками данных** — REST, realtime и потребление в компонентах.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Источники данных | Как устроена работа с данными в проекте? |
|
||||||
|
| REST: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
|
||||||
|
| REST: Ручное создание | Как написать REST-клиент вручную? |
|
||||||
|
| REST: Серверные компоненты | Как получать данные в серверных компонентах? |
|
||||||
|
| REST: Клиентские компоненты | Как получать данные в клиентских компонентах? |
|
||||||
|
| Realtime | Как работать с realtime-каналами и сокетами? |
|
||||||
|
|||||||
103
docs/docs/DEVELOP.md
Normal file
103
docs/docs/DEVELOP.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
title: Гид для агента
|
||||||
|
description: Что AI-агент обязан прочитать перед началом работы, а что — по задаче.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Обязательное чтение перед началом работы
|
||||||
|
|
||||||
|
Этот документ определяет **строгий порядок действий агента перед выполнением любых задач**.
|
||||||
|
|
||||||
|
## Общее правило
|
||||||
|
|
||||||
|
Перед началом работы над **любой задачей** агент **обязан ознакомиться с базовой документацией проекта**.
|
||||||
|
|
||||||
|
Нарушение этого порядка считается ошибкой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок обязательного чтения
|
||||||
|
|
||||||
|
Агент должен читать документацию **строго в следующем порядке**:
|
||||||
|
|
||||||
|
### 1. Архитектура (КРИТИЧЕСКИ ВАЖНО)
|
||||||
|
|
||||||
|
* [Архитектура: Обзор](./basics/architecture/index.md)
|
||||||
|
* [Архитектура: Слои](./basics/architecture/layers.md)
|
||||||
|
* [Архитектура: Модули](./basics/architecture/modules.md)
|
||||||
|
* [Архитектура: Сегменты](./basics/architecture/segments.md)
|
||||||
|
|
||||||
|
**Архитектура — это самое важное в проекте.**
|
||||||
|
|
||||||
|
Агент обязан:
|
||||||
|
|
||||||
|
* строго понимать архитектурный подход (SLM)
|
||||||
|
* соблюдать архитектуру **на 100% без отклонений**
|
||||||
|
* не предлагать решений, нарушающих архитектурные принципы
|
||||||
|
* не упрощать архитектуру даже ради скорости выполнения задачи
|
||||||
|
|
||||||
|
Любое нарушение архитектуры недопустимо.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Базовые правила
|
||||||
|
|
||||||
|
После архитектуры необходимо изучить:
|
||||||
|
|
||||||
|
* [Технологии и библиотеки](./basics/tech-stack.md)
|
||||||
|
* [Именование](./basics/naming.md)
|
||||||
|
* [Стиль кода](./basics/code-style.md)
|
||||||
|
* [Документирование](./basics/documentation.md)
|
||||||
|
* [Типизация](./basics/typing.md)
|
||||||
|
|
||||||
|
Агент обязан применять эти правила во всех решениях.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Использование карты документации
|
||||||
|
|
||||||
|
Для поиска дополнительных сведений агент должен использовать:
|
||||||
|
|
||||||
|
* [MAP.md](./MAP.md)
|
||||||
|
|
||||||
|
MAP.md содержит ссылки на все прикладные и вспомогательные разделы.
|
||||||
|
|
||||||
|
Агент может:
|
||||||
|
|
||||||
|
* переходить к нужным разделам через MAP.md
|
||||||
|
* уточнять детали реализации
|
||||||
|
* искать примеры и частные случаи
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
Агенту запрещено:
|
||||||
|
|
||||||
|
* начинать выполнение задачи без изучения архитектуры
|
||||||
|
* игнорировать базовые правила
|
||||||
|
* принимать решения, противоречащие архитектуре
|
||||||
|
* придумывать собственные подходы, если они не описаны в документации
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ожидаемое поведение агента
|
||||||
|
|
||||||
|
Перед выполнением задачи агент должен:
|
||||||
|
|
||||||
|
1. Изучить архитектуру
|
||||||
|
2. Изучить базовые правила
|
||||||
|
3. При необходимости открыть MAP.md и найти релевантные разделы
|
||||||
|
4. Только после этого приступать к решению задачи
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритеты
|
||||||
|
|
||||||
|
При принятии решений агент должен руководствоваться следующим приоритетом:
|
||||||
|
|
||||||
|
1. **Архитектура**
|
||||||
|
2. Базовые правила
|
||||||
|
3. Документация из MAP.md
|
||||||
|
4. Задача пользователя
|
||||||
|
|
||||||
|
Если задача противоречит архитектуре — задача должна быть переосмыслена, а не выполнена напрямую.
|
||||||
66
docs/docs/MAP.md
Normal file
66
docs/docs/MAP.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Карта документации
|
||||||
|
|
||||||
|
Список всех разделов архива с относительными ссылками. Точка входа
|
||||||
|
— `DEVELOP.md` рядом с этим файлом.
|
||||||
|
|
||||||
|
## Подсказки
|
||||||
|
|
||||||
|
- [Подсказки](./workflow.md) — Короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
|
|
||||||
|
## Базовые правила
|
||||||
|
|
||||||
|
- [Технологии и библиотеки](./basics/tech-stack.md) — Какие библиотеки и инструменты используются в проекте.
|
||||||
|
- [Именование](./basics/naming.md) — Как называть переменные, файлы и прочие сущности в коде.
|
||||||
|
- [Архитектура: Обзор](./basics/architecture/index.md) — Архитектурный подход проекта: что такое SLM и как он устроен.
|
||||||
|
- [Архитектура: Слои](./basics/architecture/layers.md) — Из каких слоёв состоит SLM-архитектура и как они связаны.
|
||||||
|
- [Архитектура: Модули](./basics/architecture/modules.md) — Что такое модуль в SLM-архитектуре и как он устроен.
|
||||||
|
- [Архитектура: Сегменты](./basics/architecture/segments.md) — Что такое сегмент модуля в SLM-архитектуре и какие они бывают.
|
||||||
|
- [Стиль кода](./basics/code-style.md) — Как оформляется код в проекте.
|
||||||
|
- [Документирование](./basics/documentation.md) — Что и как документировать в коде.
|
||||||
|
- [Типизация](./basics/typing.md) — Как типизируется код в проекте.
|
||||||
|
|
||||||
|
## Создание проекта
|
||||||
|
|
||||||
|
- [Из шаблона](./creating-project/from-template.md) — Создание нового проекта на основе готового шаблона.
|
||||||
|
- [По гайду вручную](./creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона.
|
||||||
|
- [Чистый Next.js](./creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
|
||||||
|
|
||||||
|
## Работа с данными
|
||||||
|
|
||||||
|
- [Введение](./data/index.md) — Какие источники данных используются в проекте и как с ними работать.
|
||||||
|
- [REST](./data/rest/index.md) — Как правильно работать с REST API в проекте.
|
||||||
|
- [REST: Создание клиента](./data/rest/clients/index.md) — Как выбрать способ создания REST-клиента и где размещать его части.
|
||||||
|
- [REST: Создание клиента: Автогенерация из OpenAPI](./data/rest/clients/auto.md) — Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
|
||||||
|
- [REST: Создание клиента: Ручное создание](./data/rest/clients/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||||
|
- [REST: Создание клиента: GET-хуки REST-клиента](./data/rest/clients/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||||
|
- [REST: Использование: Стратегии получения данных](./data/rest/strategies/index.md) — Как выбрать способ получения REST-данных в зависимости от места и сценария.
|
||||||
|
- [REST: Использование: Серверный await](./data/rest/strategies/server-await.md) — Получение REST-данных на сервере прямым await метода клиента.
|
||||||
|
- [REST: Использование: Параллельные серверные запросы](./data/rest/strategies/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall.
|
||||||
|
- [REST: Использование: Передача промиса ниже](./data/rest/strategies/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
|
||||||
|
- [REST: Использование: Начальные данные для клиентских хуков](./data/rest/strategies/client-hooks-initial-data.md) — Как передать серверный промис в SWR fallback, чтобы клиентские GET-хуки получили начальные данные.
|
||||||
|
- [REST: Использование: Клиентский GET-хук](./data/rest/strategies/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
|
||||||
|
- [REST: Использование: Business-композиция](./data/rest/strategies/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
|
||||||
|
- [Realtime](./data/realtime.md) — Работа с push-данными от сервера: подписки и события.
|
||||||
|
|
||||||
|
## Прикладные разделы
|
||||||
|
|
||||||
|
- [Структура проекта](./applied/project-structure.md) — Из чего состоит проект и где что лежит.
|
||||||
|
- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||||
|
- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей.
|
||||||
|
- [Модуль](./applied/module.md) — Как создавать и организовывать SLM-модули в проекте.
|
||||||
|
- [Стили: Настройка](./applied/styles/styles-setup.md) — Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
|
||||||
|
- [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте.
|
||||||
|
- [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают.
|
||||||
|
- [SVG-спрайты: Настройка](./applied/svg-sprites/svg-sprites-setup.md) — Подключение SVG-спрайтов в новом проекте.
|
||||||
|
- [SVG-спрайты: Использование](./applied/svg-sprites/svg-sprites-usage.md) — Как добавлять и использовать SVG-иконки в коде.
|
||||||
|
- [Изображения](./applied/images.md) — Как подключать изображения через Next.js Image в проекте.
|
||||||
|
- [Шрифты](./applied/fonts.md) — Как подключать шрифты через Next.js Font в проекте.
|
||||||
|
- [Алиасы импортов](./applied/aliases.md) — Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||||
|
- [Шаблоны генерации](./applied/templates/templates-intro.md) — Что такое шаблоны кодогенерации и какие проблемы они решают.
|
||||||
|
- [Шаблоны генерации: Настройка](./applied/templates/templates-setup.md) — Первичная установка шаблонов кодогенерации в проект.
|
||||||
|
- [Шаблоны генерации: Создание шаблонов](./applied/templates/templates-create.md) — Структура шаблонов, синтаксис переменных и примеры.
|
||||||
|
- [Шаблоны генерации: Использование](./applied/templates/templates-usage.md) — Генерация файлов из шаблонов через VS Code плагин и CLI.
|
||||||
|
- [Biome](./applied/biome.md) — Установка и настройка линтера-форматтера в новом проекте.
|
||||||
|
- [PostCSS](./applied/postcss.md) — Установка и настройка CSS-процессора в новом проекте.
|
||||||
|
- [VS Code](./applied/vscode.md) — Единые настройки редактора и расширений для команды.
|
||||||
|
- [Локализация](./applied/localization.md) — Как организовать локализацию как infrastructure-модуль.
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Алиасы
|
title: Алиасы импортов
|
||||||
|
description: Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||||
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared]
|
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Алиасы
|
# Алиасы импортов
|
||||||
|
|
||||||
Импорты в проекте идут через алиасы слоёв SLM-архитектуры — по одному на каждый слой `src/`. Префикс `@/` **не используется**: имя слоя само по себе однозначно адресует код.
|
Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||||
|
|
||||||
Слои и направление зависимостей — [Архитектура: слои](/docs/basics/architecture/reference/layers).
|
|
||||||
|
|
||||||
## Конфиг
|
## Конфиг
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app,
|
|||||||
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
|
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
|
||||||
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
|
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
|
||||||
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
|
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
|
||||||
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/reference/layers)).
|
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/layers)).
|
||||||
|
|
||||||
**Хорошо**
|
**Хорошо**
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Biome
|
title: Biome
|
||||||
|
description: Установка и настройка линтера-форматтера в новом проекте.
|
||||||
keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier]
|
keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Biome
|
# Biome
|
||||||
|
|
||||||
Единый линтер и форматтер для JS/TS/JSON в проекте. Заменяет связку ESLint + Prettier одним инструментом.
|
Установка и настройка линтера-форматтера в новом проекте.
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@@ -77,4 +78,4 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
|
|||||||
|
|
||||||
## Интеграция с VS Code
|
## Интеграция с VS Code
|
||||||
|
|
||||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/applied/vscode).
|
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](/docs/applied/vscode).
|
||||||
|
|||||||
165
docs/docs/applied/component.md
Normal file
165
docs/docs/applied/component.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
title: Компонент
|
||||||
|
description: Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Компонент
|
||||||
|
|
||||||
|
Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Архитектурное определение компонента описано в разделе [Модули → Компонент](/docs/basics/architecture/modules#компонент), а структура сегмента `ui/` — в разделе [Сегменты → ui/](/docs/basics/architecture/segments#сегмент-ui).
|
||||||
|
|
||||||
|
Эта страница не повторяет архитектурные ограничения. Она показывает, каким должен быть результат генерации компонента: структура папки, `.tsx`, типы, стили и локальный экспорт.
|
||||||
|
|
||||||
|
::: danger Компоненты не создаются вручную
|
||||||
|
Компоненты в проекте создаются только через кодогенератор: через [VS Code](/docs/applied/templates/templates-usage#через-vs-code) или [CLI](/docs/applied/templates/templates-usage#через-cli).
|
||||||
|
|
||||||
|
Ручное создание компонента запрещено. Это грубое нарушение правил работы в проекте для разработчика и AI-ассистента.
|
||||||
|
|
||||||
|
Если в проекте нет шаблона `.templates/component`, сначала создайте шаблон по разделу [Создание шаблонов](/docs/applied/templates/templates-create), и только потом генерируйте компонент на его основе.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Создание
|
||||||
|
|
||||||
|
1. Проверьте, что в проекте есть шаблон `.templates/component`.
|
||||||
|
2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create).
|
||||||
|
3. Сгенерируйте компонент через [VS Code или CLI](/docs/applied/templates/templates-usage).
|
||||||
|
|
||||||
|
Структура и код ниже показывают ожидаемый результат генерации. Их нельзя использовать как инструкцию для ручного создания файлов.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
Компонент размещается в `ui/{component-name}/` родительского модуля.
|
||||||
|
|
||||||
|
Для каждого компонента обязательны `.tsx`, типы, стили и локальный `index.ts`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
user-card/
|
||||||
|
└── ui/
|
||||||
|
└── user-status/
|
||||||
|
├── styles/
|
||||||
|
│ └── user-status.module.css
|
||||||
|
├── types/
|
||||||
|
│ └── user-status-props.type.ts
|
||||||
|
├── user-status.tsx
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Реализация
|
||||||
|
|
||||||
|
Пример ниже показывает файлы базового компонента.
|
||||||
|
|
||||||
|
### Типы
|
||||||
|
|
||||||
|
Файл типов делится на три части:
|
||||||
|
|
||||||
|
- `UserStatusParams` — собственные параметры компонента. Здесь лежат только данные, которые нужны именно этому компоненту.
|
||||||
|
- `RootAttrs` — параметры корневой обёртки: `div`, `span`, `a`, `button` или другого HTML-элемента. Если компонент сам управляет `children`, они исключаются через `Omit`.
|
||||||
|
- `UserStatusProps` — итоговые пропсы компонента. Тип объединяет собственные параметры и параметры корневой обёртки.
|
||||||
|
|
||||||
|
Собственные параметры и их поля документируются по правилам раздела [Документирование → Типы, интерфейсы, enum](/docs/basics/documentation#типы-интерфейсы-enum).
|
||||||
|
|
||||||
|
`user-card/ui/user-status/types/user-status-props.type.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ComponentPropsWithoutRef } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры UserStatus.
|
||||||
|
*/
|
||||||
|
export type UserStatusParams = {
|
||||||
|
/** Текст статуса пользователя. */
|
||||||
|
label: string
|
||||||
|
/** Доступен ли пользователь сейчас. */
|
||||||
|
isOnline: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Атрибуты корневого элемента без children. */
|
||||||
|
type RootAttrs = Omit<ComponentPropsWithoutRef<'span'>, 'children'>
|
||||||
|
|
||||||
|
export type UserStatusProps = RootAttrs & UserStatusParams
|
||||||
|
```
|
||||||
|
|
||||||
|
### TSX
|
||||||
|
|
||||||
|
В `.tsx` лежит только сам компонент:
|
||||||
|
|
||||||
|
- Компонент объявляется через `const` и именованный экспорт.
|
||||||
|
- `React.FC` не используется.
|
||||||
|
- Параметры компонента типизируются через `Props`.
|
||||||
|
- Возвращаемый тип не указывается: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
|
||||||
|
- JSDoc-комментарий обязателен и пишется по правилам раздела [Документирование → Компоненты](/docs/basics/documentation#компоненты).
|
||||||
|
- Пропсы деструктурируются в теле компонента, а не в сигнатуре.
|
||||||
|
- Из пропсов обязательно выделяются `className` и `...rootAttrs`.
|
||||||
|
- Функция конкатенации CSS-классов импортируется и именуется `cl`.
|
||||||
|
- Корневой CSS-класс всегда называется `.root`.
|
||||||
|
|
||||||
|
Комментарий описывает назначение и сценарии применения компонента, а не DOM-разметку или внутреннюю реализацию.
|
||||||
|
|
||||||
|
`className` — внешний CSS-класс, который родитель может передать компоненту. `rootAttrs` — остальные атрибуты корневой обёртки: `id`, `aria-*`, `data-*`, обработчики событий и другие HTML-атрибуты. Они прокидываются на корневой DOM-элемент компонента.
|
||||||
|
|
||||||
|
`.root` нужен, чтобы в DevTools быстро находить корневой DOM-узел компонента и одинаково подключать внешний `className` к реальному корню.
|
||||||
|
|
||||||
|
`user-card/ui/user-status/user-status.tsx`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import cl from 'clsx'
|
||||||
|
import type { UserStatusProps } from './types/user-status-props.type'
|
||||||
|
import styles from './styles/user-status.module.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Статус пользователя в карточке профиля.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - отображения текущей доступности пользователя
|
||||||
|
* - визуального выделения онлайн- и офлайн-состояний
|
||||||
|
*/
|
||||||
|
export const UserStatus = (props: UserStatusProps) => {
|
||||||
|
const { label, isOnline, className, ...rootAttrs } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...rootAttrs}
|
||||||
|
className={cl(styles.root, isOnline && styles.online, className)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Стили
|
||||||
|
|
||||||
|
`user-card/ui/user-status/styles/user-status.module.css`
|
||||||
|
|
||||||
|
```css
|
||||||
|
.root {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальный экспорт
|
||||||
|
|
||||||
|
`user-card/ui/user-status/index.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export { UserStatus } from './user-status'
|
||||||
|
export type { UserStatusProps } from './types/user-status-props.type'
|
||||||
|
```
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
title: Компоненты
|
|
||||||
---
|
|
||||||
|
|
||||||
# Компоненты
|
|
||||||
|
|
||||||
Правила написания React-компонентов: файловая структура модуля, типизация пропсов, документирование и реализация. Раздел охватывает компоненты всех слоёв — от `shared/ui` до `screens`.
|
|
||||||
|
|
||||||
Архитектурные слои и их назначение описаны в разделе [Архитектура](/docs/basics/architecture/).
|
|
||||||
|
|
||||||
|
|
||||||
## Правила организации
|
|
||||||
|
|
||||||
1. Один компонент — один файл.
|
|
||||||
2. Компонент не содержит бизнес-логики — логика и сайд-эффекты выносятся в хуки или сторы.
|
|
||||||
3. Дочерние компоненты размещаются в сегменте `ui/` и подчиняются тем же правилам структуры.
|
|
||||||
4. Публичный API модуля — только `index.ts`. Прямые импорты внутренних файлов запрещены.
|
|
||||||
|
|
||||||
## Базовая структура компонента
|
|
||||||
|
|
||||||
Минимальный набор файлов: компонент, стили, типы и публичный экспорт.
|
|
||||||
|
|
||||||
```text
|
|
||||||
container/
|
|
||||||
├── styles/
|
|
||||||
│ └── container.module.css
|
|
||||||
├── types/
|
|
||||||
│ └── container.type.ts
|
|
||||||
├── container.tsx
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Именования
|
|
||||||
|
|
||||||
- Имя корневого css класса всегда `.root`
|
|
||||||
- Тип пропсов именуется `{ComponentName}Props`.
|
|
||||||
- Тип пользовательских параметров именуется `{ComponentName}Params`.
|
|
||||||
|
|
||||||
## Типизация
|
|
||||||
|
|
||||||
Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений.
|
|
||||||
|
|
||||||
- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно.
|
|
||||||
- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра.
|
|
||||||
- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных.
|
|
||||||
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/docs/basics/typing).
|
|
||||||
|
|
||||||
## Реализация
|
|
||||||
|
|
||||||
- Пропсы деструктурируются в теле компонента, не в параметрах.
|
|
||||||
- Порядок: пользовательские → системные (`children`, `className`) → `...htmlAttr`.
|
|
||||||
- `className` объединяется с корневым классом через `cl()`: `cl(styles.root, className)`.
|
|
||||||
- `...htmlAttr` прокидывается на корневой элемент.
|
|
||||||
|
|
||||||
## Пример
|
|
||||||
|
|
||||||
`container/types/container.type.ts`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Параметры компонента Container.
|
|
||||||
*/
|
|
||||||
export type ContainerParams = {}
|
|
||||||
|
|
||||||
/** HTML-атрибуты корневого элемента. */
|
|
||||||
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
|
||||||
|
|
||||||
export type ContainerProps = RootAttrs & ContainerParams
|
|
||||||
```
|
|
||||||
|
|
||||||
`container/styles/container.module.css`
|
|
||||||
|
|
||||||
```css
|
|
||||||
.root {
|
|
||||||
max-width: var(--content-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--spacing-4);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`container/container.tsx`
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import cl from 'clsx'
|
|
||||||
import type { ContainerProps } from './types/container.type'
|
|
||||||
import styles from './styles/container.module.css'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Контейнер с адаптивной максимальной шириной.
|
|
||||||
*
|
|
||||||
* Используется для:
|
|
||||||
* - обёртки контента страниц с ограничением ширины
|
|
||||||
* - центрирования блоков в лейауте
|
|
||||||
*/
|
|
||||||
export const Container = (props: ContainerProps) => {
|
|
||||||
const { children, className, ...htmlAttr } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...htmlAttr} className={cl(styles.root, className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`container/index.ts`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export { Container } from './container'
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: Шрифты
|
||||||
|
description: Как подключать шрифты через Next.js Font в проекте.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Шрифты
|
||||||
|
|
||||||
|
Как подключать шрифты через Next.js Font в проекте.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Шрифты подключаются через `next/font`. Это стандартный способ Next.js: шрифты загружаются без ручных `<link>`, `@font-face` и настройки preconnect.
|
||||||
|
|
||||||
|
Шрифт подключается в точке инициализации приложения, а в CSS используется через переменную.
|
||||||
|
|
||||||
|
## Google Fonts
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import 'shared/styles/global.css'
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ['latin', 'cyrillic'],
|
||||||
|
variable: '--font-main',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
|
||||||
|
type RootLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
return (
|
||||||
|
<html lang="ru" className={inter.variable}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* src/shared/styles/global.css */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-main), system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Локальные шрифты
|
||||||
|
|
||||||
|
Каждый локальный шрифт размещается в отдельной папке внутри `src/shared/fonts/`. В этой же папке лежит `.font.ts`, где объявляется `localFont`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/shared/fonts/
|
||||||
|
└── roboto/
|
||||||
|
├── roboto.font.ts
|
||||||
|
├── Roboto-Regular.woff2
|
||||||
|
├── Roboto-Italic.woff2
|
||||||
|
├── Roboto-Bold.woff2
|
||||||
|
└── Roboto-BoldItalic.woff2
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/shared/fonts/roboto/roboto.font.ts
|
||||||
|
import localFont from 'next/font/local'
|
||||||
|
|
||||||
|
export const roboto = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: './Roboto-Regular.woff2',
|
||||||
|
weight: '400',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: './Roboto-Italic.woff2',
|
||||||
|
weight: '400',
|
||||||
|
style: 'italic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: './Roboto-Bold.woff2',
|
||||||
|
weight: '700',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: './Roboto-BoldItalic.woff2',
|
||||||
|
weight: '700',
|
||||||
|
style: 'italic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variable: '--font-main',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/` импортирует готовый объект шрифта и только подключает его к документу:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { roboto } from 'shared/fonts/roboto/roboto.font'
|
||||||
|
import 'shared/styles/global.css'
|
||||||
|
|
||||||
|
type RootLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
return (
|
||||||
|
<html lang="ru" className={roboto.variable}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Путь в `localFont` указывается относительно `.font.ts`, поэтому файлы шрифта импортируются коротко: `./Roboto-Regular.woff2`.
|
||||||
|
|
||||||
|
Если шрифтов несколько, у каждого своя папка и свой `.font.ts`.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- Использовать `next/font/google` или `next/font/local`.
|
||||||
|
- Не подключать шрифты через ручные `<link>` и `@font-face` без необходимости.
|
||||||
|
- Подключать шрифты один раз — в корневом layout через готовый объект шрифта.
|
||||||
|
- Использовать CSS-переменные `variable`, а не жёстко прописывать семейство в каждом компоненте.
|
||||||
|
- Локальные файлы шрифтов хранить в `src/shared/fonts/{font-name}/` рядом с `{font-name}.font.ts`.
|
||||||
|
- Не объявлять `localFont` внутри `src/app/layout.tsx`; layout только импортирует готовый шрифт.
|
||||||
|
|||||||
95
docs/docs/applied/images.md
Normal file
95
docs/docs/applied/images.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Изображения
|
||||||
|
description: Как подключать изображения через Next.js Image в проекте.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Изображения
|
||||||
|
|
||||||
|
Как подключать изображения через Next.js Image в проекте.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Изображения рендерятся через компонент `Image` из `next/image`. Это сохраняет единый API для размеров, `alt`, lazy-loading и `priority`, даже если оптимизация изображений отключена.
|
||||||
|
|
||||||
|
В проекте оптимизация Next.js Image отключается через `unoptimized`, чтобы сборка и рантайм не зависели от встроенного image optimizer.
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
Отключение оптимизации задаётся глобально в `next.config.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { NextConfig } from 'next'
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого `unoptimized` не нужно повторять на каждом `Image`.
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Статические изображения, доступные по URL, размещаются в `public/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
public/
|
||||||
|
└── images/
|
||||||
|
└── user-avatar.png
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export const UserAvatar = () => {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/images/user-avatar.png"
|
||||||
|
alt="Аватар пользователя"
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- Использовать `Image` из `next/image`, не обычный `<img>`.
|
||||||
|
- Для контентных изображений всегда писать осмысленный `alt`.
|
||||||
|
- Для декоративных изображений использовать `alt=""`.
|
||||||
|
- Указывать `width` и `height`, если изображение не использует `fill`.
|
||||||
|
- При `fill` задавать `sizes` и контролировать размеры родителя стилями.
|
||||||
|
- `priority` ставить только для изображений первого экрана.
|
||||||
|
- SVG-иконки не оформлять как изображения — для них используется раздел [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-intro).
|
||||||
|
|
||||||
|
## Пример с `fill`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Image from 'next/image'
|
||||||
|
import styles from '../styles/article-card-cover.module.css'
|
||||||
|
|
||||||
|
export const ArticleCardCover = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Image
|
||||||
|
src="/images/article-cover.jpg"
|
||||||
|
alt="Обложка статьи"
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 768px) 33vw, 100vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: Локализация
|
||||||
|
description: Как организовать локализацию как infrastructure-модуль.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Локализация
|
||||||
|
|
||||||
|
Как организовать локализацию как infrastructure-модуль.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов.
|
||||||
|
|
||||||
|
Код локализации живёт в `src/infrastructure/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infrastructure-модуля.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/i18n/
|
||||||
|
├── config/
|
||||||
|
│ └── i18n.config.ts
|
||||||
|
├── dictionaries/
|
||||||
|
│ ├── ru.ts
|
||||||
|
│ └── en.ts
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-translation.hook.ts
|
||||||
|
├── providers/
|
||||||
|
│ └── i18n-provider.tsx
|
||||||
|
├── types/
|
||||||
|
│ └── i18n.type.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infrastructure/i18n`.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infrastructure/i18n/`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { I18nProvider } from 'infrastructure/i18n'
|
||||||
|
|
||||||
|
type RootLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
return (
|
||||||
|
<html lang="ru">
|
||||||
|
<body>
|
||||||
|
<I18nProvider locale="ru">{children}</I18nProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Компоненты получают переводы через готовый API модуля локализации:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTranslation } from 'infrastructure/i18n'
|
||||||
|
|
||||||
|
export const ProfileTitle = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return <h1>{t('profile.title')}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- Локализация живёт в `infrastructure/i18n/`.
|
||||||
|
- `app/` только подключает готовый provider и передаёт locale.
|
||||||
|
- Словари не импортируются напрямую в компоненты, screens или business-модули.
|
||||||
|
- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск.
|
||||||
|
- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться.
|
||||||
|
- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infrastructure-модуля.
|
||||||
|
|||||||
156
docs/docs/applied/module.md
Normal file
156
docs/docs/applied/module.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
title: Модуль
|
||||||
|
description: Как должен выглядеть сгенерированный SLM-модуль в проекте.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Модуль
|
||||||
|
|
||||||
|
Как должен выглядеть сгенерированный SLM-модуль в проекте.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Архитектурное определение модуля описано в разделе [Архитектура → Модули](/docs/basics/architecture/modules). Список сегментов описан в разделе [Архитектура → Сегменты](/docs/basics/architecture/segments).
|
||||||
|
|
||||||
|
Эта страница показывает прикладное оформление трёх типов модулей: UI, бизнес и инфраструктурный.
|
||||||
|
|
||||||
|
## Создание
|
||||||
|
|
||||||
|
1. Проверьте, что в проекте есть нужный шаблон в `.templates/`.
|
||||||
|
2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create).
|
||||||
|
3. Сгенерируйте модуль через [VS Code или CLI](/docs/applied/templates/templates-usage).
|
||||||
|
|
||||||
|
## Типы модулей
|
||||||
|
|
||||||
|
Архитектура определяет три типа модулей ([Типы модулей](/docs/basics/architecture/modules#типы-модулей)):
|
||||||
|
|
||||||
|
| Тип | Обязательный файл | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| UI-модуль | `{name}.tsx` | Модуль, выросший из компонента |
|
||||||
|
| Бизнес-модуль | `{name}.factory.ts` | Модуль вокруг публичного runtime API |
|
||||||
|
| Инфраструктурный модуль | нет | Модуль вокруг технического сервиса |
|
||||||
|
|
||||||
|
## UI-модуль
|
||||||
|
|
||||||
|
UI-модуль — это компонент, который перерос ограничения компонента: получил собственные хуки, вложенные модули в `parts/`, сценарную логику или публичный API. Внутренняя структура та же, что у компонента: корневой `.tsx`, типы, стили, `ui/`. Но без ограничений компонента.
|
||||||
|
|
||||||
|
Подробное оформление компонентов внутри `ui/` описано в разделе [Компонент](/docs/applied/component).
|
||||||
|
|
||||||
|
## Бизнес-модуль
|
||||||
|
|
||||||
|
Бизнес-модуль строится вокруг публичного runtime API. Ключевой файл — фабрика (`{name}.factory.ts`), которая возвращает всё, что нужно внешнему коду в runtime.
|
||||||
|
|
||||||
|
Архитектурное описание фабрики: [Архитектура → Фабрика](/docs/basics/architecture/modules#фабрика).
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
```text
|
||||||
|
business/customer/
|
||||||
|
├── customer.factory.ts
|
||||||
|
├── index.ts
|
||||||
|
└── types/
|
||||||
|
├── customer.type.ts
|
||||||
|
├── customer-api.type.ts
|
||||||
|
├── customer-deps.type.ts
|
||||||
|
└── customer-factory.type.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типы
|
||||||
|
|
||||||
|
`business/customer/types/customer-api.type.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type CustomerApi = {
|
||||||
|
useCustomer: () => Customer
|
||||||
|
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`business/order/types/order-deps.type.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type OrderDeps = {
|
||||||
|
customer: Pick<CustomerApi, 'useCustomer'>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`business/order/types/order-factory.type.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фабрика без зависимостей
|
||||||
|
|
||||||
|
`business/customer/customer.factory.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { CustomerFactory } from './types/customer-factory.type'
|
||||||
|
|
||||||
|
export const customerFactory: CustomerFactory = () => {
|
||||||
|
return {
|
||||||
|
useCustomer,
|
||||||
|
CustomerCard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фабрика с зависимостями
|
||||||
|
|
||||||
|
`business/order/order.factory.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { OrderFactory } from './types/order-factory.type'
|
||||||
|
|
||||||
|
export const orderFactory: OrderFactory = (deps) => {
|
||||||
|
return {
|
||||||
|
useOrder,
|
||||||
|
OrderCard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Композиция на уровне screen
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// screens/home/home.screen.tsx
|
||||||
|
import { customerFactory } from '@/business/customer'
|
||||||
|
import { orderFactory } from '@/business/order'
|
||||||
|
|
||||||
|
const customer = customerFactory()
|
||||||
|
const order = orderFactory({ customer })
|
||||||
|
|
||||||
|
const { useOrder, OrderCard } = order
|
||||||
|
|
||||||
|
export const HomeScreen = () => {
|
||||||
|
const currentOrder = useOrder()
|
||||||
|
|
||||||
|
return <OrderCard order={currentOrder} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Инфраструктурный модуль
|
||||||
|
|
||||||
|
Инфраструктурный модуль строится вокруг технического сервиса или интеграции. Его структура определяется природой сервиса — фиксированного корневого файла нет.
|
||||||
|
|
||||||
|
Архитектурное описание: [Архитектура → Типы модулей → Инфраструктурный модуль](/docs/basics/architecture/modules#инфраструктурный-модуль).
|
||||||
|
|
||||||
|
Пример модуля темы:
|
||||||
|
|
||||||
|
```text
|
||||||
|
theme/
|
||||||
|
├── index.ts
|
||||||
|
├── config/
|
||||||
|
├── hooks/
|
||||||
|
├── styles/
|
||||||
|
└── ui/
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример модуля API-клиента:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend-api/
|
||||||
|
├── backend-api.client.ts
|
||||||
|
├── config/
|
||||||
|
├── types/
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
@@ -1,30 +1,61 @@
|
|||||||
---
|
---
|
||||||
title: Файлы роутинга
|
title: Файлы роутинга
|
||||||
|
description: Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Файлы роутинга
|
# Файлы роутинга
|
||||||
|
|
||||||
Правила для специальных файлов App Router (`page.tsx`, `layout.tsx`, `error.tsx`, `not-found.tsx` и др.) — чем наш подход отличается от дефолтного.
|
Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||||
|
|
||||||
## Организация
|
## Назначение
|
||||||
|
|
||||||
- `page.tsx` — тонкий файл: только `metadata` и рендер экрана. Логика, стили и зависимости живут в экране, не в `page.tsx`.
|
`src/app/**` — точка входа приложения и слой файлового роутинга Next.js.
|
||||||
- `error.tsx` и `not-found.tsx` делегируют разметку экранам по тому же принципу.
|
|
||||||
- `layout.tsx` — точка подключения провайдеров и глобальных стилей. Вёрстка layout-обёрток выносится в слой `layouts/`.
|
|
||||||
- Стили в файлах роутинга не используются — стилизация только внутри вызываемых компонентов.
|
|
||||||
|
|
||||||
## Реализация
|
Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen.
|
||||||
|
|
||||||
- Каждый `page.tsx` экспортирует `metadata` с `title` — он подставляется в шаблон корневого layout (`%s | App`).
|
Границы слоя описаны в [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app).
|
||||||
- Корневой `layout.tsx` задаёт `metadata` с `title.template`, `description`, `metadataBase` и OpenGraph-настройками.
|
|
||||||
|
|
||||||
## Примеры
|
## Граница ответственности
|
||||||
|
|
||||||
`src/app/profile/[id]/page.tsx`
|
| Область | Где живёт |
|
||||||
|
|---|---|
|
||||||
|
| Файлы маршрутов (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`) | `src/app/**` |
|
||||||
|
| Параметры маршрута, `metadata`, `redirect()`, `notFound()` | `src/app/**` |
|
||||||
|
| Серверные запросы для первого рендера | `src/app/**`, через готовые клиенты и сервисы нижних слоёв |
|
||||||
|
| Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв |
|
||||||
|
| UI страницы | `screens/` |
|
||||||
|
| Каркас страницы: header, footer, sidebar | `layouts/` |
|
||||||
|
| Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infrastructure/`, `shared/`) |
|
||||||
|
| CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` |
|
||||||
|
|
||||||
|
## Что можно делать в `page.tsx`
|
||||||
|
|
||||||
|
- Экспортировать `metadata` или `generateMetadata`.
|
||||||
|
- Читать `params` и `searchParams`.
|
||||||
|
- Нормализовать и валидировать параметры маршрута.
|
||||||
|
- Делать серверные запросы для первого рендера через готовые клиенты или сервисы.
|
||||||
|
- Вызывать `redirect()` и `notFound()`.
|
||||||
|
- Готовить начальные данные для screen.
|
||||||
|
- Готовить SWR `fallback` и передавать его в готовый провайдер.
|
||||||
|
- Подключать готовый провайдер стора страницы и передавать начальное состояние.
|
||||||
|
- Рендерить screen или композицию из готовых обёрток и screen.
|
||||||
|
|
||||||
|
## Что запрещено
|
||||||
|
|
||||||
|
- Писать UI-разметку страницы прямо в файле роутинга.
|
||||||
|
- Создавать локальные компоненты внутри `src/app/**`.
|
||||||
|
- Добавлять CSS Modules, стили компонентов, `components/`, `styles/`, `hooks/`, `stores/`, `services/` внутри `src/app/**`.
|
||||||
|
- Реализовывать провайдеры, сторы, хуки, API-клиенты или сервисы в файлах роутинга.
|
||||||
|
- Размещать бизнес-логику, мапперы и правила предметной области в файлах роутинга.
|
||||||
|
- Вызывать `useSWR` и доменные клиентские хуки в файлах роутинга.
|
||||||
|
|
||||||
|
## Страницы
|
||||||
|
|
||||||
|
Страница объявляется через `export default function`. Для серверных запросов используется `async function`.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { ProfileScreen } from '@/screens/profile'
|
import { ProfileScreen } from 'screens/profile'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Профиль',
|
title: 'Профиль',
|
||||||
@@ -42,12 +73,105 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`src/app/error.tsx`
|
## Данные первого рендера
|
||||||
|
|
||||||
|
Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import { userApi } from 'infrastructure/backend-api'
|
||||||
|
import { UserScreen } from 'screens/user'
|
||||||
|
|
||||||
|
type UserPageProps = {
|
||||||
|
params: Promise<{ id: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function UserPage({ params }: UserPageProps) {
|
||||||
|
const { id } = await params
|
||||||
|
const user = await userApi.users.get(id)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UserScreen user={user} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если данные нужны нескольким клиентским SWR-хукам, файл роутинга может обернуть дерево в `SWRConfig` и передать `fallback`. Запросы стартуют на сервере, а клиентские хуки получают данные из кеша.
|
||||||
|
|
||||||
|
Ключи `fallback` должны совпадать с ключами внутри GET-хуков REST-клиента. Для array-key используется `unstable_serialize`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { SWRConfig, unstable_serialize } from 'swr'
|
||||||
|
import {
|
||||||
|
backendApi,
|
||||||
|
getCurrentUserKey,
|
||||||
|
getPostListKey,
|
||||||
|
} from 'infrastructure/backend-api'
|
||||||
|
|
||||||
|
type FeedLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function FeedLayout({ children }: FeedLayoutProps) {
|
||||||
|
const userPromise = backendApi.user.getCurrent()
|
||||||
|
const postsPromise = backendApi.posts.list()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fallback: {
|
||||||
|
[unstable_serialize(getCurrentUserKey())]: userPromise,
|
||||||
|
[unstable_serialize(getPostListKey())]: postsPromise,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SWRConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [REST → Стратегии получения данных](/docs/data/rest/strategies/), [REST → Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data).
|
||||||
|
|
||||||
|
## Инициализация состояния
|
||||||
|
|
||||||
|
Файл роутинга может подключить готовый провайдер стора страницы, если состояние зависит от маршрута или данных первого рендера. Реализация стора и провайдера не размещается в `src/app/**`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ProfileScreen, ProfileStoreProvider } from 'screens/profile'
|
||||||
|
|
||||||
|
type ProfilePageProps = {
|
||||||
|
params: Promise<{ id: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProfileStoreProvider initialState={{ userId: id }}>
|
||||||
|
<ProfileScreen />
|
||||||
|
</ProfileStoreProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
`layout.tsx` подключает готовую инициализацию приложения: глобальные стили, провайдеры и верхнеуровневые обёртки из нижних слоёв.
|
||||||
|
|
||||||
|
Вёрстка layout-каркаса выносится в слой `layouts/`. Реализация провайдеров, стилей и UI не размещается в `app/`.
|
||||||
|
|
||||||
|
## Error и Not Found
|
||||||
|
|
||||||
|
`error.tsx` и `not-found.tsx` делегируют разметку готовым screen или widget. В файле роутинга остаётся только адаптация API Next.js к пропсам нижнего слоя.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ErrorScreen } from '@/screens/error'
|
import { ErrorScreen } from 'screens/error'
|
||||||
|
|
||||||
type ErrorPageProps = {
|
type ErrorPageProps = {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string }
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: PostCSS
|
title: PostCSS
|
||||||
|
description: Установка и настройка CSS-процессора в новом проекте.
|
||||||
keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор]
|
keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор]
|
||||||
---
|
---
|
||||||
|
|
||||||
# PostCSS
|
# PostCSS
|
||||||
|
|
||||||
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
Установка и настройка CSS-процессора в новом проекте.
|
||||||
|
|
||||||
Правила написания CSS в компонентах — [Использование](/docs/applied/styles/usage).
|
|
||||||
|
|
||||||
## Зачем PostCSS
|
## Зачем PostCSS
|
||||||
|
|
||||||
@@ -68,4 +67,4 @@ export default {
|
|||||||
|
|
||||||
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
||||||
|
|
||||||
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/applied/styles/usage), раздел «Импорт стилей»).
|
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](/docs/applied/styles/styles-usage), раздел «Импорт стилей»).
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Структура проекта
|
title: Структура проекта
|
||||||
|
description: Из чего состоит проект и где что лежит.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Структура проекта
|
# Структура проекта
|
||||||
|
|
||||||
Файловая организация Next.js-проекта по архитектуре SLM.
|
Из чего состоит проект и где что лежит.
|
||||||
|
|
||||||
## Корень репозитория
|
## Корень репозитория
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ public/
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
src/
|
||||||
├── app/ # Роутинг Next.js, провайдеры, глобальные стили
|
├── app/ # Роутинг Next.js и точка входа приложения
|
||||||
├── layouts/ # Каркасы страниц (header, footer, sidebar)
|
├── layouts/ # Каркасы страниц (header, footer, sidebar)
|
||||||
├── screens/ # Контент конкретной страницы
|
├── screens/ # Контент конкретной страницы
|
||||||
├── widgets/ # Составные блоки интерфейса, не привязанные к домену
|
├── widgets/ # Составные блоки интерфейса, не привязанные к домену
|
||||||
@@ -54,12 +55,13 @@ src/
|
|||||||
|
|
||||||
### Папка `app/`
|
### Папка `app/`
|
||||||
|
|
||||||
Точка входа приложения: инициализация (провайдеры, глобальные стили) и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
|
Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
|
||||||
|
`app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы.
|
||||||
|
|
||||||
|
Подробнее о границах слоя: [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app).
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/app/
|
src/app/
|
||||||
├── providers/ # Провайдеры приложения
|
|
||||||
├── styles/ # Глобальные стили
|
|
||||||
├── layout.tsx # Корневой layout
|
├── layout.tsx # Корневой layout
|
||||||
└── page.tsx # Главная страница
|
└── page.tsx # Главная страница
|
||||||
```
|
```
|
||||||
@@ -78,7 +80,7 @@ src/app/
|
|||||||
└── store/ # Шаблон стора
|
└── store/ # Шаблон стора
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробнее о генерации описано в разделе [Шаблоны и генерация кода](./templates-generation).
|
Подробнее о генерации описано в разделе [Шаблоны генерации](/docs/applied/templates/templates-intro).
|
||||||
|
|
||||||
## Конфигурационные файлы
|
## Конфигурационные файлы
|
||||||
|
|
||||||
|
|||||||
176
docs/docs/applied/styles/styles-setup.md
Normal file
176
docs/docs/applied/styles/styles-setup.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
title: Настройка стилей
|
||||||
|
description: "Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили."
|
||||||
|
keywords: [variables.css, media.css, global.css, shared/styles, токены, переменные, custom-media, breakpoints, подключение стилей, базовые стили, инициализация]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Настройка стилей
|
||||||
|
|
||||||
|
Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Установлен PostCSS или любой другой pre/post-процессор с поддержкой `@custom-media`.
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
Состав глобальных стилей — три файла:
|
||||||
|
|
||||||
|
| Файл | Роль |
|
||||||
|
|------|------|
|
||||||
|
| `variables.css` | Токены проекта (цвета, отступы, радиусы) |
|
||||||
|
| `media.css` | Custom media queries (брейкпоинты по ширине и высоте) |
|
||||||
|
| `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз |
|
||||||
|
|
||||||
|
Правила подключения:
|
||||||
|
|
||||||
|
- В приложение импортируется **только** `global.css`.
|
||||||
|
- `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`.
|
||||||
|
- `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](/docs/applied/postcss)).
|
||||||
|
|
||||||
|
## Корневой `font-size`
|
||||||
|
|
||||||
|
Базовая единица `rem` в проекте привязана к **16px**: корневой `font-size` не переопределяется. `html { font-size: ... }` писать запрещено — пользовательская настройка размера шрифта в браузере должна работать (a11y). Все `rem`-значения в `media.css` и других стилях трактуются как `1rem = 16px по умолчанию`.
|
||||||
|
|
||||||
|
Reset браузерных дефолтов (`box-sizing`, сброс `margin`, типографика) каноном не задаётся — каждый проект решает сам. Если заводится — подключается через `global.css`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### 1. Создать файлы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p src/shared/styles
|
||||||
|
touch src/shared/styles/variables.css src/shared/styles/media.css src/shared/styles/global.css
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Заполнить `media.css`
|
||||||
|
|
||||||
|
Файл `src/shared/styles/media.css`. Стандартный набор брейкпоинтов проекта; редактировать только при согласованном изменении шкалы.
|
||||||
|
|
||||||
|
Единица — `rem` (реагирует на корневой `font-size`). Перевод исходит из дефолтного `html { font-size: 16px }`, т.е. `1rem = 16px`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* src/shared/styles/media.css */
|
||||||
|
|
||||||
|
/* Ширина — Mobile First (min-width), кроме --xs (max-width) */
|
||||||
|
@custom-media --xs (max-width: 35.9375rem); /* 575px — до sm */
|
||||||
|
@custom-media --sm (min-width: 36rem); /* 576px — телефон альбом / малый планшет */
|
||||||
|
@custom-media --md (min-width: 48rem); /* 768px — планшет */
|
||||||
|
@custom-media --lg (min-width: 62rem); /* 992px — малый десктоп */
|
||||||
|
@custom-media --xl (min-width: 75rem); /* 1200px — десктоп */
|
||||||
|
@custom-media --2xl (min-width: 88rem); /* 1408px — широкий десктоп */
|
||||||
|
@custom-media --3xl (min-width: 120rem); /* 1920px — full HD+ */
|
||||||
|
|
||||||
|
/* Высота — min-height */
|
||||||
|
@custom-media --h-xs (min-height: 41.6875rem); /* 667px — iPhone SE портрет */
|
||||||
|
@custom-media --h-sm (min-height: 43.875rem); /* 702px */
|
||||||
|
@custom-media --h-md (min-height: 50.625rem); /* 810px — iPad портрет */
|
||||||
|
@custom-media --h-lg (min-height: 56.25rem); /* 900px */
|
||||||
|
@custom-media --h-xl (min-height: 62.5rem); /* 1000px */
|
||||||
|
@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */
|
||||||
|
@custom-media --h-3xl (min-height: 75rem); /* 1200px */
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- только `@custom-media` на верхнем уровне;
|
||||||
|
- имена короткие, по шкале (`--xs` … `--3xl`); высотные — с префиксом `--h-`;
|
||||||
|
- единица — `rem`, не `em`/`px`; пиксельное значение указывается комментарием;
|
||||||
|
- значения ширины — `min-width` (Mobile First), исключение `--xs` — `max-width` (блок «строго меньше `--sm`»);
|
||||||
|
- значения высоты — `min-height`.
|
||||||
|
|
||||||
|
### 3. Заполнить `variables.css`
|
||||||
|
|
||||||
|
Файл `src/shared/styles/variables.css`. Набор токенов под проект расширяется по мере роста дизайн-системы.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* src/shared/styles/variables.css */
|
||||||
|
:root {
|
||||||
|
/* Цвета */
|
||||||
|
--color-primary: #3b82f6;
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-bg-hover: #f5f5f5;
|
||||||
|
--color-text: #1a1a1a;
|
||||||
|
|
||||||
|
/* Отступы */
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
|
||||||
|
/* Скругления */
|
||||||
|
--radius-1: 4px;
|
||||||
|
--radius-2: 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- все токены определяются в `:root` — без вложенных селекторов;
|
||||||
|
- именование — `kebab-case` по ролям: `--color-*`, `--space-*`, `--radius-*`;
|
||||||
|
- `px` — основная единица для пространственных токенов;
|
||||||
|
- темы накладываются поверх через `[data-theme="..."] { ... }` — в отдельном файле темы или здесь же.
|
||||||
|
|
||||||
|
`variables.css` напрямую в приложение не импортируется — только через `global.css`.
|
||||||
|
|
||||||
|
### 4. Заполнить `global.css`
|
||||||
|
|
||||||
|
Файл `src/shared/styles/global.css`. Единственный глобальный файл, импортируемый в точку инициализации приложения. Внутри — `@import` остальных глобалов относительным путём.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* src/shared/styles/global.css */
|
||||||
|
@import './variables.css';
|
||||||
|
|
||||||
|
/* Сюда же подключаются будущие глобалы через @import:
|
||||||
|
* @import './reset.css';
|
||||||
|
* @import './typography.css';
|
||||||
|
* @import './themes.css';
|
||||||
|
* media.css НЕ импортируется — он работает через PostCSS.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- пути в `@import` — относительные (`./variables.css`), не через алиасы; нативный CSS `@import` не понимает tsconfig-paths;
|
||||||
|
- `media.css` в `global.css` **не импортируется**;
|
||||||
|
- собственные глобальные правила (`html { ... }`, `body { ... }`) писать **не здесь**, а в отдельных файлах рядом (`reset.css`, `typography.css`) и подключать через `@import`. `global.css` — только точка сборки;
|
||||||
|
- порядок `@import` определяет порядок каскада: токены первыми, дальше резеты / темы / типографика.
|
||||||
|
|
||||||
|
### 5. Подключить `global.css` в layout
|
||||||
|
|
||||||
|
Импорт делается **один раз** — в корневом layout приложения:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import 'shared/styles/global.css'
|
||||||
|
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'App',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="ru">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`variables.css` и `media.css` в layout **не импортируются напрямую** — только через `global.css` (variables) или через PostCSS на сборке (media).
|
||||||
|
|
||||||
|
## Проверка установки
|
||||||
|
|
||||||
|
- В `src/shared/styles/` присутствуют три файла: `variables.css`, `media.css`, `global.css`. В `src/app/` папки `styles/` нет.
|
||||||
|
- В `src/app/layout.tsx` есть `import 'shared/styles/global.css'`. Импортов `variables.css` и `media.css` там нет.
|
||||||
|
- В проекте **не появились** PostCSS-пакеты и `postcss.config.*` — этот раздел их не ставит.
|
||||||
|
- `npm run build` завершается успешно.
|
||||||
|
|
||||||
|
## Дальше
|
||||||
|
|
||||||
|
- [PostCSS](/docs/applied/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность.
|
||||||
|
- [Использование стилей](/docs/applied/styles/styles-usage) — правила написания CSS в компонентах.
|
||||||
|
- [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) — стили иконок отдельно от глобальных.
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
---
|
---
|
||||||
title: Использование
|
title: Использование стилей
|
||||||
|
description: Как пишутся стили в проекте.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Использование
|
# Использование стилей
|
||||||
|
|
||||||
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/applied/styles/postcss).
|
Как пишутся стили в проекте.
|
||||||
|
|
||||||
## Общие правила
|
## Общие правила
|
||||||
|
|
||||||
- Только **PostCSS** и **CSS Modules** для кастомной стилизации.
|
- Только **PostCSS** и **CSS Modules** для кастомной стилизации.
|
||||||
- Подход **Mobile First** — стили пишутся от мобильных к десктопу.
|
- Подход **Mobile First** — стили пишутся от мобильных к десктопу.
|
||||||
- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`).
|
- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`).
|
||||||
|
- Корневой класс каждого CSS Module компонента всегда называется `.root` — это упрощает ориентацию в DevTools и отладку DOM.
|
||||||
- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`.
|
- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`.
|
||||||
|
|
||||||
**Хорошо**
|
**Хорошо**
|
||||||
@@ -142,13 +144,13 @@ title: Использование
|
|||||||
|
|
||||||
## CSS-переменные
|
## CSS-переменные
|
||||||
|
|
||||||
- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `app/styles/variables.css` через `:root`.
|
- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `src/shared/styles/variables.css` через `:root`.
|
||||||
- Файл переменных подключается один раз в корневом layout/entry point — после этого переменные доступны глобально через каскад.
|
- Файл переменных подключается через `src/shared/styles/global.css`, который импортируется один раз в `src/app/layout.tsx`.
|
||||||
- Не дублировать магические значения в компонентах.
|
- Не дублировать магические значения в компонентах.
|
||||||
|
|
||||||
**Хорошо**
|
**Хорошо**
|
||||||
```css
|
```css
|
||||||
/* app/styles/variables.css */
|
/* src/shared/styles/variables.css */
|
||||||
:root {
|
:root {
|
||||||
--color-primary: #3b82f6;
|
--color-primary: #3b82f6;
|
||||||
--color-bg: #ffffff;
|
--color-bg: #ffffff;
|
||||||
@@ -182,11 +184,11 @@ title: Использование
|
|||||||
|
|
||||||
## Custom Media
|
## Custom Media
|
||||||
|
|
||||||
- Breakpoints определяются через Custom Media Queries в `app/styles/media.css`.
|
- Breakpoints определяются через Custom Media Queries в `src/shared/styles/media.css`.
|
||||||
- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей.
|
- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей.
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* app/styles/media.css */
|
/* src/shared/styles/media.css */
|
||||||
@custom-media --sm (min-width: 36em);
|
@custom-media --sm (min-width: 36em);
|
||||||
@custom-media --md (min-width: 62em);
|
@custom-media --md (min-width: 62em);
|
||||||
@custom-media --lg (min-width: 82em);
|
@custom-media --lg (min-width: 82em);
|
||||||
31
docs/docs/applied/svg-sprites/svg-sprites-intro.md
Normal file
31
docs/docs/applied/svg-sprites/svg-sprites-intro.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: SVG-спрайты
|
||||||
|
description: "Что такое SVG-спрайты и какие проблемы они решают."
|
||||||
|
---
|
||||||
|
|
||||||
|
# SVG-спрайты
|
||||||
|
|
||||||
|
Что такое SVG-спрайты и какие проблемы они решают.
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Иконки в проекте — это десятки и сотни SVG-файлов, которые нужно как-то доставлять в интерфейс. Подход «один `<img>` на иконку» или инлайн SVG в каждом компоненте приводят к трём проблемам:
|
||||||
|
|
||||||
|
- **Дублирование.** Инлайн SVG в нескольких компонентах — один и тот же код размазан по проекту. Изменение иконки требует правок в десяти местах.
|
||||||
|
- **Размер бандла.** Каждый инлайн SVG — полный XML-код, который попадает в JS-бандл. Сотня иконок × средний размер SVG = сотни килобайт, которые браузер парсит как JavaScript, а не как статику.
|
||||||
|
- **Нет управления цветом.** Инлайн SVG жёстко закрашивает иконку. Сменить цвет по состоянию (`:hover`, `._disabled`) — значит дублировать SVG или городить `currentColor`-хаки в каждом компоненте.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
SVG-спрайты — это единый файл-контейнер, в который собираются все иконки проекта. В коде используется один React-компонент `<SvgSprite icon="name"/>`, а браузер загружает спрайт как статику — один раз, с кешированием.
|
||||||
|
|
||||||
|
Что дают SVG-спрайты:
|
||||||
|
|
||||||
|
- **Один источник.** Каждая иконка — один SVG-файл в `src/shared/sprites/`. Обновил файл — иконка обновилась везде.
|
||||||
|
- **Лёгкий бандл.** Спрайт отдаётся как статический файл из `public/`, не попадает в JavaScript. Типы имён иконок генерируются автоматически — автодополнение работает без ручных описаний.
|
||||||
|
- **Цвет через CSS.** При сборке цвета в SVG заменяются на CSS-переменные. Цвет иконки меняется через `color` родителя или через переменные `--icon-color-N` — как любой другой стиль.
|
||||||
|
|
||||||
|
## Состав раздела
|
||||||
|
|
||||||
|
- [Настройка](/docs/applied/svg-sprites/svg-sprites-setup) — подключение пакета, конфигурация, первая генерация.
|
||||||
|
- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент `<SvgSprite/>`, управление цветом.
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Установка и настройка
|
title: Настройка SVG-спрайтов
|
||||||
|
description: Подключение SVG-спрайтов в новом проекте.
|
||||||
keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts]
|
keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Установка и настройка
|
# Настройка SVG-спрайтов
|
||||||
|
Подключение SVG-спрайтов в новом проекте.
|
||||||
Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета.
|
|
||||||
|
|
||||||
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/applied/svg-sprites/usage).
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- Node.js 18+
|
|
||||||
- React 18+
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
@@ -22,7 +15,7 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
|||||||
npm install @gromlab/svg-sprites
|
npm install @gromlab/svg-sprites
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Создать `svg-sprites.config.ts` в корне проекта (см. «Стандартный конфиг»).
|
2. Создать `svg-sprites.config.ts` в корне проекта (см. [Стандартный конфиг](#стандартныи-конфиг)).
|
||||||
|
|
||||||
3. Создать папку входа для SVG-файлов в слое `shared`:
|
3. Создать папку входа для SVG-файлов в слое `shared`:
|
||||||
|
|
||||||
@@ -60,6 +53,33 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
|||||||
npm run sprite
|
npm run sprite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
7. Подключить спрайт в layout. Глобальный спрайт (иконки) подключается через `<link rel="preload">` в корневом layout — браузер загрузит файл заранее и закеширует:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import 'shared/styles/global.css'
|
||||||
|
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'App',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<link rel="preload" href="/sprites/icons.sprite.stack.svg" as="image" />
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Локальные спрайты (если есть) подключаются аналогично в layout конкретной страницы или маршрута.
|
||||||
|
|
||||||
## Стандартный конфиг
|
## Стандартный конфиг
|
||||||
|
|
||||||
Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине.
|
Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине.
|
||||||
@@ -106,3 +126,7 @@ transform: {
|
|||||||
### Режим
|
### Режим
|
||||||
|
|
||||||
По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`.
|
По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`.
|
||||||
|
|
||||||
|
## Дальше
|
||||||
|
|
||||||
|
- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент `<SvgSprite/>`, управление цветом.
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Использование
|
title: Использование SVG-спрайтов
|
||||||
|
description: Как добавлять и использовать SVG-иконки в коде.
|
||||||
keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color]
|
keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Использование
|
# Использование SVG-спрайтов
|
||||||
|
|
||||||
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/applied/svg-sprites/setup).
|
Как добавлять и использовать SVG-иконки в коде.
|
||||||
|
|
||||||
## Шаги
|
## Шаги
|
||||||
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
---
|
|
||||||
title: Шаблоны и генерация кода
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- @formatter:off -->
|
|
||||||
::: v-pre
|
|
||||||
|
|
||||||
# Шаблоны и генерация кода
|
|
||||||
|
|
||||||
Как работают шаблоны, как их создавать, синтаксис переменных и как генерировать код с помощью расширения VS Code и CLI.
|
|
||||||
|
|
||||||
## Структура шаблонов
|
|
||||||
|
|
||||||
Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон.
|
|
||||||
|
|
||||||
```text
|
|
||||||
.templates/
|
|
||||||
├── component/ # шаблон компонента
|
|
||||||
│ └── {{name.kebabCase}}/
|
|
||||||
│ ├── styles/
|
|
||||||
│ │ └── {{name.kebabCase}}.module.css
|
|
||||||
│ ├── types/
|
|
||||||
│ │ └── {{name.kebabCase}}.type.ts
|
|
||||||
│ ├── {{name.kebabCase}}.tsx
|
|
||||||
│ └── index.ts
|
|
||||||
└── store/ # шаблон Zustand стора
|
|
||||||
└── {{name.kebabCase}}/
|
|
||||||
├── {{name.kebabCase}}.store.ts
|
|
||||||
├── {{name.kebabCase}}.type.ts
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Синтаксис шаблонов
|
|
||||||
|
|
||||||
Переменные работают в именах файлов/папок и внутри файлов. Базовая переменная — `name`.
|
|
||||||
|
|
||||||
```text
|
|
||||||
{{variable}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Модификаторы меняют регистр и формат записи:
|
|
||||||
|
|
||||||
```text
|
|
||||||
{{name.pascalCase}} → MyButton
|
|
||||||
{{name.camelCase}} → myButton
|
|
||||||
{{name.kebabCase}} → my-button
|
|
||||||
{{name.snakeCase}} → my_button
|
|
||||||
{{name.screamingSnakeCase}} → MY_BUTTON
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как создать новый шаблон
|
|
||||||
|
|
||||||
1. Создать папку в `.templates/` с именем шаблона (например `hook`).
|
|
||||||
2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом.
|
|
||||||
3. Шаблон сразу доступен и в расширении VS Code, и в CLI.
|
|
||||||
|
|
||||||
Пример — создание шаблона для хука:
|
|
||||||
|
|
||||||
```text
|
|
||||||
.templates/
|
|
||||||
└── hook/
|
|
||||||
└── {{name.kebabCase}}/
|
|
||||||
├── {{name.kebabCase}}.hook.ts
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// .templates/hook/{{name.kebabCase}}.hook.ts
|
|
||||||
export const {{name.camelCase}} = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// .templates/hook/index.ts
|
|
||||||
export { {{name.camelCase}} } from './{{name.kebabCase}}.hook'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры шаблонов
|
|
||||||
|
|
||||||
### Шаблон компонента
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// .templates/component/index.ts
|
|
||||||
export { {{name.pascalCase}} } from './{{name.kebabCase}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// .templates/component/types/{{name.kebabCase}}.type.ts
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Параметры {{name.pascalCase}}.
|
|
||||||
*/
|
|
||||||
export type {{name.pascalCase}}Params = {}
|
|
||||||
|
|
||||||
/** HTML-атрибуты корневого элемента. */
|
|
||||||
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
|
||||||
|
|
||||||
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params
|
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// .templates/component/{{name.kebabCase}}.tsx
|
|
||||||
import cl from 'clsx'
|
|
||||||
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
|
||||||
import styles from './styles/{{name.kebabCase}}.module.css'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {{name.pascalCase}}.
|
|
||||||
*/
|
|
||||||
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
|
||||||
const { children, className, ...htmlAttr } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...htmlAttr} className={cl(styles.root, className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* .templates/component/styles/{{name.kebabCase}}.module.css */
|
|
||||||
.root {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Генерация через VS Code
|
|
||||||
|
|
||||||
Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора.
|
|
||||||
|
|
||||||
1. ПКМ на целевой папке в проводнике VS Code.
|
|
||||||
2. **Generate from template** → выбрать шаблон.
|
|
||||||
3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`.
|
|
||||||
|
|
||||||
## Генерация через CLI
|
|
||||||
|
|
||||||
[@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx @gromlab/create <шаблон> <имя> <путь>
|
|
||||||
```
|
|
||||||
|
|
||||||
| Команда | Что создаёт |
|
|
||||||
|---|---|
|
|
||||||
| `npx @gromlab/create component button src/shared/ui` | Компонент |
|
|
||||||
| `npx @gromlab/create module auth src/business` | Бизнес-модуль |
|
|
||||||
| `npx @gromlab/create widget header src/widgets` | Виджет |
|
|
||||||
| `npx @gromlab/create layout admin src/layouts` | Layout |
|
|
||||||
| `npx @gromlab/create store auth src/business/auth/stores` | Стор |
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
97
docs/docs/applied/templates/templates-create.md
Normal file
97
docs/docs/applied/templates/templates-create.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Создание шаблонов генерации
|
||||||
|
description: "Структура шаблонов, синтаксис переменных и примеры."
|
||||||
|
keywords: [шаблоны, templates, .templates, syntax, переменные, kebabCase, pascalCase, scaffold]
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- @formatter:off -->
|
||||||
|
::: v-pre
|
||||||
|
|
||||||
|
# Создание шаблонов генерации
|
||||||
|
|
||||||
|
Структура шаблонов, синтаксис переменных и примеры.
|
||||||
|
|
||||||
|
## Структура шаблонов
|
||||||
|
|
||||||
|
Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон.
|
||||||
|
|
||||||
|
```text
|
||||||
|
.templates/
|
||||||
|
├── component/ # шаблон компонента
|
||||||
|
│ └── {{name.kebabCase}}/
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ └── {{name.kebabCase}}.module.css
|
||||||
|
│ ├── types/
|
||||||
|
│ │ └── {{name.kebabCase}}-props.type.ts
|
||||||
|
│ ├── {{name.kebabCase}}.tsx
|
||||||
|
│ └── index.ts
|
||||||
|
└── store/ # шаблон Zustand стора
|
||||||
|
└── {{name.kebabCase}}/
|
||||||
|
├── {{name.kebabCase}}.store.ts
|
||||||
|
├── {{name.kebabCase}}.type.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обязательный шаблон компонента
|
||||||
|
|
||||||
|
Перед созданием компонентов в проекте должен существовать шаблон `.templates/component`.
|
||||||
|
|
||||||
|
Если шаблона нет, компонент не создаётся вручную. Сначала создаётся шаблон компонента, затем компонент генерируется через [VS Code или CLI](/docs/applied/templates/templates-usage).
|
||||||
|
|
||||||
|
## Синтаксис шаблонов
|
||||||
|
|
||||||
|
### Переменные
|
||||||
|
|
||||||
|
Переменные работают в именах файлов/папок и внутри файлов:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{variable}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Переменные могут быть любыми. `name` — дефолтная, подставляется генератором автоматически. Если реализация требует дополнительных параметров — можно использовать произвольные наборы переменных.
|
||||||
|
|
||||||
|
### Модификаторы
|
||||||
|
|
||||||
|
Модификаторы меняют регистр и формат записи переменной:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{name.pascalCase}} → MyButton
|
||||||
|
{{name.camelCase}} → myButton
|
||||||
|
{{name.kebabCase}} → my-button
|
||||||
|
{{name.snakeCase}} → my_button
|
||||||
|
{{name.screamingSnakeCase}} → MY_BUTTON
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как создать новый шаблон
|
||||||
|
|
||||||
|
1. Создать папку в `.templates/` с именем шаблона (например `hook`).
|
||||||
|
2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом.
|
||||||
|
3. Шаблон сразу доступен и в расширении VS Code, и в CLI.
|
||||||
|
|
||||||
|
Пример — создание шаблона для хука:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.templates/
|
||||||
|
└── hook/
|
||||||
|
└── {{name.kebabCase}}/
|
||||||
|
├── {{name.kebabCase}}.hook.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// .templates/hook/{{name.kebabCase}}.hook.ts
|
||||||
|
export const {{name.camelCase}} = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// .templates/hook/index.ts
|
||||||
|
export { {{name.camelCase}} } from './{{name.kebabCase}}.hook'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Дальше
|
||||||
|
|
||||||
|
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.
|
||||||
|
|
||||||
|
:::
|
||||||
32
docs/docs/applied/templates/templates-intro.md
Normal file
32
docs/docs/applied/templates/templates-intro.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Шаблоны генерации
|
||||||
|
description: "Что такое шаблоны кодогенерации и какие проблемы они решают."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Шаблоны генерации
|
||||||
|
|
||||||
|
Что такое шаблоны кодогенерации и какие проблемы они решают.
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам:
|
||||||
|
|
||||||
|
- **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили.
|
||||||
|
- **Время.** Создание одного компонента с типами, стилями и экспортом — 5–10 минут рутины. За спринт набегают часы.
|
||||||
|
- **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически.
|
||||||
|
|
||||||
|
Что дают шаблоны:
|
||||||
|
|
||||||
|
- **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика.
|
||||||
|
- **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику.
|
||||||
|
- **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения.
|
||||||
|
|
||||||
|
## Состав раздела
|
||||||
|
|
||||||
|
- [Настройка](/docs/applied/templates/templates-setup) — первичная установка: скачивание стандартного набора шаблонов в проект.
|
||||||
|
- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры.
|
||||||
|
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.
|
||||||
44
docs/docs/applied/templates/templates-setup.md
Normal file
44
docs/docs/applied/templates/templates-setup.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
title: Настройка шаблонов генерации
|
||||||
|
description: Первичная установка шаблонов кодогенерации в проект.
|
||||||
|
keywords: [шаблоны, templates, .templates, tiged, generator, генератор шаблонов, скачать шаблоны, scaffold]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Настройка шаблонов генерации
|
||||||
|
|
||||||
|
Первичная установка шаблонов кодогенерации в проект.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Проверить, что `.templates/` отсутствует (или согласовать перезапись, если папка уже есть).
|
||||||
|
|
||||||
|
2. Скачать папку из эталонного репозитория:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tiged git@gromlab.ru:templates/nextjs-template.git/.templates .templates
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Если `tiged` падает в default-режиме (HTTP-tarball у Gitea) — повторить с явным git-режимом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tiged --mode=git git@gromlab.ru:templates/nextjs-template.git/.templates .templates
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Проверить генерацию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @gromlab/create component test src/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
После проверки — удалить тестовый модуль.
|
||||||
|
|
||||||
|
## Проверка установки
|
||||||
|
|
||||||
|
- В корне проекта есть папка `.templates/`.
|
||||||
|
- Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор).
|
||||||
|
- Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок.
|
||||||
|
|
||||||
|
## Дальше
|
||||||
|
|
||||||
|
- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры.
|
||||||
|
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.
|
||||||
45
docs/docs/applied/templates/templates-usage.md
Normal file
45
docs/docs/applied/templates/templates-usage.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Использование шаблонов генерации
|
||||||
|
description: Генерация файлов из шаблонов через VS Code плагин и CLI.
|
||||||
|
keywords: [шаблоны, templates, generate, VS Code, CLI, gromlab/create, npx, scaffold]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Использование шаблонов генерации
|
||||||
|
|
||||||
|
Генерация файлов из шаблонов через VS Code плагин и CLI.
|
||||||
|
|
||||||
|
::: danger Ручное создание запрещено
|
||||||
|
Файлы, для которых есть шаблоны в `.templates/`, создаются только генератором. Ручное создание компонента, модуля, стора или другого шаблонного блока запрещено.
|
||||||
|
|
||||||
|
Если нужного шаблона нет, сначала создайте шаблон в `.templates/`, затем сгенерируйте код на его основе.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Через VS Code
|
||||||
|
|
||||||
|
Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора.
|
||||||
|
|
||||||
|
1. ПКМ на целевой папке в проводнике VS Code.
|
||||||
|
2. **Generate from template** → выбрать шаблон.
|
||||||
|
3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`.
|
||||||
|
|
||||||
|
Расширение устанавливается разово на машину разработчика, не через проект.
|
||||||
|
|
||||||
|
## Через CLI
|
||||||
|
|
||||||
|
[@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @gromlab/create <шаблон> <имя> [путь]
|
||||||
|
```
|
||||||
|
|
||||||
|
Путь не обязателен — по умолчанию генерация происходит в текущую директорию.
|
||||||
|
|
||||||
|
| Команда | Что создаёт |
|
||||||
|
|---|---|
|
||||||
|
| `npx @gromlab/create component button` | Компонент в текущей папке |
|
||||||
|
| `npx @gromlab/create module auth src/business` | Бизнес-модуль |
|
||||||
|
| `npx @gromlab/create widget header src/widgets` | Виджет |
|
||||||
|
| `npx @gromlab/create layout admin src/layouts` | Layout |
|
||||||
|
| `npx @gromlab/create store auth src/business/auth/stores` | Стор |
|
||||||
|
|
||||||
|
CLI вызывается через `npx`, в `package.json` отдельно не добавляется.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Настройка VS Code
|
title: VS Code
|
||||||
|
description: Единые настройки редактора и расширений для команды.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Настройка VS Code
|
# VS Code
|
||||||
|
|
||||||
Каждый проект содержит папку `.vscode/` с конфигурацией редактора. Это гарантирует, что все участники команды работают с одинаковыми настройками форматирования, линтинга и расширениями.
|
Единые настройки редактора и расширений для команды.
|
||||||
|
|
||||||
## Структура `.vscode/`
|
## Структура `.vscode/`
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
---
|
|
||||||
title: Архитектура
|
|
||||||
---
|
|
||||||
|
|
||||||
# SLM Design
|
# SLM Design
|
||||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||||
|
|
||||||
|
::: warning Локальная копия
|
||||||
|
Документация по архитектуре — локальная копия. Оригинал находится на сайте [slm-design.gromlab.ru](https://slm-design.gromlab.ru/).
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Разделы спецификации
|
||||||
|
|
||||||
|
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
|
||||||
|
|
||||||
|
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||||
|
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||||
|
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||||
|
|
||||||
|
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
|
||||||
|
|
||||||
## Преимущества
|
## Преимущества
|
||||||
|
|
||||||
### Вертикальная организация домена
|
### Вертикальная организация домена
|
||||||
@@ -17,7 +27,7 @@ Cross-domain зависимости в бизнес-слое реализуют
|
|||||||
|
|
||||||
### Разделение ответственности без перегрузки слоёв
|
### Разделение ответственности без перегрузки слоёв
|
||||||
|
|
||||||
Сервисы приложения (`infrastructure/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||||
|
|
||||||
### Горизонтальная инкапсуляция
|
### Горизонтальная инкапсуляция
|
||||||
|
|
||||||
@@ -71,7 +81,7 @@ src/
|
|||||||
│ ├── orders/
|
│ ├── orders/
|
||||||
│ └── chat/
|
│ └── chat/
|
||||||
│
|
│
|
||||||
├── infrastructure/
|
├── infra/
|
||||||
│ ├── theme/
|
│ ├── theme/
|
||||||
│ ├── i18n/
|
│ ├── i18n/
|
||||||
│ ├── backend-api/
|
│ ├── backend-api/
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
---
|
|
||||||
title: Слои
|
|
||||||
---
|
|
||||||
|
|
||||||
# Слои
|
# Слои
|
||||||
|
|
||||||
Слои SLM: назначение, классификация, направление зависимостей, правила.
|
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
|
||||||
|
|
||||||
## Определение
|
## Определение
|
||||||
|
|
||||||
@@ -17,7 +13,7 @@ title: Слои
|
|||||||
| Группа | Слои | Описание |
|
| Группа | Слои | Описание |
|
||||||
|--------|------|----------|
|
|--------|------|----------|
|
||||||
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
||||||
| Ядро | `business`, `infrastructure`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||||
|
|
||||||
## Направление зависимостей
|
## Направление зависимостей
|
||||||
@@ -25,12 +21,12 @@ title: Слои
|
|||||||
Любой импорт между модулями — только через публичный API.
|
Любой импорт между модулями — только через публичный API.
|
||||||
|
|
||||||
```
|
```
|
||||||
app → [ layouts | screens ] → widgets → business → infrastructure → ui → shared
|
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||||
```
|
```
|
||||||
|
|
||||||
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
||||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
||||||
- Модули одного слоя `infrastructure` и `ui` могут импортировать друг друга через публичный API
|
- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API
|
||||||
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
||||||
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
||||||
|
|
||||||
@@ -128,7 +124,7 @@ src/widgets/
|
|||||||
|
|
||||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
||||||
|
|
||||||
Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. Cross-domain зависимости по коду реализуются через фабрику. `import type` между доменами разрешён напрямую.
|
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`.
|
||||||
|
|
||||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
||||||
|
|
||||||
@@ -159,19 +155,20 @@ src/business/
|
|||||||
|
|
||||||
- Один модуль = один бизнес-домен
|
- Один модуль = один бизнес-домен
|
||||||
- Циклические зависимости между доменами запрещены
|
- Циклические зависимости между доменами запрещены
|
||||||
- Импорт кода между доменами — через фабрику. `import type` — напрямую
|
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
|
||||||
|
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
|
||||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||||
|
|
||||||
## Слой Infrastructure
|
## Слой infra
|
||||||
|
|
||||||
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
||||||
|
|
||||||
Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`.
|
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
|
||||||
|
|
||||||
Отличие от `shared/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/infrastructure/
|
src/infra/
|
||||||
├── theme/
|
├── theme/
|
||||||
├── i18n/
|
├── i18n/
|
||||||
├── backend-api/
|
├── backend-api/
|
||||||
@@ -184,7 +181,7 @@ src/infrastructure/
|
|||||||
### Требования
|
### Требования
|
||||||
|
|
||||||
- Один модуль = один техсервис
|
- Один модуль = один техсервис
|
||||||
- Импортирует `infrastructure/`, `ui/`, `shared/`
|
- Импортирует `infra/`, `ui/`, `shared/`
|
||||||
|
|
||||||
## Слой UI
|
## Слой UI
|
||||||
|
|
||||||
@@ -235,7 +232,7 @@ src/ui/
|
|||||||
|
|
||||||
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
|
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
|
||||||
|
|
||||||
Отличие от `infrastructure/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||||
|
|
||||||
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
|
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
|
||||||
|
|
||||||
284
docs/docs/basics/architecture/modules.md
Normal file
284
docs/docs/basics/architecture/modules.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Модули
|
||||||
|
|
||||||
|
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
|
||||||
|
|
||||||
|
## Определение
|
||||||
|
|
||||||
|
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
|
||||||
|
|
||||||
|
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
|
||||||
|
|
||||||
|
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||||
|
|
||||||
|
Главная граница модуля — не папка, а ответственность.
|
||||||
|
|
||||||
|
## Компонент
|
||||||
|
|
||||||
|
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
|
||||||
|
|
||||||
|
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
|
||||||
|
|
||||||
|
> Компонент отображает. Модуль организует.
|
||||||
|
|
||||||
|
Компонент не может:
|
||||||
|
|
||||||
|
- Импортировать код проекта за пределами родительского модуля.
|
||||||
|
- Владеть архитектурными зависимостями.
|
||||||
|
- Содержать любые компоненты.
|
||||||
|
- Содержать любые модули.
|
||||||
|
- Делать внешние запросы.
|
||||||
|
- Самостоятельно получать данные.
|
||||||
|
- Выбирать источник данных.
|
||||||
|
- Композировать данные.
|
||||||
|
- Вызывать сценарные хуки.
|
||||||
|
- Оркестрировать сценарий.
|
||||||
|
- Композировать модули.
|
||||||
|
- Решать, как устроен процесс.
|
||||||
|
- Содержать бизнес-логику.
|
||||||
|
- Содержать сценарную логику.
|
||||||
|
|
||||||
|
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
|
||||||
|
|
||||||
|
```text
|
||||||
|
auth/
|
||||||
|
├── ui/
|
||||||
|
│ └── logout-button/
|
||||||
|
│ ├── logout-button.tsx
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ └── logout-button.module.css
|
||||||
|
│ ├── types/
|
||||||
|
│ │ └── logout-button-props.type.ts
|
||||||
|
│ └── index.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что считается модулем
|
||||||
|
|
||||||
|
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
|
||||||
|
|
||||||
|
Примеры модулей:
|
||||||
|
|
||||||
|
- `screens/home/` — модуль страницы.
|
||||||
|
- `widgets/page-heading/` — модуль виджета.
|
||||||
|
- `business/auth/` — модуль бизнес-домена.
|
||||||
|
- `infra/theme/` — модуль инфраструктурного сервиса.
|
||||||
|
- `ui/button/` — модуль UI-kit сущности.
|
||||||
|
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
|
||||||
|
|
||||||
|
Не считаются модулями:
|
||||||
|
|
||||||
|
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
|
||||||
|
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
|
||||||
|
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||||
|
|
||||||
|
## Типы модулей
|
||||||
|
|
||||||
|
Тип модуля определяет обязательный корневой файл и стартовую структуру.
|
||||||
|
|
||||||
|
### UI-модуль
|
||||||
|
|
||||||
|
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
|
||||||
|
|
||||||
|
```text
|
||||||
|
header/
|
||||||
|
├── header.tsx
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
|
||||||
|
|
||||||
|
### Бизнес-модуль
|
||||||
|
|
||||||
|
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
|
||||||
|
|
||||||
|
Бизнес-модуль обязан иметь фабрику в корне:
|
||||||
|
|
||||||
|
```text
|
||||||
|
auth/
|
||||||
|
├── auth.factory.ts
|
||||||
|
├── index.ts
|
||||||
|
└── types/
|
||||||
|
```
|
||||||
|
|
||||||
|
Фабрика возвращает публичный runtime API модуля.
|
||||||
|
|
||||||
|
### Инфраструктурный модуль
|
||||||
|
|
||||||
|
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
|
||||||
|
|
||||||
|
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
|
||||||
|
|
||||||
|
```text
|
||||||
|
theme/
|
||||||
|
├── index.ts
|
||||||
|
├── config/
|
||||||
|
├── hooks/
|
||||||
|
├── styles/
|
||||||
|
└── ui/
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend-api/
|
||||||
|
├── backend-api.client.ts
|
||||||
|
├── config/
|
||||||
|
├── types/
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
|
||||||
|
|
||||||
|
```text
|
||||||
|
{module-name}/
|
||||||
|
├── {module-name}.factory.ts # фабрика (для business-модулей)
|
||||||
|
├── {module-name}.tsx # корневой файл модуля (опционален)
|
||||||
|
├── ui/ # компоненты модуля
|
||||||
|
├── parts/ # вложенные модули
|
||||||
|
├── hooks/ # хуки
|
||||||
|
├── stores/ # сторы состояния
|
||||||
|
├── services/ # внешние источники данных
|
||||||
|
├── mappers/ # трансформация данных между форматами
|
||||||
|
├── types/ # типы
|
||||||
|
├── styles/ # стили
|
||||||
|
├── lib/ # утилиты модуля
|
||||||
|
├── config/ # константы и конфигурация
|
||||||
|
└── index.ts # публичный API
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
|
||||||
|
|
||||||
|
## Публичный API
|
||||||
|
|
||||||
|
Внешний код импортирует модуль только через публичный API.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Хорошо
|
||||||
|
import { customerFactory } from '@/business/customer'
|
||||||
|
import type { Customer } from '@/business/customer'
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Плохо
|
||||||
|
import { validateToken } from '@/business/auth/lib/tokens'
|
||||||
|
```
|
||||||
|
|
||||||
|
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
|
||||||
|
|
||||||
|
Внутренние сегменты модуля остаются деталями реализации.
|
||||||
|
|
||||||
|
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/customer/index.ts
|
||||||
|
export { customerFactory } from './customer.factory'
|
||||||
|
|
||||||
|
export type { Customer } from './types/customer.type'
|
||||||
|
export type { CustomerApi } from './types/customer-api.type'
|
||||||
|
export type { CustomerDeps } from './types/customer-deps.type'
|
||||||
|
export type { CustomerFactory } from './types/customer-factory.type'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Фабрика
|
||||||
|
|
||||||
|
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
|
||||||
|
|
||||||
|
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
|
||||||
|
|
||||||
|
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью.
|
||||||
|
|
||||||
|
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||||
|
|
||||||
|
### Структура business-модуля
|
||||||
|
|
||||||
|
```text
|
||||||
|
business/customer/
|
||||||
|
├── customer.factory.ts
|
||||||
|
├── index.ts
|
||||||
|
└── types/
|
||||||
|
├── customer.type.ts
|
||||||
|
├── customer-api.type.ts
|
||||||
|
├── customer-deps.type.ts
|
||||||
|
└── customer-factory.type.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типы
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/customer/types/customer-api.type.ts
|
||||||
|
export type CustomerApi = {
|
||||||
|
useCustomer: () => Customer
|
||||||
|
CustomerCard: (props: CustomerCardProps) => ReactNode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/order/types/order-deps.type.ts
|
||||||
|
export type OrderDeps = {
|
||||||
|
customer: Pick<CustomerApi, 'useCustomer'>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/order/types/order-factory.type.ts
|
||||||
|
export type OrderFactory = (deps: OrderDeps) => OrderApi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фабрика без зависимостей
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/customer/customer.factory.ts
|
||||||
|
import type { CustomerFactory } from './types/customer-factory.type'
|
||||||
|
|
||||||
|
export const customerFactory: CustomerFactory = () => {
|
||||||
|
return {
|
||||||
|
useCustomer,
|
||||||
|
CustomerCard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фабрика с зависимостями
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// business/order/order.factory.ts
|
||||||
|
import type { OrderFactory } from './types/order-factory.type'
|
||||||
|
|
||||||
|
export const orderFactory: OrderFactory = (deps) => {
|
||||||
|
return {
|
||||||
|
useOrder,
|
||||||
|
OrderCard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Композиция на уровне screen
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// screens/home/home.screen.tsx
|
||||||
|
import { customerFactory } from '@/business/customer'
|
||||||
|
import { orderFactory } from '@/business/order'
|
||||||
|
|
||||||
|
const customer = customerFactory()
|
||||||
|
const order = orderFactory({ customer })
|
||||||
|
|
||||||
|
const { useOrder, OrderCard } = order
|
||||||
|
|
||||||
|
export const HomeScreen = () => {
|
||||||
|
const currentOrder = useOrder()
|
||||||
|
|
||||||
|
return <OrderCard order={currentOrder} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Жизненный цикл
|
||||||
|
|
||||||
|
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||||
|
|
||||||
|
- Нужен на одной странице → `screens/{name}/parts/`
|
||||||
|
- Появился в 2+ местах → поднимается по природе:
|
||||||
|
- абстрактный UI → `ui/`
|
||||||
|
- блок с данными/логикой → `widgets/`
|
||||||
|
- представление бизнес-домена → `business/{area}/parts/`
|
||||||
|
|
||||||
|
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
---
|
|
||||||
title: Модули
|
|
||||||
---
|
|
||||||
|
|
||||||
# Модули
|
|
||||||
|
|
||||||
Модули SLM: состав, границы, взаимодействие с остальным кодом.
|
|
||||||
|
|
||||||
## Определение
|
|
||||||
|
|
||||||
**Модуль — универсальный строительный блок архитектуры. Живёт на слое и содержит всё необходимое для своей работы: компоненты, хуки, сторы, сервисы, типы, стили. Набор содержимого не фиксирован — включаются только нужные части.**
|
|
||||||
|
|
||||||
## Модуль vs компонент
|
|
||||||
|
|
||||||
**Компонент** — один `.tsx` файл. Не имеет своих сегментов, использует сегменты родительского модуля. Живёт в корне или `ui/` сегменте модуля.
|
|
||||||
|
|
||||||
**Модуль** — папка, которая может содержать корневой компонент, сегменты (`hooks/`, `types/`, `styles/`, `ui/`, `parts/` и т.д.) и публичный API (`index.ts`).
|
|
||||||
|
|
||||||
```text
|
|
||||||
auth/
|
|
||||||
├── ui/
|
|
||||||
│ ├── auth-guard.tsx
|
|
||||||
│ └── logout-button.tsx
|
|
||||||
├── parts/
|
|
||||||
│ ├── login-form/
|
|
||||||
│ ├── registration-form/
|
|
||||||
│ └── restore-form/
|
|
||||||
├── hooks/
|
|
||||||
├── stores/
|
|
||||||
├── types/
|
|
||||||
├── auth.tsx # корневой компонент (опционален)
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
|
|
||||||
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль может состоять даже из одного `index.ts` с реэкспортом типов.
|
|
||||||
|
|
||||||
```text
|
|
||||||
{module-name}/
|
|
||||||
├── {module-name}.tsx # корневой компонент (опционален)
|
|
||||||
├── ui/ # компоненты модуля (только .tsx)
|
|
||||||
├── parts/ # вложенные модули (со своими сегментами)
|
|
||||||
├── hooks/ # хуки
|
|
||||||
├── stores/ # сторы состояния
|
|
||||||
├── services/ # внешние источники данных
|
|
||||||
├── mappers/ # трансформация данных между форматами
|
|
||||||
├── types/ # типы
|
|
||||||
├── styles/ # стили
|
|
||||||
├── lib/ # утилиты модуля
|
|
||||||
├── config/ # константы
|
|
||||||
└── index.ts # публичный API
|
|
||||||
```
|
|
||||||
|
|
||||||
Подробное описание каждого сегмента — в разделе [Сегменты](/docs/basics/architecture/reference/segments).
|
|
||||||
|
|
||||||
## Публичный API
|
|
||||||
|
|
||||||
Модуль экспортирует наружу только то, что нужно другим. Всё остальное — внутреннее.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// business/auth/index.ts
|
|
||||||
export type { User, Session } from './types/user.types'
|
|
||||||
export { useAuth } from './hooks/use-auth.hook'
|
|
||||||
export { AuthGuard } from './ui/auth-guard'
|
|
||||||
```
|
|
||||||
|
|
||||||
Импорт в обход `index.ts` запрещён:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Плохо
|
|
||||||
import { validateToken } from '@/business/auth/lib/tokens'
|
|
||||||
|
|
||||||
// Хорошо
|
|
||||||
import { useAuth } from '@/business/auth'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Фабрика
|
|
||||||
|
|
||||||
Если модуль зависит от кода другого бизнес-домена — он экспортирует фабрику. Фабрика декларирует необходимые зависимости и возвращает API модуля. Точка использования (screen, widget, layout) предоставляет зависимости при вызове.
|
|
||||||
|
|
||||||
Модуль без cross-domain зависимостей экспортирует API напрямую. Типы всегда экспортируются напрямую — `import type` не является runtime-зависимостью.
|
|
||||||
|
|
||||||
### Модуль без зависимостей — прямой экспорт:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// business/auth/index.ts
|
|
||||||
export { useAuth } from './hooks/use-auth'
|
|
||||||
export { useCurrentUser } from './hooks/use-current-user'
|
|
||||||
export type { User, Session } from './types'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Модуль с зависимостями — фабрика:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// business/chat/types/deps.ts
|
|
||||||
import type { User } from '@/business/auth'
|
|
||||||
|
|
||||||
export interface ChatDeps {
|
|
||||||
useCurrentUser: () => User | null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// business/chat/index.ts
|
|
||||||
import type { ChatDeps } from './types/deps'
|
|
||||||
|
|
||||||
export function chatFactory(deps: ChatDeps) {
|
|
||||||
return {
|
|
||||||
useMessages: (roomId: string) => {
|
|
||||||
const user = deps.useCurrentUser()
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
useSendMessage: (roomId: string) => {
|
|
||||||
const user = deps.useCurrentUser()
|
|
||||||
return (text: string) => { /* ... */ }
|
|
||||||
},
|
|
||||||
useChatRooms: () => {
|
|
||||||
const user = deps.useCurrentUser()
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
ChatBadge: ({ count }: { count: number }) => { /* ... */ },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Message, ChatRoom } from './types'
|
|
||||||
export type { ChatDeps } from './types/deps'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Использование на странице:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// screens/support/support.tsx
|
|
||||||
import { useCurrentUser } from '@/business/auth'
|
|
||||||
import { chatFactory } from '@/business/chat'
|
|
||||||
|
|
||||||
const chat = chatFactory({ useCurrentUser })
|
|
||||||
|
|
||||||
export function SupportScreen() {
|
|
||||||
const { useMessages, useSendMessage, ChatBadge } = chat
|
|
||||||
const messages = useMessages('support')
|
|
||||||
const sendMessage = useSendMessage('support')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ChatBadge count={messages.length} />
|
|
||||||
{messages.map(m => <MessageBubble key={m.id} {...m} />)}
|
|
||||||
<MessageInput onSend={sendMessage} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Жизненный цикл
|
|
||||||
|
|
||||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
|
||||||
|
|
||||||
- Нужен на одной странице → `screens/{name}/parts/`
|
|
||||||
- Появился в 2+ местах → поднимается по природе:
|
|
||||||
- абстрактный UI → `ui/`
|
|
||||||
- блок с данными/логикой → `widgets/`
|
|
||||||
- представление бизнес-домена → `business/{area}/parts/`
|
|
||||||
|
|
||||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
---
|
|
||||||
title: Сегменты
|
|
||||||
---
|
|
||||||
|
|
||||||
# Сегменты
|
# Сегменты
|
||||||
|
|
||||||
Сегменты SLM: типы, назначение, что лежит внутри каждого.
|
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
|
||||||
|
|
||||||
## Определение
|
## Определение
|
||||||
|
|
||||||
@@ -14,7 +10,7 @@ title: Сегменты
|
|||||||
|
|
||||||
| Сегмент | Содержимое |
|
| Сегмент | Содержимое |
|
||||||
|---------|------------|
|
|---------|------------|
|
||||||
| `ui/` | Компоненты модуля — только `.tsx` файлы |
|
| `ui/` | Презентационные компоненты родительского модуля |
|
||||||
| `parts/` | Вложенные модули со своими сегментами |
|
| `parts/` | Вложенные модули со своими сегментами |
|
||||||
| `hooks/` | React-хуки |
|
| `hooks/` | React-хуки |
|
||||||
| `stores/` | Сторы состояния |
|
| `stores/` | Сторы состояния |
|
||||||
@@ -27,24 +23,48 @@ title: Сегменты
|
|||||||
|
|
||||||
## Сегмент ui/
|
## Сегмент ui/
|
||||||
|
|
||||||
Компоненты, принадлежащие модулю. Содержит только `.tsx` файлы — без своих сегментов, стилей, типов, хуков. Использует сегменты родительского модуля.
|
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
|
||||||
|
|
||||||
|
Компонент в `ui/`:
|
||||||
|
|
||||||
|
- Находится в собственной папке.
|
||||||
|
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
|
||||||
|
- Не содержит любые компоненты.
|
||||||
|
- Не содержит любые модули.
|
||||||
|
- Не импортирует код проекта за пределами родительского модуля.
|
||||||
|
- Не делает внешние запросы.
|
||||||
|
- Не вызывает сценарные хуки.
|
||||||
|
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
|
||||||
|
- Не содержит бизнес-логику или сценарную логику.
|
||||||
|
|
||||||
|
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
|
||||||
|
|
||||||
|
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
auth/
|
user/
|
||||||
├── ui/
|
├── ui/
|
||||||
│ ├── auth-provider.tsx
|
│ ├── user-avatar/
|
||||||
│ ├── auth-guard.tsx
|
│ │ ├── user-avatar.tsx
|
||||||
│ └── logout-button.tsx
|
│ │ ├── styles/
|
||||||
|
│ │ │ └── user-avatar.module.css
|
||||||
|
│ │ ├── types/
|
||||||
|
│ │ │ └── user-avatar-props.type.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ └── user-status/
|
||||||
|
│ ├── user-status.tsx
|
||||||
|
│ └── index.ts
|
||||||
├── types/
|
├── types/
|
||||||
├── hooks/
|
├── hooks/
|
||||||
|
├── user.tsx
|
||||||
└── index.ts
|
└── index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Если компоненту нужны собственные сегменты — это уже не `ui/`, а `parts/`.
|
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`.
|
||||||
|
|
||||||
## Сегмент parts/
|
## Сегмент parts/
|
||||||
|
|
||||||
Вложенные модули со своими сегментами. Каждый элемент `parts/` — полноценный модуль: папка с компонентом, хуками, стилями, типами и т.д.
|
Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
home/
|
home/
|
||||||
@@ -52,17 +72,20 @@ home/
|
|||||||
│ ├── hero-section/
|
│ ├── hero-section/
|
||||||
│ │ ├── hero-section.tsx
|
│ │ ├── hero-section.tsx
|
||||||
│ │ ├── styles/
|
│ │ ├── styles/
|
||||||
│ │ └── parts/
|
│ │ ├── parts/
|
||||||
│ │ └── top-banner/
|
│ │ │ └── top-banner/
|
||||||
│ │ └── top-banner.tsx
|
│ │ │ ├── top-banner.tsx
|
||||||
|
│ │ │ └── index.ts
|
||||||
|
│ │ └── index.ts
|
||||||
│ └── features-section/
|
│ └── features-section/
|
||||||
│ ├── features-section.tsx
|
│ ├── features-section.tsx
|
||||||
│ └── hooks/
|
│ ├── hooks/
|
||||||
|
│ └── index.ts
|
||||||
├── home.screen.tsx
|
├── home.screen.tsx
|
||||||
└── index.ts
|
└── index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Отличие от `ui/`: элемент `parts/` — модуль со своими сегментами. Элемент `ui/` — компонент, один `.tsx` файл.
|
Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности.
|
||||||
|
|
||||||
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Стиль кода
|
title: Стиль кода
|
||||||
|
description: Как оформляется код в проекте.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Стиль кода
|
# Стиль кода
|
||||||
|
|
||||||
Единые правила оформления кода: форматирование, импорты, читаемость.
|
Как оформляется код в проекте.
|
||||||
|
|
||||||
## Отступы
|
## Отступы
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Документирование
|
title: Документирование
|
||||||
|
description: Что и как документировать в коде.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Документирование
|
# Документирование
|
||||||
|
|
||||||
Правила документирования кода: что и когда документировать через JSDoc.
|
Что и как документировать в коде.
|
||||||
|
|
||||||
## Общие правила
|
## Общие правила
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Именование
|
title: Именование
|
||||||
|
description: Как называть переменные, файлы и прочие сущности в коде.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Именование
|
# Именование
|
||||||
|
|
||||||
Соглашения об именовании в коде: что и как называть.
|
Как называть переменные, файлы и прочие сущности в коде.
|
||||||
|
|
||||||
## Базовые правила
|
## Базовые правила
|
||||||
|
|
||||||
@@ -33,6 +34,12 @@ title: Именование
|
|||||||
- `.store.ts` — стор
|
- `.store.ts` — стор
|
||||||
- `.service.ts` — сервис
|
- `.service.ts` — сервис
|
||||||
|
|
||||||
|
**Корневые компоненты слоёв**
|
||||||
|
- `.screen.tsx` — корневой компонент screen-модуля: `screens/profile/profile.screen.tsx`, компонент `ProfileScreen`
|
||||||
|
- `.layout.tsx` — корневой компонент layout-модуля: `layouts/main/main.layout.tsx`, компонент `MainLayout`
|
||||||
|
|
||||||
|
Обычные и вложенные модули не получают суффикс слоя: `ui/button/button.tsx`, `screens/profile/parts/activity-feed/activity-feed.tsx`.
|
||||||
|
|
||||||
**Типы и контракты**
|
**Типы и контракты**
|
||||||
- `.type.ts` — типы и интерфейсы
|
- `.type.ts` — типы и интерфейсы
|
||||||
- `.interface.ts` — интерфейсы
|
- `.interface.ts` — интерфейсы
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Технологии и библиотеки
|
title: Технологии и библиотеки
|
||||||
|
description: Какие библиотеки и инструменты используются в проекте.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Технологии и библиотеки
|
# Технологии и библиотеки
|
||||||
|
|
||||||
Базовый стек проекта по областям: UI, архитектура, данные, состояние, локализация, тестирование, стили, генерация кода.
|
Какие библиотеки и инструменты используются в проекте.
|
||||||
|
|
||||||
## Что используем
|
## Что используем
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
---
|
---
|
||||||
title: Типизация
|
title: Типизация
|
||||||
|
description: Как типизируется код в проекте.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Типизация
|
# Типизация
|
||||||
|
|
||||||
Правила типизации в TypeScript: общие принципы и работа с динамическими типами.
|
Как типизируется код в проекте.
|
||||||
|
|
||||||
## Общие правила
|
## Общие правила
|
||||||
|
|
||||||
- Указывать типы для параметров компонентов, возвращаемых значений и параметров функций.
|
- Указывать типы для параметров компонентов и параметров функций.
|
||||||
- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов.
|
- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов.
|
||||||
- Избегать `any` и `unknown` без необходимости.
|
- Избегать `any` и `unknown` без необходимости.
|
||||||
- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины.
|
- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины.
|
||||||
|
|
||||||
|
## React-компоненты
|
||||||
|
|
||||||
|
- Пропсы компонента типизировать через отдельный `Props`.
|
||||||
|
- Возвращаемый тип компонента не указывать: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
|
||||||
|
|
||||||
## Функции
|
## Функции
|
||||||
|
|
||||||
- Для публичных функций указывать возвращаемый тип.
|
- Для публичных функций указывать возвращаемый тип.
|
||||||
|
|||||||
51
docs/docs/creating-project/from-template.md
Normal file
51
docs/docs/creating-project/from-template.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Создание проекта из шаблона
|
||||||
|
description: Создание нового проекта на основе готового шаблона.
|
||||||
|
keywords: [создать проект из шаблона, шаблон, template, tiged, degit, клонировать шаблон, эталонный шаблон, быстрый старт, scaffold, новый проект]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Создание проекта из шаблона
|
||||||
|
|
||||||
|
Создание нового проекта на основе готового шаблона.
|
||||||
|
|
||||||
|
## Что внутри
|
||||||
|
|
||||||
|
Шаблон — готовый скелет проекта с применёнными правилами стайлгайда:
|
||||||
|
|
||||||
|
- **Стек:** Next.js (App Router), TypeScript, React.
|
||||||
|
- **Архитектура:** структура папок по SLM, алиасы импортов.
|
||||||
|
- **Качество кода:** Biome (линтер и форматер), настройки VS Code.
|
||||||
|
- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты.
|
||||||
|
- **Ассеты:** генерация SVG-спрайтов.
|
||||||
|
- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов.
|
||||||
|
в
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Склонировать шаблон в родительском каталоге будущего проекта:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tiged git@gromlab.ru:templates/nextjs.git my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
`tiged` копирует снимок репозитория без истории git. Имя каталога (`my-app`) заменяется на нужное.
|
||||||
|
|
||||||
|
2. Установить зависимости:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd my-app
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Проверить сборку:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка должна завершиться без ошибок.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- **Шаблон — источник истины.** Не добавлять, не удалять и не переименовывать файлы шаблона «для приведения к канону»: шаблон уже канонический. Любое несоответствие — баг шаблона, а не проекта.
|
||||||
|
- **Менеджер пакетов — npm.** Отклонение (pnpm, yarn, bun) — только по явному решению с пониманием, что стайлгайд этого не предусматривает.
|
||||||
|
- **Не инициализировать git заново** автоматически. `tiged` намеренно не создаёт `.git/` — решение о репозитории принимает разработчик.
|
||||||
90
docs/docs/creating-project/manual.md
Normal file
90
docs/docs/creating-project/manual.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: Создание проекта вручную
|
||||||
|
description: Поэтапное создание нового проекта без использования шаблона.
|
||||||
|
keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Создание проекта вручную
|
||||||
|
|
||||||
|
Поэтапное создание нового проекта без использования шаблона.
|
||||||
|
|
||||||
|
## Состав эталонного проекта
|
||||||
|
|
||||||
|
| Компонент | Роль | Раздел |
|
||||||
|
|-----------|------|--------|
|
||||||
|
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/creating-project/nextjs) |
|
||||||
|
| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/applied/aliases) |
|
||||||
|
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/applied/biome) |
|
||||||
|
| Стили | Глобальные токены и breakpoints | [Стили](/docs/applied/styles/styles-setup) |
|
||||||
|
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/docs/applied/postcss) |
|
||||||
|
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) |
|
||||||
|
| VS Code | Настройки редактора и расширения | [VS Code](/docs/applied/vscode) |
|
||||||
|
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/docs/applied/templates/templates-setup) |
|
||||||
|
|
||||||
|
Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном.
|
||||||
|
|
||||||
|
## Канон раскладки
|
||||||
|
|
||||||
|
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)).
|
||||||
|
|
||||||
|
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
|
||||||
|
|
||||||
|
## Порядок установки
|
||||||
|
|
||||||
|
Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами.
|
||||||
|
|
||||||
|
### 1. Next.js
|
||||||
|
|
||||||
|
Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
|
||||||
|
|
||||||
|
См. [Next.js](/docs/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить.
|
||||||
|
|
||||||
|
### 2. Алиасы
|
||||||
|
|
||||||
|
Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов.
|
||||||
|
|
||||||
|
См. [Алиасы](/docs/applied/aliases).
|
||||||
|
|
||||||
|
### 3. Biome
|
||||||
|
|
||||||
|
Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки.
|
||||||
|
|
||||||
|
См. [Biome](/docs/applied/biome).
|
||||||
|
|
||||||
|
### 4. Стили (базовая инфраструктура)
|
||||||
|
|
||||||
|
Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится.
|
||||||
|
|
||||||
|
См. [Стили](/docs/applied/styles/styles-setup).
|
||||||
|
|
||||||
|
### 5. PostCSS
|
||||||
|
|
||||||
|
CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`.
|
||||||
|
|
||||||
|
См. [PostCSS](/docs/applied/postcss).
|
||||||
|
|
||||||
|
### 6. SVG-спрайты
|
||||||
|
|
||||||
|
Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента `<SvgSprite/>`.
|
||||||
|
|
||||||
|
См. [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup).
|
||||||
|
|
||||||
|
### 7. VS Code
|
||||||
|
|
||||||
|
Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`).
|
||||||
|
|
||||||
|
См. [VS Code](/docs/applied/vscode).
|
||||||
|
|
||||||
|
### 8. Шаблоны генерации
|
||||||
|
|
||||||
|
Папка `.templates/` для генератора модулей `@gromlab/create`.
|
||||||
|
|
||||||
|
См. [Шаблоны генерации](/docs/applied/templates/templates-setup).
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome).
|
||||||
|
- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден.
|
||||||
|
- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
|
||||||
|
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
|
||||||
|
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.
|
||||||
112
docs/docs/creating-project/nextjs.md
Normal file
112
docs/docs/creating-project/nextjs.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: Чистая установка Next.js
|
||||||
|
description: "Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку."
|
||||||
|
keywords: [next.js, create-next-app, npx, установка, инициализация, фреймворк, скаффолдинг, app router, typescript]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Чистая установка Next.js
|
||||||
|
|
||||||
|
Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Node.js 18.18+ (рекомендуется LTS 20+).
|
||||||
|
- npm 10+.
|
||||||
|
- Рабочая папка пуста, либо для установки выбрана подпапка (`create-next-app@16+` отказывается ставиться в непустую директорию).
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### 1. Инициализация через `create-next-app`
|
||||||
|
|
||||||
|
Флаги зафиксированы и не согласовываются — это канон стайлгайда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-next-app@latest my-app \
|
||||||
|
--typescript \
|
||||||
|
--app \
|
||||||
|
--src-dir \
|
||||||
|
--import-alias "@/*" \
|
||||||
|
--no-eslint \
|
||||||
|
--no-tailwind \
|
||||||
|
--use-npm
|
||||||
|
```
|
||||||
|
|
||||||
|
| Флаг | Значение | Почему так |
|
||||||
|
|------|----------|------------|
|
||||||
|
| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](/docs/basics/typing)) |
|
||||||
|
| `--app` | App Router | Pages Router не используется |
|
||||||
|
| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](/docs/applied/project-structure)) |
|
||||||
|
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/docs/applied/aliases)) |
|
||||||
|
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/docs/applied/biome)) |
|
||||||
|
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/docs/applied/styles/styles-usage)) |
|
||||||
|
| `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах |
|
||||||
|
|
||||||
|
### 2. Очистить дефолтный шаблон
|
||||||
|
|
||||||
|
`create-next-app` генерирует демо-страницу со стилями и ассетами, а Next.js 16+ дополнительно кладёт в корень собственные `AGENTS.md` и `CLAUDE.md` — всё это удаляется.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm src/app/page.module.css
|
||||||
|
rm src/app/globals.css
|
||||||
|
rm public/next.svg public/vercel.svg public/file.svg public/globe.svg public/window.svg
|
||||||
|
rm -f AGENTS.md CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Заменить `src/app/page.tsx` на минимальный:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/page.tsx
|
||||||
|
export default function HomePage() {
|
||||||
|
return <h1>Home</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Очистить `src/app/layout.tsx` от импорта шрифтов и `globals.css`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/layout.tsx
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'App',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="ru">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Создать папку `src/shared/styles/`
|
||||||
|
|
||||||
|
Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](/docs/applied/project-structure)).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p src/shared/styles
|
||||||
|
```
|
||||||
|
|
||||||
|
Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы.
|
||||||
|
- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает.
|
||||||
|
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/docs/applied/aliases)).
|
||||||
|
- **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся.
|
||||||
|
- **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе.
|
||||||
|
|
||||||
|
## Проверка установки
|
||||||
|
|
||||||
|
- В корне проекта: `next.config.ts`, `tsconfig.json`, `package.json`.
|
||||||
|
- В `package.json`: Next.js установлен, нет `eslint`, `tailwindcss`.
|
||||||
|
- В `src/app/` присутствуют минимальные `page.tsx` и `layout.tsx`. `globals.css`, `page.module.css` отсутствуют. Каталогов `styles/`, `assets/`, `providers/`, `components/` в `src/app/` нет.
|
||||||
|
- Папка `src/shared/styles/` создана (пустая).
|
||||||
|
- В `src/` из слоёв SLM присутствуют только `app/` и `shared/` (с `styles/`). Посторонних каталогов нет.
|
||||||
|
- В `public/` удалены `next.svg`, `vercel.svg`, `file.svg`, `globe.svg`, `window.svg`.
|
||||||
|
- В корне проекта нет `AGENTS.md` и `CLAUDE.md` от Next.js.
|
||||||
|
- `npm run build` завершается успешно.
|
||||||
|
- Пакетный менеджер — npm (нет `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`).
|
||||||
60
docs/docs/data/index.md
Normal file
60
docs/docs/data/index.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Источники данных
|
||||||
|
description: Какие источники данных используются в проекте и как с ними работать.
|
||||||
|
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Источники данных
|
||||||
|
|
||||||
|
Какие источники данных используются в проекте и как с ними работать.
|
||||||
|
|
||||||
|
## Принципы раздела
|
||||||
|
|
||||||
|
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`.
|
||||||
|
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
|
||||||
|
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
|
||||||
|
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
|
||||||
|
|
||||||
|
## Карта раздела
|
||||||
|
|
||||||
|
### REST
|
||||||
|
|
||||||
|
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
|
||||||
|
|
||||||
|
- [REST](/docs/data/rest/) — обзор раздела: создание клиента и использование.
|
||||||
|
- **Создание клиента** — как оформляется REST API в проекте:
|
||||||
|
- [Обзор](/docs/data/rest/clients/) — когда нужен клиент и как выбрать подход.
|
||||||
|
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
|
||||||
|
- [Ручное создание](/docs/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками.
|
||||||
|
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks) — прозрачные SWR-обёртки над GET-методами клиента.
|
||||||
|
- **Использование** — как получать данные через готовый клиент:
|
||||||
|
- [Стратегии получения данных](/docs/data/rest/strategies/) — как выбрать способ получения данных под ситуацию.
|
||||||
|
- [Серверный await](/docs/data/rest/strategies/server-await) — прямой `await` метода клиента в Server Components.
|
||||||
|
- [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests) — запуск независимых серверных запросов без waterfall.
|
||||||
|
- [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down) — серверный стриминг через промис и `Suspense`.
|
||||||
|
- [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data) — серверный промис в `SWRConfig fallback`.
|
||||||
|
- [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook) — получение данных в Client Components через готовый GET-хук.
|
||||||
|
- [Business-композиция](/docs/data/rest/strategies/business-composition) — доменная интерпретация и композиция REST-данных.
|
||||||
|
|
||||||
|
### Realtime
|
||||||
|
|
||||||
|
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||||
|
|
||||||
|
- [Realtime](/docs/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки.
|
||||||
|
|
||||||
|
## Что даёт раздел
|
||||||
|
|
||||||
|
После прочтения раздела понятно:
|
||||||
|
|
||||||
|
- Где живёт код работы с API и почему именно там.
|
||||||
|
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
|
||||||
|
- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infrastructure/{service-name}/hooks/`.
|
||||||
|
- Как выбрать стратегию получения REST-данных под конкретную ситуацию.
|
||||||
|
- Как подключать realtime-источники в общую модель работы с данными.
|
||||||
|
- Какие правила обязательны и какие отклонения допустимы.
|
||||||
|
|
||||||
|
## Что не входит в раздел
|
||||||
|
|
||||||
|
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/applied/stores).
|
||||||
|
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/).
|
||||||
|
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Отдельный прикладной раздел для них пока не ведётся.
|
||||||
79
docs/docs/data/realtime.md
Normal file
79
docs/docs/data/realtime.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Realtime
|
||||||
|
description: "Работа с push-данными от сервера: подписки и события."
|
||||||
|
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Realtime
|
||||||
|
|
||||||
|
Работа с push-данными от сервера: подписки и события.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
|
||||||
|
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
|
||||||
|
- **Использование на клиенте — два сценария:**
|
||||||
|
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
|
||||||
|
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
|
||||||
|
|
||||||
|
## Размещение клиента
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── {channel-name}/
|
||||||
|
├── connection.ts # установление соединения, реконнект
|
||||||
|
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
|
||||||
|
├── types.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование через SWR
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import useSWRSubscription from 'swr/subscription'
|
||||||
|
import { subscribe } from 'infrastructure/notifications'
|
||||||
|
|
||||||
|
export function NotificationCounter() {
|
||||||
|
const { data: count } = useSWRSubscription(
|
||||||
|
['notifications', 'count'],
|
||||||
|
(key, { next }) =>
|
||||||
|
subscribe('notifications.count', (value: number) => next(null, value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return <span>{count ?? 0}</span>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
|
||||||
|
|
||||||
|
## Прямая подписка
|
||||||
|
|
||||||
|
Для побочных эффектов, которые не влияют на состояние UI напрямую:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { subscribe } from 'infrastructure/notifications'
|
||||||
|
import { showToast } from 'ui/toast'
|
||||||
|
|
||||||
|
export function NotificationsToaster() {
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribe('notifications.new', (notification) => {
|
||||||
|
showToast(notification.message)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
|
||||||
|
|
||||||
|
## Запрет прямых соединений
|
||||||
|
|
||||||
|
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`.
|
||||||
|
|
||||||
|
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.
|
||||||
193
docs/docs/data/rest/clients/auto.md
Normal file
193
docs/docs/data/rest/clients/auto.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
title: Автогенерация из OpenAPI
|
||||||
|
description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
|
||||||
|
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Автогенерация из OpenAPI
|
||||||
|
|
||||||
|
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
|
||||||
|
|
||||||
|
## Пример API
|
||||||
|
|
||||||
|
В примерах используется Swagger Petstore:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://petstore3.swagger.io/api/v3/openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Имена модуля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/pet-store-api/
|
||||||
|
petStoreApi
|
||||||
|
pet-store-api.generated.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Скрипт генерации
|
||||||
|
|
||||||
|
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infrastructure/pet-store-api/generated -n pet-store-api.generated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
|
||||||
|
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
|
||||||
|
- `-o` — директория для сгенерированного файла.
|
||||||
|
- `-n` — имя сгенерированного файла без `.ts`.
|
||||||
|
|
||||||
|
Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции.
|
||||||
|
|
||||||
|
## Генерация
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run codegen:pet-store-api
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/pet-store-api/generated/
|
||||||
|
└── pet-store-api.generated.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Сгенерированный файл не правится руками и коммитится в репозиторий.
|
||||||
|
|
||||||
|
## Проверка методов
|
||||||
|
|
||||||
|
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
|
||||||
|
|
||||||
|
Для Petstore нужны GET-операции вида:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
petStoreApi.pet.findPetsByStatus(...)
|
||||||
|
petStoreApi.pet.getPetById(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
|
||||||
|
|
||||||
|
## `client.ts`
|
||||||
|
|
||||||
|
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/client.ts
|
||||||
|
import { Api, HttpClient } from './generated/pet-store-api.generated'
|
||||||
|
|
||||||
|
const httpClient = new HttpClient({
|
||||||
|
baseUrl: 'https://petstore3.swagger.io/api/v3',
|
||||||
|
baseApiParams: {
|
||||||
|
secure: false,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const petStoreApi = new Api(httpClient)
|
||||||
|
```
|
||||||
|
|
||||||
|
В реальном проекте вместо строки `baseUrl` обычно берётся из env-переменной, нормализуется в `client.ts` и передаётся в `HttpClient` через конфиг.
|
||||||
|
|
||||||
|
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
|
||||||
|
|
||||||
|
## Расширение сгенерированных типов
|
||||||
|
|
||||||
|
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/biocad-less-api/
|
||||||
|
├── generated/
|
||||||
|
│ └── biocad-less-api.generated.ts
|
||||||
|
├── types/
|
||||||
|
│ ├── term.ts
|
||||||
|
│ └── index.ts
|
||||||
|
├── client.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример расширения generated-типа:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/biocad-less-api/types/term.ts
|
||||||
|
import type { TermRecordItem } from '../generated/biocad-less-api.generated'
|
||||||
|
|
||||||
|
declare module '../generated/biocad-less-api.generated' {
|
||||||
|
interface TermRecordItem {
|
||||||
|
media?: {
|
||||||
|
file?: string
|
||||||
|
title?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TermRecordItemExtended = Omit<
|
||||||
|
TermRecordItem,
|
||||||
|
'categories' | 'tags' | 'fields'
|
||||||
|
> & {
|
||||||
|
categories?: Array<{
|
||||||
|
_id?: string
|
||||||
|
id?: string
|
||||||
|
slug?: string
|
||||||
|
name?: string
|
||||||
|
}>
|
||||||
|
tags?: Array<{
|
||||||
|
_id?: string
|
||||||
|
id?: string
|
||||||
|
slug?: string
|
||||||
|
name?: string
|
||||||
|
}>
|
||||||
|
fields?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/biocad-less-api/types/index.ts
|
||||||
|
export type { TermRecordItemExtended } from './term'
|
||||||
|
```
|
||||||
|
|
||||||
|
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
|
||||||
|
|
||||||
|
## Публичный API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/index.ts
|
||||||
|
export { petStoreApi } from './client'
|
||||||
|
export type { Pet } from './generated/pet-store-api.generated'
|
||||||
|
export * from './hooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
Наружу импортируют только из `infrastructure/pet-store-api`, не из `generated/`.
|
||||||
|
|
||||||
|
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/biocad-less-api/index.ts
|
||||||
|
export type { TermRecordItemExtended } from './types'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Регенерация
|
||||||
|
|
||||||
|
При изменении OpenAPI-схемы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run codegen:pet-store-api
|
||||||
|
```
|
||||||
|
|
||||||
|
Что меняется:
|
||||||
|
|
||||||
|
- `generated/pet-store-api.generated.ts` — перезаписывается генератором.
|
||||||
|
- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
|
||||||
|
|
||||||
|
Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля.
|
||||||
|
|
||||||
|
## Следующий шаг
|
||||||
|
|
||||||
|
После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](/docs/data/rest/clients/hooks) для Client Components.
|
||||||
206
docs/docs/data/rest/clients/hooks.md
Normal file
206
docs/docs/data/rest/clients/hooks.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
---
|
||||||
|
title: GET-хуки REST-клиента
|
||||||
|
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||||
|
keywords: [rest, swr, get-хуки, client components, infrastructure]
|
||||||
|
---
|
||||||
|
|
||||||
|
# GET-хуки REST-клиента
|
||||||
|
|
||||||
|
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую.
|
||||||
|
|
||||||
|
## Где лежат
|
||||||
|
|
||||||
|
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── pet-store-api/
|
||||||
|
├── client.ts
|
||||||
|
├── generated/
|
||||||
|
├── hooks/
|
||||||
|
│ ├── use-get-pet-list.hook.ts
|
||||||
|
│ ├── use-get-pet-detail.hook.ts
|
||||||
|
│ └── index.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Контракт
|
||||||
|
|
||||||
|
- Один GET-хук = один GET-метод клиента.
|
||||||
|
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
||||||
|
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
||||||
|
- Хук принимает только параметры GET-метода и `config?: SWRConfiguration`.
|
||||||
|
- Что передали хуку, то он передаёт в GET-метод.
|
||||||
|
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
||||||
|
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
||||||
|
- Хук не объединяет несколько запросов.
|
||||||
|
- Хук не маппит DTO в доменную модель.
|
||||||
|
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||||
|
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
||||||
|
|
||||||
|
## Пример списка
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petStoreApi } from '../client'
|
||||||
|
import type { Pet } from '../generated/pet-store-api.generated'
|
||||||
|
|
||||||
|
export type PetStatus = 'available' | 'pending' | 'sold'
|
||||||
|
|
||||||
|
export const getPetListKey = (status: PetStatus) =>
|
||||||
|
['pet-store-api', 'pet', 'list', status] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка питомцев по статусу.
|
||||||
|
*/
|
||||||
|
export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => {
|
||||||
|
const isReady = status !== null
|
||||||
|
const key = isReady ? getPetListKey(status) : null
|
||||||
|
const fetcher = () => petStoreApi.pet.findPetsByStatus({ status })
|
||||||
|
|
||||||
|
return useSWR<Pet[]>(key, fetcher, config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Функция `getPetListKey` нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через `SWRConfig fallback`.
|
||||||
|
|
||||||
|
Пример начальных данных для клиентского хука:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { SWRConfig, unstable_serialize } from 'swr'
|
||||||
|
import {
|
||||||
|
getPetListKey,
|
||||||
|
petStoreApi,
|
||||||
|
} from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
export default function PetsLayout({ children }: { children: ReactNode }) {
|
||||||
|
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fallback: {
|
||||||
|
[unstable_serialize(getPetListKey('available'))]: petsPromise,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SWRConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { data: pets } = useGetPetList('available')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Пример detail-запроса
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petStoreApi } from '../client'
|
||||||
|
import type { Pet } from '../generated/pet-store-api.generated'
|
||||||
|
|
||||||
|
export const getPetDetailKey = (id: number) =>
|
||||||
|
['pet-store-api', 'pet', 'detail', id] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение питомца по идентификатору.
|
||||||
|
*/
|
||||||
|
export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => {
|
||||||
|
const isReady = id !== null
|
||||||
|
const key = isReady ? getPetDetailKey(id) : null
|
||||||
|
const fetcher = () => petStoreApi.pet.getPetById(id)
|
||||||
|
|
||||||
|
return useSWR<Pet>(key, fetcher, config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Отложенный запрос через `null`
|
||||||
|
|
||||||
|
GET-хук может принимать `null` для обязательного параметра. `null` означает, что параметр ещё не готов и запрос выполнять нельзя.
|
||||||
|
|
||||||
|
Внутри хука это выражается через `isReady`: если параметр не готов, ключ SWR становится `null`, и SWR не вызывает fetcher.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const isReady = id !== null
|
||||||
|
const key = isReady ? getPetDetailKey(id) : null
|
||||||
|
```
|
||||||
|
|
||||||
|
`null` не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через `SWRConfig fallback`.
|
||||||
|
|
||||||
|
Для числовых идентификаторов не используйте проверку `if (id)`: значение `0` тоже валидное число. Проверяйте явно: `id !== null`.
|
||||||
|
|
||||||
|
## Экспорт
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/hooks/index.ts
|
||||||
|
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
|
||||||
|
export type { PetStatus } from './use-get-pet-list.hook'
|
||||||
|
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/index.ts
|
||||||
|
export { petStoreApi } from './client'
|
||||||
|
export type { Pet } from './generated/pet-store-api.generated'
|
||||||
|
export * from './hooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Где заканчивается infrastructure
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Хорошо: infrastructure, прозрачный GET-хук
|
||||||
|
const { data: pets } = useGetPetList('available')
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Хорошо: business, доменная интерпретация
|
||||||
|
export const useAvailablePets = () => {
|
||||||
|
const query = useGetPetList('available')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
hasPets: Boolean(query.data?.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||||||
|
|
||||||
|
## Что запрещено
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Плохо — useSWR в компоненте
|
||||||
|
const { data } = useSWR(
|
||||||
|
['pet-store-api', 'pet', 'list', status],
|
||||||
|
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Плохо — несколько GET внутри infrastructure-хука
|
||||||
|
export const usePetDashboard = () => {
|
||||||
|
const available = useGetPetList('available')
|
||||||
|
const sold = useGetPetList('sold')
|
||||||
|
|
||||||
|
return { available, sold }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||||
|
export const useGetPetList = (status: PetStatus) => {
|
||||||
|
const query = useSWR(...)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
hasPets: Boolean(query.data?.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook).
|
||||||
75
docs/docs/data/rest/clients/index.md
Normal file
75
docs/docs/data/rest/clients/index.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Создание клиента
|
||||||
|
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API.
|
||||||
|
keywords: [rest, клиент, infrastructure, методы, openapi, get-хуки, swr]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Создание клиента
|
||||||
|
|
||||||
|
REST-клиент — это infrastructure-модуль, через который проект работает с внешним REST API.
|
||||||
|
|
||||||
|
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов.
|
||||||
|
|
||||||
|
## Из чего состоит клиент
|
||||||
|
|
||||||
|
REST-клиент состоит из трёх основных частей:
|
||||||
|
|
||||||
|
1. **Клиент** — самописная оболочка над транспортом.
|
||||||
|
2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API.
|
||||||
|
3. **GET-хуки** — SWR-обёртки для GET-запросов.
|
||||||
|
|
||||||
|
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
|
||||||
|
|
||||||
|
## Клиент
|
||||||
|
|
||||||
|
Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса.
|
||||||
|
|
||||||
|
Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
|
||||||
|
|
||||||
|
`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика.
|
||||||
|
|
||||||
|
## Методы
|
||||||
|
|
||||||
|
Методы описывают конкретные запросы к API.
|
||||||
|
|
||||||
|
Они появляются одним из двух способов:
|
||||||
|
|
||||||
|
- генерируются из OpenAPI в `generated/`;
|
||||||
|
- создаются вручную в `methods/`.
|
||||||
|
|
||||||
|
Подробности:
|
||||||
|
|
||||||
|
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto)
|
||||||
|
- [Ручное создание](/docs/data/rest/clients/manual)
|
||||||
|
|
||||||
|
## GET-хуки
|
||||||
|
|
||||||
|
Для GET-запросов добавляются GET-хуки REST-клиента.
|
||||||
|
|
||||||
|
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
|
||||||
|
|
||||||
|
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
|
||||||
|
|
||||||
|
Подробности:
|
||||||
|
|
||||||
|
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks)
|
||||||
|
|
||||||
|
## Структура модуля
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/{service-name}/
|
||||||
|
├── client.ts # самописная оболочка и инстанс клиента
|
||||||
|
├── generated/ или methods/ # методы API
|
||||||
|
├── hooks/ # GET-хуки REST-клиента
|
||||||
|
├── types/ # DTO, типы API и расширения типов
|
||||||
|
├── errors/ # ошибки API, если нужны
|
||||||
|
└── index.ts # публичный API
|
||||||
|
```
|
||||||
|
|
||||||
|
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
|
||||||
|
|
||||||
|
## Что делаем дальше
|
||||||
|
|
||||||
|
1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/data/rest/clients/auto) или [Ручное создание](/docs/data/rest/clients/manual).
|
||||||
|
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/data/rest/clients/hooks).
|
||||||
|
3. После создания клиента переходите к [Стратегиям получения данных](/docs/data/rest/strategies/).
|
||||||
187
docs/docs/data/rest/clients/manual.md
Normal file
187
docs/docs/data/rest/clients/manual.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
---
|
||||||
|
title: Ручное создание
|
||||||
|
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||||
|
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrastructure]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ручное создание
|
||||||
|
|
||||||
|
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||||
|
|
||||||
|
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
|
||||||
|
|
||||||
|
## Что нужно создать
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── pet-project-api/
|
||||||
|
├── methods/
|
||||||
|
│ └── posts.ts
|
||||||
|
├── hooks/
|
||||||
|
│ └── index.ts
|
||||||
|
├── types/
|
||||||
|
│ ├── client.ts
|
||||||
|
│ ├── post.ts
|
||||||
|
│ └── index.ts
|
||||||
|
├── errors/
|
||||||
|
│ └── pet-project-api.error.ts
|
||||||
|
├── client.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
| Файл | Роль |
|
||||||
|
|------|------|
|
||||||
|
| `client.ts` | Базовый транспорт и создание инстанса клиента |
|
||||||
|
| `methods/` | Методы API по сущностям |
|
||||||
|
| `types/` | DTO запросов, ответов и типы клиента |
|
||||||
|
| `errors/` | Ошибки конкретного API |
|
||||||
|
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
|
||||||
|
| `index.ts` | Публичный API REST-модуля |
|
||||||
|
|
||||||
|
## DTO и типы API
|
||||||
|
|
||||||
|
DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/post.ts
|
||||||
|
export type PostDto = {
|
||||||
|
id: string
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostListQueryDto = {
|
||||||
|
limit?: number
|
||||||
|
category?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/index.ts
|
||||||
|
export type { PostDto, PostListQueryDto } from './post'
|
||||||
|
```
|
||||||
|
|
||||||
|
Типы, которые нужны только базовому транспорту, можно держать отдельно:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/client.ts
|
||||||
|
export type QueryParams = Record<string, string | number | boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ошибка API
|
||||||
|
|
||||||
|
Ошибка API тоже относится к REST-модулю.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
|
||||||
|
export class PetProjectApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly status: number,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'PetProjectApiError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Базовый клиент
|
||||||
|
|
||||||
|
`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/client.ts
|
||||||
|
import { PetProjectApiError } from './errors/pet-project-api.error'
|
||||||
|
import type { QueryParams } from './types/client'
|
||||||
|
|
||||||
|
export class PetProjectApiClient {
|
||||||
|
constructor(
|
||||||
|
private readonly baseUrl: string,
|
||||||
|
private readonly defaultHeaders: Record<string, string> = {},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async get<T>(path: string, params: QueryParams = {}): Promise<T> {
|
||||||
|
const base = `${this.baseUrl.replace(/\/+$/, '')}/`
|
||||||
|
const url = new URL(path.replace(/^\/+/, ''), base)
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
url.searchParams.set(key, String(value))
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...this.defaultHeaders,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new PetProjectApiError(response.status, response.statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API.
|
||||||
|
|
||||||
|
## Методы API
|
||||||
|
|
||||||
|
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/methods/posts.ts
|
||||||
|
import type { PetProjectApiClient } from '../client'
|
||||||
|
import type { PostDto, PostListQueryDto } from '../types/post'
|
||||||
|
|
||||||
|
export function postsMethods(client: PetProjectApiClient) {
|
||||||
|
return {
|
||||||
|
/** GET /posts */
|
||||||
|
list: (query: PostListQueryDto = {}) =>
|
||||||
|
client.get<PostDto[]>('posts', query),
|
||||||
|
|
||||||
|
/** GET /posts/{slug} */
|
||||||
|
get: (slug: string) =>
|
||||||
|
client.get<PostDto>(`posts/${slug}`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`.
|
||||||
|
|
||||||
|
## Публичный API
|
||||||
|
|
||||||
|
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/index.ts
|
||||||
|
import { PetProjectApiClient } from './client'
|
||||||
|
import { postsMethods } from './methods/posts'
|
||||||
|
|
||||||
|
const client = new PetProjectApiClient(
|
||||||
|
process.env.NEXT_PUBLIC_API_URL ?? '',
|
||||||
|
{ 'Content-Type': 'application/json' },
|
||||||
|
)
|
||||||
|
|
||||||
|
export const petProjectApi = {
|
||||||
|
posts: postsMethods(client),
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PetProjectApiError } from './errors/pet-project-api.error'
|
||||||
|
export type { PostDto, PostListQueryDto } from './types'
|
||||||
|
export * from './hooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
Внешний код импортирует только из `infrastructure/pet-project-api`, не из внутренних файлов модуля.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- `fetch` используется только внутри базового клиента.
|
||||||
|
- DTO запросов и ответов живут в `types/`.
|
||||||
|
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
|
||||||
|
- Методы лежат в `methods/` и возвращают DTO.
|
||||||
|
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
|
||||||
|
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||||
|
|
||||||
|
Следующий шаг: [GET-хуки REST-клиента](/docs/data/rest/clients/hooks) или [Стратегии получения данных](/docs/data/rest/strategies/).
|
||||||
74
docs/docs/data/rest/index.md
Normal file
74
docs/docs/data/rest/index.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: REST
|
||||||
|
description: Как правильно работать с REST API в проекте.
|
||||||
|
keywords: [rest, api, данные, infrastructure, клиент, swr, стратегии]
|
||||||
|
---
|
||||||
|
|
||||||
|
# REST
|
||||||
|
|
||||||
|
Раздел описывает, как правильно работать с REST API в проекте: создать клиент сервиса и выбрать способ получения данных в приложении.
|
||||||
|
|
||||||
|
REST в проекте проходит через два главных этапа:
|
||||||
|
|
||||||
|
1. Создание клиента.
|
||||||
|
2. Использование.
|
||||||
|
|
||||||
|
## 1. Создание клиента
|
||||||
|
|
||||||
|
На этом этапе внешний API оформляется как модуль слоя `infrastructure/`.
|
||||||
|
|
||||||
|
Клиент отвечает за:
|
||||||
|
|
||||||
|
- генерацию или ручное описание методов API;
|
||||||
|
- настройку `baseUrl`;
|
||||||
|
- заголовки и авторизацию;
|
||||||
|
- обработку ошибок;
|
||||||
|
- кастомизацию и расширение типов;
|
||||||
|
- GET-хуки для клиентских компонентов;
|
||||||
|
- публичный API модуля.
|
||||||
|
|
||||||
|
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
|
||||||
|
|
||||||
|
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
|
||||||
|
|
||||||
|
Подробнее:
|
||||||
|
|
||||||
|
- [Создание клиента](/docs/data/rest/clients/)
|
||||||
|
- [Автогенерация из OpenAPI](/docs/data/rest/clients/auto)
|
||||||
|
- [Ручное создание](/docs/data/rest/clients/manual)
|
||||||
|
- [GET-хуки REST-клиента](/docs/data/rest/clients/hooks)
|
||||||
|
|
||||||
|
## 2. Использование
|
||||||
|
|
||||||
|
После создания клиента нужно определить рендер страницы и выбрать, как получать данные в конкретном месте приложения.
|
||||||
|
|
||||||
|
Раздел использования отвечает на вопросы:
|
||||||
|
|
||||||
|
- как понять, можно ли сохранить static/ISR;
|
||||||
|
- когда страница становится dynamic/SSR;
|
||||||
|
- когда получать данные через серверный `await`;
|
||||||
|
- когда запускать несколько серверных запросов параллельно;
|
||||||
|
- когда передавать промис ниже по дереву;
|
||||||
|
- когда передавать начальные данные клиентским GET-хукам;
|
||||||
|
- когда использовать GET-хук в клиентском компоненте;
|
||||||
|
- когда выносить композицию и бизнес-смысл в `business/`.
|
||||||
|
|
||||||
|
Подробнее:
|
||||||
|
|
||||||
|
- [Стратегии получения данных](/docs/data/rest/strategies/)
|
||||||
|
- [Серверный await](/docs/data/rest/strategies/server-await)
|
||||||
|
- [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests)
|
||||||
|
- [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down)
|
||||||
|
- [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data)
|
||||||
|
- [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook)
|
||||||
|
- [Business-композиция](/docs/data/rest/strategies/business-composition)
|
||||||
|
|
||||||
|
## Как читать раздел
|
||||||
|
|
||||||
|
Если API ещё не подключён — начните с [Создания клиента](/docs/data/rest/clients/).
|
||||||
|
|
||||||
|
Если клиент уже есть, но непонятно как получить данные — начните со [Стратегий получения данных](/docs/data/rest/strategies/).
|
||||||
|
|
||||||
|
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/data/rest/clients/hooks).
|
||||||
|
|
||||||
|
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.
|
||||||
121
docs/docs/data/rest/strategies/business-composition.md
Normal file
121
docs/docs/data/rest/strategies/business-composition.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: Business-композиция
|
||||||
|
description: Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
|
||||||
|
keywords: [rest, business, композиция, hooks, domain, isAuth]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Business-композиция
|
||||||
|
|
||||||
|
Business-композиция используется, когда простого GET-метода или прозрачного GET-хука недостаточно: нужно объединить несколько источников, преобразовать DTO или вычислить доменное состояние.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Нужно объединить несколько GET-запросов.
|
||||||
|
- Нужно вычислить `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||||
|
- Нужно преобразовать DTO в доменную модель.
|
||||||
|
- Нужно спрятать бизнес-сценарий за доменным API.
|
||||||
|
|
||||||
|
Такая логика не пишется в `infrastructure/`. REST-клиент остаётся прозрачным адаптером к API.
|
||||||
|
|
||||||
|
## Пример поверх одного GET-хука
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/business/pets/hooks/use-available-pets.hook.ts
|
||||||
|
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Доменный список доступных питомцев.
|
||||||
|
*/
|
||||||
|
export const useAvailablePets = () => {
|
||||||
|
const query = useGetPetList('available')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
hasPets: Boolean(query.data?.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`useGetPetList` — infrastructure-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`.
|
||||||
|
|
||||||
|
## Пример композиции нескольких GET-хуков
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/business/pets/hooks/use-pets-dashboard.hook.ts
|
||||||
|
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные dashboard по питомцам.
|
||||||
|
*/
|
||||||
|
export const usePetsDashboard = () => {
|
||||||
|
const availablePets = useGetPetList('available')
|
||||||
|
const pendingPets = useGetPetList('pending')
|
||||||
|
const soldPets = useGetPetList('sold')
|
||||||
|
|
||||||
|
return {
|
||||||
|
availablePets,
|
||||||
|
pendingPets,
|
||||||
|
soldPets,
|
||||||
|
total:
|
||||||
|
(availablePets.data?.length ?? 0) +
|
||||||
|
(pendingPets.data?.length ?? 0) +
|
||||||
|
(soldPets.data?.length ?? 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Композиция нескольких запросов не добавляется в `infrastructure/pet-store-api/hooks/`, потому что это уже сценарий потребления данных.
|
||||||
|
|
||||||
|
## Пример auth-состояния
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/business/auth/hooks/use-auth-state.hook.ts
|
||||||
|
import { useGetCurrentUser } from 'infrastructure/backend-api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Состояние авторизации текущего пользователя.
|
||||||
|
*/
|
||||||
|
export const useAuthState = () => {
|
||||||
|
const currentUser = useGetCurrentUser()
|
||||||
|
const user = currentUser.data
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentUser,
|
||||||
|
user,
|
||||||
|
isAuth: Boolean(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`isAuth` не является частью REST-клиента. Это доменный смысл результата запроса.
|
||||||
|
|
||||||
|
## Где размещать
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/business/
|
||||||
|
└── pets/
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-available-pets.hook.ts
|
||||||
|
├── mappers/
|
||||||
|
│ └── map-pet-dto-to-pet.ts
|
||||||
|
├── types/
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Модуль `business/` экспортирует наружу готовый доменный API через `index.ts`.
|
||||||
|
|
||||||
|
## Что запрещено
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Плохо — business-смысл внутри infrastructure-хука
|
||||||
|
export const useGetPetList = (status: PetStatus) => {
|
||||||
|
const query = useSWR(...)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
hasPets: Boolean(query.data?.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте.
|
||||||
89
docs/docs/data/rest/strategies/client-get-hook.md
Normal file
89
docs/docs/data/rest/strategies/client-get-hook.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
title: Клиентский GET-хук
|
||||||
|
description: Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
|
||||||
|
keywords: [rest, client components, swr, get-хук, client state]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Клиентский GET-хук
|
||||||
|
|
||||||
|
Клиентский GET-хук используется, когда данные зависят от состояния браузера: вкладки, фильтра, поиска, пагинации, модалки или действия пользователя.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Запрос зависит от client state.
|
||||||
|
- Данные не обязательны для первого HTML.
|
||||||
|
- Пользователь меняет параметры запроса на клиенте.
|
||||||
|
- Нужны SWR-кеширование, дедупликация и ревалидация.
|
||||||
|
|
||||||
|
## Пример с вкладками
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||||
|
import type { PetStatus } from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
const statuses: PetStatus[] = ['available', 'pending', 'sold']
|
||||||
|
|
||||||
|
export function PetTabs() {
|
||||||
|
const [status, setStatus] = useState<PetStatus>('available')
|
||||||
|
const { data: pets, isLoading, error } = useGetPetList(status)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
{statuses.map((item) => (
|
||||||
|
<button key={item} type="button" onClick={() => setStatus(item)}>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <div>Загрузка...</div>}
|
||||||
|
{error && <div>Ошибка загрузки</div>}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{pets?.map((pet) => (
|
||||||
|
<li key={pet.id}>{pet.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента.
|
||||||
|
|
||||||
|
## Если хука нет
|
||||||
|
|
||||||
|
Хук добавляется в REST-модуль сервиса:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Не создавайте локальный `useSWR` в компоненте.
|
||||||
|
|
||||||
|
## Плохо
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо — прямой вызов клиента в useEffect
|
||||||
|
useEffect(() => {
|
||||||
|
petStoreApi.pet.findPetsByStatus({ status }).then(setPets)
|
||||||
|
}, [status])
|
||||||
|
|
||||||
|
// Плохо — useSWR в компоненте
|
||||||
|
const { data } = useSWR(
|
||||||
|
['pet-store-api', 'pet', 'list', status],
|
||||||
|
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI.
|
||||||
|
|
||||||
|
## Когда выбрать другую стратегию
|
||||||
|
|
||||||
|
- Данные нужны до первого HTML — [Серверный await](/docs/data/rest/strategies/server-await).
|
||||||
|
- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data).
|
||||||
|
- Нужно вычислить бизнес-состояние — [Business-композиция](/docs/data/rest/strategies/business-composition).
|
||||||
109
docs/docs/data/rest/strategies/client-hooks-initial-data.md
Normal file
109
docs/docs/data/rest/strategies/client-hooks-initial-data.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
title: Начальные данные для клиентских хуков
|
||||||
|
description: Как дать клиентским GET-хукам начальные REST-данные.
|
||||||
|
keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize, isr, ssr]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Начальные данные для клиентских хуков
|
||||||
|
|
||||||
|
Как дать клиентским GET-хукам начальные REST-данные.
|
||||||
|
|
||||||
|
Эта стратегия используется, когда данные должны быть запущены на сервере, но потребляться на клиенте через GET-хуки REST-клиента.
|
||||||
|
|
||||||
|
Технически это делается через `SWRConfig fallback`: сервер передаёт промис в fallback, а клиентский хук использует тот же SWR-ключ.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Внутри страницы есть Client Components с GET-хуками.
|
||||||
|
- Нужно начать загрузку данных на сервере раньше.
|
||||||
|
- Клиентский компонент должен остаться обычным потребителем `useGetPetList(...)`.
|
||||||
|
- Не нужно писать отдельный prop-drilling для начальных данных.
|
||||||
|
|
||||||
|
## Рендер страницы
|
||||||
|
|
||||||
|
Перед этой стратегией сначала определите рендер маршрута. Серверный preload для `fallback` подчиняется тем же правилам, что и любой серверный запрос в `page.tsx` или `layout.tsx`.
|
||||||
|
|
||||||
|
Если данные общие и могут обновляться по интервалу, сохраняйте static/ISR. Если preload зависит от cookie, headers, `searchParams`, `no-store` или персональных данных пользователя, маршрут становится dynamic/SSR.
|
||||||
|
|
||||||
|
`SWRConfig fallback` не должен быть причиной отключать ISR на всякий случай. Он только передаёт клиентскому GET-хуку данные, которые уже были запущены на сервере.
|
||||||
|
|
||||||
|
## Ключ хука
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||||
|
export const getPetListKey = (status: PetStatus) =>
|
||||||
|
['pet-store-api', 'pet', 'list', status] as const
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`.
|
||||||
|
|
||||||
|
## Пример layout
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/(routes)/pets/layout.tsx
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { SWRConfig, unstable_serialize } from 'swr'
|
||||||
|
import {
|
||||||
|
getPetListKey,
|
||||||
|
petStoreApi,
|
||||||
|
} from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
type PetsLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PetsLayout({ children }: PetsLayoutProps) {
|
||||||
|
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||||
|
status: 'available',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fallback: {
|
||||||
|
[unstable_serialize(getPetListKey('available'))]: availablePetsPromise,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SWRConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`.
|
||||||
|
|
||||||
|
## Клиентский компонент
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
export function PetList() {
|
||||||
|
const { data: pets, isLoading } = useGetPetList('available')
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{pets?.map((pet) => (
|
||||||
|
<li key={pet.id}>{pet.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента.
|
||||||
|
|
||||||
|
## Что важно
|
||||||
|
|
||||||
|
- Ключ `fallback` должен совпадать с ключом GET-хука.
|
||||||
|
- Серверный код вызывает метод клиента, а не GET-хук.
|
||||||
|
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
|
||||||
|
- Эта стратегия не означает ручную работу с кешем в компонентах.
|
||||||
|
|
||||||
|
## Когда не использовать
|
||||||
|
|
||||||
|
Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/data/rest/strategies/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook).
|
||||||
100
docs/docs/data/rest/strategies/index.md
Normal file
100
docs/docs/data/rest/strategies/index.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: Стратегии получения данных
|
||||||
|
description: Как выбрать получение REST-данных с учётом рендера страницы.
|
||||||
|
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Стратегии получения данных
|
||||||
|
|
||||||
|
Как выбрать получение REST-данных с учётом рендера страницы.
|
||||||
|
|
||||||
|
Перед выбором стратегии должен быть создан REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Создание клиента](/docs/data/rest/clients/).
|
||||||
|
|
||||||
|
## Сначала определите рендер страницы
|
||||||
|
|
||||||
|
В Next.js выбор начинается не с `await`, `Suspense` или SWR. Сначала нужно понять, какой рендер получится у маршрута: static/ISR или dynamic/SSR.
|
||||||
|
|
||||||
|
Next.js может перевести страницу в dynamic rendering автоматически, если в маршруте используются API текущего запроса. Поэтому первый вопрос такой:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Можно ли сохранить ISR, или странице нужны данные на каждый request?
|
||||||
|
```
|
||||||
|
|
||||||
|
ISR — приоритет. Если данные общие для пользователей и их можно обновлять с интервалом, не переводите страницу в SSR без необходимости.
|
||||||
|
|
||||||
|
SSR/dynamic rendering выбирается только когда данные действительно зависят от текущего request или должны пересчитываться на каждый запрос.
|
||||||
|
|
||||||
|
## Что переводит страницу в dynamic rendering
|
||||||
|
|
||||||
|
Проверьте, нужны ли странице API и настройки, которые делают маршрут динамическим:
|
||||||
|
|
||||||
|
- `cookies()` — данные зависят от cookie текущего пользователя.
|
||||||
|
- `headers()` — данные зависят от request headers.
|
||||||
|
- `draftMode()` — нужен preview/draft-режим.
|
||||||
|
- `searchParams` в `page.tsx` — данные зависят от query string.
|
||||||
|
- `cache: 'no-store'` или `revalidate: 0` в методе клиента — запрос нельзя кешировать.
|
||||||
|
- `connection()` — рендер явно ждёт request.
|
||||||
|
- `export const dynamic = 'force-dynamic'` — SSR включён вручную.
|
||||||
|
|
||||||
|
Если ничего из этого не нужно, сначала проектируйте страницу как static/ISR. Серверный `await` сам по себе не означает SSR: режим зависит от кеширования запроса и dynamic API маршрута.
|
||||||
|
|
||||||
|
## Рендер перед стратегией
|
||||||
|
|
||||||
|
| Рендер | Когда подходит | Что выбирать дальше |
|
||||||
|
|--------|----------------|---------------------|
|
||||||
|
| Static/ISR | Данные общие и могут обновляться по интервалу | Серверные стратегии: `await`, `Promise.all`, передача промиса ниже, SWR `fallback` |
|
||||||
|
| SSR/dynamic | Данные зависят от request, пользователя или должны быть свежими на каждый запрос | Серверные стратегии с учётом блокировки первого HTML |
|
||||||
|
| После гидрации | Данные зависят от вкладки, фильтра, поиска, пагинации или действия пользователя | Клиентский GET-хук |
|
||||||
|
|
||||||
|
## Как выбрать стратегию
|
||||||
|
|
||||||
|
Когда режим рендера понятен, выбирайте конкретный способ получения данных:
|
||||||
|
|
||||||
|
| Ситуация после выбора рендера | Стратегия | Где читать |
|
||||||
|
|-------------------------------|-----------|------------|
|
||||||
|
| Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](/docs/data/rest/strategies/server-await) |
|
||||||
|
| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests) |
|
||||||
|
| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down) |
|
||||||
|
| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data) |
|
||||||
|
| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/data/rest/strategies/client-get-hook) |
|
||||||
|
| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/data/rest/strategies/business-composition) |
|
||||||
|
|
||||||
|
## Правило выбора
|
||||||
|
|
||||||
|
Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Можно ли сохранить ISR?
|
||||||
|
Где нужны данные и что должно произойти до первого HTML?
|
||||||
|
```
|
||||||
|
|
||||||
|
Если данные можно кешировать между пользователями — сохраняйте static/ISR. Если данные request-specific — используйте SSR/dynamic rendering. Если данные зависят от состояния браузера — используйте GET-хук REST-клиента. Если простой GET превращается в доменный сценарий — переходите в `business/`.
|
||||||
|
|
||||||
|
## Общие запреты
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо — SSR включён на всякий случай
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
// Плохо — ISR отключён без требования к свежести на каждый request
|
||||||
|
export const revalidate = 0
|
||||||
|
|
||||||
|
// Плохо — прямой fetch в компоненте
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/pets').then(...)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Плохо — useSWR в компоненте
|
||||||
|
const { data } = useSWR(
|
||||||
|
['pet-store-api', 'pet', 'list', status],
|
||||||
|
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
hasPets: Boolean(query.data?.length),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля.
|
||||||
82
docs/docs/data/rest/strategies/parallel-server-requests.md
Normal file
82
docs/docs/data/rest/strategies/parallel-server-requests.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: Параллельные серверные запросы
|
||||||
|
description: Как запускать независимые REST-запросы на сервере без waterfall.
|
||||||
|
keywords: [rest, promise.all, параллельные запросы, server components]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Параллельные серверные запросы
|
||||||
|
|
||||||
|
Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Запросы независимы друг от друга.
|
||||||
|
- Все данные нужны текущему серверному компоненту перед возвратом UI.
|
||||||
|
- Нельзя или не нужно стримить часть UI отдельно.
|
||||||
|
|
||||||
|
## Хорошо
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||||
|
import { PetsDashboardScreen } from 'screens/pets-dashboard'
|
||||||
|
|
||||||
|
export default async function PetsDashboardPage() {
|
||||||
|
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||||
|
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||||
|
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||||
|
|
||||||
|
const [availablePets, pendingPets, soldPets] = await Promise.all([
|
||||||
|
availablePetsPromise,
|
||||||
|
pendingPetsPromise,
|
||||||
|
soldPetsPromise,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PetsDashboardScreen
|
||||||
|
availablePets={availablePets}
|
||||||
|
pendingPets={pendingPets}
|
||||||
|
soldPets={soldPets}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Плохо
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export default async function PetsDashboardPage() {
|
||||||
|
const availablePets = await petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||||
|
const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||||
|
const soldPets = await petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PetsDashboardScreen
|
||||||
|
availablePets={availablePets}
|
||||||
|
pendingPets={pendingPets}
|
||||||
|
soldPets={soldPets}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы.
|
||||||
|
|
||||||
|
## Зависимые запросы
|
||||||
|
|
||||||
|
Если второй запрос зависит от результата первого, последовательный `await` допустим:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export default async function OrderPage({ params }: OrderPageProps) {
|
||||||
|
const { id } = await params
|
||||||
|
const order = await petStoreApi.store.getOrderById(Number(id))
|
||||||
|
const pet = await petStoreApi.pet.getPetById(order.petId)
|
||||||
|
|
||||||
|
return <OrderScreen order={order} pet={pet} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Не превращайте зависимый сценарий в `Promise.all` искусственно.
|
||||||
|
|
||||||
|
## Когда выбрать другую стратегию
|
||||||
|
|
||||||
|
Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down).
|
||||||
62
docs/docs/data/rest/strategies/pass-promise-down.md
Normal file
62
docs/docs/data/rest/strategies/pass-promise-down.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: Передача промиса ниже
|
||||||
|
description: Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
|
||||||
|
keywords: [rest, promise, suspense, streaming, server components]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Передача промиса ниже
|
||||||
|
|
||||||
|
Серверный компонент может запустить запрос и передать промис вложенному server-компоненту. Это полезно, когда часть UI можно загрузить отдельно через `Suspense`.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Верхняя часть страницы может отрендериться без этих данных.
|
||||||
|
- Данные нужны только вложенному server-компоненту.
|
||||||
|
- Нужна `Suspense`-граница и серверный стриминг.
|
||||||
|
|
||||||
|
## Пример
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/(routes)/pets/page.tsx
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||||
|
import { PetListSection } from 'widgets/pet-list-section'
|
||||||
|
import { PetListSkeleton } from 'widgets/pet-list-section'
|
||||||
|
import type { Pet } from 'infrastructure/pet-store-api'
|
||||||
|
|
||||||
|
export default function PetsPage() {
|
||||||
|
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Питомцы</h1>
|
||||||
|
<Suspense fallback={<PetListSkeleton />}>
|
||||||
|
<AvailablePets petsPromise={petsPromise} />
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function AvailablePets({ petsPromise }: { petsPromise: Promise<Pet[]> }) {
|
||||||
|
const pets = await petsPromise
|
||||||
|
|
||||||
|
return <PetListSection pets={pets} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI.
|
||||||
|
|
||||||
|
## Граница стратегии
|
||||||
|
|
||||||
|
Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components.
|
||||||
|
|
||||||
|
Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data).
|
||||||
|
|
||||||
|
## Что не делать
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии
|
||||||
|
return <PetListClient petsPromise={petsPromise} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента.
|
||||||
88
docs/docs/data/rest/strategies/server-await.md
Normal file
88
docs/docs/data/rest/strategies/server-await.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: Серверный await
|
||||||
|
description: Получение REST-данных на сервере до первого HTML.
|
||||||
|
keywords: [rest, server components, await, nextjs, isr, ssr, notFound, redirect]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Серверный await
|
||||||
|
|
||||||
|
Получение REST-данных на сервере до первого HTML.
|
||||||
|
|
||||||
|
Серверный `await` — базовая стратегия для данных, которые нужны до рендера страницы или серверного блока.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
- Данные нужны для первого HTML.
|
||||||
|
- Данные влияют на `metadata`.
|
||||||
|
- По результату запроса нужно вызвать `notFound()` или `redirect()`.
|
||||||
|
- Компонент серверный и данные не зависят от состояния браузера.
|
||||||
|
|
||||||
|
## Влияние на рендер
|
||||||
|
|
||||||
|
Серверный `await` сам по себе не означает SSR. В App Router страница может остаться static/ISR, если маршрут не использует dynamic API и запросы можно кешировать.
|
||||||
|
|
||||||
|
ISR — приоритет для общих данных. Если список или детальная страница могут обновляться по интервалу, сохраняйте кеширование и не добавляйте `no-store`, `revalidate: 0` или `force-dynamic` без требования.
|
||||||
|
|
||||||
|
SSR/dynamic rendering нужен, когда данные зависят от текущего request: cookie, headers, `searchParams`, preview-режим или персональные данные пользователя.
|
||||||
|
|
||||||
|
## Пример страницы списка
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/(routes)/pets/page.tsx
|
||||||
|
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||||
|
import { PetsScreen } from 'screens/pets'
|
||||||
|
|
||||||
|
export default async function PetsPage() {
|
||||||
|
const pets = await petStoreApi.pet.findPetsByStatus({
|
||||||
|
status: 'available',
|
||||||
|
})
|
||||||
|
|
||||||
|
return <PetsScreen pets={pets} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`.
|
||||||
|
|
||||||
|
## Пример детальной страницы
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/(routes)/pets/[id]/page.tsx
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||||
|
import { PetDetailScreen } from 'screens/pet-detail'
|
||||||
|
|
||||||
|
type PetPageProps = {
|
||||||
|
params: Promise<{ id: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PetPage({ params }: PetPageProps) {
|
||||||
|
const { id } = await params
|
||||||
|
const pet = await petStoreApi.pet.getPetById(Number(id)).catch(() => null)
|
||||||
|
|
||||||
|
if (!pet) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PetDetailScreen pet={pet} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента.
|
||||||
|
|
||||||
|
## Что не делать
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо — хуки нельзя вызывать в Server Component
|
||||||
|
const { data } = useGetPetList('available')
|
||||||
|
|
||||||
|
// Плохо — прямой fetch в обход клиента
|
||||||
|
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
|
||||||
|
```
|
||||||
|
|
||||||
|
Если данные нужны на сервере, вызывайте метод REST-клиента напрямую.
|
||||||
|
|
||||||
|
## Когда выбрать другую стратегию
|
||||||
|
|
||||||
|
- Несколько независимых запросов — [Параллельные серверные запросы](/docs/data/rest/strategies/parallel-server-requests).
|
||||||
|
- Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/data/rest/strategies/pass-promise-down).
|
||||||
|
- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/data/rest/strategies/client-hooks-initial-data).
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: NextJS Style Guide
|
||||||
|
description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||||
|
---
|
||||||
|
|
||||||
# NextJS Style Guide
|
# NextJS Style Guide
|
||||||
|
|
||||||
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
|
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
@@ -15,21 +20,9 @@
|
|||||||
|
|
||||||
## Структура документации
|
## Структура документации
|
||||||
|
|
||||||
### Workflow
|
### Подсказки
|
||||||
|
|
||||||
**Что делать и в каком порядке** — пошаговые инструкции.
|
[Подсказки](/docs/workflow) — короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
|
|
||||||
| Раздел | Отвечает на вопрос |
|
|
||||||
|--------|-------------------|
|
|
||||||
| Начало работы | Что нужно знать перед началом разработки? |
|
|
||||||
| Создание проекта | Как начать новый проект? |
|
|
||||||
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
|
|
||||||
| Добавление страницы | Как добавить новую страницу в проект? |
|
|
||||||
| Добавление UI-модуля | Как создать компонент, бизнес-модуль, виджет или layout? |
|
|
||||||
| Стилизация | Как стилизовать компоненты в проекте? |
|
|
||||||
| Получение данных | Как получать данные с сервера? |
|
|
||||||
| Управление состоянием | Как работать с состоянием? |
|
|
||||||
| Локализация | Как добавлять переводы и подключать локализацию? |
|
|
||||||
|
|
||||||
### Базовые правила
|
### Базовые правила
|
||||||
|
|
||||||
@@ -38,32 +31,67 @@
|
|||||||
| Раздел | Отвечает на вопрос |
|
| Раздел | Отвечает на вопрос |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Технологии и библиотеки | Какой стек используем? |
|
| Технологии и библиотеки | Какой стек используем? |
|
||||||
| Архитектура | Как устроены слои SLM, зависимости, публичный API? |
|
|
||||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
|
||||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||||
|
| SLM Design | Что такое SLM и зачем она нужна? |
|
||||||
|
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
|
||||||
|
| Архитектура: Модули | Что такое модуль и как он устроен? |
|
||||||
|
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
|
||||||
|
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Прикладные разделы
|
### Создание проекта
|
||||||
|
|
||||||
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
|
**Как начать новый проект** — варианты установки и эталонный набор инструментов.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
|
||||||
|
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
|
||||||
|
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
|
||||||
|
|
||||||
|
### Настройка
|
||||||
|
|
||||||
|
**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Алиасы импортов | Как настроить алиасы импортов? |
|
||||||
|
| Biome | Как настроить линтер и форматтер? |
|
||||||
|
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
|
||||||
|
| Стили | Как подключить базовые стили и токены? |
|
||||||
|
| SVG-спрайты | Как подключить генерацию SVG-спрайтов? |
|
||||||
|
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
|
||||||
|
| VS Code | Как настроить редактор для проекта? |
|
||||||
|
|
||||||
|
### Использование
|
||||||
|
|
||||||
|
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
|
||||||
|
|
||||||
| Раздел | Отвечает на вопрос |
|
| Раздел | Отвечает на вопрос |
|
||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
|
||||||
| Структура проекта | Как организованы папки и файлы по SLM? |
|
| Структура проекта | Как организованы папки и файлы по SLM? |
|
||||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
| Файлы роутинга | Как описывать layout, page, loading, error, not-found? |
|
||||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
| Стили | Как писать CSS: вложенность, медиа, токены? |
|
||||||
|
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
|
||||||
| Изображения | _(не заполнен)_ |
|
| Изображения | _(не заполнен)_ |
|
||||||
| SVG-спрайты | _(не заполнен)_ |
|
|
||||||
| Видео | _(не заполнен)_ |
|
| Видео | _(не заполнен)_ |
|
||||||
| API | _(не заполнен)_ |
|
|
||||||
| Stores | _(не заполнен)_ |
|
| Stores | _(не заполнен)_ |
|
||||||
| Хуки | _(не заполнен)_ |
|
| Хуки | _(не заполнен)_ |
|
||||||
| Шрифты | _(не заполнен)_ |
|
| Шрифты | _(не заполнен)_ |
|
||||||
| Локализация | _(не заполнен)_ |
|
| Локализация | _(не заполнен)_ |
|
||||||
|
|
||||||
|
### Данные
|
||||||
|
|
||||||
|
**Как работать с источниками данных** — REST, realtime и потребление в компонентах.
|
||||||
|
|
||||||
|
| Раздел | Отвечает на вопрос |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Источники данных | Как устроена работа с данными в проекте? |
|
||||||
|
| REST: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
|
||||||
|
| REST: Ручное создание | Как написать REST-клиент вручную? |
|
||||||
|
| REST: Серверные компоненты | Как получать данные в серверных компонентах? |
|
||||||
|
| REST: Клиентские компоненты | Как получать данные в клиентских компонентах? |
|
||||||
|
| Realtime | Как работать с realtime-каналами и сокетами? |
|
||||||
|
|||||||
@@ -1,86 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Workflow
|
title: Подсказки
|
||||||
|
description: Короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Workflow
|
# Подсказки
|
||||||
|
|
||||||
Порядок действий при разработке — от создания проекта до реализации фич.
|
Короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
|
|
||||||
## Создание проекта
|
|
||||||
|
|
||||||
Инициализация нового проекта из готового шаблона.
|
|
||||||
|
|
||||||
1. Создать проект из шаблона:
|
|
||||||
```bash
|
|
||||||
npx tiged git@gromlab.ru:templates/nextjs.git my-app
|
|
||||||
cd my-app
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
2. Проект готов к разработке — стек, структура SLM, конфигурация
|
|
||||||
редактора и шаблоны генерации уже настроены.
|
|
||||||
|
|
||||||
## Генерация кода
|
|
||||||
|
|
||||||
Создание модулей из шаблонов `.templates/` вместо ручного создания файлов.
|
|
||||||
|
|
||||||
1. Определить тип модуля и соответствующий шаблон:
|
|
||||||
|
|
||||||
| Модуль | Слой | Шаблон |
|
|
||||||
|------------|--------------|-------------|
|
|
||||||
| Компонент | `ui/` | `component` |
|
|
||||||
| Бизнес-модуль | `business/` | `module` |
|
|
||||||
| Виджет | `widgets/` | `widget` |
|
|
||||||
| Layout | `layouts/` | `layout` |
|
|
||||||
| Экран | `screens/` | `screen` |
|
|
||||||
| Стор | `stores/` | `store` |
|
|
||||||
|
|
||||||
2. Сгенерировать модуль из шаблона.
|
|
||||||
3. Если подходящего шаблона нет — сначала создать шаблон, затем использовать.
|
|
||||||
|
|
||||||
Ручное создание файловой структуры модулей запрещено.
|
|
||||||
|
|
||||||
## Добавление страницы
|
|
||||||
|
|
||||||
Создание нового маршрута: экран + точка входа для роутинга.
|
|
||||||
|
|
||||||
1. Сгенерировать экран из шаблона `screen` в `src/screens/`.
|
|
||||||
2. Заполнить экран логикой и стилями.
|
|
||||||
3. Создать `page.tsx` в нужном маршруте `src/app/`.
|
|
||||||
|
|
||||||
`page.tsx` — тонкая обёртка: только `metadata` и рендер экрана.
|
|
||||||
Логика, стили и хуки размещаются в экране, не в `page.tsx`.
|
|
||||||
|
|
||||||
## Добавление UI-модуля
|
|
||||||
|
|
||||||
Создание компонента, бизнес-модуля, виджета или layout.
|
|
||||||
|
|
||||||
1. Сгенерировать модуль из соответствующего шаблона в целевой слой.
|
|
||||||
2. Заполнить модуль логикой и стилями.
|
|
||||||
3. Дочерние компоненты — генерировать из шаблона `component` в папку `ui/`
|
|
||||||
внутри родителя.
|
|
||||||
|
|
||||||
Дочерние компоненты не экспортируются через `index.ts` родителя.
|
|
||||||
|
|
||||||
## Стилизация
|
|
||||||
|
|
||||||
Выбор инструмента стилизации по приоритету.
|
|
||||||
|
|
||||||
1. Использовать Mantine-компоненты и их пропсы.
|
|
||||||
2. Если Mantine не покрывает — использовать CSS-токены
|
|
||||||
(`--color-*`, `--space-*`, `--radius-*`).
|
|
||||||
3. Если нужна кастомная стилизация — PostCSS Modules.
|
|
||||||
|
|
||||||
Инлайн-стили (`style`), магические значения и глобальные стили
|
|
||||||
вне `app/styles/` запрещены.
|
|
||||||
|
|
||||||
## Получение данных
|
|
||||||
|
|
||||||
*Раздел в разработке* — SWR, генерация API-клиентов, сокеты.
|
|
||||||
|
|
||||||
## Управление состоянием
|
|
||||||
|
|
||||||
*Раздел в разработке* — когда создавать стор, что хранить локально и глобально.
|
|
||||||
|
|
||||||
## Локализация
|
|
||||||
|
|
||||||
*Раздел в разработке* — переводы и i18next.
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
title: Генерация кода
|
|
||||||
---
|
|
||||||
|
|
||||||
# Генерация кода
|
|
||||||
|
|
||||||
Как создавать модули в проекте с помощью шаблонов — какие модули покрыты генерацией и когда стоит создавать новые шаблоны.
|
|
||||||
|
|
||||||
## Какие модули генерируются из шаблонов
|
|
||||||
|
|
||||||
| Модуль | Слой | Шаблон |
|
|
||||||
|---|---|---|
|
|
||||||
| Компонент | `ui/` | `component` |
|
|
||||||
| Бизнес-модуль | `business/` | `module` |
|
|
||||||
| Виджет | `widgets/` | `widget` |
|
|
||||||
| Layout | `layouts/` | `layout` |
|
|
||||||
| Экран | `screens/` | `screen` |
|
|
||||||
| Стор | `stores/` | `store` |
|
|
||||||
|
|
||||||
## Что нужно знать
|
|
||||||
|
|
||||||
В проекте принято создавать модули из шаблонов `.templates/`. Шаблоны задают единообразную файловую структуру и сокращают рутину — не нужно вручную создавать папки, файлы типов, стилей и экспорты.
|
|
||||||
|
|
||||||
Если для нужного модуля нет подходящего шаблона — стоит сначала создать шаблон, а затем использовать его.
|
|
||||||
|
|
||||||
## Когда создавать новый шаблон
|
|
||||||
|
|
||||||
- Повторяющаяся структура появляется больше одного раза.
|
|
||||||
- Существующий шаблон не покрывает нужный тип модуля.
|
|
||||||
|
|
||||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/applied/templates-generation).
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
title: Создание проекта
|
|
||||||
---
|
|
||||||
|
|
||||||
# Создание проекта
|
|
||||||
|
|
||||||
Как начать новый проект, соответствующий стандартам этого руководства.
|
|
||||||
|
|
||||||
## Что нужно знать
|
|
||||||
|
|
||||||
Новый проект создаётся из готового шаблона. Шаблон содержит настроенный стек, структуру SLM, конфигурацию редактора и шаблоны генерации кода — проект готов к разработке сразу после установки зависимостей.
|
|
||||||
|
|
||||||
### Создание из шаблона
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx tiged git@gromlab.ru:templates/nextjs.git my-app
|
|
||||||
cd my-app
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что входит в шаблон
|
|
||||||
|
|
||||||
- Next.js + TypeScript (App Router)
|
|
||||||
- Mantine UI + PostCSS Modules
|
|
||||||
- Biome (линтинг и форматирование)
|
|
||||||
- Zustand, SWR
|
|
||||||
- Структура SLM (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`)
|
|
||||||
- Шаблоны генерации (`.templates/`)
|
|
||||||
- Конфигурация VS Code (`.vscode/`)
|
|
||||||
- CSS-токены (цвета, отступы, радиусы, медиа)
|
|
||||||
- Open Graph метаданные
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: Добавление UI-модуля
|
|
||||||
---
|
|
||||||
|
|
||||||
# Добавление UI-модуля
|
|
||||||
|
|
||||||
Как создать компонент, бизнес-модуль, виджет или layout в проекте.
|
|
||||||
|
|
||||||
## Что нужно знать
|
|
||||||
|
|
||||||
Все UI-модули создаются только из шаблонов `.templates/`. Ручное создание файловой структуры запрещено. Если подходящего шаблона нет — сначала создать шаблон в `.templates/`, затем использовать его.
|
|
||||||
|
|
||||||
## Порядок действий
|
|
||||||
|
|
||||||
1. [Сгенерировать](/docs/applied/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
|
||||||
2. Заполнить модуль логикой и стилями.
|
|
||||||
|
|
||||||
## Дочерние компоненты
|
|
||||||
|
|
||||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
|
||||||
|
|
||||||
Правила написания компонентов — [Компоненты](/docs/applied/components).
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: Добавление страницы
|
|
||||||
---
|
|
||||||
|
|
||||||
# Добавление страницы
|
|
||||||
|
|
||||||
Как добавить новую страницу в проект по стандартам этого руководства.
|
|
||||||
|
|
||||||
## Что нужно знать
|
|
||||||
|
|
||||||
Страница в проекте — это два файла: экран в `src/screens/` (вся логика, стили, зависимости) и `page.tsx` в `src/app/` (точка входа для роутинга Next.js). Экран генерируется из шаблона, `page.tsx` создаётся вручную.
|
|
||||||
|
|
||||||
## Порядок действий
|
|
||||||
|
|
||||||
1. [Сгенерировать](/docs/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
|
||||||
|
|
||||||
2. Заполнить экран логикой и стилями.
|
|
||||||
|
|
||||||
3. Создать `page.tsx` в нужном маршруте `src/app/`. Файл страницы должен быть тонким — только `metadata` и рендер экрана. Никакой логики, стилей и хуков в `page.tsx` не размещается — всё это живёт в экране.
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
|
|
||||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/applied/templates-generation) из шаблона.
|
|
||||||
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
|
||||||
- Каждая страница содержит `metadata` с `title` и `description`.
|
|
||||||
|
|
||||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/applied/page-level).
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Получение данных
|
|
||||||
---
|
|
||||||
|
|
||||||
# Получение данных
|
|
||||||
|
|
||||||
Как получать данные с сервера — SWR, генерация API-клиентов, сокеты.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: Начало работы
|
|
||||||
---
|
|
||||||
|
|
||||||
# Начало работы
|
|
||||||
|
|
||||||
Что нужно знать перед началом разработки в проекте.
|
|
||||||
|
|
||||||
## Стек проекта
|
|
||||||
|
|
||||||
**Next.js** (App Router), **Mantine**, **Zustand**, **SLM Design**.
|
|
||||||
|
|
||||||
Подробнее — [Технологии и библиотеки](/docs/basics/tech-stack).
|
|
||||||
|
|
||||||
## Ключевые особенности
|
|
||||||
|
|
||||||
- **Генерация вместо ручного создания** — компоненты, бизнес-модули, виджеты, сторы и другие модули не создаются вручную. Файловая структура генерируется из шаблонов `.templates/`. Ручное создание файловой структуры модулей запрещено.
|
|
||||||
- **Biome вместо ESLint + Prettier** — один инструмент для линтинга и форматирования. Автофикс и сортировка импортов происходят автоматически при сохранении файла.
|
|
||||||
|
|
||||||
## Настройка окружения
|
|
||||||
|
|
||||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/applied/vscode).
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Локализация
|
|
||||||
---
|
|
||||||
|
|
||||||
# Локализация
|
|
||||||
|
|
||||||
Как добавлять переводы и подключать локализацию через i18next.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Управление состоянием
|
|
||||||
---
|
|
||||||
|
|
||||||
# Управление состоянием
|
|
||||||
|
|
||||||
Как работать с состоянием — когда создавать стор, что хранить локально и глобально.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: Стилизация
|
|
||||||
---
|
|
||||||
|
|
||||||
# Стилизация
|
|
||||||
|
|
||||||
Как стилизовать компоненты в проекте — приоритет инструментов и правила их применения.
|
|
||||||
|
|
||||||
## Приоритет стилизации
|
|
||||||
|
|
||||||
Основной UI-фреймворк проекта — **Mantine**. При стилизации компонентов придерживаться следующего приоритета:
|
|
||||||
|
|
||||||
1. **Mantine-компоненты и их пропсы** — в первую очередь использовать встроенные возможности Mantine (пропсы, `classNames`, `styles`).
|
|
||||||
2. **Глобальные CSS-токены** (`--color-*`, `--space-*`, `--radius-*`) — для значений, которые не покрываются Mantine.
|
|
||||||
3. **PostCSS Modules** — когда Mantine не покрывает задачу и нужна кастомная стилизация.
|
|
||||||
|
|
||||||
## Что запрещено
|
|
||||||
|
|
||||||
- **Инлайн-стили** — использование атрибута `style` в компонентах строго запрещено.
|
|
||||||
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
|
|
||||||
- **Глобальные стили** вне `app/styles/` запрещены.
|
|
||||||
|
|
||||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/applied/styles/usage).
|
|
||||||
@@ -83,20 +83,23 @@ function toggleTheme(value) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="landing__cards">
|
<section class="landing__cards">
|
||||||
<a class="landing__card" href="./docs/">
|
<a class="landing__card" href="/docs/">
|
||||||
<h3>Документация</h3>
|
<h3>Документация</h3>
|
||||||
<p>Все разделы: процессы разработки, базовые правила, прикладные руководства.</p>
|
<p>Все разделы: процессы разработки, базовые правила, прикладные руководства.</p>
|
||||||
<span class="landing__cta">Открыть →</span>
|
<span class="landing__cta">Открыть →</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="landing__card landing__card--multi">
|
<div class="landing__card landing__card--multi">
|
||||||
<h3>Ассистенту</h3>
|
<h3>Ассистенту</h3>
|
||||||
<p>Карта документации в формате llms.txt для AI-агентов.</p>
|
<p>
|
||||||
|
Карта документации для AI-агентов:
|
||||||
|
<code>/llms.txt</code>, <code>/llms-full.txt</code>.
|
||||||
|
</p>
|
||||||
<div class="landing__buttons">
|
<div class="landing__buttons">
|
||||||
<a class="landing__button" href="./llms.txt" target="_blank" rel="noopener">llms.txt</a>
|
<a class="landing__button" href="/llms.txt">llms.txt</a>
|
||||||
<a class="landing__button" href="./llms-full.txt" target="_blank" rel="noopener">llms-full.txt</a>
|
<a class="landing__button" href="/llms-full.txt">llms-full.txt</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="landing__card" href="./nextjs-style-guide.zip">
|
<a class="landing__card" href="/nextjs-style-guide.zip">
|
||||||
<h3>Скачать правила</h3>
|
<h3>Скачать правила</h3>
|
||||||
<p>Архив всех Markdown-файлов одним ZIP.</p>
|
<p>Архив всех Markdown-файлов одним ZIP.</p>
|
||||||
<span class="landing__cta">Скачать →</span>
|
<span class="landing__cta">Скачать →</span>
|
||||||
|
|||||||
246
generate-llms.ts
246
generate-llms.ts
@@ -14,6 +14,9 @@ const PUBLIC_DIR = 'docs/public';
|
|||||||
/** Префикс URL документации. Соответствует структуре `docs/docs/...`. */
|
/** Префикс URL документации. Соответствует структуре `docs/docs/...`. */
|
||||||
const DOC_PREFIX = '/docs/';
|
const DOC_PREFIX = '/docs/';
|
||||||
|
|
||||||
|
/** Канонический хост сайта (для sitemap/robots). Можно переопределить через ENV. */
|
||||||
|
const SITE_URL = (process.env.SITE_URL || 'https://nextjs-style-guide.gromlab.ru').replace(/\/$/, '');
|
||||||
|
|
||||||
interface SidebarItem {
|
interface SidebarItem {
|
||||||
text: string;
|
text: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
@@ -101,40 +104,41 @@ const linkToSiteUrl = (link: string): string =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Развернуть sidebar в плоский список с сохранением группы и
|
* Развернуть sidebar в плоский список с сохранением группы и
|
||||||
* опционального префикса вложенной группы.
|
* накопленного префикса вложенных групп. Поддерживает произвольную
|
||||||
|
* глубину вложенности — префиксы подгрупп склеиваются через `: `.
|
||||||
*/
|
*/
|
||||||
const flattenSidebar = (sidebar: SidebarItem[]): Entry[] => {
|
const flattenSidebar = (sidebar: SidebarItem[]): Entry[] => {
|
||||||
const entries: Entry[] = [];
|
const entries: Entry[] = [];
|
||||||
|
|
||||||
for (const top of sidebar) {
|
const walk = (
|
||||||
const section = top.text;
|
items: SidebarItem[],
|
||||||
|
section: string,
|
||||||
|
prefix: string | null,
|
||||||
|
): void => {
|
||||||
|
for (const item of items) {
|
||||||
|
const hasChildren = !!item.items && item.items.length > 0;
|
||||||
|
|
||||||
if (top.link && !top.items) {
|
if (item.link) {
|
||||||
entries.push({ section, prefix: null, text: top.text, link: top.link });
|
entries.push({ section, prefix, text: item.text, link: item.link });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
const nextPrefix = prefix ? `${prefix}: ${item.text}` : item.text;
|
||||||
|
walk(item.items!, section, nextPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const top of sidebar) {
|
||||||
|
const hasChildren = !!top.items && top.items.length > 0;
|
||||||
|
|
||||||
|
if (top.link && !hasChildren) {
|
||||||
|
entries.push({ section: top.text, prefix: null, text: top.text, link: top.link });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!top.items) continue;
|
if (hasChildren) {
|
||||||
|
walk(top.items!, top.text, null);
|
||||||
for (const item of top.items) {
|
|
||||||
if (item.items) {
|
|
||||||
for (const sub of item.items) {
|
|
||||||
if (!sub.link) continue;
|
|
||||||
entries.push({
|
|
||||||
section,
|
|
||||||
prefix: item.text,
|
|
||||||
text: sub.text,
|
|
||||||
link: sub.link,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (item.link) {
|
|
||||||
entries.push({
|
|
||||||
section,
|
|
||||||
prefix: null,
|
|
||||||
text: item.text,
|
|
||||||
link: item.link,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +227,6 @@ const copyDirSync = (
|
|||||||
const srcPath = path.join(src, entry.name);
|
const srcPath = path.join(src, entry.name);
|
||||||
const destPath = path.join(dest, entry.name);
|
const destPath = path.join(dest, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
fs.mkdirSync(destPath, { recursive: true });
|
|
||||||
count += copyDirSync(srcPath, destPath, filter);
|
count += copyDirSync(srcPath, destPath, filter);
|
||||||
} else if (entry.isFile() && filter(entry.name)) {
|
} else if (entry.isFile() && filter(entry.name)) {
|
||||||
fs.mkdirSync(dest, { recursive: true });
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
@@ -237,13 +240,22 @@ const copyDirSync = (
|
|||||||
/**
|
/**
|
||||||
* Скопировать все `.md`-файлы документации в `docs/public/docs/`,
|
* Скопировать все `.md`-файлы документации в `docs/public/docs/`,
|
||||||
* чтобы они попали в build `dist/` и были доступны по URL `/docs/path.md`.
|
* чтобы они попали в build `dist/` и были доступны по URL `/docs/path.md`.
|
||||||
|
*
|
||||||
|
* `DEVELOP.md` исключается — это точка входа архива, ссылается
|
||||||
|
* на офлайн-файлы (`MAP.md`), которых нет на сайте.
|
||||||
*/
|
*/
|
||||||
const copyMdFiles = (): void => {
|
const copyMdFiles = (): void => {
|
||||||
const srcDir = 'docs/docs';
|
const srcDir = 'docs/docs';
|
||||||
const destDir = path.join(PUBLIC_DIR, 'docs');
|
const destDir = path.join(PUBLIC_DIR, 'docs');
|
||||||
if (!fs.existsSync(srcDir)) return;
|
if (!fs.existsSync(srcDir)) return;
|
||||||
|
|
||||||
const copied = copyDirSync(srcDir, destDir, (name) => name.endsWith('.md'));
|
fs.rmSync(destDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
const copied = copyDirSync(
|
||||||
|
srcDir,
|
||||||
|
destDir,
|
||||||
|
(name) => name.endsWith('.md') && name !== 'DEVELOP.md',
|
||||||
|
);
|
||||||
console.log(`скопировано ${copied} .md-файлов в ${destDir}`);
|
console.log(`скопировано ${copied} .md-файлов в ${destDir}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,98 +312,33 @@ const transformLinksInDir = (rootDir: string): void => {
|
|||||||
walk(rootDir);
|
walk(rootDir);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Сгенерировать `README.md` — точка входа архива. Карта документации
|
|
||||||
* с относительными ссылками, описаниями из frontmatter/первого абзаца
|
|
||||||
* и метаинфо сборки.
|
|
||||||
*/
|
|
||||||
const buildArchiveReadme = (rootDir: string): void => {
|
|
||||||
const sidebar = cfg.themeConfig.sidebar;
|
|
||||||
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
|
|
||||||
const context = cfg.llmsContext;
|
|
||||||
|
|
||||||
const entries = flattenSidebar(sidebar).filter(
|
|
||||||
// «Главная» из sidebar — это страница раздела для веба, в архиве не нужна.
|
|
||||||
(e) => e.section !== 'Главная',
|
|
||||||
);
|
|
||||||
const grouped = groupBySection(entries);
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
lines.push(`# ${cfg.title}`);
|
|
||||||
lines.push('');
|
|
||||||
if (blockquote) {
|
|
||||||
lines.push(`> ${blockquote}`);
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
if (context) {
|
|
||||||
lines.push(context);
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push('## Содержание');
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
for (const [section, items] of grouped) {
|
|
||||||
lines.push(`### ${section}`);
|
|
||||||
lines.push('');
|
|
||||||
for (const entry of items) {
|
|
||||||
const targetRel = './' + linkToArchiveRel(entry.link);
|
|
||||||
const filePath = path.join(rootDir, linkToArchiveRel(entry.link));
|
|
||||||
|
|
||||||
let description: string | null = null;
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const raw = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const { data, body } = parseFrontmatter(raw);
|
|
||||||
description = data.description || firstParagraphAfterH1(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
const display = entry.prefix
|
|
||||||
? `${entry.prefix}: ${entry.text}`
|
|
||||||
: entry.text;
|
|
||||||
const descPart = description ? ` — ${description}` : '';
|
|
||||||
lines.push(`- [${display}](${targetRel})${descPart}`);
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push('---');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(`Версия: ${VERSION} · Сборка: ${BUILD_DATE}`);
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(rootDir, 'README.md'), lines.join('\n'), 'utf8');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Собрать `nextjs-style-guide.zip`. Внутри — папка `nextjs-style-guide/`
|
* Собрать `nextjs-style-guide.zip`. Внутри — папка `nextjs-style-guide/`
|
||||||
* с `.md`-файлами, README, `llms-full.txt` и `VERSION`. Внутренние ссылки
|
* с `.md`-файлами, DEVELOP.md (точка входа), MAP.md (навигационная карта)
|
||||||
* преобразуются в относительные.
|
* и `VERSION`. Внутренние ссылки преобразуются в относительные.
|
||||||
|
*
|
||||||
|
* Точка входа архива — `docs/docs/DEVELOP.md`, навигационная карта —
|
||||||
|
* `docs/docs/MAP.md`. Оба файла редактируются вручную и копируются
|
||||||
|
* в архив как есть.
|
||||||
*/
|
*/
|
||||||
const buildZip = (): void => {
|
const buildZip = (): void => {
|
||||||
|
fs.rmSync(path.resolve(PUBLIC_DIR, 'nextjs-style-guide'), { recursive: true, force: true });
|
||||||
|
|
||||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-'));
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-'));
|
||||||
const stage = path.join(tmpRoot, 'nextjs-style-guide');
|
const stage = path.join(tmpRoot, 'nextjs-style-guide');
|
||||||
fs.mkdirSync(stage, { recursive: true });
|
fs.mkdirSync(stage, { recursive: true });
|
||||||
|
|
||||||
// 1. Копируем все .md в staging.
|
// 1. Копируем все .md в staging (включая DEVELOP.md и MAP.md).
|
||||||
copyDirSync('docs/docs', stage, (name) => name.endsWith('.md'));
|
copyDirSync('docs/docs', stage, (name) => name.endsWith('.md'));
|
||||||
|
|
||||||
// 2. Удаляем веб-index.md — в архиве его роль выполняет README.md.
|
// 2. Удаляем веб-index.md — в архиве его роль выполняет DEVELOP.md.
|
||||||
const indexPath = path.join(stage, 'index.md');
|
const indexPath = path.join(stage, 'index.md');
|
||||||
if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath);
|
if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath);
|
||||||
|
|
||||||
// 3. Преобразуем абсолютные ссылки `/docs/...` в относительные.
|
// 3. Преобразуем абсолютные ссылки `/docs/...` в относительные.
|
||||||
transformLinksInDir(stage);
|
transformLinksInDir(stage);
|
||||||
|
|
||||||
// 4. Генерируем точку входа README.md.
|
// 4. Метаинформация сборки.
|
||||||
buildArchiveReadme(stage);
|
|
||||||
|
|
||||||
// 5. Кладём llms-full.txt — удобно для одноразового чтения LLM.
|
|
||||||
const llmsFullSrc = path.join(PUBLIC_DIR, 'llms-full.txt');
|
|
||||||
if (fs.existsSync(llmsFullSrc)) {
|
|
||||||
fs.copyFileSync(llmsFullSrc, path.join(stage, 'llms-full.txt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Метаинформация сборки.
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(stage, 'VERSION'),
|
path.join(stage, 'VERSION'),
|
||||||
`${VERSION}\n${BUILD_DATE}\n`,
|
`${VERSION}\n${BUILD_DATE}\n`,
|
||||||
@@ -487,6 +434,85 @@ const writeManifest = (): void => {
|
|||||||
console.log(`${PUBLIC_DIR}/manifest.json создан`);
|
console.log(`${PUBLIC_DIR}/manifest.json создан`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сгенерировать `robots.txt` с указанием sitemap и явными ссылками
|
||||||
|
* на llms.txt/llms-full.txt — стандартные файлы, которые читают агенты.
|
||||||
|
*/
|
||||||
|
const buildRobots = (): void => {
|
||||||
|
const lines = [
|
||||||
|
'User-agent: *',
|
||||||
|
'Allow: /',
|
||||||
|
'',
|
||||||
|
`Sitemap: ${SITE_URL}/sitemap.xml`,
|
||||||
|
'',
|
||||||
|
'# Карта документации для AI-агентов:',
|
||||||
|
`# ${SITE_URL}/llms.txt`,
|
||||||
|
`# ${SITE_URL}/llms-full.txt`,
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(PUBLIC_DIR, 'robots.txt'), lines.join('\n'), 'utf8');
|
||||||
|
console.log(`${PUBLIC_DIR}/robots.txt создан`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сгенерировать `sitemap.xml` из sidebar + корневые ресурсы для LLM
|
||||||
|
* (llms.txt, llms-full.txt) — чтобы агенты, читающие sitemap, видели их.
|
||||||
|
*/
|
||||||
|
const buildSitemap = (): void => {
|
||||||
|
const sidebar = cfg.themeConfig.sidebar;
|
||||||
|
const entries = flattenSidebar(sidebar);
|
||||||
|
|
||||||
|
const urls = new Set<string>();
|
||||||
|
urls.add(`${SITE_URL}/`);
|
||||||
|
urls.add(`${SITE_URL}/llms.txt`);
|
||||||
|
urls.add(`${SITE_URL}/llms-full.txt`);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const link = entry.link;
|
||||||
|
// cleanUrls: канон без `.html`. Index-страницы — каталог со слешем.
|
||||||
|
urls.add(`${SITE_URL}${link}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = BUILD_DATE.slice(0, 10);
|
||||||
|
const xml = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||||
|
...[...urls].map(
|
||||||
|
(loc) => ` <url><loc>${loc}</loc><lastmod>${today}</lastmod></url>`,
|
||||||
|
),
|
||||||
|
'</urlset>',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(PUBLIC_DIR, 'sitemap.xml'), xml, 'utf8');
|
||||||
|
console.log(`${PUBLIC_DIR}/sitemap.xml создан`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразовать абсолютные ссылки `index.md` в рабочие при открытии
|
||||||
|
* README в репозитории:
|
||||||
|
* - `/docs/foo` → относительный путь `docs/docs/foo.md`;
|
||||||
|
* - корневые ресурсы (`/llms.txt`, `/llms-full.txt`, `*.zip`, `/manifest.json`,
|
||||||
|
* `/sitemap.xml`, `/robots.txt`) — генерируемые, в репозитории отсутствуют,
|
||||||
|
* поэтому ссылки переписываются на абсолютный `SITE_URL`.
|
||||||
|
*/
|
||||||
|
const transformReadmeLinks = (content: string): string => {
|
||||||
|
const linkRe = /\]\((\/[^)\s]*)\)/g;
|
||||||
|
return content.replace(linkRe, (match, href: string) => {
|
||||||
|
const [pathPart, hash = ''] = href.split('#');
|
||||||
|
const hashPart = hash ? `#${hash}` : '';
|
||||||
|
|
||||||
|
if (pathPart.startsWith(DOC_PREFIX)) {
|
||||||
|
const rel = linkToArchiveRel(pathPart);
|
||||||
|
return `](docs/docs/${rel}${hashPart})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `](${SITE_URL}${pathPart}${hashPart})`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/** Скопировать `index.md` документации в корневой README без frontmatter. */
|
/** Скопировать `index.md` документации в корневой README без frontmatter. */
|
||||||
const buildReadme = (): void => {
|
const buildReadme = (): void => {
|
||||||
const indexPath = 'docs/docs/index.md';
|
const indexPath = 'docs/docs/index.md';
|
||||||
@@ -496,7 +522,15 @@ const buildReadme = (): void => {
|
|||||||
}
|
}
|
||||||
const raw = fs.readFileSync(indexPath, 'utf8');
|
const raw = fs.readFileSync(indexPath, 'utf8');
|
||||||
const { body } = parseFrontmatter(raw);
|
const { body } = parseFrontmatter(raw);
|
||||||
fs.writeFileSync('README.md', body.trimStart(), 'utf8');
|
const transformed = transformReadmeLinks(body.trimStart());
|
||||||
|
|
||||||
|
// Порядок: H1 → описание (первый абзац) → ссылка на сайт.
|
||||||
|
const withSiteLink = transformed.replace(
|
||||||
|
/^(#\s[^\n]*\n\n[^\n]+\n)/,
|
||||||
|
`$1\nСайт: ${SITE_URL}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync('README.md', withSiteLink, 'utf8');
|
||||||
console.log(`README.md обновлён из ${indexPath}`);
|
console.log(`README.md обновлён из ${indexPath}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -505,4 +539,6 @@ buildLlmsFull();
|
|||||||
copyMdFiles();
|
copyMdFiles();
|
||||||
buildZip();
|
buildZip();
|
||||||
writeManifest();
|
writeManifest();
|
||||||
|
buildRobots();
|
||||||
|
buildSitemap();
|
||||||
buildReadme();
|
buildReadme();
|
||||||
|
|||||||
153
notes
153
notes
@@ -1,153 +0,0 @@
|
|||||||
ФЛОУ
|
|
||||||
- после создания компонента, заменить шаблонный коментарий документа на реальный.
|
|
||||||
|
|
||||||
|
|
||||||
Проблема, неочевидность слоев (наследие FSD)
|
|
||||||
|
|
||||||
|
|
||||||
Архитектурные слои проекта
|
|
||||||
Каждый нижний слой не знает о существовании верхних. Импорты идут только сверху вниз.
|
|
||||||
pages → layouts → screens → widgets → features → entities → shared
|
|
||||||
---
|
|
||||||
1. Pages (pages/)
|
|
||||||
Точка входа маршрута. Только связывает layout и screen.
|
|
||||||
Правила:
|
|
||||||
- Никакой логики, стилей, разметки кроме композиции
|
|
||||||
- Один page = один layout + один screen
|
|
||||||
Пример:
|
|
||||||
// pages/knv-new.js
|
|
||||||
import { KnvScreen } from 'src/screens/knv'
|
|
||||||
import { MainLayout } from 'src/layouts/main'
|
|
||||||
const KnvNewPage = () => (
|
|
||||||
<MainLayout>
|
|
||||||
<KnvScreen />
|
|
||||||
</MainLayout>
|
|
||||||
)
|
|
||||||
---
|
|
||||||
2. Layouts (src/layouts/)
|
|
||||||
Каркас страницы — общие элементы, которые одинаковы на всех страницах в рамках этого layout.
|
|
||||||
Содержит в ui/: header, footer, sidebar — дочерние компоненты, которые привязаны к layout и не переиспользуются отдельно.
|
|
||||||
Критерий: компонент одинаков на всех страницах, использующих этот layout? → layouts/{name}/ui/
|
|
||||||
Пример:
|
|
||||||
src/layouts/main/
|
|
||||||
├── main.layout.tsx # <Header /> + children + <Footer />
|
|
||||||
├── ui/
|
|
||||||
│ ├── header/ # всегда одинаковый на всех страницах
|
|
||||||
│ └── footer/ # всегда одинаковый на всех страницах
|
|
||||||
---
|
|
||||||
3. Screens (src/screens/)
|
|
||||||
Контент конкретной страницы. Собирает свои секции и переиспользуемые widgets/features/entities.
|
|
||||||
Содержит в ui/: блоки, которые существуют только на этой странице и не переиспользуются.
|
|
||||||
Критерий: компонент используется только на одной странице? → screens/{name}/ui/
|
|
||||||
Пример:
|
|
||||||
src/screens/knv/
|
|
||||||
├── knv.screen.tsx
|
|
||||||
├── ui/
|
|
||||||
│ ├── hero-section/ # hero только на главной КНВ
|
|
||||||
│ ├── products-section/ # секция препаратов только на главной
|
|
||||||
│ ├── diseases-section/ # секция заболеваний только на главной
|
|
||||||
│ └── doctor-section/ # секция врачей только на главной
|
|
||||||
Каждая секция внутри может использовать shared/ui компоненты:
|
|
||||||
// screens/knv/ui/products-section/products-section.widget.tsx
|
|
||||||
import { Carousel } from 'src/shared/ui/carousel'
|
|
||||||
import { ProductCard } from './ui/product-card' // локальный, пока не переиспользуется
|
|
||||||
Когда локальный компонент начинает использоваться на 2+ страницах — выносим в entities/ или shared/ui.
|
|
||||||
---
|
|
||||||
4. Widgets (src/widgets/)
|
|
||||||
Составные блоки с данными/логикой, которые переиспользуются на 2+ страницах.
|
|
||||||
Критерий: блок с бизнес-логикой + данными используется на нескольких страницах? → widgets/
|
|
||||||
Пример: Слайдер «Популярные препараты» с загрузкой данных из API, который показывается и на главной, и на странице заболевания, и в каталоге:
|
|
||||||
src/widgets/
|
|
||||||
├── popular-products-slider/
|
|
||||||
│ ├── popular-products-slider.widget.tsx # Carousel + ProductCard + useProducts()
|
|
||||||
│ ├── hooks/
|
|
||||||
│ │ └── use-products.hook.ts # запрос данных
|
|
||||||
Не widget: секция «Подобрать врача» которая есть только на главной → screens/knv/ui/
|
|
||||||
---
|
|
||||||
5. Features (src/features/)
|
|
||||||
Пользовательское действие или интерактивный сценарий. Содержит бизнес-логику взаимодействия.
|
|
||||||
Критерий: это действие пользователя (отправить форму, авторизоваться, добавить в корзину)? → features/
|
|
||||||
Примеры:
|
|
||||||
src/features/
|
|
||||||
├── auth/ # авторизация (форма + логика + стор)
|
|
||||||
│ ├── auth.feature.tsx
|
|
||||||
│ ├── hooks/
|
|
||||||
│ │ └── use-auth.hook.ts
|
|
||||||
│ └── stores/
|
|
||||||
│ └── auth.store.ts
|
|
||||||
│
|
|
||||||
├── order-drug/ # заказ препарата (кнопка + модалка + API)
|
|
||||||
│ ├── order-drug.feature.tsx
|
|
||||||
│ └── hooks/
|
|
||||||
│ └── use-order.hook.ts
|
|
||||||
Не feature: отображение карточки препарата без взаимодействия → entities/ или shared/ui
|
|
||||||
---
|
|
||||||
6. Entities (src/entities/)
|
|
||||||
Бизнес-сущность с её отображением и типами. Привязана к домену (препарат, заболевание, врач, пользователь).
|
|
||||||
Критерий: это представление бизнес-объекта, которое переиспользуется в разных контекстах? → entities/
|
|
||||||
Примеры:
|
|
||||||
src/entities/
|
|
||||||
├── product/ # Препарат
|
|
||||||
│ ├── ui/
|
|
||||||
│ │ └── product-card/ # карточка препарата (каталог, слайдеры, поиск)
|
|
||||||
│ ├── types/
|
|
||||||
│ │ └── product.type.ts # { id, name, mnn, indication }
|
|
||||||
│ └── index.ts
|
|
||||||
│
|
|
||||||
├── disease/ # Заболевание
|
|
||||||
│ ├── ui/
|
|
||||||
│ │ └── disease-card/
|
|
||||||
│ ├── types/
|
|
||||||
│ │ └── disease.type.ts
|
|
||||||
│ └── index.ts
|
|
||||||
Отличие от shared/ui: entity-компонент знает о бизнес-домене (принимает Product, а не абстрактные пропсы). shared/ui Button не знает ничего о бизнесе.
|
|
||||||
---
|
|
||||||
7. Shared (src/shared/)
|
|
||||||
Переиспользуемые компоненты, утилиты, стили без бизнес-логики.
|
|
||||||
Критерий: компонент не знает о бизнес-домене, работает с абстрактными данными? → shared/
|
|
||||||
src/shared/
|
|
||||||
├── ui/ # UI-компоненты
|
|
||||||
│ ├── carousel/ # принимает children, не знает о препаратах
|
|
||||||
│ ├── container/
|
|
||||||
│ ├── section/
|
|
||||||
│ └── icon-svg/
|
|
||||||
├── styles/ # CSS-переменные, media
|
|
||||||
│ ├── variables.css
|
|
||||||
│ └── media.css
|
|
||||||
├── sprites/ # SVG-спрайты
|
|
||||||
└── lib/ # утилиты, хелперы
|
|
||||||
---
|
|
||||||
Сводная таблица принятия решений
|
|
||||||
Вопрос Да → Нет ↓
|
|
||||||
Это точка входа маршрута? pages/ ↓
|
|
||||||
Одинаков на всех страницах layout? layouts/{name}/ui/ ↓
|
|
||||||
Используется только на одной странице? screens/{name}/ui/ ↓
|
|
||||||
Составной блок с данными на 2+ страницах? widgets/ ↓
|
|
||||||
Это действие пользователя с логикой? features/ ↓
|
|
||||||
Привязан к бизнес-сущности? entities/ ↓
|
|
||||||
Абстрактный UI без бизнес-логики? shared/ui/ —
|
|
||||||
|
|
||||||
|
|
||||||
И еще размышлений
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Что у нас по факту
|
|
||||||
Мы взяли FSD и добавили:
|
|
||||||
- Слой Layouts — каркас страницы
|
|
||||||
- Слой Screens — контент страницы с локальными компонентами
|
|
||||||
- Правило scope — компонент живёт максимально близко к месту использования, и поднимается на уровень выше только при переиспользовании
|
|
||||||
Это не FSD в чистом виде. Но и не что-то принципиально новое.
|
|
||||||
Существующие названия
|
|
||||||
Feature-Sliced Design (FSD) — то от чего отталкиваемся, но расходимся в деталях.
|
|
||||||
Screaming Architecture (Robert C. Martin) — структура проекта "кричит" о том, что он делает. Открываешь screens/knv/ui/ — видишь КНВ. Открываешь widgets/ — видишь глобальное. Это близко к нашему подходу.
|
|
||||||
Colocation Principle — держи код рядом с тем, где он используется. Именно этот принцип отличает нас от FSD: page-specific компоненты лежат внутри screen, а не в глобальном widgets/.
|
|
||||||
Как назвать
|
|
||||||
Я бы не стал придумывать новое название. Это:
|
|
||||||
> FSD с принципом колокации (FSD + Colocation)
|
|
||||||
Или проще — адаптированный FSD. Суть отличия укладывается в одно правило:
|
|
||||||
> Компонент живёт на самом низком уровне, где он используется. Поднимается выше только при переиспользовании на 2+ страницах.
|
|
||||||
Если хочется краткое название для внутренней документации — можно SLD (Scoped Layer Design), подчёркивая что каждый компонент привязан к scope (shared → entity → feature → widget → screen → layout → page).
|
|
||||||
Reference in New Issue
Block a user