@featurevisor/core 1.35.3 → 2.0.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 (252) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +0 -6
  3. package/coverage/clover.xml +321 -237
  4. package/coverage/coverage-final.json +8 -8
  5. package/coverage/lcov-report/index.html +77 -47
  6. package/coverage/lcov-report/lib/builder/allocator.js.html +14 -14
  7. package/coverage/lcov-report/lib/builder/index.html +16 -16
  8. package/coverage/lcov-report/lib/builder/revision.js.html +3 -3
  9. package/coverage/lcov-report/lib/builder/traffic.js.html +88 -64
  10. package/coverage/lcov-report/lib/list/index.html +116 -0
  11. package/coverage/lcov-report/lib/{tester → list}/matrix.js.html +90 -66
  12. package/coverage/lcov-report/lib/tester/helpers.js.html +295 -0
  13. package/coverage/lcov-report/lib/tester/index.html +20 -35
  14. package/coverage/lcov-report/src/builder/allocator.ts.html +2 -2
  15. package/coverage/lcov-report/src/builder/index.html +15 -15
  16. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  17. package/coverage/lcov-report/src/builder/traffic.ts.html +96 -24
  18. package/coverage/lcov-report/src/list/index.html +116 -0
  19. package/coverage/lcov-report/src/{tester → list}/matrix.ts.html +87 -21
  20. package/coverage/lcov-report/src/tester/helpers.ts.html +313 -0
  21. package/coverage/lcov-report/src/tester/index.html +20 -35
  22. package/coverage/lcov.info +592 -436
  23. package/lib/assess-distribution/index.d.ts +1 -1
  24. package/lib/assess-distribution/index.js +102 -162
  25. package/lib/assess-distribution/index.js.map +1 -1
  26. package/lib/benchmark/index.js +87 -143
  27. package/lib/benchmark/index.js.map +1 -1
  28. package/lib/builder/allocator.d.ts +1 -1
  29. package/lib/builder/allocator.js +12 -12
  30. package/lib/builder/allocator.js.map +1 -1
  31. package/lib/builder/allocator.spec.js +22 -22
  32. package/lib/builder/allocator.spec.js.map +1 -1
  33. package/lib/builder/buildDatafile.d.ts +4 -3
  34. package/lib/builder/buildDatafile.js +311 -388
  35. package/lib/builder/buildDatafile.js.map +1 -1
  36. package/lib/builder/buildProject.d.ts +2 -1
  37. package/lib/builder/buildProject.js +96 -183
  38. package/lib/builder/buildProject.js.map +1 -1
  39. package/lib/builder/convertToV1.d.ts +10 -0
  40. package/lib/builder/convertToV1.js +119 -0
  41. package/lib/builder/convertToV1.js.map +1 -0
  42. package/lib/builder/getFeatureRanges.d.ts +1 -1
  43. package/lib/builder/getFeatureRanges.js +32 -105
  44. package/lib/builder/getFeatureRanges.js.map +1 -1
  45. package/lib/builder/hashes.d.ts +4 -0
  46. package/lib/builder/hashes.js +70 -0
  47. package/lib/builder/hashes.js.map +1 -0
  48. package/lib/builder/revision.js +2 -2
  49. package/lib/builder/revision.js.map +1 -1
  50. package/lib/builder/revision.spec.js +1 -1
  51. package/lib/builder/revision.spec.js.map +1 -1
  52. package/lib/builder/traffic.d.ts +1 -1
  53. package/lib/builder/traffic.js +57 -49
  54. package/lib/builder/traffic.js.map +1 -1
  55. package/lib/builder/traffic.spec.js +14 -14
  56. package/lib/builder/traffic.spec.js.map +1 -1
  57. package/lib/cli/cli.js +60 -129
  58. package/lib/cli/cli.js.map +1 -1
  59. package/lib/cli/plugins.js +14 -16
  60. package/lib/cli/plugins.js.map +1 -1
  61. package/lib/config/parsers.js +1 -1
  62. package/lib/config/parsers.js.map +1 -1
  63. package/lib/config/projectConfig.d.ts +8 -6
  64. package/lib/config/projectConfig.js +31 -72
  65. package/lib/config/projectConfig.js.map +1 -1
  66. package/lib/datasource/adapter.d.ts +1 -1
  67. package/lib/datasource/adapter.js +2 -5
  68. package/lib/datasource/adapter.js.map +1 -1
  69. package/lib/datasource/datasource.d.ts +2 -1
  70. package/lib/datasource/datasource.js +107 -148
  71. package/lib/datasource/datasource.js.map +1 -1
  72. package/lib/datasource/filesystemAdapter.d.ts +1 -1
  73. package/lib/datasource/filesystemAdapter.js +224 -360
  74. package/lib/datasource/filesystemAdapter.js.map +1 -1
  75. package/lib/evaluate/index.d.ts +1 -1
  76. package/lib/evaluate/index.js +120 -188
  77. package/lib/evaluate/index.js.map +1 -1
  78. package/lib/find-duplicate-segments/findDuplicateSegments.d.ts +1 -1
  79. package/lib/find-duplicate-segments/findDuplicateSegments.js +40 -128
  80. package/lib/find-duplicate-segments/findDuplicateSegments.js.map +1 -1
  81. package/lib/find-duplicate-segments/index.js +27 -82
  82. package/lib/find-duplicate-segments/index.js.map +1 -1
  83. package/lib/find-usage/index.d.ts +7 -5
  84. package/lib/find-usage/index.js +333 -507
  85. package/lib/find-usage/index.js.map +1 -1
  86. package/lib/generate-code/index.js +36 -91
  87. package/lib/generate-code/index.js.map +1 -1
  88. package/lib/generate-code/typescript.js +117 -157
  89. package/lib/generate-code/typescript.js.map +1 -1
  90. package/lib/index.d.ts +0 -1
  91. package/lib/index.js +0 -1
  92. package/lib/index.js.map +1 -1
  93. package/lib/info/index.js +45 -133
  94. package/lib/info/index.js.map +1 -1
  95. package/lib/init/index.d.ts +1 -1
  96. package/lib/init/index.js +16 -64
  97. package/lib/init/index.js.map +1 -1
  98. package/lib/linter/attributeSchema.d.ts +21 -6
  99. package/lib/linter/attributeSchema.js +18 -4
  100. package/lib/linter/attributeSchema.js.map +1 -1
  101. package/lib/linter/checkCircularDependency.d.ts +1 -1
  102. package/lib/linter/checkCircularDependency.js +22 -80
  103. package/lib/linter/checkCircularDependency.js.map +1 -1
  104. package/lib/linter/checkPercentageExceedingSlot.d.ts +1 -1
  105. package/lib/linter/checkPercentageExceedingSlot.js +36 -76
  106. package/lib/linter/checkPercentageExceedingSlot.js.map +1 -1
  107. package/lib/linter/conditionSchema.d.ts +1 -1
  108. package/lib/linter/conditionSchema.js +89 -41
  109. package/lib/linter/conditionSchema.js.map +1 -1
  110. package/lib/linter/featureSchema.d.ts +345 -197
  111. package/lib/linter/featureSchema.js +313 -172
  112. package/lib/linter/featureSchema.js.map +1 -1
  113. package/lib/linter/groupSchema.js +6 -6
  114. package/lib/linter/groupSchema.js.map +1 -1
  115. package/lib/linter/lintProject.js +306 -480
  116. package/lib/linter/lintProject.js.map +1 -1
  117. package/lib/linter/printError.js +7 -7
  118. package/lib/linter/printError.js.map +1 -1
  119. package/lib/linter/segmentSchema.js +2 -2
  120. package/lib/linter/segmentSchema.js.map +1 -1
  121. package/lib/linter/testSchema.d.ts +155 -3
  122. package/lib/linter/testSchema.js +47 -17
  123. package/lib/linter/testSchema.js.map +1 -1
  124. package/lib/list/index.d.ts +1 -0
  125. package/lib/list/index.js +349 -517
  126. package/lib/list/index.js.map +1 -1
  127. package/lib/{tester → list}/matrix.d.ts +1 -1
  128. package/lib/{tester → list}/matrix.js +50 -42
  129. package/lib/list/matrix.js.map +1 -0
  130. package/lib/{tester → list}/matrix.spec.js +7 -7
  131. package/lib/list/matrix.spec.js.map +1 -0
  132. package/lib/site/exportSite.js +25 -71
  133. package/lib/site/exportSite.js.map +1 -1
  134. package/lib/site/generateHistory.d.ts +1 -1
  135. package/lib/site/generateHistory.js +26 -82
  136. package/lib/site/generateHistory.js.map +1 -1
  137. package/lib/site/generateSiteSearchIndex.d.ts +1 -1
  138. package/lib/site/generateSiteSearchIndex.js +182 -259
  139. package/lib/site/generateSiteSearchIndex.js.map +1 -1
  140. package/lib/site/getLastModifiedFromHistory.d.ts +1 -1
  141. package/lib/site/getLastModifiedFromHistory.js +2 -2
  142. package/lib/site/getLastModifiedFromHistory.js.map +1 -1
  143. package/lib/site/getOwnerAndRepoFromUrl.js +6 -6
  144. package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -1
  145. package/lib/site/getRelativePaths.js +7 -7
  146. package/lib/site/getRelativePaths.js.map +1 -1
  147. package/lib/site/getRepoDetails.js +20 -20
  148. package/lib/site/getRepoDetails.js.map +1 -1
  149. package/lib/site/index.js +25 -73
  150. package/lib/site/index.js.map +1 -1
  151. package/lib/site/serveSite.js +10 -10
  152. package/lib/site/serveSite.js.map +1 -1
  153. package/lib/tester/helpers.d.ts +2 -0
  154. package/lib/tester/helpers.js +71 -0
  155. package/lib/tester/helpers.js.map +1 -0
  156. package/lib/tester/helpers.spec.js +115 -0
  157. package/lib/tester/helpers.spec.js.map +1 -0
  158. package/lib/tester/index.d.ts +0 -1
  159. package/lib/tester/index.js +0 -1
  160. package/lib/tester/index.js.map +1 -1
  161. package/lib/tester/prettyDuration.js +11 -11
  162. package/lib/tester/prettyDuration.js.map +1 -1
  163. package/lib/tester/printTestResult.d.ts +1 -1
  164. package/lib/tester/printTestResult.js +35 -15
  165. package/lib/tester/printTestResult.js.map +1 -1
  166. package/lib/tester/testFeature.d.ts +4 -2
  167. package/lib/tester/testFeature.js +264 -226
  168. package/lib/tester/testFeature.js.map +1 -1
  169. package/lib/tester/testProject.d.ts +3 -7
  170. package/lib/tester/testProject.js +145 -246
  171. package/lib/tester/testProject.js.map +1 -1
  172. package/lib/tester/testSegment.d.ts +5 -2
  173. package/lib/tester/testSegment.js +65 -102
  174. package/lib/tester/testSegment.js.map +1 -1
  175. package/lib/utils/extractKeys.d.ts +2 -1
  176. package/lib/utils/extractKeys.js +57 -12
  177. package/lib/utils/extractKeys.js.map +1 -1
  178. package/lib/utils/git.d.ts +1 -1
  179. package/lib/utils/git.js +23 -23
  180. package/lib/utils/git.js.map +1 -1
  181. package/lib/utils/pretty.js +2 -4
  182. package/lib/utils/pretty.js.map +1 -1
  183. package/package.json +5 -6
  184. package/src/assess-distribution/index.ts +3 -2
  185. package/src/benchmark/index.ts +3 -3
  186. package/src/builder/allocator.spec.ts +1 -1
  187. package/src/builder/allocator.ts +1 -1
  188. package/src/builder/buildDatafile.ts +161 -124
  189. package/src/builder/buildProject.ts +6 -3
  190. package/src/builder/convertToV1.ts +166 -0
  191. package/src/builder/getFeatureRanges.ts +1 -1
  192. package/src/builder/hashes.ts +109 -0
  193. package/src/builder/traffic.ts +40 -16
  194. package/src/cli/cli.ts +1 -1
  195. package/src/cli/plugins.ts +0 -2
  196. package/src/config/projectConfig.ts +13 -10
  197. package/src/datasource/adapter.ts +1 -1
  198. package/src/datasource/datasource.ts +23 -2
  199. package/src/datasource/filesystemAdapter.ts +11 -12
  200. package/src/evaluate/index.ts +7 -6
  201. package/src/find-duplicate-segments/findDuplicateSegments.ts +1 -1
  202. package/src/find-usage/index.ts +111 -44
  203. package/src/generate-code/index.ts +1 -3
  204. package/src/generate-code/typescript.ts +7 -29
  205. package/src/index.ts +0 -1
  206. package/src/info/index.ts +2 -2
  207. package/src/init/index.ts +2 -2
  208. package/src/linter/attributeSchema.ts +18 -2
  209. package/src/linter/checkCircularDependency.ts +1 -1
  210. package/src/linter/checkPercentageExceedingSlot.ts +28 -8
  211. package/src/linter/conditionSchema.ts +66 -10
  212. package/src/linter/featureSchema.ts +312 -116
  213. package/src/linter/lintProject.ts +9 -4
  214. package/src/linter/testSchema.ts +42 -3
  215. package/src/list/index.ts +18 -30
  216. package/src/{tester → list}/matrix.ts +33 -11
  217. package/src/site/exportSite.ts +2 -4
  218. package/src/site/generateHistory.ts +1 -1
  219. package/src/site/generateSiteSearchIndex.ts +58 -50
  220. package/src/site/getLastModifiedFromHistory.ts +1 -1
  221. package/src/tester/helpers.spec.ts +149 -0
  222. package/src/tester/helpers.ts +76 -0
  223. package/src/tester/index.ts +0 -1
  224. package/src/tester/printTestResult.ts +25 -3
  225. package/src/tester/testFeature.ts +270 -124
  226. package/src/tester/testProject.ts +28 -49
  227. package/src/tester/testSegment.ts +48 -40
  228. package/src/utils/extractKeys.ts +58 -1
  229. package/src/utils/git.ts +1 -1
  230. package/tsconfig.cjs.json +1 -0
  231. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +0 -151
  232. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +0 -157
  233. package/lib/restore/index.d.ts +0 -4
  234. package/lib/restore/index.js +0 -91
  235. package/lib/restore/index.js.map +0 -1
  236. package/lib/tester/checkIfArraysAreEqual.d.ts +0 -1
  237. package/lib/tester/checkIfArraysAreEqual.js +0 -18
  238. package/lib/tester/checkIfArraysAreEqual.js.map +0 -1
  239. package/lib/tester/checkIfObjectsAreEqual.d.ts +0 -1
  240. package/lib/tester/checkIfObjectsAreEqual.js +0 -23
  241. package/lib/tester/checkIfObjectsAreEqual.js.map +0 -1
  242. package/lib/tester/checkIfObjectsAreEqual.spec.js +0 -26
  243. package/lib/tester/checkIfObjectsAreEqual.spec.js.map +0 -1
  244. package/lib/tester/matrix.js.map +0 -1
  245. package/lib/tester/matrix.spec.js.map +0 -1
  246. package/src/restore/index.ts +0 -42
  247. package/src/tester/checkIfArraysAreEqual.ts +0 -16
  248. package/src/tester/checkIfObjectsAreEqual.spec.ts +0 -31
  249. package/src/tester/checkIfObjectsAreEqual.ts +0 -24
  250. /package/lib/{tester → list}/matrix.spec.d.ts +0 -0
  251. /package/lib/tester/{checkIfObjectsAreEqual.spec.d.ts → helpers.spec.d.ts} +0 -0
  252. /package/src/{tester → list}/matrix.spec.ts +0 -0
@@ -163,6 +163,7 @@ function superRefineVariableValue(
163
163
  });
164
164
  }
165
165
  }
166
+ // eslint-disable-next-line
166
167
  } catch (e) {
167
168
  ctx.addIssue({
168
169
  code: z.ZodIssueCode.custom,
@@ -175,6 +176,131 @@ function superRefineVariableValue(
175
176
  }
176
177
  }
177
178
 
179
+ function refineForce({
180
+ ctx,
181
+ parsedFeature, // eslint-disable-line
182
+ variableSchemaByKey,
183
+ variationValues,
184
+ force,
185
+ pathPrefix,
186
+ projectConfig,
187
+ }) {
188
+ force.forEach((f, fN) => {
189
+ // force[n].variation
190
+ if (f.variation) {
191
+ if (variationValues.indexOf(f.variation) === -1) {
192
+ ctx.addIssue({
193
+ code: z.ZodIssueCode.custom,
194
+ message: `Unknown variation "${f.variation}" in force`,
195
+ path: [...pathPrefix, fN, "variation"],
196
+ });
197
+ }
198
+ }
199
+
200
+ // force[n].variables[key]
201
+ if (f.variables) {
202
+ Object.keys(f.variables).forEach((variableKey) => {
203
+ superRefineVariableValue(
204
+ projectConfig,
205
+ variableSchemaByKey[variableKey],
206
+ f.variables[variableKey],
207
+ pathPrefix.concat([fN, "variables", variableKey]),
208
+ ctx,
209
+ );
210
+ });
211
+ }
212
+ });
213
+ }
214
+
215
+ function refineRules({
216
+ ctx,
217
+ parsedFeature,
218
+ variableSchemaByKey,
219
+ variationValues,
220
+ rules,
221
+ pathPrefix,
222
+ projectConfig,
223
+ }) {
224
+ rules.forEach((rule, ruleN) => {
225
+ // rules[n].variables[key]
226
+ if (rule.variables) {
227
+ Object.keys(rule.variables).forEach((variableKey) => {
228
+ superRefineVariableValue(
229
+ projectConfig,
230
+ variableSchemaByKey[variableKey],
231
+ rule.variables[variableKey],
232
+ pathPrefix.concat([ruleN, "variables", variableKey]),
233
+ ctx,
234
+ );
235
+ });
236
+ }
237
+
238
+ // rules[n].variationWeights
239
+ if (rule.variationWeights) {
240
+ if (!parsedFeature.variations) {
241
+ ctx.addIssue({
242
+ code: z.ZodIssueCode.custom,
243
+ message:
244
+ "Variation weights are overridden from rule, but no variations are present in feature.",
245
+ path: pathPrefix.concat([ruleN, "variationWeights"]),
246
+ });
247
+ } else {
248
+ const overriddenVariationValues = Object.keys(rule.variationWeights);
249
+ const overriddenVariationWeights: number[] = Object.values(rule.variationWeights);
250
+
251
+ // unique keys
252
+ if (overriddenVariationValues.length !== new Set(overriddenVariationValues).size) {
253
+ ctx.addIssue({
254
+ code: z.ZodIssueCode.custom,
255
+ message:
256
+ "Duplicate variation values found in variationWeights: " +
257
+ overriddenVariationValues.join(", "),
258
+ path: pathPrefix.concat([ruleN, "variationWeights"]),
259
+ });
260
+ }
261
+
262
+ // all original variations must be used
263
+ const missingVariations = variationValues.filter(
264
+ (v) => !overriddenVariationValues.includes(v),
265
+ );
266
+
267
+ if (missingVariations.length > 0) {
268
+ ctx.addIssue({
269
+ code: z.ZodIssueCode.custom,
270
+ message: "Missing variations: " + missingVariations.join(", "),
271
+ path: pathPrefix.concat([ruleN, "variationWeights"]),
272
+ });
273
+ }
274
+
275
+ // unknown variations
276
+ const unknownVariations = overriddenVariationValues.filter(
277
+ (v) => !variationValues.includes(v),
278
+ );
279
+
280
+ if (unknownVariations.length > 0) {
281
+ ctx.addIssue({
282
+ code: z.ZodIssueCode.custom,
283
+ message:
284
+ "Variation weights contain unknown variations: " + unknownVariations.join(", "),
285
+ path: pathPrefix.concat([ruleN, "variationWeights"]),
286
+ });
287
+ }
288
+
289
+ // weights sum must be 100
290
+ const weightsSum = overriddenVariationWeights.reduce((sum, weight) => sum + weight, 0);
291
+
292
+ if (weightsSum !== 100) {
293
+ ctx.addIssue({
294
+ code: z.ZodIssueCode.custom,
295
+ message: "Variation weights must sum to 100",
296
+ path: pathPrefix.concat([ruleN, "variationWeights"]),
297
+ });
298
+ }
299
+ }
300
+ }
301
+ });
302
+ }
303
+
178
304
  export function getFeatureZodSchema(
179
305
  projectConfig: ProjectConfig,
180
306
  conditionsZodSchema,
@@ -243,6 +369,7 @@ export function getFeatureZodSchema(
243
369
  enabled: z.boolean().optional(),
244
370
  variation: variationValueZodSchema.optional(),
245
371
  variables: z.record(variableValueZodSchema).optional(),
372
+ variationWeights: z.record(z.number().min(0).max(100)).optional(),
246
373
  })
247
374
  .strict(),
248
375
  )
@@ -304,26 +431,6 @@ export function getFeatureZodSchema(
304
431
  )
305
432
  .optional();
306
433
 
307
- const environmentZodSchema = z
308
- .object({
309
- expose: exposeSchema,
310
- rules: rulesSchema,
311
- force: forceSchema,
312
- })
313
- .strict();
314
-
315
- let allEnvironmentsZodSchema: z.ZodTypeAny = z.never();
316
-
317
- if (Array.isArray(projectConfig.environments)) {
318
- const allEnvironmentsSchema = {};
319
-
320
- projectConfig.environments.forEach((environmentKey) => {
321
- allEnvironmentsSchema[environmentKey] = environmentZodSchema;
322
- });
323
-
324
- allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
325
- }
326
-
327
434
  const attributeKeyZodSchema = z.string().refine(
328
435
  (value) => value === "*" || availableAttributeKeys.includes(value),
329
436
  (value) => ({
@@ -338,11 +445,14 @@ export function getFeatureZodSchema(
338
445
  }),
339
446
  );
340
447
 
448
+ const environmentKeys = projectConfig.environments || [];
449
+
341
450
  const featureZodSchema = z
342
451
  .object({
343
452
  archived: z.boolean().optional(),
344
453
  deprecated: z.boolean().optional(),
345
454
  description: z.string(),
455
+
346
456
  tags: z
347
457
  .array(
348
458
  z.string().refine(
@@ -360,6 +470,7 @@ export function getFeatureZodSchema(
360
470
  message: "Duplicate tags found: " + value.join(", "),
361
471
  }),
362
472
  ),
473
+
363
474
  required: z
364
475
  .array(
365
476
  z.union([
@@ -373,6 +484,7 @@ export function getFeatureZodSchema(
373
484
  ]),
374
485
  )
375
486
  .optional(),
487
+
376
488
  bucketBy: z.union([
377
489
  attributeKeyZodSchema,
378
490
  z.array(attributeKeyZodSchema),
@@ -382,34 +494,24 @@ export function getFeatureZodSchema(
382
494
  })
383
495
  .strict(),
384
496
  ]),
385
- // @TODO: in v2, this will become a dictionary
497
+
386
498
  variablesSchema: z
387
- .array(
499
+ .record(
388
500
  z
389
501
  .object({
390
502
  deprecated: z.boolean().optional(),
391
- key: z
392
- .string()
393
- .min(1)
394
- .refine((value) => value !== "variation", {
395
- message: `variable key cannot be "variation"`,
396
- }),
397
503
  type: z.enum(["string", "integer", "boolean", "double", "array", "object", "json"]),
398
504
  description: z.string().optional(),
399
505
  defaultValue: variableValueZodSchema,
506
+ useDefaultWhenDisabled: z.boolean().optional(),
507
+ disabledValue: variableValueZodSchema.optional(),
400
508
  })
401
509
  .strict(),
402
510
  )
403
- .refine(
404
- (value) => {
405
- const keys = value.map((v) => v.key);
406
- return keys.length === new Set(keys).size;
407
- },
408
- (value) => ({
409
- message: "Duplicate variable keys found: " + value.map((v) => v.key).join(", "),
410
- }),
411
- )
412
511
  .optional(),
512
+
513
+ disabledVariationValue: variationValueZodSchema.optional(),
514
+
413
515
  variations: z
414
516
  .array(
415
517
  z
@@ -417,32 +519,25 @@ export function getFeatureZodSchema(
417
519
  description: z.string().optional(),
418
520
  value: variationValueZodSchema,
419
521
  weight: z.number().min(0).max(100),
420
- variables: z
421
- .array(
422
- z
423
- .object({
424
- key: z.string().min(1),
425
- value: variableValueZodSchema,
426
- overrides: z
427
- .array(
428
- z.union([
429
- z
430
- .object({
431
- conditions: conditionsZodSchema,
432
- value: variableValueZodSchema,
433
- })
434
- .strict(),
435
- z
436
- .object({
437
- segments: groupSegmentsZodSchema,
438
- value: variableValueZodSchema,
439
- })
440
- .strict(),
441
- ]),
442
- )
443
- .optional(),
444
- })
445
- .strict(),
522
+ variables: z.record(variableValueZodSchema).optional(),
523
+ variableOverrides: z
524
+ .record(
525
+ z.array(
526
+ z.union([
527
+ z
528
+ .object({
529
+ conditions: conditionsZodSchema,
530
+ value: variableValueZodSchema,
531
+ })
532
+ .strict(),
533
+ z
534
+ .object({
535
+ segments: groupSegmentsZodSchema,
536
+ value: variableValueZodSchema,
537
+ })
538
+ .strict(),
539
+ ]),
540
+ ),
446
541
  )
447
542
  .optional(),
448
543
  })
@@ -459,94 +554,195 @@ export function getFeatureZodSchema(
459
554
  )
460
555
  .optional(),
461
556
 
462
- // with environments
463
- environments: Array.isArray(projectConfig.environments)
464
- ? allEnvironmentsZodSchema
465
- : z.never().optional(),
557
+ expose:
558
+ projectConfig.environments === false
559
+ ? exposeSchema.optional()
560
+ : z.record(z.enum(environmentKeys as [string, ...string[]]), exposeSchema).optional(),
466
561
 
467
- // no environments
468
- expose: projectConfig.environments === false ? exposeSchema : z.never().optional(),
469
- rules: projectConfig.environments === false ? rulesSchema : z.never().optional(),
470
- force: projectConfig.environments === false ? forceSchema : z.never().optional(),
562
+ force:
563
+ projectConfig.environments === false
564
+ ? forceSchema
565
+ : z.record(z.enum(environmentKeys as [string, ...string[]]), forceSchema).optional(),
566
+
567
+ rules:
568
+ projectConfig.environments === false
569
+ ? rulesSchema
570
+ : z.record(z.enum(environmentKeys as [string, ...string[]]), rulesSchema),
471
571
  })
472
572
  .strict()
473
573
  .superRefine((value, ctx) => {
574
+ // disabledVariationValue
575
+ if (value.disabledVariationValue) {
576
+ if (!value.variations) {
577
+ ctx.addIssue({
578
+ code: z.ZodIssueCode.custom,
579
+ message: "Disabled variation value is set, but no variations are present in feature.",
580
+ path: ["disabledVariationValue"],
581
+ });
582
+ } else {
583
+ const variationValues = value.variations.map((v) => v.value);
584
+
585
+ if (variationValues.indexOf(value.disabledVariationValue) === -1) {
586
+ ctx.addIssue({
587
+ code: z.ZodIssueCode.custom,
588
+ message: `Disabled variation value "${value.disabledVariationValue}" is not one of the defined variations: ${variationValues.join(", ")}`,
589
+ path: ["disabledVariationValue"],
590
+ });
591
+ }
592
+ }
593
+ }
594
+
474
595
  if (!value.variablesSchema) {
475
596
  return;
476
597
  }
477
598
 
478
- const allVariablesSchema = value.variablesSchema;
479
- const variableSchemaByKey = {};
599
+ const variableSchemaByKey = value.variablesSchema;
600
+ const variationValues: string[] = [];
601
+
602
+ if (value.variations) {
603
+ value.variations.forEach((variation) => {
604
+ variationValues.push(variation.value);
605
+ });
606
+ }
607
+
608
+ // variablesSchema[key]
609
+ const variableKeys = Object.keys(variableSchemaByKey);
610
+ variableKeys.forEach((variableKey) => {
611
+ const variableSchema = variableSchemaByKey[variableKey];
612
+
613
+ if (variableKey === "variation") {
614
+ ctx.addIssue({
615
+ code: z.ZodIssueCode.custom,
616
+ message: `Variable key "${variableKey}" is reserved and cannot be used.`,
617
+ path: ["variablesSchema", variableKey],
618
+ });
619
+ }
480
620
 
481
- // variablesSchema[n].defaultValue
482
- allVariablesSchema.forEach((variableSchema, n) => {
483
- variableSchemaByKey[variableSchema.key] = variableSchema;
621
+ // defaultValue
622
+ superRefineVariableValue(
623
+ projectConfig,
624
+ variableSchema,
625
+ variableSchema.defaultValue,
626
+ ["variablesSchema", variableKey, "defaultValue"],
627
+ ctx,
628
+ );
484
629
 
630
+ // disabledValue
485
631
  superRefineVariableValue(
486
632
  projectConfig,
487
633
  variableSchema,
488
634
  variableSchema.defaultValue,
489
- ["variablesSchema", n, "defaultValue"],
635
+ ["variablesSchema", variableKey, "disabledValue"],
490
636
  ctx,
491
637
  );
492
638
  });
493
639
 
494
- // variations[n].variables[n].value
640
+ // variations
495
641
  if (value.variations) {
496
642
  value.variations.forEach((variation, variationN) => {
497
643
  if (!variation.variables) {
498
644
  return;
499
645
  }
500
646
 
501
- variation.variables.forEach((variable, variableN) => {
647
+ // variations[n].variables[key]
648
+ for (const variableKey of Object.keys(variation.variables)) {
649
+ const variableValue = variation.variables[variableKey];
650
+
502
651
  superRefineVariableValue(
503
652
  projectConfig,
504
- variableSchemaByKey[variable.key],
505
- variable.value,
506
- ["variations", variationN, "variables", variableN, "value"],
653
+ variableSchemaByKey[variableKey],
654
+ variableValue,
655
+ ["variations", variationN, "variables", variableKey],
507
656
  ctx,
508
657
  );
509
658
 
510
- // variations[n].variables[n].overrides[n].value
511
- if (variable.overrides) {
512
- variable.overrides.forEach((override, overrideN) => {
513
- superRefineVariableValue(
514
- projectConfig,
515
- variableSchemaByKey[variable.key],
516
- override.value,
517
- [
518
- "variations",
519
- variationN,
520
- "variables",
521
- variableN,
522
- "overrides",
523
- overrideN,
524
- "value",
525
- ],
526
- ctx,
527
- );
528
- });
659
+ // variations[n].variableOverrides[n].value
660
+ if (variation.variableOverrides) {
661
+ for (const variableKey of Object.keys(variation.variableOverrides)) {
662
+ const overrides = variation.variableOverrides[variableKey];
663
+
664
+ if (Array.isArray(overrides)) {
665
+ overrides.forEach((override, overrideN) => {
666
+ superRefineVariableValue(
667
+ projectConfig,
668
+ variableSchemaByKey[variableKey],
669
+ override.value,
670
+ [
671
+ "variations",
672
+ variationN,
673
+ "variableOverrides",
674
+ variableKey,
675
+ overrideN,
676
+ "value",
677
+ ],
678
+ ctx,
679
+ );
680
+ });
681
+ }
682
+ }
529
683
  }
530
- });
684
+ }
531
685
  });
532
686
  }
533
687
 
534
- // environments[key].rules[n].variables[key]
535
- Object.keys(value.environments).forEach((environmentKey) => {
536
- value.environments[environmentKey].rules.forEach((rule, ruleN) => {
537
- if (rule.variables) {
538
- Object.keys(rule.variables).forEach((variableKey) => {
539
- superRefineVariableValue(
540
- projectConfig,
541
- variableSchemaByKey[variableKey],
542
- rule.variables[variableKey],
543
- ["environments", environmentKey, "rules", ruleN, "variables", variableKey],
544
- ctx,
545
- );
688
+ if (environmentKeys.length > 0) {
689
+ // with environments
690
+ for (const environmentKey of environmentKeys) {
691
+ // rules
692
+ if (value.rules && value.rules[environmentKey]) {
693
+ refineRules({
694
+ parsedFeature: value,
695
+ variableSchemaByKey,
696
+ variationValues,
697
+ rules: value.rules[environmentKey],
698
+ pathPrefix: ["rules", environmentKey],
699
+ ctx,
700
+ projectConfig,
546
701
  });
547
702
  }
548
- });
549
- });
703
+
704
+ // force
705
+ if (value.force && value.force[environmentKey]) {
706
+ refineForce({
707
+ parsedFeature: value,
708
+ variableSchemaByKey,
709
+ variationValues,
710
+ force: value.force[environmentKey],
711
+ pathPrefix: ["force", environmentKey],
712
+ ctx,
713
+ projectConfig,
714
+ });
715
+ }
716
+ }
717
+ } else {
718
+ // no environments
719
+
720
+ // rules
721
+ if (value.rules) {
722
+ refineRules({
723
+ parsedFeature: value,
724
+ variableSchemaByKey,
725
+ variationValues,
726
+ rules: value.rules,
727
+ pathPrefix: ["rules"],
728
+ ctx,
729
+ projectConfig,
730
+ });
731
+ }
732
+
733
+ // force
734
+ if (value.force) {
735
+ refineForce({
736
+ parsedFeature: value,
737
+ variableSchemaByKey,
738
+ variationValues,
739
+ force: value.force,
740
+ pathPrefix: ["force"],
741
+ ctx,
742
+ projectConfig,
743
+ });
744
+ }
745
+ }
550
746
  });
551
747
 
552
748
  return featureZodSchema;
@@ -24,6 +24,9 @@ export interface LintProjectOptions {
24
24
  const ENTITY_NAME_REGEX = /^[a-zA-Z0-9_\-./]+$/;
25
25
  const ENTITY_NAME_REGEX_ERROR = "Names must be alphanumeric and can contain _, -, and .";
26
26
 
27
+ const ATTRIBUTE_NAME_REGEX = /^[a-zA-Z0-9_\-/]+$/;
28
+ const ATTRIBUTE_NAME_REGEX_ERROR = "Names must be alphanumeric and can contain _, and -";
29
+
27
30
  async function getAuthorsOfEntity(datasource, entityType, entityKey): Promise<string[]> {
28
31
  const entries = await datasource.listHistoryEntries(entityType, entityKey);
29
32
  const authors: string[] = Array.from(new Set(entries.map((entry) => entry.author)));
@@ -88,7 +91,7 @@ export async function lintProject(
88
91
  for (const key of filteredKeys) {
89
92
  const fullPath = getFullPathFromKey("attribute", key);
90
93
 
91
- if (!ENTITY_NAME_REGEX.test(key)) {
94
+ if (!ATTRIBUTE_NAME_REGEX.test(key)) {
92
95
  console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
93
96
 
94
97
  if (options.authors) {
@@ -97,7 +100,7 @@ export async function lintProject(
97
100
  }
98
101
 
99
102
  console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
100
- console.log(CLI_FORMAT_RED, ` ${ENTITY_NAME_REGEX_ERROR}`);
103
+ console.log(CLI_FORMAT_RED, ` ${ATTRIBUTE_NAME_REGEX_ERROR}`);
101
104
  console.log("");
102
105
  hasError = true;
103
106
  }
@@ -137,11 +140,13 @@ export async function lintProject(
137
140
  }
138
141
  }
139
142
 
143
+ const flattenedAttributes = await datasource.listFlattenedAttributes();
144
+
140
145
  // lint segments
141
146
  const segments = await datasource.listSegments();
142
147
  const conditionsZodSchema = getConditionsZodSchema(
143
148
  projectConfig,
144
- attributes as [string, ...string[]],
149
+ flattenedAttributes as [string, ...string[]],
145
150
  );
146
151
  const segmentZodSchema = getSegmentZodSchema(projectConfig, conditionsZodSchema);
147
152
 
@@ -209,7 +214,7 @@ export async function lintProject(
209
214
  const featureZodSchema = getFeatureZodSchema(
210
215
  projectConfig,
211
216
  conditionsZodSchema,
212
- attributes as [string, ...string[]],
217
+ flattenedAttributes as [string, ...string[]],
213
218
  segments as [string, ...string[]],
214
219
  features as [string, ...string[]],
215
220
  );
@@ -82,10 +82,49 @@ export function getTestsZodSchema(
82
82
  }),
83
83
  )
84
84
  : z.never().optional(),
85
- context: z.record(z.unknown()),
86
- expectedToBeEnabled: z.boolean(),
87
- expectedVariation: z.string().optional(),
85
+
86
+ // parent
87
+ sticky: z.record(z.record(z.any())).optional(),
88
+ context: z.record(z.unknown()).optional(),
89
+
90
+ defaultVariationValue: z.string().optional(),
91
+ defaultVariableValues: z.record(z.unknown()).optional(),
92
+
93
+ expectedToBeEnabled: z.boolean().optional(),
94
+ expectedVariation: z.string().nullable().optional(),
88
95
  expectedVariables: z.record(z.unknown()).optional(),
96
+ expectedEvaluations: z
97
+ .object({
98
+ flag: z.record(z.any()).optional(),
99
+ variation: z.record(z.any()).optional(),
100
+ variables: z.record(z.record(z.any())).optional(),
101
+ })
102
+ .optional(),
103
+
104
+ children: z
105
+ .array(
106
+ z.object({
107
+ // copied from parent
108
+ sticky: z.record(z.record(z.any())).optional(),
109
+ context: z.record(z.unknown()).optional(),
110
+
111
+ defaultVariationValue: z.string().optional(),
112
+ defaultVariableValues: z.record(z.unknown()).optional(),
113
+
114
+ expectedToBeEnabled: z.boolean().optional(),
115
+ expectedVariation: z.string().nullable().optional(),
116
+ expectedVariables: z.record(z.unknown()).optional(),
117
+
118
+ expectedEvaluations: z
119
+ .object({
120
+ flag: z.record(z.any()).optional(),
121
+ variation: z.record(z.any()).optional(),
122
+ variables: z.record(z.record(z.any())).optional(),
123
+ })
124
+ .optional(),
125
+ }),
126
+ )
127
+ .optional(),
89
128
  })
90
129
  .strict(),
91
130
  ),