@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.
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 +90 -63
  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 +101 -23
  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 -48
  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 +41 -15
  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
@@ -1,4 +1,4 @@
1
- import { Condition, FeatureKey, SegmentKey, AttributeKey } from "@featurevisor/types";
1
+ import type { Condition, FeatureKey, SegmentKey, AttributeKey } from "@featurevisor/types";
2
2
 
3
3
  import { Dependencies } from "../dependencies";
4
4
  import { Plugin } from "../cli";
@@ -54,10 +54,12 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
54
54
  // variable overrides inside variations
55
55
  if (feature.variations) {
56
56
  feature.variations.forEach((variation) => {
57
- if (variation.variables) {
58
- variation.variables.forEach((variable) => {
59
- if (variable.overrides) {
60
- variable.overrides.forEach((override) => {
57
+ if (variation.variableOverrides) {
58
+ Object.keys(variation.variableOverrides).forEach((variableKey) => {
59
+ const overrides = variation.variableOverrides?.[variableKey];
60
+
61
+ if (overrides) {
62
+ overrides.forEach((override) => {
61
63
  if (override.segments) {
62
64
  extractSegmentKeysFromGroupSegments(override.segments).forEach((segmentKey) =>
63
65
  usageInFeatures[featureKey].segments.add(segmentKey),
@@ -79,13 +81,9 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
79
81
  // with environments
80
82
  if (Array.isArray(projectConfig.environments)) {
81
83
  projectConfig.environments.forEach((environment) => {
82
- if (!feature.environments) {
83
- return;
84
- }
85
-
86
84
  // force
87
- if (feature.environments[environment].force) {
88
- feature.environments[environment].force?.forEach((force) => {
85
+ if (feature.force && feature.force[environment]) {
86
+ feature.force[environment].forEach((force) => {
89
87
  if (force.segments) {
90
88
  extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
91
89
  usageInFeatures[featureKey].segments.add(segmentKey),
@@ -101,8 +99,8 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
101
99
  }
102
100
 
103
101
  // rules
104
- if (feature.environments[environment].rules) {
105
- feature.environments[environment].rules?.forEach((rule) => {
102
+ if (feature.rules && feature.rules[environment]) {
103
+ feature.rules[environment].forEach((rule) => {
106
104
  extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
107
105
  usageInFeatures[featureKey].segments.add(segmentKey),
108
106
  );
@@ -114,7 +112,7 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
114
112
  // no environments
115
113
  if (projectConfig.environments === false) {
116
114
  // force
117
- if (feature.force) {
115
+ if (Array.isArray(feature.force)) {
118
116
  feature.force.forEach((force) => {
119
117
  if (force.segments) {
120
118
  extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
@@ -131,7 +129,7 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
131
129
  }
132
130
 
133
131
  // rules
134
- if (feature.rules) {
132
+ if (Array.isArray(feature.rules)) {
135
133
  feature.rules.forEach((rule) => {
136
134
  extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
137
135
  usageInFeatures[featureKey].segments.add(segmentKey),
@@ -172,14 +170,27 @@ export async function findAllUsageInSegments(deps: Dependencies): Promise<UsageI
172
170
  return usageInSegments;
173
171
  }
174
172
 
173
+ export async function findFeatureUsage(
174
+ usageInFeatures: UsageInFeatures,
175
+ searchFeatureKey: FeatureKey,
176
+ ): Promise<Set<FeatureKey>> {
177
+ const usedInFeatures = new Set<FeatureKey>();
178
+
179
+ for (const featureKey in usageInFeatures) {
180
+ if (usageInFeatures[featureKey].features.has(searchFeatureKey)) {
181
+ usedInFeatures.add(featureKey);
182
+ }
183
+ }
184
+
185
+ return usedInFeatures;
186
+ }
187
+
175
188
  export async function findSegmentUsage(
176
- deps: Dependencies,
189
+ usageInFeatures: UsageInFeatures,
177
190
  segmentKey: SegmentKey,
178
191
  ): Promise<Set<FeatureKey>> {
179
192
  const usedInFeatures = new Set<FeatureKey>();
180
193
 
181
- const usageInFeatures = await findAllUsageInFeatures(deps);
182
-
183
194
  for (const featureKey in usageInFeatures) {
184
195
  if (usageInFeatures[featureKey].segments.has(segmentKey)) {
185
196
  usedInFeatures.add(featureKey);
@@ -195,7 +206,8 @@ export interface AttributeUsage {
195
206
  }
196
207
 
197
208
  export async function findAttributeUsage(
198
- deps: Dependencies,
209
+ usageInFeatures: UsageInFeatures,
210
+ usageInSegments: UsageInSegments,
199
211
  attributeKey: AttributeKey,
200
212
  ): Promise<AttributeUsage> {
201
213
  const usedIn: AttributeUsage = {
@@ -203,9 +215,6 @@ export async function findAttributeUsage(
203
215
  segments: new Set<SegmentKey>(),
204
216
  };
205
217
 
206
- const usageInFeatures = await findAllUsageInFeatures(deps);
207
- const usageInSegments = await findAllUsageInSegments(deps);
208
-
209
218
  for (const featureKey in usageInFeatures) {
210
219
  if (usageInFeatures[featureKey].attributes.has(attributeKey)) {
211
220
  usedIn.features.add(featureKey);
@@ -221,12 +230,14 @@ export async function findAttributeUsage(
221
230
  return usedIn;
222
231
  }
223
232
 
224
- export async function findUnusedSegments(deps: Dependencies): Promise<Set<SegmentKey>> {
233
+ export async function findUnusedSegments(
234
+ deps: Dependencies,
235
+ usageInFeatures: UsageInFeatures,
236
+ ): Promise<Set<SegmentKey>> {
225
237
  const { datasource } = deps;
226
238
  const unusedSegments = new Set<SegmentKey>();
227
239
 
228
240
  const allSegmentKeys = await datasource.listSegments();
229
- const usageInFeatures = await findAllUsageInFeatures(deps);
230
241
  const usedSegmentKeys = new Set<SegmentKey>();
231
242
 
232
243
  for (const featureKey in usageInFeatures) {
@@ -244,13 +255,15 @@ export async function findUnusedSegments(deps: Dependencies): Promise<Set<Segmen
244
255
  return unusedSegments;
245
256
  }
246
257
 
247
- export async function findUnusedAttributes(deps: Dependencies): Promise<Set<AttributeKey>> {
258
+ export async function findUnusedAttributes(
259
+ deps: Dependencies,
260
+ usageInFeatures: UsageInFeatures,
261
+ usageInSegments: UsageInSegments,
262
+ ): Promise<Set<AttributeKey>> {
248
263
  const { datasource } = deps;
249
264
  const unusedAttributes = new Set<AttributeKey>();
250
265
 
251
266
  const allAttributeKeys = await datasource.listAttributes();
252
- const usageInFeatures = await findAllUsageInFeatures(deps);
253
- const usageInSegments = await findAllUsageInSegments(deps);
254
267
  const usedAttributeKeys = new Set<AttributeKey>();
255
268
 
256
269
  for (const featureKey in usageInFeatures) {
@@ -275,6 +288,7 @@ export async function findUnusedAttributes(deps: Dependencies): Promise<Set<Attr
275
288
  }
276
289
 
277
290
  export interface FindUsageOptions {
291
+ feature?: string;
278
292
  segment?: string;
279
293
  attribute?: string;
280
294
 
@@ -289,9 +303,36 @@ export async function findUsageInProject(deps: Dependencies, options: FindUsageO
289
303
 
290
304
  console.log("");
291
305
 
306
+ const usageInFeatures = await findAllUsageInFeatures(deps);
307
+ const usageInSegments = await findAllUsageInSegments(deps);
308
+
309
+ // feature
310
+ if (options.feature) {
311
+ const usedInFeatures = await findFeatureUsage(usageInFeatures, options.feature);
312
+
313
+ if (usedInFeatures.size === 0) {
314
+ console.log(`Feature "${options.feature}" is not used in any features.`);
315
+ } else {
316
+ console.log(`Feature "${options.feature}" is used in the following features:\n`);
317
+
318
+ for (const featureKey of Array.from(usedInFeatures)) {
319
+ if (options.authors) {
320
+ const entries = await datasource.listHistoryEntries("feature", featureKey);
321
+ const authors = Array.from(new Set(entries.map((entry) => entry.author)));
322
+
323
+ console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
324
+ } else {
325
+ console.log(` - ${featureKey}`);
326
+ }
327
+ }
328
+ }
329
+
330
+ return;
331
+ }
332
+
292
333
  // segment
293
334
  if (options.segment) {
294
- const usedInFeatures = await findSegmentUsage(deps, options.segment);
335
+ const usedInFeatures = await findSegmentUsage(usageInFeatures, options.segment);
295
336
 
296
337
  if (usedInFeatures.size === 0) {
297
338
  console.log(`Segment "${options.segment}" is not used in any features.`);
@@ -315,7 +356,7 @@ export async function findUsageInProject(deps: Dependencies, options: FindUsageO
315
356
 
316
357
  // attribute
317
358
  if (options.attribute) {
318
- const usedIn = await findAttributeUsage(deps, options.attribute);
359
+ const usedIn = await findAttributeUsage(usageInFeatures, usageInSegments, options.attribute);
319
360
 
320
361
  if (usedIn.features.size === 0 && usedIn.segments.size === 0) {
321
362
  console.log(`Attribute "${options.attribute}" is not used in any features or segments.`);
@@ -323,36 +364,61 @@ export async function findUsageInProject(deps: Dependencies, options: FindUsageO
323
364
  return;
324
365
  }
325
366
 
326
- if (usedIn.features.size > 0) {
327
- console.log(`Attribute "${options.attribute}" is used in the following features:\n`);
367
+ if (usedIn.segments.size > 0) {
368
+ console.log(`Attribute "${options.attribute}" is used in the following segments:\n`);
328
369
 
329
- for (const featureKey of Array.from(usedIn.features)) {
370
+ for (const segmentKey of Array.from(usedIn.segments)) {
330
371
  if (options.authors) {
331
- const entries = await datasource.listHistoryEntries("feature", featureKey);
372
+ const entries = await datasource.listHistoryEntries("segment", segmentKey);
332
373
  const authors = Array.from(new Set(entries.map((entry) => entry.author)));
333
374
 
334
- console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
375
+ console.log(` - ${segmentKey} (Authors: ${authors.join(", ")})`);
335
376
  } else {
336
- console.log(` - ${featureKey}`);
377
+ console.log(` - ${segmentKey}`);
337
378
  }
338
379
  }
339
380
 
340
- console.log("");
381
+ // features affected by above segments
382
+ const affectedFeatures = new Set<FeatureKey>();
383
+
384
+ for (const segmentKey of Array.from(usedIn.segments)) {
385
+ const featureKeys = await findSegmentUsage(usageInFeatures, segmentKey);
386
+ featureKeys.forEach((featureKey) => affectedFeatures.add(featureKey));
387
+ }
388
+
389
+ if (affectedFeatures.size > 0) {
390
+ console.log(`\nSegments above are used in the following features:\n`);
391
+
392
+ for (const featureKey of Array.from(affectedFeatures)) {
393
+ if (options.authors) {
394
+ const entries = await datasource.listHistoryEntries("feature", featureKey);
395
+ const authors = Array.from(new Set(entries.map((entry) => entry.author)));
396
+
397
+ console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
398
+ } else {
399
+ console.log(` - ${featureKey}`);
400
+ }
401
+ }
402
+
403
+ console.log("");
404
+ }
341
405
  }
342
406
 
343
- if (usedIn.segments.size > 0) {
344
- console.log(`Attribute "${options.attribute}" is used in the following segments:\n`);
407
+ if (usedIn.features.size > 0) {
408
+ console.log(`Attribute "${options.attribute}" is used directly in the following features:\n`);
345
409
 
346
- for (const segmentKey of Array.from(usedIn.segments)) {
410
+ for (const featureKey of Array.from(usedIn.features)) {
347
411
  if (options.authors) {
348
- const entries = await datasource.listHistoryEntries("segment", segmentKey);
412
+ const entries = await datasource.listHistoryEntries("feature", featureKey);
349
413
  const authors = Array.from(new Set(entries.map((entry) => entry.author)));
350
414
 
351
- console.log(` - ${segmentKey} (Authors: ${authors.join(", ")})`);
415
+ console.log(` - ${featureKey} (Authors: ${authors.join(", ")})`);
352
416
  } else {
353
- console.log(` - ${segmentKey}`);
417
+ console.log(` - ${featureKey}`);
354
418
  }
355
419
  }
420
+
421
+ console.log("");
356
422
  }
357
423
 
358
424
  return;
@@ -360,7 +426,7 @@ export async function findUsageInProject(deps: Dependencies, options: FindUsageO
360
426
 
361
427
  // unused segments
362
428
  if (options.unusedSegments) {
363
- const unusedSegments = await findUnusedSegments(deps);
429
+ const unusedSegments = await findUnusedSegments(deps, usageInFeatures);
364
430
 
365
431
  if (unusedSegments.size === 0) {
366
432
  console.log("No unused segments found.");
@@ -384,7 +450,7 @@ export async function findUsageInProject(deps: Dependencies, options: FindUsageO
384
450
 
385
451
  // unused attributes
386
452
  if (options.unusedAttributes) {
387
- const unusedAttributes = await findUnusedAttributes(deps);
453
+ const unusedAttributes = await findUnusedAttributes(deps, usageInFeatures, usageInSegments);
388
454
 
389
455
  if (unusedAttributes.size === 0) {
390
456
  console.log("No unused attributes found.");
@@ -420,6 +486,7 @@ export const findUsagePlugin: Plugin = {
420
486
  options: parsed,
421
487
  },
422
488
  {
489
+ feature: parsed.feature,
423
490
  segment: parsed.segment,
424
491
  attribute: parsed.attribute,
425
492
  unusedSegments: parsed.unusedSegments,
@@ -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 { generateTypeScriptCodeForProject } from "./typescript";
7
5
  import { Dependencies } from "../dependencies";
8
6
  import { Plugin } from "../cli";
@@ -32,7 +30,7 @@ export async function generateCodeForProject(
32
30
 
33
31
  if (!fs.existsSync(absolutePath)) {
34
32
  console.log(`Creating output directory: ${absolutePath}`);
35
- mkdirp.sync(absolutePath);
33
+ fs.mkdirSync(absolutePath, { recursive: true });
36
34
  } else {
37
35
  console.log(`Output directory already exists at: ${absolutePath}`);
38
36
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
 
4
- import { Attribute } from "@featurevisor/types";
4
+ import type { Attribute } from "@featurevisor/types";
5
5
  import { Dependencies } from "../dependencies";
6
6
 
7
7
  function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string) {
@@ -19,7 +19,7 @@ function convertFeaturevisorTypeToTypeScriptType(featurevisorType: string) {
19
19
  case "array":
20
20
  return "string[]";
21
21
  case "object":
22
- return "any"; // @TODO: do a flat dictionary
22
+ return "any"; // @NOTE: do a flat dictionary
23
23
  case "json":
24
24
  return "any";
25
25
  default:
@@ -47,30 +47,6 @@ function getRelativePath(from, to) {
47
47
  return relativePath;
48
48
  }
49
49
 
50
- function getFeaturevisorTypeFromValue(value) {
51
- if (typeof value === "boolean") {
52
- return "boolean";
53
- }
54
-
55
- if (typeof value === "string") {
56
- return "string";
57
- }
58
-
59
- if (typeof value === "number") {
60
- if (Number.isInteger(value)) {
61
- return "integer";
62
- }
63
-
64
- return "double";
65
- }
66
-
67
- if (value instanceof Date) {
68
- return "date";
69
- }
70
-
71
- throw new Error("Could not detect Featurevisor type from value");
72
- }
73
-
74
50
  const instanceSnippet = `
75
51
  import { FeaturevisorInstance } from "@featurevisor/sdk";
76
52
 
@@ -121,7 +97,7 @@ export async function generateTypeScriptCodeForProject(deps: Dependencies, outpu
121
97
  })
122
98
  .join("\n");
123
99
  const contextContent = `
124
- import { AttributeKey, AttributeValue } from "@featurevisor/types";
100
+ import type { AttributeKey, AttributeValue } from "@featurevisor/types";
125
101
 
126
102
  export interface Context {
127
103
  ${attributeProperties}
@@ -152,8 +128,10 @@ ${attributeProperties}
152
128
  let variableMethods = "";
153
129
 
154
130
  if (parsedFeature.variablesSchema) {
155
- for (const variableSchema of parsedFeature.variablesSchema) {
156
- const variableKey = variableSchema.key;
131
+ const variableKeys = Object.keys(parsedFeature.variablesSchema);
132
+
133
+ for (const variableKey of variableKeys) {
134
+ const variableSchema = parsedFeature.variablesSchema[variableKey];
157
135
  const variableType = variableSchema.type;
158
136
 
159
137
  const internalMethodName = `getVariable${
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@ export * from "./tester";
5
5
  export * from "./init";
6
6
  export * from "./site";
7
7
  export * from "./generate-code";
8
- export * from "./restore";
9
8
  export * from "./find-duplicate-segments";
10
9
  export * from "./find-usage";
11
10
  export * from "./dependencies";
package/src/info/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Dependencies } from "../dependencies";
2
- import { getMatrixCombinations } from "../tester/matrix";
2
+ import { getMatrixCombinations } from "../list/matrix";
3
3
  import { Plugin } from "../cli";
4
4
 
5
5
  export async function showProjectInfo(deps: Dependencies) {
@@ -22,7 +22,7 @@ export async function showProjectInfo(deps: Dependencies) {
22
22
  const feature = await datasource.readFeature(featureKey);
23
23
 
24
24
  if (feature.variablesSchema) {
25
- variablesCount += feature.variablesSchema.length;
25
+ variablesCount += Object.keys(feature.variablesSchema).length;
26
26
  }
27
27
  }
28
28
 
package/src/init/index.ts CHANGED
@@ -3,7 +3,7 @@ import * as tar from "tar";
3
3
 
4
4
  import { Plugin } from "../cli";
5
5
 
6
- export const DEFAULT_EXAMPLE = "example-yml";
6
+ export const DEFAULT_EXAMPLE = "yml";
7
7
 
8
8
  export const EXAMPLES_ORG_NAME = "fahad19";
9
9
  export const EXAMPLES_REPO_NAME = "featurevisor";
@@ -12,7 +12,7 @@ export const EXAMPLES_BRANCH_NAME = "main";
12
12
  export const EXAMPLES_TAR_URL = `https://codeload.github.com/${EXAMPLES_ORG_NAME}/${EXAMPLES_REPO_NAME}/tar.gz/${EXAMPLES_BRANCH_NAME}`;
13
13
 
14
14
  function getExamplePath(exampleName: string) {
15
- return `${EXAMPLES_REPO_NAME}-${EXAMPLES_BRANCH_NAME}/examples/${exampleName}/`;
15
+ return `${EXAMPLES_REPO_NAME}-${EXAMPLES_BRANCH_NAME}/examples/example-${exampleName}/`;
16
16
  }
17
17
 
18
18
  export function initProject(
@@ -1,12 +1,28 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  export function getAttributeZodSchema() {
4
+ const propertySchema = z.object({
5
+ type: z.enum([
6
+ "boolean",
7
+ "string",
8
+ "integer",
9
+ "double",
10
+ "date",
11
+ "semver",
12
+ "array",
13
+
14
+ // @NOTE: intentionally ignored for now to not allow nesting
15
+ // "object",
16
+ ]),
17
+ description: z.string().optional(),
18
+ });
19
+
4
20
  const attributeZodSchema = z
5
21
  .object({
6
22
  archived: z.boolean().optional(),
7
- type: z.enum(["boolean", "string", "integer", "double", "date", "semver"]),
23
+ type: z.enum(["boolean", "string", "integer", "double", "date", "semver", "object", "array"]),
8
24
  description: z.string(),
9
- capture: z.boolean().optional(),
25
+ properties: z.record(z.string(), propertySchema).optional(),
10
26
  })
11
27
  .strict();
12
28
 
@@ -1,4 +1,4 @@
1
- import { FeatureKey, Required } from "@featurevisor/types";
1
+ import type { FeatureKey, Required } from "@featurevisor/types";
2
2
 
3
3
  import { Datasource } from "../datasource";
4
4
 
@@ -1,8 +1,8 @@
1
- import { Group } from "@featurevisor/types";
1
+ import type { Group, Rule } from "@featurevisor/types";
2
2
 
3
3
  import { Datasource } from "../datasource";
4
4
 
5
- // @TODO: ideally in future, this check should be done from Feature level,
5
+ // @NOTE: ideally in future, this check should be done from Feature level,
6
6
  // as well as Group level as done here
7
7
  export async function checkForFeatureExceedingGroupSlotPercentage(
8
8
  datasource: Datasource,
@@ -22,16 +22,36 @@ export async function checkForFeatureExceedingGroupSlotPercentage(
22
22
 
23
23
  const parsedFeature = await datasource.readFeature(featureKey);
24
24
 
25
- const environmentKeys = Object.keys(parsedFeature.environments);
26
- for (const environmentKey of environmentKeys) {
27
- const environment = parsedFeature.environments[environmentKey];
28
- const rules = environment.rules;
25
+ const hasEnvironments =
26
+ parsedFeature.rules &&
27
+ !Array.isArray(parsedFeature.rules) &&
28
+ Object.keys(parsedFeature.rules).length > 0;
29
+
30
+ if (hasEnvironments && parsedFeature.rules) {
31
+ // with environments
32
+ const environmentKeys = Object.keys(parsedFeature.rules);
33
+
34
+ for (const environmentKey of environmentKeys) {
35
+ const rules = parsedFeature.rules[environmentKey];
36
+
37
+ for (const rule of rules) {
38
+ if (rule.percentage > maxPercentageForRule) {
39
+ // @NOTE: this does not help with same feature belonging to multiple slots. fix that.
40
+ throw new Error(
41
+ `Feature ${featureKey}'s rule ${rule.key} in ${environmentKey} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
42
+ );
43
+ }
44
+ }
45
+ }
46
+ } else if (parsedFeature.rules) {
47
+ // no environments
48
+ const rules = parsedFeature.rules as Rule[];
29
49
 
30
50
  for (const rule of rules) {
31
51
  if (rule.percentage > maxPercentageForRule) {
32
- // @TODO: this does not help with same feature belonging to multiple slots. fix that.
52
+ // @NOTE: this does not help with same feature belonging to multiple slots. fix that.
33
53
  throw new Error(
34
- `Feature ${featureKey}'s rule ${rule.key} in ${environmentKey} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
54
+ `Feature ${featureKey}'s rule ${rule.key} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
35
55
  );
36
56
  }
37
57
  }
@@ -4,7 +4,14 @@ import { ProjectConfig } from "../config";
4
4
 
5
5
  const commonOperators: [string, ...string[]] = ["equals", "notEquals"];
6
6
  const numericOperators = ["greaterThan", "greaterThanOrEquals", "lessThan", "lessThanOrEquals"];
7
- const stringOperators = ["contains", "notContains", "startsWith", "endsWith"];
7
+ const stringOperators = [
8
+ "contains",
9
+ "notContains",
10
+ "startsWith",
11
+ "endsWith",
12
+ "includes",
13
+ "notIncludes",
14
+ ];
8
15
  const semverOperators = [
9
16
  "semverEquals",
10
17
  "semverNotEquals",
@@ -15,6 +22,8 @@ const semverOperators = [
15
22
  ];
16
23
  const dateOperators = ["before", "after"];
17
24
  const arrayOperators = ["in", "notIn"];
25
+ const regexOperators = ["matches", "notMatches"];
26
+ const operatorsWithoutValue = ["exists", "notExists"];
18
27
 
19
28
  export function getConditionsZodSchema(
20
29
  projectConfig: ProjectConfig,
@@ -35,15 +44,27 @@ export function getConditionsZodSchema(
35
44
  ...semverOperators,
36
45
  ...dateOperators,
37
46
  ...arrayOperators,
47
+ ...regexOperators,
48
+ ...operatorsWithoutValue,
38
49
  ]),
39
- value: z.union([
40
- z.string(),
41
- z.array(z.string()),
42
- z.number(),
43
- z.boolean(),
44
- z.date(),
45
- z.null(),
46
- ]),
50
+ value: z
51
+ .union([z.string(), z.array(z.string()), z.number(), z.boolean(), z.date(), z.null()])
52
+ .optional(),
53
+ regexFlags: z
54
+ .string()
55
+ .refine(
56
+ (value) => {
57
+ if (typeof value === "undefined") {
58
+ return true;
59
+ }
60
+
61
+ return /^[gimsuy]{1,}$/.test(value);
62
+ },
63
+ {
64
+ message: `regexFlags must of one or more of these characters: g, i, m, s, u, y`,
65
+ },
66
+ )
67
+ .optional(),
47
68
  })
48
69
  .superRefine((data, context) => {
49
70
  // common
@@ -109,6 +130,35 @@ export function getConditionsZodSchema(
109
130
  path: ["value"],
110
131
  });
111
132
  }
133
+
134
+ // regex
135
+ if (regexOperators.includes(data.operator)) {
136
+ if (typeof data.value !== "string") {
137
+ context.addIssue({
138
+ code: z.ZodIssueCode.custom,
139
+ message: `when operator is "${data.operator}", value must be a string`,
140
+ path: ["value"],
141
+ });
142
+ }
143
+ } else {
144
+ // regex flags are not needed
145
+ if (data.regexFlags) {
146
+ context.addIssue({
147
+ code: z.ZodIssueCode.custom,
148
+ message: `when operator is nether "matches" nor "notMatches", regexFlags are not needed`,
149
+ path: ["regexFlags"],
150
+ });
151
+ }
152
+ }
153
+
154
+ // operators without value
155
+ if (operatorsWithoutValue.includes(data.operator) && data.value !== undefined) {
156
+ context.addIssue({
157
+ code: z.ZodIssueCode.custom,
158
+ message: `when operator is "${data.operator}", value is not needed`,
159
+ path: ["value"],
160
+ });
161
+ }
112
162
  });
113
163
 
114
164
  const andOrNotConditionZodSchema = z.union([
@@ -129,9 +179,15 @@ export function getConditionsZodSchema(
129
179
  .strict(),
130
180
  ]);
131
181
 
182
+ const everyoneZodSchema = z.literal("*");
183
+
132
184
  const conditionZodSchema = z.union([andOrNotConditionZodSchema, plainConditionZodSchema]);
133
185
 
134
- const conditionsZodSchema = z.union([conditionZodSchema, z.array(conditionZodSchema)]);
186
+ const conditionsZodSchema = z.union([
187
+ conditionZodSchema,
188
+ z.array(conditionZodSchema),
189
+ everyoneZodSchema,
190
+ ]);
135
191
 
136
192
  return conditionsZodSchema;
137
193
  }