@formspec/build 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (108) hide show
  1. package/README.md +138 -0
  2. package/dist/__tests__/analyzer-edge-cases.test.d.ts +13 -0
  3. package/dist/__tests__/analyzer-edge-cases.test.d.ts.map +1 -0
  4. package/dist/__tests__/analyzer.test.d.ts +5 -0
  5. package/dist/__tests__/analyzer.test.d.ts.map +1 -0
  6. package/dist/__tests__/codegen.test.d.ts +5 -0
  7. package/dist/__tests__/codegen.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorator-pipeline.test.d.ts +11 -0
  9. package/dist/__tests__/decorator-pipeline.test.d.ts.map +1 -0
  10. package/dist/__tests__/fixtures/edge-cases.d.ts +110 -0
  11. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/example-a-builtins.d.ts +12 -0
  13. package/dist/__tests__/fixtures/example-a-builtins.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures/example-b-decorators.d.ts +5 -0
  15. package/dist/__tests__/fixtures/example-b-decorators.d.ts.map +1 -0
  16. package/dist/__tests__/fixtures/example-b-extended.d.ts +5 -0
  17. package/dist/__tests__/fixtures/example-b-extended.d.ts.map +1 -0
  18. package/dist/__tests__/fixtures/example-c-custom.d.ts +5 -0
  19. package/dist/__tests__/fixtures/example-c-custom.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/example-c-decorators.d.ts +5 -0
  21. package/dist/__tests__/fixtures/example-c-decorators.d.ts.map +1 -0
  22. package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts +6 -0
  23. package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts.map +1 -0
  24. package/dist/__tests__/fixtures/example-e-decorators.d.ts +11 -0
  25. package/dist/__tests__/fixtures/example-e-decorators.d.ts.map +1 -0
  26. package/dist/__tests__/fixtures/example-e-no-namespace.d.ts +5 -0
  27. package/dist/__tests__/fixtures/example-e-no-namespace.d.ts.map +1 -0
  28. package/dist/__tests__/fixtures/example-interface-types.d.ts +102 -0
  29. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -0
  30. package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts +16 -0
  31. package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts.map +1 -0
  32. package/dist/__tests__/fixtures/example-nested-class.d.ts +45 -0
  33. package/dist/__tests__/fixtures/example-nested-class.d.ts.map +1 -0
  34. package/dist/__tests__/fixtures/sample-forms.d.ts +55 -0
  35. package/dist/__tests__/fixtures/sample-forms.d.ts.map +1 -0
  36. package/dist/__tests__/interface-types.test.d.ts +11 -0
  37. package/dist/__tests__/interface-types.test.d.ts.map +1 -0
  38. package/dist/__tests__/jsdoc-constraints.test.d.ts +10 -0
  39. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -0
  40. package/dist/analyzer/class-analyzer.d.ts +139 -0
  41. package/dist/analyzer/class-analyzer.d.ts.map +1 -0
  42. package/dist/analyzer/decorator-extractor.d.ts +78 -0
  43. package/dist/analyzer/decorator-extractor.d.ts.map +1 -0
  44. package/dist/analyzer/jsdoc-constraints.d.ts +47 -0
  45. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -0
  46. package/dist/analyzer/program.d.ts +53 -0
  47. package/dist/analyzer/program.d.ts.map +1 -0
  48. package/dist/analyzer/type-converter.d.ts +75 -0
  49. package/dist/analyzer/type-converter.d.ts.map +1 -0
  50. package/dist/browser.cjs +549 -0
  51. package/dist/browser.cjs.map +1 -0
  52. package/dist/browser.d.ts +56 -0
  53. package/dist/browser.d.ts.map +1 -0
  54. package/dist/browser.js +501 -0
  55. package/dist/browser.js.map +1 -0
  56. package/dist/build.d.ts +890 -0
  57. package/dist/cli.cjs +2267 -0
  58. package/dist/cli.cjs.map +1 -0
  59. package/dist/cli.js +2204 -103
  60. package/dist/cli.js.map +1 -1
  61. package/dist/codegen/index.d.ts +75 -0
  62. package/dist/codegen/index.d.ts.map +1 -0
  63. package/dist/generators/class-schema.d.ts +114 -0
  64. package/dist/generators/class-schema.d.ts.map +1 -0
  65. package/dist/generators/method-schema.d.ts +67 -0
  66. package/dist/generators/method-schema.d.ts.map +1 -0
  67. package/dist/index.cjs +2093 -0
  68. package/dist/index.cjs.map +1 -0
  69. package/dist/index.d.ts +11 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2024 -104
  72. package/dist/index.js.map +1 -1
  73. package/dist/internals.cjs +1345 -0
  74. package/dist/internals.cjs.map +1 -0
  75. package/dist/internals.d.ts +21 -0
  76. package/dist/internals.d.ts.map +1 -0
  77. package/dist/internals.js +1298 -0
  78. package/dist/internals.js.map +1 -0
  79. package/dist/json-schema/generator.d.ts.map +1 -1
  80. package/dist/json-schema/schema.d.ts +16 -0
  81. package/dist/json-schema/schema.d.ts.map +1 -0
  82. package/dist/json-schema/types.d.ts +28 -0
  83. package/dist/json-schema/types.d.ts.map +1 -1
  84. package/dist/ui-schema/generator.d.ts +15 -0
  85. package/dist/ui-schema/generator.d.ts.map +1 -1
  86. package/dist/ui-schema/schema.d.ts +357 -0
  87. package/dist/ui-schema/schema.d.ts.map +1 -0
  88. package/dist/ui-schema/types.d.ts +8 -73
  89. package/dist/ui-schema/types.d.ts.map +1 -1
  90. package/package.json +25 -7
  91. package/dist/__tests__/cli.test.js +0 -178
  92. package/dist/__tests__/cli.test.js.map +0 -1
  93. package/dist/__tests__/edge-cases.test.js +0 -209
  94. package/dist/__tests__/edge-cases.test.js.map +0 -1
  95. package/dist/__tests__/generator.test.js +0 -208
  96. package/dist/__tests__/generator.test.js.map +0 -1
  97. package/dist/__tests__/integration.test.js +0 -163
  98. package/dist/__tests__/integration.test.js.map +0 -1
  99. package/dist/__tests__/write-schemas.test.js +0 -196
  100. package/dist/__tests__/write-schemas.test.js.map +0 -1
  101. package/dist/json-schema/generator.js +0 -146
  102. package/dist/json-schema/generator.js.map +0 -1
  103. package/dist/json-schema/types.js +0 -7
  104. package/dist/json-schema/types.js.map +0 -1
  105. package/dist/ui-schema/generator.js +0 -150
  106. package/dist/ui-schema/generator.js.map +0 -1
  107. package/dist/ui-schema/types.js +0 -8
  108. package/dist/ui-schema/types.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,109 +1,2029 @@
1
- /**
2
- * `@formspec/build` - Build tools for FormSpec
3
- *
4
- * This package provides generators to compile FormSpec forms into:
5
- * - JSON Schema (for validation)
6
- * - JSON Forms UI Schema (for rendering)
7
- *
8
- * @example
9
- * ```typescript
10
- * import { buildFormSchemas } from "@formspec/build";
11
- * import { formspec, field, group } from "@formspec/dsl";
12
- *
13
- * const form = formspec(
14
- * group("Customer",
15
- * field.text("name", { label: "Name", required: true }),
16
- * field.text("email", { label: "Email" }),
17
- * ),
18
- * );
19
- *
20
- * const { jsonSchema, uiSchema } = buildFormSchemas(form);
21
- * ```
22
- *
23
- * @packageDocumentation
24
- */
25
- import { generateJsonSchema } from "./json-schema/generator.js";
26
- import { generateUiSchema } from "./ui-schema/generator.js";
27
- import * as fs from "node:fs";
28
- import * as path from "node:path";
29
- // Re-export individual generators
30
- export { generateJsonSchema } from "./json-schema/generator.js";
31
- export { generateUiSchema } from "./ui-schema/generator.js";
32
- /**
33
- * Builds both JSON Schema and UI Schema from a FormSpec.
34
- *
35
- * This is a convenience function that combines `generateJsonSchema`
36
- * and `generateUiSchema`.
37
- *
38
- * @example
39
- * ```typescript
40
- * const form = formspec(
41
- * field.text("name", { required: true }),
42
- * field.number("age", { min: 0 }),
43
- * );
44
- *
45
- * const { jsonSchema, uiSchema } = buildFormSchemas(form);
46
- *
47
- * // Use with JSON Forms renderer
48
- * <JsonForms
49
- * schema={jsonSchema}
50
- * uischema={uiSchema}
51
- * data={formData}
52
- * renderers={materialRenderers}
53
- * />
54
- * ```
55
- *
56
- * @param form - The FormSpec to build schemas from
57
- * @returns Object containing both jsonSchema and uiSchema
58
- */
59
- export function buildFormSchemas(form) {
1
+ // src/json-schema/schema.ts
2
+ import { z } from "zod";
3
+ var jsonSchemaTypeSchema = z.enum([
4
+ "string",
5
+ "number",
6
+ "integer",
7
+ "boolean",
8
+ "object",
9
+ "array",
10
+ "null"
11
+ ]);
12
+ var jsonSchema7Schema = z.lazy(
13
+ () => z.object({
14
+ $schema: z.string().optional(),
15
+ $id: z.string().optional(),
16
+ $ref: z.string().optional(),
17
+ // Metadata
18
+ title: z.string().optional(),
19
+ description: z.string().optional(),
20
+ deprecated: z.boolean().optional(),
21
+ // Type
22
+ type: z.union([jsonSchemaTypeSchema, z.array(jsonSchemaTypeSchema)]).optional(),
23
+ // String validation
24
+ minLength: z.number().optional(),
25
+ maxLength: z.number().optional(),
26
+ pattern: z.string().optional(),
27
+ // Number validation
28
+ minimum: z.number().optional(),
29
+ maximum: z.number().optional(),
30
+ exclusiveMinimum: z.number().optional(),
31
+ exclusiveMaximum: z.number().optional(),
32
+ // Enum
33
+ enum: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).readonly().optional(),
34
+ const: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
35
+ // Object
36
+ properties: z.record(z.string(), jsonSchema7Schema).optional(),
37
+ required: z.array(z.string()).optional(),
38
+ additionalProperties: z.union([z.boolean(), jsonSchema7Schema]).optional(),
39
+ // Array
40
+ items: z.union([jsonSchema7Schema, z.array(jsonSchema7Schema)]).optional(),
41
+ minItems: z.number().optional(),
42
+ maxItems: z.number().optional(),
43
+ // Composition
44
+ allOf: z.array(jsonSchema7Schema).optional(),
45
+ anyOf: z.array(jsonSchema7Schema).optional(),
46
+ oneOf: z.array(jsonSchema7Schema).optional(),
47
+ not: jsonSchema7Schema.optional(),
48
+ // Conditional
49
+ if: jsonSchema7Schema.optional(),
50
+ then: jsonSchema7Schema.optional(),
51
+ else: jsonSchema7Schema.optional(),
52
+ // Format
53
+ format: z.string().optional(),
54
+ // Default
55
+ default: z.unknown().optional(),
56
+ // FormSpec extensions
57
+ "x-formspec-source": z.string().optional(),
58
+ "x-formspec-params": z.array(z.string()).readonly().optional(),
59
+ "x-formspec-schemaSource": z.string().optional()
60
+ }).passthrough()
61
+ );
62
+
63
+ // src/json-schema/generator.ts
64
+ import { z as z2 } from "zod";
65
+ function parseOrThrow(schema, value, label) {
66
+ try {
67
+ return schema.parse(value);
68
+ } catch (error) {
69
+ if (error instanceof z2.ZodError) {
70
+ throw new Error(
71
+ `Generated ${label} failed validation:
72
+ ${error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
73
+ );
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+ function generateNestedSchema(elements) {
79
+ const properties = {};
80
+ const required = [];
81
+ collectFields(elements, properties, required);
82
+ const uniqueRequired = [...new Set(required)];
83
+ return {
84
+ type: "object",
85
+ properties,
86
+ ...uniqueRequired.length > 0 && { required: uniqueRequired }
87
+ };
88
+ }
89
+ function fieldToJsonSchema(field) {
90
+ const base = {};
91
+ if (field.label !== void 0) {
92
+ base.title = field.label;
93
+ }
94
+ switch (field._field) {
95
+ case "text":
96
+ return { ...base, type: "string" };
97
+ case "number":
98
+ return {
99
+ ...base,
100
+ type: "number",
101
+ ...field.min !== void 0 && { minimum: field.min },
102
+ ...field.max !== void 0 && { maximum: field.max }
103
+ };
104
+ case "boolean":
105
+ return { ...base, type: "boolean" };
106
+ case "enum": {
107
+ const opts = field.options;
108
+ const isObjectOptions = opts.length > 0 && opts.every(
109
+ (opt) => typeof opt === "object" && "id" in opt && "label" in opt
110
+ );
111
+ if (isObjectOptions) {
112
+ return {
113
+ ...base,
114
+ type: "string",
115
+ oneOf: opts.map((o) => ({
116
+ const: o.id,
117
+ title: o.label
118
+ }))
119
+ };
120
+ }
121
+ return { ...base, type: "string", enum: opts };
122
+ }
123
+ case "dynamic_enum":
124
+ return {
125
+ ...base,
126
+ type: "string",
127
+ "x-formspec-source": field.source,
128
+ ...field.params !== void 0 && field.params.length > 0 && { "x-formspec-params": field.params }
129
+ };
130
+ case "dynamic_schema":
131
+ return {
132
+ ...base,
133
+ type: "object",
134
+ additionalProperties: true,
135
+ "x-formspec-schemaSource": field.schemaSource
136
+ };
137
+ case "array": {
138
+ const arrayField = field;
139
+ return {
140
+ ...base,
141
+ type: "array",
142
+ items: generateNestedSchema(arrayField.items),
143
+ ...arrayField.minItems !== void 0 && { minItems: arrayField.minItems },
144
+ ...arrayField.maxItems !== void 0 && { maxItems: arrayField.maxItems }
145
+ };
146
+ }
147
+ case "object": {
148
+ const objectField = field;
149
+ const nestedSchema = generateNestedSchema(objectField.properties);
150
+ return {
151
+ ...base,
152
+ ...nestedSchema
153
+ };
154
+ }
155
+ default: {
156
+ const _exhaustive = field;
157
+ return _exhaustive;
158
+ }
159
+ }
160
+ }
161
+ function collectFields(elements, properties, required) {
162
+ for (const element of elements) {
163
+ switch (element._type) {
164
+ case "field":
165
+ properties[element.name] = fieldToJsonSchema(element);
166
+ if (element.required === true) {
167
+ required.push(element.name);
168
+ }
169
+ break;
170
+ case "group":
171
+ collectFields(element.elements, properties, required);
172
+ break;
173
+ case "conditional":
174
+ collectFields(
175
+ element.elements,
176
+ properties,
177
+ required
178
+ );
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ function generateJsonSchema(form) {
184
+ const properties = {};
185
+ const required = [];
186
+ collectFields(form.elements, properties, required);
187
+ const uniqueRequired = [...new Set(required)];
188
+ const result = {
189
+ $schema: "https://json-schema.org/draft-07/schema#",
190
+ type: "object",
191
+ properties,
192
+ ...uniqueRequired.length > 0 && { required: uniqueRequired }
193
+ };
194
+ return parseOrThrow(jsonSchema7Schema, result, "JSON Schema");
195
+ }
196
+
197
+ // src/ui-schema/schema.ts
198
+ import { z as z3 } from "zod";
199
+ var jsonPointerSchema = z3.string();
200
+ var ruleEffectSchema = z3.enum(["SHOW", "HIDE", "ENABLE", "DISABLE"]);
201
+ var uiSchemaElementTypeSchema = z3.enum([
202
+ "Control",
203
+ "VerticalLayout",
204
+ "HorizontalLayout",
205
+ "Group",
206
+ "Categorization",
207
+ "Category",
208
+ "Label"
209
+ ]);
210
+ var ruleConditionSchema = z3.lazy(
211
+ () => z3.object({
212
+ const: z3.unknown().optional(),
213
+ enum: z3.array(z3.unknown()).readonly().optional(),
214
+ type: z3.string().optional(),
215
+ not: ruleConditionSchema.optional(),
216
+ minimum: z3.number().optional(),
217
+ maximum: z3.number().optional(),
218
+ exclusiveMinimum: z3.number().optional(),
219
+ exclusiveMaximum: z3.number().optional(),
220
+ minLength: z3.number().optional(),
221
+ properties: z3.record(z3.string(), ruleConditionSchema).optional(),
222
+ required: z3.array(z3.string()).optional(),
223
+ allOf: z3.array(ruleConditionSchema).optional()
224
+ }).strict()
225
+ );
226
+ var schemaBasedConditionSchema = z3.object({
227
+ scope: jsonPointerSchema,
228
+ schema: ruleConditionSchema
229
+ }).strict();
230
+ var ruleSchema = z3.object({
231
+ effect: ruleEffectSchema,
232
+ condition: schemaBasedConditionSchema
233
+ }).strict();
234
+ var uiSchemaElementSchema = z3.lazy(
235
+ () => z3.union([
236
+ controlSchema,
237
+ verticalLayoutSchema,
238
+ horizontalLayoutSchema,
239
+ groupLayoutSchema,
240
+ categorizationSchema,
241
+ categorySchema,
242
+ labelElementSchema
243
+ ])
244
+ );
245
+ var controlSchema = z3.object({
246
+ type: z3.literal("Control"),
247
+ scope: jsonPointerSchema,
248
+ label: z3.union([z3.string(), z3.literal(false)]).optional(),
249
+ rule: ruleSchema.optional(),
250
+ options: z3.record(z3.string(), z3.unknown()).optional()
251
+ }).passthrough();
252
+ var verticalLayoutSchema = z3.lazy(
253
+ () => z3.object({
254
+ type: z3.literal("VerticalLayout"),
255
+ elements: z3.array(uiSchemaElementSchema),
256
+ rule: ruleSchema.optional(),
257
+ options: z3.record(z3.string(), z3.unknown()).optional()
258
+ }).passthrough()
259
+ );
260
+ var horizontalLayoutSchema = z3.lazy(
261
+ () => z3.object({
262
+ type: z3.literal("HorizontalLayout"),
263
+ elements: z3.array(uiSchemaElementSchema),
264
+ rule: ruleSchema.optional(),
265
+ options: z3.record(z3.string(), z3.unknown()).optional()
266
+ }).passthrough()
267
+ );
268
+ var groupLayoutSchema = z3.lazy(
269
+ () => z3.object({
270
+ type: z3.literal("Group"),
271
+ label: z3.string(),
272
+ elements: z3.array(uiSchemaElementSchema),
273
+ rule: ruleSchema.optional(),
274
+ options: z3.record(z3.string(), z3.unknown()).optional()
275
+ }).passthrough()
276
+ );
277
+ var categorySchema = z3.lazy(
278
+ () => z3.object({
279
+ type: z3.literal("Category"),
280
+ label: z3.string(),
281
+ elements: z3.array(uiSchemaElementSchema),
282
+ rule: ruleSchema.optional(),
283
+ options: z3.record(z3.string(), z3.unknown()).optional()
284
+ }).passthrough()
285
+ );
286
+ var categorizationSchema = z3.lazy(
287
+ () => z3.object({
288
+ type: z3.literal("Categorization"),
289
+ elements: z3.array(categorySchema),
290
+ label: z3.string().optional(),
291
+ rule: ruleSchema.optional(),
292
+ options: z3.record(z3.string(), z3.unknown()).optional()
293
+ }).passthrough()
294
+ );
295
+ var labelElementSchema = z3.object({
296
+ type: z3.literal("Label"),
297
+ text: z3.string(),
298
+ rule: ruleSchema.optional(),
299
+ options: z3.record(z3.string(), z3.unknown()).optional()
300
+ }).passthrough();
301
+ var uiSchema = z3.lazy(
302
+ () => z3.union([verticalLayoutSchema, horizontalLayoutSchema, groupLayoutSchema, categorizationSchema])
303
+ );
304
+
305
+ // src/ui-schema/generator.ts
306
+ import { z as z4 } from "zod";
307
+ function parseOrThrow2(schema, value, label) {
308
+ try {
309
+ return schema.parse(value);
310
+ } catch (error) {
311
+ if (error instanceof z4.ZodError) {
312
+ throw new Error(
313
+ `Generated ${label} failed validation:
314
+ ${error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
315
+ );
316
+ }
317
+ throw error;
318
+ }
319
+ }
320
+ function fieldToScope(fieldName) {
321
+ return `#/properties/${fieldName}`;
322
+ }
323
+ function createShowRule(fieldName, value) {
324
+ return {
325
+ effect: "SHOW",
326
+ condition: {
327
+ scope: fieldToScope(fieldName),
328
+ schema: { const: value }
329
+ }
330
+ };
331
+ }
332
+ function combineRules(parentRule, childRule) {
333
+ const parentCondition = parentRule.condition;
334
+ const childCondition = childRule.condition;
335
+ return {
336
+ effect: "SHOW",
337
+ condition: {
338
+ scope: "#",
339
+ schema: {
340
+ allOf: [
341
+ {
342
+ properties: {
343
+ [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
344
+ }
345
+ },
346
+ {
347
+ properties: {
348
+ [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
349
+ }
350
+ }
351
+ ]
352
+ }
353
+ }
354
+ };
355
+ }
356
+ function elementsToUiSchema(elements, parentRule) {
357
+ const result = [];
358
+ for (const element of elements) {
359
+ switch (element._type) {
360
+ case "field": {
361
+ const control = {
362
+ type: "Control",
363
+ scope: fieldToScope(element.name),
364
+ ...element.label !== void 0 && { label: element.label },
365
+ ...parentRule !== void 0 && { rule: parentRule }
366
+ };
367
+ result.push(control);
368
+ break;
369
+ }
370
+ case "group": {
371
+ const groupElement = element;
372
+ const group = {
373
+ type: "Group",
374
+ label: groupElement.label,
375
+ elements: elementsToUiSchema(groupElement.elements, parentRule),
376
+ ...parentRule !== void 0 && { rule: parentRule }
377
+ };
378
+ result.push(group);
379
+ break;
380
+ }
381
+ case "conditional": {
382
+ const conditionalElement = element;
383
+ const newRule = createShowRule(conditionalElement.field, conditionalElement.value);
384
+ const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
385
+ const childElements = elementsToUiSchema(conditionalElement.elements, combinedRule);
386
+ result.push(...childElements);
387
+ break;
388
+ }
389
+ }
390
+ }
391
+ return result;
392
+ }
393
+ function formSpecFieldToElement(field, scopePrefix = "#/properties") {
394
+ const control = {
395
+ type: "Control",
396
+ scope: `${scopePrefix}/${field.id}`
397
+ };
398
+ if (field.label !== void 0) {
399
+ control.label = field.label;
400
+ }
401
+ if (field.showWhen !== void 0 && typeof field.showWhen === "object" && "field" in field.showWhen && "value" in field.showWhen) {
402
+ const sw = field.showWhen;
403
+ control.rule = {
404
+ effect: "SHOW",
405
+ condition: {
406
+ scope: `#/properties/${sw.field}`,
407
+ schema: { const: sw.value }
408
+ }
409
+ };
410
+ }
411
+ return control;
412
+ }
413
+ function generateUiSchemaFromFields(fields) {
414
+ const groupMap = /* @__PURE__ */ new Map();
415
+ const orderedKeys = [];
416
+ const ungrouped = [];
417
+ for (const field of fields) {
418
+ const element = formSpecFieldToElement(field);
419
+ if (field.group !== void 0) {
420
+ if (!groupMap.has(field.group)) {
421
+ groupMap.set(field.group, []);
422
+ orderedKeys.push(field.group);
423
+ }
424
+ groupMap.get(field.group).push(element);
425
+ } else {
426
+ orderedKeys.push(null);
427
+ ungrouped.push(element);
428
+ }
429
+ }
430
+ const elements = [];
431
+ let ungroupedIndex = 0;
432
+ for (const key of orderedKeys) {
433
+ if (key === null) {
434
+ const el = ungrouped[ungroupedIndex++];
435
+ if (el !== void 0) {
436
+ elements.push(el);
437
+ }
438
+ } else {
439
+ const groupElements = groupMap.get(key) ?? [];
440
+ const groupLayout = {
441
+ type: "Group",
442
+ label: key,
443
+ elements: groupElements
444
+ };
445
+ elements.push(groupLayout);
446
+ }
447
+ }
448
+ const result = {
449
+ type: "VerticalLayout",
450
+ elements
451
+ };
452
+ return parseOrThrow2(uiSchema, result, "UI Schema");
453
+ }
454
+ function generateUiSchema(form) {
455
+ const result = {
456
+ type: "VerticalLayout",
457
+ elements: elementsToUiSchema(form.elements)
458
+ };
459
+ return parseOrThrow2(uiSchema, result, "UI Schema");
460
+ }
461
+
462
+ // src/index.ts
463
+ import * as fs2 from "fs";
464
+ import * as path3 from "path";
465
+
466
+ // src/json-schema/types.ts
467
+ function setSchemaExtension(schema, key, value) {
468
+ schema[key] = value;
469
+ }
470
+ function getSchemaExtension(schema, key) {
471
+ return schema[key];
472
+ }
473
+
474
+ // src/analyzer/type-converter.ts
475
+ import * as ts4 from "typescript";
476
+
477
+ // src/analyzer/class-analyzer.ts
478
+ import * as ts3 from "typescript";
479
+
480
+ // src/analyzer/decorator-extractor.ts
481
+ import * as ts from "typescript";
482
+ import "@formspec/core";
483
+ function extractDecorators(member) {
484
+ const decorators = [];
485
+ const modifiers = ts.canHaveDecorators(member) ? ts.getDecorators(member) : void 0;
486
+ if (!modifiers) return decorators;
487
+ for (const decorator of modifiers) {
488
+ const info = parseDecorator(decorator);
489
+ if (info) {
490
+ decorators.push(info);
491
+ }
492
+ }
493
+ return decorators;
494
+ }
495
+ function parseDecorator(decorator) {
496
+ const expr = decorator.expression;
497
+ if (ts.isIdentifier(expr)) {
498
+ return {
499
+ name: expr.text,
500
+ args: [],
501
+ node: decorator
502
+ };
503
+ }
504
+ if (ts.isCallExpression(expr)) {
505
+ const callee = expr.expression;
506
+ let name = null;
507
+ if (ts.isIdentifier(callee)) {
508
+ name = callee.text;
509
+ } else if (ts.isPropertyAccessExpression(callee)) {
510
+ name = callee.name.text;
511
+ }
512
+ if (!name) return null;
513
+ const args = expr.arguments.map(extractArgValue);
514
+ return {
515
+ name,
516
+ args,
517
+ node: decorator
518
+ };
519
+ }
520
+ return null;
521
+ }
522
+ function extractArgValue(node) {
523
+ if (ts.isStringLiteral(node)) {
524
+ return node.text;
525
+ }
526
+ if (ts.isNumericLiteral(node)) {
527
+ return Number(node.text);
528
+ }
529
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
530
+ return true;
531
+ }
532
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
533
+ return false;
534
+ }
535
+ if (node.kind === ts.SyntaxKind.NullKeyword) {
536
+ return null;
537
+ }
538
+ if (ts.isPrefixUnaryExpression(node)) {
539
+ if (node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
540
+ return -Number(node.operand.text);
541
+ }
542
+ if (node.operator === ts.SyntaxKind.PlusToken && ts.isNumericLiteral(node.operand)) {
543
+ return Number(node.operand.text);
544
+ }
545
+ }
546
+ if (ts.isArrayLiteralExpression(node)) {
547
+ return node.elements.map((el) => {
548
+ if (ts.isSpreadElement(el)) {
549
+ return null;
550
+ }
551
+ return extractArgValue(el);
552
+ });
553
+ }
554
+ if (ts.isObjectLiteralExpression(node)) {
555
+ const obj = {};
556
+ for (const prop of node.properties) {
557
+ if (ts.isPropertyAssignment(prop)) {
558
+ const key = getPropertyName(prop.name);
559
+ if (key) {
560
+ obj[key] = extractArgValue(prop.initializer);
561
+ }
562
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
563
+ const key = prop.name.text;
564
+ obj[key] = null;
565
+ }
566
+ }
567
+ return obj;
568
+ }
569
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
570
+ return node.text;
571
+ }
572
+ if (ts.isRegularExpressionLiteral(node)) {
573
+ const regexText = node.text;
574
+ const lastSlash = regexText.lastIndexOf("/");
575
+ if (lastSlash > 0) {
576
+ return regexText.substring(1, lastSlash);
577
+ }
578
+ return regexText;
579
+ }
580
+ if (ts.isNewExpression(node)) {
581
+ if (ts.isIdentifier(node.expression) && node.expression.text === "RegExp" && node.arguments && node.arguments.length > 0) {
582
+ const firstArg = node.arguments[0];
583
+ if (firstArg && ts.isStringLiteral(firstArg)) {
584
+ return firstArg.text;
585
+ }
586
+ }
587
+ }
588
+ if (ts.isIdentifier(node)) {
589
+ return null;
590
+ }
591
+ return null;
592
+ }
593
+ function getPropertyName(name) {
594
+ if (ts.isIdentifier(name)) {
595
+ return name.text;
596
+ }
597
+ if (ts.isStringLiteral(name)) {
598
+ return name.text;
599
+ }
600
+ if (ts.isNumericLiteral(name)) {
601
+ return name.text;
602
+ }
603
+ return null;
604
+ }
605
+ var FORMSPEC_DECORATORS = {
606
+ // Display metadata
607
+ Field: { argTypes: ["object"] },
608
+ // Grouping
609
+ Group: { argTypes: ["string"] },
610
+ // Conditional display
611
+ ShowWhen: { argTypes: ["object"] },
612
+ // Enum options
613
+ EnumOptions: { argTypes: ["array"] },
614
+ // Numeric constraints
615
+ Minimum: { argTypes: ["number"] },
616
+ Maximum: { argTypes: ["number"] },
617
+ ExclusiveMinimum: { argTypes: ["number"] },
618
+ ExclusiveMaximum: { argTypes: ["number"] },
619
+ // String constraints
620
+ MinLength: { argTypes: ["number"] },
621
+ MaxLength: { argTypes: ["number"] },
622
+ Pattern: { argTypes: ["string"] }
623
+ };
624
+ function isFormSpecDecoratorsPath(fileName) {
625
+ const normalized = fileName.replace(/\\/g, "/");
626
+ return normalized.includes("node_modules/@formspec/decorators") || normalized.includes("/packages/decorators/");
627
+ }
628
+ function resolveDecorator(decorator, checker) {
629
+ const expr = decorator.expression;
630
+ let targetNode;
631
+ let name;
632
+ if (ts.isIdentifier(expr)) {
633
+ targetNode = expr;
634
+ name = expr.text;
635
+ } else if (ts.isCallExpression(expr)) {
636
+ if (ts.isIdentifier(expr.expression)) {
637
+ targetNode = expr.expression;
638
+ name = expr.expression.text;
639
+ } else {
640
+ return null;
641
+ }
642
+ } else {
643
+ return null;
644
+ }
645
+ if (name in FORMSPEC_DECORATORS) {
646
+ const symbol = checker.getSymbolAtLocation(targetNode);
647
+ if (symbol) {
648
+ const declarations = symbol.declarations;
649
+ if (declarations && declarations.length > 0) {
650
+ const decl = declarations[0];
651
+ if (decl) {
652
+ const sourceFile = decl.getSourceFile();
653
+ const fileName = sourceFile.fileName;
654
+ if (isFormSpecDecoratorsPath(fileName)) {
655
+ return {
656
+ name,
657
+ isFormSpec: true,
658
+ isMarker: !ts.isCallExpression(expr)
659
+ };
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }
665
+ const resolvedSymbol = checker.getSymbolAtLocation(targetNode);
666
+ if (!resolvedSymbol) return null;
667
+ const type = checker.getTypeOfSymbol(resolvedSymbol);
668
+ const props = type.getProperties();
669
+ let extendsBuiltin;
670
+ let extensionName;
671
+ let isMarker = false;
672
+ for (const prop of props) {
673
+ const escapedName = prop.getEscapedName();
674
+ if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extends") || escapedName.includes("FORMSPEC_EXTENDS"))) {
675
+ const propType = checker.getTypeOfSymbol(prop);
676
+ if (propType.isStringLiteral()) {
677
+ extendsBuiltin = propType.value;
678
+ }
679
+ }
680
+ if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extension") || escapedName.includes("FORMSPEC_EXTENSION"))) {
681
+ const propType = checker.getTypeOfSymbol(prop);
682
+ if (propType.isStringLiteral()) {
683
+ extensionName = propType.value;
684
+ }
685
+ }
686
+ if (escapedName.startsWith("__@") && (escapedName.includes("formspec.marker") || escapedName.includes("FORMSPEC_MARKER"))) {
687
+ isMarker = true;
688
+ }
689
+ }
690
+ if (extendsBuiltin) {
691
+ return {
692
+ name,
693
+ extendsBuiltin,
694
+ isFormSpec: true,
695
+ isMarker: false
696
+ };
697
+ }
698
+ if (extensionName) {
699
+ return {
700
+ name,
701
+ extensionName,
702
+ isFormSpec: true,
703
+ isMarker
704
+ };
705
+ }
706
+ if (isMarker) {
707
+ return {
708
+ name,
709
+ isFormSpec: true,
710
+ isMarker: true
711
+ };
712
+ }
713
+ return null;
714
+ }
715
+
716
+ // src/analyzer/jsdoc-constraints.ts
717
+ import * as ts2 from "typescript";
718
+ import { CONSTRAINT_TAG_DEFINITIONS } from "@formspec/core";
719
+ function extractJSDocConstraints(node) {
720
+ const results = [];
721
+ const jsDocTags = ts2.getJSDocTags(node);
722
+ for (const tag of jsDocTags) {
723
+ const tagName = tag.tagName.text;
724
+ if (!(tagName in CONSTRAINT_TAG_DEFINITIONS)) {
725
+ continue;
726
+ }
727
+ const constraintName = tagName;
728
+ const expectedType = CONSTRAINT_TAG_DEFINITIONS[constraintName];
729
+ const commentText = getTagCommentText(tag);
730
+ if (commentText === void 0 || commentText === "") {
731
+ continue;
732
+ }
733
+ const trimmed = commentText.trim();
734
+ if (trimmed === "") {
735
+ continue;
736
+ }
737
+ if (expectedType === "number") {
738
+ const value = Number(trimmed);
739
+ if (Number.isNaN(value)) {
740
+ continue;
741
+ }
742
+ results.push(createSyntheticDecorator(constraintName, value));
743
+ } else if (expectedType === "json") {
744
+ try {
745
+ const parsed = JSON.parse(trimmed);
746
+ if (!Array.isArray(parsed)) {
747
+ continue;
748
+ }
749
+ results.push(createSyntheticDecorator(constraintName, parsed));
750
+ } catch {
751
+ continue;
752
+ }
753
+ } else {
754
+ results.push(createSyntheticDecorator(constraintName, trimmed));
755
+ }
756
+ }
757
+ return results;
758
+ }
759
+ function extractJSDocFieldMetadata(node) {
760
+ const jsDocTags = ts2.getJSDocTags(node);
761
+ let displayName;
762
+ let description;
763
+ for (const tag of jsDocTags) {
764
+ const tagName = tag.tagName.text;
765
+ const commentText = getTagCommentText(tag);
766
+ if (commentText === void 0 || commentText.trim() === "") {
767
+ continue;
768
+ }
769
+ const trimmed = commentText.trim();
770
+ if (tagName === "Field_displayName") {
771
+ displayName = trimmed;
772
+ } else if (tagName === "Field_description") {
773
+ description = trimmed;
774
+ }
775
+ }
776
+ if (displayName === void 0 && description === void 0) {
777
+ return null;
778
+ }
779
+ const fieldOpts = {
780
+ ...displayName !== void 0 ? { displayName } : {},
781
+ ...description !== void 0 ? { description } : {}
782
+ };
783
+ return createSyntheticDecorator("Field", fieldOpts);
784
+ }
785
+ function getTagCommentText(tag) {
786
+ if (tag.comment === void 0) {
787
+ return void 0;
788
+ }
789
+ if (typeof tag.comment === "string") {
790
+ return tag.comment;
791
+ }
792
+ return ts2.getTextOfJSDocComment(tag.comment);
793
+ }
794
+ function createSyntheticDecorator(name, value) {
795
+ return {
796
+ name,
797
+ args: [value],
798
+ node: void 0
799
+ };
800
+ }
801
+
802
+ // src/analyzer/class-analyzer.ts
803
+ function analyzeClass(classDecl, checker) {
804
+ const name = classDecl.name?.text ?? "AnonymousClass";
805
+ const fields = [];
806
+ const instanceMethods = [];
807
+ const staticMethods = [];
808
+ for (const member of classDecl.members) {
809
+ if (ts3.isPropertyDeclaration(member)) {
810
+ const fieldInfo = analyzeField(member, checker);
811
+ if (fieldInfo) {
812
+ fields.push(fieldInfo);
813
+ }
814
+ } else if (ts3.isMethodDeclaration(member)) {
815
+ const methodInfo = analyzeMethod(member, checker);
816
+ if (methodInfo) {
817
+ const isStatic = member.modifiers?.some((m) => m.kind === ts3.SyntaxKind.StaticKeyword);
818
+ if (isStatic) {
819
+ staticMethods.push(methodInfo);
820
+ } else {
821
+ instanceMethods.push(methodInfo);
822
+ }
823
+ }
824
+ }
825
+ }
826
+ return {
827
+ name,
828
+ fields,
829
+ instanceMethods,
830
+ staticMethods
831
+ };
832
+ }
833
+ function analyzeField(prop, checker) {
834
+ if (!ts3.isIdentifier(prop.name)) {
835
+ return null;
836
+ }
837
+ const name = prop.name.text;
838
+ const typeNode = prop.type;
839
+ const type = checker.getTypeAtLocation(prop);
840
+ const optional = prop.questionToken !== void 0;
841
+ const decorators = extractDecorators(prop);
842
+ for (const dec of decorators) {
843
+ if (dec.node) {
844
+ const resolved = resolveDecorator(dec.node, checker);
845
+ if (resolved) {
846
+ dec.resolved = resolved;
847
+ }
848
+ }
849
+ }
850
+ if (prop.type) {
851
+ const aliasConstraints = extractTypeAliasConstraints(prop.type, checker);
852
+ decorators.push(...aliasConstraints);
853
+ }
854
+ const jsdocConstraints = extractJSDocConstraints(prop);
855
+ decorators.push(...jsdocConstraints);
856
+ const deprecated = hasDeprecatedTag(prop);
857
+ const defaultValue = extractDefaultValue(prop.initializer);
858
+ return {
859
+ name,
860
+ typeNode,
861
+ type,
862
+ optional,
863
+ decorators,
864
+ deprecated,
865
+ defaultValue
866
+ };
867
+ }
868
+ function extractTypeAliasConstraints(typeNode, checker) {
869
+ if (!ts3.isTypeReferenceNode(typeNode)) return [];
870
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
871
+ if (!symbol?.declarations) return [];
872
+ const aliasDecl = symbol.declarations.find(ts3.isTypeAliasDeclaration);
873
+ if (!aliasDecl) return [];
874
+ if (ts3.isTypeLiteralNode(aliasDecl.type)) return [];
875
+ return extractJSDocConstraints(aliasDecl);
876
+ }
877
+ function hasDeprecatedTag(node) {
878
+ const jsDocTags = ts3.getJSDocTags(node);
879
+ return jsDocTags.some((tag) => tag.tagName.text === "deprecated");
880
+ }
881
+ function extractDefaultValue(initializer) {
882
+ if (!initializer) return void 0;
883
+ if (ts3.isStringLiteral(initializer)) {
884
+ return initializer.text;
885
+ }
886
+ if (ts3.isNumericLiteral(initializer)) {
887
+ return Number(initializer.text);
888
+ }
889
+ if (initializer.kind === ts3.SyntaxKind.TrueKeyword) {
890
+ return true;
891
+ }
892
+ if (initializer.kind === ts3.SyntaxKind.FalseKeyword) {
893
+ return false;
894
+ }
895
+ if (initializer.kind === ts3.SyntaxKind.NullKeyword) {
896
+ return null;
897
+ }
898
+ if (ts3.isPrefixUnaryExpression(initializer)) {
899
+ if (initializer.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(initializer.operand)) {
900
+ return -Number(initializer.operand.text);
901
+ }
902
+ }
903
+ return void 0;
904
+ }
905
+ function analyzeMethod(method, checker) {
906
+ if (!ts3.isIdentifier(method.name)) {
907
+ return null;
908
+ }
909
+ const name = method.name.text;
910
+ const parameters = [];
911
+ for (const param of method.parameters) {
912
+ if (ts3.isIdentifier(param.name)) {
913
+ const paramInfo = analyzeParameter(param, checker);
914
+ parameters.push(paramInfo);
915
+ }
916
+ }
917
+ const returnTypeNode = method.type;
918
+ const signature = checker.getSignatureFromDeclaration(method);
919
+ const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getTypeAtLocation(method);
920
+ return {
921
+ name,
922
+ parameters,
923
+ returnTypeNode,
924
+ returnType
925
+ };
926
+ }
927
+ function analyzeParameter(param, checker) {
928
+ const name = ts3.isIdentifier(param.name) ? param.name.text : "param";
929
+ const typeNode = param.type;
930
+ const type = checker.getTypeAtLocation(param);
931
+ const formSpecExportName = detectFormSpecReference(typeNode);
932
+ const optional = param.questionToken !== void 0 || param.initializer !== void 0;
933
+ return {
934
+ name,
935
+ typeNode,
936
+ type,
937
+ formSpecExportName,
938
+ optional
939
+ };
940
+ }
941
+ function detectFormSpecReference(typeNode) {
942
+ if (!typeNode) return null;
943
+ if (!ts3.isTypeReferenceNode(typeNode)) return null;
944
+ const typeName = ts3.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : ts3.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right.text : null;
945
+ if (typeName !== "InferSchema" && typeName !== "InferFormSchema") return null;
946
+ const typeArg = typeNode.typeArguments?.[0];
947
+ if (!typeArg || !ts3.isTypeQueryNode(typeArg)) return null;
948
+ if (ts3.isIdentifier(typeArg.exprName)) {
949
+ return typeArg.exprName.text;
950
+ }
951
+ if (ts3.isQualifiedName(typeArg.exprName)) {
952
+ return typeArg.exprName.right.text;
953
+ }
954
+ return null;
955
+ }
956
+ function analyzeInterface(interfaceDecl, checker) {
957
+ const name = interfaceDecl.name.text;
958
+ const fields = [];
959
+ for (const member of interfaceDecl.members) {
960
+ if (ts3.isPropertySignature(member)) {
961
+ const fieldInfo = analyzeInterfaceProperty(member, checker);
962
+ if (fieldInfo) {
963
+ fields.push(fieldInfo);
964
+ }
965
+ }
966
+ }
967
+ return {
968
+ name,
969
+ fields,
970
+ instanceMethods: [],
971
+ staticMethods: []
972
+ };
973
+ }
974
+ function analyzeTypeAlias(typeAlias, checker) {
975
+ if (!ts3.isTypeLiteralNode(typeAlias.type)) {
976
+ const sourceFile = typeAlias.getSourceFile();
977
+ const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
978
+ const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
60
979
  return {
61
- jsonSchema: generateJsonSchema(form),
62
- uiSchema: generateUiSchema(form),
980
+ ok: false,
981
+ error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
63
982
  };
983
+ }
984
+ const name = typeAlias.name.text;
985
+ const fields = [];
986
+ for (const member of typeAlias.type.members) {
987
+ if (ts3.isPropertySignature(member)) {
988
+ const fieldInfo = analyzeInterfaceProperty(member, checker);
989
+ if (fieldInfo) {
990
+ fields.push(fieldInfo);
991
+ }
992
+ }
993
+ }
994
+ return {
995
+ ok: true,
996
+ analysis: {
997
+ name,
998
+ fields,
999
+ instanceMethods: [],
1000
+ staticMethods: []
1001
+ }
1002
+ };
1003
+ }
1004
+ function analyzeInterfaceProperty(prop, checker) {
1005
+ if (!ts3.isIdentifier(prop.name)) {
1006
+ return null;
1007
+ }
1008
+ const name = prop.name.text;
1009
+ const typeNode = prop.type;
1010
+ const type = checker.getTypeAtLocation(prop);
1011
+ const optional = prop.questionToken !== void 0;
1012
+ const decorators = [];
1013
+ if (typeNode) {
1014
+ const aliasConstraints = extractTypeAliasConstraints(typeNode, checker);
1015
+ decorators.push(...aliasConstraints);
1016
+ }
1017
+ const fieldMetadata = extractJSDocFieldMetadata(prop);
1018
+ if (fieldMetadata) {
1019
+ decorators.push(fieldMetadata);
1020
+ }
1021
+ const jsdocConstraints = extractJSDocConstraints(prop);
1022
+ decorators.push(...jsdocConstraints);
1023
+ const deprecated = hasDeprecatedTag(prop);
1024
+ return {
1025
+ name,
1026
+ typeNode,
1027
+ type,
1028
+ optional,
1029
+ decorators,
1030
+ deprecated,
1031
+ defaultValue: void 0
1032
+ };
1033
+ }
1034
+
1035
+ // src/analyzer/type-converter.ts
1036
+ function getNamedTypeFieldInfoMap(type, checker) {
1037
+ const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1038
+ (s) => s?.declarations != null && s.declarations.length > 0
1039
+ );
1040
+ for (const symbol of symbols) {
1041
+ const declarations = symbol.declarations;
1042
+ if (!declarations) continue;
1043
+ const classDecl = declarations.find(ts4.isClassDeclaration);
1044
+ if (classDecl) {
1045
+ const map = /* @__PURE__ */ new Map();
1046
+ for (const member of classDecl.members) {
1047
+ if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1048
+ const fieldInfo = analyzeField(member, checker);
1049
+ if (fieldInfo) map.set(fieldInfo.name, fieldInfo);
1050
+ }
1051
+ }
1052
+ return map;
1053
+ }
1054
+ const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1055
+ if (interfaceDecl) {
1056
+ return buildFieldInfoMapFromSignatures(interfaceDecl.members, checker);
1057
+ }
1058
+ const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1059
+ if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
1060
+ return buildFieldInfoMapFromSignatures(typeAliasDecl.type.members, checker);
1061
+ }
1062
+ }
1063
+ return null;
1064
+ }
1065
+ function buildFieldInfoMapFromSignatures(members, checker) {
1066
+ const map = /* @__PURE__ */ new Map();
1067
+ for (const member of members) {
1068
+ if (ts4.isPropertySignature(member)) {
1069
+ const fieldInfo = analyzeInterfaceProperty(member, checker);
1070
+ if (fieldInfo) {
1071
+ map.set(fieldInfo.name, fieldInfo);
1072
+ }
1073
+ }
1074
+ }
1075
+ return map;
1076
+ }
1077
+ function getObjectPropertyInfos(type, checker) {
1078
+ const fieldInfoMap = getNamedTypeFieldInfoMap(type, checker);
1079
+ const result = [];
1080
+ for (const prop of type.getProperties()) {
1081
+ const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1082
+ if (!declaration) continue;
1083
+ const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1084
+ const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1085
+ const fieldInfo = fieldInfoMap?.get(prop.name) ?? void 0;
1086
+ result.push({ name: prop.name, type: propType, optional, fieldInfo });
1087
+ }
1088
+ return result;
1089
+ }
1090
+ function convertType(type, checker) {
1091
+ return convertTypeInternal(type, checker, /* @__PURE__ */ new Set());
1092
+ }
1093
+ function convertTypeInternal(type, checker, visiting) {
1094
+ if (type.flags & ts4.TypeFlags.String) {
1095
+ return { jsonSchema: { type: "string" }, formSpecFieldType: "text" };
1096
+ }
1097
+ if (type.flags & ts4.TypeFlags.Number) {
1098
+ return { jsonSchema: { type: "number" }, formSpecFieldType: "number" };
1099
+ }
1100
+ if (type.flags & ts4.TypeFlags.Boolean) {
1101
+ return { jsonSchema: { type: "boolean" }, formSpecFieldType: "boolean" };
1102
+ }
1103
+ if (type.flags & ts4.TypeFlags.Null) {
1104
+ return { jsonSchema: { type: "null" }, formSpecFieldType: "null" };
1105
+ }
1106
+ if (type.flags & ts4.TypeFlags.Undefined) {
1107
+ return { jsonSchema: {}, formSpecFieldType: "undefined" };
1108
+ }
1109
+ if (type.isStringLiteral()) {
1110
+ return {
1111
+ jsonSchema: { const: type.value },
1112
+ formSpecFieldType: "enum"
1113
+ };
1114
+ }
1115
+ if (type.isNumberLiteral()) {
1116
+ return {
1117
+ jsonSchema: { const: type.value },
1118
+ formSpecFieldType: "number"
1119
+ };
1120
+ }
1121
+ if (type.isUnion()) {
1122
+ return convertUnionType(type, checker, visiting);
1123
+ }
1124
+ if (checker.isArrayType(type)) {
1125
+ return convertArrayType(type, checker, visiting);
1126
+ }
1127
+ if (type.flags & ts4.TypeFlags.Object) {
1128
+ return convertObjectType(type, checker, visiting);
1129
+ }
1130
+ return { jsonSchema: {}, formSpecFieldType: "unknown" };
1131
+ }
1132
+ function convertUnionType(type, checker, visiting) {
1133
+ const types = type.types;
1134
+ const nonNullTypes = types.filter(
1135
+ (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1136
+ );
1137
+ const hasNull = types.some((t) => t.flags & ts4.TypeFlags.Null);
1138
+ const isBooleanUnion = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1139
+ if (isBooleanUnion) {
1140
+ const result = {
1141
+ jsonSchema: { type: "boolean" },
1142
+ formSpecFieldType: "boolean"
1143
+ };
1144
+ if (hasNull) {
1145
+ result.jsonSchema = { oneOf: [{ type: "boolean" }, { type: "null" }] };
1146
+ }
1147
+ return result;
1148
+ }
1149
+ const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1150
+ if (allStringLiterals && nonNullTypes.length > 0) {
1151
+ const enumValues = nonNullTypes.map((t) => t.value);
1152
+ const result = {
1153
+ jsonSchema: { enum: enumValues },
1154
+ formSpecFieldType: "enum"
1155
+ };
1156
+ if (hasNull) {
1157
+ result.jsonSchema = { oneOf: [{ enum: enumValues }, { type: "null" }] };
1158
+ }
1159
+ return result;
1160
+ }
1161
+ const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1162
+ if (allNumberLiterals && nonNullTypes.length > 0) {
1163
+ const enumValues = nonNullTypes.map((t) => t.value);
1164
+ const result = {
1165
+ jsonSchema: { enum: enumValues },
1166
+ formSpecFieldType: "enum"
1167
+ };
1168
+ if (hasNull) {
1169
+ result.jsonSchema = { oneOf: [{ enum: enumValues }, { type: "null" }] };
1170
+ }
1171
+ return result;
1172
+ }
1173
+ if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1174
+ const result = convertTypeInternal(nonNullTypes[0], checker, visiting);
1175
+ if (hasNull) {
1176
+ result.jsonSchema = { oneOf: [result.jsonSchema, { type: "null" }] };
1177
+ }
1178
+ return result;
1179
+ }
1180
+ const schemas = nonNullTypes.map((t) => convertTypeInternal(t, checker, visiting).jsonSchema);
1181
+ if (hasNull) {
1182
+ schemas.push({ type: "null" });
1183
+ }
1184
+ return {
1185
+ jsonSchema: { oneOf: schemas },
1186
+ formSpecFieldType: "union"
1187
+ };
1188
+ }
1189
+ function convertArrayType(type, checker, visiting) {
1190
+ const typeArgs = type.typeArguments;
1191
+ const elementType = typeArgs?.[0];
1192
+ const itemSchema = elementType ? convertTypeInternal(elementType, checker, visiting).jsonSchema : {};
1193
+ return {
1194
+ jsonSchema: {
1195
+ type: "array",
1196
+ items: itemSchema
1197
+ },
1198
+ formSpecFieldType: "array"
1199
+ };
1200
+ }
1201
+ function convertObjectType(type, checker, visiting) {
1202
+ if (visiting.has(type)) {
1203
+ return { jsonSchema: { type: "object" }, formSpecFieldType: "object" };
1204
+ }
1205
+ visiting.add(type);
1206
+ const properties = {};
1207
+ const required = [];
1208
+ for (const propInfo of getObjectPropertyInfos(type, checker)) {
1209
+ const propSchema = convertTypeInternal(propInfo.type, checker, visiting).jsonSchema;
1210
+ properties[propInfo.name] = propInfo.fieldInfo ? applyDecoratorsToSchema(propSchema, propInfo.fieldInfo.decorators, propInfo.fieldInfo) : propSchema;
1211
+ if (!propInfo.optional) {
1212
+ required.push(propInfo.name);
1213
+ }
1214
+ }
1215
+ visiting.delete(type);
1216
+ return {
1217
+ jsonSchema: {
1218
+ type: "object",
1219
+ properties,
1220
+ ...required.length > 0 ? { required } : {}
1221
+ },
1222
+ formSpecFieldType: "object"
1223
+ };
1224
+ }
1225
+ function createFormSpecField(fieldName, type, decorators, optional, checker, visitedTypes = /* @__PURE__ */ new Set()) {
1226
+ const { formSpecFieldType } = convertType(type, checker);
1227
+ const field = {
1228
+ _field: formSpecFieldType,
1229
+ id: fieldName
1230
+ };
1231
+ if (!optional) {
1232
+ field.required = true;
1233
+ }
1234
+ if (formSpecFieldType === "object" && type.flags & ts4.TypeFlags.Object) {
1235
+ if (!visitedTypes.has(type)) {
1236
+ visitedTypes.add(type);
1237
+ const nestedFields = [];
1238
+ for (const propInfo of getObjectPropertyInfos(type, checker)) {
1239
+ nestedFields.push(
1240
+ createFormSpecField(
1241
+ propInfo.name,
1242
+ propInfo.type,
1243
+ propInfo.fieldInfo?.decorators ?? [],
1244
+ propInfo.optional,
1245
+ checker,
1246
+ visitedTypes
1247
+ )
1248
+ );
1249
+ }
1250
+ visitedTypes.delete(type);
1251
+ if (nestedFields.length > 0) {
1252
+ field.fields = nestedFields;
1253
+ }
1254
+ }
1255
+ }
1256
+ for (const dec of decorators) {
1257
+ applyDecoratorToField(field, dec);
1258
+ }
1259
+ return field;
1260
+ }
1261
+ function applyDecoratorToField(field, decorator) {
1262
+ const { args } = decorator;
1263
+ const resolved = decorator.resolved;
1264
+ const effectiveName = resolved?.extendsBuiltin ?? decorator.name;
1265
+ switch (effectiveName) {
1266
+ case "Field": {
1267
+ const opts = args[0];
1268
+ if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) {
1269
+ if (typeof opts["displayName"] === "string") {
1270
+ field.label = opts["displayName"];
1271
+ }
1272
+ if (typeof opts["description"] === "string") {
1273
+ field.description = opts["description"];
1274
+ }
1275
+ if (typeof opts["placeholder"] === "string") {
1276
+ field.placeholder = opts["placeholder"];
1277
+ }
1278
+ }
1279
+ break;
1280
+ }
1281
+ case "Minimum":
1282
+ if (typeof args[0] === "number") {
1283
+ field.min = args[0];
1284
+ }
1285
+ break;
1286
+ case "Maximum":
1287
+ if (typeof args[0] === "number") {
1288
+ field.max = args[0];
1289
+ }
1290
+ break;
1291
+ case "MinLength":
1292
+ if (typeof args[0] === "number") {
1293
+ field.minLength = args[0];
1294
+ }
1295
+ break;
1296
+ case "MaxLength":
1297
+ if (typeof args[0] === "number") {
1298
+ field.maxLength = args[0];
1299
+ }
1300
+ break;
1301
+ case "Pattern":
1302
+ if (typeof args[0] === "string") {
1303
+ field.pattern = args[0];
1304
+ }
1305
+ break;
1306
+ case "EnumOptions":
1307
+ if (Array.isArray(args[0])) {
1308
+ field.options = args[0];
1309
+ }
1310
+ break;
1311
+ case "ShowWhen":
1312
+ if (typeof args[0] === "object" && args[0] !== null) {
1313
+ field.showWhen = args[0];
1314
+ }
1315
+ break;
1316
+ case "Group":
1317
+ if (typeof args[0] === "string") {
1318
+ field.group = args[0];
1319
+ }
1320
+ break;
1321
+ }
1322
+ }
1323
+ function applyDecoratorsToSchema(schema, decorators, fieldInfo) {
1324
+ const result = { ...schema };
1325
+ for (const dec of decorators) {
1326
+ const { args } = dec;
1327
+ const resolved = dec.resolved;
1328
+ const effectiveName = resolved?.extendsBuiltin ?? dec.name;
1329
+ switch (effectiveName) {
1330
+ case "Field": {
1331
+ const opts = args[0];
1332
+ if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) {
1333
+ if (typeof opts["displayName"] === "string") {
1334
+ result.title = opts["displayName"];
1335
+ }
1336
+ if (typeof opts["description"] === "string") {
1337
+ result.description = opts["description"];
1338
+ }
1339
+ }
1340
+ break;
1341
+ }
1342
+ case "Minimum":
1343
+ if (typeof args[0] === "number") {
1344
+ result.minimum = args[0];
1345
+ }
1346
+ break;
1347
+ case "Maximum":
1348
+ if (typeof args[0] === "number") {
1349
+ result.maximum = args[0];
1350
+ }
1351
+ break;
1352
+ case "ExclusiveMinimum":
1353
+ if (typeof args[0] === "number") {
1354
+ result.exclusiveMinimum = args[0];
1355
+ }
1356
+ break;
1357
+ case "ExclusiveMaximum":
1358
+ if (typeof args[0] === "number") {
1359
+ result.exclusiveMaximum = args[0];
1360
+ }
1361
+ break;
1362
+ case "MinLength":
1363
+ if (typeof args[0] === "number") {
1364
+ result.minLength = args[0];
1365
+ }
1366
+ break;
1367
+ case "MaxLength":
1368
+ if (typeof args[0] === "number") {
1369
+ result.maxLength = args[0];
1370
+ }
1371
+ break;
1372
+ case "Pattern":
1373
+ if (typeof args[0] === "string") {
1374
+ result.pattern = args[0];
1375
+ }
1376
+ break;
1377
+ }
1378
+ if (resolved?.extensionName && /^[a-z][a-z0-9-]*$/.test(resolved.extensionName)) {
1379
+ const key = `x-formspec-${resolved.extensionName}`;
1380
+ if (resolved.isMarker) {
1381
+ setSchemaExtension(result, key, true);
1382
+ } else {
1383
+ setSchemaExtension(result, key, args[0] ?? true);
1384
+ }
1385
+ }
1386
+ }
1387
+ if (fieldInfo) {
1388
+ if (fieldInfo.deprecated) {
1389
+ result.deprecated = true;
1390
+ }
1391
+ if (fieldInfo.defaultValue !== void 0) {
1392
+ result.default = fieldInfo.defaultValue;
1393
+ }
1394
+ }
1395
+ return result;
1396
+ }
1397
+
1398
+ // src/analyzer/program.ts
1399
+ import * as ts5 from "typescript";
1400
+ import * as path from "path";
1401
+ function createProgramContext(filePath) {
1402
+ const absolutePath = path.resolve(filePath);
1403
+ const fileDir = path.dirname(absolutePath);
1404
+ const configPath = ts5.findConfigFile(fileDir, ts5.sys.fileExists.bind(ts5.sys), "tsconfig.json");
1405
+ let compilerOptions;
1406
+ let fileNames;
1407
+ if (configPath) {
1408
+ const configFile = ts5.readConfigFile(configPath, ts5.sys.readFile.bind(ts5.sys));
1409
+ if (configFile.error) {
1410
+ throw new Error(
1411
+ `Error reading tsconfig.json: ${ts5.flattenDiagnosticMessageText(configFile.error.messageText, "\n")}`
1412
+ );
1413
+ }
1414
+ const parsed = ts5.parseJsonConfigFileContent(
1415
+ configFile.config,
1416
+ ts5.sys,
1417
+ path.dirname(configPath)
1418
+ );
1419
+ if (parsed.errors.length > 0) {
1420
+ const errorMessages = parsed.errors.map((e) => ts5.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
1421
+ throw new Error(`Error parsing tsconfig.json: ${errorMessages}`);
1422
+ }
1423
+ compilerOptions = parsed.options;
1424
+ fileNames = parsed.fileNames.includes(absolutePath) ? parsed.fileNames : [...parsed.fileNames, absolutePath];
1425
+ } else {
1426
+ compilerOptions = {
1427
+ target: ts5.ScriptTarget.ES2022,
1428
+ module: ts5.ModuleKind.NodeNext,
1429
+ moduleResolution: ts5.ModuleResolutionKind.NodeNext,
1430
+ strict: true,
1431
+ skipLibCheck: true,
1432
+ declaration: true
1433
+ };
1434
+ fileNames = [absolutePath];
1435
+ }
1436
+ const program = ts5.createProgram(fileNames, compilerOptions);
1437
+ const sourceFile = program.getSourceFile(absolutePath);
1438
+ if (!sourceFile) {
1439
+ throw new Error(`Could not find source file: ${absolutePath}`);
1440
+ }
1441
+ return {
1442
+ program,
1443
+ checker: program.getTypeChecker(),
1444
+ sourceFile
1445
+ };
1446
+ }
1447
+ function findNodeByName(sourceFile, name, predicate, getName) {
1448
+ let result = null;
1449
+ function visit(node) {
1450
+ if (result) return;
1451
+ if (predicate(node) && getName(node) === name) {
1452
+ result = node;
1453
+ return;
1454
+ }
1455
+ ts5.forEachChild(node, visit);
1456
+ }
1457
+ visit(sourceFile);
1458
+ return result;
1459
+ }
1460
+ function findClassByName(sourceFile, className) {
1461
+ return findNodeByName(sourceFile, className, ts5.isClassDeclaration, (n) => n.name?.text);
1462
+ }
1463
+ function findInterfaceByName(sourceFile, interfaceName) {
1464
+ return findNodeByName(sourceFile, interfaceName, ts5.isInterfaceDeclaration, (n) => n.name.text);
1465
+ }
1466
+ function findTypeAliasByName(sourceFile, aliasName) {
1467
+ return findNodeByName(sourceFile, aliasName, ts5.isTypeAliasDeclaration, (n) => n.name.text);
1468
+ }
1469
+
1470
+ // src/generators/class-schema.ts
1471
+ function generateClassSchemas(analysis, checker) {
1472
+ const properties = {};
1473
+ const required = [];
1474
+ const uiElements = [];
1475
+ for (const field of analysis.fields) {
1476
+ const { jsonSchema: baseSchema } = convertType(field.type, checker);
1477
+ const fieldSchema = applyDecoratorsToSchema(baseSchema, field.decorators, field);
1478
+ properties[field.name] = fieldSchema;
1479
+ if (!field.optional) {
1480
+ required.push(field.name);
1481
+ }
1482
+ const formSpecField = createFormSpecField(
1483
+ field.name,
1484
+ field.type,
1485
+ field.decorators,
1486
+ field.optional,
1487
+ checker
1488
+ );
1489
+ uiElements.push(formSpecField);
1490
+ }
1491
+ const jsonSchema = {
1492
+ type: "object",
1493
+ properties,
1494
+ ...required.length > 0 ? { required } : {}
1495
+ };
1496
+ const uiSchema2 = generateUiSchemaFromFields(uiElements);
1497
+ return { jsonSchema, uiSchema: uiSchema2 };
1498
+ }
1499
+ function generateSchemasFromClass(options) {
1500
+ const ctx = createProgramContext(options.filePath);
1501
+ const classDecl = findClassByName(ctx.sourceFile, options.className);
1502
+ if (!classDecl) {
1503
+ throw new Error(`Class "${options.className}" not found in ${options.filePath}`);
1504
+ }
1505
+ const analysis = analyzeClass(classDecl, ctx.checker);
1506
+ return generateClassSchemas(analysis, ctx.checker);
1507
+ }
1508
+ function generateSchemas(options) {
1509
+ const ctx = createProgramContext(options.filePath);
1510
+ const classDecl = findClassByName(ctx.sourceFile, options.typeName);
1511
+ if (classDecl) {
1512
+ const analysis = analyzeClass(classDecl, ctx.checker);
1513
+ return generateClassSchemas(analysis, ctx.checker);
1514
+ }
1515
+ const interfaceDecl = findInterfaceByName(ctx.sourceFile, options.typeName);
1516
+ if (interfaceDecl) {
1517
+ const analysis = analyzeInterface(interfaceDecl, ctx.checker);
1518
+ return generateClassSchemas(analysis, ctx.checker);
1519
+ }
1520
+ const typeAlias = findTypeAliasByName(ctx.sourceFile, options.typeName);
1521
+ if (typeAlias) {
1522
+ const result = analyzeTypeAlias(typeAlias, ctx.checker);
1523
+ if (result.ok) {
1524
+ return generateClassSchemas(result.analysis, ctx.checker);
1525
+ }
1526
+ throw new Error(result.error);
1527
+ }
1528
+ throw new Error(
1529
+ `Type "${options.typeName}" not found as a class, interface, or type alias in ${options.filePath}`
1530
+ );
1531
+ }
1532
+
1533
+ // src/codegen/index.ts
1534
+ import * as ts6 from "typescript";
1535
+ import * as path2 from "path";
1536
+ import * as fs from "fs";
1537
+ function findDecoratedClasses(files, baseDir) {
1538
+ const program = ts6.createProgram(files, {
1539
+ target: ts6.ScriptTarget.ESNext,
1540
+ module: ts6.ModuleKind.ESNext,
1541
+ moduleResolution: ts6.ModuleResolutionKind.NodeNext,
1542
+ // TC39 Stage 3 decorators — do not use experimentalDecorators
1543
+ strict: true
1544
+ });
1545
+ const checker = program.getTypeChecker();
1546
+ const results = [];
1547
+ for (const sourceFile of program.getSourceFiles()) {
1548
+ if (sourceFile.isDeclarationFile) continue;
1549
+ if (!files.some((f) => path2.resolve(f) === sourceFile.fileName)) continue;
1550
+ ts6.forEachChild(sourceFile, (node) => {
1551
+ if (ts6.isClassDeclaration(node) && hasDecoratedProperties(node)) {
1552
+ const className = node.name?.text;
1553
+ if (!className) return;
1554
+ const typeMetadata = extractTypeMetadata(node, checker);
1555
+ const relativePath = path2.relative(baseDir, sourceFile.fileName);
1556
+ const exported = isClassExported(node, sourceFile);
1557
+ results.push({
1558
+ name: className,
1559
+ sourcePath: relativePath.replace(/\.tsx?$/, ""),
1560
+ typeMetadata,
1561
+ isExported: exported
1562
+ });
1563
+ }
1564
+ });
1565
+ }
1566
+ return results;
1567
+ }
1568
+ function hasDecoratedProperties(node) {
1569
+ return node.members.some((member) => {
1570
+ if (!ts6.isPropertyDeclaration(member)) return false;
1571
+ const decorators = ts6.getDecorators(member);
1572
+ return decorators !== void 0 && decorators.length > 0;
1573
+ });
1574
+ }
1575
+ function isClassExported(classNode, sourceFile) {
1576
+ const className = classNode.name?.text;
1577
+ if (!className) return false;
1578
+ const modifiers = ts6.getModifiers(classNode);
1579
+ if (modifiers) {
1580
+ const hasExport = modifiers.some((mod) => mod.kind === ts6.SyntaxKind.ExportKeyword);
1581
+ if (hasExport) return true;
1582
+ }
1583
+ for (const statement of sourceFile.statements) {
1584
+ if (ts6.isExportDeclaration(statement) && statement.exportClause) {
1585
+ if (ts6.isNamedExports(statement.exportClause)) {
1586
+ for (const element of statement.exportClause.elements) {
1587
+ const localName = element.propertyName?.text ?? element.name.text;
1588
+ if (localName === className) {
1589
+ return true;
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+ }
1595
+ return false;
1596
+ }
1597
+ function extractTypeMetadata(classNode, checker) {
1598
+ const metadata = {};
1599
+ for (const member of classNode.members) {
1600
+ if (!ts6.isPropertyDeclaration(member)) continue;
1601
+ if (!member.name || !ts6.isIdentifier(member.name)) continue;
1602
+ const symbol = checker.getSymbolAtLocation(member.name);
1603
+ if (!symbol) continue;
1604
+ const type = checker.getTypeOfSymbolAtLocation(symbol, member);
1605
+ const fieldName = member.name.text;
1606
+ const isOptional = !!(symbol.flags & ts6.SymbolFlags.Optional);
1607
+ const typeInfo = convertTypeToMetadata(type, checker);
1608
+ if (isOptional) {
1609
+ typeInfo.optional = true;
1610
+ }
1611
+ metadata[fieldName] = typeInfo;
1612
+ }
1613
+ return metadata;
1614
+ }
1615
+ function convertTypeToMetadata(type, checker, visited = /* @__PURE__ */ new Set()) {
1616
+ const isObjectType = (type.flags & ts6.TypeFlags.Object) !== 0;
1617
+ if (isObjectType) {
1618
+ if (visited.has(type)) {
1619
+ return { type: "unknown" };
1620
+ }
1621
+ visited.add(type);
1622
+ }
1623
+ if (type.isUnion()) {
1624
+ const types = type.types;
1625
+ const nonNullTypes = types.filter(
1626
+ (t) => !(t.flags & (ts6.TypeFlags.Null | ts6.TypeFlags.Undefined))
1627
+ );
1628
+ const hasNull = types.some((t) => t.flags & ts6.TypeFlags.Null);
1629
+ const isBooleanUnion = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts6.TypeFlags.BooleanLiteral);
1630
+ if (isBooleanUnion) {
1631
+ const result = { type: "boolean" };
1632
+ if (hasNull) result.nullable = true;
1633
+ return result;
1634
+ }
1635
+ const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1636
+ if (allStringLiterals && nonNullTypes.length > 0) {
1637
+ const result = {
1638
+ type: "enum",
1639
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript doesn't narrow array types from `every` predicate
1640
+ values: nonNullTypes.map((t) => t.value)
1641
+ };
1642
+ if (hasNull) result.nullable = true;
1643
+ return result;
1644
+ }
1645
+ const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1646
+ if (allNumberLiterals && nonNullTypes.length > 0) {
1647
+ const result = {
1648
+ type: "enum",
1649
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript doesn't narrow array types from `every` predicate
1650
+ values: nonNullTypes.map((t) => t.value)
1651
+ };
1652
+ if (hasNull) result.nullable = true;
1653
+ return result;
1654
+ }
1655
+ if (nonNullTypes.length === 1) {
1656
+ const singleType = nonNullTypes[0];
1657
+ if (!singleType) return { type: "unknown" };
1658
+ const result = convertTypeToMetadata(singleType, checker, visited);
1659
+ if (hasNull) result.nullable = true;
1660
+ return result;
1661
+ }
1662
+ return { type: "unknown" };
1663
+ }
1664
+ if (type.flags & ts6.TypeFlags.String) return { type: "string" };
1665
+ if (type.flags & ts6.TypeFlags.Number) return { type: "number" };
1666
+ if (type.flags & ts6.TypeFlags.Boolean) return { type: "boolean" };
1667
+ if (type.isStringLiteral()) {
1668
+ return { type: "enum", values: [type.value] };
1669
+ }
1670
+ if (type.isNumberLiteral()) {
1671
+ return { type: "enum", values: [type.value] };
1672
+ }
1673
+ if (checker.isArrayType(type)) {
1674
+ const typeArgs = type.typeArguments;
1675
+ return {
1676
+ type: "array",
1677
+ itemType: typeArgs?.[0] ? convertTypeToMetadata(typeArgs[0], checker, visited) : { type: "unknown" }
1678
+ };
1679
+ }
1680
+ if (type.flags & ts6.TypeFlags.Object) {
1681
+ const properties = {};
1682
+ for (const prop of type.getProperties()) {
1683
+ const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1684
+ if (!declaration) continue;
1685
+ const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1686
+ const propOptional = !!(prop.flags & ts6.SymbolFlags.Optional);
1687
+ const propMeta = convertTypeToMetadata(propType, checker, visited);
1688
+ if (propOptional) propMeta.optional = true;
1689
+ properties[prop.name] = propMeta;
1690
+ }
1691
+ if (Object.keys(properties).length > 0) {
1692
+ return { type: "object", properties };
1693
+ }
1694
+ return { type: "object" };
1695
+ }
1696
+ return { type: "unknown" };
1697
+ }
1698
+ function generateCodegenOutput(classes, outputPath, baseDir) {
1699
+ const outputDir = path2.dirname(path2.resolve(outputPath));
1700
+ const lines = [
1701
+ "/**",
1702
+ " * Auto-generated by FormSpec CLI.",
1703
+ " * DO NOT EDIT - changes will be overwritten.",
1704
+ " *",
1705
+ " * This file provides:",
1706
+ " * - Type metadata patches for runtime schema generation",
1707
+ " * - Inferred schema types (e.g., UserFormSchema)",
1708
+ " * - Type-safe FormSpec accessors (e.g., getUserFormFormSpec())",
1709
+ " *",
1710
+ " * Usage:",
1711
+ " * import { UserFormSchema, getUserFormFormSpec } from './__formspec_types__';",
1712
+ " *",
1713
+ " * const data: UserFormSchema = { name: 'Alice', country: 'us' };",
1714
+ " * const spec = getUserFormFormSpec();",
1715
+ " */",
1716
+ "",
1717
+ "/* eslint-disable @typescript-eslint/no-explicit-any */",
1718
+ ""
1719
+ ];
1720
+ for (const cls of classes) {
1721
+ const absoluteSourcePath = path2.resolve(baseDir, cls.sourcePath);
1722
+ const importPath = getRelativeImportPath(outputDir, absoluteSourcePath);
1723
+ lines.push(`import { ${cls.name} } from "${importPath}";`);
1724
+ }
1725
+ lines.push("");
1726
+ lines.push("// =============================================================================");
1727
+ lines.push("// Type Metadata Patches");
1728
+ lines.push("// =============================================================================");
1729
+ lines.push("");
1730
+ for (const cls of classes) {
1731
+ const metadataJson = JSON.stringify(cls.typeMetadata, null, 2).split("\n").map((line, i) => i === 0 ? line : " " + line).join("\n");
1732
+ lines.push(`(${cls.name} as any).__formspec_types__ = ${metadataJson};`);
1733
+ lines.push("");
1734
+ }
1735
+ lines.push("// =============================================================================");
1736
+ lines.push("// Inferred Schema Types");
1737
+ lines.push("// =============================================================================");
1738
+ lines.push("");
1739
+ for (const cls of classes) {
1740
+ lines.push(generateSchemaType(cls));
1741
+ lines.push("");
1742
+ }
1743
+ lines.push("// =============================================================================");
1744
+ lines.push("// Type-Safe FormSpec Accessors");
1745
+ lines.push("// =============================================================================");
1746
+ lines.push("");
1747
+ for (const cls of classes) {
1748
+ lines.push(generateTypedAccessor(cls));
1749
+ lines.push("");
1750
+ }
1751
+ return lines.join("\n");
1752
+ }
1753
+ function metadataToTypeString(metadata) {
1754
+ const baseType = metadataToBaseTypeString(metadata);
1755
+ if (metadata.nullable && metadata.optional) {
1756
+ return `${baseType} | null | undefined`;
1757
+ }
1758
+ if (metadata.nullable) {
1759
+ return `${baseType} | null`;
1760
+ }
1761
+ if (metadata.optional) {
1762
+ return `${baseType} | undefined`;
1763
+ }
1764
+ return baseType;
1765
+ }
1766
+ var VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1767
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
1768
+ // Keywords
1769
+ "break",
1770
+ "case",
1771
+ "catch",
1772
+ "continue",
1773
+ "debugger",
1774
+ "default",
1775
+ "delete",
1776
+ "do",
1777
+ "else",
1778
+ "finally",
1779
+ "for",
1780
+ "function",
1781
+ "if",
1782
+ "in",
1783
+ "instanceof",
1784
+ "new",
1785
+ "return",
1786
+ "switch",
1787
+ "this",
1788
+ "throw",
1789
+ "try",
1790
+ "typeof",
1791
+ "var",
1792
+ "void",
1793
+ "while",
1794
+ "with",
1795
+ "class",
1796
+ "const",
1797
+ "enum",
1798
+ "export",
1799
+ "extends",
1800
+ "import",
1801
+ "super",
1802
+ "implements",
1803
+ "interface",
1804
+ "let",
1805
+ "package",
1806
+ "private",
1807
+ "protected",
1808
+ "public",
1809
+ "static",
1810
+ "yield",
1811
+ // ES6+ keywords
1812
+ "async",
1813
+ "await",
1814
+ // Literal values (not technically keywords but best to quote)
1815
+ "null",
1816
+ "true",
1817
+ "false",
1818
+ // Accessor keywords
1819
+ "get",
1820
+ "set",
1821
+ // Strict mode reserved
1822
+ "arguments",
1823
+ "eval"
1824
+ ]);
1825
+ function needsPropertyQuoting(key) {
1826
+ if (!VALID_IDENTIFIER.test(key)) {
1827
+ return true;
1828
+ }
1829
+ return RESERVED_WORDS.has(key);
1830
+ }
1831
+ function escapePropertyKey(key) {
1832
+ if (needsPropertyQuoting(key)) {
1833
+ return JSON.stringify(key);
1834
+ }
1835
+ return key;
1836
+ }
1837
+ function metadataToBaseTypeString(metadata) {
1838
+ switch (metadata.type) {
1839
+ case "string":
1840
+ return "string";
1841
+ case "number":
1842
+ return "number";
1843
+ case "boolean":
1844
+ return "boolean";
1845
+ case "enum":
1846
+ if (metadata.values && metadata.values.length > 0) {
1847
+ return metadata.values.map((v) => typeof v === "string" ? JSON.stringify(v) : String(v)).join(" | ");
1848
+ }
1849
+ return "string";
1850
+ case "array":
1851
+ if (metadata.itemType) {
1852
+ const itemType = metadataToTypeString(metadata.itemType);
1853
+ if (itemType.includes("|")) {
1854
+ return `(${itemType})[]`;
1855
+ }
1856
+ return `${itemType}[]`;
1857
+ }
1858
+ return "unknown[]";
1859
+ case "object":
1860
+ if (metadata.properties && Object.keys(metadata.properties).length > 0) {
1861
+ const props = Object.entries(metadata.properties).map(([key, propMeta]) => {
1862
+ const optional = propMeta.optional ? "?" : "";
1863
+ return `${escapePropertyKey(key)}${optional}: ${metadataToTypeString(propMeta)}`;
1864
+ }).join("; ");
1865
+ return `{ ${props} }`;
1866
+ }
1867
+ return "Record<string, unknown>";
1868
+ default:
1869
+ return "unknown";
1870
+ }
1871
+ }
1872
+ function generateSchemaType(cls) {
1873
+ const props = Object.entries(cls.typeMetadata).map(([fieldName, metadata]) => {
1874
+ const optional = metadata.optional ? "?" : "";
1875
+ const typeStr = metadataToTypeString(metadata);
1876
+ return ` ${escapePropertyKey(fieldName)}${optional}: ${typeStr};`;
1877
+ }).join("\n");
1878
+ return `export type ${cls.name}Schema = {
1879
+ ${props}
1880
+ };`;
1881
+ }
1882
+ function metadataTypeToFieldType(type) {
1883
+ switch (type) {
1884
+ case "string":
1885
+ return "text";
1886
+ case "number":
1887
+ return "number";
1888
+ case "boolean":
1889
+ return "boolean";
1890
+ case "enum":
1891
+ return "enum";
1892
+ case "array":
1893
+ return "array";
1894
+ case "object":
1895
+ return "object";
1896
+ default:
1897
+ return "text";
1898
+ }
1899
+ }
1900
+ function generateTypedAccessor(cls) {
1901
+ const lines = [];
1902
+ const elementTypes = Object.entries(cls.typeMetadata).map(([fieldName, metadata]) => {
1903
+ const fieldType = metadataTypeToFieldType(metadata.type);
1904
+ const required = !metadata.optional;
1905
+ let elementType = `{ readonly _field: "${fieldType}"; readonly id: "${fieldName}"; readonly required: ${String(required)}`;
1906
+ if (metadata.type === "enum" && metadata.values) {
1907
+ const optionValues = metadata.values.map((v) => typeof v === "string" ? JSON.stringify(v) : String(v)).join(", ");
1908
+ elementType += `; readonly options: readonly [${optionValues}]`;
1909
+ }
1910
+ elementType += " }";
1911
+ return elementType;
1912
+ });
1913
+ lines.push(`export type ${cls.name}Elements = readonly [
1914
+ ${elementTypes.join(",\n ")}
1915
+ ];`);
1916
+ lines.push("");
1917
+ lines.push(`export type ${cls.name}FormSpec = { readonly elements: ${cls.name}Elements };`);
1918
+ lines.push("");
1919
+ lines.push(`/**`);
1920
+ lines.push(` * Type-safe FormSpec accessor for ${cls.name}.`);
1921
+ lines.push(` * Reads the patched type metadata from the class.`);
1922
+ lines.push(` */`);
1923
+ lines.push(`export function get${cls.name}FormSpec(): ${cls.name}FormSpec {`);
1924
+ lines.push(
1925
+ ` const types = (${cls.name} as any).__formspec_types__ as Record<string, unknown>[];`
1926
+ );
1927
+ lines.push(` return { elements: types } as unknown as ${cls.name}FormSpec;`);
1928
+ lines.push(`}`);
1929
+ return lines.join("\n");
1930
+ }
1931
+ function getRelativeImportPath(outputDir, sourcePath) {
1932
+ let relativePath = path2.relative(outputDir, sourcePath);
1933
+ if (!relativePath.startsWith(".")) {
1934
+ relativePath = "./" + relativePath;
1935
+ }
1936
+ relativePath = relativePath.replace(/\\/g, "/");
1937
+ return relativePath + ".js";
1938
+ }
1939
+ function runCodegen(options) {
1940
+ const baseDir = options.baseDir ?? path2.dirname(options.output);
1941
+ const absoluteFiles = options.files.map((f) => path2.resolve(f));
1942
+ console.log(`Scanning ${String(absoluteFiles.length)} file(s) for decorated classes...`);
1943
+ const classes = findDecoratedClasses(absoluteFiles, baseDir);
1944
+ if (classes.length === 0) {
1945
+ console.log("No decorated classes found.");
1946
+ return;
1947
+ }
1948
+ console.log(`Found ${String(classes.length)} decorated class(es):`);
1949
+ for (const cls of classes) {
1950
+ const fieldCount = Object.keys(cls.typeMetadata).length;
1951
+ console.log(` - ${cls.name} (${String(fieldCount)} field(s))`);
1952
+ }
1953
+ const unexported = classes.filter((cls) => !cls.isExported);
1954
+ if (unexported.length > 0) {
1955
+ console.warn(
1956
+ `
1957
+ \u26A0\uFE0F Warning: The following decorated classes are not exported from their source files:`
1958
+ );
1959
+ for (const cls of unexported) {
1960
+ console.warn(` - ${cls.name} (${cls.sourcePath})`);
1961
+ }
1962
+ console.warn(
1963
+ `
1964
+ The generated code will fail to compile because it cannot import these classes.`
1965
+ );
1966
+ console.warn(` To fix this, add 'export' to the class declaration, for example:`);
1967
+ console.warn(` export class YourClassName { ... }
1968
+ `);
1969
+ }
1970
+ const output = generateCodegenOutput(classes, options.output, baseDir);
1971
+ const outputDir = path2.dirname(options.output);
1972
+ if (!fs.existsSync(outputDir)) {
1973
+ fs.mkdirSync(outputDir, { recursive: true });
1974
+ }
1975
+ fs.writeFileSync(options.output, output);
1976
+ console.log(`
1977
+ Generated: ${options.output}`);
1978
+ }
1979
+
1980
+ // src/index.ts
1981
+ function buildFormSchemas(form) {
1982
+ return {
1983
+ jsonSchema: generateJsonSchema(form),
1984
+ uiSchema: generateUiSchema(form)
1985
+ };
64
1986
  }
65
- /**
66
- * Builds and writes both JSON Schema and UI Schema files to disk.
67
- *
68
- * This is a convenience function for build-time schema generation.
69
- * It creates the output directory if it doesn't exist.
70
- *
71
- * @example
72
- * ```typescript
73
- * import { formspec, field } from "formspec";
74
- * import { writeSchemas } from "@formspec/build";
75
- *
76
- * const ProductForm = formspec(
77
- * field.text("name", { required: true }),
78
- * field.enum("status", ["draft", "active"]),
79
- * );
80
- *
81
- * // Write schemas to ./generated/product-schema.json and ./generated/product-uischema.json
82
- * const { jsonSchemaPath, uiSchemaPath } = writeSchemas(ProductForm, {
83
- * outDir: "./generated",
84
- * name: "product",
85
- * });
86
- *
87
- * console.log(`Generated: ${jsonSchemaPath}, ${uiSchemaPath}`);
88
- * ```
89
- *
90
- * @param form - The FormSpec to build schemas from
91
- * @param options - Output options (directory, file name, indentation)
92
- * @returns Object containing paths to the generated files
93
- */
94
- export function writeSchemas(form, options) {
95
- const { outDir, name = "schema", indent = 2 } = options;
96
- // Build schemas
97
- const { jsonSchema, uiSchema } = buildFormSchemas(form);
98
- // Ensure output directory exists
99
- if (!fs.existsSync(outDir)) {
100
- fs.mkdirSync(outDir, { recursive: true });
101
- }
102
- // Write files
103
- const jsonSchemaPath = path.join(outDir, `${name}-schema.json`);
104
- const uiSchemaPath = path.join(outDir, `${name}-uischema.json`);
105
- fs.writeFileSync(jsonSchemaPath, JSON.stringify(jsonSchema, null, indent));
106
- fs.writeFileSync(uiSchemaPath, JSON.stringify(uiSchema, null, indent));
107
- return { jsonSchemaPath, uiSchemaPath };
1987
+ function writeSchemas(form, options) {
1988
+ const { outDir, name = "schema", indent = 2 } = options;
1989
+ const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form);
1990
+ if (!fs2.existsSync(outDir)) {
1991
+ fs2.mkdirSync(outDir, { recursive: true });
1992
+ }
1993
+ const jsonSchemaPath = path3.join(outDir, `${name}-schema.json`);
1994
+ const uiSchemaPath = path3.join(outDir, `${name}-uischema.json`);
1995
+ fs2.writeFileSync(jsonSchemaPath, JSON.stringify(jsonSchema, null, indent));
1996
+ fs2.writeFileSync(uiSchemaPath, JSON.stringify(uiSchema2, null, indent));
1997
+ return { jsonSchemaPath, uiSchemaPath };
108
1998
  }
1999
+ export {
2000
+ buildFormSchemas,
2001
+ categorizationSchema,
2002
+ categorySchema,
2003
+ controlSchema,
2004
+ findDecoratedClasses,
2005
+ generateCodegenOutput,
2006
+ generateJsonSchema,
2007
+ generateSchemas,
2008
+ generateSchemasFromClass,
2009
+ generateUiSchema,
2010
+ generateUiSchemaFromFields,
2011
+ getSchemaExtension,
2012
+ groupLayoutSchema,
2013
+ horizontalLayoutSchema,
2014
+ jsonSchema7Schema,
2015
+ jsonSchemaTypeSchema,
2016
+ labelElementSchema,
2017
+ ruleConditionSchema,
2018
+ ruleEffectSchema,
2019
+ ruleSchema,
2020
+ runCodegen,
2021
+ schemaBasedConditionSchema,
2022
+ setSchemaExtension,
2023
+ uiSchemaElementSchema,
2024
+ uiSchemaElementTypeSchema,
2025
+ uiSchema as uiSchemaSchema,
2026
+ verticalLayoutSchema,
2027
+ writeSchemas
2028
+ };
109
2029
  //# sourceMappingURL=index.js.map