@featurevisor/core 2.15.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +11 -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/mutateVariables.ts.html +1 -1
  9. package/coverage/lcov-report/builder/mutator.ts.html +1 -1
  10. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  11. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  12. package/coverage/lcov-report/config/index.html +1 -1
  13. package/coverage/lcov-report/config/index.ts.html +1 -1
  14. package/coverage/lcov-report/config/projectConfig.ts.html +1 -1
  15. package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
  16. package/coverage/lcov-report/datasource/datasource.ts.html +1 -1
  17. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +1 -1
  18. package/coverage/lcov-report/datasource/index.html +1 -1
  19. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  20. package/coverage/lcov-report/index.html +1 -1
  21. package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
  22. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  23. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  24. package/coverage/lcov-report/linter/conditionSchema.ts.html +1 -1
  25. package/coverage/lcov-report/linter/featureSchema.ts.html +1 -1
  26. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  27. package/coverage/lcov-report/linter/index.html +1 -1
  28. package/coverage/lcov-report/linter/lintProject.ts.html +1 -1
  29. package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
  30. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  31. package/coverage/lcov-report/linter/schema.ts.html +1 -1
  32. package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
  33. package/coverage/lcov-report/linter/testSchema.ts.html +1 -1
  34. package/coverage/lcov-report/list/index.html +1 -1
  35. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  36. package/coverage/lcov-report/parsers/index.html +1 -1
  37. package/coverage/lcov-report/parsers/index.ts.html +1 -1
  38. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  39. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  40. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  41. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  42. package/coverage/lcov-report/tester/index.html +1 -1
  43. package/coverage/lcov-report/utils/git.ts.html +1 -1
  44. package/coverage/lcov-report/utils/index.html +1 -1
  45. package/lib/generate-code/index.d.ts +3 -0
  46. package/lib/generate-code/index.js +16 -1
  47. package/lib/generate-code/index.js.map +1 -1
  48. package/lib/generate-code/typescript.d.ts +6 -1
  49. package/lib/generate-code/typescript.js +193 -42
  50. package/lib/generate-code/typescript.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/generate-code/index.ts +20 -1
  53. package/src/generate-code/typescript.ts +266 -64
@@ -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(deps: Dependencies, outputPath: string) {
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
- const namespaceValue = getPascalCase(featureKey) + "Feature";
514
- featureNamespaces.push(namespaceValue);
515
-
516
- let variableTypeDeclarations = "";
517
- let variableMethods = "";
518
- const featureSchemaTypesUsed = new Set<string>();
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
- const internalMethodName = `getVariable${
545
- variableType === "json" ? "JSON" : getPascalCase(variableType ?? "string")
546
- }`;
601
+ const namespaceValue = getPascalCase(featureKey) + "Feature";
602
+ parsedFeatures.push({ featureKey, parsedFeature, namespaceValue });
603
+ }
547
604
 
548
- const hasGeneric =
549
- variableType === "json" || variableType === "array" || variableType === "object";
550
- const literalAssertion = isLiteralType ? ` as ${returnTypeName} | null` : "";
551
- if (useGetVariable) {
552
- variableMethods += `
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
- } else if (variableType === "json") {
558
- variableMethods += `
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
- } else if (hasGeneric) {
564
- variableMethods += `
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
- } else {
570
- variableMethods += `
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
- if (allDeclarations.length > 0) {
579
- variableTypeDeclarations = "\n\n" + allDeclarations.join("\n\n");
672
+ if (allDeclarations.length > 0) {
673
+ variableTypeDeclarations = "\n\n" + allDeclarations.join("\n\n");
674
+ }
580
675
  }
581
- }
582
676
 
583
- const schemasImportLine =
584
- featureSchemaTypesUsed.size > 0
585
- ? `import type { ${[...featureSchemaTypesUsed].sort().join(", ")} } from "./Schemas";\n\n`
586
- : "";
677
+ const schemasImportLine =
678
+ featureSchemaTypesUsed.size > 0
679
+ ? `import type { ${[...featureSchemaTypesUsed].sort().join(", ")} } from "./Schemas";\n\n`
680
+ : "";
587
681
 
588
- const featureContent = `
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
- const featureNamespaceFilePath = path.join(outputPath, `${namespaceValue}.ts`);
605
- fs.writeFileSync(featureNamespaceFilePath, featureContent);
606
- console.log(
607
- `Feature ${featureKey} file written at: ${getRelativePath(
608
- rootDirectoryPath,
609
- featureNamespaceFilePath,
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
  }),