@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.cjs CHANGED
@@ -490,8 +490,70 @@ function collectFields(elements, properties, required, ctx) {
490
490
  }
491
491
  function generateFieldSchema(field, ctx) {
492
492
  const schema = generateTypeNode(field.type, ctx);
493
- applyConstraints(schema, field.constraints);
493
+ const directConstraints = [];
494
+ const pathConstraints = [];
495
+ for (const c of field.constraints) {
496
+ if (c.path) {
497
+ pathConstraints.push(c);
498
+ } else {
499
+ directConstraints.push(c);
500
+ }
501
+ }
502
+ applyConstraints(schema, directConstraints);
494
503
  applyAnnotations(schema, field.annotations);
504
+ if (pathConstraints.length === 0) {
505
+ return schema;
506
+ }
507
+ return applyPathTargetedConstraints(schema, pathConstraints);
508
+ }
509
+ function applyPathTargetedConstraints(schema, pathConstraints) {
510
+ if (schema.type === "array" && schema.items) {
511
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
512
+ return schema;
513
+ }
514
+ const byTarget = /* @__PURE__ */ new Map();
515
+ for (const c of pathConstraints) {
516
+ const target = c.path?.segments[0];
517
+ if (!target) continue;
518
+ const group = byTarget.get(target) ?? [];
519
+ group.push(c);
520
+ byTarget.set(target, group);
521
+ }
522
+ const propertyOverrides = {};
523
+ for (const [target, constraints] of byTarget) {
524
+ const subSchema = {};
525
+ applyConstraints(subSchema, constraints);
526
+ propertyOverrides[target] = subSchema;
527
+ }
528
+ if (schema.$ref) {
529
+ const { $ref, ...rest } = schema;
530
+ const refPart = { $ref };
531
+ const overridePart = {
532
+ properties: propertyOverrides,
533
+ ...rest
534
+ };
535
+ return { allOf: [refPart, overridePart] };
536
+ }
537
+ if (schema.type === "object" && schema.properties) {
538
+ const missingOverrides = {};
539
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
540
+ if (schema.properties[target]) {
541
+ Object.assign(schema.properties[target], overrideSchema);
542
+ } else {
543
+ missingOverrides[target] = overrideSchema;
544
+ }
545
+ }
546
+ if (Object.keys(missingOverrides).length === 0) {
547
+ return schema;
548
+ }
549
+ return {
550
+ allOf: [schema, { properties: missingOverrides }]
551
+ };
552
+ }
553
+ if (schema.allOf) {
554
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
555
+ return schema;
556
+ }
495
557
  return schema;
496
558
  }
497
559
  function generateTypeNode(type, ctx) {
@@ -1066,20 +1128,31 @@ var import_core4 = require("@formspec/core");
1066
1128
  var ts2 = __toESM(require("typescript"), 1);
1067
1129
  var import_tsdoc = require("@microsoft/tsdoc");
1068
1130
  var import_core3 = require("@formspec/core");
1131
+
1132
+ // src/analyzer/json-utils.ts
1133
+ function tryParseJson(text) {
1134
+ try {
1135
+ return JSON.parse(text);
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+
1141
+ // src/analyzer/tsdoc-parser.ts
1069
1142
  var NUMERIC_CONSTRAINT_MAP = {
1070
- Minimum: "minimum",
1071
- Maximum: "maximum",
1072
- ExclusiveMinimum: "exclusiveMinimum",
1073
- ExclusiveMaximum: "exclusiveMaximum"
1143
+ minimum: "minimum",
1144
+ maximum: "maximum",
1145
+ exclusiveMinimum: "exclusiveMinimum",
1146
+ exclusiveMaximum: "exclusiveMaximum",
1147
+ multipleOf: "multipleOf"
1074
1148
  };
1075
1149
  var LENGTH_CONSTRAINT_MAP = {
1076
- MinLength: "minLength",
1077
- MaxLength: "maxLength"
1150
+ minLength: "minLength",
1151
+ maxLength: "maxLength",
1152
+ minItems: "minItems",
1153
+ maxItems: "maxItems"
1078
1154
  };
1079
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1080
- function isBuiltinConstraintName(tagName) {
1081
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
1082
- }
1155
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1083
1156
  function createFormSpecTSDocConfig() {
1084
1157
  const config = new import_tsdoc.TSDocConfiguration();
1085
1158
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1119,7 +1192,7 @@ function parseTSDocTags(node, file = "") {
1119
1192
  );
1120
1193
  const docComment = parserContext.docComment;
1121
1194
  for (const block of docComment.customBlocks) {
1122
- const tagName = block.blockTag.tagName.substring(1);
1195
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
1123
1196
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1124
1197
  const text = extractBlockText(block).trim();
1125
1198
  if (text === "") continue;
@@ -1140,7 +1213,7 @@ function parseTSDocTags(node, file = "") {
1140
1213
  }
1141
1214
  const jsDocTagsAll = ts2.getJSDocTags(node);
1142
1215
  for (const tag of jsDocTagsAll) {
1143
- const tagName = tag.tagName.text;
1216
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
1144
1217
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1145
1218
  const commentText = getTagCommentText(tag);
1146
1219
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1188,6 +1261,15 @@ function parseTSDocTags(node, file = "") {
1188
1261
  }
1189
1262
  return { constraints, annotations };
1190
1263
  }
1264
+ function extractPathTarget(text) {
1265
+ const trimmed = text.trimStart();
1266
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
1267
+ if (!match?.[1] || !match[2]) return null;
1268
+ return {
1269
+ path: { segments: [match[1]] },
1270
+ remainingText: match[2]
1271
+ };
1272
+ }
1191
1273
  function extractBlockText(block) {
1192
1274
  return extractPlainText(block.content);
1193
1275
  }
@@ -1207,12 +1289,15 @@ function extractPlainText(node) {
1207
1289
  return result;
1208
1290
  }
1209
1291
  function parseConstraintValue(tagName, text, provenance) {
1210
- if (!isBuiltinConstraintName(tagName)) {
1292
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
1211
1293
  return null;
1212
1294
  }
1295
+ const pathResult = extractPathTarget(text);
1296
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1297
+ const path3 = pathResult?.path;
1213
1298
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1214
1299
  if (expectedType === "number") {
1215
- const value = Number(text);
1300
+ const value = Number(effectiveText);
1216
1301
  if (Number.isNaN(value)) {
1217
1302
  return null;
1218
1303
  }
@@ -1222,6 +1307,7 @@ function parseConstraintValue(tagName, text, provenance) {
1222
1307
  kind: "constraint",
1223
1308
  constraintKind: numericKind,
1224
1309
  value,
1310
+ ...path3 && { path: path3 },
1225
1311
  provenance
1226
1312
  };
1227
1313
  }
@@ -1231,42 +1317,41 @@ function parseConstraintValue(tagName, text, provenance) {
1231
1317
  kind: "constraint",
1232
1318
  constraintKind: lengthKind,
1233
1319
  value,
1320
+ ...path3 && { path: path3 },
1234
1321
  provenance
1235
1322
  };
1236
1323
  }
1237
1324
  return null;
1238
1325
  }
1239
1326
  if (expectedType === "json") {
1240
- try {
1241
- const parsed = JSON.parse(text);
1242
- if (!Array.isArray(parsed)) {
1243
- return null;
1244
- }
1245
- const members = [];
1246
- for (const item of parsed) {
1247
- if (typeof item === "string" || typeof item === "number") {
1248
- members.push(item);
1249
- } else if (typeof item === "object" && item !== null && "id" in item) {
1250
- const id = item["id"];
1251
- if (typeof id === "string" || typeof id === "number") {
1252
- members.push(id);
1253
- }
1327
+ const parsed = tryParseJson(effectiveText);
1328
+ if (!Array.isArray(parsed)) {
1329
+ return null;
1330
+ }
1331
+ const members = [];
1332
+ for (const item of parsed) {
1333
+ if (typeof item === "string" || typeof item === "number") {
1334
+ members.push(item);
1335
+ } else if (typeof item === "object" && item !== null && "id" in item) {
1336
+ const id = item["id"];
1337
+ if (typeof id === "string" || typeof id === "number") {
1338
+ members.push(id);
1254
1339
  }
1255
1340
  }
1256
- return {
1257
- kind: "constraint",
1258
- constraintKind: "allowedMembers",
1259
- members,
1260
- provenance
1261
- };
1262
- } catch {
1263
- return null;
1264
1341
  }
1342
+ return {
1343
+ kind: "constraint",
1344
+ constraintKind: "allowedMembers",
1345
+ members,
1346
+ ...path3 && { path: path3 },
1347
+ provenance
1348
+ };
1265
1349
  }
1266
1350
  return {
1267
1351
  kind: "constraint",
1268
1352
  constraintKind: "pattern",
1269
- pattern: text,
1353
+ pattern: effectiveText,
1354
+ ...path3 && { path: path3 },
1270
1355
  provenance
1271
1356
  };
1272
1357
  }
@@ -1697,14 +1782,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1697
1782
  }
1698
1783
  return map;
1699
1784
  }
1700
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1785
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1786
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1701
1787
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1788
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1789
+ const aliasName = typeNode.typeName.getText();
1790
+ throw new Error(
1791
+ `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.`
1792
+ );
1793
+ }
1702
1794
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1703
1795
  if (!symbol?.declarations) return [];
1704
1796
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1705
1797
  if (!aliasDecl) return [];
1706
1798
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1707
- return extractJSDocConstraintNodes(aliasDecl, file);
1799
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1800
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1801
+ return constraints;
1708
1802
  }
1709
1803
  function provenanceForNode(node, file) {
1710
1804
  const sourceFile = node.getSourceFile();