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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +20 -20
  2. package/dist/__tests__/alias-chain-propagation.test.d.ts +9 -0
  3. package/dist/__tests__/alias-chain-propagation.test.d.ts.map +1 -0
  4. package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
  5. package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/alias-chains.d.ts +37 -0
  7. package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  9. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  10. package/dist/__tests__/fixtures/example-a-builtins.d.ts +13 -13
  11. package/dist/__tests__/fixtures/example-interface-types.d.ts +33 -33
  12. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
  13. package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -5
  14. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
  15. package/dist/__tests__/json-utils.test.d.ts +5 -0
  16. package/dist/__tests__/json-utils.test.d.ts.map +1 -0
  17. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  18. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  19. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  20. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  21. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  22. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  23. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  24. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  25. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  26. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  27. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  28. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  29. package/dist/__tests__/parity/utils.d.ts +6 -1
  30. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  31. package/dist/__tests__/path-target-parser.test.d.ts +9 -0
  32. package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
  33. package/dist/analyzer/class-analyzer.d.ts +1 -1
  34. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  35. package/dist/analyzer/jsdoc-constraints.d.ts +8 -52
  36. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  37. package/dist/analyzer/json-utils.d.ts +22 -0
  38. package/dist/analyzer/json-utils.d.ts.map +1 -0
  39. package/dist/analyzer/tsdoc-parser.d.ts +24 -12
  40. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  41. package/dist/browser.cjs +452 -94
  42. package/dist/browser.cjs.map +1 -1
  43. package/dist/browser.d.ts +15 -2
  44. package/dist/browser.d.ts.map +1 -1
  45. package/dist/browser.js +450 -94
  46. package/dist/browser.js.map +1 -1
  47. package/dist/build.d.ts +132 -5
  48. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  49. package/dist/cli.cjs +406 -104
  50. package/dist/cli.cjs.map +1 -1
  51. package/dist/cli.js +407 -104
  52. package/dist/cli.js.map +1 -1
  53. package/dist/index.cjs +386 -102
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.ts +20 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +386 -104
  58. package/dist/index.js.map +1 -1
  59. package/dist/internals.cjs +597 -172
  60. package/dist/internals.cjs.map +1 -1
  61. package/dist/internals.js +597 -172
  62. package/dist/internals.js.map +1 -1
  63. package/dist/json-schema/generator.d.ts +8 -2
  64. package/dist/json-schema/generator.d.ts.map +1 -1
  65. package/dist/json-schema/ir-generator.d.ts +25 -2
  66. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  67. package/dist/json-schema/types.d.ts +1 -1
  68. package/dist/json-schema/types.d.ts.map +1 -1
  69. package/dist/validate/constraint-validator.d.ts +3 -7
  70. package/dist/validate/constraint-validator.d.ts.map +1 -1
  71. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -182,7 +182,7 @@ function canonicalizeArrayField(field) {
182
182
  const itemsType = {
183
183
  kind: "object",
184
184
  properties: itemProperties,
185
- additionalProperties: false
185
+ additionalProperties: true
186
186
  };
187
187
  const type = { kind: "array", items: itemsType };
188
188
  const constraints = [];
@@ -217,7 +217,7 @@ function canonicalizeObjectField(field) {
217
217
  const type = {
218
218
  kind: "object",
219
219
  properties,
220
- additionalProperties: false
220
+ additionalProperties: true
221
221
  };
222
222
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
223
223
  }
@@ -415,11 +415,21 @@ var init_canonicalize = __esm({
415
415
  });
416
416
 
417
417
  // src/json-schema/ir-generator.ts
418
- function makeContext() {
419
- return { defs: {} };
418
+ function makeContext(options) {
419
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
420
+ if (!vendorPrefix.startsWith("x-")) {
421
+ throw new Error(
422
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
423
+ );
424
+ }
425
+ return {
426
+ defs: {},
427
+ extensionRegistry: options?.extensionRegistry,
428
+ vendorPrefix
429
+ };
420
430
  }
421
- function generateJsonSchemaFromIR(ir) {
422
- const ctx = makeContext();
431
+ function generateJsonSchemaFromIR(ir, options) {
432
+ const ctx = makeContext(options);
423
433
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
424
434
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
425
435
  }
@@ -462,8 +472,70 @@ function collectFields(elements, properties, required, ctx) {
462
472
  }
463
473
  function generateFieldSchema(field, ctx) {
464
474
  const schema = generateTypeNode(field.type, ctx);
465
- applyConstraints(schema, field.constraints);
466
- applyAnnotations(schema, field.annotations);
475
+ const directConstraints = [];
476
+ const pathConstraints = [];
477
+ for (const c of field.constraints) {
478
+ if (c.path) {
479
+ pathConstraints.push(c);
480
+ } else {
481
+ directConstraints.push(c);
482
+ }
483
+ }
484
+ applyConstraints(schema, directConstraints, ctx);
485
+ applyAnnotations(schema, field.annotations, ctx);
486
+ if (pathConstraints.length === 0) {
487
+ return schema;
488
+ }
489
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
490
+ }
491
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
492
+ if (schema.type === "array" && schema.items) {
493
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
494
+ return schema;
495
+ }
496
+ const byTarget = /* @__PURE__ */ new Map();
497
+ for (const c of pathConstraints) {
498
+ const target = c.path?.segments[0];
499
+ if (!target) continue;
500
+ const group = byTarget.get(target) ?? [];
501
+ group.push(c);
502
+ byTarget.set(target, group);
503
+ }
504
+ const propertyOverrides = {};
505
+ for (const [target, constraints] of byTarget) {
506
+ const subSchema = {};
507
+ applyConstraints(subSchema, constraints, ctx);
508
+ propertyOverrides[target] = subSchema;
509
+ }
510
+ if (schema.$ref) {
511
+ const { $ref, ...rest } = schema;
512
+ const refPart = { $ref };
513
+ const overridePart = {
514
+ properties: propertyOverrides,
515
+ ...rest
516
+ };
517
+ return { allOf: [refPart, overridePart] };
518
+ }
519
+ if (schema.type === "object" && schema.properties) {
520
+ const missingOverrides = {};
521
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
522
+ if (schema.properties[target]) {
523
+ Object.assign(schema.properties[target], overrideSchema);
524
+ } else {
525
+ missingOverrides[target] = overrideSchema;
526
+ }
527
+ }
528
+ if (Object.keys(missingOverrides).length === 0) {
529
+ return schema;
530
+ }
531
+ return {
532
+ allOf: [schema, { properties: missingOverrides }]
533
+ };
534
+ }
535
+ if (schema.allOf) {
536
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
537
+ return schema;
538
+ }
467
539
  return schema;
468
540
  }
469
541
  function generateTypeNode(type, ctx) {
@@ -476,6 +548,8 @@ function generateTypeNode(type, ctx) {
476
548
  return generateArrayType(type, ctx);
477
549
  case "object":
478
550
  return generateObjectType(type, ctx);
551
+ case "record":
552
+ return generateRecordType(type, ctx);
479
553
  case "union":
480
554
  return generateUnionType(type, ctx);
481
555
  case "reference":
@@ -483,7 +557,7 @@ function generateTypeNode(type, ctx) {
483
557
  case "dynamic":
484
558
  return generateDynamicType(type);
485
559
  case "custom":
486
- return generateCustomType(type);
560
+ return generateCustomType(type, ctx);
487
561
  default: {
488
562
  const _exhaustive = type;
489
563
  return _exhaustive;
@@ -532,16 +606,27 @@ function generateObjectType(type, ctx) {
532
606
  }
533
607
  return schema;
534
608
  }
609
+ function generateRecordType(type, ctx) {
610
+ return {
611
+ type: "object",
612
+ additionalProperties: generateTypeNode(type.valueType, ctx)
613
+ };
614
+ }
535
615
  function generatePropertySchema(prop, ctx) {
536
616
  const schema = generateTypeNode(prop.type, ctx);
537
- applyConstraints(schema, prop.constraints);
538
- applyAnnotations(schema, prop.annotations);
617
+ applyConstraints(schema, prop.constraints, ctx);
618
+ applyAnnotations(schema, prop.annotations, ctx);
539
619
  return schema;
540
620
  }
541
621
  function generateUnionType(type, ctx) {
542
622
  if (isBooleanUnion(type)) {
543
623
  return { type: "boolean" };
544
624
  }
625
+ if (isNullableUnion(type)) {
626
+ return {
627
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
628
+ };
629
+ }
545
630
  return {
546
631
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
547
632
  };
@@ -551,6 +636,13 @@ function isBooleanUnion(type) {
551
636
  const kinds = type.members.map((m) => m.kind);
552
637
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
553
638
  }
639
+ function isNullableUnion(type) {
640
+ if (type.members.length !== 2) return false;
641
+ const nullCount = type.members.filter(
642
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
643
+ ).length;
644
+ return nullCount === 1;
645
+ }
554
646
  function generateReferenceType(type) {
555
647
  return { $ref: `#/$defs/${type.name}` };
556
648
  }
@@ -571,10 +663,7 @@ function generateDynamicType(type) {
571
663
  "x-formspec-schemaSource": type.sourceKey
572
664
  };
573
665
  }
574
- function generateCustomType(_type) {
575
- return { type: "object" };
576
- }
577
- function applyConstraints(schema, constraints) {
666
+ function applyConstraints(schema, constraints, ctx) {
578
667
  for (const constraint of constraints) {
579
668
  switch (constraint.constraintKind) {
580
669
  case "minimum":
@@ -619,6 +708,7 @@ function applyConstraints(schema, constraints) {
619
708
  case "allowedMembers":
620
709
  break;
621
710
  case "custom":
711
+ applyCustomConstraint(schema, constraint, ctx);
622
712
  break;
623
713
  default: {
624
714
  const _exhaustive = constraint;
@@ -627,7 +717,7 @@ function applyConstraints(schema, constraints) {
627
717
  }
628
718
  }
629
719
  }
630
- function applyAnnotations(schema, annotations) {
720
+ function applyAnnotations(schema, annotations, ctx) {
631
721
  for (const annotation of annotations) {
632
722
  switch (annotation.annotationKind) {
633
723
  case "displayName":
@@ -647,6 +737,7 @@ function applyAnnotations(schema, annotations) {
647
737
  case "formatHint":
648
738
  break;
649
739
  case "custom":
740
+ applyCustomAnnotation(schema, annotation, ctx);
650
741
  break;
651
742
  default: {
652
743
  const _exhaustive = annotation;
@@ -655,6 +746,36 @@ function applyAnnotations(schema, annotations) {
655
746
  }
656
747
  }
657
748
  }
749
+ function generateCustomType(type, ctx) {
750
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
751
+ if (registration === void 0) {
752
+ throw new Error(
753
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
754
+ );
755
+ }
756
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
757
+ }
758
+ function applyCustomConstraint(schema, constraint, ctx) {
759
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
760
+ if (registration === void 0) {
761
+ throw new Error(
762
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
763
+ );
764
+ }
765
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
766
+ }
767
+ function applyCustomAnnotation(schema, annotation, ctx) {
768
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
769
+ if (registration === void 0) {
770
+ throw new Error(
771
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
772
+ );
773
+ }
774
+ if (registration.toJsonSchema === void 0) {
775
+ return;
776
+ }
777
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
778
+ }
658
779
  var init_ir_generator = __esm({
659
780
  "src/json-schema/ir-generator.ts"() {
660
781
  "use strict";
@@ -662,9 +783,9 @@ var init_ir_generator = __esm({
662
783
  });
663
784
 
664
785
  // src/json-schema/generator.ts
665
- function generateJsonSchema(form) {
786
+ function generateJsonSchema(form, options) {
666
787
  const ir = canonicalizeChainDSL(form);
667
- return generateJsonSchemaFromIR(ir);
788
+ return generateJsonSchemaFromIR(ir, options);
668
789
  }
669
790
  var init_generator = __esm({
670
791
  "src/json-schema/generator.ts"() {
@@ -925,6 +1046,61 @@ var init_types = __esm({
925
1046
  }
926
1047
  });
927
1048
 
1049
+ // src/extensions/registry.ts
1050
+ function createExtensionRegistry(extensions) {
1051
+ const typeMap = /* @__PURE__ */ new Map();
1052
+ const constraintMap = /* @__PURE__ */ new Map();
1053
+ const annotationMap = /* @__PURE__ */ new Map();
1054
+ for (const ext of extensions) {
1055
+ if (ext.types !== void 0) {
1056
+ for (const type of ext.types) {
1057
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
1058
+ if (typeMap.has(qualifiedId)) {
1059
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
1060
+ }
1061
+ typeMap.set(qualifiedId, type);
1062
+ }
1063
+ }
1064
+ if (ext.constraints !== void 0) {
1065
+ for (const constraint of ext.constraints) {
1066
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
1067
+ if (constraintMap.has(qualifiedId)) {
1068
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
1069
+ }
1070
+ constraintMap.set(qualifiedId, constraint);
1071
+ }
1072
+ }
1073
+ if (ext.annotations !== void 0) {
1074
+ for (const annotation of ext.annotations) {
1075
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
1076
+ if (annotationMap.has(qualifiedId)) {
1077
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
1078
+ }
1079
+ annotationMap.set(qualifiedId, annotation);
1080
+ }
1081
+ }
1082
+ }
1083
+ return {
1084
+ extensions,
1085
+ findType: (typeId) => typeMap.get(typeId),
1086
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
1087
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
1088
+ };
1089
+ }
1090
+ var init_registry = __esm({
1091
+ "src/extensions/registry.ts"() {
1092
+ "use strict";
1093
+ }
1094
+ });
1095
+
1096
+ // src/extensions/index.ts
1097
+ var init_extensions = __esm({
1098
+ "src/extensions/index.ts"() {
1099
+ "use strict";
1100
+ init_registry();
1101
+ }
1102
+ });
1103
+
928
1104
  // src/json-schema/schema.ts
929
1105
  import { z as z3 } from "zod";
930
1106
  var jsonSchemaTypeSchema, jsonSchema7Schema;
@@ -1070,6 +1246,20 @@ var init_program = __esm({
1070
1246
  }
1071
1247
  });
1072
1248
 
1249
+ // src/analyzer/json-utils.ts
1250
+ function tryParseJson(text) {
1251
+ try {
1252
+ return JSON.parse(text);
1253
+ } catch {
1254
+ return null;
1255
+ }
1256
+ }
1257
+ var init_json_utils = __esm({
1258
+ "src/analyzer/json-utils.ts"() {
1259
+ "use strict";
1260
+ }
1261
+ });
1262
+
1073
1263
  // src/analyzer/tsdoc-parser.ts
1074
1264
  import * as ts2 from "typescript";
1075
1265
  import {
@@ -1082,11 +1272,10 @@ import {
1082
1272
  TextRange
1083
1273
  } from "@microsoft/tsdoc";
1084
1274
  import {
1085
- BUILTIN_CONSTRAINT_DEFINITIONS
1275
+ BUILTIN_CONSTRAINT_DEFINITIONS,
1276
+ normalizeConstraintTagName,
1277
+ isBuiltinConstraintName
1086
1278
  } from "@formspec/core";
1087
- function isBuiltinConstraintName(tagName) {
1088
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
1089
- }
1090
1279
  function createFormSpecTSDocConfig() {
1091
1280
  const config = new TSDocConfiguration();
1092
1281
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1098,6 +1287,15 @@ function createFormSpecTSDocConfig() {
1098
1287
  })
1099
1288
  );
1100
1289
  }
1290
+ for (const tagName of ["displayName", "description"]) {
1291
+ config.addTagDefinition(
1292
+ new TSDocTagDefinition({
1293
+ tagName: "@" + tagName,
1294
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
1295
+ allowMultiple: true
1296
+ })
1297
+ );
1298
+ }
1101
1299
  return config;
1102
1300
  }
1103
1301
  function getParser() {
@@ -1125,7 +1323,28 @@ function parseTSDocTags(node, file = "") {
1125
1323
  );
1126
1324
  const docComment = parserContext.docComment;
1127
1325
  for (const block of docComment.customBlocks) {
1128
- const tagName = block.blockTag.tagName.substring(1);
1326
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1327
+ if (tagName === "displayName" || tagName === "description") {
1328
+ const text2 = extractBlockText(block).trim();
1329
+ if (text2 === "") continue;
1330
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
1331
+ if (tagName === "displayName") {
1332
+ annotations.push({
1333
+ kind: "annotation",
1334
+ annotationKind: "displayName",
1335
+ value: text2,
1336
+ provenance: provenance2
1337
+ });
1338
+ } else {
1339
+ annotations.push({
1340
+ kind: "annotation",
1341
+ annotationKind: "description",
1342
+ value: text2,
1343
+ provenance: provenance2
1344
+ });
1345
+ }
1346
+ continue;
1347
+ }
1129
1348
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1130
1349
  const text = extractBlockText(block).trim();
1131
1350
  if (text === "") continue;
@@ -1146,7 +1365,7 @@ function parseTSDocTags(node, file = "") {
1146
1365
  }
1147
1366
  const jsDocTagsAll = ts2.getJSDocTags(node);
1148
1367
  for (const tag of jsDocTagsAll) {
1149
- const tagName = tag.tagName.text;
1368
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
1150
1369
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1151
1370
  const commentText = getTagCommentText(tag);
1152
1371
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1157,43 +1376,17 @@ function parseTSDocTags(node, file = "") {
1157
1376
  constraints.push(constraintNode);
1158
1377
  }
1159
1378
  }
1160
- let displayName;
1161
- let description;
1162
- let displayNameTag;
1163
- let descriptionTag;
1164
- for (const tag of jsDocTagsAll) {
1165
- const tagName = tag.tagName.text;
1166
- const commentText = getTagCommentText(tag);
1167
- if (commentText === void 0 || commentText.trim() === "") {
1168
- continue;
1169
- }
1170
- const trimmed = commentText.trim();
1171
- if (tagName === "Field_displayName") {
1172
- displayName = trimmed;
1173
- displayNameTag = tag;
1174
- } else if (tagName === "Field_description") {
1175
- description = trimmed;
1176
- descriptionTag = tag;
1177
- }
1178
- }
1179
- if (displayName !== void 0 && displayNameTag) {
1180
- annotations.push({
1181
- kind: "annotation",
1182
- annotationKind: "displayName",
1183
- value: displayName,
1184
- provenance: provenanceForJSDocTag(displayNameTag, file)
1185
- });
1186
- }
1187
- if (description !== void 0 && descriptionTag) {
1188
- annotations.push({
1189
- kind: "annotation",
1190
- annotationKind: "description",
1191
- value: description,
1192
- provenance: provenanceForJSDocTag(descriptionTag, file)
1193
- });
1194
- }
1195
1379
  return { constraints, annotations };
1196
1380
  }
1381
+ function extractPathTarget(text) {
1382
+ const trimmed = text.trimStart();
1383
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
1384
+ if (!match?.[1] || !match[2]) return null;
1385
+ return {
1386
+ path: { segments: [match[1]] },
1387
+ remainingText: match[2]
1388
+ };
1389
+ }
1197
1390
  function extractBlockText(block) {
1198
1391
  return extractPlainText(block.content);
1199
1392
  }
@@ -1216,9 +1409,12 @@ function parseConstraintValue(tagName, text, provenance) {
1216
1409
  if (!isBuiltinConstraintName(tagName)) {
1217
1410
  return null;
1218
1411
  }
1412
+ const pathResult = extractPathTarget(text);
1413
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1414
+ const path4 = pathResult?.path;
1219
1415
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1220
1416
  if (expectedType === "number") {
1221
- const value = Number(text);
1417
+ const value = Number(effectiveText);
1222
1418
  if (Number.isNaN(value)) {
1223
1419
  return null;
1224
1420
  }
@@ -1228,6 +1424,7 @@ function parseConstraintValue(tagName, text, provenance) {
1228
1424
  kind: "constraint",
1229
1425
  constraintKind: numericKind,
1230
1426
  value,
1427
+ ...path4 && { path: path4 },
1231
1428
  provenance
1232
1429
  };
1233
1430
  }
@@ -1237,42 +1434,41 @@ function parseConstraintValue(tagName, text, provenance) {
1237
1434
  kind: "constraint",
1238
1435
  constraintKind: lengthKind,
1239
1436
  value,
1437
+ ...path4 && { path: path4 },
1240
1438
  provenance
1241
1439
  };
1242
1440
  }
1243
1441
  return null;
1244
1442
  }
1245
1443
  if (expectedType === "json") {
1246
- try {
1247
- const parsed = JSON.parse(text);
1248
- if (!Array.isArray(parsed)) {
1249
- return null;
1250
- }
1251
- const members = [];
1252
- for (const item of parsed) {
1253
- if (typeof item === "string" || typeof item === "number") {
1254
- members.push(item);
1255
- } else if (typeof item === "object" && item !== null && "id" in item) {
1256
- const id = item["id"];
1257
- if (typeof id === "string" || typeof id === "number") {
1258
- members.push(id);
1259
- }
1444
+ const parsed = tryParseJson(effectiveText);
1445
+ if (!Array.isArray(parsed)) {
1446
+ return null;
1447
+ }
1448
+ const members = [];
1449
+ for (const item of parsed) {
1450
+ if (typeof item === "string" || typeof item === "number") {
1451
+ members.push(item);
1452
+ } else if (typeof item === "object" && item !== null && "id" in item) {
1453
+ const id = item["id"];
1454
+ if (typeof id === "string" || typeof id === "number") {
1455
+ members.push(id);
1260
1456
  }
1261
1457
  }
1262
- return {
1263
- kind: "constraint",
1264
- constraintKind: "allowedMembers",
1265
- members,
1266
- provenance
1267
- };
1268
- } catch {
1269
- return null;
1270
1458
  }
1459
+ return {
1460
+ kind: "constraint",
1461
+ constraintKind: "allowedMembers",
1462
+ members,
1463
+ ...path4 && { path: path4 },
1464
+ provenance
1465
+ };
1271
1466
  }
1272
1467
  return {
1273
1468
  kind: "constraint",
1274
1469
  constraintKind: "pattern",
1275
- pattern: text,
1470
+ pattern: effectiveText,
1471
+ ...path4 && { path: path4 },
1276
1472
  provenance
1277
1473
  };
1278
1474
  }
@@ -1310,25 +1506,26 @@ var NUMERIC_CONSTRAINT_MAP, LENGTH_CONSTRAINT_MAP, TAGS_REQUIRING_RAW_TEXT, shar
1310
1506
  var init_tsdoc_parser = __esm({
1311
1507
  "src/analyzer/tsdoc-parser.ts"() {
1312
1508
  "use strict";
1509
+ init_json_utils();
1313
1510
  NUMERIC_CONSTRAINT_MAP = {
1314
- Minimum: "minimum",
1315
- Maximum: "maximum",
1316
- ExclusiveMinimum: "exclusiveMinimum",
1317
- ExclusiveMaximum: "exclusiveMaximum"
1511
+ minimum: "minimum",
1512
+ maximum: "maximum",
1513
+ exclusiveMinimum: "exclusiveMinimum",
1514
+ exclusiveMaximum: "exclusiveMaximum",
1515
+ multipleOf: "multipleOf"
1318
1516
  };
1319
1517
  LENGTH_CONSTRAINT_MAP = {
1320
- MinLength: "minLength",
1321
- MaxLength: "maxLength"
1518
+ minLength: "minLength",
1519
+ maxLength: "maxLength",
1520
+ minItems: "minItems",
1521
+ maxItems: "maxItems"
1322
1522
  };
1323
- TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1523
+ TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1324
1524
  }
1325
1525
  });
1326
1526
 
1327
1527
  // src/analyzer/jsdoc-constraints.ts
1328
1528
  import * as ts3 from "typescript";
1329
- import {
1330
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
1331
- } from "@formspec/core";
1332
1529
  function extractJSDocConstraintNodes(node, file = "") {
1333
1530
  const result = parseTSDocTags(node, file);
1334
1531
  return [...result.constraints];
@@ -1472,18 +1669,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1472
1669
  const tsType = checker.getTypeAtLocation(prop);
1473
1670
  const optional = prop.questionToken !== void 0;
1474
1671
  const provenance = provenanceForNode(prop, file);
1475
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1672
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1476
1673
  const constraints = [];
1477
1674
  if (prop.type) {
1478
1675
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1479
1676
  }
1480
1677
  constraints.push(...extractJSDocConstraintNodes(prop, file));
1481
- const annotations = [];
1678
+ let annotations = [];
1482
1679
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1483
1680
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1484
1681
  if (defaultAnnotation) {
1485
1682
  annotations.push(defaultAnnotation);
1486
1683
  }
1684
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1487
1685
  return {
1488
1686
  kind: "field",
1489
1687
  name,
@@ -1502,14 +1700,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1502
1700
  const tsType = checker.getTypeAtLocation(prop);
1503
1701
  const optional = prop.questionToken !== void 0;
1504
1702
  const provenance = provenanceForNode(prop, file);
1505
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1703
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1506
1704
  const constraints = [];
1507
1705
  if (prop.type) {
1508
1706
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1509
1707
  }
1510
1708
  constraints.push(...extractJSDocConstraintNodes(prop, file));
1511
- const annotations = [];
1709
+ let annotations = [];
1512
1710
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1711
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1513
1712
  return {
1514
1713
  kind: "field",
1515
1714
  name,
@@ -1520,6 +1719,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1520
1719
  provenance
1521
1720
  };
1522
1721
  }
1722
+ function applyEnumMemberDisplayNames(type, annotations) {
1723
+ if (!annotations.some(
1724
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
1725
+ )) {
1726
+ return { type, annotations: [...annotations] };
1727
+ }
1728
+ const consumed = /* @__PURE__ */ new Set();
1729
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
1730
+ if (consumed.size === 0) {
1731
+ return { type, annotations: [...annotations] };
1732
+ }
1733
+ return {
1734
+ type: nextType,
1735
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
1736
+ };
1737
+ }
1738
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
1739
+ switch (type.kind) {
1740
+ case "enum":
1741
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
1742
+ case "union": {
1743
+ return {
1744
+ ...type,
1745
+ members: type.members.map(
1746
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
1747
+ )
1748
+ };
1749
+ }
1750
+ default:
1751
+ return type;
1752
+ }
1753
+ }
1754
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
1755
+ const displayNames = /* @__PURE__ */ new Map();
1756
+ for (const annotation of annotations) {
1757
+ if (annotation.annotationKind !== "displayName") continue;
1758
+ const parsed = parseEnumMemberDisplayName(annotation.value);
1759
+ if (!parsed) continue;
1760
+ consumed.add(annotation);
1761
+ const member = type.members.find((m) => String(m.value) === parsed.value);
1762
+ if (!member) continue;
1763
+ displayNames.set(String(member.value), parsed.label);
1764
+ }
1765
+ if (displayNames.size === 0) {
1766
+ return type;
1767
+ }
1768
+ return {
1769
+ ...type,
1770
+ members: type.members.map((member) => {
1771
+ const displayName = displayNames.get(String(member.value));
1772
+ return displayName !== void 0 ? { ...member, displayName } : member;
1773
+ })
1774
+ };
1775
+ }
1776
+ function parseEnumMemberDisplayName(value) {
1777
+ const trimmed = value.trim();
1778
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1779
+ if (!match?.[1] || !match[2]) return null;
1780
+ const label = match[2].trim();
1781
+ if (label === "") return null;
1782
+ return { value: match[1], label };
1783
+ }
1523
1784
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1524
1785
  if (type.flags & ts4.TypeFlags.String) {
1525
1786
  return { kind: "primitive", primitiveKind: "string" };
@@ -1630,7 +1891,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1630
1891
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1631
1892
  return { kind: "array", items };
1632
1893
  }
1894
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1895
+ if (type.getProperties().length > 0) {
1896
+ return null;
1897
+ }
1898
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1899
+ if (!indexInfo) {
1900
+ return null;
1901
+ }
1902
+ if (visiting.has(type)) {
1903
+ return null;
1904
+ }
1905
+ visiting.add(type);
1906
+ try {
1907
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1908
+ return { kind: "record", valueType };
1909
+ } finally {
1910
+ visiting.delete(type);
1911
+ }
1912
+ }
1633
1913
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1914
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1915
+ if (recordNode) {
1916
+ return recordNode;
1917
+ }
1634
1918
  if (visiting.has(type)) {
1635
1919
  return { kind: "object", properties: [], additionalProperties: false };
1636
1920
  }
@@ -1662,7 +1946,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1662
1946
  const objectNode = {
1663
1947
  kind: "object",
1664
1948
  properties,
1665
- additionalProperties: false
1949
+ additionalProperties: true
1666
1950
  };
1667
1951
  if (typeName) {
1668
1952
  typeRegistry[typeName] = {
@@ -1731,14 +2015,22 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1731
2015
  }
1732
2016
  return map;
1733
2017
  }
1734
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
2018
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1735
2019
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
2020
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
2021
+ const aliasName = typeNode.typeName.getText();
2022
+ throw new Error(
2023
+ `Type alias chain exceeds maximum depth of ${String(MAX_ALIAS_CHAIN_DEPTH)} at alias "${aliasName}" in ${file}. Simplify the alias chain or check for circular references.`
2024
+ );
2025
+ }
1736
2026
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1737
2027
  if (!symbol?.declarations) return [];
1738
2028
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1739
2029
  if (!aliasDecl) return [];
1740
2030
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1741
- return extractJSDocConstraintNodes(aliasDecl, file);
2031
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
2032
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
2033
+ return constraints;
1742
2034
  }
1743
2035
  function provenanceForNode(node, file) {
1744
2036
  const sourceFile = node.getSourceFile();
@@ -1811,10 +2103,12 @@ function detectFormSpecReference(typeNode) {
1811
2103
  }
1812
2104
  return null;
1813
2105
  }
2106
+ var MAX_ALIAS_CHAIN_DEPTH;
1814
2107
  var init_class_analyzer = __esm({
1815
2108
  "src/analyzer/class-analyzer.ts"() {
1816
2109
  "use strict";
1817
2110
  init_jsdoc_constraints();
2111
+ MAX_ALIAS_CHAIN_DEPTH = 8;
1818
2112
  }
1819
2113
  });
1820
2114
 
@@ -1878,7 +2172,9 @@ __export(index_exports, {
1878
2172
  categorizationSchema: () => categorizationSchema,
1879
2173
  categorySchema: () => categorySchema,
1880
2174
  controlSchema: () => controlSchema,
2175
+ createExtensionRegistry: () => createExtensionRegistry,
1881
2176
  generateJsonSchema: () => generateJsonSchema,
2177
+ generateJsonSchemaFromIR: () => generateJsonSchemaFromIR,
1882
2178
  generateSchemas: () => generateSchemas,
1883
2179
  generateSchemasFromClass: () => generateSchemasFromClass,
1884
2180
  generateUiSchema: () => generateUiSchema,
@@ -1901,15 +2197,19 @@ __export(index_exports, {
1901
2197
  });
1902
2198
  import * as fs from "fs";
1903
2199
  import * as path2 from "path";
1904
- function buildFormSchemas(form) {
2200
+ function buildFormSchemas(form, options) {
1905
2201
  return {
1906
- jsonSchema: generateJsonSchema(form),
2202
+ jsonSchema: generateJsonSchema(form, options),
1907
2203
  uiSchema: generateUiSchema(form)
1908
2204
  };
1909
2205
  }
1910
2206
  function writeSchemas(form, options) {
1911
- const { outDir, name = "schema", indent = 2 } = options;
1912
- const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form);
2207
+ const { outDir, name = "schema", indent = 2, extensionRegistry, vendorPrefix } = options;
2208
+ const buildOptions = extensionRegistry === void 0 && vendorPrefix === void 0 ? void 0 : {
2209
+ extensionRegistry,
2210
+ vendorPrefix
2211
+ };
2212
+ const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form, buildOptions);
1913
2213
  if (!fs.existsSync(outDir)) {
1914
2214
  fs.mkdirSync(outDir, { recursive: true });
1915
2215
  }
@@ -1924,10 +2224,13 @@ var init_index = __esm({
1924
2224
  "use strict";
1925
2225
  init_generator();
1926
2226
  init_generator2();
2227
+ init_ir_generator();
1927
2228
  init_types();
2229
+ init_extensions();
1928
2230
  init_schema();
1929
2231
  init_schema2();
1930
2232
  init_generator();
2233
+ init_ir_generator();
1931
2234
  init_generator2();
1932
2235
  init_class_schema();
1933
2236
  }