@formspec/build 0.1.0-alpha.11 → 0.1.0-alpha.12
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 +51 -15
- package/dist/__tests__/chain-dsl-canonicalizer.test.d.ts +2 -0
- package/dist/__tests__/chain-dsl-canonicalizer.test.d.ts.map +1 -0
- package/dist/__tests__/constraint-validator.test.d.ts +2 -0
- package/dist/__tests__/constraint-validator.test.d.ts.map +1 -0
- package/dist/__tests__/extension-api.test.d.ts +2 -0
- package/dist/__tests__/extension-api.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +18 -0
- package/dist/__tests__/fixtures/example-a-builtins.d.ts.map +1 -1
- package/dist/__tests__/ir-analyzer.test.d.ts +11 -0
- package/dist/__tests__/ir-analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/ir-jsdoc-constraints.test.d.ts +12 -0
- package/dist/__tests__/ir-jsdoc-constraints.test.d.ts.map +1 -0
- package/dist/__tests__/ir-json-schema-generator.test.d.ts +11 -0
- package/dist/__tests__/ir-json-schema-generator.test.d.ts.map +1 -0
- package/dist/__tests__/ir-ui-schema-generator.test.d.ts +2 -0
- package/dist/__tests__/ir-ui-schema-generator.test.d.ts.map +1 -0
- package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -4
- package/dist/__tests__/parity/fixtures/address/chain-dsl.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/address/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/address/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/address/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/address/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/address/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/chain-dsl.d.ts +13 -0
- package/dist/__tests__/parity/fixtures/product-config/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/product-config/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/tsdoc.d.ts +28 -0
- package/dist/__tests__/parity/fixtures/product-config/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/chain-dsl.d.ts +12 -0
- package/dist/__tests__/parity/fixtures/user-registration/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/user-registration/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/user-registration/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/parity.test.d.ts +14 -0
- package/dist/__tests__/parity/parity.test.d.ts.map +1 -0
- package/dist/__tests__/parity/utils.d.ts +139 -0
- package/dist/__tests__/parity/utils.d.ts.map +1 -0
- package/dist/analyzer/class-analyzer.d.ts +54 -99
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +78 -30
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/tsdoc-parser.d.ts +61 -0
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -0
- package/dist/browser.cjs +960 -309
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +10 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +958 -308
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +65 -150
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts +18 -0
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -0
- package/dist/canonicalize/index.d.ts +8 -0
- package/dist/canonicalize/index.d.ts.map +1 -0
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts +34 -0
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -0
- package/dist/cli.cjs +1417 -1656
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1421 -1647
- package/dist/cli.js.map +1 -1
- package/dist/extensions/index.d.ts +8 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/registry.d.ts +55 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/generators/class-schema.d.ts +23 -38
- package/dist/generators/class-schema.d.ts.map +1 -1
- package/dist/generators/method-schema.d.ts +6 -8
- package/dist/generators/method-schema.d.ts.map +1 -1
- package/dist/index.cjs +1353 -1614
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1365 -1610
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +1604 -824
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.d.ts +12 -3
- package/dist/internals.d.ts.map +1 -1
- package/dist/internals.js +1607 -820
- package/dist/internals.js.map +1 -1
- package/dist/json-schema/generator.d.ts +10 -5
- package/dist/json-schema/generator.d.ts.map +1 -1
- package/dist/json-schema/ir-generator.d.ts +84 -0
- package/dist/json-schema/ir-generator.d.ts.map +1 -0
- package/dist/json-schema/schema.d.ts +3 -3
- package/dist/json-schema/types.d.ts +5 -6
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/ui-schema/generator.d.ts +5 -15
- package/dist/ui-schema/generator.d.ts.map +1 -1
- package/dist/ui-schema/ir-generator.d.ts +53 -0
- package/dist/ui-schema/ir-generator.d.ts.map +1 -0
- package/dist/validate/constraint-validator.d.ts +66 -0
- package/dist/validate/constraint-validator.d.ts.map +1 -0
- package/dist/validate/index.d.ts +9 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/package.json +5 -4
- package/dist/__tests__/analyzer-edge-cases.test.d.ts +0 -13
- package/dist/__tests__/analyzer-edge-cases.test.d.ts.map +0 -1
- package/dist/__tests__/analyzer.test.d.ts +0 -5
- package/dist/__tests__/analyzer.test.d.ts.map +0 -1
- package/dist/__tests__/codegen.test.d.ts +0 -5
- package/dist/__tests__/codegen.test.d.ts.map +0 -1
- package/dist/__tests__/decorator-pipeline.test.d.ts +0 -11
- package/dist/__tests__/decorator-pipeline.test.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-b-decorators.d.ts +0 -5
- package/dist/__tests__/fixtures/example-b-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-b-extended.d.ts +0 -5
- package/dist/__tests__/fixtures/example-b-extended.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-c-custom.d.ts +0 -5
- package/dist/__tests__/fixtures/example-c-custom.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-c-decorators.d.ts +0 -5
- package/dist/__tests__/fixtures/example-c-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts +0 -6
- package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-e-decorators.d.ts +0 -11
- package/dist/__tests__/fixtures/example-e-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-e-no-namespace.d.ts +0 -5
- package/dist/__tests__/fixtures/example-e-no-namespace.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts +0 -16
- package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-nested-class.d.ts +0 -45
- package/dist/__tests__/fixtures/example-nested-class.d.ts.map +0 -1
- package/dist/__tests__/interface-types.test.d.ts +0 -11
- package/dist/__tests__/interface-types.test.d.ts.map +0 -1
- package/dist/analyzer/decorator-extractor.d.ts +0 -78
- package/dist/analyzer/decorator-extractor.d.ts.map +0 -1
- package/dist/analyzer/type-converter.d.ts +0 -75
- package/dist/analyzer/type-converter.d.ts.map +0 -1
- package/dist/codegen/index.d.ts +0 -75
- package/dist/codegen/index.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,204 +1,603 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
var
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
]);
|
|
12
|
-
var jsonSchema7Schema = z.lazy(
|
|
13
|
-
() => z.object({
|
|
14
|
-
$schema: z.string().optional(),
|
|
15
|
-
$id: z.string().optional(),
|
|
16
|
-
$ref: z.string().optional(),
|
|
17
|
-
// Metadata
|
|
18
|
-
title: z.string().optional(),
|
|
19
|
-
description: z.string().optional(),
|
|
20
|
-
deprecated: z.boolean().optional(),
|
|
21
|
-
// Type
|
|
22
|
-
type: z.union([jsonSchemaTypeSchema, z.array(jsonSchemaTypeSchema)]).optional(),
|
|
23
|
-
// String validation
|
|
24
|
-
minLength: z.number().optional(),
|
|
25
|
-
maxLength: z.number().optional(),
|
|
26
|
-
pattern: z.string().optional(),
|
|
27
|
-
// Number validation
|
|
28
|
-
minimum: z.number().optional(),
|
|
29
|
-
maximum: z.number().optional(),
|
|
30
|
-
exclusiveMinimum: z.number().optional(),
|
|
31
|
-
exclusiveMaximum: z.number().optional(),
|
|
32
|
-
// Enum
|
|
33
|
-
enum: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).readonly().optional(),
|
|
34
|
-
const: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
35
|
-
// Object
|
|
36
|
-
properties: z.record(z.string(), jsonSchema7Schema).optional(),
|
|
37
|
-
required: z.array(z.string()).optional(),
|
|
38
|
-
additionalProperties: z.union([z.boolean(), jsonSchema7Schema]).optional(),
|
|
39
|
-
// Array
|
|
40
|
-
items: z.union([jsonSchema7Schema, z.array(jsonSchema7Schema)]).optional(),
|
|
41
|
-
minItems: z.number().optional(),
|
|
42
|
-
maxItems: z.number().optional(),
|
|
43
|
-
// Composition
|
|
44
|
-
allOf: z.array(jsonSchema7Schema).optional(),
|
|
45
|
-
anyOf: z.array(jsonSchema7Schema).optional(),
|
|
46
|
-
oneOf: z.array(jsonSchema7Schema).optional(),
|
|
47
|
-
not: jsonSchema7Schema.optional(),
|
|
48
|
-
// Conditional
|
|
49
|
-
if: jsonSchema7Schema.optional(),
|
|
50
|
-
then: jsonSchema7Schema.optional(),
|
|
51
|
-
else: jsonSchema7Schema.optional(),
|
|
52
|
-
// Format
|
|
53
|
-
format: z.string().optional(),
|
|
54
|
-
// Default
|
|
55
|
-
default: z.unknown().optional(),
|
|
56
|
-
// FormSpec extensions
|
|
57
|
-
"x-formspec-source": z.string().optional(),
|
|
58
|
-
"x-formspec-params": z.array(z.string()).readonly().optional(),
|
|
59
|
-
"x-formspec-schemaSource": z.string().optional()
|
|
60
|
-
}).passthrough()
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
// src/json-schema/generator.ts
|
|
64
|
-
import { z as z2 } from "zod";
|
|
65
|
-
function parseOrThrow(schema, value, label) {
|
|
66
|
-
try {
|
|
67
|
-
return schema.parse(value);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
if (error instanceof z2.ZodError) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
`Generated ${label} failed validation:
|
|
72
|
-
${error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
1
|
+
// src/canonicalize/chain-dsl-canonicalizer.ts
|
|
2
|
+
import { IR_VERSION } from "@formspec/core";
|
|
3
|
+
var CHAIN_DSL_PROVENANCE = {
|
|
4
|
+
surface: "chain-dsl",
|
|
5
|
+
file: "",
|
|
6
|
+
line: 0,
|
|
7
|
+
column: 0
|
|
8
|
+
};
|
|
9
|
+
function isGroup(el) {
|
|
10
|
+
return el._type === "group";
|
|
77
11
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
12
|
+
function isConditional(el) {
|
|
13
|
+
return el._type === "conditional";
|
|
14
|
+
}
|
|
15
|
+
function isField(el) {
|
|
16
|
+
return el._type === "field";
|
|
17
|
+
}
|
|
18
|
+
function canonicalizeChainDSL(form) {
|
|
83
19
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
20
|
+
kind: "form-ir",
|
|
21
|
+
irVersion: IR_VERSION,
|
|
22
|
+
elements: canonicalizeElements(form.elements),
|
|
23
|
+
typeRegistry: {},
|
|
24
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
87
25
|
};
|
|
88
26
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
27
|
+
function canonicalizeElements(elements) {
|
|
28
|
+
return elements.map(canonicalizeElement);
|
|
29
|
+
}
|
|
30
|
+
function canonicalizeElement(element) {
|
|
31
|
+
if (isField(element)) {
|
|
32
|
+
return canonicalizeField(element);
|
|
33
|
+
}
|
|
34
|
+
if (isGroup(element)) {
|
|
35
|
+
return canonicalizeGroup(element);
|
|
36
|
+
}
|
|
37
|
+
if (isConditional(element)) {
|
|
38
|
+
return canonicalizeConditional(element);
|
|
93
39
|
}
|
|
40
|
+
const _exhaustive = element;
|
|
41
|
+
throw new Error(`Unknown element type: ${JSON.stringify(_exhaustive)}`);
|
|
42
|
+
}
|
|
43
|
+
function canonicalizeField(field) {
|
|
94
44
|
switch (field._field) {
|
|
95
45
|
case "text":
|
|
96
|
-
return
|
|
46
|
+
return canonicalizeTextField(field);
|
|
97
47
|
case "number":
|
|
98
|
-
return
|
|
99
|
-
...base,
|
|
100
|
-
type: "number",
|
|
101
|
-
...field.min !== void 0 && { minimum: field.min },
|
|
102
|
-
...field.max !== void 0 && { maximum: field.max }
|
|
103
|
-
};
|
|
48
|
+
return canonicalizeNumberField(field);
|
|
104
49
|
case "boolean":
|
|
105
|
-
return
|
|
106
|
-
case "enum":
|
|
107
|
-
|
|
108
|
-
const isObjectOptions = opts.length > 0 && opts.every(
|
|
109
|
-
(opt) => typeof opt === "object" && "id" in opt && "label" in opt
|
|
110
|
-
);
|
|
111
|
-
if (isObjectOptions) {
|
|
112
|
-
return {
|
|
113
|
-
...base,
|
|
114
|
-
type: "string",
|
|
115
|
-
oneOf: opts.map((o) => ({
|
|
116
|
-
const: o.id,
|
|
117
|
-
title: o.label
|
|
118
|
-
}))
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return { ...base, type: "string", enum: opts };
|
|
122
|
-
}
|
|
50
|
+
return canonicalizeBooleanField(field);
|
|
51
|
+
case "enum":
|
|
52
|
+
return canonicalizeStaticEnumField(field);
|
|
123
53
|
case "dynamic_enum":
|
|
124
|
-
return
|
|
125
|
-
...base,
|
|
126
|
-
type: "string",
|
|
127
|
-
"x-formspec-source": field.source,
|
|
128
|
-
...field.params !== void 0 && field.params.length > 0 && { "x-formspec-params": field.params }
|
|
129
|
-
};
|
|
54
|
+
return canonicalizeDynamicEnumField(field);
|
|
130
55
|
case "dynamic_schema":
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
137
|
-
case "array": {
|
|
138
|
-
const arrayField = field;
|
|
139
|
-
return {
|
|
140
|
-
...base,
|
|
141
|
-
type: "array",
|
|
142
|
-
items: generateNestedSchema(arrayField.items),
|
|
143
|
-
...arrayField.minItems !== void 0 && { minItems: arrayField.minItems },
|
|
144
|
-
...arrayField.maxItems !== void 0 && { maxItems: arrayField.maxItems }
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
case "object": {
|
|
148
|
-
const objectField = field;
|
|
149
|
-
const nestedSchema = generateNestedSchema(objectField.properties);
|
|
150
|
-
return {
|
|
151
|
-
...base,
|
|
152
|
-
...nestedSchema
|
|
153
|
-
};
|
|
154
|
-
}
|
|
56
|
+
return canonicalizeDynamicSchemaField(field);
|
|
57
|
+
case "array":
|
|
58
|
+
return canonicalizeArrayField(field);
|
|
59
|
+
case "object":
|
|
60
|
+
return canonicalizeObjectField(field);
|
|
155
61
|
default: {
|
|
156
62
|
const _exhaustive = field;
|
|
157
|
-
|
|
63
|
+
throw new Error(`Unknown field type: ${JSON.stringify(_exhaustive)}`);
|
|
158
64
|
}
|
|
159
65
|
}
|
|
160
66
|
}
|
|
161
|
-
function
|
|
67
|
+
function canonicalizeTextField(field) {
|
|
68
|
+
const type = { kind: "primitive", primitiveKind: "string" };
|
|
69
|
+
return buildFieldNode(
|
|
70
|
+
field.name,
|
|
71
|
+
type,
|
|
72
|
+
field.required,
|
|
73
|
+
buildAnnotations(field.label, field.placeholder)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function canonicalizeNumberField(field) {
|
|
77
|
+
const type = { kind: "primitive", primitiveKind: "number" };
|
|
78
|
+
const constraints = [];
|
|
79
|
+
if (field.min !== void 0) {
|
|
80
|
+
const c = {
|
|
81
|
+
kind: "constraint",
|
|
82
|
+
constraintKind: "minimum",
|
|
83
|
+
value: field.min,
|
|
84
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
85
|
+
};
|
|
86
|
+
constraints.push(c);
|
|
87
|
+
}
|
|
88
|
+
if (field.max !== void 0) {
|
|
89
|
+
const c = {
|
|
90
|
+
kind: "constraint",
|
|
91
|
+
constraintKind: "maximum",
|
|
92
|
+
value: field.max,
|
|
93
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
94
|
+
};
|
|
95
|
+
constraints.push(c);
|
|
96
|
+
}
|
|
97
|
+
return buildFieldNode(
|
|
98
|
+
field.name,
|
|
99
|
+
type,
|
|
100
|
+
field.required,
|
|
101
|
+
buildAnnotations(field.label),
|
|
102
|
+
constraints
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function canonicalizeBooleanField(field) {
|
|
106
|
+
const type = { kind: "primitive", primitiveKind: "boolean" };
|
|
107
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
108
|
+
}
|
|
109
|
+
function canonicalizeStaticEnumField(field) {
|
|
110
|
+
const members = field.options.map((opt) => {
|
|
111
|
+
if (typeof opt === "string") {
|
|
112
|
+
return { value: opt };
|
|
113
|
+
}
|
|
114
|
+
return { value: opt.id, displayName: opt.label };
|
|
115
|
+
});
|
|
116
|
+
const type = { kind: "enum", members };
|
|
117
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
118
|
+
}
|
|
119
|
+
function canonicalizeDynamicEnumField(field) {
|
|
120
|
+
const type = {
|
|
121
|
+
kind: "dynamic",
|
|
122
|
+
dynamicKind: "enum",
|
|
123
|
+
sourceKey: field.source,
|
|
124
|
+
parameterFields: field.params ? [...field.params] : []
|
|
125
|
+
};
|
|
126
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
127
|
+
}
|
|
128
|
+
function canonicalizeDynamicSchemaField(field) {
|
|
129
|
+
const type = {
|
|
130
|
+
kind: "dynamic",
|
|
131
|
+
dynamicKind: "schema",
|
|
132
|
+
sourceKey: field.schemaSource,
|
|
133
|
+
parameterFields: []
|
|
134
|
+
};
|
|
135
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
136
|
+
}
|
|
137
|
+
function canonicalizeArrayField(field) {
|
|
138
|
+
const itemProperties = buildObjectProperties(field.items);
|
|
139
|
+
const itemsType = {
|
|
140
|
+
kind: "object",
|
|
141
|
+
properties: itemProperties,
|
|
142
|
+
additionalProperties: false
|
|
143
|
+
};
|
|
144
|
+
const type = { kind: "array", items: itemsType };
|
|
145
|
+
const constraints = [];
|
|
146
|
+
if (field.minItems !== void 0) {
|
|
147
|
+
const c = {
|
|
148
|
+
kind: "constraint",
|
|
149
|
+
constraintKind: "minItems",
|
|
150
|
+
value: field.minItems,
|
|
151
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
152
|
+
};
|
|
153
|
+
constraints.push(c);
|
|
154
|
+
}
|
|
155
|
+
if (field.maxItems !== void 0) {
|
|
156
|
+
const c = {
|
|
157
|
+
kind: "constraint",
|
|
158
|
+
constraintKind: "maxItems",
|
|
159
|
+
value: field.maxItems,
|
|
160
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
161
|
+
};
|
|
162
|
+
constraints.push(c);
|
|
163
|
+
}
|
|
164
|
+
return buildFieldNode(
|
|
165
|
+
field.name,
|
|
166
|
+
type,
|
|
167
|
+
field.required,
|
|
168
|
+
buildAnnotations(field.label),
|
|
169
|
+
constraints
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function canonicalizeObjectField(field) {
|
|
173
|
+
const properties = buildObjectProperties(field.properties);
|
|
174
|
+
const type = {
|
|
175
|
+
kind: "object",
|
|
176
|
+
properties,
|
|
177
|
+
additionalProperties: false
|
|
178
|
+
};
|
|
179
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
180
|
+
}
|
|
181
|
+
function canonicalizeGroup(g) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "group",
|
|
184
|
+
label: g.label,
|
|
185
|
+
elements: canonicalizeElements(g.elements),
|
|
186
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function canonicalizeConditional(c) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "conditional",
|
|
192
|
+
fieldName: c.field,
|
|
193
|
+
// Conditional values from the chain DSL are JSON-serializable primitives
|
|
194
|
+
// (strings, numbers, booleans) produced by the `is()` predicate helper.
|
|
195
|
+
value: assertJsonValue(c.value),
|
|
196
|
+
elements: canonicalizeElements(c.elements),
|
|
197
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assertJsonValue(v) {
|
|
201
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(v)) {
|
|
205
|
+
return v.map(assertJsonValue);
|
|
206
|
+
}
|
|
207
|
+
if (typeof v === "object") {
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [key, val] of Object.entries(v)) {
|
|
210
|
+
result[key] = assertJsonValue(val);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
throw new TypeError(`Conditional value is not a valid JsonValue: ${typeof v}`);
|
|
215
|
+
}
|
|
216
|
+
function buildFieldNode(name, type, required, annotations, constraints = []) {
|
|
217
|
+
return {
|
|
218
|
+
kind: "field",
|
|
219
|
+
name,
|
|
220
|
+
type,
|
|
221
|
+
required: required === true,
|
|
222
|
+
constraints,
|
|
223
|
+
annotations,
|
|
224
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function buildAnnotations(label, placeholder) {
|
|
228
|
+
const annotations = [];
|
|
229
|
+
if (label !== void 0) {
|
|
230
|
+
const a = {
|
|
231
|
+
kind: "annotation",
|
|
232
|
+
annotationKind: "displayName",
|
|
233
|
+
value: label,
|
|
234
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
235
|
+
};
|
|
236
|
+
annotations.push(a);
|
|
237
|
+
}
|
|
238
|
+
if (placeholder !== void 0) {
|
|
239
|
+
const a = {
|
|
240
|
+
kind: "annotation",
|
|
241
|
+
annotationKind: "placeholder",
|
|
242
|
+
value: placeholder,
|
|
243
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
244
|
+
};
|
|
245
|
+
annotations.push(a);
|
|
246
|
+
}
|
|
247
|
+
return annotations;
|
|
248
|
+
}
|
|
249
|
+
function buildObjectProperties(elements, insideConditional = false) {
|
|
250
|
+
const properties = [];
|
|
251
|
+
for (const el of elements) {
|
|
252
|
+
if (isField(el)) {
|
|
253
|
+
const fieldNode = canonicalizeField(el);
|
|
254
|
+
properties.push({
|
|
255
|
+
name: fieldNode.name,
|
|
256
|
+
type: fieldNode.type,
|
|
257
|
+
// Fields inside a conditional branch are always optional in the
|
|
258
|
+
// data schema, regardless of their `required` flag — the condition
|
|
259
|
+
// may not be met, so the field may be absent.
|
|
260
|
+
optional: insideConditional || !fieldNode.required,
|
|
261
|
+
constraints: fieldNode.constraints,
|
|
262
|
+
annotations: fieldNode.annotations,
|
|
263
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
264
|
+
});
|
|
265
|
+
} else if (isGroup(el)) {
|
|
266
|
+
properties.push(...buildObjectProperties(el.elements, insideConditional));
|
|
267
|
+
} else if (isConditional(el)) {
|
|
268
|
+
properties.push(...buildObjectProperties(el.elements, true));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return properties;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/canonicalize/tsdoc-canonicalizer.ts
|
|
275
|
+
import { IR_VERSION as IR_VERSION2 } from "@formspec/core";
|
|
276
|
+
function canonicalizeTSDoc(analysis, source) {
|
|
277
|
+
const file = source?.file ?? "";
|
|
278
|
+
const provenance = {
|
|
279
|
+
surface: "tsdoc",
|
|
280
|
+
file,
|
|
281
|
+
line: 1,
|
|
282
|
+
column: 0
|
|
283
|
+
};
|
|
284
|
+
const elements = assembleElements(analysis.fields, analysis.fieldLayouts, provenance);
|
|
285
|
+
return {
|
|
286
|
+
kind: "form-ir",
|
|
287
|
+
irVersion: IR_VERSION2,
|
|
288
|
+
elements,
|
|
289
|
+
typeRegistry: analysis.typeRegistry,
|
|
290
|
+
provenance
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function assembleElements(fields, layouts, provenance) {
|
|
294
|
+
const elements = [];
|
|
295
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
296
|
+
const topLevelOrder = [];
|
|
297
|
+
for (let i = 0; i < fields.length; i++) {
|
|
298
|
+
const field = fields[i];
|
|
299
|
+
const layout = layouts[i];
|
|
300
|
+
if (!field || !layout) continue;
|
|
301
|
+
const element = wrapInConditional(field, layout, provenance);
|
|
302
|
+
if (layout.groupLabel !== void 0) {
|
|
303
|
+
const label = layout.groupLabel;
|
|
304
|
+
let groupElements = groupMap.get(label);
|
|
305
|
+
if (!groupElements) {
|
|
306
|
+
groupElements = [];
|
|
307
|
+
groupMap.set(label, groupElements);
|
|
308
|
+
topLevelOrder.push({ type: "group", label });
|
|
309
|
+
}
|
|
310
|
+
groupElements.push(element);
|
|
311
|
+
} else {
|
|
312
|
+
topLevelOrder.push({ type: "element", element });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const entry of topLevelOrder) {
|
|
316
|
+
if (entry.type === "group") {
|
|
317
|
+
const groupElements = groupMap.get(entry.label);
|
|
318
|
+
if (groupElements) {
|
|
319
|
+
const groupNode = {
|
|
320
|
+
kind: "group",
|
|
321
|
+
label: entry.label,
|
|
322
|
+
elements: groupElements,
|
|
323
|
+
provenance
|
|
324
|
+
};
|
|
325
|
+
elements.push(groupNode);
|
|
326
|
+
groupMap.delete(entry.label);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
elements.push(entry.element);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return elements;
|
|
333
|
+
}
|
|
334
|
+
function wrapInConditional(field, layout, provenance) {
|
|
335
|
+
if (layout.showWhen === void 0) {
|
|
336
|
+
return field;
|
|
337
|
+
}
|
|
338
|
+
const conditional = {
|
|
339
|
+
kind: "conditional",
|
|
340
|
+
fieldName: layout.showWhen.field,
|
|
341
|
+
value: layout.showWhen.value,
|
|
342
|
+
elements: [field],
|
|
343
|
+
provenance
|
|
344
|
+
};
|
|
345
|
+
return conditional;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/json-schema/ir-generator.ts
|
|
349
|
+
function makeContext() {
|
|
350
|
+
return { defs: {} };
|
|
351
|
+
}
|
|
352
|
+
function generateJsonSchemaFromIR(ir) {
|
|
353
|
+
const ctx = makeContext();
|
|
354
|
+
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
355
|
+
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
356
|
+
}
|
|
357
|
+
const properties = {};
|
|
358
|
+
const required = [];
|
|
359
|
+
collectFields(ir.elements, properties, required, ctx);
|
|
360
|
+
const uniqueRequired = [...new Set(required)];
|
|
361
|
+
const result = {
|
|
362
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
363
|
+
type: "object",
|
|
364
|
+
properties,
|
|
365
|
+
...uniqueRequired.length > 0 && { required: uniqueRequired }
|
|
366
|
+
};
|
|
367
|
+
if (Object.keys(ctx.defs).length > 0) {
|
|
368
|
+
result.$defs = ctx.defs;
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function collectFields(elements, properties, required, ctx) {
|
|
162
373
|
for (const element of elements) {
|
|
163
|
-
switch (element.
|
|
374
|
+
switch (element.kind) {
|
|
164
375
|
case "field":
|
|
165
|
-
properties[element.name] =
|
|
166
|
-
if (element.required
|
|
376
|
+
properties[element.name] = generateFieldSchema(element, ctx);
|
|
377
|
+
if (element.required) {
|
|
167
378
|
required.push(element.name);
|
|
168
379
|
}
|
|
169
380
|
break;
|
|
170
381
|
case "group":
|
|
171
|
-
collectFields(element.elements, properties, required);
|
|
382
|
+
collectFields(element.elements, properties, required, ctx);
|
|
172
383
|
break;
|
|
173
384
|
case "conditional":
|
|
174
|
-
collectFields(
|
|
175
|
-
element.elements,
|
|
176
|
-
properties,
|
|
177
|
-
required
|
|
178
|
-
);
|
|
385
|
+
collectFields(element.elements, properties, required, ctx);
|
|
179
386
|
break;
|
|
387
|
+
default: {
|
|
388
|
+
const _exhaustive = element;
|
|
389
|
+
void _exhaustive;
|
|
390
|
+
}
|
|
180
391
|
}
|
|
181
392
|
}
|
|
182
393
|
}
|
|
183
|
-
function
|
|
394
|
+
function generateFieldSchema(field, ctx) {
|
|
395
|
+
const schema = generateTypeNode(field.type, ctx);
|
|
396
|
+
applyConstraints(schema, field.constraints);
|
|
397
|
+
applyAnnotations(schema, field.annotations);
|
|
398
|
+
return schema;
|
|
399
|
+
}
|
|
400
|
+
function generateTypeNode(type, ctx) {
|
|
401
|
+
switch (type.kind) {
|
|
402
|
+
case "primitive":
|
|
403
|
+
return generatePrimitiveType(type);
|
|
404
|
+
case "enum":
|
|
405
|
+
return generateEnumType(type);
|
|
406
|
+
case "array":
|
|
407
|
+
return generateArrayType(type, ctx);
|
|
408
|
+
case "object":
|
|
409
|
+
return generateObjectType(type, ctx);
|
|
410
|
+
case "union":
|
|
411
|
+
return generateUnionType(type, ctx);
|
|
412
|
+
case "reference":
|
|
413
|
+
return generateReferenceType(type);
|
|
414
|
+
case "dynamic":
|
|
415
|
+
return generateDynamicType(type);
|
|
416
|
+
case "custom":
|
|
417
|
+
return generateCustomType(type);
|
|
418
|
+
default: {
|
|
419
|
+
const _exhaustive = type;
|
|
420
|
+
return _exhaustive;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function generatePrimitiveType(type) {
|
|
425
|
+
return { type: type.primitiveKind };
|
|
426
|
+
}
|
|
427
|
+
function generateEnumType(type) {
|
|
428
|
+
const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
|
|
429
|
+
if (hasDisplayNames) {
|
|
430
|
+
return {
|
|
431
|
+
oneOf: type.members.map((m) => {
|
|
432
|
+
const entry = { const: m.value };
|
|
433
|
+
if (m.displayName !== void 0) {
|
|
434
|
+
entry.title = m.displayName;
|
|
435
|
+
}
|
|
436
|
+
return entry;
|
|
437
|
+
})
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return { enum: type.members.map((m) => m.value) };
|
|
441
|
+
}
|
|
442
|
+
function generateArrayType(type, ctx) {
|
|
443
|
+
return {
|
|
444
|
+
type: "array",
|
|
445
|
+
items: generateTypeNode(type.items, ctx)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function generateObjectType(type, ctx) {
|
|
184
449
|
const properties = {};
|
|
185
450
|
const required = [];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
451
|
+
for (const prop of type.properties) {
|
|
452
|
+
properties[prop.name] = generatePropertySchema(prop, ctx);
|
|
453
|
+
if (!prop.optional) {
|
|
454
|
+
required.push(prop.name);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const schema = { type: "object", properties };
|
|
458
|
+
if (required.length > 0) {
|
|
459
|
+
schema.required = required;
|
|
460
|
+
}
|
|
461
|
+
if (!type.additionalProperties) {
|
|
462
|
+
schema.additionalProperties = false;
|
|
463
|
+
}
|
|
464
|
+
return schema;
|
|
465
|
+
}
|
|
466
|
+
function generatePropertySchema(prop, ctx) {
|
|
467
|
+
const schema = generateTypeNode(prop.type, ctx);
|
|
468
|
+
applyConstraints(schema, prop.constraints);
|
|
469
|
+
applyAnnotations(schema, prop.annotations);
|
|
470
|
+
return schema;
|
|
471
|
+
}
|
|
472
|
+
function generateUnionType(type, ctx) {
|
|
473
|
+
if (isBooleanUnion(type)) {
|
|
474
|
+
return { type: "boolean" };
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
anyOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function isBooleanUnion(type) {
|
|
481
|
+
if (type.members.length !== 2) return false;
|
|
482
|
+
const kinds = type.members.map((m) => m.kind);
|
|
483
|
+
return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
|
|
484
|
+
}
|
|
485
|
+
function generateReferenceType(type) {
|
|
486
|
+
return { $ref: `#/$defs/${type.name}` };
|
|
487
|
+
}
|
|
488
|
+
function generateDynamicType(type) {
|
|
489
|
+
if (type.dynamicKind === "enum") {
|
|
490
|
+
const schema = {
|
|
491
|
+
type: "string",
|
|
492
|
+
"x-formspec-source": type.sourceKey
|
|
493
|
+
};
|
|
494
|
+
if (type.parameterFields.length > 0) {
|
|
495
|
+
schema["x-formspec-params"] = [...type.parameterFields];
|
|
496
|
+
}
|
|
497
|
+
return schema;
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
190
500
|
type: "object",
|
|
191
|
-
|
|
192
|
-
|
|
501
|
+
additionalProperties: true,
|
|
502
|
+
"x-formspec-schemaSource": type.sourceKey
|
|
193
503
|
};
|
|
194
|
-
|
|
504
|
+
}
|
|
505
|
+
function generateCustomType(_type) {
|
|
506
|
+
return { type: "object" };
|
|
507
|
+
}
|
|
508
|
+
function applyConstraints(schema, constraints) {
|
|
509
|
+
for (const constraint of constraints) {
|
|
510
|
+
switch (constraint.constraintKind) {
|
|
511
|
+
case "minimum":
|
|
512
|
+
schema.minimum = constraint.value;
|
|
513
|
+
break;
|
|
514
|
+
case "maximum":
|
|
515
|
+
schema.maximum = constraint.value;
|
|
516
|
+
break;
|
|
517
|
+
case "exclusiveMinimum":
|
|
518
|
+
schema.exclusiveMinimum = constraint.value;
|
|
519
|
+
break;
|
|
520
|
+
case "exclusiveMaximum":
|
|
521
|
+
schema.exclusiveMaximum = constraint.value;
|
|
522
|
+
break;
|
|
523
|
+
case "multipleOf": {
|
|
524
|
+
const { value } = constraint;
|
|
525
|
+
if (value === 1 && schema.type === "number") {
|
|
526
|
+
schema.type = "integer";
|
|
527
|
+
} else {
|
|
528
|
+
schema.multipleOf = value;
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "minLength":
|
|
533
|
+
schema.minLength = constraint.value;
|
|
534
|
+
break;
|
|
535
|
+
case "maxLength":
|
|
536
|
+
schema.maxLength = constraint.value;
|
|
537
|
+
break;
|
|
538
|
+
case "minItems":
|
|
539
|
+
schema.minItems = constraint.value;
|
|
540
|
+
break;
|
|
541
|
+
case "maxItems":
|
|
542
|
+
schema.maxItems = constraint.value;
|
|
543
|
+
break;
|
|
544
|
+
case "pattern":
|
|
545
|
+
schema.pattern = constraint.pattern;
|
|
546
|
+
break;
|
|
547
|
+
case "uniqueItems":
|
|
548
|
+
schema.uniqueItems = constraint.value;
|
|
549
|
+
break;
|
|
550
|
+
case "allowedMembers":
|
|
551
|
+
break;
|
|
552
|
+
case "custom":
|
|
553
|
+
break;
|
|
554
|
+
default: {
|
|
555
|
+
const _exhaustive = constraint;
|
|
556
|
+
void _exhaustive;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function applyAnnotations(schema, annotations) {
|
|
562
|
+
for (const annotation of annotations) {
|
|
563
|
+
switch (annotation.annotationKind) {
|
|
564
|
+
case "displayName":
|
|
565
|
+
schema.title = annotation.value;
|
|
566
|
+
break;
|
|
567
|
+
case "description":
|
|
568
|
+
schema.description = annotation.value;
|
|
569
|
+
break;
|
|
570
|
+
case "defaultValue":
|
|
571
|
+
schema.default = annotation.value;
|
|
572
|
+
break;
|
|
573
|
+
case "deprecated":
|
|
574
|
+
schema.deprecated = true;
|
|
575
|
+
break;
|
|
576
|
+
case "placeholder":
|
|
577
|
+
break;
|
|
578
|
+
case "formatHint":
|
|
579
|
+
break;
|
|
580
|
+
case "custom":
|
|
581
|
+
break;
|
|
582
|
+
default: {
|
|
583
|
+
const _exhaustive = annotation;
|
|
584
|
+
void _exhaustive;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/json-schema/generator.ts
|
|
591
|
+
function generateJsonSchema(form) {
|
|
592
|
+
const ir = canonicalizeChainDSL(form);
|
|
593
|
+
return generateJsonSchemaFromIR(ir);
|
|
195
594
|
}
|
|
196
595
|
|
|
197
596
|
// src/ui-schema/schema.ts
|
|
198
|
-
import { z
|
|
199
|
-
var jsonPointerSchema =
|
|
200
|
-
var ruleEffectSchema =
|
|
201
|
-
var uiSchemaElementTypeSchema =
|
|
597
|
+
import { z } from "zod";
|
|
598
|
+
var jsonPointerSchema = z.string();
|
|
599
|
+
var ruleEffectSchema = z.enum(["SHOW", "HIDE", "ENABLE", "DISABLE"]);
|
|
600
|
+
var uiSchemaElementTypeSchema = z.enum([
|
|
202
601
|
"Control",
|
|
203
602
|
"VerticalLayout",
|
|
204
603
|
"HorizontalLayout",
|
|
@@ -207,32 +606,32 @@ var uiSchemaElementTypeSchema = z3.enum([
|
|
|
207
606
|
"Category",
|
|
208
607
|
"Label"
|
|
209
608
|
]);
|
|
210
|
-
var ruleConditionSchema =
|
|
211
|
-
() =>
|
|
212
|
-
const:
|
|
213
|
-
enum:
|
|
214
|
-
type:
|
|
609
|
+
var ruleConditionSchema = z.lazy(
|
|
610
|
+
() => z.object({
|
|
611
|
+
const: z.unknown().optional(),
|
|
612
|
+
enum: z.array(z.unknown()).readonly().optional(),
|
|
613
|
+
type: z.string().optional(),
|
|
215
614
|
not: ruleConditionSchema.optional(),
|
|
216
|
-
minimum:
|
|
217
|
-
maximum:
|
|
218
|
-
exclusiveMinimum:
|
|
219
|
-
exclusiveMaximum:
|
|
220
|
-
minLength:
|
|
221
|
-
properties:
|
|
222
|
-
required:
|
|
223
|
-
allOf:
|
|
615
|
+
minimum: z.number().optional(),
|
|
616
|
+
maximum: z.number().optional(),
|
|
617
|
+
exclusiveMinimum: z.number().optional(),
|
|
618
|
+
exclusiveMaximum: z.number().optional(),
|
|
619
|
+
minLength: z.number().optional(),
|
|
620
|
+
properties: z.record(z.string(), ruleConditionSchema).optional(),
|
|
621
|
+
required: z.array(z.string()).optional(),
|
|
622
|
+
allOf: z.array(ruleConditionSchema).optional()
|
|
224
623
|
}).strict()
|
|
225
624
|
);
|
|
226
|
-
var schemaBasedConditionSchema =
|
|
625
|
+
var schemaBasedConditionSchema = z.object({
|
|
227
626
|
scope: jsonPointerSchema,
|
|
228
627
|
schema: ruleConditionSchema
|
|
229
628
|
}).strict();
|
|
230
|
-
var ruleSchema =
|
|
629
|
+
var ruleSchema = z.object({
|
|
231
630
|
effect: ruleEffectSchema,
|
|
232
631
|
condition: schemaBasedConditionSchema
|
|
233
632
|
}).strict();
|
|
234
|
-
var uiSchemaElementSchema =
|
|
235
|
-
() =>
|
|
633
|
+
var uiSchemaElementSchema = z.lazy(
|
|
634
|
+
() => z.union([
|
|
236
635
|
controlSchema,
|
|
237
636
|
verticalLayoutSchema,
|
|
238
637
|
horizontalLayoutSchema,
|
|
@@ -242,73 +641,73 @@ var uiSchemaElementSchema = z3.lazy(
|
|
|
242
641
|
labelElementSchema
|
|
243
642
|
])
|
|
244
643
|
);
|
|
245
|
-
var controlSchema =
|
|
246
|
-
type:
|
|
644
|
+
var controlSchema = z.object({
|
|
645
|
+
type: z.literal("Control"),
|
|
247
646
|
scope: jsonPointerSchema,
|
|
248
|
-
label:
|
|
647
|
+
label: z.union([z.string(), z.literal(false)]).optional(),
|
|
249
648
|
rule: ruleSchema.optional(),
|
|
250
|
-
options:
|
|
649
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
251
650
|
}).passthrough();
|
|
252
|
-
var verticalLayoutSchema =
|
|
253
|
-
() =>
|
|
254
|
-
type:
|
|
255
|
-
elements:
|
|
651
|
+
var verticalLayoutSchema = z.lazy(
|
|
652
|
+
() => z.object({
|
|
653
|
+
type: z.literal("VerticalLayout"),
|
|
654
|
+
elements: z.array(uiSchemaElementSchema),
|
|
256
655
|
rule: ruleSchema.optional(),
|
|
257
|
-
options:
|
|
656
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
258
657
|
}).passthrough()
|
|
259
658
|
);
|
|
260
|
-
var horizontalLayoutSchema =
|
|
261
|
-
() =>
|
|
262
|
-
type:
|
|
263
|
-
elements:
|
|
659
|
+
var horizontalLayoutSchema = z.lazy(
|
|
660
|
+
() => z.object({
|
|
661
|
+
type: z.literal("HorizontalLayout"),
|
|
662
|
+
elements: z.array(uiSchemaElementSchema),
|
|
264
663
|
rule: ruleSchema.optional(),
|
|
265
|
-
options:
|
|
664
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
266
665
|
}).passthrough()
|
|
267
666
|
);
|
|
268
|
-
var groupLayoutSchema =
|
|
269
|
-
() =>
|
|
270
|
-
type:
|
|
271
|
-
label:
|
|
272
|
-
elements:
|
|
667
|
+
var groupLayoutSchema = z.lazy(
|
|
668
|
+
() => z.object({
|
|
669
|
+
type: z.literal("Group"),
|
|
670
|
+
label: z.string(),
|
|
671
|
+
elements: z.array(uiSchemaElementSchema),
|
|
273
672
|
rule: ruleSchema.optional(),
|
|
274
|
-
options:
|
|
673
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
275
674
|
}).passthrough()
|
|
276
675
|
);
|
|
277
|
-
var categorySchema =
|
|
278
|
-
() =>
|
|
279
|
-
type:
|
|
280
|
-
label:
|
|
281
|
-
elements:
|
|
676
|
+
var categorySchema = z.lazy(
|
|
677
|
+
() => z.object({
|
|
678
|
+
type: z.literal("Category"),
|
|
679
|
+
label: z.string(),
|
|
680
|
+
elements: z.array(uiSchemaElementSchema),
|
|
282
681
|
rule: ruleSchema.optional(),
|
|
283
|
-
options:
|
|
682
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
284
683
|
}).passthrough()
|
|
285
684
|
);
|
|
286
|
-
var categorizationSchema =
|
|
287
|
-
() =>
|
|
288
|
-
type:
|
|
289
|
-
elements:
|
|
290
|
-
label:
|
|
685
|
+
var categorizationSchema = z.lazy(
|
|
686
|
+
() => z.object({
|
|
687
|
+
type: z.literal("Categorization"),
|
|
688
|
+
elements: z.array(categorySchema),
|
|
689
|
+
label: z.string().optional(),
|
|
291
690
|
rule: ruleSchema.optional(),
|
|
292
|
-
options:
|
|
691
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
293
692
|
}).passthrough()
|
|
294
693
|
);
|
|
295
|
-
var labelElementSchema =
|
|
296
|
-
type:
|
|
297
|
-
text:
|
|
694
|
+
var labelElementSchema = z.object({
|
|
695
|
+
type: z.literal("Label"),
|
|
696
|
+
text: z.string(),
|
|
298
697
|
rule: ruleSchema.optional(),
|
|
299
|
-
options:
|
|
698
|
+
options: z.record(z.string(), z.unknown()).optional()
|
|
300
699
|
}).passthrough();
|
|
301
|
-
var uiSchema =
|
|
302
|
-
() =>
|
|
700
|
+
var uiSchema = z.lazy(
|
|
701
|
+
() => z.union([verticalLayoutSchema, horizontalLayoutSchema, groupLayoutSchema, categorizationSchema])
|
|
303
702
|
);
|
|
304
703
|
|
|
305
|
-
// src/ui-schema/generator.ts
|
|
306
|
-
import { z as
|
|
307
|
-
function
|
|
704
|
+
// src/ui-schema/ir-generator.ts
|
|
705
|
+
import { z as z2 } from "zod";
|
|
706
|
+
function parseOrThrow(schema, value, label) {
|
|
308
707
|
try {
|
|
309
708
|
return schema.parse(value);
|
|
310
709
|
} catch (error) {
|
|
311
|
-
if (error instanceof
|
|
710
|
+
if (error instanceof z2.ZodError) {
|
|
312
711
|
throw new Error(
|
|
313
712
|
`Generated ${label} failed validation:
|
|
314
713
|
${error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
@@ -353,115 +752,69 @@ function combineRules(parentRule, childRule) {
|
|
|
353
752
|
}
|
|
354
753
|
};
|
|
355
754
|
}
|
|
356
|
-
function
|
|
755
|
+
function fieldNodeToControl(field, parentRule) {
|
|
756
|
+
const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
|
|
757
|
+
const control = {
|
|
758
|
+
type: "Control",
|
|
759
|
+
scope: fieldToScope(field.name),
|
|
760
|
+
...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
|
|
761
|
+
...parentRule !== void 0 && { rule: parentRule }
|
|
762
|
+
};
|
|
763
|
+
return control;
|
|
764
|
+
}
|
|
765
|
+
function groupNodeToLayout(group, parentRule) {
|
|
766
|
+
return {
|
|
767
|
+
type: "Group",
|
|
768
|
+
label: group.label,
|
|
769
|
+
elements: irElementsToUiSchema(group.elements, parentRule),
|
|
770
|
+
...parentRule !== void 0 && { rule: parentRule }
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function irElementsToUiSchema(elements, parentRule) {
|
|
357
774
|
const result = [];
|
|
358
775
|
for (const element of elements) {
|
|
359
|
-
switch (element.
|
|
776
|
+
switch (element.kind) {
|
|
360
777
|
case "field": {
|
|
361
|
-
|
|
362
|
-
type: "Control",
|
|
363
|
-
scope: fieldToScope(element.name),
|
|
364
|
-
...element.label !== void 0 && { label: element.label },
|
|
365
|
-
...parentRule !== void 0 && { rule: parentRule }
|
|
366
|
-
};
|
|
367
|
-
result.push(control);
|
|
778
|
+
result.push(fieldNodeToControl(element, parentRule));
|
|
368
779
|
break;
|
|
369
780
|
}
|
|
370
781
|
case "group": {
|
|
371
|
-
|
|
372
|
-
const group = {
|
|
373
|
-
type: "Group",
|
|
374
|
-
label: groupElement.label,
|
|
375
|
-
elements: elementsToUiSchema(groupElement.elements, parentRule),
|
|
376
|
-
...parentRule !== void 0 && { rule: parentRule }
|
|
377
|
-
};
|
|
378
|
-
result.push(group);
|
|
782
|
+
result.push(groupNodeToLayout(element, parentRule));
|
|
379
783
|
break;
|
|
380
784
|
}
|
|
381
785
|
case "conditional": {
|
|
382
|
-
const
|
|
383
|
-
const newRule = createShowRule(conditionalElement.field, conditionalElement.value);
|
|
786
|
+
const newRule = createShowRule(element.fieldName, element.value);
|
|
384
787
|
const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
|
|
385
|
-
const childElements =
|
|
788
|
+
const childElements = irElementsToUiSchema(element.elements, combinedRule);
|
|
386
789
|
result.push(...childElements);
|
|
387
790
|
break;
|
|
388
791
|
}
|
|
792
|
+
default: {
|
|
793
|
+
const _exhaustive = element;
|
|
794
|
+
void _exhaustive;
|
|
795
|
+
throw new Error("Unhandled IR element kind");
|
|
796
|
+
}
|
|
389
797
|
}
|
|
390
798
|
}
|
|
391
799
|
return result;
|
|
392
800
|
}
|
|
393
|
-
function
|
|
394
|
-
const control = {
|
|
395
|
-
type: "Control",
|
|
396
|
-
scope: `${scopePrefix}/${field.id}`
|
|
397
|
-
};
|
|
398
|
-
if (field.label !== void 0) {
|
|
399
|
-
control.label = field.label;
|
|
400
|
-
}
|
|
401
|
-
if (field.showWhen !== void 0 && typeof field.showWhen === "object" && "field" in field.showWhen && "value" in field.showWhen) {
|
|
402
|
-
const sw = field.showWhen;
|
|
403
|
-
control.rule = {
|
|
404
|
-
effect: "SHOW",
|
|
405
|
-
condition: {
|
|
406
|
-
scope: `#/properties/${sw.field}`,
|
|
407
|
-
schema: { const: sw.value }
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return control;
|
|
412
|
-
}
|
|
413
|
-
function generateUiSchemaFromFields(fields) {
|
|
414
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
415
|
-
const orderedKeys = [];
|
|
416
|
-
const ungrouped = [];
|
|
417
|
-
for (const field of fields) {
|
|
418
|
-
const element = formSpecFieldToElement(field);
|
|
419
|
-
if (field.group !== void 0) {
|
|
420
|
-
if (!groupMap.has(field.group)) {
|
|
421
|
-
groupMap.set(field.group, []);
|
|
422
|
-
orderedKeys.push(field.group);
|
|
423
|
-
}
|
|
424
|
-
groupMap.get(field.group).push(element);
|
|
425
|
-
} else {
|
|
426
|
-
orderedKeys.push(null);
|
|
427
|
-
ungrouped.push(element);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
const elements = [];
|
|
431
|
-
let ungroupedIndex = 0;
|
|
432
|
-
for (const key of orderedKeys) {
|
|
433
|
-
if (key === null) {
|
|
434
|
-
const el = ungrouped[ungroupedIndex++];
|
|
435
|
-
if (el !== void 0) {
|
|
436
|
-
elements.push(el);
|
|
437
|
-
}
|
|
438
|
-
} else {
|
|
439
|
-
const groupElements = groupMap.get(key) ?? [];
|
|
440
|
-
const groupLayout = {
|
|
441
|
-
type: "Group",
|
|
442
|
-
label: key,
|
|
443
|
-
elements: groupElements
|
|
444
|
-
};
|
|
445
|
-
elements.push(groupLayout);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
801
|
+
function generateUiSchemaFromIR(ir) {
|
|
448
802
|
const result = {
|
|
449
803
|
type: "VerticalLayout",
|
|
450
|
-
elements
|
|
804
|
+
elements: irElementsToUiSchema(ir.elements)
|
|
451
805
|
};
|
|
452
|
-
return
|
|
806
|
+
return parseOrThrow(uiSchema, result, "UI Schema");
|
|
453
807
|
}
|
|
808
|
+
|
|
809
|
+
// src/ui-schema/generator.ts
|
|
454
810
|
function generateUiSchema(form) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
elements: elementsToUiSchema(form.elements)
|
|
458
|
-
};
|
|
459
|
-
return parseOrThrow2(uiSchema, result, "UI Schema");
|
|
811
|
+
const ir = canonicalizeChainDSL(form);
|
|
812
|
+
return generateUiSchemaFromIR(ir);
|
|
460
813
|
}
|
|
461
814
|
|
|
462
815
|
// src/index.ts
|
|
463
|
-
import * as
|
|
464
|
-
import * as
|
|
816
|
+
import * as fs from "fs";
|
|
817
|
+
import * as path2 from "path";
|
|
465
818
|
|
|
466
819
|
// src/json-schema/types.ts
|
|
467
820
|
function setSchemaExtension(schema, key, value) {
|
|
@@ -471,296 +824,253 @@ function getSchemaExtension(schema, key) {
|
|
|
471
824
|
return schema[key];
|
|
472
825
|
}
|
|
473
826
|
|
|
474
|
-
// src/
|
|
475
|
-
import
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
827
|
+
// src/json-schema/schema.ts
|
|
828
|
+
import { z as z3 } from "zod";
|
|
829
|
+
var jsonSchemaTypeSchema = z3.enum([
|
|
830
|
+
"string",
|
|
831
|
+
"number",
|
|
832
|
+
"integer",
|
|
833
|
+
"boolean",
|
|
834
|
+
"object",
|
|
835
|
+
"array",
|
|
836
|
+
"null"
|
|
837
|
+
]);
|
|
838
|
+
var jsonSchema7Schema = z3.lazy(
|
|
839
|
+
() => z3.object({
|
|
840
|
+
$schema: z3.string().optional(),
|
|
841
|
+
$id: z3.string().optional(),
|
|
842
|
+
$ref: z3.string().optional(),
|
|
843
|
+
// Metadata
|
|
844
|
+
title: z3.string().optional(),
|
|
845
|
+
description: z3.string().optional(),
|
|
846
|
+
deprecated: z3.boolean().optional(),
|
|
847
|
+
// Type
|
|
848
|
+
type: z3.union([jsonSchemaTypeSchema, z3.array(jsonSchemaTypeSchema)]).optional(),
|
|
849
|
+
// String validation
|
|
850
|
+
minLength: z3.number().optional(),
|
|
851
|
+
maxLength: z3.number().optional(),
|
|
852
|
+
pattern: z3.string().optional(),
|
|
853
|
+
// Number validation
|
|
854
|
+
minimum: z3.number().optional(),
|
|
855
|
+
maximum: z3.number().optional(),
|
|
856
|
+
exclusiveMinimum: z3.number().optional(),
|
|
857
|
+
exclusiveMaximum: z3.number().optional(),
|
|
858
|
+
// Enum
|
|
859
|
+
enum: z3.array(z3.union([z3.string(), z3.number(), z3.boolean(), z3.null()])).readonly().optional(),
|
|
860
|
+
const: z3.union([z3.string(), z3.number(), z3.boolean(), z3.null()]).optional(),
|
|
861
|
+
// Object
|
|
862
|
+
properties: z3.record(z3.string(), jsonSchema7Schema).optional(),
|
|
863
|
+
required: z3.array(z3.string()).optional(),
|
|
864
|
+
additionalProperties: z3.union([z3.boolean(), jsonSchema7Schema]).optional(),
|
|
865
|
+
// Array
|
|
866
|
+
items: z3.union([jsonSchema7Schema, z3.array(jsonSchema7Schema)]).optional(),
|
|
867
|
+
minItems: z3.number().optional(),
|
|
868
|
+
maxItems: z3.number().optional(),
|
|
869
|
+
// Composition
|
|
870
|
+
allOf: z3.array(jsonSchema7Schema).optional(),
|
|
871
|
+
anyOf: z3.array(jsonSchema7Schema).optional(),
|
|
872
|
+
oneOf: z3.array(jsonSchema7Schema).optional(),
|
|
873
|
+
not: jsonSchema7Schema.optional(),
|
|
874
|
+
// Conditional
|
|
875
|
+
if: jsonSchema7Schema.optional(),
|
|
876
|
+
then: jsonSchema7Schema.optional(),
|
|
877
|
+
else: jsonSchema7Schema.optional(),
|
|
878
|
+
// Format
|
|
879
|
+
format: z3.string().optional(),
|
|
880
|
+
// Default
|
|
881
|
+
default: z3.unknown().optional(),
|
|
882
|
+
// FormSpec extensions
|
|
883
|
+
"x-formspec-source": z3.string().optional(),
|
|
884
|
+
"x-formspec-params": z3.array(z3.string()).readonly().optional(),
|
|
885
|
+
"x-formspec-schemaSource": z3.string().optional()
|
|
886
|
+
}).passthrough()
|
|
887
|
+
);
|
|
479
888
|
|
|
480
|
-
// src/analyzer/
|
|
889
|
+
// src/analyzer/program.ts
|
|
481
890
|
import * as ts from "typescript";
|
|
482
|
-
import "
|
|
483
|
-
function
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
function parseDecorator(decorator) {
|
|
496
|
-
const expr = decorator.expression;
|
|
497
|
-
if (ts.isIdentifier(expr)) {
|
|
498
|
-
return {
|
|
499
|
-
name: expr.text,
|
|
500
|
-
args: [],
|
|
501
|
-
node: decorator
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
if (ts.isCallExpression(expr)) {
|
|
505
|
-
const callee = expr.expression;
|
|
506
|
-
let name = null;
|
|
507
|
-
if (ts.isIdentifier(callee)) {
|
|
508
|
-
name = callee.text;
|
|
509
|
-
} else if (ts.isPropertyAccessExpression(callee)) {
|
|
510
|
-
name = callee.name.text;
|
|
511
|
-
}
|
|
512
|
-
if (!name) return null;
|
|
513
|
-
const args = expr.arguments.map(extractArgValue);
|
|
514
|
-
return {
|
|
515
|
-
name,
|
|
516
|
-
args,
|
|
517
|
-
node: decorator
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
return null;
|
|
521
|
-
}
|
|
522
|
-
function extractArgValue(node) {
|
|
523
|
-
if (ts.isStringLiteral(node)) {
|
|
524
|
-
return node.text;
|
|
525
|
-
}
|
|
526
|
-
if (ts.isNumericLiteral(node)) {
|
|
527
|
-
return Number(node.text);
|
|
528
|
-
}
|
|
529
|
-
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
if (ts.isPrefixUnaryExpression(node)) {
|
|
539
|
-
if (node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
|
|
540
|
-
return -Number(node.operand.text);
|
|
541
|
-
}
|
|
542
|
-
if (node.operator === ts.SyntaxKind.PlusToken && ts.isNumericLiteral(node.operand)) {
|
|
543
|
-
return Number(node.operand.text);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (ts.isArrayLiteralExpression(node)) {
|
|
547
|
-
return node.elements.map((el) => {
|
|
548
|
-
if (ts.isSpreadElement(el)) {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
return extractArgValue(el);
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
555
|
-
const obj = {};
|
|
556
|
-
for (const prop of node.properties) {
|
|
557
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
558
|
-
const key = getPropertyName(prop.name);
|
|
559
|
-
if (key) {
|
|
560
|
-
obj[key] = extractArgValue(prop.initializer);
|
|
561
|
-
}
|
|
562
|
-
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
563
|
-
const key = prop.name.text;
|
|
564
|
-
obj[key] = null;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return obj;
|
|
568
|
-
}
|
|
569
|
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
570
|
-
return node.text;
|
|
571
|
-
}
|
|
572
|
-
if (ts.isRegularExpressionLiteral(node)) {
|
|
573
|
-
const regexText = node.text;
|
|
574
|
-
const lastSlash = regexText.lastIndexOf("/");
|
|
575
|
-
if (lastSlash > 0) {
|
|
576
|
-
return regexText.substring(1, lastSlash);
|
|
577
|
-
}
|
|
578
|
-
return regexText;
|
|
579
|
-
}
|
|
580
|
-
if (ts.isNewExpression(node)) {
|
|
581
|
-
if (ts.isIdentifier(node.expression) && node.expression.text === "RegExp" && node.arguments && node.arguments.length > 0) {
|
|
582
|
-
const firstArg = node.arguments[0];
|
|
583
|
-
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
584
|
-
return firstArg.text;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
if (ts.isIdentifier(node)) {
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
function getPropertyName(name) {
|
|
594
|
-
if (ts.isIdentifier(name)) {
|
|
595
|
-
return name.text;
|
|
596
|
-
}
|
|
597
|
-
if (ts.isStringLiteral(name)) {
|
|
598
|
-
return name.text;
|
|
599
|
-
}
|
|
600
|
-
if (ts.isNumericLiteral(name)) {
|
|
601
|
-
return name.text;
|
|
602
|
-
}
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
var FORMSPEC_DECORATORS = {
|
|
606
|
-
// Display metadata
|
|
607
|
-
Field: { argTypes: ["object"] },
|
|
608
|
-
// Grouping
|
|
609
|
-
Group: { argTypes: ["string"] },
|
|
610
|
-
// Conditional display
|
|
611
|
-
ShowWhen: { argTypes: ["object"] },
|
|
612
|
-
// Enum options
|
|
613
|
-
EnumOptions: { argTypes: ["array"] },
|
|
614
|
-
// Numeric constraints
|
|
615
|
-
Minimum: { argTypes: ["number"] },
|
|
616
|
-
Maximum: { argTypes: ["number"] },
|
|
617
|
-
ExclusiveMinimum: { argTypes: ["number"] },
|
|
618
|
-
ExclusiveMaximum: { argTypes: ["number"] },
|
|
619
|
-
// String constraints
|
|
620
|
-
MinLength: { argTypes: ["number"] },
|
|
621
|
-
MaxLength: { argTypes: ["number"] },
|
|
622
|
-
Pattern: { argTypes: ["string"] }
|
|
623
|
-
};
|
|
624
|
-
function isFormSpecDecoratorsPath(fileName) {
|
|
625
|
-
const normalized = fileName.replace(/\\/g, "/");
|
|
626
|
-
return normalized.includes("node_modules/@formspec/decorators") || normalized.includes("/packages/decorators/");
|
|
627
|
-
}
|
|
628
|
-
function resolveDecorator(decorator, checker) {
|
|
629
|
-
const expr = decorator.expression;
|
|
630
|
-
let targetNode;
|
|
631
|
-
let name;
|
|
632
|
-
if (ts.isIdentifier(expr)) {
|
|
633
|
-
targetNode = expr;
|
|
634
|
-
name = expr.text;
|
|
635
|
-
} else if (ts.isCallExpression(expr)) {
|
|
636
|
-
if (ts.isIdentifier(expr.expression)) {
|
|
637
|
-
targetNode = expr.expression;
|
|
638
|
-
name = expr.expression.text;
|
|
639
|
-
} else {
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
} else {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
if (name in FORMSPEC_DECORATORS) {
|
|
646
|
-
const symbol = checker.getSymbolAtLocation(targetNode);
|
|
647
|
-
if (symbol) {
|
|
648
|
-
const declarations = symbol.declarations;
|
|
649
|
-
if (declarations && declarations.length > 0) {
|
|
650
|
-
const decl = declarations[0];
|
|
651
|
-
if (decl) {
|
|
652
|
-
const sourceFile = decl.getSourceFile();
|
|
653
|
-
const fileName = sourceFile.fileName;
|
|
654
|
-
if (isFormSpecDecoratorsPath(fileName)) {
|
|
655
|
-
return {
|
|
656
|
-
name,
|
|
657
|
-
isFormSpec: true,
|
|
658
|
-
isMarker: !ts.isCallExpression(expr)
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
const resolvedSymbol = checker.getSymbolAtLocation(targetNode);
|
|
666
|
-
if (!resolvedSymbol) return null;
|
|
667
|
-
const type = checker.getTypeOfSymbol(resolvedSymbol);
|
|
668
|
-
const props = type.getProperties();
|
|
669
|
-
let extendsBuiltin;
|
|
670
|
-
let extensionName;
|
|
671
|
-
let isMarker = false;
|
|
672
|
-
for (const prop of props) {
|
|
673
|
-
const escapedName = prop.getEscapedName();
|
|
674
|
-
if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extends") || escapedName.includes("FORMSPEC_EXTENDS"))) {
|
|
675
|
-
const propType = checker.getTypeOfSymbol(prop);
|
|
676
|
-
if (propType.isStringLiteral()) {
|
|
677
|
-
extendsBuiltin = propType.value;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extension") || escapedName.includes("FORMSPEC_EXTENSION"))) {
|
|
681
|
-
const propType = checker.getTypeOfSymbol(prop);
|
|
682
|
-
if (propType.isStringLiteral()) {
|
|
683
|
-
extensionName = propType.value;
|
|
684
|
-
}
|
|
891
|
+
import * as path from "path";
|
|
892
|
+
function createProgramContext(filePath) {
|
|
893
|
+
const absolutePath = path.resolve(filePath);
|
|
894
|
+
const fileDir = path.dirname(absolutePath);
|
|
895
|
+
const configPath = ts.findConfigFile(fileDir, ts.sys.fileExists.bind(ts.sys), "tsconfig.json");
|
|
896
|
+
let compilerOptions;
|
|
897
|
+
let fileNames;
|
|
898
|
+
if (configPath) {
|
|
899
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile.bind(ts.sys));
|
|
900
|
+
if (configFile.error) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`Error reading tsconfig.json: ${ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n")}`
|
|
903
|
+
);
|
|
685
904
|
}
|
|
686
|
-
|
|
687
|
-
|
|
905
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
906
|
+
configFile.config,
|
|
907
|
+
ts.sys,
|
|
908
|
+
path.dirname(configPath)
|
|
909
|
+
);
|
|
910
|
+
if (parsed.errors.length > 0) {
|
|
911
|
+
const errorMessages = parsed.errors.map((e) => ts.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
|
|
912
|
+
throw new Error(`Error parsing tsconfig.json: ${errorMessages}`);
|
|
688
913
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
914
|
+
compilerOptions = parsed.options;
|
|
915
|
+
fileNames = parsed.fileNames.includes(absolutePath) ? parsed.fileNames : [...parsed.fileNames, absolutePath];
|
|
916
|
+
} else {
|
|
917
|
+
compilerOptions = {
|
|
918
|
+
target: ts.ScriptTarget.ES2022,
|
|
919
|
+
module: ts.ModuleKind.NodeNext,
|
|
920
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
921
|
+
strict: true,
|
|
922
|
+
skipLibCheck: true,
|
|
923
|
+
declaration: true
|
|
696
924
|
};
|
|
925
|
+
fileNames = [absolutePath];
|
|
697
926
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
isFormSpec: true,
|
|
703
|
-
isMarker
|
|
704
|
-
};
|
|
927
|
+
const program = ts.createProgram(fileNames, compilerOptions);
|
|
928
|
+
const sourceFile = program.getSourceFile(absolutePath);
|
|
929
|
+
if (!sourceFile) {
|
|
930
|
+
throw new Error(`Could not find source file: ${absolutePath}`);
|
|
705
931
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
932
|
+
return {
|
|
933
|
+
program,
|
|
934
|
+
checker: program.getTypeChecker(),
|
|
935
|
+
sourceFile
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function findNodeByName(sourceFile, name, predicate, getName) {
|
|
939
|
+
let result = null;
|
|
940
|
+
function visit(node) {
|
|
941
|
+
if (result) return;
|
|
942
|
+
if (predicate(node) && getName(node) === name) {
|
|
943
|
+
result = node;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
ts.forEachChild(node, visit);
|
|
712
947
|
}
|
|
713
|
-
|
|
948
|
+
visit(sourceFile);
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
function findClassByName(sourceFile, className) {
|
|
952
|
+
return findNodeByName(sourceFile, className, ts.isClassDeclaration, (n) => n.name?.text);
|
|
953
|
+
}
|
|
954
|
+
function findInterfaceByName(sourceFile, interfaceName) {
|
|
955
|
+
return findNodeByName(sourceFile, interfaceName, ts.isInterfaceDeclaration, (n) => n.name.text);
|
|
956
|
+
}
|
|
957
|
+
function findTypeAliasByName(sourceFile, aliasName) {
|
|
958
|
+
return findNodeByName(sourceFile, aliasName, ts.isTypeAliasDeclaration, (n) => n.name.text);
|
|
714
959
|
}
|
|
715
960
|
|
|
961
|
+
// src/analyzer/class-analyzer.ts
|
|
962
|
+
import * as ts4 from "typescript";
|
|
963
|
+
|
|
716
964
|
// src/analyzer/jsdoc-constraints.ts
|
|
965
|
+
import * as ts3 from "typescript";
|
|
966
|
+
import {
|
|
967
|
+
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
|
|
968
|
+
} from "@formspec/core";
|
|
969
|
+
|
|
970
|
+
// src/analyzer/tsdoc-parser.ts
|
|
717
971
|
import * as ts2 from "typescript";
|
|
718
|
-
import {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
972
|
+
import {
|
|
973
|
+
TSDocParser,
|
|
974
|
+
TSDocConfiguration,
|
|
975
|
+
TSDocTagDefinition,
|
|
976
|
+
TSDocTagSyntaxKind,
|
|
977
|
+
DocPlainText,
|
|
978
|
+
DocSoftBreak,
|
|
979
|
+
TextRange
|
|
980
|
+
} from "@microsoft/tsdoc";
|
|
981
|
+
import {
|
|
982
|
+
BUILTIN_CONSTRAINT_DEFINITIONS
|
|
983
|
+
} from "@formspec/core";
|
|
984
|
+
var NUMERIC_CONSTRAINT_MAP = {
|
|
985
|
+
Minimum: "minimum",
|
|
986
|
+
Maximum: "maximum",
|
|
987
|
+
ExclusiveMinimum: "exclusiveMinimum",
|
|
988
|
+
ExclusiveMaximum: "exclusiveMaximum"
|
|
989
|
+
};
|
|
990
|
+
var LENGTH_CONSTRAINT_MAP = {
|
|
991
|
+
MinLength: "minLength",
|
|
992
|
+
MaxLength: "maxLength"
|
|
993
|
+
};
|
|
994
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
|
|
995
|
+
function isBuiltinConstraintName(tagName) {
|
|
996
|
+
return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
|
|
997
|
+
}
|
|
998
|
+
function createFormSpecTSDocConfig() {
|
|
999
|
+
const config = new TSDocConfiguration();
|
|
1000
|
+
for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
1001
|
+
config.addTagDefinition(
|
|
1002
|
+
new TSDocTagDefinition({
|
|
1003
|
+
tagName: "@" + tagName,
|
|
1004
|
+
syntaxKind: TSDocTagSyntaxKind.BlockTag,
|
|
1005
|
+
allowMultiple: true
|
|
1006
|
+
})
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return config;
|
|
1010
|
+
}
|
|
1011
|
+
var sharedParser;
|
|
1012
|
+
function getParser() {
|
|
1013
|
+
sharedParser ??= new TSDocParser(createFormSpecTSDocConfig());
|
|
1014
|
+
return sharedParser;
|
|
1015
|
+
}
|
|
1016
|
+
function parseTSDocTags(node, file = "") {
|
|
1017
|
+
const constraints = [];
|
|
1018
|
+
const annotations = [];
|
|
1019
|
+
const sourceFile = node.getSourceFile();
|
|
1020
|
+
const sourceText = sourceFile.getFullText();
|
|
1021
|
+
const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
|
|
1022
|
+
if (commentRanges) {
|
|
1023
|
+
for (const range of commentRanges) {
|
|
1024
|
+
if (range.kind !== ts2.SyntaxKind.MultiLineCommentTrivia) {
|
|
740
1025
|
continue;
|
|
741
1026
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
const parsed = JSON.parse(trimmed);
|
|
746
|
-
if (!Array.isArray(parsed)) {
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
results.push(createSyntheticDecorator(constraintName, parsed));
|
|
750
|
-
} catch {
|
|
1027
|
+
const commentText = sourceText.substring(range.pos, range.end);
|
|
1028
|
+
if (!commentText.startsWith("/**")) {
|
|
751
1029
|
continue;
|
|
752
1030
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1031
|
+
const parser = getParser();
|
|
1032
|
+
const parserContext = parser.parseRange(
|
|
1033
|
+
TextRange.fromStringRange(sourceText, range.pos, range.end)
|
|
1034
|
+
);
|
|
1035
|
+
const docComment = parserContext.docComment;
|
|
1036
|
+
for (const block of docComment.customBlocks) {
|
|
1037
|
+
const tagName = block.blockTag.tagName.substring(1);
|
|
1038
|
+
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
1039
|
+
const text = extractBlockText(block).trim();
|
|
1040
|
+
if (text === "") continue;
|
|
1041
|
+
const provenance = provenanceForComment(range, sourceFile, file, tagName);
|
|
1042
|
+
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
1043
|
+
if (constraintNode) {
|
|
1044
|
+
constraints.push(constraintNode);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (docComment.deprecatedBlock !== void 0) {
|
|
1048
|
+
annotations.push({
|
|
1049
|
+
kind: "annotation",
|
|
1050
|
+
annotationKind: "deprecated",
|
|
1051
|
+
provenance: provenanceForComment(range, sourceFile, file, "deprecated")
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
1057
|
+
for (const tag of jsDocTagsAll) {
|
|
1058
|
+
const tagName = tag.tagName.text;
|
|
1059
|
+
if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
1060
|
+
const commentText = getTagCommentText(tag);
|
|
1061
|
+
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
1062
|
+
const text = commentText.trim();
|
|
1063
|
+
const provenance = provenanceForJSDocTag(tag, file);
|
|
1064
|
+
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
1065
|
+
if (constraintNode) {
|
|
1066
|
+
constraints.push(constraintNode);
|
|
755
1067
|
}
|
|
756
1068
|
}
|
|
757
|
-
return results;
|
|
758
|
-
}
|
|
759
|
-
function extractJSDocFieldMetadata(node) {
|
|
760
|
-
const jsDocTags = ts2.getJSDocTags(node);
|
|
761
1069
|
let displayName;
|
|
762
1070
|
let description;
|
|
763
|
-
|
|
1071
|
+
let displayNameTag;
|
|
1072
|
+
let descriptionTag;
|
|
1073
|
+
for (const tag of jsDocTagsAll) {
|
|
764
1074
|
const tagName = tag.tagName.text;
|
|
765
1075
|
const commentText = getTagCommentText(tag);
|
|
766
1076
|
if (commentText === void 0 || commentText.trim() === "") {
|
|
@@ -769,18 +1079,132 @@ function extractJSDocFieldMetadata(node) {
|
|
|
769
1079
|
const trimmed = commentText.trim();
|
|
770
1080
|
if (tagName === "Field_displayName") {
|
|
771
1081
|
displayName = trimmed;
|
|
1082
|
+
displayNameTag = tag;
|
|
772
1083
|
} else if (tagName === "Field_description") {
|
|
773
1084
|
description = trimmed;
|
|
1085
|
+
descriptionTag = tag;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (displayName !== void 0 && displayNameTag) {
|
|
1089
|
+
annotations.push({
|
|
1090
|
+
kind: "annotation",
|
|
1091
|
+
annotationKind: "displayName",
|
|
1092
|
+
value: displayName,
|
|
1093
|
+
provenance: provenanceForJSDocTag(displayNameTag, file)
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
if (description !== void 0 && descriptionTag) {
|
|
1097
|
+
annotations.push({
|
|
1098
|
+
kind: "annotation",
|
|
1099
|
+
annotationKind: "description",
|
|
1100
|
+
value: description,
|
|
1101
|
+
provenance: provenanceForJSDocTag(descriptionTag, file)
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return { constraints, annotations };
|
|
1105
|
+
}
|
|
1106
|
+
function extractBlockText(block) {
|
|
1107
|
+
return extractPlainText(block.content);
|
|
1108
|
+
}
|
|
1109
|
+
function extractPlainText(node) {
|
|
1110
|
+
let result = "";
|
|
1111
|
+
if (node instanceof DocPlainText) {
|
|
1112
|
+
return node.text;
|
|
1113
|
+
}
|
|
1114
|
+
if (node instanceof DocSoftBreak) {
|
|
1115
|
+
return " ";
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof node.getChildNodes === "function") {
|
|
1118
|
+
for (const child of node.getChildNodes()) {
|
|
1119
|
+
result += extractPlainText(child);
|
|
774
1120
|
}
|
|
775
1121
|
}
|
|
776
|
-
|
|
1122
|
+
return result;
|
|
1123
|
+
}
|
|
1124
|
+
function parseConstraintValue(tagName, text, provenance) {
|
|
1125
|
+
if (!isBuiltinConstraintName(tagName)) {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
|
|
1129
|
+
if (expectedType === "number") {
|
|
1130
|
+
const value = Number(text);
|
|
1131
|
+
if (Number.isNaN(value)) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
const numericKind = NUMERIC_CONSTRAINT_MAP[tagName];
|
|
1135
|
+
if (numericKind) {
|
|
1136
|
+
return {
|
|
1137
|
+
kind: "constraint",
|
|
1138
|
+
constraintKind: numericKind,
|
|
1139
|
+
value,
|
|
1140
|
+
provenance
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
const lengthKind = LENGTH_CONSTRAINT_MAP[tagName];
|
|
1144
|
+
if (lengthKind) {
|
|
1145
|
+
return {
|
|
1146
|
+
kind: "constraint",
|
|
1147
|
+
constraintKind: lengthKind,
|
|
1148
|
+
value,
|
|
1149
|
+
provenance
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
777
1152
|
return null;
|
|
778
1153
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1154
|
+
if (expectedType === "json") {
|
|
1155
|
+
try {
|
|
1156
|
+
const parsed = JSON.parse(text);
|
|
1157
|
+
if (!Array.isArray(parsed)) {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const members = [];
|
|
1161
|
+
for (const item of parsed) {
|
|
1162
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
1163
|
+
members.push(item);
|
|
1164
|
+
} else if (typeof item === "object" && item !== null && "id" in item) {
|
|
1165
|
+
const id = item["id"];
|
|
1166
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
1167
|
+
members.push(id);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
kind: "constraint",
|
|
1173
|
+
constraintKind: "allowedMembers",
|
|
1174
|
+
members,
|
|
1175
|
+
provenance
|
|
1176
|
+
};
|
|
1177
|
+
} catch {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
kind: "constraint",
|
|
1183
|
+
constraintKind: "pattern",
|
|
1184
|
+
pattern: text,
|
|
1185
|
+
provenance
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function provenanceForComment(range, sourceFile, file, tagName) {
|
|
1189
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
|
|
1190
|
+
return {
|
|
1191
|
+
surface: "tsdoc",
|
|
1192
|
+
file,
|
|
1193
|
+
line: line + 1,
|
|
1194
|
+
column: character,
|
|
1195
|
+
tagName: "@" + tagName
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function provenanceForJSDocTag(tag, file) {
|
|
1199
|
+
const sourceFile = tag.getSourceFile();
|
|
1200
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.getStart());
|
|
1201
|
+
return {
|
|
1202
|
+
surface: "tsdoc",
|
|
1203
|
+
file,
|
|
1204
|
+
line: line + 1,
|
|
1205
|
+
column: character,
|
|
1206
|
+
tagName: "@" + tag.tagName.text
|
|
782
1207
|
};
|
|
783
|
-
return createSyntheticDecorator("Field", fieldOpts);
|
|
784
1208
|
}
|
|
785
1209
|
function getTagCommentText(tag) {
|
|
786
1210
|
if (tag.comment === void 0) {
|
|
@@ -791,30 +1215,76 @@ function getTagCommentText(tag) {
|
|
|
791
1215
|
}
|
|
792
1216
|
return ts2.getTextOfJSDocComment(tag.comment);
|
|
793
1217
|
}
|
|
794
|
-
|
|
1218
|
+
|
|
1219
|
+
// src/analyzer/jsdoc-constraints.ts
|
|
1220
|
+
function extractJSDocConstraintNodes(node, file = "") {
|
|
1221
|
+
const result = parseTSDocTags(node, file);
|
|
1222
|
+
return [...result.constraints];
|
|
1223
|
+
}
|
|
1224
|
+
function extractJSDocAnnotationNodes(node, file = "") {
|
|
1225
|
+
const result = parseTSDocTags(node, file);
|
|
1226
|
+
return [...result.annotations];
|
|
1227
|
+
}
|
|
1228
|
+
function extractDefaultValueAnnotation(initializer, file = "") {
|
|
1229
|
+
if (!initializer) return null;
|
|
1230
|
+
let value;
|
|
1231
|
+
if (ts3.isStringLiteral(initializer)) {
|
|
1232
|
+
value = initializer.text;
|
|
1233
|
+
} else if (ts3.isNumericLiteral(initializer)) {
|
|
1234
|
+
value = Number(initializer.text);
|
|
1235
|
+
} else if (initializer.kind === ts3.SyntaxKind.TrueKeyword) {
|
|
1236
|
+
value = true;
|
|
1237
|
+
} else if (initializer.kind === ts3.SyntaxKind.FalseKeyword) {
|
|
1238
|
+
value = false;
|
|
1239
|
+
} else if (initializer.kind === ts3.SyntaxKind.NullKeyword) {
|
|
1240
|
+
value = null;
|
|
1241
|
+
} else if (ts3.isPrefixUnaryExpression(initializer)) {
|
|
1242
|
+
if (initializer.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(initializer.operand)) {
|
|
1243
|
+
value = -Number(initializer.operand.text);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (value === void 0) return null;
|
|
1247
|
+
const sourceFile = initializer.getSourceFile();
|
|
1248
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(initializer.getStart());
|
|
795
1249
|
return {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1250
|
+
kind: "annotation",
|
|
1251
|
+
annotationKind: "defaultValue",
|
|
1252
|
+
value,
|
|
1253
|
+
provenance: {
|
|
1254
|
+
surface: "tsdoc",
|
|
1255
|
+
file,
|
|
1256
|
+
line: line + 1,
|
|
1257
|
+
column: character
|
|
1258
|
+
}
|
|
799
1259
|
};
|
|
800
1260
|
}
|
|
801
1261
|
|
|
802
1262
|
// src/analyzer/class-analyzer.ts
|
|
803
|
-
function
|
|
1263
|
+
function isObjectType(type) {
|
|
1264
|
+
return !!(type.flags & ts4.TypeFlags.Object);
|
|
1265
|
+
}
|
|
1266
|
+
function isTypeReference(type) {
|
|
1267
|
+
return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
|
|
1268
|
+
}
|
|
1269
|
+
function analyzeClassToIR(classDecl, checker, file = "") {
|
|
804
1270
|
const name = classDecl.name?.text ?? "AnonymousClass";
|
|
805
1271
|
const fields = [];
|
|
1272
|
+
const fieldLayouts = [];
|
|
1273
|
+
const typeRegistry = {};
|
|
1274
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
806
1275
|
const instanceMethods = [];
|
|
807
1276
|
const staticMethods = [];
|
|
808
1277
|
for (const member of classDecl.members) {
|
|
809
|
-
if (
|
|
810
|
-
const
|
|
811
|
-
if (
|
|
812
|
-
fields.push(
|
|
1278
|
+
if (ts4.isPropertyDeclaration(member)) {
|
|
1279
|
+
const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
|
|
1280
|
+
if (fieldNode) {
|
|
1281
|
+
fields.push(fieldNode);
|
|
1282
|
+
fieldLayouts.push({});
|
|
813
1283
|
}
|
|
814
|
-
} else if (
|
|
1284
|
+
} else if (ts4.isMethodDeclaration(member)) {
|
|
815
1285
|
const methodInfo = analyzeMethod(member, checker);
|
|
816
1286
|
if (methodInfo) {
|
|
817
|
-
const isStatic = member.modifiers?.some((m) => m.kind ===
|
|
1287
|
+
const isStatic = member.modifiers?.some((m) => m.kind === ts4.SyntaxKind.StaticKeyword);
|
|
818
1288
|
if (isStatic) {
|
|
819
1289
|
staticMethods.push(methodInfo);
|
|
820
1290
|
} else {
|
|
@@ -823,159 +1293,29 @@ function analyzeClass(classDecl, checker) {
|
|
|
823
1293
|
}
|
|
824
1294
|
}
|
|
825
1295
|
}
|
|
826
|
-
return {
|
|
827
|
-
name,
|
|
828
|
-
fields,
|
|
829
|
-
instanceMethods,
|
|
830
|
-
staticMethods
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
function analyzeField(prop, checker) {
|
|
834
|
-
if (!ts3.isIdentifier(prop.name)) {
|
|
835
|
-
return null;
|
|
836
|
-
}
|
|
837
|
-
const name = prop.name.text;
|
|
838
|
-
const typeNode = prop.type;
|
|
839
|
-
const type = checker.getTypeAtLocation(prop);
|
|
840
|
-
const optional = prop.questionToken !== void 0;
|
|
841
|
-
const decorators = extractDecorators(prop);
|
|
842
|
-
for (const dec of decorators) {
|
|
843
|
-
if (dec.node) {
|
|
844
|
-
const resolved = resolveDecorator(dec.node, checker);
|
|
845
|
-
if (resolved) {
|
|
846
|
-
dec.resolved = resolved;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
if (prop.type) {
|
|
851
|
-
const aliasConstraints = extractTypeAliasConstraints(prop.type, checker);
|
|
852
|
-
decorators.push(...aliasConstraints);
|
|
853
|
-
}
|
|
854
|
-
const jsdocConstraints = extractJSDocConstraints(prop);
|
|
855
|
-
decorators.push(...jsdocConstraints);
|
|
856
|
-
const deprecated = hasDeprecatedTag(prop);
|
|
857
|
-
const defaultValue = extractDefaultValue(prop.initializer);
|
|
858
|
-
return {
|
|
859
|
-
name,
|
|
860
|
-
typeNode,
|
|
861
|
-
type,
|
|
862
|
-
optional,
|
|
863
|
-
decorators,
|
|
864
|
-
deprecated,
|
|
865
|
-
defaultValue
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
function extractTypeAliasConstraints(typeNode, checker) {
|
|
869
|
-
if (!ts3.isTypeReferenceNode(typeNode)) return [];
|
|
870
|
-
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
871
|
-
if (!symbol?.declarations) return [];
|
|
872
|
-
const aliasDecl = symbol.declarations.find(ts3.isTypeAliasDeclaration);
|
|
873
|
-
if (!aliasDecl) return [];
|
|
874
|
-
if (ts3.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
875
|
-
return extractJSDocConstraints(aliasDecl);
|
|
876
|
-
}
|
|
877
|
-
function hasDeprecatedTag(node) {
|
|
878
|
-
const jsDocTags = ts3.getJSDocTags(node);
|
|
879
|
-
return jsDocTags.some((tag) => tag.tagName.text === "deprecated");
|
|
880
|
-
}
|
|
881
|
-
function extractDefaultValue(initializer) {
|
|
882
|
-
if (!initializer) return void 0;
|
|
883
|
-
if (ts3.isStringLiteral(initializer)) {
|
|
884
|
-
return initializer.text;
|
|
885
|
-
}
|
|
886
|
-
if (ts3.isNumericLiteral(initializer)) {
|
|
887
|
-
return Number(initializer.text);
|
|
888
|
-
}
|
|
889
|
-
if (initializer.kind === ts3.SyntaxKind.TrueKeyword) {
|
|
890
|
-
return true;
|
|
891
|
-
}
|
|
892
|
-
if (initializer.kind === ts3.SyntaxKind.FalseKeyword) {
|
|
893
|
-
return false;
|
|
894
|
-
}
|
|
895
|
-
if (initializer.kind === ts3.SyntaxKind.NullKeyword) {
|
|
896
|
-
return null;
|
|
897
|
-
}
|
|
898
|
-
if (ts3.isPrefixUnaryExpression(initializer)) {
|
|
899
|
-
if (initializer.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(initializer.operand)) {
|
|
900
|
-
return -Number(initializer.operand.text);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
return void 0;
|
|
904
|
-
}
|
|
905
|
-
function analyzeMethod(method, checker) {
|
|
906
|
-
if (!ts3.isIdentifier(method.name)) {
|
|
907
|
-
return null;
|
|
908
|
-
}
|
|
909
|
-
const name = method.name.text;
|
|
910
|
-
const parameters = [];
|
|
911
|
-
for (const param of method.parameters) {
|
|
912
|
-
if (ts3.isIdentifier(param.name)) {
|
|
913
|
-
const paramInfo = analyzeParameter(param, checker);
|
|
914
|
-
parameters.push(paramInfo);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
const returnTypeNode = method.type;
|
|
918
|
-
const signature = checker.getSignatureFromDeclaration(method);
|
|
919
|
-
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getTypeAtLocation(method);
|
|
920
|
-
return {
|
|
921
|
-
name,
|
|
922
|
-
parameters,
|
|
923
|
-
returnTypeNode,
|
|
924
|
-
returnType
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
function analyzeParameter(param, checker) {
|
|
928
|
-
const name = ts3.isIdentifier(param.name) ? param.name.text : "param";
|
|
929
|
-
const typeNode = param.type;
|
|
930
|
-
const type = checker.getTypeAtLocation(param);
|
|
931
|
-
const formSpecExportName = detectFormSpecReference(typeNode);
|
|
932
|
-
const optional = param.questionToken !== void 0 || param.initializer !== void 0;
|
|
933
|
-
return {
|
|
934
|
-
name,
|
|
935
|
-
typeNode,
|
|
936
|
-
type,
|
|
937
|
-
formSpecExportName,
|
|
938
|
-
optional
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
function detectFormSpecReference(typeNode) {
|
|
942
|
-
if (!typeNode) return null;
|
|
943
|
-
if (!ts3.isTypeReferenceNode(typeNode)) return null;
|
|
944
|
-
const typeName = ts3.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : ts3.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right.text : null;
|
|
945
|
-
if (typeName !== "InferSchema" && typeName !== "InferFormSchema") return null;
|
|
946
|
-
const typeArg = typeNode.typeArguments?.[0];
|
|
947
|
-
if (!typeArg || !ts3.isTypeQueryNode(typeArg)) return null;
|
|
948
|
-
if (ts3.isIdentifier(typeArg.exprName)) {
|
|
949
|
-
return typeArg.exprName.text;
|
|
950
|
-
}
|
|
951
|
-
if (ts3.isQualifiedName(typeArg.exprName)) {
|
|
952
|
-
return typeArg.exprName.right.text;
|
|
953
|
-
}
|
|
954
|
-
return null;
|
|
1296
|
+
return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
|
|
955
1297
|
}
|
|
956
|
-
function
|
|
1298
|
+
function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
|
|
957
1299
|
const name = interfaceDecl.name.text;
|
|
958
1300
|
const fields = [];
|
|
1301
|
+
const typeRegistry = {};
|
|
1302
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
959
1303
|
for (const member of interfaceDecl.members) {
|
|
960
|
-
if (
|
|
961
|
-
const
|
|
962
|
-
if (
|
|
963
|
-
fields.push(
|
|
1304
|
+
if (ts4.isPropertySignature(member)) {
|
|
1305
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
1306
|
+
if (fieldNode) {
|
|
1307
|
+
fields.push(fieldNode);
|
|
964
1308
|
}
|
|
965
1309
|
}
|
|
966
1310
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
fields,
|
|
970
|
-
instanceMethods: [],
|
|
971
|
-
staticMethods: []
|
|
972
|
-
};
|
|
1311
|
+
const fieldLayouts = fields.map(() => ({}));
|
|
1312
|
+
return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
|
|
973
1313
|
}
|
|
974
|
-
function
|
|
975
|
-
if (!
|
|
1314
|
+
function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
|
|
1315
|
+
if (!ts4.isTypeLiteralNode(typeAlias.type)) {
|
|
976
1316
|
const sourceFile = typeAlias.getSourceFile();
|
|
977
1317
|
const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
|
|
978
|
-
const kindDesc =
|
|
1318
|
+
const kindDesc = ts4.SyntaxKind[typeAlias.type.kind] ?? "unknown";
|
|
979
1319
|
return {
|
|
980
1320
|
ok: false,
|
|
981
1321
|
error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
|
|
@@ -983,11 +1323,13 @@ function analyzeTypeAlias(typeAlias, checker) {
|
|
|
983
1323
|
}
|
|
984
1324
|
const name = typeAlias.name.text;
|
|
985
1325
|
const fields = [];
|
|
1326
|
+
const typeRegistry = {};
|
|
1327
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
986
1328
|
for (const member of typeAlias.type.members) {
|
|
987
|
-
if (
|
|
988
|
-
const
|
|
989
|
-
if (
|
|
990
|
-
fields.push(
|
|
1329
|
+
if (ts4.isPropertySignature(member)) {
|
|
1330
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
1331
|
+
if (fieldNode) {
|
|
1332
|
+
fields.push(fieldNode);
|
|
991
1333
|
}
|
|
992
1334
|
}
|
|
993
1335
|
}
|
|
@@ -996,505 +1338,368 @@ function analyzeTypeAlias(typeAlias, checker) {
|
|
|
996
1338
|
analysis: {
|
|
997
1339
|
name,
|
|
998
1340
|
fields,
|
|
1341
|
+
fieldLayouts: fields.map(() => ({})),
|
|
1342
|
+
typeRegistry,
|
|
999
1343
|
instanceMethods: [],
|
|
1000
1344
|
staticMethods: []
|
|
1001
1345
|
}
|
|
1002
1346
|
};
|
|
1003
1347
|
}
|
|
1004
|
-
function
|
|
1005
|
-
if (!
|
|
1348
|
+
function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
|
|
1349
|
+
if (!ts4.isIdentifier(prop.name)) {
|
|
1006
1350
|
return null;
|
|
1007
1351
|
}
|
|
1008
1352
|
const name = prop.name.text;
|
|
1009
|
-
const
|
|
1010
|
-
const type = checker.getTypeAtLocation(prop);
|
|
1353
|
+
const tsType = checker.getTypeAtLocation(prop);
|
|
1011
1354
|
const optional = prop.questionToken !== void 0;
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1355
|
+
const provenance = provenanceForNode(prop, file);
|
|
1356
|
+
const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
1357
|
+
const constraints = [];
|
|
1358
|
+
if (prop.type) {
|
|
1359
|
+
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
1360
|
+
}
|
|
1361
|
+
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
1362
|
+
const annotations = [];
|
|
1363
|
+
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
1364
|
+
const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
|
|
1365
|
+
if (defaultAnnotation) {
|
|
1366
|
+
annotations.push(defaultAnnotation);
|
|
1367
|
+
}
|
|
1024
1368
|
return {
|
|
1369
|
+
kind: "field",
|
|
1025
1370
|
name,
|
|
1026
|
-
typeNode,
|
|
1027
1371
|
type,
|
|
1028
|
-
optional,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1372
|
+
required: !optional,
|
|
1373
|
+
constraints,
|
|
1374
|
+
annotations,
|
|
1375
|
+
provenance
|
|
1032
1376
|
};
|
|
1033
1377
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const symbols = [type.getSymbol(), type.aliasSymbol].filter(
|
|
1038
|
-
(s) => s?.declarations != null && s.declarations.length > 0
|
|
1039
|
-
);
|
|
1040
|
-
for (const symbol of symbols) {
|
|
1041
|
-
const declarations = symbol.declarations;
|
|
1042
|
-
if (!declarations) continue;
|
|
1043
|
-
const classDecl = declarations.find(ts4.isClassDeclaration);
|
|
1044
|
-
if (classDecl) {
|
|
1045
|
-
const map = /* @__PURE__ */ new Map();
|
|
1046
|
-
for (const member of classDecl.members) {
|
|
1047
|
-
if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
|
|
1048
|
-
const fieldInfo = analyzeField(member, checker);
|
|
1049
|
-
if (fieldInfo) map.set(fieldInfo.name, fieldInfo);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
return map;
|
|
1053
|
-
}
|
|
1054
|
-
const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
|
|
1055
|
-
if (interfaceDecl) {
|
|
1056
|
-
return buildFieldInfoMapFromSignatures(interfaceDecl.members, checker);
|
|
1057
|
-
}
|
|
1058
|
-
const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
|
|
1059
|
-
if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
|
|
1060
|
-
return buildFieldInfoMapFromSignatures(typeAliasDecl.type.members, checker);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
function buildFieldInfoMapFromSignatures(members, checker) {
|
|
1066
|
-
const map = /* @__PURE__ */ new Map();
|
|
1067
|
-
for (const member of members) {
|
|
1068
|
-
if (ts4.isPropertySignature(member)) {
|
|
1069
|
-
const fieldInfo = analyzeInterfaceProperty(member, checker);
|
|
1070
|
-
if (fieldInfo) {
|
|
1071
|
-
map.set(fieldInfo.name, fieldInfo);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1378
|
+
function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
|
|
1379
|
+
if (!ts4.isIdentifier(prop.name)) {
|
|
1380
|
+
return null;
|
|
1074
1381
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
1084
|
-
const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
|
|
1085
|
-
const fieldInfo = fieldInfoMap?.get(prop.name) ?? void 0;
|
|
1086
|
-
result.push({ name: prop.name, type: propType, optional, fieldInfo });
|
|
1382
|
+
const name = prop.name.text;
|
|
1383
|
+
const tsType = checker.getTypeAtLocation(prop);
|
|
1384
|
+
const optional = prop.questionToken !== void 0;
|
|
1385
|
+
const provenance = provenanceForNode(prop, file);
|
|
1386
|
+
const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
1387
|
+
const constraints = [];
|
|
1388
|
+
if (prop.type) {
|
|
1389
|
+
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
1087
1390
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
return
|
|
1391
|
+
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
1392
|
+
const annotations = [];
|
|
1393
|
+
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
1394
|
+
return {
|
|
1395
|
+
kind: "field",
|
|
1396
|
+
name,
|
|
1397
|
+
type,
|
|
1398
|
+
required: !optional,
|
|
1399
|
+
constraints,
|
|
1400
|
+
annotations,
|
|
1401
|
+
provenance
|
|
1402
|
+
};
|
|
1092
1403
|
}
|
|
1093
|
-
function
|
|
1404
|
+
function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
1094
1405
|
if (type.flags & ts4.TypeFlags.String) {
|
|
1095
|
-
return {
|
|
1406
|
+
return { kind: "primitive", primitiveKind: "string" };
|
|
1096
1407
|
}
|
|
1097
1408
|
if (type.flags & ts4.TypeFlags.Number) {
|
|
1098
|
-
return {
|
|
1409
|
+
return { kind: "primitive", primitiveKind: "number" };
|
|
1099
1410
|
}
|
|
1100
1411
|
if (type.flags & ts4.TypeFlags.Boolean) {
|
|
1101
|
-
return {
|
|
1412
|
+
return { kind: "primitive", primitiveKind: "boolean" };
|
|
1102
1413
|
}
|
|
1103
1414
|
if (type.flags & ts4.TypeFlags.Null) {
|
|
1104
|
-
return {
|
|
1415
|
+
return { kind: "primitive", primitiveKind: "null" };
|
|
1105
1416
|
}
|
|
1106
1417
|
if (type.flags & ts4.TypeFlags.Undefined) {
|
|
1107
|
-
return {
|
|
1418
|
+
return { kind: "primitive", primitiveKind: "null" };
|
|
1108
1419
|
}
|
|
1109
1420
|
if (type.isStringLiteral()) {
|
|
1110
1421
|
return {
|
|
1111
|
-
|
|
1112
|
-
|
|
1422
|
+
kind: "enum",
|
|
1423
|
+
members: [{ value: type.value }]
|
|
1113
1424
|
};
|
|
1114
1425
|
}
|
|
1115
1426
|
if (type.isNumberLiteral()) {
|
|
1116
1427
|
return {
|
|
1117
|
-
|
|
1118
|
-
|
|
1428
|
+
kind: "enum",
|
|
1429
|
+
members: [{ value: type.value }]
|
|
1119
1430
|
};
|
|
1120
1431
|
}
|
|
1121
1432
|
if (type.isUnion()) {
|
|
1122
|
-
return
|
|
1433
|
+
return resolveUnionType(type, checker, file, typeRegistry, visiting);
|
|
1123
1434
|
}
|
|
1124
1435
|
if (checker.isArrayType(type)) {
|
|
1125
|
-
return
|
|
1436
|
+
return resolveArrayType(type, checker, file, typeRegistry, visiting);
|
|
1126
1437
|
}
|
|
1127
|
-
if (type
|
|
1128
|
-
return
|
|
1438
|
+
if (isObjectType(type)) {
|
|
1439
|
+
return resolveObjectType(type, checker, file, typeRegistry, visiting);
|
|
1129
1440
|
}
|
|
1130
|
-
return {
|
|
1441
|
+
return { kind: "primitive", primitiveKind: "string" };
|
|
1131
1442
|
}
|
|
1132
|
-
function
|
|
1133
|
-
const
|
|
1134
|
-
const nonNullTypes =
|
|
1443
|
+
function resolveUnionType(type, checker, file, typeRegistry, visiting) {
|
|
1444
|
+
const allTypes = type.types;
|
|
1445
|
+
const nonNullTypes = allTypes.filter(
|
|
1135
1446
|
(t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
|
|
1136
1447
|
);
|
|
1137
|
-
const hasNull =
|
|
1138
|
-
const
|
|
1139
|
-
if (
|
|
1140
|
-
const
|
|
1141
|
-
jsonSchema: { type: "boolean" },
|
|
1142
|
-
formSpecFieldType: "boolean"
|
|
1143
|
-
};
|
|
1448
|
+
const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
|
|
1449
|
+
const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
|
|
1450
|
+
if (isBooleanUnion2) {
|
|
1451
|
+
const boolNode = { kind: "primitive", primitiveKind: "boolean" };
|
|
1144
1452
|
if (hasNull) {
|
|
1145
|
-
|
|
1453
|
+
return {
|
|
1454
|
+
kind: "union",
|
|
1455
|
+
members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1456
|
+
};
|
|
1146
1457
|
}
|
|
1147
|
-
return
|
|
1458
|
+
return boolNode;
|
|
1148
1459
|
}
|
|
1149
1460
|
const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
|
|
1150
1461
|
if (allStringLiterals && nonNullTypes.length > 0) {
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1462
|
+
const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
|
|
1463
|
+
const enumNode = {
|
|
1464
|
+
kind: "enum",
|
|
1465
|
+
members: stringTypes.map((t) => ({ value: t.value }))
|
|
1155
1466
|
};
|
|
1156
1467
|
if (hasNull) {
|
|
1157
|
-
|
|
1468
|
+
return {
|
|
1469
|
+
kind: "union",
|
|
1470
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1471
|
+
};
|
|
1158
1472
|
}
|
|
1159
|
-
return
|
|
1473
|
+
return enumNode;
|
|
1160
1474
|
}
|
|
1161
1475
|
const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
|
|
1162
1476
|
if (allNumberLiterals && nonNullTypes.length > 0) {
|
|
1163
|
-
const
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1477
|
+
const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
|
|
1478
|
+
const enumNode = {
|
|
1479
|
+
kind: "enum",
|
|
1480
|
+
members: numberTypes.map((t) => ({ value: t.value }))
|
|
1167
1481
|
};
|
|
1168
1482
|
if (hasNull) {
|
|
1169
|
-
|
|
1483
|
+
return {
|
|
1484
|
+
kind: "union",
|
|
1485
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1486
|
+
};
|
|
1170
1487
|
}
|
|
1171
|
-
return
|
|
1488
|
+
return enumNode;
|
|
1172
1489
|
}
|
|
1173
1490
|
if (nonNullTypes.length === 1 && nonNullTypes[0]) {
|
|
1174
|
-
const
|
|
1491
|
+
const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
|
|
1175
1492
|
if (hasNull) {
|
|
1176
|
-
|
|
1493
|
+
return {
|
|
1494
|
+
kind: "union",
|
|
1495
|
+
members: [inner, { kind: "primitive", primitiveKind: "null" }]
|
|
1496
|
+
};
|
|
1177
1497
|
}
|
|
1178
|
-
return
|
|
1498
|
+
return inner;
|
|
1179
1499
|
}
|
|
1180
|
-
const
|
|
1500
|
+
const members = nonNullTypes.map(
|
|
1501
|
+
(t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
|
|
1502
|
+
);
|
|
1181
1503
|
if (hasNull) {
|
|
1182
|
-
|
|
1504
|
+
members.push({ kind: "primitive", primitiveKind: "null" });
|
|
1183
1505
|
}
|
|
1184
|
-
return {
|
|
1185
|
-
jsonSchema: { oneOf: schemas },
|
|
1186
|
-
formSpecFieldType: "union"
|
|
1187
|
-
};
|
|
1506
|
+
return { kind: "union", members };
|
|
1188
1507
|
}
|
|
1189
|
-
function
|
|
1190
|
-
const typeArgs = type.typeArguments;
|
|
1508
|
+
function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
1509
|
+
const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
|
|
1191
1510
|
const elementType = typeArgs?.[0];
|
|
1192
|
-
const
|
|
1193
|
-
return {
|
|
1194
|
-
jsonSchema: {
|
|
1195
|
-
type: "array",
|
|
1196
|
-
items: itemSchema
|
|
1197
|
-
},
|
|
1198
|
-
formSpecFieldType: "array"
|
|
1199
|
-
};
|
|
1511
|
+
const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
|
|
1512
|
+
return { kind: "array", items };
|
|
1200
1513
|
}
|
|
1201
|
-
function
|
|
1514
|
+
function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
1202
1515
|
if (visiting.has(type)) {
|
|
1203
|
-
return {
|
|
1516
|
+
return { kind: "object", properties: [], additionalProperties: false };
|
|
1204
1517
|
}
|
|
1205
1518
|
visiting.add(type);
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1519
|
+
const typeName = getNamedTypeName(type);
|
|
1520
|
+
if (typeName && typeName in typeRegistry) {
|
|
1521
|
+
visiting.delete(type);
|
|
1522
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
1523
|
+
}
|
|
1524
|
+
const properties = [];
|
|
1525
|
+
const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
|
|
1526
|
+
for (const prop of type.getProperties()) {
|
|
1527
|
+
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
1528
|
+
if (!declaration) continue;
|
|
1529
|
+
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
1530
|
+
const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
|
|
1531
|
+
const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
|
|
1532
|
+
const fieldNodeInfo = fieldInfoMap?.get(prop.name);
|
|
1533
|
+
properties.push({
|
|
1534
|
+
name: prop.name,
|
|
1535
|
+
type: propTypeNode,
|
|
1536
|
+
optional,
|
|
1537
|
+
constraints: fieldNodeInfo?.constraints ?? [],
|
|
1538
|
+
annotations: fieldNodeInfo?.annotations ?? [],
|
|
1539
|
+
provenance: fieldNodeInfo?.provenance ?? provenanceForFile(file)
|
|
1540
|
+
});
|
|
1214
1541
|
}
|
|
1215
1542
|
visiting.delete(type);
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
...required.length > 0 ? { required } : {}
|
|
1221
|
-
},
|
|
1222
|
-
formSpecFieldType: "object"
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
function createFormSpecField(fieldName, type, decorators, optional, checker, visitedTypes = /* @__PURE__ */ new Set()) {
|
|
1226
|
-
const { formSpecFieldType } = convertType(type, checker);
|
|
1227
|
-
const field = {
|
|
1228
|
-
_field: formSpecFieldType,
|
|
1229
|
-
id: fieldName
|
|
1543
|
+
const objectNode = {
|
|
1544
|
+
kind: "object",
|
|
1545
|
+
properties,
|
|
1546
|
+
additionalProperties: false
|
|
1230
1547
|
};
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
for (const propInfo of getObjectPropertyInfos(type, checker)) {
|
|
1239
|
-
nestedFields.push(
|
|
1240
|
-
createFormSpecField(
|
|
1241
|
-
propInfo.name,
|
|
1242
|
-
propInfo.type,
|
|
1243
|
-
propInfo.fieldInfo?.decorators ?? [],
|
|
1244
|
-
propInfo.optional,
|
|
1245
|
-
checker,
|
|
1246
|
-
visitedTypes
|
|
1247
|
-
)
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
visitedTypes.delete(type);
|
|
1251
|
-
if (nestedFields.length > 0) {
|
|
1252
|
-
field.fields = nestedFields;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1548
|
+
if (typeName) {
|
|
1549
|
+
typeRegistry[typeName] = {
|
|
1550
|
+
name: typeName,
|
|
1551
|
+
type: objectNode,
|
|
1552
|
+
provenance: provenanceForFile(file)
|
|
1553
|
+
};
|
|
1554
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
1255
1555
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
break;
|
|
1280
|
-
}
|
|
1281
|
-
case "Minimum":
|
|
1282
|
-
if (typeof args[0] === "number") {
|
|
1283
|
-
field.min = args[0];
|
|
1284
|
-
}
|
|
1285
|
-
break;
|
|
1286
|
-
case "Maximum":
|
|
1287
|
-
if (typeof args[0] === "number") {
|
|
1288
|
-
field.max = args[0];
|
|
1289
|
-
}
|
|
1290
|
-
break;
|
|
1291
|
-
case "MinLength":
|
|
1292
|
-
if (typeof args[0] === "number") {
|
|
1293
|
-
field.minLength = args[0];
|
|
1294
|
-
}
|
|
1295
|
-
break;
|
|
1296
|
-
case "MaxLength":
|
|
1297
|
-
if (typeof args[0] === "number") {
|
|
1298
|
-
field.maxLength = args[0];
|
|
1299
|
-
}
|
|
1300
|
-
break;
|
|
1301
|
-
case "Pattern":
|
|
1302
|
-
if (typeof args[0] === "string") {
|
|
1303
|
-
field.pattern = args[0];
|
|
1304
|
-
}
|
|
1305
|
-
break;
|
|
1306
|
-
case "EnumOptions":
|
|
1307
|
-
if (Array.isArray(args[0])) {
|
|
1308
|
-
field.options = args[0];
|
|
1309
|
-
}
|
|
1310
|
-
break;
|
|
1311
|
-
case "ShowWhen":
|
|
1312
|
-
if (typeof args[0] === "object" && args[0] !== null) {
|
|
1313
|
-
field.showWhen = args[0];
|
|
1314
|
-
}
|
|
1315
|
-
break;
|
|
1316
|
-
case "Group":
|
|
1317
|
-
if (typeof args[0] === "string") {
|
|
1318
|
-
field.group = args[0];
|
|
1319
|
-
}
|
|
1320
|
-
break;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
function applyDecoratorsToSchema(schema, decorators, fieldInfo) {
|
|
1324
|
-
const result = { ...schema };
|
|
1325
|
-
for (const dec of decorators) {
|
|
1326
|
-
const { args } = dec;
|
|
1327
|
-
const resolved = dec.resolved;
|
|
1328
|
-
const effectiveName = resolved?.extendsBuiltin ?? dec.name;
|
|
1329
|
-
switch (effectiveName) {
|
|
1330
|
-
case "Field": {
|
|
1331
|
-
const opts = args[0];
|
|
1332
|
-
if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) {
|
|
1333
|
-
if (typeof opts["displayName"] === "string") {
|
|
1334
|
-
result.title = opts["displayName"];
|
|
1335
|
-
}
|
|
1336
|
-
if (typeof opts["description"] === "string") {
|
|
1337
|
-
result.description = opts["description"];
|
|
1556
|
+
return objectNode;
|
|
1557
|
+
}
|
|
1558
|
+
function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
|
|
1559
|
+
const symbols = [type.getSymbol(), type.aliasSymbol].filter(
|
|
1560
|
+
(s) => s?.declarations != null && s.declarations.length > 0
|
|
1561
|
+
);
|
|
1562
|
+
for (const symbol of symbols) {
|
|
1563
|
+
const declarations = symbol.declarations;
|
|
1564
|
+
if (!declarations) continue;
|
|
1565
|
+
const classDecl = declarations.find(ts4.isClassDeclaration);
|
|
1566
|
+
if (classDecl) {
|
|
1567
|
+
const map = /* @__PURE__ */ new Map();
|
|
1568
|
+
for (const member of classDecl.members) {
|
|
1569
|
+
if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
|
|
1570
|
+
const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
|
|
1571
|
+
if (fieldNode) {
|
|
1572
|
+
map.set(fieldNode.name, {
|
|
1573
|
+
constraints: [...fieldNode.constraints],
|
|
1574
|
+
annotations: [...fieldNode.annotations],
|
|
1575
|
+
provenance: fieldNode.provenance
|
|
1576
|
+
});
|
|
1338
1577
|
}
|
|
1339
1578
|
}
|
|
1340
|
-
break;
|
|
1341
|
-
}
|
|
1342
|
-
case "Minimum":
|
|
1343
|
-
if (typeof args[0] === "number") {
|
|
1344
|
-
result.minimum = args[0];
|
|
1345
|
-
}
|
|
1346
|
-
break;
|
|
1347
|
-
case "Maximum":
|
|
1348
|
-
if (typeof args[0] === "number") {
|
|
1349
|
-
result.maximum = args[0];
|
|
1350
|
-
}
|
|
1351
|
-
break;
|
|
1352
|
-
case "ExclusiveMinimum":
|
|
1353
|
-
if (typeof args[0] === "number") {
|
|
1354
|
-
result.exclusiveMinimum = args[0];
|
|
1355
|
-
}
|
|
1356
|
-
break;
|
|
1357
|
-
case "ExclusiveMaximum":
|
|
1358
|
-
if (typeof args[0] === "number") {
|
|
1359
|
-
result.exclusiveMaximum = args[0];
|
|
1360
|
-
}
|
|
1361
|
-
break;
|
|
1362
|
-
case "MinLength":
|
|
1363
|
-
if (typeof args[0] === "number") {
|
|
1364
|
-
result.minLength = args[0];
|
|
1365
|
-
}
|
|
1366
|
-
break;
|
|
1367
|
-
case "MaxLength":
|
|
1368
|
-
if (typeof args[0] === "number") {
|
|
1369
|
-
result.maxLength = args[0];
|
|
1370
|
-
}
|
|
1371
|
-
break;
|
|
1372
|
-
case "Pattern":
|
|
1373
|
-
if (typeof args[0] === "string") {
|
|
1374
|
-
result.pattern = args[0];
|
|
1375
|
-
}
|
|
1376
|
-
break;
|
|
1377
|
-
}
|
|
1378
|
-
if (resolved?.extensionName && /^[a-z][a-z0-9-]*$/.test(resolved.extensionName)) {
|
|
1379
|
-
const key = `x-formspec-${resolved.extensionName}`;
|
|
1380
|
-
if (resolved.isMarker) {
|
|
1381
|
-
setSchemaExtension(result, key, true);
|
|
1382
|
-
} else {
|
|
1383
|
-
setSchemaExtension(result, key, args[0] ?? true);
|
|
1384
1579
|
}
|
|
1580
|
+
return map;
|
|
1385
1581
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
result.deprecated = true;
|
|
1582
|
+
const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
|
|
1583
|
+
if (interfaceDecl) {
|
|
1584
|
+
return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
|
|
1390
1585
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1586
|
+
const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
|
|
1587
|
+
if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
|
|
1588
|
+
return buildFieldNodeInfoMap(
|
|
1589
|
+
typeAliasDecl.type.members,
|
|
1590
|
+
checker,
|
|
1591
|
+
file,
|
|
1592
|
+
typeRegistry,
|
|
1593
|
+
visiting
|
|
1594
|
+
);
|
|
1393
1595
|
}
|
|
1394
1596
|
}
|
|
1395
|
-
return
|
|
1597
|
+
return null;
|
|
1396
1598
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
if (configFile.error) {
|
|
1410
|
-
throw new Error(
|
|
1411
|
-
`Error reading tsconfig.json: ${ts5.flattenDiagnosticMessageText(configFile.error.messageText, "\n")}`
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
const parsed = ts5.parseJsonConfigFileContent(
|
|
1415
|
-
configFile.config,
|
|
1416
|
-
ts5.sys,
|
|
1417
|
-
path.dirname(configPath)
|
|
1418
|
-
);
|
|
1419
|
-
if (parsed.errors.length > 0) {
|
|
1420
|
-
const errorMessages = parsed.errors.map((e) => ts5.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
|
|
1421
|
-
throw new Error(`Error parsing tsconfig.json: ${errorMessages}`);
|
|
1599
|
+
function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
|
|
1600
|
+
const map = /* @__PURE__ */ new Map();
|
|
1601
|
+
for (const member of members) {
|
|
1602
|
+
if (ts4.isPropertySignature(member)) {
|
|
1603
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
1604
|
+
if (fieldNode) {
|
|
1605
|
+
map.set(fieldNode.name, {
|
|
1606
|
+
constraints: [...fieldNode.constraints],
|
|
1607
|
+
annotations: [...fieldNode.annotations],
|
|
1608
|
+
provenance: fieldNode.provenance
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1422
1611
|
}
|
|
1423
|
-
compilerOptions = parsed.options;
|
|
1424
|
-
fileNames = parsed.fileNames.includes(absolutePath) ? parsed.fileNames : [...parsed.fileNames, absolutePath];
|
|
1425
|
-
} else {
|
|
1426
|
-
compilerOptions = {
|
|
1427
|
-
target: ts5.ScriptTarget.ES2022,
|
|
1428
|
-
module: ts5.ModuleKind.NodeNext,
|
|
1429
|
-
moduleResolution: ts5.ModuleResolutionKind.NodeNext,
|
|
1430
|
-
strict: true,
|
|
1431
|
-
skipLibCheck: true,
|
|
1432
|
-
declaration: true
|
|
1433
|
-
};
|
|
1434
|
-
fileNames = [absolutePath];
|
|
1435
|
-
}
|
|
1436
|
-
const program = ts5.createProgram(fileNames, compilerOptions);
|
|
1437
|
-
const sourceFile = program.getSourceFile(absolutePath);
|
|
1438
|
-
if (!sourceFile) {
|
|
1439
|
-
throw new Error(`Could not find source file: ${absolutePath}`);
|
|
1440
1612
|
}
|
|
1613
|
+
return map;
|
|
1614
|
+
}
|
|
1615
|
+
function extractTypeAliasConstraintNodes(typeNode, checker, file) {
|
|
1616
|
+
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
1617
|
+
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
1618
|
+
if (!symbol?.declarations) return [];
|
|
1619
|
+
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1620
|
+
if (!aliasDecl) return [];
|
|
1621
|
+
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
1622
|
+
return extractJSDocConstraintNodes(aliasDecl, file);
|
|
1623
|
+
}
|
|
1624
|
+
function provenanceForNode(node, file) {
|
|
1625
|
+
const sourceFile = node.getSourceFile();
|
|
1626
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
1441
1627
|
return {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1628
|
+
surface: "tsdoc",
|
|
1629
|
+
file,
|
|
1630
|
+
line: line + 1,
|
|
1631
|
+
column: character
|
|
1445
1632
|
};
|
|
1446
1633
|
}
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1634
|
+
function provenanceForFile(file) {
|
|
1635
|
+
return { surface: "tsdoc", file, line: 0, column: 0 };
|
|
1636
|
+
}
|
|
1637
|
+
function getNamedTypeName(type) {
|
|
1638
|
+
const symbol = type.getSymbol();
|
|
1639
|
+
if (symbol?.declarations) {
|
|
1640
|
+
const decl = symbol.declarations[0];
|
|
1641
|
+
if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
|
|
1642
|
+
const name = ts4.isClassDeclaration(decl) ? decl.name?.text : decl.name.text;
|
|
1643
|
+
if (name) return name;
|
|
1454
1644
|
}
|
|
1455
|
-
ts5.forEachChild(node, visit);
|
|
1456
1645
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1646
|
+
const aliasSymbol = type.aliasSymbol;
|
|
1647
|
+
if (aliasSymbol?.declarations) {
|
|
1648
|
+
const aliasDecl = aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1649
|
+
if (aliasDecl) {
|
|
1650
|
+
return aliasDecl.name.text;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return null;
|
|
1459
1654
|
}
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1655
|
+
function analyzeMethod(method, checker) {
|
|
1656
|
+
if (!ts4.isIdentifier(method.name)) {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
const name = method.name.text;
|
|
1660
|
+
const parameters = [];
|
|
1661
|
+
for (const param of method.parameters) {
|
|
1662
|
+
if (ts4.isIdentifier(param.name)) {
|
|
1663
|
+
const paramInfo = analyzeParameter(param, checker);
|
|
1664
|
+
parameters.push(paramInfo);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const returnTypeNode = method.type;
|
|
1668
|
+
const signature = checker.getSignatureFromDeclaration(method);
|
|
1669
|
+
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getTypeAtLocation(method);
|
|
1670
|
+
return { name, parameters, returnTypeNode, returnType };
|
|
1462
1671
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1672
|
+
function analyzeParameter(param, checker) {
|
|
1673
|
+
const name = ts4.isIdentifier(param.name) ? param.name.text : "param";
|
|
1674
|
+
const typeNode = param.type;
|
|
1675
|
+
const type = checker.getTypeAtLocation(param);
|
|
1676
|
+
const formSpecExportName = detectFormSpecReference(typeNode);
|
|
1677
|
+
const optional = param.questionToken !== void 0 || param.initializer !== void 0;
|
|
1678
|
+
return { name, typeNode, type, formSpecExportName, optional };
|
|
1465
1679
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1680
|
+
function detectFormSpecReference(typeNode) {
|
|
1681
|
+
if (!typeNode) return null;
|
|
1682
|
+
if (!ts4.isTypeReferenceNode(typeNode)) return null;
|
|
1683
|
+
const typeName = ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : ts4.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right.text : null;
|
|
1684
|
+
if (typeName !== "InferSchema" && typeName !== "InferFormSchema") return null;
|
|
1685
|
+
const typeArg = typeNode.typeArguments?.[0];
|
|
1686
|
+
if (!typeArg || !ts4.isTypeQueryNode(typeArg)) return null;
|
|
1687
|
+
if (ts4.isIdentifier(typeArg.exprName)) {
|
|
1688
|
+
return typeArg.exprName.text;
|
|
1689
|
+
}
|
|
1690
|
+
if (ts4.isQualifiedName(typeArg.exprName)) {
|
|
1691
|
+
return typeArg.exprName.right.text;
|
|
1692
|
+
}
|
|
1693
|
+
return null;
|
|
1468
1694
|
}
|
|
1469
1695
|
|
|
1470
1696
|
// src/generators/class-schema.ts
|
|
1471
|
-
function generateClassSchemas(analysis,
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const { jsonSchema: baseSchema } = convertType(field.type, checker);
|
|
1477
|
-
const fieldSchema = applyDecoratorsToSchema(baseSchema, field.decorators, field);
|
|
1478
|
-
properties[field.name] = fieldSchema;
|
|
1479
|
-
if (!field.optional) {
|
|
1480
|
-
required.push(field.name);
|
|
1481
|
-
}
|
|
1482
|
-
const formSpecField = createFormSpecField(
|
|
1483
|
-
field.name,
|
|
1484
|
-
field.type,
|
|
1485
|
-
field.decorators,
|
|
1486
|
-
field.optional,
|
|
1487
|
-
checker
|
|
1488
|
-
);
|
|
1489
|
-
uiElements.push(formSpecField);
|
|
1490
|
-
}
|
|
1491
|
-
const jsonSchema = {
|
|
1492
|
-
type: "object",
|
|
1493
|
-
properties,
|
|
1494
|
-
...required.length > 0 ? { required } : {}
|
|
1697
|
+
function generateClassSchemas(analysis, source) {
|
|
1698
|
+
const ir = canonicalizeTSDoc(analysis, source);
|
|
1699
|
+
return {
|
|
1700
|
+
jsonSchema: generateJsonSchemaFromIR(ir),
|
|
1701
|
+
uiSchema: generateUiSchemaFromIR(ir)
|
|
1495
1702
|
};
|
|
1496
|
-
const uiSchema2 = generateUiSchemaFromFields(uiElements);
|
|
1497
|
-
return { jsonSchema, uiSchema: uiSchema2 };
|
|
1498
1703
|
}
|
|
1499
1704
|
function generateSchemasFromClass(options) {
|
|
1500
1705
|
const ctx = createProgramContext(options.filePath);
|
|
@@ -1502,26 +1707,27 @@ function generateSchemasFromClass(options) {
|
|
|
1502
1707
|
if (!classDecl) {
|
|
1503
1708
|
throw new Error(`Class "${options.className}" not found in ${options.filePath}`);
|
|
1504
1709
|
}
|
|
1505
|
-
const analysis =
|
|
1506
|
-
return generateClassSchemas(analysis,
|
|
1710
|
+
const analysis = analyzeClassToIR(classDecl, ctx.checker, options.filePath);
|
|
1711
|
+
return generateClassSchemas(analysis, { file: options.filePath });
|
|
1507
1712
|
}
|
|
1508
1713
|
function generateSchemas(options) {
|
|
1509
1714
|
const ctx = createProgramContext(options.filePath);
|
|
1715
|
+
const source = { file: options.filePath };
|
|
1510
1716
|
const classDecl = findClassByName(ctx.sourceFile, options.typeName);
|
|
1511
1717
|
if (classDecl) {
|
|
1512
|
-
const analysis =
|
|
1513
|
-
return generateClassSchemas(analysis,
|
|
1718
|
+
const analysis = analyzeClassToIR(classDecl, ctx.checker, options.filePath);
|
|
1719
|
+
return generateClassSchemas(analysis, source);
|
|
1514
1720
|
}
|
|
1515
1721
|
const interfaceDecl = findInterfaceByName(ctx.sourceFile, options.typeName);
|
|
1516
1722
|
if (interfaceDecl) {
|
|
1517
|
-
const analysis =
|
|
1518
|
-
return generateClassSchemas(analysis,
|
|
1723
|
+
const analysis = analyzeInterfaceToIR(interfaceDecl, ctx.checker, options.filePath);
|
|
1724
|
+
return generateClassSchemas(analysis, source);
|
|
1519
1725
|
}
|
|
1520
1726
|
const typeAlias = findTypeAliasByName(ctx.sourceFile, options.typeName);
|
|
1521
1727
|
if (typeAlias) {
|
|
1522
|
-
const result =
|
|
1728
|
+
const result = analyzeTypeAliasToIR(typeAlias, ctx.checker, options.filePath);
|
|
1523
1729
|
if (result.ok) {
|
|
1524
|
-
return generateClassSchemas(result.analysis,
|
|
1730
|
+
return generateClassSchemas(result.analysis, source);
|
|
1525
1731
|
}
|
|
1526
1732
|
throw new Error(result.error);
|
|
1527
1733
|
}
|
|
@@ -1530,453 +1736,6 @@ function generateSchemas(options) {
|
|
|
1530
1736
|
);
|
|
1531
1737
|
}
|
|
1532
1738
|
|
|
1533
|
-
// src/codegen/index.ts
|
|
1534
|
-
import * as ts6 from "typescript";
|
|
1535
|
-
import * as path2 from "path";
|
|
1536
|
-
import * as fs from "fs";
|
|
1537
|
-
function findDecoratedClasses(files, baseDir) {
|
|
1538
|
-
const program = ts6.createProgram(files, {
|
|
1539
|
-
target: ts6.ScriptTarget.ESNext,
|
|
1540
|
-
module: ts6.ModuleKind.ESNext,
|
|
1541
|
-
moduleResolution: ts6.ModuleResolutionKind.NodeNext,
|
|
1542
|
-
// TC39 Stage 3 decorators — do not use experimentalDecorators
|
|
1543
|
-
strict: true
|
|
1544
|
-
});
|
|
1545
|
-
const checker = program.getTypeChecker();
|
|
1546
|
-
const results = [];
|
|
1547
|
-
for (const sourceFile of program.getSourceFiles()) {
|
|
1548
|
-
if (sourceFile.isDeclarationFile) continue;
|
|
1549
|
-
if (!files.some((f) => path2.resolve(f) === sourceFile.fileName)) continue;
|
|
1550
|
-
ts6.forEachChild(sourceFile, (node) => {
|
|
1551
|
-
if (ts6.isClassDeclaration(node) && hasDecoratedProperties(node)) {
|
|
1552
|
-
const className = node.name?.text;
|
|
1553
|
-
if (!className) return;
|
|
1554
|
-
const typeMetadata = extractTypeMetadata(node, checker);
|
|
1555
|
-
const relativePath = path2.relative(baseDir, sourceFile.fileName);
|
|
1556
|
-
const exported = isClassExported(node, sourceFile);
|
|
1557
|
-
results.push({
|
|
1558
|
-
name: className,
|
|
1559
|
-
sourcePath: relativePath.replace(/\.tsx?$/, ""),
|
|
1560
|
-
typeMetadata,
|
|
1561
|
-
isExported: exported
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
return results;
|
|
1567
|
-
}
|
|
1568
|
-
function hasDecoratedProperties(node) {
|
|
1569
|
-
return node.members.some((member) => {
|
|
1570
|
-
if (!ts6.isPropertyDeclaration(member)) return false;
|
|
1571
|
-
const decorators = ts6.getDecorators(member);
|
|
1572
|
-
return decorators !== void 0 && decorators.length > 0;
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
function isClassExported(classNode, sourceFile) {
|
|
1576
|
-
const className = classNode.name?.text;
|
|
1577
|
-
if (!className) return false;
|
|
1578
|
-
const modifiers = ts6.getModifiers(classNode);
|
|
1579
|
-
if (modifiers) {
|
|
1580
|
-
const hasExport = modifiers.some((mod) => mod.kind === ts6.SyntaxKind.ExportKeyword);
|
|
1581
|
-
if (hasExport) return true;
|
|
1582
|
-
}
|
|
1583
|
-
for (const statement of sourceFile.statements) {
|
|
1584
|
-
if (ts6.isExportDeclaration(statement) && statement.exportClause) {
|
|
1585
|
-
if (ts6.isNamedExports(statement.exportClause)) {
|
|
1586
|
-
for (const element of statement.exportClause.elements) {
|
|
1587
|
-
const localName = element.propertyName?.text ?? element.name.text;
|
|
1588
|
-
if (localName === className) {
|
|
1589
|
-
return true;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
return false;
|
|
1596
|
-
}
|
|
1597
|
-
function extractTypeMetadata(classNode, checker) {
|
|
1598
|
-
const metadata = {};
|
|
1599
|
-
for (const member of classNode.members) {
|
|
1600
|
-
if (!ts6.isPropertyDeclaration(member)) continue;
|
|
1601
|
-
if (!member.name || !ts6.isIdentifier(member.name)) continue;
|
|
1602
|
-
const symbol = checker.getSymbolAtLocation(member.name);
|
|
1603
|
-
if (!symbol) continue;
|
|
1604
|
-
const type = checker.getTypeOfSymbolAtLocation(symbol, member);
|
|
1605
|
-
const fieldName = member.name.text;
|
|
1606
|
-
const isOptional = !!(symbol.flags & ts6.SymbolFlags.Optional);
|
|
1607
|
-
const typeInfo = convertTypeToMetadata(type, checker);
|
|
1608
|
-
if (isOptional) {
|
|
1609
|
-
typeInfo.optional = true;
|
|
1610
|
-
}
|
|
1611
|
-
metadata[fieldName] = typeInfo;
|
|
1612
|
-
}
|
|
1613
|
-
return metadata;
|
|
1614
|
-
}
|
|
1615
|
-
function convertTypeToMetadata(type, checker, visited = /* @__PURE__ */ new Set()) {
|
|
1616
|
-
const isObjectType = (type.flags & ts6.TypeFlags.Object) !== 0;
|
|
1617
|
-
if (isObjectType) {
|
|
1618
|
-
if (visited.has(type)) {
|
|
1619
|
-
return { type: "unknown" };
|
|
1620
|
-
}
|
|
1621
|
-
visited.add(type);
|
|
1622
|
-
}
|
|
1623
|
-
if (type.isUnion()) {
|
|
1624
|
-
const types = type.types;
|
|
1625
|
-
const nonNullTypes = types.filter(
|
|
1626
|
-
(t) => !(t.flags & (ts6.TypeFlags.Null | ts6.TypeFlags.Undefined))
|
|
1627
|
-
);
|
|
1628
|
-
const hasNull = types.some((t) => t.flags & ts6.TypeFlags.Null);
|
|
1629
|
-
const isBooleanUnion = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts6.TypeFlags.BooleanLiteral);
|
|
1630
|
-
if (isBooleanUnion) {
|
|
1631
|
-
const result = { type: "boolean" };
|
|
1632
|
-
if (hasNull) result.nullable = true;
|
|
1633
|
-
return result;
|
|
1634
|
-
}
|
|
1635
|
-
const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
|
|
1636
|
-
if (allStringLiterals && nonNullTypes.length > 0) {
|
|
1637
|
-
const result = {
|
|
1638
|
-
type: "enum",
|
|
1639
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript doesn't narrow array types from `every` predicate
|
|
1640
|
-
values: nonNullTypes.map((t) => t.value)
|
|
1641
|
-
};
|
|
1642
|
-
if (hasNull) result.nullable = true;
|
|
1643
|
-
return result;
|
|
1644
|
-
}
|
|
1645
|
-
const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
|
|
1646
|
-
if (allNumberLiterals && nonNullTypes.length > 0) {
|
|
1647
|
-
const result = {
|
|
1648
|
-
type: "enum",
|
|
1649
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript doesn't narrow array types from `every` predicate
|
|
1650
|
-
values: nonNullTypes.map((t) => t.value)
|
|
1651
|
-
};
|
|
1652
|
-
if (hasNull) result.nullable = true;
|
|
1653
|
-
return result;
|
|
1654
|
-
}
|
|
1655
|
-
if (nonNullTypes.length === 1) {
|
|
1656
|
-
const singleType = nonNullTypes[0];
|
|
1657
|
-
if (!singleType) return { type: "unknown" };
|
|
1658
|
-
const result = convertTypeToMetadata(singleType, checker, visited);
|
|
1659
|
-
if (hasNull) result.nullable = true;
|
|
1660
|
-
return result;
|
|
1661
|
-
}
|
|
1662
|
-
return { type: "unknown" };
|
|
1663
|
-
}
|
|
1664
|
-
if (type.flags & ts6.TypeFlags.String) return { type: "string" };
|
|
1665
|
-
if (type.flags & ts6.TypeFlags.Number) return { type: "number" };
|
|
1666
|
-
if (type.flags & ts6.TypeFlags.Boolean) return { type: "boolean" };
|
|
1667
|
-
if (type.isStringLiteral()) {
|
|
1668
|
-
return { type: "enum", values: [type.value] };
|
|
1669
|
-
}
|
|
1670
|
-
if (type.isNumberLiteral()) {
|
|
1671
|
-
return { type: "enum", values: [type.value] };
|
|
1672
|
-
}
|
|
1673
|
-
if (checker.isArrayType(type)) {
|
|
1674
|
-
const typeArgs = type.typeArguments;
|
|
1675
|
-
return {
|
|
1676
|
-
type: "array",
|
|
1677
|
-
itemType: typeArgs?.[0] ? convertTypeToMetadata(typeArgs[0], checker, visited) : { type: "unknown" }
|
|
1678
|
-
};
|
|
1679
|
-
}
|
|
1680
|
-
if (type.flags & ts6.TypeFlags.Object) {
|
|
1681
|
-
const properties = {};
|
|
1682
|
-
for (const prop of type.getProperties()) {
|
|
1683
|
-
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
1684
|
-
if (!declaration) continue;
|
|
1685
|
-
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
1686
|
-
const propOptional = !!(prop.flags & ts6.SymbolFlags.Optional);
|
|
1687
|
-
const propMeta = convertTypeToMetadata(propType, checker, visited);
|
|
1688
|
-
if (propOptional) propMeta.optional = true;
|
|
1689
|
-
properties[prop.name] = propMeta;
|
|
1690
|
-
}
|
|
1691
|
-
if (Object.keys(properties).length > 0) {
|
|
1692
|
-
return { type: "object", properties };
|
|
1693
|
-
}
|
|
1694
|
-
return { type: "object" };
|
|
1695
|
-
}
|
|
1696
|
-
return { type: "unknown" };
|
|
1697
|
-
}
|
|
1698
|
-
function generateCodegenOutput(classes, outputPath, baseDir) {
|
|
1699
|
-
const outputDir = path2.dirname(path2.resolve(outputPath));
|
|
1700
|
-
const lines = [
|
|
1701
|
-
"/**",
|
|
1702
|
-
" * Auto-generated by FormSpec CLI.",
|
|
1703
|
-
" * DO NOT EDIT - changes will be overwritten.",
|
|
1704
|
-
" *",
|
|
1705
|
-
" * This file provides:",
|
|
1706
|
-
" * - Type metadata patches for runtime schema generation",
|
|
1707
|
-
" * - Inferred schema types (e.g., UserFormSchema)",
|
|
1708
|
-
" * - Type-safe FormSpec accessors (e.g., getUserFormFormSpec())",
|
|
1709
|
-
" *",
|
|
1710
|
-
" * Usage:",
|
|
1711
|
-
" * import { UserFormSchema, getUserFormFormSpec } from './__formspec_types__';",
|
|
1712
|
-
" *",
|
|
1713
|
-
" * const data: UserFormSchema = { name: 'Alice', country: 'us' };",
|
|
1714
|
-
" * const spec = getUserFormFormSpec();",
|
|
1715
|
-
" */",
|
|
1716
|
-
"",
|
|
1717
|
-
"/* eslint-disable @typescript-eslint/no-explicit-any */",
|
|
1718
|
-
""
|
|
1719
|
-
];
|
|
1720
|
-
for (const cls of classes) {
|
|
1721
|
-
const absoluteSourcePath = path2.resolve(baseDir, cls.sourcePath);
|
|
1722
|
-
const importPath = getRelativeImportPath(outputDir, absoluteSourcePath);
|
|
1723
|
-
lines.push(`import { ${cls.name} } from "${importPath}";`);
|
|
1724
|
-
}
|
|
1725
|
-
lines.push("");
|
|
1726
|
-
lines.push("// =============================================================================");
|
|
1727
|
-
lines.push("// Type Metadata Patches");
|
|
1728
|
-
lines.push("// =============================================================================");
|
|
1729
|
-
lines.push("");
|
|
1730
|
-
for (const cls of classes) {
|
|
1731
|
-
const metadataJson = JSON.stringify(cls.typeMetadata, null, 2).split("\n").map((line, i) => i === 0 ? line : " " + line).join("\n");
|
|
1732
|
-
lines.push(`(${cls.name} as any).__formspec_types__ = ${metadataJson};`);
|
|
1733
|
-
lines.push("");
|
|
1734
|
-
}
|
|
1735
|
-
lines.push("// =============================================================================");
|
|
1736
|
-
lines.push("// Inferred Schema Types");
|
|
1737
|
-
lines.push("// =============================================================================");
|
|
1738
|
-
lines.push("");
|
|
1739
|
-
for (const cls of classes) {
|
|
1740
|
-
lines.push(generateSchemaType(cls));
|
|
1741
|
-
lines.push("");
|
|
1742
|
-
}
|
|
1743
|
-
lines.push("// =============================================================================");
|
|
1744
|
-
lines.push("// Type-Safe FormSpec Accessors");
|
|
1745
|
-
lines.push("// =============================================================================");
|
|
1746
|
-
lines.push("");
|
|
1747
|
-
for (const cls of classes) {
|
|
1748
|
-
lines.push(generateTypedAccessor(cls));
|
|
1749
|
-
lines.push("");
|
|
1750
|
-
}
|
|
1751
|
-
return lines.join("\n");
|
|
1752
|
-
}
|
|
1753
|
-
function metadataToTypeString(metadata) {
|
|
1754
|
-
const baseType = metadataToBaseTypeString(metadata);
|
|
1755
|
-
if (metadata.nullable && metadata.optional) {
|
|
1756
|
-
return `${baseType} | null | undefined`;
|
|
1757
|
-
}
|
|
1758
|
-
if (metadata.nullable) {
|
|
1759
|
-
return `${baseType} | null`;
|
|
1760
|
-
}
|
|
1761
|
-
if (metadata.optional) {
|
|
1762
|
-
return `${baseType} | undefined`;
|
|
1763
|
-
}
|
|
1764
|
-
return baseType;
|
|
1765
|
-
}
|
|
1766
|
-
var VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
1767
|
-
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
1768
|
-
// Keywords
|
|
1769
|
-
"break",
|
|
1770
|
-
"case",
|
|
1771
|
-
"catch",
|
|
1772
|
-
"continue",
|
|
1773
|
-
"debugger",
|
|
1774
|
-
"default",
|
|
1775
|
-
"delete",
|
|
1776
|
-
"do",
|
|
1777
|
-
"else",
|
|
1778
|
-
"finally",
|
|
1779
|
-
"for",
|
|
1780
|
-
"function",
|
|
1781
|
-
"if",
|
|
1782
|
-
"in",
|
|
1783
|
-
"instanceof",
|
|
1784
|
-
"new",
|
|
1785
|
-
"return",
|
|
1786
|
-
"switch",
|
|
1787
|
-
"this",
|
|
1788
|
-
"throw",
|
|
1789
|
-
"try",
|
|
1790
|
-
"typeof",
|
|
1791
|
-
"var",
|
|
1792
|
-
"void",
|
|
1793
|
-
"while",
|
|
1794
|
-
"with",
|
|
1795
|
-
"class",
|
|
1796
|
-
"const",
|
|
1797
|
-
"enum",
|
|
1798
|
-
"export",
|
|
1799
|
-
"extends",
|
|
1800
|
-
"import",
|
|
1801
|
-
"super",
|
|
1802
|
-
"implements",
|
|
1803
|
-
"interface",
|
|
1804
|
-
"let",
|
|
1805
|
-
"package",
|
|
1806
|
-
"private",
|
|
1807
|
-
"protected",
|
|
1808
|
-
"public",
|
|
1809
|
-
"static",
|
|
1810
|
-
"yield",
|
|
1811
|
-
// ES6+ keywords
|
|
1812
|
-
"async",
|
|
1813
|
-
"await",
|
|
1814
|
-
// Literal values (not technically keywords but best to quote)
|
|
1815
|
-
"null",
|
|
1816
|
-
"true",
|
|
1817
|
-
"false",
|
|
1818
|
-
// Accessor keywords
|
|
1819
|
-
"get",
|
|
1820
|
-
"set",
|
|
1821
|
-
// Strict mode reserved
|
|
1822
|
-
"arguments",
|
|
1823
|
-
"eval"
|
|
1824
|
-
]);
|
|
1825
|
-
function needsPropertyQuoting(key) {
|
|
1826
|
-
if (!VALID_IDENTIFIER.test(key)) {
|
|
1827
|
-
return true;
|
|
1828
|
-
}
|
|
1829
|
-
return RESERVED_WORDS.has(key);
|
|
1830
|
-
}
|
|
1831
|
-
function escapePropertyKey(key) {
|
|
1832
|
-
if (needsPropertyQuoting(key)) {
|
|
1833
|
-
return JSON.stringify(key);
|
|
1834
|
-
}
|
|
1835
|
-
return key;
|
|
1836
|
-
}
|
|
1837
|
-
function metadataToBaseTypeString(metadata) {
|
|
1838
|
-
switch (metadata.type) {
|
|
1839
|
-
case "string":
|
|
1840
|
-
return "string";
|
|
1841
|
-
case "number":
|
|
1842
|
-
return "number";
|
|
1843
|
-
case "boolean":
|
|
1844
|
-
return "boolean";
|
|
1845
|
-
case "enum":
|
|
1846
|
-
if (metadata.values && metadata.values.length > 0) {
|
|
1847
|
-
return metadata.values.map((v) => typeof v === "string" ? JSON.stringify(v) : String(v)).join(" | ");
|
|
1848
|
-
}
|
|
1849
|
-
return "string";
|
|
1850
|
-
case "array":
|
|
1851
|
-
if (metadata.itemType) {
|
|
1852
|
-
const itemType = metadataToTypeString(metadata.itemType);
|
|
1853
|
-
if (itemType.includes("|")) {
|
|
1854
|
-
return `(${itemType})[]`;
|
|
1855
|
-
}
|
|
1856
|
-
return `${itemType}[]`;
|
|
1857
|
-
}
|
|
1858
|
-
return "unknown[]";
|
|
1859
|
-
case "object":
|
|
1860
|
-
if (metadata.properties && Object.keys(metadata.properties).length > 0) {
|
|
1861
|
-
const props = Object.entries(metadata.properties).map(([key, propMeta]) => {
|
|
1862
|
-
const optional = propMeta.optional ? "?" : "";
|
|
1863
|
-
return `${escapePropertyKey(key)}${optional}: ${metadataToTypeString(propMeta)}`;
|
|
1864
|
-
}).join("; ");
|
|
1865
|
-
return `{ ${props} }`;
|
|
1866
|
-
}
|
|
1867
|
-
return "Record<string, unknown>";
|
|
1868
|
-
default:
|
|
1869
|
-
return "unknown";
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
function generateSchemaType(cls) {
|
|
1873
|
-
const props = Object.entries(cls.typeMetadata).map(([fieldName, metadata]) => {
|
|
1874
|
-
const optional = metadata.optional ? "?" : "";
|
|
1875
|
-
const typeStr = metadataToTypeString(metadata);
|
|
1876
|
-
return ` ${escapePropertyKey(fieldName)}${optional}: ${typeStr};`;
|
|
1877
|
-
}).join("\n");
|
|
1878
|
-
return `export type ${cls.name}Schema = {
|
|
1879
|
-
${props}
|
|
1880
|
-
};`;
|
|
1881
|
-
}
|
|
1882
|
-
function metadataTypeToFieldType(type) {
|
|
1883
|
-
switch (type) {
|
|
1884
|
-
case "string":
|
|
1885
|
-
return "text";
|
|
1886
|
-
case "number":
|
|
1887
|
-
return "number";
|
|
1888
|
-
case "boolean":
|
|
1889
|
-
return "boolean";
|
|
1890
|
-
case "enum":
|
|
1891
|
-
return "enum";
|
|
1892
|
-
case "array":
|
|
1893
|
-
return "array";
|
|
1894
|
-
case "object":
|
|
1895
|
-
return "object";
|
|
1896
|
-
default:
|
|
1897
|
-
return "text";
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
function generateTypedAccessor(cls) {
|
|
1901
|
-
const lines = [];
|
|
1902
|
-
const elementTypes = Object.entries(cls.typeMetadata).map(([fieldName, metadata]) => {
|
|
1903
|
-
const fieldType = metadataTypeToFieldType(metadata.type);
|
|
1904
|
-
const required = !metadata.optional;
|
|
1905
|
-
let elementType = `{ readonly _field: "${fieldType}"; readonly id: "${fieldName}"; readonly required: ${String(required)}`;
|
|
1906
|
-
if (metadata.type === "enum" && metadata.values) {
|
|
1907
|
-
const optionValues = metadata.values.map((v) => typeof v === "string" ? JSON.stringify(v) : String(v)).join(", ");
|
|
1908
|
-
elementType += `; readonly options: readonly [${optionValues}]`;
|
|
1909
|
-
}
|
|
1910
|
-
elementType += " }";
|
|
1911
|
-
return elementType;
|
|
1912
|
-
});
|
|
1913
|
-
lines.push(`export type ${cls.name}Elements = readonly [
|
|
1914
|
-
${elementTypes.join(",\n ")}
|
|
1915
|
-
];`);
|
|
1916
|
-
lines.push("");
|
|
1917
|
-
lines.push(`export type ${cls.name}FormSpec = { readonly elements: ${cls.name}Elements };`);
|
|
1918
|
-
lines.push("");
|
|
1919
|
-
lines.push(`/**`);
|
|
1920
|
-
lines.push(` * Type-safe FormSpec accessor for ${cls.name}.`);
|
|
1921
|
-
lines.push(` * Reads the patched type metadata from the class.`);
|
|
1922
|
-
lines.push(` */`);
|
|
1923
|
-
lines.push(`export function get${cls.name}FormSpec(): ${cls.name}FormSpec {`);
|
|
1924
|
-
lines.push(
|
|
1925
|
-
` const types = (${cls.name} as any).__formspec_types__ as Record<string, unknown>[];`
|
|
1926
|
-
);
|
|
1927
|
-
lines.push(` return { elements: types } as unknown as ${cls.name}FormSpec;`);
|
|
1928
|
-
lines.push(`}`);
|
|
1929
|
-
return lines.join("\n");
|
|
1930
|
-
}
|
|
1931
|
-
function getRelativeImportPath(outputDir, sourcePath) {
|
|
1932
|
-
let relativePath = path2.relative(outputDir, sourcePath);
|
|
1933
|
-
if (!relativePath.startsWith(".")) {
|
|
1934
|
-
relativePath = "./" + relativePath;
|
|
1935
|
-
}
|
|
1936
|
-
relativePath = relativePath.replace(/\\/g, "/");
|
|
1937
|
-
return relativePath + ".js";
|
|
1938
|
-
}
|
|
1939
|
-
function runCodegen(options) {
|
|
1940
|
-
const baseDir = options.baseDir ?? path2.dirname(options.output);
|
|
1941
|
-
const absoluteFiles = options.files.map((f) => path2.resolve(f));
|
|
1942
|
-
console.log(`Scanning ${String(absoluteFiles.length)} file(s) for decorated classes...`);
|
|
1943
|
-
const classes = findDecoratedClasses(absoluteFiles, baseDir);
|
|
1944
|
-
if (classes.length === 0) {
|
|
1945
|
-
console.log("No decorated classes found.");
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
console.log(`Found ${String(classes.length)} decorated class(es):`);
|
|
1949
|
-
for (const cls of classes) {
|
|
1950
|
-
const fieldCount = Object.keys(cls.typeMetadata).length;
|
|
1951
|
-
console.log(` - ${cls.name} (${String(fieldCount)} field(s))`);
|
|
1952
|
-
}
|
|
1953
|
-
const unexported = classes.filter((cls) => !cls.isExported);
|
|
1954
|
-
if (unexported.length > 0) {
|
|
1955
|
-
console.warn(
|
|
1956
|
-
`
|
|
1957
|
-
\u26A0\uFE0F Warning: The following decorated classes are not exported from their source files:`
|
|
1958
|
-
);
|
|
1959
|
-
for (const cls of unexported) {
|
|
1960
|
-
console.warn(` - ${cls.name} (${cls.sourcePath})`);
|
|
1961
|
-
}
|
|
1962
|
-
console.warn(
|
|
1963
|
-
`
|
|
1964
|
-
The generated code will fail to compile because it cannot import these classes.`
|
|
1965
|
-
);
|
|
1966
|
-
console.warn(` To fix this, add 'export' to the class declaration, for example:`);
|
|
1967
|
-
console.warn(` export class YourClassName { ... }
|
|
1968
|
-
`);
|
|
1969
|
-
}
|
|
1970
|
-
const output = generateCodegenOutput(classes, options.output, baseDir);
|
|
1971
|
-
const outputDir = path2.dirname(options.output);
|
|
1972
|
-
if (!fs.existsSync(outputDir)) {
|
|
1973
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1974
|
-
}
|
|
1975
|
-
fs.writeFileSync(options.output, output);
|
|
1976
|
-
console.log(`
|
|
1977
|
-
Generated: ${options.output}`);
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
1739
|
// src/index.ts
|
|
1981
1740
|
function buildFormSchemas(form) {
|
|
1982
1741
|
return {
|
|
@@ -1987,13 +1746,13 @@ function buildFormSchemas(form) {
|
|
|
1987
1746
|
function writeSchemas(form, options) {
|
|
1988
1747
|
const { outDir, name = "schema", indent = 2 } = options;
|
|
1989
1748
|
const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form);
|
|
1990
|
-
if (!
|
|
1991
|
-
|
|
1749
|
+
if (!fs.existsSync(outDir)) {
|
|
1750
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
1992
1751
|
}
|
|
1993
|
-
const jsonSchemaPath =
|
|
1994
|
-
const uiSchemaPath =
|
|
1995
|
-
|
|
1996
|
-
|
|
1752
|
+
const jsonSchemaPath = path2.join(outDir, `${name}-schema.json`);
|
|
1753
|
+
const uiSchemaPath = path2.join(outDir, `${name}-uischema.json`);
|
|
1754
|
+
fs.writeFileSync(jsonSchemaPath, JSON.stringify(jsonSchema, null, indent));
|
|
1755
|
+
fs.writeFileSync(uiSchemaPath, JSON.stringify(uiSchema2, null, indent));
|
|
1997
1756
|
return { jsonSchemaPath, uiSchemaPath };
|
|
1998
1757
|
}
|
|
1999
1758
|
export {
|
|
@@ -2001,13 +1760,10 @@ export {
|
|
|
2001
1760
|
categorizationSchema,
|
|
2002
1761
|
categorySchema,
|
|
2003
1762
|
controlSchema,
|
|
2004
|
-
findDecoratedClasses,
|
|
2005
|
-
generateCodegenOutput,
|
|
2006
1763
|
generateJsonSchema,
|
|
2007
1764
|
generateSchemas,
|
|
2008
1765
|
generateSchemasFromClass,
|
|
2009
1766
|
generateUiSchema,
|
|
2010
|
-
generateUiSchemaFromFields,
|
|
2011
1767
|
getSchemaExtension,
|
|
2012
1768
|
groupLayoutSchema,
|
|
2013
1769
|
horizontalLayoutSchema,
|
|
@@ -2017,7 +1773,6 @@ export {
|
|
|
2017
1773
|
ruleConditionSchema,
|
|
2018
1774
|
ruleEffectSchema,
|
|
2019
1775
|
ruleSchema,
|
|
2020
|
-
runCodegen,
|
|
2021
1776
|
schemaBasedConditionSchema,
|
|
2022
1777
|
setSchemaExtension,
|
|
2023
1778
|
uiSchemaElementSchema,
|