@formspec/build 0.1.0-alpha.14 → 0.1.0-alpha.15

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 (60) hide show
  1. package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
  2. package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  4. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures/example-a-builtins.d.ts +6 -6
  6. package/dist/__tests__/fixtures/example-interface-types.d.ts +26 -26
  7. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
  8. package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -5
  9. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
  10. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  11. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  12. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  13. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  14. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  15. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  16. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  17. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  18. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  19. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  20. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  21. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  22. package/dist/__tests__/parity/utils.d.ts +6 -1
  23. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  24. package/dist/analyzer/class-analyzer.d.ts +1 -1
  25. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  26. package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
  27. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  28. package/dist/analyzer/tsdoc-parser.d.ts +6 -8
  29. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  30. package/dist/browser.cjs +387 -98
  31. package/dist/browser.cjs.map +1 -1
  32. package/dist/browser.d.ts +15 -2
  33. package/dist/browser.d.ts.map +1 -1
  34. package/dist/browser.js +385 -98
  35. package/dist/browser.js.map +1 -1
  36. package/dist/build.d.ts +131 -5
  37. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  38. package/dist/cli.cjs +272 -69
  39. package/dist/cli.cjs.map +1 -1
  40. package/dist/cli.js +271 -72
  41. package/dist/cli.js.map +1 -1
  42. package/dist/index.cjs +257 -67
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.ts +20 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +255 -71
  47. package/dist/index.js.map +1 -1
  48. package/dist/internals.cjs +461 -137
  49. package/dist/internals.cjs.map +1 -1
  50. package/dist/internals.js +459 -139
  51. package/dist/internals.js.map +1 -1
  52. package/dist/json-schema/generator.d.ts +8 -2
  53. package/dist/json-schema/generator.d.ts.map +1 -1
  54. package/dist/json-schema/ir-generator.d.ts +24 -2
  55. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  56. package/dist/json-schema/types.d.ts +1 -1
  57. package/dist/json-schema/types.d.ts.map +1 -1
  58. package/dist/validate/constraint-validator.d.ts +3 -7
  59. package/dist/validate/constraint-validator.d.ts.map +1 -1
  60. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -23,16 +23,26 @@
23
23
  * @packageDocumentation
24
24
  */
25
25
  import type { FormElement, FormSpec } from "@formspec/core";
26
- import type { JsonSchema2020 } from "./json-schema/ir-generator.js";
26
+ import { type GenerateJsonSchemaOptions } from "./json-schema/generator.js";
27
+ import { type GenerateJsonSchemaFromIROptions, type JsonSchema2020 } from "./json-schema/ir-generator.js";
27
28
  import type { UISchema } from "./ui-schema/types.js";
28
29
  export type { JsonSchema2020 } from "./json-schema/ir-generator.js";
30
+ export type { GenerateJsonSchemaFromIROptions } from "./json-schema/ir-generator.js";
31
+ export type { GenerateJsonSchemaOptions } from "./json-schema/generator.js";
29
32
  export type { JSONSchema7, JSONSchemaType, ExtendedJSONSchema7, FormSpecSchemaExtensions, } from "./json-schema/types.js";
30
33
  export { setSchemaExtension, getSchemaExtension } from "./json-schema/types.js";
34
+ export { createExtensionRegistry } from "./extensions/index.js";
35
+ export type { ExtensionRegistry } from "./extensions/index.js";
31
36
  export type { UISchema, UISchemaElement, UISchemaElementBase, UISchemaElementType, ControlElement, VerticalLayout, HorizontalLayout, GroupLayout, Categorization, Category, LabelElement, Rule, RuleEffect, RuleConditionSchema, SchemaBasedCondition, } from "./ui-schema/types.js";
32
37
  export type { ClassSchemas, GenerateFromClassOptions, GenerateFromClassResult, GenerateSchemasOptions, } from "./generators/class-schema.js";
33
38
  export { ruleEffectSchema, uiSchemaElementTypeSchema, ruleConditionSchema, schemaBasedConditionSchema, ruleSchema, controlSchema, verticalLayoutSchema, horizontalLayoutSchema, groupLayoutSchema, categorizationSchema, categorySchema, labelElementSchema, uiSchemaElementSchema, uiSchema as uiSchemaSchema, } from "./ui-schema/schema.js";
34
39
  export { jsonSchemaTypeSchema, jsonSchema7Schema } from "./json-schema/schema.js";
35
40
  export { generateJsonSchema } from "./json-schema/generator.js";
41
+ /**
42
+ * Advanced API — consumers are responsible for supplying canonical FormIR.
43
+ * Most callers should prefer `generateJsonSchema()` or `buildFormSchemas()`.
44
+ */
45
+ export { generateJsonSchemaFromIR } from "./json-schema/ir-generator.js";
36
46
  export { generateUiSchema } from "./ui-schema/generator.js";
37
47
  export { generateSchemasFromClass, generateSchemas } from "./generators/class-schema.js";
38
48
  /**
@@ -44,6 +54,13 @@ export interface BuildResult {
44
54
  /** JSON Forms UI Schema for rendering */
45
55
  readonly uiSchema: UISchema;
46
56
  }
57
+ /**
58
+ * Options for building schemas from a FormSpec.
59
+ *
60
+ * Currently identical to `GenerateJsonSchemaOptions`. Defined separately so the
61
+ * Chain DSL surface can grow independently in the future if needed.
62
+ */
63
+ export type BuildFormSchemasOptions = GenerateJsonSchemaOptions;
47
64
  /**
48
65
  * Builds both JSON Schema and UI Schema from a FormSpec.
49
66
  *
@@ -71,11 +88,11 @@ export interface BuildResult {
71
88
  * @param form - The FormSpec to build schemas from
72
89
  * @returns Object containing both jsonSchema and uiSchema
73
90
  */
74
- export declare function buildFormSchemas<E extends readonly FormElement[]>(form: FormSpec<E>): BuildResult;
91
+ export declare function buildFormSchemas<E extends readonly FormElement[]>(form: FormSpec<E>, options?: BuildFormSchemasOptions): BuildResult;
75
92
  /**
76
93
  * Options for writing schemas to disk.
77
94
  */
78
- export interface WriteSchemasOptions {
95
+ export interface WriteSchemasOptions extends GenerateJsonSchemaFromIROptions {
79
96
  /** Output directory for the schema files */
80
97
  readonly outDir: string;
81
98
  /** Base name for the output files (without extension). Defaults to "schema" */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAQrD,YAAY,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,YAAY,EACV,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,YAAY,EACV,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,IAAI,EACJ,UAAU,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AAMtC,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,IAAI,cAAc,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAMlF,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAM5D,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAKzF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,SAAS,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,WAAW,CAKjG;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,WAAW,EAAE,EAC3D,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CAmBpB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -177,7 +177,7 @@ function canonicalizeArrayField(field) {
177
177
  const itemsType = {
178
178
  kind: "object",
179
179
  properties: itemProperties,
180
- additionalProperties: false
180
+ additionalProperties: true
181
181
  };
182
182
  const type = { kind: "array", items: itemsType };
183
183
  const constraints = [];
@@ -212,7 +212,7 @@ function canonicalizeObjectField(field) {
212
212
  const type = {
213
213
  kind: "object",
214
214
  properties,
215
- additionalProperties: false
215
+ additionalProperties: true
216
216
  };
217
217
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
218
218
  }
@@ -384,11 +384,21 @@ function wrapInConditional(field, layout, provenance) {
384
384
  }
385
385
 
386
386
  // src/json-schema/ir-generator.ts
387
- function makeContext() {
388
- return { defs: {} };
387
+ function makeContext(options) {
388
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
389
+ if (!vendorPrefix.startsWith("x-")) {
390
+ throw new Error(
391
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
392
+ );
393
+ }
394
+ return {
395
+ defs: {},
396
+ extensionRegistry: options?.extensionRegistry,
397
+ vendorPrefix
398
+ };
389
399
  }
390
- function generateJsonSchemaFromIR(ir) {
391
- const ctx = makeContext();
400
+ function generateJsonSchemaFromIR(ir, options) {
401
+ const ctx = makeContext(options);
392
402
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
393
403
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
394
404
  }
@@ -440,16 +450,16 @@ function generateFieldSchema(field, ctx) {
440
450
  directConstraints.push(c);
441
451
  }
442
452
  }
443
- applyConstraints(schema, directConstraints);
444
- applyAnnotations(schema, field.annotations);
453
+ applyConstraints(schema, directConstraints, ctx);
454
+ applyAnnotations(schema, field.annotations, ctx);
445
455
  if (pathConstraints.length === 0) {
446
456
  return schema;
447
457
  }
448
- return applyPathTargetedConstraints(schema, pathConstraints);
458
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
449
459
  }
450
- function applyPathTargetedConstraints(schema, pathConstraints) {
460
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
451
461
  if (schema.type === "array" && schema.items) {
452
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
462
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
453
463
  return schema;
454
464
  }
455
465
  const byTarget = /* @__PURE__ */ new Map();
@@ -463,7 +473,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
463
473
  const propertyOverrides = {};
464
474
  for (const [target, constraints] of byTarget) {
465
475
  const subSchema = {};
466
- applyConstraints(subSchema, constraints);
476
+ applyConstraints(subSchema, constraints, ctx);
467
477
  propertyOverrides[target] = subSchema;
468
478
  }
469
479
  if (schema.$ref) {
@@ -507,6 +517,8 @@ function generateTypeNode(type, ctx) {
507
517
  return generateArrayType(type, ctx);
508
518
  case "object":
509
519
  return generateObjectType(type, ctx);
520
+ case "record":
521
+ return generateRecordType(type, ctx);
510
522
  case "union":
511
523
  return generateUnionType(type, ctx);
512
524
  case "reference":
@@ -514,7 +526,7 @@ function generateTypeNode(type, ctx) {
514
526
  case "dynamic":
515
527
  return generateDynamicType(type);
516
528
  case "custom":
517
- return generateCustomType(type);
529
+ return generateCustomType(type, ctx);
518
530
  default: {
519
531
  const _exhaustive = type;
520
532
  return _exhaustive;
@@ -563,16 +575,27 @@ function generateObjectType(type, ctx) {
563
575
  }
564
576
  return schema;
565
577
  }
578
+ function generateRecordType(type, ctx) {
579
+ return {
580
+ type: "object",
581
+ additionalProperties: generateTypeNode(type.valueType, ctx)
582
+ };
583
+ }
566
584
  function generatePropertySchema(prop, ctx) {
567
585
  const schema = generateTypeNode(prop.type, ctx);
568
- applyConstraints(schema, prop.constraints);
569
- applyAnnotations(schema, prop.annotations);
586
+ applyConstraints(schema, prop.constraints, ctx);
587
+ applyAnnotations(schema, prop.annotations, ctx);
570
588
  return schema;
571
589
  }
572
590
  function generateUnionType(type, ctx) {
573
591
  if (isBooleanUnion(type)) {
574
592
  return { type: "boolean" };
575
593
  }
594
+ if (isNullableUnion(type)) {
595
+ return {
596
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
597
+ };
598
+ }
576
599
  return {
577
600
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
578
601
  };
@@ -582,6 +605,13 @@ function isBooleanUnion(type) {
582
605
  const kinds = type.members.map((m) => m.kind);
583
606
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
584
607
  }
608
+ function isNullableUnion(type) {
609
+ if (type.members.length !== 2) return false;
610
+ const nullCount = type.members.filter(
611
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
612
+ ).length;
613
+ return nullCount === 1;
614
+ }
585
615
  function generateReferenceType(type) {
586
616
  return { $ref: `#/$defs/${type.name}` };
587
617
  }
@@ -602,10 +632,7 @@ function generateDynamicType(type) {
602
632
  "x-formspec-schemaSource": type.sourceKey
603
633
  };
604
634
  }
605
- function generateCustomType(_type) {
606
- return { type: "object" };
607
- }
608
- function applyConstraints(schema, constraints) {
635
+ function applyConstraints(schema, constraints, ctx) {
609
636
  for (const constraint of constraints) {
610
637
  switch (constraint.constraintKind) {
611
638
  case "minimum":
@@ -650,6 +677,7 @@ function applyConstraints(schema, constraints) {
650
677
  case "allowedMembers":
651
678
  break;
652
679
  case "custom":
680
+ applyCustomConstraint(schema, constraint, ctx);
653
681
  break;
654
682
  default: {
655
683
  const _exhaustive = constraint;
@@ -658,7 +686,7 @@ function applyConstraints(schema, constraints) {
658
686
  }
659
687
  }
660
688
  }
661
- function applyAnnotations(schema, annotations) {
689
+ function applyAnnotations(schema, annotations, ctx) {
662
690
  for (const annotation of annotations) {
663
691
  switch (annotation.annotationKind) {
664
692
  case "displayName":
@@ -678,6 +706,7 @@ function applyAnnotations(schema, annotations) {
678
706
  case "formatHint":
679
707
  break;
680
708
  case "custom":
709
+ applyCustomAnnotation(schema, annotation, ctx);
681
710
  break;
682
711
  default: {
683
712
  const _exhaustive = annotation;
@@ -686,11 +715,41 @@ function applyAnnotations(schema, annotations) {
686
715
  }
687
716
  }
688
717
  }
718
+ function generateCustomType(type, ctx) {
719
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
720
+ if (registration === void 0) {
721
+ throw new Error(
722
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
723
+ );
724
+ }
725
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
726
+ }
727
+ function applyCustomConstraint(schema, constraint, ctx) {
728
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
729
+ if (registration === void 0) {
730
+ throw new Error(
731
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
732
+ );
733
+ }
734
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
735
+ }
736
+ function applyCustomAnnotation(schema, annotation, ctx) {
737
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
738
+ if (registration === void 0) {
739
+ throw new Error(
740
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
741
+ );
742
+ }
743
+ if (registration.toJsonSchema === void 0) {
744
+ return;
745
+ }
746
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
747
+ }
689
748
 
690
749
  // src/json-schema/generator.ts
691
- function generateJsonSchema(form) {
750
+ function generateJsonSchema(form, options) {
692
751
  const ir = canonicalizeChainDSL(form);
693
- return generateJsonSchemaFromIR(ir);
752
+ return generateJsonSchemaFromIR(ir, options);
694
753
  }
695
754
 
696
755
  // src/ui-schema/schema.ts
@@ -924,6 +983,48 @@ function getSchemaExtension(schema, key) {
924
983
  return schema[key];
925
984
  }
926
985
 
986
+ // src/extensions/registry.ts
987
+ function createExtensionRegistry(extensions) {
988
+ const typeMap = /* @__PURE__ */ new Map();
989
+ const constraintMap = /* @__PURE__ */ new Map();
990
+ const annotationMap = /* @__PURE__ */ new Map();
991
+ for (const ext of extensions) {
992
+ if (ext.types !== void 0) {
993
+ for (const type of ext.types) {
994
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
995
+ if (typeMap.has(qualifiedId)) {
996
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
997
+ }
998
+ typeMap.set(qualifiedId, type);
999
+ }
1000
+ }
1001
+ if (ext.constraints !== void 0) {
1002
+ for (const constraint of ext.constraints) {
1003
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
1004
+ if (constraintMap.has(qualifiedId)) {
1005
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
1006
+ }
1007
+ constraintMap.set(qualifiedId, constraint);
1008
+ }
1009
+ }
1010
+ if (ext.annotations !== void 0) {
1011
+ for (const annotation of ext.annotations) {
1012
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
1013
+ if (annotationMap.has(qualifiedId)) {
1014
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
1015
+ }
1016
+ annotationMap.set(qualifiedId, annotation);
1017
+ }
1018
+ }
1019
+ }
1020
+ return {
1021
+ extensions,
1022
+ findType: (typeId) => typeMap.get(typeId),
1023
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
1024
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
1025
+ };
1026
+ }
1027
+
927
1028
  // src/json-schema/schema.ts
928
1029
  import { z as z3 } from "zod";
929
1030
  var jsonSchemaTypeSchema = z3.enum([
@@ -1063,11 +1164,6 @@ import * as ts4 from "typescript";
1063
1164
 
1064
1165
  // src/analyzer/jsdoc-constraints.ts
1065
1166
  import * as ts3 from "typescript";
1066
- import {
1067
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
1068
- isBuiltinConstraintName as isBuiltinConstraintName2,
1069
- normalizeConstraintTagName as normalizeConstraintTagName2
1070
- } from "@formspec/core";
1071
1167
 
1072
1168
  // src/analyzer/tsdoc-parser.ts
1073
1169
  import * as ts2 from "typescript";
@@ -1121,6 +1217,15 @@ function createFormSpecTSDocConfig() {
1121
1217
  })
1122
1218
  );
1123
1219
  }
1220
+ for (const tagName of ["displayName", "description"]) {
1221
+ config.addTagDefinition(
1222
+ new TSDocTagDefinition({
1223
+ tagName: "@" + tagName,
1224
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
1225
+ allowMultiple: true
1226
+ })
1227
+ );
1228
+ }
1124
1229
  return config;
1125
1230
  }
1126
1231
  var sharedParser;
@@ -1150,6 +1255,27 @@ function parseTSDocTags(node, file = "") {
1150
1255
  const docComment = parserContext.docComment;
1151
1256
  for (const block of docComment.customBlocks) {
1152
1257
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1258
+ if (tagName === "displayName" || tagName === "description") {
1259
+ const text2 = extractBlockText(block).trim();
1260
+ if (text2 === "") continue;
1261
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
1262
+ if (tagName === "displayName") {
1263
+ annotations.push({
1264
+ kind: "annotation",
1265
+ annotationKind: "displayName",
1266
+ value: text2,
1267
+ provenance: provenance2
1268
+ });
1269
+ } else {
1270
+ annotations.push({
1271
+ kind: "annotation",
1272
+ annotationKind: "description",
1273
+ value: text2,
1274
+ provenance: provenance2
1275
+ });
1276
+ }
1277
+ continue;
1278
+ }
1153
1279
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1154
1280
  const text = extractBlockText(block).trim();
1155
1281
  if (text === "") continue;
@@ -1181,41 +1307,6 @@ function parseTSDocTags(node, file = "") {
1181
1307
  constraints.push(constraintNode);
1182
1308
  }
1183
1309
  }
1184
- let displayName;
1185
- let description;
1186
- let displayNameTag;
1187
- let descriptionTag;
1188
- for (const tag of jsDocTagsAll) {
1189
- const tagName = tag.tagName.text;
1190
- const commentText = getTagCommentText(tag);
1191
- if (commentText === void 0 || commentText.trim() === "") {
1192
- continue;
1193
- }
1194
- const trimmed = commentText.trim();
1195
- if (tagName === "Field_displayName") {
1196
- displayName = trimmed;
1197
- displayNameTag = tag;
1198
- } else if (tagName === "Field_description") {
1199
- description = trimmed;
1200
- descriptionTag = tag;
1201
- }
1202
- }
1203
- if (displayName !== void 0 && displayNameTag) {
1204
- annotations.push({
1205
- kind: "annotation",
1206
- annotationKind: "displayName",
1207
- value: displayName,
1208
- provenance: provenanceForJSDocTag(displayNameTag, file)
1209
- });
1210
- }
1211
- if (description !== void 0 && descriptionTag) {
1212
- annotations.push({
1213
- kind: "annotation",
1214
- annotationKind: "description",
1215
- value: description,
1216
- provenance: provenanceForJSDocTag(descriptionTag, file)
1217
- });
1218
- }
1219
1310
  return { constraints, annotations };
1220
1311
  }
1221
1312
  function extractPathTarget(text) {
@@ -1480,18 +1571,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1480
1571
  const tsType = checker.getTypeAtLocation(prop);
1481
1572
  const optional = prop.questionToken !== void 0;
1482
1573
  const provenance = provenanceForNode(prop, file);
1483
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1574
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1484
1575
  const constraints = [];
1485
1576
  if (prop.type) {
1486
1577
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1487
1578
  }
1488
1579
  constraints.push(...extractJSDocConstraintNodes(prop, file));
1489
- const annotations = [];
1580
+ let annotations = [];
1490
1581
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1491
1582
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1492
1583
  if (defaultAnnotation) {
1493
1584
  annotations.push(defaultAnnotation);
1494
1585
  }
1586
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1495
1587
  return {
1496
1588
  kind: "field",
1497
1589
  name,
@@ -1510,14 +1602,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1510
1602
  const tsType = checker.getTypeAtLocation(prop);
1511
1603
  const optional = prop.questionToken !== void 0;
1512
1604
  const provenance = provenanceForNode(prop, file);
1513
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1605
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1514
1606
  const constraints = [];
1515
1607
  if (prop.type) {
1516
1608
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1517
1609
  }
1518
1610
  constraints.push(...extractJSDocConstraintNodes(prop, file));
1519
- const annotations = [];
1611
+ let annotations = [];
1520
1612
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1613
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1521
1614
  return {
1522
1615
  kind: "field",
1523
1616
  name,
@@ -1528,6 +1621,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1528
1621
  provenance
1529
1622
  };
1530
1623
  }
1624
+ function applyEnumMemberDisplayNames(type, annotations) {
1625
+ if (!annotations.some(
1626
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
1627
+ )) {
1628
+ return { type, annotations: [...annotations] };
1629
+ }
1630
+ const consumed = /* @__PURE__ */ new Set();
1631
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
1632
+ if (consumed.size === 0) {
1633
+ return { type, annotations: [...annotations] };
1634
+ }
1635
+ return {
1636
+ type: nextType,
1637
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
1638
+ };
1639
+ }
1640
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
1641
+ switch (type.kind) {
1642
+ case "enum":
1643
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
1644
+ case "union": {
1645
+ return {
1646
+ ...type,
1647
+ members: type.members.map(
1648
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
1649
+ )
1650
+ };
1651
+ }
1652
+ default:
1653
+ return type;
1654
+ }
1655
+ }
1656
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
1657
+ const displayNames = /* @__PURE__ */ new Map();
1658
+ for (const annotation of annotations) {
1659
+ if (annotation.annotationKind !== "displayName") continue;
1660
+ const parsed = parseEnumMemberDisplayName(annotation.value);
1661
+ if (!parsed) continue;
1662
+ consumed.add(annotation);
1663
+ const member = type.members.find((m) => String(m.value) === parsed.value);
1664
+ if (!member) continue;
1665
+ displayNames.set(String(member.value), parsed.label);
1666
+ }
1667
+ if (displayNames.size === 0) {
1668
+ return type;
1669
+ }
1670
+ return {
1671
+ ...type,
1672
+ members: type.members.map((member) => {
1673
+ const displayName = displayNames.get(String(member.value));
1674
+ return displayName !== void 0 ? { ...member, displayName } : member;
1675
+ })
1676
+ };
1677
+ }
1678
+ function parseEnumMemberDisplayName(value) {
1679
+ const trimmed = value.trim();
1680
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1681
+ if (!match?.[1] || !match[2]) return null;
1682
+ const label = match[2].trim();
1683
+ if (label === "") return null;
1684
+ return { value: match[1], label };
1685
+ }
1531
1686
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1532
1687
  if (type.flags & ts4.TypeFlags.String) {
1533
1688
  return { kind: "primitive", primitiveKind: "string" };
@@ -1638,7 +1793,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1638
1793
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1639
1794
  return { kind: "array", items };
1640
1795
  }
1796
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1797
+ if (type.getProperties().length > 0) {
1798
+ return null;
1799
+ }
1800
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1801
+ if (!indexInfo) {
1802
+ return null;
1803
+ }
1804
+ if (visiting.has(type)) {
1805
+ return null;
1806
+ }
1807
+ visiting.add(type);
1808
+ try {
1809
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1810
+ return { kind: "record", valueType };
1811
+ } finally {
1812
+ visiting.delete(type);
1813
+ }
1814
+ }
1641
1815
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1816
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1817
+ if (recordNode) {
1818
+ return recordNode;
1819
+ }
1642
1820
  if (visiting.has(type)) {
1643
1821
  return { kind: "object", properties: [], additionalProperties: false };
1644
1822
  }
@@ -1670,7 +1848,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1670
1848
  const objectNode = {
1671
1849
  kind: "object",
1672
1850
  properties,
1673
- additionalProperties: false
1851
+ additionalProperties: true
1674
1852
  };
1675
1853
  if (typeName) {
1676
1854
  typeRegistry[typeName] = {
@@ -1873,15 +2051,19 @@ function generateSchemas(options) {
1873
2051
  }
1874
2052
 
1875
2053
  // src/index.ts
1876
- function buildFormSchemas(form) {
2054
+ function buildFormSchemas(form, options) {
1877
2055
  return {
1878
- jsonSchema: generateJsonSchema(form),
2056
+ jsonSchema: generateJsonSchema(form, options),
1879
2057
  uiSchema: generateUiSchema(form)
1880
2058
  };
1881
2059
  }
1882
2060
  function writeSchemas(form, options) {
1883
- const { outDir, name = "schema", indent = 2 } = options;
1884
- const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form);
2061
+ const { outDir, name = "schema", indent = 2, extensionRegistry, vendorPrefix } = options;
2062
+ const buildOptions = extensionRegistry === void 0 && vendorPrefix === void 0 ? void 0 : {
2063
+ extensionRegistry,
2064
+ vendorPrefix
2065
+ };
2066
+ const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form, buildOptions);
1885
2067
  if (!fs.existsSync(outDir)) {
1886
2068
  fs.mkdirSync(outDir, { recursive: true });
1887
2069
  }
@@ -1896,7 +2078,9 @@ export {
1896
2078
  categorizationSchema,
1897
2079
  categorySchema,
1898
2080
  controlSchema,
2081
+ createExtensionRegistry,
1899
2082
  generateJsonSchema,
2083
+ generateJsonSchemaFromIR,
1900
2084
  generateSchemas,
1901
2085
  generateSchemasFromClass,
1902
2086
  generateUiSchema,