--- url: /nextjs-style-guide/workflow.md description: Короткие ответы на типовые вопросы и решения для спорных ситуаций. --- # Подсказки Короткие ответы на типовые вопросы и решения для спорных ситуаций. --- --- url: /nextjs-style-guide/slm-design/architecture.md description: 'Назначение архитектуры, ключевые принципы и карта разделов документации' --- # SLM Design Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. ## Разделы спецификации Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: * [Слои](/slm-design/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. * [Модули](/slm-design/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. * [Сегменты](/slm-design/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. * [Монорепозитории](/slm-design/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business. Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории. ## Преимущества ### Вертикальная организация домена Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. ### Dependency Injection без фреймворков Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. ### Разделение ответственности без перегрузки слоёв Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. ### Горизонтальная инкапсуляция Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. ### Колокация по умолчанию Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. ### Явное разделение каркаса и контента Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. ### Масштабирование через группировку При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). ### Адаптация к монорепозиториям SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. ## Происхождение 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/ │ ├── infra/ │ ├── theme/ │ ├── i18n/ │ ├── backend-api/ │ └── logger/ │ ├── ui/ │ ├── button/ │ ├── input/ │ ├── modal/ │ ├── toast/ │ └── dropdown/ │ └── shared/ ├── lib/ ├── types/ └── styles/ ``` ## Принципы * **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. * **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. * **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. * **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. --- --- url: /nextjs-style-guide/slm-design/architecture/layers.md description: >- Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя --- # Слои Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. ## Определение **Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** ## Группы слоёв Слои делятся на три группы: | Группа | Слои | Описание | |--------|------|----------| | Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | | Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | | Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | ## Направление зависимостей Любой импорт между модулями — только через публичный API. ``` app → [ layouts | screens ] → widgets → business → infra → ui → shared ``` * `layouts` и `screens` — параллельные слои, не импортируют друг друга * Модули одного слоя в группе «Композиция» изолированы друг от друга * Модули одного слоя `infra` и `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 и сервисами. Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `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/ ``` ### Требования * Один модуль = один бизнес-домен * Циклические зависимости между доменами запрещены * Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты * Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую * Доменные типы (`User`, `Product`) живут здесь, не в `shared/` ## Слой infra Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). ```text src/infra/ ├── theme/ ├── i18n/ ├── backend-api/ ├── maps-api/ ├── logger/ ├── feature-flags/ └── realtime/ ``` ### Требования * Один модуль = один техсервис * Импортирует `infra/`, `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 Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. ```text src/shared/ ├── lib/ ├── types/ ├── styles/ └── sprites/ ``` ### Требования * Не имеет runtime-состояния --- --- url: /nextjs-style-guide/slm-design/architecture/modules.md description: >- Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента --- # Модули Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. ## Определение **Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. Главная граница модуля — не папка, а ответственность. ## Компонент **Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. > Компонент отображает. Модуль организует. Компонент не может: * Импортировать код проекта за пределами родительского модуля. * Владеть архитектурными зависимостями. * Содержать любые компоненты. * Содержать любые модули. * Делать внешние запросы. * Самостоятельно получать данные. * Выбирать источник данных. * Композировать данные. * Вызывать сценарные хуки. * Оркестрировать сценарий. * Композировать модули. * Решать, как устроен процесс. * Содержать бизнес-логику. * Содержать сценарную логику. Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. ```text auth/ ├── ui/ │ └── logout-button/ │ ├── logout-button.tsx │ ├── styles/ │ │ └── logout-button.module.css │ ├── types/ │ │ └── logout-button-props.type.ts │ └── index.ts └── index.ts ``` ## Что считается модулем Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. Примеры модулей: * `screens/home/` — модуль страницы. * `widgets/page-heading/` — модуль виджета. * `business/auth/` — модуль бизнес-домена. * `infra/theme/` — модуль инфраструктурного сервиса. * `ui/button/` — модуль UI-kit сущности. * `screens/home/parts/hero-section/` — вложенный модуль страницы. Не считаются модулями: * `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. * `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. * `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. ## Типы модулей Тип модуля определяет обязательный корневой файл и стартовую структуру. ### UI-модуль Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: ```text header/ ├── header.tsx └── index.ts ``` `ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. ### Бизнес-модуль Бизнес-модуль — модуль, который строится вокруг публичного runtime API. Бизнес-модуль обязан иметь фабрику в корне: ```text auth/ ├── auth.factory.ts ├── index.ts └── types/ ``` Фабрика возвращает публичный runtime API модуля. ### Инфраструктурный модуль Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. ```text theme/ ├── index.ts ├── config/ ├── hooks/ ├── styles/ └── ui/ ``` ```text backend-api/ ├── backend-api.client.ts ├── config/ ├── types/ └── index.ts ``` ## Структура Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. ```text {module-name}/ ├── {module-name}.factory.ts # фабрика (для business-модулей) ├── {module-name}.tsx # корневой файл модуля (опционален) ├── ui/ # компоненты модуля ├── parts/ # вложенные модули ├── hooks/ # хуки ├── stores/ # сторы состояния ├── services/ # внешние источники данных ├── mappers/ # трансформация данных между форматами ├── types/ # типы ├── styles/ # стили ├── lib/ # утилиты модуля ├── config/ # константы и конфигурация └── index.ts # публичный API ``` Подробное описание сегментов — в разделе [Сегменты](/slm-design/architecture/segments). ## Публичный API Внешний код импортирует модуль только через публичный API. ```ts // Хорошо import { customerFactory } from '@/business/customer' import type { Customer } from '@/business/customer' ``` ```ts // Плохо import { validateToken } from '@/business/auth/lib/tokens' ``` `index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. Внутренние сегменты модуля остаются деталями реализации. Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. ```ts // business/customer/index.ts export { customerFactory } from './customer.factory' export type { Customer } from './types/customer.type' export type { CustomerApi } from './types/customer-api.type' export type { CustomerDeps } from './types/customer-deps.type' export type { CustomerFactory } from './types/customer-factory.type' ``` ## Фабрика Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». ### Примеры Пример реализации фабрики в React см. в [Создание фабрики](/slm-design/examples/react/factory). Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/slm-design/examples/react/factory-composition). Пример композиции фабрик через React Provider см. в [Композиция через Provider](/slm-design/examples/react/composition-provider). ## Жизненный цикл Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. * Нужен на одной странице → `screens/{name}/parts/` * Появился в 2+ местах → поднимается по природе: * абстрактный UI → `ui/` * блок с данными/логикой → `widgets/` * представление бизнес-домена → `business/{area}/parts/` Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. --- --- url: /nextjs-style-guide/slm-design/architecture/segments.md description: >- Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов --- # Сегменты Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. ## Определение **Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** ## Обзор | Сегмент | Содержимое | |---------|------------| | `ui/` | Презентационные компоненты родительского модуля | | `parts/` | Вложенные модули со своими сегментами | | `hooks/` | React-хуки | | `stores/` | Сторы состояния | | `services/` | Работа с внешними источниками данных | | `mappers/` | Трансформация данных между форматами | | `types/` | TypeScript-типы и интерфейсы | | `styles/` | Стили | | `lib/` | Утилиты и хелперы модуля | | `config/` | Константы и конфигурация | ## Сегмент ui/ Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. Компонент в `ui/`: * Находится в собственной папке. * Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. * Не содержит любые компоненты. * Не содержит любые модули. * Не импортирует код проекта за пределами родительского модуля. * Не делает внешние запросы. * Не вызывает сценарные хуки. * Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. * Не содержит бизнес-логику или сценарную логику. Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/slm-design/architecture/modules#компонент). Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. ```text user/ ├── ui/ │ ├── user-avatar/ │ │ ├── user-avatar.tsx │ │ ├── styles/ │ │ │ └── user-avatar.module.css │ │ ├── types/ │ │ │ └── user-avatar-props.type.ts │ │ └── index.ts │ └── user-status/ │ ├── user-status.tsx │ └── index.ts ├── types/ ├── hooks/ ├── user.tsx └── index.ts ``` Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. ## Сегмент parts/ Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. ```text home/ ├── parts/ │ ├── hero-section/ │ │ ├── hero-section.tsx │ │ ├── styles/ │ │ ├── parts/ │ │ │ └── top-banner/ │ │ │ ├── top-banner.tsx │ │ │ └── index.ts │ │ └── index.ts │ └── features-section/ │ ├── features-section.tsx │ ├── hooks/ │ └── index.ts ├── home.screen.tsx └── index.ts ``` Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. Вложенность `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 ``` --- --- url: /nextjs-style-guide/slm-design/architecture/monorepo.md description: >- Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории --- # Монорепозитории Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. ## Определение **Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** ## Базовая структура Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. ```text repo/ ├── apps/ │ ├── web/ │ │ └── src/ │ │ ├── app/ │ │ ├── layouts/ │ │ ├── screens/ │ │ ├── widgets/ │ │ ├── business/ │ │ ├── infra/ │ │ ├── ui/ │ │ └── shared/ │ └── admin/ │ └── src/ │ └── ... └── packages/ ├── ui/ │ ├── button/ # самостоятельный пакет UI-модуля │ ├── input/ # самостоятельный пакет UI-модуля │ └── modal/ # самостоятельный пакет UI-модуля ├── infra/ │ ├── theme/ # самостоятельный пакет infra-модуля │ ├── backend-api/ # самостоятельный пакет infra-модуля │ └── logger/ # самостоятельный пакет infra-модуля └── shared/ # единый shared-пакет ├── package.json └── src/ ├── lib/ # переиспользуемые утилиты ├── helpers/ # переиспользуемые helpers └── index.ts ``` `apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. ## Группировка frontend-пакетов Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. ```text packages/ui/* # пакеты UI-модулей packages/infra/* # пакеты infra-модулей packages/shared # единый shared-пакет ``` Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM. ## Пакет и модуль Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации. В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению. ```text packages/ui/button/ packages/ui/modal/ packages/infra/theme/ packages/infra/backend-api/ packages/shared/ ``` ## Что остаётся в приложении Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения. ```text apps/web/src/app/ apps/web/src/layouts/ apps/web/src/screens/ apps/web/src/widgets/ apps/web/src/business/ ``` `app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения. `business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой. ## Что можно выносить В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория. | Группа | Что выносить | Пример | |--------|--------------|--------| | `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` | | `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` | | `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` | Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения. ## UI-пакеты В `packages/ui/*` размещаются переиспользуемые UI-модули. ```text packages/ui/button/ ├── package.json └── src/ ├── button.tsx ├── styles/ ├── types/ └── index.ts ``` UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. ## Infra-пакеты В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. ```text packages/infra/backend-api/ ├── package.json └── src/ ├── clients/ ├── config/ ├── types/ └── index.ts ``` Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. ## Shared-пакет `packages/shared` является единым пакетом. ```text packages/shared/ ├── package.json └── src/ ├── lib/ ├── helpers/ └── index.ts ``` В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. ## Имена пакетов и импорты Путь импорта задаётся `name` в `package.json`, а не расположением директории. ```json { "name": "@repo/theme" } ``` ```text packages/infra/theme/package.json ``` ```ts import { ThemeProvider } from '@repo/theme' ``` Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. ```ts // Хорошо import { Button } from '@repo/button' // Плохо import { Button } from '@repo/button/src/button' ``` ## Зависимости На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. ```text apps → packages packages -/→ apps ``` Внутри приложения продолжает действовать обычное направление зависимостей SLM. ```text app → [ layouts | screens ] → widgets → business → infra → ui → shared ``` Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. ## Когда не выносить Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям. ```text # Плохо apps/web/src/screens/home/parts/promo-section/ packages/ui/promo-section/ ``` Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем. ## Конфигурационные пакеты Конфигурационные пакеты не относятся к SLM-архитектуре. Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`. ## Правила * SLM применяется внутри каждого `apps/{app}/src`. * Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`. * Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями. * В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. * В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. * `packages/shared` является единым пакетом для переиспользуемых утилит и helpers. * Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях. * `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты. * Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`. * Пакеты не импортируют приложения. * Межпакетные импорты идут только через публичный API. * Deep imports внутрь пакетов запрещены. * Локальная колокация важнее преждевременного выноса в `packages/*`. --- --- url: /nextjs-style-guide/slm-design/examples/react/factory.md description: Пример создания фабрики business-модуля в React-проекте --- # Создание фабрики Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. ## Структура business-модуля Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. ```text business/customer/ ├── customer.factory.ts ├── hooks/ ├── types/ │ ├── customer.type.ts │ ├── customer-api.type.ts │ └── customer-factory.type.ts ├── ui/ └── index.ts ``` ## Тип публичного API Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. ```ts // business/customer/types/customer-api.type.ts import type { ReactNode } from 'react' import type { Customer } from './customer.type' export type CustomerCardProps = { customer: Customer } export type CustomerApi = { useCustomer: () => Customer | null CustomerCard: (props: CustomerCardProps) => ReactNode } ``` ```ts // business/customer/types/customer-factory.type.ts import type { CustomerApi } from './customer-api.type' export type CustomerFactory = () => CustomerApi ``` ## Фабрика без зависимостей Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. ```ts // business/customer/customer.factory.ts import { useCustomer } from './hooks/use-customer.hook' import { CustomerCard } from './ui/customer-card' import type { CustomerFactory } from './types/customer-factory.type' export const customerFactory: CustomerFactory = () => { return { useCustomer, CustomerCard, } } ``` ```ts // business/customer/index.ts export { customerFactory } from './customer.factory' export type { Customer } from './types/customer.type' export type { CustomerApi } from './types/customer-api.type' export type { CustomerFactory } from './types/customer-factory.type' ``` ## Фабрика с зависимостями Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. ```ts // business/order/types/order-deps.type.ts import type { CustomerApi } from '@/business/customer' export type OrderDeps = { customer: Pick } ``` ```ts // business/order/types/order-factory.type.ts import type { OrderApi } from './order-api.type' import type { OrderDeps } from './order-deps.type' export type OrderFactory = (deps: OrderDeps) => OrderApi ``` ```ts // business/order/order.factory.ts import { createUseOrder } from './hooks/use-order.hook' import { OrderCard } from './ui/order-card' import type { OrderFactory } from './types/order-factory.type' export const orderFactory: OrderFactory = (deps) => { const useOrder = createUseOrder(deps) return { useOrder, OrderCard, } } ``` --- --- url: /nextjs-style-guide/slm-design/examples/react/factory-composition.md description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте --- # Композиция фабрик Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. ## Идея Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. ## Структура screen-модуля ```text screens/home/ ├── home.screen.tsx └── index.ts ``` ## Сборка фабрик Файл: `screens/home/home.screen.tsx`. ```tsx import { customerFactory } from '@/business/customer' import { orderFactory } from '@/business/order' const customer = customerFactory() const order = orderFactory({ customer }) const { useOrder, OrderCard } = order export const HomeScreen = () => { const currentOrder = useOrder() return } ``` `customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. ## Публичный API screen-модуля Файл: `screens/home/index.ts`. ```ts export { HomeScreen } from './home.screen' ``` Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. --- --- url: /nextjs-style-guide/slm-design/examples/react/composition-provider.md description: Пример композиции бизнес-фабрик screen-модуля через React Provider --- # Композиция через Provider Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. ## Идея Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. ## Принципы 1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. 2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. 3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. 4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. 5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. 6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. ## Структура модуля ```text screens/main/ ├── main.screen.tsx ├── providers/ │ └── main-composition.provider.tsx ├── hooks/ │ └── use-main-composition.hook.ts ├── types/ │ └── main-composition.type.ts ├── parts/ │ └── featured-products/ │ ├── featured-products.tsx │ └── index.ts └── index.ts ``` Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. ## Распределение по сегментам | Файл | Сегмент | Назначение | |------|---------|------------| | `main-composition.type.ts` | `types/` | TypeScript-тип композиции | | `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | | `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | | `main.screen.tsx` | корень | Корневой компонент screen-модуля | | `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | ## Тип композиции Файл: `screens/main/types/main-composition.type.ts`. Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. ```ts import type { CatalogApi } from '@/business/catalog' import type { CartApi } from '@/business/cart' export type MainComposition = { catalog: CatalogApi cart: CartApi } ``` ## Context и Provider Файл: `screens/main/providers/main-composition.provider.tsx`. Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. ```tsx import { createContext, type ReactNode } from 'react' import type { MainComposition } from '../types/main-composition.type' export const MainCompositionContext = createContext(null) type Props = { value: MainComposition children: ReactNode } export const MainCompositionProvider = ({ value, children }: Props) => ( {children} ) ``` ## Хук доступа Файл: `screens/main/hooks/use-main-composition.hook.ts`. Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. ```ts import { useContext } from 'react' import { MainCompositionContext } from '../providers/main-composition.provider' export const useMainComposition = () => { const ctx = useContext(MainCompositionContext) if (!ctx) { throw new Error('useMainComposition must be used within MainCompositionProvider') } return ctx } ``` ## Сборка графа в роутере Файл: `app/router.tsx`. Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. ```tsx import { MainScreen, MainCompositionProvider } from '@/screens/main' import { catalogFactory } from '@/business/catalog' import { cartFactory } from '@/business/cart' import { authFactory } from '@/business/auth' const auth = authFactory() const catalog = catalogFactory() const cart = cartFactory({ auth }) const MainRoute = () => ( ) ``` ## Корневой компонент screen Файл: `screens/main/main.screen.tsx`. Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. ```tsx import { useMainComposition } from './hooks/use-main-composition.hook' import { FeaturedProducts } from './parts/featured-products' export const MainScreen = () => { const { catalog } = useMainComposition() const { useCategories, CategoryList } = catalog const categories = useCategories() return (
) } ``` ## Вложенный part Файл: `screens/main/parts/featured-products/featured-products.tsx`. Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. ```tsx import { useMainComposition } from '../../hooks/use-main-composition.hook' export const FeaturedProducts = () => { const { catalog, cart } = useMainComposition() const { useFeatured, ProductCard } = catalog const { addItem } = cart const products = useFeatured() return (
{products.map((product) => ( addItem(product.id)} /> ))}
) } ``` Файл: `screens/main/parts/featured-products/index.ts`. ```ts export { FeaturedProducts } from './featured-products' ``` ## Публичный API screen-модуля Файл: `screens/main/index.ts`. Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. ```ts export { MainScreen } from './main.screen' export { MainCompositionProvider } from './providers/main-composition.provider' ``` ## Почему тип композиции не экспортируется Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. ```ts import type { MainComposition } from '@/screens/main' ``` Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. ## Почему хук не экспортируется Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. ## Почему Provider экспортируется Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. ## Стабильность value Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. ## Расширение на другие screen-модули Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. ```text screens/checkout/providers/checkout-composition.provider.tsx screens/checkout/hooks/use-checkout-composition.hook.ts screens/checkout/types/checkout-composition.type.ts ``` Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. --- --- url: /nextjs-style-guide/basics/tech-stack.md description: Какие библиотеки и инструменты используются в проекте. --- # Технологии и библиотеки Какие библиотеки и инструменты используются в проекте. ## Что используем ### Стек * `React` / `TypeScript` — основной стек для UI и приложения. * `Next.js` — для продуктовых сайтов. ### Архитектура * `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/slm-design/architecture/). ### UI компоненты * `Mantine UI` — базовые UI-компоненты. ### Работа с данными (API) * `@gromlab/api-codegen` — генерация API‑клиентов и типов. * `SWR` — получение, кеширование, ревалидация, дедубликация. * `SWR (useSWRSubscription)` — сокеты, реалтайм подписки. ### Store * `Zustand` — глобальное состояние. ### Локализация * `i18next (i18n)` — локализация всех пользовательских текстов. ### Тестирование * `Vitest` — тестирование. ### Стили * `PostCSS Modules` — изоляция стилей. * `Mobile First` — подход к адаптивной верстке. * `clsx` — конкатенация CSS‑классов. ### Генерация * `@gromlab/create` — шаблонизатор для создания слоёв и других файлов из шаблонов. --- --- url: /nextjs-style-guide/basics/naming.md description: 'Как называть переменные, файлы и прочие сущности в коде.' --- # Именование Как называть переменные, файлы и прочие сущности в коде. ## Базовые правила | Что | Рекомендуется | | ---------------- | ---------------------- | | Папки | `kebab-case` | | Файлы | `kebab-case` | | Переменные | `camelCase` | | Константы | `SCREAMING_SNAKE_CASE` | | Классы | `PascalCase` | | React-компоненты | `PascalCase` | | Хуки | `useSomething` | | CSS классы | `camelCase` | | Ключи enum | `SCREAMING_SNAKE_CASE` | ## Именование файлов Суффикс обозначает роль или тип файла. Пишется в единственном числе. Формат: `name..ts`. **Хуки** * `use-name.hook.ts` — файл хука, функция именуется `useName` **Логика** * `.store.ts` — стор * `.service.ts` — сервис **Корневые компоненты слоёв** * `.screen.tsx` — корневой компонент screen-модуля: `screens/profile/profile.screen.tsx`, компонент `ProfileScreen` * `.layout.tsx` — корневой компонент layout-модуля: `layouts/main/main.layout.tsx`, компонент `MainLayout` Обычные и вложенные модули не получают суффикс слоя: `ui/button/button.tsx`, `screens/profile/parts/activity-feed/activity-feed.tsx`. **Типы и контракты** * `.type.ts` — типы и интерфейсы * `.interface.ts` — интерфейсы * `.enum.ts` — enum * `.dto.ts` — внешние DTO * `.schema.ts` — схемы валидации * `.constant.ts` — константы * `.config.ts` — конфигурация **Утилиты** * `.util.ts` — утилиты * `.helper.ts` — вспомогательные функции * `.lib.ts` — библиотечный код **Тесты** * `.test.ts` — тесты * `.mock.ts` — моки **Хорошо** ```text business/ └── auth-by-email/ ├── ui/ │ └── login-form.tsx ├── hooks/ │ └── use-auth.hook.ts ├── stores/ │ └── auth.store.ts ├── types/ │ └── auth.type.ts ├── auth-by-email.tsx └── index.ts ``` **Плохо** ```text business/ └── authByEmail/ ├── LoginForm.tsx ├── useAuth.ts ├── authStore.ts └── index.ts ``` ## Булевы значения * Использовать префиксы `is`, `has`, `can`, `should`. **Хорошо** ```ts const isReady = true; const hasAccess = false; const canSubmit = true; const shouldRedirect = false; ``` **Плохо** ```ts // Плохо: неясное булево значение без префикса. const ready = true; const access = false; const submit = true; ``` ## События и обработчики * Обработчики начинать с `handle`. * События и колбэки начинать с `on`. **Хорошо** ```ts const handleSubmit = () => { ... }; const onSubmit = () => { ... }; ``` **Плохо** ```ts // Плохо: неочевидное назначение имени. const submitClick = () => { ... }; ``` ## Коллекции * Для массивов использовать имена во множественном числе. * Для словарей/мап — использовать суффиксы `ById`, `Map`, `Dict`. **Хорошо** ```ts const users = []; const usersById = {} as Record; const userIds = ['u1', 'u2']; const ordersMap = new Map(); const featureFlagsDict = { beta: true, legacy: false } as Record; ``` **Плохо** ```ts // Плохо: имя не отражает, что это коллекция. const user = []; // Плохо: словарь назван как массив. const usersMap = []; // Плохо: по имени непонятно, что это словарь. const users = {} as Record; ``` --- --- url: /nextjs-style-guide/basics/code-style.md description: Как оформляется код в проекте. --- # Стиль кода Как оформляется код в проекте. ## Отступы * 2 пробела (не табы). ## Длина строк * Ориентироваться на 100 символов, но превышение допустимо, если строка читается легко. * Переносить выражение на новые строки, когда строка становится плохо читаемой. * Не переносить строку внутри строковых литералов без необходимости. **Хорошо** ```ts const config = createRequestConfig( endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId, }, params: { page, pageSize, sort: 'createdAt', }, }, timeoutMs, ); ``` **Плохо** ```ts // Плохо: длинная строка с вложенными структурами плохо читается. const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs); ``` ## Кавычки * В JavaScript/TypeScript использовать одинарные кавычки. * В JSX/TSX для атрибутов использовать двойные кавычки. * Шаблонные строки использовать только при интерполяции или многострочном тексте. **Хорошо** ```ts const label = 'Сохранить'; const title = `Привет, ${name}`; ``` ```tsx ``` **Плохо** ```ts // Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки. const label = "Сохранить"; const title = 'Привет, ' + name; ``` ```tsx // Плохо: одинарные кавычки в JSX-атрибутах. ``` ## Точки с запятой и запятые * Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным. * В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна. ## Импорты * В именованных импортах использовать пробелы внутри фигурных скобок. * Типы импортировать через `import type`. * `default` экспорт избегать, использовать именованные. `default` импорт допустим (например, стили CSS Modules, сторонние библиотеки). * Избегать импорта всего модуля через `*`. **Хорошо** ```ts import { MyComponent } from 'MyComponent'; import type { User } from '../model/types'; import styles from './styles/button.module.css'; ``` **Плохо** ```ts // Плохо: отсутствие пробелов в именованном импорте. import type {User} from '../model/types'; // Плохо: default экспорт. export default MyComponent; ``` ## Ранние возвраты (early return) * Использовать ранние возвраты для упрощения чтения. * Избегать `else` после `return`. **Хорошо** ```ts const getName = (user?: { name: string }) => { if (!user) { return 'Гость'; } return user.name; }; ``` **Плохо** ```ts // Плохо: лишний else после return усложняет чтение. const getName = (user?: { name: string }) => { if (user) { return user.name; } else { return 'Гость'; } }; ``` ## Форматирование объектов и массивов * В многострочных объектах каждое свойство на новой строке. * В многострочных массивах каждый элемент на новой строке. * Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов. * В однострочных объектах и массивах использовать пробелы после запятых. **Хорошо** ```ts const roles = ['admin', 'editor', 'viewer']; const options = { id: 1, name: 'User' }; const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 }, }; ``` **Плохо** ```ts // Плохо: нет пробелов после запятых и объект слишком длинный для одной строки. const roles = ['admin','editor','viewer']; const options = { id: 1,name: 'User' }; const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } }; ``` --- --- url: /nextjs-style-guide/basics/documentation.md description: Что и как документировать в коде. --- # Документирование Что и как документировать в коде. ## Общие правила * Документировать публичные функции, компоненты, типы, интерфейсы и enum. * Не документировать очевидное — если название говорит само за себя, комментарий не нужен. * Не документировать параметры, возвращаемые значения и типы пропсов — они видны из сигнатуры. * Описание через пользу и назначение, а не через внутреннюю реализацию. * Описание завершается точкой. ## Функции Для документирования функций используется шаблон. Описание механики опционально — добавляется когда логика нетривиальна. **Шаблон** ```ts /** * <Что делает функция в 1 строке>. * * <Опционально: описание сложной механики или важных нюансов>. */ ``` **Хорошо** ```ts /** * Форматирует цену с символом валюты. */ export const formatPrice = (value: number): string => { ... } /** * Рекурсивно собирает дерево категорий из плоского списка. * * Группирует элементы по parentId, начиная с корневых (parentId = null). * Категории без родителя попадают в корень дерева. */ export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { ... } ``` **Плохо** ```ts // Плохо: дублирует сигнатуру. /** * @param value - число * @returns строка с ценой */ ``` ## Компоненты Компонент описывает своё **назначение** и **сценарии применения** — это помогает понять, когда и где его использовать, без необходимости читать реализацию. **Шаблон** ```ts /** * <Назначение компонента в 1 строке>. * * Используется для: * - <сценарий 1> * - <сценарий 2> * - <сценарий 3> */ ``` **Хорошо** ```tsx /** * Контейнер с адаптивной максимальной шириной. * * Используется для: * - обёртки контента страниц с ограничением ширины * - центрирования блоков в лейауте */ export const Container = (props: ContainerProps) => { ... } ``` **Плохо** ```tsx // Плохо: описывает реализацию, а не назначение. /** * Рендерит div с className и htmlAttr. */ // Плохо: нет описания вообще. export const Container = (props: ContainerProps) => { ... } ``` ## Типы, интерфейсы, enum Документируются назначение сущности и каждое её поле. **Хорошо** ```ts /** * Фильтры списка задач. */ export enum TodoFilter { /** Все задачи. */ ALL = 'all', /** Только активные. */ ACTIVE = 'active', /** Только завершённые. */ COMPLETED = 'completed', } /** * Задача пользователя. */ export interface TodoItem { /** Уникальный идентификатор задачи. */ id: string; /** Текст задачи. */ text: string; /** Статус выполнения. */ completed: boolean; } ``` **Плохо** ```ts // Плохо: описывает очевидное. export interface TodoItem { /** id — это id */ id: string; } ``` --- --- url: /nextjs-style-guide/basics/typing.md description: Как типизируется код в проекте. --- # Типизация Как типизируется код в проекте. ## Общие правила * Указывать типы для параметров компонентов и параметров функций. * Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов. * Избегать `any` и `unknown` без необходимости. * Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины. ## React-компоненты * Пропсы компонента типизировать через отдельный `Props`. * Возвращаемый тип компонента не указывать: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата. ## Функции * Для публичных функций указывать возвращаемый тип. * Не полагаться на неявный вывод для важных API. **Хорошо** ```ts export const formatPrice = (value: number): string => { return `${value} ₽`; }; ``` **Плохо** ```ts // Плохо: нет явного возвращаемого типа. export const formatPrice = (value: number) => { return `${value} ₽`; }; ``` ## Работа с any/unknown * `any` использовать только для временных заглушек. * `unknown` сужать через проверки перед использованием. **Хорошо** ```ts const parse = (value: unknown): string => { if (typeof value === 'string') { return value; } return ''; }; ``` **Плохо** ```ts // Плохо: any отключает проверку типов. const parse = (value: any) => value; ``` --- --- url: /nextjs-style-guide/applied/creating-project/from-template.md description: Создание нового проекта на основе готового шаблона. --- # Создание проекта из шаблона Создание нового проекта на основе готового шаблона. ## Что внутри Шаблон — готовый скелет проекта с применёнными правилами стайлгайда: * **Стек:** Next.js (App Router), TypeScript, React. * **Архитектура:** структура папок по SLM, алиасы импортов. * **Качество кода:** Biome (линтер и форматтер), настройки VS Code. * **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты. * **Ассеты:** генерация SVG-спрайтов. * **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов. ## Установка 1. Склонировать шаблон в родительском каталоге будущего проекта: ```bash npx tiged git@gromlab.ru:templates/nextjs.git my-app ``` `tiged` копирует снимок репозитория без истории git. Имя каталога (`my-app`) заменяется на нужное. 2. Установить зависимости: ```bash cd my-app npm install ``` 3. Проверить сборку: ```bash npm run build ``` Сборка должна завершиться без ошибок. ## Правила * **Шаблон — источник истины.** Не добавлять, не удалять и не переименовывать файлы шаблона «для приведения к канону»: шаблон уже канонический. Любое несоответствие — баг шаблона, а не проекта. * **Менеджер пакетов — npm.** Отклонение (pnpm, yarn, bun) — только по явному решению с пониманием, что стайлгайд этого не предусматривает. * **Не инициализировать git заново** автоматически. `tiged` намеренно не создаёт `.git/` — решение о репозитории принимает разработчик. --- --- url: /nextjs-style-guide/applied/creating-project/manual.md description: Поэтапное создание нового проекта без использования шаблона. --- # Создание проекта вручную Поэтапное создание нового проекта без использования шаблона. ## Состав эталонного проекта | Компонент | Роль | Раздел | |-----------|------|--------| | Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/applied/creating-project/nextjs) | | Алиасы | Импорты по слоям SLM | [Алиасы](/applied/aliases) | | Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/applied/biome) | | Стили | Глобальные токены и breakpoints | [Стили](/applied/styles/styles-setup) | | PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/applied/postcss) | | SVG-спрайты | Иконки через ``, управление цветом | [SVG-спрайты](/applied/svg-sprites/svg-sprites-setup) | | VS Code | Настройки редактора и расширения | [VS Code](/applied/vscode) | | Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/applied/templates/templates-setup) | Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном. ## Канон раскладки В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/applied/project-structure), [Архитектура](/slm-design/architecture/)). В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`. ## Порядок установки Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами. ### 1. Next.js Скелет фреймворка — обязательный первый шаг, остальное опирается на него. См. [Next.js](/applied/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить. ### 2. Алиасы Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов. См. [Алиасы](/applied/aliases). ### 3. Biome Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки. См. [Biome](/applied/biome). ### 4. Стили (базовая инфраструктура) Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится. См. [Стили](/applied/styles/styles-setup). ### 5. PostCSS CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`. См. [PostCSS](/applied/postcss). ### 6. SVG-спрайты Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента ``. См. [SVG-спрайты](/applied/svg-sprites/svg-sprites-setup). ### 7. VS Code Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`). См. [VS Code](/applied/vscode). ### 8. Шаблоны генерации Папка `.templates/` для генератора модулей `@gromlab/create`. См. [Шаблоны генерации](/applied/templates/templates-setup). ## Правила * **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome). * **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден. * **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6). * **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены. * **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство. --- --- url: /nextjs-style-guide/applied/creating-project/nextjs.md description: Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. --- # Чистая установка Next.js Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. ## Требования * Node.js 18.18+ (рекомендуется LTS 20+). * npm 10+. * Рабочая папка пуста, либо для установки выбрана подпапка (`create-next-app@16+` отказывается ставиться в непустую директорию). ## Установка ### 1. Инициализация через `create-next-app` Флаги зафиксированы и не согласовываются — это канон стайлгайда: ```bash npx create-next-app@latest my-app \ --typescript \ --app \ --src-dir \ --import-alias "@/*" \ --no-eslint \ --no-tailwind \ --use-npm ``` | Флаг | Значение | Почему так | |------|----------|------------| | `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](/basics/typing)) | | `--app` | App Router | Pages Router не используется | | `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](/applied/project-structure)) | | `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/applied/aliases)) | | `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/applied/biome)) | | `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/applied/styles/styles-usage)) | | `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах | ### 2. Очистить дефолтный шаблон `create-next-app` генерирует демо-страницу со стилями и ассетами, а Next.js 16+ дополнительно кладёт в корень собственные `AGENTS.md` и `CLAUDE.md` — всё это удаляется. ```bash rm src/app/page.module.css rm src/app/globals.css rm public/next.svg public/vercel.svg public/file.svg public/globe.svg public/window.svg rm -f AGENTS.md CLAUDE.md ``` Заменить `src/app/page.tsx` на минимальный: ```tsx // src/app/page.tsx export default function HomePage() { return

Home

} ``` Очистить `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 ( {children} ) } ``` ### 3. Создать папку `src/shared/styles/` Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](/applied/project-structure)). ```bash mkdir -p src/shared/styles ``` Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах. ## Правила * **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы. * **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает. * **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/applied/aliases)). * **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся. * **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе. ## Проверка установки * В корне проекта: `next.config.ts`, `tsconfig.json`, `package.json`. * В `package.json`: Next.js установлен, нет `eslint`, `tailwindcss`. * В `src/app/` присутствуют минимальные `page.tsx` и `layout.tsx`. `globals.css`, `page.module.css` отсутствуют. Каталогов `styles/`, `assets/`, `providers/`, `components/` в `src/app/` нет. * Папка `src/shared/styles/` создана (пустая). * В `src/` из слоёв SLM присутствуют только `app/` и `shared/` (с `styles/`). Посторонних каталогов нет. * В `public/` удалены `next.svg`, `vercel.svg`, `file.svg`, `globe.svg`, `window.svg`. * В корне проекта нет `AGENTS.md` и `CLAUDE.md` от Next.js. * `npm run build` завершается успешно. * Пакетный менеджер — npm (нет `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`). --- --- url: /nextjs-style-guide/applied/project-structure.md description: Из чего состоит проект и где что лежит. --- # Структура проекта Из чего состоит проект и где что лежит. ## Корень репозитория ```text project-root/ ├── .templates/ # Шаблоны для генерации модулей ├── .vscode/ # Настройки и рекомендуемые расширения VS Code ├── public/ # Статика, доступная по прямому URL ├── src/ # Исходный код приложения ├── .env.example # Переменные окружения проекта (шаблон) ├── .env # Переменные окружения проекта (не коммитить) ├── .gitignore ├── AGENTS.md # Инструкции для AI-агентов ├── biome.json # Линтер и форматтер (вместо ESLint + Prettier) ├── next.config.ts # Конфигурация Next.js ├── package.json # Зависимости и скрипты ├── postcss.config.mjs # Конфигурация PostCSS └── tsconfig.json # Конфигурация TypeScript ``` ## Папка `public/` Хранит статические файлы, которые отдаются по прямому URL без обработки сборщиком: ```text public/ └── og-image.png ``` Компоненты, стили и другой исходный код здесь не размещаются. ## Папка `src/` ```text src/ ├── app/ # Роутинг Next.js и точка входа приложения ├── layouts/ # Каркасы страниц (header, footer, sidebar) ├── screens/ # Контент конкретной страницы ├── widgets/ # Составные блоки интерфейса, не привязанные к домену ├── business/ # Бизнес-домены (auth, catalog, orders) ├── infra/ # Техсервисы (theme, i18n, API-адаптеры) ├── ui/ # UI-кит без бизнес-логики └── shared/ # Общие ресурсы (утилиты, типы, стили) ``` Принципы организации слоёв описаны в разделе [Архитектура](/slm-design/architecture/). ### Папка `app/` Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты). `app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы. Подробнее о границах слоя: [Архитектура → Слои → App](/slm-design/architecture/layers#слой-app). ```text src/app/ ├── layout.tsx # Корневой layout └── page.tsx # Главная страница ``` ## Папка `.templates/` Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля: ```text .templates/ ├── component/ # Шаблон компонента ├── screen/ # Шаблон экрана ├── layout/ # Шаблон layout ├── widget/ # Шаблон виджета ├── module/ # Шаблон бизнес-модуля └── store/ # Шаблон стора ``` Подробнее о генерации описано в разделе [Шаблоны генерации](/applied/templates/templates-intro). ## Конфигурационные файлы | Файл | Назначение | |---|---| | `next.config.ts` | Настройки Next.js: редиректы, переменные окружения, webpack | | `tsconfig.json` | Настройки TypeScript: пути, строгость, таргет | | `biome.json` | Правила линтера и форматтера Biome | | `postcss.config.mjs` | Подключение PostCSS-плагинов (CSS Modules, custom media) | | `package.json` | Зависимости, версии, npm-скрипты | | `AGENTS.md` | Инструкции для AI-агентов, работающих в проекте | ## Переменные окружения * `.env` — переменные окружения проекта, запрещено коммитить * `.env.example` — шаблон, коммитится в репозиторий Переменные с префиксом `NEXT_PUBLIC_` доступны в клиентском коде. Остальные доступны только на сервере. --- --- url: /nextjs-style-guide/applied/page-level.md description: Как работать со страницами и другими файлами роутинга Next.js App Router. --- # Файлы роутинга Как работать со страницами и другими файлами роутинга Next.js App Router. ## Назначение `src/app/**` — точка входа приложения и слой файлового роутинга Next.js. Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen. Границы слоя описаны в [Архитектура → Слои → App](/slm-design/architecture/layers#слой-app). ## Граница ответственности | Область | Где живёт | |---|---| | Файлы маршрутов (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`) | `src/app/**` | | Параметры маршрута, `metadata`, `redirect()`, `notFound()` | `src/app/**` | | Серверные запросы для первого рендера | `src/app/**`, через готовые клиенты и сервисы нижних слоёв | | Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв | | UI страницы | `screens/` | | Каркас страницы: header, footer, sidebar | `layouts/` | | Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infra/`, `shared/`) | | CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` | ## Что можно делать в `page.tsx` * Экспортировать `metadata` или `generateMetadata`. * Читать `params` и `searchParams`. * Нормализовать и валидировать параметры маршрута. * Делать серверные запросы для первого рендера через готовые клиенты или сервисы. * Вызывать `redirect()` и `notFound()`. * Готовить начальные данные для screen. * Готовить SWR `fallback` и передавать его в готовый провайдер. * Подключать готовый провайдер стора страницы и передавать начальное состояние. * Рендерить screen или композицию из готовых обёрток и screen. ## Что запрещено * Писать UI-разметку страницы прямо в файле роутинга. * Создавать локальные компоненты внутри `src/app/**`. * Добавлять CSS Modules, стили компонентов, `components/`, `styles/`, `hooks/`, `stores/`, `services/` внутри `src/app/**`. * Реализовывать провайдеры, сторы, хуки, API-клиенты или сервисы в файлах роутинга. * Размещать бизнес-логику, мапперы и правила предметной области в файлах роутинга. * Вызывать `useSWR` и доменные клиентские хуки в файлах роутинга. ## Страницы Страница объявляется через `export default function`. Для серверных запросов используется `async function`. ```tsx import type { Metadata } from 'next' import { ProfileScreen } from 'screens/profile' export const metadata: Metadata = { title: 'Профиль', description: 'Страница профиля пользователя', } type ProfilePageProps = { params: Promise<{ id: string }> } export default async function ProfilePage({ params }: ProfilePageProps) { const { id } = await params return } ``` ## Данные первого рендера Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя. ```tsx import { notFound } from 'next/navigation' import { userApi } from 'infra/backend-api' import { UserScreen } from 'screens/user' type UserPageProps = { params: Promise<{ id: string }> } export default async function UserPage({ params }: UserPageProps) { const { id } = await params const user = await userApi.users.get(id) if (!user) { notFound() } return } ``` Если данные нужны нескольким клиентским SWR-хукам, файл роутинга может обернуть дерево в `SWRConfig` и передать `fallback`. Запросы стартуют на сервере, а клиентские хуки получают данные из кеша. Ключи `fallback` должны совпадать с ключами внутри GET-хуков REST-клиента. Для array-key используется `unstable_serialize`. ```tsx import type { ReactNode } from 'react' import { SWRConfig, unstable_serialize } from 'swr' import { backendApi, getCurrentUserKey, getPostListKey, } from 'infra/backend-api' type FeedLayoutProps = { children: ReactNode } export default async function FeedLayout({ children }: FeedLayoutProps) { const userPromise = backendApi.user.getCurrent() const postsPromise = backendApi.posts.list() return ( {children} ) } ``` Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](/applied/data-fetch/), [Начальные данные для клиентских хуков](/applied/data-fetch/client-hooks-initial-data). ## Инициализация состояния Файл роутинга может подключить готовый провайдер стора страницы, если состояние зависит от маршрута или данных первого рендера. Реализация стора и провайдера не размещается в `src/app/**`. ```tsx import { ProfileScreen, ProfileStoreProvider } from 'screens/profile' type ProfilePageProps = { params: Promise<{ id: string }> } export default async function ProfilePage({ params }: ProfilePageProps) { const { id } = await params return ( ) } ``` ## Layout `layout.tsx` подключает готовую инициализацию приложения: глобальные стили, провайдеры и верхнеуровневые обёртки из нижних слоёв. Вёрстка layout-каркаса выносится в слой `layouts/`. Реализация провайдеров, стилей и UI не размещается в `app/`. ## Error и Not Found `error.tsx` и `not-found.tsx` делегируют разметку готовым screen или widget. В файле роутинга остаётся только адаптация API Next.js к пропсам нижнего слоя. ```tsx 'use client' import { ErrorScreen } from 'screens/error' type ErrorPageProps = { error: Error & { digest?: string } reset: () => void } const ErrorPage = ({ error, reset }: ErrorPageProps) => { return } export default ErrorPage ``` --- --- url: /nextjs-style-guide/applied/component.md description: Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля. --- # Компонент Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля. ## Назначение Архитектурное определение компонента описано в разделе [Модули → Компонент](/slm-design/architecture/modules#компонент), а структура сегмента `ui/` — в разделе [Сегменты → ui/](/slm-design/architecture/segments#сегмент-ui). Эта страница не повторяет архитектурные ограничения. Она показывает, каким должен быть результат генерации компонента: структура папки, `.tsx`, типы, стили и локальный экспорт. ::: danger Компоненты не создаются вручную Компоненты в проекте создаются только через кодогенератор: через [VS Code](/applied/templates/templates-usage#через-vs-code) или [CLI](/applied/templates/templates-usage#через-cli). Ручное создание компонента запрещено. Это грубое нарушение правил работы в проекте для разработчика и AI-ассистента. Если в проекте нет шаблона `.templates/component`, сначала создайте шаблон по разделу [Создание шаблонов](/applied/templates/templates-create), и только потом генерируйте компонент на его основе. ::: ## Создание 1. Проверьте, что в проекте есть шаблон `.templates/component`. 2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/applied/templates/templates-create). 3. Сгенерируйте компонент через [VS Code или CLI](/applied/templates/templates-usage). Структура и код ниже показывают ожидаемый результат генерации. Их нельзя использовать как инструкцию для ручного создания файлов. ## Структура Компонент размещается в `ui/{component-name}/` родительского модуля. Для каждого компонента обязательны `.tsx`, типы, стили и локальный `index.ts`. ```text user-card/ └── ui/ └── user-status/ ├── styles/ │ └── user-status.module.css ├── types/ │ └── user-status-props.type.ts ├── user-status.tsx └── index.ts ``` ## Реализация Пример ниже показывает файлы базового компонента. ### Типы Файл типов делится на три части: * `UserStatusParams` — собственные параметры компонента. Здесь лежат только данные, которые нужны именно этому компоненту. * `RootAttrs` — параметры корневой обёртки: `div`, `span`, `a`, `button` или другого HTML-элемента. Если компонент сам управляет `children`, они исключаются через `Omit`. * `UserStatusProps` — итоговые пропсы компонента. Тип объединяет собственные параметры и параметры корневой обёртки. Собственные параметры и их поля документируются по правилам раздела [Документирование → Типы, интерфейсы, enum](/basics/documentation#типы-интерфейсы-enum). `user-card/ui/user-status/types/user-status-props.type.ts` ```ts import type { ComponentPropsWithoutRef } from 'react' /** * Параметры UserStatus. */ export type UserStatusParams = { /** Текст статуса пользователя. */ label: string /** Доступен ли пользователь сейчас. */ isOnline: boolean } /** Атрибуты корневого элемента без children. */ type RootAttrs = Omit, 'children'> export type UserStatusProps = RootAttrs & UserStatusParams ``` ### TSX В `.tsx` лежит только сам компонент: * Компонент объявляется через `const` и именованный экспорт. * `React.FC` не используется. * Параметры компонента типизируются через `Props`. * Возвращаемый тип не указывается: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата. * JSDoc-комментарий обязателен и пишется по правилам раздела [Документирование → Компоненты](/basics/documentation#компоненты). * Пропсы деструктурируются в теле компонента, а не в сигнатуре. * Из пропсов обязательно выделяются `className` и `...rootAttrs`. * Функция конкатенации CSS-классов импортируется и именуется `cl`. * Корневой CSS-класс всегда называется `.root`. Комментарий описывает назначение и сценарии применения компонента, а не DOM-разметку или внутреннюю реализацию. `className` — внешний CSS-класс, который родитель может передать компоненту. `rootAttrs` — остальные атрибуты корневой обёртки: `id`, `aria-*`, `data-*`, обработчики событий и другие HTML-атрибуты. Они прокидываются на корневой DOM-элемент компонента. `.root` нужен, чтобы в DevTools быстро находить корневой DOM-узел компонента и одинаково подключать внешний `className` к реальному корню. `user-card/ui/user-status/user-status.tsx` ```tsx import cl from 'clsx' import type { UserStatusProps } from './types/user-status-props.type' import styles from './styles/user-status.module.css' /** * Статус пользователя в карточке профиля. * * Используется для: * - отображения текущей доступности пользователя * - визуального выделения онлайн- и офлайн-состояний */ export const UserStatus = (props: UserStatusProps) => { const { label, isOnline, className, ...rootAttrs } = props return ( {label} ) } ``` ### Стили `user-card/ui/user-status/styles/user-status.module.css` ```css .root { display: inline-flex; align-items: center; gap: 6px; color: var(--color-text-muted); } .root::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } .online { color: var(--color-success); } ``` ### Локальный экспорт `user-card/ui/user-status/index.ts` ```ts export { UserStatus } from './user-status' export type { UserStatusProps } from './types/user-status-props.type' ``` --- --- url: /nextjs-style-guide/applied/module.md description: Как должен выглядеть сгенерированный SLM-модуль в проекте. --- # Модуль Как должен выглядеть сгенерированный SLM-модуль в проекте. ## Назначение Архитектурное определение модуля описано в разделе [Архитектура → Модули](/slm-design/architecture/modules). Список сегментов описан в разделе [Архитектура → Сегменты](/slm-design/architecture/segments). Эта страница показывает прикладное оформление трёх типов модулей: UI, бизнес и инфраструктурный. ## Создание 1. Проверьте, что в проекте есть нужный шаблон в `.templates/`. 2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/applied/templates/templates-create). 3. Сгенерируйте модуль через [VS Code или CLI](/applied/templates/templates-usage). ## Типы модулей Архитектура определяет три типа модулей ([Типы модулей](/slm-design/architecture/modules#типы-модулей)): | Тип | Обязательный файл | Описание | |---|---|---| | UI-модуль | `{name}.tsx` | Модуль, выросший из компонента | | Бизнес-модуль | `{name}.factory.ts` | Модуль вокруг публичного runtime API | | Инфраструктурный модуль | нет | Модуль вокруг технического сервиса | ## UI-модуль UI-модуль — это компонент, который перерос ограничения компонента: получил собственные хуки, вложенные модули в `parts/`, сценарную логику или публичный API. Внутренняя структура та же, что у компонента: корневой `.tsx`, типы, стили, `ui/`. Но без ограничений компонента. Подробное оформление компонентов внутри `ui/` описано в разделе [Компонент](/applied/component). ## Бизнес-модуль Бизнес-модуль строится вокруг публичного runtime API. Ключевой файл — фабрика (`{name}.factory.ts`), которая возвращает всё, что нужно внешнему коду в runtime. Архитектурное описание фабрики: [Архитектура → Фабрика](/slm-design/architecture/modules#фабрика). ### Структура ```text business/customer/ ├── customer.factory.ts ├── index.ts └── types/ ├── customer.type.ts ├── customer-api.type.ts ├── customer-deps.type.ts └── customer-factory.type.ts ``` ### Типы `business/customer/types/customer-api.type.ts` ```ts export type CustomerApi = { useCustomer: () => Customer CustomerCard: (props: CustomerCardProps) => ReactNode } ``` `business/order/types/order-deps.type.ts` ```ts export type OrderDeps = { customer: Pick } ``` `business/order/types/order-factory.type.ts` ```ts export type OrderFactory = (deps: OrderDeps) => OrderApi ``` ### Фабрика без зависимостей `business/customer/customer.factory.ts` ```ts import type { CustomerFactory } from './types/customer-factory.type' export const customerFactory: CustomerFactory = () => { return { useCustomer, CustomerCard, } } ``` ### Фабрика с зависимостями `business/order/order.factory.ts` ```ts import type { OrderFactory } from './types/order-factory.type' export const orderFactory: OrderFactory = (deps) => { return { useOrder, OrderCard, } } ``` ### Композиция на уровне screen ```tsx // screens/home/home.screen.tsx import { customerFactory } from '@/business/customer' import { orderFactory } from '@/business/order' const customer = customerFactory() const order = orderFactory({ customer }) const { useOrder, OrderCard } = order export const HomeScreen = () => { const currentOrder = useOrder() return } ``` ## Инфраструктурный модуль Инфраструктурный модуль строится вокруг технического сервиса или интеграции. Его структура определяется природой сервиса — фиксированного корневого файла нет. Архитектурное описание: [Архитектура → Типы модулей → Инфраструктурный модуль](/slm-design/architecture/modules#инфраструктурный-модуль). Пример модуля темы: ```text theme/ ├── index.ts ├── config/ ├── hooks/ ├── styles/ └── ui/ ``` Пример модуля API-клиента: ```text backend-api/ ├── backend-api.client.ts ├── config/ ├── types/ └── index.ts ``` --- --- url: /nextjs-style-guide/applied/rest-client.md description: Настройка REST-клиента сервиса для работы с внешним API. --- # REST-клиент Настройка REST-клиента сервиса для работы с внешним API. ## Настройка Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`. На этом этапе внешний API оформляется как модуль слоя `infra/`. Клиент отвечает за: * генерацию или ручное описание методов API; * настройку `baseUrl`; * заголовки и авторизацию; * обработку ошибок; * кастомизацию и расширение типов; * GET-хуки для клиентских компонентов; * прямое использование методов клиента в серверном коде и submit-функциях; * публичный API модуля. Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную. GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента. Подробнее: * [Настройка REST-клиента](/applied/rest-client/setup/) * [Автогенерация из OpenAPI](/applied/rest-client/setup/auto) * [Ручное создание](/applied/rest-client/setup/manual) * [GET-хуки REST-клиента](/applied/rest-client/setup/hooks) * [Использование REST-клиента](/applied/rest-client/usage) ## Как читать раздел Если API ещё не подключён — начните с [Настройки REST-клиента](/applied/rest-client/setup/). Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](/applied/rest-client/usage). Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](/applied/data-fetch/). Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/applied/rest-client/setup/hooks). Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`. --- --- url: /nextjs-style-guide/applied/rest-client/setup.md description: Подготовка REST-клиента сервиса к использованию. --- # Настройка REST-клиента Подготовка REST-клиента сервиса к использованию. ## Что настраиваем REST-клиент — это infra-модуль, через который проект работает с внешним REST API. На этапе настройки нужно подготовить клиент сервиса: оболочку клиента, методы API и GET-хуки для клиентских компонентов. ## Из чего состоит клиент REST-клиент состоит из трёх основных частей: 1. **Клиент** — самописная оболочка над транспортом. 2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API. 3. **GET-хуки** — SWR-обёртки для GET-запросов. Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису. ## Клиент Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса. Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта. `client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика. `baseUrl` API задаётся обязательной env-переменной без fallback-значения в коде. Не используйте записи вроде `process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL ?? 'http://localhost:8080/api/v3'` или `?? ''`: локальный URL должен лежать в `.env.local`, а отсутствие переменной должно приводить к явной ошибке конфигурации. ## Методы Методы описывают конкретные запросы к API. Они появляются одним из двух способов: * генерируются из OpenAPI в `generated/`; * создаются вручную в `methods/`. Подробности: * [Автогенерация из OpenAPI](/applied/rest-client/setup/auto) * [Ручное создание](/applied/rest-client/setup/manual) ## GET-хуки Для GET-запросов добавляются GET-хуки REST-клиента. Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components. GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`. Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`. Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration`. Подробности: * [GET-хуки REST-клиента](/applied/rest-client/setup/hooks) ## Структура модуля ```text src/infra/{service-name}/ ├── client.ts # самописная оболочка и инстанс клиента ├── generated/ или methods/ # методы API ├── hooks/ # GET-хуки REST-клиента ├── types/ # DTO, именованные response-типы и расширения типов ├── errors/ # ошибки API, если нужны └── index.ts # публичный API ``` `index.ts` — единственная точка входа в REST-модуль для внешнего кода. Если generated-метод возвращает безымянный тип вроде `Record`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`. ## Что делаем дальше 1. Создайте методы клиента: [Автогенерация из OpenAPI](/applied/rest-client/setup/auto) или [Ручное создание](/applied/rest-client/setup/manual). 2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/applied/rest-client/setup/hooks). 3. Проверьте прямые вызовы клиента: [Использование REST-клиента](/applied/rest-client/usage). 4. После настройки клиента переходите к [Получению данных](/applied/data-fetch/). --- --- url: /nextjs-style-guide/applied/rest-client/setup/auto.md description: Генерация REST-клиента из OpenAPI-спецификации. --- # Автогенерация REST-клиента Генерация REST-клиента из OpenAPI-спецификации. ## Когда использовать Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки. ## Пример API В примерах используется Swagger Petstore: ```text https://petstore3.swagger.io/api/v3/openapi.json ``` Имена модуля: ```text src/infra/pet-store-api/ petStoreApi pet-store-api.generated.ts ``` ## Скрипт генерации `@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию. ```json { "scripts": { "codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-api/generated -n pet-store-api.generated" } } ``` Параметры: * `-i` — путь к OpenAPI-спецификации: URL или локальный файл. * `-o` — директория для сгенерированного файла. * `-n` — имя сгенерированного файла без `.ts`. Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции. ## Генерация ```bash npm run codegen:pet-store-api ``` Ожидаемый результат: ```text src/infra/pet-store-api/generated/ └── pet-store-api.generated.ts ``` Сгенерированный файл не правится руками и коммитится в репозиторий. ## Проверка методов После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов. Для Petstore нужны GET-операции вида: ```ts petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }) petStoreApi.pet.getPetById({ petId: 10 }) ``` Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом. ## Алгоритм для агента После генерации агент должен действовать по шагам: 1. Открыть `generated/{service-name}.generated.ts`. 2. Найти фактические имена GET-методов клиента. 3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа. 4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента. 5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай. 6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`. 7. В key-функции вернуть `null`, если обязательные параметры не готовы. 8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration`. 9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`. 10. Экспортировать хук и key-функцию из `hooks/index.ts`. 11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`. Что агент не должен делать: * Не использовать ключ `--swr` генератора. * Не править `generated/*.generated.ts` руками. * Не добавлять GET-хуки для POST, PUT, PATCH, DELETE. * Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук. * Не создавать словари enum-маппинга внутри GET-хука. * Не объявлять DTO и response-типы в файле хука. * Не вызывать `useSWR` условно. * Не добавлять `throw` в fetcher для неготовых params. ## `client.ts` Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента. ```ts // src/infra/pet-store-api/client.ts import { Api, HttpClient } from './generated/pet-store-api.generated' const baseUrl = process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL if (!baseUrl) { throw new Error('NEXT_PUBLIC_PET_STORE_API_BASE_URL is required') } const httpClient = new HttpClient({ baseUrl, baseApiParams: { secure: false, headers: { 'Content-Type': 'application/json', }, }, }) export const petStoreApi = new Api(httpClient) ``` Локальное значение `NEXT_PUBLIC_PET_STORE_API_BASE_URL` задаётся в `.env.local`. Не добавляйте fallback вроде `?? 'http://localhost:8080/api/v3'` или `?? ''`: если env-переменная не задана, клиент должен падать с явной ошибкой конфигурации. `client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента. ## GET-хуки GET-хуки пишутся вручную после проверки generated-методов. Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`: ```ts // src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts import type { SWRConfiguration } from 'swr' import useSWR from 'swr' import { petStoreApi } from '../client' import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated' export const getPetDetailKey = (params?: GetPetByIdParams | null) => { if (!params?.petId) { return null } return ['pet-store-api', `/pet/${params.petId}`] as const } /** * Получает детальную карточку питомца с кешированием результата. */ export const useGetPetDetail = ( params?: GetPetByIdParams | null, config?: SWRConfiguration, ) => { const key = getPetDetailKey(params) const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) return useSWR(key, fetcher, config) } ``` Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](/applied/rest-client/setup/hooks). ## Расширение сгенерированных типов Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`. ```text src/infra/biocad-less-api/ ├── generated/ │ └── biocad-less-api.generated.ts ├── types/ │ ├── term.ts │ └── index.ts ├── client.ts └── index.ts ``` Пример расширения generated-типа: ```ts // src/infra/biocad-less-api/types/term.ts import type { TermRecordItem } from '../generated/biocad-less-api.generated' declare module '../generated/biocad-less-api.generated' { interface TermRecordItem { media?: { file?: string title?: string url?: string } } } export type TermRecordItemExtended = Omit< TermRecordItem, 'categories' | 'tags' | 'fields' > & { categories?: Array<{ _id?: string id?: string slug?: string name?: string }> tags?: Array<{ _id?: string id?: string slug?: string name?: string }> fields?: Record } ``` ```ts // src/infra/biocad-less-api/types/index.ts export type { TermRecordItemExtended } from './term' ``` `declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл. ## Публичный API ```ts // src/infra/pet-store-api/index.ts export { petStoreApi } from './client' export type { FindPetsByStatusParams, GetPetByIdParams, Pet, } from './generated/pet-store-api.generated' export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated' export * from './hooks' ``` Наружу импортируют только из `infra/pet-store-api`, не из `generated/`. Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`: ```ts // src/infra/biocad-less-api/index.ts export type { TermRecordItemExtended } from './types' ``` ## Регенерация При изменении OpenAPI-схемы: ```bash npm run codegen:pet-store-api ``` Что меняется: * `generated/pet-store-api.generated.ts` — перезаписывается генератором. * `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически. Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля. ## Следующий шаг После генерации и настройки `client.ts` проверьте [использование REST-клиента](/applied/rest-client/usage) или добавьте [GET-хук REST-клиента](/applied/rest-client/setup/hooks) для Client Components. --- --- url: /nextjs-style-guide/applied/rest-client/setup/manual.md description: 'Создание REST-клиента вручную, когда OpenAPI нет или он неполный.' --- # Ручное создание REST-клиента Создание REST-клиента вручную, когда OpenAPI нет или он неполный. ## Когда использовать Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации. Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом. ## Что нужно создать ```text src/infra/ └── pet-project-api/ ├── methods/ │ └── posts.ts ├── hooks/ │ └── index.ts ├── types/ │ ├── client.ts │ ├── post.ts │ └── index.ts ├── errors/ │ └── pet-project-api.error.ts ├── client.ts └── index.ts ``` | Файл | Роль | |------|------| | `client.ts` | Базовый транспорт и создание инстанса клиента | | `methods/` | Методы API по сущностям | | `types/` | DTO запросов, ответов и типы клиента | | `errors/` | Ошибки конкретного API | | `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components | | `index.ts` | Публичный API REST-модуля | ## DTO и типы API DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы. ```ts // src/infra/pet-project-api/types/post.ts export type PostDto = { id: string slug: string title: string } export type PostListQueryDto = { limit?: number category?: string } ``` ```ts // src/infra/pet-project-api/types/index.ts export type { PostDto, PostListQueryDto } from './post' ``` Типы, которые нужны только базовому транспорту, можно держать отдельно: ```ts // src/infra/pet-project-api/types/client.ts export type QueryParams = Record ``` ## Ошибка API Ошибка API тоже относится к REST-модулю. ```ts // src/infra/pet-project-api/errors/pet-project-api.error.ts export class PetProjectApiError extends Error { constructor( public readonly status: number, message: string, ) { super(message) this.name = 'PetProjectApiError' } } ``` ## Базовый клиент `client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв. ```ts // src/infra/pet-project-api/client.ts import { PetProjectApiError } from './errors/pet-project-api.error' import type { QueryParams } from './types/client' export class PetProjectApiClient { constructor( private readonly baseUrl: string, private readonly defaultHeaders: Record = {}, ) {} async get(path: string, params: QueryParams = {}): Promise { const base = `${this.baseUrl.replace(/\/+$/, '')}/` const url = new URL(path.replace(/^\/+/, ''), base) Object.entries(params).forEach(([key, value]) => { url.searchParams.set(key, String(value)) }) const response = await fetch(url, { headers: { Accept: 'application/json', ...this.defaultHeaders, }, }) if (!response.ok) { throw new PetProjectApiError(response.status, response.statusText) } return response.json() as Promise } } ``` Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API. ## Методы API Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI. ```ts // src/infra/pet-project-api/methods/posts.ts import type { PetProjectApiClient } from '../client' import type { PostDto, PostListQueryDto } from '../types/post' export function postsMethods(client: PetProjectApiClient) { return { /** GET /posts */ list: (query: PostListQueryDto = {}) => client.get('posts', query), /** GET /posts/{slug} */ get: (slug: string) => client.get(`posts/${slug}`), } } ``` Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`. ## Публичный API `index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля. ```ts // src/infra/pet-project-api/index.ts import { PetProjectApiClient } from './client' import { postsMethods } from './methods/posts' const baseUrl = process.env.NEXT_PUBLIC_PET_PROJECT_API_BASE_URL if (!baseUrl) { throw new Error('NEXT_PUBLIC_PET_PROJECT_API_BASE_URL is required') } const client = new PetProjectApiClient( baseUrl, { 'Content-Type': 'application/json' }, ) export const petProjectApi = { posts: postsMethods(client), } export { PetProjectApiError } from './errors/pet-project-api.error' export type { PostDto, PostListQueryDto } from './types' export * from './hooks' ``` Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля. ## Правила * `fetch` используется только внутри базового клиента. * DTO запросов и ответов живут в `types/`. * `client.ts` не содержит DTO, GET-хуки и бизнес-логику. * `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде. * Методы лежат в `methods/` и возвращают DTO. * GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components. * Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`. Следующий шаг: [Использование REST-клиента](/applied/rest-client/usage), [GET-хуки REST-клиента](/applied/rest-client/setup/hooks) или [Получение данных](/applied/data-fetch/). --- --- url: /nextjs-style-guide/applied/rest-client/setup/hooks.md description: Прозрачные SWR-обёртки над GET-методами REST-клиента. --- # GET-хуки REST-клиента Прозрачные SWR-обёртки над GET-методами REST-клиента. ## Зачем нужны GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую. ## Где лежат GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним: ```text src/infra/ └── pet-store-api/ ├── client.ts ├── generated/ ├── hooks/ │ ├── use-get-pet-list.hook.ts │ ├── use-get-pet-detail.hook.ts │ └── index.ts ├── types/ └── index.ts ``` ## Контракт * Один GET-хук = один GET-метод клиента. * Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`. * Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`. * Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration`. * Для GET-метода без параметров хук принимает только `config?: SWRConfiguration`. * Key-функция принимает те же `params`, что и хук. * Key-функция возвращает `null`, если обязательные параметры не готовы. * Проверка готовности запроса живёт в key-функции, а не в теле хука. * Хук вызывает `useSWR` один раз и безусловно. * Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`. * Внутри только SWR-механика: key, fetcher, `useSWR`, `config`. * Хук возвращает тип ответа API: generated-тип или DTO из `types/`. * Хук не объединяет несколько запросов. * Хук не маппит DTO в доменную модель. * Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`. * Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние. ## Формат SWR-ключа SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией. Формат ключа: ```ts ['pet-store-api', '/pet/10'] as const ``` * Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`. * Второй элемент — endpoint запроса: path и query string. * Key-функция возвращает `null`, когда запрос нельзя выполнять. * Key-функция нужна и GET-хуку, и `SWRConfig fallback`. * Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`. * Не используйте только строку endpoint без имени сервиса. Примеры ключей: ```ts export const getPetDetailKey = (params?: GetPetByIdParams | null) => { if (!params?.petId) { return null } return ['pet-store-api', `/pet/${params.petId}`] as const } ``` ```ts export const getPetListKey = (params?: FindPetsByStatusParams | null) => { if (!params?.status) { return null } return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const } ``` ```ts export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => { if (!params?.tags.length) { return null } return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const } ``` Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно. ## Пример списка ```ts // src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts import type { SWRConfiguration } from 'swr' import useSWR from 'swr' import { petStoreApi } from '../client' import type { FindPetsByStatusParams, Pet, } from '../generated/pet-store-api.generated' export const getPetListKey = (params?: FindPetsByStatusParams | null) => { if (!params?.status) { return null } return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const } /** * Получает список питомцев по статусу. */ export const useGetPetList = ( params?: FindPetsByStatusParams | null, config?: SWRConfiguration, ) => { const key = getPetListKey(params) const fetcher = () => petStoreApi.pet.findPetsByStatus( params as FindPetsByStatusParams, ) return useSWR(key, fetcher, config) } ``` `params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher. ## Пример detail-запроса ```ts // src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts import type { SWRConfiguration } from 'swr' import useSWR from 'swr' import { petStoreApi } from '../client' import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated' export const getPetDetailKey = (params?: GetPetByIdParams | null) => { if (!params?.petId) { return null } return ['pet-store-api', `/pet/${params.petId}`] as const } /** * Получает детальную карточку питомца с кешированием результата. */ export const useGetPetDetail = ( params?: GetPetByIdParams | null, config?: SWRConfiguration, ) => { const key = getPetDetailKey(params) const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) return useSWR(key, fetcher, config) } ``` ## Пример без параметров ```ts // src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts import type { SWRConfiguration } from 'swr' import useSWR from 'swr' import { petStoreApi } from '../client' import type { StoreInventory } from '../types' export const getStoreInventoryKey = () => { return ['pet-store-api', '/store/inventory'] as const } /** * Получает инвентарь магазина. */ export const useGetStoreInventory = ( config?: SWRConfiguration, ) => { return useSWR( getStoreInventoryKey(), () => petStoreApi.store.getInventory(), config, ) } ``` Если generated-метод возвращает безымянный тип вроде `Record`, а тип нужен наружу, вынесите его в `types/`. ## Отложенный запрос GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя. ```ts const key = getPetDetailKey(params) ``` Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа. Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`. ## Экспорт ```ts // src/infra/pet-store-api/hooks/index.ts export { getPetListKey, useGetPetList } from './use-get-pet-list.hook' export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook' export { getStoreInventoryKey, useGetStoreInventory, } from './use-get-store-inventory.hook' ``` ```ts // src/infra/pet-store-api/index.ts export { petStoreApi } from './client' export type { FindPetsByStatusParams, GetPetByIdParams, Pet, } from './generated/pet-store-api.generated' export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated' export * from './hooks' export type { StoreInventory } from './types' ``` Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую. ## Где заканчивается infra ```ts // Хорошо: infra, прозрачный GET-хук const { data: pets } = useGetPetList({ status: StatusEnum.Available }) ``` ```ts // Хорошо: business, доменная интерпретация export const useAvailablePets = () => { const query = useGetPetList({ status: StatusEnum.Available }) return { ...query, hasPets: Boolean(query.data?.length), } } ``` `hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`. ## Что запрещено ```ts // Плохо — useSWR в компоненте const { data } = useSWR( ['pet-store-api', '/pet/findByStatus?status=available'], () => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }), ) // Плохо — проверка готовности размазана по хуку export const useGetPetDetail = (params?: GetPetByIdParams | null) => { const key = params?.petId ? getPetDetailKey(params) : null const fetcher = () => { if (!params?.petId) { throw new Error('Pet id is required') } return petStoreApi.pet.getPetById(params) } return useSWR(key, fetcher) } // Плохо — условный вызов useSWR нарушает rules of hooks export const useGetPetDetail = (params?: GetPetByIdParams | null) => { const key = getPetDetailKey(params) if (key === null) { return useSWR(null, null) } return useSWR(key, () => petStoreApi.pet.getPetById(params)) } // Плохо — несколько GET внутри infra-хука export const usePetDashboard = () => { const available = useGetPetList({ status: StatusEnum.Available }) const sold = useGetPetList({ status: StatusEnum.Sold }) return { available, sold } } // Плохо — бизнес-флаг внутри GET-хука REST-клиента export const useGetPetList = (params?: FindPetsByStatusParams | null) => { const query = useSWR(...) return { ...query, hasPets: Boolean(query.data?.length), } } ``` Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/applied/data-fetch/client-get-hook). --- --- url: /nextjs-style-guide/applied/rest-client/usage.md description: Как вызвать готовый REST-клиент в функции. --- # Использование REST-клиента Как вызвать готовый REST-клиент в функции. ## Пример ```ts import { petStoreApi } from 'infra/pet-store-api' export const getPet = async (petId: number) => { const pet = await petStoreApi.pet.getPetById({ petId }) console.log(pet) } ``` --- --- url: /nextjs-style-guide/applied/data-fetch.md description: Как получать данные с учётом рендера страницы. --- # Получение данных Как получать данные с учётом рендера страницы. Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](/applied/rest-client/setup/). ## Сначала определите рендер страницы В Next.js выбор начинается не с `await`, `Suspense` или SWR. Сначала нужно понять, какой рендер получится у маршрута: static/ISR или dynamic/SSR. Next.js может перевести страницу в dynamic rendering автоматически, если в маршруте используются API текущего запроса. Поэтому первый вопрос такой: ```text Можно ли сохранить ISR, или странице нужны данные на каждый request? ``` ISR — приоритет. Если данные общие для пользователей и их можно обновлять с интервалом, не переводите страницу в SSR без необходимости. SSR/dynamic rendering выбирается только когда данные действительно зависят от текущего request или должны пересчитываться на каждый запрос. ## Что переводит страницу в dynamic rendering Проверьте, нужны ли странице API и настройки, которые делают маршрут динамическим: * `cookies()` — данные зависят от cookie текущего пользователя. * `headers()` — данные зависят от request headers. * `draftMode()` — нужен preview/draft-режим. * `searchParams` в `page.tsx` — данные зависят от query string. * `cache: 'no-store'` или `revalidate: 0` в методе клиента — запрос нельзя кешировать. * `connection()` — рендер явно ждёт request. * `export const dynamic = 'force-dynamic'` — SSR включён вручную. Если ничего из этого не нужно, сначала проектируйте страницу как static/ISR. Серверный `await` сам по себе не означает SSR: режим зависит от кеширования запроса и dynamic API маршрута. ## Рендер перед стратегией | Рендер | Когда подходит | Что выбирать дальше | |--------|----------------|---------------------| | Static/ISR | Данные общие и могут обновляться по интервалу | Серверные стратегии: `await`, `Promise.all`, передача промиса ниже, SWR `fallback` | | SSR/dynamic | Данные зависят от request, пользователя или должны быть свежими на каждый запрос | Серверные стратегии с учётом блокировки первого HTML | | После гидрации | Данные зависят от вкладки, фильтра, поиска, пагинации или действия пользователя | Клиентский GET-хук | ## Как выбрать стратегию Когда режим рендера понятен, выбирайте конкретный способ получения данных: | Ситуация после выбора рендера | Стратегия | Где читать | |-------------------------------|-----------|------------| | Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](/applied/data-fetch/server-await) | | Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/applied/data-fetch/parallel-server-requests) | | Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/applied/data-fetch/pass-promise-down) | | Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/applied/data-fetch/client-hooks-initial-data) | | Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/applied/data-fetch/client-get-hook) | | Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/applied/data-fetch/business-composition) | ## Правило выбора Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам: ```text Можно ли сохранить ISR? Где нужны данные и что должно произойти до первого HTML? ``` Если данные можно кешировать между пользователями — сохраняйте static/ISR. Если данные request-specific — используйте SSR/dynamic rendering. Если данные зависят от состояния браузера — используйте GET-хук REST-клиента. Если простой GET превращается в доменный сценарий — переходите в `business/`. ## Общие запреты ```tsx // Плохо — SSR включён на всякий случай export const dynamic = 'force-dynamic' // Плохо — ISR отключён без требования к свежести на каждый request export const revalidate = 0 // Плохо — прямой fetch в компоненте useEffect(() => { fetch('/api/pets').then(...) }, []) // Плохо — useSWR в компоненте const { data } = useSWR( ['pet-store-api', '/pet/findByStatus?status=available'], () => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }), ) // Плохо — бизнес-флаг внутри GET-хука REST-клиента return { ...query, hasPets: Boolean(query.data?.length), } ``` Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля. --- --- url: /nextjs-style-guide/applied/data-fetch/server-await.md description: Получение REST-данных на сервере до первого HTML. --- # Серверный await Получение REST-данных на сервере до первого HTML. Серверный `await` — базовая стратегия для данных, которые нужны до рендера страницы или серверного блока. ## Когда использовать * Данные нужны для первого HTML. * Данные влияют на `metadata`. * По результату запроса нужно вызвать `notFound()` или `redirect()`. * Компонент серверный и данные не зависят от состояния браузера. ## Влияние на рендер Серверный `await` сам по себе не означает SSR. В App Router страница может остаться static/ISR, если маршрут не использует dynamic API и запросы можно кешировать. ISR — приоритет для общих данных. Если список или детальная страница могут обновляться по интервалу, сохраняйте кеширование и не добавляйте `no-store`, `revalidate: 0` или `force-dynamic` без требования. SSR/dynamic rendering нужен, когда данные зависят от текущего request: cookie, headers, `searchParams`, preview-режим или персональные данные пользователя. ## Пример страницы списка ```tsx // src/app/(routes)/pets/page.tsx import { petStoreApi, StatusEnum } from 'infra/pet-store-api' import { PetsScreen } from 'screens/pets' export default async function PetsPage() { const pets = await petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available, }) return } ``` `page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`. ## Пример детальной страницы ```tsx // src/app/(routes)/pets/[id]/page.tsx import { notFound } from 'next/navigation' import { petStoreApi } from 'infra/pet-store-api' import { PetDetailScreen } from 'screens/pet-detail' type PetPageProps = { params: Promise<{ id: string }> } export default async function PetPage({ params }: PetPageProps) { const { id } = await params const pet = await petStoreApi.pet.getPetById({ petId: Number(id) }).catch(() => null) if (!pet) { notFound() } return } ``` Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента. ## Что не делать ```tsx // Плохо — хуки нельзя вызывать в Server Component const { data } = useGetPetList({ status: StatusEnum.Available }) // Плохо — прямой fetch в обход клиента const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus') ``` Если данные нужны на сервере, вызывайте метод REST-клиента напрямую. ## Когда выбрать другую стратегию * Несколько независимых запросов — [Параллельные серверные запросы](/applied/data-fetch/parallel-server-requests). * Часть UI можно грузить отдельно — [Передача промиса ниже](/applied/data-fetch/pass-promise-down). * Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/applied/data-fetch/client-hooks-initial-data). --- --- url: /nextjs-style-guide/applied/data-fetch/parallel-server-requests.md description: Как запускать независимые REST-запросы на сервере без waterfall. --- # Параллельные серверные запросы Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер. ## Когда использовать * Запросы независимы друг от друга. * Все данные нужны текущему серверному компоненту перед возвратом UI. * Нельзя или не нужно стримить часть UI отдельно. ## Хорошо ```tsx import { petStoreApi, StatusEnum } from 'infra/pet-store-api' import { PetsDashboardScreen } from 'screens/pets-dashboard' export default async function PetsDashboardPage() { const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available, }) const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Pending, }) const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Sold, }) const [availablePets, pendingPets, soldPets] = await Promise.all([ availablePetsPromise, pendingPetsPromise, soldPetsPromise, ]) return ( ) } ``` ## Плохо ```tsx export default async function PetsDashboardPage() { const availablePets = await petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available, }) const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Pending, }) const soldPets = await petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Sold, }) return ( ) } ``` Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы. ## Зависимые запросы Если второй запрос зависит от результата первого, последовательный `await` допустим: ```tsx export default async function OrderPage({ params }: OrderPageProps) { const { id } = await params const order = await petStoreApi.store.getOrderById({ orderId: Number(id) }) const pet = await petStoreApi.pet.getPetById({ petId: order.petId }) return } ``` Не превращайте зависимый сценарий в `Promise.all` искусственно. ## Когда выбрать другую стратегию Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/applied/data-fetch/pass-promise-down). --- --- url: /nextjs-style-guide/applied/data-fetch/pass-promise-down.md description: >- Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте. --- # Передача промиса ниже Серверный компонент может запустить запрос и передать промис вложенному server-компоненту. Это полезно, когда часть UI можно загрузить отдельно через `Suspense`. ## Когда использовать * Верхняя часть страницы может отрендериться без этих данных. * Данные нужны только вложенному server-компоненту. * Нужна `Suspense`-граница и серверный стриминг. ## Пример ```tsx // src/app/(routes)/pets/page.tsx import { Suspense } from 'react' import { petStoreApi, StatusEnum } from 'infra/pet-store-api' import { PetListSection } from 'widgets/pet-list-section' import { PetListSkeleton } from 'widgets/pet-list-section' import type { Pet } from 'infra/pet-store-api' export default function PetsPage() { const petsPromise = petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available, }) return (

Питомцы

}>
) } async function AvailablePets({ petsPromise }: { petsPromise: Promise }) { const pets = await petsPromise return } ``` Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI. ## Граница стратегии Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components. Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/applied/data-fetch/client-hooks-initial-data). ## Что не делать ```tsx // Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии return ``` Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента. --- --- url: /nextjs-style-guide/applied/data-fetch/client-hooks-initial-data.md description: Как дать клиентским GET-хукам начальные REST-данные. --- # Начальные данные для клиентских хуков Как дать клиентским GET-хукам начальные REST-данные. Эта стратегия используется, когда данные должны быть запущены на сервере, но потребляться на клиенте через GET-хуки REST-клиента. Технически это делается через `SWRConfig fallback`: сервер передаёт промис в fallback, а клиентский хук использует тот же SWR-ключ. ## Когда использовать * Внутри страницы есть Client Components с GET-хуками. * Нужно начать загрузку данных на сервере раньше. * Клиентский компонент должен остаться обычным потребителем `useGetPetList(...)`. * Не нужно писать отдельный prop-drilling для начальных данных. ## Рендер страницы Перед этой стратегией сначала определите рендер маршрута. Серверный preload для `fallback` подчиняется тем же правилам, что и любой серверный запрос в `page.tsx` или `layout.tsx`. Если данные общие и могут обновляться по интервалу, сохраняйте static/ISR. Если preload зависит от cookie, headers, `searchParams`, `no-store` или персональных данных пользователя, маршрут становится dynamic/SSR. `SWRConfig fallback` не должен быть причиной отключать ISR на всякий случай. Он только передаёт клиентскому GET-хуку данные, которые уже были запущены на сервере. ## Ключ хука ```ts // src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts export const getPetListKey = (params?: FindPetsByStatusParams | null) => { if (!params?.status) { return null } return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const } ``` Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`. ## Пример layout ```tsx // src/app/(routes)/pets/layout.tsx import type { ReactNode } from 'react' import { SWRConfig, unstable_serialize } from 'swr' import { getPetListKey, petStoreApi, StatusEnum, } from 'infra/pet-store-api' type PetsLayoutProps = { children: ReactNode } export default async function PetsLayout({ children }: PetsLayoutProps) { const params = { status: StatusEnum.Available } const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params) return ( {children} ) } ``` Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`. ## Клиентский компонент ```tsx 'use client' import { StatusEnum, useGetPetList } from 'infra/pet-store-api' export function PetList() { const { data: pets, isLoading } = useGetPetList({ status: StatusEnum.Available, }) if (isLoading) return
Загрузка...
return (
    {pets?.map((pet) => (
  • {pet.name}
  • ))}
) } ``` Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента. ## Что важно * Ключ `fallback` должен совпадать с ключом GET-хука. * `fallback` использует ту же key-функцию и те же params, что и GET-хук. * Серверный код вызывает метод клиента, а не GET-хук. * Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую. * Эта стратегия не означает ручную работу с кешем в компонентах. ## Когда не использовать Если данные нужны только серверному компоненту, используйте [Серверный await](/applied/data-fetch/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/applied/data-fetch/client-get-hook). --- --- url: /nextjs-style-guide/applied/data-fetch/client-get-hook.md description: Получение REST-данных в Client Components через готовые GET-хуки REST-клиента. --- # Клиентский GET-хук Клиентский GET-хук используется, когда данные зависят от состояния браузера: вкладки, фильтра, поиска, пагинации, модалки или действия пользователя. ## Когда использовать * Запрос зависит от client state. * Данные не обязательны для первого HTML. * Пользователь меняет параметры запроса на клиенте. * Нужны SWR-кеширование, дедупликация и ревалидация. ## Пример с вкладками ```tsx 'use client' import { useState } from 'react' import { StatusEnum, useGetPetList } from 'infra/pet-store-api' const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold] export function PetTabs() { const [status, setStatus] = useState(StatusEnum.Available) const { data: pets, isLoading, error } = useGetPetList({ status }) return (
{statuses.map((item) => ( ))}
{isLoading &&
Загрузка...
} {error &&
Ошибка загрузки
}
    {pets?.map((pet) => (
  • {pet.name}
  • ))}
) } ``` Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента. ## Если хука нет Хук добавляется в REST-модуль сервиса: ```text src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts ``` Не создавайте локальный `useSWR` в компоненте. ## Плохо ```tsx // Плохо — прямой вызов клиента в useEffect useEffect(() => { petStoreApi.pet.findPetsByStatus({ status }).then(setPets) }, [status]) // Плохо — useSWR в компоненте const { data } = useSWR( ['pet-store-api', `/pet/findByStatus?status=${status}`], () => petStoreApi.pet.findPetsByStatus({ status }), ) ``` Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI. ## Когда выбрать другую стратегию * Данные нужны до первого HTML — [Серверный await](/applied/data-fetch/server-await). * Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/applied/data-fetch/client-hooks-initial-data). * Нужно вычислить бизнес-состояние — [Business-композиция](/applied/data-fetch/business-composition). --- --- url: /nextjs-style-guide/applied/data-fetch/business-composition.md description: Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле. --- # Business-композиция Business-композиция используется, когда простого GET-метода или прозрачного GET-хука недостаточно: нужно объединить несколько источников, преобразовать DTO или вычислить доменное состояние. ## Когда использовать * Нужно объединить несколько GET-запросов. * Нужно вычислить `isAuth`, `canEdit`, `hasAccess`, `hasPets`. * Нужно преобразовать DTO в доменную модель. * Нужно спрятать бизнес-сценарий за доменным API. Такая логика не пишется в `infra/`. REST-клиент остаётся прозрачным адаптером к API. ## Пример поверх одного GET-хука ```ts // src/business/pets/hooks/use-available-pets.hook.ts import { StatusEnum, useGetPetList } from 'infra/pet-store-api' /** * Доменный список доступных питомцев. */ export const useAvailablePets = () => { const query = useGetPetList({ status: StatusEnum.Available }) return { ...query, hasPets: Boolean(query.data?.length), } } ``` `useGetPetList` — infra-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`. ## Пример композиции нескольких GET-хуков ```ts // src/business/pets/hooks/use-pets-dashboard.hook.ts import { StatusEnum, useGetPetList } from 'infra/pet-store-api' /** * Данные dashboard по питомцам. */ export const usePetsDashboard = () => { const availablePets = useGetPetList({ status: StatusEnum.Available }) const pendingPets = useGetPetList({ status: StatusEnum.Pending }) const soldPets = useGetPetList({ status: StatusEnum.Sold }) return { availablePets, pendingPets, soldPets, total: (availablePets.data?.length ?? 0) + (pendingPets.data?.length ?? 0) + (soldPets.data?.length ?? 0), } } ``` Композиция нескольких запросов не добавляется в `infra/pet-store-api/hooks/`, потому что это уже сценарий потребления данных. ## Пример auth-состояния ```ts // src/business/auth/hooks/use-auth-state.hook.ts import { useGetCurrentUser } from 'infra/backend-api' /** * Состояние авторизации текущего пользователя. */ export const useAuthState = () => { const currentUser = useGetCurrentUser() const user = currentUser.data return { ...currentUser, user, isAuth: Boolean(user), } } ``` `isAuth` не является частью REST-клиента. Это доменный смысл результата запроса. ## Где размещать ```text src/business/ └── pets/ ├── hooks/ │ └── use-available-pets.hook.ts ├── mappers/ │ └── map-pet-dto-to-pet.ts ├── types/ └── index.ts ``` Модуль `business/` экспортирует наружу готовый доменный API через `index.ts`. ## Что запрещено ```ts // Плохо — business-смысл внутри infra-хука export const useGetPetList = (params?: FindPetsByStatusParams | null) => { const query = useSWR(...) return { ...query, hasPets: Boolean(query.data?.length), } } ``` REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте. --- --- url: /nextjs-style-guide/applied/styles/styles-setup.md description: 'Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.' --- # Настройка стилей Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили. ## Требования * Установлен PostCSS или любой другой pre/post-процессор с поддержкой `@custom-media`. ## Файлы Состав глобальных стилей — три файла: | Файл | Роль | |------|------| | `variables.css` | Токены проекта (цвета, отступы, радиусы) | | `media.css` | Custom media queries (брейкпоинты по ширине и высоте) | | `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз | Правила подключения: * В приложение импортируется **только** `global.css`. * `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`. * `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](/applied/postcss)). ## Корневой `font-size` Базовая единица `rem` в проекте привязана к **16px**: корневой `font-size` не переопределяется. `html { font-size: ... }` писать запрещено — пользовательская настройка размера шрифта в браузере должна работать (a11y). Все `rem`-значения в `media.css` и других стилях трактуются как `1rem = 16px по умолчанию`. Reset браузерных дефолтов (`box-sizing`, сброс `margin`, типографика) каноном не задаётся — каждый проект решает сам. Если заводится — подключается через `global.css`. ## Установка ### 1. Создать файлы ```bash mkdir -p src/shared/styles touch src/shared/styles/variables.css src/shared/styles/media.css src/shared/styles/global.css ``` ### 2. Заполнить `media.css` Файл `src/shared/styles/media.css`. Стандартный набор брейкпоинтов проекта; редактировать только при согласованном изменении шкалы. Единица — `rem` (реагирует на корневой `font-size`). Перевод исходит из дефолтного `html { font-size: 16px }`, т.е. `1rem = 16px`. ```css /* src/shared/styles/media.css */ /* Ширина — Mobile First (min-width), кроме --xs (max-width) */ @custom-media --xs (max-width: 35.9375rem); /* 575px — до sm */ @custom-media --sm (min-width: 36rem); /* 576px — телефон альбом / малый планшет */ @custom-media --md (min-width: 48rem); /* 768px — планшет */ @custom-media --lg (min-width: 62rem); /* 992px — малый десктоп */ @custom-media --xl (min-width: 75rem); /* 1200px — десктоп */ @custom-media --2xl (min-width: 88rem); /* 1408px — широкий десктоп */ @custom-media --3xl (min-width: 120rem); /* 1920px — full HD+ */ /* Высота — min-height */ @custom-media --h-xs (min-height: 41.6875rem); /* 667px — iPhone SE портрет */ @custom-media --h-sm (min-height: 43.875rem); /* 702px */ @custom-media --h-md (min-height: 50.625rem); /* 810px — iPad портрет */ @custom-media --h-lg (min-height: 56.25rem); /* 900px */ @custom-media --h-xl (min-height: 62.5rem); /* 1000px */ @custom-media --h-2xl (min-height: 68.75rem); /* 1100px */ @custom-media --h-3xl (min-height: 75rem); /* 1200px */ ``` Правила: * только `@custom-media` на верхнем уровне; * имена короткие, по шкале (`--xs` … `--3xl`); высотные — с префиксом `--h-`; * единица — `rem`, не `em`/`px`; пиксельное значение указывается комментарием; * значения ширины — `min-width` (Mobile First), исключение `--xs` — `max-width` (блок «строго меньше `--sm`»); * значения высоты — `min-height`. ### 3. Заполнить `variables.css` Файл `src/shared/styles/variables.css`. Набор токенов под проект расширяется по мере роста дизайн-системы. ```css /* src/shared/styles/variables.css */ :root { /* Цвета */ --color-primary: #3b82f6; --color-bg: #ffffff; --color-bg-hover: #f5f5f5; --color-text: #1a1a1a; /* Отступы */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; /* Скругления */ --radius-1: 4px; --radius-2: 8px; } ``` Правила: * все токены определяются в `:root` — без вложенных селекторов; * именование — `kebab-case` по ролям: `--color-*`, `--space-*`, `--radius-*`; * `px` — основная единица для пространственных токенов; * темы накладываются поверх через `[data-theme="..."] { ... }` — в отдельном файле темы или здесь же. `variables.css` напрямую в приложение не импортируется — только через `global.css`. ### 4. Заполнить `global.css` Файл `src/shared/styles/global.css`. Единственный глобальный файл, импортируемый в точку инициализации приложения. Внутри — `@import` остальных глобалов относительным путём. ```css /* src/shared/styles/global.css */ @import './variables.css'; /* Сюда же подключаются будущие глобалы через @import: * @import './reset.css'; * @import './typography.css'; * @import './themes.css'; * media.css НЕ импортируется — он работает через PostCSS. */ ``` Правила: * пути в `@import` — относительные (`./variables.css`), не через алиасы; нативный CSS `@import` не понимает tsconfig-paths; * `media.css` в `global.css` **не импортируется**; * собственные глобальные правила (`html { ... }`, `body { ... }`) писать **не здесь**, а в отдельных файлах рядом (`reset.css`, `typography.css`) и подключать через `@import`. `global.css` — только точка сборки; * порядок `@import` определяет порядок каскада: токены первыми, дальше резеты / темы / типографика. ### 5. Подключить `global.css` в layout Импорт делается **один раз** — в корневом layout приложения: ```tsx // src/app/layout.tsx import 'shared/styles/global.css' import type { Metadata } from 'next' export const metadata: Metadata = { title: 'App', description: '', } export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` `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](/applied/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность. * [Использование стилей](/applied/styles/styles-usage) — правила написания CSS в компонентах. * [SVG-спрайты](/applied/svg-sprites/svg-sprites-setup) — стили иконок отдельно от глобальных. --- --- url: /nextjs-style-guide/applied/styles/styles-usage.md description: Как пишутся стили в проекте. --- # Использование стилей Как пишутся стили в проекте. ## Общие правила * Только **PostCSS** и **CSS Modules** для кастомной стилизации. * Подход **Mobile First** — стили пишутся от мобильных к десктопу. * Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`). * Корневой класс каждого CSS Module компонента всегда называется `.root` — это упрощает ориентацию в DevTools и отладку DOM. * Модификаторы — отдельный класс с `_`, применяется через `&._modifier`. **Хорошо** ```css .submitButton { padding: 8px 16px; &._disabled { opacity: 0.5; } } ``` **Плохо** ```css /* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */ .submit-button { padding: 8px 16px; &__icon { margin-right: 8px; } } ``` ## Вложенность * Вложенность селекторов запрещена. * Исключения: * Псевдоклассы: `&:hover`, `&:active`, `&:focus`, `&:disabled` и т.д. * Псевдоэлементы: `&::before`, `&::after`. * Медиа-запросы: `@media`. * Модификаторы: `&._active`, `&._disabled`. * Каждый вложенный блок отделяется пустой строкой от предыдущих свойств. **Хорошо** ```css .card { padding: 16px; background-color: var(--color-bg); &:hover { background-color: var(--color-bg-hover); } &::after { content: ''; display: block; } &._highlighted { border-color: var(--color-primary); } @media (--md) { padding: 24px; } } .cardTitle { font-size: 16px; @media (--md) { font-size: 20px; } } ``` **Плохо** ```css /* Плохо: вложенность селекторов, нет пустых строк между блоками. */ .card { padding: 16px; .cardTitle { font-size: 16px; } &:hover { background-color: var(--color-bg-hover); } } ``` ## Медиа-запросы * Только **Custom Media Queries**: `@media (--md) {}`. * Запрещены произвольные breakpoints: `@media (min-width: 768px)`. * `@media` пишется только **внутри** селектора. * Запрещено писать `@media` на верхнем уровне с селекторами внутри. **Хорошо** ```css .sidebar { display: none; @media (--md) { display: block; } } .sidebarTitle { font-size: 14px; @media (--md) { font-size: 18px; } } ``` **Плохо** ```css /* Плохо: @media на верхнем уровне с селекторами внутри. */ @media (--md) { .sidebar { display: block; } .sidebarTitle { font-size: 18px; } } /* Плохо: произвольный breakpoint вместо custom media. */ .sidebar { @media (min-width: 992px) { display: block; } } ``` ## CSS-переменные * Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `src/shared/styles/variables.css` через `:root`. * Файл переменных подключается через `src/shared/styles/global.css`, который импортируется один раз в `src/app/layout.tsx`. * Не дублировать магические значения в компонентах. **Хорошо** ```css /* src/shared/styles/variables.css */ :root { --color-primary: #3b82f6; --color-bg: #ffffff; --color-bg-hover: #f5f5f5; --space-1: 4px; --space-2: 8px; --space-3: 12px; --radius-1: 4px; --radius-2: 8px; } ``` ```css /* компонент */ .card { padding: var(--space-3); border-radius: var(--radius-2); background-color: var(--color-bg); } ``` **Плохо** ```css /* Плохо: магические значения вместо переменных. */ .card { padding: 12px; border-radius: 8px; background-color: #ffffff; } ``` ## Custom Media * Breakpoints определяются через Custom Media Queries в `src/shared/styles/media.css`. * Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей. ```css /* src/shared/styles/media.css */ @custom-media --sm (min-width: 36em); @custom-media --md (min-width: 62em); @custom-media --lg (min-width: 82em); ``` ## Импорт стилей * Стили компонента импортируются только внутри своего компонента. * Запрещено импортировать стили одного компонента в другой. * Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS. ## Форматирование * Пустая строка между селекторами верхнего уровня. * Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор). **Хорошо** ```css .userBar { display: none; color: var(--color-text); @media (--md) { display: flex; } } .userBarButton { background-color: var(--color-bg); &:hover { background-color: var(--color-bg-hover); } &._active { background-color: var(--color-primary); } } ``` **Плохо** ```css /* Плохо: нет пустых строк между селекторами и вложенными блоками. */ .userBar { display: none; color: var(--color-text); @media (--md) { display: flex; } } .userBarButton { background-color: var(--color-bg); &:hover { background-color: var(--color-bg-hover); } &._active { background-color: var(--color-primary); } } ``` ## Единицы измерения * `px` — основная единица измерения. * Остальные (`em`, `rem`, `%`, `vh`/`vw`) — допускаются по необходимости дизайна. ## Порядок CSS-свойств В стилях рекомендуется придерживаться логического порядка свойств: 1. Позиционирование (`position`, `top`, `left`, `z-index`). 2. Блочная модель (`display`, `width`, `height`, `margin`, `padding`). 3. Оформление (`background`, `border`, `box-shadow`, `border-radius`). 4. Текст (`font`, `color`, `text-align`, `line-height`). 5. Прочее (`transition`, `animation`, `opacity`, `cursor`). ## Комментарии * Желательно не писать комментарии в CSS. * Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение. --- --- url: /nextjs-style-guide/applied/svg-sprites/svg-sprites-intro.md description: Что такое SVG-спрайты и какие проблемы они решают. --- # SVG-спрайты Что такое SVG-спрайты и какие проблемы они решают. ## Проблема Иконки в проекте — это десятки и сотни SVG-файлов, которые нужно как-то доставлять в интерфейс. Подход «один `` на иконку» или инлайн SVG в каждом компоненте приводят к трём проблемам: * **Дублирование.** Инлайн SVG в нескольких компонентах — один и тот же код размазан по проекту. Изменение иконки требует правок в десяти местах. * **Размер бандла.** Каждый инлайн SVG — полный XML-код, который попадает в JS-бандл. Сотня иконок × средний размер SVG = сотни килобайт, которые браузер парсит как JavaScript, а не как статику. * **Нет управления цветом.** Инлайн SVG жёстко закрашивает иконку. Сменить цвет по состоянию (`:hover`, `._disabled`) — значит дублировать SVG или городить `currentColor`-хаки в каждом компоненте. ## Решение SVG-спрайты — это единый файл-контейнер, в который собираются все иконки проекта. В коде используется один React-компонент ``, а браузер загружает спрайт как статику — один раз, с кешированием. Что дают SVG-спрайты: * **Один источник.** Каждая иконка — один SVG-файл в `src/shared/sprites/`. Обновил файл — иконка обновилась везде. * **Лёгкий бандл.** Спрайт отдаётся как статический файл из `public/`, не попадает в JavaScript. Типы имён иконок генерируются автоматически — автодополнение работает без ручных описаний. * **Цвет через CSS.** При сборке цвета в SVG заменяются на CSS-переменные. Цвет иконки меняется через `color` родителя или через переменные `--icon-color-N` — как любой другой стиль. ## Состав раздела * [Настройка](/applied/svg-sprites/svg-sprites-setup) — подключение пакета, конфигурация, первая генерация. * [Использование](/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент ``, управление цветом. --- --- url: /nextjs-style-guide/applied/svg-sprites/svg-sprites-setup.md description: Подключение SVG-спрайтов в новом проекте. --- # Настройка SVG-спрайтов Подключение SVG-спрайтов в новом проекте. ## Установка 1. Установить пакет: ```bash npm install @gromlab/svg-sprites ``` 2. Создать `svg-sprites.config.ts` в корне проекта (см. [Стандартный конфиг](#стандартныи-конфиг)). 3. Создать папку входа для SVG-файлов в слое `shared`: ```bash mkdir -p src/shared/sprites/icons ``` Источники спрайтов живут в `src/shared/sprites//` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/applied/project-structure), [Архитектура](/slm-design/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим. 4. Добавить скрипты в `package.json`: ```json { "scripts": { "sprite": "svg-sprites", "predev": "svg-sprites", "prebuild": "svg-sprites" } } ``` Хуки `predev` и `prebuild` гарантируют, что спрайты и типы всегда актуальны перед запуском и сборкой. 5. Добавить сгенерированные артефакты в `.gitignore`: ```text # Сгенерированные спрайты и React-компонент /public/sprites/ /src/ui/svg-sprite/ ``` 6. Выполнить первую генерацию: ```bash npm run sprite ``` 7. Подключить спрайт в layout. Глобальный спрайт (иконки) подключается через `` в корневом 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 ( {children} ) } ``` Локальные спрайты (если есть) подключаются аналогично в layout конкретной страницы или маршрута. ## Стандартный конфиг Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине. ```ts // svg-sprites.config.ts import { defineConfig } from '@gromlab/svg-sprites' export default defineConfig({ output: 'public/sprites', publicPath: '/sprites', react: 'src/ui/svg-sprite', sprites: [ { name: 'icons', input: 'src/shared/sprites/icons' }, ], }) ``` ### Фиксированные значения | Опция | Значение | Почему так | |-------|----------|------------| | `output` | `public/sprites` | Единая папка статики Next.js | | `publicPath` | `/sprites` | URL-путь без `public/` (Next.js раздаёт `public/` как `/`) | | `react` | `src/ui/svg-sprite` | Слой `ui/` из архитектуры проекта (→ [Архитектура](/slm-design/architecture/)) | | `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` | ### Трансформации Все значения по умолчанию оставлять включёнными: ```ts transform: { removeSize: true, replaceColors: true, addTransition: true, } ``` Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию. Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально. ### Режим По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`. ## Дальше * [Использование](/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент ``, управление цветом. --- --- url: /nextjs-style-guide/applied/svg-sprites/svg-sprites-usage.md description: Как добавлять и использовать SVG-иконки в коде. --- # Использование SVG-спрайтов Как добавлять и использовать SVG-иконки в коде. ## Шаги 1. **Положить SVG в папку спрайта:** ```text src/shared/sprites/icons/new-icon.svg ``` 2. **Импортировать компонент.** Компонент `` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний: ```tsx import { SvgSprite } from 'ui/svg-sprite' ``` 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); } ``` --- --- url: /nextjs-style-guide/applied/images.md description: Как подключать изображения через Next.js Image в проекте. --- # Изображения Как подключать изображения через Next.js Image в проекте. ## Назначение Изображения рендерятся через компонент `Image` из `next/image`. Это сохраняет единый API для размеров, `alt`, lazy-loading и `priority`, даже если оптимизация изображений отключена. В проекте оптимизация Next.js Image отключается через `unoptimized`, чтобы сборка и рантайм не зависели от встроенного image optimizer. ## Настройка Отключение оптимизации задаётся глобально в `next.config.ts`: ```ts import type { NextConfig } from 'next' const nextConfig: NextConfig = { images: { unoptimized: true, }, } export default nextConfig ``` После этого `unoptimized` не нужно повторять на каждом `Image`. ## Использование Статические изображения, доступные по URL, размещаются в `public/`: ```text public/ └── images/ └── user-avatar.png ``` ```tsx import Image from 'next/image' export const UserAvatar = () => { return ( Аватар пользователя ) } ``` ## Правила * Использовать `Image` из `next/image`, не обычный ``. * Для контентных изображений всегда писать осмысленный `alt`. * Для декоративных изображений использовать `alt=""`. * Указывать `width` и `height`, если изображение не использует `fill`. * При `fill` задавать `sizes` и контролировать размеры родителя стилями. * `priority` ставить только для изображений первого экрана. * SVG-иконки не оформлять как изображения — для них используется раздел [SVG-спрайты](/applied/svg-sprites/svg-sprites-intro). ## Пример с `fill` ```tsx import Image from 'next/image' import styles from '../styles/article-card-cover.module.css' export const ArticleCardCover = () => { return (
Обложка статьи
) } ``` ```css .root { position: relative; aspect-ratio: 16 / 9; overflow: hidden; } ``` --- --- url: /nextjs-style-guide/applied/fonts.md description: Как подключать шрифты через Next.js Font в проекте. --- # Шрифты Как подключать шрифты через Next.js Font в проекте. ## Назначение Шрифты подключаются через `next/font`. Это стандартный способ Next.js: шрифты загружаются без ручных ``, `@font-face` и настройки preconnect. Шрифт подключается в точке инициализации приложения, а в CSS используется через переменную. ## Google Fonts ```tsx // src/app/layout.tsx import type { ReactNode } from 'react' import { Inter } from 'next/font/google' import 'shared/styles/global.css' const inter = Inter({ subsets: ['latin', 'cyrillic'], variable: '--font-main', display: 'swap', }) type RootLayoutProps = { children: ReactNode } export default function RootLayout({ children }: RootLayoutProps) { return ( {children} ) } ``` ```css /* src/shared/styles/global.css */ body { font-family: var(--font-main), system-ui, sans-serif; } ``` ## Локальные шрифты Каждый локальный шрифт размещается в отдельной папке внутри `src/shared/fonts/`. В этой же папке лежит `.font.ts`, где объявляется `localFont`. ```text src/shared/fonts/ └── roboto/ ├── roboto.font.ts ├── Roboto-Regular.woff2 ├── Roboto-Italic.woff2 ├── Roboto-Bold.woff2 └── Roboto-BoldItalic.woff2 ``` ```ts // src/shared/fonts/roboto/roboto.font.ts import localFont from 'next/font/local' export const roboto = localFont({ src: [ { path: './Roboto-Regular.woff2', weight: '400', style: 'normal', }, { path: './Roboto-Italic.woff2', weight: '400', style: 'italic', }, { path: './Roboto-Bold.woff2', weight: '700', style: 'normal', }, { path: './Roboto-BoldItalic.woff2', weight: '700', style: 'italic', }, ], variable: '--font-main', display: 'swap', }) ``` `app/` импортирует готовый объект шрифта и только подключает его к документу: ```tsx // src/app/layout.tsx import type { ReactNode } from 'react' import { roboto } from 'shared/fonts/roboto/roboto.font' import 'shared/styles/global.css' type RootLayoutProps = { children: ReactNode } export default function RootLayout({ children }: RootLayoutProps) { return ( {children} ) } ``` Путь в `localFont` указывается относительно `.font.ts`, поэтому файлы шрифта импортируются коротко: `./Roboto-Regular.woff2`. Если шрифтов несколько, у каждого своя папка и свой `.font.ts`. ## Правила * Использовать `next/font/google` или `next/font/local`. * Не подключать шрифты через ручные `` и `@font-face` без необходимости. * Подключать шрифты один раз — в корневом layout через готовый объект шрифта. * Использовать CSS-переменные `variable`, а не жёстко прописывать семейство в каждом компоненте. * Локальные файлы шрифтов хранить в `src/shared/fonts/{font-name}/` рядом с `{font-name}.font.ts`. * Не объявлять `localFont` внутри `src/app/layout.tsx`; layout только импортирует готовый шрифт. --- --- url: /nextjs-style-guide/applied/aliases.md description: Какие алиасы импортов есть в проекте и как ими пользоваться. --- # Алиасы импортов Какие алиасы импортов есть в проекте и как ими пользоваться. ## Конфиг `tsconfig.json` в корне проекта: ```json { "compilerOptions": { "paths": { "app/*": ["./src/app/*"], "layouts/*": ["./src/layouts/*"], "screens/*": ["./src/screens/*"], "widgets/*": ["./src/widgets/*"], "business/*": ["./src/business/*"], "infra/*": ["./src/infra/*"], "ui/*": ["./src/ui/*"], "shared/*": ["./src/shared/*"] } } } ``` Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет. ## Правила * **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля. * **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля. * **Префикс `@/` не используется.** Имя слоя — само по себе адрес. * **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/slm-design/architecture/layers)). **Хорошо** ```ts import { Button } from 'ui/button' import { useUser } from 'business/user' import { formatDate } from 'shared/utils/date' ``` **Плохо** ```ts // Относительный путь между модулями import { Button } from '../../../ui/button' // Префикс @/, которого нет в paths import { Button } from '@/ui/button' // Алиас на src — не предусмотрен import { Button } from 'src/ui/button' ``` ## Внутри модуля Внутри своего модуля — относительные пути: ```ts // src/ui/button/button.tsx import styles from './button.module.css' import { Icon } from './icon' ``` Не использовать алиас на самого себя: ```ts // Плохо — алиас вместо относительного пути внутри модуля import { Icon } from 'ui/button/icon' ``` --- --- url: /nextjs-style-guide/applied/templates/templates-intro.md description: Что такое шаблоны кодогенерации и какие проблемы они решают. --- # Шаблоны генерации Что такое шаблоны кодогенерации и какие проблемы они решают. ## Проблема Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам: * **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили. * **Время.** Создание одного компонента с типами, стилями и экспортом — 5–10 минут рутины. За спринт набегают часы. * **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок. ## Решение Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически. Что дают шаблоны: * **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика. * **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику. * **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения. ## Состав раздела * [Настройка](/applied/templates/templates-setup) — первичная установка: скачивание стандартного набора шаблонов в проект. * [Создание шаблонов](/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры. * [Использование](/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. --- --- url: /nextjs-style-guide/applied/templates/templates-setup.md description: Первичная установка шаблонов кодогенерации в проект. --- # Настройка шаблонов генерации Первичная установка шаблонов кодогенерации в проект. ## Установка 1. Проверить, что `.templates/` отсутствует (или согласовать перезапись, если папка уже есть). 2. Скачать папку из эталонного репозитория: ```bash npx tiged git@gromlab.ru:templates/nextjs-template.git/.templates .templates ``` 3. Если `tiged` падает в default-режиме (HTTP-tarball у Gitea) — повторить с явным git-режимом: ```bash npx tiged --mode=git git@gromlab.ru:templates/nextjs-template.git/.templates .templates ``` 4. Проверить генерацию: ```bash npx @gromlab/create component test src/ui ``` После проверки — удалить тестовый модуль. ## Проверка установки * В корне проекта есть папка `.templates/`. * Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор). * Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок. ## Дальше * [Создание шаблонов](/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры. * [Использование](/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. --- --- url: /nextjs-style-guide/applied/templates/templates-create.md description: 'Структура шаблонов, синтаксис переменных и примеры.' --- ::: v-pre # Создание шаблонов генерации Структура шаблонов, синтаксис переменных и примеры. ## Структура шаблонов Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон. ```text .templates/ ├── component/ # шаблон компонента │ └── {{name.kebabCase}}/ │ ├── styles/ │ │ └── {{name.kebabCase}}.module.css │ ├── types/ │ │ └── {{name.kebabCase}}-props.type.ts │ ├── {{name.kebabCase}}.tsx │ └── index.ts └── store/ # шаблон Zustand стора └── {{name.kebabCase}}/ ├── {{name.kebabCase}}.store.ts ├── {{name.kebabCase}}.type.ts └── index.ts ``` ## Обязательный шаблон компонента Перед созданием компонентов в проекте должен существовать шаблон `.templates/component`. Если шаблона нет, компонент не создаётся вручную. Сначала создаётся шаблон компонента, затем компонент генерируется через [VS Code или CLI](/applied/templates/templates-usage). ## Синтаксис шаблонов ### Переменные Переменные работают в именах файлов/папок и внутри файлов: ```text {{variable}} ``` Переменные могут быть любыми. `name` — дефолтная, подставляется генератором автоматически. Если реализация требует дополнительных параметров — можно использовать произвольные наборы переменных. ### Модификаторы Модификаторы меняют регистр и формат записи переменной: ```text {{name.pascalCase}} → MyButton {{name.camelCase}} → myButton {{name.kebabCase}} → my-button {{name.snakeCase}} → my_button {{name.screamingSnakeCase}} → MY_BUTTON ``` ## Как создать новый шаблон 1. Создать папку в `.templates/` с именем шаблона (например `hook`). 2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом. 3. Шаблон сразу доступен и в расширении VS Code, и в CLI. Пример — создание шаблона для хука: ```text .templates/ └── hook/ └── {{name.kebabCase}}/ ├── {{name.kebabCase}}.hook.ts └── index.ts ``` ```ts // .templates/hook/{{name.kebabCase}}.hook.ts export const {{name.camelCase}} = () => { } ``` ```ts // .templates/hook/index.ts export { {{name.camelCase}} } from './{{name.kebabCase}}.hook' ``` ## Дальше * [Использование](/applied/templates/templates-usage) — генерация через VS Code плагин и CLI. ::: --- --- url: /nextjs-style-guide/applied/templates/templates-usage.md description: Генерация файлов из шаблонов через VS Code плагин и CLI. --- # Использование шаблонов генерации Генерация файлов из шаблонов через VS Code плагин и CLI. ::: danger Ручное создание запрещено Файлы, для которых есть шаблоны в `.templates/`, создаются только генератором. Ручное создание компонента, модуля, стора или другого шаблонного блока запрещено. Если нужного шаблона нет, сначала создайте шаблон в `.templates/`, затем сгенерируйте код на его основе. ::: ## Через VS Code Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора. 1. ПКМ на целевой папке в проводнике VS Code. 2. **Generate from template** → выбрать шаблон. 3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`. Расширение устанавливается разово на машину разработчика, не через проект. ## Через CLI [@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется. ```bash npx @gromlab/create <шаблон> <имя> [путь] ``` Путь не обязателен — по умолчанию генерация происходит в текущую директорию. | Команда | Что создаёт | |---|---| | `npx @gromlab/create component button` | Компонент в текущей папке | | `npx @gromlab/create module auth src/business` | Бизнес-модуль | | `npx @gromlab/create widget header src/widgets` | Виджет | | `npx @gromlab/create layout admin src/layouts` | Layout | | `npx @gromlab/create store auth src/business/auth/stores` | Стор | CLI вызывается через `npx`, в `package.json` отдельно не добавляется. --- --- url: /nextjs-style-guide/applied/biome.md description: Установка и настройка линтера-форматтера в новом проекте. --- # Biome Установка и настройка линтера-форматтера в новом проекте. ## Требования * Node.js 18+. * Проект без установленного ESLint и Prettier (они конфликтуют с Biome). ## Установка 1. Установить пакет: ```bash npm install --save-dev --save-exact @biomejs/biome ``` 2. Инициализировать конфиг: ```bash npx @biomejs/biome init ``` В корне появится `biome.json` с дефолтными настройками. 3. Привести `biome.json` к стандартному виду (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`. 4. Добавить скрипты в `package.json`: ```json { "scripts": { "lint": "biome lint .", "format": "biome format --write .", "check": "biome check --write ." } } ``` | Скрипт | Что делает | |--------|-----------| | `lint` | Проверка правил без правок | | `format` | Автоформатирование всех файлов | | `check` | Lint + format + organize imports в один проход (основная команда) | ## Стандартный `biome.json` Дефолтный `biome.json`, созданный `biome init`, заменяется стандартным конфигом проекта. Стандартный `biome.json`: ```jsonc { "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true, "includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.templates", "!src/infra/**/generated"] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 }, "javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } }, "linter": { "enabled": true, "rules": { "recommended": true, "suspicious": { "noUnknownAtRules": "off" }, "correctness": { "noUnknownMediaFeatureName": "off" } }, "domains": { "next": "recommended", "react": "recommended" } }, "assist": { "actions": { "source": { "organizeImports": "on" } } } } ``` `src/infra/**/generated` исключается из Biome, потому что generated-файлы не правятся руками. При этом generated-файлы остаются в git. Правила `suspicious/noUnknownAtRules` и `correctness/noUnknownMediaFeatureName` отключены, потому что проектный CSS-стек использует `@custom-media` и другие конструкции, которые Biome может не распознавать. ## Интеграция с VS Code Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](/applied/vscode). --- --- url: /nextjs-style-guide/applied/postcss.md description: Установка и настройка CSS-процессора в новом проекте. --- # PostCSS Установка и настройка CSS-процессора в новом проекте. ## Зачем PostCSS Подключаем ради двух вещей: * **Вложенность** — `&:hover`, `&::before`, `&._active` и `@media` внутри селектора. Без процессора нативный CSS не покрывает всех нужных кейсов вложенности. * **`@custom-media`** — единые breakpoints проекта (`@media (--md)`) вместо магических `min-width`. Определяются в одном месте, переиспользуются везде. Autoprefixer и `@csstools/postcss-global-data` идут довеском под эти две задачи. ## Требования * Next.js 14+ (App Router). * Node.js 18+. CSS Modules поддерживаются Next.js из коробки — отдельной установки не требуют. ## Установка 1. Установить PostCSS-плагины как devDependencies: ```bash npm install -D postcss-custom-media postcss-nesting autoprefixer @csstools/postcss-global-data ``` 2. Создать `postcss.config.mjs` в корне проекта (см. «Конфиг»). ## Конфиг Файл `postcss.config.mjs` в корне проекта. ```js // postcss.config.mjs export default { plugins: { '@csstools/postcss-global-data': { files: ['src/shared/styles/media.css'], }, 'postcss-custom-media': {}, 'postcss-nesting': {}, autoprefixer: {}, }, } ``` ### Разбор плагинов | Плагин | Назначение | |--------|------------| | `@csstools/postcss-global-data` | Подгружает определения `@custom-media` из `src/shared/styles/media.css` перед обработкой каждого CSS-модуля. Семантика — «глобальный файл определений, который не импортируется в исходники» | | `postcss-custom-media` | Поддержка `@custom-media --md (...)` и использования `@media (--md) {}`. Определения берутся из файла, который подгрузил `postcss-global-data` | | `postcss-nesting` | Нативная CSS-вложенность: `&:hover`, `&::before`, `&._active` | | `autoprefixer` | Добавление вендорных префиксов по browserslist | ### Почему внешний файл с `@custom-media`, а не `@import` `@custom-media` — глобальные определения, одинаковые для всего проекта. Держим их в `src/shared/styles/media.css`. `@csstools/postcss-global-data` подгружает этот файл перед каждым модулем, а `postcss-custom-media` заменяет `@media (--md)` на конкретные `@media (min-width: ...)` на этапе сборки. Сами определения в бандл не попадают. Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`. Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](/applied/styles/styles-usage), раздел «Импорт стилей»). --- --- url: /nextjs-style-guide/applied/vscode.md description: Единые настройки редактора и расширений для команды. --- # VS Code Единые настройки редактора и расширений для команды. ## Структура `.vscode/` ```text .vscode/ ├── extensions.json # Рекомендуемые расширения └── settings.json # Настройки редактора для проекта ``` Оба файла коммитятся в репозиторий. ## Расширения Файл `.vscode/extensions.json` определяет список расширений, которые VS Code предложит установить при открытии проекта. ```json // .vscode/extensions.json { "recommendations": [ "biomejs.biome", "MyTemplateGenerator.mytemplategenerator", "csstools.postcss" ] } ``` | Расширение | Назначение | |---|---| | [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) | Линтинг и форматирование кода. Заменяет ESLint и Prettier | | Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) | Генерация файлов и папок из шаблонов `.templates/` через контекстное меню | | [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss) | Подсветка синтаксиса и автодополнение для PostCSS (`@custom-media`, `@nest` и др.) | ### Зачем это нужно * Новый участник команды получает все нужные расширения одним кликом. * Нет разночтений: все используют одинаковый форматтер и линтер. * Расширения привязаны к проекту, а не к конкретному разработчику. ## Настройки редактора Файл `.vscode/settings.json` переопределяет пользовательские настройки VS Code на уровне проекта. ```json // .vscode/settings.json { "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.biome": "explicit", "source.organizeImports.biome": "explicit" }, "files.associations": { "*.css": "postcss" } } ``` ### Разбор настроек | Настройка | Значение | Что делает | |---|---|---| | `editor.defaultFormatter` | `biomejs.biome` | Biome используется как единственный форматтер для всех файлов | | `editor.formatOnSave` | `true` | Код автоматически форматируется при каждом сохранении | | `codeActionsOnSave.source.fixAll.biome` | `explicit` | Biome автоматически применяет безопасные исправления при сохранении | | `codeActionsOnSave.source.organizeImports.biome` | `explicit` | Импорты сортируются и группируются автоматически при сохранении | | `files.associations` | `"*.css": "postcss"` | Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS | ### Зачем это нужно * **Единый стиль кода** -- форматирование происходит автоматически, невозможно закоммитить неформатированный код. * **Автофикс при сохранении** -- распространённые ошибки линтинга исправляются без ручного вмешательства. * **Сортировка импортов** -- импорты всегда в одном порядке, без конфликтов при мерже. * **PostCSS-подсветка** -- кастомные at-правила (`@custom-media`, `@define-mixin`) подсвечиваются корректно, а не как ошибки. ## Что не должно быть в `.vscode/` Не коммитятся файлы, специфичные для конкретного разработчика: * **Не коммитить**: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления. * **Коммитить**: только `extensions.json` и `settings.json` с общими для команды настройками. --- --- url: /nextjs-style-guide/applied/localization.md description: Как организовать локализацию как infra-модуль. --- # Локализация Как организовать локализацию как infra-модуль. ## Назначение Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов. Код локализации живёт в `src/infra/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infra-модуля. ## Структура ```text src/infra/i18n/ ├── config/ │ └── i18n.config.ts ├── dictionaries/ │ ├── ru.ts │ └── en.ts ├── hooks/ │ └── use-translation.hook.ts ├── providers/ │ └── i18n-provider.tsx ├── types/ │ └── i18n.type.ts └── index.ts ``` Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infra/i18n`. ## Подключение `app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infra/i18n/`. ```tsx // src/app/layout.tsx import type { ReactNode } from 'react' import { I18nProvider } from 'infra/i18n' type RootLayoutProps = { children: ReactNode } export default function RootLayout({ children }: RootLayoutProps) { return ( {children} ) } ``` ## Использование Компоненты получают переводы через готовый API модуля локализации: ```tsx import { useTranslation } from 'infra/i18n' export const ProfileTitle = () => { const { t } = useTranslation() return

{t('profile.title')}

} ``` ## Правила * Локализация живёт в `infra/i18n/`. * `app/` только подключает готовый provider и передаёт locale. * Словари не импортируются напрямую в компоненты, screens или business-модули. * Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск. * Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться. * Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infra-модуля. --- --- url: /nextjs-style-guide/applied/stores.md ---