@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
@@ -5,9 +5,7 @@ import {
5
5
  Feature,
6
6
  DatafileContent,
7
7
  DatafileContentV1,
8
- DatafileContentV2,
9
8
  Variation,
10
- Variable,
11
9
  VariableOverride,
12
10
  Traffic,
13
11
  SegmentKey,
@@ -18,7 +16,6 @@ import {
18
16
  ExistingState,
19
17
  FeatureKey,
20
18
  Allocation,
21
- VariableSchema,
22
19
  Expose,
23
20
  Rule,
24
21
  Force,
@@ -27,9 +24,11 @@ import {
27
24
  import { ProjectConfig, SCHEMA_VERSION } from "../config";
28
25
  import { Datasource } from "../datasource";
29
26
  import { extractAttributeKeysFromConditions, extractSegmentKeysFromGroupSegments } from "../utils";
27
+ import { generateHashForDatafile, generateHashForFeature, getSegmentHashes } from "./hashes";
30
28
 
31
29
  import { getTraffic } from "./traffic";
32
30
  import { getFeatureRanges } from "./getFeatureRanges";
31
+ import { convertToV1 } from "./convertToV1";
33
32
 
34
33
  export interface CustomDatafileOptions {
35
34
  featureKey?: string;
@@ -41,7 +40,9 @@ export interface CustomDatafileOptions {
41
40
  inflate?: number;
42
41
  }
43
42
 
44
- export async function getCustomDatafile(options: CustomDatafileOptions): Promise<DatafileContent> {
43
+ export async function getCustomDatafile(
44
+ options: CustomDatafileOptions,
45
+ ): Promise<DatafileContent | DatafileContentV1> {
45
46
  let featuresToInclude;
46
47
 
47
48
  if (options.featureKey) {
@@ -69,6 +70,7 @@ export async function getCustomDatafile(options: CustomDatafileOptions): Promise
69
70
  export interface BuildOptions {
70
71
  schemaVersion: string;
71
72
  revision: string;
73
+ revisionFromHash?: boolean;
72
74
  environment: string | false;
73
75
  tag?: string;
74
76
  features?: FeatureKey[];
@@ -80,15 +82,7 @@ export async function buildDatafile(
80
82
  datasource: Datasource,
81
83
  options: BuildOptions,
82
84
  existingState: ExistingState,
83
- ): Promise<DatafileContent> {
84
- const datafileContent: DatafileContentV1 = {
85
- schemaVersion: options.schemaVersion,
86
- revision: options.revision,
87
- attributes: [],
88
- segments: [],
89
- features: [],
90
- };
91
-
85
+ ): Promise<DatafileContent | DatafileContentV1> {
92
86
  const segmentKeysUsedByTag = new Set<SegmentKey>();
93
87
  const attributeKeysUsedByTag = new Set<AttributeKey>();
94
88
  const { featureRanges, featureIsInGroup } = await getFeatureRanges(projectConfig, datasource);
@@ -119,14 +113,14 @@ export async function buildDatafile(
119
113
  let rules: Rule[];
120
114
  let force: Force[] | undefined;
121
115
 
122
- if (options.environment && parsedFeature.environments) {
123
- expose = parsedFeature.environments[options.environment].expose;
124
- rules = parsedFeature.environments[options.environment].rules;
125
- force = parsedFeature.environments[options.environment].force;
116
+ if (options.environment) {
117
+ expose = parsedFeature.expose?.[options.environment] as Expose | undefined;
118
+ rules = parsedFeature.rules?.[options.environment] as Rule[];
119
+ force = parsedFeature.force?.[options.environment] as Force[] | undefined;
126
120
  } else {
127
- expose = parsedFeature.expose;
121
+ expose = parsedFeature.expose as Expose | undefined;
128
122
  rules = parsedFeature.rules as Rule[];
129
- force = parsedFeature.force;
123
+ force = parsedFeature.force as Force[] | undefined;
130
124
  }
131
125
 
132
126
  if (expose === false) {
@@ -151,66 +145,62 @@ export async function buildDatafile(
151
145
  deprecated: parsedFeature.deprecated === true ? true : undefined,
152
146
  bucketBy: parsedFeature.bucketBy || projectConfig.defaultBucketBy,
153
147
  required: parsedFeature.required,
148
+ disabledVariationValue: parsedFeature.disabledVariationValue,
154
149
  variations: Array.isArray(parsedFeature.variations)
155
150
  ? parsedFeature.variations.map((variation: Variation) => {
156
151
  const mappedVariation: any = {
157
152
  value: variation.value,
158
- weight: variation.weight, // @TODO: added so state files can maintain weight info, but datafiles don't need this. find a way to remove it from datafiles later
153
+ weight: variation.weight, // @NOTE: added so state files can maintain weight info, but datafiles don't need this. find a way to remove it from datafiles later
154
+ variables: variation.variables,
155
+ variableOverrides: variation.variableOverrides,
159
156
  };
160
157
 
161
- if (!variation.variables) {
162
- return mappedVariation;
163
- }
164
-
165
- mappedVariation.variables = variation.variables.map((variable: Variable) => {
166
- const mappedVariable: any = {
167
- key: variable.key,
168
- value: variable.value,
169
- };
170
-
171
- if (!variable.overrides) {
172
- return mappedVariable;
158
+ if (variation.variableOverrides) {
159
+ const variableOverrides = variation.variableOverrides;
160
+ const variableKeys = Object.keys(variableOverrides);
161
+
162
+ for (const variableKey of variableKeys) {
163
+ mappedVariation.variableOverrides[variableKey] = variableOverrides[
164
+ variableKey
165
+ ].map((override: VariableOverride) => {
166
+ if (typeof override.conditions !== "undefined") {
167
+ const extractedAttributeKeys = extractAttributeKeysFromConditions(
168
+ override.conditions,
169
+ );
170
+ extractedAttributeKeys.forEach((attributeKey) =>
171
+ attributeKeysUsedByTag.add(attributeKey),
172
+ );
173
+
174
+ return {
175
+ conditions:
176
+ projectConfig.stringify && typeof override.conditions !== "string"
177
+ ? JSON.stringify(override.conditions)
178
+ : override.conditions,
179
+ value: override.value,
180
+ };
181
+ }
182
+
183
+ if (typeof override.segments !== "undefined") {
184
+ const extractedSegmentKeys = extractSegmentKeysFromGroupSegments(
185
+ override.segments as GroupSegment | GroupSegment[],
186
+ );
187
+ extractedSegmentKeys.forEach((segmentKey) =>
188
+ segmentKeysUsedByTag.add(segmentKey),
189
+ );
190
+
191
+ return {
192
+ segments:
193
+ projectConfig.stringify && typeof override.segments !== "string"
194
+ ? JSON.stringify(override.segments)
195
+ : override.segments,
196
+ value: override.value,
197
+ };
198
+ }
199
+
200
+ return override;
201
+ });
173
202
  }
174
-
175
- mappedVariable.overrides = variable.overrides.map((override: VariableOverride) => {
176
- if (typeof override.conditions !== "undefined") {
177
- const extractedAttributeKeys = extractAttributeKeysFromConditions(
178
- override.conditions,
179
- );
180
- extractedAttributeKeys.forEach((attributeKey) =>
181
- attributeKeysUsedByTag.add(attributeKey),
182
- );
183
-
184
- return {
185
- conditions: projectConfig.stringify
186
- ? JSON.stringify(override.conditions)
187
- : override.conditions,
188
- value: override.value,
189
- };
190
- }
191
-
192
- if (typeof override.segments !== "undefined") {
193
- const extractedSegmentKeys = extractSegmentKeysFromGroupSegments(
194
- override.segments as GroupSegment | GroupSegment[],
195
- );
196
- extractedSegmentKeys.forEach((segmentKey) =>
197
- segmentKeysUsedByTag.add(segmentKey),
198
- );
199
-
200
- return {
201
- segments:
202
- typeof override.segments !== "string" && projectConfig.stringify
203
- ? JSON.stringify(override.segments)
204
- : override.segments,
205
- value: override.value,
206
- };
207
- }
208
-
209
- return override;
210
- });
211
-
212
- return mappedVariable;
213
- });
203
+ }
214
204
 
215
205
  return mappedVariation;
216
206
  })
@@ -246,39 +236,60 @@ export async function buildDatafile(
246
236
  return {
247
237
  key: t.key,
248
238
  percentage: t.percentage,
249
- allocation: t.allocation.map((a: Allocation) => {
250
- return {
251
- variation: a.variation,
252
- range: a.range,
253
- };
254
- }),
239
+ allocation:
240
+ t.allocation &&
241
+ t.allocation.map((a: Allocation) => {
242
+ return {
243
+ variation: a.variation,
244
+ range: a.range,
245
+ };
246
+ }),
255
247
  };
256
248
  }),
257
249
  };
258
250
 
259
251
  if (featureIsInGroup[featureKey] === true) {
260
- feature.ranges = featureRanges.get(feature.key);
252
+ feature.ranges = featureRanges.get(featureKey);
261
253
  }
262
254
 
263
255
  if (parsedFeature.variablesSchema) {
264
- feature.variablesSchema = parsedFeature.variablesSchema.map(function (v: VariableSchema) {
265
- return {
266
- key: v.key,
256
+ const variableKeys = Object.keys(parsedFeature.variablesSchema);
257
+ feature.variablesSchema = {};
258
+
259
+ for (const variableKey of variableKeys) {
260
+ const v = parsedFeature.variablesSchema[variableKey];
261
+
262
+ feature.variablesSchema[variableKey] = {
263
+ key: variableKey,
267
264
  type: v.type,
268
265
  defaultValue: v.defaultValue,
269
266
  deprecated: v.deprecated === true ? true : undefined,
267
+ useDefaultWhenDisabled: v.useDefaultWhenDisabled === true ? true : undefined,
268
+ disabledValue: typeof v.disabledValue !== "undefined" ? v.disabledValue : undefined,
270
269
  };
271
- });
270
+ }
272
271
  }
273
272
 
274
273
  if (force) {
275
- feature.force = force;
276
-
277
- feature.force?.forEach((f) => {
274
+ feature.force = force.map((f) => {
278
275
  if (f.segments) {
279
276
  const extractedSegmentKeys = extractSegmentKeysFromGroupSegments(f.segments);
280
277
  extractedSegmentKeys.forEach((segmentKey) => segmentKeysUsedByTag.add(segmentKey));
278
+
279
+ f.segments =
280
+ typeof f.segments !== "string" && projectConfig.stringify
281
+ ? JSON.stringify(f.segments)
282
+ : f.segments;
283
+ }
284
+
285
+ if (f.conditions) {
286
+ f.conditions =
287
+ typeof f.conditions !== "string" && projectConfig.stringify
288
+ ? JSON.stringify(f.conditions)
289
+ : f.conditions;
281
290
  }
291
+
292
+ return f;
282
293
  });
283
294
  }
284
295
 
@@ -335,7 +346,7 @@ export async function buildDatafile(
335
346
  continue;
336
347
  }
337
348
 
338
- if (attributeKeysUsedByTag.has(attributeKey) === false && !parsedAttribute.capture) {
349
+ if (attributeKeysUsedByTag.has(attributeKey) === false) {
339
350
  continue;
340
351
  }
341
352
 
@@ -344,21 +355,17 @@ export async function buildDatafile(
344
355
  type: parsedAttribute.type,
345
356
  };
346
357
 
347
- if (parsedAttribute.capture === true) {
348
- attribute.capture = true;
349
- }
350
-
351
358
  attributes.push(attribute);
352
359
  }
353
360
  }
354
361
 
355
362
  // inflate
356
- if (options.inflate) {
363
+ if (options.inflate && options.inflate >= 2) {
357
364
  const allFeatureKeys = features.map((f) => f.key);
358
365
  const allSegmentKeys = segments.map((s) => s.key);
359
366
  const allAttributeKeys = attributes.map((a) => a.key);
360
367
 
361
- for (let i = 0; i < options.inflate; i++) {
368
+ for (let i = 0; i < options.inflate - 1; i++) {
362
369
  // feature
363
370
  for (const featureKey of allFeatureKeys) {
364
371
  const originalFeature = features.find((f) => f.key === featureKey) as Feature;
@@ -391,47 +398,77 @@ export async function buildDatafile(
391
398
  }
392
399
  }
393
400
 
394
- // schema v2
395
- if (options.schemaVersion === "2") {
396
- // v2
397
- const datafileContentV2: DatafileContentV2 = {
398
- schemaVersion: "2",
401
+ // schema v1
402
+ if (options.schemaVersion === "1") {
403
+ return convertToV1({
399
404
  revision: options.revision,
400
- attributes: {},
401
- segments: {},
402
- features: {},
403
- };
405
+ projectConfig,
406
+ attributes,
407
+ features,
408
+ segments,
409
+ });
410
+ }
404
411
 
405
- datafileContentV2.attributes = attributes.reduce((acc, attribute) => {
406
- acc[attribute.key] = attribute;
407
- return acc;
408
- }, {});
412
+ // schema v2
413
+ const datafileContentV2: DatafileContent = {
414
+ schemaVersion: "2",
415
+ revision: options.revision,
416
+ segments: {},
417
+ features: {},
418
+ };
409
419
 
410
- datafileContentV2.segments = segments.reduce((acc, segment) => {
420
+ datafileContentV2.segments = segments.reduce((acc, segment) => {
421
+ // key check needed for supporting v1 datafile generation
422
+ if (segment.key) {
411
423
  acc[segment.key] = segment;
424
+ delete acc[segment.key].key; // remove key from segment, as it is not needed in v2 datafile
425
+ }
426
+
427
+ return acc;
428
+ }, {});
429
+
430
+ datafileContentV2.features = features.reduce((acc, feature) => {
431
+ if (!feature.key) {
412
432
  return acc;
413
- }, {});
433
+ }
414
434
 
415
- datafileContentV2.features = features.reduce((acc, feature) => {
416
- if (Array.isArray(feature.variablesSchema)) {
417
- feature.variablesSchema = feature.variablesSchema.reduce((vAcc, variableSchema) => {
418
- vAcc[variableSchema.key] = variableSchema;
435
+ const featureKey = feature.key as FeatureKey;
436
+ const featureV2 = feature;
419
437
 
420
- return vAcc;
421
- }, {});
438
+ // remove key, as it is not needed in v2 datafile
439
+ delete featureV2.key;
440
+
441
+ // remove variablesSchema[key].key
442
+ if (featureV2.variablesSchema) {
443
+ for (const [variableKey, variable] of Object.entries(featureV2.variablesSchema)) {
444
+ if (variable.key) {
445
+ delete featureV2.variablesSchema[variableKey].key;
446
+ }
422
447
  }
448
+ }
423
449
 
424
- acc[feature.key] = feature;
425
- return acc;
426
- }, {});
450
+ acc[featureKey] = featureV2;
427
451
 
428
- return datafileContentV2;
429
- }
452
+ return acc;
453
+ }, {});
430
454
 
431
- // default behaviour
432
- datafileContent.attributes = attributes;
433
- datafileContent.segments = segments;
434
- datafileContent.features = features;
455
+ // add feature hashes for change detection
456
+ const segmentHashes = getSegmentHashes(datafileContentV2.segments);
457
+ Object.keys(datafileContentV2.features).forEach((featureKey) => {
458
+ const hash = generateHashForFeature(featureKey, datafileContentV2.features, segmentHashes);
435
459
 
436
- return datafileContent;
460
+ datafileContentV2.features[featureKey].hash = hash;
461
+
462
+ // check needed to support --inflate option
463
+ if (existingState.features[featureKey]) {
464
+ existingState.features[featureKey].hash = hash;
465
+ }
466
+ });
467
+
468
+ if (options.revisionFromHash) {
469
+ const datafileHash = generateHashForDatafile(datafileContentV2);
470
+ datafileContentV2.revision = `${datafileHash}`;
471
+ }
472
+
473
+ return datafileContentV2;
437
474
  }
@@ -5,15 +5,17 @@ import { getNextRevision } from "./revision";
5
5
  import { buildDatafile, getCustomDatafile } from "./buildDatafile";
6
6
  import { Dependencies } from "../dependencies";
7
7
  import { Plugin } from "../cli";
8
+ import type { DatafileContent } from "@featurevisor/types";
8
9
 
9
10
  export interface BuildCLIOptions {
10
11
  revision?: string;
12
+ revisionFromHash?: boolean;
11
13
  schemaVersion?: string;
12
14
 
13
15
  // all three together
14
16
  environment?: string;
15
17
  feature?: string;
16
- print?: boolean;
18
+ json?: boolean;
17
19
  pretty?: boolean;
18
20
  stateFiles?: boolean; // --no-state-files in CLI
19
21
  inflate?: number;
@@ -48,6 +50,7 @@ async function buildForEnvironment({
48
50
  {
49
51
  schemaVersion: cliOptions.schemaVersion || SCHEMA_VERSION,
50
52
  revision: nextRevision,
53
+ revisionFromHash: cliOptions.revisionFromHash,
51
54
  environment: environment,
52
55
  tag: tag,
53
56
  inflate: cliOptions.inflate,
@@ -56,7 +59,7 @@ async function buildForEnvironment({
56
59
  );
57
60
 
58
61
  // write datafile for environment/tag
59
- await datasource.writeDatafile(datafileContent, {
62
+ await datasource.writeDatafile(datafileContent as DatafileContent, {
60
63
  environment,
61
64
  tag,
62
65
  datafilesDir: cliOptions.datafilesDir,
@@ -85,7 +88,7 @@ export async function buildProject(deps: Dependencies, cliOptions: BuildCLIOptio
85
88
  * This way we centralize the datafile generation in one place,
86
89
  * while tests can be run anywhere else.
87
90
  */
88
- if (cliOptions.environment && cliOptions.print) {
91
+ if (cliOptions.environment && cliOptions.json) {
89
92
  const datafileContent = await getCustomDatafile({
90
93
  featureKey: cliOptions.feature,
91
94
  environment: cliOptions.environment,
@@ -0,0 +1,166 @@
1
+ import type {
2
+ DatafileContentV1,
3
+ FeatureV1,
4
+ VariableSchema,
5
+ VariationV1,
6
+ Variation,
7
+ VariableV1,
8
+ Feature,
9
+ Segment,
10
+ Attribute,
11
+ } from "@featurevisor/types";
12
+
13
+ import { ProjectConfig } from "../config";
14
+
15
+ export interface ConvertToV1Options {
16
+ revision: string;
17
+ projectConfig: ProjectConfig;
18
+ attributes: Attribute[];
19
+ features: Feature[];
20
+ segments: Segment[];
21
+ }
22
+
23
+ export function convertToV1(options: ConvertToV1Options): DatafileContentV1 {
24
+ const datafileContent: DatafileContentV1 = {
25
+ schemaVersion: "1",
26
+ revision: options.revision,
27
+ attributes: options.attributes,
28
+ segments: options.segments,
29
+ features: [],
30
+ };
31
+
32
+ const { features, projectConfig } = options;
33
+
34
+ for (const feature of features) {
35
+ const featureV1: FeatureV1 = {
36
+ key: feature.key,
37
+ deprecated: feature.deprecated,
38
+ bucketBy: feature.bucketBy,
39
+ required: feature.required,
40
+ traffic: feature.traffic,
41
+ force: feature.force,
42
+ ranges: feature.ranges,
43
+ };
44
+
45
+ if (feature.variablesSchema && !Array.isArray(feature.variablesSchema)) {
46
+ const variablesSchemaObject = feature.variablesSchema;
47
+ const variablesSchemaArray: VariableSchema[] = [];
48
+ const variableKeys = Object.keys(variablesSchemaObject);
49
+
50
+ for (const variableKey of variableKeys) {
51
+ const v = variablesSchemaObject[variableKey];
52
+ variablesSchemaArray.push({
53
+ key: variableKey,
54
+ type: v.type,
55
+ defaultValue: v.defaultValue,
56
+ deprecated: v.deprecated === true ? true : undefined,
57
+ });
58
+ }
59
+
60
+ featureV1.variablesSchema = variablesSchemaArray;
61
+ }
62
+
63
+ if (feature.variations) {
64
+ const variationsV1: VariationV1[] = feature.variations.map((variation: Variation) => {
65
+ const variationV1: VariationV1 = {
66
+ value: variation.value,
67
+ weight: variation.weight || 0, // weight is not used in v1 datafile, but needed for state files
68
+ };
69
+
70
+ // variables
71
+ const variablesResult: { [key: string]: VariableV1 } = {};
72
+
73
+ if (variation.variables) {
74
+ const variableKeys = Object.keys(variation.variables);
75
+
76
+ for (const variableKey of variableKeys) {
77
+ const variableValue = variation.variables[variableKey];
78
+
79
+ variablesResult[variableKey] = {
80
+ key: variableKey,
81
+ value: variableValue,
82
+ };
83
+ }
84
+ }
85
+
86
+ if (variation.variableOverrides) {
87
+ const variableKeys = Object.keys(variation.variableOverrides);
88
+
89
+ for (const variableKey of variableKeys) {
90
+ const overrides = variation.variableOverrides[variableKey];
91
+
92
+ if (typeof variablesResult[variableKey] === "undefined") {
93
+ const variableSchema = feature.variablesSchema?.[variableKey];
94
+
95
+ variablesResult[variableKey] = {
96
+ key: variableKey,
97
+ value: variableSchema?.defaultValue, // default value if no variable is defined
98
+ };
99
+ }
100
+
101
+ variablesResult[variableKey].overrides = overrides.map((override) => {
102
+ if (typeof override.conditions !== "undefined") {
103
+ return {
104
+ conditions: projectConfig.stringify
105
+ ? JSON.stringify(override.conditions)
106
+ : override.conditions,
107
+ value: override.value,
108
+ };
109
+ }
110
+
111
+ if (typeof override.segments !== "undefined") {
112
+ return {
113
+ segments:
114
+ typeof override.segments !== "string" && projectConfig.stringify
115
+ ? JSON.stringify(override.segments)
116
+ : override.segments,
117
+ value: override.value,
118
+ };
119
+ }
120
+
121
+ return {
122
+ value: override.value,
123
+ };
124
+ });
125
+ }
126
+ }
127
+
128
+ variationV1.variables = Object.keys(variablesResult).map((variableKey) => {
129
+ const variable = variablesResult[variableKey];
130
+
131
+ return {
132
+ key: variable.key,
133
+ value: variable.value,
134
+ overrides: variable.overrides,
135
+ };
136
+ });
137
+
138
+ return variationV1;
139
+ });
140
+
141
+ featureV1.variations = variationsV1;
142
+
143
+ if (featureV1.force) {
144
+ featureV1.force = featureV1.force.map((force) => {
145
+ if (
146
+ force.conditions &&
147
+ typeof force.conditions === "string" &&
148
+ force.conditions !== "*"
149
+ ) {
150
+ force.conditions = JSON.parse(force.conditions);
151
+ }
152
+
153
+ if (force.segments && typeof force.segments === "string" && force.segments !== "*") {
154
+ force.segments = JSON.parse(force.segments);
155
+ }
156
+
157
+ return force;
158
+ });
159
+ }
160
+
161
+ datafileContent.features.push(featureV1);
162
+ }
163
+ }
164
+
165
+ return datafileContent;
166
+ }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
 
3
- import { FeatureKey, Group, Range } from "@featurevisor/types";
3
+ import type { FeatureKey, Group, Range } from "@featurevisor/types";
4
4
 
5
5
  import { ProjectConfig } from "../config";
6
6
  import { Datasource } from "../datasource";