@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
package/dist/internals.js CHANGED
@@ -461,7 +461,9 @@ import * as ts4 from "typescript";
461
461
  // src/analyzer/jsdoc-constraints.ts
462
462
  import * as ts3 from "typescript";
463
463
  import {
464
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
464
+ BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
465
+ isBuiltinConstraintName as isBuiltinConstraintName2,
466
+ normalizeConstraintTagName as normalizeConstraintTagName2
465
467
  } from "@formspec/core";
466
468
 
467
469
  // src/analyzer/tsdoc-parser.ts
@@ -476,22 +478,35 @@ import {
476
478
  TextRange
477
479
  } from "@microsoft/tsdoc";
478
480
  import {
479
- BUILTIN_CONSTRAINT_DEFINITIONS
481
+ BUILTIN_CONSTRAINT_DEFINITIONS,
482
+ normalizeConstraintTagName,
483
+ isBuiltinConstraintName
480
484
  } from "@formspec/core";
485
+
486
+ // src/analyzer/json-utils.ts
487
+ function tryParseJson(text) {
488
+ try {
489
+ return JSON.parse(text);
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+
495
+ // src/analyzer/tsdoc-parser.ts
481
496
  var NUMERIC_CONSTRAINT_MAP = {
482
- Minimum: "minimum",
483
- Maximum: "maximum",
484
- ExclusiveMinimum: "exclusiveMinimum",
485
- ExclusiveMaximum: "exclusiveMaximum"
497
+ minimum: "minimum",
498
+ maximum: "maximum",
499
+ exclusiveMinimum: "exclusiveMinimum",
500
+ exclusiveMaximum: "exclusiveMaximum",
501
+ multipleOf: "multipleOf"
486
502
  };
487
503
  var LENGTH_CONSTRAINT_MAP = {
488
- MinLength: "minLength",
489
- MaxLength: "maxLength"
504
+ minLength: "minLength",
505
+ maxLength: "maxLength",
506
+ minItems: "minItems",
507
+ maxItems: "maxItems"
490
508
  };
491
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
492
- function isBuiltinConstraintName(tagName) {
493
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
494
- }
509
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
495
510
  function createFormSpecTSDocConfig() {
496
511
  const config = new TSDocConfiguration();
497
512
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -531,7 +546,7 @@ function parseTSDocTags(node, file = "") {
531
546
  );
532
547
  const docComment = parserContext.docComment;
533
548
  for (const block of docComment.customBlocks) {
534
- const tagName = block.blockTag.tagName.substring(1);
549
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
535
550
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
536
551
  const text = extractBlockText(block).trim();
537
552
  if (text === "") continue;
@@ -552,7 +567,7 @@ function parseTSDocTags(node, file = "") {
552
567
  }
553
568
  const jsDocTagsAll = ts2.getJSDocTags(node);
554
569
  for (const tag of jsDocTagsAll) {
555
- const tagName = tag.tagName.text;
570
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
556
571
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
557
572
  const commentText = getTagCommentText(tag);
558
573
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -600,6 +615,15 @@ function parseTSDocTags(node, file = "") {
600
615
  }
601
616
  return { constraints, annotations };
602
617
  }
618
+ function extractPathTarget(text) {
619
+ const trimmed = text.trimStart();
620
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
621
+ if (!match?.[1] || !match[2]) return null;
622
+ return {
623
+ path: { segments: [match[1]] },
624
+ remainingText: match[2]
625
+ };
626
+ }
603
627
  function extractBlockText(block) {
604
628
  return extractPlainText(block.content);
605
629
  }
@@ -622,9 +646,12 @@ function parseConstraintValue(tagName, text, provenance) {
622
646
  if (!isBuiltinConstraintName(tagName)) {
623
647
  return null;
624
648
  }
649
+ const pathResult = extractPathTarget(text);
650
+ const effectiveText = pathResult ? pathResult.remainingText : text;
651
+ const path2 = pathResult?.path;
625
652
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
626
653
  if (expectedType === "number") {
627
- const value = Number(text);
654
+ const value = Number(effectiveText);
628
655
  if (Number.isNaN(value)) {
629
656
  return null;
630
657
  }
@@ -634,6 +661,7 @@ function parseConstraintValue(tagName, text, provenance) {
634
661
  kind: "constraint",
635
662
  constraintKind: numericKind,
636
663
  value,
664
+ ...path2 && { path: path2 },
637
665
  provenance
638
666
  };
639
667
  }
@@ -643,42 +671,41 @@ function parseConstraintValue(tagName, text, provenance) {
643
671
  kind: "constraint",
644
672
  constraintKind: lengthKind,
645
673
  value,
674
+ ...path2 && { path: path2 },
646
675
  provenance
647
676
  };
648
677
  }
649
678
  return null;
650
679
  }
651
680
  if (expectedType === "json") {
652
- try {
653
- const parsed = JSON.parse(text);
654
- if (!Array.isArray(parsed)) {
655
- return null;
656
- }
657
- const members = [];
658
- for (const item of parsed) {
659
- if (typeof item === "string" || typeof item === "number") {
660
- members.push(item);
661
- } else if (typeof item === "object" && item !== null && "id" in item) {
662
- const id = item["id"];
663
- if (typeof id === "string" || typeof id === "number") {
664
- members.push(id);
665
- }
681
+ const parsed = tryParseJson(effectiveText);
682
+ if (!Array.isArray(parsed)) {
683
+ return null;
684
+ }
685
+ const members = [];
686
+ for (const item of parsed) {
687
+ if (typeof item === "string" || typeof item === "number") {
688
+ members.push(item);
689
+ } else if (typeof item === "object" && item !== null && "id" in item) {
690
+ const id = item["id"];
691
+ if (typeof id === "string" || typeof id === "number") {
692
+ members.push(id);
666
693
  }
667
694
  }
668
- return {
669
- kind: "constraint",
670
- constraintKind: "allowedMembers",
671
- members,
672
- provenance
673
- };
674
- } catch {
675
- return null;
676
695
  }
696
+ return {
697
+ kind: "constraint",
698
+ constraintKind: "allowedMembers",
699
+ members,
700
+ ...path2 && { path: path2 },
701
+ provenance
702
+ };
677
703
  }
678
704
  return {
679
705
  kind: "constraint",
680
706
  constraintKind: "pattern",
681
- pattern: text,
707
+ pattern: effectiveText,
708
+ ...path2 && { path: path2 },
682
709
  provenance
683
710
  };
684
711
  }
@@ -1109,14 +1136,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1109
1136
  }
1110
1137
  return map;
1111
1138
  }
1112
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1139
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1140
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1113
1141
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1142
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1143
+ const aliasName = typeNode.typeName.getText();
1144
+ throw new Error(
1145
+ `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.`
1146
+ );
1147
+ }
1114
1148
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1115
1149
  if (!symbol?.declarations) return [];
1116
1150
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1117
1151
  if (!aliasDecl) return [];
1118
1152
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1119
- return extractJSDocConstraintNodes(aliasDecl, file);
1153
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1154
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1155
+ return constraints;
1120
1156
  }
1121
1157
  function provenanceForNode(node, file) {
1122
1158
  const sourceFile = node.getSourceFile();
@@ -1238,8 +1274,70 @@ function collectFields(elements, properties, required, ctx) {
1238
1274
  }
1239
1275
  function generateFieldSchema(field, ctx) {
1240
1276
  const schema = generateTypeNode(field.type, ctx);
1241
- applyConstraints(schema, field.constraints);
1277
+ const directConstraints = [];
1278
+ const pathConstraints = [];
1279
+ for (const c of field.constraints) {
1280
+ if (c.path) {
1281
+ pathConstraints.push(c);
1282
+ } else {
1283
+ directConstraints.push(c);
1284
+ }
1285
+ }
1286
+ applyConstraints(schema, directConstraints);
1242
1287
  applyAnnotations(schema, field.annotations);
1288
+ if (pathConstraints.length === 0) {
1289
+ return schema;
1290
+ }
1291
+ return applyPathTargetedConstraints(schema, pathConstraints);
1292
+ }
1293
+ function applyPathTargetedConstraints(schema, pathConstraints) {
1294
+ if (schema.type === "array" && schema.items) {
1295
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1296
+ return schema;
1297
+ }
1298
+ const byTarget = /* @__PURE__ */ new Map();
1299
+ for (const c of pathConstraints) {
1300
+ const target = c.path?.segments[0];
1301
+ if (!target) continue;
1302
+ const group = byTarget.get(target) ?? [];
1303
+ group.push(c);
1304
+ byTarget.set(target, group);
1305
+ }
1306
+ const propertyOverrides = {};
1307
+ for (const [target, constraints] of byTarget) {
1308
+ const subSchema = {};
1309
+ applyConstraints(subSchema, constraints);
1310
+ propertyOverrides[target] = subSchema;
1311
+ }
1312
+ if (schema.$ref) {
1313
+ const { $ref, ...rest } = schema;
1314
+ const refPart = { $ref };
1315
+ const overridePart = {
1316
+ properties: propertyOverrides,
1317
+ ...rest
1318
+ };
1319
+ return { allOf: [refPart, overridePart] };
1320
+ }
1321
+ if (schema.type === "object" && schema.properties) {
1322
+ const missingOverrides = {};
1323
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
1324
+ if (schema.properties[target]) {
1325
+ Object.assign(schema.properties[target], overrideSchema);
1326
+ } else {
1327
+ missingOverrides[target] = overrideSchema;
1328
+ }
1329
+ }
1330
+ if (Object.keys(missingOverrides).length === 0) {
1331
+ return schema;
1332
+ }
1333
+ return {
1334
+ allOf: [schema, { properties: missingOverrides }]
1335
+ };
1336
+ }
1337
+ if (schema.allOf) {
1338
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
1339
+ return schema;
1340
+ }
1243
1341
  return schema;
1244
1342
  }
1245
1343
  function generateTypeNode(type, ctx) {
@@ -1686,14 +1784,10 @@ function addUnknownExtension(ctx, message, primary) {
1686
1784
  });
1687
1785
  }
1688
1786
  function findNumeric(constraints, constraintKind) {
1689
- return constraints.find(
1690
- (c) => c.constraintKind === constraintKind
1691
- );
1787
+ return constraints.find((c) => c.constraintKind === constraintKind);
1692
1788
  }
1693
1789
  function findLength(constraints, constraintKind) {
1694
- return constraints.find(
1695
- (c) => c.constraintKind === constraintKind
1696
- );
1790
+ return constraints.find((c) => c.constraintKind === constraintKind);
1697
1791
  }
1698
1792
  function findAllowedMembers(constraints) {
1699
1793
  return constraints.filter(
@@ -1817,6 +1911,17 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1817
1911
  const isEnum = type.kind === "enum";
1818
1912
  const label = typeLabel(type);
1819
1913
  for (const constraint of constraints) {
1914
+ if (constraint.path) {
1915
+ const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1916
+ if (!isTraversable) {
1917
+ addTypeMismatch(
1918
+ ctx,
1919
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
1920
+ constraint.provenance
1921
+ );
1922
+ }
1923
+ continue;
1924
+ }
1820
1925
  const ck = constraint.constraintKind;
1821
1926
  switch (ck) {
1822
1927
  case "minimum":