@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
@@ -117,11 +117,40 @@ function canonicalizeField(field) {
117
117
  }
118
118
  function canonicalizeTextField(field) {
119
119
  const type = { kind: "primitive", primitiveKind: "string" };
120
+ const constraints = [];
121
+ if (field.minLength !== void 0) {
122
+ const c = {
123
+ kind: "constraint",
124
+ constraintKind: "minLength",
125
+ value: field.minLength,
126
+ provenance: CHAIN_DSL_PROVENANCE
127
+ };
128
+ constraints.push(c);
129
+ }
130
+ if (field.maxLength !== void 0) {
131
+ const c = {
132
+ kind: "constraint",
133
+ constraintKind: "maxLength",
134
+ value: field.maxLength,
135
+ provenance: CHAIN_DSL_PROVENANCE
136
+ };
137
+ constraints.push(c);
138
+ }
139
+ if (field.pattern !== void 0) {
140
+ const c = {
141
+ kind: "constraint",
142
+ constraintKind: "pattern",
143
+ pattern: field.pattern,
144
+ provenance: CHAIN_DSL_PROVENANCE
145
+ };
146
+ constraints.push(c);
147
+ }
120
148
  return buildFieldNode(
121
149
  field.name,
122
150
  type,
123
151
  field.required,
124
- buildAnnotations(field.label, field.placeholder)
152
+ buildAnnotations(field.label, field.placeholder),
153
+ constraints
125
154
  );
126
155
  }
127
156
  function canonicalizeNumberField(field) {
@@ -145,6 +174,15 @@ function canonicalizeNumberField(field) {
145
174
  };
146
175
  constraints.push(c);
147
176
  }
177
+ if (field.multipleOf !== void 0) {
178
+ const c = {
179
+ kind: "constraint",
180
+ constraintKind: "multipleOf",
181
+ value: field.multipleOf,
182
+ provenance: CHAIN_DSL_PROVENANCE
183
+ };
184
+ constraints.push(c);
185
+ }
148
186
  return buildFieldNode(
149
187
  field.name,
150
188
  type,
@@ -479,20 +517,31 @@ var import_core4 = require("@formspec/core");
479
517
  var ts2 = __toESM(require("typescript"), 1);
480
518
  var import_tsdoc = require("@microsoft/tsdoc");
481
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
482
531
  var NUMERIC_CONSTRAINT_MAP = {
483
- Minimum: "minimum",
484
- Maximum: "maximum",
485
- ExclusiveMinimum: "exclusiveMinimum",
486
- ExclusiveMaximum: "exclusiveMaximum"
532
+ minimum: "minimum",
533
+ maximum: "maximum",
534
+ exclusiveMinimum: "exclusiveMinimum",
535
+ exclusiveMaximum: "exclusiveMaximum",
536
+ multipleOf: "multipleOf"
487
537
  };
488
538
  var LENGTH_CONSTRAINT_MAP = {
489
- MinLength: "minLength",
490
- MaxLength: "maxLength"
539
+ minLength: "minLength",
540
+ maxLength: "maxLength",
541
+ minItems: "minItems",
542
+ maxItems: "maxItems"
491
543
  };
492
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
493
- function isBuiltinConstraintName(tagName) {
494
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
495
- }
544
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
496
545
  function createFormSpecTSDocConfig() {
497
546
  const config = new import_tsdoc.TSDocConfiguration();
498
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -532,7 +581,7 @@ function parseTSDocTags(node, file = "") {
532
581
  );
533
582
  const docComment = parserContext.docComment;
534
583
  for (const block of docComment.customBlocks) {
535
- const tagName = block.blockTag.tagName.substring(1);
584
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
536
585
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
537
586
  const text = extractBlockText(block).trim();
538
587
  if (text === "") continue;
@@ -553,7 +602,7 @@ function parseTSDocTags(node, file = "") {
553
602
  }
554
603
  const jsDocTagsAll = ts2.getJSDocTags(node);
555
604
  for (const tag of jsDocTagsAll) {
556
- const tagName = tag.tagName.text;
605
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
557
606
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
558
607
  const commentText = getTagCommentText(tag);
559
608
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -601,6 +650,15 @@ function parseTSDocTags(node, file = "") {
601
650
  }
602
651
  return { constraints, annotations };
603
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
+ }
604
662
  function extractBlockText(block) {
605
663
  return extractPlainText(block.content);
606
664
  }
@@ -620,12 +678,15 @@ function extractPlainText(node) {
620
678
  return result;
621
679
  }
622
680
  function parseConstraintValue(tagName, text, provenance) {
623
- if (!isBuiltinConstraintName(tagName)) {
681
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
624
682
  return null;
625
683
  }
684
+ const pathResult = extractPathTarget(text);
685
+ const effectiveText = pathResult ? pathResult.remainingText : text;
686
+ const path2 = pathResult?.path;
626
687
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
627
688
  if (expectedType === "number") {
628
- const value = Number(text);
689
+ const value = Number(effectiveText);
629
690
  if (Number.isNaN(value)) {
630
691
  return null;
631
692
  }
@@ -635,6 +696,7 @@ function parseConstraintValue(tagName, text, provenance) {
635
696
  kind: "constraint",
636
697
  constraintKind: numericKind,
637
698
  value,
699
+ ...path2 && { path: path2 },
638
700
  provenance
639
701
  };
640
702
  }
@@ -644,42 +706,41 @@ function parseConstraintValue(tagName, text, provenance) {
644
706
  kind: "constraint",
645
707
  constraintKind: lengthKind,
646
708
  value,
709
+ ...path2 && { path: path2 },
647
710
  provenance
648
711
  };
649
712
  }
650
713
  return null;
651
714
  }
652
715
  if (expectedType === "json") {
653
- try {
654
- const parsed = JSON.parse(text);
655
- if (!Array.isArray(parsed)) {
656
- return null;
657
- }
658
- const members = [];
659
- for (const item of parsed) {
660
- if (typeof item === "string" || typeof item === "number") {
661
- members.push(item);
662
- } else if (typeof item === "object" && item !== null && "id" in item) {
663
- const id = item["id"];
664
- if (typeof id === "string" || typeof id === "number") {
665
- members.push(id);
666
- }
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);
667
728
  }
668
729
  }
669
- return {
670
- kind: "constraint",
671
- constraintKind: "allowedMembers",
672
- members,
673
- provenance
674
- };
675
- } catch {
676
- return null;
677
730
  }
731
+ return {
732
+ kind: "constraint",
733
+ constraintKind: "allowedMembers",
734
+ members,
735
+ ...path2 && { path: path2 },
736
+ provenance
737
+ };
678
738
  }
679
739
  return {
680
740
  kind: "constraint",
681
741
  constraintKind: "pattern",
682
- pattern: text,
742
+ pattern: effectiveText,
743
+ ...path2 && { path: path2 },
683
744
  provenance
684
745
  };
685
746
  }
@@ -1110,14 +1171,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1110
1171
  }
1111
1172
  return map;
1112
1173
  }
1113
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1174
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1175
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1114
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
+ }
1115
1183
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1116
1184
  if (!symbol?.declarations) return [];
1117
1185
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1118
1186
  if (!aliasDecl) return [];
1119
1187
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1120
- return extractJSDocConstraintNodes(aliasDecl, file);
1188
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1189
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1190
+ return constraints;
1121
1191
  }
1122
1192
  function provenanceForNode(node, file) {
1123
1193
  const sourceFile = node.getSourceFile();
@@ -1239,8 +1309,70 @@ function collectFields(elements, properties, required, ctx) {
1239
1309
  }
1240
1310
  function generateFieldSchema(field, ctx) {
1241
1311
  const schema = generateTypeNode(field.type, ctx);
1242
- 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);
1243
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
+ }
1244
1376
  return schema;
1245
1377
  }
1246
1378
  function generateTypeNode(type, ctx) {
@@ -1687,14 +1819,10 @@ function addUnknownExtension(ctx, message, primary) {
1687
1819
  });
1688
1820
  }
1689
1821
  function findNumeric(constraints, constraintKind) {
1690
- return constraints.find(
1691
- (c) => c.constraintKind === constraintKind
1692
- );
1822
+ return constraints.find((c) => c.constraintKind === constraintKind);
1693
1823
  }
1694
1824
  function findLength(constraints, constraintKind) {
1695
- return constraints.find(
1696
- (c) => c.constraintKind === constraintKind
1697
- );
1825
+ return constraints.find((c) => c.constraintKind === constraintKind);
1698
1826
  }
1699
1827
  function findAllowedMembers(constraints) {
1700
1828
  return constraints.filter(
@@ -1818,6 +1946,17 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1818
1946
  const isEnum = type.kind === "enum";
1819
1947
  const label = typeLabel(type);
1820
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
+ }
1821
1960
  const ck = constraint.constraintKind;
1822
1961
  switch (ck) {
1823
1962
  case "minimum":