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

View File

@@ -0,0 +1,537 @@
---
name: rest-client
description: "Используй при создании, изменении или ревью REST API клиента и infra REST-модуля. Триггеры: REST API, REST client, API client, backend API, external API, OpenAPI, Swagger, ручной клиент без OpenAPI, @gromlab/api-codegen, @gromlab/api-codegen@5.1.0, src/infra/*-rest-api, *-rest-api-sdk, REST SDK, npm SDK, client.ts, rest-api.ts, operations-tree.ts, *HttpClient, *RestApi, generated/, operations/, operations/*, data-contracts, hooks/, types/, errors/, HttpClient, ApiRequestClient, RequestParams, ContentType, createApiClient, operationsTree, full client, minimal client, SDK exports, onRequest, onResponse, onError, JWT, refresh token, ApiError, useGet*, get*Key, useSWR, SWRConfiguration, DTO, API error, public API REST-модуля. НЕ используй для любого fetch вне REST-клиента проекта, route-level data fetching Next.js, SLM-архитектуры без REST-модуля, code style, SVG sprites или генерации шаблонов."
---
<!-- Generated from src/SKILL.md. Do not edit manually. -->
# REST Client
## Работа с REST-клиентом
### Базовые Правила
- REST-клиент сервиса оформляй как отдельный `infra/` module с именем `{name}-rest-api`: `pet-store-rest-api`, `billing-rest-api`, `maps-rest-api`.
- Генерация внутри клиента живёт в `{name}-rest-api/generated`. Generated-клиент, выносимый в npm-пакет или пакет монорепозитория, называется `{name}-rest-api-sdk`.
- Внешний код импортирует клиент, типы, enum и GET-хуки только через корневой `index.ts` REST-модуля.
- `client.ts` экспортирует только настроенный транспортный `*HttpClient`: `petStoreHttpClient`, `billingHttpClient`, `mapsHttpClient`.
- `rest-api.ts` экспортирует полный bound API-клиент `*RestApi`, собранный через `createApiClient(*HttpClient, operationsTree)`.
- Не размещай в `client.ts` DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логику.
- GET-хуки не импортируют полный `*RestApi`; они импортируют точечную operation и вызывают её через общий `*HttpClient`.
- `hooks/index.ts` содержит `'use client'`; отдельные `use-get-*.hook.ts` остаются обычными файлами без директивы.
- Если OpenAPI нет, ручные operations пиши через `@gromlab/api-codegen@5.1.0+` с тем же контрактом, что у generated SDK: `operation(http, params/body, requestParams?)`.
- Не создавай собственные `fetch`-классы, `methods/*Methods(client)` и ручные `get/post` wrappers для нового ручного клиента.
- Не правь файлы в `generated/` руками. Расширения типов, DTO и именованные response-типы держи в `types/` REST-модуля.
- UI/components не импортируют SDK-пакет, `generated/`, `operations/`, `operationsTree` и транспорт напрямую.
- SDK operations напрямую импортируются только внутри `infra/*-rest-api` и boundary-файлов минимальных clients в `compositions` или `features`.
- Для прямого REST-вызова в server code, submit-функции или сервисе используй именованный API-объект клиента из публичного API REST-модуля или минимальный composition client.
- Для GET-данных в Client Component сначала используй готовый `useGet*` hook REST-модуля.
- GET-хуки являются прозрачными SWR-обёртками над GET operation и живут в `hooks/` этого REST-модуля.
- Не выноси SWR keys, fetcher, DTO mapping, API errors и транспортные детали в UI-компоненты.
- Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` или `hasPets`, это уже не REST-клиент, а `business/`.
### Full Client vs Minimal Client
- Full client живёт только в `infra/{name}-rest-api/rest-api.ts`.
- Full client собирается из всего generated дерева: `createApiClient(nameHttpClient, operationsTree)`.
- Full client экспортируется через `infra/{name}-rest-api/index.ts` как `nameRestApi`.
- Minimal client допустим только в feature/composition boundary-файле, где нужен ограниченный набор операций для конкретного бизнес-сценария.
- Minimal client собирается из selected operations: `createApiClient(nameHttpClient, { ...только нужные операции... })`.
- GET-hook не использует bound client вообще: он вызывает `operation(nameHttpClient, params, requestParams?)`.
- `operationsTree` не импортируется в GET-хуки и minimal clients.
- Для ручного клиента `operationsTree` пишется вручную в `infra/{name}-rest-api/operations-tree.ts`.
### Рабочий Алгоритм
1. Найди REST-модуль сервиса в `infra/` и работай через его публичный API.
2. Проверь корневой `index.ts`: внешний код не должен импортировать из `generated/`, `hooks/`, `types/` или `errors/` напрямую.
3. Если нужен прямой REST-вызов в server code, submit-функции или сервисе, используй полный `*RestApi` из `infra/{name}-rest-api`.
4. Если данные нужны в Client Component и запрос является GET, сначала ищи готовый `useGet*` hook.
5. Если GET-хука нет, добавь его рядом с клиентом в `hooks/` по контракту раздела `GET-хуки REST-клиента`.
6. Если feature/composition нужен компактный клиент с небольшим набором операций, создай minimal client в boundary-файле этой composition.
7. Если нужно изменить тип ответа или дополнить generated-тип, не меняй generated-файл; добавь тип или расширение в `types/`.
8. Если REST-клиента ещё нет или нужно подключить новый внешний API, открой конкретный локальный setup-материал ниже.
9. После изменений проверь публичные экспорты, отсутствие SDK imports в UI/components и то, что SWR-механика не утекла в компоненты.
### Создание И Настройка Клиента
Создание нового REST-клиента - редкий сценарий. Не открывай setup-материалы, если задача сводится к использованию существующего клиента, GET-хука или публичного API.
- [Настройка REST-клиента](./reference/canons/setup.md) - состав REST-клиента, структура модуля и базовая настройка.
- [Автогенерация из OpenAPI](./reference/canons/auto.md) - генерация split-клиента через `@gromlab/api-codegen`.
- [Кастомизация HTTP-клиента](./reference/canons/http-client.md) - опции и хуки `HttpClient`: авторизация, refresh token, транспорт.
- [SDK-пакет REST-клиента](./reference/canons/sdk.md) - вынос generated-клиента в npm-пакет или пакет монорепозитория `{name}-rest-api-sdk`.
- [Ручное создание](./reference/canons/manual.md) - ручной REST-клиент, если OpenAPI нет или он неполный.
### Включённые Разделы
- [Использование REST-клиента](#использование-rest-клиента) - прямой вызов готового клиента.
- [GET-хуки REST-клиента](#get-хуки-rest-клиента) - контракт `useGet*`, key-функций и SWR-обёрток.
## Использование REST-клиента
Как выбрать правильную точку вызова REST API.
### Прямой вызов полного API
Для server code, submit-функции, adapter или сервиса импортируйте полный API-клиент из публичного API REST-модуля.
```ts
import { petStoreRestApi } from 'infra/pet-store-rest-api'
export const getPet = async (petId: number) => {
return petStoreRestApi.pet.getPetById({ petId })
}
```
Внешний код не импортирует SDK-пакет, `generated/`, `operations/`, `operationsTree`, `client.ts` или `rest-api.ts` напрямую.
### Minimal Client В Composition Boundary
Если business composition нужен компактный клиент из нескольких операций, собирайте его в boundary-файле этой composition.
```ts
// src/compositions/business/pet-store/orders/pet-store-rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { createOrder } from '@company/pet-store-rest-api-sdk/operations/create-order'
import { getOrder } from '@company/pet-store-rest-api-sdk/operations/get-order'
import { petStoreHttpClient } from 'infra/pet-store-rest-api'
export const petStoreOrdersRestApi = createApiClient(petStoreHttpClient, {
orders: {
create: createOrder,
get: getOrder,
},
})
```
Minimal client не экспортируется из общего `infra/{name}-rest-api`. Он принадлежит конкретному feature/composition boundary и содержит только операции этого сценария.
### GET В Client Components
Client Components используют только готовые `useGet*` hooks REST-модуля.
```tsx
import { useGetPetDetail } from 'infra/pet-store-rest-api'
export const PetCard = ({ petId }: { petId: number }) => {
const { data: pet } = useGetPetDetail({ petId })
return <div>{pet?.name}</div>
}
```
Не вызывайте `useSWR`, SDK operation или полный `*RestApi` прямо в UI-компоненте.
## GET-хуки REST-клиента
Прозрачные SWR-обёртки над GET operations REST-клиента.
### Зачем нужны
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
### Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text
src/infra/
└── pet-store-rest-api/
├── client.ts
├── rest-api.ts
├── generated/
├── hooks/
│ ├── lib/
│ │ └── create-query-string.ts
│ ├── use-get-pet-list.hook.ts
│ ├── use-get-pet-detail.hook.ts
│ └── index.ts
├── types/
└── index.ts
```
### Контракт
- Один GET-хук = одна GET operation.
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
- Для GET operation без параметров хук принимает только `config?: SWRConfiguration<Data>`.
- Key-функция принимает те же `params`, что и хук.
- Key-функция возвращает `null`, если обязательные параметры не готовы.
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
- Хук вызывает `useSWR` один раз и безусловно.
- Fetcher вызывает точечную operation через общий `*HttpClient`: `operation(nameHttpClient, params, requestParams?)`.
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает operation с `null`.
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
- Хук не объединяет несколько запросов.
- Хук не маппит DTO в доменную модель.
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
- Хук не импортирует полный `*RestApi`, `createApiClient` или `operationsTree`.
- `hooks/index.ts` содержит `'use client'`; отдельные `use-get-*.hook.ts` не содержат эту директиву.
### Формат SWR-ключа
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
Формат ключа:
```ts
['pet-store-rest-api', '/pet/10'] as const
```
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
- Второй элемент — endpoint запроса: path и query string.
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
- Не используйте произвольные части вроде `['pet-store-rest-api', 'pet', 'detail', params]`.
- Не используйте только строку endpoint без имени сервиса.
Примеры ключей:
```ts
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}
```
```ts
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-rest-api', `/pet/findByStatus?status=${params.status}`] as const
}
```
```ts
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
if (!params?.tags.length) {
return null
}
return ['pet-store-rest-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
}
```
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
### Query String Для Key
Если key зависит от query-параметров, собирайте query string отдельной маленькой функцией или общим helper внутри `hooks/lib/`.
```ts
// src/infra/pet-store-rest-api/hooks/lib/create-query-string.ts
type QueryValue = boolean | number | string | null | undefined
export const createQueryString = (query: Record<string, QueryValue>): string => {
const searchParams = new URLSearchParams()
Object.entries(query).forEach(([key, value]) => {
if (value === null || value === undefined || value === '') {
return
}
searchParams.set(key, String(value))
})
const search = searchParams.toString()
return search ? `?${search}` : ''
}
```
Key должен отражать фактический URL запроса: path плюс query string. Не кладите весь `params` object в SWR key.
### Пример списка
```ts
// src/infra/pet-store-rest-api/hooks/use-get-pet-list.hook.ts
import { findPetsByStatus } from '../generated/operations/find-pets-by-status'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import { createQueryString } from './lib/create-query-string'
import type { FindPetsByStatusParams, Pet } from '../generated'
const getPetListQuery = (params: FindPetsByStatusParams): string => {
return createQueryString({ status: params.status })
}
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-rest-api', `/pet/findByStatus${getPetListQuery(params)}`] as const
}
/**
* Получает список питомцев по статусу.
*/
export const useGetPetList = (
params?: FindPetsByStatusParams | null,
config?: SWRConfiguration<Pet[]>,
) => {
const key = getPetListKey(params)
const fetcher = () => findPetsByStatus(
petStoreHttpClient,
params as FindPetsByStatusParams,
)
return useSWR<Pet[]>(key, fetcher, config)
}
```
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
### Пример detail-запроса
```ts
// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
import { getPetById } from '../generated/operations/get-pet-by-id'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { GetPetByIdParams, Pet } from '../generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
### Пример без параметров
```ts
// src/infra/pet-store-rest-api/hooks/use-get-store-inventory.hook.ts
import { getStoreInventory } from '../generated/operations/get-store-inventory'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { StoreInventory } from '../types'
export const getStoreInventoryKey = () => {
return ['pet-store-rest-api', '/store/inventory'] as const
}
/**
* Получает инвентарь магазина.
*/
export const useGetStoreInventory = (
config?: SWRConfiguration<StoreInventory>,
) => {
return useSWR<StoreInventory>(
getStoreInventoryKey(),
() => getStoreInventory(petStoreHttpClient),
config,
)
}
```
Если generated operation возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
### Пример С Request Params
Если operation зависит от разовых headers или дополнительных query-параметров, соберите `requestParams` внутри fetcher. Key-функция должна учитывать параметры, которые меняют результат запроса.
```ts
// src/infra/cms-rest-api/hooks/use-get-post-detail.hook.ts
import { postsDetail } from '@company/cms-rest-api-sdk/operations/posts-detail'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { cmsHttpClient } from '../client'
import { createQueryString } from './lib/create-query-string'
import type {
PostDetail,
PostsDetailParams,
RequestParams,
} from '@company/cms-rest-api-sdk'
export type GetPostDetailParams = PostsDetailParams & {
app?: string
}
const getPostDetailPath = (params: GetPostDetailParams): string => {
return `/v1/posts/${params.slug}${createQueryString({
app: params.app,
status: params.status,
})}`
}
export const getPostDetailKey = (params?: GetPostDetailParams | null) => {
if (!params?.slug) {
return null
}
return ['cms-rest-api', getPostDetailPath(params)] as const
}
export const useGetPostDetail = (
params?: GetPostDetailParams | null,
config?: SWRConfiguration<PostDetail>,
) => {
const key = getPostDetailKey(params)
const fetcher = () => {
const { app, ...postParams } = params as GetPostDetailParams
const requestParams: RequestParams = {
headers: app ? { 'x-app': app } : undefined,
}
return postsDetail(cmsHttpClient, postParams, requestParams)
}
return useSWR<PostDetail>(key, fetcher, config)
}
```
### Отложенный запрос
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
```ts
const key = getPetDetailKey(params)
```
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
### Экспорт
```ts
// src/infra/pet-store-rest-api/hooks/index.ts
'use client'
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
export {
getStoreInventoryKey,
useGetStoreInventory,
} from './use-get-store-inventory.hook'
```
```ts
// src/infra/pet-store-rest-api/index.ts
export { petStoreHttpClient } from './client'
export { petStoreRestApi } from './rest-api'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated'
export * from './hooks'
export type { StoreInventory } from './types'
```
Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/` и не из `hooks/` напрямую.
### Где заканчивается infra
```ts
// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList({ status: 'available' })
```
```ts
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList({ status: 'available' })
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
### Что запрещено
```ts
// Плохо — useSWR в компоненте
const { data } = useSWR(
['pet-store-rest-api', '/pet/findByStatus?status=available'],
() => findPetsByStatus(petStoreHttpClient, { status: 'available' }),
)
// Плохо — проверка готовности размазана по хуку
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = params?.petId ? getPetDetailKey(params) : null
const fetcher = () => {
if (!params?.petId) {
throw new Error('Pet id is required')
}
return getPetById(petStoreHttpClient, params)
}
return useSWR<Pet>(key, fetcher)
}
// Плохо — условный вызов useSWR нарушает rules of hooks
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = getPetDetailKey(params)
if (key === null) {
return useSWR(null, null)
}
return useSWR(key, () => getPetById(petStoreHttpClient, params))
}
// Плохо — GET-хук импортирует полный bound client вместо точечной operation
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = getPetDetailKey(params)
return useSWR(
key,
() => petStoreRestApi.pet.getPetById(params as GetPetByIdParams),
)
}
// Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => {
const available = useGetPetList({ status: 'available' })
const sold = useGetPetList({ status: 'sold' })
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
Потребление таких хуков на уровне route-level data fetching относится к `nextjs-style-guide`.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "REST Client"
short_description: "Generated и manual REST-клиенты проекта"
default_prompt: "Use $rest-client to generate, update, or review REST clients and their integration points in the monorepo."

View File

@@ -0,0 +1,342 @@
---
title: Автогенерация REST-клиента
description: Генерация split REST-клиента из OpenAPI-спецификации.
keywords: [rest, openapi, api-codegen, автогенерация, generated, split, operations, npx]
---
# Автогенерация REST-клиента
Генерация REST-клиента из OpenAPI-спецификации.
## Когда использовать
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт split-клиент: HTTP-клиент, типы и отдельную operation-функцию на каждый endpoint. Разработчик вручную добавляет транспорт в `client.ts`, полный API в `rest-api.ts` и GET-хуки.
По умолчанию генерация идёт внутрь infra-модуля приложения в `{name}-rest-api/generated` — этот сценарий описан ниже. Если клиент нужен нескольким приложениям, generated-код выносится в отдельный пакет `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
## Пример API
В примерах используется Swagger Petstore:
```text
https://petstore3.swagger.io/api/v3/openapi.json
```
Имена модуля:
```text
src/infra/pet-store-rest-api/
petStoreRestApi
```
## Скрипт генерации
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
```json
{
"scripts": {
"codegen:pet-store-rest-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-rest-api/generated"
}
}
```
Параметры:
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
- `-o` — директория для сгенерированного split-клиента.
По умолчанию генератор работает в режиме `split`. Legacy-режим `--mode single -n <имя>` генерирует один монолитный файл и используется только в старых проектах, которые уже завязаны на монолитный generated-клиент. В новых проектах single-режим не используется.
Генератор не создаёт SWR-хуки. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = одна GET operation, без бизнес-логики и композиции.
## Генерация
```bash
npm run codegen:pet-store-rest-api
```
Ожидаемый результат:
```text
src/infra/pet-store-rest-api/generated/
├── create-api-client.ts
├── data-contracts.ts
├── http-client.ts
├── index.ts
├── operations-tree.ts
└── operations/
├── index.ts
├── get-pet-by-id.ts
└── find-pets-by-status.ts
```
Основные части:
- `http-client.ts` — fetch-based `HttpClient`: `baseUrl`, заголовки, авторизация и хуки транспорта.
- `data-contracts.ts` — TypeScript-типы из OpenAPI schemas, включая `*Params`-типы операций.
- `operations/*.ts` — отдельная typed operation-функция на каждый endpoint.
- `operations-tree.ts` — дерево всех операций для сборки полного клиента.
- `create-api-client.ts``createApiClient`, который привязывает дерево операций к `HttpClient`.
- `index.ts` — входная точка generated-кода, реэкспортирует всё перечисленное.
Файлы в `generated/` не правятся руками и коммитятся в репозиторий.
Enum-значения в split-режиме генерируются как union-типы строковых литералов. Отдельных runtime enum в `generated/` нет: в коде используются строковые литералы вроде `'available'`, а тип проверяет их допустимость.
## Проверка операций
После генерации откройте `generated/operations-tree.ts` и проверьте фактические имена operation-функций и структуру дерева.
Для Petstore нужны GET-операции вида:
```ts
import { findPetsByStatus } from './generated/operations/find-pets-by-status'
import { getPetById } from './generated/operations/get-pet-by-id'
```
Точечная operation вызывается через настроенный `HttpClient`:
```ts
getPetById(petStoreHttpClient, { petId: 10 })
findPetsByStatus(petStoreHttpClient, { status: 'available' })
```
После сборки полного клиента в `rest-api.ts` те же операции доступны как bound API:
```ts
petStoreRestApi.pet.findPetsByStatus({ status: 'available' })
petStoreRestApi.pet.getPetById({ petId: 10 })
```
Имена операций и группировка дерева зависят от `operationId` и тегов OpenAPI-схемы. В рабочих задачах всегда сверяйтесь с `generated/operations-tree.ts` и `generated/data-contracts.ts`.
## Источник Imports
Если generated-код лежит внутри infra-модуля, импортируйте operation из локального `generated/`:
```ts
import { getPetById } from '../generated/operations/get-pet-by-id'
```
Если generated-код вынесен в SDK-пакет, импортируйте operation через subpath export пакета:
```ts
import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
```
Не импортируйте весь `operations` namespace ради одной операции.
## Алгоритм для агента
После генерации агент должен действовать по шагам:
1. Открыть `generated/operations-tree.ts` и найти фактические имена нужных operation-функций.
2. Для каждой нужной операции найти тип параметров и тип ответа в `generated/data-contracts.ts`.
3. Создать или обновить `client.ts`: настроить и экспортировать только `*HttpClient`.
4. Создать или обновить `rest-api.ts`: собрать полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
5. Создать GET-хуки только для реально нужных GET-операций, не для всех операций API на всякий случай.
6. В каждом GET-хуке импортировать точечную operation из `operations/<operation-file>`.
7. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
8. В key-функции вернуть `null`, если обязательные параметры не готовы.
9. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
10. В fetcher вызвать operation через общий `*HttpClient`: `operation(nameHttpClient, params as GeneratedParams, requestParams?)`.
11. Экспортировать хук и key-функцию из `hooks/index.ts`; если это первый hook, добавить в `hooks/index.ts` директиву `'use client'`.
12. Экспортировать наружу только нужные generated-типы, DTO, `*HttpClient`, `*RestApi` и `hooks` через корневой `index.ts`.
Что агент не должен делать:
- Не править файлы в `generated/` руками.
- Не импортировать операции и `HttpClient` из `generated/` или SDK в UI/components.
- Не импортировать `operationsTree` или полный `*RestApi` в GET-хуки.
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
- Не создавать словари enum-маппинга внутри GET-хука.
- Не объявлять DTO и response-типы в файле хука.
- Не вызывать `useSWR` условно.
- Не добавлять `throw` в fetcher для неготовых params.
## `client.ts`
`client.ts` содержит только настроенный транспортный `HttpClient`.
```ts
// src/infra/pet-store-rest-api/client.ts
import { HttpClient } from './generated'
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
`client.ts` не содержит расширения типов, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логику.
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
## `rest-api.ts`
`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient, operationsTree } from './generated'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
Импорт `operationsTree` означает полный клиент: в bound API доступны все операции API.
Если generated-код вынесен в SDK-пакет, используйте subpath exports:
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
## GET-хуки
GET-хуки пишутся вручную после проверки generated-операций.
Пример для операции `getPetById`:
```ts
// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
import { getPetById } from '../generated/operations/get-pet-by-id'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { GetPetByIdParams, Pet } from '../generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
Типы импортируются как `import type` из `./generated`: generated `index.ts` реэкспортирует все типы из `data-contracts.ts`.
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
## Расширение сгенерированных типов
Файлы в `generated/` не правятся руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
```text
src/infra/biocad-less-rest-api/
├── generated/
│ ├── data-contracts.ts
│ ├── operations/
│ └── index.ts
├── types/
│ ├── term.ts
│ └── index.ts
├── client.ts
├── rest-api.ts
└── index.ts
```
Пример расширения generated-типа:
```ts
// src/infra/biocad-less-rest-api/types/term.ts
import type { TermRecordItem } from '../generated/data-contracts'
declare module '../generated/data-contracts' {
interface TermRecordItem {
media?: {
file?: string
title?: string
url?: string
}
}
}
export type TermRecordItemExtended = Omit<
TermRecordItem,
'categories' | 'tags' | 'fields'
> & {
categories?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
tags?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
fields?: Record<string, unknown>
}
```
```ts
// src/infra/biocad-less-rest-api/types/index.ts
export type { TermRecordItemExtended } from './term'
```
`declare module` нацеливается на `../generated/data-contracts` — именно там живут generated-интерфейсы. Он используется для добавления отсутствующих полей. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файлы.
## Публичный API
```ts
// src/infra/pet-store-rest-api/index.ts
export { petStoreHttpClient } from './client'
export { petStoreRestApi } from './rest-api'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated'
export * from './hooks'
```
Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/`.
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
```ts
// src/infra/biocad-less-rest-api/index.ts
export type { TermRecordItemExtended } from './types'
```
## Регенерация
При изменении OpenAPI-схемы:
```bash
npm run codegen:pet-store-rest-api
```
Что меняется:
- Папка `generated/` — перезаписывается генератором целиком.
- `client.ts`, `rest-api.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
Если после регенерации поменялись имена операций, сигнатуры или типы, это исправляется в ручном коде модуля: `rest-api.ts`, GET-хуки, minimal clients и реэкспорты.
## Следующий шаг
После генерации настройте транспорт в `client.ts` по разделу [Кастомизация HTTP-клиента](./http-client.md), соберите полный API в `rest-api.ts`, затем проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хук REST-клиента](../../SKILL.md#get-хуки-rest-клиента) для Client Components.

View File

@@ -0,0 +1,168 @@
---
title: Кастомизация HTTP-клиента
description: Настройка транспорта REST-клиента через опции и хуки HttpClient.
keywords: [rest, http, транспорт, авторизация, jwt, refresh token, onRequest, onError]
---
# Кастомизация HTTP-клиента
Настройка транспорта REST-клиента через опции и хуки `HttpClient`.
## Где живёт кастомизация
Вся настройка транспорта — `baseUrl`, заголовки, авторизация, retry — задаётся в `client.ts` REST-модуля при создании `HttpClient`.
Не размещайте авторизацию, обработку 401 и логирование в компонентах, GET-хуках или обёртках над операциями: у транспорта одна точка настройки.
В generated/SDK-сценарии `HttpClient` импортируется из generated-кода или SDK-пакета. В ручном сценарии без OpenAPI `HttpClient` импортируется из runtime-зависимости `@gromlab/api-codegen`.
```ts
import { HttpClient } from '@gromlab/api-codegen'
```
## Опции HttpClient
`HttpClient` принимает плоский конфиг: стандартные `fetch`-опции задаются вместе с хуками клиента.
| Опция | Назначение |
| --- | --- |
| `baseUrl` | Базовый URL API. |
| `headers` | Заголовки по умолчанию для всех запросов. |
| `credentials` | Политика отправки cookies: `omit`, `same-origin`, `include`. |
| `timeout` | Таймаут запроса в миллисекундах, работает через `AbortSignal`. |
| `customFetch` | Замена стандартного `fetch`: тесты, SSR, custom transport. |
| `paramsSerializer` | Кастомная сериализация query params в URL. |
| `responseParser` | Кастомный парсинг response body. |
| `onRequest` | Request-хук перед вызовом `fetch`. |
| `onResponse` | Response-хук после успешного HTTP-ответа. |
| `onError` | Error-хук для HTTP-ошибок, network errors и ошибок парсинга. |
Полный список опций — в README `@gromlab/api-codegen`.
## Контракт хуков
- `onRequest(params, context)` вызывается перед `fetch` и возвращает изменённые `params`.
- `onResponse(response, context)` вызывается после успешного ответа и возвращает `response`.
- `onError(error, context)` вызывается для HTTP-ошибок, network errors и ошибок парсинга.
- `context` содержит `url`, `request`, `retryCount` и `retry()` — повтор текущего запроса.
- `onError` должен либо бросить ошибку, либо вернуть fallback-значение, либо вернуть результат `context.retry()`. Если вернуть `undefined`, ошибка будет считаться обработанной, а вызывающий код получит `undefined` вместо исключения.
- Для защищённых endpoints generated operation передаёт `secure: true`, поэтому авторизацию можно добавлять только там, где она нужна.
## JWT-авторизация
Токен добавляется в `onRequest` только для защищённых запросов и не перезаписывает явно переданный `Authorization`.
```ts
// src/infra/pet-store-rest-api/client.ts
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
onRequest: (params) => {
const token = localStorage.getItem('access_token')
if (!params.secure || !token) {
return params
}
const headers = new Headers(params.headers)
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}`)
}
return {
...params,
headers,
}
},
})
```
## Refresh token
Обновление токена и повтор запроса выполняются в `onError` через `context.retry()`. `context.retryCount` защищает от бесконечного цикла повторов.
```ts
// src/infra/pet-store-rest-api/client.ts
import { ApiError, HttpClient } from './generated'
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
onError: async (error, context) => {
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
await refreshToken()
return context.retry()
}
throw error
},
})
```
`ApiError` экспортируется из `generated/` и содержит `status`, `statusText`, `response`, `data` и исходный `request`.
## Логирование
```ts
const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
onResponse: (response, context) => {
console.log(context.request.method, context.url, response.status)
return response
},
})
```
## Сериализация query params
```ts
const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
paramsSerializer: (query) => {
const params = new URLSearchParams()
Object.entries(query).forEach(([key, value]) => {
if (Array.isArray(value)) {
params.set(key, value.join(','))
return
}
if (value !== undefined) {
params.set(key, String(value))
}
})
return params.toString()
},
})
```
## Параметры одного вызова
Разовые настройки запроса не относятся к `HttpClient`. Они передаются последним аргументом operation:
```ts
import { getPetById } from './generated/operations/get-pet-by-id'
import { petStoreHttpClient } from './client'
await getPetById(
petStoreHttpClient,
{ petId },
{
headers: {
'X-Request-Id': requestId,
},
},
)
```
## Правила
- Кастомизация транспорта живёт только в `client.ts` REST-модуля.
- Авторизация добавляется в `onRequest` с учётом `params.secure` и без перезаписи явного `Authorization`.
- `onError` либо бросает ошибку, либо возвращает fallback или `context.retry()`; молчаливый `return` запрещён.
- Повторы запроса ограничиваются проверкой `context.retryCount`.
- Бизнес-реакции на ошибки — тосты, редиректы, UI-состояние — не размещаются в хуках `HttpClient`.
## Следующий шаг
После настройки транспорта проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).

View File

@@ -0,0 +1,305 @@
---
title: Ручное создание REST-клиента
description: Создание generated-style REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, api-codegen, operation, ApiRequestClient, RequestParams, ContentType]
---
# Ручное создание REST-клиента
Ручной клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
Ручной режим не является отдельной архитектурой. Это тот же generated-style клиент, где operation-функции написаны вручную вместо генерации из OpenAPI.
## Зависимость
Для ручного клиента установите `@gromlab/api-codegen@5.1.0+` как runtime-зависимость проекта.
```bash
bun add @gromlab/api-codegen
```
Не используйте `npx @gromlab/api-codegen` для ручного режима: `npx` нужен для генерации SDK из OpenAPI, а ручной клиент импортирует runtime API пакета напрямую.
## Что нужно создать
```text
src/infra/
└── pet-project-rest-api/
├── client.ts
├── rest-api.ts
├── operations-tree.ts
├── operations/
│ ├── posts-create.ts
│ ├── posts-detail.ts
│ ├── posts-list.ts
│ └── index.ts
├── hooks/
│ ├── lib/
│ │ └── create-query-string.ts
│ ├── use-get-post-list.hook.ts
│ └── index.ts
├── types/
│ ├── post.ts
│ └── index.ts
└── index.ts
```
| Файл | Роль |
|------|------|
| `client.ts` | Настройка и экспорт `*HttpClient` |
| `operations/` | Ручные operation-функции в стиле generated SDK |
| `operations-tree.ts` | Ручное дерево операций для `createApiClient` |
| `rest-api.ts` | Полный bound API через `createApiClient` |
| `types/` | DTO запросов, ответов и именованные response-типы |
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
| `index.ts` | Публичный API REST-модуля |
## Типы API
DTO запросов и ответов живут в `types/`. `client.ts` и operation-файлы не объявляют доменные типы.
```ts
// src/infra/pet-project-rest-api/types/post.ts
export type PostDto = {
id: string
slug: string
title: string
}
export type PostListQueryDto = {
limit?: number
category?: string
}
export type CreatePostPayload = {
title: string
}
```
```ts
// src/infra/pet-project-rest-api/types/index.ts
export type { CreatePostPayload, PostDto, PostListQueryDto } from './post'
```
Если данным нужен доменный смысл или маппинг DTO, делайте это выше, в `business/`, а не в REST-клиенте.
## Operation-Функции
Каждая ручная operation повторяет контракт generated operation:
- первым аргументом принимает `http: ApiRequestClient`;
- принимает typed params/body как последующие аргументы;
- последним аргументом принимает `requestParams: RequestParams = {}`;
- внутри вызывает только `http.request(...)`;
- не использует прямой `fetch`;
- не знает про React, SWR, UI и бизнес-логику.
```ts
// src/infra/pet-project-rest-api/operations/posts-list.ts
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { PostDto, PostListQueryDto } from '../types'
export const postsList = (
http: ApiRequestClient,
query: PostListQueryDto = {},
requestParams: RequestParams = {},
) =>
http.request<PostDto[]>({
path: '/posts',
method: 'GET',
query,
format: 'json',
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/posts-detail.ts
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { PostDto } from '../types'
export type PostsDetailParams = {
slug: string
}
export const postsDetail = (
http: ApiRequestClient,
{ slug }: PostsDetailParams,
requestParams: RequestParams = {},
) =>
http.request<PostDto>({
path: `/posts/${slug}`,
method: 'GET',
format: 'json',
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/posts-create.ts
import { ContentType } from '@gromlab/api-codegen'
import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
import type { CreatePostPayload, PostDto } from '../types'
export const postsCreate = (
http: ApiRequestClient,
body: CreatePostPayload,
requestParams: RequestParams = {},
) =>
http.request<PostDto>({
path: '/posts',
method: 'POST',
body,
type: ContentType.Json,
format: 'json',
secure: true,
...requestParams,
})
```
```ts
// src/infra/pet-project-rest-api/operations/index.ts
export { postsCreate } from './posts-create'
export { postsDetail } from './posts-detail'
export { postsList } from './posts-list'
export type { PostsDetailParams } from './posts-detail'
```
## Транспорт
`client.ts` настраивает и экспортирует только `*HttpClient`.
```ts
// src/infra/pet-project-rest-api/client.ts
import { HttpClient } from '@gromlab/api-codegen'
export const petProjectHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
## Operations Tree
`operations-tree.ts` вручную собирает дерево операций. Держите структуру такой, какой вы ожидаете видеть после будущей автогенерации.
```ts
// src/infra/pet-project-rest-api/operations-tree.ts
import { postsCreate } from './operations/posts-create'
import { postsDetail } from './operations/posts-detail'
import { postsList } from './operations/posts-list'
export const operationsTree = {
posts: {
create: postsCreate,
detail: postsDetail,
list: postsList,
},
} as const
```
## Полный API-Клиент
`rest-api.ts` собирает полный bound API через тот же `createApiClient`, что используется в generated SDK.
```ts
// src/infra/pet-project-rest-api/rest-api.ts
import { createApiClient } from '@gromlab/api-codegen'
import { petProjectHttpClient } from './client'
import { operationsTree } from './operations-tree'
export const petProjectRestApi = createApiClient(
petProjectHttpClient,
operationsTree,
)
```
После binding внешний вызов совпадает с generated-клиентом:
```ts
await petProjectRestApi.posts.create({ title: 'Новый пост' })
await petProjectRestApi.posts.detail({ slug: 'hello' })
```
## GET-Хуки
GET-хуки ручного клиента пишутся так же, как hooks для generated operations: импортируют точечную operation и вызывают её через общий `*HttpClient`.
```ts
// src/infra/pet-project-rest-api/hooks/use-get-post-list.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petProjectHttpClient } from '../client'
import { postsList } from '../operations/posts-list'
import type { PostDto, PostListQueryDto } from '../types'
export const getPostListKey = (params: PostListQueryDto = {}) => {
const searchParams = new URLSearchParams()
if (params.limit !== undefined) {
searchParams.set('limit', String(params.limit))
}
if (params.category) {
searchParams.set('category', params.category)
}
const search = searchParams.toString()
return ['pet-project-rest-api', `/posts${search ? `?${search}` : ''}`] as const
}
export const useGetPostList = (
params: PostListQueryDto = {},
config?: SWRConfiguration<PostDto[]>,
) => {
const key = getPostListKey(params)
const fetcher = () => postsList(petProjectHttpClient, params)
return useSWR<PostDto[]>(key, fetcher, config)
}
```
```ts
// src/infra/pet-project-rest-api/hooks/index.ts
'use client'
export { getPostListKey, useGetPostList } from './use-get-post-list.hook'
```
## Публичный API
```ts
// src/infra/pet-project-rest-api/index.ts
export { petProjectHttpClient } from './client'
export { petProjectRestApi } from './rest-api'
export * from './hooks'
export type { PostsDetailParams } from './operations'
export type { CreatePostPayload, PostDto, PostListQueryDto } from './types'
```
Внешний код импортирует только из `infra/pet-project-rest-api`, не из внутренних файлов модуля.
## Миграция На OpenAPI
Ручной клиент проектируйте так, чтобы его можно было заменить автогенерацией без смены API потребителей.
- Называйте operation-функции и дерево близко к будущим `operationId` и tags OpenAPI.
- Держите сигнатуры operation в стиле generated SDK: `operation(http, params/body, requestParams?)`.
- Не создавайте собственный класс клиента и методные фабрики.
- Если позже появится OpenAPI, сгенерируйте SDK и замените ручные operations на generated operations с минимальными правками `rest-api.ts`, GET-хуков и re-exports.
## Правила
- Ручной клиент использует `@gromlab/api-codegen@5.1.0+` как runtime dependency.
- `client.ts` экспортирует только `*HttpClient`.
- Ручные запросы живут в `operations/` и пишутся как generated-style operations.
- `operations-tree.ts` вручную собирает дерево для `createApiClient`.
- `rest-api.ts` экспортирует полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
- GET-хуки вызывают точечные operations через `*HttpClient`, не полный `*RestApi`.
- Не используйте прямой `fetch`, custom `RestApiClient` class, `methods/*Methods(client)` и ручные `get/post` wrappers.
- DTO запросов и ответов живут в `types/`.
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
Следующий шаг: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента), [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента) или выбор route-level data fetching по `nextjs-style-guide`.

View File

@@ -0,0 +1,166 @@
---
title: SDK-пакет REST-клиента
description: Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
keywords: [rest, sdk, npm, монорепозиторий, api-codegen, generated, пакет]
---
# SDK-пакет REST-клиента
Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
## Когда выносить
SDK-пакет нужен, когда один и тот же API используется несколькими приложениями: в монорепозитории или через публикацию в npm registry.
Если API нужен одному приложению, SDK-пакет не создаётся — генерация идёт классически внутрь infra-модуля в `{name}-rest-api/generated` по разделу [Автогенерация из OpenAPI](./auto.md).
## Нейминг
SDK-пакет называется `{name}-rest-api-sdk`.
```text
pet-store-rest-api-sdk
@company/pet-store-rest-api-sdk
```
Имя SDK-пакета образуется от имени infra-модуля: приложение с модулем `infra/pet-store-rest-api` потребляет пакет `pet-store-rest-api-sdk`.
## Что содержит SDK
SDK-пакет содержит только generated-код и `package.json` exports для точечных импортов. Это транспорт-нейтральная библиотека: она не знает про приложение, авторизацию и SWR конкретного проекта.
В SDK не размещаются:
- `client.ts` и `rest-api.ts` — настройка транспорта и bound API живут в приложении;
- GET-хуки — SWR-обёртки живут в infra-модуле приложения;
- бизнес-логика и DTO-маппинг.
## Структура пакета
```text
packages/pet-store-rest-api-sdk/
├── package.json
└── src/
└── generated/
```
Скрипт генерации внутри пакета выводит split-клиент в `src/generated`:
```json
{
"scripts": {
"codegen": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/generated"
}
}
```
`package.json` обязан открыть subpath exports для generated частей:
```json
{
"name": "@company/pet-store-rest-api-sdk",
"type": "module",
"exports": {
".": "./src/generated/index.ts",
"./create-api-client": "./src/generated/create-api-client.ts",
"./data-contracts": "./src/generated/data-contracts.ts",
"./http-client": "./src/generated/http-client.ts",
"./operations": "./src/generated/operations/index.ts",
"./operations/*": "./src/generated/operations/*.ts",
"./operations-tree": "./src/generated/operations-tree.ts"
}
}
```
Файлы в `src/generated/` не правятся руками и коммитятся в репозиторий пакета.
Корневой `package.json` монорепозитория добавляет удобный script для запуска codegen через workspace filter:
```json
{
"scripts": {
"codegen:pet-store-rest-api-sdk": "dotenv -- pnpm --filter @company/pet-store-rest-api-sdk run codegen"
}
}
```
## Потребление в приложении
Приложение оформляет REST-модуль как обычно: `src/infra/{name}-rest-api/` с `client.ts`, `rest-api.ts`, `hooks/`, `types/` и корневым `index.ts`. Меняется только источник generated-кода: вместо локальной папки `generated/` импортируется SDK-пакет.
```ts
// src/infra/pet-store-rest-api/client.ts
import { HttpClient } from '@company/pet-store-rest-api-sdk'
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
Типы в хуках и `types/` импортируются из пакета вместо `../generated`:
```ts
import type { GetPetByIdParams, Pet } from '@company/pet-store-rest-api-sdk'
```
GET-хуки импортируют точечные operations из SDK subpath:
```ts
import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
```
Остальной контракт модуля не меняется:
- настройка транспорта — [Кастомизация HTTP-клиента](./http-client.md);
- GET-хуки — [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента);
- внешний код импортирует только из `infra/pet-store-rest-api`, не из SDK-пакета напрямую.
## Minimal Client В Composition
SDK operations можно импортировать напрямую в boundary-файлах feature/composition, если там собирается минимальный клиент для конкретного бизнес-сценария.
```ts
// src/compositions/business/pet-store/orders/pet-store-rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { createOrder } from '@company/pet-store-rest-api-sdk/operations/create-order'
import { getOrder } from '@company/pet-store-rest-api-sdk/operations/get-order'
import { petStoreHttpClient } from 'infra/pet-store-rest-api'
export const petStoreOrdersRestApi = createApiClient(petStoreHttpClient, {
orders: {
create: createOrder,
get: getOrder,
},
})
```
Это исключение действует только для boundary-файлов сборки клиента. UI/components, pages и произвольные helpers не импортируют SDK напрямую.
## Регенерация
При изменении OpenAPI-схемы перегенерируется `src/generated` внутри SDK-пакета:
```bash
npm run codegen
```
Приложения получают обновление через новую версию пакета. Если поменялись имена операций или типы, правки в приложении локализованы в `rest-api.ts`, GET-хуках, minimal clients и реэкспортах.
## Правила
- SDK-пакет называется `{name}-rest-api-sdk` и содержит только generated-код.
- SDK-пакет обязан открыть subpath exports: `.`, `./operations/*`, `./create-api-client`, `./operations-tree`, `./http-client`, `./data-contracts`.
- Генерация внутри пакета идёт в `src/generated`, файлы не правятся руками.
- `client.ts`, `rest-api.ts`, авторизация и GET-хуки живут в infra-модуле приложения, не в SDK.
- Приложение импортирует SDK внутри своего `infra/{name}-rest-api` модуля.
- Boundary-файл feature/composition может импортировать SDK operations напрямую только для сборки minimal client.
- UI/components не импортируют SDK напрямую.

View File

@@ -0,0 +1,135 @@
---
title: Настройка REST-клиента
description: Подготовка REST-клиента сервиса к использованию.
keywords: [rest, клиент, infra, operation, openapi, get-хуки, swr]
---
# Настройка REST-клиента
Подготовка REST-клиента сервиса к использованию.
## Что настраиваем
REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
На этапе настройки нужно подготовить транспортный HTTP-клиент, полный API-клиент и GET-хуки для клиентских компонентов.
## Нейминг
- infra-модуль REST-клиента называется `{name}-rest-api`: `src/infra/pet-store-rest-api/`.
- Генерация внутри клиента — классический вариант — живёт в `{name}-rest-api/generated`.
- Если generated-клиент выносится в npm-пакет или пакет монорепозитория, пакет называется `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
- Производные имена образуются от имени модуля: транспорт `petStoreHttpClient`, полный API-клиент `petStoreRestApi`, SWR-ключ `['pet-store-rest-api', ...]`.
## Из чего состоит клиент
REST-клиент состоит из четырёх основных частей:
1. **Транспорт** — ручной `client.ts` с настроенным `*HttpClient`.
2. **Полный API-клиент**`rest-api.ts` с `createApiClient(*HttpClient, operationsTree)`.
3. **Операции** — operation-функции, сгенерированные из OpenAPI, поставляемые SDK-пакетом или написанные вручную через `@gromlab/api-codegen`.
4. **GET-хуки** — SWR-обёртки для GET-запросов.
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
## Транспорт
`client.ts` — ручной слой, который настраивает транспорт: `HttpClient`, заголовки, авторизацию и обработку ошибок.
Авторизация, refresh token и другие транспортные сценарии настраиваются хуками `HttpClient``onRequest`, `onResponse`, `onError`: [Кастомизация HTTP-клиента](./http-client.md).
Даже если операции генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
`client.ts` экспортирует только настроенный `*HttpClient`. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логика.
```ts
// src/infra/pet-store-rest-api/client.ts
import { HttpClient } from '@company/pet-store-rest-api-sdk'
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
## Полный API-клиент
`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
Импортируйте `operationsTree` только в `rest-api.ts`. Для GET-хуков и minimal clients используйте точечные operation imports.
## Операции
Операции описывают конкретные запросы к API.
Они появляются одним из трёх способов:
- генерируются из OpenAPI в `generated/` как отдельные operation-функции;
- поставляются SDK-пакетом `{name}-rest-api-sdk`;
- создаются вручную в `operations/` через `@gromlab/api-codegen`, если OpenAPI нет или он неполный.
Подробности:
- [Автогенерация из OpenAPI](./auto.md)
- [Ручное создание](./manual.md)
## GET-хуки
Для GET-запросов добавляются GET-хуки REST-клиента.
Это прозрачные SWR-обёртки над generated GET operations. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-rest-api', '/pet/10']`.
Хук принимает generated-параметры операции и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
`hooks/index.ts` содержит `'use client'` и экспортирует все GET-хуки. Сами `use-get-*.hook.ts` остаются обычными файлами без директивы.
Подробности:
- [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента)
## Структура модуля
```text
src/infra/{name}-rest-api/
├── client.ts # настройка и экспорт *HttpClient
├── rest-api.ts # полный *RestApi через operationsTree
├── generated/ или operations/ # локальный split-клиент или ручные operations
├── operations-tree.ts # ручное дерево операций, если нет generated/operations-tree.ts
├── hooks/ # GET-хуки REST-клиента
│ ├── lib/
│ │ └── create-query-string.ts
│ ├── use-get-*.hook.ts
│ └── index.ts # 'use client' и публичные экспорты hooks
├── types/ # DTO, именованные response-типы и расширения типов
├── errors/ # ошибки API, если нужны
└── index.ts # публичный API
```
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
Если generated-код вынесен в `{name}-rest-api-sdk`, локальной папки `generated/` внутри infra-модуля может не быть: `client.ts`, `rest-api.ts` и GET-хуки импортируют generated части из SDK subpath exports.
Если OpenAPI нет, не создавайте самописный `fetch`-класс и `methods/`. Ручной клиент пишется тем же API, что generated-клиент: `HttpClient`, `ApiRequestClient`, `RequestParams`, operation-функции и `createApiClient` из `@gromlab/api-codegen`.
Если generated operation возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
## Что делаем дальше
1. Создайте операции клиента: [Автогенерация из OpenAPI](./auto.md), SDK-пакет или [Ручное создание](./manual.md).
2. Если клиент нужен нескольким приложениям, вынесите generated-код в пакет: [SDK-пакет REST-клиента](./sdk.md).
3. Настройте транспорт — авторизацию, хуки, таймауты: [Кастомизация HTTP-клиента](./http-client.md).
4. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
5. Проверьте прямые вызовы клиента: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента).
6. После настройки клиента выбирайте стратегию route-level data fetching по `nextjs-style-guide`.

View File

@@ -0,0 +1,612 @@
---
name: slm-design
description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, business factory, adapter, assembly, framework binding, state/cache/error/lifecycle ownership или исправить deep import, цикл и environment leak. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется."
---
# SLM Design
## Рабочий контракт
Применяй SLM как способ выполнить пользовательскую задачу, а не как тему для пересказа. После чтения этого файла ты должен уметь принять типовое архитектурное решение, реализовать его в запрошенном scope и проверить результат. Открывай references только для точной формулировки правила, редкого случая или неразрешённого вопроса.
Работай в таком порядке:
1. Исследуй существующий код и локальные правила проекта.
2. Определи ответственность, владельца и минимальный scope.
3. Выбери слой, архитектурную сущность и форму домена.
4. Спроектируй публичную границу, зависимости, runtime-сборку и lifecycle.
5. До редактирования проверь решение по применимым правилам.
6. Если пользователь запросил реализацию, внеси изменения до завершённого состояния.
7. Проверь импорты, exports, граф, среды, lifecycle и тесты.
8. Кратко сообщи решение, сделанные изменения, проверки, assumptions и остаточные риски.
Не начинай широкое перемещение кода или генерацию каркаса до шагов 1-5. Не расширяй задачу до полного аудита SLM root, если локальное изменение можно корректно выполнить в меньшем scope.
## Источники и обязательность
Bundled DRAFT является рабочим источником истины для этой версии skill, но остаётся черновиком архитектуры. Используй источники в следующем порядке:
1. [`rules/level-1.md`](./reference/draft/rules/level-1.md) и [`rules/level-2.md`](./reference/draft/rules/level-2.md) - единственный источник блокирующих правил.
2. [`level-1/terminology.md`](./reference/draft/level-1/terminology.md) и [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) - обязательный смысл терминов.
3. README уровней - область применения, наследование и замены правил.
4. Тематические главы - объяснения, рекомендации и варианты проектирования.
5. Примеры - иллюстрации, а не обязательный каркас.
6. `open-questions.md` - нерешённые вопросы, а не требования.
Если тематическая глава строже реестра, не создавай из неё новое блокирующее правило. Предложи более строгую форму как рекомендацию или уточни локальную policy, если выбор влияет на API, ownership, стоимость или runtime. Если этот файл расходится с реестром или нормативной терминологией, следуй bundled DRAFT и отметь дефект skill.
При review различай:
- **Rule violation** - нарушено применимое правило с существующим кодом SLM.
- **Definition mismatch** - реализация не соответствует нормативному смыслу сущности.
- **Architectural risk** - есть доказуемый риск, но нет блокирующего правила.
- **Decision required** - DRAFT или проект оставляет значимый выбор открытым.
- **Recommendation** - улучшение, которое не является обязательным.
- **Assumption** - обратимое рабочее допущение, явно указанное в результате.
Не придумывай коды правил. Перед ссылкой на нарушение открой соответствующий реестр и проверь точную формулировку.
## Минимальная рабочая модель
### SLM root и уровни
SLM root - граница структурной архитектуры одного приложения. Сначала найди фактический root, path aliases, локальный стайлгайд и конфигурацию архитектурной проверки. Не считай `src` root автоматически и не выводи сущность только из имени папки.
Level 1 действует во всём SLM root и задаёт слои, модули, публичные API, общий dependency DAG и владение lifecycle.
Level 2 применяется отдельно к выбранной предметной области и заменяет только её доменный модуль пакетной формой. Остальные домены могут постоянно оставаться на Level 1. Одна предметная область имеет ровно одну итоговую форму.
### Слои
| Исходный слой | Может зависеть от |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
Матрица не требует проходить через каждый промежуточный слой. Разрешённый импорт не переносит владение ответственностью.
| Слой | Помещай сюда |
|---|---|
| `app` | Framework entry points: запуск, routes, преобразование внешнего input и подключение готовых API |
| `compositions` | Pages, layouts, screens, widgets, route outcomes и multi-domain UI |
| `domains` | Предметные модели, правила, сценарии и продуктовое состояние |
| `infra` | Универсальные технические capabilities без собственной предметной модели |
| `ui` | Универсальные UI-модули без зависимости от продуктовой композиции |
| `shared` | Детерминированный product-agnostic фундамент без I/O, mutable state и lifecycle |
### Архитектурные сущности
| Признак | Сущность |
|---|---|
| Самостоятельная ответственность со своим API, dependencies, state или lifecycle | Module |
| Только навигационно классифицирует modules и Groups | Group |
| Организует внутренности одного module | Segment |
| Framework UI entity, реализующая часть ответственности родителя | Component |
| Самостоятельный module, скрытый внутри parent module | Nested module |
| Framework bootstrap или route entry | Немодульная единица `app` |
| Малый deterministic product-agnostic файл без внутренней границы | Shared resource |
Module является узлом dependency graph, размещается в отдельной папке и имеет единый логический публичный API. Group, segment и component не владеют API, состоянием или lifecycle. Наличие локального `index.ts`, нескольких файлов, hook, data access или lifecycle-кода само по себе не превращает component или segment в module: всё это принадлежит ближайшему module-owner.
Nested module имеет собственную ответственность, API и узел графа, но внешний код получает его exports только через публичный API parent module.
### Пакетная форма Level 2
Минимальная структура доменного пакета:
```text
domains/<domain>/
├── metadata # optional, declarative only
├── business/ # required SLM module
├── assemblies/ # required non-empty Group
├── adapters/ # when factories have technical dependencies
└── react|vue|... # when domain-specific bindings exist
```
Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета.
`business` является единственным предметным владельцем пакета. Его публичный API состоит из фасетов:
| Путь | Содержимое |
|---|---|
| `business` | Только public types: Domain API, dependencies, factory types, error types |
| `business/factory` | Только именованные runtime factories, по одной на Domain API |
| `business/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions |
Другой публичный путь внутрь `business` является deep import. `business/runtime` не создавай для симметрии.
Роли Level 2:
- `business` определяет Domain API, модели, validation, transitions, scenario results, dependency contracts и expected domain errors.
- Adapter module реализует связанные technical dependencies поверх SDK, storage, platform API, state/query runtime или другого technical runtime.
- Assembly module выбирает adapters, вызывает factories и возвращает именованный graph готовых API для одного execution context.
- Framework binding module получает готовые Domain API и владеет одной domain-specific интеграцией с framework.
- Composition, `app`, request handler или test setup собирает междоменный graph в ацикличном порядке и владеет его общим scope.
## Универсальный цикл решения
### 1. Discover
Перед решением найди только релевантный контекст:
- локальные инструкции и стайлгайд;
- SLM root и mapping путей на слои и модули;
- существующие public entry points и package exports;
- внешних consumers затрагиваемой границы;
- runtime- и type-only imports, реэкспорты и aliases;
- state, I/O, SDK, framework runtime и источники недетерминизма;
- места создания graph и instances;
- subscriptions, timers, requests, connections и cleanup;
- тесты и команды проверки затрагиваемых owners.
Считай type-only import и reexport архитектурным ребром. Для runtime-графа дополнительно ищи arguments factories, callbacks, registries, event buses, service locators и singletons: фактическая зависимость может не иметь прямого runtime import.
### 2. Classify
Сформулируй краткую внутреннюю карточку:
```text
Task outcome:
Responsibility:
Owner:
Layer:
Entity:
Domain form:
Public consumers:
Runtime dependencies:
Environment:
State and lifecycle:
Change scope:
```
Не обязан показывать карточку пользователю, если решение однозначно. Если одно из ключевых полей неизвестно и влияет на границу, сначала исследуй код, затем задай один конкретный вопрос.
### 3. Design boundary
Определи:
- один owner каждой самостоятельной ответственности;
- минимальный публичный контракт для реальных consumers;
- разрешённые static edges;
- runtime injection и место сборки graph;
- владельцев domain state, technical cache и framework projection;
- безопасную форму expected errors;
- environment entry points и их transitive reachability;
- scope, multiplicity и cleanup каждого lifecycle resource;
- тестовую границу каждого изменяемого owner.
### 4. Validate before edits
До изменения файлов ответь:
- Соответствует ли ответственность роли слоя?
- Является ли выбранная сущность настоящим owner, а не удобной папкой?
- Есть ли у domain одна форма?
- Импортируется ли каждый чужой module через public API?
- Разрешены ли layer и cross-domain edges?
- Остаётся ли graph ацикличным?
- Совместим ли transitive graph с environment entry point?
- Есть ли owner, scope, multiplicity и cleanup у ресурсов?
- Не требует ли решение незапрошенной миграции соседних owners?
### 5. Act and verify
Если пользователь просит код, не останавливайся на рекомендации. Реализуй согласованную границу, обнови consumers и tests, удали obsolete paths и проверь завершённое состояние. Если пользователь просит только анализ, план или review, не редактируй код.
## Алгоритмы выбора
### Ответственность и владелец
1. Опиши ответственность одним предложением без имени папки, файла или библиотеки.
2. Назови одну причину её изменения.
3. Найди данные, behavior и state, которые изменяются вместе с ней.
4. Найди внешних consumers.
5. Проверь, нужны ли ей собственные API, dependencies, state или lifecycle.
6. Если самостоятельность доказана, назначь ровно один module-owner.
7. Если ответственность нельзя сформулировать или у неё конкурирующие owners, остановись до структурных изменений.
Место выполнения не переносит владение. Provider, hook, controller, route и component могут запускать чужую ответственность, не становясь её owner.
### Выбор слоя
```text
Только framework bootstrap, route entry или external input adaptation?
-> app
Page/layout/screen/widget, route outcome или multi-domain UI?
-> compositions
Domain model, scenario, validation, transition или product state?
-> domains
Technical capability без собственной domain model?
-> infra
Product-independent reusable UI?
-> ui
Deterministic, product-agnostic, без I/O/state/lifecycle?
-> shared
Иначе -> уточни ответственность, не выбирай папку по аналогии.
```
Domain-specific framework integration над готовым API может принадлежать Framework Group пакета Level 2. Зависимость от React/Vue сама по себе не переносит domain behavior в `compositions` или `app`.
### Выбор сущности
```text
Есть самостоятельный owner/API/dependencies/state/lifecycle?
Да -> module.
Нет -> часть текущего owner.
Module нужен только внутри одного parent module?
Да -> nested module.
Папка только классифицирует modules/Groups?
Да -> Group.
Папка только организует содержимое одного module?
Да -> segment.
Framework UI entity не имеет самостоятельной ответственности?
Да -> component parent module.
```
Не создавай module только из-за размера, повторного использования внутреннего helper или желания получить отдельную папку. Не оставляй самостоятельную ответственность component-ом или segment-ом только ради меньшего diff.
### Выбор формы домена
По умолчанию используй доменный модуль Level 1. Level 1 не требует factory, ports, adapters, assemblies или разделения по техническим ролям.
Рассматривай Level 2, когда конкретному домену действительно нужны:
- несколько независимо собираемых Domain API;
- разные browser/server/request assemblies;
- несколько production technical integrations;
- строгие environment boundaries;
- самостоятельные domain-specific framework modules.
Не выбирай Level 2 из-за количества файлов, одного SDK, одного hook, желания унифицировать дерево или гипотетической будущей интеграции. Зафиксируй, какую реальную потребность окупает дополнительная стоимость package, facets, assembly и adapters.
### Публичная граница
1. Перечисли реальных внешних consumers.
2. Для каждого запиши минимально необходимый contract.
3. Удали exports, которым нет consumer.
4. Не экспортируй mutable internals, concrete clients, stores, contexts, adapters или lifecycle implementation.
5. Для обычного module оставь одну логическую external entry point.
6. Для `business` используй только объявленные facets.
7. Удали deep imports и обнови package exports/aliases при необходимости.
8. Не открывай nested module напрямую за пределы parent boundary.
Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API.
### Проверка зависимости
Для каждого нового или изменённого edge:
1. Определи source owner и target owner.
2. Определи их слои и формы доменов.
3. Если owners различаются, импортируй target только через public API.
4. Проверь матрицу слоёв.
5. Если edge пересекает Level 2 package boundary, примени более строгую cross-domain модель.
6. Проверь transitive environment compatibility.
7. Добавь edge в общий module DAG и проверь цикл.
При пересечении границы Level 2 статически допустимы:
```ts
import type { OtherDomainApi } from '.../other/business'
import { deterministicValue } from '.../other/business/runtime'
```
Готовый API другого домена создаёт внешний graph owner и передаёт assembly или factory аргументом. Не импортируй из другого домена его factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `business`.
Не скрывай cross-domain dependency локальным structural interface, callback, global registry или event bus. Установи владельца контракта и отрази runtime edge в graph, иначе можно пропустить цикл.
### Runtime capabilities
| Capability | Размещение в Level 2 |
|---|---|
| SDK, HTTP/GraphQL source, storage, platform API | Adapter |
| Concrete state/query runtime для business dependency | Adapter |
| Clock, timer, random, ID, environment | Явная factory dependency с production implementation в adapter |
| Готовый API другого домена | Cross-domain dependency, передаваемая graph owner |
| Provider, hook или query projection готового Domain API | Framework binding module |
| Page-local или multi-domain UI state | Владеющий composition module |
| Универсальный technical service | `infra` module |
Adapter переводит technical arguments/results и реализует dependency contract. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation technical dependency не прячь inline в assembly или composition.
Assembly выбирает public adapters своего домена, вызывает factories и возвращает точный именованный graph API. Она не добавляет scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter.
### State и cache
```text
Domain facts, validation, transitions, commands, scenario outcomes
-> business authority
Transport/source cache
-> adapter
Framework/query projection готового Domain API
-> framework binding
State только текущей UI composition
-> composition owner
```
Raw DTO, query-library result и mutable client не являются Domain API. Technical и framework cache могут хранить и проецировать только значения, произведённые или проверенные `business`, и не создают параллельную предметную модель.
При optimistic или concurrent mutations не придумывай универсальный rollback. Сначала установи owner политики ordering, versioning, rebase/rollback и authoritative refresh.
### Errors
- Expected technical или foreign-domain failure, доступный через текущий Domain API, преобразуется текущим `business` в собственный readonly domain error со stable code.
- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract.
- Type errors экспортируются через `business`; необходимые runtime codes и guards - только через реально нужный `business/runtime`.
- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение.
- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization.
### Lifecycle и environment
Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй:
```text
Owner:
Created or started by:
Scope:
Multiplicity:
Environment:
Owned or borrowed:
Cleanup:
```
Factory или assembly не должна запускать неучтённую долгоживущую работу. Явная операция, запускающая ресурс, предоставляет cleanup. Если assembly обязана создать resource для graph, её публичный result предоставляет cleanup handle; graph owner вызывает его не позже конца scope. Assembly без собственного ресурса не возвращает пустой `dispose` для симметрии.
Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, server actions, workers, edge runtime и conditional exports сначала установи реальные executable edges, framework reference edges и runtime capabilities; не объявляй environment safety только по метке `client`/`server`.
## Рабочие процедуры
### Проектирование
1. Ограничь scope пользовательской задачей.
2. Найди SLM root, project mapping и существующие owners.
3. Построй карту consumers и текущих public paths.
4. Определи ответственность, layer, entity и domain form.
5. Спроектируй target boundaries и минимальные public contracts.
6. Классифицируй technical и cross-domain dependencies.
7. Определи graph owner, environments, state, errors и lifecycle.
8. Проверь правила и stop conditions.
9. Выдай решение, target structure, dependencies и порядок реализации.
Не предлагай файловое дерево до определения owners и boundaries. Имена файлов и segments следуют локальному стайлгайду, а не задаются SLM.
### Реализация
1. Зафиксируй принятое решение и change scope.
2. Изменяй код в dependency order: contracts и behavior раньше adapters и assembly, providers/consumers после готовых API.
3. Для Level 1 не создавай отсутствующие роли Level 2.
4. Для Level 2 сначала реализуй types, errors, behavior и factories `business`.
5. Затем реализуй production adapters, assemblies и framework bindings, которые реально нужны задаче.
6. Собери междоменный graph в composition, `app`, request handler или test setup.
7. Переведи всех затронутых consumers на public paths.
8. Удали obsolete exports, deep imports и старые boundaries в согласованном scope.
9. Добавь tests рядом с owners.
10. Запусти доступные structural, type, unit, integration и architecture checks.
Не оставляй заведомо промежуточную смешанную границу как завершённый результат. Backward compatibility добавляй только для реального внешнего consumer, persisted contract или явно согласованной phased migration.
### Миграция Level 1 -> Level 2
1. Выбери ровно один domain module и докажи потребность Level 2.
2. Найди все consumers, exports, state, I/O, framework integration и lifecycle resources.
3. Вычисли dependency-connected migration radius до редактирования.
4. Спроектируй Domain API по scenarios и consumers, а не по текущим technical segments.
5. Перенеси модели, validation, transitions, outcomes и errors под authority `business`.
6. Объяви явные factory dependencies и по одной factory на API.
7. Оформи production technical implementations как adapter modules.
8. Создай минимум одну assembly для реального execution context.
9. Перенеси domain-specific framework responsibilities в Framework Group.
10. Оставь pages, routes и multi-domain UI в `compositions`.
11. Переключи external consumers и graph roots.
12. Удали прежний root API и старую форму домена.
13. Проверь, что итог содержит одну форму и не требует миграции соседних доменов.
Временное физическое сосуществование старой и новой структуры допустимо только внутри незавершённого изменения. Не объявляй его conforming state. Если атомарный cutover невозможен, сначала согласуй ограниченную compatibility strategy и срок её удаления.
### Архитектурное ревью
1. Определи review scope, SLM root и формы затронутых доменов.
2. Построй фактическую карту owners, public boundaries, imports и runtime injection.
3. Проверь structural правила класса `A` по наблюдаемым evidence.
4. Отдельно проверь смысловые правила класса `R`; отсутствие lint error не доказывает их соблюдение.
5. Проверь transitive `business` closure и environment graph.
6. Проверь state/cache/error/lifecycle ownership.
7. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне.
8. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps.
Каждый finding содержит:
```text
Location:
Kind:
Rule or definition:
Evidence:
Impact:
Minimal remediation:
Required tests:
Confidence:
```
Не называй рекомендацию нарушением. Не подтверждай полное SLM conformance, если не исследовал весь нужный graph или не знаешь project mapping.
### Тестирование по владельцам
| Ответственность | Основная test boundary |
|---|---|
| Domain scenarios, validation, state и expected errors | `business` через соответствующую factory |
| Deterministic runtime/guards | `business` |
| Technical mapping и provider behavior | Adapter module |
| Graph composition, adapter selection, environment и cleanup | Assembly module |
| Provider, hook, form или query projection | Framework binding module |
| Multi-domain graph и lifecycle | Composition, `app` или другой graph owner |
Не повторяй полный business scenario suite в adapter, assembly и framework tests. Проверяй в каждой границе только принадлежащий ей behavior и integration contract.
## Anti-patterns
### Ownership и структура
- Выбирать слой или сущность по имени существующей папки.
- Размещать domain model или scenario в `infra`/`shared`.
- Оставлять page, route policy или multi-domain responsibility внутри домена.
- Делать Group, segment или component скрытым owner.
- Создавать общий module или Level 2 package на будущее.
- Требовать от component быть stateless: локальные data/lifecycle details допустимы, пока ответственность принадлежит parent module.
### Public boundaries
- Deep imports во внутренности module или `business`.
- Root barrel доменного пакета или Group.
- Export mutable store, context, client, adapter или singleton.
- Reexport client и server entry points через общий barrel.
- Создавать `business/runtime` без внешнего consumer.
### Business и runtime
- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `business`.
- Обходить boundary через helper, `shared` или type alias.
- Публиковать raw DTO или library-specific cache/store types в Domain API.
- Позволять adapter определять domain fallback, transition или error semantics.
- Прятать production adapter inline в assembly/composition.
- Позволять factory выбирать environment или assembly.
### Assembly, framework и cross-domain
- Добавлять scenario или API method в assembly.
- Вызывать factory/assembly из framework binding.
- Импортировать framework state, hooks или components другого домена.
- Импортировать чужую factory, assembly, adapter или API singleton.
- Прятать runtime dependency в service locator, mutable registry или event bus.
- Создавать pass-through adapter автоматически без проверки project policy и реальной boundary value.
### State и lifecycle
- Делать cache параллельной domain model.
- Строить optimistic domain value из raw form/DTO без business validation.
- Использовать file-level singleton без доказанного application scope.
- Запускать скрытую subscription/timer при создании API.
- Оставлять resource без scope или cleanup.
- Возвращать пустой `dispose` только для одинаковой формы assemblies.
### Процесс
- Выбирать Level 2 по размеру каталога.
- Генерировать полный package scaffold без потребности.
- Мигрировать соседние домены ради локального изменения.
- Копировать пример как нормативное дерево.
- Перечислять коды правил вместо анализа фактического graph и runtime.
- Задавать пользователю все открытые вопросы независимо от задачи.
## Stop conditions и адресные вопросы
Остановись до изменения публичной или runtime-границы, если:
- ответственность или owner не определены;
- одна ответственность имеет конкурирующих owners;
- неизвестны consumers изменяемого API;
- одна domain responsibility окажется в двух формах;
- planned edge создаёт цикл;
- environment compatibility нельзя установить;
- resource scope, multiplicity или cleanup неизвестны;
- изменение требует незапрошенной широкой миграции;
- локальные инструкции противоречат выбранной SLM boundary;
- корректность зависит от открытой semantics cancellation, concurrency, hydration или disposal;
- задача требует правил монорепозитория, versioning или нескольких SLM roots, которых текущий DRAFT не задаёт.
Задавай вопрос только при наличии trigger:
| Trigger | Что выяснить |
|---|---|
| L1 -> L2 или удаление старого API | Полный migration radius, атомарный cutover или compatibility strategy |
| Новая technical dependency | Ownership contract, timeout/retry/idempotency/order/subscription semantics |
| Abort или cancellable operation | Кто владеет cancellation и как она связана с cleanup/outcome |
| Публичные errors, RPC, server action | Expected failure, cancellation, unexpected defect и serialization policy |
| Store, persistence или external events | Initial state, transitions, reset, persistence и owner |
| Optimistic/concurrent mutations | Ordering, versioning, rollback/rebase и authoritative refresh |
| Assembly, lazy graph или новый root | Scope, multiplicity, owned/borrowed resources и disposal |
| SSR, hydration, RSC | Serialization boundary, validation/reset и executable/reference edges |
| Worker, edge, conditional exports | Реальные capabilities и resolver conditions |
| Готовый `infra` API совпадает с port | Нужен ли domain adapter или допустима прямая передача capability |
Можно продолжить с явным assumption только когда решение обратимо, не меняет owner/public API, не ослабляет environment boundary и не скрывает lifecycle.
## Проверочные списки
### До изменения файлов
- [ ] Найден SLM root и path mapping.
- [ ] Прочитаны локальные инструкции.
- [ ] Сформулирована responsibility.
- [ ] Назначен один owner.
- [ ] Выбраны layer и entity.
- [ ] Для domain выбрана одна form.
- [ ] Найдены реальные consumers.
- [ ] Спроектирован минимальный public API.
- [ ] Классифицированы static и runtime dependencies.
- [ ] Проверены layer, cross-domain и environment edges.
- [ ] Для resources определены scope и cleanup.
- [ ] Нет активного stop condition.
### После реализации
- [ ] Каждый module имеет отдельную boundary и public API.
- [ ] Нет deep imports и package/Group barrels.
- [ ] Layer matrix соблюдена.
- [ ] Общий module graph ацикличен.
- [ ] `business` import closure environment-neutral и technical-runtime-free.
- [ ] Cross-domain runtime APIs передаются аргументами.
- [ ] Client/server graphs не содержат несовместимый executable code.
- [ ] Facets `business` имеют допустимое содержимое и consumers.
- [ ] Production technical dependencies принадлежат нужным adapters.
- [ ] Assembly возвращает точный graph и cleanup, если владеет resource.
- [ ] Framework bindings получают готовые APIs.
- [ ] Technical и foreign errors не протекают наружу.
- [ ] Cache не подменяет business authority.
- [ ] Tests проверяют behavior соответствующих owners.
- [ ] После migration удалена старая form/boundary.
## Формат результата
Не печатай полную внутреннюю карточку и все checklists без необходимости. Пользователю нужен результат задачи.
| Режим | Обязательный результат |
|---|---|
| Design | Decision, owner/layer/form, boundaries, public APIs, dependencies, lifecycle, implementation order, assumptions |
| Implementation | Использованное решение, изменённые boundaries/files, API/import changes, tests/checks, отклонения и риски |
| Migration | Source/target forms, consumer map, phases, cutover, удаление старой boundary и completion gate |
| Review | Findings с evidence, verdict, remediation order, unresolved decisions и непроверенный scope |
Для однозначной локальной реализации достаточно кратко объяснить архитектурное решение и выполнить работу. Для дорогого, публично несовместимого или неоднозначного решения сначала покажи варианты и запроси выбор.
## Когда открывать references
| Ситуация | Reference |
|---|---|
| Нужна точная формулировка правила | [`rules/level-1.md`](./reference/draft/rules/level-1.md), [`rules/level-2.md`](./reference/draft/rules/level-2.md) |
| Неясен смысл сущности | [`level-1/terminology.md`](./reference/draft/level-1/terminology.md), [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) |
| Сложный Level 1 module/dependency/lifecycle case | [`level-1/`](./reference/draft/level-1/README.md) |
| Package, business, factory или adapters | [`level-2/domains/`](./reference/draft/level-2/domains/README.md) |
| Cross-domain или environment edge | [`level-2/dependencies.md`](./reference/draft/level-2/dependencies.md) |
| State, cache, SSR или hydration | [`state-cache.md`](./reference/draft/level-2/domains/state-cache.md), [`open-questions.md`](./reference/draft/level-2/domains/open-questions.md) |
| Assembly lifecycle и cleanup | [`assemblies.md`](./reference/draft/level-2/domains/assemblies.md) |
| Full architecture review | [`level-1/validation.md`](./reference/draft/level-1/validation.md), [`level-2/validation.md`](./reference/draft/level-2/validation.md) |
| L1 -> L2 migration example | [`auth-example.md`](./reference/draft/level-2/domains/auth-example.md) |
Будущие project examples открывай только после архитектурной классификации. Используй их как evidence конкретной реализации для похожего stack/environment, но не копируй naming, дерево или дополнительные роли без потребности. Example никогда не переопределяет rule или terminology.

View File

@@ -0,0 +1,15 @@
# Черновики SLM
> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM.
## Материалы
- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости.
- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups.
- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
## Соглашение
Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.

View File

@@ -0,0 +1,53 @@
# SLM Level 1
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 1 задаёт полную структурную основу SLM: слои, модули, доменные модули, публичные границы, граф зависимостей и владение жизненным циклом.
## Место в уровнях SLM
| Уровень | Назначение |
|---|---|
| Level 1 | Слои, доменные модули, зависимости, структурные сущности и жизненный цикл ресурсов |
| Level 2 | Опциональная пакетная форма отдельных доменов, именованные API, assemblies и явные границы сред выполнения |
Переход отдельного домена на Level 2 может требовать рефакторинга, но базовые понятия Level 1 сохраняются. Остальные домены того же SLM root могут оставаться модулями Level 1.
## Область Level 1
Level 1 описывает слои, доменные модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов.
Level 1 не задаёт обязательную внутреннюю форму доменного модуля, фабрики, порты, адаптеры, assemblies, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд.
Появление нескольких сред выполнения, нескольких независимо собираемых API или необходимости разделить бизнес-логику и технические сборки является сигналом перевести конкретный домен на [Level 2](../level-2/).
## Виды утверждений
- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила.
- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре.
- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной.
- **Пример** иллюстрирует модель и не задаёт обязательную структуру.
Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор правил: [правила SLM Level 1](../rules/level-1.md). Формат и требования к ним: [Правила SLM](../rules/).
Остальные документы Level 1 объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил.
## Основная идея
Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Доменный модуль представляет одну предметную область. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю.
Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта.
## Карта черновика
- [Терминология](./terminology.md)
- [Слои](./layers.md)
- [Доменные модули](./domains.md)
- [Зависимости](./dependencies.md)
- [Модули](./modules.md)
- [Группы](./groups.md)
- [Сегменты](./segments.md)
- [Компоненты](./components.md)
- [Вложенные модули](./nested-modules.md)
- [Жизненный цикл](./lifecycle.md)
- [Проверка](./validation.md)

View File

@@ -0,0 +1,65 @@
# Компоненты Level 1
> Пояснение нормативной модели компонентов Level 1.
Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей.
## Связанные правила
- [`SLM-L1-COMPONENT-R009`](../rules/level-1.md#slm-l1-component-r009)
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
## Файловая форма
Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами.
```text
landing/
└── ui/
└── hero.tsx
```
```text
landing/
└── ui/
└── hero/
├── hero.tsx
├── styles/
│ └── hero.module.css
└── types/
└── hero-props.type.ts
```
Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль.
## Реализация
Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления.
Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов.
Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль.
Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль.
Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM.
## Компонент и модуль
| Признак | Компонент | Модуль |
|---|---|---|
| Самостоятельная ответственность | Нет | Да |
| Собственный публичный API | Нет | Да |
| Собственная граница зависимостей | Нет | Да |
| Вспомогательные файлы | Может иметь | Может иметь |
| Сегменты и вложенные модули | Нет | Может иметь |
Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов.
## Когда нужен вложенный модуль
Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный.

View File

@@ -0,0 +1,59 @@
# Зависимости Level 1
> Пояснение нормативной модели зависимостей Level 1.
Матрица слоёв задаёт допустимые связи, а модули образуют граф зависимостей.
## Что считается зависимостью
- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
- Импорт между файлами одного модуля не пересекает модульную границу и не создаёт отдельный узел графа.
- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца.
- Вложенный модуль является обычным самостоятельным узлом графа.
- Группы, сегменты и компоненты не являются самостоятельными узлами графа.
Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв, но не образуют исходящий узел графа модулей.
Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля.
## Допустимые связи
- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей.
- Модули одного слоя могут импортировать друг друга.
- Промежуточный слой не является обязательным посредником.
- `infra` и `ui` не импортируют друг друга; их связывает владелец из `domains`, `compositions` или `app`.
Доменный модуль Level 1 может импортировать публичный API другого доменного модуля. Runtime- и type-only импорты одинаково создают ребро графа, поэтому общий граф обязан оставаться ацикличным.
```ts
// domains/orders
import type { Product } from '@/domains/catalog'
```
Level 1 не требует отдельного механизма междоменной инъекции. Более строгая модель cross-domain зависимостей задаётся Level 2.
Матрица слоёв определена в [Слоях](./layers.md).
## Связанные правила
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
## Публичный API
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимо
import { Button } from '@/ui/button/button'
```
## Циклы
```text
ui/modal → ui/button → ui/icon
ui/icon -/→ ui/modal
```

View File

@@ -0,0 +1,61 @@
# Доменные модули Level 1
> Пояснение базовой модели предметных областей без обязательной внутренней архитектуры.
## Связанные правила
- [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
## Один домен, один модуль
Связная предметная область получает один доменный модуль. Level 1 не требует выделять business, adapters, assemblies или framework bindings в самостоятельные соседние модули.
```text
domains/auth/
├── hooks/
├── services/
├── stores/
├── types/
├── ui/
└── index.ts
```
Показанные каталоги являются возможными сегментами, а не обязательным каркасом. Доменный модуль может содержать предметные типы, сценарии, состояние, framework-код, локальные адаптеры, компоненты и вложенные модули.
## Публичный API
Внешний код использует домен через обычный публичный API модуля:
```ts
import { signOut, useSession } from '@/domains/auth'
```
Глубокий импорт во внутренний сегмент нарушает модульную границу:
```ts
import { useSession } from '@/domains/auth/hooks/use-session'
```
## Groups
При большом количестве доменных модулей слой `domains` может содержать обычные навигационные Groups:
```text
domains/
├── shop/ # Group
│ ├── catalog/ # Доменный модуль
│ └── orders/ # Доменный модуль
└── cabinet/ # Group
└── profile/ # Доменный модуль
```
Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней домены.
## Переход на Level 2
Доменный модуль переводится в доменный пакет Level 2, когда ему нужны устойчивые API, несколько фабрик, разные среды выполнения или независимые SLM-модули сборок и framework-интеграции.
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним.

View File

@@ -0,0 +1,25 @@
# Группы Level 1
> Пояснение нормативной модели групп Level 1.
Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей.
## Связанное правило
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
## Пример
```text
compositions/
├── pages/ # Группа
│ ├── landing/ # Модуль
│ └── contacts/ # Модуль
└── layouts/ # Группа
└── main/ # Модуль
```
`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1.
Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу.

View File

@@ -0,0 +1,95 @@
# Слои Level 1
> Пояснение нормативной модели слоёв Level 1.
## Базовая структура
```text
src/
├── app/
├── compositions/
├── domains/
├── infra/
├── ui/
└── shared/
```
`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения.
Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса.
## Роли слоёв
### App
`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API модулей разрешённых слоёв или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM.
Точка входа может напрямую использовать `compositions`, `domains`, `infra`, `ui` или `shared`, если зависимость разрешена матрицей слоёв. Такое использование не переносит ответственность импортируемого модуля в `app`.
### Compositions
`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект.
### Domains
`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md).
Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`.
### Infra
`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели.
### UI
`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции.
### Shared
`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода.
В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом.
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API.
## Матрица зависимостей
```text
app
|
compositions
|
domains
/ \
infra ui
\ /
shared
```
Код слоя может импортировать модули своего слоя и слоёв, разрешённых строкой матрицы. Разрешённая зависимость может пропускать промежуточные роли.
| Слой | Может импортировать |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
`infra` и `ui` не импортируют друг друга. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие возможности через входной контракт либо связывается с ними в `domains` или `compositions`. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, такой код не является универсальным UI.
Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md).
## Связанные правила
- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001)
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-LAYER-R003`](../rules/level-1.md#slm-l1-layer-r003)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
## Граница Level 1
Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/).

View File

@@ -0,0 +1,31 @@
# Жизненный цикл Level 1
> Пояснение нормативной модели владения ресурсами Level 1.
Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль.
## Связанные правила
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
## Граница ресурса
Для ресурса определяются:
- модуль-владелец;
- место создания;
- момент начала работы;
- область жизни;
- допустимое число экземпляров;
- способ остановки и очистки.
Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены.
## Реализация
Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл.
Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает.
Точка входа `app` может запускать или подключать ресурс через публичный API импортируемого модуля, но не становится его владельцем.

View File

@@ -0,0 +1,43 @@
# Модули Level 1
> Пояснение нормативной модели модулей Level 1.
Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации.
## Связанные правила
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-A014`](../rules/level-1.md#slm-l1-module-a014)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-MODULE-R012`](../rules/level-1.md#slm-l1-module-r012)
## Владение
Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код.
Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль.
## Публичный API
Модуль предоставляет один логический публичный API. Конкретное имя точки входа и механизм экспорта определяет стайлгайд проекта.
Внешний код использует модуль только через публичный API. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми.
## Внутреннее устройство
Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд.
SLM не требует полного каркаса или обязательного каталога сегментов.
## Визуальный модуль
Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API.
```text
button/
├── button.tsx
└── index.ts
```
Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`.

View File

@@ -0,0 +1,30 @@
# Вложенные модули Level 1
> Пояснение нормативной модели вложенных модулей Level 1.
Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
## Связанные правила
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
## Пример
```text
landing/
├── landing.page.tsx
├── parts/
│ └── hero/
│ ├── hero.tsx
│ └── index.ts
└── index.ts
```
`parts/` здесь является примером сегмента, а не обязательным именем.
Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя.
Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса.

View File

@@ -0,0 +1,29 @@
# Сегменты Level 1
> Пояснение нормативной модели сегментов Level 1.
Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён.
## Связанное правило
- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
## Файловая форма
Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули.
Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей.
## Пример
```text
landing/ # Модуль
└── ui/ # Сегмент модуля
└── hero/ # Каталог компонента
├── hero.tsx
├── styles/ # Вспомогательный каталог компонента
└── types/ # Вспомогательный каталог компонента
```
`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов.

View File

@@ -0,0 +1,143 @@
# Терминология Level 1
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
## Базовые понятия
### SLM root
Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1.
### Ответственность
Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни.
### Владелец
Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение.
### Публичный API
Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта.
### Зависимость
Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей.
### Область жизни
Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными.
### Ресурс жизненного цикла
Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение.
### Очистка
Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу.
## Структурные сущности
### Нормативная матрица слоёв
Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок.
Для Level 1 нормативно отношение `app → compositions → domains → { infra, ui } → shared`. `infra` и `ui` являются независимыми ветвями: они не импортируют друг друга. Промежуточный слой не является обязательным посредником.
### Слой
Одна из шести верхнеуровневых ролей внутри SLM root:
| Слой | Роль |
|---|---|
| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных |
| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние |
| `infra` | Технические сервисы и возможности приложения |
| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции |
| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
Полная матрица допустимых зависимостей:
| Исходный слой | Допустимые целевые слои |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
### Модуль
Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
### Доменная ответственность
Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу.
### Доменный модуль
Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей.
Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям.
### Группа
Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей.
### Сегмент
Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей.
### Компонент
Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей.
Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность.
### Вложенный модуль
Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя.
### Точка входа фреймворка
Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей.
### Ресурс shared
Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API.
Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль.
## Структурная модель
```text
SLM root
├── app
│ └── точка входа фреймворка
├── compositions | domains | infra | ui
│ ├── группа
│ │ └── модуль
│ └── модуль
│ ├── корневые файлы
│ ├── сегмент
│ │ ├── файлы
│ │ ├── компоненты
│ │ └── вложенные модули
│ └── вложенный модуль
└── shared
├── группа
├── модуль
└── ресурс shared
```
Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта.

View File

@@ -0,0 +1,34 @@
# Проверка Level 1
> Граница автоматической проверки и архитектурного ревью Level 1.
## Автоматическая проверка
Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, доменными модулями, остальными модулями, Groups, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей.
Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент.
Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения.
Актуальный список правил скрипт получает из [канонического реестра](../rules/level-1.md).
## Архитектурное ревью
Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить:
- ответственность и её владельца;
- связность предметной области доменного модуля;
- соответствие кода роли слоя;
- необходимость экспортов публичного API;
- область жизни ресурса и достаточность очистки;
- наличие самостоятельной границы у компонента, группы или сегмента.
## Проверка доменных модулей
На ревью определяется:
- представляет ли доменный модуль одну связную предметную область;
- не разделена ли одна область на соседние модули без самостоятельных владельцев;
- не объединены ли в одном модуле несвязанные предметные области;
- остаются ли страницы, маршруты и multi-domain UI в `compositions`;
- остаются ли самостоятельные технические сервисы без предметной модели в `infra`.

View File

@@ -0,0 +1,102 @@
# SLM Level 2
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 2 предназначен для отдельных предметных областей с устойчивыми API, несколькими способами сборки или самостоятельными framework-модулями. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с явными владельцами ролей.
## Наследование Level 1
Доменный пакет Level 2 соблюдает определения и правила Level 1, кроме явно заменённых положений:
| Положение Level 1 | Статус в Level 2 |
|---|---|
| Матрица `app → compositions → domains → { infra, ui } → shared` | Сохраняется |
| Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл |
| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются только для предметной области в пакетной форме |
| Единый публичный API модуля `business` | Представлен обязательными type-only и factory-фасетами и необязательным runtime-фасетом |
| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
## Когда выбирать Level 2
Level 2 оправдан, когда одной предметной области нужны несколько независимо собираемых Domain API, разные browser/server assemblies, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога сам по себе не требует перехода.
## Цена Level 2
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет отдельные business-фасеты, минимум одну assembly, явную runtime-инъекцию cross-domain API и adapter-модули для production capabilities. Concrete state/query runtime, SDK и недетерминизм не импортируются напрямую в `business`.
Эта цена оправдана независимой сборкой API, несколькими средами, строгой environment boundary или самостоятельными framework bindings. Если домену не нужны такие свойства, он остаётся модулем Level 1.
## Базовая форма
```text
src/domains/
├── catalog/ # Доменный модуль Level 1
└── auth/ # Доменный пакет Level 2
├── README.md # Необязательная metadata
├── business/ # Обязательный SLM-модуль
│ ├── index.ts # Только public types
│ ├── factory.ts # Public factories entry
│ └── runtime.ts # Необязательный deterministic runtime
├── assemblies/ # Обязательная непустая Group
│ ├── browser/ # SLM-модуль
│ └── request/ # SLM-модуль
├── adapters/ # При наличии technical dependencies
│ └── identity-provider/ # SLM-модуль
└── react/ # Необязательная framework Group
├── session/ # SLM-модуль
└── login-form/ # SLM-модуль
```
Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API.
## Публичные границы
`business` может объявить несколько API и соответствующих фабрик. Assembly создаёт только нужный для своего контекста именованный граф:
```ts
import type {
AuthAdministrationApi,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session'
```
`business/runtime` существует только при наличии реальных внешних потребителей детерминированных runtime-экспортов. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API.
## Совместное применение форм
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей границу пакета Level 2, доменный код использует type-only публичный контракт либо детерминированный `business/runtime`; готовые API передаются runtime-аргументами в месте сборки графа.
Если доменному модулю Level 1 нужна runtime-инъекция, которой его текущий публичный API не поддерживает, этот потребитель рефакторится или сам переводится в пакетную форму. Level 2 не создаёт скрытый механизм внедрения в старый модуль.
## Карта черновика
- [Терминология](./terminology.md)
- [Доменный пакет](./domains/domain-package.md)
- [Модуль business](./domains/business.md)
- [Фабрики, зависимости и adapters](./domains/factory-ports-adapters.md)
- [Assemblies и среды выполнения](./domains/assemblies.md)
- [Состояние и кэш](./domains/state-cache.md)
- [Framework Groups и модули](./domains/framework-bindings.md)
- [Зависимости](./dependencies.md)
- [Тестирование](./domains/testing.md)
- [Проверка](./validation.md)
- [Переход auth](./domains/auth-example.md)
- [Открытые вопросы](./domains/open-questions.md)

View File

@@ -0,0 +1,109 @@
# Зависимости Level 2
> Уточнение графа зависимостей внутри и между доменными границами.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
## Матрица внутри пакета
| Исходный модуль | Допустимые зависимости |
|---|---|
| `business` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, business-safe packages, type-only контракты и `business/runtime` других доменов |
| Adapter module | Type-only barrel собственного `business`, `infra`, concrete technical runtime, `shared` |
| Assembly | Type-only barrel, `factory` и при необходимости `runtime` собственного `business`, публичные adapters своего домена, type-only API других доменов, `shared` |
| Framework binding module | Type-only barrel и `runtime` собственного `business`, публичные framework-модули своего домена, framework/state/query runtime, `ui`, `shared` |
| Место сборки графа | Assemblies либо `business/factory` и adapter-модули, `business/runtime`, framework-модули входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
## Междоменные импорты
При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { isAuthError } from '@/domains/auth/business/runtime'
```
Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
Запрещено импортировать из другого домена:
- `business/factory`;
- готовый API instance или singleton;
- assembly;
- adapter;
- framework state, hook, context, Provider или component;
- любой внутренний путь `business`.
Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
## Детерминированный runtime
`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
```ts
import {
normalizeAuthIdentifier,
} from '@/domains/auth/business/runtime'
```
Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
## Runtime-инъекция API
Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
```text
createAuthForRequest()
→ AuthSessionApi
→ createUserForRequest({ auth })
→ UserProfileApi
```
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
## Совместное применение Level 1 и Level 2
Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
## Framework-состояние
Framework binding module использует framework API только своего доменного пакета:
```ts
// Допустимо внутри domains/auth/react/login-form
import { useAuthSession } from '@/domains/auth/react/session'
// Недопустимо внутри domains/user/react/profile
import { useAuthSession } from '@/domains/auth/react/session'
```
Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
## Границы сред
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.

View File

@@ -0,0 +1,29 @@
# Доменные пакеты Level 2
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
```text
domains/auth/
├── business/
├── assemblies/ # Обязательная Group
├── adapters/ # При наличии technical dependencies
└── react/
├── session/
└── login-form/
```
Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
## Основные границы
- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary.
- [Business](./business.md) владеет Domain API и разделяет public types, factories и необязательный deterministic runtime.
- [Фабрики, зависимости и adapters](./factory-ports-adapters.md) изолируют runtime-capabilities, включая state/query runtime и недетерминизм.
- [Assemblies](./assemblies.md) обязательны и собирают именованный граф нужных API для реального контекста.
- [Состояние и кэш](./state-cache.md) разделяет предметную власть business и технические проекции.
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).

View File

@@ -0,0 +1,170 @@
# Assemblies и среды выполнения
> Пояснение повторяемой сборки именованного графа Domain API.
## Связанные правила
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ASSEMBLY-R011`](../../rules/level-2.md#slm-l2-assembly-r011)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
## Назначение
Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
```text
business/factory
├── assemblies/browser → { session: AuthSessionApi }
├── assemblies/request → { session, administration }
└── assemblies/server-action → { administration }
```
Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
## Именованный граф API
Browser assembly импортирует только фабрики и adapters нужных ей API:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { authSessionFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
export type AuthBrowserGraph = Readonly<{
session: AuthSessionApi
}>
export const createBrowserAuth = (): AuthBrowserGraph => {
const session = authSessionFactory({
phone: createPhoneHttpAdapter(),
session: createBrowserSessionAdapter(),
})
return { session }
}
```
Request assembly может собрать дополнительный API, которого нет в браузере:
```ts
import type {
AuthAdministrationApi,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
export type AuthRequestGraph = Readonly<{
administration: AuthAdministrationApi
session: AuthSessionApi
}>
```
Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
## Cross-domain input
Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import type { UserProfileApi } from '@/domains/user/business'
import { userProfileFactory } from '@/domains/user/business/factory'
import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
export type CreateUserForRequestInput = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
request: UserRequestInput
}
export type UserRequestGraph = Readonly<{
profile: UserProfileApi
}>
export const createUserForRequest = ({
auth,
request,
}: CreateUserForRequestInput): UserRequestGraph => {
const profile = userProfileFactory({
auth,
profile: createUserProfileAdapter(request),
})
return { profile }
}
```
Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
Место сборки графа выполняет runtime-связь:
```ts
const auth = createAuthForRequest(authInput)
const user = createUserForRequest({
auth: auth.session,
request: userInput,
})
```
## Environment entry points
Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
```ts
import 'server-only'
export { createAuthForRequest } from './create-auth-for-request'
```
Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
## Lifecycle
Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
```ts
const stop = auth.session.startInvalidationTracking()
try {
// Scope использует API.
} finally {
await stop()
}
```
Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
```ts
export type AuthRequestAssembly = Readonly<{
apis: AuthRequestGraph
dispose: () => Promise<void>
}>
```
```ts
const auth = createAuthForRequest(input)
try {
return await handleRequest(auth.apis)
} finally {
await auth.dispose()
}
```
Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.

View File

@@ -0,0 +1,139 @@
# Переход домена auth с Level 1
> Проверочный пример локального перехода от доменного модуля к доменному пакету.
## Связанные правила
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Исходная форма Level 1
```text
domains/
├── auth/ # Доменный модуль
│ ├── hooks/
│ ├── services/
│ ├── stores/
│ ├── ui/
│ └── index.ts # Общий API модуля
└── catalog/ # Независимый доменный модуль
└── index.ts
```
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке Auth находиться внутри одного модуля.
## Целевая форма Auth
```text
domains/
├── auth/ # Доменный пакет Level 2
│ ├── README.md
│ ├── business/ # Один SLM-модуль
│ │ ├── errors/
│ │ ├── factories/
│ │ ├── services/
│ │ ├── types/
│ │ ├── index.ts # Только public types нескольких API
│ │ ├── factory.ts # Public factories entry
│ │ └── runtime.ts # Error codes, guards, public pure runtime
│ ├── adapters/ # Group
│ │ ├── phone-http/ # SLM-модуль
│ │ ├── browser-session/ # SLM-модуль
│ │ └── request-session/ # SLM-модуль
│ ├── assemblies/ # Обязательная Group
│ │ ├── browser/ # Только AuthSessionApi
│ │ └── request/ # Session + Administration API
│ └── react/ # Framework Group
│ ├── session/ # SLM-модуль
│ └── login-form/ # SLM-модуль
└── catalog/ # По-прежнему модуль Level 1
└── index.ts
```
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
## Перенос ответственности
| Исходная часть | Владелец Level 2 | Публичный путь |
|---|---|---|
| Session-сценарии и public types | `auth/business` | `auth/business` |
| Administration-сценарии и public types | `auth/business` | `auth/business` |
| Runtime-фабрики API | `auth/business` | `auth/business/factory` |
| Коды, guards и public pure-функции | `auth/business` | `auth/business/runtime` |
| Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Browser graph | `auth/assemblies/browser` | `auth/assemblies/browser` |
| Request graph | `auth/assemblies/request` | `auth/assemblies/request` |
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
| Страница, текст и redirect | `compositions` | API конкретной composition |
## Новые импорты
```ts
import type {
AuthAdministrationApi,
AuthError,
AuthErrorCode,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
## Cross-domain граф
Если User package зависит от Auth, он получает только type-only API contract и при необходимости deterministic runtime:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { isAuthError } from '@/domains/auth/business/runtime'
export type UserDeps = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
}
```
Место сборки создаёт instances:
```ts
const auth = createBrowserAuth()
const user = createBrowserUser({ auth: auth.session })
```
User не импортирует Auth factory или assembly, а его React-модули не импортируют `useAuthSession` или Auth components.
Если User остаётся модулем Level 1, runtime-связь также выполняется снаружи доменных границ. Для этого его собственный API должен иметь явную точку передачи нужного Auth behavior; иначе именно User требуется рефакторинг или переход, но несвязанные домены не затрагиваются.
## Порядок перехода
1. Выбрать один доменный модуль и зафиксировать его внешние consumers.
2. Объявить `business` с type-only и factory entry points.
3. Разделить сценарии на независимо собираемые Domain API без дублирования методов.
4. Добавить `business/runtime`, только если внешним consumers нужны codes, guards или pure-функции.
5. Оформить каждую связную production implementation модулем `adapters/*`.
6. Создать минимум одну assembly и перенести туда повторяемый выбор API и adapters.
7. Разделить React-ответственности на модули внутри Group `react`.
8. Перенести страницы, redirects и multi-domain UI в `compositions`.
9. Перевести внешние импорты на разрешённые public paths.
10. Удалить старый root `index.ts` Auth и объявить пакетную форму checker-у.
Завершённость перехода определяется только границей Auth. Наличие других доменных модулей Level 1 не является миграционным долгом.

View File

@@ -0,0 +1,218 @@
# Модуль business
> Пояснение предметного владельца, нескольких Domain API и публичного deterministic runtime.
## Связанные правила
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
- [`SLM-L2-BUSINESS-R025`](../../rules/level-2.md#slm-l2-business-r025)
## Роль
`business` является обязательным SLM-модулем доменного пакета. Он владеет:
- публичными предметными сценариями;
- одним или несколькими именованными Domain API;
- одной публичной фабрикой для каждого API;
- типами явных зависимостей фабрик;
- предметными типами и детерминированными правилами;
- контрактами ожидаемых доменных ошибок;
- публичным представлением доменных данных и состояния.
Несколько API остаются частью одного модуля, пока относятся к одной связной предметной области. Они нужны не для копирования use cases, а для независимой сборки разных наборов сценариев, зависимостей и сред.
## Публичные фасеты
Один логический API модуля `business` имеет два обязательных и один необязательный entry point.
### Type-only barrel
Корневой `business/index.ts` экспортирует только типы:
```ts
export type {
AuthAdministrationApi,
AuthAdministrationDeps,
AuthAdministrationFactory,
AuthError,
AuthErrorCode,
AuthSessionApi,
AuthSessionDeps,
AuthSessionFactory,
AuthState,
} from './types'
```
Потребитель использует этот путь только через `import type`:
```ts
import type {
AuthSessionApi,
AuthState,
} from '@/domains/auth/business'
```
### Factory entry
`business/factory.ts` экспортирует только именованные runtime-фабрики:
```ts
export { authAdministrationFactory } from './factories/auth-administration.factory'
export { authSessionFactory } from './factories/auth-session.factory'
```
```ts
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
```
Каждому API соответствует одна фабрика. Фасет не экспортирует готовые instances, adapters или environment-specific assembly.
### Runtime entry
Необязательный `business/runtime.ts` экспортирует только публичные детерминированные значения и функции:
```ts
export {
AUTH_ERROR_CODES,
isAuthError,
} from './errors/auth-error'
export { normalizeAuthIdentifier } from './lib/normalize-auth-identifier'
```
Здесь допустимы error codes и guards, validators, value constructors, чистые продуктовые функции и immutable-константы. Все public types по-прежнему импортируются из корневого type-only barrel.
`business/runtime` не содержит:
- фабрики и готовые API instances;
- I/O или изменяемое состояние;
- state/query runtime;
- чтение clock, random, environment или platform API;
- сценарии, которым нужны runtime-зависимости.
Если внешним потребителям не нужен детерминированный runtime, файл `runtime.ts` не создаётся. Другие внешние пути внутри `business` являются deep imports.
## Несколько Domain API
```ts
export type AuthSessionApi = {
getCurrentSession: () => Promise<AuthState>
getSnapshot: () => AuthState
requestPhoneOtp: (phone: string) => Promise<void>
startInvalidationTracking: () => () => Promise<void>
verifyPhoneOtp: (code: string) => Promise<void>
}
export type AuthAdministrationApi = {
revokeUserSessions: (userId: string) => Promise<void>
}
```
`AuthSessionApi` может собираться в browser и request contexts, а `AuthAdministrationApi` только в доверенной server assembly. Browser assembly не получает метод-заглушку и не импортирует adapters административного API.
Общий `business/factory` является публичным фасетом, а не гарантией отдельного business chunk для каждой фабрики. Разделение API устраняет обязательное создание лишних adapters и instances. Если самой business-логике нужны независимо поставляемые bundle boundaries, требуется отдельный build/package mechanism за пределами текущего Level 2, а не искусственное разделение предметной области.
Один публичный сценарий принадлежит ровно одному API. Если два API постоянно требуют одинаковых методов, состояния и lifecycle, их граница пересматривается вместо дублирования.
Assembly может вернуть именованный граф нескольких API:
```ts
export type AuthBrowserGraph = Readonly<{
session: AuthSessionApi
}>
export type AuthRequestGraph = Readonly<{
administration: AuthAdministrationApi
session: AuthSessionApi
}>
```
Такой граф является составом готовых контрактов для контекста, а не новым предметным API.
## Предметная власть и состояние
Business API остаются единственной границей, определяющей предметную модель, validation, переходы и семантику результатов. Это не означает, что только `business` физически хранит байты.
Adapter или framework binding может использовать Zustand, TanStack Query, SWR, Apollo либо другой runtime для хранения и доставки значений. Такое хранилище является технической реализацией или проекцией, если:
- значения получены или проверены business API либо `business/runtime`;
- предметные переходы выполняются через business API;
- внешний DTO не становится публичной моделью напрямую;
- optimistic value создаётся или проверяется предметным владельцем;
- библиотечные cache/store types не становятся Domain API.
Подробная граница описана в [Состоянии и кэше](./state-cache.md).
## Потребители фасетов
| Потребитель | `business` | `business/factory` | `business/runtime` |
|---|---|---|---|
| Adapter своего домена | Type-only | Нет | Обычно нет |
| Assembly своего домена | Type-only | Да | При необходимости |
| Framework binding своего домена | Type-only | Нет | Да, если нужен public runtime |
| `composition` или `app` | Type-only | Для одноразовой сборки | Да |
| Код другого домена при связи с Level 2 | Type-only | Нет | Да |
| Тест | Type-only | По границе тестируемого API | По границе тестируемого владельца |
Runtime-импорт `business/runtime` другого домена остаётся архитектурным ребром и участвует в общей проверке циклов.
## Контракт ошибок
Ожидаемая ошибка имеет устойчивую безопасную readonly-форму:
```ts
export type AuthErrorCode =
| 'AUTH_PHONE_INVALID'
| 'AUTH_OTP_REQUEST_FAILED'
| 'AUTH_OTP_CODE_INVALID'
export type AuthError = Readonly<{
code: AuthErrorCode
}>
```
Если runtime-потребителям нужны constants или guard, они публикуются через `business/runtime`:
```ts
export const AUTH_ERROR_CODES = {
PHONE_INVALID: 'AUTH_PHONE_INVALID',
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
} as const
export const isAuthError = (value: unknown): value is AuthError => {
return isSafeCodedError(value, Object.values(AUTH_ERROR_CODES))
}
```
Guard не является обязательной частью каждого домена. При discriminated `Result` типизированному потребителю может быть достаточно error union; при exception, RPC или неизвестной runtime-границе guard часто нужен. Выбор `throw` или `Result` не меняет набор обязательных фасетов.
## Изоляция технических и чужих ошибок
Ошибки SDK, HTTP, database, storage и adapters не пересекают Domain API в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
```text
SDK error
→ adapter failure
→ business mapping
→ AuthErrorCode
→ приложение
```
Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные остаются во внутреннем observability-механизме.
То же относится к cross-domain вызову. Если User API использует Auth API, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`. При exception-модели зависимый business может импортировать `isAuthError` и error codes из `auth/business/runtime`, после чего преобразовать ожидаемую ошибку в собственный контракт.
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки.

View File

@@ -0,0 +1,114 @@
# Граница доменного пакета
> Пояснение контейнерной сущности Level 2 и её совместного использования с доменными модулями Level 1.
## Связанные правила
- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002)
- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004)
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
## Предметная граница
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server assemblies и React-модули, но не страницу профиля или общий database client.
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
## Корень пакета
```text
domains/auth/
├── README.md
├── business/
├── assemblies/
├── adapters/
└── react/
```
В корне разрешены:
- документация;
- ownership metadata;
- декларативный manifest или декларативная конфигурация архитектурной проверки;
- обязательный модуль `business`;
- обязательная непустая Group `assemblies`;
- непустая Group `adapters`, если хотя бы одна фабрика имеет технические зависимости;
- Framework Groups при наличии соответствующих модулей.
В корне запрещены:
- `index.ts` или другой агрегирующий executable entry point;
- runtime-файлы и side effects;
- изменяемое состояние и ресурсы lifecycle;
- реэкспорт API внутренних модулей;
- page-specific компоненты или сборка нескольких доменов.
Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета.
## Policy boundary
Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу.
Отсутствие root barrel намеренно:
- client- и server-entry points не агрегируются в один импорт;
- каждый модуль сохраняет отдельную ответственность и environment boundary;
- переименование публичного модуля является изменением его собственного контракта, а не скрывается пакетом;
- versioning целого publishable package остаётся за пределами Level 2.
## Модули и Groups
`business` размещается непосредственно в пакете. Assemblies находятся в обязательной Group `assemblies`. Production adapters являются самостоятельными модулями Group `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
```text
auth/
├── business/ # SLM-модуль
│ ├── index.ts # Только public types
│ ├── factory.ts # Public factories entry
│ └── runtime.ts # Необязательный deterministic runtime
├── adapters/ # Group при наличии technical dependencies
│ └── phone-http/ # SLM-модуль
├── assemblies/ # Обязательная Group
│ └── browser/ # SLM-модуль
└── react/ # Framework Group
├── session/ # SLM-модуль
└── login-form/ # SLM-модуль
```
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/business/factory`, опциональный `auth/business/runtime`, `auth/adapters/phone-http`, `auth/assemblies/browser` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/assemblies` или `auth/react`.
## Навигационные Groups
Слой `domains` может содержать Groups с обеими формами домена:
```text
domains/
└── commerce/ # Навигационная Group
├── catalog/ # Доменный модуль Level 1
└── orders/ # Доменный пакет Level 2
```
Такая Group отличается от Group внутри пакета допустимым составом: она содержит доменные модули, доменные пакеты и другие navigation Groups, а не внутренние модули пакета.
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы именно этого домена, а не переводом всех соседних областей.
## Границы соседних слоёв
| Ответственность | Владелец |
|---|---|
| Предметные сценарии, Domain API, доменные ошибки | `business` |
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
| Сборка API для именованного контекста | Assembly внутри пакета |
| Универсальный технический сервис | `infra` |
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
| UI, объединяющий несколько доменов | `compositions` |
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.

View File

@@ -0,0 +1,158 @@
# Фабрики, зависимости и adapters
> Пояснение границы между `business` и технической средой.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
## Одна фабрика на API
```text
явные зависимости + business factory → один Domain API
```
Модуль `business` предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет `business/factory`:
```ts
import type {
AuthAdministrationApi,
AuthAdministrationDeps,
AuthSessionApi,
AuthSessionDeps,
} from '@/domains/auth/business'
export type AuthSessionFactory = (
deps: AuthSessionDeps,
) => AuthSessionApi
export type AuthAdministrationFactory = (
deps: AuthAdministrationDeps,
) => AuthAdministrationApi
```
```ts
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
```
Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
## Технические зависимости
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
```ts
export type AuthPhoneDependency = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (code: string) => Promise<unknown>
}
```
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
Техническими зависимостями также являются:
- concrete state/query runtime;
- subscription и event source;
- browser, Node.js и framework capabilities;
- request data и abort signal;
- текущее время и timer;
- random и ID generator;
- environment и runtime configuration provider.
```ts
export type VerificationDeps = {
clock: { now: () => number }
ids: { create: () => string }
timer: { delay: (ms: number) => Promise<void> }
}
```
`Date.now()`, `new Date()` без аргумента, `Math.random()`, `crypto.randomUUID()`, скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
Cancellation также описывается business-owned контрактом. Concrete `AbortSignal` или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
## Cross-domain API dependency
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
}
```
Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. `user/business` не импортирует executable factory, assembly или instance Auth.
Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного `auth/business/runtime`. Этот импорт остаётся обычным ребром DAG.
## Adapter module
Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
```text
business dependency ← adapter → SDK / query runtime / platform / request data
```
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод Domain API, предметный fallback или код доменной ошибки.
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение не строит поведение по HTTP status, SDK error class или storage exception.
Adapter может использовать TanStack Query, Apollo, Zustand или другой state/query runtime, если реализует business-owned dependency. Library types, query keys и mutable clients не протекают в public types `business`.
## Размещение adapters
Каждая связная production-реализация является отдельным SLM-модулем в Group `adapters`:
```text
auth/adapters/
├── phone-http/
│ └── index.ts
├── browser-session/
│ └── index.ts
└── browser-runtime/
└── index.ts
```
Один adapter-модуль может реализовать несколько тесно связанных зависимостей одного technical provider. Например, `browser-runtime` может предоставить clock, timer и ID generator. Правило не требует отдельного модуля для каждого метода deps.
Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
Production adapter запрещено определять:
- закрытым сегментом assembly;
- inline-функцией в `composition` или `app`;
- частью framework binding module;
- скрытой реализацией внутри `business`.
Assembly и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
```ts
import { authSessionFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserRuntimeAdapter } from '@/domains/auth/adapters/browser-runtime'
const session = authSessionFactory({
phone: createPhoneHttpAdapter(),
runtime: createBrowserRuntimeAdapter(),
})
```
Если ни одна фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.

View File

@@ -0,0 +1,156 @@
# Framework Groups и модули
> Пояснение domain-specific framework-кода на примере React.
## Связанные правила
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Framework Group
Папка для domain-specific React binding modules называется `react`:
```text
domains/auth/react/ # Framework Group
├── session/ # SLM-модуль
│ ├── hooks/
│ ├── providers/
│ └── index.ts
├── queries/ # SLM-модуль
│ └── index.ts
└── login-form/ # SLM-модуль
├── components/
└── index.ts
```
`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
## Framework binding module
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании одного или нескольких готовых Domain API своего домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific assembly остаётся в `assemblies`, а page-specific модуль остаётся в `compositions`.
Framework binding module может:
- передавать готовые API через Provider и context;
- предоставлять domain-specific hooks;
- отображать состояние и безопасные ошибки домена;
- использовать framework-compatible state/query runtime;
- реализовывать переиспользуемую domain-specific форму или guard;
- связывать framework lifecycle с явными операциями Domain API.
Он не вызывает business-фабрику или assembly, не выбирает adapters и не создаёт новые предметные сценарии.
Framework binding импортирует типы и deterministic runtime через разные фасеты:
```ts
import type {
AuthError,
AuthSessionApi,
} from '@/domains/auth/business'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
```
Импорт `business/factory` запрещён: готовые API передаются модулю извне.
## Модуль session
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthSessionApi`:
```tsx
'use client'
type AuthSessionProviderProps = PropsWithChildren<{
api: AuthSessionApi
}>
export const AuthSessionProvider = ({
api,
children,
}: AuthSessionProviderProps) => {
return (
<AuthSessionContext.Provider value={api}>
{children}
</AuthSessionContext.Provider>
)
}
```
Публичный путь модуля:
```ts
import {
AuthSessionProvider,
useAuthSession,
} from '@/domains/auth/react/session'
```
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
## State/query runtime
`auth/react/queries` может использовать TanStack Query, SWR или другой React runtime поверх готового `AuthSessionApi`. Query cache остаётся framework projection, пока данные и transitions проходят через API, а optimistic values создаются или проверяются business-владельцем.
```ts
export const useAuthSessionQuery = () => {
const api = useAuthSession()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getCurrentSession,
})
}
```
Server prefetch и client hook могут быть разными модулями Framework Group с совместимыми environment markers. Общая query policy выносится в отдельный framework-модуль при наличии нескольких потребителей, а не дублируется в compositions.
Подробности описаны в [Состоянии и кэше](./state-cache.md).
## Модуль login-form
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, состоянием и ошибками своего домена. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain-owned guard может решить, разрешено ли действие, но политика перехода на `/login` остаётся у route composition.
## Запрет cross-domain framework imports
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого домена:
```ts
// Недопустимо: domains/user/react/profile
import { useAuthSession } from '@/domains/auth/react/session'
```
Cross-domain UI собирается в `compositions`:
```tsx
const session = useAuthSession()
return (
<UserProfile
userId={session.userId}
canEdit={session.isAuthenticated}
/>
)
```
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User factory при сборке графа, а User framework module работает уже со своим API.
## Публичные API
```ts
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.

View File

@@ -0,0 +1,52 @@
# Открытые вопросы Level 2
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
## Зафиксированные решения
- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям.
- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root.
- Одна предметная область имеет только одну форму.
- Корень пакета содержит только metadata, модуль `business` и допустимые Groups и не имеет executable API.
- `business` может объявлять несколько независимо собираемых Domain API и одну фабрику для каждого API.
- Публичный API `business` имеет обязательные type-only `business` и runtime `business/factory`, а также необязательный deterministic `business/runtime`.
- Приложение получает предметную модель, transitions и результаты через Domain API; technical и framework cache могут хранить и проецировать эти значения.
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена в исходной форме.
- Каждый доменный пакет содержит минимум одну assembly; универсальная изоморфная assembly не обязательна.
- Assembly может создавать именованный граф нескольких API и не добавляет к ним сценарии.
- При наличии технических зависимостей Group `adapters` обязательна, а каждая связная production implementation принадлежит adapter-модулю.
- Runtime cross-domain imports через пакетную границу разрешены только из `business/runtime`; type-only public contracts разрешены и входят в DAG.
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
- Cross-domain framework state, hooks, contexts и components не импортируются.
- Clock, timer, random, ID generator и environment являются явными dependencies business.
- Assembly без собственного lifecycle-ресурса не обязана возвращать пустой `dispose`.
## Владение состоянием
Нужно подробнее определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является то, что предметную модель и переходы определяет `business`, а concrete state manager реализует business-owned port либо framework projection.
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
## Передача ошибок
Нужно выбрать общую рекомендацию exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
Структура публичных фасетов от этого решения больше не зависит: type errors публикуются через `business`, а необходимые runtime codes и guards через опциональный `business/runtime`.
## Технические порты
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он описывает: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
Cross-domain API dependency уже зафиксирована как отдельный вид зависимости и не зависит от этого решения.
## Lifecycle сборки
Уже зафиксировано, что явная операция, запускающая ресурс, возвращает cleanup, а assembly с собственным ресурсом предоставляет cleanup handle результата. Ещё нужно определить async cleanup, rollback частичной сборки, repeated disposal, request abort и поведение API после завершения scope.
## Cache hydration
Нужно проверить единый способ разделять framework-neutral cache policy, client hooks, server prefetch и serialization boundary без утечки query-library types в Domain API.
## Автоматическая проверка
Нужно выбрать machine-readable формат для форм домена, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.

View File

@@ -0,0 +1,114 @@
# Состояние и кэш
> Пояснение границы между предметной властью business и техническими state/query runtimes.
## Связанные правила
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
## Библиотеки не запрещены
Запрет state/query manager в import-графе `business` не запрещает TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX или RxJS в доменном пакете. Он запрещает concrete runtime становиться предметным контрактом.
Такая библиотека может находиться:
- в adapter-модуле, если реализует техническую зависимость business-фабрики;
- в framework binding module, если доставляет готовый Domain API конкретному framework;
- в composition, если состояние принадлежит только её UI scope и не подменяет доменную модель.
## Три вида состояния
### Предметное состояние
Модель фактов и переходов домена. `business` определяет initial value, validation, допустимые команды, transitions и selectors. Concrete store может быть передан фабрике как business-owned port:
```ts
export type AuthStateDependency = {
create: (initial: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
subscribe: (listener: () => void) => () => void
}
}
```
Adapter поверх Zustand или другого manager реализует этот контракт, но не выбирает initial state и не добавляет переходы.
### Technical source cache
Кэш SDK, HTTP, GraphQL или storage-вызовов. Adapter может использовать QueryClient, Apollo cache или иной runtime для deduplication, transport retry и хранения внешних результатов. Business по-прежнему проверяет и преобразует результат до публикации предметной модели.
Query keys и библиотечные result types остаются внутри adapter. Domain API не экспортирует `UseQueryResult`, `ApolloError`, raw DTO или mutable QueryClient.
Если technical cache создаёт timers, subscriptions или другой lifecycle-ресурс для всего графа, владеющая им assembly передаёт cleanup graph owner. Cache без такого ресурса не требует искусственного `dispose`.
### Framework projection cache
Кэш, который framework binding строит поверх вызовов готового Domain API для rendering, Suspense, revalidation или UI coordination.
```ts
const useProfile = () => {
const api = useUserApi()
return useQuery({
queryKey: ['user', 'profile'],
queryFn: api.getProfile,
})
}
```
Такой cache не является параллельным предметным источником, пока его значения происходят из Domain API и все предметные изменения выполняются через API.
## Invalidation и retry
Не каждая cache policy является бизнес-правилом.
| Политика | Обычный владелец |
|---|---|
| Query key, stale time, deduplication, background refetch | Adapter или framework binding |
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
| Transport retry безопасного запроса | Adapter |
| Запрет повторной предметной команды | `business` |
| Cooldown, лимит попыток, допустимый transition | `business` |
| Freshness, влияющая на корректность сценария | `business` через явный контракт |
Если после команды требуется invalidation, framework binding может связать успешный результат API с техническими keys. Если выбор invalidation выражает предметную семантику, business возвращает устойчивый outcome/event либо публикует чистое правило через `business/runtime`; binding только отображает его на библиотечные операции.
## Optimistic updates
Framework binding не конструирует произвольную предметную модель из input формы или transport DTO. Optimistic update допустим, когда предполагаемое значение:
- возвращено командой Domain API как безопасная projection;
- создано отдельным pure-методом Domain API;
- создано или проверено публичной функцией `business/runtime`.
```ts
const optimisticProfile = projectProfileUpdate(currentProfile, command)
queryClient.setQueryData(profileKey, optimisticProfile)
```
Здесь `projectProfileUpdate` принадлежит `business/runtime`, а `setQueryData` остаётся технической операцией framework binding.
Запись raw form data или DTO напрямую в публичный product cache обходит предметного владельца и нарушает границу.
## Browser, SSR и RSC
Client hook, server prefetch и hydrate/dehydrate могут принадлежать разным framework binding modules с совместимыми environment entry points. Общая framework-specific cache policy выносится в отдельный модуль той же Framework Group, если у неё несколько реальных потребителей.
`compositions` вызывает публичный server binding для prefetch и публичный client binding для rendering. Она не повторяет query keys и mapping доменных результатов.
## Проверка на ревью
Для каждого state/query runtime определяется:
- является ли он adapter, framework projection или локальным UI state;
- откуда поступают значения;
- кто определяет transition и optimistic projection;
- где находятся library-specific types и keys;
- как invalidation соотносится с результатами Domain API;
- соответствует ли cache lifecycle области жизни API и framework scope.

View File

@@ -0,0 +1,91 @@
# Тестирование доменного пакета
> Проверка владельцев и публичных границ Level 2.
## Связанные правила
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
## Размещение
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
| Проверяемая граница | Владелец теста |
|---|---|
| Сценарии, Domain API, данные и ошибки | `business` |
| Публичная pure-функция или guard | `business/runtime` внутри тестов модуля business |
| Техническое преобразование | Adapter |
| Выбор API, dependencies и environment boundary | Assembly |
| Provider, hook, query integration, form или guard | Соответствующий framework binding module |
| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
## Business через фабрику
Каждый публичный сценарий проверяется через фабрику владеющего им API с управляемыми dependencies:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { authSessionFactory } from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
const api: AuthSessionApi = authSessionFactory(createAuthSessionTestDeps({
clock: { now: () => 1_700_000_000_000 },
phone: { requestCode: async () => ({ ok: true }) },
}))
await api.requestPhoneOtp('+79991112233')
```
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от `throw` или `Result`, но наружу проверяется только собственный AuthErrorCode.
Business-тест не использует React, реальный SDK, database, production assembly или системные clock/random. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
Если `business` объявляет несколько API, каждый тестируется через свою фабрику. Общая внутренняя pure-логика не требует повторять одинаковые cases на уровне всех API.
## Остальные модули
Тест adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
Тест обязательной assembly проверяет:
- вызов только нужных business-фабрик;
- точный именованный состав возвращённого графа;
- выбор публичных adapter-модулей;
- отсутствие несовместимого environment-кода;
- передачу cross-domain API аргументом, а не импортом;
- cleanup handle, если assembly создаёт ресурс жизненного цикла.
Assembly без собственного lifecycle-ресурса не тестирует пустой `dispose`, потому что не обязана его предоставлять.
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthSessionApi` и проверяет Provider, hook или component. Query binding дополнительно проверяет keys, invalidation и отсутствие raw DTO в публичном результате, но не повторяет полный набор business-сценариев.
## Автоматические структурные проверки
Проверка файлов, exports и import-графа подтверждает:
- отсутствие root API доменного пакета и Framework Groups;
- обязательные `business` и `business/factory`, type-only exports в корневом barrel и допустимый опциональный `business/runtime`;
- соблюдение матрицы потребителей фасетов business;
- наличие непосредственно в корне пакета непустой Group `assemblies` с объявленными модульными границами;
- отсутствие запрещённых runtime cross-domain imports;
- отсутствие type-only импортов из чужих assemblies, adapters и framework-модулей;
- отсутствие cross-domain framework hooks, contexts и components;
- отсутствие server-only достижимости из client modules;
- отсутствие runtime- и type-only циклов.
## Архитектурное ревью
На ревью проверяется, что `business/factory` экспортирует только объявленные фабрики, а `business/runtime` при наличии содержит только публичный deterministic runtime. Для каждой технической зависимости рассматриваются все production implementations: каждая связная implementation должна принадлежать одному модулю Group `adapters`, но один такой модуль может реализовать несколько тесно связанных capabilities одного provider.
Отдельно проверяются прямые обращения business к `Date.now`, `Math.random`, `crypto.randomUUID`, timers, env и другим скрытым runtime-capabilities.
Runtime-тест не заменяет автоматическую проверку или архитектурное ревью.

View File

@@ -0,0 +1,147 @@
# Терминология Level 2
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма, и не требует переводить остальные доменные модули того же SLM root.
## Формы домена
### Форма домена
Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно.
### Доменный пакет
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей.
Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением.
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
### Навигационная Group слоя `domains`
Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary.
### Модуль доменного пакета
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей.
Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
## Business
### Модуль business
Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, один или несколько именованных Domain API, соответствующие им фабрики, типы зависимостей и публичные контракты ожидаемых ошибок.
`business` является предметным владельцем данных, моделей, переходов и результатов сценариев домена. Он не зависит от конкретного фреймворка, среды или технической реализации.
### Публичные фасеты business
Объявленные entry points одного логического публичного API модуля `business`:
| Путь | Статус | Содержимое |
|---|---|---|
| `business` | Обязательный | Только public types, включая Domain API, зависимости, factory types и error types |
| `business/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
| `business/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
Фасет `business/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, предметные константы и чистые функции, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, сценарии с runtime-зависимостями или environment-specific код.
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `business` является deep import.
### Business-safe внешний пакет
Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами.
### Domain API
Именованный публичный runtime-контракт связного набора предметных сценариев внутри одного домена. Модуль `business` может объявить несколько Domain API, если они независимо собираются, имеют разные технические зависимости или нужны разным средам и потребителям.
Разделение на API не создаёт новые доменные пакеты и не разрешает дублировать один сценарий в нескольких контрактах. Каждый публичный сценарий принадлежит ровно одному Domain API.
### Фабрика business
Публичная функция фасета `business/factory`, которая получает явные зависимости и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. Фабрика не выбирает assembly или среду выполнения.
### Предметная власть business
Право определять публичную доменную модель, допустимые переходы, валидацию внешних значений и семантику результатов. Adapter, assembly или framework binding может хранить, кэшировать и проецировать значения Domain API, но не становится независимым источником предметной модели или переходов.
### Доменная ошибка
Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивый readonly-тип с кодом; при необходимости runtime-коды и guards публикуются через `business/runtime`. Ошибки SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API.
Способ передачи ошибки, например exception или discriminated `Result`, не изменяет владение и публичную безопасность ошибки.
## Техническая сборка
### Техническая зависимость
Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации. К техническим зависимостям относятся источники данных, SDK, storage, API платформы, state/query runtime, технические сервисы, данные request scope, clock, timer, random, ID generator и другие источники ввода-вывода, состояния или недетерминизма.
Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
### Adapter
SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей business-фабрик поверх SDK, storage, state/query runtime, API платформы, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly или composition.
Group `adapters` обязательна и непуста, если хотя бы одна business-фабрика имеет техническую зависимость. Фабрики без технических зависимостей не требуют создания этой Group.
Точная обязательная поведенческая форма технических портов пока не определена и остаётся открытым вопросом Level 2.
### Assembly
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного реального контекста выполнения: браузера, запроса, server action или другого окружения. При наличии технических зависимостей assembly выбирает их adapter-модули и передаёт фабрикам готовые runtime-зависимости.
Assembly может принимать готовые API других доменов аргументами. Она не добавляет предметные сценарии, методы API или собственные доменные ошибки.
Каждый доменный пакет содержит минимум одну assembly. Архитектура не ограничивает их максимальное количество и не требует универсальной изоморфной assembly.
### Ресурс assembly
Ресурс жизненного цикла, который assembly сама создаёт для возвращаемого графа API. Создание фабрики или assembly не запускает скрытую долгоживущую работу. Если технический ресурс необходимо активировать при сборке, публичный результат assembly предоставляет cleanup handle; если ресурс запускается явной операцией API, эта операция предоставляет cleanup.
Assembly без собственного ресурса жизненного цикла не обязана возвращать пустой `dispose`.
## Framework binding
### Framework Group
Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API.
### Framework binding module
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` - переиспользуемой формой авторизации.
Framework binding module получает готовые Domain API, не вызывает фабрику или assembly и не импортирует framework-состояние, hooks или компоненты другого домена. Он может использовать совместимые с его ролью state/query libraries и технически кэшировать результаты Domain API, не создавая новую предметную модель.
## Сборка графа
### Место сборки графа
Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт assemblies либо напрямую вызывает фабрики в ацикличном порядке и передаёт уже созданные API зависимым сборкам.
Место сборки владеет областью жизни совокупного графа и вызывает предоставленные cleanup handles не позже её завершения. Это координационное владение не переносит к нему предметные или технические ответственности внутренних модулей.
### Граница среды выполнения
Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking.
## Структурная модель
```text
SLM root
└── domains
├── доменный модуль Level 1
└── доменный пакет Level 2
├── metadata
├── модуль business
├── обязательная Group assemblies
│ └── assembly-модуль
├── Group adapters при наличии технических зависимостей
│ └── adapter-модуль
└── Framework Group react
├── модуль session
└── модуль login-form
```

View File

@@ -0,0 +1,81 @@
# Проверка Level 2
> Граница автоматической проверки, архитектурного ревью и тестирования Level 2.
## Конфигурация проекта
Конфигурация проверки сопоставляет физические пути с доменными модулями Level 1, доменными пакетами Level 2, metadata, SLM-модулями, Groups, фасетами `business`, техническими зависимостями, adapter-модулями, assemblies, публичными точками входа, метками сред выполнения и allowlist внешних business-safe пакетов.
Формат такой конфигурации пока не выбран. Независимо от формата проверка анализирует объявленные границы и import-граф, а не угадывает сущность только по имени папки.
## Автоматическая проверка
Автоматическая проверка блокирует:
- одновременное объявление одной предметной области доменным модулем и пакетом;
- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета;
- отсутствие `business` или несколько модулей `business` в одном пакете;
- отсутствие `business` либо `business/factory`, runtime export из корневого barrel, export не-фабрики из `business/factory`, type export из `business/runtime`, другой публичный путь либо deep import внутри `business`;
- импорт фасета `business` потребителем, которому этот фасет не разрешён;
- отсутствие непосредственно в корне пакета непустой Group `assemblies` или наличие в ней прямого дочернего элемента без объявленной модульной границы;
- deep imports во внутренние части модулей;
- runtime- или type-only достижимость framework-, adapter-, assembly-, infra- или environment-specific кода из `business`;
- запрещённый runtime-импорт через границу пакета Level 2;
- type-only импорт не из публичной точки входа владельца;
- импорт framework state, hooks, contexts или components другого домена;
- достижимость server-only кода из client-entry point и обратное несовместимое направление;
- runtime- или type-only циклы в графе модулей.
Статический анализ может отдельно находить прямые вызовы `Date.now`, `Math.random`, `crypto.randomUUID`, timers и чтение env внутри `business`. Окончательное решение о скрытом недетерминизме остаётся за ревью.
## Архитектурное ревью
На ревью определяется:
- представляет ли пакет одну связную предметную область;
- принадлежит ли каждая соседняя область ровно одной форме независимо от форм других доменов;
- принадлежат ли публичные сценарии ровно одному из именованных Domain API;
- оправдано ли разделение API разными consumers, dependencies или assemblies, а не техническим дроблением;
- остаются ли модель, validation и transitions под предметной властью `business`;
- не создаёт ли state/query cache параллельную продуктовую модель или raw DTO boundary;
- соответствует ли каждой фабрике ровно один API и остаётся ли она environment-neutral;
- содержит ли `business/runtime` только реально публичные deterministic values и functions;
- преобразует ли business ожидаемые technical и cross-domain сбои в собственные ошибки;
- является ли каждая связная production-реализация технических dependencies отдельным модулем Group `adapters`;
- представляет ли каждая assembly один реальный контекст выполнения и возвращает ли точный именованный граф;
- не запускают ли фабрики и assemblies скрытую долгоживущую работу при создании графа;
- предоставляет ли assembly cleanup только для действительно созданного ею lifecycle-ресурса и вызывает ли graph owner этот cleanup;
- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию;
- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability;
- остаются ли Framework Groups и другие Groups без реализации и агрегирующего API.
## Тестирование
Business-сценарии проверяются через соответствующие фабрики с управляемыми test fakes, включая fake clock/random/id при необходимости. Adapter module проверяет technical transformation. Assembly проверяет состав графа, выбор adapters, environment boundary и условный cleanup. Framework binding module проверяет собственный Provider, hook, cache integration или component без повторения полного набора business-сценариев.
Import-graph checks не заменяются runtime-тестами.
## Смешанный SLM root
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим пакетную границу.
Переход одного домена завершается, когда его старая модульная граница удалена и checker видит только пакет. Другие домены не входят в критерий завершения.
## Связанные правила
- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-BUSINESS-R024`](../rules/level-2.md#slm-l2-business-r024)
- [`SLM-L2-BUSINESS-R025`](../rules/level-2.md#slm-l2-business-r025)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.

View File

@@ -0,0 +1,128 @@
# Правила SLM
> Статус: системный черновик. Не является нормативной спецификацией.
Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил.
## Что считается правилом
Правило задаёт один блокирующий архитектурный инвариант.
Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила.
Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
## Код правила
```text
SLM-L{level}-{group}-{class}{number}
```
| Часть | Значение |
|---|---|
| `SLM` | Принадлежность архитектуре SLM |
| `L{level}` | Уровень архитектуры |
| `group` | Раздел правил |
| `class` | Способ проверки: `A` или `R` |
| `number` | Трёхзначный номер внутри уровня |
## Способы проверки
### `A`: автоматическая проверка
Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку.
### `R`: проверка на ревью
Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью.
Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`.
## Разделы правил
| Код | Раздел |
|---|---|
| `LAYER` | Слои |
| `DEPENDENCY` | Зависимости |
| `MODULE` | Модули |
| `GROUP` | Группы |
| `SEGMENT` | Сегменты |
| `COMPONENT` | Компоненты |
| `NESTED_MODULE` | Вложенные модули |
| `LIFECYCLE` | Жизненный цикл |
| `DOMAIN` | Домены |
| `BUSINESS` | Контракты бизнес-логики |
| `FACTORY` | Фабрики бизнес-логики |
| `ERROR` | Ошибки домена |
| `PORT` | Порты бизнес-логики |
| `ADAPTER` | Адаптеры |
| `ASSEMBLY` | Сборка API и жизненный цикл |
| `ENVIRONMENT` | Границы сред выполнения |
| `FRAMEWORK` | Модули фреймворков |
| `TEST` | Тестирование |
Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
## Формат записи
```md
### SLM-L1-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
```
Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`.
Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку.
Ссылка из тематического черновика:
```md
[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
```
## Как формулировать правила
1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня.
2. Правило защищает один архитектурный инвариант.
3. Один инвариант получает один код независимо от числа участников и способов проверки.
4. Название является кратким и устойчивым именем правила.
5. Название обозначает предмет правила, а описание полностью формулирует требование.
6. Описание объясняет допустимую границу и то, что считается нарушением.
7. Описание раскрывает названный инвариант и не вводит второе независимое требование.
8. Описание использует нормативные определения и не пересказывает их без необходимости.
9. Название и описание используют человеческий язык и только необходимые архитектурные термины.
10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании.
11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу.
12. Перед добавлением правила реестр проверяется на дубли и противоречия.
13. Код присваивается после проверки правила на примерах и контрпримерах.
## Нумерация
1. Номер уникален внутри уровня независимо от раздела и способа проверки.
2. Номер не обозначает важность или порядок выполнения.
3. Удалённый номер не переиспользуется для другого правила.
4. При изменении способа проверки номер сохраняется, но меняется полный код.
## Проверка качества
Перед принятием правила нужно ответить «да»:
- Понятно, о чём правило?
- Название кратко и однозначно называет правило?
- Понятно, что оно требует?
- Понятно, что является нарушением?
- Нельзя ли объединить его с существующим правилом?
- Не содержит ли оно рекомендацию или разрешение?
- Соответствует ли класс способу окончательной проверки?
## Проверка документов
Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения.
## Наборы правил
- [Первый уровень](./level-1.md)
- [Второй уровень](./level-2.md)

View File

@@ -0,0 +1,110 @@
# Правила SLM первого уровня
Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
## Размещение кода по слоям
### SLM-L1-LAYER-R001
> **Назначение слоёв**
>
> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
### SLM-L1-LAYER-A002
> **Направление зависимостей**
>
> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
### SLM-L1-LAYER-R003
> **Граница слоя `app`**
>
> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`.
## Границы модулей
### SLM-L1-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
### SLM-L1-MODULE-A014
> **Папка модуля**
>
> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
### SLM-L1-MODULE-R006
> **Ответственность модуля**
>
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
### SLM-L1-MODULE-R011
> **Владелец ответственности**
>
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
### SLM-L1-MODULE-R012
> **Состав публичного API**
>
> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
## Зависимости между модулями
### SLM-L1-DEPENDENCY-A005
> **Циклические зависимости**
>
> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов.
## Назначение групп
### SLM-L1-GROUP-R007
> **Назначение группы**
>
> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом.
## Назначение сегментов
### SLM-L1-SEGMENT-R008
> **Граница сегмента**
>
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей.
## Ответственность компонентов
### SLM-L1-COMPONENT-R009
> **Ответственность компонента**
>
> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
## Границы вложенных модулей
### SLM-L1-NESTED_MODULE-A010
> **Доступ к вложенному модулю**
>
> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя.
## Жизненный цикл
### SLM-L1-LIFECYCLE-R013
> **Жизненный цикл ресурсов**
>
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
## Граница доменных модулей
### SLM-L1-DOMAIN-R015
> **Доменный модуль**
>
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.

View File

@@ -0,0 +1,171 @@
# Правила SLM второго уровня
Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `business` правило `SLM-L1-MODULE-A004` уточняется правилом `SLM-L2-BUSINESS-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются после изменения модели уровней и перехода к постоянному совместному применению форм.
## Граница доменного пакета
### SLM-L2-DOMAIN-R002
> **Предметная граница пакета**
>
> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области.
### SLM-L2-DOMAIN-A003
> **Корень доменного пакета**
>
> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
### SLM-L2-GROUP-R004
> **Навигационная Group доменов**
>
> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
## Business и Domain API
### SLM-L2-BUSINESS-R005
> **Модуль business**
>
> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и объявляет один или несколько именованных Domain API этой предметной области.
### SLM-L2-BUSINESS-R006
> **Предметная власть business**
>
> Adapters, assemblies и framework binding modules транспортируют, кэшируют или проецируют доменные данные только в форме, произведённой или проверенной публичным API либо детерминированным runtime модуля `business`, и не определяют независимую предметную модель, переход или результат сценария.
### SLM-L2-BUSINESS-A007
> **Импортная замкнутость business**
>
> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe.
### SLM-L2-FACTORY-R008
> **Фабрики Domain API**
>
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `business/factory`; фабрика получает явные runtime-зависимости, создаёт только этот API и не выбирает assembly или среду выполнения.
## Ошибки домена
### SLM-L2-ERROR-R009
> **Публичный контракт ошибок**
>
> Каждый ожидаемый сбой публичного предметного сценария представлен именованным readonly-типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через type-only barrel, а необходимые внешним потребителям runtime-коды и guards только через `business/runtime`.
### SLM-L2-ERROR-R010
> **Изоляция исходных ошибок**
>
> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
## Assemblies и зависимости
### SLM-L2-ASSEMBLY-R011
> **Роль assembly**
>
> Каждая assembly является SLM-модулем одного именованного контекста выполнения, выбирает необходимые adapter-модули, вызывает одну или несколько business-фабрик и возвращает явный именованный граф их API, не добавляя предметные сценарии, методы API или собственные доменные ошибки.
### SLM-L2-DEPENDENCY-A012
> **Междоменные импорты Level 2**
>
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или корневого `business` пакета Level 2 либо runtime-импорт `business/runtime` пакета Level 2; все такие связи входят в общий ацикличный граф, а остальные runtime-экспорты другого домена не импортируются.
### SLM-L2-ENVIRONMENT-A013
> **Совместимость среды выполнения**
>
> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов.
## Framework Groups и тестирование
### SLM-L2-FRAMEWORK-R014
> **Framework Group домена**
>
> Framework binding modules доменного пакета размещаются в Group, названной по фреймворку, и каждый прямой дочерний элемент этой Group является framework binding module.
### SLM-L2-FRAMEWORK-R015
> **Framework binding module**
>
> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией.
### SLM-L2-TEST-R016
> **Проверка владельцев Level 2**
>
> Каждый публичный предметный сценарий проверяется через фабрику владеющего им Domain API, а основные тесты adapter, assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев.
## Совместное применение форм
### SLM-L2-DOMAIN-A026
> **Однозначная форма домена**
>
> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
## Внешние библиотеки business
### SLM-L2-BUSINESS-R018
> **Business-safe внешний пакет**
>
> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
## Публичные фасеты business
### SLM-L2-BUSINESS-A019
> **Публичные фасеты business**
>
> Публичный API `business` имеет обязательные entry points `business` только с type exports и `business/factory` только с именованными runtime-фабриками, может иметь `business/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
## Обязательные роли сборки
### SLM-L2-ASSEMBLY-A020
> **Обязательная Group assemblies**
>
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
### SLM-L2-ADAPTER-R021
> **Модули production adapters**
>
> Если хотя бы одна business-фабрика имеет техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одной или нескольких таких зависимостей принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
### SLM-L2-BUSINESS-A022
> **Потребители фасетов business**
>
> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только assemblies своего домена, `app`, `compositions` и тесты, а `business/runtime` импортируется только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
## Жизненный цикл assembly
### SLM-L2-ASSEMBLY-R023
> **Cleanup ресурса assembly**
>
> Создание Domain API фабрикой или assembly не запускает скрытый ресурс жизненного цикла; ресурс запускается явной операцией с cleanup, а технический ресурс, который assembly обязана создать для графа, сопровождается публичным cleanup handle, вызываемым не позже завершения области жизни графа.
## Недетерминизм business
### SLM-L2-BUSINESS-R024
> **Явные источники недетерминизма**
>
> Business-сценарий получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные зависимости фабрики и не читает их из скрытого runtime-окружения.
## Публичный runtime business
### SLM-L2-BUSINESS-R025
> **Детерминированный runtime business**
>
> Фасет `business/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Style Guide"
short_description: "Frontend code style, naming and local consistency"
default_prompt: "Use $style-guide to format or review frontend code according to the project style guide and local code conventions."

View File

@@ -0,0 +1,154 @@
# Value Predicates
`value-predicates` — небольшая библиотека runtime-предикатов для безопасной работы с `unknown`, `null`, массивами и объектами.
Файл содержит два типа утилит:
- Type guards: возвращают `value is T` и сужают тип в TypeScript.
- Boolean-предикаты: возвращают `boolean` и используются для читаемых условий без обязательного narrowing.
Проверки доменных DTO и API-ответов не размещаются здесь. Они должны жить рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.
## Группы
- `Nullish`: `isDefined`, `isNotDefined`.
- `Primitives`: `isString`, `isNumber`, `isBoolean`.
- `Strings`: `isNonEmptyString`.
- `Arrays`: `isArray`, `isArrayOf`, `isEmptyArray`, `isNonEmptyArray`.
- `Objects`: `isRecord`, `hasOwn`.
- `Combinators`: `isOneOf`.
## Правило для TSX
В conditional rendering не пишем голые проверки длины массива:
```tsx
items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
items && items.map(...)
```
Для списков используем `isEmptyArray`, `isNonEmptyArray`, а для `unknown`-данных — `isArrayOf`.
Голый `.length` допустим, когда нужен именно числовой размер для текста, расчётов или атрибутов.
## Уже типизированный массив
```tsx
const ordersData = orders.data
{isNonEmptyArray(ordersData) && (
<OrdersList orders={ordersData} />
)}
```
В этом сценарии `ordersData` уже имеет тип вроде `Order[] | null | undefined`, поэтому достаточно проверить, что массив существует и не пуст.
## Empty state
```tsx
const ordersData = orders.data
const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
{shouldShowEmptyState && (
<EmptyState />
)}
```
`isEmptyArray` считает `null` и `undefined` пустым списком. Это удобно для UI-состояний, где отсутствие данных и пустой список показывают один empty state.
## Map в JSX
```tsx
const itemsData = items
{isNonEmptyArray(itemsData) && itemsData.map((item) => (
<Card key={item.id} item={item} />
))}
```
Сначала сужаем локальную переменную, потом используем её в `map`. Не дублируем путь к данным внутри JSX.
## Unknown/API данные
```tsx
type Order = {
id: string
title: string
}
const isOrder = (value: unknown): value is Order => {
return (
isRecord(value) &&
hasOwn(value, 'id') &&
isString(value.id) &&
hasOwn(value, 'title') &&
isString(value.title)
)
}
const ordersData = response.data
{isArrayOf(ordersData, isOrder) && (
<OrdersList orders={ordersData} />
)}
```
`isArrayOf` нужен на границе с недоверенными данными: он проверяет не только массив, но и каждый элемент через item guard.
Если нужно одновременно проверить форму элементов и непустой массив:
```tsx
const canRenderOrders = isArrayOf(ordersData, isOrder) && isNonEmptyArray(ordersData)
{canRenderOrders && (
<OrdersList orders={ordersData} />
)}
```
Если такой паттерн повторится много раз, можно добавить отдельный `isNonEmptyArrayOf`, но заранее его не вводим.
## Object fields
```ts
if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
// value.code: string
}
```
`isRecord` проверяет только базовую форму объекта: не `null` и не массив. Конкретные поля всегда проверяются отдельно.
## Literal unions
```ts
const statuses = ['draft', 'published'] as const
if (isOneOf(value, statuses)) {
// value: 'draft' | 'published'
}
```
`isOneOf` удобен для runtime-проверки union-типов, собранных из `as const` массивов.
## Нормализованный массив
Если массив заранее нормализован, прямой `map` допустим:
```tsx
const ordersData = orders.data ?? []
return ordersData.map((order) => (
<OrderCard key={order.id} order={order} />
))
```
Для conditional rendering empty state всё равно используем predicate:
```tsx
{isEmptyArray(ordersData) && (
<EmptyState />
)}
```

View File

@@ -0,0 +1 @@
export * from './value-predicates'

View File

@@ -0,0 +1,169 @@
/**
* Value predicates для проверки runtime-значений.
*
* Type guards в этом файле сужают unknown-данные только до базовых типов.
* Boolean-предикаты используются для читаемых условий и не обязаны сужать тип.
* Проверки доменных DTO и API-ответов размещаются рядом с владельцем данных.
*/
/* --- Nullish --- */
/**
* Исключает только null и undefined из типа значения.
*
* `0`, `false` и пустая строка не считаются отсутствующими.
* Удобно для `.filter(isDefined)`.
*
* @example
* ```ts
* const values = [0, null, false, undefined, '']
* const definedValues = values.filter(isDefined)
* // definedValues: Array<0 | false | ''>
* ```
*/
export const isDefined = <T>(value: T | null | undefined): value is T => {
return value != null
}
/**
* Проверяет, что значение отсутствует как null или undefined.
*/
export const isNotDefined = <T>(value: T | null | undefined): value is null | undefined => {
return value == null
}
/* --- Primitives --- */
/**
* Сужает unknown-значение до string.
*/
export const isString = (value: unknown): value is string => {
return typeof value === 'string'
}
/**
* Сужает unknown-значение до конечного number.
*
* NaN и Infinity не проходят проверку.
*/
export const isNumber = (value: unknown): value is number => {
return typeof value === 'number' && Number.isFinite(value)
}
/**
* Сужает unknown-значение до boolean.
*/
export const isBoolean = (value: unknown): value is boolean => {
return typeof value === 'boolean'
}
/* --- Strings --- */
/**
* Проверяет, что значение является строкой с непустым содержимым.
*
* Пробельная строка считается пустой.
*/
export const isNonEmptyString = (value: unknown): value is string => {
return typeof value === 'string' && value.trim().length > 0
}
/* --- Arrays --- */
/**
* Проверяет, что значение является массивом.
*
* Не проверяет тип элементов. Для проверки элементов используйте `isArrayOf`.
*/
export const isArray = (value: unknown): value is unknown[] => {
return Array.isArray(value)
}
/**
* Проверяет массив и каждый его элемент через переданный предикат элемента.
*
* Используется на границах с unknown-данными, когда нужно получить `T[]`.
*
* @example
* ```ts
* if (isArrayOf(value, isString)) {
* // value: string[]
* }
* ```
*/
export const isArrayOf = <T>(value: unknown, isItem: (item: unknown) => item is T): value is T[] => {
return Array.isArray(value) && value.every(isItem)
}
/**
* Проверяет, что массив отсутствует или не содержит элементов.
*
* Null и undefined считаются пустым списком для UI-условий.
*/
export const isEmptyArray = (value: readonly unknown[] | null | undefined): boolean => {
return !Array.isArray(value) || value.length === 0
}
/**
* Проверяет, что массив существует и содержит хотя бы один элемент.
*
* Сужает тип до non-empty tuple, чтобы TypeScript знал,
* что обращение к первому элементу безопасно.
*
* @example
* ```ts
* if (isNonEmptyArray(items)) {
* const firstItem = items[0]
* }
* ```
*/
export const isNonEmptyArray = <T>(value: readonly T[] | null | undefined): value is readonly [T, ...T[]] => {
return Array.isArray(value) && value.length > 0
}
/* --- Objects --- */
/**
* Проверяет, что значение является объектом-записью.
*
* Исключает null и массивы, но не проверяет конкретную форму объекта.
*/
export const isRecord = (value: unknown): value is Record<PropertyKey, unknown> => {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Проверяет наличие собственного свойства объекта.
*
* Используйте вместе с `isRecord` перед чтением unknown-свойств.
*
* @example
* ```ts
* if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
* // value.code: string
* }
* ```
*/
export const hasOwn = <K extends PropertyKey>(value: object, key: K): value is Record<K, unknown> => {
return Object.prototype.hasOwnProperty.call(value, key)
}
/* --- Combinators --- */
/**
* Проверяет, что значение входит в список допустимых литералов.
*
* Удобно для runtime-проверки union-типов из `as const` массивов.
*
* @example
* ```ts
* const statuses = ['draft', 'published'] as const
*
* if (isOneOf(value, statuses)) {
* // value: 'draft' | 'published'
* }
* ```
*/
export const isOneOf = <T extends readonly unknown[]>(value: unknown, values: T): value is T[number] => {
return values.some((item) => item === value)
}

View File

@@ -0,0 +1,359 @@
---
name: svg-sprites-ru
description: "Используй только при настройке, изменении или диагностике @gromlab/svg-sprites. Триггеры: @gromlab/svg-sprites, svg-sprite.config.json, defineSpriteConfig, generateSprite, standalone@server, source: remote, ServerSvgInput, exact modes для standalone, React, Next.js, Vue, Nuxt, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit или Alpine.js, SpriteConfig.input, --input, SpriteViewer и --icon-color-N. НЕ используй для самописных SVG-спрайтов, inline SVG, favicon, растровых изображений, icon fonts или выбора библиотеки иконок."
---
<!-- Generated from skills/svg-sprites/src/ru/SKILL.md. Do not edit manually. -->
# @gromlab/svg-sprites
## Что делает пакет
`@gromlab/svg-sprites` — CLI-генератор SVG-спрайтов для пользовательских SVG-файлов. Пакет не содержит собственного набора иконок: он собирает SVG проекта во внешний sprite asset и создаёт типизированный нативный компонент для выбранного exact framework/bundler mode.
Пакет рассчитан на несколько независимых спрайтов в одном проекте. Каждый явно выбранный config-файл или config-less каталог описывает один спрайт и получает собственные:
- SVG asset;
- mode-specific manifest data;
- для всех modes, кроме bare `standalone`, — типы имён и production entry `.svg-sprite/index.js`;
- для framework modes — изолированный нативный компонент и declarations;
- для `standalone@vite`/`standalone@webpack` — нативный Web Component с явной функцией регистрации;
- для bare `standalone` — deployment-neutral JSON manifest без публичного URL.
- для `standalone@server` — content-addressed server release с двумя compile profiles и integrity manifest.
Количество и расположение каталогов определяет проект. Например, `name: 'file-manager'` создаёт `FileManagerIcon`, `FileManagerIconName` и `fileManagerIconNames`, а другой каталог с `name: 'navigation'` создаст отдельный `NavigationIcon`. Это примеры API отдельных спрайтов, а не фиксированные экспорты пакета.
Generated production runtime и declarations не импортируют `@gromlab/svg-sprites`. Генерация через `npx --yes @gromlab/svg-sprites <path-to-config>` не добавляет package в проект. Устанавливай его как development dependency только для Viewer, package-типов config или программного API.
Любой consumer exact mode может использовать `source: 'remote'` с одним local path
или HTTP(S) URL manifest, созданного `standalone@server`. До запуска adapter генератор
скачивает и проверяет нужный profile, после чего создаётся обычный локальный API и
asset; в runtime браузер не зависит от server manifest.
## Выбор режима
Выбери ровно один поддерживаемый mode key:
| Проект | Mode key |
|---|---|
| Static HTML / собственная публикация | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server или CI release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
| Vue + Webpack | `vue@webpack` |
| Nuxt + Vite | `nuxt@vite` |
| Nuxt + Webpack | `nuxt@webpack` |
| Svelte + Vite | `svelte@vite` |
| Svelte + Webpack | `svelte@webpack` |
| SvelteKit + Vite | `sveltekit@vite` |
| Angular application builder | `angular@application` |
| Angular + Webpack | `angular@webpack` |
| Astro + Vite | `astro@vite` |
| Solid + Vite | `solid@vite` |
| Solid + Webpack | `solid@webpack` |
| SolidStart + Vite | `solid-start@vite` |
| Preact + Vite | `preact@vite` |
| Preact + Webpack | `preact@webpack` |
| Qwik + Vite | `qwik@vite` |
| Lit + Vite | `lit@vite` |
| Lit + Webpack | `lit@webpack` |
| Alpine.js + Vite | `alpine@vite` |
| Alpine.js + Webpack | `alpine@webpack` |
| Next.js App Router + Turbopack | `next@app/turbopack` |
| Next.js App Router + Webpack 5 | `next@app/webpack` |
| Next.js Pages Router + Turbopack | `next@pages/turbopack` |
| Next.js Pages Router + Webpack 5 | `next@pages/webpack` |
Mode задаётся в config, CLI или программном API. Порядок применения: `defaults → config → CLI/API overrides`. После объединения mode обязателен.
`name` необязателен. Если он не задан, генератор преобразует имя каталога sprite-модуля в kebab-case; для каталогов `svg-sprite` и `svg-sprites` используется имя родительского каталога. Явное `name` должно уже быть записано в kebab-case и начинаться с латинской буквы.
CLI принимает ровно один путь. Путь к файлу `.ts`, `.js` или `.json` загружает именно этот конфиг независимо от имени. Путь к каталогу включает config-less генерацию, и настройки передаются флагами CLI.
```json
{
"scripts": {
"sprite:<name>": "npx --yes @gromlab/svg-sprites <path-to-config>",
"sprite:<name>:cli": "npx --yes @gromlab/svg-sprites --mode <mode-key> <sprite-directory>"
}
}
```
Генерация через `npx` не добавляет package в проект. Не придумывай сокращённые или generic mode keys и не используй удалённый `legacy`: выбери один полный key из таблицы. Bare `standalone` выбирай только когда приложение само публикует SVG, а `standalone@server` — только для централизованного release, используемого во время генерации consumers. Для нескольких спрайтов создай отдельную команду для каждого config-файла или каталога.
## Инспекция проекта
До изменений установи фактический контракт проекта:
1. Прочитай корневой `package.json`, lock-файл и workspace-конфигурацию; определи framework, bundler и существующие команды.
2. Найди config-файлы, команды `svg-sprites` и импорты generated-компонентов. Имя конфига произвольное; ориентируйся на переданный CLI путь и поля объекта.
3. Определи framework, router при его наличии и фактический bundler по scripts и конфигу. Для Next.js отдельно определи App/Pages Router и сборщик реальных `dev`/`build` команд.
4. Проверь существующие `predev`, `prebuild`, `pretypecheck` и агрегирующие scripts. Не перезаписывай их.
5. Для нового спрайта выбери целевой каталог, не навязывая конкретный слой или архитектуру приложения.
6. Проверь TypeScript и alias-настройки. Для package subpath exports нужен TypeScript 5+ с `moduleResolution: 'bundler'`, `'node16'` или `'nodenext'`.
Для обычного local consumer все input-пути считаются относительно каталога, содержащего явно переданный config-файл; в config-less режиме — относительно переданного каталога. Проверяй local `input` по этому контракту:
- `input?: string | string[]` по умолчанию равен `./icons`;
- каждая строка задаёт папку, точный SVG-файл или glob;
- папка сканируется плоско; вложенные файлы включаются только явным recursive glob, например `./icons/**/*.svg`;
- массив объединяет positive-источники, а элемент с префиксом `!` исключает свои совпадения из общего набора;
- каждый positive-источник должен разрешаться хотя бы в один SVG, поэтому отсутствующая или пустая папка, glob без совпадений, отсутствующий файл или точный путь не к SVG являются ошибкой;
- разрешённые файлы дедуплицируются и детерминированно сортируются;
- разные файлы с одинаковым basename конфликтуют, даже если получены из разных источников.
До применения этих правил выбери нужную ветку:
- `standalone@server` может объединять local strings и HTTP(S) descriptors `{ name, url, sha256? }`; `name` задаёт публичное имя иконки, а необязательный `sha256` проверяет скачанные байты;
- `source: 'remote'` требует ровно одну строку с local path или HTTP(S) URL manifest и не принимает source globs или descriptors;
- remote consumer config содержит только `mode`, `source` и `input`; name, description, transforms и generated notice приходят из проверенного server manifest.
Не копируй общий SVG в несколько папок: добавь его точный путь или подходящий glob в `input` каждого нужного спрайта. Используй `**/*.svg` только для намеренного рекурсивного включения.
## Настройка интеграции
Не воспроизводи настройку mode по памяти. После инспекции проекта выбери один exact mode и открой соответствующий файл из `references/docs/ru/guides/`. Используй guide как базовый рабочий контракт, затем адаптируй его к существующей структуре проекта.
Работай в таком порядке:
1. Определи каталог исходных SVG и каталог одного sprite-модуля. Один config создаёт один независимый спрайт; для нескольких наборов нужны отдельные config-файлы и уникальные `name`.
2. Сверь framework, router и bundler с exact mode. Для Next.js проверяй реальные `dev` и `build` scripts, а не только наличие `next.config.*`.
3. Предпочитай JSON-конфиг, если проекту не нужны package-типы config. TypeScript-конфиг также загружается через CLI, но установка package нужна, когда он импортирует `defineSpriteConfig` или типы.
4. Разрешай все `input` относительно каталога config-файла. Не меняй структуру SVG без необходимости: используй путь к папке, точный файл, glob или массив этих источников.
5. Добавь sprite-команду с явным путём к config. Сохрани существующие `dev`, `build`, `typecheck` и lifecycle hooks; встрой генерацию до первого процесса, импортирующего `.svg-sprite`.
6. Не запускай одну генерацию дважды через одновременный `predev` и `npm run sprites && ...`. Для нескольких спрайтов создай отдельные команды и один агрегирующий script.
7. Если приложение импортирует каталог sprite-модуля, создай пользовательский `index.ts` рядом с `.svg-sprite`; не помещай пользовательские файлы внутрь generated-каталога.
8. Выполни первую генерацию до typecheck или запуска приложения, затем проверь mode-specific output и фактический импорт компонента.
Для централизованного release открой `references/docs/ru/guides/standalone-server.md`.
Генерируй и публикуй весь каталог `.svg-sprite` атомарно. В каждом consumer сохрани
его собственный exact framework mode, укажи `source: 'remote'` и направь `input` на
manifest. Не копируй server files во framework output и не загружай manifest из
runtime приложения.
Не добавляй Viewer автоматически. Подключай его только по запросу пользователя или когда нужна визуальная проверка набора, цветов либо сложных SVG. Способ изоляции Viewer от production бери из exact guide: frameworks, bundlers и routers используют разные границы.
Не копируй snippets между exact modes даже при похожем API. Различаются asset URL, generated-файлы, CSS handling, router boundary и способ подключения debug-инструментов.
## Контракт generated-каталога
Например, после генерации React/Next-каталог имеет следующий вид:
```text
svg-sprite/
├── icons/ # пользовательские исходники
├── svg-sprite.config.json # рекомендуемое имя конфига
├── index.ts # необязательный пользовательский barrel
├── .gitignore # управляет генератор
└── .svg-sprite/
├── index.js
├── index.d.ts
├── icon-data.js
├── icon-data.d.ts
├── sprite.svg
├── svg-sprite.manifest.js
├── svg-sprite.manifest.d.ts
└── react/
├── react-component.js
├── react-component.d.ts
└── react-component.module.css
```
Standalone не создаёт `react/`. Bare `standalone` генерирует `sprite.svg` и `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` дополнительно генерируют `index.*`, `icon-data.*` и resolved manifest. Их `index.*` также содержит нативный generated Web Component; bare `standalone` не получает JS runtime и не создаёт `.gitignore`.
`standalone@server` генерирует `sprite.<content-hash>.svg`,
`sprite-root-viewbox.<content-hash>.svg` и `svg-sprite.manifest.json`. У него нет
consumer facade, browser runtime, Viewer entry или `.gitignore`. Manifest хранит
relative URL обоих profiles, полные SHA-256, размеры в байтах, metadata иконок и
настройки transforms.
Редактируй исходные SVG, config-файл и пользовательский `index.ts`. Не изменяй вручную содержимое `.svg-sprite`: повторная генерация его перезапишет. Во всех modes, кроме bare `standalone`, generated `.gitignore` также находится под управлением генератора. Для импорта из корня sprite-модуля создай barrel:
```ts
export * from './.svg-sprite/index.js'
```
Генератор полностью владеет каталогом `.svg-sprite` и заменяет его при каждом запуске. Никогда не помещай туда пользовательские файлы. Генератор также владеет `.gitignore`, когда выбранный mode его создаёт. Bare `standalone` сохраняет пользовательский `.gitignore`, но удаляет управляемый `.gitignore`, оставшийся после другого mode. Generated-пути не должны содержать symlink.
Каждый exact-mode adapter владеет facade, framework-каталогом, runtime нативного компонента, declarations, manifest source, styles и asset URL. React/Next используют `react/`; остальные framework modes используют собственный generated-контракт из соответствующего guide. Standalone bundler modes экспортируют Web Component helpers и типы, а bare `standalone` не создаёт facade. Manifest declarations объявляют типы локально и не импортируют generator package.
В bundler modes спрайт остаётся отдельным asset, а SVG path-данные не встраиваются в JavaScript. Content hash зависит от настроек сборщика. Bare `standalone` создаёт файл с фиксированным именем, а приложение само определяет его публичное имя и версионирование:
- Vite-based adapters используют mode-owned static asset import, сохраняющий sprite внешним;
- `standalone@vite` использует тот же Vite asset-механизм и экспортирует href helper и нативный Web Component без React;
- `standalone@webpack` использует Webpack Asset Modules и экспортирует такой же mode-local Web Component без React;
- Webpack-based adapters и все Next modes используют adapter-owned механизм внешнего asset, обычно `new URL(..., import.meta.url).href`;
- кастомный Webpack SVG loader не должен перехватывать generated `sprite.svg`;
- в Next mode generated-компонент не содержит `'use client'` и работает в Server Components, SSR и SSG; не добавляй клиентскую границу только ради иконки;
- команда сборки Next и mode key должны совпадать: Turbopack с `.../turbopack`, Webpack с `.../webpack`.
- remote consumers всё равно публикуются через локальный asset pipeline своего adapter; не сохраняй и не собирай URL server profile в generated application code.
Для bundler modes не перемещай generated sprite в `public` и не переписывай URL вручную. Для bare `standalone` не перемещай managed original: приложение может явно копировать его в deploy output и само отвечает за публичный URL и очистку копии. При смене mode перегенерируй спрайт с новым полным key.
## Использование, доступность и цвета
Имя компонента зависит от `name` конкретного спрайта. В `standalone@vite` и `standalone@webpack` значение `name: 'file-manager'` создаёт tag `<file-manager-icon>` и функцию `defineFileManagerIconElement()`:
```ts
import { defineFileManagerIconElement } from './svg-sprite'
defineFileManagerIconElement()
```
```html
<file-manager-icon icon="folder" aria-hidden="true"></file-manager-icon>
```
Нативный элемент не имеет runtime-зависимостей, сам выбирает generated ID и `viewBox`, получает URL через bundler и рендерит `<svg><use>` в Shadow DOM. Его property `icon` типизирован точным union имён, но строковые HTML attributes проверяются только в runtime. Размер по умолчанию равен `1em × 1em`; меняй его через CSS на host. Bare `standalone` Web Component не генерирует.
В component modes тот же `name: 'file-manager'` создаёт нативный компонент `FileManagerIcon`; его синтаксис и props определяет exact-mode guide. В React/Next.js значение `name: 'navigation'` создаёт `NavigationIcon`.
Импортируй компонент из корня соответствующего каталога спрайта. `width` и `height` не обязательны: размером можно управлять обычным CSS-классом.
```tsx
import { FileManagerIcon } from './svg-sprite'
export const OpenButton = () => (
<button type="button">
<FileManagerIcon icon="folder" className="icon" aria-hidden="true" />
<span>Открыть</span>
</button>
)
```
```css
.icon {
width: 24px;
height: 24px;
color: #4b5563;
}
```
`icon` принимает точные имена исходных файлов без `.svg`; неизвестное имя является ошибкой TypeScript. Для небезопасных SVG ID имён генератор хранит публичное имя, но создаёт внутренний стабильный hash ID, поэтому не собирай fragment URL из имени вручную.
По умолчанию компонент рендерит `<svg>` и принимает стандартные SVG attributes: необязательные `width`/`height`, `className`, `style`, `role`, `aria-*` и обработчики. С `wrapped={true}` корнем становится `<span>`, props относятся к span, а внутренний SVG занимает размер wrapper.
Generated-компонент не выбирает семантику за приложение и не добавляет `title`. Для декоративной иконки передай `aria-hidden="true"`; для самостоятельной смысловой иконки передай `role="img"` и доступное имя через `aria-label`. Не дублируй имя, если соседний текст уже озвучивает действие. Интерактивность размещай на `button` или `a`, а не на самой иконке.
Трансформации `removeSize`, `replaceColors` и `addTransition` включены по умолчанию. Для монохромной иконки единственный цвет получает fallback `currentColor`, поэтому управляй CSS-свойством `color`. Для многоцветной передавай типизированные custom properties:
```tsx
<FileManagerIcon
icon="folder"
style={{
'--icon-color-1': '#4b5563',
'--icon-color-2': '#14b8a6',
}}
/>
```
Автозамена рассчитана на `fill`/`stroke` attributes и inline `style`. Значения `none`, `transparent`, `inherit`, `unset`, `initial` не заменяются. CSS-классы и внешние stylesheets, gradients, patterns, filters и `url(#...)` проверяй на реальном результате. Переменные страницы работают через `<svg><use>`, но не проникают во внешний документ при `<img>` или `background-image`; CSS mask оставляет только одноцветный силуэт.
`SpriteViewer` необязателен. Установи `@gromlab/svg-sprites` как development dependency, только если проекту нужен Viewer. Он принимает manifests или статически обнаружимые loaders, показывает поиск, темы, цвета и примеры, но production-компоненты от него не зависят.
Перед подключением Viewer открой exact guide. Frameworks, bundlers и routers требуют разных debug entries или client boundaries. Не переноси способ подключения между modes.
## Проверка результата
После изменения конфига или SVG выполни обязательные проверки:
1. Запусти точную sprite-команду. Процесс должен завершиться с кодом `0` и сообщить имя, число иконок, mode и каталог `.svg-sprite`.
2. Проверь output выбранного exact mode:
- bare `standalone` создаёт `sprite.svg` и `svg-sprite.manifest.json`;
- `standalone@server` создаёт два content-addressed SVG profiles и server manifest, hashes и relative paths которого соответствуют этим файлам;
- `standalone@vite` и `standalone@webpack` дополнительно создают `index.*`, `icon-data.*` и JS manifest, но не каталог `react/`;
- framework modes также создают adapter-owned runtime нативного компонента, declaration и styles.
3. Для modes с public facade проверь `.svg-sprite/index.js`, соседний `index.d.ts`, список имён и фактический импорт через пользовательский barrel.
4. Проверь manifest: mode и target должны соответствовать выбранному adapter, а список иконок — исходным SVG. В bundler modes URL должен формироваться mode-specific способом; bare JSON manifest намеренно не содержит публичного `spriteUrl`.
5. Запусти существующий typecheck проекта, если mode создаёт типы или изменился пользовательский TypeScript-код.
6. Запусти минимальную команду приложения, затронутую изменением: `dev`, build или специализированную проверку проекта.
Не запускай полную production-сборку только ради проверки нового имени иконки. Она нужна, если менялся bundler target, router, Webpack loader, asset URL, deployment path или диагностируется production-only ошибка.
Визуальную проверку, Network и accessibility tree выполняй только при наличии запущенного приложения и браузерных инструментов. Если таких инструментов нет, не утверждай, что цвета, темы, доступность или HTTP-ответ asset проверены; явно укажи непроверенную часть.
Viewer используй для сложных цветов, transforms и массовой визуальной проверки. Не добавляй debug route ради обычной генерации одного спрайта.
## Диагностика
Сопоставь симптом с проверкой и исправляй первопричину:
| Симптом | Вероятная причина | Действие |
|---|---|---|
| `Missing sprite config file or module directory` | Не передан позиционный путь | Передай один config-файл либо каталог для config-less запуска. |
| `Expected one config file or module directory` | Передано несколько путей | Создай отдельную команду на каждый спрайт и объедини scripts. |
| `Sprite mode is required` | Mode отсутствует и в config, и в CLI | Добавь `mode` в объект или передай полный `--mode`. |
| `Unsupported sprite config extension` | Передан файл не `.ts`, `.js` или `.json` | Используй поддерживаемый формат config-файла. |
| Positive input-источник не нашёл SVG | Папка отсутствует или пуста, glob не совпал либо точный путь отсутствует или ведёт не к SVG | Разреши источник от каталога конфига и исправь `input`; каждый positive-элемент должен дать хотя бы один SVG. |
| Иконки из подпапки не появились | От папки ожидалось рекурсивное сканирование | Используй явный glob, например `./icons/**/*.svg`; папки сканируются плоско. |
| Исключённая иконка всё ещё присутствует | У исключения нет префикса `!`, оно находится не в массиве `input` или считается не от того каталога | Добавь совпадающий `!`-элемент и считай его от каталога конфига. |
| CLI выбрал не все источники | Несколько источников поместили в одно значение `--input` или пропустили option | Повтори `--input <path-or-glob>` отдельно для каждого источника или исключения. |
| Конфликт имени иконки или SVG ID | Два разных файла имеют одинаковый basename либо hash-ID столкнулся с именем | Переименуй один исходный SVG; не выбирай файл неявно. |
| `Refusing to overwrite a user file` | В корне sprite-модуля уже есть пользовательский `.gitignore`, который mode должен создать | Не перезаписывай файл: выбери другой sprite-каталог или согласуй перенос существующего `.gitignore`. |
| Нет `.svg-sprite/index.js` или имя отсутствует в autocomplete | Для bare `standalone` это ожидаемо; в остальных modes генерация не запускалась, barrel неверен либо type server держит старый модуль | Сверь exact mode, запусти sprite-команду, проверь `export * from './.svg-sprite/index.js'`, затем typecheck; при необходимости перезапусти TypeScript server. |
| SVG не загружается или URL неверен | Mode не совпадает со сборщиком, неверен Webpack `publicPath` либо кастомный loader перехватил asset | Сверь mode и build-команду, проверь Asset Modules/`publicPath`, исключи generated SVG из несовместимого loader. |
| Next build расходится между SSR и браузером | Модуль сгенерирован для другого bundler/router или URL переписан вручную | Верни generated `new URL(...)`, выбери точный Next mode и перегенерируй. |
| `color` не меняет многоцветную иконку | У иконки несколько переменных или она показана через `<img>`/CSS background | Используй `<FileManagerIcon>`/`<svg><use>` и нужные `--icon-color-N`. |
| Gradient/filter выглядит неверно | Автозамена цветов не гарантирует сложные paint servers | Изучи generated SVG; при необходимости отключи `replaceColors` для спрайта или упрости источник. |
| Viewer пуст | Manifest не создан, loader не обнаружен сборщиком или неверна Client Component boundary | Сначала сгенерируй спрайт, затем сверь manifest import и способ подключения с exact guide; в App Router оставь `'use client'` только в компоненте Viewer. |
| Remote manifest отклонён | Это не schema `standalone@server`, profile path небезопасен или metadata противоречивы | Опубликуй неизменённый полный server release и направь `input` на его JSON manifest. |
| Не прошла integrity-проверка remote sprite | SVG устарел, обрезан или изменён отдельно от manifest | Атомарно переопубликуй manifest и оба content-addressed profiles; никогда не перезаписывай hashed SVG другими байтами. |
При неизвестной ошибке зафиксируй полную CLI-команду, mode, путь к config-файлу или каталогу и первый stack/error message. Затем минимально воспроизведи проблему на одном спрайте, не удаляя пользовательские файлы и управляемый `.gitignore`.
## Карта reference-документации
References являются частью собранного skill. Открывай только документы, относящиеся к текущей задаче, но перед изменением интеграции exact-mode guide обязателен.
### Обзор
- [README пакета](./references/README_RU.md) — возможности, основной React/Next.js пример, все поддерживаемые families и ссылки на документацию.
### Конфигурация
- [Конфигурация](./references/docs/ru/configuration.md) — JSON, JavaScript, TypeScript, поля config, `input` и запуск CLI.
### Exact-mode guides
- [`standalone`](./references/docs/ru/guides/standalone.md) — static HTML и собственная публикация SVG.
- [`standalone@vite`](./references/docs/ru/guides/standalone-vite.md) — vanilla-приложение с Vite и Web Component.
- [`standalone@webpack`](./references/docs/ru/guides/standalone-webpack.md) — vanilla-приложение с Webpack 5 и Web Component.
- [`standalone@server`](./references/docs/ru/guides/standalone-server.md) — централизованный content-addressed release и remote consumers.
- [`react@vite`](./references/docs/ru/guides/react-vite.md) — React с Vite.
- [`react@webpack`](./references/docs/ru/guides/react-webpack.md) — React с Webpack 5.
- [`vue@vite`](./references/docs/ru/guides/vue-vite.md) — Vue с Vite.
- [`vue@webpack`](./references/docs/ru/guides/vue-webpack.md) — Vue с Webpack.
- [`nuxt@vite`](./references/docs/ru/guides/nuxt-vite.md) — Nuxt с Vite.
- [`nuxt@webpack`](./references/docs/ru/guides/nuxt-webpack.md) — Nuxt с Webpack.
- [`svelte@vite`](./references/docs/ru/guides/svelte-vite.md) — Svelte с Vite.
- [`svelte@webpack`](./references/docs/ru/guides/svelte-webpack.md) — Svelte с Webpack.
- [`sveltekit@vite`](./references/docs/ru/guides/sveltekit-vite.md) — SvelteKit с Vite.
- [`angular@application`](./references/docs/ru/guides/angular-application.md) — Angular application builder.
- [`angular@webpack`](./references/docs/ru/guides/angular-webpack.md) — Angular с Webpack.
- [`astro@vite`](./references/docs/ru/guides/astro-vite.md) — Astro с Vite.
- [`solid@vite`](./references/docs/ru/guides/solid-vite.md) — Solid с Vite.
- [`solid@webpack`](./references/docs/ru/guides/solid-webpack.md) — Solid с Webpack.
- [`solid-start@vite`](./references/docs/ru/guides/solid-start-vite.md) — SolidStart с Vite.
- [`preact@vite`](./references/docs/ru/guides/preact-vite.md) — Preact с Vite.
- [`preact@webpack`](./references/docs/ru/guides/preact-webpack.md) — Preact с Webpack.
- [`qwik@vite`](./references/docs/ru/guides/qwik-vite.md) — Qwik с Vite.
- [`lit@vite`](./references/docs/ru/guides/lit-vite.md) — Lit с Vite.
- [`lit@webpack`](./references/docs/ru/guides/lit-webpack.md) — Lit с Webpack.
- [`alpine@vite`](./references/docs/ru/guides/alpine-vite.md) — Alpine.js с Vite.
- [`alpine@webpack`](./references/docs/ru/guides/alpine-webpack.md) — Alpine.js с Webpack.
- [`next@app/turbopack`](./references/docs/ru/guides/next-app-turbopack.md) — Next.js App Router с Turbopack.
- [`next@app/webpack`](./references/docs/ru/guides/next-app-webpack.md) — Next.js App Router с Webpack.
- [`next@pages/turbopack`](./references/docs/ru/guides/next-pages-turbopack.md) — Next.js Pages Router с Turbopack.
- [`next@pages/webpack`](./references/docs/ru/guides/next-pages-webpack.md) — Next.js Pages Router с Webpack.
### Технические справочники
- [Технический справочник](./references/docs/ru/reference/technical.md) — requirements, CLI, naming, generated API, assets, transforms, цвета, Viewer, Git, CI и диагностика.
- [Программный API](./references/docs/ru/reference/programmatic-api.md) — `generateSprite`, overrides, config API, compiler и Viewer runtime.
### Agent-specific reference
- [Сложные SVG](./references/complex-svg.md) — gradients, patterns, filters, masks, `url(#...)`, `viewBox`, fragment IDs и визуальная диагностика.

View File

@@ -0,0 +1,295 @@
# @gromlab/svg-sprites
[🇬🇧 English](https://github.com/gromlab-ru/svg-sprites/blob/master/README.md) | 🇷🇺 Русский
![npm](https://img.shields.io/npm/v/@gromlab/svg-sprites) ![license](https://img.shields.io/npm/l/@gromlab/svg-sprites)
`@gromlab/svg-sprites` — CLI-инструмент для генерации SVG-спрайтов в современных веб-приложениях. Он собирает выбранные SVG-иконки в один или несколько внешних кешируемых спрайтов и подготавливает их для использования в интерфейсе.
Каждый exact mode создаёт нативный типизированный компонент для своего framework и bundler: Web Component, React, Vue, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit или Alpine.js. SVG во всех случаях остаётся отдельным кешируемым asset.
## SVG-спрайт так же прост, как обычная SVG-иконка
Для всего спрайта генерируется один типизированный React-компонент. Выберите иконку через `icon`, а редактор покажет автокомплит всех доступных имён.
```tsx
<AppIcon icon="search" width={24} height={24} />
```
Компонент принимает привычные SVG-атрибуты: размеры, `color`, `className`, `style`, `aria-*` и обработчики событий. Если нужен внешний контейнер, добавьте `wrapped`.
```tsx
<AppIcon icon="search" wrapped className="iconWrapper" />
```
В приложении не приходится работать со спрайтом напрямую. Вы используете его так же, как обычную SVG-иконку, но получаете один компонент, автокомплит и TypeScript-проверку всех имён.
## AI-friendly из коробки
`@gromlab/svg-sprites` сразу рассчитан на работу с AI-агентами. Подключите готовый skill и поручите агенту настройку, миграцию или диагностику без длинных инструкций и ручного изучения документации.
[🇷🇺 Скачать AI skill (на русском)](https://github.com/gromlab-ru/svg-sprites/releases/latest/download/svg-sprites-ru.zip)
[🇬🇧 Скачать AI skill (на английском)](https://github.com/gromlab-ru/svg-sprites/releases/latest/download/svg-sprites.zip)
## От SVG до компонента за три шага
Основной пример использует Next.js App Router и Turbopack.
### 1. Укажите нужные иконки
Создайте папки для исходных иконок и спрайта:
```text
assets/
├── app-icons/
│ └── svg-sprite.config.json
└── svg-icons/
├── search.svg
└── settings.svg
```
Создайте конфигурацию спрайта:
```json
{
"mode": "next@app/turbopack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
`input` поддерживает пути к папкам, отдельным SVG и glob-шаблоны.
### 2. Добавьте генерацию
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"prebuild": "npm run sprites"
}
}
```
Создайте точку входа для сгенерированного API:
```ts
// assets/app-icons/index.ts
export * from './.svg-sprite/index.js'
```
Первый запуск:
```bash
npm run sprites
```
Пакет создаст `AppIcon`, TypeScript-типы и отдельный SVG-спрайт.
### 3. Используйте как обычную иконку
```tsx
// app/page.tsx
import { AppIcon } from '../assets/app-icons'
export default function SearchButton() {
return (
<button type="button">
<AppIcon icon="search" width={20} height={20} />
Найти
</button>
)
}
```
Это Server Component. Для иконки не нужны provider, `'use client'` или ручная сборка URL.
## Типизированный React-компонент с автокомплитом
Каждый спрайт получает собственный готовый компонент. Свойство `icon` формируется из реальных имён SVG, поэтому редактор показывает точный список доступных иконок, а TypeScript сразу обнаруживает опечатки.
```tsx
<AppIcon icon="search" /> // доступная иконка
<AppIcon icon="serach" /> // ошибка TypeScript
```
После добавления новой SVG-иконки и повторной генерации её имя автоматически появляется в типах и автокомплите. Не нужно вручную поддерживать компоненты, union-типы или реестр имён.
## Next.js App Router и SSR из коробки
Generated-компоненты работают в Server Components, SSR и SSG без `'use client'`.
Подключение иконки не переносит страницу на клиент, не требует provider и не создаёт дополнительную границу гидратации.
Один и тот же компонент можно использовать в `page.tsx`, `layout.tsx`, серверных и клиентских компонентах.
## Множественные спрайты вместо одного глобального
Проект не ограничен одним набором иконок. Создавайте независимые спрайты для общих элементов, отдельных страниц и крупных UI-модулей.
```tsx
<AppIcon icon="search" />
<AnalyticsIcon icon="chart" />
<EditorIcon icon="bold" />
```
Каждый набор получает собственный типизированный компонент и SVG asset, поэтому разделы приложения не несут иконки, которые им не нужны.
## Каждая иконка хранится в одном экземпляре
В библиотеке исходников каждая SVG-иконка хранится в одном экземпляре и может входить в любое количество спрайтов. Общие иконки не приходится копировать между страницами и модулями: они обновляются для всех наборов из одного места.
```text
search.svg ─┬─→ AppIcon
├─→ AnalyticsIcon
└─→ EditorIcon
```
Спрайты разделяются ради производительности, но библиотека исходных иконок остаётся единой.
## Браузерное кеширование
При стандартной конфигурации Vite, Webpack или Next.js каждый спрайт выпускается отдельным версионированным SVG-файлом.
Пока набор иконок не меняется, браузер может использовать сохранённую копию независимо от обновлений JavaScript приложения.
Изменение React-компонентов не требует повторно загружать геометрию всех иконок.
## JavaScript без SVG-балласта
Контуры иконок остаются во внешних SVG assets и не увеличивают chunks приложения.
```text
React-код → JavaScript chunks
SVG-иконки → отдельные SVG assets
```
JavaScript отвечает за интерфейс и поведение, а графика загружается и кешируется отдельно.
## Трансформации SVG из коробки
Во время генерации пакет автоматически подготавливает исходные SVG для интерфейса:
- удаляет фиксированные `width` и `height`;
- сохраняет существующий `viewBox`;
- преобразует `fill` и `stroke` в CSS-переменные;
- добавляет плавные transitions непосредственно в цветные элементы иконки.
Каждую трансформацию можно настроить или отключить независимо.
## Каждый цвет под контролем CSS
При генерации цвета `fill` и `stroke` автоматически преобразуются в CSS-переменные `--icon-color-N`.
Монохромная иконка наследует `currentColor`:
```tsx
<AppIcon icon="search" color="rebeccapurple" />
```
В многоцветной иконке каждый цвет можно менять отдельно:
```tsx
<AppIcon
icon="user"
style={{
'--icon-color-1': '#2563eb',
'--icon-color-2': '#dbeafe',
}}
/>
```
Темы, состояния и hover-эффекты создаются без редактирования SVG и дополнительных копий иконки.
## SpriteViewer: все спрайты на одной debug-странице
`SpriteViewer` рендерит спрайты всех поддерживаемых exact modes в одном месте. Один Web Component отвечает за визуал, а для React также доступен тонкий bridge к нему.
Для каждой иконки видны созданные CSS-переменные и их fallback-цвета. Значения можно менять прямо в Viewer и сразу наблюдать результат.
Здесь же доступны готовые примеры для framework из manifest, `<svg><use>`, `<img>` и CSS.
![SpriteViewer](https://raw.githubusercontent.com/gromlab-ru/svg-sprites/master/preview-image.png)
Viewer подключается только к внутренней debug-странице и не становится частью generated-компонентов иконок.
Bare standalone подключает Viewer через browser script и HTML element. Bundler и framework modes используют npm entry Web Component; React и Next.js также могут импортировать bridge из `@gromlab/svg-sprites/react`.
## 30 exact modes
Пакет поддерживает 30 изолированных exact modes: `standalone@server` для серверной генерации универсального SVG-спрайта и 29 consumer modes для современных frameworks и bundlers.
`standalone@server` позволяет заранее сгенерировать SVG-спрайт на сервере или в CI/CD и опубликовать его для совместного использования. Такой спрайт не привязан к конкретному framework или bundler и подходит всем consumer modes.
29 consumer modes охватывают standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit и Alpine.js в поддерживаемых вариантах Vite, Webpack, Turbopack и application builder.
Все 29 consumer modes могут работать как со спрайтами, сгенерированными локально в проекте, так и с универсальными спрайтами, заранее сгенерированными на сервере через `standalone@server`. API компонентов и способ использования иконок в приложении в обоих сценариях остаются одинаковыми.
Интеграционная матрица охватывает все 30 exact modes. Отдельный producer-стенд проверяет серверную генерацию универсального спрайта, а каждое из 29 consumer-приложений генерирует и рендерит два независимых спрайта: локальный и удалённый.
Все consumer-приложения проходят production build и Playwright-тесты, а типизированные modes дополнительно проверяются штатным toolchain фреймворка. Каждый E2E-тест подтверждает, что локальный и удалённый спрайты загружаются и отрисовываются, а также проверяет отсутствие browser errors и отображение обеих групп в SpriteViewer.
## Чистый Git
Bundler и framework modes создают локальный `.gitignore`, который исключает generated-файлы и не позволяет им засорять историю, pull requests и код проекта. Bare `standalone` оставляет политику репозитория приложению.
В bundler и framework modes в репозитории остаются исходные SVG, конфигурация и правило `.gitignore`, а локально и в CI спрайты, компоненты и типы заново создаются через `prebuild`.
## В production только иконки
Генерация полностью работает через `npx`, без добавления package в проект. Устанавливайте его как development dependency, только если нужны Viewer, типы конфига или программный API.
Production-компоненты используют только локальный generated-код, стили и внешний SVG-файл. Compiler и CLI не попадают в клиентское приложение, а `SpriteViewer` подключается отдельно только там, где нужна debug-страница.
## Документация
README знакомит с возможностями проекта и показывает основной сценарий использования. Для настройки выберите руководство под свой стек.
### Серверная генерация
- [Standalone + Server](docs/ru/guides/standalone-server.md)
### Быстрый старт для consumer modes
- [Bare standalone](docs/ru/guides/standalone.md)
- [Standalone + Vite](docs/ru/guides/standalone-vite.md)
- [Standalone + Webpack 5](docs/ru/guides/standalone-webpack.md)
- [React + Vite](docs/ru/guides/react-vite.md)
- [React + Webpack 5](docs/ru/guides/react-webpack.md)
- [Vue + Vite](docs/ru/guides/vue-vite.md)
- [Vue + Webpack](docs/ru/guides/vue-webpack.md)
- [Nuxt + Vite](docs/ru/guides/nuxt-vite.md)
- [Nuxt + Webpack](docs/ru/guides/nuxt-webpack.md)
- [Svelte + Vite](docs/ru/guides/svelte-vite.md)
- [Svelte + Webpack](docs/ru/guides/svelte-webpack.md)
- [SvelteKit + Vite](docs/ru/guides/sveltekit-vite.md)
- [Angular application builder](docs/ru/guides/angular-application.md)
- [Angular + Webpack](docs/ru/guides/angular-webpack.md)
- [Astro + Vite](docs/ru/guides/astro-vite.md)
- [Solid + Vite](docs/ru/guides/solid-vite.md)
- [Solid + Webpack](docs/ru/guides/solid-webpack.md)
- [SolidStart + Vite](docs/ru/guides/solid-start-vite.md)
- [Preact + Vite](docs/ru/guides/preact-vite.md)
- [Preact + Webpack](docs/ru/guides/preact-webpack.md)
- [Qwik + Vite](docs/ru/guides/qwik-vite.md)
- [Lit + Vite](docs/ru/guides/lit-vite.md)
- [Lit + Webpack](docs/ru/guides/lit-webpack.md)
- [Alpine.js + Vite](docs/ru/guides/alpine-vite.md)
- [Alpine.js + Webpack](docs/ru/guides/alpine-webpack.md)
- [Next.js App Router + Turbopack](docs/ru/guides/next-app-turbopack.md)
- [Next.js App Router + Webpack](docs/ru/guides/next-app-webpack.md)
- [Next.js Pages Router + Turbopack](docs/ru/guides/next-pages-turbopack.md)
- [Next.js Pages Router + Webpack](docs/ru/guides/next-pages-webpack.md)
### Технические материалы
- [Индекс документации](docs/ru/README.md)
- [Конфигурация](docs/ru/configuration.md)
- [Технический справочник](docs/ru/reference/technical.md)
- [Программный API](docs/ru/reference/programmatic-api.md)
## Лицензия
MIT

View File

@@ -0,0 +1,176 @@
# Сложные SVG: диагностика и безопасная генерация
## Когда открывать
Открывай этот документ, если исходник содержит `<defs>`, gradients, patterns, filters, masks, clip paths, внутренние `<style>`/classes, `url(#id)`, CSS variables, `<use>`, text, нестандартный `viewBox`, пробелы в имени файла или визуально меняется после генерации. Также открывай его при жалобах на цвет, размер, обрезание или конфликт fragment ID.
## Сначала классифицируй риск
Проверь исходный SVG до редактирования:
```bash
npm run sprite:file-manager
```
Используй фактический package script нужного спрайта. Затем сравни source с `.svg-sprite/sprite.svg` и manifest, не делая вывод только по успешному exit code.
Особого внимания требуют:
- `fill="url(#gradient)"`, `stroke="url(#pattern)"`;
- `filter="url(#shadow)"`, `mask="url(#mask)"`, `clip-path="url(#clip)"`;
- CSS rules внутри `<style>` и внешние stylesheets;
- цвета через classes, presentation attributes и inline `style` одновременно;
- `currentColor`, уже существующие `var(...)`, `context-fill` и `context-stroke`;
- повторяющиеся IDs в `<defs>` разных файлов;
- SVG без `viewBox` или с width/height, не соответствующими viewBox;
- embedded images, fonts, scripts или external references.
## Фактический pipeline
Компилятор сначала применяет SVGO `preset-default`, сохраняя `viewBox`, затем custom transforms в таком порядке:
1. `removeSize` удаляет `width` и `height` с корневого `<svg>`.
2. `replaceColors` собирает значения `fill` и `stroke` из attributes и inline `style`, затем заменяет их на `var(--icon-color-N, fallback)`.
3. `addTransition` добавляет inline transition цветным `path`, `circle`, `ellipse`, `rect`, `line`, `polyline`, `polygon`, `text`, `tspan` и `use`.
Все три опции по умолчанию `true` и применяются ко всему спрайту, не к отдельной иконке.
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'react@vite',
name: 'illustrations',
transform: {
removeSize: false,
replaceColors: false,
addTransition: false,
},
})
```
Это config для одного из потенциально многих sprite-модулей; его каталог не обязан совпадать с module/feature-каталогом. Для Next укажи соответствующий полный `mode` с тем же `transform`.
## Размеры и viewBox
`removeSize: true` удаляет intrinsic `width`/`height`, но не создаёт отсутствующий `viewBox`. Если source не имеет корректного `viewBox`, generated icon может получить неверное масштабирование или нулевую область просмотра.
Правильная подготовка source:
```svg
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="..." />
</svg>
```
Если физические размеры являются частью контракта иллюстрации, установи `removeSize: false` и проверь поведение component props. Не используй сохранение width/height как замену отсутствующему viewBox.
React compile оставляет root sprite `rootViewBox` выключенным; Next включает его. У каждой shape всё равно должен быть собственный корректный viewBox, который попадает в manifest и используется Viewer.
## Цвета
Для одного обнаруженного цвета fallback становится `currentColor`:
```svg
stroke="var(--icon-color-1, currentColor)"
```
Для нескольких цветов сохраняются исходные fallbacks:
```svg
fill="var(--icon-color-1, #798198)"
fill="var(--icon-color-2, #ffffff)"
```
Значения `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются. Сравнение цветов нормализует регистр и пробелы, но не приводит эквивалентные формы (`#fff`, `#ffffff`, `rgb(...)`) к одному цвету.
Автоматический анализ надёжен прежде всего для `fill`/`stroke` attributes и inline `style`. Он не разбирает CSS selectors во внутреннем `<style>` и внешний stylesheet как полноценный CSS AST.
Для `url(#...)`, уже вложенных `var(...)`, gradients и patterns автоматическая замена требует проверки generated output. Если ссылка на paint server изменилась или Viewer неверно показывает controls, отключи `replaceColors` для всего этого спрайта:
```ts
transform: {
replaceColors: false,
}
```
Если рядом нужны обычные recolorable icons, вынеси сложные иллюстрации в отдельный sprite с отдельным config. Это предпочтительнее ручной правки generated SVG.
`addTransition` независим от `replaceColors`. При сохранении исходных цветов transition всё равно может добавиться. Для filters, анимаций или собственного CSS отключай обе опции, если inline transition меняет поведение.
## Defs, references и IDs
После SVGO и сборки проверь, что каждая ссылка `url(#id)` или `<use href="#id">` указывает на реально существующий ID внутри соответствующей shape. Не предполагай, что IDs останутся буквальной копией source: optimizer/compiler может их изменить.
Проверяй как минимум:
- gradient/pattern применяется к нужному path;
- filter region не обрезает blur/shadow;
- mask и clipPath сохраняют coordinate system (`userSpaceOnUse`/`objectBoundingBox`);
- internal `<use>` не спутан с внешним fragment спрайта;
- одинаковые IDs из разных source SVG не создают cross-icon collision в итоговом документе;
- external file/URL references допустимы в production CSP и deployment.
Если IDs конфликтуют, сначала сделай source IDs уникальными и обнови все ссылки внутри SVG. Не правь compiled sprite.
## Имена файлов и внешний fragment
`FileManagerIcon` в примерах ниже — только пример generated-имени для отдельного config с `name: 'file-manager'`; это не фиксированное имя API.
Безопасный basename соответствует:
```text
^[a-zA-Z][a-zA-Z0-9_-]*$
```
Он сохраняется как fragment ID. Остальные имена, например `folder open.svg` или `24-check.svg`, остаются публичными значениями TypeScript `icon`, но получают стабильный ID `icon-<16 hex>`.
```tsx
<FileManagerIcon icon="folder open" />
```
Не создавай вручную `#folder open`. Используй generated component либо `.svg-sprite/svg-sprite.manifest.js`, где записаны `name` и фактический `id`.
Разные файлы с одинаковым basename запрещены даже из разных directories. Переименуй один source осмысленно; порядок или пересечение источников не выбирают победителя.
## Способ отображения
Для управления `color` и `--icon-color-N` используй generated React-компонент или `<svg><use>`:
```tsx
<FileManagerIcon
icon="diagram"
style={{
'--icon-color-1': '#334155',
'--icon-color-2': '#38bdf8',
}}
/>
```
Generated style type допускает `--icon-color-${number}`. `<img>` и CSS `background-image` загружают SVG как изолированный document, поэтому variables страницы внутрь не передаются. CSS mask оставляет только силуэт и теряет gradients, filters и различия цветов.
External stack fragment support и поведение paint servers могут различаться между browsers. Для критичной сложной графики при диагностике runtime и наличии browser-инструментов проверь целевые browsers; при несовместимости SVG sprite может быть неподходящим способом доставки именно этой иллюстрации.
## Обязательная проверка
1. Запусти генерацию с правильным mode.
2. Запусти typecheck проекта.
3. Открой generated sprite и найди shape по ID из manifest.
4. Статически сверь `viewBox`, IDs, `url(#...)`, colors и inline styles.
5. Если менялись target/pipeline или диагностируется runtime, собери production bundle и проверь внешний hashed SVG.
6. При наличии SpriteViewer и визуальных инструментов проверь default colors и каждую `--icon-color-N` отдельно.
7. При наличии browser-инструментов и соответствующем runtime-риске проверь SSR/hydration для Next.js и целевые browsers для external fragments.
8. Не утверждай визуальную или a11y эквивалентность source и результата без доступных инструментов и фактического сравнения.
## Типовые симптомы и действия
- Иконка стала полностью `currentColor`: pipeline увидел один цвет. Если исходная семантика сложнее, отключи `replaceColors` или нормализуй source attributes.
- Gradient исчез: проверь, не преобразован ли `fill="url(#...)"`, существует ли target ID и не конфликтует ли он с другим icon.
- Shadow обрезан: проверь filter region и viewBox; `removeSize` сам по себе не расширяет область.
- Цветовые controls Viewer отсутствуют: цвет задан через class/stylesheet либо `replaceColors: false`; это ожидаемо.
- Transition дублируется или мешает animation: существующий inline `transition` не перезаписывается, но generated CSS также добавляет transitions; отключи `addTransition` для sprite.
- `<img>` игнорирует variables: смени rendering на `<svg><use>`/generated component, не пытайся передать page variables в изолированный SVG.
- Ручной fragment не работает для имени с пробелом: используй ID из manifest.
- Один сложный icon требует иных transforms: вынеси его в отдельный sprite; per-icon transform config отсутствует.
Для mode-specific запуска и проверки вернись к exact-mode guide, выбранному в основном `SKILL.md`.

View File

@@ -0,0 +1,56 @@
# Документация
Для настройки выберите guide одного exact mode. Каждый guide является
самостоятельным документом и без изменений используется в AI skills.
Общий формат JSON, JavaScript и TypeScript config-файлов описан в [руководстве по конфигурации](configuration.md).
## Быстрый старт для consumer modes
| Проект | Exact mode | Guide |
|---|---|---|
| Static HTML или собственная публикация | `standalone` | [Bare standalone](guides/standalone.md) |
| Vanilla + Vite | `standalone@vite` | [Standalone + Vite](guides/standalone-vite.md) |
| Vanilla + Webpack 5 | `standalone@webpack` | [Standalone + Webpack](guides/standalone-webpack.md) |
| React + Vite | `react@vite` | [React + Vite](guides/react-vite.md) |
| React + Webpack 5 | `react@webpack` | [React + Webpack](guides/react-webpack.md) |
| Vue + Vite | `vue@vite` | [Vue + Vite](guides/vue-vite.md) |
| Vue + Webpack | `vue@webpack` | [Vue + Webpack](guides/vue-webpack.md) |
| Nuxt + Vite | `nuxt@vite` | [Nuxt + Vite](guides/nuxt-vite.md) |
| Nuxt + Webpack | `nuxt@webpack` | [Nuxt + Webpack](guides/nuxt-webpack.md) |
| Svelte + Vite | `svelte@vite` | [Svelte + Vite](guides/svelte-vite.md) |
| Svelte + Webpack | `svelte@webpack` | [Svelte + Webpack](guides/svelte-webpack.md) |
| SvelteKit + Vite | `sveltekit@vite` | [SvelteKit + Vite](guides/sveltekit-vite.md) |
| Angular application builder | `angular@application` | [Angular application builder](guides/angular-application.md) |
| Angular + Webpack | `angular@webpack` | [Angular + Webpack](guides/angular-webpack.md) |
| Astro + Vite | `astro@vite` | [Astro + Vite](guides/astro-vite.md) |
| Solid + Vite | `solid@vite` | [Solid + Vite](guides/solid-vite.md) |
| Solid + Webpack | `solid@webpack` | [Solid + Webpack](guides/solid-webpack.md) |
| SolidStart + Vite | `solid-start@vite` | [SolidStart + Vite](guides/solid-start-vite.md) |
| Preact + Vite | `preact@vite` | [Preact + Vite](guides/preact-vite.md) |
| Preact + Webpack | `preact@webpack` | [Preact + Webpack](guides/preact-webpack.md) |
| Qwik + Vite | `qwik@vite` | [Qwik + Vite](guides/qwik-vite.md) |
| Lit + Vite | `lit@vite` | [Lit + Vite](guides/lit-vite.md) |
| Lit + Webpack | `lit@webpack` | [Lit + Webpack](guides/lit-webpack.md) |
| Alpine.js + Vite | `alpine@vite` | [Alpine.js + Vite](guides/alpine-vite.md) |
| Alpine.js + Webpack | `alpine@webpack` | [Alpine.js + Webpack](guides/alpine-webpack.md) |
| Next.js App Router + Turbopack | `next@app/turbopack` | [App Router + Turbopack](guides/next-app-turbopack.md) |
| Next.js App Router + Webpack | `next@app/webpack` | [App Router + Webpack](guides/next-app-webpack.md) |
| Next.js Pages Router + Turbopack | `next@pages/turbopack` | [Pages Router + Turbopack](guides/next-pages-turbopack.md) |
| Next.js Pages Router + Webpack | `next@pages/webpack` | [Pages Router + Webpack](guides/next-pages-webpack.md) |
Все consumer guides используют один порядок:
1. Генерация спрайта через `npx` без добавления package в проект.
2. Использование спрайта в приложении.
3. Необязательное подключение Viewer для дебага и превью.
## Серверная генерация
Используйте [`standalone@server`](guides/standalone-server.md), чтобы сгенерировать на сервере или в CI/CD универсальный SVG-спрайт для всех consumer modes.
## Справочники
- [Конфигурация](configuration.md)
- [Технический справочник](reference/technical.md)
- [Программный API](reference/programmatic-api.md)

View File

@@ -0,0 +1,139 @@
# Конфигурация
Каждый config-файл описывает один независимый спрайт. CLI не ищет конфиг автоматически, поэтому всегда передавайте путь явно:
```bash
npx --yes @gromlab/svg-sprites path/to/svg-sprite.config.json
```
## JSON
JSON подходит для большинства проектов и не требует локальной установки пакета:
```json
{
"mode": "next@app/turbopack",
"name": "app",
"description": "Общие иконки приложения",
"input": [
"./icons",
"../../assets/icons/**/*.svg",
"!../../assets/icons/deprecated-*.svg"
],
"transform": {
"removeSize": true,
"replaceColors": true,
"addTransition": true
},
"generatedNotice": true
}
```
| Поле | По умолчанию | Назначение |
|---|---|---|
| `mode` | Нет | Exact mode, соответствующий framework и сборщику |
| `source` | `local` | `local` для исходных SVG или `remote` для manifest от `standalone@server` |
| `name` | Kebab-case имени каталога модуля; для `svg-sprite` и `svg-sprites` — имени родительского каталога | Имя спрайта; в modes с компонентом также задаёт имя компонента и типов |
| `description` | Нет | Описание для типов и Viewer |
| `input` | `./icons` | Каталог, SVG-файл, glob-шаблон или массив источников |
| `transform` | Все включены | Настройки подготовки SVG |
| `generatedNotice` | `true` | Вид предупреждения в generated-файлах |
Пути и glob-шаблоны в `input` считаются от каталога config-файла. Паттерн с префиксом `!` исключает совпадения.
## Удалённо собранный спрайт
Consumer config для server manifest содержит только mode, source и input:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://assets.example/releases/app/svg-sprite.manifest.json"
}
```
`input` принимает один HTTP(S) URL или локальный путь к manifest. Имя, описание,
transforms и generated notice берутся из manifest. Генератор скачивает и проверяет
подходящий SVG profile, после чего adapter создаёт обычные локальные компоненты,
типы и asset для сборщика.
## Серверная сборка
`standalone@server` объединяет local paths/globs и HTTP(S) SVG descriptors:
```js
export default {
mode: 'standalone@server',
name: 'app',
input: [
'./icons/**/*.svg',
{
name: 'remote-logo',
url: 'https://assets.example/logo.svg',
},
],
}
```
Mode создаёт два content-addressed SVG profiles и `svg-sprite.manifest.json`.
`sha256` у HTTP input необязателен; если он указан, это должен быть ожидаемый
64-символьный hexadecimal SHA-256 digest, по которому сборка проверит полученные байты.
## JavaScript
JavaScript-конфиг экспортирует обычный объект по умолчанию:
```js
export default {
mode: 'react@vite',
name: 'icons',
input: './icons',
}
```
Передайте CLI путь к `.js`-файлу так же, как к JSON:
```bash
npx --yes @gromlab/svg-sprites path/to/svg-sprite.config.js
```
## TypeScript
Для проверки конфига TypeScript установите пакет как dev dependency:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Используйте `defineSpriteConfig`:
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'react@vite',
name: 'icons',
input: './icons',
})
```
Или примените `satisfies` с type-only импортом:
```ts
import type { SpriteConfig } from '@gromlab/svg-sprites'
export default {
mode: 'react@vite',
name: 'icons',
input: './icons',
} satisfies SpriteConfig
```
CLI загружает `.ts`-конфиг напрямую:
```bash
npx --yes @gromlab/svg-sprites path/to/svg-sprite.config.ts
```
Полный список modes, CLI-флагов, правил именования и transform-опций находится в [техническом справочнике](reference/technical.md).

View File

@@ -0,0 +1,91 @@
# SVG-спрайт для Alpine.js на Vite
Инструкция по быстрому созданию SVG-спрайта в Alpine.js-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "alpine@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Генератор не нужно добавлять в зависимости приложения: запускайте его через `npx`.
Добавьте генерацию перед запуском разработки и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Alpine plugin `appAlpinePlugin`, директиву `x-app-icon` и magic `$appIconHref`.
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Зарегистрируйте generated plugin до запуска Alpine:
```js
import Alpine from 'alpinejs'
import { appAlpinePlugin } from '../assets/app-icons/index.js'
Alpine.plugin(appAlpinePlugin)
Alpine.start()
```
Используйте реактивную директиву на SVG-элементе:
```html
<svg
x-data="{ iconName: 'icon-name' }"
x-app-icon="iconName"
role="img"
aria-label="Готово"
style="width:24px;height:24px;color:#334155;--icon-color-2:#f59e0b"
></svg>
```
Выражение директивы возвращает имя исходного SVG без расширения. Монохромная иконка наследует `color`, а слои многоцветной иконки переопределяются через `--icon-color-N`. Vite подключает generated CSS и выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице и нужен только при разработке. Установите его отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer на development-страницу:
```html
<gromlab-sprite-viewer viewer-title="Иконки проекта"></gromlab-sprite-viewer>
<script type="module" src="/src/svg-sprite-debug.js"></script>
```
Создайте `src/svg-sprite-debug.js`:
```js
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
```
Запустите `npm run dev` и откройте development-страницу. Viewer не зависит от Alpine plugin.

View File

@@ -0,0 +1,103 @@
# SVG-спрайт для Alpine.js на Webpack 5
Инструкция по быстрому созданию SVG-спрайта в Alpine.js-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "alpine@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Генератор не нужно добавлять в зависимости приложения: запускайте его через `npx`.
Добавьте генерацию перед запуском разработки и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Alpine plugin `appAlpinePlugin`, директиву `x-app-icon` и magic `$appIconHref`.
Generated CSS Alpine импортируется с query `?inline`. Добавьте правило Asset Modules в `webpack.config.js`:
```js
export default {
module: {
rules: [
{
test: /\.css$/,
resourceQuery: /inline/,
type: 'asset/source',
},
],
},
}
```
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Зарегистрируйте generated plugin до запуска Alpine:
```js
import Alpine from 'alpinejs'
import { appAlpinePlugin } from '../assets/app-icons/index.js'
Alpine.plugin(appAlpinePlugin)
Alpine.start()
```
Используйте реактивную директиву на SVG-элементе:
```html
<svg
x-data="{ iconName: 'icon-name' }"
x-app-icon="iconName"
role="img"
aria-label="Готово"
style="width:24px;height:24px;color:#334155;--icon-color-2:#f59e0b"
></svg>
```
Выражение директивы возвращает имя исходного SVG без расширения. Монохромная иконка наследует `color`, а слои многоцветной иконки переопределяются через `--icon-color-N`. Webpack 5 выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице и нужен только при разработке. Установите его отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer в development entry:
```js
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.createElement('gromlab-sprite-viewer')
viewer.viewerTitle = 'Иконки проекта'
viewer.sources = [spriteManifest]
document.body.append(viewer)
```
Подключайте этот entry только при разработке. Viewer не зависит от Alpine plugin.

View File

@@ -0,0 +1,94 @@
# SVG-спрайт для Angular с Application Builder
Инструкция по быстрому созданию SVG-спрайта в Angular-приложении на `@angular/build:application`.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "angular@application",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Запускайте генерацию через `npx` перед разработкой и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"prestart": "npm run sprites",
"start": "ng serve",
"prebuild": "npm run sprites",
"build": "ng build"
}
}
```
Application Builder выпускает импортированный SVG отдельным файлом при включённом file loader. Добавьте опцию в build target файла `angular.json`:
```json
{
"builder": "@angular/build:application",
"options": {
"loader": { ".svg": "file" }
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index'
```
Импортируйте сгенерированный standalone-компонент. Значение `name: "app"` создаёт `AppIcon` с селектором `app-icon`:
```ts
import { Component } from '@angular/core'
import { AppIcon } from '../assets/app-icons'
@Component({
selector: 'app-root',
standalone: true,
imports: [AppIcon],
template: `
<app-icon
icon="icon-name"
role="img"
aria-label="Готово"
style="width: 24px; height: 24px; color: #334155; --icon-color-2: #f59e0b"
/>
`,
})
export class AppComponent {}
```
Input `icon` типизирован именами исходных файлов. Монохромные иконки наследуют `color`, отдельные цвета задаются через `--icon-color-N`.
## Дебаг и превью
Viewer необязателен и нужен только при разработке:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Импортируйте `@gromlab/svg-sprites/viewer/element`, добавьте `CUSTOM_ELEMENTS_SCHEMA` и поместите `<gromlab-sprite-viewer [sources]="viewerSources" />` в шаблон. Загрузите generated manifest без framework-specific metadata:
```ts
readonly viewerSources = [async () => {
const { default: manifest } = await import(
'../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
)
const { usage: _usage, ...viewerManifest } = manifest
return viewerManifest
}]
```
Viewer использует тот же production URL спрайта, что и `AppIcon`.

View File

@@ -0,0 +1,93 @@
# SVG-спрайт для Angular на Webpack
Инструкция по быстрому созданию SVG-спрайта в Angular-приложении со штатным Webpack browser builder из Angular CLI.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "angular@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Mode предназначен для workspace, где build target использует официальный Webpack builder:
```json
{
"builder": "@angular-devkit/build-angular:browser"
}
```
Пакет не нужно добавлять в зависимости проекта. Генерируйте спрайт через `npx` перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"prestart": "npm run sprites",
"start": "ng serve",
"prebuild": "npm run sprites",
"build": "ng build"
}
}
```
Webpack разрешает generated-выражение `new URL(..., import.meta.url)` и выпускает `sprite.svg` как production asset.
## Использование спрайта
Создайте `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index'
```
Импортируйте сгенерированный standalone-компонент. Значение `name: "app"` создаёт `AppIcon` с селектором `app-icon`:
```ts
import { Component } from '@angular/core'
import { AppIcon } from '../assets/app-icons'
@Component({
selector: 'app-root',
standalone: true,
imports: [AppIcon],
template: `
<app-icon
icon="icon-name"
role="img"
aria-label="Готово"
style="width: 24px; height: 24px; color: #334155; --icon-color-2: #f59e0b"
/>
`,
})
export class AppComponent {}
```
Input `icon` типизирован именами исходных файлов. Монохромные иконки наследуют `color`, отдельные цвета задаются через `--icon-color-N`.
## Дебаг и превью
Viewer необязателен и нужен только при разработке:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Импортируйте `@gromlab/svg-sprites/viewer/element`, добавьте `CUSTOM_ELEMENTS_SCHEMA` и поместите `<gromlab-sprite-viewer [sources]="viewerSources" />` в шаблон:
```ts
readonly viewerSources = [async () => {
const { default: manifest } = await import(
'../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
)
const { usage: _usage, ...viewerManifest } = manifest
return viewerManifest
}]
```
Viewer и `AppIcon` используют один выпущенный Webpack URL спрайта.

View File

@@ -0,0 +1,92 @@
# SVG-спрайт для Astro на Vite
Инструкция по быстрому созданию SVG-спрайта в Astro-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "astro@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Генерируйте спрайт через `npx` перед разработкой и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "astro dev",
"prebuild": "npm run sprites",
"build": "astro check && astro build"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Создайте `assets/app-icons/index.d.ts` для того же типизированного API:
```ts
export * from './.svg-sprite/index.js'
```
Значение `name: "app"` создаёт нативный Astro-компонент `AppIcon`. Используйте его на странице:
```astro
---
import { AppIcon } from '../../assets/app-icons/index.js'
---
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
```
Prop `icon` типизирован именами исходных файлов. Vite выпускает `sprite.svg` из статического asset import компонента.
## Дебаг и превью
Viewer необязателен и нужен только при разработке:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer на страницу и подключите generated manifest в клиентском скрипте:
```astro
<gromlab-sprite-viewer id="sprite-viewer"></gromlab-sprite-viewer>
<script>
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
const viewer = document.querySelector<SpriteViewerElement>('#sprite-viewer')!
viewer.sources = [async () => {
const { default: manifest } = await import(
'../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
)
const { usage: _usage, ...viewerManifest } = manifest
return viewerManifest
}]
</script>
```
Manifest сохраняет Astro usage metadata, а Viewer отображает тот же production-спрайт.

View File

@@ -0,0 +1,86 @@
# SVG-спрайт для Lit на Vite
Инструкция по быстрому созданию SVG-спрайта в Lit-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "lit@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Генератор не нужно добавлять в зависимости приложения: запускайте его через `npx`.
Добавьте генерацию перед запуском разработки и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Lit-класс `AppIcon`, тег `<app-icon>` и функцию регистрации `defineAppIcon`.
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Зарегистрируйте компонент перед его отображением:
```js
import { defineAppIcon } from '../assets/app-icons/index.js'
defineAppIcon()
document.querySelector('#app').innerHTML = `
<app-icon
icon="icon-name"
role="img"
aria-label="Готово"
style="width:24px;height:24px;color:#334155;--icon-color-2:#f59e0b"
></app-icon>
`
```
Свойство `icon` принимает имя исходного SVG без расширения. Монохромная иконка наследует `color`, а слои многоцветной иконки переопределяются через `--icon-color-N`. Vite подключает CSS компонента и выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице и нужен только при разработке. Установите его отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer на development-страницу:
```html
<gromlab-sprite-viewer viewer-title="Иконки проекта"></gromlab-sprite-viewer>
<script type="module" src="/src/svg-sprite-debug.js"></script>
```
Создайте `src/svg-sprite-debug.js`:
```js
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
```
Запустите `npm run dev` и откройте development-страницу. Viewer не требуется для работы `AppIcon`.

View File

@@ -0,0 +1,98 @@
# SVG-спрайт для Lit на Webpack 5
Инструкция по быстрому созданию SVG-спрайта в Lit-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "lit@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Генератор не нужно добавлять в зависимости приложения: запускайте его через `npx`.
Добавьте генерацию перед запуском разработки и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Lit-класс `AppIcon`, тег `<app-icon>` и функцию регистрации `defineAppIcon`.
Generated CSS Lit импортируется с query `?inline`. Добавьте правило Asset Modules в `webpack.config.js`:
```js
export default {
module: {
rules: [
{
test: /\.css$/,
resourceQuery: /inline/,
type: 'asset/source',
},
],
},
}
```
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Зарегистрируйте компонент перед его отображением:
```js
import { defineAppIcon } from '../assets/app-icons/index.js'
defineAppIcon()
document.querySelector('#app').innerHTML = `
<app-icon
icon="icon-name"
role="img"
aria-label="Готово"
style="width:24px;height:24px;color:#334155;--icon-color-2:#f59e0b"
></app-icon>
`
```
Свойство `icon` принимает имя исходного SVG без расширения. Монохромная иконка наследует `color`, а слои многоцветной иконки переопределяются через `--icon-color-N`. Webpack 5 выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице и нужен только при разработке. Установите его отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer в development entry:
```js
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.createElement('gromlab-sprite-viewer')
viewer.viewerTitle = 'Иконки проекта'
viewer.sources = [spriteManifest]
document.body.append(viewer)
```
Подключайте этот entry только при разработке. Viewer не требуется для работы `AppIcon`.

View File

@@ -0,0 +1,108 @@
# SVG-спрайт для Next.js App Router с Turbopack
Инструкция по быстрому созданию SVG-спрайта в приложении Next.js с App Router и Turbopack.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "next@app/turbopack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "next dev --turbopack",
"prebuild": "npm run sprites",
"build": "next build --turbopack"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент в Server Component:
```tsx
// app/page.tsx
import { AppIcon } from '../assets/app-icons'
export default function Page() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Для `AppIcon` не нужен `'use client'`. Turbopack сам добавляет `sprite.svg` в итоговую сборку, поэтому переносить его в `public` не нужно.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте Client Component `app/svg-sprite/SvgSpriteViewer.tsx`:
```tsx
'use client'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
export function SvgSpriteViewer() {
return <SpriteViewer sources={sources} title="Иконки проекта" />
}
```
Создайте маршрут `app/svg-sprite/page.tsx`:
```tsx
import { notFound } from 'next/navigation'
import { SvgSpriteViewer } from './SvgSpriteViewer'
export default function SvgSpritePage() {
if (process.env.NODE_ENV !== 'development') notFound()
return <SvgSpriteViewer />
}
```
Запустите `npm run dev` и откройте `/svg-sprite`. В production маршрут вернёт 404.

View File

@@ -0,0 +1,108 @@
# SVG-спрайт для Next.js App Router с Webpack
Инструкция по быстрому созданию SVG-спрайта в приложении Next.js с App Router и Webpack.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "next@app/webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "next dev --webpack",
"prebuild": "npm run sprites",
"build": "next build --webpack"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент в Server Component:
```tsx
// app/page.tsx
import { AppIcon } from '../assets/app-icons'
export default function Page() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Для `AppIcon` не нужен `'use client'`. Next.js сам добавляет `sprite.svg` в итоговую сборку, поэтому переносить его в `public` не нужно.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте Client Component `app/svg-sprite/SvgSpriteViewer.tsx`:
```tsx
'use client'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
export function SvgSpriteViewer() {
return <SpriteViewer sources={sources} title="Иконки проекта" />
}
```
Создайте маршрут `app/svg-sprite/page.tsx`:
```tsx
import { notFound } from 'next/navigation'
import { SvgSpriteViewer } from './SvgSpriteViewer'
export default function SvgSpritePage() {
if (process.env.NODE_ENV !== 'development') notFound()
return <SvgSpriteViewer />
}
```
Запустите `npm run dev` и откройте `/svg-sprite`. В production маршрут вернёт 404.

View File

@@ -0,0 +1,98 @@
# SVG-спрайт для Next.js Pages Router с Turbopack
Инструкция по быстрому созданию SVG-спрайта в приложении Next.js с Pages Router и Turbopack.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "next@pages/turbopack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "next dev --turbopack",
"prebuild": "npm run sprites",
"build": "next build --turbopack"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент на странице:
```tsx
// pages/index.tsx
import { AppIcon } from '../assets/app-icons'
export default function Page() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Компонент работает с SSR, SSG и клиентскими переходами. Turbopack сам добавляет `sprite.svg` в итоговую сборку, поэтому переносить его в `public` не нужно.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте страницу `pages/svg-sprite.tsx`:
```tsx
import type { GetStaticProps } from 'next'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
export default function SvgSpritePage() {
return <SpriteViewer sources={sources} title="Иконки проекта" />
}
export const getStaticProps: GetStaticProps = () =>
process.env.NODE_ENV === 'development'
? { props: {} }
: { notFound: true }
```
Запустите `npm run dev` и откройте `/svg-sprite`. В production маршрут вернёт 404.

View File

@@ -0,0 +1,98 @@
# SVG-спрайт для Next.js Pages Router с Webpack
Инструкция по быстрому созданию SVG-спрайта в приложении Next.js с Pages Router и Webpack.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "next@pages/webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "next dev --webpack",
"prebuild": "npm run sprites",
"build": "next build --webpack"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент на странице:
```tsx
// pages/index.tsx
import { AppIcon } from '../assets/app-icons'
export default function Page() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Компонент работает с SSR, SSG и клиентскими переходами. Next.js сам добавляет `sprite.svg` в итоговую сборку, поэтому переносить его в `public` не нужно.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте страницу `pages/svg-sprite.tsx`:
```tsx
import type { GetStaticProps } from 'next'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
export default function SvgSpritePage() {
return <SpriteViewer sources={sources} title="Иконки проекта" />
}
export const getStaticProps: GetStaticProps = () =>
process.env.NODE_ENV === 'development'
? { props: {} }
: { notFound: true }
```
Запустите `npm run dev` и откройте `/svg-sprite`. В production маршрут вернёт 404.

View File

@@ -0,0 +1,100 @@
# SVG-спрайт для Nuxt на Vite
Инструкция по быстрому созданию SVG-спрайта в Nuxt-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "nuxt@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "nuxt dev",
"prebuild": "npm run sprites",
"build": "nuxt build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Vue-компонент `AppIcon`. Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент на странице или в layout Nuxt:
```vue
<script setup>
import { AppIcon } from '../assets/app-icons/index.js'
</script>
<template>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
</template>
```
`AppIcon` безопасен для SSR и не требует client-only обёртки. Vite выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте `app/components/SvgSpriteViewer.client.vue`, чтобы browser-only Viewer не выполнялся во время SSR:
```vue
<script setup>
import '@gromlab/svg-sprites/viewer/element'
const sources = [
() => import('../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
</script>
<template>
<gromlab-sprite-viewer :sources="sources" viewer-title="Иконки проекта" />
</template>
```
Отметьте `gromlab-sprite-viewer` как custom element в `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
vue: {
compilerOptions: {
isCustomElement: (tag) => tag === 'gromlab-sprite-viewer',
},
},
})
```
Покажите `<SvgSpriteViewer />` на странице разработки. Viewer изолирован от generated runtime компонента `AppIcon`.

View File

@@ -0,0 +1,113 @@
# SVG-спрайт для Nuxt на Webpack
Инструкция по быстрому созданию SVG-спрайта в Nuxt-приложении на Webpack.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "nuxt@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Подключите Webpack builder Nuxt в `nuxt.config.ts`:
```bash
npm install --save-dev @nuxt/webpack-builder
```
```ts
export default defineNuxtConfig({
builder: '@nuxt/webpack-builder',
})
```
Добавьте команды генерации в `package.json`:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "nuxt dev",
"prebuild": "npm run sprites",
"build": "nuxt build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Vue-компонент `AppIcon`. Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент на странице или в layout Nuxt:
```vue
<script setup>
import { AppIcon } from '../assets/app-icons/index.js'
</script>
<template>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
</template>
```
`AppIcon` безопасен для SSR и не требует client-only обёртки. Webpack выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте `app/components/SvgSpriteViewer.client.vue`, чтобы browser-only Viewer не выполнялся во время SSR:
```vue
<script setup>
import '@gromlab/svg-sprites/viewer/element'
const sources = [
() => import('../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
</script>
<template>
<gromlab-sprite-viewer :sources="sources" viewer-title="Иконки проекта" />
</template>
```
Дополните существующие настройки `nuxt.config.ts`, чтобы Vue считал Viewer custom element:
```ts
export default defineNuxtConfig({
builder: '@nuxt/webpack-builder',
vue: {
compilerOptions: {
isCustomElement: (tag) => tag === 'gromlab-sprite-viewer',
},
},
})
```
Покажите `<SvgSpriteViewer />` на странице разработки. Viewer изолирован от generated runtime компонента `AppIcon`.

View File

@@ -0,0 +1,75 @@
# SVG-спрайт для Preact на Vite
Инструкция по быстрому созданию SVG-спрайта в Preact-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "preact@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Запускайте его через `npx` перед разработкой и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && vite build"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.js` и `assets/app-icons/index.d.ts` с одинаковым экспортом:
```js
export * from './.svg-sprite/index.js'
```
Используйте сгенерированный Preact-компонент на plain JavaScript:
```jsx
import { AppIcon } from '../assets/app-icons/index.js'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
)
}
```
Vite автоматически выпускает импортированный `sprite.svg` как production asset.
## Дебаг и превью
Установите Viewer только для разработки:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Подключите его в отладочной entry:
```js
import '@gromlab/svg-sprites/viewer/element'
const viewer = document.createElement('gromlab-sprite-viewer')
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
document.body.append(viewer)
```

View File

@@ -0,0 +1,75 @@
# SVG-спрайт для Preact на Webpack
Инструкция по быстрому созданию SVG-спрайта в Preact-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "preact@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Запускайте его через `npx` перед запуском и сборкой Webpack:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && webpack --mode production"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.js` и `assets/app-icons/index.d.ts` с одинаковым экспортом:
```js
export * from './.svg-sprite/index.js'
```
Используйте сгенерированный Preact-компонент:
```jsx
import { AppIcon } from '../assets/app-icons/index.js'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
)
}
```
Webpack Asset Modules выпускают `sprite.svg` из сгенерированного выражения `new URL(...)`.
## Дебаг и превью
Установите Viewer только для разработки:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Подключите его в отдельной development entry:
```js
import '@gromlab/svg-sprites/viewer/element'
const viewer = document.createElement('gromlab-sprite-viewer')
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
document.body.append(viewer)
```

View File

@@ -0,0 +1,82 @@
# SVG-спрайт для Qwik на Vite
Инструкция по быстрому созданию SVG-спрайта в SSR-приложении Qwik на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "qwik@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Перегенерируйте спрайт через `npx` перед запуском и сборкой Vite:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite --mode ssr",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && vite build"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Сгенерированный компонент является Qwik `component$` и безопасен во время SSR:
```tsx
import { component$ } from '@builder.io/qwik'
import { AppIcon } from '../assets/app-icons'
export default component$(() => (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
))
```
Компонент использует статический Vite asset import и не обращается к browser globals во время SSR.
## Дебаг и превью
Viewer работает только в браузере и нужен лишь для разработки:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Загрузите его из visible task:
```tsx
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
export const IconViewer = component$(() => {
const host = useSignal<HTMLElement>()
useVisibleTask$(async () => {
await import('@gromlab/svg-sprites/viewer/element')
const viewer = document.createElement('gromlab-sprite-viewer') as SpriteViewerElement
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
host.value?.append(viewer)
})
return <div ref={host} />
})
```

View File

@@ -0,0 +1,115 @@
# SVG-спрайт для React на Vite
Инструкция по быстрому созданию SVG-спрайта в React-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "react@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```tsx
import { AppIcon } from '../assets/app-icons'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Свойство `icon` принимает имена исходных SVG без расширения. Монохромная иконка наследует `color`, а цвета многоцветной переопределяются через `--icon-color-N`.
Vite сам подключает стили компонента и добавляет `sprite.svg` в итоговую сборку.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте `svg-sprite.html` в корне проекта:
```html
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Иконки проекта</title>
</head>
<body>
<!-- React-корень Viewer для дебага и превью SVG-спрайта -->
<div id="svg-sprite-viewer"></div>
<!-- Подключение создаваемого ниже скрипта дебаггера -->
<script type="module" src="/src/svg-sprite-debug.tsx"></script>
</body>
</html>
```
Создайте `src/svg-sprite-debug.tsx`:
```tsx
import { createRoot } from 'react-dom/client'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
createRoot(document.getElementById('svg-sprite-viewer')!).render(
<SpriteViewer sources={sources} title="Иконки проекта" />,
)
```
Запустите `npm run dev` и откройте `/svg-sprite.html`.
Стандартная production-сборка Vite использует только `index.html` и не включает страницу Viewer.

View File

@@ -0,0 +1,132 @@
# SVG-спрайт для React на Webpack 5
Инструкция по быстрому созданию SVG-спрайта в React-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "react@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт React-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```tsx
import { AppIcon } from '../assets/app-icons'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
role="img"
aria-label="Готово"
style={{
color: '#334155',
'--icon-color-2': '#f59e0b',
}}
/>
)
}
```
Свойство `icon` принимает имена исходных SVG без расширения. Монохромная иконка наследует `color`, а цвета многоцветной переопределяются через `--icon-color-N`.
Компонент использует CSS Modules. Если проект ещё не обрабатывает их, установите loaders:
```bash
npm install --save-dev style-loader css-loader
```
Затем добавьте правило с default export в `webpack.config.js`:
```js
{
test: /\.module\.css$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: { namedExport: false } },
},
],
}
```
Webpack 5 сам добавляет `sprite.svg` в итоговую сборку.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки.
Установите Viewer:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте entry `src/svg-sprite-debug.tsx`:
```tsx
import { createRoot } from 'react-dom/client'
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(
<SpriteViewer sources={sources} title="Иконки проекта" />,
)
```
Добавьте скрипт к основному entry только в development-режиме. Сохраните остальные настройки `webpack.config.js`:
```js
export default (_env, argv) => ({
// Остальные настройки Webpack.
entry: [
'./src/main.tsx',
...(argv.mode === 'development' ? ['./src/svg-sprite-debug.tsx'] : []),
],
})
```
Запустите `npm run dev`. Viewer появится на основной странице приложения и не попадёт в production-сборку.

View File

@@ -0,0 +1,83 @@
# SVG-спрайт для SolidStart на Vite
Инструкция по быстрому созданию SVG-спрайта в SSR-приложении SolidStart на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "solid-start@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Перегенерируйте спрайт через `npx` перед запуском и сборкой Vinxi:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vinxi dev",
"prebuild": "npm run sprites",
"build": "vinxi build"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Сгенерированный компонент безопасно рендерится на сервере:
```tsx
import { AppIcon } from '../assets/app-icons'
export default function Home() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
)
}
```
Компонент использует статический Vite asset import и не обращается к browser globals во время SSR.
## Дебаг и превью
Viewer работает только в браузере и нужен лишь для разработки:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Загрузите его из `onMount`, чтобы исключить из серверного рендера:
```tsx
import { onMount } from 'solid-js'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
export function IconViewer() {
let host!: HTMLDivElement
onMount(async () => {
await import('@gromlab/svg-sprites/viewer/element')
const viewer = document.createElement('gromlab-sprite-viewer') as SpriteViewerElement
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
host.append(viewer)
})
return <div ref={host} />
}
```

View File

@@ -0,0 +1,83 @@
# SVG-спрайт для Solid на Vite
Инструкция по быстрому созданию SVG-спрайта в Solid-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "solid@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Запускайте его через `npx` перед разработкой и production-сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && vite build"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Имя `app` создаёт Solid-компонент `AppIcon`:
```tsx
import { AppIcon } from '../assets/app-icons'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
)
}
```
Vite выпускает `sprite.svg` как production asset. Монохромные иконки наследуют `color`, многоцветные используют `--icon-color-N`.
## Дебаг и превью
Viewer нужен только во время разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Подключите его в отладочном компоненте после запуска браузерного кода:
```tsx
import { onMount } from 'solid-js'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
export function IconViewer() {
let host!: HTMLDivElement
onMount(async () => {
await import('@gromlab/svg-sprites/viewer/element')
const viewer = document.createElement('gromlab-sprite-viewer') as SpriteViewerElement
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
host.append(viewer)
})
return <div ref={host} />
}
```

View File

@@ -0,0 +1,75 @@
# SVG-спрайт для Solid на Webpack
Инструкция по быстрому созданию SVG-спрайта в Solid-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "solid@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта. Запускайте его через `npx` перед запуском и сборкой Webpack:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "tsc --noEmit && webpack --mode production"
}
}
```
## Использование спрайта
Создайте `assets/app-icons/index.js` и `assets/app-icons/index.d.ts` с одинаковым экспортом:
```js
export * from './.svg-sprite/index.js'
```
Используйте сгенерированный Solid-компонент:
```jsx
import { AppIcon } from '../assets/app-icons/index.js'
export function SaveIcon() {
return (
<AppIcon
icon="icon-name"
width={24}
height={24}
aria-label="Готово"
style={{ color: '#334155', '--icon-color-2': '#f59e0b' }}
/>
)
}
```
Webpack Asset Modules выпускают `sprite.svg` из сгенерированного `new URL(...)`. Обработка `.jsx` должна охватывать сгенерированный Solid-компонент.
## Дебаг и превью
Установите Viewer только для разработки:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Подключите его в отдельной development entry:
```js
import '@gromlab/svg-sprites/viewer/element'
const viewer = document.createElement('gromlab-sprite-viewer')
viewer.sources = [() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js')]
document.body.append(viewer)
```

View File

@@ -0,0 +1,113 @@
# Универсальный SVG-спрайт на сервере
Сгенерируйте в CI или server worker универсальный SVG-спрайт, который смогут использовать приложения с разными frameworks и bundlers.
## Генерация спрайта
Устанавливать пакет в worker не нужно.
### 1. Подготовьте рабочий каталог
Поместите исходные SVG в папку `icons` текущего workspace:
```text
.
└── icons/
├── search.svg
└── settings.svg
```
Имена файлов без расширения станут именами иконок.
### 2. Запустите генерацию
Передайте mode, имя спрайта и путь к SVG через CLI:
```bash
npx --yes @gromlab/svg-sprites \
--mode standalone@server \
--name app \
--input './icons/**/*.svg' \
.
```
Config-файл для этого worker-сценария не нужен. Результат появится в `./.svg-sprite`:
```text
.
├── icons/
│ ├── search.svg
│ └── settings.svg
└── .svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
### 3. Опубликуйте результат
Загрузите содержимое `.svg-sprite` в отдельный каталог S3 bucket:
```bash
aws s3 sync ./.svg-sprite/ s3://my-bucket/app-icons/
```
Этот же каталог можно раздавать через CDN. В публичном URL нет сегмента `.svg-sprite`:
```text
https://cdn.example.com/app-icons/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
`standalone@server` также можно запускать через JSON, JavaScript или TypeScript config. Config подходит для постоянных настроек, локальных SVG из нескольких каталогов и SVG, загружаемых по HTTP(S).
## Использование спрайта
В consumer-приложении создайте обычный config. Например, для React с Vite:
```text
src/app-icons/
├── index.ts
└── svg-sprite.config.json
```
Укажите consumer mode и URL manifest из CDN:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://cdn.example.com/app-icons/svg-sprite.manifest.json"
}
```
Добавьте пользовательскую точку входа:
```ts
// src/app-icons/index.ts
export * from './.svg-sprite/index.js'
```
Запустите обычную генерацию:
```bash
npx --yes @gromlab/svg-sprites src/app-icons/svg-sprite.config.json
```
После этого используйте generated-компонент так же, как со спрайтом из локальных SVG:
```tsx
import { AppIcon } from './app-icons'
export function SearchButton() {
return <AppIcon icon="search" aria-label="Поиск" />
}
```
Тот же CDN manifest поддерживают все 29 consumer modes. В каждом из них сохраняется нативный API выбранного framework и bundler.
## Дебаг и превью
`standalone@server` не создаёт отдельную страницу для просмотра иконок. Подключите опубликованный спрайт к consumer-приложению и откройте его в SpriteViewer: удалённый набор будет отображаться так же, как локальный.

View File

@@ -0,0 +1,114 @@
# SVG-спрайт для Vite без фреймворка
Инструкция по быстрому созданию SVG-спрайта в приложении на Vite без фреймворка.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "standalone@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт элемент `<app-icon>`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Зарегистрируйте элемент в `src/main.ts`:
```ts
import { defineAppIconElement } from '../assets/app-icons'
import './style.css'
defineAppIconElement()
```
Используйте иконку в HTML:
```html
<app-icon icon="icon-name" role="img" aria-label="Готово"></app-icon>
```
Значение `icon` — имя исходного SVG без расширения. Размер и цвета настраиваются через CSS:
```css
app-icon {
font-size: 24px;
color: #334155;
--icon-color-2: #f59e0b;
}
```
Монохромная иконка наследует `color`, а цвета многоцветной иконки переопределяются через `--icon-color-N`. Нужные переменные показывает Viewer.
Vite сам добавит `sprite.svg` в итоговую сборку. Копировать его в `public` не нужно.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте `svg-sprite.html` в корне проекта:
```html
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Иконки проекта</title>
</head>
<body>
<!-- Компонент Viewer для дебага и превью SVG-спрайта -->
<gromlab-sprite-viewer viewer-title="Иконки проекта"></gromlab-sprite-viewer>
<!-- Подключение создаваемого ниже скрипта дебаггера -->
<script type="module" src="/src/svg-sprite-debug.ts"></script>
</body>
</html>
```
Создайте `src/svg-sprite-debug.ts`:
```ts
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.querySelector<SpriteViewerElement>('gromlab-sprite-viewer')!
viewer.sources = [spriteManifest]
```
Запустите `npm run dev` и откройте `/svg-sprite.html`.
Viewer не требуется для работы `<app-icon>` и не подключается к основному коду приложения.

View File

@@ -0,0 +1,111 @@
# SVG-спрайт для Webpack 5 без фреймворка
Инструкция по быстрому созданию SVG-спрайта в приложении на Webpack 5 без фреймворка.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "standalone@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт элемент `<app-icon>`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Зарегистрируйте элемент в основном entry приложения:
```ts
import { defineAppIconElement } from '../assets/app-icons'
import './style.css'
defineAppIconElement()
```
Используйте иконку в HTML:
```html
<app-icon icon="icon-name" role="img" aria-label="Готово"></app-icon>
```
Значение `icon` — имя исходного SVG без расширения. Размер и цвета настраиваются через CSS:
```css
app-icon {
font-size: 24px;
color: #334155;
--icon-color-2: #f59e0b;
}
```
Монохромная иконка наследует `color`, а цвета многоцветной иконки переопределяются через `--icon-color-N`. Нужные переменные показывает Viewer.
Webpack 5 сам добавит `sprite.svg` в итоговую сборку.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки.
Установите Viewer:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте entry `src/svg-sprite-debug.ts`:
```ts
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.createElement('gromlab-sprite-viewer') as SpriteViewerElement
viewer.viewerTitle = 'Иконки проекта'
viewer.sources = [spriteManifest]
document.body.append(viewer)
```
Добавьте скрипт к основному entry только в development-режиме. Сохраните остальные настройки `webpack.config.js`:
```js
export default (_env, argv) => ({
// Остальные настройки Webpack.
entry: [
'./src/main.ts',
...(argv.mode === 'development' ? ['./src/svg-sprite-debug.ts'] : []),
],
})
```
Запустите `npm run dev`. Viewer появится на основной странице приложения.
Viewer добавляется только в development-сборку и не попадает в production.

View File

@@ -0,0 +1,83 @@
# SVG-спрайт для сайта без сборщика
Соберите SVG-иконки в один файл и используйте их на HTML-странице.
## Генерация спрайта
Устанавливать пакет в проект не нужно.
### 1. Создайте конфиг спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
```json
{
"mode": "standalone",
"name": "icons",
"input": "../svg-icons/**/*.svg"
}
```
### 2. Сгенерируйте спрайт
Передайте команде путь к конфигу:
```bash
npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json
```
Пакет соберёт иконки в каталог `.svg-sprite` рядом с конфигом:
```text
assets/app-icons/.svg-sprite/
├── sprite.svg
└── svg-sprite.manifest.json
```
- `sprite.svg` — готовый спрайт для использования на сайте.
- `svg-sprite.manifest.json` — данные об иконках для Viewer.
Каталог `.svg-sprite` создаётся автоматически и полностью заменяется при каждой генерации. Не редактируйте его содержимое вручную.
### 3. Используйте иконку
В `index.html` укажите путь к созданному `sprite.svg`. После `#` добавьте имя нужной иконки без расширения `.svg`:
```html
<svg
width="24"
height="24"
aria-label="Готово"
>
<use href="./assets/app-icons/.svg-sprite/sprite.svg#icon-name"></use>
</svg>
```
## Дебаг и превью
`sprite.svg` — технический файл, а не галерея иконок. При его открытии нельзя удобно просмотреть весь набор. Кроме того, градиенты, маски, фильтры и ссылки на внутренние `id` могут отображаться с артефактами.
Для визуальной проверки используйте официальный Viewer. Он показывает все иконки спрайта и помогает проверить их цвета и отображение.
Viewer необязателен и предназначен только для разработки. Устанавливать пакет через npm не нужно.
Viewer работает напрямую с файлами из `.svg-sprite`. Ничего копировать не нужно.
### Добавьте Viewer на страницу
Добавьте в `index.html` module script и укажите пути к generated manifest и спрайту:
```html
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@gromlab/svg-sprites/dist/viewer-element.js"
></script>
<gromlab-sprite-viewer
viewer-title="Иконки проекта"
manifest-url="./assets/app-icons/.svg-sprite/svg-sprite.manifest.json"
sprite-url="./assets/app-icons/.svg-sprite/sprite.svg"
></gromlab-sprite-viewer>
```
Viewer можно вынести в отдельный HTML-файл в корне сайта, предназначенный только для разработки и проверки иконок.

View File

@@ -0,0 +1,95 @@
# SVG-спрайт для Svelte на Vite
Инструкция по быстрому созданию SVG-спрайта в Svelte-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "svelte@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Svelte-компонент `AppIcon`.
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```svelte
<script>
import { AppIcon } from '../assets/app-icons/index.js'
</script>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
```
Свойство `icon` принимает имена исходных SVG без расширения. Монохромная иконка наследует `color`, а цвета многоцветной переопределяются через `--icon-color-N`.
Vite сам подключает стили компонента и выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer на страницу или в компонент, используемый только при разработке:
```svelte
<script>
import '@gromlab/svg-sprites/viewer/element'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
function connectViewer(node) {
node.sources = sources
}
</script>
<gromlab-sprite-viewer
use:connectViewer
viewer-title="Иконки проекта"
></gromlab-sprite-viewer>
```
Запустите `npm run dev` и откройте страницу с Viewer. Не импортируйте этот отладочный компонент из production entry.

View File

@@ -0,0 +1,105 @@
# SVG-спрайт для Svelte на Webpack 5
Инструкция по быстрому созданию SVG-спрайта в Svelte-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "svelte@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Svelte-компонент `AppIcon`.
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```svelte
<script>
import { AppIcon } from '../assets/app-icons/index.js'
</script>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
```
Generated-компонент является нативным `.svelte`-файлом. Обычное правило `svelte-loader` должно обрабатывать `.svelte`-файлы в `assets`:
```js
{
test: /\.svelte$/,
use: {
loader: 'svelte-loader',
options: { emitCss: false },
},
}
```
Webpack 5 обрабатывает asset URL из компонента и выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer в Svelte-компонент, используемый только при разработке:
```svelte
<script>
import '@gromlab/svg-sprites/viewer/element'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
function connectViewer(node) {
node.sources = sources
}
</script>
<gromlab-sprite-viewer
use:connectViewer
viewer-title="Иконки проекта"
></gromlab-sprite-viewer>
```
Запустите `npm run dev` и откройте страницу с Viewer. Не импортируйте этот отладочный компонент из production entry.

View File

@@ -0,0 +1,91 @@
# SVG-спрайт для SvelteKit на Vite
Инструкция по быстрому созданию SVG-спрайта в SvelteKit-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "sveltekit@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite dev",
"prebuild": "npm run sprites",
"build": "vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт SSR-safe Svelte-компонент `AppIcon`.
Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент в `src/routes/+page.svelte`:
```svelte
<script>
import { AppIcon } from '../../assets/app-icons/index.js'
</script>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
```
Свойство `icon` принимает имена исходных SVG без расширения. В компоненте нет browser-only инициализации, поэтому страница может рендериться на сервере. Vite выпускает `sprite.svg` отдельным production asset.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте отладочный route, например `src/routes/svg-sprite/+page.svelte`. Загружайте custom element из action, чтобы регистрация выполнялась только в браузере:
```svelte
<script>
const sources = [
() => import('../../../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
function connectViewer(node) {
void import('@gromlab/svg-sprites/viewer/element').then(() => {
node.sources = sources
node.viewerTitle = 'Иконки проекта'
})
}
</script>
<gromlab-sprite-viewer use:connectViewer></gromlab-sprite-viewer>
```
Запустите `npm run dev` и откройте `/svg-sprite`. Action не выполняется во время SSR.

View File

@@ -0,0 +1,105 @@
# SVG-спрайт для Vue на Vite
Инструкция по быстрому созданию SVG-спрайта в Vue-приложении на Vite.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "vue@vite",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "vite",
"prebuild": "npm run sprites",
"build": "vue-tsc --noEmit && vite build"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Vue-компонент `AppIcon`.
Создайте точку входа `assets/app-icons/index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```vue
<script setup lang="ts">
import { AppIcon } from '../assets/app-icons'
</script>
<template>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
</template>
```
Свойство `icon` принимает имена исходных SVG без расширения. Монохромная иконка наследует `color`, а цвета многоцветной переопределяются через `--icon-color-N`.
Vite сам подключает стили компонента и добавляет `sprite.svg` в итоговую сборку.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Создайте `svg-sprite.html` в корне проекта:
```html
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Иконки проекта</title>
</head>
<body>
<gromlab-sprite-viewer viewer-title="Иконки проекта"></gromlab-sprite-viewer>
<script type="module" src="/src/svg-sprite-debug.ts"></script>
</body>
</html>
```
Создайте `src/svg-sprite-debug.ts`:
```ts
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
import spriteManifest from '../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.querySelector<SpriteViewerElement>('gromlab-sprite-viewer')!
viewer.sources = [spriteManifest]
```
Запустите `npm run dev` и откройте `/svg-sprite.html`.
Viewer не требуется для работы `AppIcon` и не подключается к основному коду приложения.

View File

@@ -0,0 +1,126 @@
# SVG-спрайт для Vue на Webpack 5
Инструкция по быстрому созданию SVG-спрайта в Vue-приложении на Webpack 5.
## Генерация спрайта
Выберите каталог для будущего SVG-спрайта, например `assets/app-icons`, и создайте в нём `svg-sprite.config.json`. В `input` укажите путь к существующим SVG относительно файла конфигурации. Перемещать или копировать иконки не требуется.
Пример конфига:
```json
{
"mode": "vue@webpack",
"name": "app",
"input": "../svg-icons/**/*.svg"
}
```
Пакет не нужно добавлять в зависимости проекта: генерация запускается через `npx`.
Добавьте команды генерации в `package.json`. Сгенерированные файлы по умолчанию исключены из Git, поэтому `predev` и `prebuild` пересобирают спрайт перед каждым запуском и сборкой:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json",
"predev": "npm run sprites",
"dev": "webpack serve --mode development",
"prebuild": "npm run sprites",
"build": "webpack --mode production"
}
}
```
## Использование спрайта
Значение `name: "app"` создаёт Vue-компонент `AppIcon`. Создайте `assets/app-icons/index.js`:
```js
export * from './.svg-sprite/index.js'
```
Используйте компонент в приложении:
```vue
<script setup>
import { AppIcon } from '../assets/app-icons/index.js'
</script>
<template>
<AppIcon
icon="icon-name"
width="24"
height="24"
role="img"
aria-label="Готово"
style="color: #334155; --icon-color-2: #f59e0b"
/>
</template>
```
Свойство `icon` принимает имена исходных SVG без расширения. Монохромная иконка наследует `color`, а цвета многоцветной переопределяются через `--icon-color-N`.
Компонент использует CSS Modules. Если проект ещё не обрабатывает их, установите `style-loader` и `css-loader`, затем добавьте правило с default export:
```bash
npm install --save-dev style-loader css-loader
```
```js
{
test: /\.module\.css$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: { namedExport: false } },
},
],
}
```
Webpack 5 сам добавляет `sprite.svg` в итоговую сборку.
## Дебаг и превью
Viewer показывает все иконки на одной странице, позволяет проверить их отображение, изменить цвета и посмотреть связанные CSS-переменные. Он нужен только для разработки и устанавливается отдельно:
```bash
npm install --save-dev @gromlab/svg-sprites
```
Добавьте Viewer в Vue-компонент, подключаемый только при разработке:
```vue
<script setup>
import '@gromlab/svg-sprites/viewer/element'
const sources = [
() => import('../assets/app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
</script>
<template>
<gromlab-sprite-viewer
:sources="sources"
viewer-title="Иконки проекта"
/>
</template>
```
Настройте Vue Loader так, чтобы `gromlab-sprite-viewer` считался custom element:
```js
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
isCustomElement: (tag) => tag === 'gromlab-sprite-viewer',
},
},
}
```
Покажите компонент Viewer на странице разработки. Viewer не требуется для работы `AppIcon`.

View File

@@ -0,0 +1,183 @@
# Программный API
[Индекс документации](../README.md)
Пакет распространяется как ESM и предоставляет единый Node.js API генерации. Framework-neutral Viewer находится в `@gromlab/svg-sprites/viewer`, auto-register entry — в `@gromlab/svg-sprites/viewer/element`, React bridge — в `@gromlab/svg-sprites/react`.
## `generateSprite`
```ts
import { generateSprite } from '@gromlab/svg-sprites'
const result = await generateSprite(
'src/ui/file-manager/svg-sprite/svg-sprite.config.ts',
)
```
Результат содержит имя и mode спрайта, количество иконок и абсолютные filesystem paths:
```ts
result.name
result.mode
result.target
result.iconCount
result.rootDir
result.generatedDir
result.spritePath
result.manifestPath
```
Next.js modes дополнительно возвращают `router` и `bundler`. `standalone@server`
возвращает `target: 'server'`; его `spritePath` указывает на стандартный
content-addressed profile, а `manifestPath` — на server manifest.
Для static standalone mode `result.spritePath` можно использовать в build-скрипте,
чтобы опубликовать SVG по URL приложения:
```ts
import { copyFile } from 'node:fs/promises'
const result = await generateSprite('src/sprite/svg-sprite.config.ts', {
mode: 'standalone',
})
await copyFile(result.spritePath, 'dist/app-icons/sprite.svg')
```
`spritePath` является filesystem path, а не browser URL. Deployment-neutral JSON
manifest доступен через `result.manifestPath` и копируется независимо от SVG.
Первый аргумент принимает абсолютный или относительный путь к config-файлу с любым именем и расширением `.ts`, `.js` или `.json`. Каталог вместо файла включает config-less режим: корнем sprite-модуля становится этот каталог.
Второй аргумент содержит необязательные overrides и всегда имеет приоритет над конфигом:
```ts
await generateSprite('src/ui/file-manager/svg-sprite/custom-config.json', {
mode: 'react@webpack',
name: 'documents',
input: ['./assets', '../../shared/search.svg'],
transform: {
addTransition: false,
},
generatedNotice: false,
})
```
Порядок разрешения настроек:
```text
defaults → config → API overrides
```
Для полностью программной генерации передайте каталог и все обязательные настройки через overrides:
```ts
await generateSprite('src/ui/file-manager/svg-sprite', {
mode: 'react@vite',
name: 'file-manager',
input: [
'../../shared/search.svg',
'../../shared/settings.svg',
],
})
```
## Конфигурация
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'react@vite',
name: 'file-manager',
description: 'Иконки файлового менеджера',
input: ['./icons', '../../shared/check.svg'],
transform: {
removeSize: true,
replaceColors: true,
addTransition: true,
},
generatedNotice: true,
})
```
`input` принимает одну папку, SVG-файл или glob-паттерн либо массив, объединяющий такие источники. Если поле не задано, используется `./icons`; относительные пути считаются от папки с конфигом.
`defineSpriteConfig` является identity helper для TypeScript autocomplete. JS может экспортировать тот же объект через `export default`, а JSON содержит объект непосредственно.
Публичные типы `ServerSvgInput`, `ServerSpriteManifest`, `ServerSpriteAsset` и
`SpriteCompileProfile` описывают inputs и release data для `standalone@server`.
Consumer использует тот же API с `source: 'remote'` и одним local path или HTTP(S)
URL manifest в `input`.
## Специализированные обёртки
Специализированные функции доступны как обёртки над `generateSprite`:
```ts
import { generateNextSprite, generateReactSprite } from '@gromlab/svg-sprites'
await generateReactSprite('path/to/config.ts', 'vite')
await generateNextSprite('path/to/config.ts', {
router: 'app',
bundler: 'turbopack',
})
```
Явно переданный target перекрывает `mode` из файла. Для нового кода используйте `generateSprite`.
## Config API
```ts
import {
isSpriteMode,
loadSpriteConfig,
resolveSpriteConfig,
resolveSpriteConfigSource,
validateSpriteConfig,
} from '@gromlab/svg-sprites'
```
- `isSpriteMode(value)` проверяет, является ли значение поддерживаемым exact mode.
- `loadSpriteConfig(file)` загружает явно указанный `.ts`, `.js` или `.json` файл.
- `resolveSpriteConfigSource(source)` разрешает путь как config-файл или config-less каталог.
- `validateSpriteConfig(value)` выполняет runtime-валидацию объекта.
- `resolveSpriteConfig(root, config, overrides)` объединяет значения, добавляет defaults и разрешает пути относительно `root`.
## Низкоуровневый compiler
```ts
import {
compileSprite,
compileSpriteContent,
createShapeTransform,
} from '@gromlab/svg-sprites'
```
Эти функции предназначены для собственного orchestration. Стандартная генерация должна выполняться через `generateSprite`.
## Viewer runtime
```ts
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
```
Browser entry регистрирует `<gromlab-sprite-viewer>`. Bare standalone также может загрузить самостоятельный `dist/viewer-element.js` без bundler.
Для ручной регистрации импортируйте runtime без auto-register entry:
```ts
import { defineSpriteViewerElement } from '@gromlab/svg-sprites/viewer'
defineSpriteViewerElement()
```
Этот entry также экспортирует типы `SpriteViewerElement`, `SpriteViewerManifest`, `SpriteViewerSource`, `SpriteViewerSources` и связанные типы manifest и loaders.
React bridge сохраняет компонентный API:
```tsx
import { SpriteViewer } from '@gromlab/svg-sprites/react'
```
`SpriteViewer` принимает generated manifests, remote standalone sources, lazy loaders или результат `import.meta.glob`. React entry содержит `'use client'` и предназначен для debug-инструментов; production-компоненты импортируются из локальных sprite-модулей приложения.

View File

@@ -0,0 +1,716 @@
# Технический справочник
[Индекс документации](../README.md)
[Конфигурация JSON, JavaScript и TypeScript](../configuration.md)
Справочник по конфигурации, generated API и поведению `@gromlab/svg-sprites`. Пошаговую установку смотрите в руководстве для вашего стека:
- [Bare standalone](../guides/standalone.md)
- [Standalone + Vite](../guides/standalone-vite.md)
- [Standalone + Webpack 5](../guides/standalone-webpack.md)
- [React + Vite](../guides/react-vite.md)
- [React + Webpack 5](../guides/react-webpack.md)
- [Next.js App Router + Turbopack](../guides/next-app-turbopack.md)
- [Next.js App Router + Webpack](../guides/next-app-webpack.md)
- [Next.js Pages Router + Turbopack](../guides/next-pages-turbopack.md)
- [Next.js Pages Router + Webpack](../guides/next-pages-webpack.md)
- [Vue + Vite](../guides/vue-vite.md)
- [Vue + Webpack](../guides/vue-webpack.md)
- [Nuxt + Vite](../guides/nuxt-vite.md)
- [Nuxt + Webpack](../guides/nuxt-webpack.md)
- [Svelte + Vite](../guides/svelte-vite.md)
- [Svelte + Webpack](../guides/svelte-webpack.md)
- [SvelteKit + Vite](../guides/sveltekit-vite.md)
- [Angular application builder](../guides/angular-application.md)
- [Angular + Webpack](../guides/angular-webpack.md)
- [Astro + Vite](../guides/astro-vite.md)
- [Solid + Vite](../guides/solid-vite.md)
- [Solid + Webpack](../guides/solid-webpack.md)
- [SolidStart + Vite](../guides/solid-start-vite.md)
- [Preact + Vite](../guides/preact-vite.md)
- [Preact + Webpack](../guides/preact-webpack.md)
- [Qwik + Vite](../guides/qwik-vite.md)
- [Lit + Vite](../guides/lit-vite.md)
- [Lit + Webpack](../guides/lit-webpack.md)
- [Alpine.js + Vite](../guides/alpine-vite.md)
- [Alpine.js + Webpack](../guides/alpine-webpack.md)
## Требования
- Node.js 18 или новее;
- пакет распространяется как ESM и подключается через `import`;
- React 18 или 19 требуется только для React/Next generated-компонентов и `@gromlab/svg-sprites/react`;
- для типизации package exports используйте TypeScript 5+ с `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`.
Для генерации не нужна dependency проекта. Запускайте CLI через `npx`:
```bash
npx --yes @gromlab/svg-sprites path/to/svg-sprite.config.json
```
Устанавливайте пакет как development dependency, только если проекту нужны
Viewer, типы конфига или программный API:
```bash
npm install --save-dev @gromlab/svg-sprites
```
## CLI и режимы генерации
Для генерации CLI принимает ровно один путь: явно выбранный config-файл либо каталог для config-less генерации:
```text
svg-sprites [options] <config-file-or-directory>
```
| Среда | Mode |
|---|---|
| Static HTML / собственная публикация | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
| Vue + Webpack | `vue@webpack` |
| Nuxt + Vite | `nuxt@vite` |
| Nuxt + Webpack | `nuxt@webpack` |
| Svelte + Vite | `svelte@vite` |
| Svelte + Webpack | `svelte@webpack` |
| SvelteKit + Vite | `sveltekit@vite` |
| Angular application builder | `angular@application` |
| Angular + Webpack | `angular@webpack` |
| Astro + Vite | `astro@vite` |
| Solid + Vite | `solid@vite` |
| Solid + Webpack | `solid@webpack` |
| SolidStart + Vite | `solid-start@vite` |
| Preact + Vite | `preact@vite` |
| Preact + Webpack | `preact@webpack` |
| Qwik + Vite | `qwik@vite` |
| Lit + Vite | `lit@vite` |
| Lit + Webpack | `lit@webpack` |
| Alpine.js + Vite | `alpine@vite` |
| Alpine.js + Webpack | `alpine@webpack` |
| Next.js App Router + Turbopack | `next@app/turbopack` |
| Next.js App Router + Webpack 5 | `next@app/webpack` |
| Next.js Pages Router + Turbopack | `next@pages/turbopack` |
| Next.js Pages Router + Webpack 5 | `next@pages/webpack` |
Config-файл может иметь любое имя и расширение `.ts`, `.js` или `.json`. CLI не ищет конфиг по соглашению: файл нужно передать явно. В руководствах используется рекомендуемое имя `svg-sprite.config.json`.
Если передан каталог, все настройки берутся из CLI. Если передан config-файл, CLI-параметры перекрывают значения файла. Общий порядок: `defaults → config → CLI`.
`--help` и `-h` выводят справку без обязательного пути. Для генерации доступны `--mode`, `--source <local|remote>`, `--name`, `--description`, повторяемый `--input <path-or-glob>`, а также пары `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition` и `--generated-notice`/`--no-generated-notice`. Переданные transform-флаги перекрывают отдельные поля, а хотя бы один `--input` полностью заменяет значение `input` из config.
В CLI заключайте glob-паттерны в одинарные кавычки, чтобы shell не раскрыл их до запуска генератора:
```bash
svg-sprites --input './icons/**/*.svg' --input '!./icons/legacy/**' svg-sprite.config.ts
```
Mode должен соответствовать способу публикации приложения. Bare `standalone` оставляет публичный URL приложению; Vite и Webpack modes генерируют bundler-specific подключение SVG asset.
## Единая конфигурация
Каждый config-файл описывает один независимый спрайт.
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'next@app/turbopack',
name: 'app',
description: 'Общие иконки приложения',
input: [
'./local-icons',
'../../assets/icons/*.svg',
'!../../assets/icons/deprecated-*.svg',
],
transform: {
removeSize: true,
replaceColors: true,
addTransition: true,
},
generatedNotice: true,
})
```
| Опция | Тип | По умолчанию | Назначение |
|---|---|---|---|
| `mode` | `SpriteMode` | Нет | Режим генерации; можно передать через CLI/API |
| `source` | `local \| remote` | `local` | Исходные SVG либо готовый server manifest |
| `name` | `string` | Выводится из каталога | Имя спрайта; в modes с компонентом также задаёт имя компонента и публичных типов |
| `description` | `string` | Нет | Описание для типов и debug manifest |
| `input` | `SpriteInput \| SpriteInput[]` | `./icons` | Локальные SVG sources, server HTTP descriptors либо один remote manifest в зависимости от mode и source |
| `transform` | `TransformOptions` | Все включены | Настройки подготовки SVG |
| `generatedNotice` | `boolean` | `true` | Полное или короткое предупреждение в generated-файлах |
При `source: 'remote'` поле `input` содержит один local path или HTTP(S) URL
manifest, созданного `standalone@server`. Remote consumer config может содержать
только `mode`, `source` и `input`: name, description, transforms и generated notice
проверяются и наследуются из server manifest. До codegen генератор скачивает profile,
необходимый exact consumer mode, и проверяет его SHA-256 и размер. Runtime-зависимости
от server manifest нет.
### Имя спрайта
`name` записывается в kebab-case и должно начинаться с латинской буквы:
```text
app → AppIcon
file-manager → FileManagerIcon
```
Если `name` не задано, генератор преобразует имя каталога в kebab-case. Для каталога с именем `svg-sprite` или `svg-sprites` используется имя родительского каталога.
### Источники иконок
`SpriteConfig.input` является необязательным и имеет тип `string | string[]`. Если поле отсутствует, источником служит папка `./icons` относительно папки конфига. В config-less режиме относительные пути считаются от каталога, переданного CLI или API.
Каждая строка без префикса `!` может быть путём к конкретной папке, конкретному файлу `.svg` или glob-паттерном. Папка включает только непосредственные дочерние `*.svg`. Для рекурсивного обхода вложенных каталогов укажите явный паттерн, например `icons/**/*.svg`.
Массив объединяет все включающие источники. Паттерн с префиксом `!` глобально исключает совпадения из общего результата независимо от того, какой источник их добавил.
Поддерживается следующий glob-синтаксис:
| Синтаксис | Значение |
|---|---|
| `*` | Любые символы внутри одного сегмента пути |
| `**` | Любое число вложенных каталогов |
| `?` | Один символ внутри сегмента пути |
| `{a,b}` | Одна из альтернатив |
| `[abc]` | Один символ из набора или диапазона |
| `!pattern` | Исключение совпадений из всего объединённого input |
Каждый включающий источник или паттерн должен найти хотя бы один SVG, иначе генерация завершается ошибкой. Повторяющиеся пути удаляются, а итоговый список файлов детерминированно сортируется. Разные SVG с одинаковым basename по-прежнему считаются конфликтом, потому что basename задаёт публичное имя иконки.
### Server SVG inputs
`standalone@server` принимает те же local strings и HTTP(S) descriptors в массиве
`input`:
```ts
{
name: 'brand-logo',
url: 'https://assets.example.com/brand-logo.svg',
sha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
}
```
`name` становится публичным именем иконки. Необязательный `sha256` проверяется по
скачанным байтам. URL credentials и активное SVG-содержимое, включая scripts,
event handlers, `foreignObject` и doctype, запрещены. Один HTTP source ограничен
2 MiB, все источники вместе — 25 MiB, timeout запроса равен 15 секундам. Local и
HTTP entries используют единое пространство имён, поэтому duplicate icon names
завершают генерацию с ошибкой.
## Generated-модуль
После генерации React- или Next.js-каталог спрайта выглядит так:
```text
app-icons/
├── .gitignore
├── svg-sprite.config.json
├── index.ts # необязательный пользовательский barrel
└── .svg-sprite/
├── index.js
├── index.d.ts
├── icon-data.js
├── icon-data.d.ts
├── sprite.svg
├── svg-sprite.manifest.js
├── svg-sprite.manifest.d.ts
└── react/
├── react-component.js
├── react-component.d.ts
└── react-component.module.css
```
| Файл | Назначение |
|---|---|
| `.svg-sprite/index.js` | Mode-specific production facade и runtime-список имён |
| `.svg-sprite/index.d.ts` | Публичные декларации facade, компонента и union-типа имён |
| `.svg-sprite/svg-sprite.manifest.js` | Debug metadata и URL asset для `SpriteViewer` |
| `.svg-sprite/sprite.svg` | Собранный SVG-спрайт |
| `.svg-sprite/react/react-component.js` | Runtime React-компонента без TypeScript и JSX |
| `.svg-sprite/react/react-component.d.ts` | Props, style и declaration React-компонента |
| `.svg-sprite/react/react-component.module.css` | Стили конкретной React-реализации |
| `.svg-sprite/icon-data.js` | Runtime-список имён и внутренние IDs |
| `.svg-sprite/*.d.ts` | TypeScript-декларации соответствующих JS-модулей |
Standalone-контракты не создают каталог `react/`. Bare `standalone` содержит только
runtime asset и deployment-neutral manifest data:
```text
.svg-sprite/
├── sprite.svg
└── svg-sprite.manifest.json
```
`standalone@vite` и `standalone@webpack` дополнительно создают `index.*`,
`icon-data.*` и resolved `svg-sprite.manifest.*`. Их facade содержит нативный
generated Web Component без внешних runtime-зависимостей. Bare `standalone`
намеренно не создаёт JavaScript-компонент.
`standalone@server` создаёт готовый к публикации release без JavaScript runtime и
`.gitignore`:
```text
.svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
Manifest описывает оба compile profiles через relative `href`, полный SHA-256 и
размер в байтах. Публикуйте весь каталог атомарно; consumer разрешает каждый profile
относительно URL или local path manifest.
`.svg-sprite` полностью управляется генератором и при каждой генерации заменяется целиком. Любые добавленные в него файлы будут удалены. Пользовательские файлы размещайте рядом, например в корневом `index.ts`:
```ts
export * from './.svg-sprite/index.js'
```
## Standalone Web Component и TypeScript
В modes `standalone@vite` и `standalone@webpack` спрайт с `name: 'app'`
экспортирует функцию регистрации `defineAppIconElement()` и tag `<app-icon>`:
```ts
import { defineAppIconElement } from '@/ui/app-icons'
defineAppIconElement()
```
После регистрации элемент можно использовать в HTML:
```html
<app-icon icon="search" aria-hidden="true"></app-icon>
<app-icon
icon="settings"
role="img"
aria-label="Настройки"
></app-icon>
```
Компонент рендерит `<svg><use>` в открытом Shadow DOM, сам выбирает внутренний
ID и `viewBox`, а URL asset получает через соответствующий Vite или Webpack
механизм. Размер host по умолчанию равен `1em × 1em`; `class`, `style`, `color`
и `--icon-color-N` задаются обычным CSS.
Generated `HTMLElementTagNameMap` типизирует property API:
```ts
const icon = document.createElement('app-icon')
icon.icon = 'search'
icon.icon = 'unknown' // ошибка TypeScript
```
Значения атрибутов в обычной HTML-разметке TypeScript не проверяет. Поэтому
неизвестный `icon="unknown"` дополнительно проверяется в runtime: компонент
скрывает внутренний SVG и сообщает об ошибке, не создавая fragment
`#undefined`. Повторный вызов `defineAppIconElement()` безопасен для того же
спрайта; конфликт с другим элементом под tag `<app-icon>` завершается ошибкой.
## React-компонент и TypeScript
Спрайт с `name: 'app'` экспортирует:
```ts
export { AppIcon, appIconNames }
export type { AppIconName, AppIconProps, AppIconStyle }
```
### Имена иконок
Имена SVG-файлов становятся допустимыми значениями `icon`:
```tsx
<AppIcon icon="search" />
<AppIcon icon="unknown" /> // ошибка TypeScript
```
Runtime-список содержит те же значения:
```ts
import { appIconNames } from '@/ui/app-icons'
// readonly ['search', 'settings', 'user']
```
Имена с пробелами и другими небезопасными для SVG ID символами остаются частью публичного API. Для внутреннего fragment ID генератор создаёт стабильный безопасный hash:
```text
folder open.svg → icon="folder open" → id="icon-<stable-hash>"
```
Для таких имён используйте generated-компонент или `id` из debug manifest, а не формируйте fragment ID вручную.
### SVG-атрибуты
По умолчанию компонент рендерит `<svg>` и принимает стандартные SVG-атрибуты:
```tsx
<AppIcon
icon="search"
width={24}
height={24}
color="rebeccapurple"
className="searchIcon"
aria-label="Поиск"
/>
```
Компонент не добавляет accessibility-семантику автоматически. Передавайте подходящие `aria-*`, `role` или подпись в зависимости от назначения иконки.
### Обёртка
`wrapped` рендерит `<span>` с внутренним SVG. Остальные props в этом режиме относятся к `<span>`:
```tsx
<AppIcon icon="search" wrapped className="iconWrapper" />
```
### Типизированные CSS-переменные
`AppIconStyle` расширяет `CSSProperties` и поддерживает свойства вида `--icon-color-N`:
```tsx
<AppIcon
icon="user"
style={{
'--icon-color-1': '#2563eb',
'--icon-color-2': '#dbeafe',
}}
/>
```
## Множественные спрайты
Каждый каталог с конфигом создаёт независимый mode-specific контракт. Framework modes создают нативный компонент и declarations, standalone bundler modes — Web Component и declarations, а bare `standalone` — SVG и JSON manifest:
```text
app-icons → AppIcon → общие иконки
analytics-icons → AnalyticsIcon → иконки страницы аналитики
editor-icons → EditorIcon → иконки редактора
```
Один исходный SVG можно добавить через `input` в несколько конфигураций. Копировать файл в каталоги каждого спрайта не требуется.
Для нескольких спрайтов добавьте отдельную CLI-команду для каждого каталога или объедините команды в общем npm script.
## Форматы и способы отображения
Все текущие modes создают формат `stack`.
| Формат | `<svg><use>` | `<img>` | CSS background |
|---|---:|---:|---:|
| `stack` | Да | Да | Да |
### Generated-компонент
Используйте generated native-компонент из guide выбранного exact mode. Он знает внутренние ID, формирует URL и предоставляет TypeScript API. Для React и Next.js это выглядит так:
```tsx
<AppIcon icon="search" width={24} height={24} />
```
Для `standalone@vite` и `standalone@webpack` используйте generated Web Component:
```html
<app-icon icon="search" style="font-size: 24px"></app-icon>
```
### Вручную через `<svg><use>`
Способ получения `spriteUrl` зависит от сборщика.
Static HTML после публикации `.svg-sprite/sprite.svg` приложением:
```html
<svg aria-hidden="true">
<use href="/assets/icons.svg#search"></use>
</svg>
```
Standalone Vite/Webpack предоставляет generated `getAppIconHref()` и mapping
внутренних IDs. Не конструируйте fragment из небезопасного имени файла вручную.
Vite:
```ts
import spriteUrl from './.svg-sprite/sprite.svg?no-inline'
```
Webpack 5, Turbopack и Next.js:
```ts
const spriteUrl = new URL('./.svg-sprite/sprite.svg', import.meta.url).href
```
После получения URL используйте его в JSX:
```tsx
<svg width="24" height="24" aria-label="Поиск">
<use href={`${spriteUrl}#search`} />
</svg>
```
Для имён, небезопасных как SVG ID, используйте внутренний `id` из manifest.
### Через `<img>`
```tsx
<img src={`${spriteUrl}#search`} width={24} height={24} alt="Поиск" />
```
SVG внутри `<img>` изолирован от CSS страницы. `color` и `--icon-color-N` на внешнем элементе не изменяют его внутренние цвета.
### Через CSS
```css
.icon {
background: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat;
}
```
Для одноцветного силуэта можно использовать mask:
```css
.icon {
background-color: currentColor;
mask: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat;
}
```
Mask не сохраняет исходные цвета, gradients и различия между `fill` и `stroke`.
Путь в CSS разрешается относительно самого CSS-файла. В примерах CSS-файл находится рядом с `svg-sprite.config.ts`.
## Assets и кеширование
Generated component или standalone facade передаёт SVG сборщику как отдельный asset:
- Vite использует статический импорт с `?no-inline`;
- Webpack 5, Turbopack и Next.js используют `new URL(..., import.meta.url)`;
- SVG path-данные не сериализуются в generated JavaScript.
Bare `standalone` не участвует в asset pipeline: приложение само копирует или
публикует `sprite.svg` и отвечает за URL, версионирование и cache policy.
При стандартном именовании assets сборщик добавляет content hash:
```text
/assets/sprite-<hash>.svg
```
Это позволяет кешировать SVG отдельно от JavaScript. Изменение React-кода не меняет содержимое спрайта, а изменение иконок создаёт новую версию asset.
HTTP cache headers, CDN и `Cache-Control` настраиваются приложением или платформой размещения. Для Webpack имя итогового файла зависит от `assetModuleFilename` проекта.
## Трансформации SVG
Все трансформации включены по умолчанию и настраиваются независимо:
| Опция | Что делает |
|---|---|
| `removeSize` | Удаляет `width` и `height` с корневого `<svg>`, сохраняя существующий `viewBox` |
| `replaceColors` | Заменяет найденные `fill` и `stroke` на `--icon-color-N` |
| `addTransition` | Добавляет transitions для `fill` и `stroke` в цветные элементы и generated styles |
Чтобы отключить отдельную операцию:
```ts
export default defineSpriteConfig({
mode: 'next@app/turbopack',
transform: {
removeSize: false,
replaceColors: false,
addTransition: false,
},
})
```
Исходные SVG не изменяются. Трансформации применяются только к содержимому generated-спрайта.
## Управление цветами
### Монохромные иконки
Если найден один цвет, fallback становится `currentColor`:
```svg
stroke="var(--icon-color-1, currentColor)"
```
Цвет задаётся через prop или CSS:
```tsx
<AppIcon icon="search" color="rebeccapurple" />
```
### Многоцветные иконки
Каждый уникальный цвет получает отдельную переменную с исходным fallback:
```svg
fill="var(--icon-color-1, #798198)"
fill="var(--icon-color-2, #ffffff)"
fill="var(--icon-color-3, #129d9d)"
```
Можно заменить только необходимые значения:
```css
.icon {
--icon-color-1: #4b5563;
--icon-color-3: #14b8a6;
}
```
### Ограничения
- `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются;
- надёжнее всего обрабатываются цвета в атрибутах `fill`, `stroke` и inline `style`;
- CSS-классы и внешние stylesheets внутри SVG не являются основным сценарием трансформации;
- значения `url(#...)` могут быть заменены вместе с цветами, поэтому gradients и patterns требуют отдельного спрайта с `replaceColors: false`;
- masks, filters и сложные внутренние CSS-правила требуют визуальной проверки;
- CSS-переменные страницы доступны через `<svg><use>`, но не внутри `<img>` и CSS background.
Для сложной иконки можно отключить `replaceColors` в конфигурации отдельного спрайта.
## SpriteViewer
Viewer использует один Web Component с Shadow DOM для всех modes. React и будущие framework-компоненты являются bridge к этому же элементу, поэтому визуал и поведение не дублируются.
Bare `standalone` подключает самостоятельный browser bundle и передаёт URL JSON manifest и опубликованного SVG:
```html
<script
type="module"
src="https://unpkg.com/@gromlab/svg-sprites@<version>/dist/viewer-element.js"
></script>
<gromlab-sprite-viewer
viewer-title="Иконки проекта"
manifest-url="/app-icons/manifest.json"
sprite-url="/app-icons/sprite.svg"
></gromlab-sprite-viewer>
```
`viewer-element.js` не имеет дополнительных runtime-файлов и может быть скопирован с остальными static assets для self-hosting.
`standalone@vite` и `standalone@webpack` регистрируют тот же элемент через npm entry и передают generated JS manifest через свойство `sources`:
```ts
import '@gromlab/svg-sprites/viewer/element'
import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer'
import spriteManifest from './svg-sprite/.svg-sprite/svg-sprite.manifest.js'
const viewer = document.querySelector<SpriteViewerElement>('gromlab-sprite-viewer')!
viewer.sources = [spriteManifest]
```
React и Next.js сохраняют компонентный API:
```tsx
import { SpriteViewer } from '@gromlab/svg-sprites/react'
```
Он принимает готовые manifests, remote standalone sources, массив lazy loaders или record формата `import.meta.glob`.
Vite:
```tsx
import { SpriteViewer } from '@gromlab/svg-sprites/react'
import type { SpriteManifestModule } from '@gromlab/svg-sprites/react'
const sources = import.meta.glob<SpriteManifestModule>(
'/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js',
)
export const IconsDebugPage = () => (
<SpriteViewer sources={sources} title="Иконки проекта" />
)
```
Webpack и Next.js:
```tsx
const sources = [
() => import('@/ui/app-icons/.svg-sprite/svg-sprite.manifest.js'),
() => import('@/features/analytics/icons/.svg-sprite/svg-sprite.manifest.js'),
]
export const IconsDebugPage = () => (
<SpriteViewer sources={sources} />
)
```
Viewer показывает группы, поиск, `viewBox`, CSS-переменные и fallback-цвета. Framework manifests получают вкладку своего framework, а также SVG, IMG и CSS; standalone manifests получают SVG, IMG и CSS. Цветовые значения можно менять в интерфейсе и сразу проверять результат.
### Тема Viewer
По умолчанию `colorTheme="auto"` следует `prefers-color-scheme`. Можно передать `light` или `dark` явно:
```tsx
<SpriteViewer sources={sources} colorTheme="dark" />
```
Для синхронизации с темой приложения:
```tsx
<SpriteViewer
sources={sources}
colorTheme={appTheme}
onColorThemeChange={setAppTheme}
/>
```
`@gromlab/svg-sprites/react` содержит `'use client'` и рендерит Web Component host; внутренний Shadow DOM создаётся после загрузки browser runtime. В Next.js App Router размещайте Viewer внутри отдельной Client Component boundary и используйте только на debug-маршруте или во внутреннем инструменте.
## Generated-файлы, Git и CI
Все modes, кроме bare `standalone`, создают локальный `.gitignore` для:
```text
/.svg-sprite/
```
Локальный `.gitignore` следует один раз добавить в репозиторий. Он исключает остальные generated-файлы, поэтому генерацию нужно запускать перед командами, которые импортируют sprite-модуль:
```json
{
"scripts": {
"sprites": "npx --yes @gromlab/svg-sprites src/ui/app-icons/svg-sprite.config.ts",
"predev": "npm run sprites",
"prebuild": "npm run sprites",
"pretypecheck": "npm run sprites"
}
}
```
CI должен выполнять generation script до сборки или проверки типов. Локальная установка package не нужна, если CI не использует Viewer, package-типы config или программный API.
Bare `standalone` не создаёт `.gitignore` и сохраняет пользовательский файл. Если после другого mode остался управляемый `.gitignore`, bare mode удалит его. В остальных modes генератор откажется перезаписать пользовательский `.gitignore` без generated marker. Корневой `index.ts` остаётся пользовательским и может переэкспортировать generated API.
## Диагностика
- Для всех modes, кроме bare `standalone`: если нет `.svg-sprite/index.js`, запустите generation script до импорта generated-модуля.
- Не найден источник: передайте существующий config-файл или каталог sprite-модуля.
- Не указан mode: добавьте `mode` в config либо передайте `--mode`.
- Иконка отсутствует в типе: проверьте `input`, расширение `.svg`, glob-исключения и необходимость `**/*.svg` для вложенных папок.
- Конфликт имени: два разных SVG имеют одинаковый basename; переименуйте один файл.
- `Refusing to overwrite a user file`: в корне sprite-модуля находится пользовательский `.gitignore`, который генератор не может заменить.
- Иконка не меняет цвет: используйте `<svg><use>` или generated-компонент и проверьте `replaceColors`.
- Webpack выдаёт неверный URL: проверьте Asset Modules, `output.publicPath` и SVG loaders.
- Static sprite возвращает 404: проверьте post-generation copy или server alias и не передавайте filesystem `spritePath` в HTML.
- Viewer не видит спрайт: для bundler modes проверьте путь к `.svg-sprite/svg-sprite.manifest.js`; для bare `standalone` — URL опубликованных `svg-sprite.manifest.json` и `sprite.svg`. Выполните генерацию до запуска приложения.
- Build и mode не совпадают: используйте target, соответствующий фактическому сборщику.
Для собственного orchestration и низкоуровневой компиляции смотрите [Программный API](programmatic-api.md).

View File

@@ -0,0 +1,351 @@
---
name: template-generation
description: "Используй при создании, изменении или проверке повторяемой файловой структуры и шаблонов генерации. Триггеры: .templates, @gromlab/create, Template File Generator, scaffold, шаблон, генератор, создать компонент, модуль, layout, screen, widget, business, store, hook, service, page-entry, boilerplate, index.ts, типы, стили, тесты, повторить структуру без copy-paste, настроить генерацию файлов. НЕ используй для одноразовой точечной правки, code style, SLM-архитектуры без генерации файлов, Next.js routing, REST-клиентов или SVG sprites."
---
<!-- Generated from src/SKILL.md. Do not edit manually. -->
# Template Generation
## Генерация Файлов
Шаблоны генерации - способ создавать повторяемые структуры проекта через генератор, а не вручную.
Шаблон фиксирует проектное соглашение один раз: структуру папок, имена файлов, экспорты, типы, стили, тесты, базовую реализацию и правила именования. После этого страницы, модули, компоненты и другие повторяемые сущности создаются одинаково, без копипасты и случайных отличий.
### Рабочий Алгоритм
1. Определи, создаётся ли повторяемая структура или одноразовая правка.
2. Если задача затрагивает размещение кода, сначала определи архитектурное место через профильный skill. Для SLM Design используй `slm-design`.
3. Найди область шаблонов: корень проекта, приложение, пакет или самостоятельный участок монорепозитория.
4. Проверь наличие `.templates/`, локального README, npm scripts, scaffold-скриптов и документации проекта.
5. Если подходящий шаблон есть, запусти генератор из каталога области шаблонов.
6. Если шаблона нет, но структура повторяемая, создай шаблон в `.templates/` выбранной области и затем сгенерируй через него нужные файлы.
7. Если структура одноразовая или шаблон усложнит задачу, создай файлы вручную и не добавляй новый шаблон.
8. После генерации проверь структуру, имена, экспорты, публичный API и соответствие архитектуре проекта.
9. Если существующий шаблон устарел, исправь шаблон и затем регенерируй или точечно приведи результат к актуальному соглашению.
Не ограничивайся выполнением команды. Выбери правильный путь: использовать существующий шаблон, создать новый, обновить устаревший или отказаться от шаблона.
### Жёсткие Правила
- Не создавай повторяемую структуру вручную, пока не проверил наличие подходящего шаблона или генератора.
- Не копируй существующий модуль как способ генерации новой сущности.
- Не создавай новый шаблон в корне репозитория, если соглашение относится только к конкретному приложению или пакету.
- Если в `.templates/` есть `README.md`, прочитай его перед выбором шаблона.
- Для `@gromlab/create` запускай команду из каталога, где лежит нужная `.templates/`.
- Не передавай в `@gromlab/create` путь к шаблонам через flags. Позиционный `[путь]` является путём вывода.
- Не выдумывай внешний источник шаблонов. Используй локальные инструкции проекта или явное указание пользователя.
- Не меняй существующий генератор, CLI или набор шаблонов без задачи на изменение генерации.
- Не закрепляй в шаблоне нарушение архитектуры, импортов, публичного API или code style проекта.
### Локальные Материалы Skill
Каноны ниже достаточны для выбора шаблона, области и команды генерации. Для редких сценариев открывай только нужный локальный файл:
- [Настройка шаблонов](./reference/canons/setup.md) - установка и проверка набора шаблонов.
- [Next.js App Router + SLM](./reference/examples/nextjs-app-router-slm/README.md) - рабочий набор `.templates/`.
### Разделы Спецификации
- [Выбор Шаблона](#выбор-шаблона) - когда использовать существующий шаблон, когда создавать новый и когда отказаться от шаблона.
- [Область Шаблонов](#область-шаблонов) - где лежит `.templates/` в обычном проекте и монорепозитории.
- [Использование](#использование-шаблонов) - генерация через CLI, VS Code расширение и проектные генераторы.
- [Создание Шаблонов](#создание-шаблонов) - структура `.templates/`, переменные, модификаторы и требования к шаблону.
- [Настройка](./reference/canons/setup.md) - первичная установка или проверка набора шаблонов.
- [Примеры Next.js App Router + SLM](./reference/examples/nextjs-app-router-slm/README.md) - рабочий набор `.templates/` для Next.js App Router и SLM Design.
### Проблема
Каждый новый модуль, компонент, store или scaffold требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к расхождениям, забытым `index.ts`, неверным именам, устаревшим копиям и ошибкам переименования после copy-paste.
### Решение
Повторяемые структуры создаются через `.templates/` и проектный генератор. Генератор принимает имя сущности, подставляет его в переменные шаблона и создаёт готовую структуру в нужной области проекта.
### Принципы
- Сначала проверь существующий шаблон или генератор проекта.
- Если шаблон есть, используй генератор вместо ручного создания файлов.
- Если шаблона нет, но структура повторяемая, сначала создай шаблон, затем сгенерируй через него нужные файлы.
- Ручное создание допустимо для уникального одноразового кода, точечных правок и случаев, где шаблон усложняет задачу больше, чем помогает.
- В монорепозитории выбирай `.templates/` внутри правильной области: приложения, пакета или самостоятельного участка репозитория.
- Локальные инструкции проекта и README внутри `.templates/` имеют приоритет над общими примерами.
## Выбор Шаблона
### Главное Правило
Когда нужно создать повторяемую структуру файлов, сначала проверь наличие шаблона или генератора.
Если подходящий шаблон есть, используй его.
Если шаблона нет, но сущность повторяемая и может понадобиться снова, сначала создай шаблон, затем создай файлы проекта через него.
### Повторяемые Структуры
К повторяемым структурам относятся:
- страницы;
- модули;
- компоненты;
- layout;
- screen;
- widget;
- business-домен;
- store;
- state manager;
- hook;
- service;
- scaffold из нескольких связанных файлов или папок.
Не создавай такие структуры вручную по умолчанию. Сначала проверь, можно ли создать их через шаблон или генератор проекта.
### Что Оформлять В Шаблон
Оформляй структуру в шаблон, если выполняется хотя бы одно условие:
- структура состоит из нескольких связанных файлов или папок;
- имена файлов, папок, типов, компонентов или экспортов выводятся из одного имени сущности;
- есть повторяемые экспорты, типы, стили, тесты или другой boilerplate;
- такая сущность может понадобиться в проекте повторно;
- ручное создание легко приведёт к расхождениям между похожими сущностями;
- структура отражает архитектурное или командное соглашение проекта.
### Когда Писать Вручную
Создавай без шаблона, если:
- код уникален для конкретной задачи;
- меняется существующий файл, а не создаётся повторяемая структура;
- структура не будет переиспользоваться;
- это небольшое точечное изменение;
- шаблон усложнит работу больше, чем поможет;
- человек явно попросил не использовать шаблоны.
Если есть сомнение, считай scaffold, boilerplate и группы связанных файлов кандидатами на шаблон.
### Анти-Паттерны
- Копировать существующий модуль и переименовывать его вручную.
- Создавать компонент, модуль или store руками при наличии подходящего шаблона.
- Добавлять новый тип повторяемой структуры без шаблона, если он отражает командное соглашение.
- Создавать шаблон в глобальной области, если структура нужна только конкретному приложению или пакету.
## Область Шаблонов
### Определение
Каталог, внутри которого лежит `.templates/`, является областью шаблонов.
Для `apps/web/.templates/` область шаблонов - `apps/web`. Для `packages/ui/.templates/` область шаблонов - `packages/ui`.
### Размещение
`.templates/` не обязана лежать в корне git-репозитория. В монорепозитории у каждого приложения или пакета могут быть свои шаблоны:
```text
apps/web/.templates/
apps/admin/.templates/
packages/ui/.templates/
```
Шаблоны размещаются в той области, где действует соответствующее проектное соглашение.
### Выбор Области
Перед генерацией определи правильную область шаблонов:
1. Найди ближайшую или явно подходящую `.templates/`.
2. Проверь README внутри `.templates/`, если он есть.
3. Убедись, что выбранный шаблон относится к нужному приложению, пакету или модулю.
4. Создавай новый шаблон в той `.templates/`, которая принадлежит этой области.
5. Запускай CLI из каталога области шаблонов.
Не считай корень репозитория единственным местом для `.templates/`.
### Локальный README
Если в найденной `.templates/` есть `README.md`, прочитай его перед выбором шаблона.
README может описывать доступные шаблоны, назначение, область применения, параметры и примеры команд. Локальный README имеет приоритет над общими примерами этого reference.
### Монорепозиторий
В монорепозитории выбирай `.templates/` по месту создаваемой сущности:
- код приложения `apps/web` генерируется из `apps/web/.templates/`;
- код админки `apps/admin` генерируется из `apps/admin/.templates/`;
- общий UI-пакет генерируется из `packages/ui/.templates/` или локальных шаблонов конкретного пакета;
- общий infra/shared-пакет использует шаблоны своей области.
Если шаблон нужен только одному приложению, не выноси его в корень репозитория без причины.
## Использование Шаблонов
### Приоритет Инструментов
Используй генератор проекта, если он явно задан локальной документацией или scripts.
Если проект использует `.templates/` без другого генератора, основной способ для AI-агента - CLI `@gromlab/create` через `npx`.
Для разработчика-человека удобный способ - VS Code расширение `Template File Generator | gromlab`.
### CLI
```bash
npx @gromlab/create <шаблон> <имя> [путь]
```
CLI ищет `.templates/` только в текущей рабочей директории.
Позиционный `[путь]` - папка вывода относительно текущей рабочей директории, а не путь к шаблонам.
В монорепозитории запускай CLI из каталога области шаблонов.
Глобальная установка не нужна. Используй CLI через `npx`.
Не используй `--templates`, `--templatesPath`, `--templates-path`, `--out` или `--output`: эти опции не поддерживаются CLI.
### Примеры CLI
```bash
npx @gromlab/create component header-nav src/compositions/layouts/default-layout/ui
npx @gromlab/create module hero-section src/compositions/screens/home/parts
npx @gromlab/create widget header src/compositions/widgets
npx @gromlab/create layout default-layout src/compositions/layouts
npx @gromlab/create business auth src/business
npx @gromlab/create store auth src/business/auth/stores
```
Пример для монорепозитория с шаблонами в `apps/web/.templates/`:
```bash
# рабочая директория: apps/web
npx @gromlab/create module button src/ui
```
### VS Code
`Template File Generator | gromlab` позволяет создавать файлы и папки из `.templates/` через интерфейс редактора:
1. Открыть контекстное меню на целевой папке.
2. Выбрать `Generate from template`.
3. Выбрать шаблон.
4. Ввести имя сущности.
Расширение устанавливается на машину разработчика, а не в проект.
### После Генерации
После генерации проверь:
- файлы созданы в правильной области проекта;
- имена файлов, типов, компонентов и экспортов соответствуют шаблону;
- публичные API не открывают лишние внутренние детали;
- результат не нарушает архитектуру проекта;
- если шаблон устарел, обнови шаблон, а не исправляй каждый новый scaffold вручную.
## Создание Шаблонов
<!-- @formatter:off -->
### Структура
Шаблоны лежат в `.templates/` внутри нужной области шаблонов. Каждый подкаталог внутри `.templates/` - отдельный шаблон.
```text
.templates/
├── component/
├── module/
├── screen/
├── layout/
├── widget/
├── business/
├── business-with-deps/
├── business-composition/
├── page-entry/
├── store/
└── hook/
```
### Содержимое Шаблона
Шаблон должен описывать проектное соглашение, а не только создавать пустые файлы.
Фиксируй в шаблоне:
- структуру папок;
- имена файлов;
- публичные и локальные экспорты;
- типы;
- стили;
- тесты, если они приняты в проекте;
- базовый boilerplate;
- правила именования.
После создания шаблона используй его для генерации нужной структуры.
### Переменные
Используй переменные для частей, которые меняются между сгенерированными сущностями.
Переменные можно применять в именах файлов, именах папок и содержимом файлов:
```text
{{name}}
{{name.pascalCase}}
{{name.camelCase}}
{{name.kebabCase}}
{{name.snakeCase}}
{{name.screamingSnakeCase}}
```
`name` - дефолтная переменная, которую генератор получает вторым позиционным аргументом.
### Пример
```text
.templates/component/
└── {{name.kebabCase}}/
├── styles/
│ └── {{name.kebabCase}}.module.css
├── types/
│ └── {{name.kebabCase}}-props.type.ts
├── {{name.kebabCase}}.tsx
└── index.ts
```
```ts
// .templates/component/{{name.kebabCase}}/{{name.kebabCase}}.tsx
export const {{name.pascalCase}} = () => {
return null
}
```
```ts
// .templates/component/{{name.kebabCase}}/index.ts
export { {{name.pascalCase}} } from './{{name.kebabCase}}'
```
### README Для Шаблонов
Если набор шаблонов неочевиден, добавь `.templates/README.md`.
Опиши в README:
- список шаблонов;
- назначение каждого шаблона;
- область применения;
- обязательные параметры;
- примеры команд;
- отличия похожих шаблонов.
### Запреты
- Не создавай шаблон, который закрепляет нарушение архитектуры проекта.
- Не добавляй в шаблон продуктовые детали, если шаблон должен быть общим для приложения или пакета.
- Не используй copy-paste существующего модуля вместо шаблона для повторяемой структуры.
- Не придумывай внешний источник шаблонов без инструкции проекта или явного указания человека.
<!-- @formatter:on -->

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Template Generation"
short_description: "Шаблоны генерации и повторяемые структуры файлов"
default_prompt: "Use $template-generation to create, update, or apply file generation templates for repeatable project structures."

View File

@@ -0,0 +1,37 @@
---
title: Настройка Шаблонов
description: Первичная установка и проверка набора шаблонов генерации
---
# Настройка Шаблонов
## Когда Нужна Настройка
Настройка нужна, если в проекте или выбранной области монорепозитория нет `.templates/`, но команда использует генерацию файлов как стандартный способ создания повторяемых структур.
Не перезаписывай существующую `.templates/` без согласования.
## Установка Стандартного Набора
Если проекту нужен стандартный набор Next.js App Router + SLM, сначала проверь локальный пример [nextjs-app-router-slm](../examples/nextjs-app-router-slm/README.md) и перенеси в `.templates/` только подходящие шаблоны.
Если в `./examples` нет подходящего шаблона, создай нужный шаблон в процессе работы с проектом на основе фактической структуры проекта, локальных соглашений и правил генерации.
## Проверка Установки
Проверь генерацию тестовой сущности из области шаблонов:
```bash
npx @gromlab/create component test src/ui
```
После проверки удали тестовую сущность.
## Чеклист
- В правильной области проекта есть `.templates/`.
- Внутри `.templates/` есть нужные шаблоны или согласованный кастомный набор.
- Если есть `.templates/README.md`, он описывает назначение шаблонов.
- CLI запускается из области шаблонов.
- Пробная генерация отрабатывает без ошибок.
- Тестовый scaffold удалён после проверки.

View File

@@ -0,0 +1,18 @@
'use client'
import { useContext } from 'react'
import { {{name.pascalCase}}BusinessContext } from '../providers/{{name.kebabCase}}-business.provider'
import type { {{name.pascalCase}}Business } from '../types/{{name.kebabCase}}-business.type'
/**
* Возвращает business API, доступный внутри композиционного модуля {{name.pascalCase}}.
*/
export const use{{name.pascalCase}}Business = (): {{name.pascalCase}}Business => {
const business = useContext({{name.pascalCase}}BusinessContext)
if (!business) {
throw new Error('use{{name.pascalCase}}Business must be used within {{name.pascalCase}}BusinessProvider')
}
return business
}

View File

@@ -0,0 +1,4 @@
export { {{name.pascalCase}}BusinessProvider } from './providers/{{name.kebabCase}}-business.provider'
export { use{{name.pascalCase}}Business } from './hooks/use-{{name.kebabCase}}-business.hook'
export type { {{name.pascalCase}}Business } from './types/{{name.kebabCase}}-business.type'
export type { {{name.pascalCase}}BusinessProviderProps } from './types/{{name.kebabCase}}-business-provider-props.type'

View File

@@ -0,0 +1,27 @@
'use client'
import { createContext } from 'react'
import type { {{name.pascalCase}}Business } from '../types/{{name.kebabCase}}-business.type'
import type { {{name.pascalCase}}BusinessProviderProps } from '../types/{{name.kebabCase}}-business-provider-props.type'
/**
* Context business API для композиционного модуля {{name.pascalCase}}.
*/
export const {{name.pascalCase}}BusinessContext = createContext<{{name.pascalCase}}Business | null>(null)
/**
* Провайдер business API для композиционного модуля {{name.pascalCase}}.
*
* Используется для:
* - передачи собранных business-фабрик вложенным модулям
* - сохранения единой client boundary для business API
*/
export const {{name.pascalCase}}BusinessProvider = (props: {{name.pascalCase}}BusinessProviderProps) => {
const { children, value } = props
return (
<{{name.pascalCase}}BusinessContext.Provider value={value}>
{children}
</{{name.pascalCase}}BusinessContext.Provider>
)
}

View File

@@ -0,0 +1,12 @@
import type { ReactNode } from 'react'
import type { {{name.pascalCase}}Business } from './{{name.kebabCase}}-business.type'
/**
* Параметры провайдера business API для {{name.pascalCase}}.
*/
export type {{name.pascalCase}}BusinessProviderProps = {
/** Вложенное дерево композиционного модуля. */
children: ReactNode
/** Собранный business API. */
value: {{name.pascalCase}}Business
}

View File

@@ -0,0 +1,4 @@
/**
* Business API, доступный внутри композиционного модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Business = object

View File

@@ -0,0 +1,5 @@
export { {{name.camelCase}}Factory } from './{{name.kebabCase}}.factory'
export type { {{name.pascalCase}} } from './types/{{name.kebabCase}}.type'
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}}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 @@
/**
* Зависимости бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Deps = object

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,4 @@
/**
* Доменная сущность {{name.pascalCase}}.
*/
export type {{name.pascalCase}} = object

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,4 @@
export { {{name.camelCase}}Factory } from './{{name.kebabCase}}.factory'
export type { {{name.pascalCase}} } from './types/{{name.kebabCase}}.type'
export type { {{name.pascalCase}}Api } from './types/{{name.kebabCase}}-api.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,6 @@
import type { {{name.pascalCase}}Api } from './{{name.kebabCase}}-api.type'
/**
* Фабрика публичного API бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Factory = () => {{name.pascalCase}}Api

View File

@@ -0,0 +1,4 @@
/**
* Доменная сущность {{name.pascalCase}}.
*/
export type {{name.pascalCase}} = object

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 = () => {
return {}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More