@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
package/dist/internals.js CHANGED
@@ -325,6 +325,7 @@ function canonicalizeTSDoc(analysis, source) {
325
325
  irVersion: IR_VERSION2,
326
326
  elements,
327
327
  typeRegistry: analysis.typeRegistry,
328
+ ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
328
329
  provenance
329
330
  };
330
331
  }
@@ -501,8 +502,8 @@ var LENGTH_CONSTRAINT_MAP = {
501
502
  minItems: "minItems",
502
503
  maxItems: "maxItems"
503
504
  };
504
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
505
- function createFormSpecTSDocConfig() {
505
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
506
+ function createFormSpecTSDocConfig(extensionTagNames = []) {
506
507
  const config = new TSDocConfiguration();
507
508
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
508
509
  config.addTagDefinition(
@@ -513,7 +514,16 @@ function createFormSpecTSDocConfig() {
513
514
  })
514
515
  );
515
516
  }
516
- for (const tagName of ["displayName", "description"]) {
517
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
518
+ config.addTagDefinition(
519
+ new TSDocTagDefinition({
520
+ tagName: "@" + tagName,
521
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
522
+ allowMultiple: true
523
+ })
524
+ );
525
+ }
526
+ for (const tagName of extensionTagNames) {
517
527
  config.addTagDefinition(
518
528
  new TSDocTagDefinition({
519
529
  tagName: "@" + tagName,
@@ -524,14 +534,31 @@ function createFormSpecTSDocConfig() {
524
534
  }
525
535
  return config;
526
536
  }
527
- var sharedParser;
528
- function getParser() {
529
- sharedParser ??= new TSDocParser(createFormSpecTSDocConfig());
530
- return sharedParser;
531
- }
532
- function parseTSDocTags(node, file = "") {
537
+ var parserCache = /* @__PURE__ */ new Map();
538
+ function getParser(options) {
539
+ const extensionTagNames = [
540
+ ...options?.extensionRegistry?.extensions.flatMap(
541
+ (extension) => (extension.constraintTags ?? []).map((tag) => tag.tagName)
542
+ ) ?? []
543
+ ].sort();
544
+ const cacheKey = extensionTagNames.join("|");
545
+ const existing = parserCache.get(cacheKey);
546
+ if (existing) {
547
+ return existing;
548
+ }
549
+ const parser = new TSDocParser(createFormSpecTSDocConfig(extensionTagNames));
550
+ parserCache.set(cacheKey, parser);
551
+ return parser;
552
+ }
553
+ function parseTSDocTags(node, file = "", options) {
533
554
  const constraints = [];
534
555
  const annotations = [];
556
+ let displayName;
557
+ let description;
558
+ let placeholder;
559
+ let displayNameProvenance;
560
+ let descriptionProvenance;
561
+ let placeholderProvenance;
535
562
  const sourceFile = node.getSourceFile();
536
563
  const sourceText = sourceFile.getFullText();
537
564
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -544,52 +571,92 @@ function parseTSDocTags(node, file = "") {
544
571
  if (!commentText.startsWith("/**")) {
545
572
  continue;
546
573
  }
547
- const parser = getParser();
574
+ const parser = getParser(options);
548
575
  const parserContext = parser.parseRange(
549
576
  TextRange.fromStringRange(sourceText, range.pos, range.end)
550
577
  );
551
578
  const docComment = parserContext.docComment;
552
579
  for (const block of docComment.customBlocks) {
553
580
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
554
- if (tagName === "displayName" || tagName === "description") {
581
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
555
582
  const text2 = extractBlockText(block).trim();
556
583
  if (text2 === "") continue;
557
584
  const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
558
585
  if (tagName === "displayName") {
586
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
587
+ displayName = text2;
588
+ displayNameProvenance = provenance2;
589
+ }
590
+ } else if (tagName === "format") {
559
591
  annotations.push({
560
592
  kind: "annotation",
561
- annotationKind: "displayName",
593
+ annotationKind: "format",
562
594
  value: text2,
563
595
  provenance: provenance2
564
596
  });
565
597
  } else {
566
- annotations.push({
567
- kind: "annotation",
568
- annotationKind: "description",
569
- value: text2,
570
- provenance: provenance2
571
- });
598
+ if (tagName === "description" && description === void 0) {
599
+ description = text2;
600
+ descriptionProvenance = provenance2;
601
+ } else if (tagName === "placeholder" && placeholder === void 0) {
602
+ placeholder = text2;
603
+ placeholderProvenance = provenance2;
604
+ }
572
605
  }
573
606
  continue;
574
607
  }
575
608
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
576
609
  const text = extractBlockText(block).trim();
577
- if (text === "") continue;
610
+ const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
611
+ if (text === "" && expectedType !== "boolean") continue;
578
612
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
579
- const constraintNode = parseConstraintValue(tagName, text, provenance);
613
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
580
614
  if (constraintNode) {
581
615
  constraints.push(constraintNode);
582
616
  }
583
617
  }
584
618
  if (docComment.deprecatedBlock !== void 0) {
619
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
585
620
  annotations.push({
586
621
  kind: "annotation",
587
622
  annotationKind: "deprecated",
623
+ ...message !== "" && { message },
588
624
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
589
625
  });
590
626
  }
627
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
628
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
629
+ if (remarks !== "") {
630
+ description = remarks;
631
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
632
+ }
633
+ }
591
634
  }
592
635
  }
636
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
637
+ annotations.push({
638
+ kind: "annotation",
639
+ annotationKind: "displayName",
640
+ value: displayName,
641
+ provenance: displayNameProvenance
642
+ });
643
+ }
644
+ if (description !== void 0 && descriptionProvenance !== void 0) {
645
+ annotations.push({
646
+ kind: "annotation",
647
+ annotationKind: "description",
648
+ value: description,
649
+ provenance: descriptionProvenance
650
+ });
651
+ }
652
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
653
+ annotations.push({
654
+ kind: "annotation",
655
+ annotationKind: "placeholder",
656
+ value: placeholder,
657
+ provenance: placeholderProvenance
658
+ });
659
+ }
593
660
  const jsDocTagsAll = ts2.getJSDocTags(node);
594
661
  for (const tag of jsDocTagsAll) {
595
662
  const tagName = normalizeConstraintTagName(tag.tagName.text);
@@ -598,13 +665,40 @@ function parseTSDocTags(node, file = "") {
598
665
  if (commentText === void 0 || commentText.trim() === "") continue;
599
666
  const text = commentText.trim();
600
667
  const provenance = provenanceForJSDocTag(tag, file);
601
- const constraintNode = parseConstraintValue(tagName, text, provenance);
668
+ if (tagName === "defaultValue") {
669
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
670
+ annotations.push(defaultValueNode);
671
+ continue;
672
+ }
673
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
602
674
  if (constraintNode) {
603
675
  constraints.push(constraintNode);
604
676
  }
605
677
  }
606
678
  return { constraints, annotations };
607
679
  }
680
+ function extractDisplayNameMetadata(node) {
681
+ let displayName;
682
+ const memberDisplayNames = /* @__PURE__ */ new Map();
683
+ for (const tag of ts2.getJSDocTags(node)) {
684
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
685
+ if (tagName !== "displayName") continue;
686
+ const commentText = getTagCommentText(tag);
687
+ if (commentText === void 0) continue;
688
+ const text = commentText.trim();
689
+ if (text === "") continue;
690
+ const memberTarget = parseMemberTargetDisplayName(text);
691
+ if (memberTarget) {
692
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
693
+ continue;
694
+ }
695
+ displayName ??= text;
696
+ }
697
+ return {
698
+ ...displayName !== void 0 && { displayName },
699
+ memberDisplayNames
700
+ };
701
+ }
608
702
  function extractPathTarget(text) {
609
703
  const trimmed = text.trimStart();
610
704
  const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
@@ -632,7 +726,11 @@ function extractPlainText(node) {
632
726
  }
633
727
  return result;
634
728
  }
635
- function parseConstraintValue(tagName, text, provenance) {
729
+ function parseConstraintValue(tagName, text, provenance, options) {
730
+ const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
731
+ if (customConstraint) {
732
+ return customConstraint;
733
+ }
636
734
  if (!isBuiltinConstraintName(tagName)) {
637
735
  return null;
638
736
  }
@@ -667,7 +765,45 @@ function parseConstraintValue(tagName, text, provenance) {
667
765
  }
668
766
  return null;
669
767
  }
768
+ if (expectedType === "boolean") {
769
+ const trimmed = effectiveText.trim();
770
+ if (trimmed !== "" && trimmed !== "true") {
771
+ return null;
772
+ }
773
+ if (tagName === "uniqueItems") {
774
+ return {
775
+ kind: "constraint",
776
+ constraintKind: "uniqueItems",
777
+ value: true,
778
+ ...path2 && { path: path2 },
779
+ provenance
780
+ };
781
+ }
782
+ return null;
783
+ }
670
784
  if (expectedType === "json") {
785
+ if (tagName === "const") {
786
+ const trimmedText = effectiveText.trim();
787
+ if (trimmedText === "") return null;
788
+ try {
789
+ const parsed2 = JSON.parse(trimmedText);
790
+ return {
791
+ kind: "constraint",
792
+ constraintKind: "const",
793
+ value: parsed2,
794
+ ...path2 && { path: path2 },
795
+ provenance
796
+ };
797
+ } catch {
798
+ return {
799
+ kind: "constraint",
800
+ constraintKind: "const",
801
+ value: trimmedText,
802
+ ...path2 && { path: path2 },
803
+ provenance
804
+ };
805
+ }
806
+ }
671
807
  const parsed = tryParseJson(effectiveText);
672
808
  if (!Array.isArray(parsed)) {
673
809
  return null;
@@ -699,6 +835,111 @@ function parseConstraintValue(tagName, text, provenance) {
699
835
  provenance
700
836
  };
701
837
  }
838
+ function parseExtensionConstraintValue(tagName, text, provenance, options) {
839
+ const pathResult = extractPathTarget(text);
840
+ const effectiveText = pathResult ? pathResult.remainingText : text;
841
+ const path2 = pathResult?.path;
842
+ const registry = options?.extensionRegistry;
843
+ if (registry === void 0) {
844
+ return null;
845
+ }
846
+ const directTag = registry.findConstraintTag(tagName);
847
+ if (directTag !== void 0) {
848
+ return makeCustomConstraintNode(
849
+ directTag.extensionId,
850
+ directTag.registration.constraintName,
851
+ directTag.registration.parseValue(effectiveText),
852
+ provenance,
853
+ path2,
854
+ registry
855
+ );
856
+ }
857
+ if (!isBuiltinConstraintName(tagName)) {
858
+ return null;
859
+ }
860
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
861
+ if (broadenedTypeId === void 0) {
862
+ return null;
863
+ }
864
+ const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
865
+ if (broadened === void 0) {
866
+ return null;
867
+ }
868
+ return makeCustomConstraintNode(
869
+ broadened.extensionId,
870
+ broadened.registration.constraintName,
871
+ broadened.registration.parseValue(effectiveText),
872
+ provenance,
873
+ path2,
874
+ registry
875
+ );
876
+ }
877
+ function getBroadenedCustomTypeId(fieldType) {
878
+ if (fieldType?.kind === "custom") {
879
+ return fieldType.typeId;
880
+ }
881
+ if (fieldType?.kind !== "union") {
882
+ return void 0;
883
+ }
884
+ const customMembers = fieldType.members.filter(
885
+ (member) => member.kind === "custom"
886
+ );
887
+ if (customMembers.length !== 1) {
888
+ return void 0;
889
+ }
890
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
891
+ const allOtherMembersAreNull = nonCustomMembers.every(
892
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
893
+ );
894
+ const customMember = customMembers[0];
895
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
896
+ }
897
+ function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path2, registry) {
898
+ const constraintId = `${extensionId}/${constraintName}`;
899
+ const registration = registry.findConstraint(constraintId);
900
+ if (registration === void 0) {
901
+ throw new Error(
902
+ `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
903
+ );
904
+ }
905
+ return {
906
+ kind: "constraint",
907
+ constraintKind: "custom",
908
+ constraintId,
909
+ payload,
910
+ compositionRule: registration.compositionRule,
911
+ ...path2 && { path: path2 },
912
+ provenance
913
+ };
914
+ }
915
+ function parseDefaultValueValue(text, provenance) {
916
+ const trimmed = text.trim();
917
+ let value;
918
+ if (trimmed === "null") {
919
+ value = null;
920
+ } else if (trimmed === "true") {
921
+ value = true;
922
+ } else if (trimmed === "false") {
923
+ value = false;
924
+ } else {
925
+ const parsed = tryParseJson(trimmed);
926
+ value = parsed !== null ? parsed : trimmed;
927
+ }
928
+ return {
929
+ kind: "annotation",
930
+ annotationKind: "defaultValue",
931
+ value,
932
+ provenance
933
+ };
934
+ }
935
+ function isMemberTargetDisplayName(text) {
936
+ return parseMemberTargetDisplayName(text) !== null;
937
+ }
938
+ function parseMemberTargetDisplayName(text) {
939
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
940
+ if (!match?.[1] || !match[2]) return null;
941
+ return { target: match[1], label: match[2].trim() };
942
+ }
702
943
  function provenanceForComment(range, sourceFile, file, tagName) {
703
944
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
704
945
  return {
@@ -731,12 +972,12 @@ function getTagCommentText(tag) {
731
972
  }
732
973
 
733
974
  // src/analyzer/jsdoc-constraints.ts
734
- function extractJSDocConstraintNodes(node, file = "") {
735
- const result = parseTSDocTags(node, file);
975
+ function extractJSDocConstraintNodes(node, file = "", options) {
976
+ const result = parseTSDocTags(node, file, options);
736
977
  return [...result.constraints];
737
978
  }
738
- function extractJSDocAnnotationNodes(node, file = "") {
739
- const result = parseTSDocTags(node, file);
979
+ function extractJSDocAnnotationNodes(node, file = "", options) {
980
+ const result = parseTSDocTags(node, file, options);
740
981
  return [...result.annotations];
741
982
  }
742
983
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -780,17 +1021,43 @@ function isObjectType(type) {
780
1021
  function isTypeReference(type) {
781
1022
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
782
1023
  }
783
- function analyzeClassToIR(classDecl, checker, file = "") {
1024
+ var RESOLVING_TYPE_PLACEHOLDER = {
1025
+ kind: "object",
1026
+ properties: [],
1027
+ additionalProperties: true
1028
+ };
1029
+ function makeParseOptions(extensionRegistry, fieldType) {
1030
+ if (extensionRegistry === void 0 && fieldType === void 0) {
1031
+ return void 0;
1032
+ }
1033
+ return {
1034
+ ...extensionRegistry !== void 0 && { extensionRegistry },
1035
+ ...fieldType !== void 0 && { fieldType }
1036
+ };
1037
+ }
1038
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
784
1039
  const name = classDecl.name?.text ?? "AnonymousClass";
785
1040
  const fields = [];
786
1041
  const fieldLayouts = [];
787
1042
  const typeRegistry = {};
1043
+ const annotations = extractJSDocAnnotationNodes(
1044
+ classDecl,
1045
+ file,
1046
+ makeParseOptions(extensionRegistry)
1047
+ );
788
1048
  const visiting = /* @__PURE__ */ new Set();
789
1049
  const instanceMethods = [];
790
1050
  const staticMethods = [];
791
1051
  for (const member of classDecl.members) {
792
1052
  if (ts4.isPropertyDeclaration(member)) {
793
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1053
+ const fieldNode = analyzeFieldToIR(
1054
+ member,
1055
+ checker,
1056
+ file,
1057
+ typeRegistry,
1058
+ visiting,
1059
+ extensionRegistry
1060
+ );
794
1061
  if (fieldNode) {
795
1062
  fields.push(fieldNode);
796
1063
  fieldLayouts.push({});
@@ -807,25 +1074,53 @@ function analyzeClassToIR(classDecl, checker, file = "") {
807
1074
  }
808
1075
  }
809
1076
  }
810
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
1077
+ return {
1078
+ name,
1079
+ fields,
1080
+ fieldLayouts,
1081
+ typeRegistry,
1082
+ ...annotations.length > 0 && { annotations },
1083
+ instanceMethods,
1084
+ staticMethods
1085
+ };
811
1086
  }
812
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1087
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
813
1088
  const name = interfaceDecl.name.text;
814
1089
  const fields = [];
815
1090
  const typeRegistry = {};
1091
+ const annotations = extractJSDocAnnotationNodes(
1092
+ interfaceDecl,
1093
+ file,
1094
+ makeParseOptions(extensionRegistry)
1095
+ );
816
1096
  const visiting = /* @__PURE__ */ new Set();
817
1097
  for (const member of interfaceDecl.members) {
818
1098
  if (ts4.isPropertySignature(member)) {
819
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1099
+ const fieldNode = analyzeInterfacePropertyToIR(
1100
+ member,
1101
+ checker,
1102
+ file,
1103
+ typeRegistry,
1104
+ visiting,
1105
+ extensionRegistry
1106
+ );
820
1107
  if (fieldNode) {
821
1108
  fields.push(fieldNode);
822
1109
  }
823
1110
  }
824
1111
  }
825
1112
  const fieldLayouts = fields.map(() => ({}));
826
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
1113
+ return {
1114
+ name,
1115
+ fields,
1116
+ fieldLayouts,
1117
+ typeRegistry,
1118
+ ...annotations.length > 0 && { annotations },
1119
+ instanceMethods: [],
1120
+ staticMethods: []
1121
+ };
827
1122
  }
828
- function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1123
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
829
1124
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
830
1125
  const sourceFile = typeAlias.getSourceFile();
831
1126
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -838,10 +1133,22 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
838
1133
  const name = typeAlias.name.text;
839
1134
  const fields = [];
840
1135
  const typeRegistry = {};
1136
+ const annotations = extractJSDocAnnotationNodes(
1137
+ typeAlias,
1138
+ file,
1139
+ makeParseOptions(extensionRegistry)
1140
+ );
841
1141
  const visiting = /* @__PURE__ */ new Set();
842
1142
  for (const member of typeAlias.type.members) {
843
1143
  if (ts4.isPropertySignature(member)) {
844
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1144
+ const fieldNode = analyzeInterfacePropertyToIR(
1145
+ member,
1146
+ checker,
1147
+ file,
1148
+ typeRegistry,
1149
+ visiting,
1150
+ extensionRegistry
1151
+ );
845
1152
  if (fieldNode) {
846
1153
  fields.push(fieldNode);
847
1154
  }
@@ -854,12 +1161,13 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
854
1161
  fields,
855
1162
  fieldLayouts: fields.map(() => ({})),
856
1163
  typeRegistry,
1164
+ ...annotations.length > 0 && { annotations },
857
1165
  instanceMethods: [],
858
1166
  staticMethods: []
859
1167
  }
860
1168
  };
861
1169
  }
862
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1170
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
863
1171
  if (!ts4.isIdentifier(prop.name)) {
864
1172
  return null;
865
1173
  }
@@ -867,16 +1175,28 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
867
1175
  const tsType = checker.getTypeAtLocation(prop);
868
1176
  const optional = prop.questionToken !== void 0;
869
1177
  const provenance = provenanceForNode(prop, file);
870
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1178
+ let type = resolveTypeNode(
1179
+ tsType,
1180
+ checker,
1181
+ file,
1182
+ typeRegistry,
1183
+ visiting,
1184
+ prop,
1185
+ extensionRegistry
1186
+ );
871
1187
  const constraints = [];
872
1188
  if (prop.type) {
873
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1189
+ constraints.push(
1190
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1191
+ );
874
1192
  }
875
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1193
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
876
1194
  let annotations = [];
877
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1195
+ annotations.push(
1196
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1197
+ );
878
1198
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
879
- if (defaultAnnotation) {
1199
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
880
1200
  annotations.push(defaultAnnotation);
881
1201
  }
882
1202
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
@@ -890,7 +1210,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
890
1210
  provenance
891
1211
  };
892
1212
  }
893
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
1213
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
894
1214
  if (!ts4.isIdentifier(prop.name)) {
895
1215
  return null;
896
1216
  }
@@ -898,14 +1218,26 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
898
1218
  const tsType = checker.getTypeAtLocation(prop);
899
1219
  const optional = prop.questionToken !== void 0;
900
1220
  const provenance = provenanceForNode(prop, file);
901
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1221
+ let type = resolveTypeNode(
1222
+ tsType,
1223
+ checker,
1224
+ file,
1225
+ typeRegistry,
1226
+ visiting,
1227
+ prop,
1228
+ extensionRegistry
1229
+ );
902
1230
  const constraints = [];
903
1231
  if (prop.type) {
904
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1232
+ constraints.push(
1233
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1234
+ );
905
1235
  }
906
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1236
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
907
1237
  let annotations = [];
908
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1238
+ annotations.push(
1239
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1240
+ );
909
1241
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
910
1242
  return {
911
1243
  kind: "field",
@@ -979,7 +1311,66 @@ function parseEnumMemberDisplayName(value) {
979
1311
  if (label === "") return null;
980
1312
  return { value: match[1], label };
981
1313
  }
982
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1314
+ function resolveRegisteredCustomType(sourceNode, extensionRegistry, checker) {
1315
+ if (sourceNode === void 0 || extensionRegistry === void 0) {
1316
+ return null;
1317
+ }
1318
+ const typeNode = extractTypeNodeFromSource(sourceNode);
1319
+ if (typeNode === void 0) {
1320
+ return null;
1321
+ }
1322
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker);
1323
+ }
1324
+ function resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker) {
1325
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1326
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode.type, extensionRegistry, checker);
1327
+ }
1328
+ const typeName = getTypeNodeRegistrationName(typeNode);
1329
+ if (typeName === null) {
1330
+ return null;
1331
+ }
1332
+ const registration = extensionRegistry.findTypeByName(typeName);
1333
+ if (registration !== void 0) {
1334
+ return {
1335
+ kind: "custom",
1336
+ typeId: `${registration.extensionId}/${registration.registration.typeName}`,
1337
+ payload: null
1338
+ };
1339
+ }
1340
+ if (ts4.isTypeReferenceNode(typeNode) && ts4.isIdentifier(typeNode.typeName)) {
1341
+ const aliasDecl = checker.getSymbolAtLocation(typeNode.typeName)?.declarations?.find(ts4.isTypeAliasDeclaration);
1342
+ if (aliasDecl !== void 0) {
1343
+ return resolveRegisteredCustomTypeFromTypeNode(aliasDecl.type, extensionRegistry, checker);
1344
+ }
1345
+ }
1346
+ return null;
1347
+ }
1348
+ function extractTypeNodeFromSource(sourceNode) {
1349
+ if (ts4.isPropertyDeclaration(sourceNode) || ts4.isPropertySignature(sourceNode) || ts4.isParameter(sourceNode) || ts4.isTypeAliasDeclaration(sourceNode)) {
1350
+ return sourceNode.type;
1351
+ }
1352
+ if (ts4.isTypeNode(sourceNode)) {
1353
+ return sourceNode;
1354
+ }
1355
+ return void 0;
1356
+ }
1357
+ function getTypeNodeRegistrationName(typeNode) {
1358
+ if (ts4.isTypeReferenceNode(typeNode)) {
1359
+ return ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.right.text;
1360
+ }
1361
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1362
+ return getTypeNodeRegistrationName(typeNode.type);
1363
+ }
1364
+ if (typeNode.kind === ts4.SyntaxKind.BigIntKeyword || typeNode.kind === ts4.SyntaxKind.StringKeyword || typeNode.kind === ts4.SyntaxKind.NumberKeyword || typeNode.kind === ts4.SyntaxKind.BooleanKeyword) {
1365
+ return typeNode.getText();
1366
+ }
1367
+ return null;
1368
+ }
1369
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1370
+ const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1371
+ if (customType) {
1372
+ return customType;
1373
+ }
983
1374
  if (type.flags & ts4.TypeFlags.String) {
984
1375
  return { kind: "primitive", primitiveKind: "string" };
985
1376
  }
@@ -1008,88 +1399,162 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1008
1399
  };
1009
1400
  }
1010
1401
  if (type.isUnion()) {
1011
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1402
+ return resolveUnionType(
1403
+ type,
1404
+ checker,
1405
+ file,
1406
+ typeRegistry,
1407
+ visiting,
1408
+ sourceNode,
1409
+ extensionRegistry
1410
+ );
1012
1411
  }
1013
1412
  if (checker.isArrayType(type)) {
1014
- return resolveArrayType(type, checker, file, typeRegistry, visiting);
1413
+ return resolveArrayType(
1414
+ type,
1415
+ checker,
1416
+ file,
1417
+ typeRegistry,
1418
+ visiting,
1419
+ sourceNode,
1420
+ extensionRegistry
1421
+ );
1015
1422
  }
1016
1423
  if (isObjectType(type)) {
1017
- return resolveObjectType(type, checker, file, typeRegistry, visiting);
1424
+ return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1018
1425
  }
1019
1426
  return { kind: "primitive", primitiveKind: "string" };
1020
1427
  }
1021
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
1428
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1429
+ const typeName = getNamedTypeName(type);
1430
+ const namedDecl = getNamedTypeDeclaration(type);
1431
+ if (typeName && typeName in typeRegistry) {
1432
+ return { kind: "reference", name: typeName, typeArguments: [] };
1433
+ }
1022
1434
  const allTypes = type.types;
1435
+ const unionMemberTypeNodes = extractUnionMemberTypeNodes(sourceNode, checker);
1436
+ const nonNullSourceNodes = unionMemberTypeNodes.filter(
1437
+ (memberTypeNode) => !isNullishTypeNode(resolveAliasedTypeNode(memberTypeNode, checker))
1438
+ );
1023
1439
  const nonNullTypes = allTypes.filter(
1024
- (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1440
+ (memberType) => !(memberType.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1025
1441
  );
1442
+ const nonNullMembers = nonNullTypes.map((memberType, index) => ({
1443
+ memberType,
1444
+ sourceNode: nonNullSourceNodes.length === nonNullTypes.length ? nonNullSourceNodes[index] : void 0
1445
+ }));
1026
1446
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1447
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1448
+ if (namedDecl) {
1449
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
1450
+ memberDisplayNames.set(value, label);
1451
+ }
1452
+ }
1453
+ if (sourceNode) {
1454
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
1455
+ memberDisplayNames.set(value, label);
1456
+ }
1457
+ }
1458
+ const registerNamed = (result) => {
1459
+ if (!typeName) {
1460
+ return result;
1461
+ }
1462
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1463
+ typeRegistry[typeName] = {
1464
+ name: typeName,
1465
+ type: result,
1466
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1467
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
1468
+ };
1469
+ return { kind: "reference", name: typeName, typeArguments: [] };
1470
+ };
1471
+ const applyMemberLabels = (members2) => members2.map((value) => {
1472
+ const displayName = memberDisplayNames.get(String(value));
1473
+ return displayName !== void 0 ? { value, displayName } : { value };
1474
+ });
1027
1475
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1028
1476
  if (isBooleanUnion2) {
1029
1477
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1030
- if (hasNull) {
1031
- return {
1032
- kind: "union",
1033
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1034
- };
1035
- }
1036
- return boolNode;
1478
+ const result = hasNull ? {
1479
+ kind: "union",
1480
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1481
+ } : boolNode;
1482
+ return registerNamed(result);
1037
1483
  }
1038
1484
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1039
1485
  if (allStringLiterals && nonNullTypes.length > 0) {
1040
1486
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1041
1487
  const enumNode = {
1042
1488
  kind: "enum",
1043
- members: stringTypes.map((t) => ({ value: t.value }))
1489
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1044
1490
  };
1045
- if (hasNull) {
1046
- return {
1047
- kind: "union",
1048
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1049
- };
1050
- }
1051
- return enumNode;
1491
+ const result = hasNull ? {
1492
+ kind: "union",
1493
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1494
+ } : enumNode;
1495
+ return registerNamed(result);
1052
1496
  }
1053
1497
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1054
1498
  if (allNumberLiterals && nonNullTypes.length > 0) {
1055
1499
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1056
1500
  const enumNode = {
1057
1501
  kind: "enum",
1058
- members: numberTypes.map((t) => ({ value: t.value }))
1502
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1059
1503
  };
1060
- if (hasNull) {
1061
- return {
1062
- kind: "union",
1063
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1064
- };
1065
- }
1066
- return enumNode;
1067
- }
1068
- if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1069
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1070
- if (hasNull) {
1071
- return {
1072
- kind: "union",
1073
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1074
- };
1075
- }
1076
- return inner;
1077
- }
1078
- const members = nonNullTypes.map(
1079
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1504
+ const result = hasNull ? {
1505
+ kind: "union",
1506
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1507
+ } : enumNode;
1508
+ return registerNamed(result);
1509
+ }
1510
+ if (nonNullMembers.length === 1 && nonNullMembers[0]) {
1511
+ const inner = resolveTypeNode(
1512
+ nonNullMembers[0].memberType,
1513
+ checker,
1514
+ file,
1515
+ typeRegistry,
1516
+ visiting,
1517
+ nonNullMembers[0].sourceNode ?? sourceNode,
1518
+ extensionRegistry
1519
+ );
1520
+ const result = hasNull ? {
1521
+ kind: "union",
1522
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
1523
+ } : inner;
1524
+ return registerNamed(result);
1525
+ }
1526
+ const members = nonNullMembers.map(
1527
+ ({ memberType, sourceNode: memberSourceNode }) => resolveTypeNode(
1528
+ memberType,
1529
+ checker,
1530
+ file,
1531
+ typeRegistry,
1532
+ visiting,
1533
+ memberSourceNode ?? sourceNode,
1534
+ extensionRegistry
1535
+ )
1080
1536
  );
1081
1537
  if (hasNull) {
1082
1538
  members.push({ kind: "primitive", primitiveKind: "null" });
1083
1539
  }
1084
- return { kind: "union", members };
1540
+ return registerNamed({ kind: "union", members });
1085
1541
  }
1086
- function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1542
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1087
1543
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1088
1544
  const elementType = typeArgs?.[0];
1089
- const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1545
+ const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
1546
+ const items = elementType ? resolveTypeNode(
1547
+ elementType,
1548
+ checker,
1549
+ file,
1550
+ typeRegistry,
1551
+ visiting,
1552
+ elementSourceNode,
1553
+ extensionRegistry
1554
+ ) : { kind: "primitive", primitiveKind: "string" };
1090
1555
  return { kind: "array", items };
1091
1556
  }
1092
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1557
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1093
1558
  if (type.getProperties().length > 0) {
1094
1559
  return null;
1095
1560
  }
@@ -1097,39 +1562,123 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1097
1562
  if (!indexInfo) {
1098
1563
  return null;
1099
1564
  }
1100
- if (visiting.has(type)) {
1101
- return null;
1102
- }
1103
- visiting.add(type);
1104
- try {
1105
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1106
- return { kind: "record", valueType };
1107
- } finally {
1108
- visiting.delete(type);
1109
- }
1565
+ const valueType = resolveTypeNode(
1566
+ indexInfo.type,
1567
+ checker,
1568
+ file,
1569
+ typeRegistry,
1570
+ visiting,
1571
+ void 0,
1572
+ extensionRegistry
1573
+ );
1574
+ return { kind: "record", valueType };
1110
1575
  }
1111
- function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1112
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1113
- if (recordNode) {
1114
- return recordNode;
1576
+ function typeNodeContainsReference(type, targetName) {
1577
+ switch (type.kind) {
1578
+ case "reference":
1579
+ return type.name === targetName;
1580
+ case "array":
1581
+ return typeNodeContainsReference(type.items, targetName);
1582
+ case "record":
1583
+ return typeNodeContainsReference(type.valueType, targetName);
1584
+ case "union":
1585
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
1586
+ case "object":
1587
+ return type.properties.some(
1588
+ (property) => typeNodeContainsReference(property.type, targetName)
1589
+ );
1590
+ case "primitive":
1591
+ case "enum":
1592
+ case "dynamic":
1593
+ case "custom":
1594
+ return false;
1595
+ default: {
1596
+ const _exhaustive = type;
1597
+ return _exhaustive;
1598
+ }
1115
1599
  }
1600
+ }
1601
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1602
+ const typeName = getNamedTypeName(type);
1603
+ const namedTypeName = typeName ?? void 0;
1604
+ const namedDecl = getNamedTypeDeclaration(type);
1605
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
1606
+ const clearNamedTypeRegistration = () => {
1607
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
1608
+ return;
1609
+ }
1610
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
1611
+ };
1116
1612
  if (visiting.has(type)) {
1613
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1614
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1615
+ }
1117
1616
  return { kind: "object", properties: [], additionalProperties: false };
1118
1617
  }
1618
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
1619
+ typeRegistry[namedTypeName] = {
1620
+ name: namedTypeName,
1621
+ type: RESOLVING_TYPE_PLACEHOLDER,
1622
+ provenance: provenanceForDeclaration(namedDecl, file)
1623
+ };
1624
+ }
1119
1625
  visiting.add(type);
1120
- const typeName = getNamedTypeName(type);
1121
- if (typeName && typeName in typeRegistry) {
1626
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
1627
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
1628
+ visiting.delete(type);
1629
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1630
+ }
1631
+ }
1632
+ const recordNode = tryResolveRecordType(
1633
+ type,
1634
+ checker,
1635
+ file,
1636
+ typeRegistry,
1637
+ visiting,
1638
+ extensionRegistry
1639
+ );
1640
+ if (recordNode) {
1122
1641
  visiting.delete(type);
1123
- return { kind: "reference", name: typeName, typeArguments: [] };
1642
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1643
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
1644
+ if (!isRecursiveRecord) {
1645
+ clearNamedTypeRegistration();
1646
+ return recordNode;
1647
+ }
1648
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1649
+ typeRegistry[namedTypeName] = {
1650
+ name: namedTypeName,
1651
+ type: recordNode,
1652
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1653
+ provenance: provenanceForDeclaration(namedDecl, file)
1654
+ };
1655
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1656
+ }
1657
+ return recordNode;
1124
1658
  }
1125
1659
  const properties = [];
1126
- const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
1660
+ const fieldInfoMap = getNamedTypeFieldNodeInfoMap(
1661
+ type,
1662
+ checker,
1663
+ file,
1664
+ typeRegistry,
1665
+ visiting,
1666
+ extensionRegistry
1667
+ );
1127
1668
  for (const prop of type.getProperties()) {
1128
1669
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1129
1670
  if (!declaration) continue;
1130
1671
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1131
1672
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1132
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1673
+ const propTypeNode = resolveTypeNode(
1674
+ propType,
1675
+ checker,
1676
+ file,
1677
+ typeRegistry,
1678
+ visiting,
1679
+ declaration,
1680
+ extensionRegistry
1681
+ );
1133
1682
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1134
1683
  properties.push({
1135
1684
  name: prop.name,
@@ -1146,17 +1695,19 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1146
1695
  properties,
1147
1696
  additionalProperties: true
1148
1697
  };
1149
- if (typeName) {
1150
- typeRegistry[typeName] = {
1151
- name: typeName,
1698
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1699
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1700
+ typeRegistry[namedTypeName] = {
1701
+ name: namedTypeName,
1152
1702
  type: objectNode,
1153
- provenance: provenanceForFile(file)
1703
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1704
+ provenance: provenanceForDeclaration(namedDecl, file)
1154
1705
  };
1155
- return { kind: "reference", name: typeName, typeArguments: [] };
1706
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1156
1707
  }
1157
1708
  return objectNode;
1158
1709
  }
1159
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
1710
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1160
1711
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1161
1712
  (s) => s?.declarations != null && s.declarations.length > 0
1162
1713
  );
@@ -1168,7 +1719,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1168
1719
  const map = /* @__PURE__ */ new Map();
1169
1720
  for (const member of classDecl.members) {
1170
1721
  if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1171
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1722
+ const fieldNode = analyzeFieldToIR(
1723
+ member,
1724
+ checker,
1725
+ file,
1726
+ typeRegistry,
1727
+ visiting,
1728
+ extensionRegistry
1729
+ );
1172
1730
  if (fieldNode) {
1173
1731
  map.set(fieldNode.name, {
1174
1732
  constraints: [...fieldNode.constraints],
@@ -1182,7 +1740,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1182
1740
  }
1183
1741
  const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1184
1742
  if (interfaceDecl) {
1185
- return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
1743
+ return buildFieldNodeInfoMap(
1744
+ interfaceDecl.members,
1745
+ checker,
1746
+ file,
1747
+ typeRegistry,
1748
+ visiting,
1749
+ extensionRegistry
1750
+ );
1186
1751
  }
1187
1752
  const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1188
1753
  if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
@@ -1191,17 +1756,68 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1191
1756
  checker,
1192
1757
  file,
1193
1758
  typeRegistry,
1194
- visiting
1759
+ visiting,
1760
+ extensionRegistry
1195
1761
  );
1196
1762
  }
1197
1763
  }
1198
1764
  return null;
1199
1765
  }
1200
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1766
+ function extractArrayElementTypeNode(sourceNode, checker) {
1767
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1768
+ if (typeNode === void 0) {
1769
+ return void 0;
1770
+ }
1771
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1772
+ if (ts4.isArrayTypeNode(resolvedTypeNode)) {
1773
+ return resolvedTypeNode.elementType;
1774
+ }
1775
+ if (ts4.isTypeReferenceNode(resolvedTypeNode) && ts4.isIdentifier(resolvedTypeNode.typeName) && resolvedTypeNode.typeName.text === "Array" && resolvedTypeNode.typeArguments?.[0]) {
1776
+ return resolvedTypeNode.typeArguments[0];
1777
+ }
1778
+ return void 0;
1779
+ }
1780
+ function extractUnionMemberTypeNodes(sourceNode, checker) {
1781
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1782
+ if (!typeNode) {
1783
+ return [];
1784
+ }
1785
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1786
+ return ts4.isUnionTypeNode(resolvedTypeNode) ? [...resolvedTypeNode.types] : [];
1787
+ }
1788
+ function resolveAliasedTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1789
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1790
+ return resolveAliasedTypeNode(typeNode.type, checker, visited);
1791
+ }
1792
+ if (!ts4.isTypeReferenceNode(typeNode) || !ts4.isIdentifier(typeNode.typeName)) {
1793
+ return typeNode;
1794
+ }
1795
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1796
+ const aliasDecl = symbol?.declarations?.find(ts4.isTypeAliasDeclaration);
1797
+ if (aliasDecl === void 0 || visited.has(aliasDecl)) {
1798
+ return typeNode;
1799
+ }
1800
+ visited.add(aliasDecl);
1801
+ return resolveAliasedTypeNode(aliasDecl.type, checker, visited);
1802
+ }
1803
+ function isNullishTypeNode(typeNode) {
1804
+ if (typeNode.kind === ts4.SyntaxKind.NullKeyword || typeNode.kind === ts4.SyntaxKind.UndefinedKeyword) {
1805
+ return true;
1806
+ }
1807
+ return ts4.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts4.SyntaxKind.NullKeyword || typeNode.literal.kind === ts4.SyntaxKind.UndefinedKeyword);
1808
+ }
1809
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
1201
1810
  const map = /* @__PURE__ */ new Map();
1202
1811
  for (const member of members) {
1203
1812
  if (ts4.isPropertySignature(member)) {
1204
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1813
+ const fieldNode = analyzeInterfacePropertyToIR(
1814
+ member,
1815
+ checker,
1816
+ file,
1817
+ typeRegistry,
1818
+ visiting,
1819
+ extensionRegistry
1820
+ );
1205
1821
  if (fieldNode) {
1206
1822
  map.set(fieldNode.name, {
1207
1823
  constraints: [...fieldNode.constraints],
@@ -1214,7 +1830,7 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1214
1830
  return map;
1215
1831
  }
1216
1832
  var MAX_ALIAS_CHAIN_DEPTH = 8;
1217
- function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1833
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegistry, depth = 0) {
1218
1834
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1219
1835
  if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1220
1836
  const aliasName = typeNode.typeName.getText();
@@ -1227,8 +1843,29 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1227
1843
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1228
1844
  if (!aliasDecl) return [];
1229
1845
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1230
- const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1231
- constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1846
+ const aliasFieldType = resolveTypeNode(
1847
+ checker.getTypeAtLocation(aliasDecl.type),
1848
+ checker,
1849
+ file,
1850
+ {},
1851
+ /* @__PURE__ */ new Set(),
1852
+ aliasDecl.type,
1853
+ extensionRegistry
1854
+ );
1855
+ const constraints = extractJSDocConstraintNodes(
1856
+ aliasDecl,
1857
+ file,
1858
+ makeParseOptions(extensionRegistry, aliasFieldType)
1859
+ );
1860
+ constraints.push(
1861
+ ...extractTypeAliasConstraintNodes(
1862
+ aliasDecl.type,
1863
+ checker,
1864
+ file,
1865
+ extensionRegistry,
1866
+ depth + 1
1867
+ )
1868
+ );
1232
1869
  return constraints;
1233
1870
  }
1234
1871
  function provenanceForNode(node, file) {
@@ -1244,6 +1881,12 @@ function provenanceForNode(node, file) {
1244
1881
  function provenanceForFile(file) {
1245
1882
  return { surface: "tsdoc", file, line: 0, column: 0 };
1246
1883
  }
1884
+ function provenanceForDeclaration(node, file) {
1885
+ if (!node) {
1886
+ return provenanceForFile(file);
1887
+ }
1888
+ return provenanceForNode(node, file);
1889
+ }
1247
1890
  function getNamedTypeName(type) {
1248
1891
  const symbol = type.getSymbol();
1249
1892
  if (symbol?.declarations) {
@@ -1262,6 +1905,20 @@ function getNamedTypeName(type) {
1262
1905
  }
1263
1906
  return null;
1264
1907
  }
1908
+ function getNamedTypeDeclaration(type) {
1909
+ const symbol = type.getSymbol();
1910
+ if (symbol?.declarations) {
1911
+ const decl = symbol.declarations[0];
1912
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
1913
+ return decl;
1914
+ }
1915
+ }
1916
+ const aliasSymbol = type.aliasSymbol;
1917
+ if (aliasSymbol?.declarations) {
1918
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
1919
+ }
1920
+ return void 0;
1921
+ }
1265
1922
  function analyzeMethod(method, checker) {
1266
1923
  if (!ts4.isIdentifier(method.name)) {
1267
1924
  return null;
@@ -1321,6 +1978,9 @@ function generateJsonSchemaFromIR(ir, options) {
1321
1978
  const ctx = makeContext(options);
1322
1979
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1323
1980
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1981
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
1982
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
1983
+ }
1324
1984
  }
1325
1985
  const properties = {};
1326
1986
  const required = [];
@@ -1332,6 +1992,9 @@ function generateJsonSchemaFromIR(ir, options) {
1332
1992
  properties,
1333
1993
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1334
1994
  };
1995
+ if (ir.annotations && ir.annotations.length > 0) {
1996
+ applyAnnotations(result, ir.annotations, ctx);
1997
+ }
1335
1998
  if (Object.keys(ctx.defs).length > 0) {
1336
1999
  result.$defs = ctx.defs;
1337
2000
  }
@@ -1361,22 +2024,51 @@ function collectFields(elements, properties, required, ctx) {
1361
2024
  }
1362
2025
  function generateFieldSchema(field, ctx) {
1363
2026
  const schema = generateTypeNode(field.type, ctx);
2027
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1364
2028
  const directConstraints = [];
2029
+ const itemConstraints = [];
1365
2030
  const pathConstraints = [];
1366
2031
  for (const c of field.constraints) {
1367
2032
  if (c.path) {
1368
2033
  pathConstraints.push(c);
2034
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
2035
+ itemConstraints.push(c);
1369
2036
  } else {
1370
2037
  directConstraints.push(c);
1371
2038
  }
1372
2039
  }
1373
2040
  applyConstraints(schema, directConstraints, ctx);
1374
- applyAnnotations(schema, field.annotations, ctx);
2041
+ if (itemStringSchema !== void 0) {
2042
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
2043
+ }
2044
+ const rootAnnotations = [];
2045
+ const itemAnnotations = [];
2046
+ for (const annotation of field.annotations) {
2047
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
2048
+ itemAnnotations.push(annotation);
2049
+ } else {
2050
+ rootAnnotations.push(annotation);
2051
+ }
2052
+ }
2053
+ applyAnnotations(schema, rootAnnotations, ctx);
2054
+ if (itemStringSchema !== void 0) {
2055
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
2056
+ }
1375
2057
  if (pathConstraints.length === 0) {
1376
2058
  return schema;
1377
2059
  }
1378
2060
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1379
2061
  }
2062
+ function isStringItemConstraint(constraint) {
2063
+ switch (constraint.constraintKind) {
2064
+ case "minLength":
2065
+ case "maxLength":
2066
+ case "pattern":
2067
+ return true;
2068
+ default:
2069
+ return false;
2070
+ }
2071
+ }
1380
2072
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1381
2073
  if (schema.type === "array" && schema.items) {
1382
2074
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -1594,6 +2286,9 @@ function applyConstraints(schema, constraints, ctx) {
1594
2286
  case "uniqueItems":
1595
2287
  schema.uniqueItems = constraint.value;
1596
2288
  break;
2289
+ case "const":
2290
+ schema.const = constraint.value;
2291
+ break;
1597
2292
  case "allowedMembers":
1598
2293
  break;
1599
2294
  case "custom":
@@ -1618,8 +2313,14 @@ function applyAnnotations(schema, annotations, ctx) {
1618
2313
  case "defaultValue":
1619
2314
  schema.default = annotation.value;
1620
2315
  break;
2316
+ case "format":
2317
+ schema.format = annotation.value;
2318
+ break;
1621
2319
  case "deprecated":
1622
2320
  schema.deprecated = true;
2321
+ if (annotation.message !== void 0 && annotation.message !== "") {
2322
+ schema["x-formspec-deprecation-description"] = annotation.message;
2323
+ }
1623
2324
  break;
1624
2325
  case "placeholder":
1625
2326
  break;
@@ -1651,7 +2352,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
1651
2352
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1652
2353
  );
1653
2354
  }
1654
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
2355
+ assignVendorPrefixedExtensionKeywords(
2356
+ schema,
2357
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
2358
+ ctx.vendorPrefix,
2359
+ `custom constraint "${constraint.constraintId}"`
2360
+ );
1655
2361
  }
1656
2362
  function applyCustomAnnotation(schema, annotation, ctx) {
1657
2363
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -1663,7 +2369,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
1663
2369
  if (registration.toJsonSchema === void 0) {
1664
2370
  return;
1665
2371
  }
1666
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
2372
+ assignVendorPrefixedExtensionKeywords(
2373
+ schema,
2374
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
2375
+ ctx.vendorPrefix,
2376
+ `custom annotation "${annotation.annotationId}"`
2377
+ );
2378
+ }
2379
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
2380
+ for (const [key, value] of Object.entries(extensionSchema)) {
2381
+ if (!key.startsWith(`${vendorPrefix}-`)) {
2382
+ throw new Error(
2383
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
2384
+ );
2385
+ }
2386
+ schema[key] = value;
2387
+ }
1667
2388
  }
1668
2389
 
1669
2390
  // src/ui-schema/schema.ts
@@ -1801,25 +2522,31 @@ function createShowRule(fieldName, value) {
1801
2522
  }
1802
2523
  };
1803
2524
  }
2525
+ function flattenConditionSchema(scope, schema) {
2526
+ if (schema.allOf === void 0) {
2527
+ if (scope === "#") {
2528
+ return [schema];
2529
+ }
2530
+ const fieldName = scope.replace("#/properties/", "");
2531
+ return [
2532
+ {
2533
+ properties: {
2534
+ [fieldName]: schema
2535
+ }
2536
+ }
2537
+ ];
2538
+ }
2539
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
2540
+ }
1804
2541
  function combineRules(parentRule, childRule) {
1805
- const parentCondition = parentRule.condition;
1806
- const childCondition = childRule.condition;
1807
2542
  return {
1808
2543
  effect: "SHOW",
1809
2544
  condition: {
1810
2545
  scope: "#",
1811
2546
  schema: {
1812
2547
  allOf: [
1813
- {
1814
- properties: {
1815
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1816
- }
1817
- },
1818
- {
1819
- properties: {
1820
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1821
- }
1822
- }
2548
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2549
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1823
2550
  ]
1824
2551
  }
1825
2552
  }
@@ -1827,10 +2554,14 @@ function combineRules(parentRule, childRule) {
1827
2554
  }
1828
2555
  function fieldNodeToControl(field, parentRule) {
1829
2556
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2557
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1830
2558
  const control = {
1831
2559
  type: "Control",
1832
2560
  scope: fieldToScope(field.name),
1833
2561
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2562
+ ...placeholderAnnotation !== void 0 && {
2563
+ options: { placeholder: placeholderAnnotation.value }
2564
+ },
1834
2565
  ...parentRule !== void 0 && { rule: parentRule }
1835
2566
  };
1836
2567
  return control;
@@ -1880,15 +2611,16 @@ function generateUiSchemaFromIR(ir) {
1880
2611
  }
1881
2612
 
1882
2613
  // src/generators/class-schema.ts
1883
- function generateClassSchemas(analysis, source) {
2614
+ function generateClassSchemas(analysis, source, options) {
1884
2615
  const ir = canonicalizeTSDoc(analysis, source);
1885
2616
  return {
1886
- jsonSchema: generateJsonSchemaFromIR(ir),
2617
+ jsonSchema: generateJsonSchemaFromIR(ir, options),
1887
2618
  uiSchema: generateUiSchemaFromIR(ir)
1888
2619
  };
1889
2620
  }
1890
2621
 
1891
2622
  // src/validate/constraint-validator.ts
2623
+ import { normalizeConstraintTagName as normalizeConstraintTagName2 } from "@formspec/core";
1892
2624
  function addContradiction(ctx, message, primary, related) {
1893
2625
  ctx.diagnostics.push({
1894
2626
  code: "CONTRADICTING_CONSTRAINTS",
@@ -1916,6 +2648,15 @@ function addUnknownExtension(ctx, message, primary) {
1916
2648
  relatedLocations: []
1917
2649
  });
1918
2650
  }
2651
+ function addUnknownPathTarget(ctx, message, primary) {
2652
+ ctx.diagnostics.push({
2653
+ code: "UNKNOWN_PATH_TARGET",
2654
+ message,
2655
+ severity: "error",
2656
+ primaryLocation: primary,
2657
+ relatedLocations: []
2658
+ });
2659
+ }
1919
2660
  function addConstraintBroadening(ctx, message, primary, related) {
1920
2661
  ctx.diagnostics.push({
1921
2662
  code: "CONSTRAINT_BROADENING",
@@ -1925,6 +2666,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
1925
2666
  relatedLocations: [related]
1926
2667
  });
1927
2668
  }
2669
+ function getExtensionIdFromConstraintId(constraintId) {
2670
+ const separator = constraintId.lastIndexOf("/");
2671
+ if (separator <= 0) {
2672
+ return null;
2673
+ }
2674
+ return constraintId.slice(0, separator);
2675
+ }
1928
2676
  function findNumeric(constraints, constraintKind) {
1929
2677
  return constraints.find((c) => c.constraintKind === constraintKind);
1930
2678
  }
@@ -1936,6 +2684,45 @@ function findAllowedMembers(constraints) {
1936
2684
  (c) => c.constraintKind === "allowedMembers"
1937
2685
  );
1938
2686
  }
2687
+ function findConstConstraints(constraints) {
2688
+ return constraints.filter(
2689
+ (c) => c.constraintKind === "const"
2690
+ );
2691
+ }
2692
+ function jsonValueEquals(left, right) {
2693
+ if (left === right) {
2694
+ return true;
2695
+ }
2696
+ if (Array.isArray(left) || Array.isArray(right)) {
2697
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2698
+ return false;
2699
+ }
2700
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
2701
+ }
2702
+ if (isJsonObject(left) || isJsonObject(right)) {
2703
+ if (!isJsonObject(left) || !isJsonObject(right)) {
2704
+ return false;
2705
+ }
2706
+ const leftKeys = Object.keys(left).sort();
2707
+ const rightKeys = Object.keys(right).sort();
2708
+ if (leftKeys.length !== rightKeys.length) {
2709
+ return false;
2710
+ }
2711
+ return leftKeys.every((key, index) => {
2712
+ const rightKey = rightKeys[index];
2713
+ if (rightKey !== key) {
2714
+ return false;
2715
+ }
2716
+ const leftValue = left[key];
2717
+ const rightValue = right[rightKey];
2718
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2719
+ });
2720
+ }
2721
+ return false;
2722
+ }
2723
+ function isJsonObject(value) {
2724
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2725
+ }
1939
2726
  function isOrderedBoundConstraint(constraint) {
1940
2727
  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";
1941
2728
  }
@@ -2056,6 +2843,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
2056
2843
  strongestByKey.set(key, constraint);
2057
2844
  }
2058
2845
  }
2846
+ function compareCustomConstraintStrength(current, previous) {
2847
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
2848
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
2849
+ switch (current.role.bound) {
2850
+ case "lower":
2851
+ return equalPayloadTiebreaker;
2852
+ case "upper":
2853
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
2854
+ case "exact":
2855
+ return order === 0 ? 0 : Number.NaN;
2856
+ default: {
2857
+ const _exhaustive = current.role.bound;
2858
+ return _exhaustive;
2859
+ }
2860
+ }
2861
+ }
2862
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
2863
+ if (currentInclusive === previousInclusive) {
2864
+ return 0;
2865
+ }
2866
+ return currentInclusive ? -1 : 1;
2867
+ }
2868
+ function customConstraintsContradict(lower, upper) {
2869
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
2870
+ if (order > 0) {
2871
+ return true;
2872
+ }
2873
+ if (order < 0) {
2874
+ return false;
2875
+ }
2876
+ return !lower.role.inclusive || !upper.role.inclusive;
2877
+ }
2878
+ function describeCustomConstraintTag(constraint) {
2879
+ return constraint.provenance.tagName ?? constraint.constraintId;
2880
+ }
2881
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
2882
+ if (ctx.extensionRegistry === void 0) {
2883
+ return;
2884
+ }
2885
+ const strongestByKey = /* @__PURE__ */ new Map();
2886
+ const lowerByFamily = /* @__PURE__ */ new Map();
2887
+ const upperByFamily = /* @__PURE__ */ new Map();
2888
+ for (const constraint of constraints) {
2889
+ if (constraint.constraintKind !== "custom") {
2890
+ continue;
2891
+ }
2892
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
2893
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
2894
+ continue;
2895
+ }
2896
+ const entry = {
2897
+ constraint,
2898
+ comparePayloads: registration.comparePayloads,
2899
+ role: registration.semanticRole
2900
+ };
2901
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
2902
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
2903
+ const previous = strongestByKey.get(boundKey);
2904
+ if (previous !== void 0) {
2905
+ const strength = compareCustomConstraintStrength(entry, previous);
2906
+ if (Number.isNaN(strength)) {
2907
+ addContradiction(
2908
+ ctx,
2909
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
2910
+ constraint.provenance,
2911
+ previous.constraint.provenance
2912
+ );
2913
+ continue;
2914
+ }
2915
+ if (strength < 0) {
2916
+ addConstraintBroadening(
2917
+ ctx,
2918
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
2919
+ constraint.provenance,
2920
+ previous.constraint.provenance
2921
+ );
2922
+ continue;
2923
+ }
2924
+ if (strength > 0) {
2925
+ strongestByKey.set(boundKey, entry);
2926
+ }
2927
+ } else {
2928
+ strongestByKey.set(boundKey, entry);
2929
+ }
2930
+ if (registration.semanticRole.bound === "lower") {
2931
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2932
+ } else if (registration.semanticRole.bound === "upper") {
2933
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2934
+ }
2935
+ }
2936
+ for (const [familyKey, lower] of lowerByFamily) {
2937
+ const upper = upperByFamily.get(familyKey);
2938
+ if (upper === void 0) {
2939
+ continue;
2940
+ }
2941
+ if (!customConstraintsContradict(lower, upper)) {
2942
+ continue;
2943
+ }
2944
+ addContradiction(
2945
+ ctx,
2946
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
2947
+ lower.constraint.provenance,
2948
+ upper.constraint.provenance
2949
+ );
2950
+ }
2951
+ }
2059
2952
  function checkNumericContradictions(ctx, fieldName, constraints) {
2060
2953
  const min = findNumeric(constraints, "minimum");
2061
2954
  const max = findNumeric(constraints, "maximum");
@@ -2142,6 +3035,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
2142
3035
  }
2143
3036
  }
2144
3037
  }
3038
+ function checkConstContradictions(ctx, fieldName, constraints) {
3039
+ const constConstraints = findConstConstraints(constraints);
3040
+ if (constConstraints.length < 2) return;
3041
+ const first = constConstraints[0];
3042
+ if (first === void 0) return;
3043
+ for (let i = 1; i < constConstraints.length; i++) {
3044
+ const current = constConstraints[i];
3045
+ if (current === void 0) continue;
3046
+ if (jsonValueEquals(first.value, current.value)) {
3047
+ continue;
3048
+ }
3049
+ addContradiction(
3050
+ ctx,
3051
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
3052
+ first.provenance,
3053
+ current.provenance
3054
+ );
3055
+ }
3056
+ }
2145
3057
  function typeLabel(type) {
2146
3058
  switch (type.kind) {
2147
3059
  case "primitive":
@@ -2214,6 +3126,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2214
3126
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2215
3127
  const isArray = effectiveType.kind === "array";
2216
3128
  const isEnum = effectiveType.kind === "enum";
3129
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
3130
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
2217
3131
  const label = typeLabel(effectiveType);
2218
3132
  const ck = constraint.constraintKind;
2219
3133
  switch (ck) {
@@ -2234,10 +3148,10 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2234
3148
  case "minLength":
2235
3149
  case "maxLength":
2236
3150
  case "pattern": {
2237
- if (!isString) {
3151
+ if (!isString && !isStringArray) {
2238
3152
  addTypeMismatch(
2239
3153
  ctx,
2240
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
3154
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2241
3155
  constraint.provenance
2242
3156
  );
2243
3157
  }
@@ -2265,6 +3179,37 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2265
3179
  }
2266
3180
  break;
2267
3181
  }
3182
+ case "const": {
3183
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
3184
+ if (!isPrimitiveConstType) {
3185
+ addTypeMismatch(
3186
+ ctx,
3187
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
3188
+ constraint.provenance
3189
+ );
3190
+ break;
3191
+ }
3192
+ if (effectiveType.kind === "primitive") {
3193
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
3194
+ if (valueType !== effectiveType.primitiveKind) {
3195
+ addTypeMismatch(
3196
+ ctx,
3197
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
3198
+ constraint.provenance
3199
+ );
3200
+ }
3201
+ break;
3202
+ }
3203
+ const memberValues = effectiveType.members.map((member) => member.value);
3204
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
3205
+ addTypeMismatch(
3206
+ ctx,
3207
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
3208
+ constraint.provenance
3209
+ );
3210
+ }
3211
+ break;
3212
+ }
2268
3213
  case "custom": {
2269
3214
  checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2270
3215
  break;
@@ -2283,9 +3228,9 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2283
3228
  const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2284
3229
  const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2285
3230
  if (resolution.kind === "missing-property") {
2286
- addTypeMismatch(
3231
+ addUnknownPathTarget(
2287
3232
  ctx,
2288
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
3233
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2289
3234
  constraint.provenance
2290
3235
  );
2291
3236
  continue;
@@ -2315,8 +3260,30 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
2315
3260
  );
2316
3261
  return;
2317
3262
  }
2318
- if (registration.applicableTypes === null) return;
2319
- if (!registration.applicableTypes.includes(type.kind)) {
3263
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : normalizeConstraintTagName2(constraint.provenance.tagName.replace(/^@/, ""));
3264
+ if (normalizedTagName !== void 0) {
3265
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3266
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3267
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && tagRegistration.registration.isApplicableToType?.(type) === false) {
3268
+ addTypeMismatch(
3269
+ ctx,
3270
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3271
+ constraint.provenance
3272
+ );
3273
+ return;
3274
+ }
3275
+ }
3276
+ if (registration.applicableTypes === null) {
3277
+ if (registration.isApplicableToType?.(type) === false) {
3278
+ addTypeMismatch(
3279
+ ctx,
3280
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3281
+ constraint.provenance
3282
+ );
3283
+ }
3284
+ return;
3285
+ }
3286
+ if (!registration.applicableTypes.includes(type.kind) || registration.isApplicableToType?.(type) === false) {
2320
3287
  addTypeMismatch(
2321
3288
  ctx,
2322
3289
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -2345,7 +3312,9 @@ function validateConstraints(ctx, name, type, constraints) {
2345
3312
  checkNumericContradictions(ctx, name, constraints);
2346
3313
  checkLengthContradictions(ctx, name, constraints);
2347
3314
  checkAllowedMembersContradiction(ctx, name, constraints);
3315
+ checkConstContradictions(ctx, name, constraints);
2348
3316
  checkConstraintBroadening(ctx, name, constraints);
3317
+ checkCustomConstraintSemantics(ctx, name, constraints);
2349
3318
  checkTypeApplicability(ctx, name, type, constraints);
2350
3319
  }
2351
3320
  function validateElement(ctx, element) {
@@ -2387,7 +3356,10 @@ function validateIR(ir, options) {
2387
3356
  // src/extensions/registry.ts
2388
3357
  function createExtensionRegistry(extensions) {
2389
3358
  const typeMap = /* @__PURE__ */ new Map();
3359
+ const typeNameMap = /* @__PURE__ */ new Map();
2390
3360
  const constraintMap = /* @__PURE__ */ new Map();
3361
+ const constraintTagMap = /* @__PURE__ */ new Map();
3362
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
2391
3363
  const annotationMap = /* @__PURE__ */ new Map();
2392
3364
  for (const ext of extensions) {
2393
3365
  if (ext.types !== void 0) {
@@ -2397,6 +3369,27 @@ function createExtensionRegistry(extensions) {
2397
3369
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
2398
3370
  }
2399
3371
  typeMap.set(qualifiedId, type);
3372
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
3373
+ if (typeNameMap.has(sourceTypeName)) {
3374
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
3375
+ }
3376
+ typeNameMap.set(sourceTypeName, {
3377
+ extensionId: ext.extensionId,
3378
+ registration: type
3379
+ });
3380
+ }
3381
+ if (type.builtinConstraintBroadenings !== void 0) {
3382
+ for (const broadening of type.builtinConstraintBroadenings) {
3383
+ const key = `${qualifiedId}:${broadening.tagName}`;
3384
+ if (builtinBroadeningMap.has(key)) {
3385
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
3386
+ }
3387
+ builtinBroadeningMap.set(key, {
3388
+ extensionId: ext.extensionId,
3389
+ registration: broadening
3390
+ });
3391
+ }
3392
+ }
2400
3393
  }
2401
3394
  }
2402
3395
  if (ext.constraints !== void 0) {
@@ -2408,6 +3401,17 @@ function createExtensionRegistry(extensions) {
2408
3401
  constraintMap.set(qualifiedId, constraint);
2409
3402
  }
2410
3403
  }
3404
+ if (ext.constraintTags !== void 0) {
3405
+ for (const tag of ext.constraintTags) {
3406
+ if (constraintTagMap.has(tag.tagName)) {
3407
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
3408
+ }
3409
+ constraintTagMap.set(tag.tagName, {
3410
+ extensionId: ext.extensionId,
3411
+ registration: tag
3412
+ });
3413
+ }
3414
+ }
2411
3415
  if (ext.annotations !== void 0) {
2412
3416
  for (const annotation of ext.annotations) {
2413
3417
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -2421,7 +3425,10 @@ function createExtensionRegistry(extensions) {
2421
3425
  return {
2422
3426
  extensions,
2423
3427
  findType: (typeId) => typeMap.get(typeId),
3428
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
2424
3429
  findConstraint: (constraintId) => constraintMap.get(constraintId),
3430
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
3431
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
2425
3432
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
2426
3433
  };
2427
3434
  }