@featurevisor/core 1.34.2 → 1.35.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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +2 -2
  3. package/coverage/lcov-report/index.html +1 -1
  4. package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
  5. package/coverage/lcov-report/lib/builder/index.html +1 -1
  6. package/coverage/lcov-report/lib/builder/revision.js.html +1 -1
  7. package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
  8. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
  9. package/coverage/lcov-report/lib/tester/index.html +1 -1
  10. package/coverage/lcov-report/lib/tester/matrix.js.html +1 -1
  11. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  12. package/coverage/lcov-report/src/builder/index.html +1 -1
  13. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  14. package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
  15. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
  16. package/coverage/lcov-report/src/tester/index.html +1 -1
  17. package/coverage/lcov-report/src/tester/matrix.ts.html +1 -1
  18. package/lib/cli/plugins.js +2 -0
  19. package/lib/cli/plugins.js.map +1 -1
  20. package/lib/datasource/filesystemAdapter.js +6 -2
  21. package/lib/datasource/filesystemAdapter.js.map +1 -1
  22. package/lib/index.d.ts +1 -0
  23. package/lib/index.js +1 -0
  24. package/lib/index.js.map +1 -1
  25. package/lib/list/index.d.ts +4 -0
  26. package/lib/list/index.js +555 -0
  27. package/lib/list/index.js.map +1 -0
  28. package/lib/tester/index.d.ts +1 -0
  29. package/lib/tester/index.js +1 -0
  30. package/lib/tester/index.js.map +1 -1
  31. package/package.json +5 -5
  32. package/src/cli/plugins.ts +2 -0
  33. package/src/datasource/filesystemAdapter.ts +7 -1
  34. package/src/index.ts +1 -0
  35. package/src/list/index.ts +496 -0
  36. package/src/tester/index.ts +1 -0
@@ -86,7 +86,10 @@ export class FilesystemAdapter extends Adapter {
86
86
  getEntityPath(entityType: EntityType, entityKey: string): string {
87
87
  const basePath = this.getEntityDirectoryPath(entityType);
88
88
 
89
- return path.join(basePath, `${entityKey}.${this.parser.extension}`);
89
+ // taking care of windows paths
90
+ const relativeEntityPath = entityKey.replace(/\//g, path.sep);
91
+
92
+ return path.join(basePath, `${relativeEntityPath}.${this.parser.extension}`);
90
93
  }
91
94
 
92
95
  async listEntities(entityType: EntityType): Promise<string[]> {
@@ -103,6 +106,9 @@ export class FilesystemAdapter extends Adapter {
103
106
 
104
107
  // remove the extension from the end
105
108
  .map((filterPath) => filterPath.replace(`.${this.parser.extension}`, ""))
109
+
110
+ // take care of windows paths
111
+ .map((filterPath) => filterPath.replace(/\\/g, "/"))
106
112
  );
107
113
  }
108
114
 
package/src/index.ts CHANGED
@@ -14,4 +14,5 @@ export * from "./benchmark";
14
14
  export * from "./evaluate";
15
15
  export * from "./assess-distribution";
16
16
  export * from "./info";
17
+ export * from "./list";
17
18
  export * from "./cli";
@@ -0,0 +1,496 @@
1
+ import {
2
+ ParsedFeature,
3
+ Segment,
4
+ Attribute,
5
+ TestFeature,
6
+ TestSegment,
7
+ FeatureAssertion,
8
+ SegmentAssertion,
9
+ } from "@featurevisor/types";
10
+
11
+ import { Dependencies } from "../dependencies";
12
+ import { Plugin } from "../cli";
13
+ import { getFeatureAssertionsFromMatrix, getSegmentAssertionsFromMatrix } from "../tester";
14
+
15
+ async function getEntitiesWithTests(
16
+ deps: Dependencies,
17
+ ): Promise<{ features: string[]; segments: string[] }> {
18
+ const { datasource } = deps;
19
+
20
+ const featuresWithTests = new Set<string>();
21
+ const segmentsWithTests = new Set<string>();
22
+
23
+ const tests = await datasource.listTests();
24
+ for (const testKey of tests) {
25
+ const test = await datasource.readTest(testKey);
26
+
27
+ if ((test as TestFeature).feature) {
28
+ featuresWithTests.add((test as TestFeature).feature);
29
+ }
30
+
31
+ if ((test as TestSegment).segment) {
32
+ segmentsWithTests.add((test as TestSegment).segment);
33
+ }
34
+ }
35
+
36
+ return {
37
+ features: Array.from(featuresWithTests),
38
+ segments: Array.from(segmentsWithTests),
39
+ };
40
+ }
41
+
42
+ async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
43
+ const { datasource, options } = deps;
44
+
45
+ const result: T[] = [];
46
+ let entityKeys: string[] = [];
47
+
48
+ if (entityType === "feature") {
49
+ entityKeys = await datasource.listFeatures();
50
+ } else if (entityType === "segment") {
51
+ entityKeys = await datasource.listSegments();
52
+ } else if (entityType === "attribute") {
53
+ entityKeys = await datasource.listAttributes();
54
+ } else if (entityType === "test") {
55
+ entityKeys = await datasource.listTests();
56
+ }
57
+
58
+ if (entityKeys.length === 0) {
59
+ return result;
60
+ }
61
+
62
+ let entitiesWithTests: { features: string[]; segments: string[] } = {
63
+ features: [],
64
+ segments: [],
65
+ };
66
+ let entitiesWithTestsInitialized = false;
67
+
68
+ async function initializeEntitiesWithTests() {
69
+ if (entitiesWithTestsInitialized) {
70
+ return;
71
+ }
72
+
73
+ entitiesWithTests = await getEntitiesWithTests(deps);
74
+ entitiesWithTestsInitialized = true;
75
+ }
76
+
77
+ for (const key of entityKeys) {
78
+ let entity = {} as T;
79
+
80
+ if (entityType === "feature") {
81
+ entity = (await datasource.readFeature(key)) as T;
82
+ } else if (entityType === "segment") {
83
+ entity = (await datasource.readSegment(key)) as T;
84
+ } else if (entityType === "attribute") {
85
+ entity = (await datasource.readAttribute(key)) as T;
86
+ } else if (entityType === "test") {
87
+ entity = (await datasource.readTest(key)) as T;
88
+ }
89
+
90
+ // filter
91
+ if (entityType === "feature") {
92
+ const parsedFeature = entity as ParsedFeature;
93
+
94
+ // --archived=true|false
95
+ if (parsedFeature.archived) {
96
+ const archivedStatus = options.archived === "false";
97
+
98
+ if (parsedFeature.archived !== archivedStatus) {
99
+ continue;
100
+ }
101
+ }
102
+
103
+ // --description=<pattern>
104
+ if (options.description) {
105
+ const description = parsedFeature.description || "";
106
+
107
+ const regex = new RegExp(options.description, "i");
108
+ if (!regex.test(description)) {
109
+ continue;
110
+ }
111
+ }
112
+
113
+ // --disabledIn=<environment>
114
+ if (
115
+ options.disabledIn &&
116
+ parsedFeature.environments &&
117
+ parsedFeature.environments[options.disabledIn]
118
+ ) {
119
+ const disabledInEnvironment = parsedFeature.environments[options.disabledIn].rules.every(
120
+ (rule) => {
121
+ return rule.percentage === 0;
122
+ },
123
+ );
124
+
125
+ if (!disabledInEnvironment) {
126
+ continue;
127
+ }
128
+ }
129
+
130
+ // --enabledIn=<environment>
131
+ if (
132
+ options.enabledIn &&
133
+ parsedFeature.environments &&
134
+ parsedFeature.environments[options.enabledIn]
135
+ ) {
136
+ const enabledInEnvironment = parsedFeature.environments[options.enabledIn].rules.some(
137
+ (rule) => {
138
+ return rule.percentage > 0;
139
+ },
140
+ );
141
+
142
+ if (!enabledInEnvironment) {
143
+ continue;
144
+ }
145
+ }
146
+
147
+ // --keyPattern=<pattern>
148
+ if (options.keyPattern) {
149
+ const regex = new RegExp(options.keyPattern, "i");
150
+ if (!regex.test(key)) {
151
+ continue;
152
+ }
153
+ }
154
+
155
+ // --tag=<tag>
156
+ if (options.tag) {
157
+ const tags = Array.isArray(options.tag) ? options.tag : [options.tag];
158
+ const hasTags = tags.every((tag) => parsedFeature.tags.includes(tag));
159
+
160
+ if (!hasTags) {
161
+ continue;
162
+ }
163
+ }
164
+
165
+ // --variable=<variableKey>
166
+ if (options.variable) {
167
+ const lookForVariables = Array.isArray(options.variable)
168
+ ? options.variable
169
+ : [options.variable];
170
+
171
+ let variablesInFeature: string[] = [];
172
+ if (Array.isArray(parsedFeature.variablesSchema)) {
173
+ variablesInFeature = parsedFeature.variablesSchema.map((variable) => variable.key);
174
+ } else if (parsedFeature.variablesSchema) {
175
+ variablesInFeature = Object.keys(parsedFeature.variablesSchema);
176
+ }
177
+
178
+ const hasVariables = lookForVariables.every((variable) =>
179
+ variablesInFeature.includes(variable),
180
+ );
181
+
182
+ if (!hasVariables) {
183
+ continue;
184
+ }
185
+ }
186
+
187
+ // --variation=<variationValue>
188
+ if (options.variation) {
189
+ const lookForVariations = Array.isArray(options.variation)
190
+ ? options.variation
191
+ : [options.variation];
192
+
193
+ let variationsInFeature: string[] = parsedFeature.variations
194
+ ? parsedFeature.variations.map((v) => v.value)
195
+ : [];
196
+
197
+ const hasVariations = lookForVariations.every((variation) =>
198
+ variationsInFeature.includes(variation),
199
+ );
200
+
201
+ if (!hasVariations) {
202
+ continue;
203
+ }
204
+ }
205
+
206
+ // --with-tests
207
+ if (options.withTests) {
208
+ await initializeEntitiesWithTests();
209
+
210
+ if (!entitiesWithTests.features.includes(key)) {
211
+ continue;
212
+ }
213
+ }
214
+
215
+ // --with-variables
216
+ if (options.withVariables) {
217
+ const hasVariables = parsedFeature.variablesSchema;
218
+
219
+ if (!hasVariables) {
220
+ continue;
221
+ }
222
+ }
223
+
224
+ // --with-variations
225
+ if (options.withVariations) {
226
+ const hasVariations = parsedFeature.variations;
227
+
228
+ if (!hasVariations) {
229
+ continue;
230
+ }
231
+ }
232
+
233
+ // --without-tests
234
+ if (options.withoutTests) {
235
+ await initializeEntitiesWithTests();
236
+
237
+ if (entitiesWithTests.features.includes(key)) {
238
+ continue;
239
+ }
240
+ }
241
+
242
+ // --without-variables
243
+ if (options.withoutVariables) {
244
+ const hasVariables = parsedFeature.variablesSchema;
245
+
246
+ if (hasVariables) {
247
+ continue;
248
+ }
249
+ }
250
+
251
+ // --without-variations
252
+ if (options.withoutVariations) {
253
+ const hasVariations = parsedFeature.variations;
254
+
255
+ if (hasVariations) {
256
+ continue;
257
+ }
258
+ }
259
+ } else if (entityType === "segment") {
260
+ const segment = entity as Segment;
261
+
262
+ // --archived=true|false
263
+ if (segment.archived) {
264
+ const archivedStatus = options.archived === "false";
265
+
266
+ if (segment.archived !== archivedStatus) {
267
+ continue;
268
+ }
269
+ }
270
+
271
+ // --description=<pattern>
272
+ if (options.description) {
273
+ const description = segment.description || "";
274
+
275
+ const regex = new RegExp(options.description, "i");
276
+ if (!regex.test(description)) {
277
+ continue;
278
+ }
279
+ }
280
+
281
+ // --keyPattern=<pattern>
282
+ if (options.keyPattern) {
283
+ const regex = new RegExp(options.keyPattern, "i");
284
+ if (!regex.test(key)) {
285
+ continue;
286
+ }
287
+ }
288
+
289
+ // --with-tests
290
+ if (options.withTests) {
291
+ await initializeEntitiesWithTests();
292
+
293
+ if (!entitiesWithTests.segments.includes(key)) {
294
+ continue;
295
+ }
296
+ }
297
+
298
+ // --without-tests
299
+ if (options.withoutTests) {
300
+ await initializeEntitiesWithTests();
301
+
302
+ if (entitiesWithTests.segments.includes(key)) {
303
+ continue;
304
+ }
305
+ }
306
+ } else if (entityType === "attribute") {
307
+ const attribute = entity as Attribute;
308
+
309
+ // --archived=true|false
310
+ if (options.archived) {
311
+ const archivedStatus = options.archived === "false";
312
+
313
+ if (attribute.archived !== archivedStatus) {
314
+ continue;
315
+ }
316
+ }
317
+
318
+ // --description=<pattern>
319
+ if (options.description) {
320
+ const description = attribute.description || "";
321
+
322
+ const regex = new RegExp(options.description, "i");
323
+ if (!regex.test(description)) {
324
+ continue;
325
+ }
326
+ }
327
+
328
+ // --keyPattern=<pattern>
329
+ if (options.keyPattern) {
330
+ const regex = new RegExp(options.keyPattern, "i");
331
+ if (!regex.test(key)) {
332
+ continue;
333
+ }
334
+ }
335
+ } else if (entityType === "test") {
336
+ let test = entity as TestFeature | TestSegment;
337
+ const testEntityKey = (test as TestFeature).feature || (test as TestSegment).segment;
338
+ const testEntityType = (test as TestSegment).segment ? "segment" : "feature";
339
+ let testAssertions = test.assertions;
340
+
341
+ // --apply-matrix
342
+ if (options.applyMatrix) {
343
+ if (testEntityType === "feature") {
344
+ let assertionsAfterApplyingMatrix: FeatureAssertion[] = [];
345
+ for (let aIndex = 0; aIndex < testAssertions.length; aIndex++) {
346
+ const processedAssertions = getFeatureAssertionsFromMatrix(
347
+ aIndex,
348
+ testAssertions[aIndex] as FeatureAssertion,
349
+ );
350
+ assertionsAfterApplyingMatrix =
351
+ assertionsAfterApplyingMatrix.concat(processedAssertions);
352
+ }
353
+
354
+ testAssertions = assertionsAfterApplyingMatrix;
355
+ } else if (testEntityType === "segment") {
356
+ let assertionsAfterApplyingMatrix: SegmentAssertion[] = [];
357
+ for (let aIndex = 0; aIndex < testAssertions.length; aIndex++) {
358
+ const processedAssertions = getSegmentAssertionsFromMatrix(
359
+ aIndex,
360
+ testAssertions[aIndex] as SegmentAssertion,
361
+ );
362
+ assertionsAfterApplyingMatrix =
363
+ assertionsAfterApplyingMatrix.concat(processedAssertions);
364
+ }
365
+
366
+ testAssertions = assertionsAfterApplyingMatrix;
367
+ }
368
+ }
369
+
370
+ // --keyPattern=<pattern>
371
+ if (options.keyPattern) {
372
+ const regex = new RegExp(options.keyPattern, "i");
373
+ if (!regex.test(testEntityKey)) {
374
+ continue;
375
+ }
376
+ }
377
+
378
+ // --assertionPattern=<pattern>
379
+ if (options.assertionPattern) {
380
+ const regex = new RegExp(options.assertionPattern, "i");
381
+ testAssertions = testAssertions.filter((assertion) => {
382
+ if (!assertion.description) {
383
+ return false;
384
+ }
385
+
386
+ return regex.test(assertion.description);
387
+ }) as FeatureAssertion[] | SegmentAssertion[];
388
+
389
+ if (testAssertions.length === 0) {
390
+ continue;
391
+ }
392
+ }
393
+
394
+ (entity as TestFeature | TestSegment).assertions = testAssertions;
395
+ }
396
+
397
+ result.push({
398
+ ...entity,
399
+ key,
400
+ });
401
+ }
402
+
403
+ return result;
404
+ }
405
+
406
+ function ucfirst(str: string) {
407
+ return str.charAt(0).toUpperCase() + str.slice(1);
408
+ }
409
+
410
+ function printResult({ result, entityType, options }) {
411
+ if (options.json) {
412
+ console.log(options.pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result));
413
+ return;
414
+ }
415
+
416
+ if (result.length === 0) {
417
+ console.log(`No ${entityType}s found.`);
418
+ return;
419
+ }
420
+
421
+ console.log(`\n${ucfirst(entityType)}s:\n`);
422
+
423
+ for (const item of result) {
424
+ console.log(`- ${item.key}`);
425
+ }
426
+
427
+ console.log(`\n\nFound ${result.length} ${entityType}s.`);
428
+ }
429
+
430
+ export async function listProject(deps: Dependencies) {
431
+ const { rootDirectoryPath, projectConfig, datasource, options } = deps;
432
+
433
+ // features
434
+ if (options.features) {
435
+ const result = await listEntities<ParsedFeature>(deps, "feature");
436
+
437
+ return printResult({
438
+ result,
439
+ entityType: "feature",
440
+ options,
441
+ });
442
+ }
443
+
444
+ // segments
445
+ if (options.segments) {
446
+ const result = await listEntities<Segment>(deps, "segment");
447
+
448
+ return printResult({
449
+ result,
450
+ entityType: "segment",
451
+ options,
452
+ });
453
+ }
454
+
455
+ // attributes
456
+ if (options.attributes) {
457
+ const result = await listEntities<Attribute>(deps, "attribute");
458
+
459
+ return printResult({
460
+ result,
461
+ entityType: "attribute",
462
+ options,
463
+ });
464
+ }
465
+
466
+ // tests
467
+ if (options.tests) {
468
+ const result = await listEntities<Attribute>(deps, "test");
469
+
470
+ return printResult({
471
+ result,
472
+ entityType: "test",
473
+ options,
474
+ });
475
+ }
476
+
477
+ console.log("\nNothing to list. \n\nPlease pass `--features`, `--segments`, or `--attributes`.");
478
+ }
479
+
480
+ export const listPlugin: Plugin = {
481
+ command: "list",
482
+ handler: async function ({ rootDirectoryPath, projectConfig, datasource, parsed }) {
483
+ await listProject({
484
+ rootDirectoryPath,
485
+ projectConfig,
486
+ datasource,
487
+ options: parsed,
488
+ });
489
+ },
490
+ examples: [
491
+ {
492
+ command: "list",
493
+ description: "list entities",
494
+ },
495
+ ],
496
+ };
@@ -1,2 +1,3 @@
1
1
  export * from "./testProject";
2
2
  export * from "./testFeature";
3
+ export * from "./matrix";