@featurevisor/core 2.25.0 → 2.26.1

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 (106) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +1499 -1476
  3. package/coverage/coverage-final.json +18 -17
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  8. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  9. package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
  10. package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
  11. package/coverage/lcov-report/builder/hashes.ts.html +1 -1
  12. package/coverage/lcov-report/builder/index.html +1 -1
  13. package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
  14. package/coverage/lcov-report/builder/mutator.ts.html +52 -52
  15. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  16. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  17. package/coverage/lcov-report/config/index.html +1 -1
  18. package/coverage/lcov-report/config/index.ts.html +1 -1
  19. package/coverage/lcov-report/config/projectConfig.ts.html +15 -15
  20. package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
  21. package/coverage/lcov-report/datasource/datasource.ts.html +18 -18
  22. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +43 -43
  23. package/coverage/lcov-report/datasource/index.html +1 -1
  24. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  25. package/coverage/lcov-report/generate-code/index.html +1 -1
  26. package/coverage/lcov-report/generate-code/typescript.ts.html +3 -3
  27. package/coverage/lcov-report/index.html +18 -18
  28. package/coverage/lcov-report/linter/attributeSchema.ts.html +130 -130
  29. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +11 -11
  30. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +15 -15
  31. package/coverage/lcov-report/linter/conditionSchema.ts.html +150 -150
  32. package/coverage/lcov-report/linter/featureSchema.ts.html +458 -443
  33. package/coverage/lcov-report/linter/groupSchema.ts.html +24 -27
  34. package/coverage/lcov-report/linter/index.html +83 -68
  35. package/coverage/lcov-report/linter/lintProject.ts.html +158 -158
  36. package/coverage/lcov-report/linter/mutationNotation.ts.html +104 -104
  37. package/coverage/lcov-report/linter/printError.ts.html +79 -28
  38. package/coverage/lcov-report/linter/schema.ts.html +129 -135
  39. package/coverage/lcov-report/linter/segmentSchema.ts.html +15 -9
  40. package/coverage/lcov-report/linter/testSchema.ts.html +67 -94
  41. package/coverage/lcov-report/linter/zodHelpers.ts.html +133 -0
  42. package/coverage/lcov-report/list/index.html +1 -1
  43. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  44. package/coverage/lcov-report/tester/cliFormat.ts.html +6 -6
  45. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  46. package/coverage/lcov-report/tester/index.html +1 -1
  47. package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
  48. package/coverage/lcov-report/utils/git.ts.html +1 -1
  49. package/coverage/lcov-report/utils/index.html +1 -1
  50. package/coverage/lcov-report/utils/index.ts.html +1 -1
  51. package/coverage/lcov-report/utils/pretty.ts.html +1 -1
  52. package/coverage/lcov.info +3031 -2984
  53. package/lib/generate-code/typescript.js +2 -2
  54. package/lib/linter/attributeSchema.d.ts +19 -352
  55. package/lib/linter/attributeSchema.spec.js.map +1 -1
  56. package/lib/linter/conditionSchema.d.ts +1 -1
  57. package/lib/linter/conditionSchema.js +2 -3
  58. package/lib/linter/conditionSchema.js.map +1 -1
  59. package/lib/linter/conditionSchema.spec.js.map +1 -1
  60. package/lib/linter/featureSchema.d.ts +75 -964
  61. package/lib/linter/featureSchema.js +64 -53
  62. package/lib/linter/featureSchema.js.map +1 -1
  63. package/lib/linter/featureSchema.spec.js +28 -0
  64. package/lib/linter/featureSchema.spec.js.map +1 -1
  65. package/lib/linter/groupSchema.d.ts +4 -28
  66. package/lib/linter/groupSchema.js +3 -5
  67. package/lib/linter/groupSchema.js.map +1 -1
  68. package/lib/linter/lintProject.spec.js +101 -13
  69. package/lib/linter/lintProject.spec.js.map +1 -1
  70. package/lib/linter/printError.js +21 -7
  71. package/lib/linter/printError.js.map +1 -1
  72. package/lib/linter/schema.d.ts +9 -2
  73. package/lib/linter/schema.js +2 -6
  74. package/lib/linter/schema.js.map +1 -1
  75. package/lib/linter/schema.spec.js +3 -2
  76. package/lib/linter/schema.spec.js.map +1 -1
  77. package/lib/linter/segmentSchema.d.ts +1 -9
  78. package/lib/linter/segmentSchema.js +3 -1
  79. package/lib/linter/segmentSchema.js.map +1 -1
  80. package/lib/linter/segmentSchema.spec.js.map +1 -1
  81. package/lib/linter/testSchema.d.ts +16 -222
  82. package/lib/linter/testSchema.js +23 -38
  83. package/lib/linter/testSchema.js.map +1 -1
  84. package/lib/linter/testSchema.spec.d.ts +1 -0
  85. package/lib/linter/testSchema.spec.js +162 -0
  86. package/lib/linter/testSchema.spec.js.map +1 -0
  87. package/lib/linter/zodHelpers.d.ts +2 -0
  88. package/lib/linter/zodHelpers.js +15 -0
  89. package/lib/linter/zodHelpers.js.map +1 -0
  90. package/package.json +3 -3
  91. package/src/generate-code/typescript.ts +2 -2
  92. package/src/linter/attributeSchema.spec.ts +3 -3
  93. package/src/linter/conditionSchema.spec.ts +3 -3
  94. package/src/linter/conditionSchema.ts +4 -4
  95. package/src/linter/featureSchema.spec.ts +39 -4
  96. package/src/linter/featureSchema.ts +82 -77
  97. package/src/linter/groupSchema.ts +11 -12
  98. package/src/linter/lintProject.spec.ts +121 -13
  99. package/src/linter/printError.ts +27 -10
  100. package/src/linter/schema.spec.ts +12 -11
  101. package/src/linter/schema.ts +7 -9
  102. package/src/linter/segmentSchema.spec.ts +5 -5
  103. package/src/linter/segmentSchema.ts +3 -1
  104. package/src/linter/testSchema.spec.ts +193 -0
  105. package/src/linter/testSchema.ts +36 -45
  106. package/src/linter/zodHelpers.ts +16 -0
@@ -74,7 +74,7 @@ function getSegmentSchema() {
74
74
  return getSegmentZodSchema(projectConfig, conditionsZodSchema);
75
75
  }
76
76
 
77
- function parseSegment(input: unknown): z.SafeParseReturnType<unknown, unknown> {
77
+ function parseSegment(input: unknown): z.ZodSafeParseResult<unknown> {
78
78
  return getSegmentSchema().safeParse(input);
79
79
  }
80
80
 
@@ -82,7 +82,7 @@ function expectSegmentSuccess(input: unknown): void {
82
82
  const result = parseSegment(input);
83
83
  expect(result.success).toBe(true);
84
84
  if (!result.success) {
85
- const err = (result as z.SafeParseError<unknown>).error;
85
+ const err = (result as z.ZodSafeParseError<unknown>).error;
86
86
  const msg = err.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
87
87
  throw new Error(`Expected segment to pass: ${msg}`);
88
88
  }
@@ -92,7 +92,7 @@ function expectSegmentFailure(input: unknown, messageSubstring?: string): z.ZodE
92
92
  const result = parseSegment(input);
93
93
  expect(result.success).toBe(false);
94
94
  if (result.success) throw new Error("Expected segment to fail");
95
- const err = (result as z.SafeParseError<unknown>).error;
95
+ const err = (result as z.ZodSafeParseError<unknown>).error;
96
96
  if (messageSubstring) {
97
97
  const messages = err.issues
98
98
  .map((i) => (typeof i.message === "string" ? i.message : ""))
@@ -134,7 +134,7 @@ describe("segmentSchema.ts :: getSegmentZodSchema", () => {
134
134
  });
135
135
  expect(result.success).toBe(false);
136
136
  if (!result.success) {
137
- const err = (result as z.SafeParseError<unknown>).error;
137
+ const err = (result as z.ZodSafeParseError<unknown>).error;
138
138
  const path = err.issues.map((i) => i.path.join(".")).join(" ");
139
139
  expect(path).toContain("description");
140
140
  }
@@ -346,7 +346,7 @@ describe("segmentSchema.ts :: getSegmentZodSchema", () => {
346
346
  extraKey: true,
347
347
  });
348
348
  expect(result.success).toBe(false);
349
- const err = (result as z.SafeParseError<unknown>).error;
349
+ const err = (result as z.ZodSafeParseError<unknown>).error;
350
350
  const messages = err.issues
351
351
  .map((i) => (typeof i.message === "string" ? i.message : ""))
352
352
  .join(" ");
@@ -6,7 +6,9 @@ export function getSegmentZodSchema(projectConfig: ProjectConfig, conditionsZodS
6
6
  const segmentZodSchema = z
7
7
  .object({
8
8
  archived: z.boolean().optional(),
9
- description: z.string(),
9
+ description: z.string({
10
+ error: (issue) => (issue.input === undefined ? "Required" : undefined),
11
+ }),
10
12
  conditions: conditionsZodSchema,
11
13
  })
12
14
  .strict();
@@ -0,0 +1,193 @@
1
+ import { z } from "zod";
2
+
3
+ import type { ProjectConfig } from "../config";
4
+ import { getLintIssuesFromZodError } from "./printError";
5
+ import { getTestsZodSchema } from "./testSchema";
6
+
7
+ function minimalProjectConfig(overrides: Partial<ProjectConfig> = {}): ProjectConfig {
8
+ return {
9
+ featuresDirectoryPath: "",
10
+ segmentsDirectoryPath: "",
11
+ attributesDirectoryPath: "",
12
+ groupsDirectoryPath: "",
13
+ schemasDirectoryPath: "",
14
+ testsDirectoryPath: "",
15
+ stateDirectoryPath: "",
16
+ datafilesDirectoryPath: "",
17
+ datafileNamePattern: "",
18
+ revisionFileName: "",
19
+ siteExportDirectoryPath: "",
20
+ environmentsDirectoryPath: "",
21
+ environments: ["staging", "production"],
22
+ splitByEnvironment: false,
23
+ tags: ["all", "beta"],
24
+ adapter: {},
25
+ plugins: [],
26
+ defaultBucketBy: "userId",
27
+ parser: "yml",
28
+ prettyState: true,
29
+ prettyDatafile: false,
30
+ stringify: true,
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function getSchema(projectConfig = minimalProjectConfig()) {
36
+ return getTestsZodSchema(projectConfig, ["checkout"], ["desktop"]);
37
+ }
38
+
39
+ function parseTest(input: unknown): z.ZodSafeParseResult<unknown> {
40
+ return getSchema().safeParse(input);
41
+ }
42
+
43
+ function expectTestSuccess(input: unknown): void {
44
+ const result = parseTest(input);
45
+ expect(result.success).toBe(true);
46
+ if (!result.success) {
47
+ const error = (result as z.ZodSafeParseError<unknown>).error;
48
+ throw new Error(error.issues.map((issue) => issue.message).join("; "));
49
+ }
50
+ }
51
+
52
+ function expectTestFailure(input: unknown, messageSubstring: string): z.ZodError {
53
+ const result = parseTest(input);
54
+ expect(result.success).toBe(false);
55
+ if (result.success) {
56
+ throw new Error("Expected test schema to fail");
57
+ }
58
+
59
+ const error = (result as z.ZodSafeParseError<unknown>).error;
60
+ expect(error.issues.map((issue) => issue.message).join(" ")).toContain(messageSubstring);
61
+
62
+ return error;
63
+ }
64
+
65
+ describe("testSchema.ts :: getTestsZodSchema", () => {
66
+ it("accepts a valid feature test with matrix, context, sticky, and expected evaluations", () => {
67
+ expectTestSuccess({
68
+ feature: "checkout",
69
+ assertions: [
70
+ {
71
+ matrix: {
72
+ country: ["nl", "de"],
73
+ loggedIn: [true, false],
74
+ score: [1, 2],
75
+ empty: [null],
76
+ },
77
+ description: "feature assertion",
78
+ at: "${{ score }}",
79
+ environment: "staging",
80
+ tag: "all",
81
+ sticky: {
82
+ flag: { checkout: "cached" },
83
+ },
84
+ context: {
85
+ userId: "user-1",
86
+ },
87
+ defaultVariableValues: {
88
+ title: "Fallback",
89
+ },
90
+ expectedToBeEnabled: true,
91
+ expectedVariation: "control",
92
+ expectedVariables: {
93
+ title: "Checkout",
94
+ },
95
+ expectedEvaluations: {
96
+ flag: { reason: "rule" },
97
+ variation: { reason: "bucketed" },
98
+ variables: { title: { reason: "default" } },
99
+ },
100
+ children: [
101
+ {
102
+ context: {
103
+ userId: "child-1",
104
+ },
105
+ expectedToBeEnabled: false,
106
+ expectedVariation: null,
107
+ },
108
+ ],
109
+ },
110
+ ],
111
+ });
112
+ });
113
+
114
+ it("accepts matrix placeholders for environment values", () => {
115
+ expectTestSuccess({
116
+ feature: "checkout",
117
+ assertions: [
118
+ {
119
+ matrix: {
120
+ environment: ["staging", "production"],
121
+ },
122
+ at: 1,
123
+ environment: "${{ environment }}",
124
+ },
125
+ ],
126
+ });
127
+ });
128
+
129
+ it("rejects unknown environments with a precise path", () => {
130
+ const error = expectTestFailure(
131
+ {
132
+ feature: "checkout",
133
+ assertions: [
134
+ {
135
+ at: 1,
136
+ environment: "qa",
137
+ },
138
+ ],
139
+ },
140
+ 'Unknown environment "qa"',
141
+ );
142
+
143
+ expect(error.issues.some((issue) => issue.path.join(".") === "assertions.0.environment")).toBe(
144
+ true,
145
+ );
146
+ });
147
+
148
+ it("rejects unknown feature and segment keys", () => {
149
+ expectTestFailure(
150
+ {
151
+ feature: "not-real",
152
+ assertions: [{ at: 1, environment: "staging" }],
153
+ },
154
+ 'Unknown feature "not-real"',
155
+ );
156
+
157
+ expectTestFailure(
158
+ {
159
+ segment: "not-real",
160
+ assertions: [
161
+ {
162
+ context: {},
163
+ expectedToMatch: true,
164
+ },
165
+ ],
166
+ },
167
+ 'Unknown segment "not-real"',
168
+ );
169
+ });
170
+
171
+ it("rejects invalid matrix values", () => {
172
+ const error = expectTestFailure(
173
+ {
174
+ feature: "checkout",
175
+ assertions: [
176
+ {
177
+ matrix: {
178
+ country: [{ code: "nl" }],
179
+ },
180
+ at: 1,
181
+ environment: "staging",
182
+ },
183
+ ],
184
+ },
185
+ "Invalid input",
186
+ );
187
+
188
+ const lintIssues = getLintIssuesFromZodError(error);
189
+ expect(lintIssues.some((issue) => issue.path.join(".").includes("matrix.country.0"))).toBe(
190
+ true,
191
+ );
192
+ });
193
+ });
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { ProjectConfig } from "../config";
4
+ import { refineWithMessage } from "./zodHelpers";
4
5
 
5
6
  export function getTestsZodSchema(
6
7
  projectConfig: ProjectConfig,
@@ -10,6 +11,7 @@ export function getTestsZodSchema(
10
11
  const scopeNames = projectConfig.scopes ? projectConfig.scopes.map((scope) => scope.name) : [];
11
12
 
12
13
  const matrixZodSchema = z.record(
14
+ z.string(),
13
15
  z.array(
14
16
  z.union([
15
17
  // allowed values in arrays
@@ -23,18 +25,17 @@ export function getTestsZodSchema(
23
25
 
24
26
  const segmentTestZodSchema = z
25
27
  .object({
26
- segment: z.string().refine(
28
+ segment: refineWithMessage(
29
+ z.string(),
27
30
  (value) => availableSegmentKeys.includes(value),
28
- (value) => ({
29
- message: `Unknown segment "${value}"`,
30
- }),
31
+ (value) => `Unknown segment "${value}"`,
31
32
  ),
32
33
  assertions: z.array(
33
34
  z
34
35
  .object({
35
36
  matrix: matrixZodSchema.optional(),
36
37
  description: z.string().optional(),
37
- context: z.record(z.unknown()),
38
+ context: z.record(z.string(), z.unknown()),
38
39
  expectedToMatch: z.boolean(),
39
40
  })
40
41
  .strict(),
@@ -44,11 +45,10 @@ export function getTestsZodSchema(
44
45
 
45
46
  const featureTestZodSchema = z
46
47
  .object({
47
- feature: z.string().refine(
48
+ feature: refineWithMessage(
49
+ z.string(),
48
50
  (value) => availableFeatureKeys.includes(value),
49
- (value) => ({
50
- message: `Unknown feature "${value}"`,
51
- }),
51
+ (value) => `Unknown feature "${value}"`,
52
52
  ),
53
53
  assertions: z.array(
54
54
  z
@@ -62,7 +62,8 @@ export function getTestsZodSchema(
62
62
  z.string(),
63
63
  ]),
64
64
  environment: Array.isArray(projectConfig.environments)
65
- ? z.string().refine(
65
+ ? refineWithMessage(
66
+ z.string(),
66
67
  (value) => {
67
68
  if (value.indexOf("${{") === 0) {
68
69
  // allow unknown strings for matrix
@@ -79,45 +80,35 @@ export function getTestsZodSchema(
79
80
 
80
81
  return false;
81
82
  },
82
- (value) => ({
83
- message: `Unknown environment "${value}"`,
84
- }),
83
+ (value) => `Unknown environment "${value}"`,
85
84
  )
86
85
  : z.never().optional(),
87
- tag: z
88
- .string()
89
- .refine(
90
- (value) => projectConfig.tags.includes(value),
91
- (value) => ({
92
- message: `Unknown tag "${value}"`,
93
- }),
94
- )
95
- .optional(),
96
- scope: z
97
- .string()
98
- .refine(
99
- (value) => scopeNames.includes(value),
100
- (value) => ({
101
- message: `Unknown scope "${value}"`,
102
- }),
103
- )
104
- .optional(),
86
+ tag: refineWithMessage(
87
+ z.string(),
88
+ (value) => projectConfig.tags.includes(value),
89
+ (value) => `Unknown tag "${value}"`,
90
+ ).optional(),
91
+ scope: refineWithMessage(
92
+ z.string(),
93
+ (value) => scopeNames.includes(value),
94
+ (value) => `Unknown scope "${value}"`,
95
+ ).optional(),
105
96
 
106
97
  // parent
107
- sticky: z.record(z.record(z.any())).optional(),
108
- context: z.record(z.unknown()).optional(),
98
+ sticky: z.record(z.string(), z.record(z.string(), z.any())).optional(),
99
+ context: z.record(z.string(), z.unknown()).optional(),
109
100
 
110
101
  defaultVariationValue: z.string().optional(),
111
- defaultVariableValues: z.record(z.unknown()).optional(),
102
+ defaultVariableValues: z.record(z.string(), z.unknown()).optional(),
112
103
 
113
104
  expectedToBeEnabled: z.boolean().optional(),
114
105
  expectedVariation: z.string().nullable().optional(),
115
- expectedVariables: z.record(z.unknown()).optional(),
106
+ expectedVariables: z.record(z.string(), z.unknown()).optional(),
116
107
  expectedEvaluations: z
117
108
  .object({
118
- flag: z.record(z.any()).optional(),
119
- variation: z.record(z.any()).optional(),
120
- variables: z.record(z.record(z.any())).optional(),
109
+ flag: z.record(z.string(), z.any()).optional(),
110
+ variation: z.record(z.string(), z.any()).optional(),
111
+ variables: z.record(z.string(), z.record(z.string(), z.any())).optional(),
121
112
  })
122
113
  .optional(),
123
114
 
@@ -125,21 +116,21 @@ export function getTestsZodSchema(
125
116
  .array(
126
117
  z.object({
127
118
  // copied from parent
128
- sticky: z.record(z.record(z.any())).optional(),
129
- context: z.record(z.unknown()).optional(),
119
+ sticky: z.record(z.string(), z.record(z.string(), z.any())).optional(),
120
+ context: z.record(z.string(), z.unknown()).optional(),
130
121
 
131
122
  defaultVariationValue: z.string().optional(),
132
- defaultVariableValues: z.record(z.unknown()).optional(),
123
+ defaultVariableValues: z.record(z.string(), z.unknown()).optional(),
133
124
 
134
125
  expectedToBeEnabled: z.boolean().optional(),
135
126
  expectedVariation: z.string().nullable().optional(),
136
- expectedVariables: z.record(z.unknown()).optional(),
127
+ expectedVariables: z.record(z.string(), z.unknown()).optional(),
137
128
 
138
129
  expectedEvaluations: z
139
130
  .object({
140
- flag: z.record(z.any()).optional(),
141
- variation: z.record(z.any()).optional(),
142
- variables: z.record(z.record(z.any())).optional(),
131
+ flag: z.record(z.string(), z.any()).optional(),
132
+ variation: z.record(z.string(), z.any()).optional(),
133
+ variables: z.record(z.string(), z.record(z.string(), z.any())).optional(),
143
134
  })
144
135
  .optional(),
145
136
  }),
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ export function refineWithMessage<T>(
4
+ schema: z.ZodType<T>,
5
+ predicate: (value: T) => boolean,
6
+ getMessage: (value: T) => string,
7
+ ) {
8
+ return schema.superRefine((value, ctx) => {
9
+ if (!predicate(value)) {
10
+ ctx.addIssue({
11
+ code: z.ZodIssueCode.custom,
12
+ message: getMessage(value),
13
+ });
14
+ }
15
+ });
16
+ }