@featurevisor/core 2.8.0 → 2.10.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 (30) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +2 -2
  3. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  4. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  7. package/coverage/lcov-report/builder/index.html +1 -1
  8. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  9. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  10. package/coverage/lcov-report/index.html +1 -1
  11. package/coverage/lcov-report/list/index.html +1 -1
  12. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  13. package/coverage/lcov-report/parsers/index.html +1 -1
  14. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  15. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  16. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  17. package/coverage/lcov-report/tester/index.html +1 -1
  18. package/lib/generate-code/typescript.js +150 -16
  19. package/lib/generate-code/typescript.js.map +1 -1
  20. package/lib/linter/featureSchema.d.ts +114 -100
  21. package/lib/linter/featureSchema.js +222 -80
  22. package/lib/linter/featureSchema.js.map +1 -1
  23. package/lib/linter/propertySchema.d.ts +5 -0
  24. package/lib/linter/propertySchema.js +43 -0
  25. package/lib/linter/propertySchema.js.map +1 -0
  26. package/package.json +6 -10
  27. package/src/generate-code/typescript.ts +168 -18
  28. package/src/linter/featureSchema.ts +282 -95
  29. package/src/linter/propertySchema.ts +47 -0
  30. package/tsconfig.cjs.json +2 -1
@@ -1,10 +1,10 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
 
4
- import type { Attribute } from "@featurevisor/types";
4
+ import type { Attribute, PropertySchema, VariableSchema } from "@featurevisor/types";
5
5
  import { Dependencies } from "../dependencies";
6
6
 
7
- function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string) {
7
+ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): string {
8
8
  switch (featurevisorType) {
9
9
  case "boolean":
10
10
  return "boolean";
@@ -19,14 +19,141 @@ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string) {
19
19
  case "array":
20
20
  return "string[]";
21
21
  case "object":
22
- return "any"; // @NOTE: do a flat dictionary
22
+ return "Record<string, unknown>";
23
23
  case "json":
24
- return "any";
24
+ return "unknown";
25
25
  default:
26
26
  throw new Error(`Unknown type: ${featurevisorType}`);
27
27
  }
28
28
  }
29
29
 
30
+ /**
31
+ * Converts a PropertySchema (items or properties entry) to a TypeScript type string.
32
+ * Handles nested object/array with structured items and properties.
33
+ */
34
+ function propertySchemaToTypeScriptType(schema: PropertySchema): string {
35
+ const type = schema.type;
36
+ if (!type) {
37
+ return "unknown";
38
+ }
39
+ switch (type) {
40
+ case "boolean":
41
+ return "boolean";
42
+ case "string":
43
+ return "string";
44
+ case "integer":
45
+ case "double":
46
+ return "number";
47
+ case "array":
48
+ if (schema.items) {
49
+ return `(${propertySchemaToTypeScriptType(schema.items)})[]`;
50
+ }
51
+ return "string[]";
52
+ case "object": {
53
+ const props = schema.properties;
54
+ if (props && typeof props === "object" && Object.keys(props).length > 0) {
55
+ const requiredSet = new Set(schema.required || []);
56
+ const entries = Object.entries(props)
57
+ .map(([k, v]) => {
58
+ const propType = propertySchemaToTypeScriptType(v);
59
+ const optional = requiredSet.size > 0 && !requiredSet.has(k);
60
+ return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
61
+ })
62
+ .join("; ");
63
+ return `{ ${entries} }`;
64
+ }
65
+ return "Record<string, unknown>";
66
+ }
67
+ default:
68
+ return "unknown";
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Generates TypeScript type/interface declarations and metadata for a variable.
74
+ * Returns declarations to emit (interface or type alias) plus the type name and generic to use in the getter.
75
+ */
76
+ function generateVariableTypeDeclarations(
77
+ variableKey: string,
78
+ variableSchema: VariableSchema,
79
+ ): { declarations: string[]; returnTypeName: string; genericArg: string } {
80
+ const typeName = getPascalCase(variableKey) + "Variable";
81
+ const itemTypeName = getPascalCase(variableKey) + "VariableItem";
82
+ const type = variableSchema.type;
83
+ const declarations: string[] = [];
84
+
85
+ if (type === "json") {
86
+ return { declarations: [], returnTypeName: "T", genericArg: "T" };
87
+ }
88
+
89
+ if (type === "object") {
90
+ const props = variableSchema.properties;
91
+ if (props && typeof props === "object" && Object.keys(props).length > 0) {
92
+ const requiredSet = new Set((variableSchema.required as string[]) || []);
93
+ const entries = Object.entries(props)
94
+ .map(([k, v]) => {
95
+ const propType = propertySchemaToTypeScriptType(v as PropertySchema);
96
+ const optional = requiredSet.size > 0 && !requiredSet.has(k);
97
+ return optional
98
+ ? `${INDENT_NS_BODY}${k}?: ${propType};`
99
+ : `${INDENT_NS_BODY}${k}: ${propType};`;
100
+ })
101
+ .join("\n");
102
+ declarations.push(`${INDENT_NS}export interface ${typeName} {\n${entries}\n${INDENT_NS}}`);
103
+ return { declarations, returnTypeName: typeName, genericArg: typeName };
104
+ }
105
+ declarations.push(`${INDENT_NS}export type ${typeName} = Record<string, unknown>;`);
106
+ return { declarations, returnTypeName: typeName, genericArg: typeName };
107
+ }
108
+
109
+ 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)
115
+ .map(([k, v]) => {
116
+ const propType = propertySchemaToTypeScriptType(v);
117
+ const optional = requiredSet.size > 0 && !requiredSet.has(k);
118
+ return optional
119
+ ? `${INDENT_NS_BODY}${k}?: ${propType};`
120
+ : `${INDENT_NS_BODY}${k}: ${propType};`;
121
+ })
122
+ .join("\n");
123
+ declarations.push(
124
+ `${INDENT_NS}export interface ${itemTypeName} {\n${entries}\n${INDENT_NS}}`,
125
+ );
126
+ // getVariableArray<T> returns T[] | null, so generic is item type
127
+ return {
128
+ declarations,
129
+ returnTypeName: `${itemTypeName}[]`,
130
+ genericArg: itemTypeName,
131
+ };
132
+ }
133
+ // array of primitive (e.g. string): emit item type only, use item[] in methods
134
+ const itemType = propertySchemaToTypeScriptType(items);
135
+ declarations.push(`${INDENT_NS}export type ${itemTypeName} = ${itemType};`);
136
+ return {
137
+ declarations,
138
+ returnTypeName: `${itemTypeName}[]`,
139
+ genericArg: itemTypeName,
140
+ };
141
+ }
142
+ // array without items (default string[]): emit item type only
143
+ declarations.push(`${INDENT_NS}export type ${itemTypeName} = string;`);
144
+ return {
145
+ declarations,
146
+ returnTypeName: `${itemTypeName}[]`,
147
+ genericArg: itemTypeName,
148
+ };
149
+ }
150
+
151
+ // primitive: boolean, string, integer, double
152
+ const primitiveType = convertFeaturevisorTypeToTypeScriptType(type);
153
+ declarations.push(`${INDENT_NS}export type ${typeName} = ${primitiveType};`);
154
+ return { declarations, returnTypeName: typeName, genericArg: typeName };
155
+ }
156
+
30
157
  function getPascalCase(str) {
31
158
  // Remove special characters and split the string into an array of words
32
159
  const words = str.replace(/[^a-zA-Z0-9]/g, " ").split(" ");
@@ -47,6 +174,10 @@ function getRelativePath(from, to) {
47
174
  return relativePath;
48
175
  }
49
176
 
177
+ // Indentation for generated namespace content (2 spaces per level)
178
+ const INDENT_NS = " ";
179
+ const INDENT_NS_BODY = " ";
180
+
50
181
  const instanceSnippet = `
51
182
  import { FeaturevisorInstance } from "@featurevisor/sdk";
52
183
 
@@ -125,33 +256,52 @@ ${attributeProperties}
125
256
  const namespaceValue = getPascalCase(featureKey) + "Feature";
126
257
  featureNamespaces.push(namespaceValue);
127
258
 
259
+ let variableTypeDeclarations = "";
128
260
  let variableMethods = "";
129
261
 
130
262
  if (parsedFeature.variablesSchema) {
131
263
  const variableKeys = Object.keys(parsedFeature.variablesSchema);
264
+ const allDeclarations: string[] = [];
132
265
 
133
266
  for (const variableKey of variableKeys) {
134
267
  const variableSchema = parsedFeature.variablesSchema[variableKey];
135
268
  const variableType = variableSchema.type;
269
+ const { declarations, returnTypeName, genericArg } = generateVariableTypeDeclarations(
270
+ variableKey,
271
+ variableSchema,
272
+ );
273
+ allDeclarations.push(...declarations);
136
274
 
137
275
  const internalMethodName = `getVariable${
138
276
  variableType === "json" ? "JSON" : getPascalCase(variableType)
139
277
  }`;
140
278
 
141
- if (variableType === "json" || variableType === "object") {
279
+ const hasGeneric =
280
+ variableType === "json" || variableType === "array" || variableType === "object";
281
+ if (variableType === "json") {
282
+ variableMethods += `
283
+
284
+ ${INDENT_NS}export function get${getPascalCase(variableKey)}<T = unknown>(context: Context = {}): T | null {
285
+ ${INDENT_NS_BODY}return getInstance().${internalMethodName}<T>(key, "${variableKey}", context);
286
+ ${INDENT_NS}}`;
287
+ } else if (hasGeneric) {
142
288
  variableMethods += `
143
289
 
144
- export function get${getPascalCase(variableKey)}<T>(context: Context = {}) {
145
- return getInstance().${internalMethodName}<T>(key, "${variableKey}", context);
146
- }`;
290
+ ${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
291
+ ${INDENT_NS_BODY}return getInstance().${internalMethodName}<${genericArg}>(key, "${variableKey}", context);
292
+ ${INDENT_NS}}`;
147
293
  } else {
148
294
  variableMethods += `
149
295
 
150
- export function get${getPascalCase(variableKey)}(context: Context = {}) {
151
- return getInstance().${internalMethodName}(key, "${variableKey}", context);
152
- }`;
296
+ ${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
297
+ ${INDENT_NS_BODY}return getInstance().${internalMethodName}(key, "${variableKey}", context);
298
+ ${INDENT_NS}}`;
153
299
  }
154
300
  }
301
+
302
+ if (allDeclarations.length > 0) {
303
+ variableTypeDeclarations = "\n\n" + allDeclarations.join("\n\n");
304
+ }
155
305
  }
156
306
 
157
307
  const featureContent = `
@@ -159,15 +309,15 @@ import { Context } from "./Context";
159
309
  import { getInstance } from "./instance";
160
310
 
161
311
  export namespace ${namespaceValue} {
162
- export const key = "${featureKey}";
312
+ ${INDENT_NS}export const key = "${featureKey}";${variableTypeDeclarations}
163
313
 
164
- export function isEnabled(context: Context = {}) {
165
- return getInstance().isEnabled(key, context);
166
- }
314
+ ${INDENT_NS}export function isEnabled(context: Context = {}) {
315
+ ${INDENT_NS_BODY}return getInstance().isEnabled(key, context);
316
+ ${INDENT_NS}}
167
317
 
168
- export function getVariation(context: Context = {}) {
169
- return getInstance().getVariation(key, context);
170
- }${variableMethods}
318
+ ${INDENT_NS}export function getVariation(context: Context = {}) {
319
+ ${INDENT_NS_BODY}return getInstance().getVariation(key, context);
320
+ ${INDENT_NS}}${variableMethods}
171
321
  }
172
322
  `.trimStart();
173
323