@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.cjs CHANGED
@@ -91,11 +91,40 @@ function canonicalizeField(field) {
91
91
  }
92
92
  function canonicalizeTextField(field) {
93
93
  const type = { kind: "primitive", primitiveKind: "string" };
94
+ const constraints = [];
95
+ if (field.minLength !== void 0) {
96
+ const c = {
97
+ kind: "constraint",
98
+ constraintKind: "minLength",
99
+ value: field.minLength,
100
+ provenance: CHAIN_DSL_PROVENANCE
101
+ };
102
+ constraints.push(c);
103
+ }
104
+ if (field.maxLength !== void 0) {
105
+ const c = {
106
+ kind: "constraint",
107
+ constraintKind: "maxLength",
108
+ value: field.maxLength,
109
+ provenance: CHAIN_DSL_PROVENANCE
110
+ };
111
+ constraints.push(c);
112
+ }
113
+ if (field.pattern !== void 0) {
114
+ const c = {
115
+ kind: "constraint",
116
+ constraintKind: "pattern",
117
+ pattern: field.pattern,
118
+ provenance: CHAIN_DSL_PROVENANCE
119
+ };
120
+ constraints.push(c);
121
+ }
94
122
  return buildFieldNode(
95
123
  field.name,
96
124
  type,
97
125
  field.required,
98
- buildAnnotations(field.label, field.placeholder)
126
+ buildAnnotations(field.label, field.placeholder),
127
+ constraints
99
128
  );
100
129
  }
101
130
  function canonicalizeNumberField(field) {
@@ -119,6 +148,15 @@ function canonicalizeNumberField(field) {
119
148
  };
120
149
  constraints.push(c);
121
150
  }
151
+ if (field.multipleOf !== void 0) {
152
+ const c = {
153
+ kind: "constraint",
154
+ constraintKind: "multipleOf",
155
+ value: field.multipleOf,
156
+ provenance: CHAIN_DSL_PROVENANCE
157
+ };
158
+ constraints.push(c);
159
+ }
122
160
  return buildFieldNode(
123
161
  field.name,
124
162
  type,
@@ -446,8 +484,70 @@ function collectFields(elements, properties, required, ctx) {
446
484
  }
447
485
  function generateFieldSchema(field, ctx) {
448
486
  const schema = generateTypeNode(field.type, ctx);
449
- applyConstraints(schema, field.constraints);
487
+ const directConstraints = [];
488
+ const pathConstraints = [];
489
+ for (const c of field.constraints) {
490
+ if (c.path) {
491
+ pathConstraints.push(c);
492
+ } else {
493
+ directConstraints.push(c);
494
+ }
495
+ }
496
+ applyConstraints(schema, directConstraints);
450
497
  applyAnnotations(schema, field.annotations);
498
+ if (pathConstraints.length === 0) {
499
+ return schema;
500
+ }
501
+ return applyPathTargetedConstraints(schema, pathConstraints);
502
+ }
503
+ function applyPathTargetedConstraints(schema, pathConstraints) {
504
+ if (schema.type === "array" && schema.items) {
505
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
506
+ return schema;
507
+ }
508
+ const byTarget = /* @__PURE__ */ new Map();
509
+ for (const c of pathConstraints) {
510
+ const target = c.path?.segments[0];
511
+ if (!target) continue;
512
+ const group = byTarget.get(target) ?? [];
513
+ group.push(c);
514
+ byTarget.set(target, group);
515
+ }
516
+ const propertyOverrides = {};
517
+ for (const [target, constraints] of byTarget) {
518
+ const subSchema = {};
519
+ applyConstraints(subSchema, constraints);
520
+ propertyOverrides[target] = subSchema;
521
+ }
522
+ if (schema.$ref) {
523
+ const { $ref, ...rest } = schema;
524
+ const refPart = { $ref };
525
+ const overridePart = {
526
+ properties: propertyOverrides,
527
+ ...rest
528
+ };
529
+ return { allOf: [refPart, overridePart] };
530
+ }
531
+ if (schema.type === "object" && schema.properties) {
532
+ const missingOverrides = {};
533
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
534
+ if (schema.properties[target]) {
535
+ Object.assign(schema.properties[target], overrideSchema);
536
+ } else {
537
+ missingOverrides[target] = overrideSchema;
538
+ }
539
+ }
540
+ if (Object.keys(missingOverrides).length === 0) {
541
+ return schema;
542
+ }
543
+ return {
544
+ allOf: [schema, { properties: missingOverrides }]
545
+ };
546
+ }
547
+ if (schema.allOf) {
548
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
549
+ return schema;
550
+ }
451
551
  return schema;
452
552
  }
453
553
  function generateTypeNode(type, ctx) {
@@ -1056,10 +1156,21 @@ var init_program = __esm({
1056
1156
  }
1057
1157
  });
1058
1158
 
1059
- // src/analyzer/tsdoc-parser.ts
1060
- function isBuiltinConstraintName(tagName) {
1061
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
1159
+ // src/analyzer/json-utils.ts
1160
+ function tryParseJson(text) {
1161
+ try {
1162
+ return JSON.parse(text);
1163
+ } catch {
1164
+ return null;
1165
+ }
1062
1166
  }
1167
+ var init_json_utils = __esm({
1168
+ "src/analyzer/json-utils.ts"() {
1169
+ "use strict";
1170
+ }
1171
+ });
1172
+
1173
+ // src/analyzer/tsdoc-parser.ts
1063
1174
  function createFormSpecTSDocConfig() {
1064
1175
  const config = new import_tsdoc.TSDocConfiguration();
1065
1176
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1098,7 +1209,7 @@ function parseTSDocTags(node, file = "") {
1098
1209
  );
1099
1210
  const docComment = parserContext.docComment;
1100
1211
  for (const block of docComment.customBlocks) {
1101
- const tagName = block.blockTag.tagName.substring(1);
1212
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
1102
1213
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1103
1214
  const text = extractBlockText(block).trim();
1104
1215
  if (text === "") continue;
@@ -1119,7 +1230,7 @@ function parseTSDocTags(node, file = "") {
1119
1230
  }
1120
1231
  const jsDocTagsAll = ts2.getJSDocTags(node);
1121
1232
  for (const tag of jsDocTagsAll) {
1122
- const tagName = tag.tagName.text;
1233
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
1123
1234
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1124
1235
  const commentText = getTagCommentText(tag);
1125
1236
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1167,6 +1278,15 @@ function parseTSDocTags(node, file = "") {
1167
1278
  }
1168
1279
  return { constraints, annotations };
1169
1280
  }
1281
+ function extractPathTarget(text) {
1282
+ const trimmed = text.trimStart();
1283
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
1284
+ if (!match?.[1] || !match[2]) return null;
1285
+ return {
1286
+ path: { segments: [match[1]] },
1287
+ remainingText: match[2]
1288
+ };
1289
+ }
1170
1290
  function extractBlockText(block) {
1171
1291
  return extractPlainText(block.content);
1172
1292
  }
@@ -1186,12 +1306,15 @@ function extractPlainText(node) {
1186
1306
  return result;
1187
1307
  }
1188
1308
  function parseConstraintValue(tagName, text, provenance) {
1189
- if (!isBuiltinConstraintName(tagName)) {
1309
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
1190
1310
  return null;
1191
1311
  }
1312
+ const pathResult = extractPathTarget(text);
1313
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1314
+ const path4 = pathResult?.path;
1192
1315
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1193
1316
  if (expectedType === "number") {
1194
- const value = Number(text);
1317
+ const value = Number(effectiveText);
1195
1318
  if (Number.isNaN(value)) {
1196
1319
  return null;
1197
1320
  }
@@ -1201,6 +1324,7 @@ function parseConstraintValue(tagName, text, provenance) {
1201
1324
  kind: "constraint",
1202
1325
  constraintKind: numericKind,
1203
1326
  value,
1327
+ ...path4 && { path: path4 },
1204
1328
  provenance
1205
1329
  };
1206
1330
  }
@@ -1210,42 +1334,41 @@ function parseConstraintValue(tagName, text, provenance) {
1210
1334
  kind: "constraint",
1211
1335
  constraintKind: lengthKind,
1212
1336
  value,
1337
+ ...path4 && { path: path4 },
1213
1338
  provenance
1214
1339
  };
1215
1340
  }
1216
1341
  return null;
1217
1342
  }
1218
1343
  if (expectedType === "json") {
1219
- try {
1220
- const parsed = JSON.parse(text);
1221
- if (!Array.isArray(parsed)) {
1222
- return null;
1223
- }
1224
- const members = [];
1225
- for (const item of parsed) {
1226
- if (typeof item === "string" || typeof item === "number") {
1227
- members.push(item);
1228
- } else if (typeof item === "object" && item !== null && "id" in item) {
1229
- const id = item["id"];
1230
- if (typeof id === "string" || typeof id === "number") {
1231
- members.push(id);
1232
- }
1344
+ const parsed = tryParseJson(effectiveText);
1345
+ if (!Array.isArray(parsed)) {
1346
+ return null;
1347
+ }
1348
+ const members = [];
1349
+ for (const item of parsed) {
1350
+ if (typeof item === "string" || typeof item === "number") {
1351
+ members.push(item);
1352
+ } else if (typeof item === "object" && item !== null && "id" in item) {
1353
+ const id = item["id"];
1354
+ if (typeof id === "string" || typeof id === "number") {
1355
+ members.push(id);
1233
1356
  }
1234
1357
  }
1235
- return {
1236
- kind: "constraint",
1237
- constraintKind: "allowedMembers",
1238
- members,
1239
- provenance
1240
- };
1241
- } catch {
1242
- return null;
1243
1358
  }
1359
+ return {
1360
+ kind: "constraint",
1361
+ constraintKind: "allowedMembers",
1362
+ members,
1363
+ ...path4 && { path: path4 },
1364
+ provenance
1365
+ };
1244
1366
  }
1245
1367
  return {
1246
1368
  kind: "constraint",
1247
1369
  constraintKind: "pattern",
1248
- pattern: text,
1370
+ pattern: effectiveText,
1371
+ ...path4 && { path: path4 },
1249
1372
  provenance
1250
1373
  };
1251
1374
  }
@@ -1286,17 +1409,21 @@ var init_tsdoc_parser = __esm({
1286
1409
  ts2 = __toESM(require("typescript"), 1);
1287
1410
  import_tsdoc = require("@microsoft/tsdoc");
1288
1411
  import_core3 = require("@formspec/core");
1412
+ init_json_utils();
1289
1413
  NUMERIC_CONSTRAINT_MAP = {
1290
- Minimum: "minimum",
1291
- Maximum: "maximum",
1292
- ExclusiveMinimum: "exclusiveMinimum",
1293
- ExclusiveMaximum: "exclusiveMaximum"
1414
+ minimum: "minimum",
1415
+ maximum: "maximum",
1416
+ exclusiveMinimum: "exclusiveMinimum",
1417
+ exclusiveMaximum: "exclusiveMaximum",
1418
+ multipleOf: "multipleOf"
1294
1419
  };
1295
1420
  LENGTH_CONSTRAINT_MAP = {
1296
- MinLength: "minLength",
1297
- MaxLength: "maxLength"
1421
+ minLength: "minLength",
1422
+ maxLength: "maxLength",
1423
+ minItems: "minItems",
1424
+ maxItems: "maxItems"
1298
1425
  };
1299
- TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1426
+ TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1300
1427
  }
1301
1428
  });
1302
1429
 
@@ -1349,6 +1476,7 @@ var init_jsdoc_constraints = __esm({
1349
1476
  ts3 = __toESM(require("typescript"), 1);
1350
1477
  import_core4 = require("@formspec/core");
1351
1478
  init_tsdoc_parser();
1479
+ init_json_utils();
1352
1480
  }
1353
1481
  });
1354
1482
 
@@ -1705,14 +1833,22 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1705
1833
  }
1706
1834
  return map;
1707
1835
  }
1708
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1836
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1709
1837
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1838
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1839
+ const aliasName = typeNode.typeName.getText();
1840
+ throw new Error(
1841
+ `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.`
1842
+ );
1843
+ }
1710
1844
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1711
1845
  if (!symbol?.declarations) return [];
1712
1846
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1713
1847
  if (!aliasDecl) return [];
1714
1848
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1715
- return extractJSDocConstraintNodes(aliasDecl, file);
1849
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1850
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1851
+ return constraints;
1716
1852
  }
1717
1853
  function provenanceForNode(node, file) {
1718
1854
  const sourceFile = node.getSourceFile();
@@ -1785,12 +1921,13 @@ function detectFormSpecReference(typeNode) {
1785
1921
  }
1786
1922
  return null;
1787
1923
  }
1788
- var ts4;
1924
+ var ts4, MAX_ALIAS_CHAIN_DEPTH;
1789
1925
  var init_class_analyzer = __esm({
1790
1926
  "src/analyzer/class-analyzer.ts"() {
1791
1927
  "use strict";
1792
1928
  ts4 = __toESM(require("typescript"), 1);
1793
1929
  init_jsdoc_constraints();
1930
+ MAX_ALIAS_CHAIN_DEPTH = 8;
1794
1931
  }
1795
1932
  });
1796
1933