@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.
- package/CHANGELOG.md +22 -0
- package/coverage/clover.xml +1499 -1028
- package/coverage/coverage-final.json +21 -17
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +38 -8
- package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
- package/coverage/lcov-report/builder/mutator.ts.html +796 -0
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +30 -15
- package/coverage/lcov-report/config/index.ts.html +88 -0
- package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
- package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
- package/coverage/lcov-report/datasource/datasource.ts.html +77 -26
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +693 -69
- package/coverage/lcov-report/datasource/index.html +35 -35
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +53 -53
- package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
- package/coverage/lcov-report/linter/featureSchema.ts.html +848 -389
- package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/index.html +50 -35
- package/coverage/lcov-report/linter/lintProject.ts.html +437 -92
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
- package/coverage/lcov-report/linter/printError.ts.html +9 -9
- package/coverage/lcov-report/linter/schema.ts.html +69 -69
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +10 -10
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +118 -16
- package/coverage/lcov-report/utils/index.html +14 -14
- package/coverage/lcov.info +2918 -1932
- package/lib/builder/buildDatafile.js +8 -3
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/mutateVariables.d.ts +14 -0
- package/lib/builder/mutateVariables.js +90 -0
- package/lib/builder/mutateVariables.js.map +1 -0
- package/lib/builder/mutateVariables.spec.d.ts +1 -0
- package/lib/builder/mutateVariables.spec.js +1045 -0
- package/lib/builder/mutateVariables.spec.js.map +1 -0
- package/lib/builder/mutator.d.ts +20 -0
- package/lib/builder/mutator.js +223 -0
- package/lib/builder/mutator.js.map +1 -0
- package/lib/builder/mutator.spec.d.ts +1 -0
- package/lib/builder/mutator.spec.js +368 -0
- package/lib/builder/mutator.spec.js.map +1 -0
- package/lib/config/projectConfig.d.ts +3 -0
- package/lib/config/projectConfig.js +7 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/config/projectConfig.spec.d.ts +1 -0
- package/lib/config/projectConfig.spec.js +24 -0
- package/lib/config/projectConfig.spec.js.map +1 -0
- package/lib/datasource/adapter.d.ts +7 -0
- package/lib/datasource/adapter.js +7 -0
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +3 -1
- package/lib/datasource/datasource.js +6 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +8 -0
- package/lib/datasource/filesystemAdapter.js +134 -7
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
- package/lib/datasource/filesystemAdapter.spec.js +88 -0
- package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
- package/lib/linter/conditionSchema.spec.js +2 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +7 -7
- package/lib/linter/featureSchema.js +176 -103
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +167 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.d.ts +2 -0
- package/lib/linter/lintProject.js +85 -8
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.js +37 -0
- package/lib/linter/lintProject.spec.js.map +1 -1
- package/lib/linter/mutationNotation.d.ts +47 -0
- package/lib/linter/mutationNotation.js +381 -0
- package/lib/linter/mutationNotation.js.map +1 -0
- package/lib/linter/mutationNotation.spec.d.ts +1 -0
- package/lib/linter/mutationNotation.spec.js +549 -0
- package/lib/linter/mutationNotation.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.js +2 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/lib/utils/git.js +32 -7
- package/lib/utils/git.js.map +1 -1
- package/package.json +2 -2
- package/src/builder/buildDatafile.ts +29 -3
- package/src/builder/mutateVariables.spec.ts +1134 -0
- package/src/builder/mutateVariables.ts +105 -0
- package/src/builder/mutator.spec.ts +413 -0
- package/src/builder/mutator.ts +237 -0
- package/src/config/projectConfig.spec.ts +31 -0
- package/src/config/projectConfig.ts +11 -0
- package/src/datasource/adapter.ts +21 -0
- package/src/datasource/datasource.ts +18 -1
- package/src/datasource/filesystemAdapter.spec.ts +153 -0
- package/src/datasource/filesystemAdapter.ts +216 -8
- package/src/linter/conditionSchema.spec.ts +2 -0
- package/src/linter/featureSchema.spec.ts +203 -0
- package/src/linter/featureSchema.ts +320 -167
- package/src/linter/lintProject.spec.ts +74 -0
- package/src/linter/lintProject.ts +123 -8
- package/src/linter/mutationNotation.spec.ts +642 -0
- package/src/linter/mutationNotation.ts +408 -0
- package/src/linter/segmentSchema.spec.ts +2 -0
- package/src/utils/git.ts +41 -7
|
@@ -15,6 +15,46 @@ function createTempProjectFromExample1() {
|
|
|
15
15
|
return tempRoot;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function createTempSplitProject() {
|
|
19
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "featurevisor-lint-split-"));
|
|
20
|
+
|
|
21
|
+
fs.writeFileSync(
|
|
22
|
+
path.join(tempRoot, "featurevisor.config.js"),
|
|
23
|
+
[
|
|
24
|
+
"module.exports = {",
|
|
25
|
+
" environments: ['staging', 'production'],",
|
|
26
|
+
" splitByEnvironment: true,",
|
|
27
|
+
" tags: ['all'],",
|
|
28
|
+
"};",
|
|
29
|
+
].join("\n"),
|
|
30
|
+
"utf8",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
fs.mkdirSync(path.join(tempRoot, "features"), { recursive: true });
|
|
34
|
+
fs.mkdirSync(path.join(tempRoot, "environments", "staging"), { recursive: true });
|
|
35
|
+
fs.mkdirSync(path.join(tempRoot, "environments", "production"), { recursive: true });
|
|
36
|
+
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(tempRoot, "features", "foo.yml"),
|
|
39
|
+
["key: foo", "description: Foo", "tags:", " - all", "bucketBy: userId"].join("\n"),
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(tempRoot, "environments", "staging", "foo.yml"),
|
|
45
|
+
["rules:", " - key: everyone", " segments: '*'", " percentage: 100"].join("\n"),
|
|
46
|
+
"utf8",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(tempRoot, "environments", "production", "foo.yml"),
|
|
51
|
+
["rules:", " - key: everyone", " segments: '*'", " percentage: 0"].join("\n"),
|
|
52
|
+
"utf8",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return tempRoot;
|
|
56
|
+
}
|
|
57
|
+
|
|
18
58
|
function getDeps(rootDirectoryPath: string) {
|
|
19
59
|
const projectConfig = getProjectConfig(rootDirectoryPath);
|
|
20
60
|
const datasource = new Datasource(projectConfig, rootDirectoryPath);
|
|
@@ -112,4 +152,38 @@ describe("core: lintProject", function () {
|
|
|
112
152
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
113
153
|
expect(processExitSpy).not.toHaveBeenCalled();
|
|
114
154
|
});
|
|
155
|
+
|
|
156
|
+
it("reports missing split environment file with environment file path", async () => {
|
|
157
|
+
const splitProjectPath = createTempSplitProject();
|
|
158
|
+
fs.unlinkSync(path.join(splitProjectPath, "environments", "production", "foo.yml"));
|
|
159
|
+
|
|
160
|
+
const result = await lintProject(getDeps(splitProjectPath) as any, {
|
|
161
|
+
json: true,
|
|
162
|
+
entityType: "feature",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result.hasError).toBe(true);
|
|
166
|
+
expect(result.errors[0].filePath).toContain(path.join("environments", "production", "foo.yml"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("reports split environment feature schema errors against environment file path", async () => {
|
|
170
|
+
const splitProjectPath = createTempSplitProject();
|
|
171
|
+
fs.writeFileSync(
|
|
172
|
+
path.join(splitProjectPath, "environments", "staging", "foo.yml"),
|
|
173
|
+
["rules:", " - key: everyone", " segments: '*'", " percentage: invalid"].join("\n"),
|
|
174
|
+
"utf8",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = await lintProject(getDeps(splitProjectPath) as any, {
|
|
178
|
+
json: true,
|
|
179
|
+
entityType: "feature",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.hasError).toBe(true);
|
|
183
|
+
expect(
|
|
184
|
+
result.errors.some((error) =>
|
|
185
|
+
error.filePath.includes(path.join("environments", "staging", "foo.yml")),
|
|
186
|
+
),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
});
|
|
115
189
|
});
|
|
@@ -37,6 +37,8 @@ export interface LintErrorItem {
|
|
|
37
37
|
path: (string | number)[];
|
|
38
38
|
code?: string;
|
|
39
39
|
value?: unknown;
|
|
40
|
+
/** Present only when splitByEnvironment is true and the error is from an environment-specific file (rules/force/expose). */
|
|
41
|
+
environment?: string;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface LintResult {
|
|
@@ -97,6 +99,62 @@ export async function lintProject(
|
|
|
97
99
|
return fullPath;
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
async function getFeaturePathFromIssuePath(
|
|
103
|
+
featureKey: string,
|
|
104
|
+
issuePath: (string | number)[] = [],
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const defaultPath = getFullPathFromKey("feature", featureKey);
|
|
107
|
+
|
|
108
|
+
if (!projectConfig.splitByEnvironment) {
|
|
109
|
+
return defaultPath;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(projectConfig.environments)) {
|
|
113
|
+
return defaultPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [topLevelKey, environment] = issuePath;
|
|
117
|
+
if (
|
|
118
|
+
(topLevelKey === "rules" || topLevelKey === "force" || topLevelKey === "expose") &&
|
|
119
|
+
typeof environment === "string"
|
|
120
|
+
) {
|
|
121
|
+
const sourcePath = await datasource.getFeaturePropertySourcePath(
|
|
122
|
+
featureKey,
|
|
123
|
+
topLevelKey,
|
|
124
|
+
environment,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (sourcePath) {
|
|
128
|
+
return sourcePath;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return defaultPath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* When the error is from an environment-specific file (splitByEnvironment), the Zod path
|
|
137
|
+
* is against the merged feature (e.g. rules.production.1.variables.foo). Return the path
|
|
138
|
+
* as it appears in that file (e.g. rules.1.variables.foo) by stripping the environment segment.
|
|
139
|
+
*/
|
|
140
|
+
function getPathRelativeToFeatureFile(
|
|
141
|
+
issuePath: (string | number)[],
|
|
142
|
+
targetPath: string,
|
|
143
|
+
defaultFeaturePath: string,
|
|
144
|
+
): (string | number)[] {
|
|
145
|
+
if (targetPath === defaultFeaturePath || issuePath.length < 2) {
|
|
146
|
+
return issuePath;
|
|
147
|
+
}
|
|
148
|
+
const [topLevelKey, second] = issuePath;
|
|
149
|
+
if (
|
|
150
|
+
(topLevelKey === "rules" || topLevelKey === "force" || topLevelKey === "expose") &&
|
|
151
|
+
typeof second === "string"
|
|
152
|
+
) {
|
|
153
|
+
return [topLevelKey, ...issuePath.slice(2)];
|
|
154
|
+
}
|
|
155
|
+
return issuePath;
|
|
156
|
+
}
|
|
157
|
+
|
|
100
158
|
async function printEntityHeader(entityType: LintEntityType, key: string, fullPath: string) {
|
|
101
159
|
if (isJsonMode) {
|
|
102
160
|
return;
|
|
@@ -159,10 +217,18 @@ export async function lintProject(
|
|
|
159
217
|
fullPath: string,
|
|
160
218
|
error: unknown,
|
|
161
219
|
) {
|
|
220
|
+
const pathFromError =
|
|
221
|
+
error &&
|
|
222
|
+
typeof error === "object" &&
|
|
223
|
+
"featurevisorFilePath" in error &&
|
|
224
|
+
typeof error.featurevisorFilePath === "string"
|
|
225
|
+
? error.featurevisorFilePath
|
|
226
|
+
: undefined;
|
|
227
|
+
const targetPath = pathFromError || fullPath;
|
|
162
228
|
const message = error instanceof Error ? error.message : String(error);
|
|
163
229
|
|
|
164
230
|
recordError({
|
|
165
|
-
filePath: path.relative(process.cwd(),
|
|
231
|
+
filePath: path.relative(process.cwd(), targetPath),
|
|
166
232
|
entityType,
|
|
167
233
|
key,
|
|
168
234
|
message,
|
|
@@ -171,7 +237,7 @@ export async function lintProject(
|
|
|
171
237
|
});
|
|
172
238
|
|
|
173
239
|
if (!isJsonMode) {
|
|
174
|
-
await printEntityHeader(entityType, key,
|
|
240
|
+
await printEntityHeader(entityType, key, targetPath);
|
|
175
241
|
console.log("");
|
|
176
242
|
console.log(error);
|
|
177
243
|
}
|
|
@@ -184,22 +250,71 @@ export async function lintProject(
|
|
|
184
250
|
error: ZodError,
|
|
185
251
|
) {
|
|
186
252
|
const issues = getLintIssuesFromZodError(error);
|
|
253
|
+
const defaultFeaturePath =
|
|
254
|
+
entityType === "feature" ? getFullPathFromKey("feature", key) : fullPath;
|
|
255
|
+
|
|
256
|
+
const issuesWithTargetPath: {
|
|
257
|
+
issue: (typeof issues)[0];
|
|
258
|
+
targetPath: string;
|
|
259
|
+
pathRelativeToFile: (string | number)[];
|
|
260
|
+
}[] = [];
|
|
261
|
+
for (const issue of issues) {
|
|
262
|
+
const targetPath =
|
|
263
|
+
entityType === "feature" ? await getFeaturePathFromIssuePath(key, issue.path) : fullPath;
|
|
264
|
+
const pathRelativeToFile =
|
|
265
|
+
entityType === "feature"
|
|
266
|
+
? getPathRelativeToFeatureFile(issue.path, targetPath, defaultFeaturePath)
|
|
267
|
+
: issue.path;
|
|
268
|
+
const isFromEnvFile =
|
|
269
|
+
entityType === "feature" &&
|
|
270
|
+
targetPath !== defaultFeaturePath &&
|
|
271
|
+
issue.path.length >= 2 &&
|
|
272
|
+
(issue.path[0] === "rules" || issue.path[0] === "force" || issue.path[0] === "expose") &&
|
|
273
|
+
typeof issue.path[1] === "string";
|
|
274
|
+
const environment = isFromEnvFile ? (issue.path[1] as string) : undefined;
|
|
275
|
+
issuesWithTargetPath.push({ issue, targetPath, pathRelativeToFile });
|
|
187
276
|
|
|
188
|
-
issues.forEach((issue) => {
|
|
189
277
|
recordError({
|
|
190
|
-
filePath: path.relative(process.cwd(),
|
|
278
|
+
filePath: path.relative(process.cwd(), targetPath),
|
|
191
279
|
entityType,
|
|
192
280
|
key,
|
|
193
281
|
message: issue.message,
|
|
194
|
-
path:
|
|
282
|
+
path: pathRelativeToFile,
|
|
195
283
|
code: issue.code,
|
|
196
284
|
value: issue.value,
|
|
285
|
+
...(environment !== undefined && { environment }),
|
|
197
286
|
});
|
|
198
|
-
}
|
|
287
|
+
}
|
|
199
288
|
|
|
200
289
|
if (!isJsonMode) {
|
|
201
|
-
|
|
202
|
-
|
|
290
|
+
if (entityType === "feature" && issuesWithTargetPath.length > 0) {
|
|
291
|
+
const byPath = new Map<
|
|
292
|
+
string,
|
|
293
|
+
{
|
|
294
|
+
issue: (typeof issuesWithTargetPath)[0]["issue"];
|
|
295
|
+
pathRelativeToFile: (string | number)[];
|
|
296
|
+
}[]
|
|
297
|
+
>();
|
|
298
|
+
for (const { issue, targetPath, pathRelativeToFile } of issuesWithTargetPath) {
|
|
299
|
+
const pathKey = targetPath;
|
|
300
|
+
if (!byPath.has(pathKey)) byPath.set(pathKey, []);
|
|
301
|
+
byPath.get(pathKey)!.push({ issue, pathRelativeToFile });
|
|
302
|
+
}
|
|
303
|
+
for (const [targetPathKey, groupItems] of byPath) {
|
|
304
|
+
await printEntityHeader(entityType, key, targetPathKey);
|
|
305
|
+
for (const { issue, pathRelativeToFile } of groupItems) {
|
|
306
|
+
console.log(CLI_FORMAT_RED, ` => Error: ${issue.message}`);
|
|
307
|
+
console.log(" Path:", pathRelativeToFile.join("."));
|
|
308
|
+
if (typeof issue.value !== "undefined" && issue.value !== "undefined") {
|
|
309
|
+
console.log(" Value:", issue.value);
|
|
310
|
+
}
|
|
311
|
+
console.log("");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
await printEntityHeader(entityType, key, fullPath);
|
|
316
|
+
printZodError(error);
|
|
317
|
+
}
|
|
203
318
|
}
|
|
204
319
|
}
|
|
205
320
|
|