Compare commits
21 Commits
v2
...
7965ec2a96
| Author | SHA1 | Date | |
|---|---|---|---|
| 7965ec2a96 | |||
| 7c224fed99 | |||
| 74cbd43a23 | |||
| ef58a02609 | |||
| e265799c26 | |||
| 781efc52f1 | |||
| ec01ae2e1c | |||
| 3d93efd90a | |||
| e5e4ace91a | |||
| 5a773a5b4f | |||
| f645b2ad40 | |||
| 90bf360c06 | |||
| 5cf0f0f8ba | |||
| 464c709859 | |||
| 64db18917b | |||
| ae103e962e | |||
| 99c0995cb6 | |||
| d621e6b57d | |||
| 787223010f | |||
| f5732904f4 | |||
| 36304c14f0 |
@@ -20,6 +20,9 @@ jobs:
|
||||
echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> $GITHUB_ENV
|
||||
REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> $GITHUB_ENV
|
||||
# Версия сборки: тег если есть, иначе короткий SHA
|
||||
BUILD_VERSION=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)
|
||||
echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -47,6 +50,8 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -135,3 +135,7 @@ dist
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
docs/.vitepress
|
||||
|
||||
# Генерируется через `npm run llms`
|
||||
docs/public/
|
||||
generated/
|
||||
@@ -1,112 +1,164 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
const ruSidebar = [
|
||||
const sidebar = [
|
||||
{
|
||||
text: 'Главная',
|
||||
link: '/docs/',
|
||||
},
|
||||
{
|
||||
text: 'Workflow',
|
||||
link: '/workflow',
|
||||
link: '/docs/workflow',
|
||||
},
|
||||
{
|
||||
text: 'Базовые правила',
|
||||
items: [
|
||||
{ text: 'Технологии и библиотеки', link: '/basics/tech-stack' },
|
||||
{ text: 'Именование', link: '/basics/naming' },
|
||||
{ text: 'Архитектура', link: '/basics/architecture' },
|
||||
{ text: 'Стиль кода', link: '/basics/code-style' },
|
||||
{ text: 'Документирование', link: '/basics/documentation' },
|
||||
{ text: 'Типизация', link: '/basics/typing' },
|
||||
{ text: 'Технологии и библиотеки', link: '/docs/basics/tech-stack' },
|
||||
{ text: 'Именование', link: '/docs/basics/naming' },
|
||||
{
|
||||
text: 'Архитектура',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/docs/basics/architecture/' },
|
||||
{ text: 'Слои', link: '/docs/basics/architecture/reference/layers' },
|
||||
{ text: 'Модули', link: '/docs/basics/architecture/reference/modules' },
|
||||
{ text: 'Сегменты', link: '/docs/basics/architecture/reference/segments' },
|
||||
],
|
||||
},
|
||||
{ text: 'Стиль кода', link: '/docs/basics/code-style' },
|
||||
{ text: 'Документирование', link: '/docs/basics/documentation' },
|
||||
{ text: 'Типизация', link: '/docs/basics/typing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Прикладные разделы',
|
||||
text: 'Установка и настройка',
|
||||
items: [
|
||||
{ text: 'Структура проекта', link: '/applied/project-structure' },
|
||||
{ text: 'Компоненты', link: '/applied/components' },
|
||||
{ text: 'Страницы (App Router)', link: '/applied/page-level' },
|
||||
{ text: 'Шаблоны и генерация кода', link: '/applied/templates-generation' },
|
||||
{ text: 'Стили', link: '/applied/styles' },
|
||||
{ text: 'Изображения', link: '/applied/images-sprites' },
|
||||
{ text: 'SVG-спрайты', link: '/applied/svg-sprites' },
|
||||
{ text: 'Видео', link: '/applied/video' },
|
||||
{ text: 'API', link: '/applied/api' },
|
||||
{ text: 'Stores', link: '/applied/stores' },
|
||||
{ text: 'Хуки', link: '/applied/hooks' },
|
||||
{ text: 'Шрифты', link: '/applied/fonts' },
|
||||
{ text: 'Локализация', link: '/applied/localization' },
|
||||
{ text: 'Настройка VS Code', link: '/applied/vscode' },
|
||||
{
|
||||
text: 'Создание проекта',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Из шаблона', link: '/docs/setup/project-from-template' },
|
||||
{ text: 'Вручную', link: '/docs/setup/project-manual' },
|
||||
],
|
||||
},
|
||||
{ text: 'Next.js', link: '/docs/setup/nextjs' },
|
||||
{ text: 'Алиасы', link: '/docs/setup/aliases' },
|
||||
{ text: 'Biome', link: '/docs/setup/biome' },
|
||||
{ text: 'Стили', link: '/docs/setup/styles' },
|
||||
{ text: 'PostCSS', link: '/docs/setup/postcss' },
|
||||
{ text: 'SVG-спрайты', link: '/docs/setup/svg-sprites' },
|
||||
{ text: 'Шаблоны генерации', link: '/docs/setup/templates' },
|
||||
{ text: 'VS Code', link: '/docs/setup/vscode' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Использование',
|
||||
items: [
|
||||
{ text: 'Структура проекта', link: '/docs/usage/project-structure' },
|
||||
{ text: 'Компоненты', link: '/docs/usage/components' },
|
||||
{ text: 'Страницы (App Router)', link: '/docs/usage/page-level' },
|
||||
{
|
||||
text: 'Данные',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/docs/usage/data/' },
|
||||
{
|
||||
text: 'REST',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Клиенты',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Автоматическая генерация', link: '/docs/usage/data/rest/clients/auto' },
|
||||
{ text: 'Ручная генерация', link: '/docs/usage/data/rest/clients/manual' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Получение данных',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Серверные компоненты', link: '/docs/usage/data/rest/fetching/server' },
|
||||
{ text: 'Клиентские компоненты', link: '/docs/usage/data/rest/fetching/client' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ text: 'Realtime', link: '/docs/usage/data/realtime' },
|
||||
],
|
||||
},
|
||||
{ text: 'Шаблоны и генерация кода', link: '/docs/usage/templates-generation' },
|
||||
{ text: 'Стили', link: '/docs/usage/styles' },
|
||||
// Неактивные разделы: страницы существуют, но пока пустые.
|
||||
// Оставляем в sidebar без `link`, чтобы видеть план, но без перехода.
|
||||
{ text: 'Изображения · в разработке' },
|
||||
{ text: 'SVG-спрайты', link: '/docs/usage/svg-sprites' },
|
||||
{ text: 'Видео · в разработке' },
|
||||
{ text: 'Stores · в разработке' },
|
||||
{ text: 'Хуки · в разработке' },
|
||||
{ text: 'Шрифты · в разработке' },
|
||||
{ text: 'Локализация · в разработке' },
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
const enSidebar = [
|
||||
{
|
||||
text: 'Processes',
|
||||
items: [
|
||||
{ text: 'Getting Started', link: '/en/workflow/getting-started' },
|
||||
{ text: 'Creating an App', link: '/en/workflow/creating-app' },
|
||||
{ text: 'Creating Pages', link: '/en/workflow/creating-pages' },
|
||||
{ text: 'Creating Components', link: '/en/workflow/creating-components' },
|
||||
{ text: 'Styling', link: '/en/workflow/styling' },
|
||||
{ text: 'Data Fetching', link: '/en/workflow/data-fetching' },
|
||||
{ text: 'State Management', link: '/en/workflow/state-management' },
|
||||
{ text: 'Localization', link: '/en/workflow/localization' },
|
||||
],
|
||||
/**
|
||||
* Vite-плагин: отдаёт `.txt` и `.md` с явной кодировкой UTF-8.
|
||||
* Без этого браузер декодирует как ISO-8859-1 и кириллица ломается.
|
||||
*/
|
||||
const utf8TextPlugin = {
|
||||
name: 'utf8-text-files',
|
||||
configureServer(server: any) {
|
||||
server.middlewares.use((req: any, res: any, next: any) => {
|
||||
const url: string = req.url || '';
|
||||
if (url.endsWith('.txt') || url.endsWith('.md')) {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
{
|
||||
text: 'Basic Rules',
|
||||
items: [
|
||||
{ text: 'Tech Stack', link: '/en/basics/tech-stack' },
|
||||
{ text: 'Architecture', link: '/en/basics/architecture' },
|
||||
{ text: 'Code Style', link: '/en/basics/code-style' },
|
||||
{ text: 'Naming', link: '/en/basics/naming' },
|
||||
{ text: 'Documentation', link: '/en/basics/documentation' },
|
||||
{ text: 'Typing', link: '/en/basics/typing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Applied Sections',
|
||||
items: [
|
||||
{ text: 'VS Code Setup', link: '/en/applied/vscode' },
|
||||
{ text: 'Project Structure', link: '/en/applied/project-structure' },
|
||||
{ text: 'Components', link: '/en/applied/components' },
|
||||
{ text: 'Page-level Components', link: '/en/applied/page-level' },
|
||||
{ text: 'Templates & Code Generation', link: '/en/applied/templates-generation' },
|
||||
{ text: 'Styles', link: '/en/applied/styles' },
|
||||
{ text: 'Images', link: '/en/applied/images-sprites' },
|
||||
{ text: 'SVG Sprites', link: '/en/applied/svg-sprites' },
|
||||
{ text: 'Video', link: '/en/applied/video' },
|
||||
{ text: 'API', link: '/en/applied/api' },
|
||||
{ text: 'Stores', link: '/en/applied/stores' },
|
||||
{ text: 'Hooks', link: '/en/applied/hooks' },
|
||||
{ text: 'Fonts', link: '/en/applied/fonts' },
|
||||
{ text: 'Localization', link: '/en/applied/localization' },
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'docs',
|
||||
title: 'NextJS Style Guide',
|
||||
description: 'Правила и стандарты разработки на NextJS и TypeScript',
|
||||
|
||||
rewrites: {
|
||||
'ru/:rest*': ':rest*',
|
||||
},
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: 'Русский',
|
||||
// `docs/public/` содержит сгенерированные `.md`-копии и `llms.txt` для LLM
|
||||
// (попадают в корень `dist/` как статика). Исключаем из сканирования
|
||||
// страниц, иначе VitePress рендерит их как HTML-страницы.
|
||||
srcExclude: ['public/**'],
|
||||
lang: 'ru-RU',
|
||||
title: 'NextJS Style Guide',
|
||||
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
|
||||
|
||||
// Чистые URL без `.html` — канон для индексации.
|
||||
// Серверная поддержка реализована в Caddyfile (try_files + редирект).
|
||||
cleanUrls: true,
|
||||
|
||||
// Дублируем указатель на llms.txt в <head> — для агентов,
|
||||
// которые читают HTML, но не парсят полный DOM/href.
|
||||
head: [
|
||||
['link', { rel: 'alternate', type: 'text/plain', href: '/llms.txt', title: 'llms.txt' }],
|
||||
['link', { rel: 'alternate', type: 'text/plain', href: '/llms-full.txt', title: 'llms-full.txt' }],
|
||||
],
|
||||
|
||||
vite: {
|
||||
plugins: [utf8TextPlugin],
|
||||
define: {
|
||||
__BUILD_VERSION__: JSON.stringify(process.env.BUILD_VERSION || 'dev'),
|
||||
},
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
sidebar: ruSidebar,
|
||||
sidebar,
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://gromlab.ru/docs/nextjs-style-guide' },
|
||||
],
|
||||
},
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en-US',
|
||||
link: '/en/',
|
||||
themeConfig: {
|
||||
sidebar: enSidebar,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Расширенный блок описания для llms.txt — даёт LLM полный
|
||||
// технический контекст: стек, методология, охват тем.
|
||||
// Используется в generate-llms.ts.
|
||||
llmsBlockquote:
|
||||
'Стандарты разработки frontend-приложений на Next.js (App Router) + TypeScript + React с архитектурой SLM (Scoped Layered Module Design — модульная архитектура со слоями ответственности, где каждый модуль содержит всё необходимое: компоненты, хуки, сторы, типы, стили).',
|
||||
llmsContext:
|
||||
'Стек: React, TypeScript, Next.js App Router, Mantine UI, SWR, Zustand, i18next, PostCSS Modules, Vitest, clsx.\n\nДокументация покрывает архитектуру SLM (слои, модули, сегменты, направление зависимостей, публичный API), правила оформления кода (именование, форматирование, импорты, типизация, JSDoc), реализацию компонентов и хуков, работу с App Router, кодогенерацию из шаблонов, стилизацию (Mobile First, токены), работу с API и сокетами, управление состоянием через Zustand, локализацию, ассеты (шрифты, изображения, SVG-спрайты) и настройку VS Code.',
|
||||
} as any);
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
При работе с документацией следовать правилам из CONTRIBUTING.md.
|
||||
|
||||
- Язык документации и коммитов — русский.
|
||||
- После изменений в `.md`-файлах — запустить `npm run docs` для обновления RULES.md.
|
||||
- При добавлении нового раздела — обновить сайдбар (`.vitepress/config.ts`)
|
||||
и порядок файлов (`concat-md.js`).
|
||||
- После изменений в `.md`-файлах — запустить `npm run llms` для обновления `llms.txt` и README.
|
||||
- При добавлении нового раздела — обновить сайдбар (`.vitepress/config.ts`):
|
||||
он же является источником порядка и группировки для `llms.txt`.
|
||||
|
||||
123
CONTRIBUTING.md
123
CONTRIBUTING.md
@@ -7,9 +7,8 @@
|
||||
Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
|
||||
|
||||
- Движок: VitePress
|
||||
- Языки: русский (основной), английский
|
||||
- Русская версия: `docs/ru/`
|
||||
- Английская версия: `docs/en/`
|
||||
- Язык: русский
|
||||
- Контент: `docs/docs/`
|
||||
|
||||
## Команды
|
||||
|
||||
@@ -17,50 +16,58 @@
|
||||
|---------|-----------|
|
||||
| `npm run dev` | Локальный сервер разработки |
|
||||
| `npm run build` | Сборка статического сайта |
|
||||
| `npm run docs` | Генерация `generated/{lang}/RULES.md` — единый файл для AI-ассистентов |
|
||||
| `npm run llms` | Генерация `llms.txt` (карта документации для LLM) и README |
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
docs/
|
||||
├── ru/ # Русская версия (основная)
|
||||
│ ├── index.md # Главная страница
|
||||
│ ├── basics/ # Базовые правила
|
||||
│ │ ├── tech-stack.md
|
||||
│ │ ├── architecture.md
|
||||
│ │ ├── code-style.md
|
||||
│ │ ├── naming.md
|
||||
│ │ ├── documentation.md
|
||||
│ │ └── typing.md
|
||||
│ └── applied/ # Прикладные разделы
|
||||
│ ├── vscode.md
|
||||
│ ├── project-structure.md
|
||||
│ ├── components.md
|
||||
│ ├── page-level.md
|
||||
│ ├── templates-generation.md
|
||||
│ ├── styles.md
|
||||
│ ├── images-sprites.md
|
||||
├── index.md # Лендинг (URL `/`)
|
||||
└── docs/ # Контент документации (URL `/docs/...`)
|
||||
├── index.md # Главная страница
|
||||
├── workflow.md
|
||||
├── workflow/ # Процессы разработки
|
||||
├── basics/ # Базовые правила
|
||||
│ ├── tech-stack.md
|
||||
│ ├── architecture/
|
||||
│ ├── code-style.md
|
||||
│ ├── naming.md
|
||||
│ ├── documentation.md
|
||||
│ └── typing.md
|
||||
├── setup/ # Установка: разовая настройка проекта
|
||||
│ ├── aliases.md
|
||||
│ ├── biome.md
|
||||
│ ├── postcss.md
|
||||
│ ├── svg-sprites.md
|
||||
│ ├── video.md
|
||||
│ ├── api.md
|
||||
│ ├── stores.md
|
||||
│ ├── hooks.md
|
||||
│ ├── fonts.md
|
||||
│ └── localization.md
|
||||
├── en/ # Английская версия (зеркало ru/)
|
||||
│ └── vscode.md
|
||||
└── usage/ # Использование: повседневная работа
|
||||
├── project-structure.md
|
||||
├── components.md
|
||||
├── page-level.md
|
||||
├── templates-generation.md
|
||||
├── styles.md
|
||||
├── images-sprites.md
|
||||
├── svg-sprites.md
|
||||
├── video.md
|
||||
├── data/
|
||||
├── stores.md
|
||||
├── hooks.md
|
||||
├── fonts.md
|
||||
└── localization.md
|
||||
.vitepress/
|
||||
├── config.ts # Конфигурация VitePress, сайдбары, локали
|
||||
generated/
|
||||
├── ru/RULES.md # Сгенерированный единый файл (ru)
|
||||
└── en/RULES.md # Сгенерированный единый файл (en)
|
||||
concat-md.js # Скрипт генерации RULES.md
|
||||
└── config.ts # Конфигурация VitePress, сайдбар
|
||||
generate-llms.ts # Скрипт генерации llms.txt и README
|
||||
```
|
||||
|
||||
Сгенерированные артефакты (`docs/public/`): `llms.txt`, `llms-full.txt`,
|
||||
`nextjs-style-guide.zip`, `manifest.json`, копии `.md` в `docs/public/docs/`.
|
||||
|
||||
### Добавление нового раздела
|
||||
|
||||
1. Создать `.md`-файл в нужной папке (`basics/` или `applied/`).
|
||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод).
|
||||
3. Добавить файл в массив `fileOrder` — `concat-md.js` (для генерации RULES.md).
|
||||
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/`, `docs/docs/setup/` или `docs/docs/usage/`).
|
||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
||||
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
||||
|
||||
## Два типа документации
|
||||
|
||||
@@ -184,6 +191,50 @@ title: Название раздела
|
||||
- Подсекции внутри `h2` — `h3`.
|
||||
- `h4` не используется.
|
||||
|
||||
### Вводный абзац
|
||||
|
||||
Абзац сразу после `h1` отвечает на вопрос «о чём этот раздел?».
|
||||
Он попадает в `llms.txt` и `README.md` архива как краткое описание,
|
||||
поэтому должен быть плотным и без воды.
|
||||
|
||||
**Правила:**
|
||||
|
||||
- Не начинать с «Раздел описывает», «Этот раздел», «В этом разделе»,
|
||||
«Здесь рассмотрено», «В этом документе».
|
||||
- Начинать с подлежащего — самой темы (`Слои SLM:`, `Соглашения об именовании:`).
|
||||
- Двоеточие или тире для перечисления **категорий и областей**, а не
|
||||
конкретных значений из содержимого.
|
||||
- Не дублировать содержимое: если внутри раздела 12 правил —
|
||||
не перечислять их во вводном абзаце.
|
||||
- Не аргументировать («единые правила делают код предсказуемым»).
|
||||
- 1–2 предложения.
|
||||
|
||||
**Проверка:** если при добавлении нового правила/инструмента/раздела
|
||||
вводный абзац придётся править — он слишком конкретный.
|
||||
|
||||
**Хорошо:**
|
||||
|
||||
```markdown
|
||||
Слои SLM: назначение, классификация, направление зависимостей, правила.
|
||||
```
|
||||
|
||||
```markdown
|
||||
Базовый стек проекта по областям: UI, архитектура, данные, состояние,
|
||||
локализация, тестирование, стили, генерация кода.
|
||||
```
|
||||
|
||||
**Плохо:**
|
||||
|
||||
```markdown
|
||||
Раздел описывает слои SLM: что такое слой, какие бывают, как между
|
||||
ними направлены зависимости и какие правила действуют на каждом.
|
||||
```
|
||||
|
||||
```markdown
|
||||
Этот раздел описывает базовый стек технологий и библиотек, принятый в
|
||||
проекте. React, TypeScript, Next.js, SWR, Zustand, i18next.
|
||||
```
|
||||
|
||||
### Примеры кода
|
||||
|
||||
- Блоки кода с указанием языка: ` ```tsx `, ` ```css `, ` ```bash `, ` ```text `.
|
||||
|
||||
43
Caddyfile
43
Caddyfile
@@ -1,5 +1,44 @@
|
||||
:8080 {
|
||||
root * /srv
|
||||
file_server
|
||||
try_files {path} /index.html
|
||||
|
||||
# Устаревшие пути llms.txt в подпапках → корень.
|
||||
# Без этого опечатка `/docs/llms.txt` уходит в SPA-фолбэк и
|
||||
# отдаёт HTML под видом text/plain — агент верит, что получил llms.txt.
|
||||
redir /docs/llms.txt /llms.txt 301
|
||||
redir /docs/llms-full.txt /llms-full.txt 301
|
||||
|
||||
# Чистые URL: запросы вида `/docs/foo.html` редиректим на `/docs/foo`.
|
||||
# Канон сайта — без `.html` (cleanUrls в VitePress).
|
||||
# Не трогаем index.html в корне — он не имеет смысловой пары без расширения.
|
||||
@legacyHtml {
|
||||
path_regexp legacyHtml ^(/.+)\.html$
|
||||
not path /index.html
|
||||
}
|
||||
redir @legacyHtml {re.legacyHtml.1} 301
|
||||
|
||||
# Подсказка агентам, где лежит карта документации (RFC 8288).
|
||||
# Позволяет найти llms.txt без парсинга DOM — по HTTP-заголовку.
|
||||
header Link "</llms.txt>; rel=\"llms\""
|
||||
|
||||
# Кириллица в .txt/.md ломается без явного charset.
|
||||
# Применяем заголовок только к РЕАЛЬНО существующим файлам,
|
||||
# иначе SPA-фолбэк (HTML) уезжает с Content-Type: text/plain.
|
||||
@existingText {
|
||||
path *.txt *.md
|
||||
file
|
||||
}
|
||||
header @existingText Content-Type "text/plain; charset=utf-8"
|
||||
|
||||
# Несуществующие .txt/.md → 404, не HTML-фолбэк.
|
||||
# Это критично для llms.txt: агент должен получить честный 404,
|
||||
# а не валидный «как бы текст» с лендингом внутри.
|
||||
@missingText {
|
||||
path *.txt *.md
|
||||
not file
|
||||
}
|
||||
respond @missingText 404
|
||||
|
||||
file_server
|
||||
# cleanUrls: пробуем точное совпадение → +.html → каталог → SPA-фолбэк.
|
||||
try_files {path} {path}.html {path}/ /index.html
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
FROM node:24-alpine AS build
|
||||
WORKDIR /app
|
||||
# zip нужен для упаковки nextjs-style-guide.zip
|
||||
RUN apk add --no-cache zip
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
ARG BUILD_VERSION=dev
|
||||
ENV BUILD_VERSION=${BUILD_VERSION}
|
||||
RUN npm run llms && npm run build
|
||||
|
||||
FROM caddy:2-alpine
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
109
README.md
109
README.md
@@ -1,57 +1,80 @@
|
||||
# NextJS Style Guide
|
||||
|
||||
Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure.
|
||||
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
|
||||
|
||||
## Documentation Structure
|
||||
Сайт: https://nextjs-style-guide.gromlab.ru
|
||||
|
||||
### Processes
|
||||
## Использование
|
||||
|
||||
**What to do** in a specific situation — step-by-step instructions.
|
||||
**Для AI-агентов:**
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Getting Started | What tools to install before starting development? |
|
||||
| Creating an App | How to create a new project, where to get a template? |
|
||||
| Creating Pages | How to add a page: routing and screen? |
|
||||
| Creating Components | How to generate components using templates? |
|
||||
| Styling | What to use: Mantine, tokens, or PostCSS? |
|
||||
| Data Fetching | How to fetch data: SWR, codegen, sockets? |
|
||||
| State Management | When and how to create a store (Zustand)? |
|
||||
| Localization | How to add translations and work with i18next? |
|
||||
- [llms.txt](https://nextjs-style-guide.gromlab.ru/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
|
||||
- [llms-full.txt](https://nextjs-style-guide.gromlab.ru/llms-full.txt) — Вся документация одним файлом.
|
||||
|
||||
### Basic Rules
|
||||
**Для проекта:**
|
||||
|
||||
**What the code should look like** — standards not tied to a specific technology.
|
||||
- [nextjs-style-guide.zip](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Tech Stack | What stack do we use? |
|
||||
| Architecture | How are FSD layers, dependencies, and public API structured? |
|
||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||
| Naming | How to name files, variables, components, hooks? |
|
||||
| Documentation | How to write JSDoc: what to document and what not? |
|
||||
| Typing | How to type: type vs interface, any/unknown? |
|
||||
## Структура документации
|
||||
|
||||
### Applied Sections
|
||||
### Workflow
|
||||
|
||||
**How a specific area works** — rules, structure, and code examples for specific technologies and tools.
|
||||
[Workflow](docs/docs/workflow.md) — пошаговая карта типовых задач: с чего начать, как добавить страницу/компонент, как подключить данные, стили, локализацию.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Project Structure | How are folders and files organized by FSD? |
|
||||
| Components | How is a component structured: files, props, clsx? |
|
||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||
| Images | _(not filled)_ |
|
||||
| SVG Sprites | _(not filled)_ |
|
||||
| Video | _(not filled)_ |
|
||||
| API | _(not filled)_ |
|
||||
| Stores | _(not filled)_ |
|
||||
| Hooks | _(not filled)_ |
|
||||
| Fonts | _(not filled)_ |
|
||||
| Localization | _(not filled)_ |
|
||||
### Базовые правила
|
||||
|
||||
## For Assistants
|
||||
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
|
||||
|
||||
Full documentation in a single MD file: https://gromlab.ru/docs/frontend-style-guide/raw/branch/main/generated/en/RULES.md
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Технологии и библиотеки | Какой стек используем? |
|
||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||
| Архитектура: Обзор | Что такое SLM и зачем она нужна? |
|
||||
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
|
||||
| Архитектура: Модули | Что такое модуль и как он устроен? |
|
||||
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
|
||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||
|
||||
### Установка и настройка
|
||||
|
||||
**Как поднять и сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Создание проекта: Из шаблона | Как начать проект из готового шаблона? |
|
||||
| Создание проекта: Вручную | Как поднять проект с нуля без шаблона? |
|
||||
| Next.js | Как настроить Next.js под проект? |
|
||||
| Алиасы | Как настроить путевые алиасы импортов? |
|
||||
| Biome | Как настроить Biome (линтер и форматер)? |
|
||||
| Стили | Как подключить и настроить стили проекта? |
|
||||
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
|
||||
| SVG-спрайты | Как настроить генерацию SVG-спрайтов? |
|
||||
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
|
||||
| VS Code | Как настроить редактор для проекта? |
|
||||
|
||||
### Использование
|
||||
|
||||
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Структура проекта | Как организованы папки и файлы по SLM? |
|
||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||
| Страницы (App Router) | Как описывать layout, page, loading, error, not-found? |
|
||||
| Данные: Введение | Как устроена работа с данными в проекте? |
|
||||
| Данные: REST: Клиенты: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
|
||||
| Данные: REST: Клиенты: Ручная генерация | Как написать REST-клиент вручную? |
|
||||
| Данные: REST: Получение данных: Серверные компоненты | Как получать данные в серверных компонентах? |
|
||||
| Данные: REST: Получение данных: Клиентские компоненты | Как получать данные в клиентских компонентах (SWR)? |
|
||||
| Данные: Realtime | Как работать с realtime-каналами и сокетами? |
|
||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||
| Изображения | _(не заполнен)_ |
|
||||
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
|
||||
| Видео | _(не заполнен)_ |
|
||||
| Stores | _(не заполнен)_ |
|
||||
| Хуки | _(не заполнен)_ |
|
||||
| Шрифты | _(не заполнен)_ |
|
||||
| Локализация | _(не заполнен)_ |
|
||||
|
||||
60
README_RU.md
60
README_RU.md
@@ -1,60 +0,0 @@
|
||||
# NextJS Style Guide
|
||||
|
||||
Правила и стандарты разработки на NextJS и TypeScript: архитектура, типизация, стили, компоненты, API и инфраструктурные разделы.
|
||||
|
||||
## Для ассистентов
|
||||
|
||||
Полная документация в одном MD файле: https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/ru/RULES.md
|
||||
|
||||
## Структура документации
|
||||
|
||||
### Workflow
|
||||
|
||||
**Что делать и в каком порядке** — пошаговые инструкции.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Начало работы | Что нужно знать перед началом разработки? |
|
||||
| Создание проекта | Как начать новый проект? |
|
||||
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
|
||||
| Добавление страницы | Как добавить новую страницу в проект? |
|
||||
| Добавление UI-модуля | Как создать компонент, фичу, виджет, сущность или layout? |
|
||||
| Стилизация | Как стилизовать компоненты в проекте? |
|
||||
| Получение данных | Как получать данные с сервера? |
|
||||
| Управление состоянием | Как работать с состоянием? |
|
||||
| Локализация | Как добавлять переводы и подключать локализацию? |
|
||||
|
||||
### Базовые правила
|
||||
|
||||
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Технологии и библиотеки | Какой стек используем? |
|
||||
| Архитектура | Как устроены слои FSD, зависимости, публичный API? |
|
||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||
|
||||
### Прикладные разделы
|
||||
|
||||
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
||||
| Структура проекта | Как организованы папки и файлы по FSD? |
|
||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||
| Изображения | _(не заполнен)_ |
|
||||
| SVG-спрайты | _(не заполнен)_ |
|
||||
| Видео | _(не заполнен)_ |
|
||||
| API | _(не заполнен)_ |
|
||||
| Stores | _(не заполнен)_ |
|
||||
| Хуки | _(не заполнен)_ |
|
||||
| Шрифты | _(не заполнен)_ |
|
||||
| Локализация | _(не заполнен)_ |
|
||||
|
||||
107
concat-md.js
107
concat-md.js
@@ -1,107 +0,0 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// Явный порядок файлов внутри каждого языка
|
||||
const fileOrder = [
|
||||
// index
|
||||
"index.md",
|
||||
// workflow
|
||||
"workflow.md",
|
||||
// basics
|
||||
"basics/tech-stack.md",
|
||||
"basics/naming.md",
|
||||
"basics/architecture.md",
|
||||
"basics/code-style.md",
|
||||
"basics/documentation.md",
|
||||
"basics/typing.md",
|
||||
// applied
|
||||
"applied/project-structure.md",
|
||||
"applied/components.md",
|
||||
"applied/page-level.md",
|
||||
"applied/templates-generation.md",
|
||||
"applied/styles.md",
|
||||
"applied/images-sprites.md",
|
||||
"applied/svg-sprites.md",
|
||||
"applied/video.md",
|
||||
"applied/api.md",
|
||||
"applied/stores.md",
|
||||
"applied/hooks.md",
|
||||
"applied/fonts.md",
|
||||
"applied/localization.md",
|
||||
"applied/vscode.md",
|
||||
];
|
||||
|
||||
// Удалить frontmatter из содержимого md-файла
|
||||
const stripFrontmatter = (content) =>
|
||||
content.replace(/^---[\s\S]*?---\n*/m, "");
|
||||
|
||||
// Сдвинуть уровень заголовков на 1 вниз (h1→h2, h2→h3, ...)
|
||||
// Не трогает заголовки внутри блоков кода
|
||||
const shiftHeadings = (content) => {
|
||||
const lines = content.split("\n");
|
||||
let inCodeBlock = false;
|
||||
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.startsWith("```")) inCodeBlock = !inCodeBlock;
|
||||
if (inCodeBlock) return line;
|
||||
if (/^#{1,5}\s/.test(line)) return "#" + line;
|
||||
return line;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
// Собрать RULES.md с мета-якорями для каждого файла
|
||||
const buildRules = (lang) => {
|
||||
const srcDir = `./docs/${lang}`;
|
||||
const outDir = `./generated/${lang}`;
|
||||
const outFile = path.join(outDir, "RULES.md");
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.log(`Пропуск ${lang}: папка ${srcDir} не найдена`);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const parts = [];
|
||||
|
||||
for (const file of fileOrder) {
|
||||
const filePath = path.join(srcDir, file);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const content = stripFrontmatter(raw).trim();
|
||||
if (!content) continue;
|
||||
|
||||
// Мета-якорь: путь VitePress без расширения
|
||||
const route = "/" + file.replace(/\.md$/, "");
|
||||
// index.md остаётся без сдвига (его h1 — главный заголовок документа)
|
||||
const processed = file === "index.md" ? content : shiftHeadings(content);
|
||||
parts.push(`<!-- ${route} -->\n${processed}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(outFile, parts.join("\n\n"), "utf8");
|
||||
console.log(`RULES.md (${lang}) создан: ${outFile}`);
|
||||
};
|
||||
|
||||
// Собираем RULES.md для обоих языков
|
||||
buildRules("ru");
|
||||
buildRules("en");
|
||||
|
||||
// Генерируем README из index.md
|
||||
const buildReadme = (lang, outFile) => {
|
||||
const indexPath = `./docs/${lang}/index.md`;
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.log(`Пропуск README (${lang}): ${indexPath} не найден`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = stripFrontmatter(fs.readFileSync(indexPath, "utf8"));
|
||||
fs.writeFileSync(outFile, content, "utf8");
|
||||
console.log(`${outFile} создан из ${indexPath}`);
|
||||
};
|
||||
|
||||
buildReadme("en", "./README.md");
|
||||
buildReadme("ru", "./README_RU.md");
|
||||
98
docs/docs/basics/architecture/index.md
Normal file
98
docs/docs/basics/architecture/index.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Архитектура
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
|
||||
|
||||
### Dependency Injection без фреймворков
|
||||
|
||||
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
|
||||
|
||||
### Разделение ответственности без перегрузки слоёв
|
||||
|
||||
Сервисы приложения (`infrastructure/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
|
||||
### Горизонтальная инкапсуляция
|
||||
|
||||
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
|
||||
### Колокация по умолчанию
|
||||
|
||||
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
|
||||
|
||||
### Явное разделение каркаса и контента
|
||||
|
||||
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
|
||||
|
||||
### Масштабирование через группировку
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
|
||||
## Происхождение
|
||||
|
||||
SLM Design вырос на основе:
|
||||
|
||||
- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей
|
||||
- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое
|
||||
- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию
|
||||
- **Colocation Principle** — код живёт рядом с местом использования
|
||||
|
||||
## Пример структуры проекта
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── main/
|
||||
│ └── dashboard/
|
||||
│
|
||||
├── screens/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── about/
|
||||
│
|
||||
├── widgets/
|
||||
│ ├── page-heading/
|
||||
│ ├── hero-section/
|
||||
│ └── promo-banner/
|
||||
│
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
│ ├── catalog/
|
||||
│ ├── orders/
|
||||
│ └── chat/
|
||||
│
|
||||
├── infrastructure/
|
||||
│ ├── theme/
|
||||
│ ├── i18n/
|
||||
│ ├── backend-api/
|
||||
│ └── logger/
|
||||
│
|
||||
├── ui/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── modal/
|
||||
│ ├── toast/
|
||||
│ └── dropdown/
|
||||
│
|
||||
└── shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
└── styles/
|
||||
```
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
|
||||
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
|
||||
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
252
docs/docs/basics/architecture/reference/layers.md
Normal file
252
docs/docs/basics/architecture/reference/layers.md
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
title: Слои
|
||||
---
|
||||
|
||||
# Слои
|
||||
|
||||
Слои SLM: назначение, классификация, направление зависимостей, правила.
|
||||
|
||||
## Определение
|
||||
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
|
||||
## Группы слоёв
|
||||
|
||||
Слои делятся на три группы:
|
||||
|
||||
| Группа | Слои | Описание |
|
||||
|--------|------|----------|
|
||||
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
||||
| Ядро | `business`, `infrastructure`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||
|
||||
## Направление зависимостей
|
||||
|
||||
Любой импорт между модулями — только через публичный API.
|
||||
|
||||
```
|
||||
app → [ layouts | screens ] → widgets → business → infrastructure → ui → shared
|
||||
```
|
||||
|
||||
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
||||
- Модули одного слоя `infrastructure` и `ui` могут импортировать друг друга через публичный API
|
||||
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
||||
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
||||
|
||||
|
||||
## Слой App
|
||||
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen.
|
||||
|
||||
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
|
||||
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
|
||||
- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует
|
||||
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
|
||||
- Никем не импортируется
|
||||
|
||||
## Слой Layouts
|
||||
|
||||
Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||
|
||||
```text
|
||||
src/layouts/
|
||||
├── main/
|
||||
├── dashboard/
|
||||
└── auth/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую
|
||||
|
||||
## Слой Screens
|
||||
|
||||
Контент конкретной страницы: собирает её из модулей нижних слоёв.
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── home/
|
||||
├── products/
|
||||
├── product-detail/
|
||||
├── about/
|
||||
└── contacts/
|
||||
```
|
||||
|
||||
Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
├── shop/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── cart/
|
||||
├── account/
|
||||
│ ├── profile/
|
||||
│ ├── settings/
|
||||
│ └── order-history/
|
||||
└── info/
|
||||
├── about/
|
||||
├── contacts/
|
||||
└── faq/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business`
|
||||
|
||||
## Слой Widgets
|
||||
|
||||
Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts.
|
||||
|
||||
Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget.
|
||||
|
||||
```text
|
||||
src/widgets/
|
||||
├── page-heading/
|
||||
├── hero-section/
|
||||
├── onboarding-checklist/
|
||||
├── promo-banner/
|
||||
└── error-boundary/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/`
|
||||
- Используется в нескольких screens или layouts
|
||||
|
||||
## Слой Business
|
||||
|
||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. Cross-domain зависимости по коду реализуются через фабрику. `import type` между доменами разрешён напрямую.
|
||||
|
||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── auth/
|
||||
├── catalog/
|
||||
├── orders/
|
||||
├── checkout/
|
||||
└── chat/
|
||||
```
|
||||
|
||||
Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`).
|
||||
|
||||
```text
|
||||
src/business/
|
||||
├── commerce/
|
||||
│ ├── catalog/
|
||||
│ ├── cart/
|
||||
│ ├── orders/
|
||||
│ └── checkout/
|
||||
└── communication/
|
||||
├── chat/
|
||||
└── notifications/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один бизнес-домен
|
||||
- Циклические зависимости между доменами запрещены
|
||||
- Импорт кода между доменами — через фабрику. `import type` — напрямую
|
||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||
|
||||
## Слой Infrastructure
|
||||
|
||||
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`.
|
||||
|
||||
Отличие от `shared/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
├── theme/
|
||||
├── i18n/
|
||||
├── backend-api/
|
||||
├── maps-api/
|
||||
├── logger/
|
||||
├── feature-flags/
|
||||
└── realtime/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Один модуль = один техсервис
|
||||
- Импортирует `infrastructure/`, `ui/`, `shared/`
|
||||
|
||||
## Слой UI
|
||||
|
||||
UI-кит без бизнес-логики: button, carousel, toast, modal.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`.
|
||||
|
||||
Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── button/
|
||||
├── input/
|
||||
├── icon/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── toast/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах.
|
||||
|
||||
```text
|
||||
src/ui/
|
||||
├── primitives/
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── icon/
|
||||
│ └── badge/
|
||||
└── composites/
|
||||
├── carousel/
|
||||
├── modal/
|
||||
├── dropdown/
|
||||
├── tabs/
|
||||
└── tooltip/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не содержит бизнес-логику
|
||||
- Импортирует только `ui/` и `shared/`
|
||||
|
||||
## Слой Shared
|
||||
|
||||
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
|
||||
|
||||
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
|
||||
|
||||
Отличие от `infrastructure/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
|
||||
|
||||
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
|
||||
|
||||
```text
|
||||
src/shared/
|
||||
├── lib/
|
||||
├── types/
|
||||
├── styles/
|
||||
└── sprites/
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Не имеет runtime-состояния
|
||||
164
docs/docs/basics/architecture/reference/modules.md
Normal file
164
docs/docs/basics/architecture/reference/modules.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
title: Модули
|
||||
---
|
||||
|
||||
# Модули
|
||||
|
||||
Модули SLM: состав, границы, взаимодействие с остальным кодом.
|
||||
|
||||
## Определение
|
||||
|
||||
**Модуль — универсальный строительный блок архитектуры. Живёт на слое и содержит всё необходимое для своей работы: компоненты, хуки, сторы, сервисы, типы, стили. Набор содержимого не фиксирован — включаются только нужные части.**
|
||||
|
||||
## Модуль vs компонент
|
||||
|
||||
**Компонент** — один `.tsx` файл. Не имеет своих сегментов, использует сегменты родительского модуля. Живёт в корне или `ui/` сегменте модуля.
|
||||
|
||||
**Модуль** — папка, которая может содержать корневой компонент, сегменты (`hooks/`, `types/`, `styles/`, `ui/`, `parts/` и т.д.) и публичный API (`index.ts`).
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── ui/
|
||||
│ ├── auth-guard.tsx
|
||||
│ └── logout-button.tsx
|
||||
├── parts/
|
||||
│ ├── login-form/
|
||||
│ ├── registration-form/
|
||||
│ └── restore-form/
|
||||
├── hooks/
|
||||
├── stores/
|
||||
├── types/
|
||||
├── auth.tsx # корневой компонент (опционален)
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Структура
|
||||
|
||||
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль может состоять даже из одного `index.ts` с реэкспортом типов.
|
||||
|
||||
```text
|
||||
{module-name}/
|
||||
├── {module-name}.tsx # корневой компонент (опционален)
|
||||
├── ui/ # компоненты модуля (только .tsx)
|
||||
├── parts/ # вложенные модули (со своими сегментами)
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы состояния
|
||||
├── services/ # внешние источники данных
|
||||
├── mappers/ # трансформация данных между форматами
|
||||
├── types/ # типы
|
||||
├── styles/ # стили
|
||||
├── lib/ # утилиты модуля
|
||||
├── config/ # константы
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
Подробное описание каждого сегмента — в разделе [Сегменты](/docs/basics/architecture/reference/segments).
|
||||
|
||||
## Публичный API
|
||||
|
||||
Модуль экспортирует наружу только то, что нужно другим. Всё остальное — внутреннее.
|
||||
|
||||
```ts
|
||||
// business/auth/index.ts
|
||||
export type { User, Session } from './types/user.types'
|
||||
export { useAuth } from './hooks/use-auth.hook'
|
||||
export { AuthGuard } from './ui/auth-guard'
|
||||
```
|
||||
|
||||
Импорт в обход `index.ts` запрещён:
|
||||
|
||||
```ts
|
||||
// Плохо
|
||||
import { validateToken } from '@/business/auth/lib/tokens'
|
||||
|
||||
// Хорошо
|
||||
import { useAuth } from '@/business/auth'
|
||||
```
|
||||
|
||||
## Фабрика
|
||||
|
||||
Если модуль зависит от кода другого бизнес-домена — он экспортирует фабрику. Фабрика декларирует необходимые зависимости и возвращает API модуля. Точка использования (screen, widget, layout) предоставляет зависимости при вызове.
|
||||
|
||||
Модуль без cross-domain зависимостей экспортирует API напрямую. Типы всегда экспортируются напрямую — `import type` не является runtime-зависимостью.
|
||||
|
||||
### Модуль без зависимостей — прямой экспорт:
|
||||
|
||||
```ts
|
||||
// business/auth/index.ts
|
||||
export { useAuth } from './hooks/use-auth'
|
||||
export { useCurrentUser } from './hooks/use-current-user'
|
||||
export type { User, Session } from './types'
|
||||
```
|
||||
|
||||
### Модуль с зависимостями — фабрика:
|
||||
|
||||
```ts
|
||||
// business/chat/types/deps.ts
|
||||
import type { User } from '@/business/auth'
|
||||
|
||||
export interface ChatDeps {
|
||||
useCurrentUser: () => User | null
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// business/chat/index.ts
|
||||
import type { ChatDeps } from './types/deps'
|
||||
|
||||
export function chatFactory(deps: ChatDeps) {
|
||||
return {
|
||||
useMessages: (roomId: string) => {
|
||||
const user = deps.useCurrentUser()
|
||||
// ...
|
||||
},
|
||||
useSendMessage: (roomId: string) => {
|
||||
const user = deps.useCurrentUser()
|
||||
return (text: string) => { /* ... */ }
|
||||
},
|
||||
useChatRooms: () => {
|
||||
const user = deps.useCurrentUser()
|
||||
// ...
|
||||
},
|
||||
ChatBadge: ({ count }: { count: number }) => { /* ... */ },
|
||||
}
|
||||
}
|
||||
|
||||
export type { Message, ChatRoom } from './types'
|
||||
export type { ChatDeps } from './types/deps'
|
||||
```
|
||||
|
||||
### Использование на странице:
|
||||
|
||||
```tsx
|
||||
// screens/support/support.tsx
|
||||
import { useCurrentUser } from '@/business/auth'
|
||||
import { chatFactory } from '@/business/chat'
|
||||
|
||||
const chat = chatFactory({ useCurrentUser })
|
||||
|
||||
export function SupportScreen() {
|
||||
const { useMessages, useSendMessage, ChatBadge } = chat
|
||||
const messages = useMessages('support')
|
||||
const sendMessage = useSendMessage('support')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChatBadge count={messages.length} />
|
||||
{messages.map(m => <MessageBubble key={m.id} {...m} />)}
|
||||
<MessageInput onSend={sendMessage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||
|
||||
- Нужен на одной странице → `screens/{name}/parts/`
|
||||
- Появился в 2+ местах → поднимается по природе:
|
||||
- абстрактный UI → `ui/`
|
||||
- блок с данными/логикой → `widgets/`
|
||||
- представление бизнес-домена → `business/{area}/parts/`
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
153
docs/docs/basics/architecture/reference/segments.md
Normal file
153
docs/docs/basics/architecture/reference/segments.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: Сегменты
|
||||
---
|
||||
|
||||
# Сегменты
|
||||
|
||||
Сегменты SLM: типы, назначение, что лежит внутри каждого.
|
||||
|
||||
## Определение
|
||||
|
||||
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
|
||||
|
||||
## Обзор
|
||||
|
||||
| Сегмент | Содержимое |
|
||||
|---------|------------|
|
||||
| `ui/` | Компоненты модуля — только `.tsx` файлы |
|
||||
| `parts/` | Вложенные модули со своими сегментами |
|
||||
| `hooks/` | React-хуки |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `services/` | Работа с внешними источниками данных |
|
||||
| `mappers/` | Трансформация данных между форматами |
|
||||
| `types/` | TypeScript-типы и интерфейсы |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты и хелперы модуля |
|
||||
| `config/` | Константы и конфигурация |
|
||||
|
||||
## Сегмент ui/
|
||||
|
||||
Компоненты, принадлежащие модулю. Содержит только `.tsx` файлы — без своих сегментов, стилей, типов, хуков. Использует сегменты родительского модуля.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── ui/
|
||||
│ ├── auth-provider.tsx
|
||||
│ ├── auth-guard.tsx
|
||||
│ └── logout-button.tsx
|
||||
├── types/
|
||||
├── hooks/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Если компоненту нужны собственные сегменты — это уже не `ui/`, а `parts/`.
|
||||
|
||||
## Сегмент parts/
|
||||
|
||||
Вложенные модули со своими сегментами. Каждый элемент `parts/` — полноценный модуль: папка с компонентом, хуками, стилями, типами и т.д.
|
||||
|
||||
```text
|
||||
home/
|
||||
├── parts/
|
||||
│ ├── hero-section/
|
||||
│ │ ├── hero-section.tsx
|
||||
│ │ ├── styles/
|
||||
│ │ └── parts/
|
||||
│ │ └── top-banner/
|
||||
│ │ └── top-banner.tsx
|
||||
│ └── features-section/
|
||||
│ ├── features-section.tsx
|
||||
│ └── hooks/
|
||||
├── home.screen.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Отличие от `ui/`: элемент `parts/` — модуль со своими сегментами. Элемент `ui/` — компонент, один `.tsx` файл.
|
||||
|
||||
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
|
||||
|
||||
Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
|
||||
|
||||
## Сегмент hooks/
|
||||
|
||||
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
|
||||
|
||||
```text
|
||||
hooks/
|
||||
├── use-auth.hook.ts
|
||||
├── use-session.hook.ts
|
||||
└── use-permissions.hook.ts
|
||||
```
|
||||
|
||||
## Сегмент stores/
|
||||
|
||||
Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.).
|
||||
|
||||
```text
|
||||
stores/
|
||||
├── auth.store.ts
|
||||
└── session.store.ts
|
||||
```
|
||||
|
||||
## Сегмент services/
|
||||
|
||||
Работа с внешними источниками данных: API-вызовы, запросы, подписки.
|
||||
|
||||
```text
|
||||
services/
|
||||
├── auth.service.ts
|
||||
└── token.service.ts
|
||||
```
|
||||
|
||||
## Сегмент mappers/
|
||||
|
||||
Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel.
|
||||
|
||||
```text
|
||||
mappers/
|
||||
├── map-user.ts
|
||||
├── map-product.ts
|
||||
└── map-order-to-dto.ts
|
||||
```
|
||||
|
||||
## Сегмент types/
|
||||
|
||||
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
|
||||
|
||||
```text
|
||||
types/
|
||||
├── user.type.ts
|
||||
└── session.type.ts
|
||||
```
|
||||
|
||||
## Сегмент styles/
|
||||
|
||||
Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.).
|
||||
|
||||
```text
|
||||
styles/
|
||||
├── auth.module.css
|
||||
└── login-form.module.css
|
||||
```
|
||||
|
||||
## Сегмент lib/
|
||||
|
||||
Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов.
|
||||
|
||||
```text
|
||||
lib/
|
||||
├── validate-email.ts
|
||||
└── format-phone.ts
|
||||
```
|
||||
|
||||
Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`.
|
||||
|
||||
## Сегмент config/
|
||||
|
||||
Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения.
|
||||
|
||||
```text
|
||||
config/
|
||||
├── routes.ts
|
||||
└── constants.ts
|
||||
```
|
||||
@@ -4,7 +4,7 @@ title: Стиль кода
|
||||
|
||||
# Стиль кода
|
||||
|
||||
Раздел описывает единые правила оформления кода: отступы, переносы, кавычки, порядок импортов и базовую читаемость.
|
||||
Единые правила оформления кода: форматирование, импорты, читаемость.
|
||||
|
||||
## Отступы
|
||||
|
||||
@@ -4,8 +4,7 @@ title: Документирование
|
||||
|
||||
# Документирование
|
||||
|
||||
Этот раздел описывает правила документирования кода: когда и как писать
|
||||
комментарии к компонентам, функциям, типам и интерфейсам.
|
||||
Правила документирования кода: что и когда документировать через JSDoc.
|
||||
|
||||
## Общие правила
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Именование
|
||||
|
||||
# Именование
|
||||
|
||||
Этот раздел описывает соглашения об именовании в проекте. Единые правила делают код предсказуемым и упрощают навигацию по проекту.
|
||||
Соглашения об именовании в коде: что и как называть.
|
||||
|
||||
## Базовые правила
|
||||
|
||||
@@ -53,7 +53,7 @@ title: Именование
|
||||
|
||||
**Хорошо**
|
||||
```text
|
||||
features/
|
||||
business/
|
||||
└── auth-by-email/
|
||||
├── ui/
|
||||
│ └── login-form.tsx
|
||||
@@ -62,14 +62,14 @@ features/
|
||||
├── stores/
|
||||
│ └── auth.store.ts
|
||||
├── types/
|
||||
│ └── auth.interface.ts
|
||||
├── auth-by-email.feature.tsx
|
||||
│ └── auth.type.ts
|
||||
├── auth-by-email.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
**Плохо**
|
||||
```text
|
||||
features/
|
||||
business/
|
||||
└── authByEmail/
|
||||
├── LoginForm.tsx
|
||||
├── useAuth.ts
|
||||
@@ -4,7 +4,7 @@ title: Технологии и библиотеки
|
||||
|
||||
# Технологии и библиотеки
|
||||
|
||||
Этот раздел описывает базовый стек технологий и библиотек, принятый в проекте.
|
||||
Базовый стек проекта по областям: UI, архитектура, данные, состояние, локализация, тестирование, стили, генерация кода.
|
||||
|
||||
## Что используем
|
||||
|
||||
@@ -13,7 +13,7 @@ title: Технологии и библиотеки
|
||||
- `Next.js` — для продуктовых сайтов.
|
||||
|
||||
### Архитектура
|
||||
- `FSD (Feature-Sliced Design)` — структура проекта и границы модулей. Используется кастомизированная версия — подробнее в разделе [Архитектура](/basics/architecture).
|
||||
- `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/docs/basics/architecture/).
|
||||
|
||||
### UI компоненты
|
||||
- `Mantine UI` — базовые UI-компоненты.
|
||||
@@ -4,7 +4,7 @@ title: Типизация
|
||||
|
||||
# Типизация
|
||||
|
||||
Этот раздел описывает правила типизации: как типизировать компоненты, функции и работу с `any`/`unknown`.
|
||||
Правила типизации в TypeScript: общие принципы и работа с динамическими типами.
|
||||
|
||||
## Общие правила
|
||||
|
||||
78
docs/docs/index.md
Normal file
78
docs/docs/index.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# NextJS Style Guide
|
||||
|
||||
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
|
||||
|
||||
## Использование
|
||||
|
||||
**Для AI-агентов:**
|
||||
|
||||
- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
|
||||
- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом.
|
||||
|
||||
**Для проекта:**
|
||||
|
||||
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
||||
|
||||
## Структура документации
|
||||
|
||||
### Workflow
|
||||
|
||||
[Workflow](/docs/workflow) — пошаговая карта типовых задач: с чего начать, как добавить страницу/компонент, как подключить данные, стили, локализацию.
|
||||
|
||||
### Базовые правила
|
||||
|
||||
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Технологии и библиотеки | Какой стек используем? |
|
||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||
| Архитектура: Обзор | Что такое SLM и зачем она нужна? |
|
||||
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
|
||||
| Архитектура: Модули | Что такое модуль и как он устроен? |
|
||||
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
|
||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||
|
||||
### Установка и настройка
|
||||
|
||||
**Как поднять и сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Создание проекта: Из шаблона | Как начать проект из готового шаблона? |
|
||||
| Создание проекта: Вручную | Как поднять проект с нуля без шаблона? |
|
||||
| Next.js | Как настроить Next.js под проект? |
|
||||
| Алиасы | Как настроить путевые алиасы импортов? |
|
||||
| Biome | Как настроить Biome (линтер и форматер)? |
|
||||
| Стили | Как подключить и настроить стили проекта? |
|
||||
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
|
||||
| SVG-спрайты | Как настроить генерацию SVG-спрайтов? |
|
||||
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
|
||||
| VS Code | Как настроить редактор для проекта? |
|
||||
|
||||
### Использование
|
||||
|
||||
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Структура проекта | Как организованы папки и файлы по SLM? |
|
||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||
| Страницы (App Router) | Как описывать layout, page, loading, error, not-found? |
|
||||
| Данные: Введение | Как устроена работа с данными в проекте? |
|
||||
| Данные: REST: Клиенты: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
|
||||
| Данные: REST: Клиенты: Ручная генерация | Как написать REST-клиент вручную? |
|
||||
| Данные: REST: Получение данных: Серверные компоненты | Как получать данные в серверных компонентах? |
|
||||
| Данные: REST: Получение данных: Клиентские компоненты | Как получать данные в клиентских компонентах (SWR)? |
|
||||
| Данные: Realtime | Как работать с realtime-каналами и сокетами? |
|
||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||
| Изображения | _(не заполнен)_ |
|
||||
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
|
||||
| Видео | _(не заполнен)_ |
|
||||
| Stores | _(не заполнен)_ |
|
||||
| Хуки | _(не заполнен)_ |
|
||||
| Шрифты | _(не заполнен)_ |
|
||||
| Локализация | _(не заполнен)_ |
|
||||
78
docs/docs/setup/aliases.md
Normal file
78
docs/docs/setup/aliases.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Алиасы
|
||||
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared]
|
||||
---
|
||||
|
||||
# Алиасы
|
||||
|
||||
Импорты в проекте идут через алиасы слоёв SLM-архитектуры — по одному на каждый слой `src/`. Префикс `@/` **не используется**: имя слоя само по себе однозначно адресует код.
|
||||
|
||||
Слои и направление зависимостей — [Архитектура: слои](/docs/basics/architecture/reference/layers).
|
||||
|
||||
## Конфиг
|
||||
|
||||
`tsconfig.json` в корне проекта:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"app/*": ["./src/app/*"],
|
||||
"layouts/*": ["./src/layouts/*"],
|
||||
"screens/*": ["./src/screens/*"],
|
||||
"widgets/*": ["./src/widgets/*"],
|
||||
"business/*": ["./src/business/*"],
|
||||
"infrastructure/*": ["./src/infrastructure/*"],
|
||||
"ui/*": ["./src/ui/*"],
|
||||
"shared/*": ["./src/shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет.
|
||||
|
||||
## Правила
|
||||
|
||||
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
|
||||
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
|
||||
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
|
||||
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/reference/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'
|
||||
```
|
||||
80
docs/docs/setup/biome.md
Normal file
80
docs/docs/setup/biome.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Biome
|
||||
keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier]
|
||||
---
|
||||
|
||||
# Biome
|
||||
|
||||
Единый линтер и форматтер для JS/TS/JSON в проекте. Заменяет связку ESLint + Prettier одним инструментом.
|
||||
|
||||
## Требования
|
||||
|
||||
- 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` к стандартному виду — добавить override для `*.css` (см. «Стандартный `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`, кастомизируется ровно одним блоком — `overrides` для `*.css` с отключённым правилом `suspicious/noUnknownAtRules`. Этот override **обязателен по умолчанию во всех проектах**, независимо от того, подключены ли уже стили: проектный CSS-стек использует `@custom-media` и другие нестандартные at-правила, которые Biome не распознаёт; без override `npm run lint` падает.
|
||||
|
||||
Фрагмент, который добавляется в `biome.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.css"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Если в `biome.json` уже есть массив `overrides` — добавить элемент в него; не дублировать массив.
|
||||
|
||||
Прочая настройка правил Biome — отдельная задача, не входит в стандартный канон.
|
||||
|
||||
## Интеграция с VS Code
|
||||
|
||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/setup/vscode).
|
||||
111
docs/docs/setup/nextjs.md
Normal file
111
docs/docs/setup/nextjs.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Next.js
|
||||
keywords: [next.js, create-next-app, npx, установка, инициализация, фреймворк, скаффолдинг, app router, typescript]
|
||||
---
|
||||
|
||||
# Next.js
|
||||
|
||||
Установка фреймворка через `create-next-app` и очистка дефолтного шаблона. На выходе — чистый скелет с App Router и TypeScript.
|
||||
|
||||
## Требования
|
||||
|
||||
- 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/usage/project-structure)) |
|
||||
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/docs/setup/aliases)) |
|
||||
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/docs/setup/biome)) |
|
||||
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/docs/usage/styles)) |
|
||||
| `--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/usage/project-structure)).
|
||||
|
||||
```bash
|
||||
mkdir -p src/shared/styles
|
||||
```
|
||||
|
||||
Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах.
|
||||
|
||||
## Правила
|
||||
|
||||
- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы.
|
||||
- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает.
|
||||
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/docs/setup/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`).
|
||||
71
docs/docs/setup/postcss.md
Normal file
71
docs/docs/setup/postcss.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: PostCSS
|
||||
keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор]
|
||||
---
|
||||
|
||||
# PostCSS
|
||||
|
||||
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
||||
|
||||
Правила написания CSS в компонентах — [Использование](/docs/usage/styles).
|
||||
|
||||
## Зачем 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/usage/styles), раздел «Импорт стилей»).
|
||||
47
docs/docs/setup/project-from-template.md
Normal file
47
docs/docs/setup/project-from-template.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Из шаблона
|
||||
keywords: [создать проект из шаблона, шаблон, template, tiged, degit, клонировать шаблон, эталонный шаблон, быстрый старт, scaffold, новый проект]
|
||||
---
|
||||
|
||||
# Создание проекта из шаблона
|
||||
|
||||
Клонирование эталонного шаблона стайлгайда одной командой. Дефолтный сценарий создания проекта.
|
||||
|
||||
В шаблоне уже настроены Next.js, Biome, стили, PostCSS, SVG-спрайты, VS Code и шаблоны генерации. Если нужен контроль над каждым шагом или шаблон недоступен — [Создание проекта вручную](/docs/setup/project-manual).
|
||||
|
||||
## Требования
|
||||
|
||||
- Node.js 18.18+ (рекомендуется LTS 20+).
|
||||
- npm 10+.
|
||||
- Доступ к `gromlab.ru` (SSH-ключ добавлен).
|
||||
|
||||
## Установка
|
||||
|
||||
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/` — решение о репозитории принимает разработчик.
|
||||
91
docs/docs/setup/project-manual.md
Normal file
91
docs/docs/setup/project-manual.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Вручную
|
||||
keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка]
|
||||
---
|
||||
|
||||
# Создание проекта вручную
|
||||
|
||||
Сборка эталонного Next.js-проекта шаг за шагом — для случаев, когда нужен контроль над каждым шагом или шаблон недоступен.
|
||||
|
||||
Если шаблон доступен — быстрее использовать [Создание проекта из шаблона](/docs/setup/project-from-template).
|
||||
|
||||
## Состав эталонного проекта
|
||||
|
||||
| Компонент | Роль | Раздел |
|
||||
|-----------|------|--------|
|
||||
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/setup/nextjs) |
|
||||
| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/setup/aliases) |
|
||||
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/setup/biome) |
|
||||
| Стили | Глобальные токены и breakpoints | [Стили](/docs/setup/styles) |
|
||||
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/docs/setup/postcss) |
|
||||
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](/docs/setup/svg-sprites) |
|
||||
| VS Code | Настройки редактора и расширения | [VS Code](/docs/setup/vscode) |
|
||||
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/docs/setup/templates) |
|
||||
|
||||
Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном.
|
||||
|
||||
## Канон раскладки
|
||||
|
||||
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)).
|
||||
|
||||
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
|
||||
|
||||
## Порядок установки
|
||||
|
||||
Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами.
|
||||
|
||||
### 1. Next.js
|
||||
|
||||
Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
|
||||
|
||||
См. [Next.js](/docs/setup/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить.
|
||||
|
||||
### 2. Алиасы
|
||||
|
||||
Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов.
|
||||
|
||||
См. [Алиасы](/docs/setup/aliases).
|
||||
|
||||
### 3. Biome
|
||||
|
||||
Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки.
|
||||
|
||||
См. [Biome](/docs/setup/biome).
|
||||
|
||||
### 4. Стили (базовая инфраструктура)
|
||||
|
||||
Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится.
|
||||
|
||||
См. [Стили](/docs/setup/styles).
|
||||
|
||||
### 5. PostCSS
|
||||
|
||||
CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`.
|
||||
|
||||
См. [PostCSS](/docs/setup/postcss).
|
||||
|
||||
### 6. SVG-спрайты
|
||||
|
||||
Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента `<SvgSprite/>`.
|
||||
|
||||
См. [SVG-спрайты](/docs/setup/svg-sprites).
|
||||
|
||||
### 7. VS Code
|
||||
|
||||
Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`).
|
||||
|
||||
См. [VS Code](/docs/setup/vscode).
|
||||
|
||||
### 8. Шаблоны генерации
|
||||
|
||||
Папка `.templates/` для генератора модулей `@gromlab/create`.
|
||||
|
||||
См. [Шаблоны генерации](/docs/setup/templates).
|
||||
|
||||
## Правила
|
||||
|
||||
- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome).
|
||||
- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден.
|
||||
- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
|
||||
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
|
||||
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.
|
||||
178
docs/docs/setup/styles.md
Normal file
178
docs/docs/setup/styles.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
title: Стили
|
||||
keywords: [variables.css, media.css, global.css, shared/styles, токены, переменные, custom-media, breakpoints, подключение стилей, базовые стили, инициализация]
|
||||
---
|
||||
|
||||
# Стили
|
||||
|
||||
Базовая стилевая инфраструктура: токены, breakpoints и точка сборки глобальных стилей в `src/shared/styles/`.
|
||||
|
||||
CSS-процессор — отдельный шаг ([PostCSS](/docs/setup/postcss)). Правила написания CSS в компонентах — [Стили: использование](/docs/usage/styles).
|
||||
|
||||
## Требования
|
||||
|
||||
- Структура `src/` соответствует SLM ([Структура проекта](/docs/usage/project-structure)). Глобальные стили живут в `src/shared/styles/`, не в `src/app/`.
|
||||
- В проекте нет `globals.css` с кастомным содержимым и не установлен `tailwindcss`.
|
||||
|
||||
## Файлы
|
||||
|
||||
Состав глобальных стилей — три файла:
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `variables.css` | Токены проекта (цвета, отступы, радиусы) |
|
||||
| `media.css` | Custom media queries (брейкпоинты по ширине и высоте) |
|
||||
| `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз |
|
||||
|
||||
Правила подключения:
|
||||
|
||||
- В приложение импортируется **только** `global.css`.
|
||||
- `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`.
|
||||
- `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](/docs/setup/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/setup/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность.
|
||||
- [Стили: использование](/docs/usage/styles) — правила написания CSS в компонентах.
|
||||
- [SVG-спрайты](/docs/setup/svg-sprites) — стили иконок отдельно от глобальных.
|
||||
108
docs/docs/setup/svg-sprites.md
Normal file
108
docs/docs/setup/svg-sprites.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Установка и настройка
|
||||
keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts]
|
||||
---
|
||||
|
||||
# Установка и настройка
|
||||
|
||||
Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета.
|
||||
|
||||
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/usage/svg-sprites).
|
||||
|
||||
## Требования
|
||||
|
||||
- Node.js 18+
|
||||
- React 18+
|
||||
|
||||
## Установка
|
||||
|
||||
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/usage/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
|
||||
```
|
||||
|
||||
## Стандартный конфиг
|
||||
|
||||
Файл `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`.
|
||||
106
docs/docs/setup/templates.md
Normal file
106
docs/docs/setup/templates.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Шаблоны генерации
|
||||
keywords: [шаблоны, templates, .templates, tiged, generator, генератор шаблонов, добавить шаблон, скачать шаблоны, scaffold]
|
||||
---
|
||||
|
||||
# Шаблоны генерации
|
||||
|
||||
Папка `.templates/` в корне проекта для генератора модулей `@gromlab/create`.
|
||||
|
||||
Синтаксис шаблонов и работа генератора — [Шаблоны и генерация кода](/docs/usage/templates-generation).
|
||||
|
||||
## Требования
|
||||
|
||||
- Доступен `npx` (ставится вместе с npm).
|
||||
- Доступ к `gromlab.ru` (SSH-ключ добавлен) — для скачивания эталонного набора.
|
||||
|
||||
## Развилка
|
||||
|
||||
Сценарий зависит от состояния проекта:
|
||||
|
||||
| Состояние | Что делать |
|
||||
|-----------|------------|
|
||||
| В корне проекта **нет** `.templates/` | [Скачать стандартный набор](#скачать-стандартный-набор) |
|
||||
| `.templates/` **есть**, нужен новый специфический шаблон | [Создать шаблон вручную](#создать-шаблон-вручную) |
|
||||
| `.templates/` **есть**, нужно обновить до эталона | Скачать заново с подтверждением перезаписи |
|
||||
|
||||
## Скачать стандартный набор
|
||||
|
||||
### Установка
|
||||
|
||||
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-widget src/ui
|
||||
```
|
||||
|
||||
После проверки — удалить тестовый модуль.
|
||||
|
||||
### Правила
|
||||
|
||||
- **Скачанные файлы не править.** Источник истины — репозиторий `templates/nextjs-template`. Изменения стандартных шаблонов делаются в нём, не в отдельных проектах.
|
||||
- **Расположение — только `.templates/` в корне проекта.** Это требование расширения VS Code и CLI ([Шаблоны и генерация кода](/docs/usage/templates-generation)).
|
||||
- **Если доступа к `gromlab.ru` нет** — не восстанавливать содержимое из памяти: разойдётся с каноном. Запросить шаблоны иным путём.
|
||||
|
||||
## Создать шаблон вручную
|
||||
|
||||
Если стандартного набора недостаточно и нужен специфический шаблон под проект.
|
||||
|
||||
### Установка
|
||||
|
||||
1. Прочитать [Шаблоны и генерация кода](/docs/usage/templates-generation) — секции «Структура шаблонов», «Синтаксис шаблонов», «Как создать новый шаблон».
|
||||
|
||||
2. Определить:
|
||||
- имя шаблона (папка внутри `.templates/`);
|
||||
- состав файлов;
|
||||
- слой SLM предполагаемых потребителей ([Архитектура: слои](/docs/basics/architecture/reference/layers));
|
||||
- минимальное содержимое каждого файла.
|
||||
|
||||
3. Проверить, что имя шаблона не конфликтует с существующей папкой в `.templates/`.
|
||||
|
||||
4. Создать структуру `.templates/{name}/` по канону из [Шаблоны и генерация кода](/docs/usage/templates-generation) — синтаксис переменных, правила именования файлов и содержимого.
|
||||
|
||||
5. Проверить генерацию:
|
||||
|
||||
```bash
|
||||
npx @gromlab/create {name} test-entity {целевой путь}
|
||||
```
|
||||
|
||||
После проверки — удалить тестовый артефакт.
|
||||
|
||||
### Правила
|
||||
|
||||
- **Если запрос покрыт стандартными шаблонами** (`component`, `widget`, `store`, `layout`, `screen`, `business`) — не создавать копию.
|
||||
- **Стандартные шаблоны не трогать.** Правка стандарта — задача репозитория `templates/nextjs-template`, не отдельного проекта.
|
||||
- **Синтаксис переменных, правила регистра, минимальный boilerplate** — в [Шаблоны и генерация кода](/docs/usage/templates-generation). Здесь не дублируется.
|
||||
|
||||
## Общие правила
|
||||
|
||||
- VS Code-расширение `gromlab.vscode-templateFileGenerator` устанавливается разово на машину разработчика, а не через этот раздел ([Шаблоны и генерация кода](/docs/usage/templates-generation)).
|
||||
- CLI `@gromlab/create` вызывается через `npx`, в `package.json` отдельно не добавляется.
|
||||
- Менеджер пакетов (npm/pnpm/yarn/bun) не влияет: `tiged` и `@gromlab/create` запускаются через `npx`.
|
||||
|
||||
## Проверка установки
|
||||
|
||||
- В корне проекта есть папка `.templates/`.
|
||||
- Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор).
|
||||
- В корне проекта нет каталогов `.git/` и `.github/` из репозитория-шаблона.
|
||||
- Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок.
|
||||
|
||||
## Дальше
|
||||
|
||||
- [Шаблоны и генерация кода](/docs/usage/templates-generation) — синтаксис, использование, создание новых шаблонов.
|
||||
@@ -6,7 +6,7 @@ title: Компоненты
|
||||
|
||||
Правила написания React-компонентов: файловая структура модуля, типизация пропсов, документирование и реализация. Раздел охватывает компоненты всех слоёв — от `shared/ui` до `screens`.
|
||||
|
||||
Архитектурные слои и их назначение описаны в разделе [Архитектура](/basics/architecture).
|
||||
Архитектурные слои и их назначение описаны в разделе [Архитектура](/docs/basics/architecture/).
|
||||
|
||||
|
||||
## Правила организации
|
||||
@@ -43,7 +43,7 @@ container/
|
||||
- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно.
|
||||
- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра.
|
||||
- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных.
|
||||
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing).
|
||||
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/docs/basics/typing).
|
||||
|
||||
## Реализация
|
||||
|
||||
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: Структура проекта
|
||||
|
||||
# Структура проекта
|
||||
|
||||
Раздел описывает расположение файлов и папок в проекте Next.js (App Router).
|
||||
Файловая организация Next.js-проекта по архитектуре SLM.
|
||||
|
||||
## Корень репозитория
|
||||
|
||||
@@ -41,19 +41,20 @@ public/
|
||||
```text
|
||||
src/
|
||||
├── app/ # Роутинг Next.js, провайдеры, глобальные стили
|
||||
├── screens/ # Собраные страницы (UI)
|
||||
├── layouts/ # Шаблоны
|
||||
├── widgets/ # Крупные самостоятельные блоки интерфейса
|
||||
├── features/ # Пользовательские сценарии
|
||||
├── entities/ # Бизнес-сущности
|
||||
└── shared/ # Переиспользуемый код (UI, утилиты, типы и др.)
|
||||
├── layouts/ # Каркасы страниц (header, footer, sidebar)
|
||||
├── screens/ # Контент конкретной страницы
|
||||
├── widgets/ # Составные блоки интерфейса, не привязанные к домену
|
||||
├── business/ # Бизнес-домены (auth, catalog, orders)
|
||||
├── infrastructure/ # Техсервисы (theme, i18n, API-адаптеры)
|
||||
├── ui/ # UI-кит без бизнес-логики
|
||||
└── shared/ # Общие ресурсы (утилиты, типы, стили)
|
||||
```
|
||||
|
||||
Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture).
|
||||
Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture/).
|
||||
|
||||
### Папка `app/`
|
||||
|
||||
Совмещает два слоя: инициализацию приложения по FSD (провайдеры, глобальные стили) и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
|
||||
Точка входа приложения: инициализация (провайдеры, глобальные стили) и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
|
||||
|
||||
```text
|
||||
src/app/
|
||||
@@ -73,8 +74,7 @@ src/app/
|
||||
├── screen/ # Шаблон экрана
|
||||
├── layout/ # Шаблон layout
|
||||
├── widget/ # Шаблон виджета
|
||||
├── feature/ # Шаблон фичи
|
||||
├── entity/ # Шаблон сущности
|
||||
├── module/ # Шаблон бизнес-модуля
|
||||
└── store/ # Шаблон стора
|
||||
```
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: Стили
|
||||
title: Использование
|
||||
---
|
||||
|
||||
# Стили
|
||||
# Использование
|
||||
|
||||
Раздел описывает правила написания CSS: PostCSS Modules, вложенность, медиа-запросы, переменные, форматирование.
|
||||
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/setup/postcss).
|
||||
|
||||
## Общие правила
|
||||
|
||||
55
docs/docs/usage/svg-sprites.md
Normal file
55
docs/docs/usage/svg-sprites.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Использование
|
||||
keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color]
|
||||
---
|
||||
|
||||
# Использование
|
||||
|
||||
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/setup/svg-sprites).
|
||||
|
||||
## Шаги
|
||||
|
||||
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);
|
||||
}
|
||||
```
|
||||
@@ -146,11 +146,10 @@ npx @gromlab/create <шаблон> <имя> <путь>
|
||||
| Команда | Что создаёт |
|
||||
|---|---|
|
||||
| `npx @gromlab/create component button src/shared/ui` | Компонент |
|
||||
| `npx @gromlab/create feature auth src/features` | Фичу |
|
||||
| `npx @gromlab/create module auth src/business` | Бизнес-модуль |
|
||||
| `npx @gromlab/create widget header src/widgets` | Виджет |
|
||||
| `npx @gromlab/create entity user src/entities` | Сущность |
|
||||
| `npx @gromlab/create layout admin src/layouts` | Layout |
|
||||
| `npx @gromlab/create store auth src/shared/model` | Стор |
|
||||
| `npx @gromlab/create store auth src/business/auth/stores` | Стор |
|
||||
|
||||
:::
|
||||
|
||||
@@ -16,7 +16,7 @@ title: Workflow
|
||||
cd my-app
|
||||
npm install
|
||||
```
|
||||
2. Проект готов к разработке — стек, структура FSD, конфигурация
|
||||
2. Проект готов к разработке — стек, структура SLM, конфигурация
|
||||
редактора и шаблоны генерации уже настроены.
|
||||
|
||||
## Генерация кода
|
||||
@@ -27,13 +27,12 @@ title: Workflow
|
||||
|
||||
| Модуль | Слой | Шаблон |
|
||||
|------------|--------------|-------------|
|
||||
| Компонент | `shared/ui/` | `component` |
|
||||
| Фича | `features/` | `feature` |
|
||||
| Компонент | `ui/` | `component` |
|
||||
| Бизнес-модуль | `business/` | `module` |
|
||||
| Виджет | `widgets/` | `widget` |
|
||||
| Сущность | `entities/` | `entity` |
|
||||
| Layout | `layouts/` | `layout` |
|
||||
| Экран | `screens/` | `screen` |
|
||||
| Стор | `model/` | `store` |
|
||||
| Стор | `stores/` | `store` |
|
||||
|
||||
2. Сгенерировать модуль из шаблона.
|
||||
3. Если подходящего шаблона нет — сначала создать шаблон, затем использовать.
|
||||
@@ -53,7 +52,7 @@ title: Workflow
|
||||
|
||||
## Добавление UI-модуля
|
||||
|
||||
Создание компонента, фичи, виджета, сущности или layout.
|
||||
Создание компонента, бизнес-модуля, виджета или layout.
|
||||
|
||||
1. Сгенерировать модуль из соответствующего шаблона в целевой слой.
|
||||
2. Заполнить модуль логикой и стилями.
|
||||
@@ -10,13 +10,12 @@ title: Генерация кода
|
||||
|
||||
| Модуль | Слой | Шаблон |
|
||||
|---|---|---|
|
||||
| Компонент | `shared/ui/` | `component` |
|
||||
| Фича | `features/` | `feature` |
|
||||
| Компонент | `ui/` | `component` |
|
||||
| Бизнес-модуль | `business/` | `module` |
|
||||
| Виджет | `widgets/` | `widget` |
|
||||
| Сущность | `entities/` | `entity` |
|
||||
| Layout | `layouts/` | `layout` |
|
||||
| Экран | `screens/` | `screen` |
|
||||
| Стор | `model/` | `store` |
|
||||
| Стор | `stores/` | `store` |
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
@@ -29,4 +28,4 @@ title: Генерация кода
|
||||
- Повторяющаяся структура появляется больше одного раза.
|
||||
- Существующий шаблон не покрывает нужный тип модуля.
|
||||
|
||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/applied/templates-generation).
|
||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/usage/templates-generation).
|
||||
@@ -8,7 +8,7 @@ title: Создание проекта
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
Новый проект создаётся из готового шаблона. Шаблон содержит настроенный стек, структуру FSD, конфигурацию редактора и шаблоны генерации кода — проект готов к разработке сразу после установки зависимостей.
|
||||
Новый проект создаётся из готового шаблона. Шаблон содержит настроенный стек, структуру SLM, конфигурацию редактора и шаблоны генерации кода — проект готов к разработке сразу после установки зависимостей.
|
||||
|
||||
### Создание из шаблона
|
||||
|
||||
@@ -24,7 +24,7 @@ npm install
|
||||
- Mantine UI + PostCSS Modules
|
||||
- Biome (линтинг и форматирование)
|
||||
- Zustand, SWR
|
||||
- Структура FSD (`screens/`, `widgets/`, `features/`, `entities/`, `shared/`)
|
||||
- Структура SLM (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`)
|
||||
- Шаблоны генерации (`.templates/`)
|
||||
- Конфигурация VS Code (`.vscode/`)
|
||||
- CSS-токены (цвета, отступы, радиусы, медиа)
|
||||
@@ -4,7 +4,7 @@ title: Добавление UI-модуля
|
||||
|
||||
# Добавление UI-модуля
|
||||
|
||||
Как создать компонент, фичу, виджет, сущность или layout в проекте.
|
||||
Как создать компонент, бизнес-модуль, виджет или layout в проекте.
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
@@ -12,11 +12,11 @@ title: Добавление UI-модуля
|
||||
|
||||
## Порядок действий
|
||||
|
||||
1. [Сгенерировать](/applied/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
||||
1. [Сгенерировать](/docs/usage/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
||||
2. Заполнить модуль логикой и стилями.
|
||||
|
||||
## Дочерние компоненты
|
||||
|
||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/usage/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||
|
||||
Правила написания компонентов — [Компоненты](/applied/components).
|
||||
Правила написания компонентов — [Компоненты](/docs/usage/components).
|
||||
@@ -12,7 +12,7 @@ title: Добавление страницы
|
||||
|
||||
## Порядок действий
|
||||
|
||||
1. [Сгенерировать](/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
||||
1. [Сгенерировать](/docs/usage/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
||||
|
||||
2. Заполнить экран логикой и стилями.
|
||||
|
||||
@@ -20,8 +20,8 @@ title: Добавление страницы
|
||||
|
||||
## Правила
|
||||
|
||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/applied/templates-generation) из шаблона.
|
||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/usage/templates-generation) из шаблона.
|
||||
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
||||
- Каждая страница содержит `metadata` с `title` и `description`.
|
||||
|
||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/applied/page-level).
|
||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/usage/page-level).
|
||||
@@ -8,15 +8,15 @@ title: Начало работы
|
||||
|
||||
## Стек проекта
|
||||
|
||||
**Next.js** (App Router), **Mantine**, **Zustand**, **FSD**.
|
||||
**Next.js** (App Router), **Mantine**, **Zustand**, **SLM Design**.
|
||||
|
||||
Подробнее — [Технологии и библиотеки](/basics/tech-stack).
|
||||
Подробнее — [Технологии и библиотеки](/docs/basics/tech-stack).
|
||||
|
||||
## Ключевые особенности
|
||||
|
||||
- **Генерация вместо ручного создания** — компоненты, фичи, виджеты, сторы и другие модули не создаются вручную. Файловая структура генерируется из шаблонов `.templates/`. Ручное создание файловой структуры модулей запрещено.
|
||||
- **Генерация вместо ручного создания** — компоненты, бизнес-модули, виджеты, сторы и другие модули не создаются вручную. Файловая структура генерируется из шаблонов `.templates/`. Ручное создание файловой структуры модулей запрещено.
|
||||
- **Biome вместо ESLint + Prettier** — один инструмент для линтинга и форматирования. Автофикс и сортировка импортов происходят автоматически при сохранении файла.
|
||||
|
||||
## Настройка окружения
|
||||
|
||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/applied/vscode).
|
||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/setup/vscode).
|
||||
@@ -20,4 +20,4 @@ title: Стилизация
|
||||
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
|
||||
- **Глобальные стили** вне `app/styles/` запрещены.
|
||||
|
||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили](/applied/styles).
|
||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/usage/styles).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: API
|
||||
---
|
||||
|
||||
# API
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Components
|
||||
---
|
||||
|
||||
# Components
|
||||
|
||||
Rules for creating UI components across all FSD layers.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Fonts
|
||||
---
|
||||
|
||||
# Fonts
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Hooks
|
||||
---
|
||||
|
||||
# Hooks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Images
|
||||
---
|
||||
|
||||
# Images
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Localization
|
||||
---
|
||||
|
||||
# Localization
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Page-level Components
|
||||
---
|
||||
|
||||
# Page-level Components
|
||||
|
||||
Next.js App Router special files used by the framework by convention: `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Project Structure
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
Base project structure and principles of module organization at folder and file level.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Stores
|
||||
---
|
||||
|
||||
# Stores
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Styles
|
||||
---
|
||||
|
||||
# Styles
|
||||
|
||||
CSS writing rules: PostCSS Modules, nesting, media queries, variables, formatting.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: SVG Sprites
|
||||
---
|
||||
|
||||
# SVG Sprites
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Templates & Code Generation
|
||||
---
|
||||
|
||||
# Templates & Code Generation
|
||||
|
||||
Template tools, syntax, and examples for code generation.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Video
|
||||
---
|
||||
|
||||
# Video
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
Architecture based on FSD (Feature-Sliced Design) and strict module boundaries.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Code Style
|
||||
---
|
||||
|
||||
# Code Style
|
||||
|
||||
Unified code formatting rules: indentation, line breaks, quotes, import order, and readability.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Documentation
|
||||
---
|
||||
|
||||
# Documentation
|
||||
|
||||
Documentation should help understand the purpose of an entity, not duplicate its types or obvious details.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Naming
|
||||
---
|
||||
|
||||
# Naming
|
||||
|
||||
Naming should be predictable, concise, and reflect the meaning of the entity.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Tech Stack
|
||||
---
|
||||
|
||||
# Tech Stack
|
||||
|
||||
Base technology stack and libraries used in projects.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Typing
|
||||
---
|
||||
|
||||
# Typing
|
||||
|
||||
Typing is required for all public interfaces, functions, and components.
|
||||
@@ -1,57 +0,0 @@
|
||||
# NextJS Style Guide
|
||||
|
||||
Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
### Processes
|
||||
|
||||
**What to do** in a specific situation — step-by-step instructions.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Getting Started | What tools to install before starting development? |
|
||||
| Creating an App | How to create a new project, where to get a template? |
|
||||
| Creating Pages | How to add a page: routing and screen? |
|
||||
| Creating Components | How to generate components using templates? |
|
||||
| Styling | What to use: Mantine, tokens, or PostCSS? |
|
||||
| Data Fetching | How to fetch data: SWR, codegen, sockets? |
|
||||
| State Management | When and how to create a store (Zustand)? |
|
||||
| Localization | How to add translations and work with i18next? |
|
||||
|
||||
### Basic Rules
|
||||
|
||||
**What the code should look like** — standards not tied to a specific technology.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Tech Stack | What stack do we use? |
|
||||
| Architecture | How are FSD layers, dependencies, and public API structured? |
|
||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||
| Naming | How to name files, variables, components, hooks? |
|
||||
| Documentation | How to write JSDoc: what to document and what not? |
|
||||
| Typing | How to type: type vs interface, any/unknown? |
|
||||
|
||||
### Applied Sections
|
||||
|
||||
**How a specific area works** — rules, structure, and code examples for specific technologies and tools.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Project Structure | How are folders and files organized by FSD? |
|
||||
| Components | How is a component structured: files, props, clsx? |
|
||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||
| Images | _(not filled)_ |
|
||||
| SVG Sprites | _(not filled)_ |
|
||||
| Video | _(not filled)_ |
|
||||
| API | _(not filled)_ |
|
||||
| Stores | _(not filled)_ |
|
||||
| Hooks | _(not filled)_ |
|
||||
| Fonts | _(not filled)_ |
|
||||
| Localization | _(not filled)_ |
|
||||
|
||||
## For Assistants
|
||||
|
||||
Full documentation in a single MD file: https://gromlab.ru/docs/frontend-style-guide/raw/branch/main/generated/en/RULES.md
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Creating an App
|
||||
---
|
||||
|
||||
# Creating an App
|
||||
|
||||
How to create a new application: choosing a project template and initialization.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Creating Components
|
||||
---
|
||||
|
||||
# Creating Components
|
||||
|
||||
Generating components using templates, working with child components.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Creating Pages
|
||||
---
|
||||
|
||||
# Creating Pages
|
||||
|
||||
Page creation pattern: routing (page.tsx) and screen.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Data Fetching
|
||||
---
|
||||
|
||||
# Data Fetching
|
||||
|
||||
How to fetch data: SWR, API client codegen, sockets.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Getting Started
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
Setting up the environment and installing tools before starting development.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Localization
|
||||
---
|
||||
|
||||
# Localization
|
||||
|
||||
How to add translations and work with i18next.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: State Management
|
||||
---
|
||||
|
||||
# State Management
|
||||
|
||||
When and how to create a store (Zustand), what to store locally vs globally.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Styling
|
||||
---
|
||||
|
||||
# Styling
|
||||
|
||||
Styling tools priority and rules for their application.
|
||||
371
docs/index.md
Normal file
371
docs/index.md
Normal file
@@ -0,0 +1,371 @@
|
||||
---
|
||||
layout: false
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const THEME_KEY = 'vitepress-theme-appearance'
|
||||
|
||||
// __BUILD_VERSION__ подставляется Vite-define из ENV `BUILD_VERSION`
|
||||
// (см. .vitepress/config.ts). В dev и build всегда определена.
|
||||
const buildVersion = __BUILD_VERSION__
|
||||
|
||||
const theme = ref('auto')
|
||||
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem(THEME_KEY)
|
||||
theme.value = savedTheme === 'dark' || savedTheme === 'light' ? savedTheme : 'auto'
|
||||
})
|
||||
|
||||
function setTheme(value) {
|
||||
theme.value = value
|
||||
if (value === 'auto') {
|
||||
localStorage.removeItem(THEME_KEY)
|
||||
} else {
|
||||
localStorage.setItem(THEME_KEY, value)
|
||||
}
|
||||
const isDark = value === 'dark' || (value === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
document.documentElement.classList.toggle('dark', isDark)
|
||||
}
|
||||
|
||||
/**
|
||||
* Клик по кнопке темы:
|
||||
* - по активной → переключение в auto;
|
||||
* - по неактивной → выбор этого варианта.
|
||||
*/
|
||||
function toggleTheme(value) {
|
||||
setTheme(theme.value === value ? 'auto' : value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="landing">
|
||||
<section class="landing__hero">
|
||||
<h1 class="landing__title">NextJS Style Guide</h1>
|
||||
<ClientOnly>
|
||||
<p class="landing__tagline">Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.</p>
|
||||
<div class="landing__controls">
|
||||
<a
|
||||
class="landing__repo"
|
||||
href="https://gromlab.ru/docs/nextjs-style-guide"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.08 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3Z"/>
|
||||
</svg>
|
||||
<span>Репозиторий</span>
|
||||
</a>
|
||||
<div class="seg seg--icons" role="group" aria-label="Тема">
|
||||
<button
|
||||
type="button"
|
||||
class="seg__btn"
|
||||
:class="{ 'seg__btn--active': theme === 'light' }"
|
||||
:aria-pressed="theme === 'light'"
|
||||
title="Светлая"
|
||||
@click="toggleTheme('light')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="seg__btn"
|
||||
:class="{ 'seg__btn--active': theme === 'dark' }"
|
||||
:aria-pressed="theme === 'dark'"
|
||||
title="Тёмная"
|
||||
@click="toggleTheme('dark')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</section>
|
||||
|
||||
<section class="landing__cards">
|
||||
<a class="landing__card" href="/docs/">
|
||||
<h3>Документация</h3>
|
||||
<p>Все разделы: процессы разработки, базовые правила, прикладные руководства.</p>
|
||||
<span class="landing__cta">Открыть →</span>
|
||||
</a>
|
||||
<div class="landing__card landing__card--multi">
|
||||
<h3>Ассистенту</h3>
|
||||
<p>
|
||||
Карта документации для AI-агентов:
|
||||
<code>/llms.txt</code>, <code>/llms-full.txt</code>.
|
||||
</p>
|
||||
<div class="landing__buttons">
|
||||
<a class="landing__button" href="/llms.txt">llms.txt</a>
|
||||
<a class="landing__button" href="/llms-full.txt">llms-full.txt</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="landing__card" href="/nextjs-style-guide.zip">
|
||||
<h3>Скачать правила</h3>
|
||||
<p>Архив всех Markdown-файлов одним ZIP.</p>
|
||||
<span class="landing__cta">Скачать →</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<p class="landing__version">v{{ buildVersion }}</p>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
.landing {
|
||||
min-height: 100vh;
|
||||
padding: 48px 32px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 64px;
|
||||
}
|
||||
|
||||
.landing__controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.landing__controls > * {
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.landing__repo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 999px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.landing__repo:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.landing__repo svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 999px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.seg__btn {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, background-color 0.15s;
|
||||
}
|
||||
|
||||
.seg__btn:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.seg__btn--active {
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.seg--icons .seg__btn {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.seg__btn svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.landing__hero {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.landing__title {
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin: 0 0 16px;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.landing__tagline {
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 0 auto;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.landing__cards {
|
||||
max-width: 1100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.landing__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.landing__card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.landing__card h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.landing__card p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.landing__cta {
|
||||
margin-top: auto;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.landing__buttons {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.landing__button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: var(--vp-font-family-mono, monospace);
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.landing__button:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.landing__version {
|
||||
text-align: center;
|
||||
margin: 24px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-family: var(--vp-font-family-mono, monospace);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.landing {
|
||||
padding: 48px 20px 56px;
|
||||
gap: 40px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.landing__title {
|
||||
font-size: 36px;
|
||||
}
|
||||
.landing__tagline {
|
||||
font-size: 16px;
|
||||
}
|
||||
.landing__cards {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.landing__controls {
|
||||
gap: 8px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.landing {
|
||||
padding: 44px 16px 48px;
|
||||
gap: 36px;
|
||||
}
|
||||
.landing__title {
|
||||
font-size: 30px;
|
||||
}
|
||||
.landing__tagline {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.landing__controls {
|
||||
margin-top: 32px;
|
||||
}
|
||||
/* Репозиторий — только иконка, без текста, чтобы все контролы влезли в строку */
|
||||
.landing__repo {
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.landing__repo span {
|
||||
display: none;
|
||||
}
|
||||
.seg__btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.landing__card {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: SVG-спрайты
|
||||
---
|
||||
|
||||
# SVG-спрайты
|
||||
@@ -1,471 +0,0 @@
|
||||
---
|
||||
title: Архитектура
|
||||
---
|
||||
|
||||
# Архитектура
|
||||
|
||||
Раздел описывает архитектуру проекта: из каких слоёв состоит приложение,
|
||||
как организован код внутри слоёв и какие правила управляют зависимостями.
|
||||
|
||||
## Что нужно знать
|
||||
|
||||
SLM Design (Scoped Layered Module Design) — архитектурный подход
|
||||
к проектированию фронтенд-приложений, предложенный Громовым Сергеем в 2026 г.
|
||||
|
||||
Вырос на основе:
|
||||
|
||||
- [Feature-Sliced Design](https://feature-sliced.design) — слои и направление зависимостей
|
||||
- Screaming Architecture — структура говорит сама за себя
|
||||
- Colocation Principle — код рядом с местом использования
|
||||
|
||||
Переосмыслив эти подходы, SLM Design отличается от FSD в трёх аспектах:
|
||||
где живёт код (колокация), как он организован (модули)
|
||||
и как масштабируется (подъём при переиспользовании).
|
||||
|
||||
## Терминология
|
||||
|
||||
Архитектура оперирует четырьмя ключевыми понятиями:
|
||||
|
||||
- **Слой** — содержит модули
|
||||
- **Модуль** — содержит сегменты
|
||||
- **Компонент** — содержит сегменты
|
||||
- **Сегмент** — папка внутри модуля или компонента, группирующая код по назначению: UI-элементы (`ui/`), хуки (`hooks/`), типы (`types/`), стили (`styles/`) и другие
|
||||
|
||||
Модуль и компонент устроены одинаково — оба имеют сегменты. Разница в том, где они живут и обязателен ли UI.
|
||||
|
||||
```text
|
||||
Слой
|
||||
└── Модуль
|
||||
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
|
||||
└── ui/
|
||||
└── Компонент
|
||||
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
|
||||
└── ui/
|
||||
└── Компонент → ...
|
||||
```
|
||||
|
||||
### Слой
|
||||
|
||||
Архитектурный уровень. Содержит только модули. Определяет назначение кода и правила зависимостей.
|
||||
|
||||
### Модуль
|
||||
|
||||
Единица первого уровня слоя, объединённая по смыслу. Может содержать компонент, логику, типы, стили — или любую комбинацию. Имеет публичный API (`index.ts`) и внутреннюю структуру из сегментов.
|
||||
|
||||
Модуль — не обязательно UI. Feature `analytics` может быть только стором и сервисом. Entity `session` может быть только типами и хуком.
|
||||
|
||||
Модуль не может содержать вложенных модулей. Вложенные единицы с UI размещаются в сегменте `ui/` как компоненты.
|
||||
|
||||
### Компонент
|
||||
|
||||
Вложенная единица внутри сегмента `ui/` модуля (или другого компонента). Публичный `.tsx` файл обязателен. Именуется без суффикса слоя.
|
||||
|
||||
Компонент может иметь собственные сегменты (`hooks/`, `styles/`, `types/` и т.д.), `index.ts` и свой `ui/` с ещё более вложенными компонентами.
|
||||
|
||||
Отличия от модуля:
|
||||
|
||||
| | Модуль | Компонент |
|
||||
|--|--------|-----------|
|
||||
| Где живёт | В корне слоя | В `ui/` модуля или другого компонента |
|
||||
| Публичный `.tsx` | С суффиксом слоя, опционален | Без суффикса, обязателен |
|
||||
| Может не иметь UI | Да | Нет |
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
features/auth-by-email/ # модуль
|
||||
├── auth-by-email.feature.tsx # публичный .tsx модуля (с суффиксом, опционален)
|
||||
├── ui/ # сегмент: компоненты
|
||||
│ ├── login-form/ # компонент
|
||||
│ │ ├── login-form.tsx # публичный .tsx компонента (без суффикса, обязателен)
|
||||
│ │ ├── ui/ # вложенные компоненты
|
||||
│ │ │ └── password-field/
|
||||
│ │ │ └── password-field.tsx
|
||||
│ │ ├── hooks/
|
||||
│ │ │ └── use-validation.hook.ts
|
||||
│ │ ├── styles/
|
||||
│ │ │ └── login-form.module.css
|
||||
│ │ └── index.ts
|
||||
│ └── reset-password/ # компонент
|
||||
│ ├── reset-password.tsx
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
│ └── use-auth.hook.ts
|
||||
├── stores/
|
||||
│ └── auth.store.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Сегмент
|
||||
|
||||
Техническая папка внутри модуля или компонента, группирующая код по назначению. Набор не фиксирован — включаются только те сегменты, которые нужны.
|
||||
|
||||
| Сегмент | Назначение |
|
||||
|---------|-----------|
|
||||
| `ui/` | Вложенные компоненты |
|
||||
| `hooks/` | React-хуки |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `types/` | Интерфейсы, типы, enums, DTO |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты |
|
||||
| `services/` | Внешние источники данных |
|
||||
| `helpers/` | Вспомогательные функции |
|
||||
| `config/` | Константы, конфигурация |
|
||||
|
||||
## Ключевой принцип
|
||||
|
||||
> Модуль живёт на самом низком уровне, где он используется.
|
||||
> Поднимается выше только при переиспользовании в 2+ местах.
|
||||
|
||||
Если модуль используется только в одном месте — он остаётся на текущем уровне.
|
||||
Как только он начинает использоваться в 2+ местах — выносится на уровень выше.
|
||||
В крайнем случае — в `shared/`, где он доступен всем.
|
||||
|
||||
## Слои
|
||||
|
||||
Каждый нижний слой не знает о существовании верхних. Импорты идут строго сверху вниз.
|
||||
|
||||
```
|
||||
app → layouts → screens → widgets → features → entities → shared
|
||||
```
|
||||
|
||||
| Слой | Что лежит | Импортирует |
|
||||
|------|-----------|-------------|
|
||||
| **App** | Роутинг, провайдеры, глобальные стили. Композиция layout + screen для маршрута. | Все слои ниже |
|
||||
| **Layouts** | Каркас страницы, общий для группы маршрутов. | widgets, features, entities, shared |
|
||||
| **Screens** | Контент конкретной страницы. | widgets, features, entities, shared |
|
||||
| **Widgets** | Составные блоки с данными/логикой, переиспользуемые в 2+ местах. | features, entities, shared |
|
||||
| **Features** | Пользовательское действие или интерактивный сценарий. | entities, shared |
|
||||
| **Entities** | Бизнес-сущность с отображением и типами. | shared |
|
||||
| **Shared** | Переиспользуемые компоненты, утилиты, стили без бизнес-логики. | ничего |
|
||||
|
||||
Принципы:
|
||||
|
||||
- Импорты строго сверху вниз
|
||||
- Модули одного слоя не знают друг о друге
|
||||
- Layout получает контекстно-зависимые блоки через пропсы от app, а не импортирует их сам
|
||||
- `entities/` и `features/` создаются осознанно — это не результат «вынесения» компонента из screen
|
||||
|
||||
### App
|
||||
|
||||
Точка входа приложения: роутинг (Next.js App Router), провайдеры, глобальные стили.
|
||||
Находится на самом высоком уровне абстракции — может импортировать любой слой ниже.
|
||||
Никакой бизнес-логики — только композиция.
|
||||
|
||||
```text
|
||||
app/
|
||||
├── layout.tsx # RootLayout: провайдеры, глобальные стили
|
||||
├── page.tsx # Главная: MainLayout + HomeScreen
|
||||
├── knv-new/
|
||||
│ └── page.tsx # КНВ: MainLayout + KnvScreen
|
||||
└── catalog/
|
||||
└── page.tsx # Каталог: MainLayout + CatalogScreen
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/knv-new/page.tsx
|
||||
import { MainLayout } from '@/layouts/main'
|
||||
import { KnvScreen } from '@/screens/knv'
|
||||
|
||||
export default function KnvNewPage() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<KnvScreen />
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Если layout требует разный контент в зависимости от страницы — app передаёт его через пропсы:
|
||||
|
||||
```tsx
|
||||
// app/knv-new/page.tsx
|
||||
import { MainLayout } from '@/layouts/main'
|
||||
import { KnvScreen } from '@/screens/knv'
|
||||
import { KnvHeader } from '@/widgets/knv-header'
|
||||
|
||||
export default function KnvNewPage() {
|
||||
return (
|
||||
<MainLayout header={<KnvHeader />}>
|
||||
<KnvScreen />
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Layouts
|
||||
|
||||
Каркас страницы — общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||
|
||||
Если компонент внутри layout начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
|
||||
|
||||
```text
|
||||
src/layouts/
|
||||
└── main/
|
||||
├── main.layout.tsx
|
||||
├── ui/
|
||||
│ ├── header/
|
||||
│ │ └── header.tsx
|
||||
│ └── footer/
|
||||
│ └── footer.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Screens
|
||||
|
||||
Контент конкретной страницы. Собирает локальные секции и переиспользуемые модули из нижних слоёв.
|
||||
|
||||
Если компонент внутри screen начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
|
||||
|
||||
```text
|
||||
src/screens/
|
||||
└── knv/
|
||||
├── knv.screen.tsx
|
||||
├── ui/
|
||||
│ ├── hero-section/
|
||||
│ │ └── hero-section.tsx
|
||||
│ ├── products-section/
|
||||
│ │ └── products-section.tsx
|
||||
│ └── diseases-section/
|
||||
│ └── diseases-section.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Widgets
|
||||
|
||||
Составные блоки с данными и логикой, переиспользуемые в 2+ местах.
|
||||
|
||||
Если блок с логикой нужен только в одном месте — это компонент внутри `screens/{name}/ui/` или `layouts/{name}/ui/`, а не widget.
|
||||
|
||||
```text
|
||||
src/widgets/
|
||||
└── popular-products-slider/
|
||||
├── popular-products-slider.widget.tsx
|
||||
├── ui/
|
||||
│ └── slider-card/
|
||||
│ └── slider-card.tsx
|
||||
├── hooks/
|
||||
│ └── use-products.hook.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
Пользовательское действие или интерактивный сценарий: авторизация, заказ, добавление в корзину.
|
||||
|
||||
Feature создаётся осознанно, когда есть действие пользователя с бизнес-логикой. Компонент опционален — feature может экспортировать хуки, сторы, компоненты или всё вместе.
|
||||
|
||||
```text
|
||||
src/features/
|
||||
└── auth-by-email/
|
||||
├── auth-by-email.feature.tsx
|
||||
├── ui/
|
||||
│ ├── login-form/
|
||||
│ │ └── login-form.tsx
|
||||
│ └── reset-password/
|
||||
│ └── reset-password.tsx
|
||||
├── hooks/
|
||||
│ └── use-auth.hook.ts
|
||||
├── stores/
|
||||
│ └── auth.store.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Entities
|
||||
|
||||
Бизнес-сущность с отображением и типами: препарат, заболевание, врач, пользователь.
|
||||
|
||||
Entity создаётся осознанно, когда появляется бизнес-сущность. Компонент опционален — entity может быть только типами и хуком.
|
||||
|
||||
Отличие от `shared/ui/`: entity-компонент знает о бизнес-домене (принимает `Product`, а не абстрактные пропсы).
|
||||
|
||||
```text
|
||||
src/entities/
|
||||
├── product/
|
||||
│ ├── product.entity.tsx
|
||||
│ ├── ui/
|
||||
│ │ └── product-card/
|
||||
│ │ └── product-card.tsx
|
||||
│ ├── types/
|
||||
│ │ └── product.type.ts
|
||||
│ └── index.ts
|
||||
│
|
||||
├── session/
|
||||
│ ├── types/
|
||||
│ │ └── session.type.ts
|
||||
│ ├── hooks/
|
||||
│ │ └── use-session.hook.ts
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
### Shared
|
||||
|
||||
Переиспользуемые компоненты, утилиты, стили без бизнес-логики. Не знает о бизнес-домене — работает с абстрактными данными.
|
||||
|
||||
Структурирован как набор сегментов:
|
||||
|
||||
```text
|
||||
src/shared/
|
||||
├── ui/
|
||||
│ ├── icon/
|
||||
│ │ └── icon.tsx
|
||||
│ ├── carousel/
|
||||
│ │ ├── carousel.tsx
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── carousel-slide/
|
||||
│ │ │ │ └── carousel-slide.tsx
|
||||
│ │ │ └── carousel-dots/
|
||||
│ │ │ └── carousel-dots.tsx
|
||||
│ │ └── index.ts
|
||||
│ ├── container/
|
||||
│ └── button/
|
||||
├── lib/
|
||||
│ ├── format-date.ts
|
||||
│ └── cn.ts
|
||||
├── styles/
|
||||
│ ├── variables.css
|
||||
│ └── media.css
|
||||
└── sprites/
|
||||
```
|
||||
|
||||
## Модуль
|
||||
|
||||
### Структура
|
||||
|
||||
```text
|
||||
{name}/
|
||||
├── {name}.{суффикс}.tsx # компонент (опционален)
|
||||
├── ui/ # вложенные компоненты
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы
|
||||
├── types/ # типы, интерфейсы, enums
|
||||
├── styles/ # стили
|
||||
├── lib/ # утилиты
|
||||
├── services/ # внешние источники данных
|
||||
├── helpers/ # вспомогательные функции
|
||||
├── config/ # константы, конфигурация
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
### Именование компонента
|
||||
|
||||
Суффикс слоя получают **только модули первого уровня слоя** — те, что лежат непосредственно в корне слоя. Все компоненты (в `ui/`, любой глубины) именуются без суффикса. Без исключений.
|
||||
|
||||
| Слой | Суффикс | Пример |
|
||||
|------|---------|--------|
|
||||
| Layouts | `.layout.tsx` | `main.layout.tsx` |
|
||||
| Screens | `.screen.tsx` | `knv.screen.tsx` |
|
||||
| Widgets | `.widget.tsx` | `popular-products-slider.widget.tsx` |
|
||||
| Features | `.feature.tsx` | `auth-by-email.feature.tsx` |
|
||||
| Entities | `.entity.tsx` | `product.entity.tsx` |
|
||||
|
||||
Примеры:
|
||||
|
||||
```text
|
||||
features/auth-by-email/auth-by-email.feature.tsx # модуль первого уровня → суффикс
|
||||
features/auth-by-email/ui/login-form/login-form.tsx # компонент в ui/ → без суффикса
|
||||
shared/ui/carousel/carousel.tsx # компонент в shared → без суффикса
|
||||
```
|
||||
|
||||
### Правила импорта
|
||||
|
||||
Три уровня правил:
|
||||
|
||||
**Между слоями** — импорты строго сверху вниз:
|
||||
|
||||
```
|
||||
app → layouts → screens → widgets → features → entities → shared
|
||||
```
|
||||
|
||||
**Внутри модуля (не shared)** — сегменты доступны друг другу и компонентам. Компоненты внутри одного `ui/` не импортируют друг друга:
|
||||
|
||||
```text
|
||||
features/auth-by-email/
|
||||
├── ui/
|
||||
│ ├── login-form/ # НЕ может импортировать reset-password
|
||||
│ └── reset-password/ # НЕ может импортировать login-form
|
||||
```
|
||||
|
||||
Если двум компонентам нужен общий код — он поднимается на уровень выше:
|
||||
|
||||
```text
|
||||
features/auth/ui/login-form/ui/email-input/ # нужен соседу
|
||||
→ features/auth/ui/email-input/ # поднимаем на уровень
|
||||
→ shared/ui/email-input/ # если нужен за пределами фичи
|
||||
```
|
||||
|
||||
Компоненты наследуют правила зависимостей **родительского слоя**:
|
||||
|
||||
- Компонент внутри `features/auth/ui/login-form/` может импортировать `entities/` и `shared/` — как и сам feature
|
||||
- Компонент внутри `widgets/hero/ui/hero-stats/` может импортировать `features/`, `entities/`, `shared/` — как и сам widget
|
||||
|
||||
**Shared** — без ограничений на внутренние импорты. Компоненты в `shared/ui/` могут импортировать друг друга (`button` использует `icon`), `ui/` может использовать `lib/` и другие сегменты. Shared — фундамент, его компоненты строятся друг на друге.
|
||||
|
||||
Правила импорта между слоями enforceable через ESLint — настройка границ слоёв и запрет обратных зависимостей.
|
||||
|
||||
## Жизненный цикл модуля
|
||||
|
||||
Модуль не проектируется «на вырост». Он рождается на самом низком уровне
|
||||
и поднимается только когда появляется реальная потребность.
|
||||
|
||||
Пример пути компонента `product-card`:
|
||||
|
||||
1. **Начало:** `screens/catalog/ui/product-card/` — нужен только на странице каталога.
|
||||
2. **Переиспользование:** появился на странице поиска — выносим выше.
|
||||
Куда именно зависит от природы:
|
||||
- Составной блок с данными и логикой → `widgets/product-card/`
|
||||
- Представление бизнес-сущности → `entities/product/ui/product-card/`
|
||||
- Абстрактный UI без бизнес-логики → `shared/ui/product-card/`
|
||||
|
||||
Основной триггер подъёма — переиспользование в 2+ местах.
|
||||
Но если очевидно что модуль будет переиспользоваться — разумно разместить его на нужном уровне сразу.
|
||||
|
||||
Как происходит подъём на практике:
|
||||
|
||||
1. Разработчик обнаруживает что компонент нужен в другом месте
|
||||
2. Определяет целевой слой по природе компонента (widget / entity / shared)
|
||||
3. Перемещает папку, обновляет импорты, добавляет суффикс если это модуль первого уровня слоя
|
||||
4. Ревью подтверждает что подъём обоснован
|
||||
|
||||
Подъём — это обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
|
||||
## Граничные случаи
|
||||
|
||||
| Ситуация | Решение | Почему |
|
||||
|----------|---------|--------|
|
||||
| Фильтр каталога только на одной странице, но с хуками и стором | Модуль в `screens/catalog/` с сегментами `hooks/`, `stores/` | Не feature — feature это действие пользователя с бизнес-логикой (авторизация, заказ), а не UI с состоянием |
|
||||
| Компонент используется в 2 местах на одной странице | Остаётся в `screens/{name}/ui/` | Переиспользование внутри одного screen — не повод выносить в widget |
|
||||
| Entity без UI (только типы и хук) | Нормально | Модуль не обязан иметь UI. `entities/session/` с типами и хуком — валидный модуль |
|
||||
| Компонент нужен и в layout, и в screen | Выносить в `widgets/` или `shared/ui/` | Два разных слоя используют один компонент → переиспользование → подъём |
|
||||
| Модуль screen содержит бизнес-логику (хуки, стор, сервисы) | Нормально | Это не значит что он должен стать feature. Модуль с логикой внутри screen — обычное дело, пока он не переиспользуется |
|
||||
| Компонент в `shared/ui/button` хочет использовать `shared/ui/icon` | Разрешено | Shared — исключение: внутренние импорты без ограничений. Это фундамент, его компоненты строятся друг на друге |
|
||||
|
||||
## Запрещено
|
||||
|
||||
- **Не создавать feature без бизнес-логики** — кнопка без состояния и сайд-эффектов это компонент в `ui/`, а не feature
|
||||
- **Не класть доменные типы в shared** — если тип знает о Product, Disease, User — он живёт в `entities/`, не в `shared/`
|
||||
- **Не создавать entity или feature как результат «вынесения»** — они создаются осознанно: появилась бизнес-сущность → entity, появилось действие пользователя → feature
|
||||
- **Не импортировать соседние компоненты в `ui/` (кроме shared)** — если двум компонентам нужен общий код, он поднимается на уровень выше
|
||||
- **Не хранить в shared «помойку для всего»** — shared содержит переиспользуемые компоненты и утилиты без бизнес-логики, а не код который «непонятно куда положить»
|
||||
|
||||
## Почему так, а не иначе
|
||||
|
||||
### Почему `ui/` а не `modules/`
|
||||
|
||||
Внутри сегмента `ui/` всегда лежат единицы с обязательным UI (компоненты). Название точно отражает содержимое. `modules/` был нейтральнее, но скрывал природу вложенных единиц и создавал путаницу — модуль внутри модуля размывал понятие «модуль как единица слоя».
|
||||
|
||||
### Почему модуль и компонент — разные понятия
|
||||
|
||||
Модуль — единица первого уровня слоя, может не иметь UI. Компонент — вложенная единица с обязательным UI. Разделение снимает вопрос «а если нет `.tsx` — это всё ещё компонент?» и делает название сегмента `ui/` честным.
|
||||
|
||||
### Почему shared без ограничений на внутренние импорты
|
||||
|
||||
Shared — фундамент. Его компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. Запрещать это — бороться с реальностью. Поднимать некуда — shared уже нижний слой.
|
||||
|
||||
### Почему нет слоя pages
|
||||
|
||||
Роутинг живёт в `app/` (Next.js App Router). Отдельный слой `pages` конфликтовал бы с файловой структурой Next.js и дублировал ответственность `app/`.
|
||||
|
||||
### Почему компоненты в одном `ui/` не импортируют друг друга (кроме shared)
|
||||
|
||||
Независимые компоненты легко выносить в другой слой при переиспользовании. Если компонент A зависит от соседа B — при подъёме A придётся тянуть B. Это усложняет рефакторинг и нарушает принцип колокации.
|
||||
@@ -1,60 +0,0 @@
|
||||
# NextJS Style Guide
|
||||
|
||||
Правила и стандарты разработки на NextJS и TypeScript: архитектура, типизация, стили, компоненты, API и инфраструктурные разделы.
|
||||
|
||||
## Для ассистентов
|
||||
|
||||
Полная документация в одном MD файле: https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/ru/RULES.md
|
||||
|
||||
## Структура документации
|
||||
|
||||
### Workflow
|
||||
|
||||
**Что делать и в каком порядке** — пошаговые инструкции.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Начало работы | Что нужно знать перед началом разработки? |
|
||||
| Создание проекта | Как начать новый проект? |
|
||||
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
|
||||
| Добавление страницы | Как добавить новую страницу в проект? |
|
||||
| Добавление UI-модуля | Как создать компонент, фичу, виджет, сущность или layout? |
|
||||
| Стилизация | Как стилизовать компоненты в проекте? |
|
||||
| Получение данных | Как получать данные с сервера? |
|
||||
| Управление состоянием | Как работать с состоянием? |
|
||||
| Локализация | Как добавлять переводы и подключать локализацию? |
|
||||
|
||||
### Базовые правила
|
||||
|
||||
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Технологии и библиотеки | Какой стек используем? |
|
||||
| Архитектура | Как устроены слои FSD, зависимости, публичный API? |
|
||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||
|
||||
### Прикладные разделы
|
||||
|
||||
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
|
||||
|
||||
| Раздел | Отвечает на вопрос |
|
||||
|--------|-------------------|
|
||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
||||
| Структура проекта | Как организованы папки и файлы по FSD? |
|
||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||
| Изображения | _(не заполнен)_ |
|
||||
| SVG-спрайты | _(не заполнен)_ |
|
||||
| Видео | _(не заполнен)_ |
|
||||
| API | _(не заполнен)_ |
|
||||
| Stores | _(не заполнен)_ |
|
||||
| Хуки | _(не заполнен)_ |
|
||||
| Шрифты | _(не заполнен)_ |
|
||||
| Локализация | _(не заполнен)_ |
|
||||
|
||||
601
generate-llms.ts
Normal file
601
generate-llms.ts
Normal file
@@ -0,0 +1,601 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import config from './.vitepress/config';
|
||||
|
||||
/** Версия сборки. Передаётся CI через ENV; локально — `dev`. */
|
||||
const VERSION = process.env.BUILD_VERSION || 'dev';
|
||||
const BUILD_DATE = new Date().toISOString();
|
||||
|
||||
/** Корневая папка для генерируемой статики (попадает в build dist). */
|
||||
const PUBLIC_DIR = 'docs/public';
|
||||
|
||||
/** Префикс URL документации. Соответствует структуре `docs/docs/...`. */
|
||||
const DOC_PREFIX = '/docs/';
|
||||
|
||||
/** Канонический хост сайта (для sitemap/robots). Можно переопределить через ENV. */
|
||||
const SITE_URL = (process.env.SITE_URL || 'https://nextjs-style-guide.gromlab.ru').replace(/\/$/, '');
|
||||
|
||||
interface SidebarItem {
|
||||
text: string;
|
||||
link?: string;
|
||||
items?: SidebarItem[];
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
/** Название группы верхнего уровня (sidebar[].text) */
|
||||
section: string;
|
||||
/** Префикс из вложенной группы (например "Архитектура") */
|
||||
prefix: string | null;
|
||||
/** Текст пункта в sidebar */
|
||||
text: string;
|
||||
/** Ссылка из sidebar */
|
||||
link: string;
|
||||
}
|
||||
|
||||
/** Разобрать YAML frontmatter (плоский, без вложенностей) */
|
||||
const parseFrontmatter = (
|
||||
content: string,
|
||||
): { data: Record<string, string>; body: string } => {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
if (!match) return { data: {}, body: content };
|
||||
|
||||
const data: Record<string, string> = {};
|
||||
for (const line of match[1].split('\n')) {
|
||||
const lineMatch = line.match(/^([^:]+):\s*(.*)$/);
|
||||
if (!lineMatch) continue;
|
||||
let value = lineMatch[2].trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
data[lineMatch[1].trim()] = value;
|
||||
}
|
||||
return { data, body: match[2] };
|
||||
};
|
||||
|
||||
/** Первый абзац после h1 — однострочное описание для llms.txt */
|
||||
const firstParagraphAfterH1 = (body: string): string | null => {
|
||||
const lines = body.split('\n');
|
||||
const h1Idx = lines.findIndex((l) => /^#\s/.test(l));
|
||||
if (h1Idx === -1) return null;
|
||||
|
||||
let i = h1Idx + 1;
|
||||
while (i < lines.length && lines[i].trim() === '') i++;
|
||||
|
||||
const para: string[] = [];
|
||||
while (
|
||||
i < lines.length &&
|
||||
lines[i].trim() !== '' &&
|
||||
!lines[i].startsWith('#')
|
||||
) {
|
||||
para.push(lines[i].trim());
|
||||
i++;
|
||||
}
|
||||
return para.join(' ').trim() || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Преобразовать sidebar `link` (например `/docs/foo`) в относительный
|
||||
* путь файла внутри `docs/docs/`. Префикс `/docs/` отрезается.
|
||||
*/
|
||||
const linkToRel = (link: string): string => {
|
||||
let rel = link.startsWith(DOC_PREFIX)
|
||||
? link.slice(DOC_PREFIX.length)
|
||||
: link.replace(/^\//, '');
|
||||
if (rel === '' || rel.endsWith('/')) {
|
||||
rel += 'index.md';
|
||||
} else {
|
||||
rel += '.md';
|
||||
}
|
||||
return rel;
|
||||
};
|
||||
|
||||
const linkToFilePath = (link: string): string =>
|
||||
path.join('docs/docs', linkToRel(link));
|
||||
|
||||
/** Абсолютный URL `.md`-копии страницы на сайте. */
|
||||
const linkToSiteUrl = (link: string): string =>
|
||||
`${DOC_PREFIX}${linkToRel(link)}`;
|
||||
|
||||
/**
|
||||
* Развернуть sidebar в плоский список с сохранением группы и
|
||||
* накопленного префикса вложенных групп. Поддерживает произвольную
|
||||
* глубину вложенности — префиксы подгрупп склеиваются через `: `.
|
||||
*/
|
||||
const flattenSidebar = (sidebar: SidebarItem[]): Entry[] => {
|
||||
const entries: Entry[] = [];
|
||||
|
||||
const walk = (
|
||||
items: SidebarItem[],
|
||||
section: string,
|
||||
prefix: string | null,
|
||||
): void => {
|
||||
for (const item of items) {
|
||||
const hasChildren = !!item.items && item.items.length > 0;
|
||||
|
||||
if (item.link) {
|
||||
entries.push({ section, prefix, text: item.text, link: item.link });
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
const nextPrefix = prefix ? `${prefix}: ${item.text}` : item.text;
|
||||
walk(item.items!, section, nextPrefix);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const top of sidebar) {
|
||||
const hasChildren = !!top.items && top.items.length > 0;
|
||||
|
||||
if (top.link && !hasChildren) {
|
||||
entries.push({ section: top.text, prefix: null, text: top.text, link: top.link });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
walk(top.items!, top.text, null);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
const groupBySection = (entries: Entry[]): Map<string, Entry[]> => {
|
||||
const map = new Map<string, Entry[]>();
|
||||
for (const entry of entries) {
|
||||
const list = map.get(entry.section);
|
||||
if (list) list.push(entry);
|
||||
else map.set(entry.section, [entry]);
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
interface SiteConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
themeConfig: { sidebar: SidebarItem[] };
|
||||
llmsBlockquote?: string;
|
||||
llmsContext?: string;
|
||||
}
|
||||
|
||||
const cfg = config as unknown as SiteConfig;
|
||||
|
||||
const buildLlms = (): void => {
|
||||
const sidebar = cfg.themeConfig.sidebar;
|
||||
const blockquote = cfg.llmsBlockquote ?? cfg.description;
|
||||
const context = cfg.llmsContext;
|
||||
|
||||
const entries = flattenSidebar(sidebar);
|
||||
const grouped = groupBySection(entries);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# ${cfg.title}`);
|
||||
lines.push('');
|
||||
lines.push(`> ${blockquote}`);
|
||||
lines.push('');
|
||||
if (context) {
|
||||
lines.push(context);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
for (const [section, items] of grouped) {
|
||||
lines.push(`## ${section}`);
|
||||
lines.push('');
|
||||
|
||||
for (const entry of items) {
|
||||
const filePath = linkToFilePath(entry.link);
|
||||
const url = linkToSiteUrl(entry.link);
|
||||
|
||||
let description: string | null = null;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, body } = parseFrontmatter(raw);
|
||||
description = data.description || firstParagraphAfterH1(body);
|
||||
} else {
|
||||
console.warn(`файл не найден: ${filePath}`);
|
||||
}
|
||||
|
||||
const display = entry.prefix
|
||||
? `${entry.prefix}: ${entry.text}`
|
||||
: entry.text;
|
||||
const descPart = description ? `: ${description}` : '';
|
||||
lines.push(`- [${display}](${url})${descPart}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
const outFile = path.join(PUBLIC_DIR, 'llms.txt');
|
||||
fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
|
||||
console.log(`${outFile} создан`);
|
||||
};
|
||||
|
||||
/** Рекурсивно скопировать дерево, фильтруя по предикату. */
|
||||
const copyDirSync = (
|
||||
src: string,
|
||||
dest: string,
|
||||
filter: (name: string) => boolean = () => true,
|
||||
): number => {
|
||||
let count = 0;
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
fs.mkdirSync(destPath, { recursive: true });
|
||||
count += copyDirSync(srcPath, destPath, filter);
|
||||
} else if (entry.isFile() && filter(entry.name)) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Скопировать все `.md`-файлы документации в `docs/public/docs/`,
|
||||
* чтобы они попали в build `dist/` и были доступны по URL `/docs/path.md`.
|
||||
*/
|
||||
const copyMdFiles = (): void => {
|
||||
const srcDir = 'docs/docs';
|
||||
const destDir = path.join(PUBLIC_DIR, 'docs');
|
||||
if (!fs.existsSync(srcDir)) return;
|
||||
|
||||
const copied = copyDirSync(srcDir, destDir, (name) => name.endsWith('.md'));
|
||||
console.log(`скопировано ${copied} .md-файлов в ${destDir}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Преобразовать sidebar `link` в относительный путь файла внутри архива
|
||||
* (от корня папки `nextjs-style-guide/`).
|
||||
*/
|
||||
const linkToArchiveRel = (link: string): string => {
|
||||
let rel = link.startsWith(DOC_PREFIX)
|
||||
? link.slice(DOC_PREFIX.length)
|
||||
: link.replace(/^\//, '');
|
||||
if (rel === '' || rel.endsWith('/')) {
|
||||
rel += 'index.md';
|
||||
} else {
|
||||
rel += '.md';
|
||||
}
|
||||
return rel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Заменить во всех `.md` архива ссылки `[text](/docs/foo)` на относительные
|
||||
* пути от расположения файла. Без этого внутренние ссылки в распакованной
|
||||
* папке не работают.
|
||||
*/
|
||||
const transformLinksInDir = (rootDir: string): void => {
|
||||
const linkRe = /\]\(\/docs\/([^)\s#]*)(#[^)]*)?\)/g;
|
||||
|
||||
const walk = (dir: string): void => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||||
|
||||
const content = fs.readFileSync(full, 'utf8');
|
||||
const fileDir = path.dirname(full);
|
||||
|
||||
const updated = content.replace(linkRe, (_match, route, hash = '') => {
|
||||
const targetRel = linkToArchiveRel(`${DOC_PREFIX}${route}`);
|
||||
const targetAbs = path.join(rootDir, targetRel);
|
||||
let rel = path.relative(fileDir, targetAbs);
|
||||
if (!rel.startsWith('.')) rel = './' + rel;
|
||||
return `](${rel}${hash})`;
|
||||
});
|
||||
|
||||
if (updated !== content) {
|
||||
fs.writeFileSync(full, updated, 'utf8');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(rootDir);
|
||||
};
|
||||
|
||||
/**
|
||||
* Сгенерировать `README.md` — точка входа архива. Карта документации
|
||||
* с относительными ссылками, описаниями из frontmatter/первого абзаца
|
||||
* и метаинфо сборки.
|
||||
*/
|
||||
const buildArchiveReadme = (rootDir: string): void => {
|
||||
const sidebar = cfg.themeConfig.sidebar;
|
||||
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
|
||||
const context = cfg.llmsContext;
|
||||
|
||||
const entries = flattenSidebar(sidebar).filter(
|
||||
// «Главная» из sidebar — это страница раздела для веба, в архиве не нужна.
|
||||
(e) => e.section !== 'Главная',
|
||||
);
|
||||
const grouped = groupBySection(entries);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# ${cfg.title}`);
|
||||
lines.push('');
|
||||
if (blockquote) {
|
||||
lines.push(`> ${blockquote}`);
|
||||
lines.push('');
|
||||
}
|
||||
if (context) {
|
||||
lines.push(context);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('## Содержание');
|
||||
lines.push('');
|
||||
|
||||
for (const [section, items] of grouped) {
|
||||
lines.push(`### ${section}`);
|
||||
lines.push('');
|
||||
for (const entry of items) {
|
||||
const targetRel = './' + linkToArchiveRel(entry.link);
|
||||
const filePath = path.join(rootDir, linkToArchiveRel(entry.link));
|
||||
|
||||
let description: string | null = null;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, body } = parseFrontmatter(raw);
|
||||
description = data.description || firstParagraphAfterH1(body);
|
||||
}
|
||||
|
||||
const display = entry.prefix
|
||||
? `${entry.prefix}: ${entry.text}`
|
||||
: entry.text;
|
||||
const descPart = description ? ` — ${description}` : '';
|
||||
lines.push(`- [${display}](${targetRel})${descPart}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
lines.push(`Версия: ${VERSION} · Сборка: ${BUILD_DATE}`);
|
||||
lines.push('');
|
||||
|
||||
fs.writeFileSync(path.join(rootDir, 'README.md'), lines.join('\n'), 'utf8');
|
||||
};
|
||||
|
||||
/**
|
||||
* Собрать `nextjs-style-guide.zip`. Внутри — папка `nextjs-style-guide/`
|
||||
* с `.md`-файлами, README, `llms-full.txt` и `VERSION`. Внутренние ссылки
|
||||
* преобразуются в относительные.
|
||||
*/
|
||||
const buildZip = (): void => {
|
||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-'));
|
||||
const stage = path.join(tmpRoot, 'nextjs-style-guide');
|
||||
fs.mkdirSync(stage, { recursive: true });
|
||||
|
||||
// 1. Копируем все .md в staging.
|
||||
copyDirSync('docs/docs', stage, (name) => name.endsWith('.md'));
|
||||
|
||||
// 2. Удаляем веб-index.md — в архиве его роль выполняет README.md.
|
||||
const indexPath = path.join(stage, 'index.md');
|
||||
if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath);
|
||||
|
||||
// 3. Преобразуем абсолютные ссылки `/docs/...` в относительные.
|
||||
transformLinksInDir(stage);
|
||||
|
||||
// 4. Генерируем точку входа README.md.
|
||||
buildArchiveReadme(stage);
|
||||
|
||||
// 5. Кладём llms-full.txt — удобно для одноразового чтения LLM.
|
||||
const llmsFullSrc = path.join(PUBLIC_DIR, 'llms-full.txt');
|
||||
if (fs.existsSync(llmsFullSrc)) {
|
||||
fs.copyFileSync(llmsFullSrc, path.join(stage, 'llms-full.txt'));
|
||||
}
|
||||
|
||||
// 6. Метаинформация сборки.
|
||||
fs.writeFileSync(
|
||||
path.join(stage, 'VERSION'),
|
||||
`${VERSION}\n${BUILD_DATE}\n`,
|
||||
);
|
||||
|
||||
const outFile = path.resolve(PUBLIC_DIR, 'nextjs-style-guide.zip');
|
||||
fs.rmSync(outFile, { force: true });
|
||||
|
||||
execFileSync('zip', ['-rq', outFile, 'nextjs-style-guide'], {
|
||||
cwd: tmpRoot,
|
||||
});
|
||||
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
console.log(`${outFile} создан (${VERSION})`);
|
||||
};
|
||||
|
||||
/** Удалить YAML frontmatter из исходника `.md`. */
|
||||
const stripFrontmatter = (content: string): string =>
|
||||
content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
||||
|
||||
/**
|
||||
* Сдвинуть уровень заголовков на 1 вниз (h1→h2, h2→h3, ...).
|
||||
* Игнорирует строки внутри блоков кода.
|
||||
*/
|
||||
const shiftHeadings = (content: string): string => {
|
||||
const lines = content.split('\n');
|
||||
let inCodeBlock = false;
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.startsWith('```')) inCodeBlock = !inCodeBlock;
|
||||
if (inCodeBlock) return line;
|
||||
if (/^#{1,5}\s/.test(line)) return '#' + line;
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Собрать `llms-full.txt` — все страницы в одном файле.
|
||||
* Порядок страниц повторяет порядок в sidebar.
|
||||
*/
|
||||
const buildLlmsFull = (): void => {
|
||||
const sidebar = cfg.themeConfig.sidebar;
|
||||
const entries = flattenSidebar(sidebar);
|
||||
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(`# ${cfg.title}`);
|
||||
parts.push('');
|
||||
if (blockquote) parts.push(`> ${blockquote}`);
|
||||
if (cfg.llmsContext) {
|
||||
parts.push('');
|
||||
parts.push(cfg.llmsContext);
|
||||
}
|
||||
parts.push('');
|
||||
|
||||
for (const entry of entries) {
|
||||
const filePath = linkToFilePath(entry.link);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const content = shiftHeadings(stripFrontmatter(raw)).trim();
|
||||
if (!content) continue;
|
||||
|
||||
// Мета-якорь: путь страницы для ориентации LLM
|
||||
parts.push(`<!-- ${entry.link} -->`);
|
||||
parts.push('');
|
||||
parts.push(content);
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
const outFile = path.join(PUBLIC_DIR, 'llms-full.txt');
|
||||
fs.writeFileSync(outFile, parts.join('\n'), 'utf8');
|
||||
console.log(`${outFile} создан`);
|
||||
};
|
||||
|
||||
/** Манифест сборки — для лендинга и внешних потребителей. */
|
||||
const writeManifest = (): void => {
|
||||
const manifest = {
|
||||
version: VERSION,
|
||||
buildDate: BUILD_DATE,
|
||||
llms: '/llms.txt',
|
||||
llmsFull: '/llms-full.txt',
|
||||
zip: '/nextjs-style-guide.zip',
|
||||
};
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(PUBLIC_DIR, 'manifest.json'),
|
||||
JSON.stringify(manifest, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
console.log(`${PUBLIC_DIR}/manifest.json создан`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Сгенерировать `robots.txt` с указанием sitemap и явными ссылками
|
||||
* на llms.txt/llms-full.txt — стандартные файлы, которые читают агенты.
|
||||
*/
|
||||
const buildRobots = (): void => {
|
||||
const lines = [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
'',
|
||||
`Sitemap: ${SITE_URL}/sitemap.xml`,
|
||||
'',
|
||||
'# Карта документации для AI-агентов:',
|
||||
`# ${SITE_URL}/llms.txt`,
|
||||
`# ${SITE_URL}/llms-full.txt`,
|
||||
'',
|
||||
];
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(PUBLIC_DIR, 'robots.txt'), lines.join('\n'), 'utf8');
|
||||
console.log(`${PUBLIC_DIR}/robots.txt создан`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Сгенерировать `sitemap.xml` из sidebar + корневые ресурсы для LLM
|
||||
* (llms.txt, llms-full.txt) — чтобы агенты, читающие sitemap, видели их.
|
||||
*/
|
||||
const buildSitemap = (): void => {
|
||||
const sidebar = cfg.themeConfig.sidebar;
|
||||
const entries = flattenSidebar(sidebar);
|
||||
|
||||
const urls = new Set<string>();
|
||||
urls.add(`${SITE_URL}/`);
|
||||
urls.add(`${SITE_URL}/llms.txt`);
|
||||
urls.add(`${SITE_URL}/llms-full.txt`);
|
||||
|
||||
for (const entry of entries) {
|
||||
const link = entry.link;
|
||||
// cleanUrls: канон без `.html`. Index-страницы — каталог со слешем.
|
||||
urls.add(`${SITE_URL}${link}`);
|
||||
}
|
||||
|
||||
const today = BUILD_DATE.slice(0, 10);
|
||||
const xml = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
...[...urls].map(
|
||||
(loc) => ` <url><loc>${loc}</loc><lastmod>${today}</lastmod></url>`,
|
||||
),
|
||||
'</urlset>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(PUBLIC_DIR, 'sitemap.xml'), xml, 'utf8');
|
||||
console.log(`${PUBLIC_DIR}/sitemap.xml создан`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Преобразовать абсолютные ссылки `index.md` в рабочие при открытии
|
||||
* README в репозитории:
|
||||
* - `/docs/foo` → относительный путь `docs/docs/foo.md`;
|
||||
* - корневые ресурсы (`/llms.txt`, `/llms-full.txt`, `*.zip`, `/manifest.json`,
|
||||
* `/sitemap.xml`, `/robots.txt`) — генерируемые, в репозитории отсутствуют,
|
||||
* поэтому ссылки переписываются на абсолютный `SITE_URL`.
|
||||
*/
|
||||
const transformReadmeLinks = (content: string): string => {
|
||||
const linkRe = /\]\((\/[^)\s]*)\)/g;
|
||||
return content.replace(linkRe, (match, href: string) => {
|
||||
const [pathPart, hash = ''] = href.split('#');
|
||||
const hashPart = hash ? `#${hash}` : '';
|
||||
|
||||
if (pathPart.startsWith(DOC_PREFIX)) {
|
||||
const rel = linkToArchiveRel(pathPart);
|
||||
return `](docs/docs/${rel}${hashPart})`;
|
||||
}
|
||||
|
||||
return `](${SITE_URL}${pathPart}${hashPart})`;
|
||||
});
|
||||
};
|
||||
|
||||
/** Скопировать `index.md` документации в корневой README без frontmatter. */
|
||||
const buildReadme = (): void => {
|
||||
const indexPath = 'docs/docs/index.md';
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.warn(`Пропуск README.md: ${indexPath} не найден`);
|
||||
return;
|
||||
}
|
||||
const raw = fs.readFileSync(indexPath, 'utf8');
|
||||
const { body } = parseFrontmatter(raw);
|
||||
const transformed = transformReadmeLinks(body.trimStart());
|
||||
|
||||
// Порядок: H1 → описание (первый абзац) → ссылка на сайт.
|
||||
const withSiteLink = transformed.replace(
|
||||
/^(#\s[^\n]*\n\n[^\n]+\n)/,
|
||||
`$1\nСайт: ${SITE_URL}\n`,
|
||||
);
|
||||
|
||||
fs.writeFileSync('README.md', withSiteLink, 'utf8');
|
||||
console.log(`README.md обновлён из ${indexPath}`);
|
||||
};
|
||||
|
||||
buildLlms();
|
||||
buildLlmsFull();
|
||||
copyMdFiles();
|
||||
buildZip();
|
||||
writeManifest();
|
||||
buildRobots();
|
||||
buildSitemap();
|
||||
buildReadme();
|
||||
@@ -1,137 +0,0 @@
|
||||
<!-- /index -->
|
||||
# NextJS Style Guide
|
||||
|
||||
Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
### Processes
|
||||
|
||||
**What to do** in a specific situation — step-by-step instructions.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Getting Started | What tools to install before starting development? |
|
||||
| Creating an App | How to create a new project, where to get a template? |
|
||||
| Creating Pages | How to add a page: routing and screen? |
|
||||
| Creating Components | How to generate components using templates? |
|
||||
| Styling | What to use: Mantine, tokens, or PostCSS? |
|
||||
| Data Fetching | How to fetch data: SWR, codegen, sockets? |
|
||||
| State Management | When and how to create a store (Zustand)? |
|
||||
| Localization | How to add translations and work with i18next? |
|
||||
|
||||
### Basic Rules
|
||||
|
||||
**What the code should look like** — standards not tied to a specific technology.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Tech Stack | What stack do we use? |
|
||||
| Architecture | How are FSD layers, dependencies, and public API structured? |
|
||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||
| Naming | How to name files, variables, components, hooks? |
|
||||
| Documentation | How to write JSDoc: what to document and what not? |
|
||||
| Typing | How to type: type vs interface, any/unknown? |
|
||||
|
||||
### Applied Sections
|
||||
|
||||
**How a specific area works** — rules, structure, and code examples for specific technologies and tools.
|
||||
|
||||
| Section | Answers the question |
|
||||
|---------|---------------------|
|
||||
| Project Structure | How are folders and files organized by FSD? |
|
||||
| Components | How is a component structured: files, props, clsx? |
|
||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||
| Images | _(not filled)_ |
|
||||
| SVG Sprites | _(not filled)_ |
|
||||
| Video | _(not filled)_ |
|
||||
| API | _(not filled)_ |
|
||||
| Stores | _(not filled)_ |
|
||||
| Hooks | _(not filled)_ |
|
||||
| Fonts | _(not filled)_ |
|
||||
| Localization | _(not filled)_ |
|
||||
|
||||
## For Assistants
|
||||
|
||||
Full documentation in a single MD file: https://gromlab.ru/docs/frontend-style-guide/raw/branch/main/generated/en/RULES.md
|
||||
|
||||
<!-- /basics/tech-stack -->
|
||||
## Tech Stack
|
||||
|
||||
Base technology stack and libraries used in projects.
|
||||
|
||||
<!-- /basics/naming -->
|
||||
## Naming
|
||||
|
||||
Naming should be predictable, concise, and reflect the meaning of the entity.
|
||||
|
||||
<!-- /basics/architecture -->
|
||||
## Architecture
|
||||
|
||||
Architecture based on FSD (Feature-Sliced Design) and strict module boundaries.
|
||||
|
||||
<!-- /basics/code-style -->
|
||||
## Code Style
|
||||
|
||||
Unified code formatting rules: indentation, line breaks, quotes, import order, and readability.
|
||||
|
||||
<!-- /basics/documentation -->
|
||||
## Documentation
|
||||
|
||||
Documentation should help understand the purpose of an entity, not duplicate its types or obvious details.
|
||||
|
||||
<!-- /basics/typing -->
|
||||
## Typing
|
||||
|
||||
Typing is required for all public interfaces, functions, and components.
|
||||
|
||||
<!-- /applied/project-structure -->
|
||||
## Project Structure
|
||||
|
||||
Base project structure and principles of module organization at folder and file level.
|
||||
|
||||
<!-- /applied/components -->
|
||||
## Components
|
||||
|
||||
Rules for creating UI components across all FSD layers.
|
||||
|
||||
<!-- /applied/page-level -->
|
||||
## Page-level Components
|
||||
|
||||
Next.js App Router special files used by the framework by convention: `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`.
|
||||
|
||||
<!-- /applied/templates-generation -->
|
||||
## Templates & Code Generation
|
||||
|
||||
Template tools, syntax, and examples for code generation.
|
||||
|
||||
<!-- /applied/styles -->
|
||||
## Styles
|
||||
|
||||
CSS writing rules: PostCSS Modules, nesting, media queries, variables, formatting.
|
||||
|
||||
<!-- /applied/images-sprites -->
|
||||
## Images
|
||||
|
||||
<!-- /applied/svg-sprites -->
|
||||
## SVG Sprites
|
||||
|
||||
<!-- /applied/video -->
|
||||
## Video
|
||||
|
||||
<!-- /applied/api -->
|
||||
## API
|
||||
|
||||
<!-- /applied/stores -->
|
||||
## Stores
|
||||
|
||||
<!-- /applied/hooks -->
|
||||
## Hooks
|
||||
|
||||
<!-- /applied/fonts -->
|
||||
## Fonts
|
||||
|
||||
<!-- /applied/localization -->
|
||||
## Localization
|
||||
File diff suppressed because it is too large
Load Diff
528
package-lock.json
generated
528
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "nextjs-style-guide",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.2",
|
||||
"vitepress": "^1.6.3"
|
||||
}
|
||||
},
|
||||
@@ -643,6 +644,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
@@ -660,6 +678,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
@@ -677,6 +712,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
@@ -1659,6 +1711,19 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
@@ -2088,6 +2153,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
@@ -2248,6 +2323,459 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"docs": "node ./concat-md.js",
|
||||
"llms": "tsx ./generate-llms.ts",
|
||||
"dev": "vitepress dev .",
|
||||
"build": "vitepress build .",
|
||||
"serve": "vitepress serve ."
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.2",
|
||||
"vitepress": "^1.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user