@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.
- package/README.md +20 -20
- package/dist/__tests__/alias-chain-propagation.test.d.ts +9 -0
- package/dist/__tests__/alias-chain-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts +37 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +7 -7
- package/dist/__tests__/fixtures/example-interface-types.d.ts +17 -17
- package/dist/__tests__/guards.test.d.ts +2 -0
- package/dist/__tests__/guards.test.d.ts.map +1 -0
- package/dist/__tests__/json-utils.test.d.ts +5 -0
- package/dist/__tests__/json-utils.test.d.ts.map +1 -0
- package/dist/__tests__/path-target-parser.test.d.ts +9 -0
- package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +2 -2
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/json-utils.d.ts +22 -0
- package/dist/analyzer/json-utils.d.ts.map +1 -0
- package/dist/analyzer/tsdoc-parser.d.ts +18 -4
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
- package/dist/browser.cjs +115 -8
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.js +115 -8
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +1 -0
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
- package/dist/cli.cjs +179 -42
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +184 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +173 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +178 -42
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +186 -47
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.js +191 -48
- package/dist/internals.js.map +1 -1
- package/dist/json-schema/ir-generator.d.ts +1 -0
- package/dist/json-schema/ir-generator.d.ts.map +1 -1
- package/dist/validate/constraint-validator.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/internals.js
CHANGED
|
@@ -66,11 +66,40 @@ function canonicalizeField(field) {
|
|
|
66
66
|
}
|
|
67
67
|
function canonicalizeTextField(field) {
|
|
68
68
|
const type = { kind: "primitive", primitiveKind: "string" };
|
|
69
|
+
const constraints = [];
|
|
70
|
+
if (field.minLength !== void 0) {
|
|
71
|
+
const c = {
|
|
72
|
+
kind: "constraint",
|
|
73
|
+
constraintKind: "minLength",
|
|
74
|
+
value: field.minLength,
|
|
75
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
76
|
+
};
|
|
77
|
+
constraints.push(c);
|
|
78
|
+
}
|
|
79
|
+
if (field.maxLength !== void 0) {
|
|
80
|
+
const c = {
|
|
81
|
+
kind: "constraint",
|
|
82
|
+
constraintKind: "maxLength",
|
|
83
|
+
value: field.maxLength,
|
|
84
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
85
|
+
};
|
|
86
|
+
constraints.push(c);
|
|
87
|
+
}
|
|
88
|
+
if (field.pattern !== void 0) {
|
|
89
|
+
const c = {
|
|
90
|
+
kind: "constraint",
|
|
91
|
+
constraintKind: "pattern",
|
|
92
|
+
pattern: field.pattern,
|
|
93
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
94
|
+
};
|
|
95
|
+
constraints.push(c);
|
|
96
|
+
}
|
|
69
97
|
return buildFieldNode(
|
|
70
98
|
field.name,
|
|
71
99
|
type,
|
|
72
100
|
field.required,
|
|
73
|
-
buildAnnotations(field.label, field.placeholder)
|
|
101
|
+
buildAnnotations(field.label, field.placeholder),
|
|
102
|
+
constraints
|
|
74
103
|
);
|
|
75
104
|
}
|
|
76
105
|
function canonicalizeNumberField(field) {
|
|
@@ -94,6 +123,15 @@ function canonicalizeNumberField(field) {
|
|
|
94
123
|
};
|
|
95
124
|
constraints.push(c);
|
|
96
125
|
}
|
|
126
|
+
if (field.multipleOf !== void 0) {
|
|
127
|
+
const c = {
|
|
128
|
+
kind: "constraint",
|
|
129
|
+
constraintKind: "multipleOf",
|
|
130
|
+
value: field.multipleOf,
|
|
131
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
132
|
+
};
|
|
133
|
+
constraints.push(c);
|
|
134
|
+
}
|
|
97
135
|
return buildFieldNode(
|
|
98
136
|
field.name,
|
|
99
137
|
type,
|
|
@@ -423,7 +461,9 @@ import * as ts4 from "typescript";
|
|
|
423
461
|
// src/analyzer/jsdoc-constraints.ts
|
|
424
462
|
import * as ts3 from "typescript";
|
|
425
463
|
import {
|
|
426
|
-
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
|
|
464
|
+
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
|
|
465
|
+
isBuiltinConstraintName as isBuiltinConstraintName2,
|
|
466
|
+
normalizeConstraintTagName as normalizeConstraintTagName2
|
|
427
467
|
} from "@formspec/core";
|
|
428
468
|
|
|
429
469
|
// src/analyzer/tsdoc-parser.ts
|
|
@@ -438,22 +478,35 @@ import {
|
|
|
438
478
|
TextRange
|
|
439
479
|
} from "@microsoft/tsdoc";
|
|
440
480
|
import {
|
|
441
|
-
BUILTIN_CONSTRAINT_DEFINITIONS
|
|
481
|
+
BUILTIN_CONSTRAINT_DEFINITIONS,
|
|
482
|
+
normalizeConstraintTagName,
|
|
483
|
+
isBuiltinConstraintName
|
|
442
484
|
} from "@formspec/core";
|
|
485
|
+
|
|
486
|
+
// src/analyzer/json-utils.ts
|
|
487
|
+
function tryParseJson(text) {
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(text);
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/analyzer/tsdoc-parser.ts
|
|
443
496
|
var NUMERIC_CONSTRAINT_MAP = {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
497
|
+
minimum: "minimum",
|
|
498
|
+
maximum: "maximum",
|
|
499
|
+
exclusiveMinimum: "exclusiveMinimum",
|
|
500
|
+
exclusiveMaximum: "exclusiveMaximum",
|
|
501
|
+
multipleOf: "multipleOf"
|
|
448
502
|
};
|
|
449
503
|
var LENGTH_CONSTRAINT_MAP = {
|
|
450
|
-
|
|
451
|
-
|
|
504
|
+
minLength: "minLength",
|
|
505
|
+
maxLength: "maxLength",
|
|
506
|
+
minItems: "minItems",
|
|
507
|
+
maxItems: "maxItems"
|
|
452
508
|
};
|
|
453
|
-
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["
|
|
454
|
-
function isBuiltinConstraintName(tagName) {
|
|
455
|
-
return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
|
|
456
|
-
}
|
|
509
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
|
|
457
510
|
function createFormSpecTSDocConfig() {
|
|
458
511
|
const config = new TSDocConfiguration();
|
|
459
512
|
for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
@@ -493,7 +546,7 @@ function parseTSDocTags(node, file = "") {
|
|
|
493
546
|
);
|
|
494
547
|
const docComment = parserContext.docComment;
|
|
495
548
|
for (const block of docComment.customBlocks) {
|
|
496
|
-
const tagName = block.blockTag.tagName.substring(1);
|
|
549
|
+
const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
|
|
497
550
|
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
498
551
|
const text = extractBlockText(block).trim();
|
|
499
552
|
if (text === "") continue;
|
|
@@ -514,7 +567,7 @@ function parseTSDocTags(node, file = "") {
|
|
|
514
567
|
}
|
|
515
568
|
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
516
569
|
for (const tag of jsDocTagsAll) {
|
|
517
|
-
const tagName = tag.tagName.text;
|
|
570
|
+
const tagName = normalizeConstraintTagName(tag.tagName.text);
|
|
518
571
|
if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
519
572
|
const commentText = getTagCommentText(tag);
|
|
520
573
|
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
@@ -562,6 +615,15 @@ function parseTSDocTags(node, file = "") {
|
|
|
562
615
|
}
|
|
563
616
|
return { constraints, annotations };
|
|
564
617
|
}
|
|
618
|
+
function extractPathTarget(text) {
|
|
619
|
+
const trimmed = text.trimStart();
|
|
620
|
+
const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
|
|
621
|
+
if (!match?.[1] || !match[2]) return null;
|
|
622
|
+
return {
|
|
623
|
+
path: { segments: [match[1]] },
|
|
624
|
+
remainingText: match[2]
|
|
625
|
+
};
|
|
626
|
+
}
|
|
565
627
|
function extractBlockText(block) {
|
|
566
628
|
return extractPlainText(block.content);
|
|
567
629
|
}
|
|
@@ -584,9 +646,12 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
584
646
|
if (!isBuiltinConstraintName(tagName)) {
|
|
585
647
|
return null;
|
|
586
648
|
}
|
|
649
|
+
const pathResult = extractPathTarget(text);
|
|
650
|
+
const effectiveText = pathResult ? pathResult.remainingText : text;
|
|
651
|
+
const path2 = pathResult?.path;
|
|
587
652
|
const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
|
|
588
653
|
if (expectedType === "number") {
|
|
589
|
-
const value = Number(
|
|
654
|
+
const value = Number(effectiveText);
|
|
590
655
|
if (Number.isNaN(value)) {
|
|
591
656
|
return null;
|
|
592
657
|
}
|
|
@@ -596,6 +661,7 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
596
661
|
kind: "constraint",
|
|
597
662
|
constraintKind: numericKind,
|
|
598
663
|
value,
|
|
664
|
+
...path2 && { path: path2 },
|
|
599
665
|
provenance
|
|
600
666
|
};
|
|
601
667
|
}
|
|
@@ -605,42 +671,41 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
605
671
|
kind: "constraint",
|
|
606
672
|
constraintKind: lengthKind,
|
|
607
673
|
value,
|
|
674
|
+
...path2 && { path: path2 },
|
|
608
675
|
provenance
|
|
609
676
|
};
|
|
610
677
|
}
|
|
611
678
|
return null;
|
|
612
679
|
}
|
|
613
680
|
if (expectedType === "json") {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
members.push(id);
|
|
627
|
-
}
|
|
681
|
+
const parsed = tryParseJson(effectiveText);
|
|
682
|
+
if (!Array.isArray(parsed)) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const members = [];
|
|
686
|
+
for (const item of parsed) {
|
|
687
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
688
|
+
members.push(item);
|
|
689
|
+
} else if (typeof item === "object" && item !== null && "id" in item) {
|
|
690
|
+
const id = item["id"];
|
|
691
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
692
|
+
members.push(id);
|
|
628
693
|
}
|
|
629
694
|
}
|
|
630
|
-
return {
|
|
631
|
-
kind: "constraint",
|
|
632
|
-
constraintKind: "allowedMembers",
|
|
633
|
-
members,
|
|
634
|
-
provenance
|
|
635
|
-
};
|
|
636
|
-
} catch {
|
|
637
|
-
return null;
|
|
638
695
|
}
|
|
696
|
+
return {
|
|
697
|
+
kind: "constraint",
|
|
698
|
+
constraintKind: "allowedMembers",
|
|
699
|
+
members,
|
|
700
|
+
...path2 && { path: path2 },
|
|
701
|
+
provenance
|
|
702
|
+
};
|
|
639
703
|
}
|
|
640
704
|
return {
|
|
641
705
|
kind: "constraint",
|
|
642
706
|
constraintKind: "pattern",
|
|
643
|
-
pattern:
|
|
707
|
+
pattern: effectiveText,
|
|
708
|
+
...path2 && { path: path2 },
|
|
644
709
|
provenance
|
|
645
710
|
};
|
|
646
711
|
}
|
|
@@ -1071,14 +1136,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
|
|
|
1071
1136
|
}
|
|
1072
1137
|
return map;
|
|
1073
1138
|
}
|
|
1074
|
-
|
|
1139
|
+
var MAX_ALIAS_CHAIN_DEPTH = 8;
|
|
1140
|
+
function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
|
|
1075
1141
|
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
1142
|
+
if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
|
|
1143
|
+
const aliasName = typeNode.typeName.getText();
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`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.`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1076
1148
|
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
1077
1149
|
if (!symbol?.declarations) return [];
|
|
1078
1150
|
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1079
1151
|
if (!aliasDecl) return [];
|
|
1080
1152
|
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
1081
|
-
|
|
1153
|
+
const constraints = extractJSDocConstraintNodes(aliasDecl, file);
|
|
1154
|
+
constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
|
|
1155
|
+
return constraints;
|
|
1082
1156
|
}
|
|
1083
1157
|
function provenanceForNode(node, file) {
|
|
1084
1158
|
const sourceFile = node.getSourceFile();
|
|
@@ -1200,8 +1274,70 @@ function collectFields(elements, properties, required, ctx) {
|
|
|
1200
1274
|
}
|
|
1201
1275
|
function generateFieldSchema(field, ctx) {
|
|
1202
1276
|
const schema = generateTypeNode(field.type, ctx);
|
|
1203
|
-
|
|
1277
|
+
const directConstraints = [];
|
|
1278
|
+
const pathConstraints = [];
|
|
1279
|
+
for (const c of field.constraints) {
|
|
1280
|
+
if (c.path) {
|
|
1281
|
+
pathConstraints.push(c);
|
|
1282
|
+
} else {
|
|
1283
|
+
directConstraints.push(c);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
applyConstraints(schema, directConstraints);
|
|
1204
1287
|
applyAnnotations(schema, field.annotations);
|
|
1288
|
+
if (pathConstraints.length === 0) {
|
|
1289
|
+
return schema;
|
|
1290
|
+
}
|
|
1291
|
+
return applyPathTargetedConstraints(schema, pathConstraints);
|
|
1292
|
+
}
|
|
1293
|
+
function applyPathTargetedConstraints(schema, pathConstraints) {
|
|
1294
|
+
if (schema.type === "array" && schema.items) {
|
|
1295
|
+
schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
|
|
1296
|
+
return schema;
|
|
1297
|
+
}
|
|
1298
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (const c of pathConstraints) {
|
|
1300
|
+
const target = c.path?.segments[0];
|
|
1301
|
+
if (!target) continue;
|
|
1302
|
+
const group = byTarget.get(target) ?? [];
|
|
1303
|
+
group.push(c);
|
|
1304
|
+
byTarget.set(target, group);
|
|
1305
|
+
}
|
|
1306
|
+
const propertyOverrides = {};
|
|
1307
|
+
for (const [target, constraints] of byTarget) {
|
|
1308
|
+
const subSchema = {};
|
|
1309
|
+
applyConstraints(subSchema, constraints);
|
|
1310
|
+
propertyOverrides[target] = subSchema;
|
|
1311
|
+
}
|
|
1312
|
+
if (schema.$ref) {
|
|
1313
|
+
const { $ref, ...rest } = schema;
|
|
1314
|
+
const refPart = { $ref };
|
|
1315
|
+
const overridePart = {
|
|
1316
|
+
properties: propertyOverrides,
|
|
1317
|
+
...rest
|
|
1318
|
+
};
|
|
1319
|
+
return { allOf: [refPart, overridePart] };
|
|
1320
|
+
}
|
|
1321
|
+
if (schema.type === "object" && schema.properties) {
|
|
1322
|
+
const missingOverrides = {};
|
|
1323
|
+
for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
|
|
1324
|
+
if (schema.properties[target]) {
|
|
1325
|
+
Object.assign(schema.properties[target], overrideSchema);
|
|
1326
|
+
} else {
|
|
1327
|
+
missingOverrides[target] = overrideSchema;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (Object.keys(missingOverrides).length === 0) {
|
|
1331
|
+
return schema;
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
allOf: [schema, { properties: missingOverrides }]
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
if (schema.allOf) {
|
|
1338
|
+
schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
|
|
1339
|
+
return schema;
|
|
1340
|
+
}
|
|
1205
1341
|
return schema;
|
|
1206
1342
|
}
|
|
1207
1343
|
function generateTypeNode(type, ctx) {
|
|
@@ -1648,14 +1784,10 @@ function addUnknownExtension(ctx, message, primary) {
|
|
|
1648
1784
|
});
|
|
1649
1785
|
}
|
|
1650
1786
|
function findNumeric(constraints, constraintKind) {
|
|
1651
|
-
return constraints.find(
|
|
1652
|
-
(c) => c.constraintKind === constraintKind
|
|
1653
|
-
);
|
|
1787
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1654
1788
|
}
|
|
1655
1789
|
function findLength(constraints, constraintKind) {
|
|
1656
|
-
return constraints.find(
|
|
1657
|
-
(c) => c.constraintKind === constraintKind
|
|
1658
|
-
);
|
|
1790
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1659
1791
|
}
|
|
1660
1792
|
function findAllowedMembers(constraints) {
|
|
1661
1793
|
return constraints.filter(
|
|
@@ -1779,6 +1911,17 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
|
|
|
1779
1911
|
const isEnum = type.kind === "enum";
|
|
1780
1912
|
const label = typeLabel(type);
|
|
1781
1913
|
for (const constraint of constraints) {
|
|
1914
|
+
if (constraint.path) {
|
|
1915
|
+
const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
|
|
1916
|
+
if (!isTraversable) {
|
|
1917
|
+
addTypeMismatch(
|
|
1918
|
+
ctx,
|
|
1919
|
+
`Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
|
|
1920
|
+
constraint.provenance
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1782
1925
|
const ck = constraint.constraintKind;
|
|
1783
1926
|
switch (ck) {
|
|
1784
1927
|
case "minimum":
|