@featurevisor/core 1.35.3 → 2.0.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 (251) hide show
  1. package/README.md +0 -6
  2. package/coverage/clover.xml +321 -237
  3. package/coverage/coverage-final.json +8 -8
  4. package/coverage/lcov-report/index.html +77 -47
  5. package/coverage/lcov-report/lib/builder/allocator.js.html +14 -14
  6. package/coverage/lcov-report/lib/builder/index.html +16 -16
  7. package/coverage/lcov-report/lib/builder/revision.js.html +3 -3
  8. package/coverage/lcov-report/lib/builder/traffic.js.html +88 -64
  9. package/coverage/lcov-report/lib/list/index.html +116 -0
  10. package/coverage/lcov-report/lib/{tester → list}/matrix.js.html +90 -66
  11. package/coverage/lcov-report/lib/tester/helpers.js.html +295 -0
  12. package/coverage/lcov-report/lib/tester/index.html +20 -35
  13. package/coverage/lcov-report/src/builder/allocator.ts.html +2 -2
  14. package/coverage/lcov-report/src/builder/index.html +15 -15
  15. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  16. package/coverage/lcov-report/src/builder/traffic.ts.html +96 -24
  17. package/coverage/lcov-report/src/list/index.html +116 -0
  18. package/coverage/lcov-report/src/{tester → list}/matrix.ts.html +87 -21
  19. package/coverage/lcov-report/src/tester/helpers.ts.html +313 -0
  20. package/coverage/lcov-report/src/tester/index.html +20 -35
  21. package/coverage/lcov.info +592 -436
  22. package/lib/assess-distribution/index.d.ts +1 -1
  23. package/lib/assess-distribution/index.js +102 -162
  24. package/lib/assess-distribution/index.js.map +1 -1
  25. package/lib/benchmark/index.js +87 -143
  26. package/lib/benchmark/index.js.map +1 -1
  27. package/lib/builder/allocator.d.ts +1 -1
  28. package/lib/builder/allocator.js +12 -12
  29. package/lib/builder/allocator.js.map +1 -1
  30. package/lib/builder/allocator.spec.js +22 -22
  31. package/lib/builder/allocator.spec.js.map +1 -1
  32. package/lib/builder/buildDatafile.d.ts +4 -3
  33. package/lib/builder/buildDatafile.js +311 -388
  34. package/lib/builder/buildDatafile.js.map +1 -1
  35. package/lib/builder/buildProject.d.ts +2 -1
  36. package/lib/builder/buildProject.js +96 -183
  37. package/lib/builder/buildProject.js.map +1 -1
  38. package/lib/builder/convertToV1.d.ts +10 -0
  39. package/lib/builder/convertToV1.js +119 -0
  40. package/lib/builder/convertToV1.js.map +1 -0
  41. package/lib/builder/getFeatureRanges.d.ts +1 -1
  42. package/lib/builder/getFeatureRanges.js +32 -105
  43. package/lib/builder/getFeatureRanges.js.map +1 -1
  44. package/lib/builder/hashes.d.ts +4 -0
  45. package/lib/builder/hashes.js +70 -0
  46. package/lib/builder/hashes.js.map +1 -0
  47. package/lib/builder/revision.js +2 -2
  48. package/lib/builder/revision.js.map +1 -1
  49. package/lib/builder/revision.spec.js +1 -1
  50. package/lib/builder/revision.spec.js.map +1 -1
  51. package/lib/builder/traffic.d.ts +1 -1
  52. package/lib/builder/traffic.js +57 -49
  53. package/lib/builder/traffic.js.map +1 -1
  54. package/lib/builder/traffic.spec.js +14 -14
  55. package/lib/builder/traffic.spec.js.map +1 -1
  56. package/lib/cli/cli.js +60 -129
  57. package/lib/cli/cli.js.map +1 -1
  58. package/lib/cli/plugins.js +14 -16
  59. package/lib/cli/plugins.js.map +1 -1
  60. package/lib/config/parsers.js +1 -1
  61. package/lib/config/parsers.js.map +1 -1
  62. package/lib/config/projectConfig.d.ts +8 -6
  63. package/lib/config/projectConfig.js +31 -72
  64. package/lib/config/projectConfig.js.map +1 -1
  65. package/lib/datasource/adapter.d.ts +1 -1
  66. package/lib/datasource/adapter.js +2 -5
  67. package/lib/datasource/adapter.js.map +1 -1
  68. package/lib/datasource/datasource.d.ts +2 -1
  69. package/lib/datasource/datasource.js +107 -148
  70. package/lib/datasource/datasource.js.map +1 -1
  71. package/lib/datasource/filesystemAdapter.d.ts +1 -1
  72. package/lib/datasource/filesystemAdapter.js +224 -360
  73. package/lib/datasource/filesystemAdapter.js.map +1 -1
  74. package/lib/evaluate/index.d.ts +1 -1
  75. package/lib/evaluate/index.js +120 -188
  76. package/lib/evaluate/index.js.map +1 -1
  77. package/lib/find-duplicate-segments/findDuplicateSegments.d.ts +1 -1
  78. package/lib/find-duplicate-segments/findDuplicateSegments.js +40 -128
  79. package/lib/find-duplicate-segments/findDuplicateSegments.js.map +1 -1
  80. package/lib/find-duplicate-segments/index.js +27 -82
  81. package/lib/find-duplicate-segments/index.js.map +1 -1
  82. package/lib/find-usage/index.d.ts +7 -5
  83. package/lib/find-usage/index.js +333 -507
  84. package/lib/find-usage/index.js.map +1 -1
  85. package/lib/generate-code/index.js +36 -91
  86. package/lib/generate-code/index.js.map +1 -1
  87. package/lib/generate-code/typescript.js +117 -157
  88. package/lib/generate-code/typescript.js.map +1 -1
  89. package/lib/index.d.ts +0 -1
  90. package/lib/index.js +0 -1
  91. package/lib/index.js.map +1 -1
  92. package/lib/info/index.js +45 -133
  93. package/lib/info/index.js.map +1 -1
  94. package/lib/init/index.d.ts +1 -1
  95. package/lib/init/index.js +16 -64
  96. package/lib/init/index.js.map +1 -1
  97. package/lib/linter/attributeSchema.d.ts +21 -6
  98. package/lib/linter/attributeSchema.js +18 -4
  99. package/lib/linter/attributeSchema.js.map +1 -1
  100. package/lib/linter/checkCircularDependency.d.ts +1 -1
  101. package/lib/linter/checkCircularDependency.js +22 -80
  102. package/lib/linter/checkCircularDependency.js.map +1 -1
  103. package/lib/linter/checkPercentageExceedingSlot.d.ts +1 -1
  104. package/lib/linter/checkPercentageExceedingSlot.js +36 -76
  105. package/lib/linter/checkPercentageExceedingSlot.js.map +1 -1
  106. package/lib/linter/conditionSchema.d.ts +1 -1
  107. package/lib/linter/conditionSchema.js +89 -41
  108. package/lib/linter/conditionSchema.js.map +1 -1
  109. package/lib/linter/featureSchema.d.ts +345 -197
  110. package/lib/linter/featureSchema.js +313 -172
  111. package/lib/linter/featureSchema.js.map +1 -1
  112. package/lib/linter/groupSchema.js +6 -6
  113. package/lib/linter/groupSchema.js.map +1 -1
  114. package/lib/linter/lintProject.js +306 -480
  115. package/lib/linter/lintProject.js.map +1 -1
  116. package/lib/linter/printError.js +7 -7
  117. package/lib/linter/printError.js.map +1 -1
  118. package/lib/linter/segmentSchema.js +2 -2
  119. package/lib/linter/segmentSchema.js.map +1 -1
  120. package/lib/linter/testSchema.d.ts +155 -3
  121. package/lib/linter/testSchema.js +47 -17
  122. package/lib/linter/testSchema.js.map +1 -1
  123. package/lib/list/index.d.ts +1 -0
  124. package/lib/list/index.js +349 -517
  125. package/lib/list/index.js.map +1 -1
  126. package/lib/{tester → list}/matrix.d.ts +1 -1
  127. package/lib/{tester → list}/matrix.js +50 -42
  128. package/lib/list/matrix.js.map +1 -0
  129. package/lib/{tester → list}/matrix.spec.js +7 -7
  130. package/lib/list/matrix.spec.js.map +1 -0
  131. package/lib/site/exportSite.js +25 -71
  132. package/lib/site/exportSite.js.map +1 -1
  133. package/lib/site/generateHistory.d.ts +1 -1
  134. package/lib/site/generateHistory.js +26 -82
  135. package/lib/site/generateHistory.js.map +1 -1
  136. package/lib/site/generateSiteSearchIndex.d.ts +1 -1
  137. package/lib/site/generateSiteSearchIndex.js +182 -259
  138. package/lib/site/generateSiteSearchIndex.js.map +1 -1
  139. package/lib/site/getLastModifiedFromHistory.d.ts +1 -1
  140. package/lib/site/getLastModifiedFromHistory.js +2 -2
  141. package/lib/site/getLastModifiedFromHistory.js.map +1 -1
  142. package/lib/site/getOwnerAndRepoFromUrl.js +6 -6
  143. package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -1
  144. package/lib/site/getRelativePaths.js +7 -7
  145. package/lib/site/getRelativePaths.js.map +1 -1
  146. package/lib/site/getRepoDetails.js +20 -20
  147. package/lib/site/getRepoDetails.js.map +1 -1
  148. package/lib/site/index.js +25 -73
  149. package/lib/site/index.js.map +1 -1
  150. package/lib/site/serveSite.js +10 -10
  151. package/lib/site/serveSite.js.map +1 -1
  152. package/lib/tester/helpers.d.ts +2 -0
  153. package/lib/tester/helpers.js +71 -0
  154. package/lib/tester/helpers.js.map +1 -0
  155. package/lib/tester/helpers.spec.js +115 -0
  156. package/lib/tester/helpers.spec.js.map +1 -0
  157. package/lib/tester/index.d.ts +0 -1
  158. package/lib/tester/index.js +0 -1
  159. package/lib/tester/index.js.map +1 -1
  160. package/lib/tester/prettyDuration.js +11 -11
  161. package/lib/tester/prettyDuration.js.map +1 -1
  162. package/lib/tester/printTestResult.d.ts +1 -1
  163. package/lib/tester/printTestResult.js +35 -15
  164. package/lib/tester/printTestResult.js.map +1 -1
  165. package/lib/tester/testFeature.d.ts +4 -2
  166. package/lib/tester/testFeature.js +264 -226
  167. package/lib/tester/testFeature.js.map +1 -1
  168. package/lib/tester/testProject.d.ts +3 -7
  169. package/lib/tester/testProject.js +145 -246
  170. package/lib/tester/testProject.js.map +1 -1
  171. package/lib/tester/testSegment.d.ts +5 -2
  172. package/lib/tester/testSegment.js +65 -102
  173. package/lib/tester/testSegment.js.map +1 -1
  174. package/lib/utils/extractKeys.d.ts +2 -1
  175. package/lib/utils/extractKeys.js +57 -12
  176. package/lib/utils/extractKeys.js.map +1 -1
  177. package/lib/utils/git.d.ts +1 -1
  178. package/lib/utils/git.js +23 -23
  179. package/lib/utils/git.js.map +1 -1
  180. package/lib/utils/pretty.js +2 -4
  181. package/lib/utils/pretty.js.map +1 -1
  182. package/package.json +5 -6
  183. package/src/assess-distribution/index.ts +3 -2
  184. package/src/benchmark/index.ts +3 -3
  185. package/src/builder/allocator.spec.ts +1 -1
  186. package/src/builder/allocator.ts +1 -1
  187. package/src/builder/buildDatafile.ts +161 -124
  188. package/src/builder/buildProject.ts +6 -3
  189. package/src/builder/convertToV1.ts +166 -0
  190. package/src/builder/getFeatureRanges.ts +1 -1
  191. package/src/builder/hashes.ts +109 -0
  192. package/src/builder/traffic.ts +40 -16
  193. package/src/cli/cli.ts +1 -1
  194. package/src/cli/plugins.ts +0 -2
  195. package/src/config/projectConfig.ts +13 -10
  196. package/src/datasource/adapter.ts +1 -1
  197. package/src/datasource/datasource.ts +23 -2
  198. package/src/datasource/filesystemAdapter.ts +11 -12
  199. package/src/evaluate/index.ts +7 -6
  200. package/src/find-duplicate-segments/findDuplicateSegments.ts +1 -1
  201. package/src/find-usage/index.ts +111 -44
  202. package/src/generate-code/index.ts +1 -3
  203. package/src/generate-code/typescript.ts +7 -29
  204. package/src/index.ts +0 -1
  205. package/src/info/index.ts +2 -2
  206. package/src/init/index.ts +2 -2
  207. package/src/linter/attributeSchema.ts +18 -2
  208. package/src/linter/checkCircularDependency.ts +1 -1
  209. package/src/linter/checkPercentageExceedingSlot.ts +28 -8
  210. package/src/linter/conditionSchema.ts +66 -10
  211. package/src/linter/featureSchema.ts +312 -116
  212. package/src/linter/lintProject.ts +9 -4
  213. package/src/linter/testSchema.ts +42 -3
  214. package/src/list/index.ts +18 -30
  215. package/src/{tester → list}/matrix.ts +33 -11
  216. package/src/site/exportSite.ts +2 -4
  217. package/src/site/generateHistory.ts +1 -1
  218. package/src/site/generateSiteSearchIndex.ts +58 -50
  219. package/src/site/getLastModifiedFromHistory.ts +1 -1
  220. package/src/tester/helpers.spec.ts +149 -0
  221. package/src/tester/helpers.ts +76 -0
  222. package/src/tester/index.ts +0 -1
  223. package/src/tester/printTestResult.ts +25 -3
  224. package/src/tester/testFeature.ts +270 -124
  225. package/src/tester/testProject.ts +28 -49
  226. package/src/tester/testSegment.ts +48 -40
  227. package/src/utils/extractKeys.ts +58 -1
  228. package/src/utils/git.ts +1 -1
  229. package/tsconfig.cjs.json +1 -0
  230. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +0 -151
  231. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +0 -157
  232. package/lib/restore/index.d.ts +0 -4
  233. package/lib/restore/index.js +0 -91
  234. package/lib/restore/index.js.map +0 -1
  235. package/lib/tester/checkIfArraysAreEqual.d.ts +0 -1
  236. package/lib/tester/checkIfArraysAreEqual.js +0 -18
  237. package/lib/tester/checkIfArraysAreEqual.js.map +0 -1
  238. package/lib/tester/checkIfObjectsAreEqual.d.ts +0 -1
  239. package/lib/tester/checkIfObjectsAreEqual.js +0 -23
  240. package/lib/tester/checkIfObjectsAreEqual.js.map +0 -1
  241. package/lib/tester/checkIfObjectsAreEqual.spec.js +0 -26
  242. package/lib/tester/checkIfObjectsAreEqual.spec.js.map +0 -1
  243. package/lib/tester/matrix.js.map +0 -1
  244. package/lib/tester/matrix.spec.js.map +0 -1
  245. package/src/restore/index.ts +0 -42
  246. package/src/tester/checkIfArraysAreEqual.ts +0 -16
  247. package/src/tester/checkIfObjectsAreEqual.spec.ts +0 -31
  248. package/src/tester/checkIfObjectsAreEqual.ts +0 -24
  249. /package/lib/{tester → list}/matrix.spec.d.ts +0 -0
  250. /package/lib/tester/{checkIfObjectsAreEqual.spec.d.ts → helpers.spec.d.ts} +0 -0
  251. /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
  ),