@featurevisor/core 2.14.0 → 2.16.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 +774 -652
- package/coverage/coverage-final.json +19 -18
- 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 +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/config/index.html +30 -15
- package/coverage/lcov-report/config/index.ts.html +88 -0
- package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
- package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
- package/coverage/lcov-report/datasource/datasource.ts.html +76 -25
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +690 -66
- package/coverage/lcov-report/datasource/index.html +35 -35
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +43 -43
- package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
- 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 +152 -164
- package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/index.html +34 -34
- package/coverage/lcov-report/linter/lintProject.ts.html +428 -83
- package/coverage/lcov-report/linter/mutationNotation.ts.html +3 -3
- package/coverage/lcov-report/linter/printError.ts.html +9 -9
- package/coverage/lcov-report/linter/schema.ts.html +4 -4
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- 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 +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- 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 +118 -16
- package/coverage/lcov-report/utils/index.html +14 -14
- package/coverage/lcov.info +1374 -1107
- package/lib/config/projectConfig.d.ts +3 -0
- package/lib/config/projectConfig.js +7 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/config/projectConfig.spec.d.ts +1 -0
- package/lib/config/projectConfig.spec.js +24 -0
- package/lib/config/projectConfig.spec.js.map +1 -0
- package/lib/datasource/adapter.d.ts +7 -0
- package/lib/datasource/adapter.js +7 -0
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +3 -1
- package/lib/datasource/datasource.js +6 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +8 -0
- package/lib/datasource/filesystemAdapter.js +134 -7
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
- package/lib/datasource/filesystemAdapter.spec.js +88 -0
- package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
- package/lib/generate-code/index.d.ts +3 -0
- package/lib/generate-code/index.js +16 -1
- package/lib/generate-code/index.js.map +1 -1
- package/lib/generate-code/typescript.d.ts +6 -1
- package/lib/generate-code/typescript.js +193 -42
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/conditionSchema.spec.js +2 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.js +46 -49
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +17 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.d.ts +2 -0
- package/lib/linter/lintProject.js +85 -8
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.js +37 -0
- package/lib/linter/lintProject.spec.js.map +1 -1
- package/lib/linter/segmentSchema.spec.js +2 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/lib/utils/git.js +32 -7
- package/lib/utils/git.js.map +1 -1
- package/package.json +2 -2
- package/src/config/projectConfig.spec.ts +31 -0
- package/src/config/projectConfig.ts +11 -0
- package/src/datasource/adapter.ts +21 -0
- package/src/datasource/datasource.ts +18 -1
- package/src/datasource/filesystemAdapter.spec.ts +153 -0
- package/src/datasource/filesystemAdapter.ts +216 -8
- package/src/generate-code/index.ts +20 -1
- package/src/generate-code/typescript.ts +266 -64
- package/src/linter/conditionSchema.spec.ts +2 -0
- package/src/linter/featureSchema.spec.ts +21 -0
- package/src/linter/featureSchema.ts +84 -88
- package/src/linter/lintProject.spec.ts +74 -0
- package/src/linter/lintProject.ts +123 -8
- package/src/linter/segmentSchema.spec.ts +2 -0
- package/src/utils/git.ts +41 -7
|
@@ -3,12 +3,19 @@ import * as path from "path";
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
Attribute,
|
|
6
|
+
ParsedFeature,
|
|
6
7
|
Schema,
|
|
7
8
|
VariableSchema,
|
|
8
9
|
VariableSchemaWithInline,
|
|
9
10
|
} from "@featurevisor/types";
|
|
10
11
|
import { Dependencies } from "../dependencies";
|
|
11
12
|
|
|
13
|
+
export interface TypeScriptGenerationOptions {
|
|
14
|
+
tag?: string | string[];
|
|
15
|
+
react?: boolean;
|
|
16
|
+
individualFeatures?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string): string {
|
|
13
20
|
switch (featurevisorType) {
|
|
14
21
|
case "boolean":
|
|
@@ -383,6 +390,63 @@ function getRelativePath(from, to) {
|
|
|
383
390
|
return relativePath;
|
|
384
391
|
}
|
|
385
392
|
|
|
393
|
+
function getVariationUnionFromFeature(parsedFeature: ParsedFeature): string | null {
|
|
394
|
+
if (!parsedFeature.variations || parsedFeature.variations.length === 0) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const values = Array.from(
|
|
399
|
+
new Set(parsedFeature.variations.map((variation) => JSON.stringify(variation.value))),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return values.length > 0 ? values.join(" | ") : null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getVariableTypeForFeaturesMap(
|
|
406
|
+
variableSchema: VariableSchema,
|
|
407
|
+
schemasByKey: Record<string, Schema>,
|
|
408
|
+
schemaTypeNames?: Record<string, string>,
|
|
409
|
+
): { typeName: string; schemaTypesUsed: string[] } {
|
|
410
|
+
const schemaTypesUsed: string[] = [];
|
|
411
|
+
const addSchemaUsed = (name: string) => {
|
|
412
|
+
if (name && !schemaTypesUsed.includes(name)) {
|
|
413
|
+
schemaTypesUsed.push(name);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (
|
|
418
|
+
schemaTypeNames &&
|
|
419
|
+
"schema" in variableSchema &&
|
|
420
|
+
variableSchema.schema &&
|
|
421
|
+
schemasByKey[variableSchema.schema]
|
|
422
|
+
) {
|
|
423
|
+
const schemaTypeName = schemaTypeNames[variableSchema.schema];
|
|
424
|
+
addSchemaUsed(schemaTypeName);
|
|
425
|
+
return { typeName: schemaTypeName, schemaTypesUsed };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const effective = getEffectiveVariableSchema(variableSchema, schemasByKey);
|
|
429
|
+
if (!effective) {
|
|
430
|
+
return { typeName: "unknown", schemaTypesUsed };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if ("type" in effective && effective.type === "json") {
|
|
434
|
+
return { typeName: "unknown", schemaTypesUsed };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const typeName = schemaToTypeScriptType(effective as Schema, schemasByKey, schemaTypeNames);
|
|
438
|
+
|
|
439
|
+
if (schemaTypeNames) {
|
|
440
|
+
Object.values(schemaTypeNames).forEach((name) => {
|
|
441
|
+
if (typeName.includes(name)) {
|
|
442
|
+
addSchemaUsed(name);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { typeName, schemaTypesUsed };
|
|
448
|
+
}
|
|
449
|
+
|
|
386
450
|
/**
|
|
387
451
|
* Generates the content of Schemas.ts: one exported type per schema key, using schema refs between schemas.
|
|
388
452
|
*/
|
|
@@ -423,8 +487,19 @@ export function getInstance(): FeaturevisorInstance {
|
|
|
423
487
|
}
|
|
424
488
|
`.trimStart();
|
|
425
489
|
|
|
426
|
-
export async function generateTypeScriptCodeForProject(
|
|
490
|
+
export async function generateTypeScriptCodeForProject(
|
|
491
|
+
deps: Dependencies,
|
|
492
|
+
outputPath: string,
|
|
493
|
+
options: TypeScriptGenerationOptions = {},
|
|
494
|
+
) {
|
|
427
495
|
const { rootDirectoryPath, datasource } = deps;
|
|
496
|
+
const selectedTags = options.tag
|
|
497
|
+
? Array.isArray(options.tag)
|
|
498
|
+
? options.tag
|
|
499
|
+
: [options.tag]
|
|
500
|
+
: [];
|
|
501
|
+
const shouldGenerateReact = Boolean(options.react);
|
|
502
|
+
const shouldGenerateIndividualFeatures = options.individualFeatures !== false;
|
|
428
503
|
|
|
429
504
|
console.log("\nGenerating TypeScript code...\n");
|
|
430
505
|
|
|
@@ -474,7 +549,6 @@ ${attributeProperties}
|
|
|
474
549
|
);
|
|
475
550
|
|
|
476
551
|
// features
|
|
477
|
-
const featureNamespaces: string[] = [];
|
|
478
552
|
const featureFiles = await datasource.listFeatures();
|
|
479
553
|
|
|
480
554
|
// Load schemas for resolving variable schema references
|
|
@@ -503,89 +577,109 @@ ${attributeProperties}
|
|
|
503
577
|
schemaTypeNames[k] = getPascalCase(k) + "Schema";
|
|
504
578
|
}
|
|
505
579
|
|
|
580
|
+
const parsedFeatures: {
|
|
581
|
+
featureKey: string;
|
|
582
|
+
parsedFeature: ParsedFeature;
|
|
583
|
+
namespaceValue: string;
|
|
584
|
+
}[] = [];
|
|
585
|
+
|
|
506
586
|
for (const featureKey of featureFiles) {
|
|
507
|
-
const parsedFeature = await datasource.readFeature(featureKey);
|
|
587
|
+
const parsedFeature = (await datasource.readFeature(featureKey)) as ParsedFeature;
|
|
508
588
|
|
|
509
589
|
if (typeof parsedFeature.archived !== "undefined" && parsedFeature.archived) {
|
|
510
590
|
continue;
|
|
511
591
|
}
|
|
512
592
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (parsedFeature.variablesSchema) {
|
|
521
|
-
const variableKeys = Object.keys(parsedFeature.variablesSchema);
|
|
522
|
-
const allDeclarations: string[] = [];
|
|
523
|
-
|
|
524
|
-
for (const variableKey of variableKeys) {
|
|
525
|
-
const variableSchema = parsedFeature.variablesSchema[variableKey];
|
|
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(
|
|
536
|
-
variableKey,
|
|
537
|
-
variableSchema,
|
|
538
|
-
schemasByKey,
|
|
539
|
-
hasSchemasFile ? schemaTypeNames : undefined,
|
|
540
|
-
);
|
|
541
|
-
schemaTypesUsed.forEach((t) => featureSchemaTypesUsed.add(t));
|
|
542
|
-
allDeclarations.push(...declarations);
|
|
593
|
+
if (selectedTags.length > 0) {
|
|
594
|
+
const featureTags = Array.isArray(parsedFeature.tags) ? parsedFeature.tags : [];
|
|
595
|
+
const hasTags = selectedTags.every((tag) => featureTags.includes(tag));
|
|
596
|
+
if (!hasTags) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
543
600
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
601
|
+
const namespaceValue = getPascalCase(featureKey) + "Feature";
|
|
602
|
+
parsedFeatures.push({ featureKey, parsedFeature, namespaceValue });
|
|
603
|
+
}
|
|
547
604
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
605
|
+
const featureNamespaces: string[] = [];
|
|
606
|
+
if (shouldGenerateIndividualFeatures) {
|
|
607
|
+
for (const { featureKey, parsedFeature, namespaceValue } of parsedFeatures) {
|
|
608
|
+
featureNamespaces.push(namespaceValue);
|
|
609
|
+
|
|
610
|
+
let variableTypeDeclarations = "";
|
|
611
|
+
let variableMethods = "";
|
|
612
|
+
const featureSchemaTypesUsed = new Set<string>();
|
|
613
|
+
|
|
614
|
+
if (parsedFeature.variablesSchema) {
|
|
615
|
+
const variableKeys = Object.keys(parsedFeature.variablesSchema);
|
|
616
|
+
const allDeclarations: string[] = [];
|
|
617
|
+
|
|
618
|
+
for (const variableKey of variableKeys) {
|
|
619
|
+
const variableSchema = parsedFeature.variablesSchema[variableKey];
|
|
620
|
+
const effective = getEffectiveVariableSchema(variableSchema, schemasByKey);
|
|
621
|
+
const variableType = effective?.type;
|
|
622
|
+
const {
|
|
623
|
+
declarations,
|
|
624
|
+
returnTypeName,
|
|
625
|
+
genericArg,
|
|
626
|
+
isLiteralType,
|
|
627
|
+
useGetVariable,
|
|
628
|
+
schemaTypesUsed,
|
|
629
|
+
} = generateVariableTypeDeclarations(
|
|
630
|
+
variableKey,
|
|
631
|
+
variableSchema,
|
|
632
|
+
schemasByKey,
|
|
633
|
+
hasSchemasFile ? schemaTypeNames : undefined,
|
|
634
|
+
);
|
|
635
|
+
schemaTypesUsed.forEach((t) => featureSchemaTypesUsed.add(t));
|
|
636
|
+
allDeclarations.push(...declarations);
|
|
637
|
+
|
|
638
|
+
const internalMethodName = `getVariable${
|
|
639
|
+
variableType === "json" ? "JSON" : getPascalCase(variableType ?? "string")
|
|
640
|
+
}`;
|
|
641
|
+
|
|
642
|
+
const hasGeneric =
|
|
643
|
+
variableType === "json" || variableType === "array" || variableType === "object";
|
|
644
|
+
const literalAssertion = isLiteralType ? ` as ${returnTypeName} | null` : "";
|
|
645
|
+
if (useGetVariable) {
|
|
646
|
+
variableMethods += `
|
|
553
647
|
|
|
554
648
|
${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
|
|
555
649
|
${INDENT_NS_BODY}return getInstance().getVariable(key, "${variableKey}", context)${literalAssertion};
|
|
556
650
|
${INDENT_NS}}`;
|
|
557
|
-
|
|
558
|
-
|
|
651
|
+
} else if (variableType === "json") {
|
|
652
|
+
variableMethods += `
|
|
559
653
|
|
|
560
654
|
${INDENT_NS}export function get${getPascalCase(variableKey)}<T = unknown>(context: Context = {}): T | null {
|
|
561
655
|
${INDENT_NS_BODY}return getInstance().${internalMethodName}<T>(key, "${variableKey}", context);
|
|
562
656
|
${INDENT_NS}}`;
|
|
563
|
-
|
|
564
|
-
|
|
657
|
+
} else if (hasGeneric) {
|
|
658
|
+
variableMethods += `
|
|
565
659
|
|
|
566
660
|
${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
|
|
567
661
|
${INDENT_NS_BODY}return getInstance().${internalMethodName}<${genericArg}>(key, "${variableKey}", context);
|
|
568
662
|
${INDENT_NS}}`;
|
|
569
|
-
|
|
570
|
-
|
|
663
|
+
} else {
|
|
664
|
+
variableMethods += `
|
|
571
665
|
|
|
572
666
|
${INDENT_NS}export function get${getPascalCase(variableKey)}(context: Context = {}): ${returnTypeName} | null {
|
|
573
667
|
${INDENT_NS_BODY}return getInstance().${internalMethodName}(key, "${variableKey}", context)${literalAssertion};
|
|
574
668
|
${INDENT_NS}}`;
|
|
669
|
+
}
|
|
575
670
|
}
|
|
576
|
-
}
|
|
577
671
|
|
|
578
|
-
|
|
579
|
-
|
|
672
|
+
if (allDeclarations.length > 0) {
|
|
673
|
+
variableTypeDeclarations = "\n\n" + allDeclarations.join("\n\n");
|
|
674
|
+
}
|
|
580
675
|
}
|
|
581
|
-
}
|
|
582
676
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
677
|
+
const schemasImportLine =
|
|
678
|
+
featureSchemaTypesUsed.size > 0
|
|
679
|
+
? `import type { ${[...featureSchemaTypesUsed].sort().join(", ")} } from "./Schemas";\n\n`
|
|
680
|
+
: "";
|
|
587
681
|
|
|
588
|
-
|
|
682
|
+
const featureContent = `
|
|
589
683
|
import { Context } from "./Context";
|
|
590
684
|
import { getInstance } from "./instance";
|
|
591
685
|
${schemasImportLine}export namespace ${namespaceValue} {
|
|
@@ -601,14 +695,119 @@ ${INDENT_NS}}${variableMethods}
|
|
|
601
695
|
}
|
|
602
696
|
`.trimStart();
|
|
603
697
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
698
|
+
const featureNamespaceFilePath = path.join(outputPath, `${namespaceValue}.ts`);
|
|
699
|
+
fs.writeFileSync(featureNamespaceFilePath, featureContent);
|
|
700
|
+
console.log(
|
|
701
|
+
`Feature ${featureKey} file written at: ${getRelativePath(
|
|
702
|
+
rootDirectoryPath,
|
|
703
|
+
featureNamespaceFilePath,
|
|
704
|
+
)}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const featuresTypeSchemasUsed = new Set<string>();
|
|
710
|
+
const featureTypeEntries = parsedFeatures
|
|
711
|
+
.map(({ featureKey, parsedFeature }) => {
|
|
712
|
+
const featureLines: string[] = [];
|
|
713
|
+
const variationUnion = getVariationUnionFromFeature(parsedFeature);
|
|
714
|
+
|
|
715
|
+
if (variationUnion) {
|
|
716
|
+
featureLines.push(`${INDENT_NS_BODY}variation: ${variationUnion};`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (parsedFeature.variablesSchema) {
|
|
720
|
+
for (const [variableKey, variableSchema] of Object.entries(parsedFeature.variablesSchema)) {
|
|
721
|
+
const { typeName, schemaTypesUsed } = getVariableTypeForFeaturesMap(
|
|
722
|
+
variableSchema,
|
|
723
|
+
schemasByKey,
|
|
724
|
+
hasSchemasFile ? schemaTypeNames : undefined,
|
|
725
|
+
);
|
|
726
|
+
schemaTypesUsed.forEach((name) => featuresTypeSchemasUsed.add(name));
|
|
727
|
+
featureLines.push(`${INDENT_NS_BODY}${JSON.stringify(variableKey)}: ${typeName};`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return `${INDENT_NS}${JSON.stringify(featureKey)}: {\n${featureLines.join("\n")}\n${INDENT_NS}};`;
|
|
732
|
+
})
|
|
733
|
+
.join("\n");
|
|
734
|
+
|
|
735
|
+
const featuresSchemasImportLine =
|
|
736
|
+
featuresTypeSchemasUsed.size > 0
|
|
737
|
+
? `import type { ${[...featuresTypeSchemasUsed].sort().join(", ")} } from "./Schemas";\n\n`
|
|
738
|
+
: "";
|
|
739
|
+
|
|
740
|
+
const featuresFileContent = `
|
|
741
|
+
${featuresSchemasImportLine}export type Features = {
|
|
742
|
+
${featureTypeEntries}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
export type FeatureKey = keyof Features;
|
|
746
|
+
export type VariableKey<F extends FeatureKey> = Extract<Exclude<keyof Features[F], "variation">, string>;
|
|
747
|
+
export type VariableType<F extends FeatureKey, V extends VariableKey<F>> = Features[F][V];
|
|
748
|
+
export type Variation<F extends FeatureKey> = Features[F] extends { variation: infer V } ? V : never;
|
|
749
|
+
`.trimStart();
|
|
750
|
+
const featuresFilePath = path.join(outputPath, "Features.ts");
|
|
751
|
+
fs.writeFileSync(featuresFilePath, featuresFileContent);
|
|
752
|
+
console.log(`Features file written at: ${getRelativePath(rootDirectoryPath, featuresFilePath)}`);
|
|
753
|
+
|
|
754
|
+
const functionsFileContent = `
|
|
755
|
+
import { FeatureKey, Variation, VariableKey, VariableType } from "./Features";
|
|
756
|
+
import { Context } from "./Context";
|
|
757
|
+
import { getInstance } from "./instance";
|
|
758
|
+
|
|
759
|
+
export function isEnabled(featureKey: FeatureKey, context: Context = {}): boolean {
|
|
760
|
+
return getInstance().isEnabled(featureKey, context);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function getVariation<F extends FeatureKey>(featureKey: F, context: Context = {}): Variation<F> | null {
|
|
764
|
+
return getInstance().getVariation(featureKey, context) as Variation<F> | null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function getVariable<F extends FeatureKey, V extends VariableKey<F>>(
|
|
768
|
+
featureKey: F,
|
|
769
|
+
variableKey: V,
|
|
770
|
+
context: Context = {},
|
|
771
|
+
): VariableType<F, V> | null {
|
|
772
|
+
return getInstance().getVariable(featureKey, variableKey, context) as VariableType<F, V> | null;
|
|
773
|
+
}
|
|
774
|
+
`.trimStart();
|
|
775
|
+
const functionsFilePath = path.join(outputPath, "Functions.ts");
|
|
776
|
+
fs.writeFileSync(functionsFilePath, functionsFileContent);
|
|
777
|
+
console.log(
|
|
778
|
+
`Functions file written at: ${getRelativePath(rootDirectoryPath, functionsFilePath)}`,
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
if (shouldGenerateReact) {
|
|
782
|
+
const reactFileContent = `
|
|
783
|
+
import {
|
|
784
|
+
useFlag as useFlagOriginal,
|
|
785
|
+
useVariation as useVariationOriginal,
|
|
786
|
+
useVariable as useVariableOriginal,
|
|
787
|
+
} from "@featurevisor/react";
|
|
788
|
+
|
|
789
|
+
import { FeatureKey, Variation, VariableKey, VariableType } from "./Features";
|
|
790
|
+
import { Context } from "./Context";
|
|
791
|
+
|
|
792
|
+
export function useFlag(featureKey: FeatureKey, context: Context = {}): boolean {
|
|
793
|
+
return useFlagOriginal(featureKey, context);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function useVariation<F extends FeatureKey>(featureKey: F, context: Context = {}): Variation<F> | null {
|
|
797
|
+
return useVariationOriginal(featureKey, context) as Variation<F> | null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function useVariable<F extends FeatureKey, V extends VariableKey<F>>(
|
|
801
|
+
featureKey: F,
|
|
802
|
+
variableKey: V,
|
|
803
|
+
context: Context = {},
|
|
804
|
+
): VariableType<F, V> | null {
|
|
805
|
+
return useVariableOriginal(featureKey, variableKey, context) as VariableType<F, V> | null;
|
|
806
|
+
}
|
|
807
|
+
`.trimStart();
|
|
808
|
+
const reactFilePath = path.join(outputPath, "React.ts");
|
|
809
|
+
fs.writeFileSync(reactFilePath, reactFileContent);
|
|
810
|
+
console.log(`React file written at: ${getRelativePath(rootDirectoryPath, reactFilePath)}`);
|
|
612
811
|
}
|
|
613
812
|
|
|
614
813
|
// index
|
|
@@ -617,6 +816,9 @@ ${INDENT_NS}}${variableMethods}
|
|
|
617
816
|
`export * from "./Context";`,
|
|
618
817
|
`export * from "./instance";`,
|
|
619
818
|
...(hasSchemasFile ? [`export * from "./Schemas";`] : []),
|
|
819
|
+
`export * from "./Features";`,
|
|
820
|
+
`export * from "./Functions";`,
|
|
821
|
+
...(shouldGenerateReact ? [`export * from "./React";`] : []),
|
|
620
822
|
...featureNamespaces.map((featureNamespace) => {
|
|
621
823
|
return `export * from "./${featureNamespace}";`;
|
|
622
824
|
}),
|
|
@@ -21,7 +21,9 @@ function minimalProjectConfig(): ProjectConfig {
|
|
|
21
21
|
datafileNamePattern: "",
|
|
22
22
|
revisionFileName: "",
|
|
23
23
|
siteExportDirectoryPath: "",
|
|
24
|
+
environmentsDirectoryPath: "",
|
|
24
25
|
environments: ["staging", "production"],
|
|
26
|
+
splitByEnvironment: false,
|
|
25
27
|
tags: ["all"],
|
|
26
28
|
adapter: {},
|
|
27
29
|
plugins: [],
|
|
@@ -25,7 +25,9 @@ function minimalProjectConfig(overrides: Partial<ProjectConfig> = {}): ProjectCo
|
|
|
25
25
|
datafileNamePattern: "",
|
|
26
26
|
revisionFileName: "",
|
|
27
27
|
siteExportDirectoryPath: "",
|
|
28
|
+
environmentsDirectoryPath: "",
|
|
28
29
|
environments: ["staging", "production"],
|
|
30
|
+
splitByEnvironment: false,
|
|
29
31
|
tags: ["all"],
|
|
30
32
|
adapter: {},
|
|
31
33
|
plugins: [],
|
|
@@ -678,6 +680,25 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
678
680
|
"not defined in",
|
|
679
681
|
);
|
|
680
682
|
});
|
|
683
|
+
|
|
684
|
+
it("rejects rule variable key when feature has no variablesSchema", () => {
|
|
685
|
+
expectParseFailure(
|
|
686
|
+
baseFeature({
|
|
687
|
+
rules: {
|
|
688
|
+
staging: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
689
|
+
production: [
|
|
690
|
+
{
|
|
691
|
+
key: "r1",
|
|
692
|
+
segments: "*",
|
|
693
|
+
percentage: 80,
|
|
694
|
+
variables: { blah123: "some value" },
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
}),
|
|
699
|
+
"not defined in",
|
|
700
|
+
);
|
|
701
|
+
});
|
|
681
702
|
});
|
|
682
703
|
|
|
683
704
|
describe("force: variables", () => {
|
|
@@ -1465,15 +1465,9 @@ export function getFeatureZodSchema(
|
|
|
1465
1465
|
}
|
|
1466
1466
|
}
|
|
1467
1467
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// Every variable value is validated against its schema from variablesSchema. Sources covered:
|
|
1473
|
-
// 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
|
|
1474
|
-
// 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
|
|
1475
|
-
// 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
|
|
1476
|
-
const variableSchemaByKey = value.variablesSchema;
|
|
1468
|
+
// When variablesSchema is absent, variableSchemaByKey is {} so any variable key used in
|
|
1469
|
+
// rules/force/variations will be reported as "not defined in variablesSchema".
|
|
1470
|
+
const variableSchemaByKey = value.variablesSchema ?? {};
|
|
1477
1471
|
const variationValues: string[] = [];
|
|
1478
1472
|
|
|
1479
1473
|
if (value.variations) {
|
|
@@ -1482,100 +1476,102 @@ export function getFeatureZodSchema(
|
|
|
1482
1476
|
});
|
|
1483
1477
|
}
|
|
1484
1478
|
|
|
1485
|
-
// variablesSchema[key]
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1479
|
+
// variablesSchema[key] — only when feature defines variablesSchema
|
|
1480
|
+
if (value.variablesSchema) {
|
|
1481
|
+
const variableKeys = Object.keys(variableSchemaByKey);
|
|
1482
|
+
variableKeys.forEach((variableKey) => {
|
|
1483
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1489
1484
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
schemasByKey,
|
|
1495
|
-
);
|
|
1496
|
-
if (!resolvedSchema) {
|
|
1497
|
-
ctx.addIssue({
|
|
1498
|
-
code: z.ZodIssueCode.custom,
|
|
1499
|
-
message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1500
|
-
path: ["variablesSchema", variableKey],
|
|
1501
|
-
});
|
|
1502
|
-
} else {
|
|
1503
|
-
refineRequiredKeysInSchema(
|
|
1504
|
-
resolvedSchema as SchemaLikeForRequired,
|
|
1505
|
-
["variablesSchema", variableKey],
|
|
1506
|
-
ctx,
|
|
1485
|
+
// When variable references a schema by name, ensure it resolves and validate the referenced schema
|
|
1486
|
+
if ("schema" in variableSchema && variableSchema.schema) {
|
|
1487
|
+
const resolvedSchema = resolveVariableSchema(
|
|
1488
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1507
1489
|
schemasByKey,
|
|
1508
1490
|
);
|
|
1491
|
+
if (!resolvedSchema) {
|
|
1492
|
+
ctx.addIssue({
|
|
1493
|
+
code: z.ZodIssueCode.custom,
|
|
1494
|
+
message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1495
|
+
path: ["variablesSchema", variableKey],
|
|
1496
|
+
});
|
|
1497
|
+
} else {
|
|
1498
|
+
refineRequiredKeysInSchema(
|
|
1499
|
+
resolvedSchema as SchemaLikeForRequired,
|
|
1500
|
+
["variablesSchema", variableKey],
|
|
1501
|
+
ctx,
|
|
1502
|
+
schemasByKey,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1509
1505
|
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// When type and enum are both present, all enum values must match the type
|
|
1513
|
-
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1514
|
-
if (
|
|
1515
|
-
effectiveSchema &&
|
|
1516
|
-
effectiveSchema.type &&
|
|
1517
|
-
Array.isArray(effectiveSchema.enum) &&
|
|
1518
|
-
effectiveSchema.enum.length > 0
|
|
1519
|
-
) {
|
|
1520
|
-
refineEnumMatchesType(
|
|
1521
|
-
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1522
|
-
["variablesSchema", variableKey],
|
|
1523
|
-
ctx,
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
1506
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
)
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1542
|
-
pathPrefix,
|
|
1543
|
-
ctx,
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1507
|
+
// When type and enum are both present, all enum values must match the type
|
|
1508
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1509
|
+
if (
|
|
1510
|
+
effectiveSchema &&
|
|
1511
|
+
effectiveSchema.type &&
|
|
1512
|
+
Array.isArray(effectiveSchema.enum) &&
|
|
1513
|
+
effectiveSchema.enum.length > 0
|
|
1514
|
+
) {
|
|
1515
|
+
refineEnumMatchesType(
|
|
1516
|
+
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1517
|
+
["variablesSchema", variableKey],
|
|
1518
|
+
ctx,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1546
1521
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1522
|
+
// Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
|
|
1523
|
+
if (!("schema" in variableSchema) || !variableSchema.schema) {
|
|
1524
|
+
const pathPrefix = ["variablesSchema", variableKey];
|
|
1525
|
+
refineMinimumMaximum(
|
|
1526
|
+
variableSchema as Parameters<typeof refineMinimumMaximum>[0],
|
|
1527
|
+
pathPrefix,
|
|
1528
|
+
ctx,
|
|
1529
|
+
);
|
|
1530
|
+
refineStringLengthPattern(
|
|
1531
|
+
variableSchema as Parameters<typeof refineStringLengthPattern>[0],
|
|
1532
|
+
pathPrefix,
|
|
1533
|
+
ctx,
|
|
1534
|
+
);
|
|
1535
|
+
refineArrayItems(
|
|
1536
|
+
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1537
|
+
pathPrefix,
|
|
1538
|
+
ctx,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1554
1541
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
variableKey,
|
|
1563
|
-
schemasByKey,
|
|
1564
|
-
);
|
|
1542
|
+
if (variableKey === "variation") {
|
|
1543
|
+
ctx.addIssue({
|
|
1544
|
+
code: z.ZodIssueCode.custom,
|
|
1545
|
+
message: `Variable key "${variableKey}" is reserved and cannot be used.`,
|
|
1546
|
+
path: ["variablesSchema", variableKey],
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1565
1549
|
|
|
1566
|
-
|
|
1567
|
-
if (variableSchema.disabledValue !== undefined) {
|
|
1550
|
+
// defaultValue
|
|
1568
1551
|
superRefineVariableValue(
|
|
1569
1552
|
projectConfig,
|
|
1570
1553
|
variableSchema,
|
|
1571
|
-
variableSchema.
|
|
1572
|
-
["variablesSchema", variableKey, "
|
|
1554
|
+
variableSchema.defaultValue,
|
|
1555
|
+
["variablesSchema", variableKey, "defaultValue"],
|
|
1573
1556
|
ctx,
|
|
1574
1557
|
variableKey,
|
|
1575
1558
|
schemasByKey,
|
|
1576
1559
|
);
|
|
1577
|
-
|
|
1578
|
-
|
|
1560
|
+
|
|
1561
|
+
// disabledValue (only when present)
|
|
1562
|
+
if (variableSchema.disabledValue !== undefined) {
|
|
1563
|
+
superRefineVariableValue(
|
|
1564
|
+
projectConfig,
|
|
1565
|
+
variableSchema,
|
|
1566
|
+
variableSchema.disabledValue,
|
|
1567
|
+
["variablesSchema", variableKey, "disabledValue"],
|
|
1568
|
+
ctx,
|
|
1569
|
+
variableKey,
|
|
1570
|
+
schemasByKey,
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1579
1575
|
|
|
1580
1576
|
// variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
|
|
1581
1577
|
if (value.variations) {
|