@famgia/omnify-typescript 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/ai-guides/react-form-guide.md +259 -0
  2. package/ai-guides/typescript-guide.md +53 -0
  3. package/dist/{chunk-4L77AHAC.js → chunk-6I4O23X6.js} +521 -66
  4. package/dist/chunk-6I4O23X6.js.map +1 -0
  5. package/dist/index.cjs +761 -65
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +138 -2
  8. package/dist/index.d.ts +138 -2
  9. package/dist/index.js +227 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/plugin.cjs +624 -75
  12. package/dist/plugin.cjs.map +1 -1
  13. package/dist/plugin.d.cts +6 -0
  14. package/dist/plugin.d.ts +6 -0
  15. package/dist/plugin.js +96 -11
  16. package/dist/plugin.js.map +1 -1
  17. package/package.json +4 -3
  18. package/scripts/postinstall.js +29 -40
  19. package/stubs/JapaneseAddressField.tsx.stub +289 -0
  20. package/stubs/JapaneseBankField.tsx.stub +212 -0
  21. package/stubs/JapaneseNameField.tsx.stub +194 -0
  22. package/stubs/ai-guides/checklists/react.md.stub +108 -0
  23. package/stubs/ai-guides/cursor/react-design.mdc.stub +289 -0
  24. package/stubs/ai-guides/cursor/react-form.mdc.stub +277 -0
  25. package/stubs/ai-guides/cursor/react-services.mdc.stub +304 -0
  26. package/stubs/ai-guides/cursor/react.mdc.stub +305 -0
  27. package/stubs/ai-guides/react/README.md.stub +221 -0
  28. package/stubs/ai-guides/react/antd-guide.md.stub +294 -0
  29. package/stubs/ai-guides/react/checklist.md.stub +108 -0
  30. package/stubs/ai-guides/react/datetime-guide.md.stub +137 -0
  31. package/stubs/ai-guides/react/design-philosophy.md.stub +363 -0
  32. package/stubs/ai-guides/react/i18n-guide.md.stub +211 -0
  33. package/stubs/ai-guides/react/laravel-integration.md.stub +181 -0
  34. package/stubs/ai-guides/react/service-pattern.md.stub +180 -0
  35. package/stubs/ai-guides/react/tanstack-query.md.stub +339 -0
  36. package/stubs/ai-guides/react/types-guide.md.stub +524 -0
  37. package/stubs/components-index.ts.stub +13 -0
  38. package/stubs/form-validation.ts.stub +106 -0
  39. package/stubs/rules/index.ts.stub +48 -0
  40. package/stubs/rules/kana.ts.stub +291 -0
  41. package/stubs/use-form-mutation.ts.stub +117 -0
  42. package/stubs/zod-i18n.ts.stub +32 -0
  43. package/dist/chunk-4L77AHAC.js.map +0 -1
package/dist/plugin.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/plugin.ts
@@ -108,6 +118,10 @@ function getPropertyType(property, _allSchemas) {
108
118
  return "unknown";
109
119
  }
110
120
  }
121
+ if (property.type === "EnumRef") {
122
+ const enumRefProp = property;
123
+ return enumRefProp.enum;
124
+ }
111
125
  if (property.type === "Enum") {
112
126
  const enumProp = property;
113
127
  if (typeof enumProp.enum === "string") {
@@ -136,7 +150,7 @@ function propertyToTSProperties(propertyName, property, allSchemas, options = {}
136
150
  for (const field of customType.expand) {
137
151
  const fieldName = `${propertyName}_${toSnakeCase(field.suffix)}`;
138
152
  const fieldOverride = baseProp.fields?.[field.suffix];
139
- const isNullable = fieldOverride?.nullable ?? baseProp.nullable ?? false;
153
+ const isNullable = fieldOverride?.nullable ?? field.sql?.nullable ?? baseProp.nullable ?? false;
140
154
  const tsType = field.typescript?.type ?? "string";
141
155
  expandedProps.push({
142
156
  name: fieldName,
@@ -278,12 +292,24 @@ function schemaToInterface(schema, allSchemas, options = {}) {
278
292
  }
279
293
  }
280
294
  }
295
+ const enumDependencySet = /* @__PURE__ */ new Set();
296
+ if (schema.properties) {
297
+ for (const property of Object.values(schema.properties)) {
298
+ if (property.type === "EnumRef") {
299
+ const enumRefProp = property;
300
+ if (enumRefProp.enum) {
301
+ enumDependencySet.add(enumRefProp.enum);
302
+ }
303
+ }
304
+ }
305
+ }
281
306
  const schemaDisplayName = resolveDisplayName(schema.displayName, options);
282
307
  return {
283
308
  name: toInterfaceName(schema.name),
284
309
  properties,
285
310
  comment: schemaDisplayName ?? schema.name,
286
- dependencies: dependencySet.size > 0 ? Array.from(dependencySet).sort() : void 0
311
+ dependencies: dependencySet.size > 0 ? Array.from(dependencySet).sort() : void 0,
312
+ enumDependencies: enumDependencySet.size > 0 ? Array.from(enumDependencySet).sort() : void 0
287
313
  };
288
314
  }
289
315
  function formatProperty(property) {
@@ -330,7 +356,11 @@ function resolveDisplayName2(value, options = {}) {
330
356
  });
331
357
  }
332
358
  function toEnumMemberName(value) {
333
- return value.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9]/g, "");
359
+ let result = value.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9]/g, "");
360
+ if (/^\d/.test(result)) {
361
+ result = "_" + result;
362
+ }
363
+ return result;
334
364
  }
335
365
  function toEnumName(schemaName) {
336
366
  return schemaName;
@@ -384,6 +414,46 @@ function generateEnums(schemas, options = {}) {
384
414
  }
385
415
  return enums;
386
416
  }
417
+ function pluginEnumToTSEnum(enumDef, options = {}) {
418
+ const values = enumDef.values.map((v) => {
419
+ let label;
420
+ if (v.label !== void 0) {
421
+ if (typeof v.label === "string") {
422
+ label = v.label;
423
+ } else if (options.multiLocale) {
424
+ label = v.label;
425
+ } else {
426
+ label = resolveDisplayName2(v.label, options);
427
+ }
428
+ }
429
+ return {
430
+ name: toEnumMemberName(v.value),
431
+ value: v.value,
432
+ label,
433
+ extra: v.extra
434
+ };
435
+ });
436
+ let comment;
437
+ if (enumDef.displayName !== void 0) {
438
+ if (typeof enumDef.displayName === "string") {
439
+ comment = enumDef.displayName;
440
+ } else {
441
+ comment = resolveDisplayName2(enumDef.displayName, options);
442
+ }
443
+ }
444
+ return {
445
+ name: enumDef.name,
446
+ values,
447
+ comment: comment ?? enumDef.name
448
+ };
449
+ }
450
+ function generatePluginEnums(pluginEnums, options = {}) {
451
+ const enums = [];
452
+ for (const enumDef of pluginEnums.values()) {
453
+ enums.push(pluginEnumToTSEnum(enumDef, options));
454
+ }
455
+ return enums;
456
+ }
387
457
  function isMultiLocaleLabel(label) {
388
458
  return label !== void 0 && typeof label === "object";
389
459
  }
@@ -404,7 +474,7 @@ ${enumValues}
404
474
  `);
405
475
  parts.push(`/** All ${name} values */
406
476
  `);
407
- parts.push(`export const ${name}Values = Object.values(${name});
477
+ parts.push(`export const ${name}Values = Object.values(${name}) as ${name}[];
408
478
 
409
479
  `);
410
480
  parts.push(`/** Type guard for ${name} */
@@ -422,10 +492,10 @@ ${enumValues}
422
492
  if (hasMultiLocale) {
423
493
  const labelEntries = values.filter((v) => v.label !== void 0).map((v) => {
424
494
  if (isMultiLocaleLabel(v.label)) {
425
- const locales = Object.entries(v.label).map(([locale, text]) => `${locale}: '${text}'`).join(", ");
495
+ const locales = Object.entries(v.label).map(([locale, text]) => `'${locale}': '${escapeString(text)}'`).join(", ");
426
496
  return ` [${name}.${v.name}]: { ${locales} },`;
427
497
  }
428
- return ` [${name}.${v.name}]: { default: '${v.label}' },`;
498
+ return ` [${name}.${v.name}]: { default: '${escapeString(String(v.label))}' },`;
429
499
  }).join("\n");
430
500
  parts.push(`const ${lowerFirst(name)}Labels: Partial<Record<${name}, Record<string, string>>> = {
431
501
  ${labelEntries}
@@ -442,13 +512,15 @@ ${labelEntries}
442
512
  `);
443
513
  parts.push(` if (locale && labels[locale]) return labels[locale];
444
514
  `);
445
- parts.push(` return Object.values(labels)[0] ?? value;
515
+ parts.push(` // Fallback: ja \u2192 en \u2192 first available
516
+ `);
517
+ parts.push(` return labels['ja'] ?? labels['en'] ?? Object.values(labels)[0] ?? value;
446
518
  `);
447
519
  parts.push(`}
448
520
 
449
521
  `);
450
522
  } else {
451
- const labelEntries = values.filter((v) => v.label !== void 0).map((v) => ` [${name}.${v.name}]: '${v.label}',`).join("\n");
523
+ const labelEntries = values.filter((v) => v.label !== void 0).map((v) => ` [${name}.${v.name}]: '${escapeString(String(v.label))}',`).join("\n");
452
524
  parts.push(`const ${lowerFirst(name)}Labels: Partial<Record<${name}, string>> = {
453
525
  ${labelEntries}
454
526
  };
@@ -504,6 +576,9 @@ ${extraEntries}
504
576
  function lowerFirst(str) {
505
577
  return str.charAt(0).toLowerCase() + str.slice(1);
506
578
  }
579
+ function escapeString(str) {
580
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
581
+ }
507
582
  function formatTypeAlias(alias) {
508
583
  const { name, type, comment } = alias;
509
584
  const parts = [];
@@ -1017,6 +1092,13 @@ function getZodSchemaForType(propDef, fieldName, customTypes) {
1017
1092
  case "Json":
1018
1093
  schema = "z.unknown()";
1019
1094
  break;
1095
+ case "EnumRef":
1096
+ if (typeof def.enum === "string") {
1097
+ schema = `z.nativeEnum(${def.enum})`;
1098
+ } else {
1099
+ schema = "z.string()";
1100
+ }
1101
+ break;
1020
1102
  case "Enum":
1021
1103
  if (typeof def.enum === "string") {
1022
1104
  schema = `${def.enum}Schema`;
@@ -1062,15 +1144,35 @@ function generateCompoundTypeSchemas(propName, propDef, customType, options) {
1062
1144
  for (const field of customType.expand) {
1063
1145
  const fieldName = `${toSnakeCase(propName)}_${toSnakeCase(field.suffix)}`;
1064
1146
  const fieldOverride = propFields?.[field.suffix];
1065
- const isNullable = fieldOverride?.nullable ?? propDef.nullable ?? false;
1066
- const length = fieldOverride?.length ?? field.sql?.length;
1147
+ const isNullable = fieldOverride?.nullable ?? field.sql?.nullable ?? propDef.nullable ?? false;
1148
+ const pluginRules = field.rules;
1149
+ const overrideRules = fieldOverride?.rules;
1150
+ const length = fieldOverride?.length ?? overrideRules?.maxLength ?? pluginRules?.maxLength ?? field.sql?.length;
1151
+ const minLength = overrideRules?.minLength ?? pluginRules?.minLength;
1152
+ const pattern = overrideRules?.pattern ?? pluginRules?.pattern;
1153
+ const format = overrideRules?.format ?? pluginRules?.format;
1067
1154
  let schema = "z.string()";
1155
+ if (format === "email") {
1156
+ schema = "z.string().email()";
1157
+ } else if (format === "url") {
1158
+ schema = "z.string().url()";
1159
+ } else if (format === "phone") {
1160
+ schema = "z.string()";
1161
+ } else if (format === "postal_code") {
1162
+ schema = `z.string().regex(/^\\d{3}-?\\d{4}$/)`;
1163
+ }
1068
1164
  if (!isNullable) {
1069
- schema += ".min(1)";
1165
+ const min = minLength ?? 1;
1166
+ schema += `.min(${min})`;
1167
+ } else if (minLength) {
1168
+ schema += `.min(${minLength})`;
1070
1169
  }
1071
1170
  if (length) {
1072
1171
  schema += `.max(${length})`;
1073
1172
  }
1173
+ if (pattern && !format) {
1174
+ schema += `.regex(/${pattern}/)`;
1175
+ }
1074
1176
  if (isNullable) {
1075
1177
  schema += ".optional().nullable()";
1076
1178
  }
@@ -1126,6 +1228,7 @@ function generateDisplayNames(schema, options) {
1126
1228
  schema.name
1127
1229
  );
1128
1230
  const propertyDisplayNames = {};
1231
+ const propertyPlaceholders = {};
1129
1232
  if (schema.properties) {
1130
1233
  for (const [propName, propDef] of Object.entries(schema.properties)) {
1131
1234
  const prop = propDef;
@@ -1133,19 +1236,47 @@ function generateDisplayNames(schema, options) {
1133
1236
  if (customTypes) {
1134
1237
  const customType = customTypes.get(propDef.type);
1135
1238
  if (customType?.compound && customType.expand) {
1136
- for (const field of customType.expand) {
1137
- const expandedFieldName = `${fieldName}_${toSnakeCase(field.suffix)}`;
1138
- propertyDisplayNames[expandedFieldName] = getMultiLocaleDisplayName2(
1239
+ if (prop.displayName) {
1240
+ propertyDisplayNames[fieldName] = getMultiLocaleDisplayName2(
1139
1241
  prop.displayName,
1140
1242
  locales,
1141
1243
  fallbackLocale,
1142
1244
  propName
1143
1245
  );
1144
- for (const locale of locales) {
1145
- propertyDisplayNames[expandedFieldName] = {
1146
- ...propertyDisplayNames[expandedFieldName],
1147
- [locale]: `${propertyDisplayNames[expandedFieldName][locale]} (${field.suffix})`
1148
- };
1246
+ }
1247
+ for (const field of customType.expand) {
1248
+ const expandedFieldName = `${fieldName}_${toSnakeCase(field.suffix)}`;
1249
+ const fieldOverride = prop.fields?.[field.suffix];
1250
+ const labelSource = fieldOverride?.displayName ?? field.label;
1251
+ if (labelSource) {
1252
+ propertyDisplayNames[expandedFieldName] = getMultiLocaleDisplayName2(
1253
+ labelSource,
1254
+ locales,
1255
+ fallbackLocale,
1256
+ field.suffix
1257
+ );
1258
+ } else {
1259
+ propertyDisplayNames[expandedFieldName] = getMultiLocaleDisplayName2(
1260
+ prop.displayName,
1261
+ locales,
1262
+ fallbackLocale,
1263
+ propName
1264
+ );
1265
+ for (const locale of locales) {
1266
+ propertyDisplayNames[expandedFieldName] = {
1267
+ ...propertyDisplayNames[expandedFieldName],
1268
+ [locale]: `${propertyDisplayNames[expandedFieldName][locale]} (${field.suffix})`
1269
+ };
1270
+ }
1271
+ }
1272
+ const placeholderSource = fieldOverride?.placeholder ?? field.placeholder;
1273
+ if (placeholderSource) {
1274
+ propertyPlaceholders[expandedFieldName] = getMultiLocaleDisplayName2(
1275
+ placeholderSource,
1276
+ locales,
1277
+ fallbackLocale,
1278
+ ""
1279
+ );
1149
1280
  }
1150
1281
  }
1151
1282
  continue;
@@ -1157,9 +1288,17 @@ function generateDisplayNames(schema, options) {
1157
1288
  fallbackLocale,
1158
1289
  propName
1159
1290
  );
1291
+ if (prop.placeholder) {
1292
+ propertyPlaceholders[fieldName] = getMultiLocaleDisplayName2(
1293
+ prop.placeholder,
1294
+ locales,
1295
+ fallbackLocale,
1296
+ ""
1297
+ );
1298
+ }
1160
1299
  }
1161
1300
  }
1162
- return { displayName, propertyDisplayNames };
1301
+ return { displayName, propertyDisplayNames, propertyPlaceholders };
1163
1302
  }
1164
1303
  function getExcludedFields(schema, customTypes) {
1165
1304
  const createExclude = /* @__PURE__ */ new Set();
@@ -1203,24 +1342,44 @@ function formatZodSchemasSection(schemaName, zodSchemas, displayNames, excludedF
1203
1342
  const lowerName = schemaName.charAt(0).toLowerCase() + schemaName.slice(1);
1204
1343
  parts.push(`// ============================================================================
1205
1344
  `);
1206
- parts.push(`// Display Names
1345
+ parts.push(`// I18n (Internationalization)
1207
1346
  `);
1208
1347
  parts.push(`// ============================================================================
1209
1348
 
1210
1349
  `);
1211
- parts.push(`/** Display name for ${schemaName} */
1350
+ parts.push(`/**
1212
1351
  `);
1213
- parts.push(`export const ${schemaName}DisplayName = ${JSON.stringify(displayNames.displayName, null, 2)} as const;
1214
-
1352
+ parts.push(` * Unified i18n object for ${schemaName}
1215
1353
  `);
1216
- parts.push(`/** Property display names for ${schemaName} */
1354
+ parts.push(` * Contains model label and all field labels/placeholders
1355
+ `);
1356
+ parts.push(` */
1357
+ `);
1358
+ parts.push(`export const ${lowerName}I18n = {
1359
+ `);
1360
+ parts.push(` /** Model display name */
1361
+ `);
1362
+ parts.push(` label: ${JSON.stringify(displayNames.displayName)},
1363
+ `);
1364
+ parts.push(` /** Field labels and placeholders */
1365
+ `);
1366
+ parts.push(` fields: {
1367
+ `);
1368
+ for (const [propName, labelMap] of Object.entries(displayNames.propertyDisplayNames)) {
1369
+ const placeholderMap = displayNames.propertyPlaceholders[propName];
1370
+ parts.push(` ${propName}: {
1371
+ `);
1372
+ parts.push(` label: ${JSON.stringify(labelMap)},
1217
1373
  `);
1218
- parts.push(`export const ${schemaName}PropertyDisplayNames = {
1374
+ if (placeholderMap) {
1375
+ parts.push(` placeholder: ${JSON.stringify(placeholderMap)},
1219
1376
  `);
1220
- for (const [propName, localeMap] of Object.entries(displayNames.propertyDisplayNames)) {
1221
- parts.push(` ${propName}: ${JSON.stringify(localeMap)},
1377
+ }
1378
+ parts.push(` },
1222
1379
  `);
1223
1380
  }
1381
+ parts.push(` },
1382
+ `);
1224
1383
  parts.push(`} as const;
1225
1384
 
1226
1385
  `);
@@ -1277,35 +1436,50 @@ function formatZodSchemasSection(schemaName, zodSchemas, displayNames, excludedF
1277
1436
  `);
1278
1437
  parts.push(`// ============================================================================
1279
1438
  `);
1280
- parts.push(`// Helper Functions
1439
+ parts.push(`// I18n Helper Functions
1281
1440
  `);
1282
1441
  parts.push(`// ============================================================================
1283
1442
 
1284
1443
  `);
1285
- parts.push(`/** Get display name for a specific locale */
1444
+ parts.push(`/** Get model label for a specific locale */
1286
1445
  `);
1287
- parts.push(`export function get${schemaName}DisplayName(locale: string): string {
1446
+ parts.push(`export function get${schemaName}Label(locale: string): string {
1288
1447
  `);
1289
- parts.push(` return ${schemaName}DisplayName[locale as keyof typeof ${schemaName}DisplayName] ?? ${schemaName}DisplayName['en'] ?? '${schemaName}';
1448
+ parts.push(` return ${lowerName}I18n.label[locale as keyof typeof ${lowerName}I18n.label] ?? ${lowerName}I18n.label['en'] ?? '${schemaName}';
1290
1449
  `);
1291
1450
  parts.push(`}
1292
1451
 
1293
1452
  `);
1294
- parts.push(`/** Get property display name for a specific locale */
1453
+ parts.push(`/** Get field label for a specific locale */
1295
1454
  `);
1296
- parts.push(`export function get${schemaName}PropertyDisplayName(property: string, locale: string): string {
1455
+ parts.push(`export function get${schemaName}FieldLabel(field: string, locale: string): string {
1456
+ `);
1457
+ parts.push(` const fieldI18n = ${lowerName}I18n.fields[field as keyof typeof ${lowerName}I18n.fields];
1458
+ `);
1459
+ parts.push(` if (!fieldI18n) return field;
1460
+ `);
1461
+ parts.push(` return fieldI18n.label[locale as keyof typeof fieldI18n.label] ?? fieldI18n.label['en'] ?? field;
1462
+ `);
1463
+ parts.push(`}
1464
+
1465
+ `);
1466
+ parts.push(`/** Get field placeholder for a specific locale */
1467
+ `);
1468
+ parts.push(`export function get${schemaName}FieldPlaceholder(field: string, locale: string): string {
1297
1469
  `);
1298
- parts.push(` const names = ${schemaName}PropertyDisplayNames[property as keyof typeof ${schemaName}PropertyDisplayNames];
1470
+ parts.push(` const fieldI18n = ${lowerName}I18n.fields[field as keyof typeof ${lowerName}I18n.fields];
1299
1471
  `);
1300
- parts.push(` if (!names) return property;
1472
+ parts.push(` if (!fieldI18n || !('placeholder' in fieldI18n)) return '';
1301
1473
  `);
1302
- parts.push(` return names[locale as keyof typeof names] ?? names['en'] ?? property;
1474
+ parts.push(` const placeholder = fieldI18n.placeholder as Record<string, string>;
1475
+ `);
1476
+ parts.push(` return placeholder[locale] ?? placeholder['en'] ?? '';
1303
1477
  `);
1304
1478
  parts.push(`}
1305
1479
  `);
1306
1480
  return parts.join("");
1307
1481
  }
1308
- function formatZodModelFile(schemaName) {
1482
+ function formatZodModelFile(schemaName, _hasPlaceholders = false) {
1309
1483
  const lowerName = schemaName.charAt(0).toLowerCase() + schemaName.slice(1);
1310
1484
  return `/**
1311
1485
  * ${schemaName} Model
@@ -1321,10 +1495,10 @@ import {
1321
1495
  base${schemaName}Schemas,
1322
1496
  base${schemaName}CreateSchema,
1323
1497
  base${schemaName}UpdateSchema,
1324
- ${schemaName}DisplayName,
1325
- ${schemaName}PropertyDisplayNames,
1326
- get${schemaName}DisplayName,
1327
- get${schemaName}PropertyDisplayName,
1498
+ ${lowerName}I18n,
1499
+ get${schemaName}Label,
1500
+ get${schemaName}FieldLabel,
1501
+ get${schemaName}FieldPlaceholder,
1328
1502
  } from './base/${schemaName}.js';
1329
1503
 
1330
1504
  // ============================================================================
@@ -1350,12 +1524,12 @@ export const ${lowerName}UpdateSchema = base${schemaName}UpdateSchema;
1350
1524
  export type ${schemaName}Create = z.infer<typeof ${lowerName}CreateSchema>;
1351
1525
  export type ${schemaName}Update = z.infer<typeof ${lowerName}UpdateSchema>;
1352
1526
 
1353
- // Re-export display names and helpers
1527
+ // Re-export i18n and helpers
1354
1528
  export {
1355
- ${schemaName}DisplayName,
1356
- ${schemaName}PropertyDisplayNames,
1357
- get${schemaName}DisplayName,
1358
- get${schemaName}PropertyDisplayName,
1529
+ ${lowerName}I18n,
1530
+ get${schemaName}Label,
1531
+ get${schemaName}FieldLabel,
1532
+ get${schemaName}FieldPlaceholder,
1359
1533
  };
1360
1534
 
1361
1535
  // Re-export base type for internal use
@@ -1471,13 +1645,24 @@ function generateBaseInterfaceFile(schemaName, schemas, options) {
1471
1645
  parts.push(`import type { ${commonImports.join(", ")} } from '../common.js';
1472
1646
  `);
1473
1647
  }
1648
+ if (iface.enumDependencies && iface.enumDependencies.length > 0) {
1649
+ const enumPrefix = options.enumImportPrefix ? `../${options.enumImportPrefix}` : "../enum";
1650
+ const pluginEnumNames = new Set(
1651
+ options.pluginEnums ? Array.from(options.pluginEnums.keys()) : []
1652
+ );
1653
+ for (const enumName of iface.enumDependencies) {
1654
+ const enumPath = pluginEnumNames.has(enumName) ? `${enumPrefix}/plugin/${enumName}.js` : `${enumPrefix}/${enumName}.js`;
1655
+ parts.push(`import { ${enumName} } from '${enumPath}';
1656
+ `);
1657
+ }
1658
+ }
1474
1659
  if (iface.dependencies && iface.dependencies.length > 0) {
1475
1660
  for (const dep of iface.dependencies) {
1476
1661
  parts.push(`import type { ${dep} } from './${dep}.js';
1477
1662
  `);
1478
1663
  }
1479
1664
  parts.push("\n");
1480
- } else if (commonImports.length > 0 || options.generateZodSchemas) {
1665
+ } else if (commonImports.length > 0 || options.generateZodSchemas || iface.enumDependencies && iface.enumDependencies.length > 0) {
1481
1666
  parts.push("\n");
1482
1667
  }
1483
1668
  parts.push(formatInterface(iface));
@@ -1498,15 +1683,17 @@ function generateBaseInterfaceFile(schemaName, schemas, options) {
1498
1683
  overwrite: true
1499
1684
  };
1500
1685
  }
1501
- function generateEnumFile(enumDef) {
1686
+ function generateEnumFile(enumDef, isPluginEnum = false) {
1502
1687
  const parts = [generateBaseHeader()];
1503
1688
  parts.push(formatEnum(enumDef));
1504
1689
  parts.push("\n");
1690
+ const filePath = isPluginEnum ? `plugin/${enumDef.name}.ts` : `${enumDef.name}.ts`;
1505
1691
  return {
1506
- filePath: `enum/${enumDef.name}.ts`,
1692
+ filePath,
1507
1693
  content: parts.join(""),
1508
1694
  types: [enumDef.name],
1509
- overwrite: true
1695
+ overwrite: true,
1696
+ category: "enum"
1510
1697
  };
1511
1698
  }
1512
1699
  function generateTypeAliasFile(alias) {
@@ -1514,10 +1701,11 @@ function generateTypeAliasFile(alias) {
1514
1701
  parts.push(formatTypeAlias(alias));
1515
1702
  parts.push("\n");
1516
1703
  return {
1517
- filePath: `enum/${alias.name}.ts`,
1704
+ filePath: `${alias.name}.ts`,
1518
1705
  content: parts.join(""),
1519
1706
  types: [alias.name],
1520
- overwrite: true
1707
+ overwrite: true,
1708
+ category: "enum"
1521
1709
  };
1522
1710
  }
1523
1711
  function generateModelFile(schemaName, options) {
@@ -1558,6 +1746,88 @@ function generateModelFile(schemaName, options) {
1558
1746
  // Never overwrite user models
1559
1747
  };
1560
1748
  }
1749
+ var DEFAULT_VALIDATION_MESSAGES = {
1750
+ required: {
1751
+ en: "${displayName} is required",
1752
+ ja: "${displayName}\u306F\u5FC5\u9808\u3067\u3059",
1753
+ vi: "${displayName} l\xE0 b\u1EAFt bu\u1ED9c",
1754
+ ko: "${displayName}\uC740(\uB294) \uD544\uC218\uC785\uB2C8\uB2E4",
1755
+ "zh-CN": "${displayName}\u662F\u5FC5\u586B\u9879",
1756
+ "zh-TW": "${displayName}\u70BA\u5FC5\u586B\u6B04\u4F4D",
1757
+ th: "${displayName} \u0E08\u0E33\u0E40\u0E1B\u0E47\u0E19\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E23\u0E2D\u0E01",
1758
+ es: "${displayName} es obligatorio"
1759
+ },
1760
+ minLength: {
1761
+ en: "${displayName} must be at least ${min} characters",
1762
+ ja: "${displayName}\u306F${min}\u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1763
+ vi: "${displayName} ph\u1EA3i c\xF3 \xEDt nh\u1EA5t ${min} k\xFD t\u1EF1",
1764
+ ko: "${displayName}\uC740(\uB294) ${min}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
1765
+ "zh-CN": "${displayName}\u81F3\u5C11\u9700\u8981${min}\u4E2A\u5B57\u7B26",
1766
+ "zh-TW": "${displayName}\u81F3\u5C11\u9700\u8981${min}\u500B\u5B57\u5143",
1767
+ th: "${displayName} \u0E15\u0E49\u0E2D\u0E07\u0E21\u0E35\u0E2D\u0E22\u0E48\u0E32\u0E07\u0E19\u0E49\u0E2D\u0E22 ${min} \u0E15\u0E31\u0E27\u0E2D\u0E31\u0E01\u0E29\u0E23",
1768
+ es: "${displayName} debe tener al menos ${min} caracteres"
1769
+ },
1770
+ maxLength: {
1771
+ en: "${displayName} must be at most ${max} characters",
1772
+ ja: "${displayName}\u306F${max}\u6587\u5B57\u4EE5\u5185\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1773
+ vi: "${displayName} kh\xF4ng \u0111\u01B0\u1EE3c qu\xE1 ${max} k\xFD t\u1EF1",
1774
+ ko: "${displayName}\uC740(\uB294) ${max}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4",
1775
+ "zh-CN": "${displayName}\u6700\u591A${max}\u4E2A\u5B57\u7B26",
1776
+ "zh-TW": "${displayName}\u6700\u591A${max}\u500B\u5B57\u5143",
1777
+ th: "${displayName} \u0E15\u0E49\u0E2D\u0E07\u0E44\u0E21\u0E48\u0E40\u0E01\u0E34\u0E19 ${max} \u0E15\u0E31\u0E27\u0E2D\u0E31\u0E01\u0E29\u0E23",
1778
+ es: "${displayName} debe tener como m\xE1ximo ${max} caracteres"
1779
+ },
1780
+ min: {
1781
+ en: "${displayName} must be at least ${min}",
1782
+ ja: "${displayName}\u306F${min}\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1783
+ vi: "${displayName} ph\u1EA3i l\u1EDBn h\u01A1n ho\u1EB7c b\u1EB1ng ${min}",
1784
+ ko: "${displayName}\uC740(\uB294) ${min} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4",
1785
+ "zh-CN": "${displayName}\u5FC5\u987B\u5927\u4E8E\u7B49\u4E8E${min}",
1786
+ "zh-TW": "${displayName}\u5FC5\u9808\u5927\u65BC\u7B49\u65BC${min}",
1787
+ th: "${displayName} \u0E15\u0E49\u0E2D\u0E07\u0E21\u0E32\u0E01\u0E01\u0E27\u0E48\u0E32\u0E2B\u0E23\u0E37\u0E2D\u0E40\u0E17\u0E48\u0E32\u0E01\u0E31\u0E1A ${min}",
1788
+ es: "${displayName} debe ser al menos ${min}"
1789
+ },
1790
+ max: {
1791
+ en: "${displayName} must be at most ${max}",
1792
+ ja: "${displayName}\u306F${max}\u4EE5\u4E0B\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1793
+ vi: "${displayName} ph\u1EA3i nh\u1ECF h\u01A1n ho\u1EB7c b\u1EB1ng ${max}",
1794
+ ko: "${displayName}\uC740(\uB294) ${max} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4",
1795
+ "zh-CN": "${displayName}\u5FC5\u987B\u5C0F\u4E8E\u7B49\u4E8E${max}",
1796
+ "zh-TW": "${displayName}\u5FC5\u9808\u5C0F\u65BC\u7B49\u65BC${max}",
1797
+ th: "${displayName} \u0E15\u0E49\u0E2D\u0E07\u0E19\u0E49\u0E2D\u0E22\u0E01\u0E27\u0E48\u0E32\u0E2B\u0E23\u0E37\u0E2D\u0E40\u0E17\u0E48\u0E32\u0E01\u0E31\u0E1A ${max}",
1798
+ es: "${displayName} debe ser como m\xE1ximo ${max}"
1799
+ },
1800
+ email: {
1801
+ en: "Please enter a valid email address",
1802
+ ja: "\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1803
+ vi: "Vui l\xF2ng nh\u1EADp \u0111\u1ECBa ch\u1EC9 email h\u1EE3p l\u1EC7",
1804
+ ko: "\uC720\uD6A8\uD55C \uC774\uBA54\uC77C \uC8FC\uC18C\uB97C \uC785\uB825\uD558\uC138\uC694",
1805
+ "zh-CN": "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740",
1806
+ "zh-TW": "\u8ACB\u8F38\u5165\u6709\u6548\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740",
1807
+ th: "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E01\u0E23\u0E2D\u0E01\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E17\u0E35\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
1808
+ es: "Por favor, introduce una direcci\xF3n de correo electr\xF3nico v\xE1lida"
1809
+ },
1810
+ url: {
1811
+ en: "Please enter a valid URL",
1812
+ ja: "\u6709\u52B9\u306AURL\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
1813
+ vi: "Vui l\xF2ng nh\u1EADp URL h\u1EE3p l\u1EC7",
1814
+ ko: "\uC720\uD6A8\uD55C URL\uC744 \uC785\uB825\uD558\uC138\uC694",
1815
+ "zh-CN": "\u8BF7\u8F93\u5165\u6709\u6548\u7684URL",
1816
+ "zh-TW": "\u8ACB\u8F38\u5165\u6709\u6548\u7684\u7DB2\u5740",
1817
+ th: "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E01\u0E23\u0E2D\u0E01 URL \u0E17\u0E35\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
1818
+ es: "Por favor, introduce una URL v\xE1lida"
1819
+ },
1820
+ pattern: {
1821
+ en: "${displayName} format is invalid",
1822
+ ja: "${displayName}\u306E\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093",
1823
+ vi: "${displayName} kh\xF4ng \u0111\xFAng \u0111\u1ECBnh d\u1EA1ng",
1824
+ ko: "${displayName} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
1825
+ "zh-CN": "${displayName}\u683C\u5F0F\u4E0D\u6B63\u786E",
1826
+ "zh-TW": "${displayName}\u683C\u5F0F\u4E0D\u6B63\u78BA",
1827
+ th: "\u0E23\u0E39\u0E1B\u0E41\u0E1A\u0E1A${displayName}\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
1828
+ es: "El formato de ${displayName} no es v\xE1lido"
1829
+ }
1830
+ };
1561
1831
  function generateCommonFile(options) {
1562
1832
  const locales = options.localeConfig?.locales ?? ["ja", "en"];
1563
1833
  const localeUnion = locales.map((l) => `'${l}'`).join(" | ");
@@ -1605,15 +1875,142 @@ export type DateString = string;
1605
1875
  overwrite: true
1606
1876
  };
1607
1877
  }
1608
- function generateIndexFile(schemas, enums, typeAliases, options) {
1878
+ function generateI18nFile(options) {
1879
+ const locales = options.localeConfig?.locales ?? ["ja", "en"];
1880
+ const defaultLocale = options.localeConfig?.defaultLocale ?? "ja";
1881
+ const fallbackLocale = options.localeConfig?.fallbackLocale ?? "en";
1882
+ const userMessages = options.localeConfig?.messages ?? {};
1883
+ const mergedMessages = {};
1884
+ for (const [key, defaultMsgs] of Object.entries(DEFAULT_VALIDATION_MESSAGES)) {
1885
+ mergedMessages[key] = {};
1886
+ for (const locale of locales) {
1887
+ if (defaultMsgs[locale]) {
1888
+ mergedMessages[key][locale] = defaultMsgs[locale];
1889
+ }
1890
+ }
1891
+ }
1892
+ for (const [key, userMsgs] of Object.entries(userMessages)) {
1893
+ if (userMsgs) {
1894
+ if (!mergedMessages[key]) {
1895
+ mergedMessages[key] = {};
1896
+ }
1897
+ for (const [locale, msg] of Object.entries(userMsgs)) {
1898
+ mergedMessages[key][locale] = msg;
1899
+ }
1900
+ }
1901
+ }
1902
+ const messagesJson = JSON.stringify(mergedMessages, null, 2);
1903
+ const content = `${generateBaseHeader()}
1904
+ import type { LocaleMap } from './common.js';
1905
+
1906
+ /**
1907
+ * Default locale for this project.
1908
+ */
1909
+ export const defaultLocale = '${defaultLocale}' as const;
1910
+
1911
+ /**
1912
+ * Fallback locale when requested locale is not found.
1913
+ */
1914
+ export const fallbackLocale = '${fallbackLocale}' as const;
1915
+
1916
+ /**
1917
+ * Supported locales in this project.
1918
+ */
1919
+ export const supportedLocales = ${JSON.stringify(locales)} as const;
1920
+
1921
+ /**
1922
+ * Validation messages for all supported locales.
1923
+ * Use getMessage(key, locale, params) to get formatted message.
1924
+ */
1925
+ export const validationMessages = ${messagesJson} as const;
1926
+
1927
+ /**
1928
+ * Get validation message for a specific key and locale.
1929
+ * Supports template placeholders: \${displayName}, \${min}, \${max}, etc.
1930
+ *
1931
+ * @param key - Message key (e.g., 'required', 'minLength')
1932
+ * @param locale - Locale code (e.g., 'ja', 'en')
1933
+ * @param params - Template parameters to replace
1934
+ * @returns Formatted message string
1935
+ *
1936
+ * @example
1937
+ * getMessage('required', 'ja', { displayName: '\u6C0F\u540D' })
1938
+ * // => '\u6C0F\u540D\u306F\u5FC5\u9808\u3067\u3059'
1939
+ */
1940
+ export function getMessage(
1941
+ key: string,
1942
+ locale: string,
1943
+ params: Record<string, string | number> = {}
1944
+ ): string {
1945
+ const messages = validationMessages[key as keyof typeof validationMessages];
1946
+ if (!messages) return key;
1947
+
1948
+ let message = (messages as LocaleMap)[locale]
1949
+ ?? (messages as LocaleMap)[fallbackLocale]
1950
+ ?? (messages as LocaleMap)[defaultLocale]
1951
+ ?? key;
1952
+
1953
+ // Replace template placeholders
1954
+ for (const [param, value] of Object.entries(params)) {
1955
+ message = message.replace(new RegExp(\`\\\\$\\{\${param}\\}\`, 'g'), String(value));
1956
+ }
1957
+
1958
+ return message;
1959
+ }
1960
+
1961
+ /**
1962
+ * Get all validation messages for a specific locale.
1963
+ *
1964
+ * @param locale - Locale code
1965
+ * @returns Object with all messages for the locale
1966
+ */
1967
+ export function getMessages(locale: string): Record<string, string> {
1968
+ const result: Record<string, string> = {};
1969
+ for (const [key, messages] of Object.entries(validationMessages)) {
1970
+ result[key] = (messages as LocaleMap)[locale]
1971
+ ?? (messages as LocaleMap)[fallbackLocale]
1972
+ ?? (messages as LocaleMap)[defaultLocale]
1973
+ ?? key;
1974
+ }
1975
+ return result;
1976
+ }
1977
+ `;
1978
+ return {
1979
+ filePath: "i18n.ts",
1980
+ content,
1981
+ types: ["validationMessages", "getMessage", "getMessages"],
1982
+ overwrite: true
1983
+ };
1984
+ }
1985
+ function generateIndexFile(schemas, enums, pluginEnums, typeAliases, options) {
1609
1986
  const parts = [generateBaseHeader()];
1610
1987
  parts.push(`// Common Types
1611
1988
  `);
1612
1989
  parts.push(`export type { LocaleMap, Locale, ValidationRule, DateTimeString, DateString } from './common.js';
1613
1990
 
1614
1991
  `);
1615
- if (enums.length > 0 || typeAliases.length > 0) {
1616
- parts.push(`// Enums
1992
+ parts.push(`// i18n (Internationalization)
1993
+ `);
1994
+ parts.push(`export {
1995
+ `);
1996
+ parts.push(` defaultLocale,
1997
+ `);
1998
+ parts.push(` fallbackLocale,
1999
+ `);
2000
+ parts.push(` supportedLocales,
2001
+ `);
2002
+ parts.push(` validationMessages,
2003
+ `);
2004
+ parts.push(` getMessage,
2005
+ `);
2006
+ parts.push(` getMessages,
2007
+ `);
2008
+ parts.push(`} from './i18n.js';
2009
+
2010
+ `);
2011
+ const enumPrefix = options.enumImportPrefix ?? "./enum";
2012
+ if (enums.length > 0) {
2013
+ parts.push(`// Schema Enums
1617
2014
  `);
1618
2015
  for (const enumDef of enums) {
1619
2016
  parts.push(`export {
@@ -1628,9 +2025,35 @@ function generateIndexFile(schemas, enums, typeAliases, options) {
1628
2025
  `);
1629
2026
  parts.push(` get${enumDef.name}Extra,
1630
2027
  `);
1631
- parts.push(`} from './enum/${enumDef.name}.js';
2028
+ parts.push(`} from '${enumPrefix}/${enumDef.name}.js';
1632
2029
  `);
1633
2030
  }
2031
+ parts.push("\n");
2032
+ }
2033
+ if (pluginEnums.length > 0) {
2034
+ parts.push(`// Plugin Enums
2035
+ `);
2036
+ for (const enumDef of pluginEnums) {
2037
+ parts.push(`export {
2038
+ `);
2039
+ parts.push(` ${enumDef.name},
2040
+ `);
2041
+ parts.push(` ${enumDef.name}Values,
2042
+ `);
2043
+ parts.push(` is${enumDef.name},
2044
+ `);
2045
+ parts.push(` get${enumDef.name}Label,
2046
+ `);
2047
+ parts.push(` get${enumDef.name}Extra,
2048
+ `);
2049
+ parts.push(`} from '${enumPrefix}/plugin/${enumDef.name}.js';
2050
+ `);
2051
+ }
2052
+ parts.push("\n");
2053
+ }
2054
+ if (typeAliases.length > 0) {
2055
+ parts.push(`// Inline Enums (Type Aliases)
2056
+ `);
1634
2057
  for (const alias of typeAliases) {
1635
2058
  parts.push(`export {
1636
2059
  `);
@@ -1644,13 +2067,13 @@ function generateIndexFile(schemas, enums, typeAliases, options) {
1644
2067
  `);
1645
2068
  parts.push(` get${alias.name}Extra,
1646
2069
  `);
1647
- parts.push(`} from './enum/${alias.name}.js';
2070
+ parts.push(`} from '${enumPrefix}/${alias.name}.js';
1648
2071
  `);
1649
2072
  }
1650
2073
  parts.push("\n");
1651
2074
  }
1652
2075
  if (options.generateZodSchemas) {
1653
- parts.push(`// Models (with Zod schemas and Create/Update types)
2076
+ parts.push(`// Models (with Zod schemas, i18n, and Create/Update types)
1654
2077
  `);
1655
2078
  for (const schema of Object.values(schemas)) {
1656
2079
  if (schema.kind === "enum") continue;
@@ -1666,13 +2089,13 @@ function generateIndexFile(schemas, enums, typeAliases, options) {
1666
2089
  `);
1667
2090
  parts.push(` ${lowerName}UpdateSchema,
1668
2091
  `);
1669
- parts.push(` ${schema.name}DisplayName,
2092
+ parts.push(` ${lowerName}I18n,
1670
2093
  `);
1671
- parts.push(` ${schema.name}PropertyDisplayNames,
2094
+ parts.push(` get${schema.name}Label,
1672
2095
  `);
1673
- parts.push(` get${schema.name}DisplayName,
2096
+ parts.push(` get${schema.name}FieldLabel,
1674
2097
  `);
1675
- parts.push(` get${schema.name}PropertyDisplayName,
2098
+ parts.push(` get${schema.name}FieldPlaceholder,
1676
2099
  `);
1677
2100
  parts.push(`} from './${schema.name}.js';
1678
2101
  `);
@@ -1718,9 +2141,47 @@ function generateIndexFile(schemas, enums, typeAliases, options) {
1718
2141
  function generateTypeScript(schemas, options = {}) {
1719
2142
  const opts = { ...DEFAULT_OPTIONS, ...options };
1720
2143
  const files = [];
2144
+ const schemasByName = /* @__PURE__ */ new Map();
2145
+ for (const schema of Object.values(schemas)) {
2146
+ const paths = schemasByName.get(schema.name) ?? [];
2147
+ paths.push(schema.relativePath ?? schema.filePath);
2148
+ schemasByName.set(schema.name, paths);
2149
+ }
2150
+ const duplicateSchemas = Array.from(schemasByName.entries()).filter(([, paths]) => paths.length > 1);
2151
+ if (duplicateSchemas.length > 0) {
2152
+ const errors = duplicateSchemas.map(
2153
+ ([name, paths]) => ` - "${name}" defined in: ${paths.join(", ")}`
2154
+ ).join("\n");
2155
+ throw new Error(
2156
+ `Duplicate schema/enum names detected. Names must be globally unique:
2157
+ ${errors}
2158
+ Hint: Rename to unique names like "Blog${duplicateSchemas[0][0]}" or "Shop${duplicateSchemas[0][0]}"`
2159
+ );
2160
+ }
2161
+ if (opts.pluginEnums && opts.pluginEnums.size > 0) {
2162
+ const conflicts = [];
2163
+ for (const schema of Object.values(schemas)) {
2164
+ if (schema.kind === "enum" && opts.pluginEnums.has(schema.name)) {
2165
+ conflicts.push(`"${schema.name}" (schema: ${schema.relativePath ?? schema.filePath}, plugin enum)`);
2166
+ }
2167
+ }
2168
+ if (conflicts.length > 0) {
2169
+ throw new Error(
2170
+ `Schema enum conflicts with plugin enum:
2171
+ - ${conflicts.join("\n - ")}
2172
+ Hint: Rename your schema enum to avoid conflict with plugin-provided enums`
2173
+ );
2174
+ }
2175
+ }
1721
2176
  const enums = generateEnums(schemas, opts);
1722
2177
  for (const enumDef of enums) {
1723
- files.push(generateEnumFile(enumDef));
2178
+ files.push(generateEnumFile(enumDef, false));
2179
+ }
2180
+ if (opts.pluginEnums && opts.pluginEnums.size > 0) {
2181
+ const pluginEnums = generatePluginEnums(opts.pluginEnums, opts);
2182
+ for (const enumDef of pluginEnums) {
2183
+ files.push(generateEnumFile(enumDef, true));
2184
+ }
1724
2185
  }
1725
2186
  const typeAliases = extractInlineEnums(schemas, opts);
1726
2187
  for (const alias of typeAliases) {
@@ -1741,11 +2202,19 @@ function generateTypeScript(schemas, options = {}) {
1741
2202
  files.push(...rulesFiles);
1742
2203
  }
1743
2204
  files.push(generateCommonFile(opts));
1744
- files.push(generateIndexFile(schemas, enums, typeAliases, opts));
2205
+ files.push(generateI18nFile(opts));
2206
+ const pluginEnumsList = opts.pluginEnums ? generatePluginEnums(opts.pluginEnums, opts) : [];
2207
+ files.push(generateIndexFile(schemas, enums, pluginEnumsList, typeAliases, opts));
1745
2208
  return files;
1746
2209
  }
1747
2210
 
1748
2211
  // src/plugin.ts
2212
+ var import_fs = __toESM(require("fs"), 1);
2213
+ var import_path = __toESM(require("path"), 1);
2214
+ var import_url = require("url");
2215
+ var import_meta = {};
2216
+ var __filename = (0, import_url.fileURLToPath)(import_meta.url);
2217
+ var __dirname = import_path.default.dirname(__filename);
1749
2218
  var TYPESCRIPT_CONFIG_SCHEMA = {
1750
2219
  fields: [
1751
2220
  {
@@ -1763,15 +2232,38 @@ var TYPESCRIPT_CONFIG_SCHEMA = {
1763
2232
  description: "Generate Zod schemas alongside TypeScript types for form validation",
1764
2233
  default: true,
1765
2234
  group: "output"
2235
+ },
2236
+ {
2237
+ key: "stubsPath",
2238
+ type: "path",
2239
+ label: "React Stubs Path",
2240
+ description: "Directory for React utility stubs (hooks, components). Leave empty to disable.",
2241
+ default: "omnify",
2242
+ group: "output"
1766
2243
  }
1767
2244
  ]
1768
2245
  };
1769
2246
  function resolveOptions(options) {
1770
2247
  return {
1771
2248
  modelsPath: options?.modelsPath ?? "types/schemas",
1772
- generateZodSchemas: options?.generateZodSchemas ?? true
2249
+ generateZodSchemas: options?.generateZodSchemas ?? true,
2250
+ stubsPath: options?.stubsPath ?? "omnify"
1773
2251
  };
1774
2252
  }
2253
+ var STUB_FILES = [
2254
+ {
2255
+ stub: "CompoundField.tsx.stub",
2256
+ output: "components/CompoundField.tsx"
2257
+ },
2258
+ {
2259
+ stub: "use-form-mutation.ts.stub",
2260
+ output: "hooks/use-form-mutation.ts"
2261
+ },
2262
+ {
2263
+ stub: "form-validation.ts.stub",
2264
+ output: "lib/form-validation.ts"
2265
+ }
2266
+ ];
1775
2267
  function typescriptPlugin(options) {
1776
2268
  const resolved = resolveOptions(options);
1777
2269
  return {
@@ -1786,17 +2278,74 @@ function typescriptPlugin(options) {
1786
2278
  const files = generateTypeScript(ctx.schemas, {
1787
2279
  generateZodSchemas: resolved.generateZodSchemas,
1788
2280
  localeConfig: ctx.localeConfig,
1789
- customTypes: ctx.customTypes
2281
+ customTypes: ctx.customTypes,
2282
+ pluginEnums: ctx.pluginEnums
1790
2283
  });
1791
- return files.map((file) => ({
1792
- path: `${resolved.modelsPath}/${file.filePath}`,
1793
- content: file.content,
1794
- type: "type",
1795
- overwrite: file.overwrite,
1796
- metadata: {
1797
- types: file.types
2284
+ return files.map((file) => {
2285
+ let outputPath;
2286
+ if (file.category === "enum") {
2287
+ const enumPath = resolved.modelsPath.replace(/\/schemas\/?$/, "/enum");
2288
+ outputPath = `${enumPath}/${file.filePath}`;
2289
+ } else {
2290
+ outputPath = `${resolved.modelsPath}/${file.filePath}`;
1798
2291
  }
1799
- }));
2292
+ return {
2293
+ path: outputPath,
2294
+ content: file.content,
2295
+ type: "type",
2296
+ skipIfExists: !file.overwrite,
2297
+ // Invert: overwrite=true means skipIfExists=false
2298
+ metadata: {
2299
+ types: file.types
2300
+ }
2301
+ };
2302
+ });
2303
+ }
2304
+ },
2305
+ {
2306
+ name: "typescript-stubs",
2307
+ description: "Generate React utility stubs (hooks, components)",
2308
+ generate: async () => {
2309
+ if (resolved.stubsPath === false) {
2310
+ return [];
2311
+ }
2312
+ const outputs = [];
2313
+ const stubsDir = import_path.default.join(__dirname, "..", "stubs");
2314
+ for (const { stub, output } of STUB_FILES) {
2315
+ const stubPath = import_path.default.join(stubsDir, stub);
2316
+ if (import_fs.default.existsSync(stubPath)) {
2317
+ const content = import_fs.default.readFileSync(stubPath, "utf-8");
2318
+ outputs.push({
2319
+ path: `${resolved.stubsPath}/${output}`,
2320
+ content,
2321
+ type: "other",
2322
+ skipIfExists: true
2323
+ // Never overwrite - user can customize
2324
+ });
2325
+ }
2326
+ }
2327
+ outputs.push({
2328
+ path: `${resolved.stubsPath}/components/index.ts`,
2329
+ content: `export { CompoundField, type FieldConfig } from './CompoundField';
2330
+ `,
2331
+ type: "other",
2332
+ skipIfExists: true
2333
+ });
2334
+ outputs.push({
2335
+ path: `${resolved.stubsPath}/hooks/index.ts`,
2336
+ content: `export { useFormMutation } from './use-form-mutation';
2337
+ `,
2338
+ type: "other",
2339
+ skipIfExists: true
2340
+ });
2341
+ outputs.push({
2342
+ path: `${resolved.stubsPath}/lib/index.ts`,
2343
+ content: `export { zodRule, requiredRule } from './form-validation';
2344
+ `,
2345
+ type: "other",
2346
+ skipIfExists: true
2347
+ });
2348
+ return outputs;
1800
2349
  }
1801
2350
  }
1802
2351
  ]