@featurevisor/core 1.30.1 → 1.32.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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +11 -11
  3. package/coverage/coverage-final.json +2 -2
  4. package/coverage/lcov-report/index.html +7 -7
  5. package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
  6. package/coverage/lcov-report/lib/builder/index.html +1 -1
  7. package/coverage/lcov-report/lib/builder/revision.js.html +1 -1
  8. package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
  9. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
  10. package/coverage/lcov-report/lib/tester/index.html +5 -5
  11. package/coverage/lcov-report/lib/tester/matrix.js.html +5 -5
  12. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  13. package/coverage/lcov-report/src/builder/index.html +1 -1
  14. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  15. package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
  16. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
  17. package/coverage/lcov-report/src/tester/index.html +5 -5
  18. package/coverage/lcov-report/src/tester/matrix.ts.html +5 -5
  19. package/coverage/lcov.info +30 -22
  20. package/lib/assess-distribution/index.d.ts +1 -1
  21. package/lib/assess-distribution/index.js +2 -2
  22. package/lib/assess-distribution/index.js.map +1 -1
  23. package/lib/benchmark/index.d.ts +1 -1
  24. package/lib/benchmark/index.js +2 -2
  25. package/lib/benchmark/index.js.map +1 -1
  26. package/lib/builder/buildDatafile.d.ts +1 -1
  27. package/lib/builder/buildDatafile.js +59 -40
  28. package/lib/builder/buildDatafile.js.map +1 -1
  29. package/lib/builder/buildProject.js +87 -50
  30. package/lib/builder/buildProject.js.map +1 -1
  31. package/lib/config/projectConfig.d.ts +1 -1
  32. package/lib/datasource/adapter.d.ts +3 -3
  33. package/lib/datasource/adapter.js.map +1 -1
  34. package/lib/datasource/datasource.d.ts +2 -2
  35. package/lib/datasource/datasource.js.map +1 -1
  36. package/lib/datasource/filesystemAdapter.d.ts +1 -1
  37. package/lib/datasource/filesystemAdapter.js +9 -3
  38. package/lib/datasource/filesystemAdapter.js.map +1 -1
  39. package/lib/evaluate/index.d.ts +1 -1
  40. package/lib/evaluate/index.js +10 -7
  41. package/lib/evaluate/index.js.map +1 -1
  42. package/lib/find-usage/index.js +60 -26
  43. package/lib/find-usage/index.js.map +1 -1
  44. package/lib/linter/featureSchema.d.ts +194 -5
  45. package/lib/linter/featureSchema.js +70 -60
  46. package/lib/linter/featureSchema.js.map +1 -1
  47. package/lib/linter/testSchema.d.ts +1 -1
  48. package/lib/linter/testSchema.js +16 -13
  49. package/lib/linter/testSchema.js.map +1 -1
  50. package/lib/site/generateSiteSearchIndex.js +65 -24
  51. package/lib/site/generateSiteSearchIndex.js.map +1 -1
  52. package/lib/tester/matrix.js +2 -2
  53. package/lib/tester/matrix.js.map +1 -1
  54. package/lib/tester/testFeature.d.ts +3 -4
  55. package/lib/tester/testFeature.js +1 -2
  56. package/lib/tester/testFeature.js.map +1 -1
  57. package/lib/tester/testProject.d.ts +1 -3
  58. package/lib/tester/testProject.js +28 -12
  59. package/lib/tester/testProject.js.map +1 -1
  60. package/package.json +5 -5
  61. package/src/assess-distribution/index.ts +3 -3
  62. package/src/benchmark/index.ts +3 -3
  63. package/src/builder/buildDatafile.ts +33 -8
  64. package/src/builder/buildProject.ts +72 -31
  65. package/src/config/projectConfig.ts +1 -1
  66. package/src/datasource/adapter.ts +6 -3
  67. package/src/datasource/datasource.ts +2 -2
  68. package/src/datasource/filesystemAdapter.ts +12 -4
  69. package/src/evaluate/index.ts +11 -7
  70. package/src/find-usage/index.ts +65 -29
  71. package/src/linter/featureSchema.ts +100 -82
  72. package/src/linter/testSchema.ts +21 -16
  73. package/src/site/generateSiteSearchIndex.ts +71 -24
  74. package/src/tester/matrix.ts +2 -2
  75. package/src/tester/testFeature.ts +3 -2
  76. package/src/tester/testProject.ts +27 -8
@@ -227,93 +227,102 @@ export function getFeatureZodSchema(
227
227
 
228
228
  const groupSegmentsZodSchema = z.union([z.array(groupSegmentZodSchema), groupSegmentZodSchema]);
229
229
 
230
- const environmentZodSchema = z
231
- .object({
232
- expose: z
233
- .union([
234
- z.boolean(),
235
- z.array(z.string().refine((value) => projectConfig.tags.includes(value))),
236
- ])
237
- .optional(),
238
- rules: z
239
- .array(
240
- z
241
- .object({
242
- key: z.string(),
243
- description: z.string().optional(),
244
- segments: groupSegmentsZodSchema,
245
- percentage: z.number().min(0).max(100),
246
-
247
- enabled: z.boolean().optional(),
248
- variation: variationValueZodSchema.optional(),
249
- variables: z.record(variableValueZodSchema).optional(),
250
- })
251
- .strict(),
252
- )
253
-
254
- // must have at least one rule
255
- .refine(
256
- (value) => value.length > 0,
257
- () => ({
258
- message: "Must have at least one rule",
259
- }),
260
- )
230
+ const exposeSchema = z
231
+ .union([z.boolean(), z.array(z.string().refine((value) => projectConfig.tags.includes(value)))])
232
+ .optional();
233
+
234
+ const rulesSchema = z
235
+ .array(
236
+ z
237
+ .object({
238
+ key: z.string(),
239
+ description: z.string().optional(),
240
+ segments: groupSegmentsZodSchema,
241
+ percentage: z.number().min(0).max(100),
242
+
243
+ enabled: z.boolean().optional(),
244
+ variation: variationValueZodSchema.optional(),
245
+ variables: z.record(variableValueZodSchema).optional(),
246
+ })
247
+ .strict(),
248
+ )
249
+
250
+ // must have at least one rule
251
+ .refine(
252
+ (value) => value.length > 0,
253
+ () => ({
254
+ message: "Must have at least one rule",
255
+ }),
256
+ )
257
+
258
+ // duplicate rules
259
+ .refine(
260
+ (value) => {
261
+ const keys = value.map((v) => v.key);
262
+ return keys.length === new Set(keys).size;
263
+ },
264
+ (value) => ({
265
+ message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
266
+ }),
267
+ )
261
268
 
262
- // duplicate rules
263
- .refine(
264
- (value) => {
265
- const keys = value.map((v) => v.key);
266
- return keys.length === new Set(keys).size;
267
- },
268
- (value) => ({
269
- message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
270
- }),
271
- )
269
+ // enforce catch-all rule
270
+ .refine(
271
+ (value) => {
272
+ if (!projectConfig.enforceCatchAllRule) {
273
+ return true;
274
+ }
272
275
 
273
- // enforce catch-all rule
274
- .refine(
275
- (value) => {
276
- if (!projectConfig.enforceCatchAllRule) {
277
- return true;
278
- }
276
+ const hasCatchAllAsLastRule = value[value.length - 1].segments === "*";
277
+ return hasCatchAllAsLastRule;
278
+ },
279
+ () => ({
280
+ message: `Missing catch-all rule with \`segments: "*"\` at the end`,
281
+ }),
282
+ );
283
+
284
+ const forceSchema = z
285
+ .array(
286
+ z.union([
287
+ z
288
+ .object({
289
+ segments: groupSegmentsZodSchema,
290
+ enabled: z.boolean().optional(),
291
+ variation: variationValueZodSchema.optional(),
292
+ variables: z.record(variableValueZodSchema).optional(),
293
+ })
294
+ .strict(),
295
+ z
296
+ .object({
297
+ conditions: conditionsZodSchema,
298
+ enabled: z.boolean().optional(),
299
+ variation: variationValueZodSchema.optional(),
300
+ variables: z.record(variableValueZodSchema).optional(),
301
+ })
302
+ .strict(),
303
+ ]),
304
+ )
305
+ .optional();
279
306
 
280
- const hasCatchAllAsLastRule = value[value.length - 1].segments === "*";
281
- return hasCatchAllAsLastRule;
282
- },
283
- () => ({
284
- message: `Missing catch-all rule with \`segments: "*"\` at the end`,
285
- }),
286
- ),
287
- force: z
288
- .array(
289
- z.union([
290
- z
291
- .object({
292
- segments: groupSegmentsZodSchema,
293
- enabled: z.boolean().optional(),
294
- variation: variationValueZodSchema.optional(),
295
- variables: z.record(variableValueZodSchema).optional(),
296
- })
297
- .strict(),
298
- z
299
- .object({
300
- conditions: conditionsZodSchema,
301
- enabled: z.boolean().optional(),
302
- variation: variationValueZodSchema.optional(),
303
- variables: z.record(variableValueZodSchema).optional(),
304
- })
305
- .strict(),
306
- ]),
307
- )
308
- .optional(),
307
+ const environmentZodSchema = z
308
+ .object({
309
+ expose: exposeSchema,
310
+ rules: rulesSchema,
311
+ force: forceSchema,
309
312
  })
310
313
  .strict();
311
314
 
312
- const allEnvironmentsSchema = {};
313
- projectConfig.environments.forEach((environmentKey) => {
314
- allEnvironmentsSchema[environmentKey] = environmentZodSchema;
315
- });
316
- const allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
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
+ }
317
326
 
318
327
  const attributeKeyZodSchema = z.string().refine(
319
328
  (value) => value === "*" || availableAttributeKeys.includes(value),
@@ -447,7 +456,16 @@ export function getFeatureZodSchema(
447
456
  }),
448
457
  )
449
458
  .optional(),
450
- environments: allEnvironmentsZodSchema,
459
+
460
+ // with environments
461
+ environments: Array.isArray(projectConfig.environments)
462
+ ? allEnvironmentsZodSchema
463
+ : z.never().optional(),
464
+
465
+ // no environments
466
+ expose: projectConfig.environments === false ? exposeSchema : z.never().optional(),
467
+ rules: projectConfig.environments === false ? rulesSchema : z.never().optional(),
468
+ force: projectConfig.environments === false ? forceSchema : z.never().optional(),
451
469
  })
452
470
  .strict()
453
471
  .superRefine((value, ctx) => {
@@ -59,24 +59,29 @@ export function getTestsZodSchema(
59
59
  // because of supporting matrix
60
60
  z.string(),
61
61
  ]),
62
- environment: z.string().refine(
63
- (value) => {
64
- if (value.indexOf("${{") === 0) {
65
- // allow unknown strings for matrix
66
- return true;
67
- }
62
+ environment: Array.isArray(projectConfig.environments)
63
+ ? z.string().refine(
64
+ (value) => {
65
+ if (value.indexOf("${{") === 0) {
66
+ // allow unknown strings for matrix
67
+ return true;
68
+ }
68
69
 
69
- // otherwise only known environments should be passed
70
- if (projectConfig.environments.includes(value)) {
71
- return true;
72
- }
70
+ // otherwise only known environments should be passed
71
+ if (
72
+ Array.isArray(projectConfig.environments) &&
73
+ projectConfig.environments.includes(value)
74
+ ) {
75
+ return true;
76
+ }
73
77
 
74
- return false;
75
- },
76
- (value) => ({
77
- message: `Unknown environment "${value}"`,
78
- }),
79
- ),
78
+ return false;
79
+ },
80
+ (value) => ({
81
+ message: `Unknown environment "${value}"`,
82
+ }),
83
+ )
84
+ : z.never().optional(),
80
85
  context: z.record(z.unknown()),
81
86
  expectedToBeEnabled: z.boolean(),
82
87
  expectedVariation: z.string().optional(),
@@ -23,6 +23,10 @@ export async function generateSiteSearchIndex(
23
23
 
24
24
  const result: SearchIndex = {
25
25
  links: undefined,
26
+ projectConfig: {
27
+ tags: projectConfig.tags,
28
+ environments: projectConfig.environments,
29
+ },
26
30
  entities: {
27
31
  attributes: [],
28
32
  segments: [],
@@ -115,10 +119,52 @@ export async function generateSiteSearchIndex(
115
119
  });
116
120
  }
117
121
 
118
- Object.keys(parsed.environments).forEach((environmentKey) => {
119
- const env = parsed.environments[environmentKey];
122
+ // with environments
123
+ if (parsed.environments) {
124
+ Object.keys(parsed.environments).forEach((environmentKey) => {
125
+ const env = parsed.environments[environmentKey];
126
+
127
+ env.rules.forEach((rule) => {
128
+ if (rule.segments && rule.segments !== "*") {
129
+ extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
130
+ if (!segmentsUsedInFeatures[segmentKey]) {
131
+ segmentsUsedInFeatures[segmentKey] = new Set();
132
+ }
133
+
134
+ segmentsUsedInFeatures[segmentKey].add(entityName);
135
+ });
136
+ }
137
+ });
138
+
139
+ if (env.force) {
140
+ env.force.forEach((force) => {
141
+ if (force.segments && force.segments !== "*") {
142
+ extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
143
+ if (!segmentsUsedInFeatures[segmentKey]) {
144
+ segmentsUsedInFeatures[segmentKey] = new Set();
145
+ }
120
146
 
121
- env.rules.forEach((rule) => {
147
+ segmentsUsedInFeatures[segmentKey].add(entityName);
148
+ });
149
+ }
150
+
151
+ if (force.conditions) {
152
+ extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
153
+ if (!attributesUsedInFeatures[attributeKey]) {
154
+ attributesUsedInFeatures[attributeKey] = new Set();
155
+ }
156
+
157
+ attributesUsedInFeatures[attributeKey].add(entityName);
158
+ });
159
+ }
160
+ });
161
+ }
162
+ });
163
+ }
164
+
165
+ // no environments
166
+ if (parsed.rules) {
167
+ parsed.rules.forEach((rule) => {
122
168
  if (rule.segments && rule.segments !== "*") {
123
169
  extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
124
170
  if (!segmentsUsedInFeatures[segmentKey]) {
@@ -129,32 +175,33 @@ export async function generateSiteSearchIndex(
129
175
  });
130
176
  }
131
177
  });
178
+ }
132
179
 
133
- if (env.force) {
134
- env.force.forEach((force) => {
135
- if (force.segments && force.segments !== "*") {
136
- extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
137
- if (!segmentsUsedInFeatures[segmentKey]) {
138
- segmentsUsedInFeatures[segmentKey] = new Set();
139
- }
180
+ if (parsed.force) {
181
+ parsed.force.forEach((force) => {
182
+ if (force.segments && force.segments !== "*") {
183
+ extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
184
+ if (!segmentsUsedInFeatures[segmentKey]) {
185
+ segmentsUsedInFeatures[segmentKey] = new Set();
186
+ }
140
187
 
141
- segmentsUsedInFeatures[segmentKey].add(entityName);
142
- });
143
- }
188
+ segmentsUsedInFeatures[segmentKey].add(entityName);
189
+ });
190
+ }
144
191
 
145
- if (force.conditions) {
146
- extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
147
- if (!attributesUsedInFeatures[attributeKey]) {
148
- attributesUsedInFeatures[attributeKey] = new Set();
149
- }
192
+ if (force.conditions) {
193
+ extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
194
+ if (!attributesUsedInFeatures[attributeKey]) {
195
+ attributesUsedInFeatures[attributeKey] = new Set();
196
+ }
150
197
 
151
- attributesUsedInFeatures[attributeKey].add(entityName);
152
- });
153
- }
154
- });
155
- }
156
- });
198
+ attributesUsedInFeatures[attributeKey].add(entityName);
199
+ });
200
+ }
201
+ });
202
+ }
157
203
 
204
+ // push
158
205
  result.entities.features.push({
159
206
  ...parsed,
160
207
  key: entityName,
@@ -105,7 +105,7 @@ export function getFeatureAssertionsFromMatrix(
105
105
  ): FeatureAssertion[] {
106
106
  if (!assertionWithMatrix.matrix) {
107
107
  const assertion = { ...assertionWithMatrix };
108
- assertion.description = `Assertion #${aIndex + 1}: (${assertion.environment}) ${
108
+ assertion.description = `Assertion #${aIndex + 1}:${assertion.environment ? ` (${assertion.environment})` : ""} ${
109
109
  assertion.description || `at ${assertion.at}%`
110
110
  }`;
111
111
 
@@ -118,7 +118,7 @@ export function getFeatureAssertionsFromMatrix(
118
118
  for (let cIndex = 0; cIndex < combinations.length; cIndex++) {
119
119
  const combination = combinations[cIndex];
120
120
  const assertion = applyCombinationToFeatureAssertion(combination, assertionWithMatrix);
121
- assertion.description = `Assertion #${aIndex + 1}: (${assertion.environment}) ${
121
+ assertion.description = `Assertion #${aIndex + 1}:${assertion.environment ? ` (${assertion.environment})` : ""} ${
122
122
  assertion.description || `at ${assertion.at}%`
123
123
  }`;
124
124
 
@@ -13,6 +13,7 @@ import { ProjectConfig } from "../config";
13
13
  import { checkIfArraysAreEqual } from "./checkIfArraysAreEqual";
14
14
  import { checkIfObjectsAreEqual } from "./checkIfObjectsAreEqual";
15
15
  import { getFeatureAssertionsFromMatrix } from "./matrix";
16
+ import type { DatafileContentByEnvironment } from "./testProject";
16
17
 
17
18
  export async function testFeature(
18
19
  datasource: Datasource,
@@ -20,7 +21,7 @@ export async function testFeature(
20
21
  test: TestFeature,
21
22
  options: { verbose?: boolean; showDatafile?: boolean } = {},
22
23
  patterns,
23
- datafileContentByEnvironment: { [environment: string]: DatafileContent } = {},
24
+ datafileContentByEnvironment: DatafileContentByEnvironment,
24
25
  ): Promise<TestResult> {
25
26
  const testStartTime = Date.now();
26
27
  const featureKey = test.feature;
@@ -54,7 +55,7 @@ export async function testFeature(
54
55
  continue;
55
56
  }
56
57
 
57
- const datafileContent = datafileContentByEnvironment[assertion.environment];
58
+ const datafileContent = datafileContentByEnvironment.get(assertion.environment || false);
58
59
 
59
60
  if (options.showDatafile) {
60
61
  console.log("");
@@ -36,9 +36,7 @@ export interface ExecutionResult {
36
36
  };
37
37
  }
38
38
 
39
- export interface DatafileContentByEnvironment {
40
- [environment: string]: DatafileContent;
41
- }
39
+ export type DatafileContentByEnvironment = Map<string | false, DatafileContent>;
42
40
 
43
41
  export async function executeTest(
44
42
  testFile: string,
@@ -151,23 +149,44 @@ export async function testProject(
151
149
  let passedAssertionsCount = 0;
152
150
  let failedAssertionsCount = 0;
153
151
 
154
- const datafileContentByEnvironment: DatafileContentByEnvironment = {};
152
+ const datafileContentByEnvironment: DatafileContentByEnvironment = new Map();
153
+
154
+ // with environments
155
+ if (Array.isArray(projectConfig.environments)) {
156
+ for (const environment of projectConfig.environments) {
157
+ const existingState = await datasource.readState(environment);
158
+ const datafileContent = await buildDatafile(
159
+ projectConfig,
160
+ datasource,
161
+ {
162
+ schemaVersion: options.schemaVersion || SCHEMA_VERSION,
163
+ revision: "include-all-features",
164
+ environment: environment,
165
+ inflate: options.inflate,
166
+ },
167
+ existingState,
168
+ );
169
+
170
+ datafileContentByEnvironment.set(environment, datafileContent);
171
+ }
172
+ }
155
173
 
156
- for (const environment of projectConfig.environments) {
157
- const existingState = await datasource.readState(environment);
174
+ // no environments
175
+ if (projectConfig.environments === false) {
176
+ const existingState = await datasource.readState(false);
158
177
  const datafileContent = await buildDatafile(
159
178
  projectConfig,
160
179
  datasource,
161
180
  {
162
181
  schemaVersion: options.schemaVersion || SCHEMA_VERSION,
163
182
  revision: "include-all-features",
164
- environment: environment,
183
+ environment: false,
165
184
  inflate: options.inflate,
166
185
  },
167
186
  existingState,
168
187
  );
169
188
 
170
- datafileContentByEnvironment[environment] = datafileContent;
189
+ datafileContentByEnvironment.set(false, datafileContent);
171
190
  }
172
191
 
173
192
  for (const testFile of testFiles) {