@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.
- 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 +88 -64
- 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 +96 -24
- 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 -49
- 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 +40 -16
- 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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FeatureKey,
|
|
5
|
+
Feature,
|
|
6
|
+
SegmentKey,
|
|
7
|
+
Segment,
|
|
8
|
+
DatafileContent,
|
|
9
|
+
} from "@featurevisor/types";
|
|
10
|
+
|
|
11
|
+
import { extractSegmentsFromFeature } from "../utils";
|
|
12
|
+
|
|
13
|
+
const base62chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
14
|
+
|
|
15
|
+
function generateHashFromString(str: string, length = 10): string {
|
|
16
|
+
const hashBuffer = crypto.createHash("sha256").update(str).digest();
|
|
17
|
+
|
|
18
|
+
// Convert buffer to base62 (alphanumeric)
|
|
19
|
+
let num = BigInt("0x" + hashBuffer.toString("hex"));
|
|
20
|
+
let base62 = "";
|
|
21
|
+
while (num > 0) {
|
|
22
|
+
// Convert the remainder to a number for indexing
|
|
23
|
+
const remainder = Number(num % 62n);
|
|
24
|
+
base62 = base62chars[remainder] + base62;
|
|
25
|
+
num = num / 62n;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Return first 10 chars for a short hash (adjust length as needed)
|
|
29
|
+
return base62.slice(0, length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSegmentHashes(
|
|
33
|
+
segments: Record<SegmentKey, Segment>,
|
|
34
|
+
): Record<SegmentKey, string> {
|
|
35
|
+
const result: Record<SegmentKey, string> = {};
|
|
36
|
+
|
|
37
|
+
for (const segmentKey of Object.keys(segments)) {
|
|
38
|
+
const segment = segments[segmentKey];
|
|
39
|
+
result[segmentKey] = generateHashFromString(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
conditions: segment.conditions,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateHashForFeature(
|
|
50
|
+
featureKey: FeatureKey,
|
|
51
|
+
features: Record<FeatureKey, Feature>,
|
|
52
|
+
segmentHashes: Record<SegmentKey, string>,
|
|
53
|
+
): string {
|
|
54
|
+
const feature = features[featureKey];
|
|
55
|
+
|
|
56
|
+
if (!feature) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const requiredFeatureKeys: string[] = [];
|
|
61
|
+
if (feature.required) {
|
|
62
|
+
for (const r of feature.required) {
|
|
63
|
+
if (typeof r === "string") {
|
|
64
|
+
requiredFeatureKeys.push(r);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof r === "object" && r.key) {
|
|
68
|
+
requiredFeatureKeys.push(r.key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requiredFeatureHashes = requiredFeatureKeys.map((key) =>
|
|
74
|
+
generateHashForFeature(key, features, segmentHashes),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const usedSegments = extractSegmentsFromFeature(feature);
|
|
78
|
+
const usedSegmentHashes = Object.keys(usedSegments).map(
|
|
79
|
+
(segmentKey) => segmentHashes[segmentKey],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return generateHashFromString(
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
featureKey,
|
|
85
|
+
feature,
|
|
86
|
+
requiredFeatureHashes,
|
|
87
|
+
usedSegmentHashes,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function generateHashForDatafile(datafileContent: DatafileContent): string {
|
|
93
|
+
const featureHashes = Object.keys(datafileContent.features).reduce(
|
|
94
|
+
(acc, featureKey) => {
|
|
95
|
+
acc[featureKey] = datafileContent.features[featureKey].hash || "";
|
|
96
|
+
return acc;
|
|
97
|
+
},
|
|
98
|
+
{} as Record<FeatureKey, string>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const hash = generateHashFromString(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
schemaVersion: datafileContent.schemaVersion,
|
|
104
|
+
featureHashes,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return hash;
|
|
109
|
+
}
|
package/src/builder/traffic.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
Rule,
|
|
3
|
+
ExistingFeature,
|
|
4
|
+
Traffic,
|
|
5
|
+
Variation,
|
|
6
|
+
Range,
|
|
7
|
+
Percentage,
|
|
8
|
+
} from "@featurevisor/types";
|
|
2
9
|
import { MAX_BUCKETED_NUMBER } from "@featurevisor/sdk";
|
|
3
10
|
|
|
4
11
|
import { getAllocation, getUpdatedAvailableRangesAfterFilling } from "./allocator";
|
|
@@ -71,7 +78,7 @@ export function getTraffic(
|
|
|
71
78
|
): Traffic[] {
|
|
72
79
|
const result: Traffic[] = [];
|
|
73
80
|
|
|
74
|
-
// @
|
|
81
|
+
// @NOTE: may be pass from builder directly?
|
|
75
82
|
const availableRanges =
|
|
76
83
|
ranges && ranges.length > 0 ? ranges : ([[0, MAX_BUCKETED_NUMBER]] as Range[]);
|
|
77
84
|
|
|
@@ -83,6 +90,7 @@ export function getTraffic(
|
|
|
83
90
|
segments: parsedRule.segments,
|
|
84
91
|
percentage: rulePercentage * (MAX_BUCKETED_NUMBER / 100),
|
|
85
92
|
allocation: [],
|
|
93
|
+
variationWeights: parsedRule.variationWeights,
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
// overrides
|
|
@@ -104,11 +112,15 @@ export function getTraffic(
|
|
|
104
112
|
!existingTrafficRule || // new rule
|
|
105
113
|
variationsChanged || // variations changed
|
|
106
114
|
rulePercentageDiff < 0 || // percentage decreased
|
|
107
|
-
rangesChanged
|
|
115
|
+
rangesChanged || // belongs to a group, and group ranges changed
|
|
116
|
+
// @NOTE: this means, if variationWeights is present, it will always rebucket.
|
|
117
|
+
// worth checking if we can maintain consistent bucketing for this use case as well.
|
|
118
|
+
// but this use case is unlikely to hit in practice because it doesn't matter if the feature itself is 100% rolled out.
|
|
119
|
+
traffic.variationWeights; // variation weights overridden
|
|
108
120
|
|
|
109
121
|
let updatedAvailableRanges = JSON.parse(JSON.stringify(availableRanges));
|
|
110
122
|
|
|
111
|
-
if (existingTrafficRule && !needsRebucketing) {
|
|
123
|
+
if (existingTrafficRule && existingTrafficRule.allocation && !needsRebucketing) {
|
|
112
124
|
// increase: build on top of existing allocations
|
|
113
125
|
let existingSum = 0;
|
|
114
126
|
|
|
@@ -128,7 +140,13 @@ export function getTraffic(
|
|
|
128
140
|
|
|
129
141
|
if (Array.isArray(variations)) {
|
|
130
142
|
variations.forEach(function (variation) {
|
|
131
|
-
|
|
143
|
+
let weight = variation.weight as number;
|
|
144
|
+
|
|
145
|
+
if (traffic.variationWeights && traffic.variationWeights[variation.value]) {
|
|
146
|
+
// override weight from rule
|
|
147
|
+
weight = traffic.variationWeights[variation.value];
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
const percentage = weight * (MAX_BUCKETED_NUMBER / 100);
|
|
133
151
|
|
|
134
152
|
const toFillValue = needsRebucketing
|
|
@@ -137,10 +155,12 @@ export function getTraffic(
|
|
|
137
155
|
const rangesToFill = getAllocation(updatedAvailableRanges, toFillValue);
|
|
138
156
|
|
|
139
157
|
rangesToFill.forEach(function (range) {
|
|
140
|
-
traffic.allocation
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
if (traffic.allocation) {
|
|
159
|
+
traffic.allocation.push({
|
|
160
|
+
variation: variation.value,
|
|
161
|
+
range,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
144
164
|
});
|
|
145
165
|
|
|
146
166
|
updatedAvailableRanges = getUpdatedAvailableRangesAfterFilling(
|
|
@@ -150,15 +170,19 @@ export function getTraffic(
|
|
|
150
170
|
});
|
|
151
171
|
}
|
|
152
172
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
if (traffic.allocation) {
|
|
174
|
+
traffic.allocation = traffic.allocation.filter((a) => {
|
|
175
|
+
if (a.range && a.range[0] === a.range[1]) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
160
181
|
|
|
161
|
-
|
|
182
|
+
if (traffic.allocation.length === 0) {
|
|
183
|
+
delete traffic.allocation;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
162
186
|
|
|
163
187
|
result.push(traffic);
|
|
164
188
|
});
|
package/src/cli/cli.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function runCLI(runnerOptions: RunnerOptions) {
|
|
|
51
51
|
y = y.command({
|
|
52
52
|
command: plugin.command,
|
|
53
53
|
handler: async function (parsed: ParsedOptions) {
|
|
54
|
-
// @
|
|
54
|
+
// @NOTE: in future, allow yargs options to be defined via plugins
|
|
55
55
|
if (parsed.schemaVersion && typeof parsed.schemaVersion !== "string") {
|
|
56
56
|
parsed.schemaVersion = parsed.schemaVersion.toString();
|
|
57
57
|
}
|
package/src/cli/plugins.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Plugin } from "./cli";
|
|
|
3
3
|
import { initPlugin } from "../init";
|
|
4
4
|
import { lintPlugin } from "../linter";
|
|
5
5
|
import { buildPlugin } from "../builder";
|
|
6
|
-
import { restorePlugin } from "../restore";
|
|
7
6
|
import { testPlugin } from "../tester";
|
|
8
7
|
import { generateCodePlugin } from "../generate-code";
|
|
9
8
|
import { findDuplicateSegmentsPlugin } from "../find-duplicate-segments";
|
|
@@ -23,7 +22,6 @@ export const nonProjectPlugins: Plugin[] = [initPlugin];
|
|
|
23
22
|
export const projectBasedPlugins: Plugin[] = [
|
|
24
23
|
lintPlugin,
|
|
25
24
|
buildPlugin,
|
|
26
|
-
restorePlugin,
|
|
27
25
|
testPlugin,
|
|
28
26
|
generateCodePlugin,
|
|
29
27
|
findDuplicateSegmentsPlugin,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
|
|
3
|
-
import { BucketBy } from "@featurevisor/types";
|
|
3
|
+
import type { BucketBy } from "@featurevisor/types";
|
|
4
4
|
|
|
5
5
|
import { Parser, parsers } from "./parsers";
|
|
6
6
|
import { FilesystemAdapter } from "../datasource/filesystemAdapter";
|
|
@@ -12,7 +12,8 @@ export const ATTRIBUTES_DIRECTORY_NAME = "attributes";
|
|
|
12
12
|
export const GROUPS_DIRECTORY_NAME = "groups";
|
|
13
13
|
export const TESTS_DIRECTORY_NAME = "tests";
|
|
14
14
|
export const STATE_DIRECTORY_NAME = ".featurevisor";
|
|
15
|
-
export const
|
|
15
|
+
export const DATAFILES_DIRECTORY_NAME = "datafiles";
|
|
16
|
+
export const DATAFILE_NAME_PATTERN = "featurevisor-%s.json";
|
|
16
17
|
export const SITE_EXPORT_DIRECTORY_NAME = "out";
|
|
17
18
|
|
|
18
19
|
export const CONFIG_MODULE_NAME = "featurevisor.config.js";
|
|
@@ -22,12 +23,12 @@ export const DEFAULT_ENVIRONMENTS = ["staging", "production"];
|
|
|
22
23
|
export const DEFAULT_TAGS = ["all"];
|
|
23
24
|
export const DEFAULT_BUCKET_BY_ATTRIBUTE = "userId";
|
|
24
25
|
|
|
25
|
-
export const DEFAULT_PRETTY_STATE =
|
|
26
|
+
export const DEFAULT_PRETTY_STATE = true;
|
|
26
27
|
export const DEFAULT_PRETTY_DATAFILE = false;
|
|
27
28
|
|
|
28
29
|
export const DEFAULT_PARSER: Parser = "yml";
|
|
29
30
|
|
|
30
|
-
export const SCHEMA_VERSION = "
|
|
31
|
+
export const SCHEMA_VERSION = "2"; // default schema version
|
|
31
32
|
|
|
32
33
|
export interface ProjectConfig {
|
|
33
34
|
featuresDirectoryPath: string;
|
|
@@ -36,13 +37,14 @@ export interface ProjectConfig {
|
|
|
36
37
|
groupsDirectoryPath: string;
|
|
37
38
|
testsDirectoryPath: string;
|
|
38
39
|
stateDirectoryPath: string;
|
|
39
|
-
|
|
40
|
+
datafilesDirectoryPath: string;
|
|
41
|
+
datafileNamePattern: string;
|
|
40
42
|
siteExportDirectoryPath: string;
|
|
41
43
|
|
|
42
44
|
environments: string[] | false;
|
|
43
45
|
tags: string[];
|
|
44
46
|
|
|
45
|
-
adapter: any; // @
|
|
47
|
+
adapter: any; // @NOTE: type this properly later
|
|
46
48
|
plugins: Plugin[];
|
|
47
49
|
|
|
48
50
|
defaultBucketBy: BucketBy;
|
|
@@ -80,7 +82,8 @@ export function getProjectConfig(rootDirectoryPath: string): ProjectConfig {
|
|
|
80
82
|
groupsDirectoryPath: path.join(rootDirectoryPath, GROUPS_DIRECTORY_NAME),
|
|
81
83
|
testsDirectoryPath: path.join(rootDirectoryPath, TESTS_DIRECTORY_NAME),
|
|
82
84
|
stateDirectoryPath: path.join(rootDirectoryPath, STATE_DIRECTORY_NAME),
|
|
83
|
-
|
|
85
|
+
datafilesDirectoryPath: path.join(rootDirectoryPath, DATAFILES_DIRECTORY_NAME),
|
|
86
|
+
datafileNamePattern: DATAFILE_NAME_PATTERN,
|
|
84
87
|
siteExportDirectoryPath: path.join(rootDirectoryPath, SITE_EXPORT_DIRECTORY_NAME),
|
|
85
88
|
|
|
86
89
|
enforceCatchAllRule: false,
|
|
@@ -121,7 +124,7 @@ export function getProjectConfig(rootDirectoryPath: string): ProjectConfig {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
export interface ShowProjectConfigOptions {
|
|
124
|
-
|
|
127
|
+
json?: boolean;
|
|
125
128
|
pretty?: boolean;
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -129,7 +132,7 @@ export function showProjectConfig(
|
|
|
129
132
|
projectConfig: ProjectConfig,
|
|
130
133
|
options: ShowProjectConfigOptions = {},
|
|
131
134
|
) {
|
|
132
|
-
if (options.
|
|
135
|
+
if (options.json) {
|
|
133
136
|
console.log(
|
|
134
137
|
options.pretty ? JSON.stringify(projectConfig, null, 2) : JSON.stringify(projectConfig),
|
|
135
138
|
);
|
|
@@ -157,7 +160,7 @@ export const configPlugin: Plugin = {
|
|
|
157
160
|
handler: async ({ rootDirectoryPath, parsed }) => {
|
|
158
161
|
const projectConfig = getProjectConfig(rootDirectoryPath);
|
|
159
162
|
showProjectConfig(projectConfig, {
|
|
160
|
-
|
|
163
|
+
json: parsed.json,
|
|
161
164
|
pretty: parsed.pretty,
|
|
162
165
|
});
|
|
163
166
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
ParsedFeature,
|
|
3
3
|
Segment,
|
|
4
4
|
Attribute,
|
|
@@ -27,7 +27,7 @@ export class Datasource {
|
|
|
27
27
|
this.adapter = new config.adapter(config, rootDirectoryPath);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// @
|
|
30
|
+
// @NOTE: only site generator needs it, find a way to get it out of here later
|
|
31
31
|
getExtension() {
|
|
32
32
|
return (this.config.parser as CustomParser).extension;
|
|
33
33
|
}
|
|
@@ -145,6 +145,27 @@ export class Datasource {
|
|
|
145
145
|
return this.adapter.listEntities("attribute");
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
async listFlattenedAttributes() {
|
|
149
|
+
const attributes = await this.listAttributes();
|
|
150
|
+
const result: string[] = [];
|
|
151
|
+
|
|
152
|
+
for (const key of attributes) {
|
|
153
|
+
const attribute = await this.readAttribute(key);
|
|
154
|
+
|
|
155
|
+
result.push(key);
|
|
156
|
+
|
|
157
|
+
if (attribute.type === "object") {
|
|
158
|
+
// @NOTE: in future, this can be recursive
|
|
159
|
+
const propertyKeys = Object.keys(attribute.properties || {});
|
|
160
|
+
for (const propertyKey of propertyKeys) {
|
|
161
|
+
result.push(`${key}.${propertyKey}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
attributeExists(attributeKey: AttributeKey) {
|
|
149
170
|
return this.adapter.entityExists("attribute", attributeKey);
|
|
150
171
|
}
|
|
@@ -2,9 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execSync, spawn } from "child_process";
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {
|
|
5
|
+
import type {
|
|
8
6
|
ExistingState,
|
|
9
7
|
EnvironmentKey,
|
|
10
8
|
DatafileContent,
|
|
@@ -19,8 +17,6 @@ import { Adapter, DatafileOptions } from "./adapter";
|
|
|
19
17
|
import { ProjectConfig, CustomParser } from "../config";
|
|
20
18
|
import { getCommit } from "../utils/git";
|
|
21
19
|
|
|
22
|
-
const commitRegex = /^commit (\w+)\nAuthor: (.+) <(.+)>\nDate: (.+)\n\n(.+)/gm;
|
|
23
|
-
|
|
24
20
|
export function getExistingStateFilePath(
|
|
25
21
|
projectConfig: ProjectConfig,
|
|
26
22
|
environment: EnvironmentKey | false,
|
|
@@ -129,7 +125,7 @@ export class FilesystemAdapter extends Adapter {
|
|
|
129
125
|
const filePath = this.getEntityPath(entityType, entityKey);
|
|
130
126
|
|
|
131
127
|
if (!fs.existsSync(this.getEntityDirectoryPath(entityType))) {
|
|
132
|
-
|
|
128
|
+
fs.mkdirSync(this.getEntityDirectoryPath(entityType), { recursive: true });
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
fs.writeFileSync(filePath, this.parser.stringify(entity));
|
|
@@ -166,7 +162,7 @@ export class FilesystemAdapter extends Adapter {
|
|
|
166
162
|
const filePath = getExistingStateFilePath(this.config, environment);
|
|
167
163
|
|
|
168
164
|
if (!fs.existsSync(this.config.stateDirectoryPath)) {
|
|
169
|
-
|
|
165
|
+
fs.mkdirSync(this.config.stateDirectoryPath, { recursive: true });
|
|
170
166
|
}
|
|
171
167
|
fs.writeFileSync(
|
|
172
168
|
filePath,
|
|
@@ -198,6 +194,7 @@ export class FilesystemAdapter extends Adapter {
|
|
|
198
194
|
}
|
|
199
195
|
|
|
200
196
|
return "0";
|
|
197
|
+
// eslint-disable-next-line
|
|
201
198
|
} catch (e) {
|
|
202
199
|
return "0";
|
|
203
200
|
}
|
|
@@ -207,7 +204,7 @@ export class FilesystemAdapter extends Adapter {
|
|
|
207
204
|
const filePath = getRevisionFilePath(this.config);
|
|
208
205
|
|
|
209
206
|
if (!fs.existsSync(this.config.stateDirectoryPath)) {
|
|
210
|
-
|
|
207
|
+
fs.mkdirSync(this.config.stateDirectoryPath, { recursive: true });
|
|
211
208
|
}
|
|
212
209
|
|
|
213
210
|
fs.writeFileSync(filePath, revision);
|
|
@@ -217,8 +214,10 @@ export class FilesystemAdapter extends Adapter {
|
|
|
217
214
|
* Datafile
|
|
218
215
|
*/
|
|
219
216
|
getDatafilePath(options: DatafileOptions): string {
|
|
220
|
-
const
|
|
221
|
-
|
|
217
|
+
const pattern = this.config.datafileNamePattern || "featurevisor-%s.json";
|
|
218
|
+
|
|
219
|
+
const fileName = pattern.replace("%s", `tag-${options.tag}`);
|
|
220
|
+
const dir = options.datafilesDir || this.config.datafilesDirectoryPath;
|
|
222
221
|
|
|
223
222
|
if (options.environment) {
|
|
224
223
|
return path.join(dir, options.environment, fileName);
|
|
@@ -236,12 +235,12 @@ export class FilesystemAdapter extends Adapter {
|
|
|
236
235
|
}
|
|
237
236
|
|
|
238
237
|
async writeDatafile(datafileContent: DatafileContent, options: DatafileOptions): Promise<void> {
|
|
239
|
-
const dir = options.datafilesDir || this.config.
|
|
238
|
+
const dir = options.datafilesDir || this.config.datafilesDirectoryPath;
|
|
240
239
|
|
|
241
240
|
const outputEnvironmentDirPath = options.environment
|
|
242
241
|
? path.join(dir, options.environment)
|
|
243
242
|
: dir;
|
|
244
|
-
|
|
243
|
+
fs.mkdirSync(outputEnvironmentDirPath, { recursive: true });
|
|
245
244
|
|
|
246
245
|
const outputFilePath = this.getDatafilePath(options);
|
|
247
246
|
|
package/src/evaluate/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from "@featurevisor/types";
|
|
1
|
+
import type { Context, DatafileContent } from "@featurevisor/types";
|
|
2
2
|
import {
|
|
3
3
|
Evaluation,
|
|
4
4
|
createInstance,
|
|
@@ -53,7 +53,7 @@ export interface EvaluateOptions {
|
|
|
53
53
|
environment?: string;
|
|
54
54
|
feature: string;
|
|
55
55
|
context: Record<string, unknown>;
|
|
56
|
-
|
|
56
|
+
json?: boolean;
|
|
57
57
|
pretty?: boolean;
|
|
58
58
|
verbose?: boolean;
|
|
59
59
|
schemaVersion?: string;
|
|
@@ -84,9 +84,9 @@ export async function evaluateFeature(deps: Dependencies, options: EvaluateOptio
|
|
|
84
84
|
|
|
85
85
|
let logs: Log[] = [];
|
|
86
86
|
const f = createInstance({
|
|
87
|
-
datafile: datafileContent,
|
|
87
|
+
datafile: datafileContent as DatafileContent,
|
|
88
88
|
logger: createLogger({
|
|
89
|
-
|
|
89
|
+
level: "debug",
|
|
90
90
|
handler: (level, message, details) => {
|
|
91
91
|
logs.push({
|
|
92
92
|
level,
|
|
@@ -134,7 +134,7 @@ export async function evaluateFeature(deps: Dependencies, options: EvaluateOptio
|
|
|
134
134
|
variables: variableEvaluations,
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
if (options.
|
|
137
|
+
if (options.json) {
|
|
138
138
|
console.log(
|
|
139
139
|
options.pretty ? JSON.stringify(allEvaluations, null, 2) : JSON.stringify(allEvaluations),
|
|
140
140
|
);
|
|
@@ -214,7 +214,8 @@ export const evaluatePlugin: Plugin = {
|
|
|
214
214
|
environment: parsed.environment,
|
|
215
215
|
feature: parsed.feature,
|
|
216
216
|
context: parsed.context ? JSON.parse(parsed.context) : {},
|
|
217
|
-
|
|
217
|
+
// @NOTE: introduce optional --at?
|
|
218
|
+
json: parsed.json,
|
|
218
219
|
pretty: parsed.pretty,
|
|
219
220
|
verbose: parsed.verbose,
|
|
220
221
|
},
|