@featurevisor/core 2.10.0 → 2.12.0
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/CHANGELOG.md +22 -0
- package/coverage/clover.xml +684 -3
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/index.html +25 -10
- package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
- package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
- package/coverage/lcov-report/linter/index.html +161 -0
- package/coverage/lcov-report/linter/schema.ts.html +1471 -0
- package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/json.ts.html +1 -1
- package/coverage/lcov-report/parsers/yml.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov.info +1471 -0
- package/lib/builder/buildDatafile.js +15 -1
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/config/projectConfig.d.ts +2 -0
- package/lib/config/projectConfig.js +3 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/datasource.d.ts +6 -1
- package/lib/datasource/datasource.js +16 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.js +10 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/generate-code/typescript.js +283 -49
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/conditionSchema.spec.d.ts +1 -0
- package/lib/linter/conditionSchema.spec.js +331 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +153 -17
- package/lib/linter/featureSchema.js +536 -49
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.d.ts +1 -0
- package/lib/linter/featureSchema.spec.js +978 -0
- package/lib/linter/featureSchema.spec.js.map +1 -0
- package/lib/linter/lintProject.js +67 -1
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/schema.d.ts +42 -0
- package/lib/linter/schema.js +417 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/schema.spec.d.ts +1 -0
- package/lib/linter/schema.spec.js +483 -0
- package/lib/linter/schema.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.d.ts +1 -0
- package/lib/linter/segmentSchema.spec.js +231 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -0
- package/lib/tester/testFeature.js +5 -3
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/utils/git.js +3 -0
- package/lib/utils/git.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.ts +17 -1
- package/src/config/projectConfig.ts +3 -0
- package/src/datasource/datasource.ts +23 -0
- package/src/datasource/filesystemAdapter.ts +7 -0
- package/src/generate-code/typescript.ts +333 -52
- package/src/linter/conditionSchema.spec.ts +446 -0
- package/src/linter/featureSchema.spec.ts +1218 -0
- package/src/linter/featureSchema.ts +747 -70
- package/src/linter/lintProject.ts +84 -0
- package/src/linter/schema.spec.ts +617 -0
- package/src/linter/schema.ts +462 -0
- package/src/linter/segmentSchema.spec.ts +273 -0
- package/src/tester/testFeature.ts +5 -3
- package/src/utils/git.ts +2 -0
- package/lib/linter/propertySchema.d.ts +0 -5
- package/lib/linter/propertySchema.js +0 -43
- package/lib/linter/propertySchema.js.map +0 -1
- package/src/linter/propertySchema.ts +0 -47
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
Attribute,
|
|
6
|
+
Schema,
|
|
7
|
+
VariableSchema,
|
|
8
|
+
VariableSchemaWithInline,
|
|
9
|
+
} from "@featurevisor/types";
|
|
5
10
|
import { Dependencies } from "../dependencies";
|
|
6
11
|
|
|
7
12
|
function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): string {
|
|
@@ -28,35 +33,87 @@ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): stri
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
32
|
-
* Handles nested object/array with structured items and properties.
|
|
36
|
+
* Resolve a schema to its full definition, following schema references (schema: key).
|
|
33
37
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
38
|
+
function resolveSchema(schema: Schema, schemasByKey: Record<string, Schema>): Schema {
|
|
39
|
+
if (schema.schema && schemasByKey[schema.schema]) {
|
|
40
|
+
return resolveSchema(schemasByKey[schema.schema], schemasByKey);
|
|
41
|
+
}
|
|
42
|
+
return schema;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Emit a TypeScript literal type for a primitive const value, or null if not a primitive. */
|
|
46
|
+
function constToLiteralType(constVal: unknown): string | null {
|
|
47
|
+
if (constVal === null || constVal === undefined) return null;
|
|
48
|
+
if (typeof constVal === "string") return JSON.stringify(constVal);
|
|
49
|
+
if (typeof constVal === "number") return String(constVal);
|
|
50
|
+
if (typeof constVal === "boolean") return constVal ? "true" : "false";
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Emit a TypeScript union of literal types for an enum array (primitives only), or null. */
|
|
55
|
+
function enumToUnionType(enumArr: unknown[]): string | null {
|
|
56
|
+
if (enumArr.length === 0) return null;
|
|
57
|
+
const literals: string[] = [];
|
|
58
|
+
for (const v of enumArr) {
|
|
59
|
+
const lit = constToLiteralType(v);
|
|
60
|
+
if (lit === null) return null;
|
|
61
|
+
literals.push(lit);
|
|
62
|
+
}
|
|
63
|
+
return literals.join(" | ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Converts a Schema (items or properties entry) to a TypeScript type string.
|
|
68
|
+
* Handles nested object/array and resolves schema references recursively.
|
|
69
|
+
* When schema has `oneOf`, emits a union of each branch type. When schema has primitive `const` or `enum`, emits a literal or union type.
|
|
70
|
+
* When schemaTypeNames is provided and schema is a reference (schema: key), returns that type name instead of inlining.
|
|
71
|
+
*/
|
|
72
|
+
function schemaToTypeScriptType(
|
|
73
|
+
schema: Schema,
|
|
74
|
+
schemasByKey: Record<string, Schema>,
|
|
75
|
+
schemaTypeNames?: Record<string, string>,
|
|
76
|
+
): string {
|
|
77
|
+
if (schema?.schema && schemaTypeNames?.[schema.schema]) {
|
|
78
|
+
return schemaTypeNames[schema.schema];
|
|
79
|
+
}
|
|
80
|
+
const resolved = resolveSchema(schema, schemasByKey);
|
|
81
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
82
|
+
const parts = resolved.oneOf.map((branch) =>
|
|
83
|
+
schemaToTypeScriptType(branch as Schema, schemasByKey, schemaTypeNames),
|
|
84
|
+
);
|
|
85
|
+
return parts.join(" | ");
|
|
86
|
+
}
|
|
87
|
+
const literalFromConst = resolved.const !== undefined ? constToLiteralType(resolved.const) : null;
|
|
88
|
+
const unionFromEnum =
|
|
89
|
+
resolved.enum && Array.isArray(resolved.enum) && resolved.enum.length > 0
|
|
90
|
+
? enumToUnionType(resolved.enum)
|
|
91
|
+
: null;
|
|
92
|
+
const type = resolved.type;
|
|
36
93
|
if (!type) {
|
|
37
|
-
return "unknown";
|
|
94
|
+
return literalFromConst ?? unionFromEnum ?? "unknown";
|
|
38
95
|
}
|
|
39
96
|
switch (type) {
|
|
40
97
|
case "boolean":
|
|
41
|
-
return "boolean";
|
|
98
|
+
return literalFromConst ?? unionFromEnum ?? "boolean";
|
|
42
99
|
case "string":
|
|
43
|
-
return "string";
|
|
100
|
+
return literalFromConst ?? unionFromEnum ?? "string";
|
|
44
101
|
case "integer":
|
|
45
102
|
case "double":
|
|
46
|
-
return "number";
|
|
103
|
+
return literalFromConst ?? unionFromEnum ?? "number";
|
|
47
104
|
case "array":
|
|
48
|
-
if (
|
|
49
|
-
return `(${
|
|
105
|
+
if (resolved.items) {
|
|
106
|
+
return `(${schemaToTypeScriptType(resolved.items, schemasByKey, schemaTypeNames)})[]`;
|
|
50
107
|
}
|
|
51
108
|
return "string[]";
|
|
52
109
|
case "object": {
|
|
53
|
-
const props =
|
|
110
|
+
const props = resolved.properties;
|
|
54
111
|
if (props && typeof props === "object" && Object.keys(props).length > 0) {
|
|
55
|
-
const requiredSet = new Set(
|
|
112
|
+
const requiredSet = new Set(resolved.required || []);
|
|
56
113
|
const entries = Object.entries(props)
|
|
57
114
|
.map(([k, v]) => {
|
|
58
|
-
const propType =
|
|
59
|
-
const optional =
|
|
115
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
116
|
+
const optional = !requiredSet.has(k);
|
|
60
117
|
return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
|
|
61
118
|
})
|
|
62
119
|
.join("; ");
|
|
@@ -69,52 +126,181 @@ function propertySchemaToTypeScriptType(schema: PropertySchema): string {
|
|
|
69
126
|
}
|
|
70
127
|
}
|
|
71
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Resolve variable schema to the schema shape used for code gen (inline or from schema reference).
|
|
131
|
+
*/
|
|
132
|
+
function getEffectiveVariableSchema(
|
|
133
|
+
variableSchema: VariableSchema,
|
|
134
|
+
schemasByKey: Record<string, Schema>,
|
|
135
|
+
): VariableSchemaWithInline | Schema | undefined {
|
|
136
|
+
if ("schema" in variableSchema && variableSchema.schema) {
|
|
137
|
+
return schemasByKey[variableSchema.schema];
|
|
138
|
+
}
|
|
139
|
+
return variableSchema as VariableSchemaWithInline;
|
|
140
|
+
}
|
|
141
|
+
|
|
72
142
|
/**
|
|
73
143
|
* Generates TypeScript type/interface declarations and metadata for a variable.
|
|
74
144
|
* Returns declarations to emit (interface or type alias) plus the type name and generic to use in the getter.
|
|
145
|
+
* When isLiteralType is true, the getter return must be asserted so the SDK's primitive return type matches the literal.
|
|
146
|
+
* When schemaTypeNames is provided, direct schema refs and schema refs in array items use those type names and schemaTypesUsed is populated.
|
|
75
147
|
*/
|
|
76
148
|
function generateVariableTypeDeclarations(
|
|
77
149
|
variableKey: string,
|
|
78
150
|
variableSchema: VariableSchema,
|
|
79
|
-
|
|
151
|
+
schemasByKey: Record<string, Schema>,
|
|
152
|
+
schemaTypeNames?: Record<string, string>,
|
|
153
|
+
): {
|
|
154
|
+
declarations: string[];
|
|
155
|
+
returnTypeName: string;
|
|
156
|
+
genericArg: string;
|
|
157
|
+
isLiteralType?: boolean;
|
|
158
|
+
useGetVariable?: boolean;
|
|
159
|
+
schemaTypesUsed: string[];
|
|
160
|
+
} {
|
|
80
161
|
const typeName = getPascalCase(variableKey) + "Variable";
|
|
81
162
|
const itemTypeName = getPascalCase(variableKey) + "VariableItem";
|
|
82
|
-
const
|
|
163
|
+
const schemaTypesUsed: string[] = [];
|
|
164
|
+
|
|
165
|
+
const addSchemaUsed = (name: string) => {
|
|
166
|
+
if (name && !schemaTypesUsed.includes(name)) schemaTypesUsed.push(name);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Direct schema reference: emit type alias to schema type and reuse it
|
|
170
|
+
if (
|
|
171
|
+
schemaTypeNames &&
|
|
172
|
+
"schema" in variableSchema &&
|
|
173
|
+
variableSchema.schema &&
|
|
174
|
+
schemasByKey[variableSchema.schema]
|
|
175
|
+
) {
|
|
176
|
+
const schemaKey = variableSchema.schema;
|
|
177
|
+
const schemaTypeName = schemaTypeNames[schemaKey];
|
|
178
|
+
const resolvedSchema = resolveSchema(schemasByKey[schemaKey], schemasByKey);
|
|
179
|
+
const isOneOf =
|
|
180
|
+
resolvedSchema.oneOf &&
|
|
181
|
+
Array.isArray(resolvedSchema.oneOf) &&
|
|
182
|
+
resolvedSchema.oneOf.length > 0 &&
|
|
183
|
+
!resolvedSchema.type;
|
|
184
|
+
const isLiteralSchema =
|
|
185
|
+
resolvedSchema.const !== undefined ||
|
|
186
|
+
(resolvedSchema.enum && Array.isArray(resolvedSchema.enum) && resolvedSchema.enum.length > 0);
|
|
187
|
+
addSchemaUsed(schemaTypeName);
|
|
188
|
+
// getVariableArray<T> expects T to be the element type, not the full array type
|
|
189
|
+
let genericArg = schemaTypeName;
|
|
190
|
+
if (resolvedSchema.type === "array" && resolvedSchema.items) {
|
|
191
|
+
const itemsSchema = resolvedSchema.items as Schema;
|
|
192
|
+
if ("schema" in itemsSchema && itemsSchema.schema && schemaTypeNames[itemsSchema.schema]) {
|
|
193
|
+
genericArg = schemaTypeNames[itemsSchema.schema];
|
|
194
|
+
addSchemaUsed(genericArg);
|
|
195
|
+
} else {
|
|
196
|
+
genericArg = schemaToTypeScriptType(itemsSchema, schemasByKey, schemaTypeNames);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
declarations: [`${INDENT_NS}export type ${typeName} = ${schemaTypeName};`],
|
|
201
|
+
returnTypeName: schemaTypeName,
|
|
202
|
+
genericArg,
|
|
203
|
+
useGetVariable: isOneOf,
|
|
204
|
+
isLiteralType: isOneOf || isLiteralSchema,
|
|
205
|
+
schemaTypesUsed,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const effective = getEffectiveVariableSchema(variableSchema, schemasByKey);
|
|
210
|
+
const type = effective?.type;
|
|
83
211
|
const declarations: string[] = [];
|
|
84
212
|
|
|
85
213
|
if (type === "json") {
|
|
86
|
-
return { declarations: [], returnTypeName: "T", genericArg: "T" };
|
|
214
|
+
return { declarations: [], returnTypeName: "T", genericArg: "T", schemaTypesUsed };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const effectiveOneOf =
|
|
218
|
+
effective && "oneOf" in effective && Array.isArray((effective as Schema).oneOf)
|
|
219
|
+
? (effective as Schema).oneOf
|
|
220
|
+
: undefined;
|
|
221
|
+
if (effectiveOneOf && effectiveOneOf.length > 0 && !type) {
|
|
222
|
+
const unionType = effectiveOneOf
|
|
223
|
+
.map((branch) => schemaToTypeScriptType(branch as Schema, schemasByKey, schemaTypeNames))
|
|
224
|
+
.join(" | ");
|
|
225
|
+
if (schemaTypeNames) {
|
|
226
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
227
|
+
if (unionType.includes(n)) addSchemaUsed(n);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
declarations.push(`${INDENT_NS}export type ${typeName} = ${unionType};`);
|
|
231
|
+
return {
|
|
232
|
+
declarations,
|
|
233
|
+
returnTypeName: typeName,
|
|
234
|
+
genericArg: typeName,
|
|
235
|
+
isLiteralType: true,
|
|
236
|
+
useGetVariable: true,
|
|
237
|
+
schemaTypesUsed,
|
|
238
|
+
};
|
|
87
239
|
}
|
|
88
240
|
|
|
89
241
|
if (type === "object") {
|
|
90
|
-
const
|
|
242
|
+
const resolvedEffective =
|
|
243
|
+
effective && "properties" in effective
|
|
244
|
+
? (resolveSchema(effective as Schema, schemasByKey) as Schema)
|
|
245
|
+
: undefined;
|
|
246
|
+
const props = resolvedEffective?.properties;
|
|
91
247
|
if (props && typeof props === "object" && Object.keys(props).length > 0) {
|
|
92
|
-
const requiredSet = new Set(
|
|
248
|
+
const requiredSet = new Set(resolvedEffective?.required || []);
|
|
93
249
|
const entries = Object.entries(props)
|
|
94
250
|
.map(([k, v]) => {
|
|
95
|
-
const propType =
|
|
96
|
-
|
|
251
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
252
|
+
if (schemaTypeNames)
|
|
253
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
254
|
+
if (propType.includes(n)) addSchemaUsed(n);
|
|
255
|
+
});
|
|
256
|
+
const optional = !requiredSet.has(k);
|
|
97
257
|
return optional
|
|
98
258
|
? `${INDENT_NS_BODY}${k}?: ${propType};`
|
|
99
259
|
: `${INDENT_NS_BODY}${k}: ${propType};`;
|
|
100
260
|
})
|
|
101
261
|
.join("\n");
|
|
102
262
|
declarations.push(`${INDENT_NS}export interface ${typeName} {\n${entries}\n${INDENT_NS}}`);
|
|
103
|
-
return { declarations, returnTypeName: typeName, genericArg: typeName };
|
|
263
|
+
return { declarations, returnTypeName: typeName, genericArg: typeName, schemaTypesUsed };
|
|
104
264
|
}
|
|
105
265
|
declarations.push(`${INDENT_NS}export type ${typeName} = Record<string, unknown>;`);
|
|
106
|
-
return { declarations, returnTypeName: typeName, genericArg: typeName };
|
|
266
|
+
return { declarations, returnTypeName: typeName, genericArg: typeName, schemaTypesUsed };
|
|
107
267
|
}
|
|
108
268
|
|
|
109
269
|
if (type === "array") {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
270
|
+
const itemsSchema = effective && "items" in effective ? (effective.items as Schema) : undefined;
|
|
271
|
+
if (itemsSchema) {
|
|
272
|
+
const itemsRef =
|
|
273
|
+
schemaTypeNames &&
|
|
274
|
+
"schema" in itemsSchema &&
|
|
275
|
+
itemsSchema.schema &&
|
|
276
|
+
schemasByKey[itemsSchema.schema]
|
|
277
|
+
? schemaTypeNames[itemsSchema.schema]
|
|
278
|
+
: null;
|
|
279
|
+
if (itemsRef) {
|
|
280
|
+
addSchemaUsed(itemsRef);
|
|
281
|
+
declarations.push(`${INDENT_NS}export type ${itemTypeName} = ${itemsRef};`);
|
|
282
|
+
return {
|
|
283
|
+
declarations,
|
|
284
|
+
returnTypeName: `${itemTypeName}[]`,
|
|
285
|
+
genericArg: itemTypeName,
|
|
286
|
+
schemaTypesUsed,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const resolvedItems = resolveSchema(itemsSchema, schemasByKey);
|
|
290
|
+
if (
|
|
291
|
+
resolvedItems.type === "object" &&
|
|
292
|
+
resolvedItems.properties &&
|
|
293
|
+
Object.keys(resolvedItems.properties).length > 0
|
|
294
|
+
) {
|
|
295
|
+
const requiredSet = new Set(resolvedItems.required || []);
|
|
296
|
+
const entries = Object.entries(resolvedItems.properties)
|
|
115
297
|
.map(([k, v]) => {
|
|
116
|
-
const propType =
|
|
117
|
-
|
|
298
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
299
|
+
if (schemaTypeNames)
|
|
300
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
301
|
+
if (propType.includes(n)) addSchemaUsed(n);
|
|
302
|
+
});
|
|
303
|
+
const optional = !requiredSet.has(k);
|
|
118
304
|
return optional
|
|
119
305
|
? `${INDENT_NS_BODY}${k}?: ${propType};`
|
|
120
306
|
: `${INDENT_NS_BODY}${k}: ${propType};`;
|
|
@@ -123,35 +309,58 @@ function generateVariableTypeDeclarations(
|
|
|
123
309
|
declarations.push(
|
|
124
310
|
`${INDENT_NS}export interface ${itemTypeName} {\n${entries}\n${INDENT_NS}}`,
|
|
125
311
|
);
|
|
126
|
-
// getVariableArray<T> returns T[] | null, so generic is item type
|
|
127
312
|
return {
|
|
128
313
|
declarations,
|
|
129
314
|
returnTypeName: `${itemTypeName}[]`,
|
|
130
315
|
genericArg: itemTypeName,
|
|
316
|
+
schemaTypesUsed,
|
|
131
317
|
};
|
|
132
318
|
}
|
|
133
|
-
|
|
134
|
-
|
|
319
|
+
const itemType = schemaToTypeScriptType(resolvedItems, schemasByKey, schemaTypeNames);
|
|
320
|
+
if (schemaTypeNames)
|
|
321
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
322
|
+
if (itemType.includes(n)) addSchemaUsed(n);
|
|
323
|
+
});
|
|
135
324
|
declarations.push(`${INDENT_NS}export type ${itemTypeName} = ${itemType};`);
|
|
136
325
|
return {
|
|
137
326
|
declarations,
|
|
138
327
|
returnTypeName: `${itemTypeName}[]`,
|
|
139
328
|
genericArg: itemTypeName,
|
|
329
|
+
schemaTypesUsed,
|
|
140
330
|
};
|
|
141
331
|
}
|
|
142
|
-
// array without items (default string[]): emit item type only
|
|
143
332
|
declarations.push(`${INDENT_NS}export type ${itemTypeName} = string;`);
|
|
144
333
|
return {
|
|
145
334
|
declarations,
|
|
146
335
|
returnTypeName: `${itemTypeName}[]`,
|
|
147
336
|
genericArg: itemTypeName,
|
|
337
|
+
schemaTypesUsed,
|
|
148
338
|
};
|
|
149
339
|
}
|
|
150
340
|
|
|
151
|
-
// primitive: boolean, string, integer, double
|
|
152
|
-
const
|
|
341
|
+
// primitive: boolean, string, integer, double (or unknown when schema ref unresolved)
|
|
342
|
+
// When schema has primitive const or enum, emit literal or union type
|
|
343
|
+
const effectiveConst =
|
|
344
|
+
effective && "const" in effective && (effective as Schema).const !== undefined
|
|
345
|
+
? (effective as Schema).const
|
|
346
|
+
: undefined;
|
|
347
|
+
const effectiveEnum =
|
|
348
|
+
effective && "enum" in effective && Array.isArray((effective as Schema).enum)
|
|
349
|
+
? (effective as Schema).enum
|
|
350
|
+
: undefined;
|
|
351
|
+
const literalType = effectiveConst !== undefined ? constToLiteralType(effectiveConst) : null;
|
|
352
|
+
const enumUnion =
|
|
353
|
+
effectiveEnum && effectiveEnum.length > 0 ? enumToUnionType(effectiveEnum) : null;
|
|
354
|
+
const primitiveType =
|
|
355
|
+
literalType ?? enumUnion ?? (type ? convertFeaturevisorTypeToTypeScriptType(type) : "unknown");
|
|
153
356
|
declarations.push(`${INDENT_NS}export type ${typeName} = ${primitiveType};`);
|
|
154
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
declarations,
|
|
359
|
+
returnTypeName: typeName,
|
|
360
|
+
genericArg: typeName,
|
|
361
|
+
isLiteralType: literalType !== null || enumUnion !== null,
|
|
362
|
+
schemaTypesUsed,
|
|
363
|
+
};
|
|
155
364
|
}
|
|
156
365
|
|
|
157
366
|
function getPascalCase(str) {
|
|
@@ -174,6 +383,28 @@ function getRelativePath(from, to) {
|
|
|
174
383
|
return relativePath;
|
|
175
384
|
}
|
|
176
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Generates the content of Schemas.ts: one exported type per schema key, using schema refs between schemas.
|
|
388
|
+
*/
|
|
389
|
+
function generateSchemasFileContent(
|
|
390
|
+
schemaKeys: string[],
|
|
391
|
+
schemasByKey: Record<string, Schema>,
|
|
392
|
+
): string {
|
|
393
|
+
const schemaTypeNames: Record<string, string> = {};
|
|
394
|
+
for (const k of schemaKeys) {
|
|
395
|
+
schemaTypeNames[k] = getPascalCase(k) + "Schema";
|
|
396
|
+
}
|
|
397
|
+
const lines: string[] = [];
|
|
398
|
+
for (const key of schemaKeys) {
|
|
399
|
+
const schema = schemasByKey[key];
|
|
400
|
+
if (!schema) continue;
|
|
401
|
+
const name = schemaTypeNames[key];
|
|
402
|
+
const typeStr = schemaToTypeScriptType(schema, schemasByKey, schemaTypeNames);
|
|
403
|
+
lines.push(`export type ${name} = ${typeStr};`);
|
|
404
|
+
}
|
|
405
|
+
return lines.join("\n") + "\n";
|
|
406
|
+
}
|
|
407
|
+
|
|
177
408
|
// Indentation for generated namespace content (2 spaces per level)
|
|
178
409
|
const INDENT_NS = " ";
|
|
179
410
|
const INDENT_NS_BODY = " ";
|
|
@@ -246,6 +477,32 @@ ${attributeProperties}
|
|
|
246
477
|
const featureNamespaces: string[] = [];
|
|
247
478
|
const featureFiles = await datasource.listFeatures();
|
|
248
479
|
|
|
480
|
+
// Load schemas for resolving variable schema references
|
|
481
|
+
const schemaListKeys = await datasource.listSchemas();
|
|
482
|
+
const schemasByKey: Record<string, Schema> = {};
|
|
483
|
+
for (const key of schemaListKeys) {
|
|
484
|
+
try {
|
|
485
|
+
schemasByKey[key] = await datasource.readSchema(key);
|
|
486
|
+
} catch {
|
|
487
|
+
// Schema file may be invalid; skip for code gen
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const schemaKeys = Object.keys(schemasByKey);
|
|
491
|
+
const hasSchemasFile = schemaKeys.length > 0;
|
|
492
|
+
if (hasSchemasFile) {
|
|
493
|
+
const schemasContent = generateSchemasFileContent(schemaKeys, schemasByKey);
|
|
494
|
+
const schemasFilePath = path.join(outputPath, "Schemas.ts");
|
|
495
|
+
fs.writeFileSync(schemasFilePath, schemasContent);
|
|
496
|
+
console.log(
|
|
497
|
+
`Schemas type file written at: ${getRelativePath(rootDirectoryPath, schemasFilePath)}`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const schemaTypeNames: Record<string, string> = {};
|
|
502
|
+
for (const k of schemaKeys) {
|
|
503
|
+
schemaTypeNames[k] = getPascalCase(k) + "Schema";
|
|
504
|
+
}
|
|
505
|
+
|
|
249
506
|
for (const featureKey of featureFiles) {
|
|
250
507
|
const parsedFeature = await datasource.readFeature(featureKey);
|
|
251
508
|
|
|
@@ -258,6 +515,7 @@ ${attributeProperties}
|
|
|
258
515
|
|
|
259
516
|
let variableTypeDeclarations = "";
|
|
260
517
|
let variableMethods = "";
|
|
518
|
+
const featureSchemaTypesUsed = new Set<string>();
|
|
261
519
|
|
|
262
520
|
if (parsedFeature.variablesSchema) {
|
|
263
521
|
const variableKeys = Object.keys(parsedFeature.variablesSchema);
|
|
@@ -265,20 +523,38 @@ ${attributeProperties}
|
|
|
265
523
|
|
|
266
524
|
for (const variableKey of variableKeys) {
|
|
267
525
|
const variableSchema = parsedFeature.variablesSchema[variableKey];
|
|
268
|
-
const
|
|
269
|
-
const
|
|
526
|
+
const effective = getEffectiveVariableSchema(variableSchema, schemasByKey);
|
|
527
|
+
const variableType = effective?.type;
|
|
528
|
+
const {
|
|
529
|
+
declarations,
|
|
530
|
+
returnTypeName,
|
|
531
|
+
genericArg,
|
|
532
|
+
isLiteralType,
|
|
533
|
+
useGetVariable,
|
|
534
|
+
schemaTypesUsed,
|
|
535
|
+
} = generateVariableTypeDeclarations(
|
|
270
536
|
variableKey,
|
|
271
537
|
variableSchema,
|
|
538
|
+
schemasByKey,
|
|
539
|
+
hasSchemasFile ? schemaTypeNames : undefined,
|
|
272
540
|
);
|
|
541
|
+
schemaTypesUsed.forEach((t) => featureSchemaTypesUsed.add(t));
|
|
273
542
|
allDeclarations.push(...declarations);
|
|
274
543
|
|
|
275
544
|
const internalMethodName = `getVariable${
|
|
276
|
-
variableType === "json" ? "JSON" : getPascalCase(variableType)
|
|
545
|
+
variableType === "json" ? "JSON" : getPascalCase(variableType ?? "string")
|
|
277
546
|
}`;
|
|
278
547
|
|
|
279
548
|
const hasGeneric =
|
|
280
549
|
variableType === "json" || variableType === "array" || variableType === "object";
|
|
281
|
-
|
|
550
|
+
const literalAssertion = isLiteralType ? ` as ${returnTypeName} | null` : "";
|
|
551
|
+
if (useGetVariable) {
|
|
552
|
+
variableMethods += `
|
|
553
|
+
|
|
554
|
+
${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
|
|
555
|
+
${INDENT_NS_BODY}return getInstance().getVariable(key, "${variableKey}", context)${literalAssertion};
|
|
556
|
+
${INDENT_NS}}`;
|
|
557
|
+
} else if (variableType === "json") {
|
|
282
558
|
variableMethods += `
|
|
283
559
|
|
|
284
560
|
${INDENT_NS}export function get${getPascalCase(variableKey)}<T = unknown>(context: Context = {}): T | null {
|
|
@@ -294,7 +570,7 @@ ${INDENT_NS}}`;
|
|
|
294
570
|
variableMethods += `
|
|
295
571
|
|
|
296
572
|
${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
|
|
297
|
-
${INDENT_NS_BODY}return getInstance().${internalMethodName}(key, "${variableKey}", context);
|
|
573
|
+
${INDENT_NS_BODY}return getInstance().${internalMethodName}(key, "${variableKey}", context)${literalAssertion};
|
|
298
574
|
${INDENT_NS}}`;
|
|
299
575
|
}
|
|
300
576
|
}
|
|
@@ -304,11 +580,15 @@ ${INDENT_NS}}`;
|
|
|
304
580
|
}
|
|
305
581
|
}
|
|
306
582
|
|
|
583
|
+
const schemasImportLine =
|
|
584
|
+
featureSchemaTypesUsed.size > 0
|
|
585
|
+
? `import type { ${[...featureSchemaTypesUsed].sort().join(", ")} } from "./Schemas";\n\n`
|
|
586
|
+
: "";
|
|
587
|
+
|
|
307
588
|
const featureContent = `
|
|
308
589
|
import { Context } from "./Context";
|
|
309
590
|
import { getInstance } from "./instance";
|
|
310
|
-
|
|
311
|
-
export namespace ${namespaceValue} {
|
|
591
|
+
${schemasImportLine}export namespace ${namespaceValue} {
|
|
312
592
|
${INDENT_NS}export const key = "${featureKey}";${variableTypeDeclarations}
|
|
313
593
|
|
|
314
594
|
${INDENT_NS}export function isEnabled(context: Context = {}) {
|
|
@@ -333,13 +613,14 @@ ${INDENT_NS}}${variableMethods}
|
|
|
333
613
|
|
|
334
614
|
// index
|
|
335
615
|
const indexContent =
|
|
336
|
-
[
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
616
|
+
[
|
|
617
|
+
`export * from "./Context";`,
|
|
618
|
+
`export * from "./instance";`,
|
|
619
|
+
...(hasSchemasFile ? [`export * from "./Schemas";`] : []),
|
|
620
|
+
...featureNamespaces.map((featureNamespace) => {
|
|
621
|
+
return `export * from "./${featureNamespace}";`;
|
|
622
|
+
}),
|
|
623
|
+
].join("\n") + "\n";
|
|
343
624
|
const indexFilePath = path.join(outputPath, "index.ts");
|
|
344
625
|
fs.writeFileSync(indexFilePath, indexContent);
|
|
345
626
|
console.log(`Index file written at: ${getRelativePath(rootDirectoryPath, indexFilePath)}`);
|