@featurevisor/core 2.13.0 → 2.15.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 (121) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +1499 -1028
  3. package/coverage/coverage-final.json +21 -17
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +38 -8
  9. package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
  10. package/coverage/lcov-report/builder/mutator.ts.html +796 -0
  11. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  12. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  13. package/coverage/lcov-report/config/index.html +30 -15
  14. package/coverage/lcov-report/config/index.ts.html +88 -0
  15. package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
  16. package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
  17. package/coverage/lcov-report/datasource/datasource.ts.html +77 -26
  18. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +693 -69
  19. package/coverage/lcov-report/datasource/index.html +35 -35
  20. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  21. package/coverage/lcov-report/index.html +53 -53
  22. package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
  23. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  24. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  25. package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
  26. package/coverage/lcov-report/linter/featureSchema.ts.html +848 -389
  27. package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
  28. package/coverage/lcov-report/linter/index.html +50 -35
  29. package/coverage/lcov-report/linter/lintProject.ts.html +437 -92
  30. package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
  31. package/coverage/lcov-report/linter/printError.ts.html +9 -9
  32. package/coverage/lcov-report/linter/schema.ts.html +69 -69
  33. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  34. package/coverage/lcov-report/linter/testSchema.ts.html +10 -10
  35. package/coverage/lcov-report/list/index.html +1 -1
  36. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  37. package/coverage/lcov-report/parsers/index.html +1 -1
  38. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  39. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  40. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  41. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  42. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  43. package/coverage/lcov-report/tester/index.html +1 -1
  44. package/coverage/lcov-report/utils/git.ts.html +118 -16
  45. package/coverage/lcov-report/utils/index.html +14 -14
  46. package/coverage/lcov.info +2918 -1932
  47. package/lib/builder/buildDatafile.js +8 -3
  48. package/lib/builder/buildDatafile.js.map +1 -1
  49. package/lib/builder/mutateVariables.d.ts +14 -0
  50. package/lib/builder/mutateVariables.js +90 -0
  51. package/lib/builder/mutateVariables.js.map +1 -0
  52. package/lib/builder/mutateVariables.spec.d.ts +1 -0
  53. package/lib/builder/mutateVariables.spec.js +1045 -0
  54. package/lib/builder/mutateVariables.spec.js.map +1 -0
  55. package/lib/builder/mutator.d.ts +20 -0
  56. package/lib/builder/mutator.js +223 -0
  57. package/lib/builder/mutator.js.map +1 -0
  58. package/lib/builder/mutator.spec.d.ts +1 -0
  59. package/lib/builder/mutator.spec.js +368 -0
  60. package/lib/builder/mutator.spec.js.map +1 -0
  61. package/lib/config/projectConfig.d.ts +3 -0
  62. package/lib/config/projectConfig.js +7 -1
  63. package/lib/config/projectConfig.js.map +1 -1
  64. package/lib/config/projectConfig.spec.d.ts +1 -0
  65. package/lib/config/projectConfig.spec.js +24 -0
  66. package/lib/config/projectConfig.spec.js.map +1 -0
  67. package/lib/datasource/adapter.d.ts +7 -0
  68. package/lib/datasource/adapter.js +7 -0
  69. package/lib/datasource/adapter.js.map +1 -1
  70. package/lib/datasource/datasource.d.ts +3 -1
  71. package/lib/datasource/datasource.js +6 -0
  72. package/lib/datasource/datasource.js.map +1 -1
  73. package/lib/datasource/filesystemAdapter.d.ts +8 -0
  74. package/lib/datasource/filesystemAdapter.js +134 -7
  75. package/lib/datasource/filesystemAdapter.js.map +1 -1
  76. package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
  77. package/lib/datasource/filesystemAdapter.spec.js +88 -0
  78. package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
  79. package/lib/linter/conditionSchema.spec.js +2 -0
  80. package/lib/linter/conditionSchema.spec.js.map +1 -1
  81. package/lib/linter/featureSchema.d.ts +7 -7
  82. package/lib/linter/featureSchema.js +176 -103
  83. package/lib/linter/featureSchema.js.map +1 -1
  84. package/lib/linter/featureSchema.spec.js +167 -0
  85. package/lib/linter/featureSchema.spec.js.map +1 -1
  86. package/lib/linter/lintProject.d.ts +2 -0
  87. package/lib/linter/lintProject.js +85 -8
  88. package/lib/linter/lintProject.js.map +1 -1
  89. package/lib/linter/lintProject.spec.js +37 -0
  90. package/lib/linter/lintProject.spec.js.map +1 -1
  91. package/lib/linter/mutationNotation.d.ts +47 -0
  92. package/lib/linter/mutationNotation.js +381 -0
  93. package/lib/linter/mutationNotation.js.map +1 -0
  94. package/lib/linter/mutationNotation.spec.d.ts +1 -0
  95. package/lib/linter/mutationNotation.spec.js +549 -0
  96. package/lib/linter/mutationNotation.spec.js.map +1 -0
  97. package/lib/linter/segmentSchema.spec.js +2 -0
  98. package/lib/linter/segmentSchema.spec.js.map +1 -1
  99. package/lib/utils/git.js +32 -7
  100. package/lib/utils/git.js.map +1 -1
  101. package/package.json +2 -2
  102. package/src/builder/buildDatafile.ts +29 -3
  103. package/src/builder/mutateVariables.spec.ts +1134 -0
  104. package/src/builder/mutateVariables.ts +105 -0
  105. package/src/builder/mutator.spec.ts +413 -0
  106. package/src/builder/mutator.ts +237 -0
  107. package/src/config/projectConfig.spec.ts +31 -0
  108. package/src/config/projectConfig.ts +11 -0
  109. package/src/datasource/adapter.ts +21 -0
  110. package/src/datasource/datasource.ts +18 -1
  111. package/src/datasource/filesystemAdapter.spec.ts +153 -0
  112. package/src/datasource/filesystemAdapter.ts +216 -8
  113. package/src/linter/conditionSchema.spec.ts +2 -0
  114. package/src/linter/featureSchema.spec.ts +203 -0
  115. package/src/linter/featureSchema.ts +320 -167
  116. package/src/linter/lintProject.spec.ts +74 -0
  117. package/src/linter/lintProject.ts +123 -8
  118. package/src/linter/mutationNotation.spec.ts +642 -0
  119. package/src/linter/mutationNotation.ts +408 -0
  120. package/src/linter/segmentSchema.spec.ts +2 -0
  121. package/src/utils/git.ts +41 -7
@@ -0,0 +1,408 @@
1
+ import type { Schema } from "@featurevisor/types";
2
+
3
+ import { parseNotation, type MutationOperation, type PathPart } from "../builder/mutator";
4
+
5
+ export type { MutationOperation, PathPart };
6
+ export type PathSegment = PathPart;
7
+
8
+ const OPERATION_SUFFIX = /:((?:append|prepend|after|before|remove))$/;
9
+
10
+ export interface ParsedMutationKey {
11
+ rootKey: string;
12
+ pathSegments: PathSegment[];
13
+ allSegments: PathSegment[];
14
+ operation: MutationOperation;
15
+ }
16
+
17
+ /**
18
+ * Returns true if the key looks like mutation notation (contains path or operation).
19
+ */
20
+ export function isMutationKey(key: string): boolean {
21
+ const k = key.trim();
22
+ return k.includes(".") || k.includes("[") || OPERATION_SUFFIX.test(k);
23
+ }
24
+
25
+ /**
26
+ * Parse a full variable key (e.g. "config.width", "items[1].name", "tags:append")
27
+ * into root variable name, path segments within the variable, and operation.
28
+ * Uses the shared parseNotation from the mutator.
29
+ */
30
+ export function parseMutationKey(key: string): ParsedMutationKey | null {
31
+ const rest = key.trim();
32
+ if (!rest) return null;
33
+
34
+ const { segments, operation } = parseNotation(rest);
35
+ const rootKey = segments.length > 0 && "key" in segments[0] ? segments[0].key : "";
36
+ const first = segments[0];
37
+ const firstPathPart: PathSegment[] =
38
+ first && ("index" in first || "selector" in first)
39
+ ? [
40
+ "index" in first
41
+ ? { key: "", index: first.index }
42
+ : { key: "", selector: first.selector! },
43
+ ]
44
+ : [];
45
+ const pathWithinVariable = firstPathPart.concat(segments.slice(1));
46
+ return { rootKey, pathSegments: pathWithinVariable, allSegments: segments, operation };
47
+ }
48
+
49
+ /**
50
+ * Resolve a schema reference (schema.schema -> schemasByKey[name]).
51
+ * Follows one level of reference; the resolved schema may itself have oneOf or another ref.
52
+ */
53
+ function resolveSchemaRef(
54
+ schema: Schema | null,
55
+ schemasByKey: Record<string, Schema> | undefined,
56
+ ): Schema | null {
57
+ if (!schema || typeof schema !== "object") return null;
58
+ if (schema.schema && schemasByKey?.[schema.schema]) {
59
+ return resolveSchemaRef(schemasByKey[schema.schema], schemasByKey);
60
+ }
61
+ return schema;
62
+ }
63
+
64
+ /**
65
+ * Return true if the schema is a oneOf (multiple possible shapes); path resolution cannot descend through oneOf.
66
+ */
67
+ function isOneOfSchema(schema: Schema): boolean {
68
+ return Array.isArray(schema.oneOf) && schema.oneOf.length > 0;
69
+ }
70
+
71
+ /**
72
+ * Resolve the schema at a path within a variable schema.
73
+ * pathSegments are the path *within* the variable (e.g. for variable "config", path [ {key:"width"} ];
74
+ * for variable "items", path [ {index:0}, {key:"name"} ]).
75
+ * Returns the schema at that path, or null if the path is invalid.
76
+ * Does not descend through oneOf (path through oneOf is considered invalid for mutation targets).
77
+ */
78
+ export function resolveSchemaAtPath(
79
+ variableSchema: Schema | null,
80
+ pathSegments: PathSegment[],
81
+ schemasByKey?: Record<string, Schema>,
82
+ ): Schema | null {
83
+ let current: Schema | null = resolveSchemaRef(variableSchema, schemasByKey);
84
+ if (!current) return null;
85
+
86
+ for (const seg of pathSegments) {
87
+ if (isOneOfSchema(current)) return null;
88
+ if (seg.key) {
89
+ if (current.type !== "object") return null;
90
+ const props = current.properties;
91
+ if (!props || typeof props !== "object") return null;
92
+ const next = props[seg.key];
93
+ if (next === undefined) return null;
94
+ current = resolveSchemaRef(next, schemasByKey);
95
+ if (!current) return null;
96
+ }
97
+ if ("index" in seg || "selector" in seg) {
98
+ if (current.type !== "array") return null;
99
+ const itemSchema = current.items;
100
+ if (!itemSchema || typeof itemSchema !== "object") return null;
101
+ current = resolveSchemaRef(itemSchema, schemasByKey);
102
+ if (!current) return null;
103
+ }
104
+ }
105
+ return current;
106
+ }
107
+
108
+ /**
109
+ * Return the schema of the container at the end of path (object or array) and the last segment.
110
+ * Used to check if we can do append/prepend (must be array) or remove (object key or array element).
111
+ * Does not descend through oneOf.
112
+ */
113
+ function getContainerSchemaAtPath(
114
+ variableSchema: Schema | null,
115
+ pathSegments: PathSegment[],
116
+ schemasByKey?: Record<string, Schema>,
117
+ ): { containerSchema: Schema; lastSegment: PathSegment; parentSchema: Schema } | null {
118
+ if (pathSegments.length === 0) {
119
+ const resolved = variableSchema ? resolveSchemaRef(variableSchema, schemasByKey) : null;
120
+ return resolved
121
+ ? { containerSchema: resolved, lastSegment: { key: "" }, parentSchema: resolved }
122
+ : null;
123
+ }
124
+ let current: Schema | null = resolveSchemaRef(variableSchema, schemasByKey);
125
+ if (!current) return null;
126
+ const pathWithoutLast = pathSegments.slice(0, -1);
127
+ const lastSegment = pathSegments[pathSegments.length - 1];
128
+ for (const seg of pathWithoutLast) {
129
+ if (isOneOfSchema(current)) return null;
130
+ if ("index" in seg || "selector" in seg) {
131
+ if (current.type !== "array") return null;
132
+ const itemSchema = current.items;
133
+ if (!itemSchema || typeof itemSchema !== "object") return null;
134
+ current = resolveSchemaRef(itemSchema, schemasByKey);
135
+ } else {
136
+ if (current.type !== "object") return null;
137
+ const props = current.properties;
138
+ if (!props || typeof props !== "object") return null;
139
+ const next = props[seg.key];
140
+ if (next === undefined) return null;
141
+ current = resolveSchemaRef(next, schemasByKey);
142
+ }
143
+ if (!current) return null;
144
+ }
145
+ if (!current) return null;
146
+ const parentSchema = current;
147
+ if ("index" in lastSegment || "selector" in lastSegment) {
148
+ return { containerSchema: parentSchema, lastSegment, parentSchema };
149
+ }
150
+ const propSchema = parentSchema.properties?.[lastSegment.key];
151
+ const resolvedProp =
152
+ propSchema && typeof propSchema === "object"
153
+ ? resolveSchemaRef(propSchema, schemasByKey)
154
+ : null;
155
+ return resolvedProp ? { containerSchema: resolvedProp, lastSegment, parentSchema } : null;
156
+ }
157
+
158
+ export interface MutationValidationResult {
159
+ valid: boolean;
160
+ rootKey: string;
161
+ pathSegments: PathSegment[];
162
+ operation: MutationOperation;
163
+ valueSchema: Schema | null;
164
+ error?: string;
165
+ }
166
+
167
+ /**
168
+ * Validate mutation key against variable schema: root exists, path valid, operation allowed.
169
+ * Returns valueSchema to validate the value against (for set: schema at path; for append/prepend/after/before: item schema).
170
+ * Uses Schema from @featurevisor/types; validates required (no :remove on required props) and does not allow path through oneOf.
171
+ */
172
+ export function validateMutationKey(
173
+ key: string,
174
+ variableSchemaByKey: Record<string, Schema>,
175
+ schemasByKey?: Record<string, Schema>,
176
+ ): MutationValidationResult {
177
+ const parsed = parseMutationKey(key);
178
+ if (!parsed) {
179
+ return {
180
+ valid: false,
181
+ rootKey: "",
182
+ pathSegments: [],
183
+ operation: "set",
184
+ valueSchema: null,
185
+ error: `Invalid mutation notation: "${key}"`,
186
+ };
187
+ }
188
+ const { rootKey, pathSegments, operation } = parsed;
189
+ if (!rootKey) {
190
+ return {
191
+ valid: false,
192
+ rootKey: "",
193
+ pathSegments: [],
194
+ operation,
195
+ valueSchema: null,
196
+ error: `Mutation key must start with a variable name: "${key}"`,
197
+ };
198
+ }
199
+ const variableSchema = variableSchemaByKey[rootKey];
200
+ if (!variableSchema) {
201
+ return {
202
+ valid: false,
203
+ rootKey,
204
+ pathSegments,
205
+ operation,
206
+ valueSchema: null,
207
+ error: `Variable "${rootKey}" is not defined in \`variablesSchema\`.`,
208
+ };
209
+ }
210
+ const resolvedRoot = resolveSchemaRef(variableSchema, schemasByKey);
211
+ if (!resolvedRoot) {
212
+ const refName =
213
+ variableSchema && typeof variableSchema === "object" && "schema" in variableSchema
214
+ ? (variableSchema as { schema?: string }).schema
215
+ : undefined;
216
+ return {
217
+ valid: false,
218
+ rootKey,
219
+ pathSegments,
220
+ operation,
221
+ valueSchema: null,
222
+ error:
223
+ refName != null
224
+ ? `Schema "${refName}" could not be loaded for variable "${rootKey}".`
225
+ : `Could not resolve schema for variable "${rootKey}".`,
226
+ };
227
+ }
228
+ if (pathSegments.length > 0 && isOneOfSchema(resolvedRoot)) {
229
+ return {
230
+ valid: false,
231
+ rootKey,
232
+ pathSegments,
233
+ operation,
234
+ valueSchema: null,
235
+ error: `Cannot mutate path into variable "${rootKey}" (root schema is \`oneOf\`; path resolution not defined).`,
236
+ };
237
+ }
238
+
239
+ const container = getContainerSchemaAtPath(variableSchema, pathSegments, schemasByKey);
240
+ const valueSchemaAtPath = resolveSchemaAtPath(variableSchema, pathSegments, schemasByKey);
241
+
242
+ switch (operation) {
243
+ case "append":
244
+ case "prepend": {
245
+ if (!container) {
246
+ return {
247
+ valid: false,
248
+ rootKey,
249
+ pathSegments,
250
+ operation,
251
+ valueSchema: null,
252
+ error: `Path "${key}" is invalid for variable "${rootKey}" (path does not exist in schema).`,
253
+ };
254
+ }
255
+ const arrResolved = container.containerSchema;
256
+ if (!arrResolved || arrResolved.type !== "array") {
257
+ return {
258
+ valid: false,
259
+ rootKey,
260
+ pathSegments,
261
+ operation,
262
+ valueSchema: null,
263
+ error: `Operation ":${operation}" is only allowed on array variables or object properties of type array; path "${key}" does not point to an array.`,
264
+ };
265
+ }
266
+ if (isOneOfSchema(arrResolved)) {
267
+ return {
268
+ valid: false,
269
+ rootKey,
270
+ pathSegments,
271
+ operation,
272
+ valueSchema: null,
273
+ error: `Operation ":${operation}" is not allowed when array \`items\` is \`oneOf\` (path "${key}").`,
274
+ };
275
+ }
276
+ const itemSchema =
277
+ arrResolved.items && typeof arrResolved.items === "object"
278
+ ? resolveSchemaRef(arrResolved.items, schemasByKey)
279
+ : null;
280
+ return {
281
+ valid: true,
282
+ rootKey,
283
+ pathSegments,
284
+ operation,
285
+ valueSchema: itemSchema,
286
+ };
287
+ }
288
+ case "after":
289
+ case "before":
290
+ case "remove": {
291
+ if (!container) {
292
+ return {
293
+ valid: false,
294
+ rootKey,
295
+ pathSegments,
296
+ operation,
297
+ valueSchema: null,
298
+ error: `Path "${key}" is invalid for variable "${rootKey}" (path does not exist in schema).`,
299
+ };
300
+ }
301
+ const last = container.lastSegment;
302
+ if ("index" in last || "selector" in last) {
303
+ if (container.parentSchema.type !== "array") {
304
+ return {
305
+ valid: false,
306
+ rootKey,
307
+ pathSegments,
308
+ operation,
309
+ valueSchema: null,
310
+ error: `Operation ":${operation}" with array index/selector is only allowed on arrays; path "${key}" does not point to an array element.`,
311
+ };
312
+ }
313
+ if (operation === "after" || operation === "before") {
314
+ const parentItems = container.parentSchema.items;
315
+ if (parentItems && typeof parentItems === "object" && isOneOfSchema(parentItems)) {
316
+ return {
317
+ valid: false,
318
+ rootKey,
319
+ pathSegments,
320
+ operation,
321
+ valueSchema: null,
322
+ error: `Operation ":${operation}" is not allowed when array \`items\` is \`oneOf\` (path "${key}").`,
323
+ };
324
+ }
325
+ const itemSchema =
326
+ parentItems && typeof parentItems === "object"
327
+ ? resolveSchemaRef(parentItems, schemasByKey)
328
+ : null;
329
+ return { valid: true, rootKey, pathSegments, operation, valueSchema: itemSchema };
330
+ }
331
+ return { valid: true, rootKey, pathSegments, operation, valueSchema: null };
332
+ }
333
+ if (container.parentSchema.type !== "object") {
334
+ return {
335
+ valid: false,
336
+ rootKey,
337
+ pathSegments,
338
+ operation,
339
+ valueSchema: null,
340
+ error: `Operation ":${operation}" on a property is only allowed on objects; path "${key}" does not point to an object property.`,
341
+ };
342
+ }
343
+ const requiredKeys = container.parentSchema.required;
344
+ if (
345
+ operation === "remove" &&
346
+ Array.isArray(requiredKeys) &&
347
+ requiredKeys.includes(last.key)
348
+ ) {
349
+ return {
350
+ valid: false,
351
+ rootKey,
352
+ pathSegments,
353
+ operation,
354
+ valueSchema: null,
355
+ error: `Cannot remove required property "${last.key}" from variable "${rootKey}" (listed in schema \`required\`).`,
356
+ };
357
+ }
358
+ return { valid: true, rootKey, pathSegments, operation, valueSchema: null };
359
+ }
360
+ case "set": {
361
+ if (valueSchemaAtPath === null && pathSegments.length > 0) {
362
+ return {
363
+ valid: false,
364
+ rootKey,
365
+ pathSegments,
366
+ operation,
367
+ valueSchema: null,
368
+ error: `Path "${key}" is invalid for variable "${rootKey}" (path does not exist in schema).`,
369
+ };
370
+ }
371
+ if (valueSchemaAtPath && isOneOfSchema(valueSchemaAtPath)) {
372
+ return {
373
+ valid: false,
374
+ rootKey,
375
+ pathSegments,
376
+ operation,
377
+ valueSchema: null,
378
+ error: `Cannot set value at path "${key}" (target schema is \`oneOf\`; mutation target must be a single schema).`,
379
+ };
380
+ }
381
+ return {
382
+ valid: true,
383
+ rootKey,
384
+ pathSegments,
385
+ operation,
386
+ valueSchema: valueSchemaAtPath,
387
+ };
388
+ }
389
+ default:
390
+ return {
391
+ valid: true,
392
+ rootKey,
393
+ pathSegments,
394
+ operation,
395
+ valueSchema: valueSchemaAtPath,
396
+ };
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Parse a path-map key (relative to a variable) into path segments for resolveSchemaAtPath.
402
+ * e.g. "display.fontSize" -> [{key:"display"},{key:"fontSize"}], "[0].name" -> [{key:"",index:0},{key:"name"}].
403
+ */
404
+ export function parsePathMapKey(relativePath: string): PathSegment[] | null {
405
+ const parsed = parseMutationKey(relativePath);
406
+ if (!parsed) return null;
407
+ return parsed.allSegments.length > 0 ? parsed.allSegments : null;
408
+ }
@@ -22,7 +22,9 @@ function minimalProjectConfig(): ProjectConfig {
22
22
  datafileNamePattern: "",
23
23
  revisionFileName: "",
24
24
  siteExportDirectoryPath: "",
25
+ environmentsDirectoryPath: "",
25
26
  environments: ["staging", "production"],
27
+ splitByEnvironment: false,
26
28
  tags: ["all"],
27
29
  adapter: {},
28
30
  plugins: [],
package/src/utils/git.ts CHANGED
@@ -5,6 +5,12 @@ import type { Commit, EntityDiff, EntityType } from "@featurevisor/types";
5
5
  import { ProjectConfig } from "../config";
6
6
  import { CustomParser } from "../parsers";
7
7
 
8
+ function isWithinDirectory(directoryPath: string, fileDirectoryPath: string): boolean {
9
+ return (
10
+ fileDirectoryPath === directoryPath || fileDirectoryPath.startsWith(directoryPath + path.sep)
11
+ );
12
+ }
13
+
8
14
  function parseGitCommitShowOutput(gitShowOutput: string) {
9
15
  const result = {
10
16
  hash: "",
@@ -83,17 +89,22 @@ export function getCommit(
83
89
 
84
90
  // get entity type
85
91
  let type: EntityType = "attribute";
86
- if (relativeDir === projectConfig.attributesDirectoryPath) {
92
+ if (isWithinDirectory(projectConfig.attributesDirectoryPath, relativeDir)) {
87
93
  type = "attribute";
88
- } else if (relativeDir === projectConfig.segmentsDirectoryPath) {
94
+ } else if (isWithinDirectory(projectConfig.segmentsDirectoryPath, relativeDir)) {
89
95
  type = "segment";
90
- } else if (relativeDir === projectConfig.featuresDirectoryPath) {
96
+ } else if (isWithinDirectory(projectConfig.featuresDirectoryPath, relativeDir)) {
91
97
  type = "feature";
92
- } else if (relativeDir === projectConfig.groupsDirectoryPath) {
98
+ } else if (
99
+ projectConfig.splitByEnvironment &&
100
+ isWithinDirectory(projectConfig.environmentsDirectoryPath, relativeDir)
101
+ ) {
102
+ type = "feature";
103
+ } else if (isWithinDirectory(projectConfig.groupsDirectoryPath, relativeDir)) {
93
104
  type = "group";
94
- } else if (relativeDir === projectConfig.schemasDirectoryPath) {
105
+ } else if (isWithinDirectory(projectConfig.schemasDirectoryPath, relativeDir)) {
95
106
  type = "schema";
96
- } else if (relativeDir === projectConfig.testsDirectoryPath) {
107
+ } else if (isWithinDirectory(projectConfig.testsDirectoryPath, relativeDir)) {
97
108
  type = "test";
98
109
  } else {
99
110
  // unknown type
@@ -109,7 +120,30 @@ export function getCommit(
109
120
  return;
110
121
  }
111
122
 
112
- const key = fileName.replace(extensionWithDot, "");
123
+ let key = fileName.replace(extensionWithDot, "");
124
+
125
+ if (
126
+ type === "feature" &&
127
+ projectConfig.splitByEnvironment &&
128
+ absolutePath.startsWith(projectConfig.environmentsDirectoryPath + path.sep)
129
+ ) {
130
+ const featureRelativePath = absolutePath
131
+ .replace(projectConfig.environmentsDirectoryPath + path.sep, "")
132
+ .split(path.sep)
133
+ .slice(1)
134
+ .join(path.sep)
135
+ .replace(extensionWithDot, "");
136
+
137
+ key = featureRelativePath.replace(/\\/g, "/");
138
+ } else if (
139
+ type === "feature" &&
140
+ absolutePath.startsWith(projectConfig.featuresDirectoryPath + path.sep)
141
+ ) {
142
+ key = absolutePath
143
+ .replace(projectConfig.featuresDirectoryPath + path.sep, "")
144
+ .replace(extensionWithDot, "")
145
+ .replace(/\\/g, "/");
146
+ }
113
147
 
114
148
  const entityDiff: EntityDiff = {
115
149
  type,