@formspec/build 0.1.0-alpha.16 → 0.1.0-alpha.19
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 +74 -128
- package/dist/__tests__/class-schema.test.d.ts +2 -0
- package/dist/__tests__/class-schema.test.d.ts.map +1 -0
- package/dist/__tests__/date-extension.integration.test.d.ts +2 -0
- package/dist/__tests__/date-extension.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/class-schema-regressions.d.ts +83 -0
- package/dist/__tests__/fixtures/class-schema-regressions.d.ts.map +1 -0
- package/dist/__tests__/fixtures/example-date-extension.d.ts +12 -0
- package/dist/__tests__/fixtures/example-date-extension.d.ts.map +1 -0
- package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
- package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
- package/dist/__tests__/fixtures/extension-forms.d.ts +7 -0
- package/dist/__tests__/fixtures/extension-forms.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +1 -0
- package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -1
- package/dist/__tests__/fixtures/named-primitive-aliases.d.ts +15 -0
- package/dist/__tests__/fixtures/named-primitive-aliases.d.ts.map +1 -0
- package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts +14 -0
- package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts.map +1 -0
- package/dist/__tests__/fixtures/sample-forms.d.ts +10 -0
- package/dist/__tests__/fixtures/sample-forms.d.ts.map +1 -1
- package/dist/__tests__/generate-schemas.test.d.ts +2 -0
- package/dist/__tests__/generate-schemas.test.d.ts.map +1 -0
- package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
- package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
- package/dist/__tests__/parity/parity.test.d.ts +6 -2
- package/dist/__tests__/parity/parity.test.d.ts.map +1 -1
- package/dist/__tests__/parity/utils.d.ts +9 -4
- package/dist/__tests__/parity/utils.d.ts.map +1 -1
- package/dist/analyzer/class-analyzer.d.ts +5 -4
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/program.d.ts +15 -0
- package/dist/analyzer/program.d.ts.map +1 -1
- package/dist/analyzer/tsdoc-parser.d.ts +23 -2
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
- package/dist/browser.cjs +269 -11
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.js +269 -11
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +28 -2
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
- package/dist/cli.cjs +1640 -282
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1638 -281
- package/dist/cli.js.map +1 -1
- package/dist/extensions/registry.d.ts +25 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/generators/class-schema.d.ts +4 -4
- package/dist/generators/class-schema.d.ts.map +1 -1
- package/dist/generators/method-schema.d.ts.map +1 -1
- package/dist/generators/mixed-authoring.d.ts.map +1 -1
- package/dist/index.cjs +1615 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1615 -271
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +990 -236
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.js +988 -234
- package/dist/internals.js.map +1 -1
- 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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as ts from "typescript";
|
|
16
16
|
import type { ConstraintNode, AnnotationNode } from "@formspec/core";
|
|
17
|
+
import { type ParseTSDocOptions } from "./tsdoc-parser.js";
|
|
17
18
|
/**
|
|
18
19
|
* Extracts constraints from JSDoc comments on a TypeScript AST node and returns
|
|
19
20
|
* canonical {@link ConstraintNode} objects.
|
|
@@ -25,7 +26,7 @@ import type { ConstraintNode, AnnotationNode } from "@formspec/core";
|
|
|
25
26
|
* @param file - Absolute path to the source file for provenance
|
|
26
27
|
* @returns Canonical constraint nodes for each valid constraint tag
|
|
27
28
|
*/
|
|
28
|
-
export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string): ConstraintNode[];
|
|
29
|
+
export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string, options?: ParseTSDocOptions): ConstraintNode[];
|
|
29
30
|
/**
|
|
30
31
|
* Extracts canonical annotation tags from a node and returns
|
|
31
32
|
* {@link AnnotationNode} objects.
|
|
@@ -34,7 +35,7 @@ export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string
|
|
|
34
35
|
* @param file - Absolute path to the source file for provenance
|
|
35
36
|
* @returns Canonical annotation nodes
|
|
36
37
|
*/
|
|
37
|
-
export declare function extractJSDocAnnotationNodes(node: ts.Node, file?: string): AnnotationNode[];
|
|
38
|
+
export declare function extractJSDocAnnotationNodes(node: ts.Node, file?: string, options?: ParseTSDocOptions): AnnotationNode[];
|
|
38
39
|
/**
|
|
39
40
|
* Checks if a node has a TSDoc `@deprecated` tag.
|
|
40
41
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsdoc-constraints.d.ts","sourceRoot":"","sources":["../../src/analyzer/jsdoc-constraints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"jsdoc-constraints.d.ts","sourceRoot":"","sources":["../../src/analyzer/jsdoc-constraints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAyC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMlG;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,EAAE,CAGlB;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,EAAE,CAGlB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,EACtC,IAAI,SAAK,GACR,cAAc,GAAG,IAAI,CAwCvB"}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* using the project's tsconfig.json for compiler options.
|
|
6
6
|
*/
|
|
7
7
|
import * as ts from "typescript";
|
|
8
|
+
import { type IRClassAnalysis } from "./class-analyzer.js";
|
|
9
|
+
import type { ExtensionRegistry } from "../extensions/index.js";
|
|
8
10
|
/**
|
|
9
11
|
* Result of creating a TypeScript program for analysis.
|
|
10
12
|
*/
|
|
@@ -50,4 +52,17 @@ export declare function findInterfaceByName(sourceFile: ts.SourceFile, interface
|
|
|
50
52
|
* @returns The type alias declaration node, or null if not found
|
|
51
53
|
*/
|
|
52
54
|
export declare function findTypeAliasByName(sourceFile: ts.SourceFile, aliasName: string): ts.TypeAliasDeclaration | null;
|
|
55
|
+
/**
|
|
56
|
+
* Analyzes a named type (class, interface, or type alias) from a TypeScript
|
|
57
|
+
* source file and returns an `IRClassAnalysis`.
|
|
58
|
+
*
|
|
59
|
+
* Tries each declaration kind in order: class → interface → type alias.
|
|
60
|
+
* Throws if the name is not found or if the type alias analysis fails.
|
|
61
|
+
*
|
|
62
|
+
* @param filePath - Absolute or relative path to the TypeScript source file (resolved internally)
|
|
63
|
+
* @param typeName - Name of the class, interface, or type alias to analyze
|
|
64
|
+
* @param extensionRegistry - Optional extension registry for custom type handling
|
|
65
|
+
* @returns IR analysis result
|
|
66
|
+
*/
|
|
67
|
+
export declare function analyzeNamedTypeToIR(filePath: string, typeName: string, extensionRegistry?: ExtensionRegistry): IRClassAnalysis;
|
|
53
68
|
//# sourceMappingURL=program.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/analyzer/program.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/analyzer/program.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;IACpB,uCAAuC;IACvC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC;IACxB,qCAAqC;IACrC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CA6DrE;AA6BD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,gBAAgB,GAAG,IAAI,CAE5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,aAAa,EAAE,MAAM,GACpB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,eAAe,CAyBjB"}
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The `@deprecated` tag is a standard TSDoc block tag, parsed structurally.
|
|
22
22
|
*
|
|
23
|
+
* Description precedence is:
|
|
24
|
+
* 1. Explicit `@description`
|
|
25
|
+
* 2. Explicit `@remarks`
|
|
26
|
+
* 3. Implicit summary text (free text in the comment block)
|
|
27
|
+
*
|
|
23
28
|
* **Fallback strategy**: TSDoc treats `{` / `}` as inline tag delimiters and
|
|
24
29
|
* `@` as a tag prefix, so content containing these characters (e.g. JSON
|
|
25
30
|
* objects in `@EnumOptions`, regex patterns with `@` in `@Pattern`) gets
|
|
@@ -28,7 +33,8 @@
|
|
|
28
33
|
* verbatim.
|
|
29
34
|
*/
|
|
30
35
|
import * as ts from "typescript";
|
|
31
|
-
import { type ConstraintNode, type AnnotationNode, type PathTarget } from "@formspec/core";
|
|
36
|
+
import { type ConstraintNode, type AnnotationNode, type PathTarget, type TypeNode } from "@formspec/core";
|
|
37
|
+
import type { ExtensionRegistry } from "../extensions/index.js";
|
|
32
38
|
/**
|
|
33
39
|
* Result of parsing a single JSDoc comment attached to a TS AST node.
|
|
34
40
|
*/
|
|
@@ -38,6 +44,21 @@ export interface TSDocParseResult {
|
|
|
38
44
|
/** Annotation IR nodes extracted from canonical TSDoc block tags. */
|
|
39
45
|
readonly annotations: readonly AnnotationNode[];
|
|
40
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Optional extension-aware parsing inputs for TSDoc extraction.
|
|
49
|
+
*/
|
|
50
|
+
export interface ParseTSDocOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Extension registry used to resolve custom tags and custom-type-specific
|
|
53
|
+
* broadening of built-in constraint tags.
|
|
54
|
+
*/
|
|
55
|
+
readonly extensionRegistry?: ExtensionRegistry;
|
|
56
|
+
/**
|
|
57
|
+
* Effective field/type node for the declaration being parsed. Required when
|
|
58
|
+
* built-in tags may broaden onto a custom type.
|
|
59
|
+
*/
|
|
60
|
+
readonly fieldType?: TypeNode;
|
|
61
|
+
}
|
|
41
62
|
/**
|
|
42
63
|
* Display-name metadata extracted from a node's JSDoc tags.
|
|
43
64
|
*
|
|
@@ -62,7 +83,7 @@ export interface DisplayNameMetadata {
|
|
|
62
83
|
* @param file - Absolute source file path for provenance
|
|
63
84
|
* @returns Parsed constraint and annotation nodes
|
|
64
85
|
*/
|
|
65
|
-
export declare function parseTSDocTags(node: ts.Node, file?: string): TSDocParseResult;
|
|
86
|
+
export declare function parseTSDocTags(node: ts.Node, file?: string, options?: ParseTSDocOptions): TSDocParseResult;
|
|
66
87
|
/**
|
|
67
88
|
* Checks if a TS AST node has a `@deprecated` tag using the TSDoc parser.
|
|
68
89
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tsdoc-parser.d.ts","sourceRoot":"","sources":["../../src/analyzer/tsdoc-parser.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tsdoc-parser.d.ts","sourceRoot":"","sources":["../../src/analyzer/tsdoc-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAYjC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,cAAc,EAInB,KAAK,UAAU,EAEf,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AA+GhE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,gBAAgB,CAiLlB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAsB5D;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,mBAAmB,CA2B7E;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQpD"}
|
package/dist/browser.cjs
CHANGED
|
@@ -70,6 +70,7 @@ function canonicalizeChainDSL(form) {
|
|
|
70
70
|
kind: "form-ir",
|
|
71
71
|
irVersion: import_core.IR_VERSION,
|
|
72
72
|
elements: canonicalizeElements(form.elements),
|
|
73
|
+
rootAnnotations: [],
|
|
73
74
|
typeRegistry: {},
|
|
74
75
|
provenance: CHAIN_DSL_PROVENANCE
|
|
75
76
|
};
|
|
@@ -380,6 +381,9 @@ function generateJsonSchemaFromIR(ir, options) {
|
|
|
380
381
|
const ctx = makeContext(options);
|
|
381
382
|
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
382
383
|
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
384
|
+
if (typeDef.constraints && typeDef.constraints.length > 0) {
|
|
385
|
+
applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
|
|
386
|
+
}
|
|
383
387
|
if (typeDef.annotations && typeDef.annotations.length > 0) {
|
|
384
388
|
applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
|
|
385
389
|
}
|
|
@@ -548,7 +552,9 @@ function generateTypeNode(type, ctx) {
|
|
|
548
552
|
}
|
|
549
553
|
}
|
|
550
554
|
function generatePrimitiveType(type) {
|
|
551
|
-
return {
|
|
555
|
+
return {
|
|
556
|
+
type: type.primitiveKind === "integer" || type.primitiveKind === "bigint" ? "integer" : type.primitiveKind
|
|
557
|
+
};
|
|
552
558
|
}
|
|
553
559
|
function generateEnumType(type) {
|
|
554
560
|
const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
|
|
@@ -721,7 +727,7 @@ function applyAnnotations(schema, annotations, ctx) {
|
|
|
721
727
|
case "deprecated":
|
|
722
728
|
schema.deprecated = true;
|
|
723
729
|
if (annotation.message !== void 0 && annotation.message !== "") {
|
|
724
|
-
schema[
|
|
730
|
+
schema[`${ctx.vendorPrefix}-deprecation-description`] = annotation.message;
|
|
725
731
|
}
|
|
726
732
|
break;
|
|
727
733
|
case "placeholder":
|
|
@@ -754,7 +760,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
|
|
|
754
760
|
`Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
|
|
755
761
|
);
|
|
756
762
|
}
|
|
757
|
-
|
|
763
|
+
assignVendorPrefixedExtensionKeywords(
|
|
764
|
+
schema,
|
|
765
|
+
registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
|
|
766
|
+
ctx.vendorPrefix,
|
|
767
|
+
`custom constraint "${constraint.constraintId}"`
|
|
768
|
+
);
|
|
758
769
|
}
|
|
759
770
|
function applyCustomAnnotation(schema, annotation, ctx) {
|
|
760
771
|
const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
|
|
@@ -766,7 +777,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
|
|
|
766
777
|
if (registration.toJsonSchema === void 0) {
|
|
767
778
|
return;
|
|
768
779
|
}
|
|
769
|
-
|
|
780
|
+
assignVendorPrefixedExtensionKeywords(
|
|
781
|
+
schema,
|
|
782
|
+
registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
|
|
783
|
+
ctx.vendorPrefix,
|
|
784
|
+
`custom annotation "${annotation.annotationId}"`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
|
|
788
|
+
for (const [key, value] of Object.entries(extensionSchema)) {
|
|
789
|
+
if (!key.startsWith(`${vendorPrefix}-`)) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
schema[key] = value;
|
|
795
|
+
}
|
|
770
796
|
}
|
|
771
797
|
|
|
772
798
|
// src/json-schema/generator.ts
|
|
@@ -1015,7 +1041,10 @@ function getSchemaExtension(schema, key) {
|
|
|
1015
1041
|
// src/extensions/registry.ts
|
|
1016
1042
|
function createExtensionRegistry(extensions) {
|
|
1017
1043
|
const typeMap = /* @__PURE__ */ new Map();
|
|
1044
|
+
const typeNameMap = /* @__PURE__ */ new Map();
|
|
1018
1045
|
const constraintMap = /* @__PURE__ */ new Map();
|
|
1046
|
+
const constraintTagMap = /* @__PURE__ */ new Map();
|
|
1047
|
+
const builtinBroadeningMap = /* @__PURE__ */ new Map();
|
|
1019
1048
|
const annotationMap = /* @__PURE__ */ new Map();
|
|
1020
1049
|
for (const ext of extensions) {
|
|
1021
1050
|
if (ext.types !== void 0) {
|
|
@@ -1025,6 +1054,27 @@ function createExtensionRegistry(extensions) {
|
|
|
1025
1054
|
throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
|
|
1026
1055
|
}
|
|
1027
1056
|
typeMap.set(qualifiedId, type);
|
|
1057
|
+
for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
|
|
1058
|
+
if (typeNameMap.has(sourceTypeName)) {
|
|
1059
|
+
throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
|
|
1060
|
+
}
|
|
1061
|
+
typeNameMap.set(sourceTypeName, {
|
|
1062
|
+
extensionId: ext.extensionId,
|
|
1063
|
+
registration: type
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (type.builtinConstraintBroadenings !== void 0) {
|
|
1067
|
+
for (const broadening of type.builtinConstraintBroadenings) {
|
|
1068
|
+
const key = `${qualifiedId}:${broadening.tagName}`;
|
|
1069
|
+
if (builtinBroadeningMap.has(key)) {
|
|
1070
|
+
throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
|
|
1071
|
+
}
|
|
1072
|
+
builtinBroadeningMap.set(key, {
|
|
1073
|
+
extensionId: ext.extensionId,
|
|
1074
|
+
registration: broadening
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1028
1078
|
}
|
|
1029
1079
|
}
|
|
1030
1080
|
if (ext.constraints !== void 0) {
|
|
@@ -1036,6 +1086,17 @@ function createExtensionRegistry(extensions) {
|
|
|
1036
1086
|
constraintMap.set(qualifiedId, constraint);
|
|
1037
1087
|
}
|
|
1038
1088
|
}
|
|
1089
|
+
if (ext.constraintTags !== void 0) {
|
|
1090
|
+
for (const tag of ext.constraintTags) {
|
|
1091
|
+
if (constraintTagMap.has(tag.tagName)) {
|
|
1092
|
+
throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
|
|
1093
|
+
}
|
|
1094
|
+
constraintTagMap.set(tag.tagName, {
|
|
1095
|
+
extensionId: ext.extensionId,
|
|
1096
|
+
registration: tag
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1039
1100
|
if (ext.annotations !== void 0) {
|
|
1040
1101
|
for (const annotation of ext.annotations) {
|
|
1041
1102
|
const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
|
|
@@ -1049,7 +1110,10 @@ function createExtensionRegistry(extensions) {
|
|
|
1049
1110
|
return {
|
|
1050
1111
|
extensions,
|
|
1051
1112
|
findType: (typeId) => typeMap.get(typeId),
|
|
1113
|
+
findTypeByName: (typeName) => typeNameMap.get(typeName),
|
|
1052
1114
|
findConstraint: (constraintId) => constraintMap.get(constraintId),
|
|
1115
|
+
findConstraintTag: (tagName) => constraintTagMap.get(tagName),
|
|
1116
|
+
findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
|
|
1053
1117
|
findAnnotation: (annotationId) => annotationMap.get(annotationId)
|
|
1054
1118
|
};
|
|
1055
1119
|
}
|
|
@@ -1117,6 +1181,7 @@ var jsonSchema7Schema = import_zod3.z.lazy(
|
|
|
1117
1181
|
);
|
|
1118
1182
|
|
|
1119
1183
|
// src/validate/constraint-validator.ts
|
|
1184
|
+
var import_core3 = require("@formspec/core");
|
|
1120
1185
|
function addContradiction(ctx, message, primary, related) {
|
|
1121
1186
|
ctx.diagnostics.push({
|
|
1122
1187
|
code: "CONTRADICTING_CONSTRAINTS",
|
|
@@ -1162,6 +1227,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
|
|
|
1162
1227
|
relatedLocations: [related]
|
|
1163
1228
|
});
|
|
1164
1229
|
}
|
|
1230
|
+
function getExtensionIdFromConstraintId(constraintId) {
|
|
1231
|
+
const separator = constraintId.lastIndexOf("/");
|
|
1232
|
+
if (separator <= 0) {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
return constraintId.slice(0, separator);
|
|
1236
|
+
}
|
|
1165
1237
|
function findNumeric(constraints, constraintKind) {
|
|
1166
1238
|
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1167
1239
|
}
|
|
@@ -1332,6 +1404,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
|
|
|
1332
1404
|
strongestByKey.set(key, constraint);
|
|
1333
1405
|
}
|
|
1334
1406
|
}
|
|
1407
|
+
function compareCustomConstraintStrength(current, previous) {
|
|
1408
|
+
const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
|
|
1409
|
+
const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
|
|
1410
|
+
switch (current.role.bound) {
|
|
1411
|
+
case "lower":
|
|
1412
|
+
return equalPayloadTiebreaker;
|
|
1413
|
+
case "upper":
|
|
1414
|
+
return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
|
|
1415
|
+
case "exact":
|
|
1416
|
+
return order === 0 ? 0 : Number.NaN;
|
|
1417
|
+
default: {
|
|
1418
|
+
const _exhaustive = current.role.bound;
|
|
1419
|
+
return _exhaustive;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function compareSemanticInclusivity(currentInclusive, previousInclusive) {
|
|
1424
|
+
if (currentInclusive === previousInclusive) {
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1427
|
+
return currentInclusive ? -1 : 1;
|
|
1428
|
+
}
|
|
1429
|
+
function customConstraintsContradict(lower, upper) {
|
|
1430
|
+
const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
|
|
1431
|
+
if (order > 0) {
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
if (order < 0) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
return !lower.role.inclusive || !upper.role.inclusive;
|
|
1438
|
+
}
|
|
1439
|
+
function describeCustomConstraintTag(constraint) {
|
|
1440
|
+
return constraint.provenance.tagName ?? constraint.constraintId;
|
|
1441
|
+
}
|
|
1442
|
+
function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
|
|
1443
|
+
if (ctx.extensionRegistry === void 0) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const strongestByKey = /* @__PURE__ */ new Map();
|
|
1447
|
+
const lowerByFamily = /* @__PURE__ */ new Map();
|
|
1448
|
+
const upperByFamily = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const constraint of constraints) {
|
|
1450
|
+
if (constraint.constraintKind !== "custom") {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
|
|
1454
|
+
if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const entry = {
|
|
1458
|
+
constraint,
|
|
1459
|
+
comparePayloads: registration.comparePayloads,
|
|
1460
|
+
role: registration.semanticRole
|
|
1461
|
+
};
|
|
1462
|
+
const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
|
|
1463
|
+
const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
|
|
1464
|
+
const previous = strongestByKey.get(boundKey);
|
|
1465
|
+
if (previous !== void 0) {
|
|
1466
|
+
const strength = compareCustomConstraintStrength(entry, previous);
|
|
1467
|
+
if (Number.isNaN(strength)) {
|
|
1468
|
+
addContradiction(
|
|
1469
|
+
ctx,
|
|
1470
|
+
`Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
|
|
1471
|
+
constraint.provenance,
|
|
1472
|
+
previous.constraint.provenance
|
|
1473
|
+
);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (strength < 0) {
|
|
1477
|
+
addConstraintBroadening(
|
|
1478
|
+
ctx,
|
|
1479
|
+
`Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
|
|
1480
|
+
constraint.provenance,
|
|
1481
|
+
previous.constraint.provenance
|
|
1482
|
+
);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (strength > 0) {
|
|
1486
|
+
strongestByKey.set(boundKey, entry);
|
|
1487
|
+
}
|
|
1488
|
+
} else {
|
|
1489
|
+
strongestByKey.set(boundKey, entry);
|
|
1490
|
+
}
|
|
1491
|
+
if (registration.semanticRole.bound === "lower") {
|
|
1492
|
+
lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
|
|
1493
|
+
} else if (registration.semanticRole.bound === "upper") {
|
|
1494
|
+
upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const [familyKey, lower] of lowerByFamily) {
|
|
1498
|
+
const upper = upperByFamily.get(familyKey);
|
|
1499
|
+
if (upper === void 0) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (!customConstraintsContradict(lower, upper)) {
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
addContradiction(
|
|
1506
|
+
ctx,
|
|
1507
|
+
`Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
|
|
1508
|
+
lower.constraint.provenance,
|
|
1509
|
+
upper.constraint.provenance
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1335
1513
|
function checkNumericContradictions(ctx, fieldName, constraints) {
|
|
1336
1514
|
const min = findNumeric(constraints, "minimum");
|
|
1337
1515
|
const max = findNumeric(constraints, "maximum");
|
|
@@ -1479,6 +1657,26 @@ function dereferenceType(ctx, type) {
|
|
|
1479
1657
|
}
|
|
1480
1658
|
return current;
|
|
1481
1659
|
}
|
|
1660
|
+
function collectReferencedTypeConstraints(ctx, type) {
|
|
1661
|
+
const collected = [];
|
|
1662
|
+
let current = type;
|
|
1663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1664
|
+
while (current.kind === "reference") {
|
|
1665
|
+
if (seen.has(current.name)) {
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
seen.add(current.name);
|
|
1669
|
+
const definition = ctx.typeRegistry[current.name];
|
|
1670
|
+
if (definition === void 0) {
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
if (definition.constraints !== void 0) {
|
|
1674
|
+
collected.push(...definition.constraints);
|
|
1675
|
+
}
|
|
1676
|
+
current = definition.type;
|
|
1677
|
+
}
|
|
1678
|
+
return collected;
|
|
1679
|
+
}
|
|
1482
1680
|
function resolvePathTargetType(ctx, type, segments) {
|
|
1483
1681
|
const effectiveType = dereferenceType(ctx, type);
|
|
1484
1682
|
if (segments.length === 0) {
|
|
@@ -1500,12 +1698,33 @@ function resolvePathTargetType(ctx, type, segments) {
|
|
|
1500
1698
|
}
|
|
1501
1699
|
return { kind: "unresolvable", type: effectiveType };
|
|
1502
1700
|
}
|
|
1701
|
+
function isNullType(type) {
|
|
1702
|
+
return type.kind === "primitive" && type.primitiveKind === "null";
|
|
1703
|
+
}
|
|
1704
|
+
function collectCustomConstraintCandidateTypes(ctx, type) {
|
|
1705
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
1706
|
+
const candidates = [effectiveType];
|
|
1707
|
+
if (effectiveType.kind === "array") {
|
|
1708
|
+
candidates.push(...collectCustomConstraintCandidateTypes(ctx, effectiveType.items));
|
|
1709
|
+
}
|
|
1710
|
+
if (effectiveType.kind === "union") {
|
|
1711
|
+
const memberTypes = effectiveType.members.map((member) => dereferenceType(ctx, member));
|
|
1712
|
+
const nonNullMembers = memberTypes.filter((member) => !isNullType(member));
|
|
1713
|
+
if (nonNullMembers.length === 1 && nonNullMembers.length < memberTypes.length) {
|
|
1714
|
+
const [nullableMember] = nonNullMembers;
|
|
1715
|
+
if (nullableMember !== void 0) {
|
|
1716
|
+
candidates.push(...collectCustomConstraintCandidateTypes(ctx, nullableMember));
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return candidates;
|
|
1721
|
+
}
|
|
1503
1722
|
function formatPathTargetFieldName(fieldName, path) {
|
|
1504
1723
|
return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
|
|
1505
1724
|
}
|
|
1506
1725
|
function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
1507
1726
|
const effectiveType = dereferenceType(ctx, type);
|
|
1508
|
-
const isNumber = effectiveType.kind === "primitive" &&
|
|
1727
|
+
const isNumber = effectiveType.kind === "primitive" && ["number", "integer", "bigint"].includes(effectiveType.primitiveKind);
|
|
1509
1728
|
const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
|
|
1510
1729
|
const isArray = effectiveType.kind === "array";
|
|
1511
1730
|
const isEnum = effectiveType.kind === "enum";
|
|
@@ -1563,7 +1782,9 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
|
1563
1782
|
break;
|
|
1564
1783
|
}
|
|
1565
1784
|
case "const": {
|
|
1566
|
-
const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(
|
|
1785
|
+
const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "integer", "bigint", "boolean", "null"].includes(
|
|
1786
|
+
effectiveType.primitiveKind
|
|
1787
|
+
) || effectiveType.kind === "enum";
|
|
1567
1788
|
if (!isPrimitiveConstType) {
|
|
1568
1789
|
addTypeMismatch(
|
|
1569
1790
|
ctx,
|
|
@@ -1574,7 +1795,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
|
1574
1795
|
}
|
|
1575
1796
|
if (effectiveType.kind === "primitive") {
|
|
1576
1797
|
const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
|
|
1577
|
-
|
|
1798
|
+
const expectedValueType = effectiveType.primitiveKind === "integer" || effectiveType.primitiveKind === "bigint" ? "number" : effectiveType.primitiveKind;
|
|
1799
|
+
if (valueType !== expectedValueType) {
|
|
1578
1800
|
addTypeMismatch(
|
|
1579
1801
|
ctx,
|
|
1580
1802
|
`Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
|
|
@@ -1643,8 +1865,37 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
|
1643
1865
|
);
|
|
1644
1866
|
return;
|
|
1645
1867
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1868
|
+
const candidateTypes = collectCustomConstraintCandidateTypes(ctx, type);
|
|
1869
|
+
const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core3.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
|
|
1870
|
+
if (normalizedTagName !== void 0) {
|
|
1871
|
+
const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
|
|
1872
|
+
const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
|
|
1873
|
+
if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && !candidateTypes.some(
|
|
1874
|
+
(candidateType) => tagRegistration.registration.isApplicableToType?.(candidateType) !== false
|
|
1875
|
+
)) {
|
|
1876
|
+
addTypeMismatch(
|
|
1877
|
+
ctx,
|
|
1878
|
+
`Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
|
|
1879
|
+
constraint.provenance
|
|
1880
|
+
);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (registration.applicableTypes === null) {
|
|
1885
|
+
if (!candidateTypes.some((candidateType) => registration.isApplicableToType?.(candidateType) !== false)) {
|
|
1886
|
+
addTypeMismatch(
|
|
1887
|
+
ctx,
|
|
1888
|
+
`Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
|
|
1889
|
+
constraint.provenance
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const applicableTypes = registration.applicableTypes;
|
|
1895
|
+
const matchesApplicableType = candidateTypes.some(
|
|
1896
|
+
(candidateType) => applicableTypes.includes(candidateType.kind) && registration.isApplicableToType?.(candidateType) !== false
|
|
1897
|
+
);
|
|
1898
|
+
if (!matchesApplicableType) {
|
|
1648
1899
|
addTypeMismatch(
|
|
1649
1900
|
ctx,
|
|
1650
1901
|
`Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
|
|
@@ -1653,7 +1904,10 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
|
1653
1904
|
}
|
|
1654
1905
|
}
|
|
1655
1906
|
function validateFieldNode(ctx, field) {
|
|
1656
|
-
validateConstraints(ctx, field.name, field.type,
|
|
1907
|
+
validateConstraints(ctx, field.name, field.type, [
|
|
1908
|
+
...collectReferencedTypeConstraints(ctx, field.type),
|
|
1909
|
+
...field.constraints
|
|
1910
|
+
]);
|
|
1657
1911
|
if (field.type.kind === "object") {
|
|
1658
1912
|
for (const prop of field.type.properties) {
|
|
1659
1913
|
validateObjectProperty(ctx, field.name, prop);
|
|
@@ -1662,7 +1916,10 @@ function validateFieldNode(ctx, field) {
|
|
|
1662
1916
|
}
|
|
1663
1917
|
function validateObjectProperty(ctx, parentName, prop) {
|
|
1664
1918
|
const qualifiedName = `${parentName}.${prop.name}`;
|
|
1665
|
-
validateConstraints(ctx, qualifiedName, prop.type,
|
|
1919
|
+
validateConstraints(ctx, qualifiedName, prop.type, [
|
|
1920
|
+
...collectReferencedTypeConstraints(ctx, prop.type),
|
|
1921
|
+
...prop.constraints
|
|
1922
|
+
]);
|
|
1666
1923
|
if (prop.type.kind === "object") {
|
|
1667
1924
|
for (const nestedProp of prop.type.properties) {
|
|
1668
1925
|
validateObjectProperty(ctx, qualifiedName, nestedProp);
|
|
@@ -1675,6 +1932,7 @@ function validateConstraints(ctx, name, type, constraints) {
|
|
|
1675
1932
|
checkAllowedMembersContradiction(ctx, name, constraints);
|
|
1676
1933
|
checkConstContradictions(ctx, name, constraints);
|
|
1677
1934
|
checkConstraintBroadening(ctx, name, constraints);
|
|
1935
|
+
checkCustomConstraintSemantics(ctx, name, constraints);
|
|
1678
1936
|
checkTypeApplicability(ctx, name, type, constraints);
|
|
1679
1937
|
}
|
|
1680
1938
|
function validateElement(ctx, element) {
|