@featurevisor/core 1.35.3 → 2.0.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 (252) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +0 -6
  3. package/coverage/clover.xml +321 -237
  4. package/coverage/coverage-final.json +8 -8
  5. package/coverage/lcov-report/index.html +77 -47
  6. package/coverage/lcov-report/lib/builder/allocator.js.html +14 -14
  7. package/coverage/lcov-report/lib/builder/index.html +16 -16
  8. package/coverage/lcov-report/lib/builder/revision.js.html +3 -3
  9. package/coverage/lcov-report/lib/builder/traffic.js.html +88 -64
  10. package/coverage/lcov-report/lib/list/index.html +116 -0
  11. package/coverage/lcov-report/lib/{tester → list}/matrix.js.html +90 -66
  12. package/coverage/lcov-report/lib/tester/helpers.js.html +295 -0
  13. package/coverage/lcov-report/lib/tester/index.html +20 -35
  14. package/coverage/lcov-report/src/builder/allocator.ts.html +2 -2
  15. package/coverage/lcov-report/src/builder/index.html +15 -15
  16. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  17. package/coverage/lcov-report/src/builder/traffic.ts.html +96 -24
  18. package/coverage/lcov-report/src/list/index.html +116 -0
  19. package/coverage/lcov-report/src/{tester → list}/matrix.ts.html +87 -21
  20. package/coverage/lcov-report/src/tester/helpers.ts.html +313 -0
  21. package/coverage/lcov-report/src/tester/index.html +20 -35
  22. package/coverage/lcov.info +592 -436
  23. package/lib/assess-distribution/index.d.ts +1 -1
  24. package/lib/assess-distribution/index.js +102 -162
  25. package/lib/assess-distribution/index.js.map +1 -1
  26. package/lib/benchmark/index.js +87 -143
  27. package/lib/benchmark/index.js.map +1 -1
  28. package/lib/builder/allocator.d.ts +1 -1
  29. package/lib/builder/allocator.js +12 -12
  30. package/lib/builder/allocator.js.map +1 -1
  31. package/lib/builder/allocator.spec.js +22 -22
  32. package/lib/builder/allocator.spec.js.map +1 -1
  33. package/lib/builder/buildDatafile.d.ts +4 -3
  34. package/lib/builder/buildDatafile.js +311 -388
  35. package/lib/builder/buildDatafile.js.map +1 -1
  36. package/lib/builder/buildProject.d.ts +2 -1
  37. package/lib/builder/buildProject.js +96 -183
  38. package/lib/builder/buildProject.js.map +1 -1
  39. package/lib/builder/convertToV1.d.ts +10 -0
  40. package/lib/builder/convertToV1.js +119 -0
  41. package/lib/builder/convertToV1.js.map +1 -0
  42. package/lib/builder/getFeatureRanges.d.ts +1 -1
  43. package/lib/builder/getFeatureRanges.js +32 -105
  44. package/lib/builder/getFeatureRanges.js.map +1 -1
  45. package/lib/builder/hashes.d.ts +4 -0
  46. package/lib/builder/hashes.js +70 -0
  47. package/lib/builder/hashes.js.map +1 -0
  48. package/lib/builder/revision.js +2 -2
  49. package/lib/builder/revision.js.map +1 -1
  50. package/lib/builder/revision.spec.js +1 -1
  51. package/lib/builder/revision.spec.js.map +1 -1
  52. package/lib/builder/traffic.d.ts +1 -1
  53. package/lib/builder/traffic.js +57 -49
  54. package/lib/builder/traffic.js.map +1 -1
  55. package/lib/builder/traffic.spec.js +14 -14
  56. package/lib/builder/traffic.spec.js.map +1 -1
  57. package/lib/cli/cli.js +60 -129
  58. package/lib/cli/cli.js.map +1 -1
  59. package/lib/cli/plugins.js +14 -16
  60. package/lib/cli/plugins.js.map +1 -1
  61. package/lib/config/parsers.js +1 -1
  62. package/lib/config/parsers.js.map +1 -1
  63. package/lib/config/projectConfig.d.ts +8 -6
  64. package/lib/config/projectConfig.js +31 -72
  65. package/lib/config/projectConfig.js.map +1 -1
  66. package/lib/datasource/adapter.d.ts +1 -1
  67. package/lib/datasource/adapter.js +2 -5
  68. package/lib/datasource/adapter.js.map +1 -1
  69. package/lib/datasource/datasource.d.ts +2 -1
  70. package/lib/datasource/datasource.js +107 -148
  71. package/lib/datasource/datasource.js.map +1 -1
  72. package/lib/datasource/filesystemAdapter.d.ts +1 -1
  73. package/lib/datasource/filesystemAdapter.js +224 -360
  74. package/lib/datasource/filesystemAdapter.js.map +1 -1
  75. package/lib/evaluate/index.d.ts +1 -1
  76. package/lib/evaluate/index.js +120 -188
  77. package/lib/evaluate/index.js.map +1 -1
  78. package/lib/find-duplicate-segments/findDuplicateSegments.d.ts +1 -1
  79. package/lib/find-duplicate-segments/findDuplicateSegments.js +40 -128
  80. package/lib/find-duplicate-segments/findDuplicateSegments.js.map +1 -1
  81. package/lib/find-duplicate-segments/index.js +27 -82
  82. package/lib/find-duplicate-segments/index.js.map +1 -1
  83. package/lib/find-usage/index.d.ts +7 -5
  84. package/lib/find-usage/index.js +333 -507
  85. package/lib/find-usage/index.js.map +1 -1
  86. package/lib/generate-code/index.js +36 -91
  87. package/lib/generate-code/index.js.map +1 -1
  88. package/lib/generate-code/typescript.js +117 -157
  89. package/lib/generate-code/typescript.js.map +1 -1
  90. package/lib/index.d.ts +0 -1
  91. package/lib/index.js +0 -1
  92. package/lib/index.js.map +1 -1
  93. package/lib/info/index.js +45 -133
  94. package/lib/info/index.js.map +1 -1
  95. package/lib/init/index.d.ts +1 -1
  96. package/lib/init/index.js +16 -64
  97. package/lib/init/index.js.map +1 -1
  98. package/lib/linter/attributeSchema.d.ts +21 -6
  99. package/lib/linter/attributeSchema.js +18 -4
  100. package/lib/linter/attributeSchema.js.map +1 -1
  101. package/lib/linter/checkCircularDependency.d.ts +1 -1
  102. package/lib/linter/checkCircularDependency.js +22 -80
  103. package/lib/linter/checkCircularDependency.js.map +1 -1
  104. package/lib/linter/checkPercentageExceedingSlot.d.ts +1 -1
  105. package/lib/linter/checkPercentageExceedingSlot.js +36 -76
  106. package/lib/linter/checkPercentageExceedingSlot.js.map +1 -1
  107. package/lib/linter/conditionSchema.d.ts +1 -1
  108. package/lib/linter/conditionSchema.js +89 -41
  109. package/lib/linter/conditionSchema.js.map +1 -1
  110. package/lib/linter/featureSchema.d.ts +345 -197
  111. package/lib/linter/featureSchema.js +313 -172
  112. package/lib/linter/featureSchema.js.map +1 -1
  113. package/lib/linter/groupSchema.js +6 -6
  114. package/lib/linter/groupSchema.js.map +1 -1
  115. package/lib/linter/lintProject.js +306 -480
  116. package/lib/linter/lintProject.js.map +1 -1
  117. package/lib/linter/printError.js +7 -7
  118. package/lib/linter/printError.js.map +1 -1
  119. package/lib/linter/segmentSchema.js +2 -2
  120. package/lib/linter/segmentSchema.js.map +1 -1
  121. package/lib/linter/testSchema.d.ts +155 -3
  122. package/lib/linter/testSchema.js +47 -17
  123. package/lib/linter/testSchema.js.map +1 -1
  124. package/lib/list/index.d.ts +1 -0
  125. package/lib/list/index.js +349 -517
  126. package/lib/list/index.js.map +1 -1
  127. package/lib/{tester → list}/matrix.d.ts +1 -1
  128. package/lib/{tester → list}/matrix.js +50 -42
  129. package/lib/list/matrix.js.map +1 -0
  130. package/lib/{tester → list}/matrix.spec.js +7 -7
  131. package/lib/list/matrix.spec.js.map +1 -0
  132. package/lib/site/exportSite.js +25 -71
  133. package/lib/site/exportSite.js.map +1 -1
  134. package/lib/site/generateHistory.d.ts +1 -1
  135. package/lib/site/generateHistory.js +26 -82
  136. package/lib/site/generateHistory.js.map +1 -1
  137. package/lib/site/generateSiteSearchIndex.d.ts +1 -1
  138. package/lib/site/generateSiteSearchIndex.js +182 -259
  139. package/lib/site/generateSiteSearchIndex.js.map +1 -1
  140. package/lib/site/getLastModifiedFromHistory.d.ts +1 -1
  141. package/lib/site/getLastModifiedFromHistory.js +2 -2
  142. package/lib/site/getLastModifiedFromHistory.js.map +1 -1
  143. package/lib/site/getOwnerAndRepoFromUrl.js +6 -6
  144. package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -1
  145. package/lib/site/getRelativePaths.js +7 -7
  146. package/lib/site/getRelativePaths.js.map +1 -1
  147. package/lib/site/getRepoDetails.js +20 -20
  148. package/lib/site/getRepoDetails.js.map +1 -1
  149. package/lib/site/index.js +25 -73
  150. package/lib/site/index.js.map +1 -1
  151. package/lib/site/serveSite.js +10 -10
  152. package/lib/site/serveSite.js.map +1 -1
  153. package/lib/tester/helpers.d.ts +2 -0
  154. package/lib/tester/helpers.js +71 -0
  155. package/lib/tester/helpers.js.map +1 -0
  156. package/lib/tester/helpers.spec.js +115 -0
  157. package/lib/tester/helpers.spec.js.map +1 -0
  158. package/lib/tester/index.d.ts +0 -1
  159. package/lib/tester/index.js +0 -1
  160. package/lib/tester/index.js.map +1 -1
  161. package/lib/tester/prettyDuration.js +11 -11
  162. package/lib/tester/prettyDuration.js.map +1 -1
  163. package/lib/tester/printTestResult.d.ts +1 -1
  164. package/lib/tester/printTestResult.js +35 -15
  165. package/lib/tester/printTestResult.js.map +1 -1
  166. package/lib/tester/testFeature.d.ts +4 -2
  167. package/lib/tester/testFeature.js +264 -226
  168. package/lib/tester/testFeature.js.map +1 -1
  169. package/lib/tester/testProject.d.ts +3 -7
  170. package/lib/tester/testProject.js +145 -246
  171. package/lib/tester/testProject.js.map +1 -1
  172. package/lib/tester/testSegment.d.ts +5 -2
  173. package/lib/tester/testSegment.js +65 -102
  174. package/lib/tester/testSegment.js.map +1 -1
  175. package/lib/utils/extractKeys.d.ts +2 -1
  176. package/lib/utils/extractKeys.js +57 -12
  177. package/lib/utils/extractKeys.js.map +1 -1
  178. package/lib/utils/git.d.ts +1 -1
  179. package/lib/utils/git.js +23 -23
  180. package/lib/utils/git.js.map +1 -1
  181. package/lib/utils/pretty.js +2 -4
  182. package/lib/utils/pretty.js.map +1 -1
  183. package/package.json +5 -6
  184. package/src/assess-distribution/index.ts +3 -2
  185. package/src/benchmark/index.ts +3 -3
  186. package/src/builder/allocator.spec.ts +1 -1
  187. package/src/builder/allocator.ts +1 -1
  188. package/src/builder/buildDatafile.ts +161 -124
  189. package/src/builder/buildProject.ts +6 -3
  190. package/src/builder/convertToV1.ts +166 -0
  191. package/src/builder/getFeatureRanges.ts +1 -1
  192. package/src/builder/hashes.ts +109 -0
  193. package/src/builder/traffic.ts +40 -16
  194. package/src/cli/cli.ts +1 -1
  195. package/src/cli/plugins.ts +0 -2
  196. package/src/config/projectConfig.ts +13 -10
  197. package/src/datasource/adapter.ts +1 -1
  198. package/src/datasource/datasource.ts +23 -2
  199. package/src/datasource/filesystemAdapter.ts +11 -12
  200. package/src/evaluate/index.ts +7 -6
  201. package/src/find-duplicate-segments/findDuplicateSegments.ts +1 -1
  202. package/src/find-usage/index.ts +111 -44
  203. package/src/generate-code/index.ts +1 -3
  204. package/src/generate-code/typescript.ts +7 -29
  205. package/src/index.ts +0 -1
  206. package/src/info/index.ts +2 -2
  207. package/src/init/index.ts +2 -2
  208. package/src/linter/attributeSchema.ts +18 -2
  209. package/src/linter/checkCircularDependency.ts +1 -1
  210. package/src/linter/checkPercentageExceedingSlot.ts +28 -8
  211. package/src/linter/conditionSchema.ts +66 -10
  212. package/src/linter/featureSchema.ts +312 -116
  213. package/src/linter/lintProject.ts +9 -4
  214. package/src/linter/testSchema.ts +42 -3
  215. package/src/list/index.ts +18 -30
  216. package/src/{tester → list}/matrix.ts +33 -11
  217. package/src/site/exportSite.ts +2 -4
  218. package/src/site/generateHistory.ts +1 -1
  219. package/src/site/generateSiteSearchIndex.ts +58 -50
  220. package/src/site/getLastModifiedFromHistory.ts +1 -1
  221. package/src/tester/helpers.spec.ts +149 -0
  222. package/src/tester/helpers.ts +76 -0
  223. package/src/tester/index.ts +0 -1
  224. package/src/tester/printTestResult.ts +25 -3
  225. package/src/tester/testFeature.ts +270 -124
  226. package/src/tester/testProject.ts +28 -49
  227. package/src/tester/testSegment.ts +48 -40
  228. package/src/utils/extractKeys.ts +58 -1
  229. package/src/utils/git.ts +1 -1
  230. package/tsconfig.cjs.json +1 -0
  231. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +0 -151
  232. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +0 -157
  233. package/lib/restore/index.d.ts +0 -4
  234. package/lib/restore/index.js +0 -91
  235. package/lib/restore/index.js.map +0 -1
  236. package/lib/tester/checkIfArraysAreEqual.d.ts +0 -1
  237. package/lib/tester/checkIfArraysAreEqual.js +0 -18
  238. package/lib/tester/checkIfArraysAreEqual.js.map +0 -1
  239. package/lib/tester/checkIfObjectsAreEqual.d.ts +0 -1
  240. package/lib/tester/checkIfObjectsAreEqual.js +0 -23
  241. package/lib/tester/checkIfObjectsAreEqual.js.map +0 -1
  242. package/lib/tester/checkIfObjectsAreEqual.spec.js +0 -26
  243. package/lib/tester/checkIfObjectsAreEqual.spec.js.map +0 -1
  244. package/lib/tester/matrix.js.map +0 -1
  245. package/lib/tester/matrix.spec.js.map +0 -1
  246. package/src/restore/index.ts +0 -42
  247. package/src/tester/checkIfArraysAreEqual.ts +0 -16
  248. package/src/tester/checkIfObjectsAreEqual.spec.ts +0 -31
  249. package/src/tester/checkIfObjectsAreEqual.ts +0 -24
  250. /package/lib/{tester → list}/matrix.spec.d.ts +0 -0
  251. /package/lib/tester/{checkIfObjectsAreEqual.spec.d.ts → helpers.spec.d.ts} +0 -0
  252. /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
+ }
@@ -1,4 +1,11 @@
1
- import { Rule, ExistingFeature, Traffic, Variation, Range, Percentage } from "@featurevisor/types";
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
- // @TODO: may be pass from builder directly?
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; // belongs to a group, and group ranges changed
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
- const weight = variation.weight as number;
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.push({
141
- variation: variation.value,
142
- range,
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
- traffic.allocation = traffic.allocation.filter((a) => {
154
- if (a.range && a.range[0] === a.range[1]) {
155
- return false;
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
- return true;
159
- });
179
+ return true;
180
+ });
160
181
 
161
- // @TODO: in v2, remove "allocation" property if an empty array
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
- // @TODO: in future, allow yargs options to be defined via plugins
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
  }
@@ -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 OUTPUT_DIRECTORY_NAME = "dist";
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 = false;
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 = "1"; // default 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
- outputDirectoryPath: string;
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; // @TODO: type this properly later
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
- outputDirectoryPath: path.join(rootDirectoryPath, OUTPUT_DIRECTORY_NAME),
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
- print?: boolean;
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.print) {
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
- print: parsed.print,
163
+ json: parsed.json,
161
164
  pretty: parsed.pretty,
162
165
  });
163
166
  },
@@ -1,4 +1,4 @@
1
- import {
1
+ import type {
2
2
  DatafileContent,
3
3
  EnvironmentKey,
4
4
  ExistingState,
@@ -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
- // @TODO: only site generator needs it, find a way to get it out of here later
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 * as mkdirp from "mkdirp";
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
- mkdirp.sync(this.getEntityDirectoryPath(entityType));
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
- mkdirp.sync(this.config.stateDirectoryPath);
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
- mkdirp.sync(this.config.stateDirectoryPath);
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 fileName = `datafile-tag-${options.tag}.json`;
221
- const dir = options.datafilesDir || this.config.outputDirectoryPath;
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.outputDirectoryPath;
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
- mkdirp.sync(outputEnvironmentDirPath);
243
+ fs.mkdirSync(outputEnvironmentDirPath, { recursive: true });
245
244
 
246
245
  const outputFilePath = this.getDatafilePath(options);
247
246
 
@@ -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
- print?: boolean;
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
- levels: ["error", "warn", "info", "debug"],
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.print) {
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
- print: parsed.print,
217
+ // @NOTE: introduce optional --at?
218
+ json: parsed.json,
218
219
  pretty: parsed.pretty,
219
220
  verbose: parsed.verbose,
220
221
  },
@@ -1,6 +1,6 @@
1
1
  import * as crypto from "crypto";
2
2
 
3
- import { HistoryEntry, SegmentKey } from "@featurevisor/types";
3
+ import type { HistoryEntry, SegmentKey } from "@featurevisor/types";
4
4
 
5
5
  import { Dependencies } from "../dependencies";
6
6