@featurevisor/core 2.16.0 → 2.18.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 +798 -764
- package/coverage/coverage-final.json +5 -5
- 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/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +18 -18
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/index.ts.html +1 -1
- package/coverage/lcov-report/config/projectConfig.ts.html +1 -1
- package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
- package/coverage/lcov-report/datasource/datasource.ts.html +1 -1
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +1 -1
- package/coverage/lcov-report/datasource/index.html +1 -1
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +18 -18
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
- package/coverage/lcov-report/linter/featureSchema.ts.html +365 -188
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +34 -34
- package/coverage/lcov-report/linter/lintProject.ts.html +1 -1
- package/coverage/lcov-report/linter/mutationNotation.ts.html +99 -66
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +170 -80
- package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/testSchema.ts.html +1 -1
- 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/index.ts.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/cliFormat.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-report/utils/git.ts.html +1 -1
- package/coverage/lcov-report/utils/index.html +1 -1
- package/coverage/lcov.info +1653 -1567
- package/json-schema/attribute.json +25 -9
- package/json-schema/feature.json +319 -238
- package/json-schema/segment.json +76 -45
- package/lib/generate-code/typescript.js +118 -50
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +9 -0
- package/lib/linter/featureSchema.js +38 -8
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +93 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/mutationNotation.js +21 -11
- package/lib/linter/mutationNotation.js.map +1 -1
- package/lib/linter/mutationNotation.spec.js +18 -0
- package/lib/linter/mutationNotation.spec.js.map +1 -1
- package/lib/linter/schema.d.ts +1 -0
- package/lib/linter/schema.js +13 -0
- package/lib/linter/schema.js.map +1 -1
- package/lib/linter/schema.spec.js +51 -0
- package/lib/linter/schema.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/generate-code/typescript.ts +150 -52
- package/src/linter/featureSchema.spec.ts +118 -0
- package/src/linter/featureSchema.ts +65 -6
- package/src/linter/mutationNotation.spec.ts +23 -0
- package/src/linter/mutationNotation.ts +18 -7
- package/src/linter/schema.spec.ts +72 -0
- package/src/linter/schema.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featurevisor/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "Core package of Featurevisor for Node.js usage",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
},
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@featurevisor/sdk": "2.
|
|
44
|
-
"@featurevisor/site": "2.
|
|
45
|
-
"@featurevisor/types": "2.
|
|
43
|
+
"@featurevisor/sdk": "2.18.0",
|
|
44
|
+
"@featurevisor/site": "2.18.0",
|
|
45
|
+
"@featurevisor/types": "2.18.0",
|
|
46
46
|
"axios": "^1.3.4",
|
|
47
47
|
"tar": "^7.5.7",
|
|
48
48
|
"yaml": "^1.10.2",
|
|
49
49
|
"yargs": "^17.7.2",
|
|
50
50
|
"zod": "^3.22.4"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "469800809b026bb42e1bc7298649d6e49e47992d"
|
|
53
53
|
}
|
|
@@ -39,6 +39,80 @@ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): stri
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function shouldWrapArrayItemType(typeName: string): boolean {
|
|
43
|
+
const trimmed = typeName.trim();
|
|
44
|
+
return trimmed.includes("|") || trimmed.includes("&");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatObjectKey(key: string): string {
|
|
48
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildObjectTypeFromSchema(
|
|
52
|
+
schema: Schema,
|
|
53
|
+
schemasByKey: Record<string, Schema>,
|
|
54
|
+
schemaTypeNames?: Record<string, string>,
|
|
55
|
+
): string {
|
|
56
|
+
const props = schema.properties;
|
|
57
|
+
const additional = schema.additionalProperties;
|
|
58
|
+
const hasProps = props && typeof props === "object" && Object.keys(props).length > 0;
|
|
59
|
+
const hasAdditional = additional && typeof additional === "object";
|
|
60
|
+
|
|
61
|
+
if (hasProps && hasAdditional) {
|
|
62
|
+
const requiredSet = new Set(schema.required || []);
|
|
63
|
+
const propEntries = Object.entries(props as Record<string, Schema>).map(([k, v]) => {
|
|
64
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
65
|
+
const optional = !requiredSet.has(k);
|
|
66
|
+
return optional
|
|
67
|
+
? `${formatObjectKey(k)}?: ${propType}`
|
|
68
|
+
: `${formatObjectKey(k)}: ${propType}`;
|
|
69
|
+
});
|
|
70
|
+
const propUnion = Object.entries(props as Record<string, Schema>)
|
|
71
|
+
.map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
|
|
72
|
+
.join(" | ");
|
|
73
|
+
const additionalType = schemaToTypeScriptType(
|
|
74
|
+
additional as Schema,
|
|
75
|
+
schemasByKey,
|
|
76
|
+
schemaTypeNames,
|
|
77
|
+
);
|
|
78
|
+
const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
|
|
79
|
+
return `{ ${propEntries.join("; ")}; [key: string]: ${indexType} }`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (hasProps) {
|
|
83
|
+
const requiredSet = new Set(schema.required || []);
|
|
84
|
+
const entries = Object.entries(props as Record<string, Schema>)
|
|
85
|
+
.map(([k, v]) => {
|
|
86
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
87
|
+
const optional = !requiredSet.has(k);
|
|
88
|
+
return optional
|
|
89
|
+
? `${formatObjectKey(k)}?: ${propType}`
|
|
90
|
+
: `${formatObjectKey(k)}: ${propType}`;
|
|
91
|
+
})
|
|
92
|
+
.join("; ");
|
|
93
|
+
return `{ ${entries} }`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasAdditional) {
|
|
97
|
+
const additionalType = schemaToTypeScriptType(
|
|
98
|
+
additional as Schema,
|
|
99
|
+
schemasByKey,
|
|
100
|
+
schemaTypeNames,
|
|
101
|
+
);
|
|
102
|
+
return `Record<string, ${additionalType}>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return "Record<string, unknown>";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatTypeImport(typeNames: string[], fromPath: string): string {
|
|
109
|
+
if (typeNames.length === 0) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `import type {\n${typeNames.map((name) => ` ${name},`).join("\n")}\n} from "${fromPath}";\n\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
42
116
|
/**
|
|
43
117
|
* Resolve a schema to its full definition, following schema references (schema: key).
|
|
44
118
|
*/
|
|
@@ -110,23 +184,12 @@ function schemaToTypeScriptType(
|
|
|
110
184
|
return literalFromConst ?? unionFromEnum ?? "number";
|
|
111
185
|
case "array":
|
|
112
186
|
if (resolved.items) {
|
|
113
|
-
|
|
187
|
+
const itemType = schemaToTypeScriptType(resolved.items, schemasByKey, schemaTypeNames);
|
|
188
|
+
return shouldWrapArrayItemType(itemType) ? `(${itemType})[]` : `${itemType}[]`;
|
|
114
189
|
}
|
|
115
190
|
return "string[]";
|
|
116
191
|
case "object": {
|
|
117
|
-
|
|
118
|
-
if (props && typeof props === "object" && Object.keys(props).length > 0) {
|
|
119
|
-
const requiredSet = new Set(resolved.required || []);
|
|
120
|
-
const entries = Object.entries(props)
|
|
121
|
-
.map(([k, v]) => {
|
|
122
|
-
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
123
|
-
const optional = !requiredSet.has(k);
|
|
124
|
-
return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
|
|
125
|
-
})
|
|
126
|
-
.join("; ");
|
|
127
|
-
return `{ ${entries} }`;
|
|
128
|
-
}
|
|
129
|
-
return "Record<string, unknown>";
|
|
192
|
+
return buildObjectTypeFromSchema(resolved, schemasByKey, schemaTypeNames);
|
|
130
193
|
}
|
|
131
194
|
default:
|
|
132
195
|
return "unknown";
|
|
@@ -247,13 +310,17 @@ function generateVariableTypeDeclarations(
|
|
|
247
310
|
|
|
248
311
|
if (type === "object") {
|
|
249
312
|
const resolvedEffective =
|
|
250
|
-
effective && "properties" in effective
|
|
313
|
+
effective && ("properties" in effective || "additionalProperties" in effective)
|
|
251
314
|
? (resolveSchema(effective as Schema, schemasByKey) as Schema)
|
|
252
315
|
: undefined;
|
|
253
316
|
const props = resolvedEffective?.properties;
|
|
254
|
-
|
|
317
|
+
const additional = resolvedEffective?.additionalProperties;
|
|
318
|
+
if (
|
|
319
|
+
(props && typeof props === "object" && Object.keys(props).length > 0) ||
|
|
320
|
+
(additional && typeof additional === "object")
|
|
321
|
+
) {
|
|
255
322
|
const requiredSet = new Set(resolvedEffective?.required || []);
|
|
256
|
-
const entries = Object.entries(props)
|
|
323
|
+
const entries = Object.entries(props ?? {})
|
|
257
324
|
.map(([k, v]) => {
|
|
258
325
|
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
259
326
|
if (schemaTypeNames)
|
|
@@ -262,11 +329,29 @@ function generateVariableTypeDeclarations(
|
|
|
262
329
|
});
|
|
263
330
|
const optional = !requiredSet.has(k);
|
|
264
331
|
return optional
|
|
265
|
-
? `${INDENT_NS_BODY}${k}?: ${propType};`
|
|
266
|
-
: `${INDENT_NS_BODY}${k}: ${propType};`;
|
|
332
|
+
? `${INDENT_NS_BODY}${formatObjectKey(k)}?: ${propType};`
|
|
333
|
+
: `${INDENT_NS_BODY}${formatObjectKey(k)}: ${propType};`;
|
|
267
334
|
})
|
|
268
|
-
.
|
|
269
|
-
|
|
335
|
+
.filter(Boolean);
|
|
336
|
+
if (additional && typeof additional === "object") {
|
|
337
|
+
const additionalType = schemaToTypeScriptType(
|
|
338
|
+
additional as Schema,
|
|
339
|
+
schemasByKey,
|
|
340
|
+
schemaTypeNames,
|
|
341
|
+
);
|
|
342
|
+
if (schemaTypeNames)
|
|
343
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
344
|
+
if (additionalType.includes(n)) addSchemaUsed(n);
|
|
345
|
+
});
|
|
346
|
+
const propUnion = Object.entries(props ?? {})
|
|
347
|
+
.map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
|
|
348
|
+
.join(" | ");
|
|
349
|
+
const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
|
|
350
|
+
entries.push(`${INDENT_NS_BODY}[key: string]: ${indexType};`);
|
|
351
|
+
}
|
|
352
|
+
declarations.push(
|
|
353
|
+
`${INDENT_NS}export interface ${typeName} {\n${entries.join("\n")}\n${INDENT_NS}}`,
|
|
354
|
+
);
|
|
270
355
|
return { declarations, returnTypeName: typeName, genericArg: typeName, schemaTypesUsed };
|
|
271
356
|
}
|
|
272
357
|
declarations.push(`${INDENT_NS}export type ${typeName} = Record<string, unknown>;`);
|
|
@@ -534,7 +619,7 @@ export async function generateTypeScriptCodeForProject(
|
|
|
534
619
|
})
|
|
535
620
|
.join("\n");
|
|
536
621
|
const contextContent = `
|
|
537
|
-
import type { AttributeKey, AttributeValue } from "@featurevisor/
|
|
622
|
+
import type { AttributeKey, AttributeValue } from "@featurevisor/sdk";
|
|
538
623
|
|
|
539
624
|
export interface Context {
|
|
540
625
|
${attributeProperties}
|
|
@@ -542,7 +627,7 @@ ${attributeProperties}
|
|
|
542
627
|
}
|
|
543
628
|
`.trimStart();
|
|
544
629
|
|
|
545
|
-
const contextTypeFilePath = path.join(outputPath, "
|
|
630
|
+
const contextTypeFilePath = path.join(outputPath, "context.ts");
|
|
546
631
|
fs.writeFileSync(contextTypeFilePath, contextContent);
|
|
547
632
|
console.log(
|
|
548
633
|
`Context type file written at: ${getRelativePath(rootDirectoryPath, contextTypeFilePath)}`,
|
|
@@ -565,7 +650,7 @@ ${attributeProperties}
|
|
|
565
650
|
const hasSchemasFile = schemaKeys.length > 0;
|
|
566
651
|
if (hasSchemasFile) {
|
|
567
652
|
const schemasContent = generateSchemasFileContent(schemaKeys, schemasByKey);
|
|
568
|
-
const schemasFilePath = path.join(outputPath, "
|
|
653
|
+
const schemasFilePath = path.join(outputPath, "schemas.ts");
|
|
569
654
|
fs.writeFileSync(schemasFilePath, schemasContent);
|
|
570
655
|
console.log(
|
|
571
656
|
`Schemas type file written at: ${getRelativePath(rootDirectoryPath, schemasFilePath)}`,
|
|
@@ -674,13 +759,10 @@ ${INDENT_NS}}`;
|
|
|
674
759
|
}
|
|
675
760
|
}
|
|
676
761
|
|
|
677
|
-
const schemasImportLine =
|
|
678
|
-
featureSchemaTypesUsed.size > 0
|
|
679
|
-
? `import type { ${[...featureSchemaTypesUsed].sort().join(", ")} } from "./Schemas";\n\n`
|
|
680
|
-
: "";
|
|
762
|
+
const schemasImportLine = formatTypeImport([...featureSchemaTypesUsed].sort(), "./schemas");
|
|
681
763
|
|
|
682
764
|
const featureContent = `
|
|
683
|
-
import { Context } from "./
|
|
765
|
+
import { Context } from "./context";
|
|
684
766
|
import { getInstance } from "./instance";
|
|
685
767
|
${schemasImportLine}export namespace ${namespaceValue} {
|
|
686
768
|
${INDENT_NS}export const key = "${featureKey}";${variableTypeDeclarations}
|
|
@@ -724,18 +806,22 @@ ${INDENT_NS}}${variableMethods}
|
|
|
724
806
|
hasSchemasFile ? schemaTypeNames : undefined,
|
|
725
807
|
);
|
|
726
808
|
schemaTypesUsed.forEach((name) => featuresTypeSchemasUsed.add(name));
|
|
727
|
-
featureLines.push(`${INDENT_NS_BODY}${
|
|
809
|
+
featureLines.push(`${INDENT_NS_BODY}${formatObjectKey(variableKey)}: ${typeName};`);
|
|
728
810
|
}
|
|
729
811
|
}
|
|
730
812
|
|
|
731
|
-
|
|
813
|
+
if (featureLines.length === 0) {
|
|
814
|
+
return `${INDENT_NS}${formatObjectKey(featureKey)}: null;`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return `${INDENT_NS}${formatObjectKey(featureKey)}: {\n${featureLines.join("\n")}\n${INDENT_NS}};`;
|
|
732
818
|
})
|
|
733
819
|
.join("\n");
|
|
734
820
|
|
|
735
|
-
const featuresSchemasImportLine =
|
|
736
|
-
featuresTypeSchemasUsed.
|
|
737
|
-
|
|
738
|
-
|
|
821
|
+
const featuresSchemasImportLine = formatTypeImport(
|
|
822
|
+
[...featuresTypeSchemasUsed].sort(),
|
|
823
|
+
"./schemas",
|
|
824
|
+
);
|
|
739
825
|
|
|
740
826
|
const featuresFileContent = `
|
|
741
827
|
${featuresSchemasImportLine}export type Features = {
|
|
@@ -743,24 +829,33 @@ ${featureTypeEntries}
|
|
|
743
829
|
};
|
|
744
830
|
|
|
745
831
|
export type FeatureKey = keyof Features;
|
|
746
|
-
export type VariableKey<F extends FeatureKey> =
|
|
747
|
-
|
|
748
|
-
|
|
832
|
+
export type VariableKey<F extends FeatureKey> = Features[F] extends Record<string, unknown>
|
|
833
|
+
? Extract<Exclude<keyof Features[F], "variation">, string>
|
|
834
|
+
: never;
|
|
835
|
+
export type VariableType<F extends FeatureKey, V extends VariableKey<F>> = Features[F] extends Record<string, unknown>
|
|
836
|
+
? Features[F][V]
|
|
837
|
+
: never;
|
|
838
|
+
export type Variation<F extends FeatureKey> = Features[F] extends { variation: infer V }
|
|
839
|
+
? V
|
|
840
|
+
: never;
|
|
749
841
|
`.trimStart();
|
|
750
|
-
const featuresFilePath = path.join(outputPath, "
|
|
842
|
+
const featuresFilePath = path.join(outputPath, "features.ts");
|
|
751
843
|
fs.writeFileSync(featuresFilePath, featuresFileContent);
|
|
752
844
|
console.log(`Features file written at: ${getRelativePath(rootDirectoryPath, featuresFilePath)}`);
|
|
753
845
|
|
|
754
846
|
const functionsFileContent = `
|
|
755
|
-
import { FeatureKey, Variation, VariableKey, VariableType } from "./
|
|
756
|
-
import { Context } from "./
|
|
847
|
+
import { FeatureKey, Variation, VariableKey, VariableType } from "./features";
|
|
848
|
+
import { Context } from "./context";
|
|
757
849
|
import { getInstance } from "./instance";
|
|
758
850
|
|
|
759
851
|
export function isEnabled(featureKey: FeatureKey, context: Context = {}): boolean {
|
|
760
852
|
return getInstance().isEnabled(featureKey, context);
|
|
761
853
|
}
|
|
762
854
|
|
|
763
|
-
export function getVariation<F extends FeatureKey>(
|
|
855
|
+
export function getVariation<F extends FeatureKey>(
|
|
856
|
+
featureKey: F,
|
|
857
|
+
context: Context = {},
|
|
858
|
+
): Variation<F> | null {
|
|
764
859
|
return getInstance().getVariation(featureKey, context) as Variation<F> | null;
|
|
765
860
|
}
|
|
766
861
|
|
|
@@ -772,7 +867,7 @@ export function getVariable<F extends FeatureKey, V extends VariableKey<F>>(
|
|
|
772
867
|
return getInstance().getVariable(featureKey, variableKey, context) as VariableType<F, V> | null;
|
|
773
868
|
}
|
|
774
869
|
`.trimStart();
|
|
775
|
-
const functionsFilePath = path.join(outputPath, "
|
|
870
|
+
const functionsFilePath = path.join(outputPath, "functions.ts");
|
|
776
871
|
fs.writeFileSync(functionsFilePath, functionsFileContent);
|
|
777
872
|
console.log(
|
|
778
873
|
`Functions file written at: ${getRelativePath(rootDirectoryPath, functionsFilePath)}`,
|
|
@@ -786,14 +881,17 @@ import {
|
|
|
786
881
|
useVariable as useVariableOriginal,
|
|
787
882
|
} from "@featurevisor/react";
|
|
788
883
|
|
|
789
|
-
import { FeatureKey, Variation, VariableKey, VariableType } from "./
|
|
790
|
-
import { Context } from "./
|
|
884
|
+
import { FeatureKey, Variation, VariableKey, VariableType } from "./features";
|
|
885
|
+
import { Context } from "./context";
|
|
791
886
|
|
|
792
887
|
export function useFlag(featureKey: FeatureKey, context: Context = {}): boolean {
|
|
793
888
|
return useFlagOriginal(featureKey, context);
|
|
794
889
|
}
|
|
795
890
|
|
|
796
|
-
export function useVariation<F extends FeatureKey>(
|
|
891
|
+
export function useVariation<F extends FeatureKey>(
|
|
892
|
+
featureKey: F,
|
|
893
|
+
context: Context = {},
|
|
894
|
+
): Variation<F> | null {
|
|
797
895
|
return useVariationOriginal(featureKey, context) as Variation<F> | null;
|
|
798
896
|
}
|
|
799
897
|
|
|
@@ -805,7 +903,7 @@ export function useVariable<F extends FeatureKey, V extends VariableKey<F>>(
|
|
|
805
903
|
return useVariableOriginal(featureKey, variableKey, context) as VariableType<F, V> | null;
|
|
806
904
|
}
|
|
807
905
|
`.trimStart();
|
|
808
|
-
const reactFilePath = path.join(outputPath, "
|
|
906
|
+
const reactFilePath = path.join(outputPath, "react.ts");
|
|
809
907
|
fs.writeFileSync(reactFilePath, reactFileContent);
|
|
810
908
|
console.log(`React file written at: ${getRelativePath(rootDirectoryPath, reactFilePath)}`);
|
|
811
909
|
}
|
|
@@ -813,12 +911,12 @@ export function useVariable<F extends FeatureKey, V extends VariableKey<F>>(
|
|
|
813
911
|
// index
|
|
814
912
|
const indexContent =
|
|
815
913
|
[
|
|
816
|
-
`export * from "./
|
|
914
|
+
`export * from "./context";`,
|
|
817
915
|
`export * from "./instance";`,
|
|
818
|
-
...(hasSchemasFile ? [`export * from "./
|
|
819
|
-
`export * from "./
|
|
820
|
-
`export * from "./
|
|
821
|
-
...(shouldGenerateReact ? [`export * from "./
|
|
916
|
+
...(hasSchemasFile ? [`export * from "./schemas";`] : []),
|
|
917
|
+
`export * from "./features";`,
|
|
918
|
+
`export * from "./functions";`,
|
|
919
|
+
...(shouldGenerateReact ? [`export * from "./react";`] : []),
|
|
822
920
|
...featureNamespaces.map((featureNamespace) => {
|
|
823
921
|
return `export * from "./${featureNamespace}";`;
|
|
824
922
|
}),
|
|
@@ -852,6 +852,89 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
852
852
|
);
|
|
853
853
|
});
|
|
854
854
|
|
|
855
|
+
it("accepts object variable with additionalProperties only and arbitrary keys", () => {
|
|
856
|
+
expectParseSuccess(
|
|
857
|
+
baseFeature({
|
|
858
|
+
variablesSchema: {
|
|
859
|
+
labels: {
|
|
860
|
+
type: "object",
|
|
861
|
+
additionalProperties: { type: "string" },
|
|
862
|
+
defaultValue: { title: "Welcome", subtitle: "Hello" },
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
}),
|
|
866
|
+
);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("accepts object variable with properties and additionalProperties together", () => {
|
|
870
|
+
expectParseSuccess(
|
|
871
|
+
baseFeature({
|
|
872
|
+
variablesSchema: {
|
|
873
|
+
metadata: {
|
|
874
|
+
type: "object",
|
|
875
|
+
properties: {
|
|
876
|
+
fixed: { type: "integer" },
|
|
877
|
+
},
|
|
878
|
+
additionalProperties: { type: "string" },
|
|
879
|
+
required: ["fixed"],
|
|
880
|
+
defaultValue: { fixed: 1, dynamicKey: "value" },
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
}),
|
|
884
|
+
);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it("rejects object variable when unknown key is present and additionalProperties is not defined", () => {
|
|
888
|
+
expectParseFailure(
|
|
889
|
+
baseFeature({
|
|
890
|
+
variablesSchema: {
|
|
891
|
+
settings: {
|
|
892
|
+
type: "object",
|
|
893
|
+
properties: {
|
|
894
|
+
theme: { type: "string" },
|
|
895
|
+
},
|
|
896
|
+
defaultValue: { theme: "light", subtitle: "hello" },
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
}),
|
|
900
|
+
"Unknown property",
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("accepts object variable with additionalProperties that references reusable schema", () => {
|
|
905
|
+
expectParseSuccess(
|
|
906
|
+
baseFeature({
|
|
907
|
+
variablesSchema: {
|
|
908
|
+
linksByLocale: {
|
|
909
|
+
type: "object",
|
|
910
|
+
additionalProperties: { schema: "link" },
|
|
911
|
+
defaultValue: {
|
|
912
|
+
en: { title: "Home", url: "/" },
|
|
913
|
+
de: { title: "Start", url: "/de" },
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
}),
|
|
918
|
+
);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("rejects object variable when additionalProperties value does not match schema", () => {
|
|
922
|
+
expectParseFailure(
|
|
923
|
+
baseFeature({
|
|
924
|
+
variablesSchema: {
|
|
925
|
+
linksByLocale: {
|
|
926
|
+
type: "object",
|
|
927
|
+
additionalProperties: { schema: "link" },
|
|
928
|
+
defaultValue: {
|
|
929
|
+
en: { title: "Home" },
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
}),
|
|
934
|
+
"Missing required property",
|
|
935
|
+
);
|
|
936
|
+
});
|
|
937
|
+
|
|
855
938
|
it("rejects inline object variable when defaultValue is missing required property", () => {
|
|
856
939
|
expectParseFailure(
|
|
857
940
|
baseFeature({
|
|
@@ -1417,5 +1500,40 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
1417
1500
|
},
|
|
1418
1501
|
);
|
|
1419
1502
|
});
|
|
1503
|
+
|
|
1504
|
+
it("additionalProperties type mismatch: error path includes dynamic object key", () => {
|
|
1505
|
+
expectErrorSurfaces(
|
|
1506
|
+
baseFeature({
|
|
1507
|
+
variablesSchema: {
|
|
1508
|
+
labels: {
|
|
1509
|
+
type: "object",
|
|
1510
|
+
additionalProperties: { type: "string" },
|
|
1511
|
+
defaultValue: {
|
|
1512
|
+
title: 123,
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
}),
|
|
1517
|
+
{
|
|
1518
|
+
pathContains: ["variablesSchema", "labels", "defaultValue", "title"],
|
|
1519
|
+
messageContains: "type string",
|
|
1520
|
+
},
|
|
1521
|
+
);
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
it("rejects variable schema with schema reference mixed with inline additionalProperties", () => {
|
|
1525
|
+
expectParseFailure(
|
|
1526
|
+
baseFeature({
|
|
1527
|
+
variablesSchema: {
|
|
1528
|
+
myLink: {
|
|
1529
|
+
schema: "link",
|
|
1530
|
+
additionalProperties: { type: "string" },
|
|
1531
|
+
defaultValue: { title: "Home", url: "/" },
|
|
1532
|
+
},
|
|
1533
|
+
},
|
|
1534
|
+
}),
|
|
1535
|
+
"additionalProperties",
|
|
1536
|
+
);
|
|
1537
|
+
});
|
|
1420
1538
|
});
|
|
1421
1539
|
});
|
|
@@ -51,6 +51,7 @@ function resolveVariableSchema(
|
|
|
51
51
|
type?: string;
|
|
52
52
|
items?: unknown;
|
|
53
53
|
properties?: unknown;
|
|
54
|
+
additionalProperties?: unknown;
|
|
54
55
|
required?: string[];
|
|
55
56
|
enum?: unknown[];
|
|
56
57
|
const?: unknown;
|
|
@@ -69,6 +70,7 @@ function resolveVariableSchema(
|
|
|
69
70
|
type?: string;
|
|
70
71
|
items?: unknown;
|
|
71
72
|
properties?: unknown;
|
|
73
|
+
additionalProperties?: unknown;
|
|
72
74
|
required?: string[];
|
|
73
75
|
enum?: unknown[];
|
|
74
76
|
const?: unknown;
|
|
@@ -89,6 +91,7 @@ function resolveVariableSchema(
|
|
|
89
91
|
type?: string;
|
|
90
92
|
items?: unknown;
|
|
91
93
|
properties?: unknown;
|
|
94
|
+
additionalProperties?: unknown;
|
|
92
95
|
required?: string[];
|
|
93
96
|
enum?: unknown[];
|
|
94
97
|
const?: unknown;
|
|
@@ -133,6 +136,7 @@ function valueMatchesSchema(
|
|
|
133
136
|
enum?: unknown[];
|
|
134
137
|
oneOf?: unknown[];
|
|
135
138
|
properties?: Record<string, unknown>;
|
|
139
|
+
additionalProperties?: unknown;
|
|
136
140
|
required?: string[];
|
|
137
141
|
items?: unknown;
|
|
138
142
|
minimum?: number;
|
|
@@ -195,8 +199,16 @@ function valueMatchesSchema(
|
|
|
195
199
|
if (type === "object") {
|
|
196
200
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
197
201
|
const props = resolved.properties;
|
|
198
|
-
|
|
202
|
+
const additional = resolved.additionalProperties;
|
|
199
203
|
const obj = value as Record<string, unknown>;
|
|
204
|
+
if ((!props || typeof props !== "object") && (!additional || typeof additional !== "object")) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (!props || typeof props !== "object") {
|
|
208
|
+
return Object.keys(obj).every((key) =>
|
|
209
|
+
valueMatchesSchema(additional as { [k: string]: unknown }, obj[key], schemasByKey),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
200
212
|
const required = new Set(resolved.required || []);
|
|
201
213
|
for (const key of required) {
|
|
202
214
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) return false;
|
|
@@ -205,9 +217,15 @@ function valueMatchesSchema(
|
|
|
205
217
|
}
|
|
206
218
|
for (const key of Object.keys(obj)) {
|
|
207
219
|
const propSchema = props[key];
|
|
208
|
-
if (
|
|
209
|
-
|
|
220
|
+
if (propSchema) {
|
|
221
|
+
if (!valueMatchesSchema(propSchema as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
222
|
+
return false;
|
|
223
|
+
} else if (additional && typeof additional === "object") {
|
|
224
|
+
if (!valueMatchesSchema(additional as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
225
|
+
return false;
|
|
226
|
+
} else {
|
|
210
227
|
return false;
|
|
228
|
+
}
|
|
211
229
|
}
|
|
212
230
|
return true;
|
|
213
231
|
}
|
|
@@ -259,6 +277,7 @@ function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
|
259
277
|
type SchemaLikeForRequired = {
|
|
260
278
|
type?: string;
|
|
261
279
|
properties?: Record<string, unknown>;
|
|
280
|
+
additionalProperties?: unknown;
|
|
262
281
|
required?: string[];
|
|
263
282
|
items?: unknown;
|
|
264
283
|
schema?: string;
|
|
@@ -325,6 +344,19 @@ function refineRequiredKeysInSchema(
|
|
|
325
344
|
}
|
|
326
345
|
}
|
|
327
346
|
}
|
|
347
|
+
const additionalProperties = (current as { additionalProperties?: unknown }).additionalProperties;
|
|
348
|
+
if (
|
|
349
|
+
additionalProperties &&
|
|
350
|
+
typeof additionalProperties === "object" &&
|
|
351
|
+
!Array.isArray(additionalProperties)
|
|
352
|
+
) {
|
|
353
|
+
refineRequiredKeysInSchema(
|
|
354
|
+
additionalProperties as SchemaLikeForRequired,
|
|
355
|
+
[...pathPrefix, "additionalProperties"],
|
|
356
|
+
ctx,
|
|
357
|
+
schemasByKey,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
328
360
|
|
|
329
361
|
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
330
362
|
refineRequiredKeysInSchema(
|
|
@@ -453,6 +485,7 @@ function refineVariableValueObject(
|
|
|
453
485
|
projectConfig: ProjectConfig,
|
|
454
486
|
variableSchema: {
|
|
455
487
|
properties?: Record<string, unknown>;
|
|
488
|
+
additionalProperties?: unknown;
|
|
456
489
|
required?: string[];
|
|
457
490
|
type: string;
|
|
458
491
|
},
|
|
@@ -464,6 +497,7 @@ function refineVariableValueObject(
|
|
|
464
497
|
): void {
|
|
465
498
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
466
499
|
const schemaProperties = variableSchema.properties;
|
|
500
|
+
const additionalProperties = variableSchema.additionalProperties;
|
|
467
501
|
|
|
468
502
|
if (schemaProperties && typeof schemaProperties === "object") {
|
|
469
503
|
const requiredKeys =
|
|
@@ -485,7 +519,17 @@ function refineVariableValueObject(
|
|
|
485
519
|
|
|
486
520
|
for (const key of Object.keys(variableValue)) {
|
|
487
521
|
const propSchema = schemaProperties[key];
|
|
488
|
-
if (!propSchema) {
|
|
522
|
+
if (!propSchema && additionalProperties && typeof additionalProperties === "object") {
|
|
523
|
+
superRefineVariableValue(
|
|
524
|
+
projectConfig,
|
|
525
|
+
additionalProperties as Parameters<typeof superRefineVariableValue>[1],
|
|
526
|
+
variableValue[key],
|
|
527
|
+
[...path, key],
|
|
528
|
+
ctx,
|
|
529
|
+
key,
|
|
530
|
+
schemasByKey,
|
|
531
|
+
);
|
|
532
|
+
} else if (!propSchema) {
|
|
489
533
|
ctx.addIssue({
|
|
490
534
|
code: z.ZodIssueCode.custom,
|
|
491
535
|
message: `Unknown property "${key}" in variable "${label}" (not in schema)`,
|
|
@@ -503,6 +547,18 @@ function refineVariableValueObject(
|
|
|
503
547
|
);
|
|
504
548
|
}
|
|
505
549
|
}
|
|
550
|
+
} else if (additionalProperties && typeof additionalProperties === "object") {
|
|
551
|
+
for (const key of Object.keys(variableValue)) {
|
|
552
|
+
superRefineVariableValue(
|
|
553
|
+
projectConfig,
|
|
554
|
+
additionalProperties as Parameters<typeof superRefineVariableValue>[1],
|
|
555
|
+
variableValue[key],
|
|
556
|
+
[...path, key],
|
|
557
|
+
ctx,
|
|
558
|
+
key,
|
|
559
|
+
schemasByKey,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
506
562
|
} else {
|
|
507
563
|
for (const key of Object.keys(variableValue)) {
|
|
508
564
|
const propValue = variableValue[key];
|
|
@@ -1280,6 +1336,7 @@ export function getFeatureZodSchema(
|
|
|
1280
1336
|
type: z.union([z.literal("json"), propertyTypeEnum]).optional(),
|
|
1281
1337
|
items: schemaZodSchema.optional(),
|
|
1282
1338
|
properties: z.record(schemaZodSchema).optional(),
|
|
1339
|
+
additionalProperties: schemaZodSchema.optional(),
|
|
1283
1340
|
required: z.array(z.string()).optional(),
|
|
1284
1341
|
enum: z.array(variableValueZodSchema).optional(),
|
|
1285
1342
|
const: variableValueZodSchema.optional(),
|
|
@@ -1315,7 +1372,7 @@ export function getFeatureZodSchema(
|
|
|
1315
1372
|
ctx.addIssue({
|
|
1316
1373
|
code: z.ZodIssueCode.custom,
|
|
1317
1374
|
message:
|
|
1318
|
-
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `required`, `items`). Use one or the other.",
|
|
1375
|
+
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `additionalProperties`, `required`, `items`). Use one or the other.",
|
|
1319
1376
|
path: [],
|
|
1320
1377
|
});
|
|
1321
1378
|
return;
|
|
@@ -1324,6 +1381,8 @@ export function getFeatureZodSchema(
|
|
|
1324
1381
|
const hasInlineStructure =
|
|
1325
1382
|
("type" in variableSchema && variableSchema.type != null) ||
|
|
1326
1383
|
("properties" in variableSchema && variableSchema.properties != null) ||
|
|
1384
|
+
("additionalProperties" in variableSchema &&
|
|
1385
|
+
variableSchema.additionalProperties != null) ||
|
|
1327
1386
|
("required" in variableSchema && variableSchema.required != null) ||
|
|
1328
1387
|
("items" in variableSchema && variableSchema.items != null) ||
|
|
1329
1388
|
("oneOf" in variableSchema && variableSchema.oneOf != null);
|
|
@@ -1340,7 +1399,7 @@ export function getFeatureZodSchema(
|
|
|
1340
1399
|
ctx.addIssue({
|
|
1341
1400
|
code: z.ZodIssueCode.custom,
|
|
1342
1401
|
message:
|
|
1343
|
-
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `required`, or `items`.",
|
|
1402
|
+
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `additionalProperties`, `required`, or `items`.",
|
|
1344
1403
|
path: [],
|
|
1345
1404
|
});
|
|
1346
1405
|
}
|