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

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 (39) 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__/fixtures/alias-chains.d.ts +37 -0
  5. package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/example-a-builtins.d.ts +7 -7
  7. package/dist/__tests__/fixtures/example-interface-types.d.ts +17 -17
  8. package/dist/__tests__/json-utils.test.d.ts +5 -0
  9. package/dist/__tests__/json-utils.test.d.ts.map +1 -0
  10. package/dist/__tests__/path-target-parser.test.d.ts +9 -0
  11. package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
  12. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  13. package/dist/analyzer/jsdoc-constraints.d.ts +2 -2
  14. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  15. package/dist/analyzer/json-utils.d.ts +22 -0
  16. package/dist/analyzer/json-utils.d.ts.map +1 -0
  17. package/dist/analyzer/tsdoc-parser.d.ts +18 -4
  18. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  19. package/dist/browser.cjs +76 -7
  20. package/dist/browser.cjs.map +1 -1
  21. package/dist/browser.js +76 -7
  22. package/dist/browser.js.map +1 -1
  23. package/dist/build.d.ts +1 -0
  24. package/dist/cli.cjs +140 -41
  25. package/dist/cli.cjs.map +1 -1
  26. package/dist/cli.js +145 -41
  27. package/dist/cli.js.map +1 -1
  28. package/dist/index.cjs +134 -40
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +139 -41
  31. package/dist/index.js.map +1 -1
  32. package/dist/internals.cjs +147 -46
  33. package/dist/internals.cjs.map +1 -1
  34. package/dist/internals.js +152 -47
  35. package/dist/internals.js.map +1 -1
  36. package/dist/json-schema/ir-generator.d.ts +1 -0
  37. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  38. package/dist/validate/constraint-validator.d.ts.map +1 -1
  39. package/package.json +3 -3
@@ -517,20 +517,31 @@ var import_core4 = require("@formspec/core");
517
517
  var ts2 = __toESM(require("typescript"), 1);
518
518
  var import_tsdoc = require("@microsoft/tsdoc");
519
519
  var import_core3 = require("@formspec/core");
520
+
521
+ // src/analyzer/json-utils.ts
522
+ function tryParseJson(text) {
523
+ try {
524
+ return JSON.parse(text);
525
+ } catch {
526
+ return null;
527
+ }
528
+ }
529
+
530
+ // src/analyzer/tsdoc-parser.ts
520
531
  var NUMERIC_CONSTRAINT_MAP = {
521
- Minimum: "minimum",
522
- Maximum: "maximum",
523
- ExclusiveMinimum: "exclusiveMinimum",
524
- ExclusiveMaximum: "exclusiveMaximum"
532
+ minimum: "minimum",
533
+ maximum: "maximum",
534
+ exclusiveMinimum: "exclusiveMinimum",
535
+ exclusiveMaximum: "exclusiveMaximum",
536
+ multipleOf: "multipleOf"
525
537
  };
526
538
  var LENGTH_CONSTRAINT_MAP = {
527
- MinLength: "minLength",
528
- MaxLength: "maxLength"
539
+ minLength: "minLength",
540
+ maxLength: "maxLength",
541
+ minItems: "minItems",
542
+ maxItems: "maxItems"
529
543
  };
530
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
531
- function isBuiltinConstraintName(tagName) {
532
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
533
- }
544
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
534
545
  function createFormSpecTSDocConfig() {
535
546
  const config = new import_tsdoc.TSDocConfiguration();
536
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -570,7 +581,7 @@ function parseTSDocTags(node, file = "") {
570
581
  );
571
582
  const docComment = parserContext.docComment;
572
583
  for (const block of docComment.customBlocks) {
573
- const tagName = block.blockTag.tagName.substring(1);
584
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
574
585
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
575
586
  const text = extractBlockText(block).trim();
576
587
  if (text === "") continue;
@@ -591,7 +602,7 @@ function parseTSDocTags(node, file = "") {
591
602
  }
592
603
  const jsDocTagsAll = ts2.getJSDocTags(node);
593
604
  for (const tag of jsDocTagsAll) {
594
- const tagName = tag.tagName.text;
605
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
595
606
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
596
607
  const commentText = getTagCommentText(tag);
597
608
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -639,6 +650,15 @@ function parseTSDocTags(node, file = "") {
639
650
  }
640
651
  return { constraints, annotations };
641
652
  }
653
+ function extractPathTarget(text) {
654
+ const trimmed = text.trimStart();
655
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
656
+ if (!match?.[1] || !match[2]) return null;
657
+ return {
658
+ path: { segments: [match[1]] },
659
+ remainingText: match[2]
660
+ };
661
+ }
642
662
  function extractBlockText(block) {
643
663
  return extractPlainText(block.content);
644
664
  }
@@ -658,12 +678,15 @@ function extractPlainText(node) {
658
678
  return result;
659
679
  }
660
680
  function parseConstraintValue(tagName, text, provenance) {
661
- if (!isBuiltinConstraintName(tagName)) {
681
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
662
682
  return null;
663
683
  }
684
+ const pathResult = extractPathTarget(text);
685
+ const effectiveText = pathResult ? pathResult.remainingText : text;
686
+ const path2 = pathResult?.path;
664
687
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
665
688
  if (expectedType === "number") {
666
- const value = Number(text);
689
+ const value = Number(effectiveText);
667
690
  if (Number.isNaN(value)) {
668
691
  return null;
669
692
  }
@@ -673,6 +696,7 @@ function parseConstraintValue(tagName, text, provenance) {
673
696
  kind: "constraint",
674
697
  constraintKind: numericKind,
675
698
  value,
699
+ ...path2 && { path: path2 },
676
700
  provenance
677
701
  };
678
702
  }
@@ -682,42 +706,41 @@ function parseConstraintValue(tagName, text, provenance) {
682
706
  kind: "constraint",
683
707
  constraintKind: lengthKind,
684
708
  value,
709
+ ...path2 && { path: path2 },
685
710
  provenance
686
711
  };
687
712
  }
688
713
  return null;
689
714
  }
690
715
  if (expectedType === "json") {
691
- try {
692
- const parsed = JSON.parse(text);
693
- if (!Array.isArray(parsed)) {
694
- return null;
695
- }
696
- const members = [];
697
- for (const item of parsed) {
698
- if (typeof item === "string" || typeof item === "number") {
699
- members.push(item);
700
- } else if (typeof item === "object" && item !== null && "id" in item) {
701
- const id = item["id"];
702
- if (typeof id === "string" || typeof id === "number") {
703
- members.push(id);
704
- }
716
+ const parsed = tryParseJson(effectiveText);
717
+ if (!Array.isArray(parsed)) {
718
+ return null;
719
+ }
720
+ const members = [];
721
+ for (const item of parsed) {
722
+ if (typeof item === "string" || typeof item === "number") {
723
+ members.push(item);
724
+ } else if (typeof item === "object" && item !== null && "id" in item) {
725
+ const id = item["id"];
726
+ if (typeof id === "string" || typeof id === "number") {
727
+ members.push(id);
705
728
  }
706
729
  }
707
- return {
708
- kind: "constraint",
709
- constraintKind: "allowedMembers",
710
- members,
711
- provenance
712
- };
713
- } catch {
714
- return null;
715
730
  }
731
+ return {
732
+ kind: "constraint",
733
+ constraintKind: "allowedMembers",
734
+ members,
735
+ ...path2 && { path: path2 },
736
+ provenance
737
+ };
716
738
  }
717
739
  return {
718
740
  kind: "constraint",
719
741
  constraintKind: "pattern",
720
- pattern: text,
742
+ pattern: effectiveText,
743
+ ...path2 && { path: path2 },
721
744
  provenance
722
745
  };
723
746
  }
@@ -1148,14 +1171,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1148
1171
  }
1149
1172
  return map;
1150
1173
  }
1151
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1174
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1175
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1152
1176
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1177
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1178
+ const aliasName = typeNode.typeName.getText();
1179
+ throw new Error(
1180
+ `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.`
1181
+ );
1182
+ }
1153
1183
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1154
1184
  if (!symbol?.declarations) return [];
1155
1185
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1156
1186
  if (!aliasDecl) return [];
1157
1187
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1158
- return extractJSDocConstraintNodes(aliasDecl, file);
1188
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1189
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1190
+ return constraints;
1159
1191
  }
1160
1192
  function provenanceForNode(node, file) {
1161
1193
  const sourceFile = node.getSourceFile();
@@ -1277,8 +1309,70 @@ function collectFields(elements, properties, required, ctx) {
1277
1309
  }
1278
1310
  function generateFieldSchema(field, ctx) {
1279
1311
  const schema = generateTypeNode(field.type, ctx);
1280
- applyConstraints(schema, field.constraints);
1312
+ const directConstraints = [];
1313
+ const pathConstraints = [];
1314
+ for (const c of field.constraints) {
1315
+ if (c.path) {
1316
+ pathConstraints.push(c);
1317
+ } else {
1318
+ directConstraints.push(c);
1319
+ }
1320
+ }
1321
+ applyConstraints(schema, directConstraints);
1281
1322
  applyAnnotations(schema, field.annotations);
1323
+ if (pathConstraints.length === 0) {
1324
+ return schema;
1325
+ }
1326
+ return applyPathTargetedConstraints(schema, pathConstraints);
1327
+ }
1328
+ function applyPathTargetedConstraints(schema, pathConstraints) {
1329
+ if (schema.type === "array" && schema.items) {
1330
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1331
+ return schema;
1332
+ }
1333
+ const byTarget = /* @__PURE__ */ new Map();
1334
+ for (const c of pathConstraints) {
1335
+ const target = c.path?.segments[0];
1336
+ if (!target) continue;
1337
+ const group = byTarget.get(target) ?? [];
1338
+ group.push(c);
1339
+ byTarget.set(target, group);
1340
+ }
1341
+ const propertyOverrides = {};
1342
+ for (const [target, constraints] of byTarget) {
1343
+ const subSchema = {};
1344
+ applyConstraints(subSchema, constraints);
1345
+ propertyOverrides[target] = subSchema;
1346
+ }
1347
+ if (schema.$ref) {
1348
+ const { $ref, ...rest } = schema;
1349
+ const refPart = { $ref };
1350
+ const overridePart = {
1351
+ properties: propertyOverrides,
1352
+ ...rest
1353
+ };
1354
+ return { allOf: [refPart, overridePart] };
1355
+ }
1356
+ if (schema.type === "object" && schema.properties) {
1357
+ const missingOverrides = {};
1358
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
1359
+ if (schema.properties[target]) {
1360
+ Object.assign(schema.properties[target], overrideSchema);
1361
+ } else {
1362
+ missingOverrides[target] = overrideSchema;
1363
+ }
1364
+ }
1365
+ if (Object.keys(missingOverrides).length === 0) {
1366
+ return schema;
1367
+ }
1368
+ return {
1369
+ allOf: [schema, { properties: missingOverrides }]
1370
+ };
1371
+ }
1372
+ if (schema.allOf) {
1373
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
1374
+ return schema;
1375
+ }
1282
1376
  return schema;
1283
1377
  }
1284
1378
  function generateTypeNode(type, ctx) {
@@ -1725,14 +1819,10 @@ function addUnknownExtension(ctx, message, primary) {
1725
1819
  });
1726
1820
  }
1727
1821
  function findNumeric(constraints, constraintKind) {
1728
- return constraints.find(
1729
- (c) => c.constraintKind === constraintKind
1730
- );
1822
+ return constraints.find((c) => c.constraintKind === constraintKind);
1731
1823
  }
1732
1824
  function findLength(constraints, constraintKind) {
1733
- return constraints.find(
1734
- (c) => c.constraintKind === constraintKind
1735
- );
1825
+ return constraints.find((c) => c.constraintKind === constraintKind);
1736
1826
  }
1737
1827
  function findAllowedMembers(constraints) {
1738
1828
  return constraints.filter(
@@ -1856,6 +1946,17 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1856
1946
  const isEnum = type.kind === "enum";
1857
1947
  const label = typeLabel(type);
1858
1948
  for (const constraint of constraints) {
1949
+ if (constraint.path) {
1950
+ const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1951
+ if (!isTraversable) {
1952
+ addTypeMismatch(
1953
+ ctx,
1954
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
1955
+ constraint.provenance
1956
+ );
1957
+ }
1958
+ continue;
1959
+ }
1859
1960
  const ck = constraint.constraintKind;
1860
1961
  switch (ck) {
1861
1962
  case "minimum":