From 53aa01199d119aff1aebd5780dec4b4efb0d32f0 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Wed, 13 May 2026 17:12:18 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20NextJS=20Style=20Guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен отдельный VitePress-сайт для NextJS Style Guide - удалены дубли SLM-канонов из style-guide - обновлены ссылки, сборочные скрипты, CI, Docker и README - разблокирована карточка NextJS Style Guide на главной --- .gitea/workflows/ci.yml | 3 + .gitignore | 1 + Dockerfile | 2 +- README.md | 18 +- .../style-guide/basics/architecture/index.md | 109 ------- .../style-guide/basics/architecture/layers.md | 254 --------------- .../basics/architecture/modules.md | 289 ------------------ .../basics/architecture/segments.md | 181 ----------- canons/style-guide/index.md | 4 +- canons/style-guide/slm-design/VERSION | 2 - .../slm-design/architecture/index.md | 114 ------- .../slm-design/architecture/layers.md | 254 --------------- .../slm-design/architecture/modules.md | 213 ------------- .../slm-design/architecture/monorepo.md | 235 -------------- .../slm-design/architecture/segments.md | 181 ----------- .../examples/react/composition-provider.md | 249 --------------- .../examples/react/factory-composition.md | 52 ---- .../slm-design/examples/react/factory.md | 114 ------- docs/nextjs-style-guide/.vitepress/config.ts | 27 ++ .../.vitepress/theme/index.ts | 1 + docs/nextjs-style-guide/docs.config.ts | 201 ++++++++++++ eslint.config.js | 7 +- package.json | 2 + scripts/docs/prepare.ts | 69 ++++- src/App.tsx | 11 +- src/config/docs.config.ts | 9 +- 26 files changed, 336 insertions(+), 2266 deletions(-) delete mode 100644 canons/style-guide/basics/architecture/index.md delete mode 100644 canons/style-guide/basics/architecture/layers.md delete mode 100644 canons/style-guide/basics/architecture/modules.md delete mode 100644 canons/style-guide/basics/architecture/segments.md delete mode 100644 canons/style-guide/slm-design/VERSION delete mode 100644 canons/style-guide/slm-design/architecture/index.md delete mode 100644 canons/style-guide/slm-design/architecture/layers.md delete mode 100644 canons/style-guide/slm-design/architecture/modules.md delete mode 100644 canons/style-guide/slm-design/architecture/monorepo.md delete mode 100644 canons/style-guide/slm-design/architecture/segments.md delete mode 100644 canons/style-guide/slm-design/examples/react/composition-provider.md delete mode 100644 canons/style-guide/slm-design/examples/react/factory-composition.md delete mode 100644 canons/style-guide/slm-design/examples/react/factory.md create mode 100644 docs/nextjs-style-guide/.vitepress/config.ts create mode 100644 docs/nextjs-style-guide/.vitepress/theme/index.ts create mode 100644 docs/nextjs-style-guide/docs.config.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9a2f7db..83e9171 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: - name: Сборка документации SLM Design run: npm run docs:build:slm-design + - name: Сборка документации NextJS Style Guide + run: npm run docs:build:nextjs-style-guide + - name: Сборка документации Figma Adaptive Standards run: npm run docs:build:figma-adaptive-standards diff --git a/.gitignore b/.gitignore index 84eaefb..b570071 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ docs/*/content/ docs/*/.vitepress/cache/ public/llms.txt public/slm-design/ +public/nextjs-style-guide/ public/figma-adaptive-standards/ # Editor directories and files diff --git a/Dockerfile b/Dockerfile index 1804b29..92ab3f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY package*.json ./ RUN npm ci COPY . . -RUN npm run docs:build:slm-design && npm run docs:build:figma-adaptive-standards && npm run build +RUN npm run docs:build:slm-design && npm run docs:build:nextjs-style-guide && npm run docs:build:figma-adaptive-standards && npm run build FROM caddy:2-alpine diff --git a/README.md b/README.md index c6ce21c..448b8bd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ - React/Vite-лендинг со списком документаций. - VitePress-сборка для `SLM Design`. +- VitePress-сборка для `NextJS Style Guide`. +- VitePress-сборка для `Figma Adaptive Standards`. - Корневой `llms.txt` как карта всех документаций. - Собственные `llms.txt` и `llms-full.txt` внутри каждой документации. - Docker/Caddy-конфигурация для публикации статической сборки. @@ -16,15 +18,17 @@ ## Документации - `SLM Design` — архитектура frontend-приложений через слои, модули, публичные API и DI через фабрики. -- `NextJS Style Guide` — будущие правила организации Next.js-приложений. +- `NextJS Style Guide` — практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript. - `React Style Guide` — будущие правила написания React-кода. -- `Figma Adaptive Standards` — будущие стандарты подготовки адаптивных макетов в Figma. +- `Figma Adaptive Standards` — стандарты подготовки адаптивных макетов в Figma. ## Структура ```text canons/ исходные материалы и черновики docs/slm-design/ VitePress-сайт SLM Design +docs/nextjs-style-guide/ VitePress-сайт NextJS Style Guide +docs/figma-adaptive-standards/ VitePress-сайт Figma Adaptive Standards scripts/docs/ подготовка контента для документаций scripts/site/ генерация корневых артефактов сайта src/ React-лендинг @@ -40,6 +44,8 @@ npm run dev ```bash npm run docs:build:slm-design +npm run docs:build:nextjs-style-guide +npm run docs:build:figma-adaptive-standards npm run site:generate npm run build ``` @@ -48,6 +54,8 @@ npm run build - `npm run dev` — запускает Vite dev server. - `npm run docs:build:slm-design` — подготавливает и собирает VitePress-документацию SLM Design. +- `npm run docs:build:nextjs-style-guide` — подготавливает и собирает VitePress-документацию NextJS Style Guide. +- `npm run docs:build:figma-adaptive-standards` — подготавливает и собирает VitePress-документацию Figma Adaptive Standards. - `npm run site:generate` — генерирует корневой `public/llms.txt` из `src/config/docs.config.ts` и хардкод-секций. - `npm run build` — генерирует корневые артефакты и собирает лендинг. - `npm run lint` — запускает ESLint. @@ -67,6 +75,10 @@ npm run build ```text /slm-design/llms.txt /slm-design/llms-full.txt +/nextjs-style-guide/llms.txt +/nextjs-style-guide/llms-full.txt +/figma-adaptive-standards/llms.txt +/figma-adaptive-standards/llms-full.txt ``` Корневой `llms-full.txt` намеренно не создаётся. Полные bundles остаются внутри конкретных документаций. @@ -95,6 +107,8 @@ Docker-сборка выполняет: ```bash npm run docs:build:slm-design +npm run docs:build:nextjs-style-guide +npm run docs:build:figma-adaptive-standards npm run build ``` diff --git a/canons/style-guide/basics/architecture/index.md b/canons/style-guide/basics/architecture/index.md deleted file mode 100644 index 09a7173..0000000 --- a/canons/style-guide/basics/architecture/index.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: SLM Design -description: Назначение архитектуры, ключевые принципы и карта разделов документации ---- - -# SLM Design -Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. - -## Разделы спецификации - -Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: - -- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. -- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. -- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. - -Рекомендуемый порядок чтения: обзор → слои → модули → сегменты. - -## Преимущества - -### Вертикальная организация домена - -Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. - -### Dependency Injection без фреймворков - -Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. - -### Разделение ответственности без перегрузки слоёв - -Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. - -### Горизонтальная инкапсуляция - -Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. - -### Колокация по умолчанию - -Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. - -### Явное разделение каркаса и контента - -Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. - -### Масштабирование через группировку - -При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). - -## Происхождение - -SLM Design вырос на основе: - -- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей -- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое -- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию -- **Colocation Principle** — код живёт рядом с местом использования - -## Пример структуры проекта - -```text -src/ -├── app/ -│ -├── layouts/ -│ ├── main/ -│ └── dashboard/ -│ -├── screens/ -│ ├── home/ -│ ├── products/ -│ ├── product-detail/ -│ └── about/ -│ -├── widgets/ -│ ├── page-heading/ -│ ├── hero-section/ -│ └── promo-banner/ -│ -├── business/ -│ ├── auth/ -│ ├── catalog/ -│ ├── orders/ -│ └── chat/ -│ -├── infra/ -│ ├── theme/ -│ ├── i18n/ -│ ├── backend-api/ -│ └── logger/ -│ -├── ui/ -│ ├── button/ -│ ├── input/ -│ ├── modal/ -│ ├── toast/ -│ └── dropdown/ -│ -└── shared/ - ├── lib/ - ├── types/ - └── styles/ -``` - -## Принципы - -- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. -- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. -- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. -- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/canons/style-guide/basics/architecture/layers.md b/canons/style-guide/basics/architecture/layers.md deleted file mode 100644 index 92e7438..0000000 --- a/canons/style-guide/basics/architecture/layers.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: Слои -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-состояния diff --git a/canons/style-guide/basics/architecture/modules.md b/canons/style-guide/basics/architecture/modules.md deleted file mode 100644 index 4950d7c..0000000 --- a/canons/style-guide/basics/architecture/modules.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: Модули -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 -``` - -Подробное описание сегментов — в разделе [Сегменты](./segments.md). - -## Публичный API - -Внешний код импортирует модуль только через публичный API. - -```ts -// Хорошо -import { customerFactory } from '@/business/customer' -import type { Customer } from '@/business/customer' -``` - -```ts -// Плохо -import { validateToken } from '@/business/auth/lib/tokens' -``` - -`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. - -Внутренние сегменты модуля остаются деталями реализации. - -Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. - -```ts -// business/customer/index.ts -export { customerFactory } from './customer.factory' - -export type { Customer } from './types/customer.type' -export type { CustomerApi } from './types/customer-api.type' -export type { CustomerDeps } from './types/customer-deps.type' -export type { CustomerFactory } from './types/customer-factory.type' -``` - -## Фабрика - -Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. - -Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. - -Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. - -Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». - -### Структура business-модуля - -```text -business/customer/ -├── customer.factory.ts -├── index.ts -└── types/ - ├── customer.type.ts - ├── customer-api.type.ts - ├── customer-deps.type.ts - └── customer-factory.type.ts -``` - -### Типы - -```ts -// business/customer/types/customer-api.type.ts -export type CustomerApi = { - useCustomer: () => Customer - CustomerCard: (props: CustomerCardProps) => ReactNode -} -``` - -```ts -// business/order/types/order-deps.type.ts -export type OrderDeps = { - customer: Pick -} -``` - -```ts -// business/order/types/order-factory.type.ts -export type OrderFactory = (deps: OrderDeps) => OrderApi -``` - -### Фабрика без зависимостей - -```ts -// business/customer/customer.factory.ts -import type { CustomerFactory } from './types/customer-factory.type' - -export const customerFactory: CustomerFactory = () => { - return { - useCustomer, - CustomerCard, - } -} -``` - -### Фабрика с зависимостями - -```ts -// business/order/order.factory.ts -import type { OrderFactory } from './types/order-factory.type' - -export const orderFactory: OrderFactory = (deps) => { - return { - useOrder, - OrderCard, - } -} -``` - -### Композиция на уровне screen - -```tsx -// screens/home/home.screen.tsx -import { customerFactory } from '@/business/customer' -import { orderFactory } from '@/business/order' - -const customer = customerFactory() -const order = orderFactory({ customer }) - -const { useOrder, OrderCard } = order - -export const HomeScreen = () => { - const currentOrder = useOrder() - - return -} -``` - -## Жизненный цикл - -Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. - -- Нужен на одной странице → `screens/{name}/parts/` -- Появился в 2+ местах → поднимается по природе: - - абстрактный UI → `ui/` - - блок с данными/логикой → `widgets/` - - представление бизнес-домена → `business/{area}/parts/` - -Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/canons/style-guide/basics/architecture/segments.md b/canons/style-guide/basics/architecture/segments.md deleted file mode 100644 index eeb1b52..0000000 --- a/canons/style-guide/basics/architecture/segments.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: Сегменты -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-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). - -Корневой файл модуля в `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 -``` diff --git a/canons/style-guide/index.md b/canons/style-guide/index.md index 8cec273..d870c54 100644 --- a/canons/style-guide/index.md +++ b/canons/style-guide/index.md @@ -1,11 +1,11 @@ --- title: NextJS Style Guide -description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript. +description: Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript. --- # NextJS Style Guide -Стандарты разработки фронтенд-приложений на Next.js и TypeScript. +Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript. ## Использование diff --git a/canons/style-guide/slm-design/VERSION b/canons/style-guide/slm-design/VERSION deleted file mode 100644 index d0b7f5d..0000000 --- a/canons/style-guide/slm-design/VERSION +++ /dev/null @@ -1,2 +0,0 @@ -v0.1.5 -2026-05-11T18:29:26.821Z diff --git a/canons/style-guide/slm-design/architecture/index.md b/canons/style-guide/slm-design/architecture/index.md deleted file mode 100644 index 51a7f16..0000000 --- a/canons/style-guide/slm-design/architecture/index.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: SLM Design -description: "Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили." ---- - -# SLM Design -Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. - -## Разделы спецификации - -Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: - -- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. -- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. -- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. -- [Монорепозитории](./monorepo.md) — применение 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. -- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/canons/style-guide/slm-design/architecture/layers.md b/canons/style-guide/slm-design/architecture/layers.md deleted file mode 100644 index 335c978..0000000 --- a/canons/style-guide/slm-design/architecture/layers.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: Слои -description: "Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом." ---- - -# Слои - -Раздел описывает слои 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-состояния diff --git a/canons/style-guide/slm-design/architecture/modules.md b/canons/style-guide/slm-design/architecture/modules.md deleted file mode 100644 index 2b318c4..0000000 --- a/canons/style-guide/slm-design/architecture/modules.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: Модули -description: "Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом." ---- - -# Модули - -Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. - -## Определение - -**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** - -Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. - -Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. - -Главная граница модуля — не папка, а ответственность. - -## Компонент - -**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** - -Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. - -> Компонент отображает. Модуль организует. - -Компонент не может: - -- Импортировать код проекта за пределами родительского модуля. -- Владеть архитектурными зависимостями. -- Содержать любые компоненты. -- Содержать любые модули. -- Делать внешние запросы. -- Самостоятельно получать данные. -- Выбирать источник данных. -- Композировать данные. -- Вызывать сценарные хуки. -- Оркестрировать сценарий. -- Композировать модули. -- Решать, как устроен процесс. -- Содержать бизнес-логику. -- Содержать сценарную логику. - -Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. - -```text -auth/ -├── ui/ -│ └── logout-button/ -│ ├── logout-button.tsx -│ ├── styles/ -│ │ └── logout-button.module.css -│ ├── types/ -│ │ └── logout-button-props.type.ts -│ └── index.ts -└── index.ts -``` - -## Что считается модулем - -Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. - -Примеры модулей: - -- `screens/home/` — модуль страницы. -- `widgets/page-heading/` — модуль виджета. -- `business/auth/` — модуль бизнес-домена. -- `infra/theme/` — модуль инфраструктурного сервиса. -- `ui/button/` — модуль UI-kit сущности. -- `screens/home/parts/hero-section/` — вложенный модуль страницы. - -Не считаются модулями: - -- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. -- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. -- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. - -## Типы модулей - -Тип модуля определяет обязательный корневой файл и стартовую структуру. - -### UI-модуль - -Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: - -```text -header/ -├── header.tsx -└── index.ts -``` - -`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. - -### Бизнес-модуль - -Бизнес-модуль — модуль, который строится вокруг публичного runtime API. - -Бизнес-модуль обязан иметь фабрику в корне: - -```text -auth/ -├── auth.factory.ts -├── index.ts -└── types/ -``` - -Фабрика возвращает публичный runtime API модуля. - -### Инфраструктурный модуль - -Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. - -Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. - -```text -theme/ -├── index.ts -├── config/ -├── hooks/ -├── styles/ -└── ui/ -``` - -```text -backend-api/ -├── backend-api.client.ts -├── config/ -├── types/ -└── index.ts -``` - -## Структура - -Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. - -```text -{module-name}/ -├── {module-name}.factory.ts # фабрика (для business-модулей) -├── {module-name}.tsx # корневой файл модуля (опционален) -├── ui/ # компоненты модуля -├── parts/ # вложенные модули -├── hooks/ # хуки -├── stores/ # сторы состояния -├── services/ # внешние источники данных -├── mappers/ # трансформация данных между форматами -├── types/ # типы -├── styles/ # стили -├── lib/ # утилиты модуля -├── config/ # константы и конфигурация -└── index.ts # публичный API -``` - -Подробное описание сегментов — в разделе [Сегменты](./segments.md). - -## Публичный API - -Внешний код импортирует модуль только через публичный API. - -```ts -// Хорошо -import { customerFactory } from '@/business/customer' -import type { Customer } from '@/business/customer' -``` - -```ts -// Плохо -import { validateToken } from '@/business/auth/lib/tokens' -``` - -`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. - -Внутренние сегменты модуля остаются деталями реализации. - -Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. - -```ts -// business/customer/index.ts -export { customerFactory } from './customer.factory' - -export type { Customer } from './types/customer.type' -export type { CustomerApi } from './types/customer-api.type' -export type { CustomerDeps } from './types/customer-deps.type' -export type { CustomerFactory } from './types/customer-factory.type' -``` - -## Фабрика - -Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. - -Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. - -Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. - -Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». - -### Примеры - -Пример реализации фабрики в React см. в [Создание фабрики](../examples/react/factory.md). - -Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](../examples/react/factory-composition.md). - -## Жизненный цикл - -Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. - -- Нужен на одной странице → `screens/{name}/parts/` -- Появился в 2+ местах → поднимается по природе: - - абстрактный UI → `ui/` - - блок с данными/логикой → `widgets/` - - представление бизнес-домена → `business/{area}/parts/` - -Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/canons/style-guide/slm-design/architecture/monorepo.md b/canons/style-guide/slm-design/architecture/monorepo.md deleted file mode 100644 index eb1f5d2..0000000 --- a/canons/style-guide/slm-design/architecture/monorepo.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -title: Монорепозитории -description: "Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов." ---- - -# Монорепозитории - -Раздел описывает, как применять 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/*`. diff --git a/canons/style-guide/slm-design/architecture/segments.md b/canons/style-guide/slm-design/architecture/segments.md deleted file mode 100644 index ba11b77..0000000 --- a/canons/style-guide/slm-design/architecture/segments.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: Сегменты -description: "Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит." ---- - -# Сегменты - -Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. - -## Определение - -**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** - -## Обзор - -| Сегмент | Содержимое | -|---------|------------| -| `ui/` | Презентационные компоненты родительского модуля | -| `parts/` | Вложенные модули со своими сегментами | -| `hooks/` | React-хуки | -| `stores/` | Сторы состояния | -| `services/` | Работа с внешними источниками данных | -| `mappers/` | Трансформация данных между форматами | -| `types/` | TypeScript-типы и интерфейсы | -| `styles/` | Стили | -| `lib/` | Утилиты и хелперы модуля | -| `config/` | Константы и конфигурация | - -## Сегмент ui/ - -Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. - -Компонент в `ui/`: - -- Находится в собственной папке. -- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. -- Не содержит любые компоненты. -- Не содержит любые модули. -- Не импортирует код проекта за пределами родительского модуля. -- Не делает внешние запросы. -- Не вызывает сценарные хуки. -- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. -- Не содержит бизнес-логику или сценарную логику. - -Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). - -Корневой файл модуля в `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 -``` diff --git a/canons/style-guide/slm-design/examples/react/composition-provider.md b/canons/style-guide/slm-design/examples/react/composition-provider.md deleted file mode 100644 index 4f5593b..0000000 --- a/canons/style-guide/slm-design/examples/react/composition-provider.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -title: Композиция через Provider -description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя." ---- - -# Композиция через 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. diff --git a/canons/style-guide/slm-design/examples/react/factory-composition.md b/canons/style-guide/slm-design/examples/react/factory-composition.md deleted file mode 100644 index 460321a..0000000 --- a/canons/style-guide/slm-design/examples/react/factory-composition.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Композиция фабрик -description: "Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов." ---- - -# Композиция фабрик - -Раздел показывает, как собрать 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-модуля. diff --git a/canons/style-guide/slm-design/examples/react/factory.md b/canons/style-guide/slm-design/examples/react/factory.md deleted file mode 100644 index e9d8abb..0000000 --- a/canons/style-guide/slm-design/examples/react/factory.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Создание фабрики -description: "Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API." ---- - -# Создание фабрики - -Раздел показывает, как оформить фабрику 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, - } -} -``` diff --git a/docs/nextjs-style-guide/.vitepress/config.ts b/docs/nextjs-style-guide/.vitepress/config.ts new file mode 100644 index 0000000..1075be6 --- /dev/null +++ b/docs/nextjs-style-guide/.vitepress/config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vitepress'; +import taskLists from 'markdown-it-task-lists'; +import llmstxt from 'vitepress-plugin-llms'; +import { themeSyncHead } from '../../shared/vitepress/themeHead'; +import { sidebar, site } from '../docs.config'; + +export default defineConfig({ + title: site.title, + description: site.description, + base: site.base, + outDir: site.outDir, + srcDir: 'content', + cleanUrls: true, + head: [...themeSyncHead], + vite: { + plugins: [llmstxt()], + }, + markdown: { + config(md) { + md.use(taskLists); + }, + }, + themeConfig: { + sidebar, + socialLinks: [], + }, +}); diff --git a/docs/nextjs-style-guide/.vitepress/theme/index.ts b/docs/nextjs-style-guide/.vitepress/theme/index.ts new file mode 100644 index 0000000..e0013e9 --- /dev/null +++ b/docs/nextjs-style-guide/.vitepress/theme/index.ts @@ -0,0 +1 @@ +export { default } from '../../../shared/vitepress/theme'; diff --git a/docs/nextjs-style-guide/docs.config.ts b/docs/nextjs-style-guide/docs.config.ts new file mode 100644 index 0000000..8f12bea --- /dev/null +++ b/docs/nextjs-style-guide/docs.config.ts @@ -0,0 +1,201 @@ +export const site = { + title: 'NextJS Style Guide', + description: 'Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript', + base: '/nextjs-style-guide/', + outDir: '../../public/nextjs-style-guide', +}; + +/** + * Карта монтирования исходных канонов в VitePress-документацию. + * + * SLM-разделы берутся только из корневого `canons/slm-design/`, чтобы + * NextJS-гайд не содержал собственных дублей архитектурного канона. + */ +export const mounts = [ + { target: 'index.md', source: 'style-guide/index.md' }, + { target: 'workflow.md', source: 'style-guide/workflow.md' }, + + { target: 'slm-design/architecture/index.md', source: 'slm-design/architecture/index.md' }, + { target: 'slm-design/architecture/layers.md', source: 'slm-design/architecture/layers.md' }, + { target: 'slm-design/architecture/modules.md', source: 'slm-design/architecture/modules.md' }, + { target: 'slm-design/architecture/segments.md', source: 'slm-design/architecture/segments.md' }, + { target: 'slm-design/architecture/monorepo.md', source: 'slm-design/architecture/monorepo.md' }, + { target: 'slm-design/examples/react/factory.md', source: 'slm-design/examples/react/factory.md' }, + { target: 'slm-design/examples/react/factory-composition.md', source: 'slm-design/examples/react/factory-composition.md' }, + { target: 'slm-design/examples/react/composition-provider.md', source: 'slm-design/examples/react/composition-provider.md' }, + + { target: 'basics/tech-stack.md', source: 'style-guide/basics/tech-stack.md' }, + { target: 'basics/naming.md', source: 'style-guide/basics/naming.md' }, + { target: 'basics/code-style.md', source: 'style-guide/basics/code-style.md' }, + { target: 'basics/documentation.md', source: 'style-guide/basics/documentation.md' }, + { target: 'basics/typing.md', source: 'style-guide/basics/typing.md' }, + + { target: 'applied/creating-project/from-template.md', source: 'style-guide/applied/creating-project/from-template.md' }, + { target: 'applied/creating-project/manual.md', source: 'style-guide/applied/creating-project/manual.md' }, + { target: 'applied/creating-project/nextjs.md', source: 'style-guide/applied/creating-project/nextjs.md' }, + { target: 'applied/project-structure.md', source: 'style-guide/applied/project-structure.md' }, + { target: 'applied/page-level.md', source: 'style-guide/applied/page-level.md' }, + { target: 'applied/component.md', source: 'style-guide/applied/component.md' }, + { target: 'applied/module.md', source: 'style-guide/applied/module.md' }, + { target: 'applied/rest-client/index.md', source: 'style-guide/applied/rest-client/index.md' }, + { target: 'applied/rest-client/setup/index.md', source: 'style-guide/applied/rest-client/setup/index.md' }, + { target: 'applied/rest-client/setup/auto.md', source: 'style-guide/applied/rest-client/setup/auto.md' }, + { target: 'applied/rest-client/setup/manual.md', source: 'style-guide/applied/rest-client/setup/manual.md' }, + { target: 'applied/rest-client/setup/hooks.md', source: 'style-guide/applied/rest-client/setup/hooks.md' }, + { target: 'applied/rest-client/usage.md', source: 'style-guide/applied/rest-client/usage.md' }, + { target: 'applied/data-fetch/index.md', source: 'style-guide/applied/data-fetch/index.md' }, + { target: 'applied/data-fetch/server-await.md', source: 'style-guide/applied/data-fetch/server-await.md' }, + { target: 'applied/data-fetch/parallel-server-requests.md', source: 'style-guide/applied/data-fetch/parallel-server-requests.md' }, + { target: 'applied/data-fetch/pass-promise-down.md', source: 'style-guide/applied/data-fetch/pass-promise-down.md' }, + { target: 'applied/data-fetch/client-hooks-initial-data.md', source: 'style-guide/applied/data-fetch/client-hooks-initial-data.md' }, + { target: 'applied/data-fetch/client-get-hook.md', source: 'style-guide/applied/data-fetch/client-get-hook.md' }, + { target: 'applied/data-fetch/business-composition.md', source: 'style-guide/applied/data-fetch/business-composition.md' }, + { target: 'applied/styles/styles-setup.md', source: 'style-guide/applied/styles/styles-setup.md' }, + { target: 'applied/styles/styles-usage.md', source: 'style-guide/applied/styles/styles-usage.md' }, + { target: 'applied/svg-sprites/svg-sprites-intro.md', source: 'style-guide/applied/svg-sprites/svg-sprites-intro.md' }, + { target: 'applied/svg-sprites/svg-sprites-setup.md', source: 'style-guide/applied/svg-sprites/svg-sprites-setup.md' }, + { target: 'applied/svg-sprites/svg-sprites-usage.md', source: 'style-guide/applied/svg-sprites/svg-sprites-usage.md' }, + { target: 'applied/images.md', source: 'style-guide/applied/images.md' }, + { target: 'applied/fonts.md', source: 'style-guide/applied/fonts.md' }, + { target: 'applied/aliases.md', source: 'style-guide/applied/aliases.md' }, + { target: 'applied/templates/templates-intro.md', source: 'style-guide/applied/templates/templates-intro.md' }, + { target: 'applied/templates/templates-setup.md', source: 'style-guide/applied/templates/templates-setup.md' }, + { target: 'applied/templates/templates-create.md', source: 'style-guide/applied/templates/templates-create.md' }, + { target: 'applied/templates/templates-usage.md', source: 'style-guide/applied/templates/templates-usage.md' }, + { target: 'applied/biome.md', source: 'style-guide/applied/biome.md' }, + { target: 'applied/postcss.md', source: 'style-guide/applied/postcss.md' }, + { target: 'applied/vscode.md', source: 'style-guide/applied/vscode.md' }, + { target: 'applied/localization.md', source: 'style-guide/applied/localization.md' }, + { target: 'applied/stores.md', source: 'style-guide/applied/stores.md' }, +]; + +export const routeRewrites = [ + { from: '/docs/basics/architecture', to: '/slm-design/architecture' }, + { from: '/architecture', to: '/slm-design/architecture' }, + { from: '/examples', to: '/slm-design/examples' }, +]; + +export const sidebar = [ + { + text: 'Подсказки', + link: '/workflow', + }, + { + text: 'Архитектура', + items: [ + { + text: 'Спецификация SLM', + items: [ + { text: 'Обзор', link: '/slm-design/architecture/' }, + { text: 'Слои', link: '/slm-design/architecture/layers' }, + { text: 'Модули', link: '/slm-design/architecture/modules' }, + { text: 'Сегменты', link: '/slm-design/architecture/segments' }, + { text: 'Монорепозитории', link: '/slm-design/architecture/monorepo' }, + ], + }, + { + text: 'Примеры', + collapsed: true, + items: [ + { text: 'Создание фабрики', link: '/slm-design/examples/react/factory' }, + { text: 'Композиция фабрик', link: '/slm-design/examples/react/factory-composition' }, + { text: 'Композиция через Provider', link: '/slm-design/examples/react/composition-provider' }, + ], + }, + ], + }, + { + text: 'Базовые правила', + items: [ + { text: 'Технологии и библиотеки', link: '/basics/tech-stack' }, + { text: 'Именование', link: '/basics/naming' }, + { text: 'Стиль кода', link: '/basics/code-style' }, + { text: 'Документирование', link: '/basics/documentation' }, + { text: 'Типизация', link: '/basics/typing' }, + ], + }, + { + text: 'Прикладные разделы', + items: [ + { + text: 'Создание проекта', + collapsed: true, + items: [ + { text: 'Из шаблона', link: '/applied/creating-project/from-template' }, + { text: 'По гайду вручную', link: '/applied/creating-project/manual' }, + { text: 'Чистый Next.js', link: '/applied/creating-project/nextjs' }, + ], + }, + { text: 'Структура проекта', link: '/applied/project-structure' }, + { text: 'Страницы', link: '/applied/page-level' }, + { text: 'Компонент', link: '/applied/component' }, + { text: 'Модуль', link: '/applied/module' }, + { + text: 'REST-клиент', + collapsed: true, + items: [ + { text: 'Введение', link: '/applied/rest-client/' }, + { + text: 'Настройка', + collapsed: true, + items: [ + { text: 'Обзор', link: '/applied/rest-client/setup/' }, + { text: 'Автогенерация из OpenAPI', link: '/applied/rest-client/setup/auto' }, + { text: 'Ручное создание', link: '/applied/rest-client/setup/manual' }, + { text: 'GET-хуки REST-клиента', link: '/applied/rest-client/setup/hooks' }, + ], + }, + { text: 'Использование', link: '/applied/rest-client/usage' }, + ], + }, + { + text: 'Получение данных', + collapsed: true, + items: [ + { text: 'Обзор', link: '/applied/data-fetch/' }, + { text: 'Серверный await', link: '/applied/data-fetch/server-await' }, + { text: 'Параллельные серверные запросы', link: '/applied/data-fetch/parallel-server-requests' }, + { text: 'Передача промиса ниже', link: '/applied/data-fetch/pass-promise-down' }, + { text: 'Начальные данные для клиентских хуков', link: '/applied/data-fetch/client-hooks-initial-data' }, + { text: 'Клиентский GET-хук', link: '/applied/data-fetch/client-get-hook' }, + { text: 'Business-композиция', link: '/applied/data-fetch/business-composition' }, + ], + }, + { + text: 'Стили', + collapsed: true, + items: [ + { text: 'Настройка', link: '/applied/styles/styles-setup' }, + { text: 'Использование', link: '/applied/styles/styles-usage' }, + ], + }, + { + text: 'SVG-спрайты', + collapsed: true, + items: [ + { text: 'Введение', link: '/applied/svg-sprites/svg-sprites-intro' }, + { text: 'Настройка', link: '/applied/svg-sprites/svg-sprites-setup' }, + { text: 'Использование', link: '/applied/svg-sprites/svg-sprites-usage' }, + ], + }, + { text: 'Изображения', link: '/applied/images' }, + { text: 'Шрифты', link: '/applied/fonts' }, + { text: 'Алиасы импортов', link: '/applied/aliases' }, + { + text: 'Шаблоны генерации', + collapsed: true, + items: [ + { text: 'Введение', link: '/applied/templates/templates-intro' }, + { text: 'Настройка', link: '/applied/templates/templates-setup' }, + { text: 'Создание шаблонов', link: '/applied/templates/templates-create' }, + { text: 'Использование', link: '/applied/templates/templates-usage' }, + ], + }, + { text: 'Biome', link: '/applied/biome' }, + { text: 'PostCSS', link: '/applied/postcss' }, + { text: 'VS Code', link: '/applied/vscode' }, + { text: 'Локализация', link: '/applied/localization' }, + { text: 'Stores', link: '/applied/stores' }, + ], + }, +]; diff --git a/eslint.config.js b/eslint.config.js index ef614d2..34e6109 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,12 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores([ + 'dist', + 'docs/*/content', + 'docs/*/.vitepress/cache', + 'public', + ]), { files: ['**/*.{ts,tsx}'], extends: [ diff --git a/package.json b/package.json index 1eb3133..1a1f6f8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "build": "npm run site:generate && tsc -b && vite build", "docs:prepare:slm-design": "tsx scripts/docs/prepare.ts slm-design", "docs:build:slm-design": "npm run docs:prepare:slm-design && vitepress build docs/slm-design", + "docs:prepare:nextjs-style-guide": "tsx scripts/docs/prepare.ts nextjs-style-guide", + "docs:build:nextjs-style-guide": "npm run docs:prepare:nextjs-style-guide && vitepress build docs/nextjs-style-guide", "docs:prepare:figma-adaptive-standards": "tsx scripts/docs/prepare.ts figma-adaptive-standards", "docs:build:figma-adaptive-standards": "npm run docs:prepare:figma-adaptive-standards && vitepress build docs/figma-adaptive-standards", "site:generate": "tsx scripts/site/generate-artifacts.ts", diff --git a/scripts/docs/prepare.ts b/scripts/docs/prepare.ts index e0bb10b..703adf9 100644 --- a/scripts/docs/prepare.ts +++ b/scripts/docs/prepare.ts @@ -7,11 +7,17 @@ type Page = { target: string; }; -type DocsConfig = { - mounts: Page[]; +type RouteRewrite = { + from: string; + to: string; }; -const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:)([^)\s]+\.md)(#[^)]*)?\)/gi; +type DocsConfig = { + mounts: Page[]; + routeRewrites?: RouteRewrite[]; +}; + +const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:|\/\/)([^)\s]+)(#[^)]*)?\)/gi; const siteName = process.argv[2]; @@ -33,6 +39,7 @@ const config = (await import(pathToFileURL(configPath).href)) as DocsConfig; const targetBySource = new Map( config.mounts.map((page) => [normalizePath(page.source), normalizePath(page.target)]), ); +const routeRewrites = [...(config.routeRewrites ?? [])].sort((a, b) => b.from.length - a.from.length); function normalizePath(value: string) { return value.split(path.sep).join('/').replace(/^\.\//, ''); @@ -47,17 +54,71 @@ function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) { return relative.startsWith('.') ? relative : `./${relative}`; } +function applyRouteRewrites(route: string) { + for (const rewrite of routeRewrites) { + if (route === rewrite.from || route.startsWith(`${rewrite.from}/`) || route.startsWith(`${rewrite.from}#`)) { + return `${rewrite.to}${route.slice(rewrite.from.length)}`; + } + } + + return undefined; +} + +function formatDocsRoute(route: string) { + const rewritten = applyRouteRewrites(route); + if (rewritten) return rewritten; + + if (route === '/docs') return '/'; + if (route.startsWith('/docs/')) return route.slice('/docs'.length); + + return undefined; +} + +function formatRelativeRoute(hrefPath: string, sourceDir: string) { + const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath))); + + if (sourcePath.startsWith('style-guide/')) { + const route = `/docs/${sourcePath.slice('style-guide/'.length)}`; + + return applyRouteRewrites(route); + } + + return undefined; +} + function transformMarkdownLinks(content: string, page: Page) { const sourceDir = path.posix.dirname(normalizePath(page.source)); return content.replace(MD_LINK_RE, (match, href: string, hash = '') => { const [hrefPath, query = ''] = href.split('?'); + const queryPart = query ? `?${query}` : ''; + + if (hrefPath.startsWith('/')) { + const route = formatDocsRoute(hrefPath); + + if (route) return `](${route}${queryPart}${hash})`; + + const rewritten = applyRouteRewrites(hrefPath); + + if (rewritten) return `](${rewritten}${queryPart}${hash})`; + + return match; + } + + if (!hrefPath.endsWith('.md')) { + const route = formatRelativeRoute(hrefPath, sourceDir); + + if (route) return `](${route}${queryPart}${hash})`; + + return match; + } + const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath))); const target = targetBySource.get(sourcePath); if (!target) return match; - const nextHref = formatRelativeMarkdownPath(page.target, `${target}${query ? `?${query}` : ''}`); + const nextHref = formatRelativeMarkdownPath(page.target, `${target}${queryPart}`); return `](${nextHref}${hash})`; }); diff --git a/src/App.tsx b/src/App.tsx index 69c99a2..9cd6ff9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -98,11 +98,11 @@ const applyTheme = (theme: ThemeMode) => { function useTheme() { const [theme, setTheme] = useState(getStoredTheme) - const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(getStoredTheme())) + const [systemTheme, setSystemTheme] = useState(getSystemTheme) + const resolvedTheme = theme === 'auto' ? systemTheme : theme useLayoutEffect(() => { applyTheme(theme) - setResolvedTheme(resolveTheme(theme)) }, [theme]) useEffect(() => { @@ -110,13 +110,14 @@ function useTheme() { const mediaQuery = window.matchMedia(DARK_THEME_QUERY) const handleSystemThemeChange = () => { - if (theme !== 'auto') return + const nextResolvedTheme = getSystemTheme() - const nextResolvedTheme = resolveTheme(theme) + setSystemTheme(nextResolvedTheme) + + if (theme !== 'auto') return document.documentElement.style.colorScheme = nextResolvedTheme document.documentElement.classList.toggle('dark', nextResolvedTheme === 'dark') - setResolvedTheme(nextResolvedTheme) } mediaQuery.addEventListener('change', handleSystemThemeChange) diff --git a/src/config/docs.config.ts b/src/config/docs.config.ts index 06d02a2..a4b8083 100644 --- a/src/config/docs.config.ts +++ b/src/config/docs.config.ts @@ -30,10 +30,11 @@ export const docs: DocCard[] = [ }, { title: 'NextJS Style Guide', - label: 'Стиль проекта', + label: 'Стайлгайд', mark: 'NX', - description: 'Правила организации Next.js-приложений, роутинга, серверных границ и проектных соглашений.', - status: 'Скоро', + description: 'Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.', + href: '/nextjs-style-guide/', + status: 'Доступно', accent: 'blue', links: [ { label: 'llms.txt', href: '/nextjs-style-guide/llms.txt' }, @@ -42,7 +43,7 @@ export const docs: DocCard[] = [ }, { title: 'React Style Guide', - label: 'Стиль кода', + label: 'Стайлгайд', mark: 'RE', description: 'Практики написания React-компонентов, хуков, состояния и клиентского UI-кода.', status: 'Скоро',