@featurevisor/core 1.34.1 → 1.35.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 (37) 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.d.ts +1 -1
  21. package/lib/datasource/filesystemAdapter.js +84 -57
  22. package/lib/datasource/filesystemAdapter.js.map +1 -1
  23. package/lib/index.d.ts +1 -0
  24. package/lib/index.js +1 -0
  25. package/lib/index.js.map +1 -1
  26. package/lib/list/index.d.ts +4 -0
  27. package/lib/list/index.js +555 -0
  28. package/lib/list/index.js.map +1 -0
  29. package/lib/tester/index.d.ts +1 -0
  30. package/lib/tester/index.js +1 -0
  31. package/lib/tester/index.js.map +1 -1
  32. package/package.json +5 -5
  33. package/src/cli/plugins.ts +2 -0
  34. package/src/datasource/filesystemAdapter.ts +23 -4
  35. package/src/index.ts +1 -0
  36. package/src/list/index.ts +496 -0
  37. package/src/tester/index.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { execSync } from "child_process";
3
+ import { execSync, spawn } from "child_process";
4
4
 
5
5
  import * as mkdirp from "mkdirp";
6
6
 
@@ -255,13 +255,32 @@ export class FilesystemAdapter extends Adapter {
255
255
  /**
256
256
  * History
257
257
  */
258
- getRawHistory(pathPatterns: string[]) {
258
+ async getRawHistory(pathPatterns: string[]): Promise<string> {
259
259
  const gitPaths = pathPatterns.join(" ");
260
260
 
261
261
  const logCommand = `git log --name-only --pretty=format:"%h|%an|%aI" --relative --no-merges -- ${gitPaths}`;
262
262
  const fullCommand = `(cd ${this.rootDirectoryPath} && ${logCommand})`;
263
263
 
264
- return execSync(fullCommand, { encoding: "utf8" }).toString();
264
+ return new Promise(function (resolve, reject) {
265
+ const child = spawn(fullCommand, { shell: true });
266
+ let result = "";
267
+
268
+ child.stdout.on("data", function (data) {
269
+ result += data.toString();
270
+ });
271
+
272
+ child.stderr.on("data", function (data) {
273
+ console.error(data.toString());
274
+ });
275
+
276
+ child.on("close", function (code) {
277
+ if (code === 0) {
278
+ resolve(result);
279
+ } else {
280
+ reject(code);
281
+ }
282
+ });
283
+ });
265
284
  }
266
285
 
267
286
  getPathPatterns(entityType?: EntityType, entityKey?: string): string[] {
@@ -296,7 +315,7 @@ export class FilesystemAdapter extends Adapter {
296
315
 
297
316
  async listHistoryEntries(entityType?: EntityType, entityKey?: string): Promise<HistoryEntry[]> {
298
317
  const pathPatterns = this.getPathPatterns(entityType, entityKey);
299
- const rawHistory = this.getRawHistory(pathPatterns);
318
+ const rawHistory = await this.getRawHistory(pathPatterns);
300
319
 
301
320
  const fullHistory: HistoryEntry[] = [];
302
321
  const blocks = rawHistory.split("\n\n");
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";