@formspec/build 0.1.0-alpha.12 → 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 (42) 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__/guards.test.d.ts +2 -0
  9. package/dist/__tests__/guards.test.d.ts.map +1 -0
  10. package/dist/__tests__/json-utils.test.d.ts +5 -0
  11. package/dist/__tests__/json-utils.test.d.ts.map +1 -0
  12. package/dist/__tests__/path-target-parser.test.d.ts +9 -0
  13. package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
  14. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  15. package/dist/analyzer/jsdoc-constraints.d.ts +2 -2
  16. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  17. package/dist/analyzer/json-utils.d.ts +22 -0
  18. package/dist/analyzer/json-utils.d.ts.map +1 -0
  19. package/dist/analyzer/tsdoc-parser.d.ts +18 -4
  20. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  21. package/dist/browser.cjs +115 -8
  22. package/dist/browser.cjs.map +1 -1
  23. package/dist/browser.js +115 -8
  24. package/dist/browser.js.map +1 -1
  25. package/dist/build.d.ts +1 -0
  26. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
  27. package/dist/cli.cjs +179 -42
  28. package/dist/cli.cjs.map +1 -1
  29. package/dist/cli.js +184 -42
  30. package/dist/cli.js.map +1 -1
  31. package/dist/index.cjs +173 -41
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +178 -42
  34. package/dist/index.js.map +1 -1
  35. package/dist/internals.cjs +186 -47
  36. package/dist/internals.cjs.map +1 -1
  37. package/dist/internals.js +191 -48
  38. package/dist/internals.js.map +1 -1
  39. package/dist/json-schema/ir-generator.d.ts +1 -0
  40. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  41. package/dist/validate/constraint-validator.d.ts.map +1 -1
  42. package/package.json +3 -3
package/dist/internals.js CHANGED
@@ -66,11 +66,40 @@ function canonicalizeField(field) {
66
66
  }
67
67
  function canonicalizeTextField(field) {
68
68
  const type = { kind: "primitive", primitiveKind: "string" };
69
+ const constraints = [];
70
+ if (field.minLength !== void 0) {
71
+ const c = {
72
+ kind: "constraint",
73
+ constraintKind: "minLength",
74
+ value: field.minLength,
75
+ provenance: CHAIN_DSL_PROVENANCE
76
+ };
77
+ constraints.push(c);
78
+ }
79
+ if (field.maxLength !== void 0) {
80
+ const c = {
81
+ kind: "constraint",
82
+ constraintKind: "maxLength",
83
+ value: field.maxLength,
84
+ provenance: CHAIN_DSL_PROVENANCE
85
+ };
86
+ constraints.push(c);
87
+ }
88
+ if (field.pattern !== void 0) {
89
+ const c = {
90
+ kind: "constraint",
91
+ constraintKind: "pattern",
92
+ pattern: field.pattern,
93
+ provenance: CHAIN_DSL_PROVENANCE
94
+ };
95
+ constraints.push(c);
96
+ }
69
97
  return buildFieldNode(
70
98
  field.name,
71
99
  type,
72
100
  field.required,
73
- buildAnnotations(field.label, field.placeholder)
101
+ buildAnnotations(field.label, field.placeholder),
102
+ constraints
74
103
  );
75
104
  }
76
105
  function canonicalizeNumberField(field) {
@@ -94,6 +123,15 @@ function canonicalizeNumberField(field) {
94
123
  };
95
124
  constraints.push(c);
96
125
  }
126
+ if (field.multipleOf !== void 0) {
127
+ const c = {
128
+ kind: "constraint",
129
+ constraintKind: "multipleOf",
130
+ value: field.multipleOf,
131
+ provenance: CHAIN_DSL_PROVENANCE
132
+ };
133
+ constraints.push(c);
134
+ }
97
135
  return buildFieldNode(
98
136
  field.name,
99
137
  type,
@@ -423,7 +461,9 @@ import * as ts4 from "typescript";
423
461
  // src/analyzer/jsdoc-constraints.ts
424
462
  import * as ts3 from "typescript";
425
463
  import {
426
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
464
+ BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
465
+ isBuiltinConstraintName as isBuiltinConstraintName2,
466
+ normalizeConstraintTagName as normalizeConstraintTagName2
427
467
  } from "@formspec/core";
428
468
 
429
469
  // src/analyzer/tsdoc-parser.ts
@@ -438,22 +478,35 @@ import {
438
478
  TextRange
439
479
  } from "@microsoft/tsdoc";
440
480
  import {
441
- BUILTIN_CONSTRAINT_DEFINITIONS
481
+ BUILTIN_CONSTRAINT_DEFINITIONS,
482
+ normalizeConstraintTagName,
483
+ isBuiltinConstraintName
442
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
443
496
  var NUMERIC_CONSTRAINT_MAP = {
444
- Minimum: "minimum",
445
- Maximum: "maximum",
446
- ExclusiveMinimum: "exclusiveMinimum",
447
- ExclusiveMaximum: "exclusiveMaximum"
497
+ minimum: "minimum",
498
+ maximum: "maximum",
499
+ exclusiveMinimum: "exclusiveMinimum",
500
+ exclusiveMaximum: "exclusiveMaximum",
501
+ multipleOf: "multipleOf"
448
502
  };
449
503
  var LENGTH_CONSTRAINT_MAP = {
450
- MinLength: "minLength",
451
- MaxLength: "maxLength"
504
+ minLength: "minLength",
505
+ maxLength: "maxLength",
506
+ minItems: "minItems",
507
+ maxItems: "maxItems"
452
508
  };
453
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
454
- function isBuiltinConstraintName(tagName) {
455
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
456
- }
509
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
457
510
  function createFormSpecTSDocConfig() {
458
511
  const config = new TSDocConfiguration();
459
512
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -493,7 +546,7 @@ function parseTSDocTags(node, file = "") {
493
546
  );
494
547
  const docComment = parserContext.docComment;
495
548
  for (const block of docComment.customBlocks) {
496
- const tagName = block.blockTag.tagName.substring(1);
549
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
497
550
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
498
551
  const text = extractBlockText(block).trim();
499
552
  if (text === "") continue;
@@ -514,7 +567,7 @@ function parseTSDocTags(node, file = "") {
514
567
  }
515
568
  const jsDocTagsAll = ts2.getJSDocTags(node);
516
569
  for (const tag of jsDocTagsAll) {
517
- const tagName = tag.tagName.text;
570
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
518
571
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
519
572
  const commentText = getTagCommentText(tag);
520
573
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -562,6 +615,15 @@ function parseTSDocTags(node, file = "") {
562
615
  }
563
616
  return { constraints, annotations };
564
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
+ }
565
627
  function extractBlockText(block) {
566
628
  return extractPlainText(block.content);
567
629
  }
@@ -584,9 +646,12 @@ function parseConstraintValue(tagName, text, provenance) {
584
646
  if (!isBuiltinConstraintName(tagName)) {
585
647
  return null;
586
648
  }
649
+ const pathResult = extractPathTarget(text);
650
+ const effectiveText = pathResult ? pathResult.remainingText : text;
651
+ const path2 = pathResult?.path;
587
652
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
588
653
  if (expectedType === "number") {
589
- const value = Number(text);
654
+ const value = Number(effectiveText);
590
655
  if (Number.isNaN(value)) {
591
656
  return null;
592
657
  }
@@ -596,6 +661,7 @@ function parseConstraintValue(tagName, text, provenance) {
596
661
  kind: "constraint",
597
662
  constraintKind: numericKind,
598
663
  value,
664
+ ...path2 && { path: path2 },
599
665
  provenance
600
666
  };
601
667
  }
@@ -605,42 +671,41 @@ function parseConstraintValue(tagName, text, provenance) {
605
671
  kind: "constraint",
606
672
  constraintKind: lengthKind,
607
673
  value,
674
+ ...path2 && { path: path2 },
608
675
  provenance
609
676
  };
610
677
  }
611
678
  return null;
612
679
  }
613
680
  if (expectedType === "json") {
614
- try {
615
- const parsed = JSON.parse(text);
616
- if (!Array.isArray(parsed)) {
617
- return null;
618
- }
619
- const members = [];
620
- for (const item of parsed) {
621
- if (typeof item === "string" || typeof item === "number") {
622
- members.push(item);
623
- } else if (typeof item === "object" && item !== null && "id" in item) {
624
- const id = item["id"];
625
- if (typeof id === "string" || typeof id === "number") {
626
- members.push(id);
627
- }
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);
628
693
  }
629
694
  }
630
- return {
631
- kind: "constraint",
632
- constraintKind: "allowedMembers",
633
- members,
634
- provenance
635
- };
636
- } catch {
637
- return null;
638
695
  }
696
+ return {
697
+ kind: "constraint",
698
+ constraintKind: "allowedMembers",
699
+ members,
700
+ ...path2 && { path: path2 },
701
+ provenance
702
+ };
639
703
  }
640
704
  return {
641
705
  kind: "constraint",
642
706
  constraintKind: "pattern",
643
- pattern: text,
707
+ pattern: effectiveText,
708
+ ...path2 && { path: path2 },
644
709
  provenance
645
710
  };
646
711
  }
@@ -1071,14 +1136,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1071
1136
  }
1072
1137
  return map;
1073
1138
  }
1074
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1139
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1140
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1075
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
+ }
1076
1148
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1077
1149
  if (!symbol?.declarations) return [];
1078
1150
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1079
1151
  if (!aliasDecl) return [];
1080
1152
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1081
- return extractJSDocConstraintNodes(aliasDecl, file);
1153
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1154
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1155
+ return constraints;
1082
1156
  }
1083
1157
  function provenanceForNode(node, file) {
1084
1158
  const sourceFile = node.getSourceFile();
@@ -1200,8 +1274,70 @@ function collectFields(elements, properties, required, ctx) {
1200
1274
  }
1201
1275
  function generateFieldSchema(field, ctx) {
1202
1276
  const schema = generateTypeNode(field.type, ctx);
1203
- 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);
1204
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
+ }
1205
1341
  return schema;
1206
1342
  }
1207
1343
  function generateTypeNode(type, ctx) {
@@ -1648,14 +1784,10 @@ function addUnknownExtension(ctx, message, primary) {
1648
1784
  });
1649
1785
  }
1650
1786
  function findNumeric(constraints, constraintKind) {
1651
- return constraints.find(
1652
- (c) => c.constraintKind === constraintKind
1653
- );
1787
+ return constraints.find((c) => c.constraintKind === constraintKind);
1654
1788
  }
1655
1789
  function findLength(constraints, constraintKind) {
1656
- return constraints.find(
1657
- (c) => c.constraintKind === constraintKind
1658
- );
1790
+ return constraints.find((c) => c.constraintKind === constraintKind);
1659
1791
  }
1660
1792
  function findAllowedMembers(constraints) {
1661
1793
  return constraints.filter(
@@ -1779,6 +1911,17 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1779
1911
  const isEnum = type.kind === "enum";
1780
1912
  const label = typeLabel(type);
1781
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
+ }
1782
1925
  const ck = constraint.constraintKind;
1783
1926
  switch (ck) {
1784
1927
  case "minimum":