@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/cli.js CHANGED
@@ -71,11 +71,40 @@ function canonicalizeField(field) {
71
71
  }
72
72
  function canonicalizeTextField(field) {
73
73
  const type = { kind: "primitive", primitiveKind: "string" };
74
+ const constraints = [];
75
+ if (field.minLength !== void 0) {
76
+ const c = {
77
+ kind: "constraint",
78
+ constraintKind: "minLength",
79
+ value: field.minLength,
80
+ provenance: CHAIN_DSL_PROVENANCE
81
+ };
82
+ constraints.push(c);
83
+ }
84
+ if (field.maxLength !== void 0) {
85
+ const c = {
86
+ kind: "constraint",
87
+ constraintKind: "maxLength",
88
+ value: field.maxLength,
89
+ provenance: CHAIN_DSL_PROVENANCE
90
+ };
91
+ constraints.push(c);
92
+ }
93
+ if (field.pattern !== void 0) {
94
+ const c = {
95
+ kind: "constraint",
96
+ constraintKind: "pattern",
97
+ pattern: field.pattern,
98
+ provenance: CHAIN_DSL_PROVENANCE
99
+ };
100
+ constraints.push(c);
101
+ }
74
102
  return buildFieldNode(
75
103
  field.name,
76
104
  type,
77
105
  field.required,
78
- buildAnnotations(field.label, field.placeholder)
106
+ buildAnnotations(field.label, field.placeholder),
107
+ constraints
79
108
  );
80
109
  }
81
110
  function canonicalizeNumberField(field) {
@@ -99,6 +128,15 @@ function canonicalizeNumberField(field) {
99
128
  };
100
129
  constraints.push(c);
101
130
  }
131
+ if (field.multipleOf !== void 0) {
132
+ const c = {
133
+ kind: "constraint",
134
+ constraintKind: "multipleOf",
135
+ value: field.multipleOf,
136
+ provenance: CHAIN_DSL_PROVENANCE
137
+ };
138
+ constraints.push(c);
139
+ }
102
140
  return buildFieldNode(
103
141
  field.name,
104
142
  type,
@@ -424,8 +462,70 @@ function collectFields(elements, properties, required, ctx) {
424
462
  }
425
463
  function generateFieldSchema(field, ctx) {
426
464
  const schema = generateTypeNode(field.type, ctx);
427
- applyConstraints(schema, field.constraints);
465
+ const directConstraints = [];
466
+ const pathConstraints = [];
467
+ for (const c of field.constraints) {
468
+ if (c.path) {
469
+ pathConstraints.push(c);
470
+ } else {
471
+ directConstraints.push(c);
472
+ }
473
+ }
474
+ applyConstraints(schema, directConstraints);
428
475
  applyAnnotations(schema, field.annotations);
476
+ if (pathConstraints.length === 0) {
477
+ return schema;
478
+ }
479
+ return applyPathTargetedConstraints(schema, pathConstraints);
480
+ }
481
+ function applyPathTargetedConstraints(schema, pathConstraints) {
482
+ if (schema.type === "array" && schema.items) {
483
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
484
+ return schema;
485
+ }
486
+ const byTarget = /* @__PURE__ */ new Map();
487
+ for (const c of pathConstraints) {
488
+ const target = c.path?.segments[0];
489
+ if (!target) continue;
490
+ const group = byTarget.get(target) ?? [];
491
+ group.push(c);
492
+ byTarget.set(target, group);
493
+ }
494
+ const propertyOverrides = {};
495
+ for (const [target, constraints] of byTarget) {
496
+ const subSchema = {};
497
+ applyConstraints(subSchema, constraints);
498
+ propertyOverrides[target] = subSchema;
499
+ }
500
+ if (schema.$ref) {
501
+ const { $ref, ...rest } = schema;
502
+ const refPart = { $ref };
503
+ const overridePart = {
504
+ properties: propertyOverrides,
505
+ ...rest
506
+ };
507
+ return { allOf: [refPart, overridePart] };
508
+ }
509
+ if (schema.type === "object" && schema.properties) {
510
+ const missingOverrides = {};
511
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
512
+ if (schema.properties[target]) {
513
+ Object.assign(schema.properties[target], overrideSchema);
514
+ } else {
515
+ missingOverrides[target] = overrideSchema;
516
+ }
517
+ }
518
+ if (Object.keys(missingOverrides).length === 0) {
519
+ return schema;
520
+ }
521
+ return {
522
+ allOf: [schema, { properties: missingOverrides }]
523
+ };
524
+ }
525
+ if (schema.allOf) {
526
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
527
+ return schema;
528
+ }
429
529
  return schema;
430
530
  }
431
531
  function generateTypeNode(type, ctx) {
@@ -1032,6 +1132,20 @@ var init_program = __esm({
1032
1132
  }
1033
1133
  });
1034
1134
 
1135
+ // src/analyzer/json-utils.ts
1136
+ function tryParseJson(text) {
1137
+ try {
1138
+ return JSON.parse(text);
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+ var init_json_utils = __esm({
1144
+ "src/analyzer/json-utils.ts"() {
1145
+ "use strict";
1146
+ }
1147
+ });
1148
+
1035
1149
  // src/analyzer/tsdoc-parser.ts
1036
1150
  import * as ts2 from "typescript";
1037
1151
  import {
@@ -1044,11 +1158,10 @@ import {
1044
1158
  TextRange
1045
1159
  } from "@microsoft/tsdoc";
1046
1160
  import {
1047
- BUILTIN_CONSTRAINT_DEFINITIONS
1161
+ BUILTIN_CONSTRAINT_DEFINITIONS,
1162
+ normalizeConstraintTagName,
1163
+ isBuiltinConstraintName
1048
1164
  } from "@formspec/core";
1049
- function isBuiltinConstraintName(tagName) {
1050
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
1051
- }
1052
1165
  function createFormSpecTSDocConfig() {
1053
1166
  const config = new TSDocConfiguration();
1054
1167
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1087,7 +1200,7 @@ function parseTSDocTags(node, file = "") {
1087
1200
  );
1088
1201
  const docComment = parserContext.docComment;
1089
1202
  for (const block of docComment.customBlocks) {
1090
- const tagName = block.blockTag.tagName.substring(1);
1203
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1091
1204
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1092
1205
  const text = extractBlockText(block).trim();
1093
1206
  if (text === "") continue;
@@ -1108,7 +1221,7 @@ function parseTSDocTags(node, file = "") {
1108
1221
  }
1109
1222
  const jsDocTagsAll = ts2.getJSDocTags(node);
1110
1223
  for (const tag of jsDocTagsAll) {
1111
- const tagName = tag.tagName.text;
1224
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
1112
1225
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1113
1226
  const commentText = getTagCommentText(tag);
1114
1227
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1156,6 +1269,15 @@ function parseTSDocTags(node, file = "") {
1156
1269
  }
1157
1270
  return { constraints, annotations };
1158
1271
  }
1272
+ function extractPathTarget(text) {
1273
+ const trimmed = text.trimStart();
1274
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
1275
+ if (!match?.[1] || !match[2]) return null;
1276
+ return {
1277
+ path: { segments: [match[1]] },
1278
+ remainingText: match[2]
1279
+ };
1280
+ }
1159
1281
  function extractBlockText(block) {
1160
1282
  return extractPlainText(block.content);
1161
1283
  }
@@ -1178,9 +1300,12 @@ function parseConstraintValue(tagName, text, provenance) {
1178
1300
  if (!isBuiltinConstraintName(tagName)) {
1179
1301
  return null;
1180
1302
  }
1303
+ const pathResult = extractPathTarget(text);
1304
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1305
+ const path4 = pathResult?.path;
1181
1306
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1182
1307
  if (expectedType === "number") {
1183
- const value = Number(text);
1308
+ const value = Number(effectiveText);
1184
1309
  if (Number.isNaN(value)) {
1185
1310
  return null;
1186
1311
  }
@@ -1190,6 +1315,7 @@ function parseConstraintValue(tagName, text, provenance) {
1190
1315
  kind: "constraint",
1191
1316
  constraintKind: numericKind,
1192
1317
  value,
1318
+ ...path4 && { path: path4 },
1193
1319
  provenance
1194
1320
  };
1195
1321
  }
@@ -1199,42 +1325,41 @@ function parseConstraintValue(tagName, text, provenance) {
1199
1325
  kind: "constraint",
1200
1326
  constraintKind: lengthKind,
1201
1327
  value,
1328
+ ...path4 && { path: path4 },
1202
1329
  provenance
1203
1330
  };
1204
1331
  }
1205
1332
  return null;
1206
1333
  }
1207
1334
  if (expectedType === "json") {
1208
- try {
1209
- const parsed = JSON.parse(text);
1210
- if (!Array.isArray(parsed)) {
1211
- return null;
1212
- }
1213
- const members = [];
1214
- for (const item of parsed) {
1215
- if (typeof item === "string" || typeof item === "number") {
1216
- members.push(item);
1217
- } else if (typeof item === "object" && item !== null && "id" in item) {
1218
- const id = item["id"];
1219
- if (typeof id === "string" || typeof id === "number") {
1220
- members.push(id);
1221
- }
1335
+ const parsed = tryParseJson(effectiveText);
1336
+ if (!Array.isArray(parsed)) {
1337
+ return null;
1338
+ }
1339
+ const members = [];
1340
+ for (const item of parsed) {
1341
+ if (typeof item === "string" || typeof item === "number") {
1342
+ members.push(item);
1343
+ } else if (typeof item === "object" && item !== null && "id" in item) {
1344
+ const id = item["id"];
1345
+ if (typeof id === "string" || typeof id === "number") {
1346
+ members.push(id);
1222
1347
  }
1223
1348
  }
1224
- return {
1225
- kind: "constraint",
1226
- constraintKind: "allowedMembers",
1227
- members,
1228
- provenance
1229
- };
1230
- } catch {
1231
- return null;
1232
1349
  }
1350
+ return {
1351
+ kind: "constraint",
1352
+ constraintKind: "allowedMembers",
1353
+ members,
1354
+ ...path4 && { path: path4 },
1355
+ provenance
1356
+ };
1233
1357
  }
1234
1358
  return {
1235
1359
  kind: "constraint",
1236
1360
  constraintKind: "pattern",
1237
- pattern: text,
1361
+ pattern: effectiveText,
1362
+ ...path4 && { path: path4 },
1238
1363
  provenance
1239
1364
  };
1240
1365
  }
@@ -1272,24 +1397,30 @@ var NUMERIC_CONSTRAINT_MAP, LENGTH_CONSTRAINT_MAP, TAGS_REQUIRING_RAW_TEXT, shar
1272
1397
  var init_tsdoc_parser = __esm({
1273
1398
  "src/analyzer/tsdoc-parser.ts"() {
1274
1399
  "use strict";
1400
+ init_json_utils();
1275
1401
  NUMERIC_CONSTRAINT_MAP = {
1276
- Minimum: "minimum",
1277
- Maximum: "maximum",
1278
- ExclusiveMinimum: "exclusiveMinimum",
1279
- ExclusiveMaximum: "exclusiveMaximum"
1402
+ minimum: "minimum",
1403
+ maximum: "maximum",
1404
+ exclusiveMinimum: "exclusiveMinimum",
1405
+ exclusiveMaximum: "exclusiveMaximum",
1406
+ multipleOf: "multipleOf"
1280
1407
  };
1281
1408
  LENGTH_CONSTRAINT_MAP = {
1282
- MinLength: "minLength",
1283
- MaxLength: "maxLength"
1409
+ minLength: "minLength",
1410
+ maxLength: "maxLength",
1411
+ minItems: "minItems",
1412
+ maxItems: "maxItems"
1284
1413
  };
1285
- TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1414
+ TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1286
1415
  }
1287
1416
  });
1288
1417
 
1289
1418
  // src/analyzer/jsdoc-constraints.ts
1290
1419
  import * as ts3 from "typescript";
1291
1420
  import {
1292
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
1421
+ BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
1422
+ isBuiltinConstraintName as isBuiltinConstraintName2,
1423
+ normalizeConstraintTagName as normalizeConstraintTagName2
1293
1424
  } from "@formspec/core";
1294
1425
  function extractJSDocConstraintNodes(node, file = "") {
1295
1426
  const result = parseTSDocTags(node, file);
@@ -1336,6 +1467,7 @@ var init_jsdoc_constraints = __esm({
1336
1467
  "src/analyzer/jsdoc-constraints.ts"() {
1337
1468
  "use strict";
1338
1469
  init_tsdoc_parser();
1470
+ init_json_utils();
1339
1471
  }
1340
1472
  });
1341
1473
 
@@ -1693,14 +1825,22 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1693
1825
  }
1694
1826
  return map;
1695
1827
  }
1696
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1828
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1697
1829
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1830
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1831
+ const aliasName = typeNode.typeName.getText();
1832
+ throw new Error(
1833
+ `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.`
1834
+ );
1835
+ }
1698
1836
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1699
1837
  if (!symbol?.declarations) return [];
1700
1838
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1701
1839
  if (!aliasDecl) return [];
1702
1840
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1703
- return extractJSDocConstraintNodes(aliasDecl, file);
1841
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1842
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1843
+ return constraints;
1704
1844
  }
1705
1845
  function provenanceForNode(node, file) {
1706
1846
  const sourceFile = node.getSourceFile();
@@ -1773,10 +1913,12 @@ function detectFormSpecReference(typeNode) {
1773
1913
  }
1774
1914
  return null;
1775
1915
  }
1916
+ var MAX_ALIAS_CHAIN_DEPTH;
1776
1917
  var init_class_analyzer = __esm({
1777
1918
  "src/analyzer/class-analyzer.ts"() {
1778
1919
  "use strict";
1779
1920
  init_jsdoc_constraints();
1921
+ MAX_ALIAS_CHAIN_DEPTH = 8;
1780
1922
  }
1781
1923
  });
1782
1924