@letar/zenstack-form-plugin
v1.0.1
Published
ZenStack plugin for generating Zod v4 schemas with UI metadata from schema.zmodel — works with @letar/forms
Downloads
198
Maintainers
Readme
@lena/zenstack-form-plugin
ZenStack плагин для генерации Zod схем с UI метаданными из schema.zmodel.
Установка
npm install -D @letar/zenstack-form-pluginВ монорепозитории Lena плагин уже подключён — отдельная установка не требуется.
Конфигурация
Добавьте плагин в schema.zmodel:
plugin formSchema {
provider = '@letar/zenstack-form-plugin'
output = './src/generated/form-schemas'
}В монорепозитории Lena используйте относительный путь:
provider = '../../libs/zenstack-form-plugin/dist/index.js'
i18n (опционально)
Для мультиязычных приложений добавьте опции i18n:
plugin formSchema {
provider = '@letar/zenstack-form-plugin'
output = './src/generated/form-schemas'
// i18n настройки
i18n = true // Включить генерацию i18nKey
i18nOutput = './messages/form-schemas' // Путь к файлам переводов
defaultLocale = 'ru' // Локаль по умолчанию (перезаписывается)
locales = 'ru,en' // Список локалей через запятую
}При i18n = true плагин:
- Добавляет
i18nKeyв.meta({ ui: { ... } })каждого поля - Генерирует JSON файлы переводов для каждой локали
- Генерирует TypeScript файл с типами ключей
Генерируемые файлы:
messages/form-schemas/
├── ru.json # Переводы на русском (defaultLocale — перезаписывается)
├── en.json # Переводы на английском (merge-стратегия — сохраняет существующие)
└── keys.ts # TypeScript типы ключейПример ru.json:
{
"Product": {
"name": { "title": "Название товара", "placeholder": "Введите название" }
},
"RecipeType": {
"SWEET": { "label": "Сладкое" }
}
}Пример keys.ts:
export type FormI18nKey = 'Product.name.title' | 'Product.name.placeholder' | 'RecipeType.SWEET.label'
// ...Использование
Enum с метками
Doc-комментарии /// перед значениями enum становятся метками:
enum RecipeType {
/// Сладкое
SWEET
/// Солёное
SALTY
}Генерирует:
// enums/RecipeType.form.ts
export const RecipeTypeFormSchema = z.enum(['SWEET', 'SALTY']).meta({
ui: {
options: [
{ value: 'SWEET', label: 'Сладкое' },
{ value: 'SALTY', label: 'Солёное' },
],
},
})
export const RecipeTypeLabels = {
SWEET: 'Сладкое',
SALTY: 'Солёное',
} as constМодели с @form.* директивами
Используйте /// doc-комментарии ДО поля (не после!):
model Recipe {
id String @id @default(cuid())
/// @form.title("Название рецепта")
/// @form.placeholder("Введите название")
title String
/// @form.title("Количество порций")
/// @form.fieldType("numberInput")
/// @form.props({ min: 1, max: 100 })
portions Int @default(1)
/// @form.title("Теги")
/// @form.fieldType("tags")
/// @form.placeholder("Добавить тег...")
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Генерирует:
// Recipe.form.ts
export const RecipeCreateFormSchema = z.object({
title: z.string().meta({
ui: { title: 'Название рецепта', placeholder: 'Введите название' },
}),
portions: z
.number()
.int()
.meta({
ui: { title: 'Количество порций', fieldType: 'numberInput', fieldProps: { min: 1, max: 100 } },
}),
tags: z.array(z.string()).meta({
ui: { title: 'Теги', placeholder: 'Добавить тег...', fieldType: 'tags' },
}),
})
export const RecipeUpdateFormSchema = RecipeCreateFormSchema.partial()
export const RecipeExcludedFields = ['id', 'createdAt', 'updatedAt'] as const
export type RecipeCreateForm = z.infer<typeof RecipeCreateFormSchema>
export type RecipeUpdateForm = z.infer<typeof RecipeUpdateFormSchema>Поддерживаемые директивы
| Директива | Описание | Пример |
| -------------------------- | ------------------- | -------------------------------------------- |
| @form.title("...") | Заголовок поля | /// @form.title("Название") |
| @form.placeholder("...") | Placeholder | /// @form.placeholder("Введите...") |
| @form.description("...") | Описание поля | /// @form.description("Подсказка") |
| @form.fieldType("...") | Тип компонента | /// @form.fieldType("tags") |
| @form.props({...}) | Constraints + props | /// @form.props({ min: 1, max: 100 }) |
| @form.relation({...}) | Настройки relation | /// @form.relation({ labelField: "name" }) |
| @form.exclude | Исключить из формы | /// @form.exclude |
Автоматическое разделение @form.props
Плагин автоматически разделяет @form.props на:
Zod constraints — становятся методами схемы:
min,max,step→.min(),.max(),.multipleOf()minLength,maxLength→.min(),.max()для строкpattern→.regex()email,url,uuid→.email(),.url(),.uuid()
UI props — остаются в fieldProps:
count,allowHalf(для rating)showValue,layout(для slider, radioCard)- Любые другие props
/// @form.props({ min: 1, max: 100, showValue: true })
portions IntГенерирует:
portions: z.number()
.int()
.min(1)
.max(100)
.meta({ ui: { fieldProps: { showValue: true } } })Автоматически исключаемые поля
id— первичные ключиcreatedAt,updatedAt— системные поля- Поля с атрибутом
@id - Поля с атрибутом
@relation(relation поля) - Поля, ссылающиеся на модели (например
info RecipeInfo?) - Поля с директивой
@form.exclude
Примечание: FK поля (
categoryId,userId, etc.) не исключаются автоматически. Используйте@form.relationдля создания select-поля или@form.excludeдля исключения.
Важно: формат комментариев
ZenStack связывает doc-комментарии /// с СЛЕДУЮЩИМ за ними элементом.
Правильно:
/// @form.title("Название")
title StringНеправильно:
title String
/// @form.title("Название") // Привяжется к следующему полю!Генерируемые файлы
src/generated/form-schemas/
├── index.ts # Реэкспорт всех схем
├── enums/
│ └── RecipeType.form.ts # Enum схемы с метками
├── Recipe.form.ts # Model схемы
└── ...Сборка плагина
При изменении кода плагина необходимо пересобрать:
nx build zenstack-form-plugin --skip-nx-cacheЗатем запустить генерацию:
nx zenstack:generate <app-name>Поддерживаемые типы Prisma
| Prisma тип | Zod тип |
| ---------- | ------------------------------------ |
| String | z.string() |
| Int | z.number().int() |
| Float | z.number() |
| Decimal | z.number() |
| BigInt | z.bigint() |
| Boolean | z.boolean() |
| DateTime | z.date() |
| Json | z.unknown() |
| Bytes | z.unknown() |
| Enum | EnumNameFormSchema (импортируется) |
Стратегия обновления переводов
При перегенерации i18n файлов:
| Локаль | Стратегия | Описание |
| --------------- | -------------- | -------------------------------------------------------------------------- |
| defaultLocale | Перезапись | Полностью перезаписывается из схемы |
| Другие локали | Merge | Сохраняет существующие переводы, добавляет новые ключи, удаляет устаревшие |
Примечание: Дефолтная локаль по умолчанию —
en. Для русскоязычных проектов явно указывайтеdefaultLocale = 'ru'.
Кастомные переводы валидации
Встроены переводы для en и ru. Для других языков создайте файл:
// i18n/form-validations.js
export default {
de: {
required: 'Pflichtfeld',
too_small: { string: 'Mindestens {minimum} Zeichen', ... },
// Полный интерфейс: ValidationTranslations из @letar/zenstack-form-plugin
},
}И укажите путь в конфигурации:
plugin formSchema {
provider = '@letar/zenstack-form-plugin'
i18n = true
defaultLocale = 'ru'
locales = 'ru,en,de'
validationTranslationsPath = './i18n/form-validations.js'
}Приоритет: кастомный файл → встроенные (en, ru) → fallback на английский.
Это позволяет переводчикам работать с en.json без потери изменений при перегенерации.
AI Tooling (MCP)
MCP сервер @letar/form-mcp предоставляет AI-ассистентам доступ к документации всех @form.* директив через tool get_directives.
Версия
v2.1.0
