docs: добавить раздел «Данные» и реорганизовать документацию
- Добавлен раздел «Данные»: REST (автоматическая и ручная генерация клиентов, получение данных в server и client компонентах с инкапсуляцией SWR в хуках), Realtime, введение - Прикладные разделы переименованы в «Использование», папка перенесена в `docs/docs/usage/` - Создана группа «Установка и настройка» с папкой `docs/docs/setup/` — туда вынесены PostCSS, Biome, VS Code, алиасы и установка SVG-спрайтов - Подгруппы «Стили» и «SVG-спрайты» в сайдбаре упразднены — страницы установки и использования разнесены по верхнеуровневым группам - Удалён устаревший раздел `applied/api.md` - Перекрёстные ссылки в workflow-разделах и внутри новых страниц синхронизированы с новыми путями - CONTRIBUTING.md обновлён под новую структуру папок
This commit is contained in:
@@ -30,40 +30,63 @@ const sidebar = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Прикладные разделы',
|
text: 'Установка и настройка',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Структура проекта', link: '/docs/applied/project-structure' },
|
{ text: 'Алиасы', link: '/docs/setup/aliases' },
|
||||||
{ text: 'Алиасы', link: '/docs/applied/aliases' },
|
{ text: 'Biome', link: '/docs/setup/biome' },
|
||||||
{ text: 'Компоненты', link: '/docs/applied/components' },
|
{ text: 'PostCSS', link: '/docs/setup/postcss' },
|
||||||
{ text: 'Страницы (App Router)', link: '/docs/applied/page-level' },
|
{ text: 'SVG-спрайты', link: '/docs/setup/svg-sprites' },
|
||||||
{ text: 'Шаблоны и генерация кода', link: '/docs/applied/templates-generation' },
|
{ text: 'VS Code', link: '/docs/setup/vscode' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Стили',
|
text: 'Использование',
|
||||||
|
items: [
|
||||||
|
{ text: 'Структура проекта', link: '/docs/usage/project-structure' },
|
||||||
|
{ text: 'Компоненты', link: '/docs/usage/components' },
|
||||||
|
{ text: 'Страницы (App Router)', link: '/docs/usage/page-level' },
|
||||||
|
{ text: 'Шаблоны и генерация кода', link: '/docs/usage/templates-generation' },
|
||||||
|
{ text: 'Стили', link: '/docs/usage/styles' },
|
||||||
|
{ text: 'Изображения', link: '/docs/usage/images-sprites' },
|
||||||
|
{ text: 'SVG-спрайты', link: '/docs/usage/svg-sprites' },
|
||||||
|
{ text: 'Видео', link: '/docs/usage/video' },
|
||||||
|
{
|
||||||
|
text: 'Данные',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'PostCSS', link: '/docs/applied/styles/postcss' },
|
{ text: 'Введение', link: '/docs/usage/data/' },
|
||||||
{ text: 'Использование', link: '/docs/applied/styles/usage' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ text: 'Изображения', link: '/docs/applied/images-sprites' },
|
|
||||||
{
|
{
|
||||||
text: 'SVG-спрайты',
|
text: 'REST',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Установка и настройка', link: '/docs/applied/svg-sprites/setup' },
|
{
|
||||||
{ text: 'Использование', link: '/docs/applied/svg-sprites/usage' },
|
text: 'Клиенты',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Автоматическая генерация', link: '/docs/usage/data/rest/clients/auto' },
|
||||||
|
{ text: 'Ручная генерация', link: '/docs/usage/data/rest/clients/manual' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ text: 'Видео', link: '/docs/applied/video' },
|
{
|
||||||
{ text: 'API', link: '/docs/applied/api' },
|
text: 'Получение данных',
|
||||||
{ text: 'Stores', link: '/docs/applied/stores' },
|
collapsed: true,
|
||||||
{ text: 'Хуки', link: '/docs/applied/hooks' },
|
items: [
|
||||||
{ text: 'Шрифты', link: '/docs/applied/fonts' },
|
{ text: 'Серверные компоненты', link: '/docs/usage/data/rest/fetching/server' },
|
||||||
{ text: 'Локализация', link: '/docs/applied/localization' },
|
{ text: 'Клиентские компоненты', link: '/docs/usage/data/rest/fetching/client' },
|
||||||
{ text: 'Biome', link: '/docs/applied/biome' },
|
|
||||||
{ text: 'Настройка VS Code', link: '/docs/applied/vscode' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ text: 'Realtime', link: '/docs/usage/data/realtime' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ text: 'Stores', link: '/docs/usage/stores' },
|
||||||
|
{ text: 'Хуки', link: '/docs/usage/hooks' },
|
||||||
|
{ text: 'Шрифты', link: '/docs/usage/fonts' },
|
||||||
|
{ text: 'Локализация', link: '/docs/usage/localization' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,8 +34,13 @@ docs/
|
|||||||
│ ├── naming.md
|
│ ├── naming.md
|
||||||
│ ├── documentation.md
|
│ ├── documentation.md
|
||||||
│ └── typing.md
|
│ └── typing.md
|
||||||
└── applied/ # Прикладные разделы
|
├── setup/ # Установка: разовая настройка проекта
|
||||||
├── vscode.md
|
│ ├── aliases.md
|
||||||
|
│ ├── biome.md
|
||||||
|
│ ├── postcss.md
|
||||||
|
│ ├── svg-sprites.md
|
||||||
|
│ └── vscode.md
|
||||||
|
└── usage/ # Использование: повседневная работа
|
||||||
├── project-structure.md
|
├── project-structure.md
|
||||||
├── components.md
|
├── components.md
|
||||||
├── page-level.md
|
├── page-level.md
|
||||||
@@ -44,7 +49,7 @@ docs/
|
|||||||
├── images-sprites.md
|
├── images-sprites.md
|
||||||
├── svg-sprites.md
|
├── svg-sprites.md
|
||||||
├── video.md
|
├── video.md
|
||||||
├── api.md
|
├── data/
|
||||||
├── stores.md
|
├── stores.md
|
||||||
├── hooks.md
|
├── hooks.md
|
||||||
├── fonts.md
|
├── fonts.md
|
||||||
@@ -59,7 +64,7 @@ generate-llms.ts # Скрипт генерации llms.txt и R
|
|||||||
|
|
||||||
### Добавление нового раздела
|
### Добавление нового раздела
|
||||||
|
|
||||||
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/` или `docs/docs/applied/`).
|
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/`, `docs/docs/setup/` или `docs/docs/usage/`).
|
||||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||||
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
||||||
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
|
|||||||
|
|
||||||
## Интеграция с VS Code
|
## Интеграция с VS Code
|
||||||
|
|
||||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/applied/vscode).
|
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/setup/vscode).
|
||||||
@@ -7,7 +7,7 @@ keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, a
|
|||||||
|
|
||||||
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
||||||
|
|
||||||
Правила написания CSS в компонентах — [Использование](/docs/applied/styles/usage).
|
Правила написания CSS в компонентах — [Использование](/docs/usage/styles).
|
||||||
|
|
||||||
## Зачем PostCSS
|
## Зачем PostCSS
|
||||||
|
|
||||||
@@ -68,4 +68,4 @@ export default {
|
|||||||
|
|
||||||
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
||||||
|
|
||||||
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/applied/styles/usage), раздел «Импорт стилей»).
|
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/usage/styles), раздел «Импорт стилей»).
|
||||||
@@ -7,7 +7,7 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
|||||||
|
|
||||||
Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета.
|
Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета.
|
||||||
|
|
||||||
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/applied/svg-sprites/usage).
|
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/usage/svg-sprites).
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
|||||||
mkdir -p src/shared/sprites/icons
|
mkdir -p src/shared/sprites/icons
|
||||||
```
|
```
|
||||||
|
|
||||||
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
|
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
|
||||||
|
|
||||||
4. Добавить скрипты в `package.json`:
|
4. Добавить скрипты в `package.json`:
|
||||||
|
|
||||||
50
docs/docs/usage/data/index.md
Normal file
50
docs/docs/usage/data/index.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Введение
|
||||||
|
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Введение
|
||||||
|
|
||||||
|
Работа с источниками данных в проекте: REST, realtime и любые другие каналы, которые появятся в будущем. Раздел описывает, как создаются клиенты для API и как полученные данные доходят до страниц и компонентов.
|
||||||
|
|
||||||
|
## Принципы раздела
|
||||||
|
|
||||||
|
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`.
|
||||||
|
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
|
||||||
|
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
|
||||||
|
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые хуки модуля API (`useUserList`, `usePostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
|
||||||
|
|
||||||
|
## Карта раздела
|
||||||
|
|
||||||
|
### REST
|
||||||
|
|
||||||
|
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
|
||||||
|
|
||||||
|
- **Клиенты** — как создаётся клиент REST API:
|
||||||
|
- [Автоматическая генерация](/docs/usage/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
|
||||||
|
- [Ручная генерация](/docs/usage/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками.
|
||||||
|
- **Получение данных** — как клиент используется в приложении:
|
||||||
|
- [Серверные компоненты](/docs/usage/data/rest/fetching/server) — прямой `await` метода клиента в Server Components.
|
||||||
|
- [Клиентские компоненты](/docs/usage/data/rest/fetching/client) — через готовые хуки модуля API; SWR с кешем, дедупликацией и ревалидацией скрыт внутри хука.
|
||||||
|
|
||||||
|
### Realtime
|
||||||
|
|
||||||
|
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||||
|
|
||||||
|
- [Realtime](/docs/usage/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки.
|
||||||
|
|
||||||
|
## Что даёт раздел
|
||||||
|
|
||||||
|
После прочтения раздела понятно:
|
||||||
|
|
||||||
|
- Где живёт код работы с API и почему именно там.
|
||||||
|
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
|
||||||
|
- Как получать данные на сервере и на клиенте, чтобы не ломать кеш и не плодить лишние запросы.
|
||||||
|
- Как подключать realtime-источники в общую модель работы с данными.
|
||||||
|
- Какие правила обязательны и какие отклонения допустимы.
|
||||||
|
|
||||||
|
## Что не входит в раздел
|
||||||
|
|
||||||
|
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/usage/stores).
|
||||||
|
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/).
|
||||||
|
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Это [Хуки](/docs/usage/hooks).
|
||||||
80
docs/docs/usage/data/realtime.md
Normal file
80
docs/docs/usage/data/realtime.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Realtime
|
||||||
|
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Realtime
|
||||||
|
|
||||||
|
Канал для push-данных: WebSocket, SSE, событийные шины и любой другой источник, инициирующий передачу со стороны сервера. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||||
|
|
||||||
|
Получение REST-данных — [REST](/docs/usage/data/rest/clients/auto).
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
|
||||||
|
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
|
||||||
|
- **Использование на клиенте — два сценария:**
|
||||||
|
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
|
||||||
|
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
|
||||||
|
|
||||||
|
## Размещение клиента
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── {channel-name}/
|
||||||
|
├── connection.ts # установление соединения, реконнект
|
||||||
|
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
|
||||||
|
├── types.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование через SWR
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import useSWRSubscription from 'swr/subscription'
|
||||||
|
import { subscribe } from 'infrastructure/notifications'
|
||||||
|
|
||||||
|
export function NotificationCounter() {
|
||||||
|
const { data: count } = useSWRSubscription(
|
||||||
|
['notifications', 'count'],
|
||||||
|
(key, { next }) =>
|
||||||
|
subscribe('notifications.count', (value: number) => next(null, value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return <span>{count ?? 0}</span>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
|
||||||
|
|
||||||
|
## Прямая подписка
|
||||||
|
|
||||||
|
Для побочных эффектов, которые не влияют на состояние UI напрямую:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { subscribe } from 'infrastructure/notifications'
|
||||||
|
import { showToast } from 'ui/toast'
|
||||||
|
|
||||||
|
export function NotificationsToaster() {
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribe('notifications.new', (notification) => {
|
||||||
|
showToast(notification.message)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
|
||||||
|
|
||||||
|
## Запрет прямых соединений
|
||||||
|
|
||||||
|
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`.
|
||||||
|
|
||||||
|
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.
|
||||||
280
docs/docs/usage/data/rest/clients/auto.md
Normal file
280
docs/docs/usage/data/rest/clients/auto.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
---
|
||||||
|
title: Автоматическая генерация
|
||||||
|
keywords: [api, rest, openapi, codegen, генерация, клиент, api-codegen, gromlab, infrastructure, swagger-typescript-api]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Автоматическая генерация
|
||||||
|
|
||||||
|
Если у API есть OpenAPI-спецификация — клиент генерируется утилитой [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen) (обёртка над `swagger-typescript-api`). Ручной код для таких API не пишется.
|
||||||
|
|
||||||
|
Когда схемы нет — [Ручная генерация](/docs/usage/data/rest/clients/manual).
|
||||||
|
|
||||||
|
В примерах ниже используется условный API `pet-project-api` (kebab-case в путях) / `petProjectApi` (camelCase в коде). В реальном проекте имена выбираются по конкретному API.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D @gromlab/api-codegen
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт генерации в `package.json` — по одному на каждый API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"codegen:pet-project-api": "api-codegen --config src/infrastructure/pet-project-api/config/pet-project-api.config.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфиг и опции — в репозитории [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen).
|
||||||
|
|
||||||
|
## Структура модуля
|
||||||
|
|
||||||
|
Клиент кладётся в слой `infrastructure/` отдельным модулем по имени API (kebab-case):
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── pet-project-api/
|
||||||
|
├── generated/ # сегмент сгенерированного кода
|
||||||
|
│ └── pet-project-api.generated.ts # сгенерировано — не править
|
||||||
|
├── types/ # расширения сгенерированных типов
|
||||||
|
│ ├── user.ts # declare module + Extended-тип
|
||||||
|
│ └── index.ts # реэкспорт расширений
|
||||||
|
├── hooks/ # SWR-хуки для клиентских компонентов
|
||||||
|
│ ├── use-user-list.hook.ts
|
||||||
|
│ ├── use-user-detail.hook.ts
|
||||||
|
│ └── index.ts # реэкспорт хуков
|
||||||
|
├── config/ # конфиги модуля
|
||||||
|
│ └── pet-project-api.config.ts # конфиг генерации клиента
|
||||||
|
├── client.ts # настройка HttpClient, инстанс Api
|
||||||
|
└── index.ts # публичный API модуля
|
||||||
|
```
|
||||||
|
|
||||||
|
| Файл | Роль | Кто правит |
|
||||||
|
|------|------|-----------|
|
||||||
|
| `generated/{service-name}.generated.ts` | Сгенерированный код: типы, `class Api`, `class HttpClient` | codegen, не править |
|
||||||
|
| `types/{сущность}.ts` | `declare module` + `Extended`-типы по сущности | разработчик |
|
||||||
|
| `types/index.ts` | Реэкспорт публичных расширений | разработчик |
|
||||||
|
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента | разработчик |
|
||||||
|
| `hooks/index.ts` | Реэкспорт хуков | разработчик |
|
||||||
|
| `config/{service-name}.config.ts` | Параметры генерации для конкретного API | разработчик |
|
||||||
|
| `client.ts` | `baseUrl` из env, конфиг `HttpClient`, инстанс `new Api(...)` | разработчик |
|
||||||
|
| `index.ts` | Публичный API: инстанс сервиса, расширенные типы, хуки | разработчик |
|
||||||
|
|
||||||
|
`client.ts` и `index.ts` — единственные корневые файлы модуля. Все остальные файлы живут в сегментах (`generated/`, `types/`, `hooks/`, `config/`).
|
||||||
|
|
||||||
|
Имя сгенерированного файла — `{service-name}.generated.ts` (имя сервиса в kebab-case + суффикс `.generated.ts`). Суффикс сигнализирует «не править руками».
|
||||||
|
|
||||||
|
## `client.ts`
|
||||||
|
|
||||||
|
Тонкий ручной слой поверх сгенерированного кода. Делает три вещи: читает и нормализует `baseUrl`, конфигурирует `HttpClient`, создаёт **именованный инстанс** сервиса.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/client.ts
|
||||||
|
import { Api, HttpClient } from './generated/pet-project-api.generated'
|
||||||
|
|
||||||
|
const resolvedBaseUrl = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
.replace(/\/+$/, '') // убираем хвостовой слэш
|
||||||
|
.replace(/\/v1$/, '') // версия уже в путях методов — режем дубль
|
||||||
|
|
||||||
|
const httpClient = new HttpClient({
|
||||||
|
baseApiParams: {
|
||||||
|
secure: false,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// кастомные заголовки API — если требуются
|
||||||
|
// 'X-App-Key': '...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
httpClient.baseUrl = resolvedBaseUrl
|
||||||
|
|
||||||
|
export const petProjectApi = new Api(httpClient)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Имя инстанса = имя сервиса
|
||||||
|
|
||||||
|
Инстанс называется по имени API в camelCase, не унифицированно `api`/`client`. Это даёт **процедурное обращение** и однозначность при работе с несколькими сервисами:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
const user = await petProjectApi.user.getUser(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
При нескольких API — каждый со своим именем:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
import { paymentsApi } from 'infrastructure/payments-api'
|
||||||
|
|
||||||
|
const user = await petProjectApi.user.list()
|
||||||
|
const invoice = await paymentsApi.invoices.list()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Нормализация `baseUrl`
|
||||||
|
|
||||||
|
`@gromlab/api-codegen` может включать версию (`/v1`) в `baseUrl` сгенерированного кода, а пути методов уже содержат её — отсюда дубль. Стандартный приём:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.replace(/\/+$/, '') // хвостовой слэш
|
||||||
|
.replace(/\/v1$/, '') // версия (если фигурирует в путях)
|
||||||
|
```
|
||||||
|
|
||||||
|
Подгоняется под конкретный API: если версия в путях не повторяется — второй `replace` не нужен.
|
||||||
|
|
||||||
|
## Расширения типов
|
||||||
|
|
||||||
|
Автогенерация не покрывает все реальные поля API: иногда тип `object`, иногда поле просто отсутствует. Расширения живут в `types/`, по файлу на сущность.
|
||||||
|
|
||||||
|
Две техники:
|
||||||
|
|
||||||
|
### `declare module` — добавление полей
|
||||||
|
|
||||||
|
Дополняет существующий интерфейс из `generated.ts`. Сама сгенерированная декларация не трогается.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/user.ts
|
||||||
|
import type { User } from '../generated/pet-project-api.generated'
|
||||||
|
|
||||||
|
declare module '../generated/pet-project-api.generated' {
|
||||||
|
interface User {
|
||||||
|
avatar?: {
|
||||||
|
file?: string
|
||||||
|
title?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Extended` через `Omit & {...}` — переопределение полей
|
||||||
|
|
||||||
|
Когда автогенерация даёт `object` или общий тип, а реально структура известна — создаётся отдельный тип `UserExtended` (по имени сущности + суффикс `Extended`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/user.ts
|
||||||
|
export type UserExtended = Omit<User, 'roles' | 'tags' | 'fields'> & {
|
||||||
|
roles?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
|
||||||
|
tags?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
|
||||||
|
fields?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Реэкспорт
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/index.ts
|
||||||
|
export type { UserExtended } from './user'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила
|
||||||
|
|
||||||
|
- Расширения — **только в `types/`**, не в `client.ts` и не в сгенерированном файле.
|
||||||
|
- Один файл на сущность (имя файла — kebab-case по сущности: `user.ts`, `order.ts`, `invoice.ts`).
|
||||||
|
- При регенерации `generated/{service-name}.generated.ts` файлы в `types/` не затрагиваются.
|
||||||
|
- Если сломался `Extended`-тип после regen — синхронизировать руками.
|
||||||
|
|
||||||
|
## Хуки для клиентских компонентов
|
||||||
|
|
||||||
|
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/use-user-list.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petProjectApi } from '../client'
|
||||||
|
import type { User } from '../generated/pet-project-api.generated'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка пользователей.
|
||||||
|
*/
|
||||||
|
export const useUserList = (
|
||||||
|
query?: { limit?: number; offset?: number },
|
||||||
|
config?: SWRConfiguration,
|
||||||
|
) => {
|
||||||
|
return useSWR<User[]>(
|
||||||
|
['pet-project-api', 'user', 'list', query],
|
||||||
|
() => petProjectApi.user.list(query ?? {}),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/use-user-detail.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petProjectApi } from '../client'
|
||||||
|
import type { UserExtended } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение пользователя по идентификатору.
|
||||||
|
*/
|
||||||
|
export const useUserDetail = (
|
||||||
|
id: string | null,
|
||||||
|
config?: SWRConfiguration,
|
||||||
|
) => {
|
||||||
|
const key = id ? ['pet-project-api', 'user', 'detail', id] : null
|
||||||
|
const fetcher = () => petProjectApi.user.getUser(id!) as Promise<UserExtended>
|
||||||
|
|
||||||
|
return useSWR<UserExtended>(key, fetcher, config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/index.ts
|
||||||
|
export { useUserList } from './use-user-list.hook'
|
||||||
|
export { useUserDetail } from './use-user-detail.hook'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила хуков
|
||||||
|
|
||||||
|
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
|
||||||
|
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
|
||||||
|
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
|
||||||
|
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
|
||||||
|
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
|
||||||
|
|
||||||
|
## Публичный API модуля
|
||||||
|
|
||||||
|
Из `index.ts` экспортируются инстанс, расширенные типы и хуки. Сырые типы из `generated/` экспортируются по необходимости — точечно.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/index.ts
|
||||||
|
export { petProjectApi } from './client'
|
||||||
|
export type { UserExtended } from './types'
|
||||||
|
export * from './hooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Регенерация
|
||||||
|
|
||||||
|
При изменении OpenAPI-схемы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run codegen:pet-project-api
|
||||||
|
```
|
||||||
|
|
||||||
|
Что меняется:
|
||||||
|
|
||||||
|
- `generated/{service-name}.generated.ts` — перезаписывается полностью, изменения коммитятся.
|
||||||
|
- `client.ts`, `types/`, `config/`, `index.ts` — **не трогаются** автоматически.
|
||||||
|
|
||||||
|
Поломка контракта (изменение типов в схеме) ловится TypeScript при сборке проекта. Если ломаются `Extended`-типы — синхронизировать вручную в соответствующих файлах `types/`.
|
||||||
|
|
||||||
|
## Сгенерированный файл коммитится
|
||||||
|
|
||||||
|
Файл `generated/{service-name}.generated.ts` **не добавляется в `.gitignore`** — попадает в репозиторий вместе с остальным кодом.
|
||||||
|
|
||||||
|
Причины:
|
||||||
|
|
||||||
|
- **Детерминированная сборка.** `npm run build` не зависит от доступности OpenAPI-схемы (обычно она на удалённом сервере). Сервис лёг — прод собирается.
|
||||||
|
- **Видимость изменений в PR.** Diff показывает, что именно поменялось в контракте API между версиями.
|
||||||
|
- **Простой онбординг.** После `git clone` IDE сразу видит типы, без предварительной генерации.
|
||||||
|
- **Фиксация версии контракта.** Пересборка старого коммита даёт ровно тот клиент, что был тогда.
|
||||||
|
|
||||||
|
Регенерация — **ручная команда** при обновлении схемы, не хук `predev`/`prebuild`. Запускается осознанно.
|
||||||
|
|
||||||
|
Исключение возможно, только если OpenAPI-схема лежит **в этом же репозитории** и генерация быстрая, без сети — тогда допустимо добавить сегмент `generated/` в `.gitignore` и хук `prebuild`, по аналогии со спрайтами. На практике встречается редко.
|
||||||
366
docs/docs/usage/data/rest/clients/manual.md
Normal file
366
docs/docs/usage/data/rest/clients/manual.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
---
|
||||||
|
title: Ручная генерация
|
||||||
|
keywords: [api, rest, клиент, ручной, fetch, infrastructure, api-клиент]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ручная генерация
|
||||||
|
|
||||||
|
Если у API нет OpenAPI-спецификации — клиент пишется и поддерживается вручную. Цель та же, что и у автогенерации: единая точка работы с API, без прямых `fetch` в коде приложения.
|
||||||
|
|
||||||
|
Когда схема есть — [Автоматическая генерация](/docs/usage/data/rest/clients/auto).
|
||||||
|
|
||||||
|
В примерах ниже используется условный API `pet-project-api` / `petProjectApi`. В реальном проекте имена выбираются по конкретному API.
|
||||||
|
|
||||||
|
## Структура модуля
|
||||||
|
|
||||||
|
Клиент живёт в слое `infrastructure/` отдельным модулем по имени API (kebab-case):
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/infrastructure/
|
||||||
|
└── pet-project-api/
|
||||||
|
├── methods/ # методы по сущностям API
|
||||||
|
│ ├── pages.ts
|
||||||
|
│ ├── posts.ts
|
||||||
|
│ └── forms.ts
|
||||||
|
├── hooks/ # SWR-хуки для клиентских компонентов
|
||||||
|
│ ├── use-post-detail.hook.ts
|
||||||
|
│ ├── use-post-filter.hook.ts
|
||||||
|
│ └── index.ts
|
||||||
|
├── types/ # типы клиента и доменные типы
|
||||||
|
│ ├── client.ts # типы клиента: RequestOptions, ParamValue
|
||||||
|
│ ├── post.ts # доменные типы сущности post
|
||||||
|
│ ├── form.ts # доменные типы сущности form
|
||||||
|
│ └── index.ts # реэкспорт публичных типов
|
||||||
|
├── errors/ # доменные ошибки API
|
||||||
|
│ └── pet-project-api.error.ts
|
||||||
|
├── client.ts # класс клиента: baseUrl, headers, get/post
|
||||||
|
└── index.ts # публичный API модуля
|
||||||
|
```
|
||||||
|
|
||||||
|
| Файл | Роль |
|
||||||
|
|------|------|
|
||||||
|
| `client.ts` | Класс `PetProjectApiClient`: `baseUrl`, общие заголовки, `buildUrl`, базовые `get`/`post` |
|
||||||
|
| `methods/{entity}.ts` | Методы по сущности, экспортируются фабрикой `{entity}Methods(client)` |
|
||||||
|
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента |
|
||||||
|
| `hooks/index.ts` | Реэкспорт хуков |
|
||||||
|
| `types/client.ts` | Типы инфраструктуры клиента: `RequestOptions`, `PostOptions`, `ParamValue` |
|
||||||
|
| `types/{entity}.ts` | Доменные типы: запросы, ответы, фильтры по сущности |
|
||||||
|
| `types/index.ts` | Реэкспорт публичных типов |
|
||||||
|
| `errors/{service-name}.error.ts` | Доменный класс ошибок API |
|
||||||
|
| `index.ts` | Публичный API: инстанс клиента, хуки, доменные ошибки, типы |
|
||||||
|
|
||||||
|
`methods/`, `hooks/`, `types/`, `errors/` — сегменты модуля по канону SLM. `client.ts` и `index.ts` — единственные корневые файлы.
|
||||||
|
|
||||||
|
## Типы клиента
|
||||||
|
|
||||||
|
Типы, описывающие саму инфраструктуру запросов (опции, параметры) — выносятся в `types/client.ts`. Это держит `client.ts` коротким и не смешивает декларации типов с реализацией класса.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/client.ts
|
||||||
|
export type ParamValue = string | number | (string | number)[]
|
||||||
|
|
||||||
|
export type RequestOptions = {
|
||||||
|
params?: Record<string, ParamValue>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
revalidate?: number | false
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostOptions = RequestOptions & {
|
||||||
|
type?: 'json' | 'formdata'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Базовый клиент
|
||||||
|
|
||||||
|
Класс с конфигурацией (`baseUrl`, общие заголовки) и базовыми методами `get` / `post`. Конкретные методы API размещаются в сегменте `methods/`, а не на самом классе — это держит `client.ts` коротким и не плодит «бога-класс».
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/client.ts
|
||||||
|
import { PetProjectApiError } from './errors/pet-project-api.error'
|
||||||
|
import type { ParamValue, RequestOptions, PostOptions } from './types/client'
|
||||||
|
|
||||||
|
export class PetProjectApiClient {
|
||||||
|
constructor(
|
||||||
|
private readonly baseUrl: string,
|
||||||
|
private readonly defaultHeaders: Record<string, string> = {},
|
||||||
|
) {
|
||||||
|
this.defaultHeaders = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...defaultHeaders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUrl(path: string, params?: Record<string, ParamValue>): string {
|
||||||
|
const base = this.baseUrl.replace(/\/+$/, '')
|
||||||
|
const tail = path.replace(/^\/+/, '')
|
||||||
|
const url = `${base}/${tail}`
|
||||||
|
|
||||||
|
if (!params) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = new URLSearchParams()
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => search.append(key, String(v)))
|
||||||
|
} else {
|
||||||
|
search.set(key, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${url}?${search}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||||
|
const { params, headers, revalidate } = options
|
||||||
|
const response = await fetch(this.buildUrl(path, params), {
|
||||||
|
headers: { ...this.defaultHeaders, ...headers },
|
||||||
|
...(revalidate !== undefined && { next: { revalidate } }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw await PetProjectApiError.fromResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(path: string, body: unknown, options: PostOptions = {}): Promise<T> {
|
||||||
|
const { params, headers, revalidate, type = 'json' } = options
|
||||||
|
const isJson = type === 'json'
|
||||||
|
|
||||||
|
const response = await fetch(this.buildUrl(path, params), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.defaultHeaders,
|
||||||
|
...(isJson && { 'Content-Type': 'application/json' }),
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: isJson ? JSON.stringify(body) : (body as BodyInit),
|
||||||
|
...(revalidate !== undefined && { next: { revalidate } }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw await PetProjectApiError.fromResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые требования к клиенту
|
||||||
|
|
||||||
|
- **Класс с приватным состоянием** (`baseUrl`, `defaultHeaders`) — конфигурация инкапсулирована.
|
||||||
|
- **Типы клиента — в `types/client.ts`**, не в `client.ts`. Реализация и контракты разделены.
|
||||||
|
- **Базовые методы дженерик `<T>` без дефолта.** Вызов без типа невозможен — потребитель обязан указать форму ответа.
|
||||||
|
- **Доменная ошибка вместо `null`.** При не-`ok` бросается `PetProjectApiError`. Возврат `null` глотает причины (404 vs 500 vs 401) — не использовать.
|
||||||
|
- **Дефолт POST — `json`.** `formdata` указывается явно, на конкретных методах (загрузка файлов, отправка форм).
|
||||||
|
- **Нормализация слэшей** в `buildUrl` — `baseUrl` без хвостового `/`, `path` без ведущего `/`.
|
||||||
|
- **`async/await`**, не `.then()` — линейное чтение, простая обработка ошибок.
|
||||||
|
- **Поддержка `next.revalidate`** — клиент знает о Next.js App Router и пробрасывает кеш-флаги.
|
||||||
|
|
||||||
|
## Доменная ошибка
|
||||||
|
|
||||||
|
Сетевая ошибка превращается в класс ошибки модуля. Наружу не выходит сырой `Response`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
|
||||||
|
export class PetProjectApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly status: number,
|
||||||
|
public readonly body: string,
|
||||||
|
) {
|
||||||
|
super(`PetProjectApi ${status}: ${body.slice(0, 200)}`)
|
||||||
|
this.name = 'PetProjectApiError'
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromResponse(response: Response): Promise<PetProjectApiError> {
|
||||||
|
const body = await response.text().catch(() => '')
|
||||||
|
return new PetProjectApiError(response.status, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Дополнительные подклассы по необходимости: `PetProjectApiValidationError` (400), `PetProjectApiAuthError` (401/403), `PetProjectApiNotFoundError` (404). Вводятся когда у потребителя есть **разная реакция** на разные коды; иначе хватает базового класса.
|
||||||
|
|
||||||
|
## Доменные типы
|
||||||
|
|
||||||
|
Типы запросов, ответов и фильтров — по файлу на сущность. Типы должны лежать рядом по смыслу: всё, что относится к `posts`, — в `types/post.ts`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/post.ts
|
||||||
|
export type Post = {
|
||||||
|
id: string
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
publishedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostFilter = {
|
||||||
|
limit?: number
|
||||||
|
categories?: number[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/types/index.ts
|
||||||
|
export type * from './post'
|
||||||
|
export type * from './form'
|
||||||
|
// типы клиента — внутренние, наружу не реэкспортируются
|
||||||
|
```
|
||||||
|
|
||||||
|
Типы клиента (`RequestOptions`, `PostOptions`, `ParamValue`) **не реэкспортируются** через `types/index.ts` — они нужны только внутри модуля.
|
||||||
|
|
||||||
|
## Методы
|
||||||
|
|
||||||
|
Методы группируются по сущностям в сегменте `methods/`, экспортируются фабрикой, принимающей клиент. Это даёт **процедурное обращение** в стиле автогенерированного клиента (`petProjectApi.posts.get(slug)`), а не плоский список (`petProjectApi.getPost(slug)`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/methods/posts.ts
|
||||||
|
import type { PetProjectApiClient } from '../client'
|
||||||
|
import type { Post, PostFilter } from '../types/post'
|
||||||
|
|
||||||
|
export function postsMethods(client: PetProjectApiClient) {
|
||||||
|
return {
|
||||||
|
/** GET /posts/{slug} */
|
||||||
|
get: (slug: string, options?: { revalidate?: number | false }) =>
|
||||||
|
client.get<Post>(`posts/${slug}`, options),
|
||||||
|
|
||||||
|
/** POST /posts/filter */
|
||||||
|
filter: (body: PostFilter) =>
|
||||||
|
client.post<Post[]>('posts/filter', body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/methods/forms.ts
|
||||||
|
import type { PetProjectApiClient } from '../client'
|
||||||
|
import type { Form, FormSubmissionResult } from '../types/form'
|
||||||
|
|
||||||
|
export function formsMethods(client: PetProjectApiClient) {
|
||||||
|
return {
|
||||||
|
/** GET /forms/{id} */
|
||||||
|
get: (id: string) => client.get<Form>(`forms/${id}`),
|
||||||
|
|
||||||
|
/** POST /forms/{id} — multipart/form-data */
|
||||||
|
submit: (id: string, data: FormData) =>
|
||||||
|
client.post<FormSubmissionResult>(`forms/${id}`, data, { type: 'formdata' }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила методов
|
||||||
|
|
||||||
|
- **Группировка по сущности** (`pages`, `posts`, `forms`), не плоский список.
|
||||||
|
- **Имя метода — глагол действия**: `get`, `list`, `filter`, `create`, `update`, `delete`, `submit`. Не `getPost`/`getPosts` — сущность уже в имени группы.
|
||||||
|
- **Типы запросов и ответов — в `types/{entity}.ts`**, импортируются в файл методов. В `methods/` лежит только композиция вызовов клиента, без объявлений типов.
|
||||||
|
- **Фабрика принимает клиент** — это даёт тестируемость (моковый клиент в юнит-тестах) и единый источник конфигурации.
|
||||||
|
- **Никаких знаний об UI.** Клиент не знает про React, SWR, тосты — только данные и ошибки.
|
||||||
|
|
||||||
|
## Сборка инстанса
|
||||||
|
|
||||||
|
Группы методов соединяются в один объект на уровне `index.ts`. Это даёт процедурный доступ `petProjectApi.posts.get(...)`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/index.ts
|
||||||
|
import { PetProjectApiClient } from './client'
|
||||||
|
import { pagesMethods } from './methods/pages'
|
||||||
|
import { postsMethods } from './methods/posts'
|
||||||
|
import { formsMethods } from './methods/forms'
|
||||||
|
|
||||||
|
const client = new PetProjectApiClient(process.env.NEXT_PUBLIC_API_URL, {
|
||||||
|
'X-App-Key': process.env.NEXT_PUBLIC_APP_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const petProjectApi = {
|
||||||
|
pages: pagesMethods(client),
|
||||||
|
posts: postsMethods(client),
|
||||||
|
forms: formsMethods(client),
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PetProjectApiError } from './errors/pet-project-api.error'
|
||||||
|
export type { Post, PostFilter, Page, Form } from './types'
|
||||||
|
export * from './hooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Хуки для клиентских компонентов
|
||||||
|
|
||||||
|
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/use-post-detail.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petProjectApi } from '..'
|
||||||
|
import type { Post } from '../types/post'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение поста по slug.
|
||||||
|
*/
|
||||||
|
export const usePostDetail = (
|
||||||
|
slug: string | null,
|
||||||
|
config?: SWRConfiguration,
|
||||||
|
) => {
|
||||||
|
const key = slug ? ['pet-project-api', 'post', 'detail', slug] : null
|
||||||
|
const fetcher = () => petProjectApi.posts.get(slug!)
|
||||||
|
|
||||||
|
return useSWR<Post>(key, fetcher, config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/use-post-filter.hook.ts
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import type { SWRConfiguration } from 'swr'
|
||||||
|
import { petProjectApi } from '..'
|
||||||
|
import type { Post, PostFilter } from '../types/post'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка постов по фильтру.
|
||||||
|
*/
|
||||||
|
export const usePostFilter = (
|
||||||
|
filter: PostFilter,
|
||||||
|
config?: SWRConfiguration,
|
||||||
|
) => {
|
||||||
|
return useSWR<Post[]>(
|
||||||
|
['pet-project-api', 'post', 'filter', filter],
|
||||||
|
() => petProjectApi.posts.filter(filter),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/index.ts
|
||||||
|
export { usePostDetail } from './use-post-detail.hook'
|
||||||
|
export { usePostFilter } from './use-post-filter.hook'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила хуков
|
||||||
|
|
||||||
|
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
|
||||||
|
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
|
||||||
|
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
|
||||||
|
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
|
||||||
|
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
|
||||||
|
|
||||||
|
## Запрет прямого `fetch`
|
||||||
|
|
||||||
|
В коде приложения (слои выше `infrastructure`) прямые вызовы `fetch` к API запрещены. Все запросы идут через клиент.
|
||||||
|
|
||||||
|
Исключение допускается точечно — например, разовая отладочная проверка эндпоинта в скрипте — и требует обоснования в коде (комментарий с причиной).
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
const post = await petProjectApi.posts.get('my-post')
|
||||||
|
const list = await petProjectApi.posts.filter({ limit: 10, categories: [1, 2] })
|
||||||
|
const form = await petProjectApi.forms.get('contact')
|
||||||
|
```
|
||||||
|
|
||||||
|
Стиль вызовов совпадает с автогенерированным клиентом — потребитель не различает, ручной API или сгенерирован.
|
||||||
165
docs/docs/usage/data/rest/fetching/client.md
Normal file
165
docs/docs/usage/data/rest/fetching/client.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
title: Клиентские компоненты
|
||||||
|
keywords: [swr, клиентские компоненты, useSWR, хук, мутация, useSWRMutation, кеш, ревалидация]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Клиентские компоненты
|
||||||
|
|
||||||
|
В клиентских компонентах данные получаются через **готовые хуки**, которые экспортируются из модуля API. SWR инкапсулирован в хуке — компонент не знает про `useSWR`, ключи и fetcher.
|
||||||
|
|
||||||
|
Создание клиента и хуков — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- **Только готовые хуки.** В компоненте — `usePostDetail(slug)`, не `useSWR(['post', slug], () => api.posts.get(slug))`.
|
||||||
|
- **`useSWR` пишется один раз — в `hooks/`** модуля API. В клиентских компонентах никогда напрямую.
|
||||||
|
- **Прямой вызов методов клиента в `useEffect` запрещён.** Это потеря кеша, повторные запросы и гонки.
|
||||||
|
- **Мутации — через `useSWRMutation`**, тоже инкапсулированный в хуке. В компоненте вызывается готовый `trigger`.
|
||||||
|
|
||||||
|
## Чтение
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export function PostView({ slug }: { slug: string }) {
|
||||||
|
const { data: post, error, isLoading } = usePostDetail(slug)
|
||||||
|
|
||||||
|
if (isLoading) return <Spinner />
|
||||||
|
if (error) return <ErrorView error={error} />
|
||||||
|
|
||||||
|
return <article>{post?.title}</article>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В компоненте нет `useSWR`, нет ключей, нет fetcher — только готовый хук.
|
||||||
|
|
||||||
|
## Параметризованный запрос
|
||||||
|
|
||||||
|
Хук сам обрабатывает «нет параметра — нет запроса». В компоненте можно безопасно передавать `null`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useUserDetail } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export function UserProfile({ userId }: { userId: string | null }) {
|
||||||
|
const { data: user } = useUserDetail(userId)
|
||||||
|
|
||||||
|
if (!userId) return <EmptyState />
|
||||||
|
return <UserCard user={user} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Внутри `useUserDetail` ключ становится `null`, когда `userId` не задан, и SWR не делает запрос — это поведение зашито в хук, потребитель об этом не думает.
|
||||||
|
|
||||||
|
## Мутации
|
||||||
|
|
||||||
|
Мутации тоже оборачиваются в хук модуля API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/infrastructure/pet-project-api/hooks/use-create-user.hook.ts
|
||||||
|
import useSWRMutation from 'swr/mutation'
|
||||||
|
import { mutate } from 'swr'
|
||||||
|
import { petProjectApi } from '..'
|
||||||
|
import type { User, UserCreateInput } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание пользователя с инвалидацией списка.
|
||||||
|
*/
|
||||||
|
export const useCreateUser = () => {
|
||||||
|
return useSWRMutation<User, Error, [string, string, string], UserCreateInput>(
|
||||||
|
['pet-project-api', 'user', 'create'],
|
||||||
|
(_key, { arg }) => petProjectApi.user.create(arg),
|
||||||
|
{
|
||||||
|
onSuccess: () => mutate(['pet-project-api', 'user', 'list']),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCreateUser } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export function CreateUserForm() {
|
||||||
|
const { trigger, isMutating } = useCreateUser()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
onSubmit={(input) => trigger(input)}
|
||||||
|
disabled={isMutating}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В компоненте — снова только хук. Логика инвалидации кеша зашита внутрь, потребитель её не дублирует.
|
||||||
|
|
||||||
|
## Передача config из компонента
|
||||||
|
|
||||||
|
Каждый хук принимает второй (или третий) параметр `config?: SWRConfiguration` — он пробрасывается в `useSWR`. Это даёт потребителю точечно настроить ревалидацию, `fallbackData`, `suspense` и т.п.:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export function PostView({ slug, initialPost }: Props) {
|
||||||
|
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Начальное состояние с сервера
|
||||||
|
|
||||||
|
Если данные пришли из серверного компонента (см. [Серверные компоненты](/docs/usage/data/rest/fetching/server)) — передаются в `fallbackData` через `config` хука:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// page.tsx (server)
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: { slug: string } }) {
|
||||||
|
const initialPost = await petProjectApi.posts.get(params.slug)
|
||||||
|
return <PostView slug={params.slug} initialPost={initialPost} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// post-view.tsx ('use client')
|
||||||
|
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export function PostView({ slug, initialPost }: Props) {
|
||||||
|
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
|
||||||
|
return <article>{post?.title}</article>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Для массового заполнения кеша на странице с несколькими хуками — используется `<SWRConfig fallback>` обёртка. Серверный компонент собирает данные и передаёт сериализованную карту ключей в провайдер; все вложенные хуки сразу видят кеш.
|
||||||
|
|
||||||
|
## Запрет прямых вызовов
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо — прямой fetch в обход клиента
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/users').then(...)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Плохо — клиент без SWR: нет кеша, нет дедупликации
|
||||||
|
useEffect(() => {
|
||||||
|
petProjectApi.user.list().then(setUsers)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Плохо — useSWR в компоненте: SWR должен быть в хуке модуля
|
||||||
|
const { data } = useSWR(
|
||||||
|
['pet-project-api', 'user', 'list'],
|
||||||
|
() => petProjectApi.user.list(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Хорошо — готовый хук модуля
|
||||||
|
const { data } = useUserList()
|
||||||
|
```
|
||||||
|
|
||||||
|
Если для нужной операции хука ещё нет — он добавляется в `hooks/` модуля API, не в компонент.
|
||||||
67
docs/docs/usage/data/rest/fetching/server.md
Normal file
67
docs/docs/usage/data/rest/fetching/server.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Серверные компоненты
|
||||||
|
keywords: [server components, rsc, серверные компоненты, fetch, api, app router, прямой вызов]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Серверные компоненты
|
||||||
|
|
||||||
|
В серверных компонентах (Server Components App Router) данные получаются **прямым вызовом метода API-клиента**. SWR и хуки здесь не применяются — они для клиентского кода.
|
||||||
|
|
||||||
|
Создание клиента — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация.
|
||||||
|
|
||||||
|
## Правила
|
||||||
|
|
||||||
|
- **Прямой `await` метода клиента.** Никаких хуков, обёрток состояний, `useEffect` — серверный компонент не имеет жизненного цикла React-клиента.
|
||||||
|
- **Ошибки бросаются.** Не оборачивать `try/catch` без необходимости — Next.js поднимет ближайший `error.tsx`.
|
||||||
|
- **Параллельные запросы — через `Promise.all`.** Последовательный `await` за `await` блокирует рендер.
|
||||||
|
|
||||||
|
## Шаблон
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/app/(routes)/users/page.tsx
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
|
||||||
|
export default async function UsersPage() {
|
||||||
|
const users = await petProjectApi.user.list()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{users.map((user) => (
|
||||||
|
<li key={user.id}>{user.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параллельные запросы
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
const [users, orders] = await Promise.all([
|
||||||
|
petProjectApi.user.list(),
|
||||||
|
petProjectApi.order.list(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return <Dashboard users={users} orders={orders} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Передача данных в клиентский компонент
|
||||||
|
|
||||||
|
Серверный компонент получает данные и передаёт их пропсами в клиентский. На клиенте данные становятся начальным состоянием — при необходимости перезапрашиваются через SWR (см. [Клиентские компоненты](/docs/usage/data/rest/fetching/client)).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// page.tsx (server)
|
||||||
|
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||||
|
import { UsersList } from 'widgets/users-list'
|
||||||
|
|
||||||
|
export default async function UsersPage() {
|
||||||
|
const initialUsers = await petProjectApi.user.list()
|
||||||
|
return <UsersList initialUsers={initialUsers} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запрет прямого `fetch`
|
||||||
|
|
||||||
|
Серверный компонент тоже использует только клиент из `infrastructure/`. Прямой `fetch` в `page.tsx` или в server-action запрещён теми же правилами, что и на клиенте.
|
||||||
@@ -4,7 +4,7 @@ title: Использование
|
|||||||
|
|
||||||
# Использование
|
# Использование
|
||||||
|
|
||||||
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/applied/styles/postcss).
|
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/setup/postcss).
|
||||||
|
|
||||||
## Общие правила
|
## Общие правила
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превь
|
|||||||
|
|
||||||
# Использование
|
# Использование
|
||||||
|
|
||||||
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/applied/svg-sprites/setup).
|
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/setup/svg-sprites).
|
||||||
|
|
||||||
## Шаги
|
## Шаги
|
||||||
|
|
||||||
@@ -28,4 +28,4 @@ title: Генерация кода
|
|||||||
- Повторяющаяся структура появляется больше одного раза.
|
- Повторяющаяся структура появляется больше одного раза.
|
||||||
- Существующий шаблон не покрывает нужный тип модуля.
|
- Существующий шаблон не покрывает нужный тип модуля.
|
||||||
|
|
||||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/applied/templates-generation).
|
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/usage/templates-generation).
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ title: Добавление UI-модуля
|
|||||||
|
|
||||||
## Порядок действий
|
## Порядок действий
|
||||||
|
|
||||||
1. [Сгенерировать](/docs/applied/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
1. [Сгенерировать](/docs/usage/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
||||||
2. Заполнить модуль логикой и стилями.
|
2. Заполнить модуль логикой и стилями.
|
||||||
|
|
||||||
## Дочерние компоненты
|
## Дочерние компоненты
|
||||||
|
|
||||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/usage/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||||
|
|
||||||
Правила написания компонентов — [Компоненты](/docs/applied/components).
|
Правила написания компонентов — [Компоненты](/docs/usage/components).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ title: Добавление страницы
|
|||||||
|
|
||||||
## Порядок действий
|
## Порядок действий
|
||||||
|
|
||||||
1. [Сгенерировать](/docs/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
1. [Сгенерировать](/docs/usage/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
||||||
|
|
||||||
2. Заполнить экран логикой и стилями.
|
2. Заполнить экран логикой и стилями.
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ title: Добавление страницы
|
|||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
|
|
||||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/applied/templates-generation) из шаблона.
|
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/usage/templates-generation) из шаблона.
|
||||||
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
||||||
- Каждая страница содержит `metadata` с `title` и `description`.
|
- Каждая страница содержит `metadata` с `title` и `description`.
|
||||||
|
|
||||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/applied/page-level).
|
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/usage/page-level).
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ title: Начало работы
|
|||||||
|
|
||||||
## Настройка окружения
|
## Настройка окружения
|
||||||
|
|
||||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/applied/vscode).
|
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/setup/vscode).
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ title: Стилизация
|
|||||||
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
|
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
|
||||||
- **Глобальные стили** вне `app/styles/` запрещены.
|
- **Глобальные стили** вне `app/styles/` запрещены.
|
||||||
|
|
||||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/applied/styles/usage).
|
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/usage/styles).
|
||||||
|
|||||||
Reference in New Issue
Block a user