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

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 (52) hide show
  1. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  2. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  3. package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
  4. package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
  5. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +31 -0
  6. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  7. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  8. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  9. package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
  10. package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/parity/utils.d.ts +5 -3
  12. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  13. package/dist/analyzer/class-analyzer.d.ts +8 -5
  14. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  15. package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
  16. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  17. package/dist/analyzer/tsdoc-parser.d.ts +38 -4
  18. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  19. package/dist/browser.cjs +371 -21
  20. package/dist/browser.cjs.map +1 -1
  21. package/dist/browser.d.ts.map +1 -1
  22. package/dist/browser.js +371 -21
  23. package/dist/browser.js.map +1 -1
  24. package/dist/build.d.ts +67 -3
  25. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  26. package/dist/cli.cjs +1159 -150
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +1159 -150
  29. package/dist/cli.js.map +1 -1
  30. package/dist/extensions/registry.d.ts +25 -1
  31. package/dist/extensions/registry.d.ts.map +1 -1
  32. package/dist/generators/class-schema.d.ts +4 -4
  33. package/dist/generators/class-schema.d.ts.map +1 -1
  34. package/dist/generators/mixed-authoring.d.ts +45 -0
  35. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  36. package/dist/index.cjs +1146 -149
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1145 -149
  41. package/dist/index.js.map +1 -1
  42. package/dist/internals.cjs +1156 -149
  43. package/dist/internals.cjs.map +1 -1
  44. package/dist/internals.js +1154 -147
  45. package/dist/internals.js.map +1 -1
  46. package/dist/json-schema/ir-generator.d.ts +3 -2
  47. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  48. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  49. package/dist/validate/constraint-validator.d.ts.map +1 -1
  50. package/package.json +3 -3
  51. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -9
  52. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
@@ -376,6 +376,7 @@ function canonicalizeTSDoc(analysis, source) {
376
376
  irVersion: import_core2.IR_VERSION,
377
377
  elements,
378
378
  typeRegistry: analysis.typeRegistry,
379
+ ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
379
380
  provenance
380
381
  };
381
382
  }
@@ -540,8 +541,8 @@ var LENGTH_CONSTRAINT_MAP = {
540
541
  minItems: "minItems",
541
542
  maxItems: "maxItems"
542
543
  };
543
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
544
- function createFormSpecTSDocConfig() {
544
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
545
+ function createFormSpecTSDocConfig(extensionTagNames = []) {
545
546
  const config = new import_tsdoc.TSDocConfiguration();
546
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
547
548
  config.addTagDefinition(
@@ -552,7 +553,16 @@ function createFormSpecTSDocConfig() {
552
553
  })
553
554
  );
554
555
  }
555
- for (const tagName of ["displayName", "description"]) {
556
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
557
+ config.addTagDefinition(
558
+ new import_tsdoc.TSDocTagDefinition({
559
+ tagName: "@" + tagName,
560
+ syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
561
+ allowMultiple: true
562
+ })
563
+ );
564
+ }
565
+ for (const tagName of extensionTagNames) {
556
566
  config.addTagDefinition(
557
567
  new import_tsdoc.TSDocTagDefinition({
558
568
  tagName: "@" + tagName,
@@ -563,14 +573,31 @@ function createFormSpecTSDocConfig() {
563
573
  }
564
574
  return config;
565
575
  }
566
- var sharedParser;
567
- function getParser() {
568
- sharedParser ??= new import_tsdoc.TSDocParser(createFormSpecTSDocConfig());
569
- return sharedParser;
570
- }
571
- function parseTSDocTags(node, file = "") {
576
+ var parserCache = /* @__PURE__ */ new Map();
577
+ function getParser(options) {
578
+ const extensionTagNames = [
579
+ ...options?.extensionRegistry?.extensions.flatMap(
580
+ (extension) => (extension.constraintTags ?? []).map((tag) => tag.tagName)
581
+ ) ?? []
582
+ ].sort();
583
+ const cacheKey = extensionTagNames.join("|");
584
+ const existing = parserCache.get(cacheKey);
585
+ if (existing) {
586
+ return existing;
587
+ }
588
+ const parser = new import_tsdoc.TSDocParser(createFormSpecTSDocConfig(extensionTagNames));
589
+ parserCache.set(cacheKey, parser);
590
+ return parser;
591
+ }
592
+ function parseTSDocTags(node, file = "", options) {
572
593
  const constraints = [];
573
594
  const annotations = [];
595
+ let displayName;
596
+ let description;
597
+ let placeholder;
598
+ let displayNameProvenance;
599
+ let descriptionProvenance;
600
+ let placeholderProvenance;
574
601
  const sourceFile = node.getSourceFile();
575
602
  const sourceText = sourceFile.getFullText();
576
603
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -583,52 +610,92 @@ function parseTSDocTags(node, file = "") {
583
610
  if (!commentText.startsWith("/**")) {
584
611
  continue;
585
612
  }
586
- const parser = getParser();
613
+ const parser = getParser(options);
587
614
  const parserContext = parser.parseRange(
588
615
  import_tsdoc.TextRange.fromStringRange(sourceText, range.pos, range.end)
589
616
  );
590
617
  const docComment = parserContext.docComment;
591
618
  for (const block of docComment.customBlocks) {
592
619
  const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
593
- if (tagName === "displayName" || tagName === "description") {
620
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
594
621
  const text2 = extractBlockText(block).trim();
595
622
  if (text2 === "") continue;
596
623
  const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
597
624
  if (tagName === "displayName") {
625
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
626
+ displayName = text2;
627
+ displayNameProvenance = provenance2;
628
+ }
629
+ } else if (tagName === "format") {
598
630
  annotations.push({
599
631
  kind: "annotation",
600
- annotationKind: "displayName",
632
+ annotationKind: "format",
601
633
  value: text2,
602
634
  provenance: provenance2
603
635
  });
604
636
  } else {
605
- annotations.push({
606
- kind: "annotation",
607
- annotationKind: "description",
608
- value: text2,
609
- provenance: provenance2
610
- });
637
+ if (tagName === "description" && description === void 0) {
638
+ description = text2;
639
+ descriptionProvenance = provenance2;
640
+ } else if (tagName === "placeholder" && placeholder === void 0) {
641
+ placeholder = text2;
642
+ placeholderProvenance = provenance2;
643
+ }
611
644
  }
612
645
  continue;
613
646
  }
614
647
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
615
648
  const text = extractBlockText(block).trim();
616
- if (text === "") continue;
649
+ const expectedType = (0, import_core3.isBuiltinConstraintName)(tagName) ? import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
650
+ if (text === "" && expectedType !== "boolean") continue;
617
651
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
618
- const constraintNode = parseConstraintValue(tagName, text, provenance);
652
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
619
653
  if (constraintNode) {
620
654
  constraints.push(constraintNode);
621
655
  }
622
656
  }
623
657
  if (docComment.deprecatedBlock !== void 0) {
658
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
624
659
  annotations.push({
625
660
  kind: "annotation",
626
661
  annotationKind: "deprecated",
662
+ ...message !== "" && { message },
627
663
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
628
664
  });
629
665
  }
666
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
667
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
668
+ if (remarks !== "") {
669
+ description = remarks;
670
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
671
+ }
672
+ }
630
673
  }
631
674
  }
675
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
676
+ annotations.push({
677
+ kind: "annotation",
678
+ annotationKind: "displayName",
679
+ value: displayName,
680
+ provenance: displayNameProvenance
681
+ });
682
+ }
683
+ if (description !== void 0 && descriptionProvenance !== void 0) {
684
+ annotations.push({
685
+ kind: "annotation",
686
+ annotationKind: "description",
687
+ value: description,
688
+ provenance: descriptionProvenance
689
+ });
690
+ }
691
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
692
+ annotations.push({
693
+ kind: "annotation",
694
+ annotationKind: "placeholder",
695
+ value: placeholder,
696
+ provenance: placeholderProvenance
697
+ });
698
+ }
632
699
  const jsDocTagsAll = ts2.getJSDocTags(node);
633
700
  for (const tag of jsDocTagsAll) {
634
701
  const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
@@ -637,13 +704,40 @@ function parseTSDocTags(node, file = "") {
637
704
  if (commentText === void 0 || commentText.trim() === "") continue;
638
705
  const text = commentText.trim();
639
706
  const provenance = provenanceForJSDocTag(tag, file);
640
- const constraintNode = parseConstraintValue(tagName, text, provenance);
707
+ if (tagName === "defaultValue") {
708
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
709
+ annotations.push(defaultValueNode);
710
+ continue;
711
+ }
712
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
641
713
  if (constraintNode) {
642
714
  constraints.push(constraintNode);
643
715
  }
644
716
  }
645
717
  return { constraints, annotations };
646
718
  }
719
+ function extractDisplayNameMetadata(node) {
720
+ let displayName;
721
+ const memberDisplayNames = /* @__PURE__ */ new Map();
722
+ for (const tag of ts2.getJSDocTags(node)) {
723
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
724
+ if (tagName !== "displayName") continue;
725
+ const commentText = getTagCommentText(tag);
726
+ if (commentText === void 0) continue;
727
+ const text = commentText.trim();
728
+ if (text === "") continue;
729
+ const memberTarget = parseMemberTargetDisplayName(text);
730
+ if (memberTarget) {
731
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
732
+ continue;
733
+ }
734
+ displayName ??= text;
735
+ }
736
+ return {
737
+ ...displayName !== void 0 && { displayName },
738
+ memberDisplayNames
739
+ };
740
+ }
647
741
  function extractPathTarget(text) {
648
742
  const trimmed = text.trimStart();
649
743
  const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
@@ -671,7 +765,11 @@ function extractPlainText(node) {
671
765
  }
672
766
  return result;
673
767
  }
674
- function parseConstraintValue(tagName, text, provenance) {
768
+ function parseConstraintValue(tagName, text, provenance, options) {
769
+ const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
770
+ if (customConstraint) {
771
+ return customConstraint;
772
+ }
675
773
  if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
676
774
  return null;
677
775
  }
@@ -706,7 +804,45 @@ function parseConstraintValue(tagName, text, provenance) {
706
804
  }
707
805
  return null;
708
806
  }
807
+ if (expectedType === "boolean") {
808
+ const trimmed = effectiveText.trim();
809
+ if (trimmed !== "" && trimmed !== "true") {
810
+ return null;
811
+ }
812
+ if (tagName === "uniqueItems") {
813
+ return {
814
+ kind: "constraint",
815
+ constraintKind: "uniqueItems",
816
+ value: true,
817
+ ...path2 && { path: path2 },
818
+ provenance
819
+ };
820
+ }
821
+ return null;
822
+ }
709
823
  if (expectedType === "json") {
824
+ if (tagName === "const") {
825
+ const trimmedText = effectiveText.trim();
826
+ if (trimmedText === "") return null;
827
+ try {
828
+ const parsed2 = JSON.parse(trimmedText);
829
+ return {
830
+ kind: "constraint",
831
+ constraintKind: "const",
832
+ value: parsed2,
833
+ ...path2 && { path: path2 },
834
+ provenance
835
+ };
836
+ } catch {
837
+ return {
838
+ kind: "constraint",
839
+ constraintKind: "const",
840
+ value: trimmedText,
841
+ ...path2 && { path: path2 },
842
+ provenance
843
+ };
844
+ }
845
+ }
710
846
  const parsed = tryParseJson(effectiveText);
711
847
  if (!Array.isArray(parsed)) {
712
848
  return null;
@@ -738,6 +874,111 @@ function parseConstraintValue(tagName, text, provenance) {
738
874
  provenance
739
875
  };
740
876
  }
877
+ function parseExtensionConstraintValue(tagName, text, provenance, options) {
878
+ const pathResult = extractPathTarget(text);
879
+ const effectiveText = pathResult ? pathResult.remainingText : text;
880
+ const path2 = pathResult?.path;
881
+ const registry = options?.extensionRegistry;
882
+ if (registry === void 0) {
883
+ return null;
884
+ }
885
+ const directTag = registry.findConstraintTag(tagName);
886
+ if (directTag !== void 0) {
887
+ return makeCustomConstraintNode(
888
+ directTag.extensionId,
889
+ directTag.registration.constraintName,
890
+ directTag.registration.parseValue(effectiveText),
891
+ provenance,
892
+ path2,
893
+ registry
894
+ );
895
+ }
896
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
897
+ return null;
898
+ }
899
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
900
+ if (broadenedTypeId === void 0) {
901
+ return null;
902
+ }
903
+ const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
904
+ if (broadened === void 0) {
905
+ return null;
906
+ }
907
+ return makeCustomConstraintNode(
908
+ broadened.extensionId,
909
+ broadened.registration.constraintName,
910
+ broadened.registration.parseValue(effectiveText),
911
+ provenance,
912
+ path2,
913
+ registry
914
+ );
915
+ }
916
+ function getBroadenedCustomTypeId(fieldType) {
917
+ if (fieldType?.kind === "custom") {
918
+ return fieldType.typeId;
919
+ }
920
+ if (fieldType?.kind !== "union") {
921
+ return void 0;
922
+ }
923
+ const customMembers = fieldType.members.filter(
924
+ (member) => member.kind === "custom"
925
+ );
926
+ if (customMembers.length !== 1) {
927
+ return void 0;
928
+ }
929
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
930
+ const allOtherMembersAreNull = nonCustomMembers.every(
931
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
932
+ );
933
+ const customMember = customMembers[0];
934
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
935
+ }
936
+ function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path2, registry) {
937
+ const constraintId = `${extensionId}/${constraintName}`;
938
+ const registration = registry.findConstraint(constraintId);
939
+ if (registration === void 0) {
940
+ throw new Error(
941
+ `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
942
+ );
943
+ }
944
+ return {
945
+ kind: "constraint",
946
+ constraintKind: "custom",
947
+ constraintId,
948
+ payload,
949
+ compositionRule: registration.compositionRule,
950
+ ...path2 && { path: path2 },
951
+ provenance
952
+ };
953
+ }
954
+ function parseDefaultValueValue(text, provenance) {
955
+ const trimmed = text.trim();
956
+ let value;
957
+ if (trimmed === "null") {
958
+ value = null;
959
+ } else if (trimmed === "true") {
960
+ value = true;
961
+ } else if (trimmed === "false") {
962
+ value = false;
963
+ } else {
964
+ const parsed = tryParseJson(trimmed);
965
+ value = parsed !== null ? parsed : trimmed;
966
+ }
967
+ return {
968
+ kind: "annotation",
969
+ annotationKind: "defaultValue",
970
+ value,
971
+ provenance
972
+ };
973
+ }
974
+ function isMemberTargetDisplayName(text) {
975
+ return parseMemberTargetDisplayName(text) !== null;
976
+ }
977
+ function parseMemberTargetDisplayName(text) {
978
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
979
+ if (!match?.[1] || !match[2]) return null;
980
+ return { target: match[1], label: match[2].trim() };
981
+ }
741
982
  function provenanceForComment(range, sourceFile, file, tagName) {
742
983
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
743
984
  return {
@@ -770,12 +1011,12 @@ function getTagCommentText(tag) {
770
1011
  }
771
1012
 
772
1013
  // src/analyzer/jsdoc-constraints.ts
773
- function extractJSDocConstraintNodes(node, file = "") {
774
- const result = parseTSDocTags(node, file);
1014
+ function extractJSDocConstraintNodes(node, file = "", options) {
1015
+ const result = parseTSDocTags(node, file, options);
775
1016
  return [...result.constraints];
776
1017
  }
777
- function extractJSDocAnnotationNodes(node, file = "") {
778
- const result = parseTSDocTags(node, file);
1018
+ function extractJSDocAnnotationNodes(node, file = "", options) {
1019
+ const result = parseTSDocTags(node, file, options);
779
1020
  return [...result.annotations];
780
1021
  }
781
1022
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -819,17 +1060,43 @@ function isObjectType(type) {
819
1060
  function isTypeReference(type) {
820
1061
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
821
1062
  }
822
- function analyzeClassToIR(classDecl, checker, file = "") {
1063
+ var RESOLVING_TYPE_PLACEHOLDER = {
1064
+ kind: "object",
1065
+ properties: [],
1066
+ additionalProperties: true
1067
+ };
1068
+ function makeParseOptions(extensionRegistry, fieldType) {
1069
+ if (extensionRegistry === void 0 && fieldType === void 0) {
1070
+ return void 0;
1071
+ }
1072
+ return {
1073
+ ...extensionRegistry !== void 0 && { extensionRegistry },
1074
+ ...fieldType !== void 0 && { fieldType }
1075
+ };
1076
+ }
1077
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
823
1078
  const name = classDecl.name?.text ?? "AnonymousClass";
824
1079
  const fields = [];
825
1080
  const fieldLayouts = [];
826
1081
  const typeRegistry = {};
1082
+ const annotations = extractJSDocAnnotationNodes(
1083
+ classDecl,
1084
+ file,
1085
+ makeParseOptions(extensionRegistry)
1086
+ );
827
1087
  const visiting = /* @__PURE__ */ new Set();
828
1088
  const instanceMethods = [];
829
1089
  const staticMethods = [];
830
1090
  for (const member of classDecl.members) {
831
1091
  if (ts4.isPropertyDeclaration(member)) {
832
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1092
+ const fieldNode = analyzeFieldToIR(
1093
+ member,
1094
+ checker,
1095
+ file,
1096
+ typeRegistry,
1097
+ visiting,
1098
+ extensionRegistry
1099
+ );
833
1100
  if (fieldNode) {
834
1101
  fields.push(fieldNode);
835
1102
  fieldLayouts.push({});
@@ -846,25 +1113,53 @@ function analyzeClassToIR(classDecl, checker, file = "") {
846
1113
  }
847
1114
  }
848
1115
  }
849
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
1116
+ return {
1117
+ name,
1118
+ fields,
1119
+ fieldLayouts,
1120
+ typeRegistry,
1121
+ ...annotations.length > 0 && { annotations },
1122
+ instanceMethods,
1123
+ staticMethods
1124
+ };
850
1125
  }
851
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1126
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
852
1127
  const name = interfaceDecl.name.text;
853
1128
  const fields = [];
854
1129
  const typeRegistry = {};
1130
+ const annotations = extractJSDocAnnotationNodes(
1131
+ interfaceDecl,
1132
+ file,
1133
+ makeParseOptions(extensionRegistry)
1134
+ );
855
1135
  const visiting = /* @__PURE__ */ new Set();
856
1136
  for (const member of interfaceDecl.members) {
857
1137
  if (ts4.isPropertySignature(member)) {
858
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1138
+ const fieldNode = analyzeInterfacePropertyToIR(
1139
+ member,
1140
+ checker,
1141
+ file,
1142
+ typeRegistry,
1143
+ visiting,
1144
+ extensionRegistry
1145
+ );
859
1146
  if (fieldNode) {
860
1147
  fields.push(fieldNode);
861
1148
  }
862
1149
  }
863
1150
  }
864
1151
  const fieldLayouts = fields.map(() => ({}));
865
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
1152
+ return {
1153
+ name,
1154
+ fields,
1155
+ fieldLayouts,
1156
+ typeRegistry,
1157
+ ...annotations.length > 0 && { annotations },
1158
+ instanceMethods: [],
1159
+ staticMethods: []
1160
+ };
866
1161
  }
867
- function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1162
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
868
1163
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
869
1164
  const sourceFile = typeAlias.getSourceFile();
870
1165
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -877,10 +1172,22 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
877
1172
  const name = typeAlias.name.text;
878
1173
  const fields = [];
879
1174
  const typeRegistry = {};
1175
+ const annotations = extractJSDocAnnotationNodes(
1176
+ typeAlias,
1177
+ file,
1178
+ makeParseOptions(extensionRegistry)
1179
+ );
880
1180
  const visiting = /* @__PURE__ */ new Set();
881
1181
  for (const member of typeAlias.type.members) {
882
1182
  if (ts4.isPropertySignature(member)) {
883
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1183
+ const fieldNode = analyzeInterfacePropertyToIR(
1184
+ member,
1185
+ checker,
1186
+ file,
1187
+ typeRegistry,
1188
+ visiting,
1189
+ extensionRegistry
1190
+ );
884
1191
  if (fieldNode) {
885
1192
  fields.push(fieldNode);
886
1193
  }
@@ -893,12 +1200,13 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
893
1200
  fields,
894
1201
  fieldLayouts: fields.map(() => ({})),
895
1202
  typeRegistry,
1203
+ ...annotations.length > 0 && { annotations },
896
1204
  instanceMethods: [],
897
1205
  staticMethods: []
898
1206
  }
899
1207
  };
900
1208
  }
901
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1209
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
902
1210
  if (!ts4.isIdentifier(prop.name)) {
903
1211
  return null;
904
1212
  }
@@ -906,16 +1214,28 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
906
1214
  const tsType = checker.getTypeAtLocation(prop);
907
1215
  const optional = prop.questionToken !== void 0;
908
1216
  const provenance = provenanceForNode(prop, file);
909
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1217
+ let type = resolveTypeNode(
1218
+ tsType,
1219
+ checker,
1220
+ file,
1221
+ typeRegistry,
1222
+ visiting,
1223
+ prop,
1224
+ extensionRegistry
1225
+ );
910
1226
  const constraints = [];
911
1227
  if (prop.type) {
912
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1228
+ constraints.push(
1229
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1230
+ );
913
1231
  }
914
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1232
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
915
1233
  let annotations = [];
916
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1234
+ annotations.push(
1235
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1236
+ );
917
1237
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
918
- if (defaultAnnotation) {
1238
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
919
1239
  annotations.push(defaultAnnotation);
920
1240
  }
921
1241
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
@@ -929,7 +1249,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
929
1249
  provenance
930
1250
  };
931
1251
  }
932
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
1252
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
933
1253
  if (!ts4.isIdentifier(prop.name)) {
934
1254
  return null;
935
1255
  }
@@ -937,14 +1257,26 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
937
1257
  const tsType = checker.getTypeAtLocation(prop);
938
1258
  const optional = prop.questionToken !== void 0;
939
1259
  const provenance = provenanceForNode(prop, file);
940
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1260
+ let type = resolveTypeNode(
1261
+ tsType,
1262
+ checker,
1263
+ file,
1264
+ typeRegistry,
1265
+ visiting,
1266
+ prop,
1267
+ extensionRegistry
1268
+ );
941
1269
  const constraints = [];
942
1270
  if (prop.type) {
943
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1271
+ constraints.push(
1272
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1273
+ );
944
1274
  }
945
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1275
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
946
1276
  let annotations = [];
947
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1277
+ annotations.push(
1278
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1279
+ );
948
1280
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
949
1281
  return {
950
1282
  kind: "field",
@@ -1018,7 +1350,66 @@ function parseEnumMemberDisplayName(value) {
1018
1350
  if (label === "") return null;
1019
1351
  return { value: match[1], label };
1020
1352
  }
1021
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1353
+ function resolveRegisteredCustomType(sourceNode, extensionRegistry, checker) {
1354
+ if (sourceNode === void 0 || extensionRegistry === void 0) {
1355
+ return null;
1356
+ }
1357
+ const typeNode = extractTypeNodeFromSource(sourceNode);
1358
+ if (typeNode === void 0) {
1359
+ return null;
1360
+ }
1361
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker);
1362
+ }
1363
+ function resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker) {
1364
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1365
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode.type, extensionRegistry, checker);
1366
+ }
1367
+ const typeName = getTypeNodeRegistrationName(typeNode);
1368
+ if (typeName === null) {
1369
+ return null;
1370
+ }
1371
+ const registration = extensionRegistry.findTypeByName(typeName);
1372
+ if (registration !== void 0) {
1373
+ return {
1374
+ kind: "custom",
1375
+ typeId: `${registration.extensionId}/${registration.registration.typeName}`,
1376
+ payload: null
1377
+ };
1378
+ }
1379
+ if (ts4.isTypeReferenceNode(typeNode) && ts4.isIdentifier(typeNode.typeName)) {
1380
+ const aliasDecl = checker.getSymbolAtLocation(typeNode.typeName)?.declarations?.find(ts4.isTypeAliasDeclaration);
1381
+ if (aliasDecl !== void 0) {
1382
+ return resolveRegisteredCustomTypeFromTypeNode(aliasDecl.type, extensionRegistry, checker);
1383
+ }
1384
+ }
1385
+ return null;
1386
+ }
1387
+ function extractTypeNodeFromSource(sourceNode) {
1388
+ if (ts4.isPropertyDeclaration(sourceNode) || ts4.isPropertySignature(sourceNode) || ts4.isParameter(sourceNode) || ts4.isTypeAliasDeclaration(sourceNode)) {
1389
+ return sourceNode.type;
1390
+ }
1391
+ if (ts4.isTypeNode(sourceNode)) {
1392
+ return sourceNode;
1393
+ }
1394
+ return void 0;
1395
+ }
1396
+ function getTypeNodeRegistrationName(typeNode) {
1397
+ if (ts4.isTypeReferenceNode(typeNode)) {
1398
+ return ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.right.text;
1399
+ }
1400
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1401
+ return getTypeNodeRegistrationName(typeNode.type);
1402
+ }
1403
+ if (typeNode.kind === ts4.SyntaxKind.BigIntKeyword || typeNode.kind === ts4.SyntaxKind.StringKeyword || typeNode.kind === ts4.SyntaxKind.NumberKeyword || typeNode.kind === ts4.SyntaxKind.BooleanKeyword) {
1404
+ return typeNode.getText();
1405
+ }
1406
+ return null;
1407
+ }
1408
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1409
+ const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1410
+ if (customType) {
1411
+ return customType;
1412
+ }
1022
1413
  if (type.flags & ts4.TypeFlags.String) {
1023
1414
  return { kind: "primitive", primitiveKind: "string" };
1024
1415
  }
@@ -1047,88 +1438,162 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1047
1438
  };
1048
1439
  }
1049
1440
  if (type.isUnion()) {
1050
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1441
+ return resolveUnionType(
1442
+ type,
1443
+ checker,
1444
+ file,
1445
+ typeRegistry,
1446
+ visiting,
1447
+ sourceNode,
1448
+ extensionRegistry
1449
+ );
1051
1450
  }
1052
1451
  if (checker.isArrayType(type)) {
1053
- return resolveArrayType(type, checker, file, typeRegistry, visiting);
1452
+ return resolveArrayType(
1453
+ type,
1454
+ checker,
1455
+ file,
1456
+ typeRegistry,
1457
+ visiting,
1458
+ sourceNode,
1459
+ extensionRegistry
1460
+ );
1054
1461
  }
1055
1462
  if (isObjectType(type)) {
1056
- return resolveObjectType(type, checker, file, typeRegistry, visiting);
1463
+ return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1057
1464
  }
1058
1465
  return { kind: "primitive", primitiveKind: "string" };
1059
1466
  }
1060
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
1467
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1468
+ const typeName = getNamedTypeName(type);
1469
+ const namedDecl = getNamedTypeDeclaration(type);
1470
+ if (typeName && typeName in typeRegistry) {
1471
+ return { kind: "reference", name: typeName, typeArguments: [] };
1472
+ }
1061
1473
  const allTypes = type.types;
1474
+ const unionMemberTypeNodes = extractUnionMemberTypeNodes(sourceNode, checker);
1475
+ const nonNullSourceNodes = unionMemberTypeNodes.filter(
1476
+ (memberTypeNode) => !isNullishTypeNode(resolveAliasedTypeNode(memberTypeNode, checker))
1477
+ );
1062
1478
  const nonNullTypes = allTypes.filter(
1063
- (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1479
+ (memberType) => !(memberType.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1064
1480
  );
1481
+ const nonNullMembers = nonNullTypes.map((memberType, index) => ({
1482
+ memberType,
1483
+ sourceNode: nonNullSourceNodes.length === nonNullTypes.length ? nonNullSourceNodes[index] : void 0
1484
+ }));
1065
1485
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1486
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1487
+ if (namedDecl) {
1488
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
1489
+ memberDisplayNames.set(value, label);
1490
+ }
1491
+ }
1492
+ if (sourceNode) {
1493
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
1494
+ memberDisplayNames.set(value, label);
1495
+ }
1496
+ }
1497
+ const registerNamed = (result) => {
1498
+ if (!typeName) {
1499
+ return result;
1500
+ }
1501
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1502
+ typeRegistry[typeName] = {
1503
+ name: typeName,
1504
+ type: result,
1505
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1506
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
1507
+ };
1508
+ return { kind: "reference", name: typeName, typeArguments: [] };
1509
+ };
1510
+ const applyMemberLabels = (members2) => members2.map((value) => {
1511
+ const displayName = memberDisplayNames.get(String(value));
1512
+ return displayName !== void 0 ? { value, displayName } : { value };
1513
+ });
1066
1514
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1067
1515
  if (isBooleanUnion2) {
1068
1516
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1069
- if (hasNull) {
1070
- return {
1071
- kind: "union",
1072
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1073
- };
1074
- }
1075
- return boolNode;
1517
+ const result = hasNull ? {
1518
+ kind: "union",
1519
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1520
+ } : boolNode;
1521
+ return registerNamed(result);
1076
1522
  }
1077
1523
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1078
1524
  if (allStringLiterals && nonNullTypes.length > 0) {
1079
1525
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1080
1526
  const enumNode = {
1081
1527
  kind: "enum",
1082
- members: stringTypes.map((t) => ({ value: t.value }))
1528
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1083
1529
  };
1084
- if (hasNull) {
1085
- return {
1086
- kind: "union",
1087
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1088
- };
1089
- }
1090
- return enumNode;
1530
+ const result = hasNull ? {
1531
+ kind: "union",
1532
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1533
+ } : enumNode;
1534
+ return registerNamed(result);
1091
1535
  }
1092
1536
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1093
1537
  if (allNumberLiterals && nonNullTypes.length > 0) {
1094
1538
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1095
1539
  const enumNode = {
1096
1540
  kind: "enum",
1097
- members: numberTypes.map((t) => ({ value: t.value }))
1541
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1098
1542
  };
1099
- if (hasNull) {
1100
- return {
1101
- kind: "union",
1102
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1103
- };
1104
- }
1105
- return enumNode;
1106
- }
1107
- if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1108
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1109
- if (hasNull) {
1110
- return {
1111
- kind: "union",
1112
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1113
- };
1114
- }
1115
- return inner;
1116
- }
1117
- const members = nonNullTypes.map(
1118
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1543
+ const result = hasNull ? {
1544
+ kind: "union",
1545
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1546
+ } : enumNode;
1547
+ return registerNamed(result);
1548
+ }
1549
+ if (nonNullMembers.length === 1 && nonNullMembers[0]) {
1550
+ const inner = resolveTypeNode(
1551
+ nonNullMembers[0].memberType,
1552
+ checker,
1553
+ file,
1554
+ typeRegistry,
1555
+ visiting,
1556
+ nonNullMembers[0].sourceNode ?? sourceNode,
1557
+ extensionRegistry
1558
+ );
1559
+ const result = hasNull ? {
1560
+ kind: "union",
1561
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
1562
+ } : inner;
1563
+ return registerNamed(result);
1564
+ }
1565
+ const members = nonNullMembers.map(
1566
+ ({ memberType, sourceNode: memberSourceNode }) => resolveTypeNode(
1567
+ memberType,
1568
+ checker,
1569
+ file,
1570
+ typeRegistry,
1571
+ visiting,
1572
+ memberSourceNode ?? sourceNode,
1573
+ extensionRegistry
1574
+ )
1119
1575
  );
1120
1576
  if (hasNull) {
1121
1577
  members.push({ kind: "primitive", primitiveKind: "null" });
1122
1578
  }
1123
- return { kind: "union", members };
1579
+ return registerNamed({ kind: "union", members });
1124
1580
  }
1125
- function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1581
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1126
1582
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1127
1583
  const elementType = typeArgs?.[0];
1128
- const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1584
+ const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
1585
+ const items = elementType ? resolveTypeNode(
1586
+ elementType,
1587
+ checker,
1588
+ file,
1589
+ typeRegistry,
1590
+ visiting,
1591
+ elementSourceNode,
1592
+ extensionRegistry
1593
+ ) : { kind: "primitive", primitiveKind: "string" };
1129
1594
  return { kind: "array", items };
1130
1595
  }
1131
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1596
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1132
1597
  if (type.getProperties().length > 0) {
1133
1598
  return null;
1134
1599
  }
@@ -1136,39 +1601,123 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1136
1601
  if (!indexInfo) {
1137
1602
  return null;
1138
1603
  }
1139
- if (visiting.has(type)) {
1140
- return null;
1141
- }
1142
- visiting.add(type);
1143
- try {
1144
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1145
- return { kind: "record", valueType };
1146
- } finally {
1147
- visiting.delete(type);
1148
- }
1604
+ const valueType = resolveTypeNode(
1605
+ indexInfo.type,
1606
+ checker,
1607
+ file,
1608
+ typeRegistry,
1609
+ visiting,
1610
+ void 0,
1611
+ extensionRegistry
1612
+ );
1613
+ return { kind: "record", valueType };
1149
1614
  }
1150
- function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1151
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1152
- if (recordNode) {
1153
- return recordNode;
1615
+ function typeNodeContainsReference(type, targetName) {
1616
+ switch (type.kind) {
1617
+ case "reference":
1618
+ return type.name === targetName;
1619
+ case "array":
1620
+ return typeNodeContainsReference(type.items, targetName);
1621
+ case "record":
1622
+ return typeNodeContainsReference(type.valueType, targetName);
1623
+ case "union":
1624
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
1625
+ case "object":
1626
+ return type.properties.some(
1627
+ (property) => typeNodeContainsReference(property.type, targetName)
1628
+ );
1629
+ case "primitive":
1630
+ case "enum":
1631
+ case "dynamic":
1632
+ case "custom":
1633
+ return false;
1634
+ default: {
1635
+ const _exhaustive = type;
1636
+ return _exhaustive;
1637
+ }
1154
1638
  }
1639
+ }
1640
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1641
+ const typeName = getNamedTypeName(type);
1642
+ const namedTypeName = typeName ?? void 0;
1643
+ const namedDecl = getNamedTypeDeclaration(type);
1644
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
1645
+ const clearNamedTypeRegistration = () => {
1646
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
1647
+ return;
1648
+ }
1649
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
1650
+ };
1155
1651
  if (visiting.has(type)) {
1652
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1653
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1654
+ }
1156
1655
  return { kind: "object", properties: [], additionalProperties: false };
1157
1656
  }
1657
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
1658
+ typeRegistry[namedTypeName] = {
1659
+ name: namedTypeName,
1660
+ type: RESOLVING_TYPE_PLACEHOLDER,
1661
+ provenance: provenanceForDeclaration(namedDecl, file)
1662
+ };
1663
+ }
1158
1664
  visiting.add(type);
1159
- const typeName = getNamedTypeName(type);
1160
- if (typeName && typeName in typeRegistry) {
1665
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
1666
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
1667
+ visiting.delete(type);
1668
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1669
+ }
1670
+ }
1671
+ const recordNode = tryResolveRecordType(
1672
+ type,
1673
+ checker,
1674
+ file,
1675
+ typeRegistry,
1676
+ visiting,
1677
+ extensionRegistry
1678
+ );
1679
+ if (recordNode) {
1161
1680
  visiting.delete(type);
1162
- return { kind: "reference", name: typeName, typeArguments: [] };
1681
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1682
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
1683
+ if (!isRecursiveRecord) {
1684
+ clearNamedTypeRegistration();
1685
+ return recordNode;
1686
+ }
1687
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1688
+ typeRegistry[namedTypeName] = {
1689
+ name: namedTypeName,
1690
+ type: recordNode,
1691
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1692
+ provenance: provenanceForDeclaration(namedDecl, file)
1693
+ };
1694
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1695
+ }
1696
+ return recordNode;
1163
1697
  }
1164
1698
  const properties = [];
1165
- const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
1699
+ const fieldInfoMap = getNamedTypeFieldNodeInfoMap(
1700
+ type,
1701
+ checker,
1702
+ file,
1703
+ typeRegistry,
1704
+ visiting,
1705
+ extensionRegistry
1706
+ );
1166
1707
  for (const prop of type.getProperties()) {
1167
1708
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1168
1709
  if (!declaration) continue;
1169
1710
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1170
1711
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1171
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1712
+ const propTypeNode = resolveTypeNode(
1713
+ propType,
1714
+ checker,
1715
+ file,
1716
+ typeRegistry,
1717
+ visiting,
1718
+ declaration,
1719
+ extensionRegistry
1720
+ );
1172
1721
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1173
1722
  properties.push({
1174
1723
  name: prop.name,
@@ -1185,17 +1734,19 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1185
1734
  properties,
1186
1735
  additionalProperties: true
1187
1736
  };
1188
- if (typeName) {
1189
- typeRegistry[typeName] = {
1190
- name: typeName,
1737
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1738
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1739
+ typeRegistry[namedTypeName] = {
1740
+ name: namedTypeName,
1191
1741
  type: objectNode,
1192
- provenance: provenanceForFile(file)
1742
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1743
+ provenance: provenanceForDeclaration(namedDecl, file)
1193
1744
  };
1194
- return { kind: "reference", name: typeName, typeArguments: [] };
1745
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1195
1746
  }
1196
1747
  return objectNode;
1197
1748
  }
1198
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
1749
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1199
1750
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1200
1751
  (s) => s?.declarations != null && s.declarations.length > 0
1201
1752
  );
@@ -1207,7 +1758,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1207
1758
  const map = /* @__PURE__ */ new Map();
1208
1759
  for (const member of classDecl.members) {
1209
1760
  if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1210
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1761
+ const fieldNode = analyzeFieldToIR(
1762
+ member,
1763
+ checker,
1764
+ file,
1765
+ typeRegistry,
1766
+ visiting,
1767
+ extensionRegistry
1768
+ );
1211
1769
  if (fieldNode) {
1212
1770
  map.set(fieldNode.name, {
1213
1771
  constraints: [...fieldNode.constraints],
@@ -1221,7 +1779,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1221
1779
  }
1222
1780
  const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1223
1781
  if (interfaceDecl) {
1224
- return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
1782
+ return buildFieldNodeInfoMap(
1783
+ interfaceDecl.members,
1784
+ checker,
1785
+ file,
1786
+ typeRegistry,
1787
+ visiting,
1788
+ extensionRegistry
1789
+ );
1225
1790
  }
1226
1791
  const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1227
1792
  if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
@@ -1230,17 +1795,68 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1230
1795
  checker,
1231
1796
  file,
1232
1797
  typeRegistry,
1233
- visiting
1798
+ visiting,
1799
+ extensionRegistry
1234
1800
  );
1235
1801
  }
1236
1802
  }
1237
1803
  return null;
1238
1804
  }
1239
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1805
+ function extractArrayElementTypeNode(sourceNode, checker) {
1806
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1807
+ if (typeNode === void 0) {
1808
+ return void 0;
1809
+ }
1810
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1811
+ if (ts4.isArrayTypeNode(resolvedTypeNode)) {
1812
+ return resolvedTypeNode.elementType;
1813
+ }
1814
+ if (ts4.isTypeReferenceNode(resolvedTypeNode) && ts4.isIdentifier(resolvedTypeNode.typeName) && resolvedTypeNode.typeName.text === "Array" && resolvedTypeNode.typeArguments?.[0]) {
1815
+ return resolvedTypeNode.typeArguments[0];
1816
+ }
1817
+ return void 0;
1818
+ }
1819
+ function extractUnionMemberTypeNodes(sourceNode, checker) {
1820
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1821
+ if (!typeNode) {
1822
+ return [];
1823
+ }
1824
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1825
+ return ts4.isUnionTypeNode(resolvedTypeNode) ? [...resolvedTypeNode.types] : [];
1826
+ }
1827
+ function resolveAliasedTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1828
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1829
+ return resolveAliasedTypeNode(typeNode.type, checker, visited);
1830
+ }
1831
+ if (!ts4.isTypeReferenceNode(typeNode) || !ts4.isIdentifier(typeNode.typeName)) {
1832
+ return typeNode;
1833
+ }
1834
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1835
+ const aliasDecl = symbol?.declarations?.find(ts4.isTypeAliasDeclaration);
1836
+ if (aliasDecl === void 0 || visited.has(aliasDecl)) {
1837
+ return typeNode;
1838
+ }
1839
+ visited.add(aliasDecl);
1840
+ return resolveAliasedTypeNode(aliasDecl.type, checker, visited);
1841
+ }
1842
+ function isNullishTypeNode(typeNode) {
1843
+ if (typeNode.kind === ts4.SyntaxKind.NullKeyword || typeNode.kind === ts4.SyntaxKind.UndefinedKeyword) {
1844
+ return true;
1845
+ }
1846
+ return ts4.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts4.SyntaxKind.NullKeyword || typeNode.literal.kind === ts4.SyntaxKind.UndefinedKeyword);
1847
+ }
1848
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
1240
1849
  const map = /* @__PURE__ */ new Map();
1241
1850
  for (const member of members) {
1242
1851
  if (ts4.isPropertySignature(member)) {
1243
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1852
+ const fieldNode = analyzeInterfacePropertyToIR(
1853
+ member,
1854
+ checker,
1855
+ file,
1856
+ typeRegistry,
1857
+ visiting,
1858
+ extensionRegistry
1859
+ );
1244
1860
  if (fieldNode) {
1245
1861
  map.set(fieldNode.name, {
1246
1862
  constraints: [...fieldNode.constraints],
@@ -1253,7 +1869,7 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1253
1869
  return map;
1254
1870
  }
1255
1871
  var MAX_ALIAS_CHAIN_DEPTH = 8;
1256
- function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1872
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegistry, depth = 0) {
1257
1873
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1258
1874
  if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1259
1875
  const aliasName = typeNode.typeName.getText();
@@ -1266,8 +1882,29 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1266
1882
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1267
1883
  if (!aliasDecl) return [];
1268
1884
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1269
- const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1270
- constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1885
+ const aliasFieldType = resolveTypeNode(
1886
+ checker.getTypeAtLocation(aliasDecl.type),
1887
+ checker,
1888
+ file,
1889
+ {},
1890
+ /* @__PURE__ */ new Set(),
1891
+ aliasDecl.type,
1892
+ extensionRegistry
1893
+ );
1894
+ const constraints = extractJSDocConstraintNodes(
1895
+ aliasDecl,
1896
+ file,
1897
+ makeParseOptions(extensionRegistry, aliasFieldType)
1898
+ );
1899
+ constraints.push(
1900
+ ...extractTypeAliasConstraintNodes(
1901
+ aliasDecl.type,
1902
+ checker,
1903
+ file,
1904
+ extensionRegistry,
1905
+ depth + 1
1906
+ )
1907
+ );
1271
1908
  return constraints;
1272
1909
  }
1273
1910
  function provenanceForNode(node, file) {
@@ -1283,6 +1920,12 @@ function provenanceForNode(node, file) {
1283
1920
  function provenanceForFile(file) {
1284
1921
  return { surface: "tsdoc", file, line: 0, column: 0 };
1285
1922
  }
1923
+ function provenanceForDeclaration(node, file) {
1924
+ if (!node) {
1925
+ return provenanceForFile(file);
1926
+ }
1927
+ return provenanceForNode(node, file);
1928
+ }
1286
1929
  function getNamedTypeName(type) {
1287
1930
  const symbol = type.getSymbol();
1288
1931
  if (symbol?.declarations) {
@@ -1301,6 +1944,20 @@ function getNamedTypeName(type) {
1301
1944
  }
1302
1945
  return null;
1303
1946
  }
1947
+ function getNamedTypeDeclaration(type) {
1948
+ const symbol = type.getSymbol();
1949
+ if (symbol?.declarations) {
1950
+ const decl = symbol.declarations[0];
1951
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
1952
+ return decl;
1953
+ }
1954
+ }
1955
+ const aliasSymbol = type.aliasSymbol;
1956
+ if (aliasSymbol?.declarations) {
1957
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
1958
+ }
1959
+ return void 0;
1960
+ }
1304
1961
  function analyzeMethod(method, checker) {
1305
1962
  if (!ts4.isIdentifier(method.name)) {
1306
1963
  return null;
@@ -1360,6 +2017,9 @@ function generateJsonSchemaFromIR(ir, options) {
1360
2017
  const ctx = makeContext(options);
1361
2018
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1362
2019
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
2020
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
2021
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
2022
+ }
1363
2023
  }
1364
2024
  const properties = {};
1365
2025
  const required = [];
@@ -1371,6 +2031,9 @@ function generateJsonSchemaFromIR(ir, options) {
1371
2031
  properties,
1372
2032
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1373
2033
  };
2034
+ if (ir.annotations && ir.annotations.length > 0) {
2035
+ applyAnnotations(result, ir.annotations, ctx);
2036
+ }
1374
2037
  if (Object.keys(ctx.defs).length > 0) {
1375
2038
  result.$defs = ctx.defs;
1376
2039
  }
@@ -1400,22 +2063,51 @@ function collectFields(elements, properties, required, ctx) {
1400
2063
  }
1401
2064
  function generateFieldSchema(field, ctx) {
1402
2065
  const schema = generateTypeNode(field.type, ctx);
2066
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1403
2067
  const directConstraints = [];
2068
+ const itemConstraints = [];
1404
2069
  const pathConstraints = [];
1405
2070
  for (const c of field.constraints) {
1406
2071
  if (c.path) {
1407
2072
  pathConstraints.push(c);
2073
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
2074
+ itemConstraints.push(c);
1408
2075
  } else {
1409
2076
  directConstraints.push(c);
1410
2077
  }
1411
2078
  }
1412
2079
  applyConstraints(schema, directConstraints, ctx);
1413
- applyAnnotations(schema, field.annotations, ctx);
2080
+ if (itemStringSchema !== void 0) {
2081
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
2082
+ }
2083
+ const rootAnnotations = [];
2084
+ const itemAnnotations = [];
2085
+ for (const annotation of field.annotations) {
2086
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
2087
+ itemAnnotations.push(annotation);
2088
+ } else {
2089
+ rootAnnotations.push(annotation);
2090
+ }
2091
+ }
2092
+ applyAnnotations(schema, rootAnnotations, ctx);
2093
+ if (itemStringSchema !== void 0) {
2094
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
2095
+ }
1414
2096
  if (pathConstraints.length === 0) {
1415
2097
  return schema;
1416
2098
  }
1417
2099
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1418
2100
  }
2101
+ function isStringItemConstraint(constraint) {
2102
+ switch (constraint.constraintKind) {
2103
+ case "minLength":
2104
+ case "maxLength":
2105
+ case "pattern":
2106
+ return true;
2107
+ default:
2108
+ return false;
2109
+ }
2110
+ }
1419
2111
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1420
2112
  if (schema.type === "array" && schema.items) {
1421
2113
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -1633,6 +2325,9 @@ function applyConstraints(schema, constraints, ctx) {
1633
2325
  case "uniqueItems":
1634
2326
  schema.uniqueItems = constraint.value;
1635
2327
  break;
2328
+ case "const":
2329
+ schema.const = constraint.value;
2330
+ break;
1636
2331
  case "allowedMembers":
1637
2332
  break;
1638
2333
  case "custom":
@@ -1657,8 +2352,14 @@ function applyAnnotations(schema, annotations, ctx) {
1657
2352
  case "defaultValue":
1658
2353
  schema.default = annotation.value;
1659
2354
  break;
2355
+ case "format":
2356
+ schema.format = annotation.value;
2357
+ break;
1660
2358
  case "deprecated":
1661
2359
  schema.deprecated = true;
2360
+ if (annotation.message !== void 0 && annotation.message !== "") {
2361
+ schema["x-formspec-deprecation-description"] = annotation.message;
2362
+ }
1662
2363
  break;
1663
2364
  case "placeholder":
1664
2365
  break;
@@ -1690,7 +2391,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
1690
2391
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1691
2392
  );
1692
2393
  }
1693
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
2394
+ assignVendorPrefixedExtensionKeywords(
2395
+ schema,
2396
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
2397
+ ctx.vendorPrefix,
2398
+ `custom constraint "${constraint.constraintId}"`
2399
+ );
1694
2400
  }
1695
2401
  function applyCustomAnnotation(schema, annotation, ctx) {
1696
2402
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -1702,7 +2408,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
1702
2408
  if (registration.toJsonSchema === void 0) {
1703
2409
  return;
1704
2410
  }
1705
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
2411
+ assignVendorPrefixedExtensionKeywords(
2412
+ schema,
2413
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
2414
+ ctx.vendorPrefix,
2415
+ `custom annotation "${annotation.annotationId}"`
2416
+ );
2417
+ }
2418
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
2419
+ for (const [key, value] of Object.entries(extensionSchema)) {
2420
+ if (!key.startsWith(`${vendorPrefix}-`)) {
2421
+ throw new Error(
2422
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
2423
+ );
2424
+ }
2425
+ schema[key] = value;
2426
+ }
1706
2427
  }
1707
2428
 
1708
2429
  // src/ui-schema/schema.ts
@@ -1840,25 +2561,31 @@ function createShowRule(fieldName, value) {
1840
2561
  }
1841
2562
  };
1842
2563
  }
2564
+ function flattenConditionSchema(scope, schema) {
2565
+ if (schema.allOf === void 0) {
2566
+ if (scope === "#") {
2567
+ return [schema];
2568
+ }
2569
+ const fieldName = scope.replace("#/properties/", "");
2570
+ return [
2571
+ {
2572
+ properties: {
2573
+ [fieldName]: schema
2574
+ }
2575
+ }
2576
+ ];
2577
+ }
2578
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
2579
+ }
1843
2580
  function combineRules(parentRule, childRule) {
1844
- const parentCondition = parentRule.condition;
1845
- const childCondition = childRule.condition;
1846
2581
  return {
1847
2582
  effect: "SHOW",
1848
2583
  condition: {
1849
2584
  scope: "#",
1850
2585
  schema: {
1851
2586
  allOf: [
1852
- {
1853
- properties: {
1854
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1855
- }
1856
- },
1857
- {
1858
- properties: {
1859
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1860
- }
1861
- }
2587
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2588
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1862
2589
  ]
1863
2590
  }
1864
2591
  }
@@ -1866,10 +2593,14 @@ function combineRules(parentRule, childRule) {
1866
2593
  }
1867
2594
  function fieldNodeToControl(field, parentRule) {
1868
2595
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2596
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1869
2597
  const control = {
1870
2598
  type: "Control",
1871
2599
  scope: fieldToScope(field.name),
1872
2600
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2601
+ ...placeholderAnnotation !== void 0 && {
2602
+ options: { placeholder: placeholderAnnotation.value }
2603
+ },
1873
2604
  ...parentRule !== void 0 && { rule: parentRule }
1874
2605
  };
1875
2606
  return control;
@@ -1919,15 +2650,16 @@ function generateUiSchemaFromIR(ir) {
1919
2650
  }
1920
2651
 
1921
2652
  // src/generators/class-schema.ts
1922
- function generateClassSchemas(analysis, source) {
2653
+ function generateClassSchemas(analysis, source, options) {
1923
2654
  const ir = canonicalizeTSDoc(analysis, source);
1924
2655
  return {
1925
- jsonSchema: generateJsonSchemaFromIR(ir),
2656
+ jsonSchema: generateJsonSchemaFromIR(ir, options),
1926
2657
  uiSchema: generateUiSchemaFromIR(ir)
1927
2658
  };
1928
2659
  }
1929
2660
 
1930
2661
  // src/validate/constraint-validator.ts
2662
+ var import_core4 = require("@formspec/core");
1931
2663
  function addContradiction(ctx, message, primary, related) {
1932
2664
  ctx.diagnostics.push({
1933
2665
  code: "CONTRADICTING_CONSTRAINTS",
@@ -1955,6 +2687,15 @@ function addUnknownExtension(ctx, message, primary) {
1955
2687
  relatedLocations: []
1956
2688
  });
1957
2689
  }
2690
+ function addUnknownPathTarget(ctx, message, primary) {
2691
+ ctx.diagnostics.push({
2692
+ code: "UNKNOWN_PATH_TARGET",
2693
+ message,
2694
+ severity: "error",
2695
+ primaryLocation: primary,
2696
+ relatedLocations: []
2697
+ });
2698
+ }
1958
2699
  function addConstraintBroadening(ctx, message, primary, related) {
1959
2700
  ctx.diagnostics.push({
1960
2701
  code: "CONSTRAINT_BROADENING",
@@ -1964,6 +2705,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
1964
2705
  relatedLocations: [related]
1965
2706
  });
1966
2707
  }
2708
+ function getExtensionIdFromConstraintId(constraintId) {
2709
+ const separator = constraintId.lastIndexOf("/");
2710
+ if (separator <= 0) {
2711
+ return null;
2712
+ }
2713
+ return constraintId.slice(0, separator);
2714
+ }
1967
2715
  function findNumeric(constraints, constraintKind) {
1968
2716
  return constraints.find((c) => c.constraintKind === constraintKind);
1969
2717
  }
@@ -1975,6 +2723,45 @@ function findAllowedMembers(constraints) {
1975
2723
  (c) => c.constraintKind === "allowedMembers"
1976
2724
  );
1977
2725
  }
2726
+ function findConstConstraints(constraints) {
2727
+ return constraints.filter(
2728
+ (c) => c.constraintKind === "const"
2729
+ );
2730
+ }
2731
+ function jsonValueEquals(left, right) {
2732
+ if (left === right) {
2733
+ return true;
2734
+ }
2735
+ if (Array.isArray(left) || Array.isArray(right)) {
2736
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2737
+ return false;
2738
+ }
2739
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
2740
+ }
2741
+ if (isJsonObject(left) || isJsonObject(right)) {
2742
+ if (!isJsonObject(left) || !isJsonObject(right)) {
2743
+ return false;
2744
+ }
2745
+ const leftKeys = Object.keys(left).sort();
2746
+ const rightKeys = Object.keys(right).sort();
2747
+ if (leftKeys.length !== rightKeys.length) {
2748
+ return false;
2749
+ }
2750
+ return leftKeys.every((key, index) => {
2751
+ const rightKey = rightKeys[index];
2752
+ if (rightKey !== key) {
2753
+ return false;
2754
+ }
2755
+ const leftValue = left[key];
2756
+ const rightValue = right[rightKey];
2757
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2758
+ });
2759
+ }
2760
+ return false;
2761
+ }
2762
+ function isJsonObject(value) {
2763
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2764
+ }
1978
2765
  function isOrderedBoundConstraint(constraint) {
1979
2766
  return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
1980
2767
  }
@@ -2095,6 +2882,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
2095
2882
  strongestByKey.set(key, constraint);
2096
2883
  }
2097
2884
  }
2885
+ function compareCustomConstraintStrength(current, previous) {
2886
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
2887
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
2888
+ switch (current.role.bound) {
2889
+ case "lower":
2890
+ return equalPayloadTiebreaker;
2891
+ case "upper":
2892
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
2893
+ case "exact":
2894
+ return order === 0 ? 0 : Number.NaN;
2895
+ default: {
2896
+ const _exhaustive = current.role.bound;
2897
+ return _exhaustive;
2898
+ }
2899
+ }
2900
+ }
2901
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
2902
+ if (currentInclusive === previousInclusive) {
2903
+ return 0;
2904
+ }
2905
+ return currentInclusive ? -1 : 1;
2906
+ }
2907
+ function customConstraintsContradict(lower, upper) {
2908
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
2909
+ if (order > 0) {
2910
+ return true;
2911
+ }
2912
+ if (order < 0) {
2913
+ return false;
2914
+ }
2915
+ return !lower.role.inclusive || !upper.role.inclusive;
2916
+ }
2917
+ function describeCustomConstraintTag(constraint) {
2918
+ return constraint.provenance.tagName ?? constraint.constraintId;
2919
+ }
2920
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
2921
+ if (ctx.extensionRegistry === void 0) {
2922
+ return;
2923
+ }
2924
+ const strongestByKey = /* @__PURE__ */ new Map();
2925
+ const lowerByFamily = /* @__PURE__ */ new Map();
2926
+ const upperByFamily = /* @__PURE__ */ new Map();
2927
+ for (const constraint of constraints) {
2928
+ if (constraint.constraintKind !== "custom") {
2929
+ continue;
2930
+ }
2931
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
2932
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
2933
+ continue;
2934
+ }
2935
+ const entry = {
2936
+ constraint,
2937
+ comparePayloads: registration.comparePayloads,
2938
+ role: registration.semanticRole
2939
+ };
2940
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
2941
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
2942
+ const previous = strongestByKey.get(boundKey);
2943
+ if (previous !== void 0) {
2944
+ const strength = compareCustomConstraintStrength(entry, previous);
2945
+ if (Number.isNaN(strength)) {
2946
+ addContradiction(
2947
+ ctx,
2948
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
2949
+ constraint.provenance,
2950
+ previous.constraint.provenance
2951
+ );
2952
+ continue;
2953
+ }
2954
+ if (strength < 0) {
2955
+ addConstraintBroadening(
2956
+ ctx,
2957
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
2958
+ constraint.provenance,
2959
+ previous.constraint.provenance
2960
+ );
2961
+ continue;
2962
+ }
2963
+ if (strength > 0) {
2964
+ strongestByKey.set(boundKey, entry);
2965
+ }
2966
+ } else {
2967
+ strongestByKey.set(boundKey, entry);
2968
+ }
2969
+ if (registration.semanticRole.bound === "lower") {
2970
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2971
+ } else if (registration.semanticRole.bound === "upper") {
2972
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2973
+ }
2974
+ }
2975
+ for (const [familyKey, lower] of lowerByFamily) {
2976
+ const upper = upperByFamily.get(familyKey);
2977
+ if (upper === void 0) {
2978
+ continue;
2979
+ }
2980
+ if (!customConstraintsContradict(lower, upper)) {
2981
+ continue;
2982
+ }
2983
+ addContradiction(
2984
+ ctx,
2985
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
2986
+ lower.constraint.provenance,
2987
+ upper.constraint.provenance
2988
+ );
2989
+ }
2990
+ }
2098
2991
  function checkNumericContradictions(ctx, fieldName, constraints) {
2099
2992
  const min = findNumeric(constraints, "minimum");
2100
2993
  const max = findNumeric(constraints, "maximum");
@@ -2181,6 +3074,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
2181
3074
  }
2182
3075
  }
2183
3076
  }
3077
+ function checkConstContradictions(ctx, fieldName, constraints) {
3078
+ const constConstraints = findConstConstraints(constraints);
3079
+ if (constConstraints.length < 2) return;
3080
+ const first = constConstraints[0];
3081
+ if (first === void 0) return;
3082
+ for (let i = 1; i < constConstraints.length; i++) {
3083
+ const current = constConstraints[i];
3084
+ if (current === void 0) continue;
3085
+ if (jsonValueEquals(first.value, current.value)) {
3086
+ continue;
3087
+ }
3088
+ addContradiction(
3089
+ ctx,
3090
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
3091
+ first.provenance,
3092
+ current.provenance
3093
+ );
3094
+ }
3095
+ }
2184
3096
  function typeLabel(type) {
2185
3097
  switch (type.kind) {
2186
3098
  case "primitive":
@@ -2253,6 +3165,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2253
3165
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2254
3166
  const isArray = effectiveType.kind === "array";
2255
3167
  const isEnum = effectiveType.kind === "enum";
3168
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
3169
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
2256
3170
  const label = typeLabel(effectiveType);
2257
3171
  const ck = constraint.constraintKind;
2258
3172
  switch (ck) {
@@ -2273,10 +3187,10 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2273
3187
  case "minLength":
2274
3188
  case "maxLength":
2275
3189
  case "pattern": {
2276
- if (!isString) {
3190
+ if (!isString && !isStringArray) {
2277
3191
  addTypeMismatch(
2278
3192
  ctx,
2279
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
3193
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2280
3194
  constraint.provenance
2281
3195
  );
2282
3196
  }
@@ -2304,6 +3218,37 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2304
3218
  }
2305
3219
  break;
2306
3220
  }
3221
+ case "const": {
3222
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
3223
+ if (!isPrimitiveConstType) {
3224
+ addTypeMismatch(
3225
+ ctx,
3226
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
3227
+ constraint.provenance
3228
+ );
3229
+ break;
3230
+ }
3231
+ if (effectiveType.kind === "primitive") {
3232
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
3233
+ if (valueType !== effectiveType.primitiveKind) {
3234
+ addTypeMismatch(
3235
+ ctx,
3236
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
3237
+ constraint.provenance
3238
+ );
3239
+ }
3240
+ break;
3241
+ }
3242
+ const memberValues = effectiveType.members.map((member) => member.value);
3243
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
3244
+ addTypeMismatch(
3245
+ ctx,
3246
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
3247
+ constraint.provenance
3248
+ );
3249
+ }
3250
+ break;
3251
+ }
2307
3252
  case "custom": {
2308
3253
  checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2309
3254
  break;
@@ -2322,9 +3267,9 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2322
3267
  const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2323
3268
  const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2324
3269
  if (resolution.kind === "missing-property") {
2325
- addTypeMismatch(
3270
+ addUnknownPathTarget(
2326
3271
  ctx,
2327
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
3272
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2328
3273
  constraint.provenance
2329
3274
  );
2330
3275
  continue;
@@ -2354,8 +3299,30 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
2354
3299
  );
2355
3300
  return;
2356
3301
  }
2357
- if (registration.applicableTypes === null) return;
2358
- if (!registration.applicableTypes.includes(type.kind)) {
3302
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core4.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
3303
+ if (normalizedTagName !== void 0) {
3304
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3305
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3306
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && tagRegistration.registration.isApplicableToType?.(type) === false) {
3307
+ addTypeMismatch(
3308
+ ctx,
3309
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3310
+ constraint.provenance
3311
+ );
3312
+ return;
3313
+ }
3314
+ }
3315
+ if (registration.applicableTypes === null) {
3316
+ if (registration.isApplicableToType?.(type) === false) {
3317
+ addTypeMismatch(
3318
+ ctx,
3319
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3320
+ constraint.provenance
3321
+ );
3322
+ }
3323
+ return;
3324
+ }
3325
+ if (!registration.applicableTypes.includes(type.kind) || registration.isApplicableToType?.(type) === false) {
2359
3326
  addTypeMismatch(
2360
3327
  ctx,
2361
3328
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -2384,7 +3351,9 @@ function validateConstraints(ctx, name, type, constraints) {
2384
3351
  checkNumericContradictions(ctx, name, constraints);
2385
3352
  checkLengthContradictions(ctx, name, constraints);
2386
3353
  checkAllowedMembersContradiction(ctx, name, constraints);
3354
+ checkConstContradictions(ctx, name, constraints);
2387
3355
  checkConstraintBroadening(ctx, name, constraints);
3356
+ checkCustomConstraintSemantics(ctx, name, constraints);
2388
3357
  checkTypeApplicability(ctx, name, type, constraints);
2389
3358
  }
2390
3359
  function validateElement(ctx, element) {
@@ -2426,7 +3395,10 @@ function validateIR(ir, options) {
2426
3395
  // src/extensions/registry.ts
2427
3396
  function createExtensionRegistry(extensions) {
2428
3397
  const typeMap = /* @__PURE__ */ new Map();
3398
+ const typeNameMap = /* @__PURE__ */ new Map();
2429
3399
  const constraintMap = /* @__PURE__ */ new Map();
3400
+ const constraintTagMap = /* @__PURE__ */ new Map();
3401
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
2430
3402
  const annotationMap = /* @__PURE__ */ new Map();
2431
3403
  for (const ext of extensions) {
2432
3404
  if (ext.types !== void 0) {
@@ -2436,6 +3408,27 @@ function createExtensionRegistry(extensions) {
2436
3408
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
2437
3409
  }
2438
3410
  typeMap.set(qualifiedId, type);
3411
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
3412
+ if (typeNameMap.has(sourceTypeName)) {
3413
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
3414
+ }
3415
+ typeNameMap.set(sourceTypeName, {
3416
+ extensionId: ext.extensionId,
3417
+ registration: type
3418
+ });
3419
+ }
3420
+ if (type.builtinConstraintBroadenings !== void 0) {
3421
+ for (const broadening of type.builtinConstraintBroadenings) {
3422
+ const key = `${qualifiedId}:${broadening.tagName}`;
3423
+ if (builtinBroadeningMap.has(key)) {
3424
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
3425
+ }
3426
+ builtinBroadeningMap.set(key, {
3427
+ extensionId: ext.extensionId,
3428
+ registration: broadening
3429
+ });
3430
+ }
3431
+ }
2439
3432
  }
2440
3433
  }
2441
3434
  if (ext.constraints !== void 0) {
@@ -2447,6 +3440,17 @@ function createExtensionRegistry(extensions) {
2447
3440
  constraintMap.set(qualifiedId, constraint);
2448
3441
  }
2449
3442
  }
3443
+ if (ext.constraintTags !== void 0) {
3444
+ for (const tag of ext.constraintTags) {
3445
+ if (constraintTagMap.has(tag.tagName)) {
3446
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
3447
+ }
3448
+ constraintTagMap.set(tag.tagName, {
3449
+ extensionId: ext.extensionId,
3450
+ registration: tag
3451
+ });
3452
+ }
3453
+ }
2450
3454
  if (ext.annotations !== void 0) {
2451
3455
  for (const annotation of ext.annotations) {
2452
3456
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -2460,13 +3464,16 @@ function createExtensionRegistry(extensions) {
2460
3464
  return {
2461
3465
  extensions,
2462
3466
  findType: (typeId) => typeMap.get(typeId),
3467
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
2463
3468
  findConstraint: (constraintId) => constraintMap.get(constraintId),
3469
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
3470
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
2464
3471
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
2465
3472
  };
2466
3473
  }
2467
3474
 
2468
3475
  // src/generators/method-schema.ts
2469
- var import_core4 = require("@formspec/core");
3476
+ var import_core5 = require("@formspec/core");
2470
3477
  function typeToJsonSchema(type, checker) {
2471
3478
  const typeRegistry = {};
2472
3479
  const visiting = /* @__PURE__ */ new Set();
@@ -2474,7 +3481,7 @@ function typeToJsonSchema(type, checker) {
2474
3481
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
2475
3482
  const ir = {
2476
3483
  kind: "form-ir",
2477
- irVersion: import_core4.IR_VERSION,
3484
+ irVersion: import_core5.IR_VERSION,
2478
3485
  elements: [
2479
3486
  {
2480
3487
  kind: "field",