@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +774 -652
  3. package/coverage/coverage-final.json +19 -18
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +1 -1
  9. package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
  10. package/coverage/lcov-report/builder/mutator.ts.html +1 -1
  11. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  12. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  13. package/coverage/lcov-report/config/index.html +30 -15
  14. package/coverage/lcov-report/config/index.ts.html +88 -0
  15. package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
  16. package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
  17. package/coverage/lcov-report/datasource/datasource.ts.html +76 -25
  18. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +690 -66
  19. package/coverage/lcov-report/datasource/index.html +35 -35
  20. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  21. package/coverage/lcov-report/index.html +43 -43
  22. package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
  23. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  24. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  25. package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
  26. package/coverage/lcov-report/linter/featureSchema.ts.html +152 -164
  27. package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
  28. package/coverage/lcov-report/linter/index.html +34 -34
  29. package/coverage/lcov-report/linter/lintProject.ts.html +428 -83
  30. package/coverage/lcov-report/linter/mutationNotation.ts.html +3 -3
  31. package/coverage/lcov-report/linter/printError.ts.html +9 -9
  32. package/coverage/lcov-report/linter/schema.ts.html +4 -4
  33. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  34. package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
  35. package/coverage/lcov-report/list/index.html +1 -1
  36. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  37. package/coverage/lcov-report/parsers/index.html +1 -1
  38. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  39. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  40. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  41. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  42. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  43. package/coverage/lcov-report/tester/index.html +1 -1
  44. package/coverage/lcov-report/utils/git.ts.html +118 -16
  45. package/coverage/lcov-report/utils/index.html +14 -14
  46. package/coverage/lcov.info +1374 -1107
  47. package/lib/config/projectConfig.d.ts +3 -0
  48. package/lib/config/projectConfig.js +7 -1
  49. package/lib/config/projectConfig.js.map +1 -1
  50. package/lib/config/projectConfig.spec.d.ts +1 -0
  51. package/lib/config/projectConfig.spec.js +24 -0
  52. package/lib/config/projectConfig.spec.js.map +1 -0
  53. package/lib/datasource/adapter.d.ts +7 -0
  54. package/lib/datasource/adapter.js +7 -0
  55. package/lib/datasource/adapter.js.map +1 -1
  56. package/lib/datasource/datasource.d.ts +3 -1
  57. package/lib/datasource/datasource.js +6 -0
  58. package/lib/datasource/datasource.js.map +1 -1
  59. package/lib/datasource/filesystemAdapter.d.ts +8 -0
  60. package/lib/datasource/filesystemAdapter.js +134 -7
  61. package/lib/datasource/filesystemAdapter.js.map +1 -1
  62. package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
  63. package/lib/datasource/filesystemAdapter.spec.js +88 -0
  64. package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
  65. package/lib/generate-code/index.d.ts +3 -0
  66. package/lib/generate-code/index.js +16 -1
  67. package/lib/generate-code/index.js.map +1 -1
  68. package/lib/generate-code/typescript.d.ts +6 -1
  69. package/lib/generate-code/typescript.js +193 -42
  70. package/lib/generate-code/typescript.js.map +1 -1
  71. package/lib/linter/conditionSchema.spec.js +2 -0
  72. package/lib/linter/conditionSchema.spec.js.map +1 -1
  73. package/lib/linter/featureSchema.js +46 -49
  74. package/lib/linter/featureSchema.js.map +1 -1
  75. package/lib/linter/featureSchema.spec.js +17 -0
  76. package/lib/linter/featureSchema.spec.js.map +1 -1
  77. package/lib/linter/lintProject.d.ts +2 -0
  78. package/lib/linter/lintProject.js +85 -8
  79. package/lib/linter/lintProject.js.map +1 -1
  80. package/lib/linter/lintProject.spec.js +37 -0
  81. package/lib/linter/lintProject.spec.js.map +1 -1
  82. package/lib/linter/segmentSchema.spec.js +2 -0
  83. package/lib/linter/segmentSchema.spec.js.map +1 -1
  84. package/lib/utils/git.js +32 -7
  85. package/lib/utils/git.js.map +1 -1
  86. package/package.json +2 -2
  87. package/src/config/projectConfig.spec.ts +31 -0
  88. package/src/config/projectConfig.ts +11 -0
  89. package/src/datasource/adapter.ts +21 -0
  90. package/src/datasource/datasource.ts +18 -1
  91. package/src/datasource/filesystemAdapter.spec.ts +153 -0
  92. package/src/datasource/filesystemAdapter.ts +216 -8
  93. package/src/generate-code/index.ts +20 -1
  94. package/src/generate-code/typescript.ts +266 -64
  95. package/src/linter/conditionSchema.spec.ts +2 -0
  96. package/src/linter/featureSchema.spec.ts +21 -0
  97. package/src/linter/featureSchema.ts +84 -88
  98. package/src/linter/lintProject.spec.ts +74 -0
  99. package/src/linter/lintProject.ts +123 -8
  100. package/src/linter/segmentSchema.spec.ts +2 -0
  101. 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(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
  }),
@@ -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
- if (!value.variablesSchema) {
1469
- return;
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
- const variableKeys = Object.keys(variableSchemaByKey);
1487
- variableKeys.forEach((variableKey) => {
1488
- const variableSchema = variableSchemaByKey[variableKey];
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
- // When variable references a schema by name, ensure it resolves and validate the referenced schema
1491
- if ("schema" in variableSchema && variableSchema.schema) {
1492
- const resolvedSchema = resolveVariableSchema(
1493
- variableSchema as Parameters<typeof resolveVariableSchema>[0],
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
- // Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
1528
- if (!("schema" in variableSchema) || !variableSchema.schema) {
1529
- const pathPrefix = ["variablesSchema", variableKey];
1530
- refineMinimumMaximum(
1531
- variableSchema as Parameters<typeof refineMinimumMaximum>[0],
1532
- pathPrefix,
1533
- ctx,
1534
- );
1535
- refineStringLengthPattern(
1536
- variableSchema as Parameters<typeof refineStringLengthPattern>[0],
1537
- pathPrefix,
1538
- ctx,
1539
- );
1540
- refineArrayItems(
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
- if (variableKey === "variation") {
1548
- ctx.addIssue({
1549
- code: z.ZodIssueCode.custom,
1550
- message: `Variable key "${variableKey}" is reserved and cannot be used.`,
1551
- path: ["variablesSchema", variableKey],
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
- // defaultValue
1556
- superRefineVariableValue(
1557
- projectConfig,
1558
- variableSchema,
1559
- variableSchema.defaultValue,
1560
- ["variablesSchema", variableKey, "defaultValue"],
1561
- ctx,
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
- // disabledValue (only when present)
1567
- if (variableSchema.disabledValue !== undefined) {
1550
+ // defaultValue
1568
1551
  superRefineVariableValue(
1569
1552
  projectConfig,
1570
1553
  variableSchema,
1571
- variableSchema.disabledValue,
1572
- ["variablesSchema", variableKey, "disabledValue"],
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) {