@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/index.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
  }
@@ -401,6 +402,9 @@ function generateJsonSchemaFromIR(ir, options) {
401
402
  const ctx = makeContext(options);
402
403
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
403
404
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
405
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
406
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
407
+ }
404
408
  }
405
409
  const properties = {};
406
410
  const required = [];
@@ -412,6 +416,9 @@ function generateJsonSchemaFromIR(ir, options) {
412
416
  properties,
413
417
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
414
418
  };
419
+ if (ir.annotations && ir.annotations.length > 0) {
420
+ applyAnnotations(result, ir.annotations, ctx);
421
+ }
415
422
  if (Object.keys(ctx.defs).length > 0) {
416
423
  result.$defs = ctx.defs;
417
424
  }
@@ -441,22 +448,51 @@ function collectFields(elements, properties, required, ctx) {
441
448
  }
442
449
  function generateFieldSchema(field, ctx) {
443
450
  const schema = generateTypeNode(field.type, ctx);
451
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
444
452
  const directConstraints = [];
453
+ const itemConstraints = [];
445
454
  const pathConstraints = [];
446
455
  for (const c of field.constraints) {
447
456
  if (c.path) {
448
457
  pathConstraints.push(c);
458
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
459
+ itemConstraints.push(c);
449
460
  } else {
450
461
  directConstraints.push(c);
451
462
  }
452
463
  }
453
464
  applyConstraints(schema, directConstraints, ctx);
454
- applyAnnotations(schema, field.annotations, ctx);
465
+ if (itemStringSchema !== void 0) {
466
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
467
+ }
468
+ const rootAnnotations = [];
469
+ const itemAnnotations = [];
470
+ for (const annotation of field.annotations) {
471
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
472
+ itemAnnotations.push(annotation);
473
+ } else {
474
+ rootAnnotations.push(annotation);
475
+ }
476
+ }
477
+ applyAnnotations(schema, rootAnnotations, ctx);
478
+ if (itemStringSchema !== void 0) {
479
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
480
+ }
455
481
  if (pathConstraints.length === 0) {
456
482
  return schema;
457
483
  }
458
484
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
459
485
  }
486
+ function isStringItemConstraint(constraint) {
487
+ switch (constraint.constraintKind) {
488
+ case "minLength":
489
+ case "maxLength":
490
+ case "pattern":
491
+ return true;
492
+ default:
493
+ return false;
494
+ }
495
+ }
460
496
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
461
497
  if (schema.type === "array" && schema.items) {
462
498
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -674,6 +710,9 @@ function applyConstraints(schema, constraints, ctx) {
674
710
  case "uniqueItems":
675
711
  schema.uniqueItems = constraint.value;
676
712
  break;
713
+ case "const":
714
+ schema.const = constraint.value;
715
+ break;
677
716
  case "allowedMembers":
678
717
  break;
679
718
  case "custom":
@@ -698,8 +737,14 @@ function applyAnnotations(schema, annotations, ctx) {
698
737
  case "defaultValue":
699
738
  schema.default = annotation.value;
700
739
  break;
740
+ case "format":
741
+ schema.format = annotation.value;
742
+ break;
701
743
  case "deprecated":
702
744
  schema.deprecated = true;
745
+ if (annotation.message !== void 0 && annotation.message !== "") {
746
+ schema["x-formspec-deprecation-description"] = annotation.message;
747
+ }
703
748
  break;
704
749
  case "placeholder":
705
750
  break;
@@ -731,7 +776,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
731
776
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
732
777
  );
733
778
  }
734
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
779
+ assignVendorPrefixedExtensionKeywords(
780
+ schema,
781
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
782
+ ctx.vendorPrefix,
783
+ `custom constraint "${constraint.constraintId}"`
784
+ );
735
785
  }
736
786
  function applyCustomAnnotation(schema, annotation, ctx) {
737
787
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -743,7 +793,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
743
793
  if (registration.toJsonSchema === void 0) {
744
794
  return;
745
795
  }
746
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
796
+ assignVendorPrefixedExtensionKeywords(
797
+ schema,
798
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
799
+ ctx.vendorPrefix,
800
+ `custom annotation "${annotation.annotationId}"`
801
+ );
802
+ }
803
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
804
+ for (const [key, value] of Object.entries(extensionSchema)) {
805
+ if (!key.startsWith(`${vendorPrefix}-`)) {
806
+ throw new Error(
807
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
808
+ );
809
+ }
810
+ schema[key] = value;
811
+ }
747
812
  }
748
813
 
749
814
  // src/json-schema/generator.ts
@@ -887,25 +952,31 @@ function createShowRule(fieldName, value) {
887
952
  }
888
953
  };
889
954
  }
955
+ function flattenConditionSchema(scope, schema) {
956
+ if (schema.allOf === void 0) {
957
+ if (scope === "#") {
958
+ return [schema];
959
+ }
960
+ const fieldName = scope.replace("#/properties/", "");
961
+ return [
962
+ {
963
+ properties: {
964
+ [fieldName]: schema
965
+ }
966
+ }
967
+ ];
968
+ }
969
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
970
+ }
890
971
  function combineRules(parentRule, childRule) {
891
- const parentCondition = parentRule.condition;
892
- const childCondition = childRule.condition;
893
972
  return {
894
973
  effect: "SHOW",
895
974
  condition: {
896
975
  scope: "#",
897
976
  schema: {
898
977
  allOf: [
899
- {
900
- properties: {
901
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
902
- }
903
- },
904
- {
905
- properties: {
906
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
907
- }
908
- }
978
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
979
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
909
980
  ]
910
981
  }
911
982
  }
@@ -913,10 +984,14 @@ function combineRules(parentRule, childRule) {
913
984
  }
914
985
  function fieldNodeToControl(field, parentRule) {
915
986
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
987
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
916
988
  const control = {
917
989
  type: "Control",
918
990
  scope: fieldToScope(field.name),
919
991
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
992
+ ...placeholderAnnotation !== void 0 && {
993
+ options: { placeholder: placeholderAnnotation.value }
994
+ },
920
995
  ...parentRule !== void 0 && { rule: parentRule }
921
996
  };
922
997
  return control;
@@ -986,7 +1061,10 @@ function getSchemaExtension(schema, key) {
986
1061
  // src/extensions/registry.ts
987
1062
  function createExtensionRegistry(extensions) {
988
1063
  const typeMap = /* @__PURE__ */ new Map();
1064
+ const typeNameMap = /* @__PURE__ */ new Map();
989
1065
  const constraintMap = /* @__PURE__ */ new Map();
1066
+ const constraintTagMap = /* @__PURE__ */ new Map();
1067
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
990
1068
  const annotationMap = /* @__PURE__ */ new Map();
991
1069
  for (const ext of extensions) {
992
1070
  if (ext.types !== void 0) {
@@ -996,6 +1074,27 @@ function createExtensionRegistry(extensions) {
996
1074
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
997
1075
  }
998
1076
  typeMap.set(qualifiedId, type);
1077
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
1078
+ if (typeNameMap.has(sourceTypeName)) {
1079
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
1080
+ }
1081
+ typeNameMap.set(sourceTypeName, {
1082
+ extensionId: ext.extensionId,
1083
+ registration: type
1084
+ });
1085
+ }
1086
+ if (type.builtinConstraintBroadenings !== void 0) {
1087
+ for (const broadening of type.builtinConstraintBroadenings) {
1088
+ const key = `${qualifiedId}:${broadening.tagName}`;
1089
+ if (builtinBroadeningMap.has(key)) {
1090
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
1091
+ }
1092
+ builtinBroadeningMap.set(key, {
1093
+ extensionId: ext.extensionId,
1094
+ registration: broadening
1095
+ });
1096
+ }
1097
+ }
999
1098
  }
1000
1099
  }
1001
1100
  if (ext.constraints !== void 0) {
@@ -1007,6 +1106,17 @@ function createExtensionRegistry(extensions) {
1007
1106
  constraintMap.set(qualifiedId, constraint);
1008
1107
  }
1009
1108
  }
1109
+ if (ext.constraintTags !== void 0) {
1110
+ for (const tag of ext.constraintTags) {
1111
+ if (constraintTagMap.has(tag.tagName)) {
1112
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
1113
+ }
1114
+ constraintTagMap.set(tag.tagName, {
1115
+ extensionId: ext.extensionId,
1116
+ registration: tag
1117
+ });
1118
+ }
1119
+ }
1010
1120
  if (ext.annotations !== void 0) {
1011
1121
  for (const annotation of ext.annotations) {
1012
1122
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -1020,7 +1130,10 @@ function createExtensionRegistry(extensions) {
1020
1130
  return {
1021
1131
  extensions,
1022
1132
  findType: (typeId) => typeMap.get(typeId),
1133
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
1023
1134
  findConstraint: (constraintId) => constraintMap.get(constraintId),
1135
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
1136
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
1024
1137
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
1025
1138
  };
1026
1139
  }
@@ -1205,8 +1318,8 @@ var LENGTH_CONSTRAINT_MAP = {
1205
1318
  minItems: "minItems",
1206
1319
  maxItems: "maxItems"
1207
1320
  };
1208
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1209
- function createFormSpecTSDocConfig() {
1321
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
1322
+ function createFormSpecTSDocConfig(extensionTagNames = []) {
1210
1323
  const config = new TSDocConfiguration();
1211
1324
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
1212
1325
  config.addTagDefinition(
@@ -1217,7 +1330,16 @@ function createFormSpecTSDocConfig() {
1217
1330
  })
1218
1331
  );
1219
1332
  }
1220
- for (const tagName of ["displayName", "description"]) {
1333
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
1334
+ config.addTagDefinition(
1335
+ new TSDocTagDefinition({
1336
+ tagName: "@" + tagName,
1337
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
1338
+ allowMultiple: true
1339
+ })
1340
+ );
1341
+ }
1342
+ for (const tagName of extensionTagNames) {
1221
1343
  config.addTagDefinition(
1222
1344
  new TSDocTagDefinition({
1223
1345
  tagName: "@" + tagName,
@@ -1228,14 +1350,31 @@ function createFormSpecTSDocConfig() {
1228
1350
  }
1229
1351
  return config;
1230
1352
  }
1231
- var sharedParser;
1232
- function getParser() {
1233
- sharedParser ??= new TSDocParser(createFormSpecTSDocConfig());
1234
- return sharedParser;
1235
- }
1236
- function parseTSDocTags(node, file = "") {
1353
+ var parserCache = /* @__PURE__ */ new Map();
1354
+ function getParser(options) {
1355
+ const extensionTagNames = [
1356
+ ...options?.extensionRegistry?.extensions.flatMap(
1357
+ (extension) => (extension.constraintTags ?? []).map((tag) => tag.tagName)
1358
+ ) ?? []
1359
+ ].sort();
1360
+ const cacheKey = extensionTagNames.join("|");
1361
+ const existing = parserCache.get(cacheKey);
1362
+ if (existing) {
1363
+ return existing;
1364
+ }
1365
+ const parser = new TSDocParser(createFormSpecTSDocConfig(extensionTagNames));
1366
+ parserCache.set(cacheKey, parser);
1367
+ return parser;
1368
+ }
1369
+ function parseTSDocTags(node, file = "", options) {
1237
1370
  const constraints = [];
1238
1371
  const annotations = [];
1372
+ let displayName;
1373
+ let description;
1374
+ let placeholder;
1375
+ let displayNameProvenance;
1376
+ let descriptionProvenance;
1377
+ let placeholderProvenance;
1239
1378
  const sourceFile = node.getSourceFile();
1240
1379
  const sourceText = sourceFile.getFullText();
1241
1380
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -1248,52 +1387,92 @@ function parseTSDocTags(node, file = "") {
1248
1387
  if (!commentText.startsWith("/**")) {
1249
1388
  continue;
1250
1389
  }
1251
- const parser = getParser();
1390
+ const parser = getParser(options);
1252
1391
  const parserContext = parser.parseRange(
1253
1392
  TextRange.fromStringRange(sourceText, range.pos, range.end)
1254
1393
  );
1255
1394
  const docComment = parserContext.docComment;
1256
1395
  for (const block of docComment.customBlocks) {
1257
1396
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1258
- if (tagName === "displayName" || tagName === "description") {
1397
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
1259
1398
  const text2 = extractBlockText(block).trim();
1260
1399
  if (text2 === "") continue;
1261
1400
  const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
1262
1401
  if (tagName === "displayName") {
1402
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
1403
+ displayName = text2;
1404
+ displayNameProvenance = provenance2;
1405
+ }
1406
+ } else if (tagName === "format") {
1263
1407
  annotations.push({
1264
1408
  kind: "annotation",
1265
- annotationKind: "displayName",
1409
+ annotationKind: "format",
1266
1410
  value: text2,
1267
1411
  provenance: provenance2
1268
1412
  });
1269
1413
  } else {
1270
- annotations.push({
1271
- kind: "annotation",
1272
- annotationKind: "description",
1273
- value: text2,
1274
- provenance: provenance2
1275
- });
1414
+ if (tagName === "description" && description === void 0) {
1415
+ description = text2;
1416
+ descriptionProvenance = provenance2;
1417
+ } else if (tagName === "placeholder" && placeholder === void 0) {
1418
+ placeholder = text2;
1419
+ placeholderProvenance = provenance2;
1420
+ }
1276
1421
  }
1277
1422
  continue;
1278
1423
  }
1279
1424
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1280
1425
  const text = extractBlockText(block).trim();
1281
- if (text === "") continue;
1426
+ const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
1427
+ if (text === "" && expectedType !== "boolean") continue;
1282
1428
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
1283
- const constraintNode = parseConstraintValue(tagName, text, provenance);
1429
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
1284
1430
  if (constraintNode) {
1285
1431
  constraints.push(constraintNode);
1286
1432
  }
1287
1433
  }
1288
1434
  if (docComment.deprecatedBlock !== void 0) {
1435
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
1289
1436
  annotations.push({
1290
1437
  kind: "annotation",
1291
1438
  annotationKind: "deprecated",
1439
+ ...message !== "" && { message },
1292
1440
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
1293
1441
  });
1294
1442
  }
1443
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
1444
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
1445
+ if (remarks !== "") {
1446
+ description = remarks;
1447
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
1448
+ }
1449
+ }
1295
1450
  }
1296
1451
  }
1452
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
1453
+ annotations.push({
1454
+ kind: "annotation",
1455
+ annotationKind: "displayName",
1456
+ value: displayName,
1457
+ provenance: displayNameProvenance
1458
+ });
1459
+ }
1460
+ if (description !== void 0 && descriptionProvenance !== void 0) {
1461
+ annotations.push({
1462
+ kind: "annotation",
1463
+ annotationKind: "description",
1464
+ value: description,
1465
+ provenance: descriptionProvenance
1466
+ });
1467
+ }
1468
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
1469
+ annotations.push({
1470
+ kind: "annotation",
1471
+ annotationKind: "placeholder",
1472
+ value: placeholder,
1473
+ provenance: placeholderProvenance
1474
+ });
1475
+ }
1297
1476
  const jsDocTagsAll = ts2.getJSDocTags(node);
1298
1477
  for (const tag of jsDocTagsAll) {
1299
1478
  const tagName = normalizeConstraintTagName(tag.tagName.text);
@@ -1302,13 +1481,40 @@ function parseTSDocTags(node, file = "") {
1302
1481
  if (commentText === void 0 || commentText.trim() === "") continue;
1303
1482
  const text = commentText.trim();
1304
1483
  const provenance = provenanceForJSDocTag(tag, file);
1305
- const constraintNode = parseConstraintValue(tagName, text, provenance);
1484
+ if (tagName === "defaultValue") {
1485
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
1486
+ annotations.push(defaultValueNode);
1487
+ continue;
1488
+ }
1489
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
1306
1490
  if (constraintNode) {
1307
1491
  constraints.push(constraintNode);
1308
1492
  }
1309
1493
  }
1310
1494
  return { constraints, annotations };
1311
1495
  }
1496
+ function extractDisplayNameMetadata(node) {
1497
+ let displayName;
1498
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1499
+ for (const tag of ts2.getJSDocTags(node)) {
1500
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
1501
+ if (tagName !== "displayName") continue;
1502
+ const commentText = getTagCommentText(tag);
1503
+ if (commentText === void 0) continue;
1504
+ const text = commentText.trim();
1505
+ if (text === "") continue;
1506
+ const memberTarget = parseMemberTargetDisplayName(text);
1507
+ if (memberTarget) {
1508
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
1509
+ continue;
1510
+ }
1511
+ displayName ??= text;
1512
+ }
1513
+ return {
1514
+ ...displayName !== void 0 && { displayName },
1515
+ memberDisplayNames
1516
+ };
1517
+ }
1312
1518
  function extractPathTarget(text) {
1313
1519
  const trimmed = text.trimStart();
1314
1520
  const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
@@ -1336,7 +1542,11 @@ function extractPlainText(node) {
1336
1542
  }
1337
1543
  return result;
1338
1544
  }
1339
- function parseConstraintValue(tagName, text, provenance) {
1545
+ function parseConstraintValue(tagName, text, provenance, options) {
1546
+ const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
1547
+ if (customConstraint) {
1548
+ return customConstraint;
1549
+ }
1340
1550
  if (!isBuiltinConstraintName(tagName)) {
1341
1551
  return null;
1342
1552
  }
@@ -1371,7 +1581,45 @@ function parseConstraintValue(tagName, text, provenance) {
1371
1581
  }
1372
1582
  return null;
1373
1583
  }
1584
+ if (expectedType === "boolean") {
1585
+ const trimmed = effectiveText.trim();
1586
+ if (trimmed !== "" && trimmed !== "true") {
1587
+ return null;
1588
+ }
1589
+ if (tagName === "uniqueItems") {
1590
+ return {
1591
+ kind: "constraint",
1592
+ constraintKind: "uniqueItems",
1593
+ value: true,
1594
+ ...path3 && { path: path3 },
1595
+ provenance
1596
+ };
1597
+ }
1598
+ return null;
1599
+ }
1374
1600
  if (expectedType === "json") {
1601
+ if (tagName === "const") {
1602
+ const trimmedText = effectiveText.trim();
1603
+ if (trimmedText === "") return null;
1604
+ try {
1605
+ const parsed2 = JSON.parse(trimmedText);
1606
+ return {
1607
+ kind: "constraint",
1608
+ constraintKind: "const",
1609
+ value: parsed2,
1610
+ ...path3 && { path: path3 },
1611
+ provenance
1612
+ };
1613
+ } catch {
1614
+ return {
1615
+ kind: "constraint",
1616
+ constraintKind: "const",
1617
+ value: trimmedText,
1618
+ ...path3 && { path: path3 },
1619
+ provenance
1620
+ };
1621
+ }
1622
+ }
1375
1623
  const parsed = tryParseJson(effectiveText);
1376
1624
  if (!Array.isArray(parsed)) {
1377
1625
  return null;
@@ -1403,6 +1651,111 @@ function parseConstraintValue(tagName, text, provenance) {
1403
1651
  provenance
1404
1652
  };
1405
1653
  }
1654
+ function parseExtensionConstraintValue(tagName, text, provenance, options) {
1655
+ const pathResult = extractPathTarget(text);
1656
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1657
+ const path3 = pathResult?.path;
1658
+ const registry = options?.extensionRegistry;
1659
+ if (registry === void 0) {
1660
+ return null;
1661
+ }
1662
+ const directTag = registry.findConstraintTag(tagName);
1663
+ if (directTag !== void 0) {
1664
+ return makeCustomConstraintNode(
1665
+ directTag.extensionId,
1666
+ directTag.registration.constraintName,
1667
+ directTag.registration.parseValue(effectiveText),
1668
+ provenance,
1669
+ path3,
1670
+ registry
1671
+ );
1672
+ }
1673
+ if (!isBuiltinConstraintName(tagName)) {
1674
+ return null;
1675
+ }
1676
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
1677
+ if (broadenedTypeId === void 0) {
1678
+ return null;
1679
+ }
1680
+ const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
1681
+ if (broadened === void 0) {
1682
+ return null;
1683
+ }
1684
+ return makeCustomConstraintNode(
1685
+ broadened.extensionId,
1686
+ broadened.registration.constraintName,
1687
+ broadened.registration.parseValue(effectiveText),
1688
+ provenance,
1689
+ path3,
1690
+ registry
1691
+ );
1692
+ }
1693
+ function getBroadenedCustomTypeId(fieldType) {
1694
+ if (fieldType?.kind === "custom") {
1695
+ return fieldType.typeId;
1696
+ }
1697
+ if (fieldType?.kind !== "union") {
1698
+ return void 0;
1699
+ }
1700
+ const customMembers = fieldType.members.filter(
1701
+ (member) => member.kind === "custom"
1702
+ );
1703
+ if (customMembers.length !== 1) {
1704
+ return void 0;
1705
+ }
1706
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
1707
+ const allOtherMembersAreNull = nonCustomMembers.every(
1708
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
1709
+ );
1710
+ const customMember = customMembers[0];
1711
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
1712
+ }
1713
+ function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path3, registry) {
1714
+ const constraintId = `${extensionId}/${constraintName}`;
1715
+ const registration = registry.findConstraint(constraintId);
1716
+ if (registration === void 0) {
1717
+ throw new Error(
1718
+ `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
1719
+ );
1720
+ }
1721
+ return {
1722
+ kind: "constraint",
1723
+ constraintKind: "custom",
1724
+ constraintId,
1725
+ payload,
1726
+ compositionRule: registration.compositionRule,
1727
+ ...path3 && { path: path3 },
1728
+ provenance
1729
+ };
1730
+ }
1731
+ function parseDefaultValueValue(text, provenance) {
1732
+ const trimmed = text.trim();
1733
+ let value;
1734
+ if (trimmed === "null") {
1735
+ value = null;
1736
+ } else if (trimmed === "true") {
1737
+ value = true;
1738
+ } else if (trimmed === "false") {
1739
+ value = false;
1740
+ } else {
1741
+ const parsed = tryParseJson(trimmed);
1742
+ value = parsed !== null ? parsed : trimmed;
1743
+ }
1744
+ return {
1745
+ kind: "annotation",
1746
+ annotationKind: "defaultValue",
1747
+ value,
1748
+ provenance
1749
+ };
1750
+ }
1751
+ function isMemberTargetDisplayName(text) {
1752
+ return parseMemberTargetDisplayName(text) !== null;
1753
+ }
1754
+ function parseMemberTargetDisplayName(text) {
1755
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
1756
+ if (!match?.[1] || !match[2]) return null;
1757
+ return { target: match[1], label: match[2].trim() };
1758
+ }
1406
1759
  function provenanceForComment(range, sourceFile, file, tagName) {
1407
1760
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
1408
1761
  return {
@@ -1435,12 +1788,12 @@ function getTagCommentText(tag) {
1435
1788
  }
1436
1789
 
1437
1790
  // src/analyzer/jsdoc-constraints.ts
1438
- function extractJSDocConstraintNodes(node, file = "") {
1439
- const result = parseTSDocTags(node, file);
1791
+ function extractJSDocConstraintNodes(node, file = "", options) {
1792
+ const result = parseTSDocTags(node, file, options);
1440
1793
  return [...result.constraints];
1441
1794
  }
1442
- function extractJSDocAnnotationNodes(node, file = "") {
1443
- const result = parseTSDocTags(node, file);
1795
+ function extractJSDocAnnotationNodes(node, file = "", options) {
1796
+ const result = parseTSDocTags(node, file, options);
1444
1797
  return [...result.annotations];
1445
1798
  }
1446
1799
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -1484,17 +1837,43 @@ function isObjectType(type) {
1484
1837
  function isTypeReference(type) {
1485
1838
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
1486
1839
  }
1487
- function analyzeClassToIR(classDecl, checker, file = "") {
1840
+ var RESOLVING_TYPE_PLACEHOLDER = {
1841
+ kind: "object",
1842
+ properties: [],
1843
+ additionalProperties: true
1844
+ };
1845
+ function makeParseOptions(extensionRegistry, fieldType) {
1846
+ if (extensionRegistry === void 0 && fieldType === void 0) {
1847
+ return void 0;
1848
+ }
1849
+ return {
1850
+ ...extensionRegistry !== void 0 && { extensionRegistry },
1851
+ ...fieldType !== void 0 && { fieldType }
1852
+ };
1853
+ }
1854
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1488
1855
  const name = classDecl.name?.text ?? "AnonymousClass";
1489
1856
  const fields = [];
1490
1857
  const fieldLayouts = [];
1491
1858
  const typeRegistry = {};
1859
+ const annotations = extractJSDocAnnotationNodes(
1860
+ classDecl,
1861
+ file,
1862
+ makeParseOptions(extensionRegistry)
1863
+ );
1492
1864
  const visiting = /* @__PURE__ */ new Set();
1493
1865
  const instanceMethods = [];
1494
1866
  const staticMethods = [];
1495
1867
  for (const member of classDecl.members) {
1496
1868
  if (ts4.isPropertyDeclaration(member)) {
1497
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1869
+ const fieldNode = analyzeFieldToIR(
1870
+ member,
1871
+ checker,
1872
+ file,
1873
+ typeRegistry,
1874
+ visiting,
1875
+ extensionRegistry
1876
+ );
1498
1877
  if (fieldNode) {
1499
1878
  fields.push(fieldNode);
1500
1879
  fieldLayouts.push({});
@@ -1511,25 +1890,53 @@ function analyzeClassToIR(classDecl, checker, file = "") {
1511
1890
  }
1512
1891
  }
1513
1892
  }
1514
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
1893
+ return {
1894
+ name,
1895
+ fields,
1896
+ fieldLayouts,
1897
+ typeRegistry,
1898
+ ...annotations.length > 0 && { annotations },
1899
+ instanceMethods,
1900
+ staticMethods
1901
+ };
1515
1902
  }
1516
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1903
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
1517
1904
  const name = interfaceDecl.name.text;
1518
1905
  const fields = [];
1519
1906
  const typeRegistry = {};
1907
+ const annotations = extractJSDocAnnotationNodes(
1908
+ interfaceDecl,
1909
+ file,
1910
+ makeParseOptions(extensionRegistry)
1911
+ );
1520
1912
  const visiting = /* @__PURE__ */ new Set();
1521
1913
  for (const member of interfaceDecl.members) {
1522
1914
  if (ts4.isPropertySignature(member)) {
1523
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1915
+ const fieldNode = analyzeInterfacePropertyToIR(
1916
+ member,
1917
+ checker,
1918
+ file,
1919
+ typeRegistry,
1920
+ visiting,
1921
+ extensionRegistry
1922
+ );
1524
1923
  if (fieldNode) {
1525
1924
  fields.push(fieldNode);
1526
1925
  }
1527
1926
  }
1528
1927
  }
1529
1928
  const fieldLayouts = fields.map(() => ({}));
1530
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
1929
+ return {
1930
+ name,
1931
+ fields,
1932
+ fieldLayouts,
1933
+ typeRegistry,
1934
+ ...annotations.length > 0 && { annotations },
1935
+ instanceMethods: [],
1936
+ staticMethods: []
1937
+ };
1531
1938
  }
1532
- function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1939
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
1533
1940
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
1534
1941
  const sourceFile = typeAlias.getSourceFile();
1535
1942
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -1542,10 +1949,22 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1542
1949
  const name = typeAlias.name.text;
1543
1950
  const fields = [];
1544
1951
  const typeRegistry = {};
1952
+ const annotations = extractJSDocAnnotationNodes(
1953
+ typeAlias,
1954
+ file,
1955
+ makeParseOptions(extensionRegistry)
1956
+ );
1545
1957
  const visiting = /* @__PURE__ */ new Set();
1546
1958
  for (const member of typeAlias.type.members) {
1547
1959
  if (ts4.isPropertySignature(member)) {
1548
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1960
+ const fieldNode = analyzeInterfacePropertyToIR(
1961
+ member,
1962
+ checker,
1963
+ file,
1964
+ typeRegistry,
1965
+ visiting,
1966
+ extensionRegistry
1967
+ );
1549
1968
  if (fieldNode) {
1550
1969
  fields.push(fieldNode);
1551
1970
  }
@@ -1558,12 +1977,13 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1558
1977
  fields,
1559
1978
  fieldLayouts: fields.map(() => ({})),
1560
1979
  typeRegistry,
1980
+ ...annotations.length > 0 && { annotations },
1561
1981
  instanceMethods: [],
1562
1982
  staticMethods: []
1563
1983
  }
1564
1984
  };
1565
1985
  }
1566
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1986
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1567
1987
  if (!ts4.isIdentifier(prop.name)) {
1568
1988
  return null;
1569
1989
  }
@@ -1571,16 +1991,28 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1571
1991
  const tsType = checker.getTypeAtLocation(prop);
1572
1992
  const optional = prop.questionToken !== void 0;
1573
1993
  const provenance = provenanceForNode(prop, file);
1574
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1994
+ let type = resolveTypeNode(
1995
+ tsType,
1996
+ checker,
1997
+ file,
1998
+ typeRegistry,
1999
+ visiting,
2000
+ prop,
2001
+ extensionRegistry
2002
+ );
1575
2003
  const constraints = [];
1576
2004
  if (prop.type) {
1577
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
2005
+ constraints.push(
2006
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
2007
+ );
1578
2008
  }
1579
- constraints.push(...extractJSDocConstraintNodes(prop, file));
2009
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1580
2010
  let annotations = [];
1581
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
2011
+ annotations.push(
2012
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
2013
+ );
1582
2014
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1583
- if (defaultAnnotation) {
2015
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
1584
2016
  annotations.push(defaultAnnotation);
1585
2017
  }
1586
2018
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
@@ -1594,7 +2026,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1594
2026
  provenance
1595
2027
  };
1596
2028
  }
1597
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
2029
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1598
2030
  if (!ts4.isIdentifier(prop.name)) {
1599
2031
  return null;
1600
2032
  }
@@ -1602,14 +2034,26 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1602
2034
  const tsType = checker.getTypeAtLocation(prop);
1603
2035
  const optional = prop.questionToken !== void 0;
1604
2036
  const provenance = provenanceForNode(prop, file);
1605
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
2037
+ let type = resolveTypeNode(
2038
+ tsType,
2039
+ checker,
2040
+ file,
2041
+ typeRegistry,
2042
+ visiting,
2043
+ prop,
2044
+ extensionRegistry
2045
+ );
1606
2046
  const constraints = [];
1607
2047
  if (prop.type) {
1608
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
2048
+ constraints.push(
2049
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
2050
+ );
1609
2051
  }
1610
- constraints.push(...extractJSDocConstraintNodes(prop, file));
2052
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1611
2053
  let annotations = [];
1612
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
2054
+ annotations.push(
2055
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
2056
+ );
1613
2057
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1614
2058
  return {
1615
2059
  kind: "field",
@@ -1683,7 +2127,66 @@ function parseEnumMemberDisplayName(value) {
1683
2127
  if (label === "") return null;
1684
2128
  return { value: match[1], label };
1685
2129
  }
1686
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
2130
+ function resolveRegisteredCustomType(sourceNode, extensionRegistry, checker) {
2131
+ if (sourceNode === void 0 || extensionRegistry === void 0) {
2132
+ return null;
2133
+ }
2134
+ const typeNode = extractTypeNodeFromSource(sourceNode);
2135
+ if (typeNode === void 0) {
2136
+ return null;
2137
+ }
2138
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker);
2139
+ }
2140
+ function resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker) {
2141
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
2142
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode.type, extensionRegistry, checker);
2143
+ }
2144
+ const typeName = getTypeNodeRegistrationName(typeNode);
2145
+ if (typeName === null) {
2146
+ return null;
2147
+ }
2148
+ const registration = extensionRegistry.findTypeByName(typeName);
2149
+ if (registration !== void 0) {
2150
+ return {
2151
+ kind: "custom",
2152
+ typeId: `${registration.extensionId}/${registration.registration.typeName}`,
2153
+ payload: null
2154
+ };
2155
+ }
2156
+ if (ts4.isTypeReferenceNode(typeNode) && ts4.isIdentifier(typeNode.typeName)) {
2157
+ const aliasDecl = checker.getSymbolAtLocation(typeNode.typeName)?.declarations?.find(ts4.isTypeAliasDeclaration);
2158
+ if (aliasDecl !== void 0) {
2159
+ return resolveRegisteredCustomTypeFromTypeNode(aliasDecl.type, extensionRegistry, checker);
2160
+ }
2161
+ }
2162
+ return null;
2163
+ }
2164
+ function extractTypeNodeFromSource(sourceNode) {
2165
+ if (ts4.isPropertyDeclaration(sourceNode) || ts4.isPropertySignature(sourceNode) || ts4.isParameter(sourceNode) || ts4.isTypeAliasDeclaration(sourceNode)) {
2166
+ return sourceNode.type;
2167
+ }
2168
+ if (ts4.isTypeNode(sourceNode)) {
2169
+ return sourceNode;
2170
+ }
2171
+ return void 0;
2172
+ }
2173
+ function getTypeNodeRegistrationName(typeNode) {
2174
+ if (ts4.isTypeReferenceNode(typeNode)) {
2175
+ return ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.right.text;
2176
+ }
2177
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
2178
+ return getTypeNodeRegistrationName(typeNode.type);
2179
+ }
2180
+ if (typeNode.kind === ts4.SyntaxKind.BigIntKeyword || typeNode.kind === ts4.SyntaxKind.StringKeyword || typeNode.kind === ts4.SyntaxKind.NumberKeyword || typeNode.kind === ts4.SyntaxKind.BooleanKeyword) {
2181
+ return typeNode.getText();
2182
+ }
2183
+ return null;
2184
+ }
2185
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2186
+ const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
2187
+ if (customType) {
2188
+ return customType;
2189
+ }
1687
2190
  if (type.flags & ts4.TypeFlags.String) {
1688
2191
  return { kind: "primitive", primitiveKind: "string" };
1689
2192
  }
@@ -1712,88 +2215,162 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1712
2215
  };
1713
2216
  }
1714
2217
  if (type.isUnion()) {
1715
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
2218
+ return resolveUnionType(
2219
+ type,
2220
+ checker,
2221
+ file,
2222
+ typeRegistry,
2223
+ visiting,
2224
+ sourceNode,
2225
+ extensionRegistry
2226
+ );
1716
2227
  }
1717
2228
  if (checker.isArrayType(type)) {
1718
- return resolveArrayType(type, checker, file, typeRegistry, visiting);
2229
+ return resolveArrayType(
2230
+ type,
2231
+ checker,
2232
+ file,
2233
+ typeRegistry,
2234
+ visiting,
2235
+ sourceNode,
2236
+ extensionRegistry
2237
+ );
1719
2238
  }
1720
2239
  if (isObjectType(type)) {
1721
- return resolveObjectType(type, checker, file, typeRegistry, visiting);
2240
+ return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1722
2241
  }
1723
2242
  return { kind: "primitive", primitiveKind: "string" };
1724
2243
  }
1725
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
2244
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2245
+ const typeName = getNamedTypeName(type);
2246
+ const namedDecl = getNamedTypeDeclaration(type);
2247
+ if (typeName && typeName in typeRegistry) {
2248
+ return { kind: "reference", name: typeName, typeArguments: [] };
2249
+ }
1726
2250
  const allTypes = type.types;
2251
+ const unionMemberTypeNodes = extractUnionMemberTypeNodes(sourceNode, checker);
2252
+ const nonNullSourceNodes = unionMemberTypeNodes.filter(
2253
+ (memberTypeNode) => !isNullishTypeNode(resolveAliasedTypeNode(memberTypeNode, checker))
2254
+ );
1727
2255
  const nonNullTypes = allTypes.filter(
1728
- (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
2256
+ (memberType) => !(memberType.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1729
2257
  );
2258
+ const nonNullMembers = nonNullTypes.map((memberType, index) => ({
2259
+ memberType,
2260
+ sourceNode: nonNullSourceNodes.length === nonNullTypes.length ? nonNullSourceNodes[index] : void 0
2261
+ }));
1730
2262
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
2263
+ const memberDisplayNames = /* @__PURE__ */ new Map();
2264
+ if (namedDecl) {
2265
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
2266
+ memberDisplayNames.set(value, label);
2267
+ }
2268
+ }
2269
+ if (sourceNode) {
2270
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
2271
+ memberDisplayNames.set(value, label);
2272
+ }
2273
+ }
2274
+ const registerNamed = (result) => {
2275
+ if (!typeName) {
2276
+ return result;
2277
+ }
2278
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2279
+ typeRegistry[typeName] = {
2280
+ name: typeName,
2281
+ type: result,
2282
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
2283
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
2284
+ };
2285
+ return { kind: "reference", name: typeName, typeArguments: [] };
2286
+ };
2287
+ const applyMemberLabels = (members2) => members2.map((value) => {
2288
+ const displayName = memberDisplayNames.get(String(value));
2289
+ return displayName !== void 0 ? { value, displayName } : { value };
2290
+ });
1731
2291
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1732
2292
  if (isBooleanUnion2) {
1733
2293
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1734
- if (hasNull) {
1735
- return {
1736
- kind: "union",
1737
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1738
- };
1739
- }
1740
- return boolNode;
2294
+ const result = hasNull ? {
2295
+ kind: "union",
2296
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
2297
+ } : boolNode;
2298
+ return registerNamed(result);
1741
2299
  }
1742
2300
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1743
2301
  if (allStringLiterals && nonNullTypes.length > 0) {
1744
2302
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1745
2303
  const enumNode = {
1746
2304
  kind: "enum",
1747
- members: stringTypes.map((t) => ({ value: t.value }))
2305
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1748
2306
  };
1749
- if (hasNull) {
1750
- return {
1751
- kind: "union",
1752
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1753
- };
1754
- }
1755
- return enumNode;
2307
+ const result = hasNull ? {
2308
+ kind: "union",
2309
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
2310
+ } : enumNode;
2311
+ return registerNamed(result);
1756
2312
  }
1757
2313
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1758
2314
  if (allNumberLiterals && nonNullTypes.length > 0) {
1759
2315
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1760
2316
  const enumNode = {
1761
2317
  kind: "enum",
1762
- members: numberTypes.map((t) => ({ value: t.value }))
2318
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1763
2319
  };
1764
- if (hasNull) {
1765
- return {
1766
- kind: "union",
1767
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1768
- };
1769
- }
1770
- return enumNode;
1771
- }
1772
- if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1773
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1774
- if (hasNull) {
1775
- return {
1776
- kind: "union",
1777
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1778
- };
1779
- }
1780
- return inner;
1781
- }
1782
- const members = nonNullTypes.map(
1783
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
2320
+ const result = hasNull ? {
2321
+ kind: "union",
2322
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
2323
+ } : enumNode;
2324
+ return registerNamed(result);
2325
+ }
2326
+ if (nonNullMembers.length === 1 && nonNullMembers[0]) {
2327
+ const inner = resolveTypeNode(
2328
+ nonNullMembers[0].memberType,
2329
+ checker,
2330
+ file,
2331
+ typeRegistry,
2332
+ visiting,
2333
+ nonNullMembers[0].sourceNode ?? sourceNode,
2334
+ extensionRegistry
2335
+ );
2336
+ const result = hasNull ? {
2337
+ kind: "union",
2338
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
2339
+ } : inner;
2340
+ return registerNamed(result);
2341
+ }
2342
+ const members = nonNullMembers.map(
2343
+ ({ memberType, sourceNode: memberSourceNode }) => resolveTypeNode(
2344
+ memberType,
2345
+ checker,
2346
+ file,
2347
+ typeRegistry,
2348
+ visiting,
2349
+ memberSourceNode ?? sourceNode,
2350
+ extensionRegistry
2351
+ )
1784
2352
  );
1785
2353
  if (hasNull) {
1786
2354
  members.push({ kind: "primitive", primitiveKind: "null" });
1787
2355
  }
1788
- return { kind: "union", members };
2356
+ return registerNamed({ kind: "union", members });
1789
2357
  }
1790
- function resolveArrayType(type, checker, file, typeRegistry, visiting) {
2358
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1791
2359
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1792
2360
  const elementType = typeArgs?.[0];
1793
- const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
2361
+ const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
2362
+ const items = elementType ? resolveTypeNode(
2363
+ elementType,
2364
+ checker,
2365
+ file,
2366
+ typeRegistry,
2367
+ visiting,
2368
+ elementSourceNode,
2369
+ extensionRegistry
2370
+ ) : { kind: "primitive", primitiveKind: "string" };
1794
2371
  return { kind: "array", items };
1795
2372
  }
1796
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
2373
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1797
2374
  if (type.getProperties().length > 0) {
1798
2375
  return null;
1799
2376
  }
@@ -1801,39 +2378,123 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1801
2378
  if (!indexInfo) {
1802
2379
  return null;
1803
2380
  }
1804
- if (visiting.has(type)) {
1805
- return null;
1806
- }
1807
- visiting.add(type);
1808
- try {
1809
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1810
- return { kind: "record", valueType };
1811
- } finally {
1812
- visiting.delete(type);
1813
- }
2381
+ const valueType = resolveTypeNode(
2382
+ indexInfo.type,
2383
+ checker,
2384
+ file,
2385
+ typeRegistry,
2386
+ visiting,
2387
+ void 0,
2388
+ extensionRegistry
2389
+ );
2390
+ return { kind: "record", valueType };
1814
2391
  }
1815
- function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1816
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1817
- if (recordNode) {
1818
- return recordNode;
2392
+ function typeNodeContainsReference(type, targetName) {
2393
+ switch (type.kind) {
2394
+ case "reference":
2395
+ return type.name === targetName;
2396
+ case "array":
2397
+ return typeNodeContainsReference(type.items, targetName);
2398
+ case "record":
2399
+ return typeNodeContainsReference(type.valueType, targetName);
2400
+ case "union":
2401
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
2402
+ case "object":
2403
+ return type.properties.some(
2404
+ (property) => typeNodeContainsReference(property.type, targetName)
2405
+ );
2406
+ case "primitive":
2407
+ case "enum":
2408
+ case "dynamic":
2409
+ case "custom":
2410
+ return false;
2411
+ default: {
2412
+ const _exhaustive = type;
2413
+ return _exhaustive;
2414
+ }
1819
2415
  }
2416
+ }
2417
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2418
+ const typeName = getNamedTypeName(type);
2419
+ const namedTypeName = typeName ?? void 0;
2420
+ const namedDecl = getNamedTypeDeclaration(type);
2421
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2422
+ const clearNamedTypeRegistration = () => {
2423
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
2424
+ return;
2425
+ }
2426
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
2427
+ };
1820
2428
  if (visiting.has(type)) {
2429
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2430
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
2431
+ }
1821
2432
  return { kind: "object", properties: [], additionalProperties: false };
1822
2433
  }
2434
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2435
+ typeRegistry[namedTypeName] = {
2436
+ name: namedTypeName,
2437
+ type: RESOLVING_TYPE_PLACEHOLDER,
2438
+ provenance: provenanceForDeclaration(namedDecl, file)
2439
+ };
2440
+ }
1823
2441
  visiting.add(type);
1824
- const typeName = getNamedTypeName(type);
1825
- if (typeName && typeName in typeRegistry) {
2442
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2443
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2444
+ visiting.delete(type);
2445
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
2446
+ }
2447
+ }
2448
+ const recordNode = tryResolveRecordType(
2449
+ type,
2450
+ checker,
2451
+ file,
2452
+ typeRegistry,
2453
+ visiting,
2454
+ extensionRegistry
2455
+ );
2456
+ if (recordNode) {
1826
2457
  visiting.delete(type);
1827
- return { kind: "reference", name: typeName, typeArguments: [] };
2458
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2459
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
2460
+ if (!isRecursiveRecord) {
2461
+ clearNamedTypeRegistration();
2462
+ return recordNode;
2463
+ }
2464
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2465
+ typeRegistry[namedTypeName] = {
2466
+ name: namedTypeName,
2467
+ type: recordNode,
2468
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
2469
+ provenance: provenanceForDeclaration(namedDecl, file)
2470
+ };
2471
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
2472
+ }
2473
+ return recordNode;
1828
2474
  }
1829
2475
  const properties = [];
1830
- const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
2476
+ const fieldInfoMap = getNamedTypeFieldNodeInfoMap(
2477
+ type,
2478
+ checker,
2479
+ file,
2480
+ typeRegistry,
2481
+ visiting,
2482
+ extensionRegistry
2483
+ );
1831
2484
  for (const prop of type.getProperties()) {
1832
2485
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1833
2486
  if (!declaration) continue;
1834
2487
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1835
2488
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1836
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
2489
+ const propTypeNode = resolveTypeNode(
2490
+ propType,
2491
+ checker,
2492
+ file,
2493
+ typeRegistry,
2494
+ visiting,
2495
+ declaration,
2496
+ extensionRegistry
2497
+ );
1837
2498
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1838
2499
  properties.push({
1839
2500
  name: prop.name,
@@ -1850,17 +2511,19 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1850
2511
  properties,
1851
2512
  additionalProperties: true
1852
2513
  };
1853
- if (typeName) {
1854
- typeRegistry[typeName] = {
1855
- name: typeName,
2514
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2515
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2516
+ typeRegistry[namedTypeName] = {
2517
+ name: namedTypeName,
1856
2518
  type: objectNode,
1857
- provenance: provenanceForFile(file)
2519
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
2520
+ provenance: provenanceForDeclaration(namedDecl, file)
1858
2521
  };
1859
- return { kind: "reference", name: typeName, typeArguments: [] };
2522
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1860
2523
  }
1861
2524
  return objectNode;
1862
2525
  }
1863
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
2526
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1864
2527
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1865
2528
  (s) => s?.declarations != null && s.declarations.length > 0
1866
2529
  );
@@ -1872,7 +2535,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1872
2535
  const map = /* @__PURE__ */ new Map();
1873
2536
  for (const member of classDecl.members) {
1874
2537
  if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1875
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
2538
+ const fieldNode = analyzeFieldToIR(
2539
+ member,
2540
+ checker,
2541
+ file,
2542
+ typeRegistry,
2543
+ visiting,
2544
+ extensionRegistry
2545
+ );
1876
2546
  if (fieldNode) {
1877
2547
  map.set(fieldNode.name, {
1878
2548
  constraints: [...fieldNode.constraints],
@@ -1886,7 +2556,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1886
2556
  }
1887
2557
  const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1888
2558
  if (interfaceDecl) {
1889
- return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
2559
+ return buildFieldNodeInfoMap(
2560
+ interfaceDecl.members,
2561
+ checker,
2562
+ file,
2563
+ typeRegistry,
2564
+ visiting,
2565
+ extensionRegistry
2566
+ );
1890
2567
  }
1891
2568
  const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1892
2569
  if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
@@ -1895,17 +2572,68 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1895
2572
  checker,
1896
2573
  file,
1897
2574
  typeRegistry,
1898
- visiting
2575
+ visiting,
2576
+ extensionRegistry
1899
2577
  );
1900
2578
  }
1901
2579
  }
1902
2580
  return null;
1903
2581
  }
1904
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
2582
+ function extractArrayElementTypeNode(sourceNode, checker) {
2583
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2584
+ if (typeNode === void 0) {
2585
+ return void 0;
2586
+ }
2587
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2588
+ if (ts4.isArrayTypeNode(resolvedTypeNode)) {
2589
+ return resolvedTypeNode.elementType;
2590
+ }
2591
+ if (ts4.isTypeReferenceNode(resolvedTypeNode) && ts4.isIdentifier(resolvedTypeNode.typeName) && resolvedTypeNode.typeName.text === "Array" && resolvedTypeNode.typeArguments?.[0]) {
2592
+ return resolvedTypeNode.typeArguments[0];
2593
+ }
2594
+ return void 0;
2595
+ }
2596
+ function extractUnionMemberTypeNodes(sourceNode, checker) {
2597
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2598
+ if (!typeNode) {
2599
+ return [];
2600
+ }
2601
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2602
+ return ts4.isUnionTypeNode(resolvedTypeNode) ? [...resolvedTypeNode.types] : [];
2603
+ }
2604
+ function resolveAliasedTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
2605
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
2606
+ return resolveAliasedTypeNode(typeNode.type, checker, visited);
2607
+ }
2608
+ if (!ts4.isTypeReferenceNode(typeNode) || !ts4.isIdentifier(typeNode.typeName)) {
2609
+ return typeNode;
2610
+ }
2611
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
2612
+ const aliasDecl = symbol?.declarations?.find(ts4.isTypeAliasDeclaration);
2613
+ if (aliasDecl === void 0 || visited.has(aliasDecl)) {
2614
+ return typeNode;
2615
+ }
2616
+ visited.add(aliasDecl);
2617
+ return resolveAliasedTypeNode(aliasDecl.type, checker, visited);
2618
+ }
2619
+ function isNullishTypeNode(typeNode) {
2620
+ if (typeNode.kind === ts4.SyntaxKind.NullKeyword || typeNode.kind === ts4.SyntaxKind.UndefinedKeyword) {
2621
+ return true;
2622
+ }
2623
+ return ts4.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts4.SyntaxKind.NullKeyword || typeNode.literal.kind === ts4.SyntaxKind.UndefinedKeyword);
2624
+ }
2625
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
1905
2626
  const map = /* @__PURE__ */ new Map();
1906
2627
  for (const member of members) {
1907
2628
  if (ts4.isPropertySignature(member)) {
1908
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
2629
+ const fieldNode = analyzeInterfacePropertyToIR(
2630
+ member,
2631
+ checker,
2632
+ file,
2633
+ typeRegistry,
2634
+ visiting,
2635
+ extensionRegistry
2636
+ );
1909
2637
  if (fieldNode) {
1910
2638
  map.set(fieldNode.name, {
1911
2639
  constraints: [...fieldNode.constraints],
@@ -1918,7 +2646,7 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1918
2646
  return map;
1919
2647
  }
1920
2648
  var MAX_ALIAS_CHAIN_DEPTH = 8;
1921
- function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
2649
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegistry, depth = 0) {
1922
2650
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1923
2651
  if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1924
2652
  const aliasName = typeNode.typeName.getText();
@@ -1931,8 +2659,29 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1931
2659
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1932
2660
  if (!aliasDecl) return [];
1933
2661
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1934
- const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1935
- constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
2662
+ const aliasFieldType = resolveTypeNode(
2663
+ checker.getTypeAtLocation(aliasDecl.type),
2664
+ checker,
2665
+ file,
2666
+ {},
2667
+ /* @__PURE__ */ new Set(),
2668
+ aliasDecl.type,
2669
+ extensionRegistry
2670
+ );
2671
+ const constraints = extractJSDocConstraintNodes(
2672
+ aliasDecl,
2673
+ file,
2674
+ makeParseOptions(extensionRegistry, aliasFieldType)
2675
+ );
2676
+ constraints.push(
2677
+ ...extractTypeAliasConstraintNodes(
2678
+ aliasDecl.type,
2679
+ checker,
2680
+ file,
2681
+ extensionRegistry,
2682
+ depth + 1
2683
+ )
2684
+ );
1936
2685
  return constraints;
1937
2686
  }
1938
2687
  function provenanceForNode(node, file) {
@@ -1948,6 +2697,12 @@ function provenanceForNode(node, file) {
1948
2697
  function provenanceForFile(file) {
1949
2698
  return { surface: "tsdoc", file, line: 0, column: 0 };
1950
2699
  }
2700
+ function provenanceForDeclaration(node, file) {
2701
+ if (!node) {
2702
+ return provenanceForFile(file);
2703
+ }
2704
+ return provenanceForNode(node, file);
2705
+ }
1951
2706
  function getNamedTypeName(type) {
1952
2707
  const symbol = type.getSymbol();
1953
2708
  if (symbol?.declarations) {
@@ -1966,6 +2721,20 @@ function getNamedTypeName(type) {
1966
2721
  }
1967
2722
  return null;
1968
2723
  }
2724
+ function getNamedTypeDeclaration(type) {
2725
+ const symbol = type.getSymbol();
2726
+ if (symbol?.declarations) {
2727
+ const decl = symbol.declarations[0];
2728
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
2729
+ return decl;
2730
+ }
2731
+ }
2732
+ const aliasSymbol = type.aliasSymbol;
2733
+ if (aliasSymbol?.declarations) {
2734
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
2735
+ }
2736
+ return void 0;
2737
+ }
1969
2738
  function analyzeMethod(method, checker) {
1970
2739
  if (!ts4.isIdentifier(method.name)) {
1971
2740
  return null;
@@ -2008,10 +2777,10 @@ function detectFormSpecReference(typeNode) {
2008
2777
  }
2009
2778
 
2010
2779
  // src/generators/class-schema.ts
2011
- function generateClassSchemas(analysis, source) {
2780
+ function generateClassSchemas(analysis, source, options) {
2012
2781
  const ir = canonicalizeTSDoc(analysis, source);
2013
2782
  return {
2014
- jsonSchema: generateJsonSchemaFromIR(ir),
2783
+ jsonSchema: generateJsonSchemaFromIR(ir, options),
2015
2784
  uiSchema: generateUiSchemaFromIR(ir)
2016
2785
  };
2017
2786
  }
@@ -2021,27 +2790,54 @@ function generateSchemasFromClass(options) {
2021
2790
  if (!classDecl) {
2022
2791
  throw new Error(`Class "${options.className}" not found in ${options.filePath}`);
2023
2792
  }
2024
- const analysis = analyzeClassToIR(classDecl, ctx.checker, options.filePath);
2025
- return generateClassSchemas(analysis, { file: options.filePath });
2793
+ const analysis = analyzeClassToIR(
2794
+ classDecl,
2795
+ ctx.checker,
2796
+ options.filePath,
2797
+ options.extensionRegistry
2798
+ );
2799
+ return generateClassSchemas(
2800
+ analysis,
2801
+ { file: options.filePath },
2802
+ {
2803
+ extensionRegistry: options.extensionRegistry,
2804
+ vendorPrefix: options.vendorPrefix
2805
+ }
2806
+ );
2026
2807
  }
2027
2808
  function generateSchemas(options) {
2028
2809
  const ctx = createProgramContext(options.filePath);
2029
2810
  const source = { file: options.filePath };
2030
2811
  const classDecl = findClassByName(ctx.sourceFile, options.typeName);
2031
2812
  if (classDecl) {
2032
- const analysis = analyzeClassToIR(classDecl, ctx.checker, options.filePath);
2033
- return generateClassSchemas(analysis, source);
2813
+ const analysis = analyzeClassToIR(
2814
+ classDecl,
2815
+ ctx.checker,
2816
+ options.filePath,
2817
+ options.extensionRegistry
2818
+ );
2819
+ return generateClassSchemas(analysis, source, options);
2034
2820
  }
2035
2821
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, options.typeName);
2036
2822
  if (interfaceDecl) {
2037
- const analysis = analyzeInterfaceToIR(interfaceDecl, ctx.checker, options.filePath);
2038
- return generateClassSchemas(analysis, source);
2823
+ const analysis = analyzeInterfaceToIR(
2824
+ interfaceDecl,
2825
+ ctx.checker,
2826
+ options.filePath,
2827
+ options.extensionRegistry
2828
+ );
2829
+ return generateClassSchemas(analysis, source, options);
2039
2830
  }
2040
2831
  const typeAlias = findTypeAliasByName(ctx.sourceFile, options.typeName);
2041
2832
  if (typeAlias) {
2042
- const result = analyzeTypeAliasToIR(typeAlias, ctx.checker, options.filePath);
2833
+ const result = analyzeTypeAliasToIR(
2834
+ typeAlias,
2835
+ ctx.checker,
2836
+ options.filePath,
2837
+ options.extensionRegistry
2838
+ );
2043
2839
  if (result.ok) {
2044
- return generateClassSchemas(result.analysis, source);
2840
+ return generateClassSchemas(result.analysis, source, options);
2045
2841
  }
2046
2842
  throw new Error(result.error);
2047
2843
  }
@@ -2050,6 +2846,205 @@ function generateSchemas(options) {
2050
2846
  );
2051
2847
  }
2052
2848
 
2849
+ // src/generators/mixed-authoring.ts
2850
+ function buildMixedAuthoringSchemas(options) {
2851
+ const { filePath, typeName, overlays, ...schemaOptions } = options;
2852
+ const analysis = analyzeNamedType(filePath, typeName, schemaOptions.extensionRegistry);
2853
+ const composedAnalysis = composeAnalysisWithOverlays(analysis, overlays);
2854
+ const ir = canonicalizeTSDoc(composedAnalysis, { file: filePath });
2855
+ return {
2856
+ jsonSchema: generateJsonSchemaFromIR(ir, schemaOptions),
2857
+ uiSchema: generateUiSchemaFromIR(ir)
2858
+ };
2859
+ }
2860
+ function analyzeNamedType(filePath, typeName, extensionRegistry) {
2861
+ const ctx = createProgramContext(filePath);
2862
+ const source = { file: filePath };
2863
+ const classDecl = findClassByName(ctx.sourceFile, typeName);
2864
+ if (classDecl !== null) {
2865
+ return analyzeClassToIR(classDecl, ctx.checker, source.file, extensionRegistry);
2866
+ }
2867
+ const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
2868
+ if (interfaceDecl !== null) {
2869
+ return analyzeInterfaceToIR(interfaceDecl, ctx.checker, source.file, extensionRegistry);
2870
+ }
2871
+ const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
2872
+ if (typeAlias !== null) {
2873
+ const result = analyzeTypeAliasToIR(typeAlias, ctx.checker, source.file, extensionRegistry);
2874
+ if (result.ok) {
2875
+ return result.analysis;
2876
+ }
2877
+ throw new Error(result.error);
2878
+ }
2879
+ throw new Error(
2880
+ `Type "${typeName}" not found as a class, interface, or type alias in ${filePath}`
2881
+ );
2882
+ }
2883
+ function composeAnalysisWithOverlays(analysis, overlays) {
2884
+ const overlayIR = canonicalizeChainDSL(overlays);
2885
+ const overlayFields = collectOverlayFields(overlayIR.elements);
2886
+ if (overlayFields.length === 0) {
2887
+ return analysis;
2888
+ }
2889
+ const overlayByName = /* @__PURE__ */ new Map();
2890
+ for (const field of overlayFields) {
2891
+ if (overlayByName.has(field.name)) {
2892
+ throw new Error(`Mixed-authoring overlays define "${field.name}" more than once`);
2893
+ }
2894
+ overlayByName.set(field.name, field);
2895
+ }
2896
+ const mergedFields = [];
2897
+ for (const baseField of analysis.fields) {
2898
+ const overlayField = overlayByName.get(baseField.name);
2899
+ if (overlayField === void 0) {
2900
+ mergedFields.push(baseField);
2901
+ continue;
2902
+ }
2903
+ mergedFields.push(mergeFieldOverlay(baseField, overlayField, analysis.typeRegistry));
2904
+ overlayByName.delete(baseField.name);
2905
+ }
2906
+ if (overlayByName.size > 0) {
2907
+ const unknownFields = [...overlayByName.keys()].sort().join(", ");
2908
+ throw new Error(
2909
+ `Mixed-authoring overlays reference fields that are not present in the static model: ${unknownFields}`
2910
+ );
2911
+ }
2912
+ return {
2913
+ ...analysis,
2914
+ fields: mergedFields
2915
+ };
2916
+ }
2917
+ function collectOverlayFields(elements) {
2918
+ const fields = [];
2919
+ for (const element of elements) {
2920
+ switch (element.kind) {
2921
+ case "field":
2922
+ fields.push(element);
2923
+ break;
2924
+ case "group":
2925
+ fields.push(...collectOverlayFields(element.elements));
2926
+ break;
2927
+ case "conditional":
2928
+ fields.push(...collectOverlayFields(element.elements));
2929
+ break;
2930
+ default: {
2931
+ const _exhaustive = element;
2932
+ void _exhaustive;
2933
+ }
2934
+ }
2935
+ }
2936
+ return fields;
2937
+ }
2938
+ function mergeFieldOverlay(baseField, overlayField, typeRegistry) {
2939
+ assertSupportedOverlayField(baseField, overlayField);
2940
+ return {
2941
+ ...baseField,
2942
+ type: mergeFieldType(baseField, overlayField, typeRegistry),
2943
+ annotations: mergeAnnotations(baseField.annotations, overlayField.annotations)
2944
+ };
2945
+ }
2946
+ function assertSupportedOverlayField(baseField, overlayField) {
2947
+ if (overlayField.constraints.length > 0) {
2948
+ throw new Error(
2949
+ `Mixed-authoring overlay for "${baseField.name}" cannot define constraints; keep constraints on the static model`
2950
+ );
2951
+ }
2952
+ if (overlayField.required && !baseField.required) {
2953
+ throw new Error(
2954
+ `Mixed-authoring overlay for "${baseField.name}" cannot change requiredness; keep requiredness on the static model`
2955
+ );
2956
+ }
2957
+ }
2958
+ function mergeFieldType(baseField, overlayField, typeRegistry) {
2959
+ const { type: baseType } = baseField;
2960
+ const { type: overlayType } = overlayField;
2961
+ if (overlayType.kind === "object" || overlayType.kind === "array") {
2962
+ throw new Error(
2963
+ `Mixed-authoring overlays do not support nested object or array overlays for "${baseField.name}"`
2964
+ );
2965
+ }
2966
+ if (overlayType.kind === "dynamic") {
2967
+ if (!isCompatibleDynamicOverlay(baseField, overlayField, typeRegistry)) {
2968
+ throw new Error(
2969
+ `Mixed-authoring overlay for "${baseField.name}" is incompatible with the static field type`
2970
+ );
2971
+ }
2972
+ return overlayType;
2973
+ }
2974
+ if (!isSameStaticTypeShape(baseType, overlayType)) {
2975
+ throw new Error(
2976
+ `Mixed-authoring overlay for "${baseField.name}" must preserve the static field type`
2977
+ );
2978
+ }
2979
+ return baseType;
2980
+ }
2981
+ function isCompatibleDynamicOverlay(baseField, overlayField, typeRegistry) {
2982
+ const overlayType = overlayField.type;
2983
+ if (overlayType.kind !== "dynamic") {
2984
+ return false;
2985
+ }
2986
+ const resolvedBaseType = resolveReferenceType(baseField.type, typeRegistry);
2987
+ if (resolvedBaseType === null) {
2988
+ return false;
2989
+ }
2990
+ if (overlayType.dynamicKind === "enum") {
2991
+ return resolvedBaseType.kind === "primitive" ? resolvedBaseType.primitiveKind === "string" : resolvedBaseType.kind === "enum";
2992
+ }
2993
+ return resolvedBaseType.kind === "object" || resolvedBaseType.kind === "record";
2994
+ }
2995
+ function resolveReferenceType(type, typeRegistry, seen = /* @__PURE__ */ new Set()) {
2996
+ if (type.kind !== "reference") {
2997
+ return type;
2998
+ }
2999
+ if (seen.has(type.name)) {
3000
+ return null;
3001
+ }
3002
+ const definition = typeRegistry[type.name];
3003
+ if (definition === void 0) {
3004
+ return null;
3005
+ }
3006
+ seen.add(type.name);
3007
+ return resolveReferenceType(definition.type, typeRegistry, seen);
3008
+ }
3009
+ function isSameStaticTypeShape(baseType, overlayType) {
3010
+ if (baseType.kind !== overlayType.kind) {
3011
+ return false;
3012
+ }
3013
+ switch (baseType.kind) {
3014
+ case "primitive":
3015
+ return overlayType.kind === "primitive" && baseType.primitiveKind === overlayType.primitiveKind;
3016
+ case "enum":
3017
+ return overlayType.kind === "enum";
3018
+ case "dynamic":
3019
+ return overlayType.kind === "dynamic" && baseType.dynamicKind === overlayType.dynamicKind && baseType.sourceKey === overlayType.sourceKey;
3020
+ case "record":
3021
+ return overlayType.kind === "record";
3022
+ case "reference":
3023
+ return overlayType.kind === "reference" && baseType.name === overlayType.name;
3024
+ case "union":
3025
+ return overlayType.kind === "union";
3026
+ case "custom":
3027
+ return overlayType.kind === "custom" && baseType.typeId === overlayType.typeId;
3028
+ case "object":
3029
+ case "array":
3030
+ return true;
3031
+ default: {
3032
+ const _exhaustive = baseType;
3033
+ return _exhaustive;
3034
+ }
3035
+ }
3036
+ }
3037
+ function mergeAnnotations(baseAnnotations, overlayAnnotations) {
3038
+ const baseKeys = new Set(baseAnnotations.map(annotationKey));
3039
+ const overlayOnly = overlayAnnotations.filter(
3040
+ (annotation) => !baseKeys.has(annotationKey(annotation))
3041
+ );
3042
+ return [...baseAnnotations, ...overlayOnly];
3043
+ }
3044
+ function annotationKey(annotation) {
3045
+ return annotation.annotationKind === "custom" ? `${annotation.annotationKind}:${annotation.annotationId}` : annotation.annotationKind;
3046
+ }
3047
+
2053
3048
  // src/index.ts
2054
3049
  function buildFormSchemas(form, options) {
2055
3050
  return {
@@ -2075,6 +3070,7 @@ function writeSchemas(form, options) {
2075
3070
  }
2076
3071
  export {
2077
3072
  buildFormSchemas,
3073
+ buildMixedAuthoringSchemas,
2078
3074
  categorizationSchema,
2079
3075
  categorySchema,
2080
3076
  controlSchema,