feat: add example

This commit is contained in:
2026-08-01 09:31:08 +03:00
parent 15805e28df
commit 26b59686a5
434 changed files with 34975 additions and 4995 deletions

113
.templates/README.md Normal file
View File

@@ -0,0 +1,113 @@
# Шаблоны монорепозитория
Шаблоны создают повторяемый boilerplate после принятого SLM-решения. Шаблон не выбирает owner, layer, scope, public API или product data path.
Запускай генератор из корня монорепозитория, где находится `.templates`:
```bash
npx @gromlab/create <template> <name> [path]
```
`[path]` — папка вывода относительно корня монорепозитория, например `apps/admin/src/compositions/screens`.
## Как выбирать шаблон
Сначала примени `slm-design`. Затем выбирай специализированный шаблон: `route`, `layout`, `screen`, `widget`, `business`, `business-composition` или `rest-api`.
Если сущность не попадает под один из специализированных шаблонов, используй `module`.
`module` — основной универсальный шаблон для обычных SLM-модулей и компонентов внутри существующих модулей.
## Доступные шаблоны
| Шаблон | Для чего | Куда генерировать |
|---|---|---|
| `route` | Leaf route module Next.js с обязательным business provider, одноимённым screen и public API. | `apps/web/src/compositions/routes` |
| `layout` | Layout composition с `*.layout.tsx`, стилями, props и public API. | `apps/{app}/src/compositions/layouts` |
| `screen` | Screen composition с `*.screen.tsx`, стилями, props и public API. | `apps/{app}/src/compositions/screens` |
| `widget` | Переиспользуемый composition widget, не привязанный к одной странице. | `apps/{app}/src/compositions/widgets` |
| `business` | Business-домен слоя `src/business`: factory, deps, domain error и public API. | `apps/{app}/src/business` |
| `business-composition` | Runtime-сборка business-домена: `create*Business` и public API. | `apps/{app}/src/compositions/business` |
| `rest-api` | Vite runtime REST-модуль поверх generated SDK: transport, errors и полный bound-клиент. Только для `apps/admin`. | `apps/admin/src/infra` |
| `module` | Универсальный SLM-модуль или компонент, если сущность не подходит под шаблоны выше. | `apps/{app}/src/**/parts`, `apps/{app}/src/**/ui`, локальные сегменты модулей |
## Route
`route` создаёт leaf route module Next.js в `apps/web/src/compositions/routes`.
Передавай имя без суффикса `-route`: генератор создаст папку `{name}`, файл `{name}.route.tsx`, компонент `{Name}Route` и обязательный `{Name}BusinessProvider`.
Route и одноимённый screen должны представлять одну leaf-страницу: `home` route подключает `HomeScreen`, `product` route подключает `ProductScreen`.
Route module не собирает общий layout. Общий каркас route group остаётся в соответствующем `app/**/layout.tsx` и подключает layout composition напрямую.
Business provider создаётся всегда, даже если route пока не владеет business API. До появления реального graph он передаёт точное пустое значение `value={{}}`.
После генерации добавляй в provider только реально используемые route-level business API и определяй их lifecycle внутри route scope.
Экспортируй route component через public API модуля и подключай его из соответствующего `app/**/page.tsx`.
## Module
`module` — fallback-шаблон проекта.
Используй его для:
- обычных вложенных модулей в `parts`;
- компонентов внутри `ui` родительского модуля;
- UI-модулей слоя `src/ui`;
- небольших composition-модулей, для которых нет отдельного шаблона;
- любой повторяемой сущности, которая не является `route`, `layout`, `screen`, `widget`, `business` или `business-composition`.
Шаблон создаёт корневой `.tsx`, `styles`, `types` и `index.ts`. После генерации убирай лишнее вручную, если конкретному модулю не нужны стили или props.
## Business
`business` создаёт каркас доменного модуля слоя `src/business`.
После генерации:
- не оставляй пустые `Api` и `Deps`: замени их реальными сценариями и минимальными runtime capabilities;
- добавь `services`, `hooks`, `mappers`, `lib` и доменные типы по мере появления логики;
- расширь `ERROR_CODES` конкретными кодами;
- добавь factory-level tests до завершения задачи.
## Business Composition
`business-composition` создаёт runtime-сборку business-домена для application scope.
После генерации:
- замени пустой вызов `factory({})` на явные private adapters и config;
- добавь отдельный `adapters/*` для каждой runtime capability из `Deps`;
- добавь `types/create-*-business-deps.type.ts`, если сборка зависит от другого business API;
- не экспортируй adapters через `index.ts`;
- добавь assembly tests до завершения задачи.
## REST API
`rest-api` создаёт Vite runtime infra-модуль для generated SDK `{name}-rest-api-sdk`. Шаблон не выбирает auth strategy и не предназначен для Next.js.
Передавай имя сервиса без суффикса `-rest-api`: `adp-client` создаст `src/infra/adp-client-rest-api`.
После генерации:
- настрой фактическую переменную окружения для base URL;
- добавь auth, cookies, CSRF и lifecycle только по фактическому контракту сервиса;
- экспортируй только transport API и точечные types, имеющие реальных consumers;
- подключай product operations через private adapters в `compositions/business/{domain}`;
- не используй GET-хуки напрямую из page, route, screen, widget или UI.
## Примеры
```bash
npx @gromlab/create route home apps/web/src/compositions/routes
npx @gromlab/create layout main apps/admin/src/compositions/layouts
npx @gromlab/create screen main apps/admin/src/compositions/screens
npx @gromlab/create widget error-state apps/admin/src/compositions/widgets
npx @gromlab/create business user apps/admin/src/business
npx @gromlab/create business-composition user apps/admin/src/compositions/business
npx @gromlab/create rest-api backend apps/admin/src/infra
npx @gromlab/create module hero-section apps/admin/src/compositions/screens/main/parts
npx @gromlab/create module submit-button apps/admin/src/compositions/screens/main/ui
```

View File

@@ -0,0 +1,9 @@
import { {{name.camelCase}}Factory } from '../../../business/{{name.kebabCase}}'
import type { {{name.pascalCase}}Api } from '../../../business/{{name.kebabCase}}'
/**
* Создаёт {{name.pascalCase}} business API с runtime-зависимостями приложения.
*/
export const create{{name.pascalCase}}Business = (): {{name.pascalCase}}Api => {
return {{name.camelCase}}Factory({})
}

View File

@@ -0,0 +1 @@
export { create{{name.pascalCase}}Business } from './create-{{name.kebabCase}}-business'

View File

@@ -0,0 +1,21 @@
import type { {{name.pascalCase}}ErrorCode } from '../types/{{name.kebabCase}}-error-code.type'
/**
* Доменная ошибка бизнес-модуля {{name.pascalCase}}.
*
* UI/i18n-слой должен использовать `code`, а не `message`, для выбора пользовательского текста.
*/
export class {{name.pascalCase}}BusinessError extends Error {
/** Код доменной ошибки для UI/i18n-слоя. */
readonly code: {{name.pascalCase}}ErrorCode
/** Исходная ошибка нижележащей runtime-зависимости. */
readonly cause: unknown
constructor(code: {{name.pascalCase}}ErrorCode, cause: unknown) {
super(code)
this.name = '{{name.pascalCase}}BusinessError'
this.code = code
this.cause = cause
}
}

View File

@@ -0,0 +1,6 @@
export { {{name.camelCase}}Factory } from './{{name.kebabCase}}.factory'
export type { {{name.pascalCase}}Api } from './types/{{name.kebabCase}}-api.type'
export type { {{name.pascalCase}}Deps } from './types/{{name.kebabCase}}-deps.type'
export type { {{name.pascalCase}}Error } from './types/{{name.kebabCase}}-error.type'
export type { {{name.pascalCase}}ErrorCode } from './types/{{name.kebabCase}}-error-code.type'
export type { {{name.pascalCase}}Factory } from './types/{{name.kebabCase}}-factory.type'

View File

@@ -0,0 +1,4 @@
/**
* Публичный API бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Api = object

View File

@@ -0,0 +1,4 @@
/**
* Runtime-зависимости бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Deps = object

View File

@@ -0,0 +1,12 @@
/**
* Коды доменных ошибок бизнес-модуля {{name.pascalCase}}.
*/
export const {{name.screamingSnakeCase}}_ERROR_CODES = {
/** Базовая доменная ошибка бизнес-модуля {{name.pascalCase}}. */
UNKNOWN: '{{name.screamingSnakeCase}}_UNKNOWN'
} as const
/**
* Код доменной ошибки бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}ErrorCode = (typeof {{name.screamingSnakeCase}}_ERROR_CODES)[keyof typeof {{name.screamingSnakeCase}}_ERROR_CODES]

View File

@@ -0,0 +1,15 @@
import type { {{name.pascalCase}}ErrorCode } from './{{name.kebabCase}}-error-code.type'
/**
* Публичный структурный контракт доменной ошибки {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Error = {
/** Исходная ошибка нижележащей runtime-зависимости. */
cause: unknown
/** Код доменной ошибки для UI/i18n-слоя. */
code: {{name.pascalCase}}ErrorCode
/** Сообщение ошибки, не используемое как UI-контракт. */
message: string
/** Имя доменной ошибки для диагностики. */
name: string
}

View File

@@ -0,0 +1,7 @@
import type { {{name.pascalCase}}Api } from './{{name.kebabCase}}-api.type'
import type { {{name.pascalCase}}Deps } from './{{name.kebabCase}}-deps.type'
/**
* Фабрика публичного API бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Factory = (deps: {{name.pascalCase}}Deps) => {{name.pascalCase}}Api

View File

@@ -0,0 +1,8 @@
import type { {{name.pascalCase}}Factory } from './types/{{name.kebabCase}}-factory.type'
/**
* Создаёт публичный API бизнес-модуля {{name.pascalCase}}.
*/
export const {{name.camelCase}}Factory: {{name.pascalCase}}Factory = (_deps) => {
return {}
}

View File

@@ -0,0 +1,2 @@
export { {{name.pascalCase}}Layout } from './{{name.kebabCase}}.layout'
export type { {{name.pascalCase}}LayoutProps } from './types/{{name.kebabCase}}-layout-props.type'

View File

@@ -0,0 +1,2 @@
.root {
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры layout {{name.pascalCase}}.
*/
export type {{name.pascalCase}}LayoutParams = object
/** Атрибуты корневого элемента. */
type RootAttrs = ComponentPropsWithoutRef<'div'>
export type {{name.pascalCase}}LayoutProps = RootAttrs & {{name.pascalCase}}LayoutParams

View File

@@ -0,0 +1,20 @@
import cl from 'clsx'
import type { {{name.pascalCase}}LayoutProps } from './types/{{name.kebabCase}}-layout-props.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение layout {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}}Layout = (props: {{name.pascalCase}}LayoutProps) => {
const { children, className, ...rootAttrs } = props
return (
<div {...rootAttrs} className={cl(styles.root, className)}>
{children}
</div>
)
}

View File

@@ -0,0 +1,2 @@
export { {{name.pascalCase}} } from './{{name.kebabCase}}'
export type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}-props.type'

View File

@@ -0,0 +1,2 @@
.root {
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Params = object
/** Атрибуты корневого элемента. */
type RootAttrs = ComponentPropsWithoutRef<'div'>
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params

View File

@@ -0,0 +1,20 @@
import cl from 'clsx'
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}-props.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
const { children, className, ...rootAttrs } = props
return (
<div {...rootAttrs} className={cl(styles.root, className)}>
{children}
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { HttpClient } from '@biocad/{{name.kebabCase}}-rest-api-sdk'
import {
{{name.screamingSnakeCase}}_REST_API_BASE_URL,
{{name.screamingSnakeCase}}_REST_API_TIMEOUT_MS
} from './config/{{name.kebabCase}}-rest-api.config'
import { {{name.camelCase}}RestApiFetch } from './lib/{{name.kebabCase}}-rest-api-fetch'
/** Транспортный HTTP-клиент {{name.pascalCase}} REST API. */
export const {{name.camelCase}}HttpClient = new HttpClient({
baseUrl: {{name.screamingSnakeCase}}_REST_API_BASE_URL,
customFetch: {{name.camelCase}}RestApiFetch,
timeout: {{name.screamingSnakeCase}}_REST_API_TIMEOUT_MS
})

View File

@@ -0,0 +1,5 @@
/** Базовый URL {{name.pascalCase}} REST API. */
export const {{name.screamingSnakeCase}}_REST_API_BASE_URL = import.meta.env.VITE_{{name.screamingSnakeCase}}_REST_API_BASE_URL ?? ''
/** Максимальная длительность REST-запроса до автоматической отмены. */
export const {{name.screamingSnakeCase}}_REST_API_TIMEOUT_MS = 30_000

View File

@@ -0,0 +1,3 @@
export { ApiError as {{name.pascalCase}}RestApiError } from '@biocad/{{name.kebabCase}}-rest-api-sdk'
export { {{name.pascalCase}}RestApiTransportError } from './{{name.kebabCase}}-rest-api-transport.error'
export type { {{name.pascalCase}}RestApiTransportErrorCode } from './{{name.kebabCase}}-rest-api-transport.error'

View File

@@ -0,0 +1,20 @@
/**
* Причина сбоя непосредственно на transport-границе {{name.pascalCase}} REST API.
*/
export type {{name.pascalCase}}RestApiTransportErrorCode = 'NETWORK_UNAVAILABLE' | 'REQUEST_TIMEOUT'
/**
* Маркирует fetch failure, не смешивая его с TypeError из прикладного кода.
*/
export class {{name.pascalCase}}RestApiTransportError extends Error {
readonly code: {{name.pascalCase}}RestApiTransportErrorCode
readonly cause: unknown
constructor(code: {{name.pascalCase}}RestApiTransportErrorCode, cause: unknown) {
super(code)
this.name = '{{name.pascalCase}}RestApiTransportError'
this.code = code
this.cause = cause
}
}

View File

@@ -0,0 +1,3 @@
export { {{name.camelCase}}HttpClient } from './client'
export { {{name.pascalCase}}RestApiError, {{name.pascalCase}}RestApiTransportError } from './errors'
export { {{name.camelCase}}RestApi } from './rest-api'

View File

@@ -0,0 +1,14 @@
import { {{name.pascalCase}}RestApiTransportError } from '../errors'
/**
* Выполняет fetch с явной классификацией transport failure.
*/
export const {{name.camelCase}}RestApiFetch: typeof fetch = async (...params) => {
try {
return await fetch(...params)
} catch (error) {
const code = error instanceof DOMException && error.name === 'AbortError' ? 'REQUEST_TIMEOUT' : 'NETWORK_UNAVAILABLE'
throw new {{name.pascalCase}}RestApiTransportError(code, error)
}
}

View File

@@ -0,0 +1,7 @@
import { createApiClient } from '@biocad/{{name.kebabCase}}-rest-api-sdk/create-api-client'
import { operationsTree } from '@biocad/{{name.kebabCase}}-rest-api-sdk/operations-tree'
import { {{name.camelCase}}HttpClient } from './client'
/** Полный bound-клиент {{name.pascalCase}} REST API. */
export const {{name.camelCase}}RestApi = createApiClient({{name.camelCase}}HttpClient, operationsTree)

View File

@@ -0,0 +1 @@
export { {{name.pascalCase}}Route } from './{{name.kebabCase}}.route'

View File

@@ -0,0 +1,18 @@
import { BusinessProvider } from '@/infra/business'
import type { {{name.pascalCase}}BusinessProviderProps } from '../types/{{name.kebabCase}}-business-provider-props.type'
/**
* Route-level provider бизнес-модулей для {{name.pascalCase}} route.
*
* Используется для:
* - владения business API в lifecycle route-ветки
*/
export const {{name.pascalCase}}BusinessProvider = (props: {{name.pascalCase}}BusinessProviderProps) => {
const { children } = props
return (
<BusinessProvider value={{}}>
{children}
</BusinessProvider>
)
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'
/**
* Параметры {{name.pascalCase}}BusinessProvider.
*/
export type {{name.pascalCase}}BusinessProviderProps = {
/** Дочернее дерево {{name.pascalCase}} route. */
children: ReactNode
}

View File

@@ -0,0 +1,16 @@
import { {{name.pascalCase}}Screen } from '@/compositions/screens/{{name.kebabCase}}'
import { {{name.pascalCase}}BusinessProvider } from './providers/{{name.kebabCase}}-business.provider'
/**
* Route module страницы {{name.pascalCase}}.
*
* Используется для:
* - сборки route-level business graph и screen страницы
*/
export const {{name.pascalCase}}Route = () => {
return (
<{{name.pascalCase}}BusinessProvider>
<{{name.pascalCase}}Screen />
</{{name.pascalCase}}BusinessProvider>
)
}

View File

@@ -0,0 +1,2 @@
export { {{name.pascalCase}}Screen } from './{{name.kebabCase}}.screen'
export type { {{name.pascalCase}}ScreenProps } from './types/{{name.kebabCase}}-screen-props.type'

View File

@@ -0,0 +1,2 @@
.root {
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры экрана {{name.pascalCase}}.
*/
export type {{name.pascalCase}}ScreenParams = object
/** Атрибуты корневого элемента. */
type RootAttrs = ComponentPropsWithoutRef<'main'>
export type {{name.pascalCase}}ScreenProps = RootAttrs & {{name.pascalCase}}ScreenParams

View File

@@ -0,0 +1,20 @@
import cl from 'clsx'
import type { {{name.pascalCase}}ScreenProps } from './types/{{name.kebabCase}}-screen-props.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение экрана {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}}Screen = (props: {{name.pascalCase}}ScreenProps) => {
const { children, className, ...rootAttrs } = props
return (
<main {...rootAttrs} className={cl(styles.root, className)}>
{children}
</main>
)
}

View File

@@ -0,0 +1,2 @@
export { {{name.pascalCase}}Widget } from './{{name.kebabCase}}.widget'
export type { {{name.pascalCase}}WidgetProps } from './types/{{name.kebabCase}}-widget-props.type'

View File

@@ -0,0 +1,2 @@
.root {
}

View File

@@ -0,0 +1,11 @@
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры виджета {{name.pascalCase}}.
*/
export type {{name.pascalCase}}WidgetParams = object
/** Атрибуты корневого элемента. */
type RootAttrs = ComponentPropsWithoutRef<'div'>
export type {{name.pascalCase}}WidgetProps = RootAttrs & {{name.pascalCase}}WidgetParams

View File

@@ -0,0 +1,20 @@
import cl from 'clsx'
import type { {{name.pascalCase}}WidgetProps } from './types/{{name.kebabCase}}-widget-props.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение виджета {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}}Widget = (props: {{name.pascalCase}}WidgetProps) => {
const { children, className, ...rootAttrs } = props
return (
<div {...rootAttrs} className={cl(styles.root, className)}>
{children}
</div>
)
}