@featurevisor/core 1.35.2 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +0 -6
- package/coverage/clover.xml +321 -237
- package/coverage/coverage-final.json +8 -8
- package/coverage/lcov-report/index.html +77 -47
- package/coverage/lcov-report/lib/builder/allocator.js.html +14 -14
- package/coverage/lcov-report/lib/builder/index.html +16 -16
- package/coverage/lcov-report/lib/builder/revision.js.html +3 -3
- package/coverage/lcov-report/lib/builder/traffic.js.html +90 -63
- package/coverage/lcov-report/lib/list/index.html +116 -0
- package/coverage/lcov-report/lib/{tester → list}/matrix.js.html +90 -66
- package/coverage/lcov-report/lib/tester/helpers.js.html +295 -0
- package/coverage/lcov-report/lib/tester/index.html +20 -35
- package/coverage/lcov-report/src/builder/allocator.ts.html +2 -2
- package/coverage/lcov-report/src/builder/index.html +15 -15
- package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/src/builder/traffic.ts.html +101 -23
- package/coverage/lcov-report/src/list/index.html +116 -0
- package/coverage/lcov-report/src/{tester → list}/matrix.ts.html +87 -21
- package/coverage/lcov-report/src/tester/helpers.ts.html +313 -0
- package/coverage/lcov-report/src/tester/index.html +20 -35
- package/coverage/lcov.info +592 -436
- package/lib/assess-distribution/index.d.ts +1 -1
- package/lib/assess-distribution/index.js +102 -162
- package/lib/assess-distribution/index.js.map +1 -1
- package/lib/benchmark/index.js +87 -143
- package/lib/benchmark/index.js.map +1 -1
- package/lib/builder/allocator.d.ts +1 -1
- package/lib/builder/allocator.js +12 -12
- package/lib/builder/allocator.js.map +1 -1
- package/lib/builder/allocator.spec.js +22 -22
- package/lib/builder/allocator.spec.js.map +1 -1
- package/lib/builder/buildDatafile.d.ts +4 -3
- package/lib/builder/buildDatafile.js +311 -388
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/buildProject.d.ts +2 -1
- package/lib/builder/buildProject.js +96 -183
- package/lib/builder/buildProject.js.map +1 -1
- package/lib/builder/convertToV1.d.ts +10 -0
- package/lib/builder/convertToV1.js +119 -0
- package/lib/builder/convertToV1.js.map +1 -0
- package/lib/builder/getFeatureRanges.d.ts +1 -1
- package/lib/builder/getFeatureRanges.js +32 -105
- package/lib/builder/getFeatureRanges.js.map +1 -1
- package/lib/builder/hashes.d.ts +4 -0
- package/lib/builder/hashes.js +70 -0
- package/lib/builder/hashes.js.map +1 -0
- package/lib/builder/revision.js +2 -2
- package/lib/builder/revision.js.map +1 -1
- package/lib/builder/revision.spec.js +1 -1
- package/lib/builder/revision.spec.js.map +1 -1
- package/lib/builder/traffic.d.ts +1 -1
- package/lib/builder/traffic.js +57 -48
- package/lib/builder/traffic.js.map +1 -1
- package/lib/builder/traffic.spec.js +14 -14
- package/lib/builder/traffic.spec.js.map +1 -1
- package/lib/cli/cli.js +60 -129
- package/lib/cli/cli.js.map +1 -1
- package/lib/cli/plugins.js +14 -16
- package/lib/cli/plugins.js.map +1 -1
- package/lib/config/parsers.js +1 -1
- package/lib/config/parsers.js.map +1 -1
- package/lib/config/projectConfig.d.ts +8 -6
- package/lib/config/projectConfig.js +31 -72
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/adapter.d.ts +1 -1
- package/lib/datasource/adapter.js +2 -5
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +2 -1
- package/lib/datasource/datasource.js +107 -148
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +1 -1
- package/lib/datasource/filesystemAdapter.js +224 -360
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/evaluate/index.d.ts +1 -1
- package/lib/evaluate/index.js +120 -188
- package/lib/evaluate/index.js.map +1 -1
- package/lib/find-duplicate-segments/findDuplicateSegments.d.ts +1 -1
- package/lib/find-duplicate-segments/findDuplicateSegments.js +40 -128
- package/lib/find-duplicate-segments/findDuplicateSegments.js.map +1 -1
- package/lib/find-duplicate-segments/index.js +27 -82
- package/lib/find-duplicate-segments/index.js.map +1 -1
- package/lib/find-usage/index.d.ts +7 -5
- package/lib/find-usage/index.js +333 -507
- package/lib/find-usage/index.js.map +1 -1
- package/lib/generate-code/index.js +36 -91
- package/lib/generate-code/index.js.map +1 -1
- package/lib/generate-code/typescript.js +117 -157
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/info/index.js +45 -133
- package/lib/info/index.js.map +1 -1
- package/lib/init/index.d.ts +1 -1
- package/lib/init/index.js +16 -64
- package/lib/init/index.js.map +1 -1
- package/lib/linter/attributeSchema.d.ts +21 -6
- package/lib/linter/attributeSchema.js +18 -4
- package/lib/linter/attributeSchema.js.map +1 -1
- package/lib/linter/checkCircularDependency.d.ts +1 -1
- package/lib/linter/checkCircularDependency.js +22 -80
- package/lib/linter/checkCircularDependency.js.map +1 -1
- package/lib/linter/checkPercentageExceedingSlot.d.ts +1 -1
- package/lib/linter/checkPercentageExceedingSlot.js +36 -76
- package/lib/linter/checkPercentageExceedingSlot.js.map +1 -1
- package/lib/linter/conditionSchema.d.ts +1 -1
- package/lib/linter/conditionSchema.js +89 -41
- package/lib/linter/conditionSchema.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +345 -197
- package/lib/linter/featureSchema.js +313 -172
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/groupSchema.js +6 -6
- package/lib/linter/groupSchema.js.map +1 -1
- package/lib/linter/lintProject.js +306 -480
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/printError.js +7 -7
- package/lib/linter/printError.js.map +1 -1
- package/lib/linter/segmentSchema.js +2 -2
- package/lib/linter/segmentSchema.js.map +1 -1
- package/lib/linter/testSchema.d.ts +155 -3
- package/lib/linter/testSchema.js +47 -17
- package/lib/linter/testSchema.js.map +1 -1
- package/lib/list/index.d.ts +1 -0
- package/lib/list/index.js +349 -517
- package/lib/list/index.js.map +1 -1
- package/lib/{tester → list}/matrix.d.ts +1 -1
- package/lib/{tester → list}/matrix.js +50 -42
- package/lib/list/matrix.js.map +1 -0
- package/lib/{tester → list}/matrix.spec.js +7 -7
- package/lib/list/matrix.spec.js.map +1 -0
- package/lib/site/exportSite.js +25 -71
- package/lib/site/exportSite.js.map +1 -1
- package/lib/site/generateHistory.d.ts +1 -1
- package/lib/site/generateHistory.js +26 -82
- package/lib/site/generateHistory.js.map +1 -1
- package/lib/site/generateSiteSearchIndex.d.ts +1 -1
- package/lib/site/generateSiteSearchIndex.js +182 -259
- package/lib/site/generateSiteSearchIndex.js.map +1 -1
- package/lib/site/getLastModifiedFromHistory.d.ts +1 -1
- package/lib/site/getLastModifiedFromHistory.js +2 -2
- package/lib/site/getLastModifiedFromHistory.js.map +1 -1
- package/lib/site/getOwnerAndRepoFromUrl.js +6 -6
- package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -1
- package/lib/site/getRelativePaths.js +7 -7
- package/lib/site/getRelativePaths.js.map +1 -1
- package/lib/site/getRepoDetails.js +20 -20
- package/lib/site/getRepoDetails.js.map +1 -1
- package/lib/site/index.js +25 -73
- package/lib/site/index.js.map +1 -1
- package/lib/site/serveSite.js +10 -10
- package/lib/site/serveSite.js.map +1 -1
- package/lib/tester/helpers.d.ts +2 -0
- package/lib/tester/helpers.js +71 -0
- package/lib/tester/helpers.js.map +1 -0
- package/lib/tester/helpers.spec.js +115 -0
- package/lib/tester/helpers.spec.js.map +1 -0
- package/lib/tester/index.d.ts +0 -1
- package/lib/tester/index.js +0 -1
- package/lib/tester/index.js.map +1 -1
- package/lib/tester/prettyDuration.js +11 -11
- package/lib/tester/prettyDuration.js.map +1 -1
- package/lib/tester/printTestResult.d.ts +1 -1
- package/lib/tester/printTestResult.js +35 -15
- package/lib/tester/printTestResult.js.map +1 -1
- package/lib/tester/testFeature.d.ts +4 -2
- package/lib/tester/testFeature.js +264 -226
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/tester/testProject.d.ts +3 -7
- package/lib/tester/testProject.js +145 -246
- package/lib/tester/testProject.js.map +1 -1
- package/lib/tester/testSegment.d.ts +5 -2
- package/lib/tester/testSegment.js +65 -102
- package/lib/tester/testSegment.js.map +1 -1
- package/lib/utils/extractKeys.d.ts +2 -1
- package/lib/utils/extractKeys.js +57 -12
- package/lib/utils/extractKeys.js.map +1 -1
- package/lib/utils/git.d.ts +1 -1
- package/lib/utils/git.js +23 -23
- package/lib/utils/git.js.map +1 -1
- package/lib/utils/pretty.js +2 -4
- package/lib/utils/pretty.js.map +1 -1
- package/package.json +5 -6
- package/src/assess-distribution/index.ts +3 -2
- package/src/benchmark/index.ts +3 -3
- package/src/builder/allocator.spec.ts +1 -1
- package/src/builder/allocator.ts +1 -1
- package/src/builder/buildDatafile.ts +161 -124
- package/src/builder/buildProject.ts +6 -3
- package/src/builder/convertToV1.ts +166 -0
- package/src/builder/getFeatureRanges.ts +1 -1
- package/src/builder/hashes.ts +109 -0
- package/src/builder/traffic.ts +41 -15
- package/src/cli/cli.ts +1 -1
- package/src/cli/plugins.ts +0 -2
- package/src/config/projectConfig.ts +13 -10
- package/src/datasource/adapter.ts +1 -1
- package/src/datasource/datasource.ts +23 -2
- package/src/datasource/filesystemAdapter.ts +11 -12
- package/src/evaluate/index.ts +7 -6
- package/src/find-duplicate-segments/findDuplicateSegments.ts +1 -1
- package/src/find-usage/index.ts +111 -44
- package/src/generate-code/index.ts +1 -3
- package/src/generate-code/typescript.ts +7 -29
- package/src/index.ts +0 -1
- package/src/info/index.ts +2 -2
- package/src/init/index.ts +2 -2
- package/src/linter/attributeSchema.ts +18 -2
- package/src/linter/checkCircularDependency.ts +1 -1
- package/src/linter/checkPercentageExceedingSlot.ts +28 -8
- package/src/linter/conditionSchema.ts +66 -10
- package/src/linter/featureSchema.ts +312 -116
- package/src/linter/lintProject.ts +9 -4
- package/src/linter/testSchema.ts +42 -3
- package/src/list/index.ts +18 -30
- package/src/{tester → list}/matrix.ts +33 -11
- package/src/site/exportSite.ts +2 -4
- package/src/site/generateHistory.ts +1 -1
- package/src/site/generateSiteSearchIndex.ts +58 -50
- package/src/site/getLastModifiedFromHistory.ts +1 -1
- package/src/tester/helpers.spec.ts +149 -0
- package/src/tester/helpers.ts +76 -0
- package/src/tester/index.ts +0 -1
- package/src/tester/printTestResult.ts +25 -3
- package/src/tester/testFeature.ts +270 -124
- package/src/tester/testProject.ts +28 -49
- package/src/tester/testSegment.ts +48 -40
- package/src/utils/extractKeys.ts +58 -1
- package/src/utils/git.ts +1 -1
- package/tsconfig.cjs.json +1 -0
- package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +0 -151
- package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +0 -157
- package/lib/restore/index.d.ts +0 -4
- package/lib/restore/index.js +0 -91
- package/lib/restore/index.js.map +0 -1
- package/lib/tester/checkIfArraysAreEqual.d.ts +0 -1
- package/lib/tester/checkIfArraysAreEqual.js +0 -18
- package/lib/tester/checkIfArraysAreEqual.js.map +0 -1
- package/lib/tester/checkIfObjectsAreEqual.d.ts +0 -1
- package/lib/tester/checkIfObjectsAreEqual.js +0 -23
- package/lib/tester/checkIfObjectsAreEqual.js.map +0 -1
- package/lib/tester/checkIfObjectsAreEqual.spec.js +0 -26
- package/lib/tester/checkIfObjectsAreEqual.spec.js.map +0 -1
- package/lib/tester/matrix.js.map +0 -1
- package/lib/tester/matrix.spec.js.map +0 -1
- package/src/restore/index.ts +0 -42
- package/src/tester/checkIfArraysAreEqual.ts +0 -16
- package/src/tester/checkIfObjectsAreEqual.spec.ts +0 -31
- package/src/tester/checkIfObjectsAreEqual.ts +0 -24
- /package/lib/{tester → list}/matrix.spec.d.ts +0 -0
- /package/lib/tester/{checkIfObjectsAreEqual.spec.d.ts → helpers.spec.d.ts} +0 -0
- /package/src/{tester → list}/matrix.spec.ts +0 -0
package/src/list/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
ParsedFeature,
|
|
3
3
|
Segment,
|
|
4
4
|
Attribute,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import { Dependencies } from "../dependencies";
|
|
12
12
|
import { Plugin } from "../cli";
|
|
13
|
-
import { getFeatureAssertionsFromMatrix, getSegmentAssertionsFromMatrix } from "
|
|
13
|
+
import { getFeatureAssertionsFromMatrix, getSegmentAssertionsFromMatrix } from "./matrix";
|
|
14
14
|
|
|
15
15
|
async function getEntitiesWithTests(
|
|
16
16
|
deps: Dependencies,
|
|
@@ -39,7 +39,7 @@ async function getEntitiesWithTests(
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
42
|
+
export async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
43
43
|
const { datasource, options } = deps;
|
|
44
44
|
|
|
45
45
|
const result: T[] = [];
|
|
@@ -111,16 +111,10 @@ async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// --disabledIn=<environment>
|
|
114
|
-
if (
|
|
115
|
-
options.disabledIn
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
) {
|
|
119
|
-
const disabledInEnvironment = parsedFeature.environments[options.disabledIn].rules.every(
|
|
120
|
-
(rule) => {
|
|
121
|
-
return rule.percentage === 0;
|
|
122
|
-
},
|
|
123
|
-
);
|
|
114
|
+
if (options.disabledIn && parsedFeature.rules && parsedFeature.rules[options.disabledIn]) {
|
|
115
|
+
const disabledInEnvironment = parsedFeature.rules[options.disabledIn].every((rule) => {
|
|
116
|
+
return rule.percentage === 0;
|
|
117
|
+
});
|
|
124
118
|
|
|
125
119
|
if (!disabledInEnvironment) {
|
|
126
120
|
continue;
|
|
@@ -128,16 +122,10 @@ async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
// --enabledIn=<environment>
|
|
131
|
-
if (
|
|
132
|
-
options.enabledIn
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
) {
|
|
136
|
-
const enabledInEnvironment = parsedFeature.environments[options.enabledIn].rules.some(
|
|
137
|
-
(rule) => {
|
|
138
|
-
return rule.percentage > 0;
|
|
139
|
-
},
|
|
140
|
-
);
|
|
125
|
+
if (options.enabledIn && parsedFeature.rules && parsedFeature.rules[options.enabledIn]) {
|
|
126
|
+
const enabledInEnvironment = parsedFeature.rules[options.enabledIn].some((rule) => {
|
|
127
|
+
return rule.percentage > 0;
|
|
128
|
+
});
|
|
141
129
|
|
|
142
130
|
if (!enabledInEnvironment) {
|
|
143
131
|
continue;
|
|
@@ -168,12 +156,7 @@ async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
|
168
156
|
? options.variable
|
|
169
157
|
: [options.variable];
|
|
170
158
|
|
|
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
|
-
}
|
|
159
|
+
let variablesInFeature: string[] = Object.keys(parsedFeature.variablesSchema || {});
|
|
177
160
|
|
|
178
161
|
const hasVariables = lookForVariables.every((variable) =>
|
|
179
162
|
variablesInFeature.includes(variable),
|
|
@@ -338,6 +321,11 @@ async function listEntities<T>(deps: Dependencies, entityType): Promise<T[]> {
|
|
|
338
321
|
const testEntityType = (test as TestSegment).segment ? "segment" : "feature";
|
|
339
322
|
let testAssertions = test.assertions;
|
|
340
323
|
|
|
324
|
+
// --entityType=<type>
|
|
325
|
+
if (options.entityType && options.entityType !== testEntityType) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
341
329
|
// --apply-matrix
|
|
342
330
|
if (options.applyMatrix) {
|
|
343
331
|
if (testEntityType === "feature") {
|
|
@@ -428,7 +416,7 @@ function printResult({ result, entityType, options }) {
|
|
|
428
416
|
}
|
|
429
417
|
|
|
430
418
|
export async function listProject(deps: Dependencies) {
|
|
431
|
-
const {
|
|
419
|
+
const { options } = deps;
|
|
432
420
|
|
|
433
421
|
// features
|
|
434
422
|
if (options.features) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssertionMatrix, FeatureAssertion, SegmentAssertion } from "@featurevisor/types";
|
|
1
|
+
import type { AssertionMatrix, FeatureAssertion, SegmentAssertion } from "@featurevisor/types";
|
|
2
2
|
|
|
3
3
|
function generateCombinations(
|
|
4
4
|
keys: string[],
|
|
@@ -73,8 +73,8 @@ export function applyCombinationToFeatureAssertion(
|
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
// context
|
|
76
|
-
flattenedAssertion.context = Object.keys(flattenedAssertion.context).reduce((acc, key) => {
|
|
77
|
-
acc[key] = applyCombinationToValue(flattenedAssertion.context[key], combination);
|
|
76
|
+
flattenedAssertion.context = Object.keys(flattenedAssertion.context || {}).reduce((acc, key) => {
|
|
77
|
+
acc[key] = applyCombinationToValue(flattenedAssertion.context?.[key], combination);
|
|
78
78
|
|
|
79
79
|
return acc;
|
|
80
80
|
}, {});
|
|
@@ -105,9 +105,20 @@ export function getFeatureAssertionsFromMatrix(
|
|
|
105
105
|
): FeatureAssertion[] {
|
|
106
106
|
if (!assertionWithMatrix.matrix) {
|
|
107
107
|
const assertion = { ...assertionWithMatrix };
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
|
|
109
|
+
let suffix;
|
|
110
|
+
|
|
111
|
+
if (assertion.environment) {
|
|
112
|
+
suffix = ` (${assertion.environment})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (assertion.description) {
|
|
116
|
+
suffix = `: ${assertion.description}`;
|
|
117
|
+
} else {
|
|
118
|
+
suffix = `: at ${assertion.at}%`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assertion.description = `Assertion #${aIndex + 1}${suffix}`;
|
|
111
122
|
|
|
112
123
|
return [assertion];
|
|
113
124
|
}
|
|
@@ -118,9 +129,20 @@ export function getFeatureAssertionsFromMatrix(
|
|
|
118
129
|
for (let cIndex = 0; cIndex < combinations.length; cIndex++) {
|
|
119
130
|
const combination = combinations[cIndex];
|
|
120
131
|
const assertion = applyCombinationToFeatureAssertion(combination, assertionWithMatrix);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
|
|
133
|
+
let suffix;
|
|
134
|
+
|
|
135
|
+
if (assertion.environment) {
|
|
136
|
+
suffix = ` (${assertion.environment})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (assertion.description) {
|
|
140
|
+
suffix = `: ${assertion.description}`;
|
|
141
|
+
} else {
|
|
142
|
+
suffix = `: at ${assertion.at}%`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
assertion.description = `Assertion #${aIndex + 1}${suffix}`;
|
|
124
146
|
|
|
125
147
|
assertions.push(assertion);
|
|
126
148
|
}
|
|
@@ -161,8 +183,8 @@ export function getSegmentAssertionsFromMatrix(
|
|
|
161
183
|
): SegmentAssertion[] {
|
|
162
184
|
if (!assertionWithMatrix.matrix) {
|
|
163
185
|
const assertion = { ...assertionWithMatrix };
|
|
164
|
-
assertion.description = `Assertion #${aIndex + 1}
|
|
165
|
-
assertion.description
|
|
186
|
+
assertion.description = `Assertion #${aIndex + 1}${
|
|
187
|
+
assertion.description ? `: ${assertion.description}` : ""
|
|
166
188
|
}`;
|
|
167
189
|
|
|
168
190
|
return [assertion];
|
package/src/site/exportSite.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
-
import * as mkdirp from "mkdirp";
|
|
5
|
-
|
|
6
4
|
import { generateHistory } from "./generateHistory";
|
|
7
5
|
import { getRepoDetails } from "./getRepoDetails";
|
|
8
6
|
import { generateSiteSearchIndex } from "./generateSiteSearchIndex";
|
|
@@ -13,7 +11,7 @@ export async function exportSite(deps: Dependencies) {
|
|
|
13
11
|
|
|
14
12
|
const hasError = false;
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
fs.mkdirSync(projectConfig.siteExportDirectoryPath, { recursive: true });
|
|
17
15
|
|
|
18
16
|
const sitePackagePath = path.dirname(require.resolve("@featurevisor/site/package.json"));
|
|
19
17
|
|
|
@@ -35,7 +33,7 @@ export async function exportSite(deps: Dependencies) {
|
|
|
35
33
|
|
|
36
34
|
// copy datafiles
|
|
37
35
|
fs.cpSync(
|
|
38
|
-
projectConfig.
|
|
36
|
+
projectConfig.datafilesDirectoryPath,
|
|
39
37
|
path.join(projectConfig.siteExportDirectoryPath, "datafiles"),
|
|
40
38
|
{ recursive: true },
|
|
41
39
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
|
|
4
|
-
import { HistoryEntry } from "@featurevisor/types";
|
|
4
|
+
import type { HistoryEntry } from "@featurevisor/types";
|
|
5
5
|
import { Dependencies } from "../dependencies";
|
|
6
6
|
|
|
7
7
|
export async function generateHistory(deps: Dependencies): Promise<HistoryEntry[]> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
HistoryEntry,
|
|
3
3
|
SearchIndex,
|
|
4
4
|
AttributeKey,
|
|
@@ -87,13 +87,15 @@ export async function generateSiteSearchIndex(
|
|
|
87
87
|
|
|
88
88
|
if (Array.isArray(parsed.variations)) {
|
|
89
89
|
parsed.variations.forEach((variation) => {
|
|
90
|
-
if (!variation.
|
|
90
|
+
if (!variation.variableOverrides) {
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
variation.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
Object.keys(variation.variableOverrides).forEach((variableKey) => {
|
|
95
|
+
const overrides = variation.variableOverrides?.[variableKey];
|
|
96
|
+
|
|
97
|
+
if (overrides) {
|
|
98
|
+
overrides.forEach((o) => {
|
|
97
99
|
if (o.conditions) {
|
|
98
100
|
extractAttributeKeysFromConditions(o.conditions).forEach((attributeKey) => {
|
|
99
101
|
if (!attributesUsedInFeatures[attributeKey]) {
|
|
@@ -119,12 +121,28 @@ export async function generateSiteSearchIndex(
|
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
//
|
|
123
|
-
if (parsed.
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// rules
|
|
125
|
+
if (parsed.rules) {
|
|
126
|
+
if (!Array.isArray(parsed.rules)) {
|
|
127
|
+
// with environments
|
|
128
|
+
Object.keys(parsed.rules).forEach((environmentKey) => {
|
|
129
|
+
const rules = parsed.rules?.[environmentKey];
|
|
130
|
+
|
|
131
|
+
rules.forEach((rule) => {
|
|
132
|
+
if (rule.segments && rule.segments !== "*") {
|
|
133
|
+
extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
|
|
134
|
+
if (!segmentsUsedInFeatures[segmentKey]) {
|
|
135
|
+
segmentsUsedInFeatures[segmentKey] = new Set();
|
|
136
|
+
}
|
|
126
137
|
|
|
127
|
-
|
|
138
|
+
segmentsUsedInFeatures[segmentKey].add(entityName);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
// no environments
|
|
145
|
+
parsed.rules.forEach((rule) => {
|
|
128
146
|
if (rule.segments && rule.segments !== "*") {
|
|
129
147
|
extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
|
|
130
148
|
if (!segmentsUsedInFeatures[segmentKey]) {
|
|
@@ -135,9 +153,15 @@ export async function generateSiteSearchIndex(
|
|
|
135
153
|
});
|
|
136
154
|
}
|
|
137
155
|
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
138
158
|
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
// force
|
|
160
|
+
if (parsed.force) {
|
|
161
|
+
if (!Array.isArray(parsed.force)) {
|
|
162
|
+
// with environments
|
|
163
|
+
Object.keys(parsed.force).forEach((environmentKey) => {
|
|
164
|
+
parsed.force?.[environmentKey].forEach((force) => {
|
|
141
165
|
if (force.segments && force.segments !== "*") {
|
|
142
166
|
extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
|
|
143
167
|
if (!segmentsUsedInFeatures[segmentKey]) {
|
|
@@ -158,47 +182,31 @@ export async function generateSiteSearchIndex(
|
|
|
158
182
|
});
|
|
159
183
|
}
|
|
160
184
|
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!segmentsUsedInFeatures[segmentKey]) {
|
|
171
|
-
segmentsUsedInFeatures[segmentKey] = new Set();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
segmentsUsedInFeatures[segmentKey].add(entityName);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
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
|
-
}
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
// no environments
|
|
188
|
+
parsed.force.forEach((force) => {
|
|
189
|
+
if (force.segments && force.segments !== "*") {
|
|
190
|
+
extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
|
|
191
|
+
if (!segmentsUsedInFeatures[segmentKey]) {
|
|
192
|
+
segmentsUsedInFeatures[segmentKey] = new Set();
|
|
193
|
+
}
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
segmentsUsedInFeatures[segmentKey].add(entityName);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
191
198
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
if (force.conditions) {
|
|
200
|
+
extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
|
|
201
|
+
if (!attributesUsedInFeatures[attributeKey]) {
|
|
202
|
+
attributesUsedInFeatures[attributeKey] = new Set();
|
|
203
|
+
}
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
attributesUsedInFeatures[attributeKey].add(entityName);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
// push
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { checkIfObjectsAreEqual, checkIfArraysAreEqual } from "./helpers";
|
|
2
|
+
|
|
3
|
+
describe("helpers", function () {
|
|
4
|
+
describe("checkIfObjectsAreEqual", function () {
|
|
5
|
+
it("should return true for two empty objects", () => {
|
|
6
|
+
expect(checkIfObjectsAreEqual({}, {})).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should return true for objects with same keys and values", () => {
|
|
10
|
+
expect(checkIfObjectsAreEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return false for objects with different keys", () => {
|
|
14
|
+
expect(checkIfObjectsAreEqual({ a: 1 }, { b: 1 })).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return false for objects with same keys but different values", () => {
|
|
18
|
+
expect(checkIfObjectsAreEqual({ a: 1 }, { a: 2 })).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return false if one object has extra keys", () => {
|
|
22
|
+
expect(checkIfObjectsAreEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
|
|
23
|
+
expect(checkIfObjectsAreEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return true for nested objects that are deeply equal", () => {
|
|
27
|
+
expect(checkIfObjectsAreEqual({ a: { b: 2 }, c: 3 }, { a: { b: 2 }, c: 3 })).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return false for nested objects that are not deeply equal", () => {
|
|
31
|
+
expect(checkIfObjectsAreEqual({ a: { b: 2 }, c: 3 }, { a: { b: 3 }, c: 3 })).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// it("should return false if types differ", () => {
|
|
35
|
+
// expect(checkIfObjectsAreEqual({ a: 1 }, null as any)).toBe(false);
|
|
36
|
+
// expect(checkIfObjectsAreEqual(null as any, { a: 1 })).toBe(false);
|
|
37
|
+
// expect(checkIfObjectsAreEqual([], {})).toBe(false);
|
|
38
|
+
// });
|
|
39
|
+
|
|
40
|
+
it("should return true for objects with array values that are equal", () => {
|
|
41
|
+
expect(checkIfObjectsAreEqual({ a: [1, 2, 3] }, { a: [1, 2, 3] })).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return false for objects with array values that are not equal", () => {
|
|
45
|
+
expect(checkIfObjectsAreEqual({ a: [1, 2, 3] }, { a: [1, 2, 4] })).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return true for objects with different key order", () => {
|
|
49
|
+
expect(checkIfObjectsAreEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return true for objects with undefined values if both have them", () => {
|
|
53
|
+
expect(checkIfObjectsAreEqual({ a: undefined }, { a: undefined })).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should return false for objects with one undefined and one missing key", () => {
|
|
57
|
+
expect(checkIfObjectsAreEqual({ a: undefined }, {})).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("checkIfArraysAreEqual", function () {
|
|
62
|
+
it("should return true for two empty arrays", () => {
|
|
63
|
+
expect(checkIfArraysAreEqual([], [])).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return true for arrays with same elements in same order", () => {
|
|
67
|
+
expect(checkIfArraysAreEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return false for arrays with same elements in different order", () => {
|
|
71
|
+
expect(checkIfArraysAreEqual([1, 2, 3], [3, 2, 1])).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return false for arrays of different lengths", () => {
|
|
75
|
+
expect(checkIfArraysAreEqual([1, 2], [1, 2, 3])).toBe(false);
|
|
76
|
+
expect(checkIfArraysAreEqual([1, 2, 3], [1, 2])).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return false for arrays with different elements", () => {
|
|
80
|
+
expect(checkIfArraysAreEqual([1, 2, 3], [1, 2, 4])).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return true for nested arrays that are deeply equal", () => {
|
|
84
|
+
expect(checkIfArraysAreEqual([[1, 2], [3]], [[1, 2], [3]])).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return false for nested arrays that are not deeply equal", () => {
|
|
88
|
+
expect(checkIfArraysAreEqual([[1, 2], [3]], [[1, 2], [4]])).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should return true for arrays with objects that are deeply equal", () => {
|
|
92
|
+
expect(checkIfArraysAreEqual([{ a: 1 }, { b: 2 }], [{ a: 1 }, { b: 2 }])).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return false for arrays with objects that are not deeply equal", () => {
|
|
96
|
+
expect(checkIfArraysAreEqual([{ a: 1 }, { b: 2 }], [{ a: 1 }, { b: 3 }])).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should return false if types differ", () => {
|
|
100
|
+
expect(checkIfArraysAreEqual([1, 2, 3], null as any)).toBe(false);
|
|
101
|
+
expect(checkIfArraysAreEqual(null as any, [1, 2, 3])).toBe(false);
|
|
102
|
+
expect(checkIfArraysAreEqual({} as any, [])).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should return true for arrays with undefined values if both have them in same positions", () => {
|
|
106
|
+
expect(checkIfArraysAreEqual([1, undefined, 3], [1, undefined, 3])).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should return false for arrays with undefined values in different positions", () => {
|
|
110
|
+
expect(checkIfArraysAreEqual([1, undefined, 3], [1, 3, undefined])).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should return true for arrays with different reference but same primitive values", () => {
|
|
114
|
+
expect(checkIfArraysAreEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return true for arrays with nested objects and arrays that are deeply equal", () => {
|
|
118
|
+
expect(
|
|
119
|
+
checkIfArraysAreEqual([{ a: [1, 2], b: { c: 3 } }], [{ a: [1, 2], b: { c: 3 } }]),
|
|
120
|
+
).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should return false for arrays with nested objects and arrays that are not deeply equal", () => {
|
|
124
|
+
expect(
|
|
125
|
+
checkIfArraysAreEqual([{ a: [1, 2], b: { c: 3 } }], [{ a: [1, 2], b: { c: 4 } }]),
|
|
126
|
+
).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return false for arrays with extra undefined at the end", () => {
|
|
130
|
+
expect(checkIfArraysAreEqual([1, 2], [1, 2, undefined])).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// it("should return true for arrays with NaN values in same positions", () => {
|
|
134
|
+
// expect(checkIfArraysAreEqual([NaN, 2], [NaN, 2])).toBe(true);
|
|
135
|
+
// });
|
|
136
|
+
|
|
137
|
+
// it("should return false for arrays with NaN values in different positions", () => {
|
|
138
|
+
// expect(checkIfArraysAreEqual([2, NaN], [NaN, 2])).toBe(false);
|
|
139
|
+
// });
|
|
140
|
+
|
|
141
|
+
it("should return true for arrays with null values in same positions", () => {
|
|
142
|
+
expect(checkIfArraysAreEqual([null, 2], [null, 2])).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should return false for arrays with null values in different positions", () => {
|
|
146
|
+
expect(checkIfArraysAreEqual([2, null], [null, 2])).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function checkIfObjectsAreEqual(obj1: any, obj2: any): boolean {
|
|
2
|
+
if (obj1 === obj2) {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof obj1 !== "object" || obj1 === null || typeof obj2 !== "object" || obj2 === null) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const keys1 = Object.keys(obj1);
|
|
11
|
+
const keys2 = Object.keys(obj2);
|
|
12
|
+
|
|
13
|
+
if (keys1.length !== keys2.length) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const key of keys1) {
|
|
18
|
+
if (!keys2.includes(key)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const val1 = obj1[key];
|
|
23
|
+
const val2 = obj2[key];
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
26
|
+
if (!checkIfArraysAreEqual(val1, val2)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
} else if (typeof val1 === "object" && typeof val2 === "object") {
|
|
30
|
+
if (!checkIfObjectsAreEqual(val1, val2)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
} else if (val1 !== val2) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function checkIfArraysAreEqual(arr1: any[], arr2: any[]): boolean {
|
|
41
|
+
if (arr1 === arr2) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (arr1.length !== arr2.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < arr1.length; i++) {
|
|
54
|
+
const val1 = arr1[i];
|
|
55
|
+
const val2 = arr2[i];
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
58
|
+
if (!checkIfArraysAreEqual(val1, val2)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
} else if (
|
|
62
|
+
typeof val1 === "object" &&
|
|
63
|
+
typeof val2 === "object" &&
|
|
64
|
+
val1 !== null &&
|
|
65
|
+
val2 !== null
|
|
66
|
+
) {
|
|
67
|
+
if (!checkIfObjectsAreEqual(val1, val2)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
} else if (val1 !== val2) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
package/src/tester/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TestResult } from "@featurevisor/types";
|
|
1
|
+
import type { TestResult } from "@featurevisor/types";
|
|
2
2
|
|
|
3
3
|
import { CLI_FORMAT_BOLD, CLI_FORMAT_RED } from "./cliFormat";
|
|
4
4
|
import { prettyDuration } from "./prettyDuration";
|
|
@@ -35,16 +35,38 @@ export function printTestResult(testResult: TestResult, relativeTestFilePath, ro
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
let section: string = error.type;
|
|
39
|
+
|
|
40
|
+
if (error.type === "flag") {
|
|
41
|
+
section = "expectedToBeEnabled";
|
|
42
|
+
} else if (error.type === "variation") {
|
|
43
|
+
section = "expectedVariation";
|
|
44
|
+
} else if (error.type === "variable") {
|
|
45
|
+
section = "expectedVariables";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error.details && error.details.childIndex !== undefined) {
|
|
49
|
+
section = `children[${error.details.childIndex}].${section}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
if (error.type === "variable") {
|
|
39
53
|
const variableKey = (error.details as any).variableKey;
|
|
40
54
|
|
|
41
|
-
console.log(CLI_FORMAT_RED, ` =>
|
|
55
|
+
console.log(CLI_FORMAT_RED, ` => ${section}.${variableKey}:`);
|
|
42
56
|
console.log(CLI_FORMAT_RED, ` => expected: ${error.expected}`);
|
|
43
57
|
console.log(CLI_FORMAT_RED, ` => received: ${error.actual}`);
|
|
44
58
|
} else {
|
|
59
|
+
if (error.type === "evaluation") {
|
|
60
|
+
if (error.details && error.details.variableKey) {
|
|
61
|
+
section = `${section}.variables.${error.details.variableKey}.${error.details.evaluationKey}`;
|
|
62
|
+
} else if (error.details && error.details.evaluationType) {
|
|
63
|
+
section = `${section}.${error.details.evaluationType}.${error.details.evaluationKey}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
console.log(
|
|
46
68
|
CLI_FORMAT_RED,
|
|
47
|
-
` => ${
|
|
69
|
+
` => ${section}: expected "${error.expected}", received "${error.actual}"`,
|
|
48
70
|
);
|
|
49
71
|
}
|
|
50
72
|
});
|