feat: добавить хаб документаций
- добавлен React/Vite-лендинг с карточками документаций - добавлена генерация корневого llms.txt из конфига документов - добавлена сборка SLM Design через VitePress - добавлены Dockerfile, Caddyfile и Gitea CI/CD - настроены контекстные Link headers для llms.txt
This commit is contained in:
77
canons/style-guide/applied/aliases.md
Normal file
77
canons/style-guide/applied/aliases.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Алиасы импортов
|
||||
description: Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infra, ui, shared]
|
||||
---
|
||||
|
||||
# Алиасы импортов
|
||||
|
||||
Какие алиасы импортов есть в проекте и как ими пользоваться.
|
||||
|
||||
## Конфиг
|
||||
|
||||
`tsconfig.json` в корне проекта:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"app/*": ["./src/app/*"],
|
||||
"layouts/*": ["./src/layouts/*"],
|
||||
"screens/*": ["./src/screens/*"],
|
||||
"widgets/*": ["./src/widgets/*"],
|
||||
"business/*": ["./src/business/*"],
|
||||
"infra/*": ["./src/infra/*"],
|
||||
"ui/*": ["./src/ui/*"],
|
||||
"shared/*": ["./src/shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет.
|
||||
|
||||
## Правила
|
||||
|
||||
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
|
||||
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
|
||||
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
|
||||
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/layers)).
|
||||
|
||||
**Хорошо**
|
||||
|
||||
```ts
|
||||
import { Button } from 'ui/button'
|
||||
import { useUser } from 'business/user'
|
||||
import { formatDate } from 'shared/utils/date'
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
|
||||
```ts
|
||||
// Относительный путь между модулями
|
||||
import { Button } from '../../../ui/button'
|
||||
|
||||
// Префикс @/, которого нет в paths
|
||||
import { Button } from '@/ui/button'
|
||||
|
||||
// Алиас на src — не предусмотрен
|
||||
import { Button } from 'src/ui/button'
|
||||
```
|
||||
|
||||
## Внутри модуля
|
||||
|
||||
Внутри своего модуля — относительные пути:
|
||||
|
||||
```ts
|
||||
// src/ui/button/button.tsx
|
||||
import styles from './button.module.css'
|
||||
import { Icon } from './icon'
|
||||
```
|
||||
|
||||
Не использовать алиас на самого себя:
|
||||
|
||||
```ts
|
||||
// Плохо — алиас вместо относительного пути внутри модуля
|
||||
import { Icon } from 'ui/button/icon'
|
||||
```
|
||||
114
canons/style-guide/applied/biome.md
Normal file
114
canons/style-guide/applied/biome.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Biome
|
||||
description: Установка и настройка линтера-форматтера в новом проекте.
|
||||
keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier]
|
||||
---
|
||||
|
||||
# Biome
|
||||
|
||||
Установка и настройка линтера-форматтера в новом проекте.
|
||||
|
||||
## Требования
|
||||
|
||||
- Node.js 18+.
|
||||
- Проект без установленного ESLint и Prettier (они конфликтуют с Biome).
|
||||
|
||||
## Установка
|
||||
|
||||
1. Установить пакет:
|
||||
|
||||
```bash
|
||||
npm install --save-dev --save-exact @biomejs/biome
|
||||
```
|
||||
|
||||
2. Инициализировать конфиг:
|
||||
|
||||
```bash
|
||||
npx @biomejs/biome init
|
||||
```
|
||||
|
||||
В корне появится `biome.json` с дефолтными настройками.
|
||||
|
||||
3. Привести `biome.json` к стандартному виду (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`.
|
||||
|
||||
4. Добавить скрипты в `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "biome lint .",
|
||||
"format": "biome format --write .",
|
||||
"check": "biome check --write ."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Скрипт | Что делает |
|
||||
|--------|-----------|
|
||||
| `lint` | Проверка правил без правок |
|
||||
| `format` | Автоформатирование всех файлов |
|
||||
| `check` | Lint + format + organize imports в один проход (основная команда) |
|
||||
|
||||
## Стандартный `biome.json`
|
||||
|
||||
Дефолтный `biome.json`, созданный `biome init`, заменяется стандартным конфигом проекта.
|
||||
|
||||
Стандартный `biome.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.templates", "!src/infra/**/generated"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 120
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnknownMediaFeatureName": "off"
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/infra/**/generated` исключается из Biome, потому что generated-файлы не правятся руками. При этом generated-файлы остаются в git.
|
||||
|
||||
Правила `suspicious/noUnknownAtRules` и `correctness/noUnknownMediaFeatureName` отключены, потому что проектный CSS-стек использует `@custom-media` и другие конструкции, которые Biome может не распознавать.
|
||||
|
||||
## Интеграция с VS Code
|
||||
|
||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](/docs/applied/vscode).
|
||||
165
canons/style-guide/applied/component.md
Normal file
165
canons/style-guide/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'
|
||||
```
|
||||
51
canons/style-guide/applied/creating-project/from-template.md
Normal file
51
canons/style-guide/applied/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
canons/style-guide/applied/creating-project/manual.md
Normal file
90
canons/style-guide/applied/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/applied/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/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)).
|
||||
|
||||
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
|
||||
|
||||
## Порядок установки
|
||||
|
||||
Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами.
|
||||
|
||||
### 1. Next.js
|
||||
|
||||
Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
|
||||
|
||||
См. [Next.js](/docs/applied/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/`, `infra/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
|
||||
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
|
||||
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.
|
||||
112
canons/style-guide/applied/creating-project/nextjs.md
Normal file
112
canons/style-guide/applied/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/`, `infra/`, `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`).
|
||||
121
canons/style-guide/applied/data-fetch/business-composition.md
Normal file
121
canons/style-guide/applied/data-fetch/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.
|
||||
|
||||
Такая логика не пишется в `infra/`. REST-клиент остаётся прозрачным адаптером к API.
|
||||
|
||||
## Пример поверх одного GET-хука
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-available-pets.hook.ts
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
/**
|
||||
* Доменный список доступных питомцев.
|
||||
*/
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`useGetPetList` — infra-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`.
|
||||
|
||||
## Пример композиции нескольких GET-хуков
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-pets-dashboard.hook.ts
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
/**
|
||||
* Данные dashboard по питомцам.
|
||||
*/
|
||||
export const usePetsDashboard = () => {
|
||||
const availablePets = useGetPetList({ status: StatusEnum.Available })
|
||||
const pendingPets = useGetPetList({ status: StatusEnum.Pending })
|
||||
const soldPets = useGetPetList({ status: StatusEnum.Sold })
|
||||
|
||||
return {
|
||||
availablePets,
|
||||
pendingPets,
|
||||
soldPets,
|
||||
total:
|
||||
(availablePets.data?.length ?? 0) +
|
||||
(pendingPets.data?.length ?? 0) +
|
||||
(soldPets.data?.length ?? 0),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Композиция нескольких запросов не добавляется в `infra/pet-store-api/hooks/`, потому что это уже сценарий потребления данных.
|
||||
|
||||
## Пример auth-состояния
|
||||
|
||||
```ts
|
||||
// src/business/auth/hooks/use-auth-state.hook.ts
|
||||
import { useGetCurrentUser } from 'infra/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-смысл внутри infra-хука
|
||||
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте.
|
||||
88
canons/style-guide/applied/data-fetch/client-get-hook.md
Normal file
88
canons/style-guide/applied/data-fetch/client-get-hook.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
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 { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold]
|
||||
|
||||
export function PetTabs() {
|
||||
const [status, setStatus] = useState(StatusEnum.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/infra/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/findByStatus?status=${status}`],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
```
|
||||
|
||||
Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
- Данные нужны до первого HTML — [Серверный await](/docs/applied/data-fetch/server-await).
|
||||
- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
|
||||
- Нужно вычислить бизнес-состояние — [Business-композиция](/docs/applied/data-fetch/business-composition).
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
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/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.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,
|
||||
StatusEnum,
|
||||
} from 'infra/pet-store-api'
|
||||
|
||||
type PetsLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default async function PetsLayout({ children }: PetsLayoutProps) {
|
||||
const params = { status: StatusEnum.Available }
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params)
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fallback: {
|
||||
[unstable_serialize(getPetListKey(params))]: availablePetsPromise,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`.
|
||||
|
||||
## Клиентский компонент
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
|
||||
|
||||
export function PetList() {
|
||||
const { data: pets, isLoading } = useGetPetList({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
|
||||
if (isLoading) return <div>Загрузка...</div>
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{pets?.map((pet) => (
|
||||
<li key={pet.id}>{pet.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента.
|
||||
|
||||
## Что важно
|
||||
|
||||
- Ключ `fallback` должен совпадать с ключом GET-хука.
|
||||
- `fallback` использует ту же key-функцию и те же params, что и GET-хук.
|
||||
- Серверный код вызывает метод клиента, а не GET-хук.
|
||||
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
|
||||
- Эта стратегия не означает ручную работу с кешем в компонентах.
|
||||
|
||||
## Когда не использовать
|
||||
|
||||
Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/applied/data-fetch/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).
|
||||
100
canons/style-guide/applied/data-fetch/index.md
Normal file
100
canons/style-guide/applied/data-fetch/index.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Получение данных
|
||||
description: Как получать данные с учётом рендера страницы.
|
||||
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
|
||||
---
|
||||
|
||||
# Получение данных
|
||||
|
||||
Как получать данные с учётом рендера страницы.
|
||||
|
||||
Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](/docs/applied/rest-client/setup/).
|
||||
|
||||
## Сначала определите рендер страницы
|
||||
|
||||
В 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/applied/data-fetch/server-await) |
|
||||
| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests) |
|
||||
| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down) |
|
||||
| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data) |
|
||||
| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook) |
|
||||
| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/applied/data-fetch/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/findByStatus?status=available'],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
|
||||
)
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
```
|
||||
|
||||
Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Параллельные серверные запросы
|
||||
description: Как запускать независимые REST-запросы на сервере без waterfall.
|
||||
keywords: [rest, promise.all, параллельные запросы, server components]
|
||||
---
|
||||
|
||||
# Параллельные серверные запросы
|
||||
|
||||
Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Запросы независимы друг от друга.
|
||||
- Все данные нужны текущему серверному компоненту перед возвратом UI.
|
||||
- Нельзя или не нужно стримить часть UI отдельно.
|
||||
|
||||
## Хорошо
|
||||
|
||||
```tsx
|
||||
import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetsDashboardScreen } from 'screens/pets-dashboard'
|
||||
|
||||
export default async function PetsDashboardPage() {
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Available,
|
||||
})
|
||||
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Pending,
|
||||
})
|
||||
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.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: StatusEnum.Available,
|
||||
})
|
||||
const pendingPets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.Pending,
|
||||
})
|
||||
const soldPets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.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({ orderId: Number(id) })
|
||||
const pet = await petStoreApi.pet.getPetById({ petId: order.petId })
|
||||
|
||||
return <OrderScreen order={order} pet={pet} />
|
||||
}
|
||||
```
|
||||
|
||||
Не превращайте зависимый сценарий в `Promise.all` искусственно.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).
|
||||
64
canons/style-guide/applied/data-fetch/pass-promise-down.md
Normal file
64
canons/style-guide/applied/data-fetch/pass-promise-down.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
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, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetListSection } from 'widgets/pet-list-section'
|
||||
import { PetListSkeleton } from 'widgets/pet-list-section'
|
||||
import type { Pet } from 'infra/pet-store-api'
|
||||
|
||||
export default function PetsPage() {
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.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/applied/data-fetch/client-hooks-initial-data).
|
||||
|
||||
## Что не делать
|
||||
|
||||
```tsx
|
||||
// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии
|
||||
return <PetListClient petsPromise={petsPromise} />
|
||||
```
|
||||
|
||||
Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента.
|
||||
88
canons/style-guide/applied/data-fetch/server-await.md
Normal file
88
canons/style-guide/applied/data-fetch/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, StatusEnum } from 'infra/pet-store-api'
|
||||
import { PetsScreen } from 'screens/pets'
|
||||
|
||||
export default async function PetsPage() {
|
||||
const pets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: StatusEnum.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 'infra/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({ petId: Number(id) }).catch(() => null)
|
||||
|
||||
if (!pet) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <PetDetailScreen pet={pet} />
|
||||
}
|
||||
```
|
||||
|
||||
Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента.
|
||||
|
||||
## Что не делать
|
||||
|
||||
```tsx
|
||||
// Плохо — хуки нельзя вызывать в Server Component
|
||||
const { data } = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
// Плохо — прямой fetch в обход клиента
|
||||
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
|
||||
```
|
||||
|
||||
Если данные нужны на сервере, вызывайте метод REST-клиента напрямую.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
- Несколько независимых запросов — [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests).
|
||||
- Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).
|
||||
- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
|
||||
128
canons/style-guide/applied/fonts.md
Normal file
128
canons/style-guide/applied/fonts.md
Normal file
@@ -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
canons/style-guide/applied/images.md
Normal file
95
canons/style-guide/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;
|
||||
}
|
||||
```
|
||||
81
canons/style-guide/applied/localization.md
Normal file
81
canons/style-guide/applied/localization.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Локализация
|
||||
description: Как организовать локализацию как infra-модуль.
|
||||
---
|
||||
|
||||
# Локализация
|
||||
|
||||
Как организовать локализацию как infra-модуль.
|
||||
|
||||
## Назначение
|
||||
|
||||
Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов.
|
||||
|
||||
Код локализации живёт в `src/infra/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infra-модуля.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
src/infra/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
|
||||
```
|
||||
|
||||
Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infra/i18n`.
|
||||
|
||||
## Подключение
|
||||
|
||||
`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infra/i18n/`.
|
||||
|
||||
```tsx
|
||||
// src/app/layout.tsx
|
||||
import type { ReactNode } from 'react'
|
||||
import { I18nProvider } from 'infra/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 'infra/i18n'
|
||||
|
||||
export const ProfileTitle = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <h1>{t('profile.title')}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
## Правила
|
||||
|
||||
- Локализация живёт в `infra/i18n/`.
|
||||
- `app/` только подключает готовый provider и передаёт locale.
|
||||
- Словари не импортируются напрямую в компоненты, screens или business-модули.
|
||||
- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск.
|
||||
- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться.
|
||||
- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infra-модуля.
|
||||
156
canons/style-guide/applied/module.md
Normal file
156
canons/style-guide/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
|
||||
```
|
||||
186
canons/style-guide/applied/page-level.md
Normal file
186
canons/style-guide/applied/page-level.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: Файлы роутинга
|
||||
description: Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||
---
|
||||
|
||||
# Файлы роутинга
|
||||
|
||||
Как работать со страницами и другими файлами роутинга Next.js App Router.
|
||||
|
||||
## Назначение
|
||||
|
||||
`src/app/**` — точка входа приложения и слой файлового роутинга Next.js.
|
||||
|
||||
Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen.
|
||||
|
||||
Границы слоя описаны в [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app).
|
||||
|
||||
## Граница ответственности
|
||||
|
||||
| Область | Где живёт |
|
||||
|---|---|
|
||||
| Файлы маршрутов (`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/`, `infra/`, `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
|
||||
import type { Metadata } from 'next'
|
||||
import { ProfileScreen } from 'screens/profile'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Профиль',
|
||||
description: 'Страница профиля пользователя',
|
||||
}
|
||||
|
||||
type ProfilePageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
const { id } = await params
|
||||
|
||||
return <ProfileScreen id={id} />
|
||||
}
|
||||
```
|
||||
|
||||
## Данные первого рендера
|
||||
|
||||
Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя.
|
||||
|
||||
```tsx
|
||||
import { notFound } from 'next/navigation'
|
||||
import { userApi } from 'infra/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 'infra/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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](/docs/applied/data-fetch/), [Начальные данные для клиентских хуков](/docs/applied/data-fetch/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
|
||||
'use client'
|
||||
|
||||
import { ErrorScreen } from 'screens/error'
|
||||
|
||||
type ErrorPageProps = {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
const ErrorPage = ({ error, reset }: ErrorPageProps) => {
|
||||
return <ErrorScreen error={error} reset={reset} />
|
||||
}
|
||||
|
||||
export default ErrorPage
|
||||
```
|
||||
70
canons/style-guide/applied/postcss.md
Normal file
70
canons/style-guide/applied/postcss.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: PostCSS
|
||||
description: Установка и настройка CSS-процессора в новом проекте.
|
||||
keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор]
|
||||
---
|
||||
|
||||
# PostCSS
|
||||
|
||||
Установка и настройка CSS-процессора в новом проекте.
|
||||
|
||||
## Зачем PostCSS
|
||||
|
||||
Подключаем ради двух вещей:
|
||||
|
||||
- **Вложенность** — `&:hover`, `&::before`, `&._active` и `@media` внутри селектора. Без процессора нативный CSS не покрывает всех нужных кейсов вложенности.
|
||||
- **`@custom-media`** — единые breakpoints проекта (`@media (--md)`) вместо магических `min-width`. Определяются в одном месте, переиспользуются везде.
|
||||
|
||||
Autoprefixer и `@csstools/postcss-global-data` идут довеском под эти две задачи.
|
||||
|
||||
## Требования
|
||||
|
||||
- Next.js 14+ (App Router).
|
||||
- Node.js 18+.
|
||||
|
||||
CSS Modules поддерживаются Next.js из коробки — отдельной установки не требуют.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Установить PostCSS-плагины как devDependencies:
|
||||
|
||||
```bash
|
||||
npm install -D postcss-custom-media postcss-nesting autoprefixer @csstools/postcss-global-data
|
||||
```
|
||||
|
||||
2. Создать `postcss.config.mjs` в корне проекта (см. «Конфиг»).
|
||||
|
||||
## Конфиг
|
||||
|
||||
Файл `postcss.config.mjs` в корне проекта.
|
||||
|
||||
```js
|
||||
// postcss.config.mjs
|
||||
export default {
|
||||
plugins: {
|
||||
'@csstools/postcss-global-data': {
|
||||
files: ['src/shared/styles/media.css'],
|
||||
},
|
||||
'postcss-custom-media': {},
|
||||
'postcss-nesting': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Разбор плагинов
|
||||
|
||||
| Плагин | Назначение |
|
||||
|--------|------------|
|
||||
| `@csstools/postcss-global-data` | Подгружает определения `@custom-media` из `src/shared/styles/media.css` перед обработкой каждого CSS-модуля. Семантика — «глобальный файл определений, который не импортируется в исходники» |
|
||||
| `postcss-custom-media` | Поддержка `@custom-media --md (...)` и использования `@media (--md) {}`. Определения берутся из файла, который подгрузил `postcss-global-data` |
|
||||
| `postcss-nesting` | Нативная CSS-вложенность: `&:hover`, `&::before`, `&._active` |
|
||||
| `autoprefixer` | Добавление вендорных префиксов по browserslist |
|
||||
|
||||
### Почему внешний файл с `@custom-media`, а не `@import`
|
||||
|
||||
`@custom-media` — глобальные определения, одинаковые для всего проекта. Держим их в `src/shared/styles/media.css`. `@csstools/postcss-global-data` подгружает этот файл перед каждым модулем, а `postcss-custom-media` заменяет `@media (--md)` на конкретные `@media (min-width: ...)` на этапе сборки. Сами определения в бандл не попадают.
|
||||
|
||||
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
||||
|
||||
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](/docs/applied/styles/styles-usage), раздел «Импорт стилей»).
|
||||
101
canons/style-guide/applied/project-structure.md
Normal file
101
canons/style-guide/applied/project-structure.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Структура проекта
|
||||
description: Из чего состоит проект и где что лежит.
|
||||
---
|
||||
|
||||
# Структура проекта
|
||||
|
||||
Из чего состоит проект и где что лежит.
|
||||
|
||||
## Корень репозитория
|
||||
|
||||
```text
|
||||
project-root/
|
||||
├── .templates/ # Шаблоны для генерации модулей
|
||||
├── .vscode/ # Настройки и рекомендуемые расширения VS Code
|
||||
├── public/ # Статика, доступная по прямому URL
|
||||
├── src/ # Исходный код приложения
|
||||
├── .env.example # Переменные окружения проекта (шаблон)
|
||||
├── .env # Переменные окружения проекта (не коммитить)
|
||||
├── .gitignore
|
||||
├── AGENTS.md # Инструкции для AI-агентов
|
||||
├── biome.json # Линтер и форматтер (вместо ESLint + Prettier)
|
||||
├── next.config.ts # Конфигурация Next.js
|
||||
├── package.json # Зависимости и скрипты
|
||||
├── postcss.config.mjs # Конфигурация PostCSS
|
||||
└── tsconfig.json # Конфигурация TypeScript
|
||||
```
|
||||
|
||||
## Папка `public/`
|
||||
|
||||
Хранит статические файлы, которые отдаются по прямому URL без обработки сборщиком:
|
||||
|
||||
```text
|
||||
public/
|
||||
└── og-image.png
|
||||
```
|
||||
|
||||
Компоненты, стили и другой исходный код здесь не размещаются.
|
||||
|
||||
## Папка `src/`
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/ # Роутинг Next.js и точка входа приложения
|
||||
├── layouts/ # Каркасы страниц (header, footer, sidebar)
|
||||
├── screens/ # Контент конкретной страницы
|
||||
├── widgets/ # Составные блоки интерфейса, не привязанные к домену
|
||||
├── business/ # Бизнес-домены (auth, catalog, orders)
|
||||
├── infra/ # Техсервисы (theme, i18n, API-адаптеры)
|
||||
├── ui/ # UI-кит без бизнес-логики
|
||||
└── shared/ # Общие ресурсы (утилиты, типы, стили)
|
||||
```
|
||||
|
||||
Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture/).
|
||||
|
||||
### Папка `app/`
|
||||
|
||||
Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
|
||||
`app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы.
|
||||
|
||||
Подробнее о границах слоя: [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app).
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── layout.tsx # Корневой layout
|
||||
└── page.tsx # Главная страница
|
||||
```
|
||||
|
||||
## Папка `.templates/`
|
||||
|
||||
Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля:
|
||||
|
||||
```text
|
||||
.templates/
|
||||
├── component/ # Шаблон компонента
|
||||
├── screen/ # Шаблон экрана
|
||||
├── layout/ # Шаблон layout
|
||||
├── widget/ # Шаблон виджета
|
||||
├── module/ # Шаблон бизнес-модуля
|
||||
└── store/ # Шаблон стора
|
||||
```
|
||||
|
||||
Подробнее о генерации описано в разделе [Шаблоны генерации](/docs/applied/templates/templates-intro).
|
||||
|
||||
## Конфигурационные файлы
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `next.config.ts` | Настройки Next.js: редиректы, переменные окружения, webpack |
|
||||
| `tsconfig.json` | Настройки TypeScript: пути, строгость, таргет |
|
||||
| `biome.json` | Правила линтера и форматтера Biome |
|
||||
| `postcss.config.mjs` | Подключение PostCSS-плагинов (CSS Modules, custom media) |
|
||||
| `package.json` | Зависимости, версии, npm-скрипты |
|
||||
| `AGENTS.md` | Инструкции для AI-агентов, работающих в проекте |
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
- `.env` — переменные окружения проекта, запрещено коммитить
|
||||
- `.env.example` — шаблон, коммитится в репозиторий
|
||||
|
||||
Переменные с префиксом `NEXT_PUBLIC_` доступны в клиентском коде. Остальные доступны только на сервере.
|
||||
50
canons/style-guide/applied/rest-client/index.md
Normal file
50
canons/style-guide/applied/rest-client/index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: REST-клиент
|
||||
description: Настройка REST-клиента сервиса для работы с внешним API.
|
||||
keywords: [rest, api, данные, infra, клиент, swr, стратегии]
|
||||
---
|
||||
|
||||
# REST-клиент
|
||||
|
||||
Настройка REST-клиента сервиса для работы с внешним API.
|
||||
|
||||
## Настройка
|
||||
|
||||
Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`.
|
||||
|
||||
На этом этапе внешний API оформляется как модуль слоя `infra/`.
|
||||
|
||||
Клиент отвечает за:
|
||||
|
||||
- генерацию или ручное описание методов API;
|
||||
- настройку `baseUrl`;
|
||||
- заголовки и авторизацию;
|
||||
- обработку ошибок;
|
||||
- кастомизацию и расширение типов;
|
||||
- GET-хуки для клиентских компонентов;
|
||||
- прямое использование методов клиента в серверном коде и submit-функциях;
|
||||
- публичный API модуля.
|
||||
|
||||
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
|
||||
|
||||
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Настройка REST-клиента](/docs/applied/rest-client/setup/)
|
||||
- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
|
||||
- [Ручное создание](/docs/applied/rest-client/setup/manual)
|
||||
- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
|
||||
- [Использование REST-клиента](/docs/applied/rest-client/usage)
|
||||
|
||||
## Как читать раздел
|
||||
|
||||
Если API ещё не подключён — начните с [Настройки REST-клиента](/docs/applied/rest-client/setup/).
|
||||
|
||||
Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](/docs/applied/rest-client/usage).
|
||||
|
||||
Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](/docs/applied/data-fetch/).
|
||||
|
||||
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks).
|
||||
|
||||
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.
|
||||
272
canons/style-guide/applied/rest-client/setup/auto.md
Normal file
272
canons/style-guide/applied/rest-client/setup/auto.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
title: Автогенерация REST-клиента
|
||||
description: Генерация REST-клиента из OpenAPI-спецификации.
|
||||
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
|
||||
---
|
||||
|
||||
# Автогенерация REST-клиента
|
||||
|
||||
Генерация REST-клиента из OpenAPI-спецификации.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
|
||||
|
||||
## Пример API
|
||||
|
||||
В примерах используется Swagger Petstore:
|
||||
|
||||
```text
|
||||
https://petstore3.swagger.io/api/v3/openapi.json
|
||||
```
|
||||
|
||||
Имена модуля:
|
||||
|
||||
```text
|
||||
src/infra/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/infra/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/infra/pet-store-api/generated/
|
||||
└── pet-store-api.generated.ts
|
||||
```
|
||||
|
||||
Сгенерированный файл не правится руками и коммитится в репозиторий.
|
||||
|
||||
## Проверка методов
|
||||
|
||||
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
|
||||
|
||||
Для Petstore нужны GET-операции вида:
|
||||
|
||||
```ts
|
||||
petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available })
|
||||
petStoreApi.pet.getPetById({ petId: 10 })
|
||||
```
|
||||
|
||||
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
|
||||
|
||||
## Алгоритм для агента
|
||||
|
||||
После генерации агент должен действовать по шагам:
|
||||
|
||||
1. Открыть `generated/{service-name}.generated.ts`.
|
||||
2. Найти фактические имена GET-методов клиента.
|
||||
3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа.
|
||||
4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента.
|
||||
5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай.
|
||||
6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
|
||||
7. В key-функции вернуть `null`, если обязательные параметры не готовы.
|
||||
8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
||||
9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`.
|
||||
10. Экспортировать хук и key-функцию из `hooks/index.ts`.
|
||||
11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`.
|
||||
|
||||
Что агент не должен делать:
|
||||
|
||||
- Не использовать ключ `--swr` генератора.
|
||||
- Не править `generated/*.generated.ts` руками.
|
||||
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
|
||||
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
|
||||
- Не создавать словари enum-маппинга внутри GET-хука.
|
||||
- Не объявлять DTO и response-типы в файле хука.
|
||||
- Не вызывать `useSWR` условно.
|
||||
- Не добавлять `throw` в fetcher для неготовых params.
|
||||
|
||||
## `client.ts`
|
||||
|
||||
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/client.ts
|
||||
import { Api, HttpClient } from './generated/pet-store-api.generated'
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('NEXT_PUBLIC_PET_STORE_API_BASE_URL is required')
|
||||
}
|
||||
|
||||
const httpClient = new HttpClient({
|
||||
baseUrl,
|
||||
baseApiParams: {
|
||||
secure: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const petStoreApi = new Api(httpClient)
|
||||
```
|
||||
|
||||
Локальное значение `NEXT_PUBLIC_PET_STORE_API_BASE_URL` задаётся в `.env.local`. Не добавляйте fallback вроде `?? 'http://localhost:8080/api/v3'` или `?? ''`: если env-переменная не задана, клиент должен падать с явной ошибкой конфигурации.
|
||||
|
||||
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
|
||||
|
||||
## GET-хуки
|
||||
|
||||
GET-хуки пишутся вручную после проверки generated-методов.
|
||||
|
||||
Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`:
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную карточку питомца с кешированием результата.
|
||||
*/
|
||||
export const useGetPetDetail = (
|
||||
params?: GetPetByIdParams | null,
|
||||
config?: SWRConfiguration<Pet>,
|
||||
) => {
|
||||
const key = getPetDetailKey(params)
|
||||
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
|
||||
|
||||
## Расширение сгенерированных типов
|
||||
|
||||
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
|
||||
|
||||
```text
|
||||
src/infra/biocad-less-api/
|
||||
├── generated/
|
||||
│ └── biocad-less-api.generated.ts
|
||||
├── types/
|
||||
│ ├── term.ts
|
||||
│ └── index.ts
|
||||
├── client.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Пример расширения generated-типа:
|
||||
|
||||
```ts
|
||||
// src/infra/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/infra/biocad-less-api/types/index.ts
|
||||
export type { TermRecordItemExtended } from './term'
|
||||
```
|
||||
|
||||
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
|
||||
|
||||
## Публичный API
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type {
|
||||
FindPetsByStatusParams,
|
||||
GetPetByIdParams,
|
||||
Pet,
|
||||
} from './generated/pet-store-api.generated'
|
||||
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infra/pet-store-api`, не из `generated/`.
|
||||
|
||||
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
|
||||
|
||||
```ts
|
||||
// src/infra/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` проверьте [использование REST-клиента](/docs/applied/rest-client/usage) или добавьте [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks) для Client Components.
|
||||
313
canons/style-guide/applied/rest-client/setup/hooks.md
Normal file
313
canons/style-guide/applied/rest-client/setup/hooks.md
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: GET-хуки REST-клиента
|
||||
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
keywords: [rest, swr, get-хуки, client components, infra]
|
||||
---
|
||||
|
||||
# GET-хуки REST-клиента
|
||||
|
||||
Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
|
||||
## Зачем нужны
|
||||
|
||||
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
|
||||
|
||||
## Где лежат
|
||||
|
||||
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── pet-store-api/
|
||||
├── client.ts
|
||||
├── generated/
|
||||
├── hooks/
|
||||
│ ├── use-get-pet-list.hook.ts
|
||||
│ ├── use-get-pet-detail.hook.ts
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Контракт
|
||||
|
||||
- Один GET-хук = один GET-метод клиента.
|
||||
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
||||
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
||||
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
||||
- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration<Data>`.
|
||||
- Key-функция принимает те же `params`, что и хук.
|
||||
- Key-функция возвращает `null`, если обязательные параметры не готовы.
|
||||
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
|
||||
- Хук вызывает `useSWR` один раз и безусловно.
|
||||
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`.
|
||||
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
||||
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
||||
- Хук не объединяет несколько запросов.
|
||||
- Хук не маппит DTO в доменную модель.
|
||||
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
||||
|
||||
## Формат SWR-ключа
|
||||
|
||||
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
|
||||
|
||||
Формат ключа:
|
||||
|
||||
```ts
|
||||
['pet-store-api', '/pet/10'] as const
|
||||
```
|
||||
|
||||
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
|
||||
- Второй элемент — endpoint запроса: path и query string.
|
||||
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
|
||||
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
|
||||
- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`.
|
||||
- Не используйте только строку endpoint без имени сервиса.
|
||||
|
||||
Примеры ключей:
|
||||
|
||||
```ts
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
|
||||
if (!params?.tags.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
|
||||
}
|
||||
```
|
||||
|
||||
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
|
||||
|
||||
## Пример списка
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type {
|
||||
FindPetsByStatusParams,
|
||||
Pet,
|
||||
} from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
||||
if (!params?.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список питомцев по статусу.
|
||||
*/
|
||||
export const useGetPetList = (
|
||||
params?: FindPetsByStatusParams | null,
|
||||
config?: SWRConfiguration<Pet[]>,
|
||||
) => {
|
||||
const key = getPetListKey(params)
|
||||
const fetcher = () => petStoreApi.pet.findPetsByStatus(
|
||||
params as FindPetsByStatusParams,
|
||||
)
|
||||
|
||||
return useSWR<Pet[]>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
|
||||
|
||||
## Пример detail-запроса
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
||||
if (!params?.petId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ['pet-store-api', `/pet/${params.petId}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную карточку питомца с кешированием результата.
|
||||
*/
|
||||
export const useGetPetDetail = (
|
||||
params?: GetPetByIdParams | null,
|
||||
config?: SWRConfiguration<Pet>,
|
||||
) => {
|
||||
const key = getPetDetailKey(params)
|
||||
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
## Пример без параметров
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { StoreInventory } from '../types'
|
||||
|
||||
export const getStoreInventoryKey = () => {
|
||||
return ['pet-store-api', '/store/inventory'] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает инвентарь магазина.
|
||||
*/
|
||||
export const useGetStoreInventory = (
|
||||
config?: SWRConfiguration<StoreInventory>,
|
||||
) => {
|
||||
return useSWR<StoreInventory>(
|
||||
getStoreInventoryKey(),
|
||||
() => petStoreApi.store.getInventory(),
|
||||
config,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
|
||||
|
||||
## Отложенный запрос
|
||||
|
||||
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
|
||||
|
||||
```ts
|
||||
const key = getPetDetailKey(params)
|
||||
```
|
||||
|
||||
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
|
||||
|
||||
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
|
||||
|
||||
## Экспорт
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/hooks/index.ts
|
||||
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
|
||||
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
|
||||
export {
|
||||
getStoreInventoryKey,
|
||||
useGetStoreInventory,
|
||||
} from './use-get-store-inventory.hook'
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type {
|
||||
FindPetsByStatusParams,
|
||||
GetPetByIdParams,
|
||||
Pet,
|
||||
} from './generated/pet-store-api.generated'
|
||||
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
export type { StoreInventory } from './types'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую.
|
||||
|
||||
## Где заканчивается infra
|
||||
|
||||
```ts
|
||||
// Хорошо: infra, прозрачный GET-хук
|
||||
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
|
||||
```
|
||||
|
||||
```ts
|
||||
// Хорошо: business, доменная интерпретация
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList({ status: StatusEnum.Available })
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```ts
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', '/pet/findByStatus?status=available'],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
|
||||
)
|
||||
|
||||
// Плохо — проверка готовности размазана по хуку
|
||||
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
|
||||
const key = params?.petId ? getPetDetailKey(params) : null
|
||||
const fetcher = () => {
|
||||
if (!params?.petId) {
|
||||
throw new Error('Pet id is required')
|
||||
}
|
||||
|
||||
return petStoreApi.pet.getPetById(params)
|
||||
}
|
||||
|
||||
return useSWR<Pet>(key, fetcher)
|
||||
}
|
||||
|
||||
// Плохо — условный вызов useSWR нарушает rules of hooks
|
||||
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
|
||||
const key = getPetDetailKey(params)
|
||||
|
||||
if (key === null) {
|
||||
return useSWR(null, null)
|
||||
}
|
||||
|
||||
return useSWR(key, () => petStoreApi.pet.getPetById(params))
|
||||
}
|
||||
|
||||
// Плохо — несколько GET внутри infra-хука
|
||||
export const usePetDashboard = () => {
|
||||
const available = useGetPetList({ status: StatusEnum.Available })
|
||||
const sold = useGetPetList({ status: StatusEnum.Sold })
|
||||
|
||||
return { available, sold }
|
||||
}
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).
|
||||
88
canons/style-guide/applied/rest-client/setup/index.md
Normal file
88
canons/style-guide/applied/rest-client/setup/index.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Настройка REST-клиента
|
||||
description: Подготовка REST-клиента сервиса к использованию.
|
||||
keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
|
||||
---
|
||||
|
||||
# Настройка REST-клиента
|
||||
|
||||
Подготовка REST-клиента сервиса к использованию.
|
||||
|
||||
## Что настраиваем
|
||||
|
||||
REST-клиент — это infra-модуль, через который проект работает с внешним 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-хуки и бизнес-логика.
|
||||
|
||||
`baseUrl` API задаётся обязательной env-переменной без fallback-значения в коде. Не используйте записи вроде `process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL ?? 'http://localhost:8080/api/v3'` или `?? ''`: локальный URL должен лежать в `.env.local`, а отсутствие переменной должно приводить к явной ошибке конфигурации.
|
||||
|
||||
## Методы
|
||||
|
||||
Методы описывают конкретные запросы к API.
|
||||
|
||||
Они появляются одним из двух способов:
|
||||
|
||||
- генерируются из OpenAPI в `generated/`;
|
||||
- создаются вручную в `methods/`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
|
||||
- [Ручное создание](/docs/applied/rest-client/setup/manual)
|
||||
|
||||
## GET-хуки
|
||||
|
||||
Для GET-запросов добавляются GET-хуки REST-клиента.
|
||||
|
||||
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
|
||||
|
||||
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
|
||||
|
||||
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`.
|
||||
|
||||
Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```text
|
||||
src/infra/{service-name}/
|
||||
├── client.ts # самописная оболочка и инстанс клиента
|
||||
├── generated/ или methods/ # методы API
|
||||
├── hooks/ # GET-хуки REST-клиента
|
||||
├── types/ # DTO, именованные response-типы и расширения типов
|
||||
├── errors/ # ошибки API, если нужны
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
|
||||
|
||||
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
|
||||
|
||||
## Что делаем дальше
|
||||
|
||||
1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) или [Ручное создание](/docs/applied/rest-client/setup/manual).
|
||||
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
|
||||
3. Проверьте прямые вызовы клиента: [Использование REST-клиента](/docs/applied/rest-client/usage).
|
||||
4. После настройки клиента переходите к [Получению данных](/docs/applied/data-fetch/).
|
||||
198
canons/style-guide/applied/rest-client/setup/manual.md
Normal file
198
canons/style-guide/applied/rest-client/setup/manual.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Ручное создание REST-клиента
|
||||
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
|
||||
---
|
||||
|
||||
# Ручное создание REST-клиента
|
||||
|
||||
Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||
|
||||
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
|
||||
|
||||
## Что нужно создать
|
||||
|
||||
```text
|
||||
src/infra/
|
||||
└── 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/infra/pet-project-api/types/post.ts
|
||||
export type PostDto = {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type PostListQueryDto = {
|
||||
limit?: number
|
||||
category?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/types/index.ts
|
||||
export type { PostDto, PostListQueryDto } from './post'
|
||||
```
|
||||
|
||||
Типы, которые нужны только базовому транспорту, можно держать отдельно:
|
||||
|
||||
```ts
|
||||
// src/infra/pet-project-api/types/client.ts
|
||||
export type QueryParams = Record<string, string | number | boolean>
|
||||
```
|
||||
|
||||
## Ошибка API
|
||||
|
||||
Ошибка API тоже относится к REST-модулю.
|
||||
|
||||
```ts
|
||||
// src/infra/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/infra/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/infra/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/infra/pet-project-api/index.ts
|
||||
import { PetProjectApiClient } from './client'
|
||||
import { postsMethods } from './methods/posts'
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PET_PROJECT_API_BASE_URL
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('NEXT_PUBLIC_PET_PROJECT_API_BASE_URL is required')
|
||||
}
|
||||
|
||||
const client = new PetProjectApiClient(
|
||||
baseUrl,
|
||||
{ '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'
|
||||
```
|
||||
|
||||
Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля.
|
||||
|
||||
## Правила
|
||||
|
||||
- `fetch` используется только внутри базового клиента.
|
||||
- DTO запросов и ответов живут в `types/`.
|
||||
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
|
||||
- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде.
|
||||
- Методы лежат в `methods/` и возвращают DTO.
|
||||
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
|
||||
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||
|
||||
Следующий шаг: [Использование REST-клиента](/docs/applied/rest-client/usage), [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) или [Получение данных](/docs/applied/data-fetch/).
|
||||
21
canons/style-guide/applied/rest-client/usage.md
Normal file
21
canons/style-guide/applied/rest-client/usage.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Использование REST-клиента
|
||||
description: Как вызвать готовый REST-клиент в функции.
|
||||
keywords: [rest, api client, submit, generated, pet-store-api]
|
||||
---
|
||||
|
||||
# Использование REST-клиента
|
||||
|
||||
Как вызвать готовый REST-клиент в функции.
|
||||
|
||||
## Пример
|
||||
|
||||
```ts
|
||||
import { petStoreApi } from 'infra/pet-store-api'
|
||||
|
||||
export const getPet = async (petId: number) => {
|
||||
const pet = await petStoreApi.pet.getPetById({ petId })
|
||||
|
||||
console.log(pet)
|
||||
}
|
||||
```
|
||||
0
canons/style-guide/applied/stores.md
Normal file
0
canons/style-guide/applied/stores.md
Normal file
176
canons/style-guide/applied/styles/styles-setup.md
Normal file
176
canons/style-guide/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) — стили иконок отдельно от глобальных.
|
||||
271
canons/style-guide/applied/styles/styles-usage.md
Normal file
271
canons/style-guide/applied/styles/styles-usage.md
Normal file
@@ -0,0 +1,271 @@
|
||||
---
|
||||
title: Использование стилей
|
||||
description: Как пишутся стили в проекте.
|
||||
---
|
||||
|
||||
# Использование стилей
|
||||
|
||||
Как пишутся стили в проекте.
|
||||
|
||||
## Общие правила
|
||||
|
||||
- Только **PostCSS** и **CSS Modules** для кастомной стилизации.
|
||||
- Подход **Mobile First** — стили пишутся от мобильных к десктопу.
|
||||
- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`).
|
||||
- Корневой класс каждого CSS Module компонента всегда называется `.root` — это упрощает ориентацию в DevTools и отладку DOM.
|
||||
- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`.
|
||||
|
||||
**Хорошо**
|
||||
```css
|
||||
.submitButton {
|
||||
padding: 8px 16px;
|
||||
|
||||
&._disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```css
|
||||
/* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */
|
||||
.submit-button {
|
||||
padding: 8px 16px;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Вложенность
|
||||
|
||||
- Вложенность селекторов запрещена.
|
||||
- Исключения:
|
||||
- Псевдоклассы: `&:hover`, `&:active`, `&:focus`, `&:disabled` и т.д.
|
||||
- Псевдоэлементы: `&::before`, `&::after`.
|
||||
- Медиа-запросы: `@media`.
|
||||
- Модификаторы: `&._active`, `&._disabled`.
|
||||
- Каждый вложенный блок отделяется пустой строкой от предыдущих свойств.
|
||||
|
||||
**Хорошо**
|
||||
```css
|
||||
.card {
|
||||
padding: 16px;
|
||||
background-color: var(--color-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
|
||||
&._highlighted {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (--md) {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 16px;
|
||||
|
||||
@media (--md) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```css
|
||||
/* Плохо: вложенность селекторов, нет пустых строк между блоками. */
|
||||
.card {
|
||||
padding: 16px;
|
||||
.cardTitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Медиа-запросы
|
||||
|
||||
- Только **Custom Media Queries**: `@media (--md) {}`.
|
||||
- Запрещены произвольные breakpoints: `@media (min-width: 768px)`.
|
||||
- `@media` пишется только **внутри** селектора.
|
||||
- Запрещено писать `@media` на верхнем уровне с селекторами внутри.
|
||||
|
||||
**Хорошо**
|
||||
```css
|
||||
.sidebar {
|
||||
display: none;
|
||||
|
||||
@media (--md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarTitle {
|
||||
font-size: 14px;
|
||||
|
||||
@media (--md) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```css
|
||||
/* Плохо: @media на верхнем уровне с селекторами внутри. */
|
||||
@media (--md) {
|
||||
.sidebar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebarTitle {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Плохо: произвольный breakpoint вместо custom media. */
|
||||
.sidebar {
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-переменные
|
||||
|
||||
- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `src/shared/styles/variables.css` через `:root`.
|
||||
- Файл переменных подключается через `src/shared/styles/global.css`, который импортируется один раз в `src/app/layout.tsx`.
|
||||
- Не дублировать магические значения в компонентах.
|
||||
|
||||
**Хорошо**
|
||||
```css
|
||||
/* src/shared/styles/variables.css */
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-hover: #f5f5f5;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--radius-1: 4px;
|
||||
--radius-2: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* компонент */
|
||||
.card {
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-2);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```css
|
||||
/* Плохо: магические значения вместо переменных. */
|
||||
.card {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Media
|
||||
|
||||
- Breakpoints определяются через Custom Media Queries в `src/shared/styles/media.css`.
|
||||
- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей.
|
||||
|
||||
```css
|
||||
/* src/shared/styles/media.css */
|
||||
@custom-media --sm (min-width: 36em);
|
||||
@custom-media --md (min-width: 62em);
|
||||
@custom-media --lg (min-width: 82em);
|
||||
```
|
||||
|
||||
## Импорт стилей
|
||||
|
||||
- Стили компонента импортируются только внутри своего компонента.
|
||||
- Запрещено импортировать стили одного компонента в другой.
|
||||
- Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS.
|
||||
|
||||
## Форматирование
|
||||
|
||||
- Пустая строка между селекторами верхнего уровня.
|
||||
- Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор).
|
||||
|
||||
**Хорошо**
|
||||
```css
|
||||
.userBar {
|
||||
display: none;
|
||||
color: var(--color-text);
|
||||
|
||||
@media (--md) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.userBarButton {
|
||||
background-color: var(--color-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
&._active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```css
|
||||
/* Плохо: нет пустых строк между селекторами и вложенными блоками. */
|
||||
.userBar {
|
||||
display: none;
|
||||
color: var(--color-text);
|
||||
@media (--md) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.userBarButton {
|
||||
background-color: var(--color-bg);
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
&._active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Единицы измерения
|
||||
|
||||
- `px` — основная единица измерения.
|
||||
- Остальные (`em`, `rem`, `%`, `vh`/`vw`) — допускаются по необходимости дизайна.
|
||||
|
||||
## Порядок CSS-свойств
|
||||
|
||||
В стилях рекомендуется придерживаться логического порядка свойств:
|
||||
|
||||
1. Позиционирование (`position`, `top`, `left`, `z-index`).
|
||||
2. Блочная модель (`display`, `width`, `height`, `margin`, `padding`).
|
||||
3. Оформление (`background`, `border`, `box-shadow`, `border-radius`).
|
||||
4. Текст (`font`, `color`, `text-align`, `line-height`).
|
||||
5. Прочее (`transition`, `animation`, `opacity`, `cursor`).
|
||||
|
||||
## Комментарии
|
||||
|
||||
- Желательно не писать комментарии в CSS.
|
||||
- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение.
|
||||
31
canons/style-guide/applied/svg-sprites/svg-sprites-intro.md
Normal file
31
canons/style-guide/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/>`, управление цветом.
|
||||
132
canons/style-guide/applied/svg-sprites/svg-sprites-setup.md
Normal file
132
canons/style-guide/applied/svg-sprites/svg-sprites-setup.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Настройка SVG-спрайтов
|
||||
description: Подключение SVG-спрайтов в новом проекте.
|
||||
keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts]
|
||||
---
|
||||
|
||||
# Настройка SVG-спрайтов
|
||||
Подключение SVG-спрайтов в новом проекте.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Установить пакет:
|
||||
|
||||
```bash
|
||||
npm install @gromlab/svg-sprites
|
||||
```
|
||||
|
||||
2. Создать `svg-sprites.config.ts` в корне проекта (см. [Стандартный конфиг](#стандартныи-конфиг)).
|
||||
|
||||
3. Создать папку входа для SVG-файлов в слое `shared`:
|
||||
|
||||
```bash
|
||||
mkdir -p src/shared/sprites/icons
|
||||
```
|
||||
|
||||
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
|
||||
|
||||
4. Добавить скрипты в `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"sprite": "svg-sprites",
|
||||
"predev": "svg-sprites",
|
||||
"prebuild": "svg-sprites"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Хуки `predev` и `prebuild` гарантируют, что спрайты и типы всегда актуальны перед запуском и сборкой.
|
||||
|
||||
5. Добавить сгенерированные артефакты в `.gitignore`:
|
||||
|
||||
```text
|
||||
# Сгенерированные спрайты и React-компонент
|
||||
/public/sprites/
|
||||
/src/ui/svg-sprite/
|
||||
```
|
||||
|
||||
6. Выполнить первую генерацию:
|
||||
|
||||
```bash
|
||||
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` в корне проекта. Это канон — отклонения только по явной причине.
|
||||
|
||||
```ts
|
||||
// svg-sprites.config.ts
|
||||
import { defineConfig } from '@gromlab/svg-sprites'
|
||||
|
||||
export default defineConfig({
|
||||
output: 'public/sprites',
|
||||
publicPath: '/sprites',
|
||||
react: 'src/ui/svg-sprite',
|
||||
sprites: [
|
||||
{ name: 'icons', input: 'src/shared/sprites/icons' },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Фиксированные значения
|
||||
|
||||
| Опция | Значение | Почему так |
|
||||
|-------|----------|------------|
|
||||
| `output` | `public/sprites` | Единая папка статики Next.js |
|
||||
| `publicPath` | `/sprites` | URL-путь без `public/` (Next.js раздаёт `public/` как `/`) |
|
||||
| `react` | `src/ui/svg-sprite` | Слой `ui/` из архитектуры проекта (→ [Архитектура](/docs/basics/architecture/)) |
|
||||
| `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` |
|
||||
|
||||
### Трансформации
|
||||
|
||||
Все значения по умолчанию оставлять включёнными:
|
||||
|
||||
```ts
|
||||
transform: {
|
||||
removeSize: true,
|
||||
replaceColors: true,
|
||||
addTransition: true,
|
||||
}
|
||||
```
|
||||
|
||||
Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию.
|
||||
|
||||
Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально.
|
||||
|
||||
### Режим
|
||||
|
||||
По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`.
|
||||
|
||||
## Дальше
|
||||
|
||||
- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент `<SvgSprite/>`, управление цветом.
|
||||
56
canons/style-guide/applied/svg-sprites/svg-sprites-usage.md
Normal file
56
canons/style-guide/applied/svg-sprites/svg-sprites-usage.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Использование SVG-спрайтов
|
||||
description: Как добавлять и использовать SVG-иконки в коде.
|
||||
keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color]
|
||||
---
|
||||
|
||||
# Использование SVG-спрайтов
|
||||
|
||||
Как добавлять и использовать SVG-иконки в коде.
|
||||
|
||||
## Шаги
|
||||
|
||||
1. **Положить SVG в папку спрайта:**
|
||||
|
||||
```text
|
||||
src/shared/sprites/icons/new-icon.svg
|
||||
```
|
||||
|
||||
2. **Импортировать компонент.** Компонент `<SvgSprite/>` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний:
|
||||
|
||||
```tsx
|
||||
import { SvgSprite } from 'ui/svg-sprite'
|
||||
|
||||
<SvgSprite icon="new-icon" />
|
||||
```
|
||||
|
||||
3. **Посмотреть и пощупать иконку — в превью.** Пакет генерирует HTML-превью рядом со спрайтом (`public/sprites/icons.preview.html`). Там виден набор иконок, имена и поведение цвета.
|
||||
|
||||
## Управление цветом
|
||||
|
||||
При сборке цвета в SVG заменяются на CSS-переменные `--icon-color-N`. Управление — через обычный CSS родителя.
|
||||
|
||||
**Моно-иконка** наследует `color` родителя (`--icon-color-1` по умолчанию `currentColor`):
|
||||
|
||||
```css
|
||||
.button {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**Точечное переопределение** — через переменную:
|
||||
|
||||
```css
|
||||
.icon-danger {
|
||||
--icon-color-1: var(--color-danger);
|
||||
}
|
||||
```
|
||||
|
||||
**Мульти-иконка** — переменные задаются явно, порядок виден в превью:
|
||||
|
||||
```css
|
||||
.folder {
|
||||
--icon-color-1: var(--color-folder-bg);
|
||||
--icon-color-2: var(--color-folder-accent);
|
||||
}
|
||||
```
|
||||
97
canons/style-guide/applied/templates/templates-create.md
Normal file
97
canons/style-guide/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
canons/style-guide/applied/templates/templates-intro.md
Normal file
32
canons/style-guide/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
canons/style-guide/applied/templates/templates-setup.md
Normal file
44
canons/style-guide/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
canons/style-guide/applied/templates/templates-usage.md
Normal file
45
canons/style-guide/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` отдельно не добавляется.
|
||||
88
canons/style-guide/applied/vscode.md
Normal file
88
canons/style-guide/applied/vscode.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: VS Code
|
||||
description: Единые настройки редактора и расширений для команды.
|
||||
---
|
||||
|
||||
# VS Code
|
||||
|
||||
Единые настройки редактора и расширений для команды.
|
||||
|
||||
## Структура `.vscode/`
|
||||
|
||||
```text
|
||||
.vscode/
|
||||
├── extensions.json # Рекомендуемые расширения
|
||||
└── settings.json # Настройки редактора для проекта
|
||||
```
|
||||
|
||||
Оба файла коммитятся в репозиторий.
|
||||
|
||||
## Расширения
|
||||
|
||||
Файл `.vscode/extensions.json` определяет список расширений, которые VS Code предложит установить при открытии проекта.
|
||||
|
||||
```json
|
||||
// .vscode/extensions.json
|
||||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome",
|
||||
"MyTemplateGenerator.mytemplategenerator",
|
||||
"csstools.postcss"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Расширение | Назначение |
|
||||
|---|---|
|
||||
| [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) | Линтинг и форматирование кода. Заменяет ESLint и Prettier |
|
||||
| Template File Generator \| gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) | Генерация файлов и папок из шаблонов `.templates/` через контекстное меню |
|
||||
| [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss) | Подсветка синтаксиса и автодополнение для PostCSS (`@custom-media`, `@nest` и др.) |
|
||||
|
||||
### Зачем это нужно
|
||||
|
||||
- Новый участник команды получает все нужные расширения одним кликом.
|
||||
- Нет разночтений: все используют одинаковый форматтер и линтер.
|
||||
- Расширения привязаны к проекту, а не к конкретному разработчику.
|
||||
|
||||
## Настройки редактора
|
||||
|
||||
Файл `.vscode/settings.json` переопределяет пользовательские настройки VS Code на уровне проекта.
|
||||
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Разбор настроек
|
||||
|
||||
| Настройка | Значение | Что делает |
|
||||
|---|---|---|
|
||||
| `editor.defaultFormatter` | `biomejs.biome` | Biome используется как единственный форматтер для всех файлов |
|
||||
| `editor.formatOnSave` | `true` | Код автоматически форматируется при каждом сохранении |
|
||||
| `codeActionsOnSave.source.fixAll.biome` | `explicit` | Biome автоматически применяет безопасные исправления при сохранении |
|
||||
| `codeActionsOnSave.source.organizeImports.biome` | `explicit` | Импорты сортируются и группируются автоматически при сохранении |
|
||||
| `files.associations` | `"*.css": "postcss"` | Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS |
|
||||
|
||||
### Зачем это нужно
|
||||
|
||||
- **Единый стиль кода** -- форматирование происходит автоматически, невозможно закоммитить неформатированный код.
|
||||
- **Автофикс при сохранении** -- распространённые ошибки линтинга исправляются без ручного вмешательства.
|
||||
- **Сортировка импортов** -- импорты всегда в одном порядке, без конфликтов при мерже.
|
||||
- **PostCSS-подсветка** -- кастомные at-правила (`@custom-media`, `@define-mixin`) подсвечиваются корректно, а не как ошибки.
|
||||
|
||||
## Что не должно быть в `.vscode/`
|
||||
|
||||
Не коммитятся файлы, специфичные для конкретного разработчика:
|
||||
|
||||
- **Не коммитить**: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления.
|
||||
- **Коммитить**: только `extensions.json` и `settings.json` с общими для команды настройками.
|
||||
Reference in New Issue
Block a user