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

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 (68) hide show
  1. package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
  2. package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/edge-cases.d.ts +22 -0
  4. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures/example-a-builtins.d.ts +6 -6
  6. package/dist/__tests__/fixtures/example-interface-types.d.ts +26 -26
  7. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
  8. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +30 -0
  9. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  10. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  11. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  12. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  13. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  14. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  15. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  16. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  17. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  18. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  19. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  20. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  21. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  22. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  23. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  24. package/dist/__tests__/parity/utils.d.ts +11 -4
  25. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  26. package/dist/analyzer/class-analyzer.d.ts +5 -3
  27. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  28. package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
  29. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  30. package/dist/analyzer/tsdoc-parser.d.ts +25 -9
  31. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  32. package/dist/browser.cjs +546 -102
  33. package/dist/browser.cjs.map +1 -1
  34. package/dist/browser.d.ts +15 -2
  35. package/dist/browser.d.ts.map +1 -1
  36. package/dist/browser.js +544 -102
  37. package/dist/browser.js.map +1 -1
  38. package/dist/build.d.ts +170 -6
  39. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  40. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  41. package/dist/cli.cjs +877 -128
  42. package/dist/cli.cjs.map +1 -1
  43. package/dist/cli.js +876 -131
  44. package/dist/cli.js.map +1 -1
  45. package/dist/generators/mixed-authoring.d.ts +45 -0
  46. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  47. package/dist/index.cjs +850 -125
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.ts +22 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +847 -129
  52. package/dist/index.js.map +1 -1
  53. package/dist/internals.cjs +946 -187
  54. package/dist/internals.cjs.map +1 -1
  55. package/dist/internals.js +944 -189
  56. package/dist/internals.js.map +1 -1
  57. package/dist/json-schema/generator.d.ts +8 -2
  58. package/dist/json-schema/generator.d.ts.map +1 -1
  59. package/dist/json-schema/ir-generator.d.ts +27 -4
  60. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  61. package/dist/json-schema/types.d.ts +1 -1
  62. package/dist/json-schema/types.d.ts.map +1 -1
  63. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  64. package/dist/validate/constraint-validator.d.ts +3 -7
  65. package/dist/validate/constraint-validator.d.ts.map +1 -1
  66. package/package.json +3 -3
  67. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -10
  68. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
package/dist/internals.js CHANGED
@@ -177,7 +177,7 @@ function canonicalizeArrayField(field) {
177
177
  const itemsType = {
178
178
  kind: "object",
179
179
  properties: itemProperties,
180
- additionalProperties: false
180
+ additionalProperties: true
181
181
  };
182
182
  const type = { kind: "array", items: itemsType };
183
183
  const constraints = [];
@@ -212,7 +212,7 @@ function canonicalizeObjectField(field) {
212
212
  const type = {
213
213
  kind: "object",
214
214
  properties,
215
- additionalProperties: false
215
+ additionalProperties: true
216
216
  };
217
217
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
218
218
  }
@@ -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
  }
@@ -460,11 +461,6 @@ import * as ts4 from "typescript";
460
461
 
461
462
  // src/analyzer/jsdoc-constraints.ts
462
463
  import * as ts3 from "typescript";
463
- import {
464
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
465
- isBuiltinConstraintName as isBuiltinConstraintName2,
466
- normalizeConstraintTagName as normalizeConstraintTagName2
467
- } from "@formspec/core";
468
464
 
469
465
  // src/analyzer/tsdoc-parser.ts
470
466
  import * as ts2 from "typescript";
@@ -506,7 +502,7 @@ var LENGTH_CONSTRAINT_MAP = {
506
502
  minItems: "minItems",
507
503
  maxItems: "maxItems"
508
504
  };
509
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
505
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
510
506
  function createFormSpecTSDocConfig() {
511
507
  const config = new TSDocConfiguration();
512
508
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -518,6 +514,15 @@ function createFormSpecTSDocConfig() {
518
514
  })
519
515
  );
520
516
  }
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
+ }
521
526
  return config;
522
527
  }
523
528
  var sharedParser;
@@ -528,6 +533,12 @@ function getParser() {
528
533
  function parseTSDocTags(node, file = "") {
529
534
  const constraints = [];
530
535
  const annotations = [];
536
+ let displayName;
537
+ let description;
538
+ let placeholder;
539
+ let displayNameProvenance;
540
+ let descriptionProvenance;
541
+ let placeholderProvenance;
531
542
  const sourceFile = node.getSourceFile();
532
543
  const sourceText = sourceFile.getFullText();
533
544
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -547,9 +558,37 @@ function parseTSDocTags(node, file = "") {
547
558
  const docComment = parserContext.docComment;
548
559
  for (const block of docComment.customBlocks) {
549
560
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
561
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
562
+ const text2 = extractBlockText(block).trim();
563
+ if (text2 === "") continue;
564
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
565
+ if (tagName === "displayName") {
566
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
567
+ displayName = text2;
568
+ displayNameProvenance = provenance2;
569
+ }
570
+ } else if (tagName === "format") {
571
+ annotations.push({
572
+ kind: "annotation",
573
+ annotationKind: "format",
574
+ value: text2,
575
+ provenance: provenance2
576
+ });
577
+ } else {
578
+ if (tagName === "description" && description === void 0) {
579
+ description = text2;
580
+ descriptionProvenance = provenance2;
581
+ } else if (tagName === "placeholder" && placeholder === void 0) {
582
+ placeholder = text2;
583
+ placeholderProvenance = provenance2;
584
+ }
585
+ }
586
+ continue;
587
+ }
550
588
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
551
589
  const text = extractBlockText(block).trim();
552
- if (text === "") continue;
590
+ const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
591
+ if (text === "" && expectedType !== "boolean") continue;
553
592
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
554
593
  const constraintNode = parseConstraintValue(tagName, text, provenance);
555
594
  if (constraintNode) {
@@ -557,14 +596,47 @@ function parseTSDocTags(node, file = "") {
557
596
  }
558
597
  }
559
598
  if (docComment.deprecatedBlock !== void 0) {
599
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
560
600
  annotations.push({
561
601
  kind: "annotation",
562
602
  annotationKind: "deprecated",
603
+ ...message !== "" && { message },
563
604
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
564
605
  });
565
606
  }
607
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
608
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
609
+ if (remarks !== "") {
610
+ description = remarks;
611
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
612
+ }
613
+ }
566
614
  }
567
615
  }
616
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
617
+ annotations.push({
618
+ kind: "annotation",
619
+ annotationKind: "displayName",
620
+ value: displayName,
621
+ provenance: displayNameProvenance
622
+ });
623
+ }
624
+ if (description !== void 0 && descriptionProvenance !== void 0) {
625
+ annotations.push({
626
+ kind: "annotation",
627
+ annotationKind: "description",
628
+ value: description,
629
+ provenance: descriptionProvenance
630
+ });
631
+ }
632
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
633
+ annotations.push({
634
+ kind: "annotation",
635
+ annotationKind: "placeholder",
636
+ value: placeholder,
637
+ provenance: placeholderProvenance
638
+ });
639
+ }
568
640
  const jsDocTagsAll = ts2.getJSDocTags(node);
569
641
  for (const tag of jsDocTagsAll) {
570
642
  const tagName = normalizeConstraintTagName(tag.tagName.text);
@@ -573,47 +645,39 @@ function parseTSDocTags(node, file = "") {
573
645
  if (commentText === void 0 || commentText.trim() === "") continue;
574
646
  const text = commentText.trim();
575
647
  const provenance = provenanceForJSDocTag(tag, file);
648
+ if (tagName === "defaultValue") {
649
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
650
+ annotations.push(defaultValueNode);
651
+ continue;
652
+ }
576
653
  const constraintNode = parseConstraintValue(tagName, text, provenance);
577
654
  if (constraintNode) {
578
655
  constraints.push(constraintNode);
579
656
  }
580
657
  }
658
+ return { constraints, annotations };
659
+ }
660
+ function extractDisplayNameMetadata(node) {
581
661
  let displayName;
582
- let description;
583
- let displayNameTag;
584
- let descriptionTag;
585
- for (const tag of jsDocTagsAll) {
586
- const tagName = tag.tagName.text;
662
+ const memberDisplayNames = /* @__PURE__ */ new Map();
663
+ for (const tag of ts2.getJSDocTags(node)) {
664
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
665
+ if (tagName !== "displayName") continue;
587
666
  const commentText = getTagCommentText(tag);
588
- if (commentText === void 0 || commentText.trim() === "") {
667
+ if (commentText === void 0) continue;
668
+ const text = commentText.trim();
669
+ if (text === "") continue;
670
+ const memberTarget = parseMemberTargetDisplayName(text);
671
+ if (memberTarget) {
672
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
589
673
  continue;
590
674
  }
591
- const trimmed = commentText.trim();
592
- if (tagName === "Field_displayName") {
593
- displayName = trimmed;
594
- displayNameTag = tag;
595
- } else if (tagName === "Field_description") {
596
- description = trimmed;
597
- descriptionTag = tag;
598
- }
599
- }
600
- if (displayName !== void 0 && displayNameTag) {
601
- annotations.push({
602
- kind: "annotation",
603
- annotationKind: "displayName",
604
- value: displayName,
605
- provenance: provenanceForJSDocTag(displayNameTag, file)
606
- });
607
- }
608
- if (description !== void 0 && descriptionTag) {
609
- annotations.push({
610
- kind: "annotation",
611
- annotationKind: "description",
612
- value: description,
613
- provenance: provenanceForJSDocTag(descriptionTag, file)
614
- });
675
+ displayName ??= text;
615
676
  }
616
- return { constraints, annotations };
677
+ return {
678
+ ...displayName !== void 0 && { displayName },
679
+ memberDisplayNames
680
+ };
617
681
  }
618
682
  function extractPathTarget(text) {
619
683
  const trimmed = text.trimStart();
@@ -677,7 +741,45 @@ function parseConstraintValue(tagName, text, provenance) {
677
741
  }
678
742
  return null;
679
743
  }
744
+ if (expectedType === "boolean") {
745
+ const trimmed = effectiveText.trim();
746
+ if (trimmed !== "" && trimmed !== "true") {
747
+ return null;
748
+ }
749
+ if (tagName === "uniqueItems") {
750
+ return {
751
+ kind: "constraint",
752
+ constraintKind: "uniqueItems",
753
+ value: true,
754
+ ...path2 && { path: path2 },
755
+ provenance
756
+ };
757
+ }
758
+ return null;
759
+ }
680
760
  if (expectedType === "json") {
761
+ if (tagName === "const") {
762
+ const trimmedText = effectiveText.trim();
763
+ if (trimmedText === "") return null;
764
+ try {
765
+ const parsed2 = JSON.parse(trimmedText);
766
+ return {
767
+ kind: "constraint",
768
+ constraintKind: "const",
769
+ value: parsed2,
770
+ ...path2 && { path: path2 },
771
+ provenance
772
+ };
773
+ } catch {
774
+ return {
775
+ kind: "constraint",
776
+ constraintKind: "const",
777
+ value: trimmedText,
778
+ ...path2 && { path: path2 },
779
+ provenance
780
+ };
781
+ }
782
+ }
681
783
  const parsed = tryParseJson(effectiveText);
682
784
  if (!Array.isArray(parsed)) {
683
785
  return null;
@@ -709,6 +811,34 @@ function parseConstraintValue(tagName, text, provenance) {
709
811
  provenance
710
812
  };
711
813
  }
814
+ function parseDefaultValueValue(text, provenance) {
815
+ const trimmed = text.trim();
816
+ let value;
817
+ if (trimmed === "null") {
818
+ value = null;
819
+ } else if (trimmed === "true") {
820
+ value = true;
821
+ } else if (trimmed === "false") {
822
+ value = false;
823
+ } else {
824
+ const parsed = tryParseJson(trimmed);
825
+ value = parsed !== null ? parsed : trimmed;
826
+ }
827
+ return {
828
+ kind: "annotation",
829
+ annotationKind: "defaultValue",
830
+ value,
831
+ provenance
832
+ };
833
+ }
834
+ function isMemberTargetDisplayName(text) {
835
+ return parseMemberTargetDisplayName(text) !== null;
836
+ }
837
+ function parseMemberTargetDisplayName(text) {
838
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
839
+ if (!match?.[1] || !match[2]) return null;
840
+ return { target: match[1], label: match[2].trim() };
841
+ }
712
842
  function provenanceForComment(range, sourceFile, file, tagName) {
713
843
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
714
844
  return {
@@ -790,11 +920,17 @@ function isObjectType(type) {
790
920
  function isTypeReference(type) {
791
921
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
792
922
  }
923
+ var RESOLVING_TYPE_PLACEHOLDER = {
924
+ kind: "object",
925
+ properties: [],
926
+ additionalProperties: true
927
+ };
793
928
  function analyzeClassToIR(classDecl, checker, file = "") {
794
929
  const name = classDecl.name?.text ?? "AnonymousClass";
795
930
  const fields = [];
796
931
  const fieldLayouts = [];
797
932
  const typeRegistry = {};
933
+ const annotations = extractJSDocAnnotationNodes(classDecl, file);
798
934
  const visiting = /* @__PURE__ */ new Set();
799
935
  const instanceMethods = [];
800
936
  const staticMethods = [];
@@ -817,12 +953,21 @@ function analyzeClassToIR(classDecl, checker, file = "") {
817
953
  }
818
954
  }
819
955
  }
820
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
956
+ return {
957
+ name,
958
+ fields,
959
+ fieldLayouts,
960
+ typeRegistry,
961
+ ...annotations.length > 0 && { annotations },
962
+ instanceMethods,
963
+ staticMethods
964
+ };
821
965
  }
822
966
  function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
823
967
  const name = interfaceDecl.name.text;
824
968
  const fields = [];
825
969
  const typeRegistry = {};
970
+ const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
826
971
  const visiting = /* @__PURE__ */ new Set();
827
972
  for (const member of interfaceDecl.members) {
828
973
  if (ts4.isPropertySignature(member)) {
@@ -833,7 +978,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
833
978
  }
834
979
  }
835
980
  const fieldLayouts = fields.map(() => ({}));
836
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
981
+ return {
982
+ name,
983
+ fields,
984
+ fieldLayouts,
985
+ typeRegistry,
986
+ ...annotations.length > 0 && { annotations },
987
+ instanceMethods: [],
988
+ staticMethods: []
989
+ };
837
990
  }
838
991
  function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
839
992
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
@@ -848,6 +1001,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
848
1001
  const name = typeAlias.name.text;
849
1002
  const fields = [];
850
1003
  const typeRegistry = {};
1004
+ const annotations = extractJSDocAnnotationNodes(typeAlias, file);
851
1005
  const visiting = /* @__PURE__ */ new Set();
852
1006
  for (const member of typeAlias.type.members) {
853
1007
  if (ts4.isPropertySignature(member)) {
@@ -864,6 +1018,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
864
1018
  fields,
865
1019
  fieldLayouts: fields.map(() => ({})),
866
1020
  typeRegistry,
1021
+ ...annotations.length > 0 && { annotations },
867
1022
  instanceMethods: [],
868
1023
  staticMethods: []
869
1024
  }
@@ -877,18 +1032,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
877
1032
  const tsType = checker.getTypeAtLocation(prop);
878
1033
  const optional = prop.questionToken !== void 0;
879
1034
  const provenance = provenanceForNode(prop, file);
880
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1035
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
881
1036
  const constraints = [];
882
1037
  if (prop.type) {
883
1038
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
884
1039
  }
885
1040
  constraints.push(...extractJSDocConstraintNodes(prop, file));
886
- const annotations = [];
1041
+ let annotations = [];
887
1042
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
888
1043
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
889
- if (defaultAnnotation) {
1044
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
890
1045
  annotations.push(defaultAnnotation);
891
1046
  }
1047
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
892
1048
  return {
893
1049
  kind: "field",
894
1050
  name,
@@ -907,14 +1063,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
907
1063
  const tsType = checker.getTypeAtLocation(prop);
908
1064
  const optional = prop.questionToken !== void 0;
909
1065
  const provenance = provenanceForNode(prop, file);
910
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1066
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
911
1067
  const constraints = [];
912
1068
  if (prop.type) {
913
1069
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
914
1070
  }
915
1071
  constraints.push(...extractJSDocConstraintNodes(prop, file));
916
- const annotations = [];
1072
+ let annotations = [];
917
1073
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1074
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
918
1075
  return {
919
1076
  kind: "field",
920
1077
  name,
@@ -925,7 +1082,69 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
925
1082
  provenance
926
1083
  };
927
1084
  }
928
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1085
+ function applyEnumMemberDisplayNames(type, annotations) {
1086
+ if (!annotations.some(
1087
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
1088
+ )) {
1089
+ return { type, annotations: [...annotations] };
1090
+ }
1091
+ const consumed = /* @__PURE__ */ new Set();
1092
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
1093
+ if (consumed.size === 0) {
1094
+ return { type, annotations: [...annotations] };
1095
+ }
1096
+ return {
1097
+ type: nextType,
1098
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
1099
+ };
1100
+ }
1101
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
1102
+ switch (type.kind) {
1103
+ case "enum":
1104
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
1105
+ case "union": {
1106
+ return {
1107
+ ...type,
1108
+ members: type.members.map(
1109
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
1110
+ )
1111
+ };
1112
+ }
1113
+ default:
1114
+ return type;
1115
+ }
1116
+ }
1117
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
1118
+ const displayNames = /* @__PURE__ */ new Map();
1119
+ for (const annotation of annotations) {
1120
+ if (annotation.annotationKind !== "displayName") continue;
1121
+ const parsed = parseEnumMemberDisplayName(annotation.value);
1122
+ if (!parsed) continue;
1123
+ consumed.add(annotation);
1124
+ const member = type.members.find((m) => String(m.value) === parsed.value);
1125
+ if (!member) continue;
1126
+ displayNames.set(String(member.value), parsed.label);
1127
+ }
1128
+ if (displayNames.size === 0) {
1129
+ return type;
1130
+ }
1131
+ return {
1132
+ ...type,
1133
+ members: type.members.map((member) => {
1134
+ const displayName = displayNames.get(String(member.value));
1135
+ return displayName !== void 0 ? { ...member, displayName } : member;
1136
+ })
1137
+ };
1138
+ }
1139
+ function parseEnumMemberDisplayName(value) {
1140
+ const trimmed = value.trim();
1141
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1142
+ if (!match?.[1] || !match[2]) return null;
1143
+ const label = match[2].trim();
1144
+ if (label === "") return null;
1145
+ return { value: match[1], label };
1146
+ }
1147
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
929
1148
  if (type.flags & ts4.TypeFlags.String) {
930
1149
  return { kind: "primitive", primitiveKind: "string" };
931
1150
  }
@@ -954,7 +1173,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
954
1173
  };
955
1174
  }
956
1175
  if (type.isUnion()) {
957
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1176
+ return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
958
1177
  }
959
1178
  if (checker.isArrayType(type)) {
960
1179
  return resolveArrayType(type, checker, file, typeRegistry, visiting);
@@ -964,70 +1183,102 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
964
1183
  }
965
1184
  return { kind: "primitive", primitiveKind: "string" };
966
1185
  }
967
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
1186
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
1187
+ const typeName = getNamedTypeName(type);
1188
+ const namedDecl = getNamedTypeDeclaration(type);
1189
+ if (typeName && typeName in typeRegistry) {
1190
+ return { kind: "reference", name: typeName, typeArguments: [] };
1191
+ }
968
1192
  const allTypes = type.types;
969
1193
  const nonNullTypes = allTypes.filter(
970
1194
  (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
971
1195
  );
972
1196
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1197
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1198
+ if (namedDecl) {
1199
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
1200
+ memberDisplayNames.set(value, label);
1201
+ }
1202
+ }
1203
+ if (sourceNode) {
1204
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
1205
+ memberDisplayNames.set(value, label);
1206
+ }
1207
+ }
1208
+ const registerNamed = (result) => {
1209
+ if (!typeName) {
1210
+ return result;
1211
+ }
1212
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1213
+ typeRegistry[typeName] = {
1214
+ name: typeName,
1215
+ type: result,
1216
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1217
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
1218
+ };
1219
+ return { kind: "reference", name: typeName, typeArguments: [] };
1220
+ };
1221
+ const applyMemberLabels = (members2) => members2.map((value) => {
1222
+ const displayName = memberDisplayNames.get(String(value));
1223
+ return displayName !== void 0 ? { value, displayName } : { value };
1224
+ });
973
1225
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
974
1226
  if (isBooleanUnion2) {
975
1227
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
976
- if (hasNull) {
977
- return {
978
- kind: "union",
979
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
980
- };
981
- }
982
- return boolNode;
1228
+ const result = hasNull ? {
1229
+ kind: "union",
1230
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1231
+ } : boolNode;
1232
+ return registerNamed(result);
983
1233
  }
984
1234
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
985
1235
  if (allStringLiterals && nonNullTypes.length > 0) {
986
1236
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
987
1237
  const enumNode = {
988
1238
  kind: "enum",
989
- members: stringTypes.map((t) => ({ value: t.value }))
1239
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
990
1240
  };
991
- if (hasNull) {
992
- return {
993
- kind: "union",
994
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
995
- };
996
- }
997
- return enumNode;
1241
+ const result = hasNull ? {
1242
+ kind: "union",
1243
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1244
+ } : enumNode;
1245
+ return registerNamed(result);
998
1246
  }
999
1247
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1000
1248
  if (allNumberLiterals && nonNullTypes.length > 0) {
1001
1249
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1002
1250
  const enumNode = {
1003
1251
  kind: "enum",
1004
- members: numberTypes.map((t) => ({ value: t.value }))
1252
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1005
1253
  };
1006
- if (hasNull) {
1007
- return {
1008
- kind: "union",
1009
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1010
- };
1011
- }
1012
- return enumNode;
1254
+ const result = hasNull ? {
1255
+ kind: "union",
1256
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1257
+ } : enumNode;
1258
+ return registerNamed(result);
1013
1259
  }
1014
1260
  if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1015
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1016
- if (hasNull) {
1017
- return {
1018
- kind: "union",
1019
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1020
- };
1021
- }
1022
- return inner;
1261
+ const inner = resolveTypeNode(
1262
+ nonNullTypes[0],
1263
+ checker,
1264
+ file,
1265
+ typeRegistry,
1266
+ visiting,
1267
+ sourceNode
1268
+ );
1269
+ const result = hasNull ? {
1270
+ kind: "union",
1271
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
1272
+ } : inner;
1273
+ return registerNamed(result);
1023
1274
  }
1024
1275
  const members = nonNullTypes.map(
1025
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1276
+ (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1026
1277
  );
1027
1278
  if (hasNull) {
1028
1279
  members.push({ kind: "primitive", primitiveKind: "null" });
1029
1280
  }
1030
- return { kind: "union", members };
1281
+ return registerNamed({ kind: "union", members });
1031
1282
  }
1032
1283
  function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1033
1284
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
@@ -1035,15 +1286,92 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1035
1286
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1036
1287
  return { kind: "array", items };
1037
1288
  }
1289
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1290
+ if (type.getProperties().length > 0) {
1291
+ return null;
1292
+ }
1293
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1294
+ if (!indexInfo) {
1295
+ return null;
1296
+ }
1297
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1298
+ return { kind: "record", valueType };
1299
+ }
1300
+ function typeNodeContainsReference(type, targetName) {
1301
+ switch (type.kind) {
1302
+ case "reference":
1303
+ return type.name === targetName;
1304
+ case "array":
1305
+ return typeNodeContainsReference(type.items, targetName);
1306
+ case "record":
1307
+ return typeNodeContainsReference(type.valueType, targetName);
1308
+ case "union":
1309
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
1310
+ case "object":
1311
+ return type.properties.some(
1312
+ (property) => typeNodeContainsReference(property.type, targetName)
1313
+ );
1314
+ case "primitive":
1315
+ case "enum":
1316
+ case "dynamic":
1317
+ case "custom":
1318
+ return false;
1319
+ default: {
1320
+ const _exhaustive = type;
1321
+ return _exhaustive;
1322
+ }
1323
+ }
1324
+ }
1038
1325
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1326
+ const typeName = getNamedTypeName(type);
1327
+ const namedTypeName = typeName ?? void 0;
1328
+ const namedDecl = getNamedTypeDeclaration(type);
1329
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
1330
+ const clearNamedTypeRegistration = () => {
1331
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
1332
+ return;
1333
+ }
1334
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
1335
+ };
1039
1336
  if (visiting.has(type)) {
1337
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1338
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1339
+ }
1040
1340
  return { kind: "object", properties: [], additionalProperties: false };
1041
1341
  }
1342
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
1343
+ typeRegistry[namedTypeName] = {
1344
+ name: namedTypeName,
1345
+ type: RESOLVING_TYPE_PLACEHOLDER,
1346
+ provenance: provenanceForDeclaration(namedDecl, file)
1347
+ };
1348
+ }
1042
1349
  visiting.add(type);
1043
- const typeName = getNamedTypeName(type);
1044
- if (typeName && typeName in typeRegistry) {
1350
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
1351
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
1352
+ visiting.delete(type);
1353
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1354
+ }
1355
+ }
1356
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1357
+ if (recordNode) {
1045
1358
  visiting.delete(type);
1046
- return { kind: "reference", name: typeName, typeArguments: [] };
1359
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1360
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
1361
+ if (!isRecursiveRecord) {
1362
+ clearNamedTypeRegistration();
1363
+ return recordNode;
1364
+ }
1365
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1366
+ typeRegistry[namedTypeName] = {
1367
+ name: namedTypeName,
1368
+ type: recordNode,
1369
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1370
+ provenance: provenanceForDeclaration(namedDecl, file)
1371
+ };
1372
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1373
+ }
1374
+ return recordNode;
1047
1375
  }
1048
1376
  const properties = [];
1049
1377
  const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
@@ -1052,7 +1380,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1052
1380
  if (!declaration) continue;
1053
1381
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1054
1382
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1055
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1383
+ const propTypeNode = resolveTypeNode(
1384
+ propType,
1385
+ checker,
1386
+ file,
1387
+ typeRegistry,
1388
+ visiting,
1389
+ declaration
1390
+ );
1056
1391
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1057
1392
  properties.push({
1058
1393
  name: prop.name,
@@ -1067,15 +1402,17 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1067
1402
  const objectNode = {
1068
1403
  kind: "object",
1069
1404
  properties,
1070
- additionalProperties: false
1405
+ additionalProperties: true
1071
1406
  };
1072
- if (typeName) {
1073
- typeRegistry[typeName] = {
1074
- name: typeName,
1407
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1408
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1409
+ typeRegistry[namedTypeName] = {
1410
+ name: namedTypeName,
1075
1411
  type: objectNode,
1076
- provenance: provenanceForFile(file)
1412
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1413
+ provenance: provenanceForDeclaration(namedDecl, file)
1077
1414
  };
1078
- return { kind: "reference", name: typeName, typeArguments: [] };
1415
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1079
1416
  }
1080
1417
  return objectNode;
1081
1418
  }
@@ -1167,6 +1504,12 @@ function provenanceForNode(node, file) {
1167
1504
  function provenanceForFile(file) {
1168
1505
  return { surface: "tsdoc", file, line: 0, column: 0 };
1169
1506
  }
1507
+ function provenanceForDeclaration(node, file) {
1508
+ if (!node) {
1509
+ return provenanceForFile(file);
1510
+ }
1511
+ return provenanceForNode(node, file);
1512
+ }
1170
1513
  function getNamedTypeName(type) {
1171
1514
  const symbol = type.getSymbol();
1172
1515
  if (symbol?.declarations) {
@@ -1185,6 +1528,20 @@ function getNamedTypeName(type) {
1185
1528
  }
1186
1529
  return null;
1187
1530
  }
1531
+ function getNamedTypeDeclaration(type) {
1532
+ const symbol = type.getSymbol();
1533
+ if (symbol?.declarations) {
1534
+ const decl = symbol.declarations[0];
1535
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
1536
+ return decl;
1537
+ }
1538
+ }
1539
+ const aliasSymbol = type.aliasSymbol;
1540
+ if (aliasSymbol?.declarations) {
1541
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
1542
+ }
1543
+ return void 0;
1544
+ }
1188
1545
  function analyzeMethod(method, checker) {
1189
1546
  if (!ts4.isIdentifier(method.name)) {
1190
1547
  return null;
@@ -1227,13 +1584,26 @@ function detectFormSpecReference(typeNode) {
1227
1584
  }
1228
1585
 
1229
1586
  // src/json-schema/ir-generator.ts
1230
- function makeContext() {
1231
- return { defs: {} };
1587
+ function makeContext(options) {
1588
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
1589
+ if (!vendorPrefix.startsWith("x-")) {
1590
+ throw new Error(
1591
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
1592
+ );
1593
+ }
1594
+ return {
1595
+ defs: {},
1596
+ extensionRegistry: options?.extensionRegistry,
1597
+ vendorPrefix
1598
+ };
1232
1599
  }
1233
- function generateJsonSchemaFromIR(ir) {
1234
- const ctx = makeContext();
1600
+ function generateJsonSchemaFromIR(ir, options) {
1601
+ const ctx = makeContext(options);
1235
1602
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1236
1603
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1604
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
1605
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
1606
+ }
1237
1607
  }
1238
1608
  const properties = {};
1239
1609
  const required = [];
@@ -1245,6 +1615,9 @@ function generateJsonSchemaFromIR(ir) {
1245
1615
  properties,
1246
1616
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1247
1617
  };
1618
+ if (ir.annotations && ir.annotations.length > 0) {
1619
+ applyAnnotations(result, ir.annotations, ctx);
1620
+ }
1248
1621
  if (Object.keys(ctx.defs).length > 0) {
1249
1622
  result.$defs = ctx.defs;
1250
1623
  }
@@ -1274,25 +1647,54 @@ function collectFields(elements, properties, required, ctx) {
1274
1647
  }
1275
1648
  function generateFieldSchema(field, ctx) {
1276
1649
  const schema = generateTypeNode(field.type, ctx);
1650
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1277
1651
  const directConstraints = [];
1652
+ const itemConstraints = [];
1278
1653
  const pathConstraints = [];
1279
1654
  for (const c of field.constraints) {
1280
1655
  if (c.path) {
1281
1656
  pathConstraints.push(c);
1657
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
1658
+ itemConstraints.push(c);
1282
1659
  } else {
1283
1660
  directConstraints.push(c);
1284
1661
  }
1285
1662
  }
1286
- applyConstraints(schema, directConstraints);
1287
- applyAnnotations(schema, field.annotations);
1663
+ applyConstraints(schema, directConstraints, ctx);
1664
+ if (itemStringSchema !== void 0) {
1665
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
1666
+ }
1667
+ const rootAnnotations = [];
1668
+ const itemAnnotations = [];
1669
+ for (const annotation of field.annotations) {
1670
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
1671
+ itemAnnotations.push(annotation);
1672
+ } else {
1673
+ rootAnnotations.push(annotation);
1674
+ }
1675
+ }
1676
+ applyAnnotations(schema, rootAnnotations, ctx);
1677
+ if (itemStringSchema !== void 0) {
1678
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
1679
+ }
1288
1680
  if (pathConstraints.length === 0) {
1289
1681
  return schema;
1290
1682
  }
1291
- return applyPathTargetedConstraints(schema, pathConstraints);
1683
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1292
1684
  }
1293
- function applyPathTargetedConstraints(schema, pathConstraints) {
1685
+ function isStringItemConstraint(constraint) {
1686
+ switch (constraint.constraintKind) {
1687
+ case "minLength":
1688
+ case "maxLength":
1689
+ case "pattern":
1690
+ return true;
1691
+ default:
1692
+ return false;
1693
+ }
1694
+ }
1695
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1294
1696
  if (schema.type === "array" && schema.items) {
1295
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1697
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1296
1698
  return schema;
1297
1699
  }
1298
1700
  const byTarget = /* @__PURE__ */ new Map();
@@ -1306,7 +1708,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
1306
1708
  const propertyOverrides = {};
1307
1709
  for (const [target, constraints] of byTarget) {
1308
1710
  const subSchema = {};
1309
- applyConstraints(subSchema, constraints);
1711
+ applyConstraints(subSchema, constraints, ctx);
1310
1712
  propertyOverrides[target] = subSchema;
1311
1713
  }
1312
1714
  if (schema.$ref) {
@@ -1350,6 +1752,8 @@ function generateTypeNode(type, ctx) {
1350
1752
  return generateArrayType(type, ctx);
1351
1753
  case "object":
1352
1754
  return generateObjectType(type, ctx);
1755
+ case "record":
1756
+ return generateRecordType(type, ctx);
1353
1757
  case "union":
1354
1758
  return generateUnionType(type, ctx);
1355
1759
  case "reference":
@@ -1357,7 +1761,7 @@ function generateTypeNode(type, ctx) {
1357
1761
  case "dynamic":
1358
1762
  return generateDynamicType(type);
1359
1763
  case "custom":
1360
- return generateCustomType(type);
1764
+ return generateCustomType(type, ctx);
1361
1765
  default: {
1362
1766
  const _exhaustive = type;
1363
1767
  return _exhaustive;
@@ -1406,16 +1810,27 @@ function generateObjectType(type, ctx) {
1406
1810
  }
1407
1811
  return schema;
1408
1812
  }
1813
+ function generateRecordType(type, ctx) {
1814
+ return {
1815
+ type: "object",
1816
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1817
+ };
1818
+ }
1409
1819
  function generatePropertySchema(prop, ctx) {
1410
1820
  const schema = generateTypeNode(prop.type, ctx);
1411
- applyConstraints(schema, prop.constraints);
1412
- applyAnnotations(schema, prop.annotations);
1821
+ applyConstraints(schema, prop.constraints, ctx);
1822
+ applyAnnotations(schema, prop.annotations, ctx);
1413
1823
  return schema;
1414
1824
  }
1415
1825
  function generateUnionType(type, ctx) {
1416
1826
  if (isBooleanUnion(type)) {
1417
1827
  return { type: "boolean" };
1418
1828
  }
1829
+ if (isNullableUnion(type)) {
1830
+ return {
1831
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1832
+ };
1833
+ }
1419
1834
  return {
1420
1835
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1421
1836
  };
@@ -1425,6 +1840,13 @@ function isBooleanUnion(type) {
1425
1840
  const kinds = type.members.map((m) => m.kind);
1426
1841
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1427
1842
  }
1843
+ function isNullableUnion(type) {
1844
+ if (type.members.length !== 2) return false;
1845
+ const nullCount = type.members.filter(
1846
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
1847
+ ).length;
1848
+ return nullCount === 1;
1849
+ }
1428
1850
  function generateReferenceType(type) {
1429
1851
  return { $ref: `#/$defs/${type.name}` };
1430
1852
  }
@@ -1445,10 +1867,7 @@ function generateDynamicType(type) {
1445
1867
  "x-formspec-schemaSource": type.sourceKey
1446
1868
  };
1447
1869
  }
1448
- function generateCustomType(_type) {
1449
- return { type: "object" };
1450
- }
1451
- function applyConstraints(schema, constraints) {
1870
+ function applyConstraints(schema, constraints, ctx) {
1452
1871
  for (const constraint of constraints) {
1453
1872
  switch (constraint.constraintKind) {
1454
1873
  case "minimum":
@@ -1490,9 +1909,13 @@ function applyConstraints(schema, constraints) {
1490
1909
  case "uniqueItems":
1491
1910
  schema.uniqueItems = constraint.value;
1492
1911
  break;
1912
+ case "const":
1913
+ schema.const = constraint.value;
1914
+ break;
1493
1915
  case "allowedMembers":
1494
1916
  break;
1495
1917
  case "custom":
1918
+ applyCustomConstraint(schema, constraint, ctx);
1496
1919
  break;
1497
1920
  default: {
1498
1921
  const _exhaustive = constraint;
@@ -1501,7 +1924,7 @@ function applyConstraints(schema, constraints) {
1501
1924
  }
1502
1925
  }
1503
1926
  }
1504
- function applyAnnotations(schema, annotations) {
1927
+ function applyAnnotations(schema, annotations, ctx) {
1505
1928
  for (const annotation of annotations) {
1506
1929
  switch (annotation.annotationKind) {
1507
1930
  case "displayName":
@@ -1513,14 +1936,21 @@ function applyAnnotations(schema, annotations) {
1513
1936
  case "defaultValue":
1514
1937
  schema.default = annotation.value;
1515
1938
  break;
1939
+ case "format":
1940
+ schema.format = annotation.value;
1941
+ break;
1516
1942
  case "deprecated":
1517
1943
  schema.deprecated = true;
1944
+ if (annotation.message !== void 0 && annotation.message !== "") {
1945
+ schema["x-formspec-deprecation-description"] = annotation.message;
1946
+ }
1518
1947
  break;
1519
1948
  case "placeholder":
1520
1949
  break;
1521
1950
  case "formatHint":
1522
1951
  break;
1523
1952
  case "custom":
1953
+ applyCustomAnnotation(schema, annotation, ctx);
1524
1954
  break;
1525
1955
  default: {
1526
1956
  const _exhaustive = annotation;
@@ -1529,6 +1959,36 @@ function applyAnnotations(schema, annotations) {
1529
1959
  }
1530
1960
  }
1531
1961
  }
1962
+ function generateCustomType(type, ctx) {
1963
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
1964
+ if (registration === void 0) {
1965
+ throw new Error(
1966
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
1967
+ );
1968
+ }
1969
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1970
+ }
1971
+ function applyCustomConstraint(schema, constraint, ctx) {
1972
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1973
+ if (registration === void 0) {
1974
+ throw new Error(
1975
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1976
+ );
1977
+ }
1978
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
1979
+ }
1980
+ function applyCustomAnnotation(schema, annotation, ctx) {
1981
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
1982
+ if (registration === void 0) {
1983
+ throw new Error(
1984
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
1985
+ );
1986
+ }
1987
+ if (registration.toJsonSchema === void 0) {
1988
+ return;
1989
+ }
1990
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
1991
+ }
1532
1992
 
1533
1993
  // src/ui-schema/schema.ts
1534
1994
  import { z } from "zod";
@@ -1665,25 +2125,31 @@ function createShowRule(fieldName, value) {
1665
2125
  }
1666
2126
  };
1667
2127
  }
2128
+ function flattenConditionSchema(scope, schema) {
2129
+ if (schema.allOf === void 0) {
2130
+ if (scope === "#") {
2131
+ return [schema];
2132
+ }
2133
+ const fieldName = scope.replace("#/properties/", "");
2134
+ return [
2135
+ {
2136
+ properties: {
2137
+ [fieldName]: schema
2138
+ }
2139
+ }
2140
+ ];
2141
+ }
2142
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
2143
+ }
1668
2144
  function combineRules(parentRule, childRule) {
1669
- const parentCondition = parentRule.condition;
1670
- const childCondition = childRule.condition;
1671
2145
  return {
1672
2146
  effect: "SHOW",
1673
2147
  condition: {
1674
2148
  scope: "#",
1675
2149
  schema: {
1676
2150
  allOf: [
1677
- {
1678
- properties: {
1679
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1680
- }
1681
- },
1682
- {
1683
- properties: {
1684
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1685
- }
1686
- }
2151
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2152
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1687
2153
  ]
1688
2154
  }
1689
2155
  }
@@ -1691,10 +2157,14 @@ function combineRules(parentRule, childRule) {
1691
2157
  }
1692
2158
  function fieldNodeToControl(field, parentRule) {
1693
2159
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2160
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1694
2161
  const control = {
1695
2162
  type: "Control",
1696
2163
  scope: fieldToScope(field.name),
1697
2164
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2165
+ ...placeholderAnnotation !== void 0 && {
2166
+ options: { placeholder: placeholderAnnotation.value }
2167
+ },
1698
2168
  ...parentRule !== void 0 && { rule: parentRule }
1699
2169
  };
1700
2170
  return control;
@@ -1753,12 +2223,9 @@ function generateClassSchemas(analysis, source) {
1753
2223
  }
1754
2224
 
1755
2225
  // src/validate/constraint-validator.ts
1756
- function makeCode(ctx, category, number) {
1757
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1758
- }
1759
2226
  function addContradiction(ctx, message, primary, related) {
1760
2227
  ctx.diagnostics.push({
1761
- code: makeCode(ctx, "CONTRADICTION", 1),
2228
+ code: "CONTRADICTING_CONSTRAINTS",
1762
2229
  message,
1763
2230
  severity: "error",
1764
2231
  primaryLocation: primary,
@@ -1767,7 +2234,7 @@ function addContradiction(ctx, message, primary, related) {
1767
2234
  }
1768
2235
  function addTypeMismatch(ctx, message, primary) {
1769
2236
  ctx.diagnostics.push({
1770
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
2237
+ code: "TYPE_MISMATCH",
1771
2238
  message,
1772
2239
  severity: "error",
1773
2240
  primaryLocation: primary,
@@ -1776,13 +2243,31 @@ function addTypeMismatch(ctx, message, primary) {
1776
2243
  }
1777
2244
  function addUnknownExtension(ctx, message, primary) {
1778
2245
  ctx.diagnostics.push({
1779
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
2246
+ code: "UNKNOWN_EXTENSION",
1780
2247
  message,
1781
2248
  severity: "warning",
1782
2249
  primaryLocation: primary,
1783
2250
  relatedLocations: []
1784
2251
  });
1785
2252
  }
2253
+ function addUnknownPathTarget(ctx, message, primary) {
2254
+ ctx.diagnostics.push({
2255
+ code: "UNKNOWN_PATH_TARGET",
2256
+ message,
2257
+ severity: "error",
2258
+ primaryLocation: primary,
2259
+ relatedLocations: []
2260
+ });
2261
+ }
2262
+ function addConstraintBroadening(ctx, message, primary, related) {
2263
+ ctx.diagnostics.push({
2264
+ code: "CONSTRAINT_BROADENING",
2265
+ message,
2266
+ severity: "error",
2267
+ primaryLocation: primary,
2268
+ relatedLocations: [related]
2269
+ });
2270
+ }
1786
2271
  function findNumeric(constraints, constraintKind) {
1787
2272
  return constraints.find((c) => c.constraintKind === constraintKind);
1788
2273
  }
@@ -1794,6 +2279,165 @@ function findAllowedMembers(constraints) {
1794
2279
  (c) => c.constraintKind === "allowedMembers"
1795
2280
  );
1796
2281
  }
2282
+ function findConstConstraints(constraints) {
2283
+ return constraints.filter(
2284
+ (c) => c.constraintKind === "const"
2285
+ );
2286
+ }
2287
+ function jsonValueEquals(left, right) {
2288
+ if (left === right) {
2289
+ return true;
2290
+ }
2291
+ if (Array.isArray(left) || Array.isArray(right)) {
2292
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2293
+ return false;
2294
+ }
2295
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
2296
+ }
2297
+ if (isJsonObject(left) || isJsonObject(right)) {
2298
+ if (!isJsonObject(left) || !isJsonObject(right)) {
2299
+ return false;
2300
+ }
2301
+ const leftKeys = Object.keys(left).sort();
2302
+ const rightKeys = Object.keys(right).sort();
2303
+ if (leftKeys.length !== rightKeys.length) {
2304
+ return false;
2305
+ }
2306
+ return leftKeys.every((key, index) => {
2307
+ const rightKey = rightKeys[index];
2308
+ if (rightKey !== key) {
2309
+ return false;
2310
+ }
2311
+ const leftValue = left[key];
2312
+ const rightValue = right[rightKey];
2313
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2314
+ });
2315
+ }
2316
+ return false;
2317
+ }
2318
+ function isJsonObject(value) {
2319
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2320
+ }
2321
+ function isOrderedBoundConstraint(constraint) {
2322
+ 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";
2323
+ }
2324
+ function pathKey(constraint) {
2325
+ return constraint.path?.segments.join(".") ?? "";
2326
+ }
2327
+ function orderedBoundFamily(kind) {
2328
+ switch (kind) {
2329
+ case "minimum":
2330
+ case "exclusiveMinimum":
2331
+ return "numeric-lower";
2332
+ case "maximum":
2333
+ case "exclusiveMaximum":
2334
+ return "numeric-upper";
2335
+ case "minLength":
2336
+ return "minLength";
2337
+ case "minItems":
2338
+ return "minItems";
2339
+ case "maxLength":
2340
+ return "maxLength";
2341
+ case "maxItems":
2342
+ return "maxItems";
2343
+ default: {
2344
+ const _exhaustive = kind;
2345
+ return _exhaustive;
2346
+ }
2347
+ }
2348
+ }
2349
+ function isNumericLowerKind(kind) {
2350
+ return kind === "minimum" || kind === "exclusiveMinimum";
2351
+ }
2352
+ function isNumericUpperKind(kind) {
2353
+ return kind === "maximum" || kind === "exclusiveMaximum";
2354
+ }
2355
+ function describeConstraintTag(constraint) {
2356
+ return `@${constraint.constraintKind}`;
2357
+ }
2358
+ function compareConstraintStrength(current, previous) {
2359
+ const family = orderedBoundFamily(current.constraintKind);
2360
+ if (family === "numeric-lower") {
2361
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
2362
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
2363
+ }
2364
+ if (current.value !== previous.value) {
2365
+ return current.value > previous.value ? 1 : -1;
2366
+ }
2367
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
2368
+ return 1;
2369
+ }
2370
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
2371
+ return -1;
2372
+ }
2373
+ return 0;
2374
+ }
2375
+ if (family === "numeric-upper") {
2376
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
2377
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
2378
+ }
2379
+ if (current.value !== previous.value) {
2380
+ return current.value < previous.value ? 1 : -1;
2381
+ }
2382
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2383
+ return 1;
2384
+ }
2385
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2386
+ return -1;
2387
+ }
2388
+ return 0;
2389
+ }
2390
+ switch (family) {
2391
+ case "minLength":
2392
+ case "minItems":
2393
+ if (current.value === previous.value) {
2394
+ return 0;
2395
+ }
2396
+ return current.value > previous.value ? 1 : -1;
2397
+ case "maxLength":
2398
+ case "maxItems":
2399
+ if (current.value === previous.value) {
2400
+ return 0;
2401
+ }
2402
+ return current.value < previous.value ? 1 : -1;
2403
+ default: {
2404
+ const _exhaustive = family;
2405
+ return _exhaustive;
2406
+ }
2407
+ }
2408
+ }
2409
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
2410
+ const strongestByKey = /* @__PURE__ */ new Map();
2411
+ for (const constraint of constraints) {
2412
+ if (!isOrderedBoundConstraint(constraint)) {
2413
+ continue;
2414
+ }
2415
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2416
+ const previous = strongestByKey.get(key);
2417
+ if (previous === void 0) {
2418
+ strongestByKey.set(key, constraint);
2419
+ continue;
2420
+ }
2421
+ const strength = compareConstraintStrength(constraint, previous);
2422
+ if (strength < 0) {
2423
+ const displayFieldName = formatPathTargetFieldName(
2424
+ fieldName,
2425
+ constraint.path?.segments ?? []
2426
+ );
2427
+ addConstraintBroadening(
2428
+ ctx,
2429
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2430
+ constraint.provenance,
2431
+ previous.provenance
2432
+ );
2433
+ continue;
2434
+ }
2435
+ if (strength <= 0) {
2436
+ continue;
2437
+ }
2438
+ strongestByKey.set(key, constraint);
2439
+ }
2440
+ }
1797
2441
  function checkNumericContradictions(ctx, fieldName, constraints) {
1798
2442
  const min = findNumeric(constraints, "minimum");
1799
2443
  const max = findNumeric(constraints, "maximum");
@@ -1880,6 +2524,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
1880
2524
  }
1881
2525
  }
1882
2526
  }
2527
+ function checkConstContradictions(ctx, fieldName, constraints) {
2528
+ const constConstraints = findConstConstraints(constraints);
2529
+ if (constConstraints.length < 2) return;
2530
+ const first = constConstraints[0];
2531
+ if (first === void 0) return;
2532
+ for (let i = 1; i < constConstraints.length; i++) {
2533
+ const current = constConstraints[i];
2534
+ if (current === void 0) continue;
2535
+ if (jsonValueEquals(first.value, current.value)) {
2536
+ continue;
2537
+ }
2538
+ addContradiction(
2539
+ ctx,
2540
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
2541
+ first.provenance,
2542
+ current.provenance
2543
+ );
2544
+ }
2545
+ }
1883
2546
  function typeLabel(type) {
1884
2547
  switch (type.kind) {
1885
2548
  case "primitive":
@@ -1890,6 +2553,8 @@ function typeLabel(type) {
1890
2553
  return "array";
1891
2554
  case "object":
1892
2555
  return "object";
2556
+ case "record":
2557
+ return "record";
1893
2558
  case "union":
1894
2559
  return "union";
1895
2560
  case "reference":
@@ -1904,85 +2569,173 @@ function typeLabel(type) {
1904
2569
  }
1905
2570
  }
1906
2571
  }
1907
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1908
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1909
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1910
- const isArray = type.kind === "array";
1911
- const isEnum = type.kind === "enum";
1912
- const label = typeLabel(type);
1913
- for (const constraint of constraints) {
1914
- if (constraint.path) {
1915
- const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1916
- if (!isTraversable) {
2572
+ function dereferenceType(ctx, type) {
2573
+ let current = type;
2574
+ const seen = /* @__PURE__ */ new Set();
2575
+ while (current.kind === "reference") {
2576
+ if (seen.has(current.name)) {
2577
+ return current;
2578
+ }
2579
+ seen.add(current.name);
2580
+ const definition = ctx.typeRegistry[current.name];
2581
+ if (definition === void 0) {
2582
+ return current;
2583
+ }
2584
+ current = definition.type;
2585
+ }
2586
+ return current;
2587
+ }
2588
+ function resolvePathTargetType(ctx, type, segments) {
2589
+ const effectiveType = dereferenceType(ctx, type);
2590
+ if (segments.length === 0) {
2591
+ return { kind: "resolved", type: effectiveType };
2592
+ }
2593
+ if (effectiveType.kind === "array") {
2594
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
2595
+ }
2596
+ if (effectiveType.kind === "object") {
2597
+ const [segment, ...rest] = segments;
2598
+ if (segment === void 0) {
2599
+ throw new Error("Invariant violation: object path traversal requires a segment");
2600
+ }
2601
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
2602
+ if (property === void 0) {
2603
+ return { kind: "missing-property", segment };
2604
+ }
2605
+ return resolvePathTargetType(ctx, property.type, rest);
2606
+ }
2607
+ return { kind: "unresolvable", type: effectiveType };
2608
+ }
2609
+ function formatPathTargetFieldName(fieldName, path2) {
2610
+ return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
2611
+ }
2612
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2613
+ const effectiveType = dereferenceType(ctx, type);
2614
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
2615
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2616
+ const isArray = effectiveType.kind === "array";
2617
+ const isEnum = effectiveType.kind === "enum";
2618
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
2619
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
2620
+ const label = typeLabel(effectiveType);
2621
+ const ck = constraint.constraintKind;
2622
+ switch (ck) {
2623
+ case "minimum":
2624
+ case "maximum":
2625
+ case "exclusiveMinimum":
2626
+ case "exclusiveMaximum":
2627
+ case "multipleOf": {
2628
+ if (!isNumber) {
1917
2629
  addTypeMismatch(
1918
2630
  ctx,
1919
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
2631
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1920
2632
  constraint.provenance
1921
2633
  );
1922
2634
  }
1923
- continue;
2635
+ break;
1924
2636
  }
1925
- const ck = constraint.constraintKind;
1926
- switch (ck) {
1927
- case "minimum":
1928
- case "maximum":
1929
- case "exclusiveMinimum":
1930
- case "exclusiveMaximum":
1931
- case "multipleOf": {
1932
- if (!isNumber) {
1933
- addTypeMismatch(
1934
- ctx,
1935
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1936
- constraint.provenance
1937
- );
1938
- }
1939
- break;
2637
+ case "minLength":
2638
+ case "maxLength":
2639
+ case "pattern": {
2640
+ if (!isString && !isStringArray) {
2641
+ addTypeMismatch(
2642
+ ctx,
2643
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2644
+ constraint.provenance
2645
+ );
1940
2646
  }
1941
- case "minLength":
1942
- case "maxLength":
1943
- case "pattern": {
1944
- if (!isString) {
1945
- addTypeMismatch(
1946
- ctx,
1947
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1948
- constraint.provenance
1949
- );
1950
- }
1951
- break;
2647
+ break;
2648
+ }
2649
+ case "minItems":
2650
+ case "maxItems":
2651
+ case "uniqueItems": {
2652
+ if (!isArray) {
2653
+ addTypeMismatch(
2654
+ ctx,
2655
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
2656
+ constraint.provenance
2657
+ );
1952
2658
  }
1953
- case "minItems":
1954
- case "maxItems":
1955
- case "uniqueItems": {
1956
- if (!isArray) {
1957
- addTypeMismatch(
1958
- ctx,
1959
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1960
- constraint.provenance
1961
- );
1962
- }
2659
+ break;
2660
+ }
2661
+ case "allowedMembers": {
2662
+ if (!isEnum) {
2663
+ addTypeMismatch(
2664
+ ctx,
2665
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2666
+ constraint.provenance
2667
+ );
2668
+ }
2669
+ break;
2670
+ }
2671
+ case "const": {
2672
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
2673
+ if (!isPrimitiveConstType) {
2674
+ addTypeMismatch(
2675
+ ctx,
2676
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
2677
+ constraint.provenance
2678
+ );
1963
2679
  break;
1964
2680
  }
1965
- case "allowedMembers": {
1966
- if (!isEnum) {
2681
+ if (effectiveType.kind === "primitive") {
2682
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
2683
+ if (valueType !== effectiveType.primitiveKind) {
1967
2684
  addTypeMismatch(
1968
2685
  ctx,
1969
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2686
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
1970
2687
  constraint.provenance
1971
2688
  );
1972
2689
  }
1973
2690
  break;
1974
2691
  }
1975
- case "custom": {
1976
- checkCustomConstraint(ctx, fieldName, type, constraint);
1977
- break;
2692
+ const memberValues = effectiveType.members.map((member) => member.value);
2693
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
2694
+ addTypeMismatch(
2695
+ ctx,
2696
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
2697
+ constraint.provenance
2698
+ );
1978
2699
  }
1979
- default: {
1980
- const _exhaustive = constraint;
1981
- throw new Error(
1982
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2700
+ break;
2701
+ }
2702
+ case "custom": {
2703
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2704
+ break;
2705
+ }
2706
+ default: {
2707
+ const _exhaustive = constraint;
2708
+ throw new Error(
2709
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2710
+ );
2711
+ }
2712
+ }
2713
+ }
2714
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2715
+ for (const constraint of constraints) {
2716
+ if (constraint.path) {
2717
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2718
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2719
+ if (resolution.kind === "missing-property") {
2720
+ addUnknownPathTarget(
2721
+ ctx,
2722
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2723
+ constraint.provenance
1983
2724
  );
2725
+ continue;
1984
2726
  }
2727
+ if (resolution.kind === "unresolvable") {
2728
+ addTypeMismatch(
2729
+ ctx,
2730
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
2731
+ constraint.provenance
2732
+ );
2733
+ continue;
2734
+ }
2735
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2736
+ continue;
1985
2737
  }
2738
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1986
2739
  }
1987
2740
  }
1988
2741
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -2026,6 +2779,8 @@ function validateConstraints(ctx, name, type, constraints) {
2026
2779
  checkNumericContradictions(ctx, name, constraints);
2027
2780
  checkLengthContradictions(ctx, name, constraints);
2028
2781
  checkAllowedMembersContradiction(ctx, name, constraints);
2782
+ checkConstContradictions(ctx, name, constraints);
2783
+ checkConstraintBroadening(ctx, name, constraints);
2029
2784
  checkTypeApplicability(ctx, name, type, constraints);
2030
2785
  }
2031
2786
  function validateElement(ctx, element) {
@@ -2052,8 +2807,8 @@ function validateElement(ctx, element) {
2052
2807
  function validateIR(ir, options) {
2053
2808
  const ctx = {
2054
2809
  diagnostics: [],
2055
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
2056
- extensionRegistry: options?.extensionRegistry
2810
+ extensionRegistry: options?.extensionRegistry,
2811
+ typeRegistry: ir.typeRegistry
2057
2812
  };
2058
2813
  for (const element of ir.elements) {
2059
2814
  validateElement(ctx, element);