@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/index.js CHANGED
@@ -431,8 +431,70 @@ function collectFields(elements, properties, required, ctx) {
431
431
  }
432
432
  function generateFieldSchema(field, ctx) {
433
433
  const schema = generateTypeNode(field.type, ctx);
434
- applyConstraints(schema, field.constraints);
434
+ const directConstraints = [];
435
+ const pathConstraints = [];
436
+ for (const c of field.constraints) {
437
+ if (c.path) {
438
+ pathConstraints.push(c);
439
+ } else {
440
+ directConstraints.push(c);
441
+ }
442
+ }
443
+ applyConstraints(schema, directConstraints);
435
444
  applyAnnotations(schema, field.annotations);
445
+ if (pathConstraints.length === 0) {
446
+ return schema;
447
+ }
448
+ return applyPathTargetedConstraints(schema, pathConstraints);
449
+ }
450
+ function applyPathTargetedConstraints(schema, pathConstraints) {
451
+ if (schema.type === "array" && schema.items) {
452
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
453
+ return schema;
454
+ }
455
+ const byTarget = /* @__PURE__ */ new Map();
456
+ for (const c of pathConstraints) {
457
+ const target = c.path?.segments[0];
458
+ if (!target) continue;
459
+ const group = byTarget.get(target) ?? [];
460
+ group.push(c);
461
+ byTarget.set(target, group);
462
+ }
463
+ const propertyOverrides = {};
464
+ for (const [target, constraints] of byTarget) {
465
+ const subSchema = {};
466
+ applyConstraints(subSchema, constraints);
467
+ propertyOverrides[target] = subSchema;
468
+ }
469
+ if (schema.$ref) {
470
+ const { $ref, ...rest } = schema;
471
+ const refPart = { $ref };
472
+ const overridePart = {
473
+ properties: propertyOverrides,
474
+ ...rest
475
+ };
476
+ return { allOf: [refPart, overridePart] };
477
+ }
478
+ if (schema.type === "object" && schema.properties) {
479
+ const missingOverrides = {};
480
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
481
+ if (schema.properties[target]) {
482
+ Object.assign(schema.properties[target], overrideSchema);
483
+ } else {
484
+ missingOverrides[target] = overrideSchema;
485
+ }
486
+ }
487
+ if (Object.keys(missingOverrides).length === 0) {
488
+ return schema;
489
+ }
490
+ return {
491
+ allOf: [schema, { properties: missingOverrides }]
492
+ };
493
+ }
494
+ if (schema.allOf) {
495
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
496
+ return schema;
497
+ }
436
498
  return schema;
437
499
  }
438
500
  function generateTypeNode(type, ctx) {
@@ -1002,7 +1064,9 @@ import * as ts4 from "typescript";
1002
1064
  // src/analyzer/jsdoc-constraints.ts
1003
1065
  import * as ts3 from "typescript";
1004
1066
  import {
1005
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
1067
+ BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
1068
+ isBuiltinConstraintName as isBuiltinConstraintName2,
1069
+ normalizeConstraintTagName as normalizeConstraintTagName2
1006
1070
  } from "@formspec/core";
1007
1071
 
1008
1072
  // src/analyzer/tsdoc-parser.ts
@@ -1017,22 +1081,35 @@ import {
1017
1081
  TextRange
1018
1082
  } from "@microsoft/tsdoc";
1019
1083
  import {
1020
- BUILTIN_CONSTRAINT_DEFINITIONS
1084
+ BUILTIN_CONSTRAINT_DEFINITIONS,
1085
+ normalizeConstraintTagName,
1086
+ isBuiltinConstraintName
1021
1087
  } from "@formspec/core";
1088
+
1089
+ // src/analyzer/json-utils.ts
1090
+ function tryParseJson(text) {
1091
+ try {
1092
+ return JSON.parse(text);
1093
+ } catch {
1094
+ return null;
1095
+ }
1096
+ }
1097
+
1098
+ // src/analyzer/tsdoc-parser.ts
1022
1099
  var NUMERIC_CONSTRAINT_MAP = {
1023
- Minimum: "minimum",
1024
- Maximum: "maximum",
1025
- ExclusiveMinimum: "exclusiveMinimum",
1026
- ExclusiveMaximum: "exclusiveMaximum"
1100
+ minimum: "minimum",
1101
+ maximum: "maximum",
1102
+ exclusiveMinimum: "exclusiveMinimum",
1103
+ exclusiveMaximum: "exclusiveMaximum",
1104
+ multipleOf: "multipleOf"
1027
1105
  };
1028
1106
  var LENGTH_CONSTRAINT_MAP = {
1029
- MinLength: "minLength",
1030
- MaxLength: "maxLength"
1107
+ minLength: "minLength",
1108
+ maxLength: "maxLength",
1109
+ minItems: "minItems",
1110
+ maxItems: "maxItems"
1031
1111
  };
1032
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1033
- function isBuiltinConstraintName(tagName) {
1034
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
1035
- }
1112
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1036
1113
  function createFormSpecTSDocConfig() {
1037
1114
  const config = new TSDocConfiguration();
1038
1115
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1072,7 +1149,7 @@ function parseTSDocTags(node, file = "") {
1072
1149
  );
1073
1150
  const docComment = parserContext.docComment;
1074
1151
  for (const block of docComment.customBlocks) {
1075
- const tagName = block.blockTag.tagName.substring(1);
1152
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1076
1153
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1077
1154
  const text = extractBlockText(block).trim();
1078
1155
  if (text === "") continue;
@@ -1093,7 +1170,7 @@ function parseTSDocTags(node, file = "") {
1093
1170
  }
1094
1171
  const jsDocTagsAll = ts2.getJSDocTags(node);
1095
1172
  for (const tag of jsDocTagsAll) {
1096
- const tagName = tag.tagName.text;
1173
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
1097
1174
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1098
1175
  const commentText = getTagCommentText(tag);
1099
1176
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1141,6 +1218,15 @@ function parseTSDocTags(node, file = "") {
1141
1218
  }
1142
1219
  return { constraints, annotations };
1143
1220
  }
1221
+ function extractPathTarget(text) {
1222
+ const trimmed = text.trimStart();
1223
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
1224
+ if (!match?.[1] || !match[2]) return null;
1225
+ return {
1226
+ path: { segments: [match[1]] },
1227
+ remainingText: match[2]
1228
+ };
1229
+ }
1144
1230
  function extractBlockText(block) {
1145
1231
  return extractPlainText(block.content);
1146
1232
  }
@@ -1163,9 +1249,12 @@ function parseConstraintValue(tagName, text, provenance) {
1163
1249
  if (!isBuiltinConstraintName(tagName)) {
1164
1250
  return null;
1165
1251
  }
1252
+ const pathResult = extractPathTarget(text);
1253
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1254
+ const path3 = pathResult?.path;
1166
1255
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1167
1256
  if (expectedType === "number") {
1168
- const value = Number(text);
1257
+ const value = Number(effectiveText);
1169
1258
  if (Number.isNaN(value)) {
1170
1259
  return null;
1171
1260
  }
@@ -1175,6 +1264,7 @@ function parseConstraintValue(tagName, text, provenance) {
1175
1264
  kind: "constraint",
1176
1265
  constraintKind: numericKind,
1177
1266
  value,
1267
+ ...path3 && { path: path3 },
1178
1268
  provenance
1179
1269
  };
1180
1270
  }
@@ -1184,42 +1274,41 @@ function parseConstraintValue(tagName, text, provenance) {
1184
1274
  kind: "constraint",
1185
1275
  constraintKind: lengthKind,
1186
1276
  value,
1277
+ ...path3 && { path: path3 },
1187
1278
  provenance
1188
1279
  };
1189
1280
  }
1190
1281
  return null;
1191
1282
  }
1192
1283
  if (expectedType === "json") {
1193
- try {
1194
- const parsed = JSON.parse(text);
1195
- if (!Array.isArray(parsed)) {
1196
- return null;
1197
- }
1198
- const members = [];
1199
- for (const item of parsed) {
1200
- if (typeof item === "string" || typeof item === "number") {
1201
- members.push(item);
1202
- } else if (typeof item === "object" && item !== null && "id" in item) {
1203
- const id = item["id"];
1204
- if (typeof id === "string" || typeof id === "number") {
1205
- members.push(id);
1206
- }
1284
+ const parsed = tryParseJson(effectiveText);
1285
+ if (!Array.isArray(parsed)) {
1286
+ return null;
1287
+ }
1288
+ const members = [];
1289
+ for (const item of parsed) {
1290
+ if (typeof item === "string" || typeof item === "number") {
1291
+ members.push(item);
1292
+ } else if (typeof item === "object" && item !== null && "id" in item) {
1293
+ const id = item["id"];
1294
+ if (typeof id === "string" || typeof id === "number") {
1295
+ members.push(id);
1207
1296
  }
1208
1297
  }
1209
- return {
1210
- kind: "constraint",
1211
- constraintKind: "allowedMembers",
1212
- members,
1213
- provenance
1214
- };
1215
- } catch {
1216
- return null;
1217
1298
  }
1299
+ return {
1300
+ kind: "constraint",
1301
+ constraintKind: "allowedMembers",
1302
+ members,
1303
+ ...path3 && { path: path3 },
1304
+ provenance
1305
+ };
1218
1306
  }
1219
1307
  return {
1220
1308
  kind: "constraint",
1221
1309
  constraintKind: "pattern",
1222
- pattern: text,
1310
+ pattern: effectiveText,
1311
+ ...path3 && { path: path3 },
1223
1312
  provenance
1224
1313
  };
1225
1314
  }
@@ -1650,14 +1739,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1650
1739
  }
1651
1740
  return map;
1652
1741
  }
1653
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1742
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1743
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1654
1744
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1745
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1746
+ const aliasName = typeNode.typeName.getText();
1747
+ throw new Error(
1748
+ `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.`
1749
+ );
1750
+ }
1655
1751
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1656
1752
  if (!symbol?.declarations) return [];
1657
1753
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1658
1754
  if (!aliasDecl) return [];
1659
1755
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1660
- return extractJSDocConstraintNodes(aliasDecl, file);
1756
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1757
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1758
+ return constraints;
1661
1759
  }
1662
1760
  function provenanceForNode(node, file) {
1663
1761
  const sourceFile = node.getSourceFile();