@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/index.cjs CHANGED
@@ -125,11 +125,40 @@ function canonicalizeField(field) {
125
125
  }
126
126
  function canonicalizeTextField(field) {
127
127
  const type = { kind: "primitive", primitiveKind: "string" };
128
+ const constraints = [];
129
+ if (field.minLength !== void 0) {
130
+ const c = {
131
+ kind: "constraint",
132
+ constraintKind: "minLength",
133
+ value: field.minLength,
134
+ provenance: CHAIN_DSL_PROVENANCE
135
+ };
136
+ constraints.push(c);
137
+ }
138
+ if (field.maxLength !== void 0) {
139
+ const c = {
140
+ kind: "constraint",
141
+ constraintKind: "maxLength",
142
+ value: field.maxLength,
143
+ provenance: CHAIN_DSL_PROVENANCE
144
+ };
145
+ constraints.push(c);
146
+ }
147
+ if (field.pattern !== void 0) {
148
+ const c = {
149
+ kind: "constraint",
150
+ constraintKind: "pattern",
151
+ pattern: field.pattern,
152
+ provenance: CHAIN_DSL_PROVENANCE
153
+ };
154
+ constraints.push(c);
155
+ }
128
156
  return buildFieldNode(
129
157
  field.name,
130
158
  type,
131
159
  field.required,
132
- buildAnnotations(field.label, field.placeholder)
160
+ buildAnnotations(field.label, field.placeholder),
161
+ constraints
133
162
  );
134
163
  }
135
164
  function canonicalizeNumberField(field) {
@@ -153,6 +182,15 @@ function canonicalizeNumberField(field) {
153
182
  };
154
183
  constraints.push(c);
155
184
  }
185
+ if (field.multipleOf !== void 0) {
186
+ const c = {
187
+ kind: "constraint",
188
+ constraintKind: "multipleOf",
189
+ value: field.multipleOf,
190
+ provenance: CHAIN_DSL_PROVENANCE
191
+ };
192
+ constraints.push(c);
193
+ }
156
194
  return buildFieldNode(
157
195
  field.name,
158
196
  type,
@@ -452,8 +490,70 @@ function collectFields(elements, properties, required, ctx) {
452
490
  }
453
491
  function generateFieldSchema(field, ctx) {
454
492
  const schema = generateTypeNode(field.type, ctx);
455
- 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);
456
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
+ }
457
557
  return schema;
458
558
  }
459
559
  function generateTypeNode(type, ctx) {
@@ -1028,20 +1128,31 @@ var import_core4 = require("@formspec/core");
1028
1128
  var ts2 = __toESM(require("typescript"), 1);
1029
1129
  var import_tsdoc = require("@microsoft/tsdoc");
1030
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
1031
1142
  var NUMERIC_CONSTRAINT_MAP = {
1032
- Minimum: "minimum",
1033
- Maximum: "maximum",
1034
- ExclusiveMinimum: "exclusiveMinimum",
1035
- ExclusiveMaximum: "exclusiveMaximum"
1143
+ minimum: "minimum",
1144
+ maximum: "maximum",
1145
+ exclusiveMinimum: "exclusiveMinimum",
1146
+ exclusiveMaximum: "exclusiveMaximum",
1147
+ multipleOf: "multipleOf"
1036
1148
  };
1037
1149
  var LENGTH_CONSTRAINT_MAP = {
1038
- MinLength: "minLength",
1039
- MaxLength: "maxLength"
1150
+ minLength: "minLength",
1151
+ maxLength: "maxLength",
1152
+ minItems: "minItems",
1153
+ maxItems: "maxItems"
1040
1154
  };
1041
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
1042
- function isBuiltinConstraintName(tagName) {
1043
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
1044
- }
1155
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
1045
1156
  function createFormSpecTSDocConfig() {
1046
1157
  const config = new import_tsdoc.TSDocConfiguration();
1047
1158
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -1081,7 +1192,7 @@ function parseTSDocTags(node, file = "") {
1081
1192
  );
1082
1193
  const docComment = parserContext.docComment;
1083
1194
  for (const block of docComment.customBlocks) {
1084
- const tagName = block.blockTag.tagName.substring(1);
1195
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
1085
1196
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1086
1197
  const text = extractBlockText(block).trim();
1087
1198
  if (text === "") continue;
@@ -1102,7 +1213,7 @@ function parseTSDocTags(node, file = "") {
1102
1213
  }
1103
1214
  const jsDocTagsAll = ts2.getJSDocTags(node);
1104
1215
  for (const tag of jsDocTagsAll) {
1105
- const tagName = tag.tagName.text;
1216
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
1106
1217
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1107
1218
  const commentText = getTagCommentText(tag);
1108
1219
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -1150,6 +1261,15 @@ function parseTSDocTags(node, file = "") {
1150
1261
  }
1151
1262
  return { constraints, annotations };
1152
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
+ }
1153
1273
  function extractBlockText(block) {
1154
1274
  return extractPlainText(block.content);
1155
1275
  }
@@ -1169,12 +1289,15 @@ function extractPlainText(node) {
1169
1289
  return result;
1170
1290
  }
1171
1291
  function parseConstraintValue(tagName, text, provenance) {
1172
- if (!isBuiltinConstraintName(tagName)) {
1292
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
1173
1293
  return null;
1174
1294
  }
1295
+ const pathResult = extractPathTarget(text);
1296
+ const effectiveText = pathResult ? pathResult.remainingText : text;
1297
+ const path3 = pathResult?.path;
1175
1298
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1176
1299
  if (expectedType === "number") {
1177
- const value = Number(text);
1300
+ const value = Number(effectiveText);
1178
1301
  if (Number.isNaN(value)) {
1179
1302
  return null;
1180
1303
  }
@@ -1184,6 +1307,7 @@ function parseConstraintValue(tagName, text, provenance) {
1184
1307
  kind: "constraint",
1185
1308
  constraintKind: numericKind,
1186
1309
  value,
1310
+ ...path3 && { path: path3 },
1187
1311
  provenance
1188
1312
  };
1189
1313
  }
@@ -1193,42 +1317,41 @@ function parseConstraintValue(tagName, text, provenance) {
1193
1317
  kind: "constraint",
1194
1318
  constraintKind: lengthKind,
1195
1319
  value,
1320
+ ...path3 && { path: path3 },
1196
1321
  provenance
1197
1322
  };
1198
1323
  }
1199
1324
  return null;
1200
1325
  }
1201
1326
  if (expectedType === "json") {
1202
- try {
1203
- const parsed = JSON.parse(text);
1204
- if (!Array.isArray(parsed)) {
1205
- return null;
1206
- }
1207
- const members = [];
1208
- for (const item of parsed) {
1209
- if (typeof item === "string" || typeof item === "number") {
1210
- members.push(item);
1211
- } else if (typeof item === "object" && item !== null && "id" in item) {
1212
- const id = item["id"];
1213
- if (typeof id === "string" || typeof id === "number") {
1214
- members.push(id);
1215
- }
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);
1216
1339
  }
1217
1340
  }
1218
- return {
1219
- kind: "constraint",
1220
- constraintKind: "allowedMembers",
1221
- members,
1222
- provenance
1223
- };
1224
- } catch {
1225
- return null;
1226
1341
  }
1342
+ return {
1343
+ kind: "constraint",
1344
+ constraintKind: "allowedMembers",
1345
+ members,
1346
+ ...path3 && { path: path3 },
1347
+ provenance
1348
+ };
1227
1349
  }
1228
1350
  return {
1229
1351
  kind: "constraint",
1230
1352
  constraintKind: "pattern",
1231
- pattern: text,
1353
+ pattern: effectiveText,
1354
+ ...path3 && { path: path3 },
1232
1355
  provenance
1233
1356
  };
1234
1357
  }
@@ -1659,14 +1782,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1659
1782
  }
1660
1783
  return map;
1661
1784
  }
1662
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1785
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1786
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1663
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
+ }
1664
1794
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1665
1795
  if (!symbol?.declarations) return [];
1666
1796
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1667
1797
  if (!aliasDecl) return [];
1668
1798
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1669
- return extractJSDocConstraintNodes(aliasDecl, file);
1799
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1800
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1801
+ return constraints;
1670
1802
  }
1671
1803
  function provenanceForNode(node, file) {
1672
1804
  const sourceFile = node.getSourceFile();