@featurevisor/core 2.11.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/coverage/clover.xml +684 -3
  3. package/coverage/coverage-final.json +4 -0
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +1 -1
  9. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  10. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  11. package/coverage/lcov-report/index.html +25 -10
  12. package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
  13. package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
  14. package/coverage/lcov-report/linter/index.html +161 -0
  15. package/coverage/lcov-report/linter/schema.ts.html +1471 -0
  16. package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
  17. package/coverage/lcov-report/list/index.html +1 -1
  18. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  19. package/coverage/lcov-report/parsers/index.html +1 -1
  20. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  21. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  22. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  23. package/coverage/lcov-report/tester/index.html +1 -1
  24. package/coverage/lcov.info +1471 -0
  25. package/lib/builder/buildDatafile.js +15 -1
  26. package/lib/builder/buildDatafile.js.map +1 -1
  27. package/lib/config/projectConfig.d.ts +2 -0
  28. package/lib/config/projectConfig.js +3 -1
  29. package/lib/config/projectConfig.js.map +1 -1
  30. package/lib/datasource/datasource.d.ts +6 -1
  31. package/lib/datasource/datasource.js +16 -0
  32. package/lib/datasource/datasource.js.map +1 -1
  33. package/lib/datasource/filesystemAdapter.js +10 -0
  34. package/lib/datasource/filesystemAdapter.js.map +1 -1
  35. package/lib/generate-code/typescript.js +280 -46
  36. package/lib/generate-code/typescript.js.map +1 -1
  37. package/lib/linter/conditionSchema.spec.d.ts +1 -0
  38. package/lib/linter/conditionSchema.spec.js +331 -0
  39. package/lib/linter/conditionSchema.spec.js.map +1 -0
  40. package/lib/linter/featureSchema.d.ts +129 -20
  41. package/lib/linter/featureSchema.js +489 -48
  42. package/lib/linter/featureSchema.js.map +1 -1
  43. package/lib/linter/featureSchema.spec.d.ts +1 -0
  44. package/lib/linter/featureSchema.spec.js +978 -0
  45. package/lib/linter/featureSchema.spec.js.map +1 -0
  46. package/lib/linter/lintProject.js +67 -1
  47. package/lib/linter/lintProject.js.map +1 -1
  48. package/lib/linter/schema.d.ts +42 -0
  49. package/lib/linter/schema.js +417 -0
  50. package/lib/linter/schema.js.map +1 -0
  51. package/lib/linter/schema.spec.d.ts +1 -0
  52. package/lib/linter/schema.spec.js +483 -0
  53. package/lib/linter/schema.spec.js.map +1 -0
  54. package/lib/linter/segmentSchema.spec.d.ts +1 -0
  55. package/lib/linter/segmentSchema.spec.js +231 -0
  56. package/lib/linter/segmentSchema.spec.js.map +1 -0
  57. package/lib/tester/testFeature.js +5 -3
  58. package/lib/tester/testFeature.js.map +1 -1
  59. package/lib/utils/git.js +3 -0
  60. package/lib/utils/git.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/builder/buildDatafile.ts +17 -1
  63. package/src/config/projectConfig.ts +3 -0
  64. package/src/datasource/datasource.ts +23 -0
  65. package/src/datasource/filesystemAdapter.ts +7 -0
  66. package/src/generate-code/typescript.ts +330 -49
  67. package/src/linter/conditionSchema.spec.ts +446 -0
  68. package/src/linter/featureSchema.spec.ts +1218 -0
  69. package/src/linter/featureSchema.ts +671 -69
  70. package/src/linter/lintProject.ts +84 -0
  71. package/src/linter/schema.spec.ts +617 -0
  72. package/src/linter/schema.ts +462 -0
  73. package/src/linter/segmentSchema.spec.ts +273 -0
  74. package/src/tester/testFeature.ts +5 -3
  75. package/src/utils/git.ts +2 -0
  76. package/lib/linter/propertySchema.d.ts +0 -5
  77. package/lib/linter/propertySchema.js +0 -43
  78. package/lib/linter/propertySchema.js.map +0 -1
  79. 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 { Attribute, PropertySchema, VariableSchema } from "@featurevisor/types";
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,34 +33,86 @@ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): stri
28
33
  }
29
34
 
30
35
  /**
31
- * Converts a PropertySchema (items or properties entry) to a TypeScript type string.
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 propertySchemaToTypeScriptType(schema: PropertySchema): string {
35
- const type = schema.type;
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 (schema.items) {
49
- return `(${propertySchemaToTypeScriptType(schema.items)})[]`;
105
+ if (resolved.items) {
106
+ return `(${schemaToTypeScriptType(resolved.items, schemasByKey, schemaTypeNames)})[]`;
50
107
  }
51
108
  return "string[]";
52
109
  case "object": {
53
- const props = schema.properties;
110
+ const props = resolved.properties;
54
111
  if (props && typeof props === "object" && Object.keys(props).length > 0) {
55
- const requiredSet = new Set(schema.required || []);
112
+ const requiredSet = new Set(resolved.required || []);
56
113
  const entries = Object.entries(props)
57
114
  .map(([k, v]) => {
58
- const propType = propertySchemaToTypeScriptType(v);
115
+ const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
59
116
  const optional = !requiredSet.has(k);
60
117
  return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
61
118
  })
@@ -69,30 +126,133 @@ 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
- ): { declarations: string[]; returnTypeName: string; genericArg: string } {
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 type = variableSchema.type;
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 props = variableSchema.properties;
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((variableSchema.required as string[]) || []);
248
+ const requiredSet = new Set(resolvedEffective?.required || []);
93
249
  const entries = Object.entries(props)
94
250
  .map(([k, v]) => {
95
- const propType = propertySchemaToTypeScriptType(v as PropertySchema);
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
+ });
96
256
  const optional = !requiredSet.has(k);
97
257
  return optional
98
258
  ? `${INDENT_NS_BODY}${k}?: ${propType};`
@@ -100,20 +260,46 @@ function generateVariableTypeDeclarations(
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
- if (variableSchema.items) {
111
- const items = variableSchema.items as PropertySchema;
112
- if (items.type === "object" && items.properties && Object.keys(items.properties).length > 0) {
113
- const requiredSet = new Set(items.required || []);
114
- const entries = Object.entries(items.properties)
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 = propertySchemaToTypeScriptType(v);
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
+ });
117
303
  const optional = !requiredSet.has(k);
118
304
  return optional
119
305
  ? `${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
- // array of primitive (e.g. string): emit item type only, use item[] in methods
134
- const itemType = propertySchemaToTypeScriptType(items);
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 primitiveType = convertFeaturevisorTypeToTypeScriptType(type);
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 { declarations, returnTypeName: typeName, genericArg: typeName };
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 variableType = variableSchema.type;
269
- const { declarations, returnTypeName, genericArg } = generateVariableTypeDeclarations(
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
- if (variableType === "json") {
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
- [`export * from "./Context";`, `export * from "./instance";`]
337
- .concat(
338
- featureNamespaces.map((featureNamespace) => {
339
- return `export * from "./${featureNamespace}";`;
340
- }),
341
- )
342
- .join("\n") + "\n";
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)}`);