@featurevisor/core 2.14.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 +11 -0
- package/coverage/clover.xml +774 -652
- package/coverage/coverage-final.json +19 -18
- 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 +1 -1
- package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +1 -1
- 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 +76 -25
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +690 -66
- 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 +43 -43
- 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 +152 -164
- package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/index.html +34 -34
- package/coverage/lcov-report/linter/lintProject.ts.html +428 -83
- package/coverage/lcov-report/linter/mutationNotation.ts.html +3 -3
- package/coverage/lcov-report/linter/printError.ts.html +9 -9
- package/coverage/lcov-report/linter/schema.ts.html +4 -4
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- 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 +1374 -1107
- 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.js +46 -49
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +17 -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/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/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 +21 -0
- package/src/linter/featureSchema.ts +84 -88
- package/src/linter/lintProject.spec.ts +74 -0
- package/src/linter/lintProject.ts +123 -8
- package/src/linter/segmentSchema.spec.ts +2 -0
- package/src/utils/git.ts +41 -7
|
@@ -1465,15 +1465,9 @@ export function getFeatureZodSchema(
|
|
|
1465
1465
|
}
|
|
1466
1466
|
}
|
|
1467
1467
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// Every variable value is validated against its schema from variablesSchema. Sources covered:
|
|
1473
|
-
// 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
|
|
1474
|
-
// 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
|
|
1475
|
-
// 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
|
|
1476
|
-
const variableSchemaByKey = value.variablesSchema;
|
|
1468
|
+
// When variablesSchema is absent, variableSchemaByKey is {} so any variable key used in
|
|
1469
|
+
// rules/force/variations will be reported as "not defined in variablesSchema".
|
|
1470
|
+
const variableSchemaByKey = value.variablesSchema ?? {};
|
|
1477
1471
|
const variationValues: string[] = [];
|
|
1478
1472
|
|
|
1479
1473
|
if (value.variations) {
|
|
@@ -1482,100 +1476,102 @@ export function getFeatureZodSchema(
|
|
|
1482
1476
|
});
|
|
1483
1477
|
}
|
|
1484
1478
|
|
|
1485
|
-
// variablesSchema[key]
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1479
|
+
// variablesSchema[key] — only when feature defines variablesSchema
|
|
1480
|
+
if (value.variablesSchema) {
|
|
1481
|
+
const variableKeys = Object.keys(variableSchemaByKey);
|
|
1482
|
+
variableKeys.forEach((variableKey) => {
|
|
1483
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1489
1484
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
schemasByKey,
|
|
1495
|
-
);
|
|
1496
|
-
if (!resolvedSchema) {
|
|
1497
|
-
ctx.addIssue({
|
|
1498
|
-
code: z.ZodIssueCode.custom,
|
|
1499
|
-
message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1500
|
-
path: ["variablesSchema", variableKey],
|
|
1501
|
-
});
|
|
1502
|
-
} else {
|
|
1503
|
-
refineRequiredKeysInSchema(
|
|
1504
|
-
resolvedSchema as SchemaLikeForRequired,
|
|
1505
|
-
["variablesSchema", variableKey],
|
|
1506
|
-
ctx,
|
|
1485
|
+
// When variable references a schema by name, ensure it resolves and validate the referenced schema
|
|
1486
|
+
if ("schema" in variableSchema && variableSchema.schema) {
|
|
1487
|
+
const resolvedSchema = resolveVariableSchema(
|
|
1488
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1507
1489
|
schemasByKey,
|
|
1508
1490
|
);
|
|
1491
|
+
if (!resolvedSchema) {
|
|
1492
|
+
ctx.addIssue({
|
|
1493
|
+
code: z.ZodIssueCode.custom,
|
|
1494
|
+
message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1495
|
+
path: ["variablesSchema", variableKey],
|
|
1496
|
+
});
|
|
1497
|
+
} else {
|
|
1498
|
+
refineRequiredKeysInSchema(
|
|
1499
|
+
resolvedSchema as SchemaLikeForRequired,
|
|
1500
|
+
["variablesSchema", variableKey],
|
|
1501
|
+
ctx,
|
|
1502
|
+
schemasByKey,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1509
1505
|
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// When type and enum are both present, all enum values must match the type
|
|
1513
|
-
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1514
|
-
if (
|
|
1515
|
-
effectiveSchema &&
|
|
1516
|
-
effectiveSchema.type &&
|
|
1517
|
-
Array.isArray(effectiveSchema.enum) &&
|
|
1518
|
-
effectiveSchema.enum.length > 0
|
|
1519
|
-
) {
|
|
1520
|
-
refineEnumMatchesType(
|
|
1521
|
-
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1522
|
-
["variablesSchema", variableKey],
|
|
1523
|
-
ctx,
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
1506
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
)
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1542
|
-
pathPrefix,
|
|
1543
|
-
ctx,
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1507
|
+
// When type and enum are both present, all enum values must match the type
|
|
1508
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1509
|
+
if (
|
|
1510
|
+
effectiveSchema &&
|
|
1511
|
+
effectiveSchema.type &&
|
|
1512
|
+
Array.isArray(effectiveSchema.enum) &&
|
|
1513
|
+
effectiveSchema.enum.length > 0
|
|
1514
|
+
) {
|
|
1515
|
+
refineEnumMatchesType(
|
|
1516
|
+
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1517
|
+
["variablesSchema", variableKey],
|
|
1518
|
+
ctx,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1546
1521
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1522
|
+
// Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
|
|
1523
|
+
if (!("schema" in variableSchema) || !variableSchema.schema) {
|
|
1524
|
+
const pathPrefix = ["variablesSchema", variableKey];
|
|
1525
|
+
refineMinimumMaximum(
|
|
1526
|
+
variableSchema as Parameters<typeof refineMinimumMaximum>[0],
|
|
1527
|
+
pathPrefix,
|
|
1528
|
+
ctx,
|
|
1529
|
+
);
|
|
1530
|
+
refineStringLengthPattern(
|
|
1531
|
+
variableSchema as Parameters<typeof refineStringLengthPattern>[0],
|
|
1532
|
+
pathPrefix,
|
|
1533
|
+
ctx,
|
|
1534
|
+
);
|
|
1535
|
+
refineArrayItems(
|
|
1536
|
+
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1537
|
+
pathPrefix,
|
|
1538
|
+
ctx,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1554
1541
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
variableKey,
|
|
1563
|
-
schemasByKey,
|
|
1564
|
-
);
|
|
1542
|
+
if (variableKey === "variation") {
|
|
1543
|
+
ctx.addIssue({
|
|
1544
|
+
code: z.ZodIssueCode.custom,
|
|
1545
|
+
message: `Variable key "${variableKey}" is reserved and cannot be used.`,
|
|
1546
|
+
path: ["variablesSchema", variableKey],
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1565
1549
|
|
|
1566
|
-
|
|
1567
|
-
if (variableSchema.disabledValue !== undefined) {
|
|
1550
|
+
// defaultValue
|
|
1568
1551
|
superRefineVariableValue(
|
|
1569
1552
|
projectConfig,
|
|
1570
1553
|
variableSchema,
|
|
1571
|
-
variableSchema.
|
|
1572
|
-
["variablesSchema", variableKey, "
|
|
1554
|
+
variableSchema.defaultValue,
|
|
1555
|
+
["variablesSchema", variableKey, "defaultValue"],
|
|
1573
1556
|
ctx,
|
|
1574
1557
|
variableKey,
|
|
1575
1558
|
schemasByKey,
|
|
1576
1559
|
);
|
|
1577
|
-
|
|
1578
|
-
|
|
1560
|
+
|
|
1561
|
+
// disabledValue (only when present)
|
|
1562
|
+
if (variableSchema.disabledValue !== undefined) {
|
|
1563
|
+
superRefineVariableValue(
|
|
1564
|
+
projectConfig,
|
|
1565
|
+
variableSchema,
|
|
1566
|
+
variableSchema.disabledValue,
|
|
1567
|
+
["variablesSchema", variableKey, "disabledValue"],
|
|
1568
|
+
ctx,
|
|
1569
|
+
variableKey,
|
|
1570
|
+
schemasByKey,
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1579
1575
|
|
|
1580
1576
|
// variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
|
|
1581
1577
|
if (value.variations) {
|
|
@@ -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
|
|
|
@@ -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 (
|
|
92
|
+
if (isWithinDirectory(projectConfig.attributesDirectoryPath, relativeDir)) {
|
|
87
93
|
type = "attribute";
|
|
88
|
-
} else if (
|
|
94
|
+
} else if (isWithinDirectory(projectConfig.segmentsDirectoryPath, relativeDir)) {
|
|
89
95
|
type = "segment";
|
|
90
|
-
} else if (
|
|
96
|
+
} else if (isWithinDirectory(projectConfig.featuresDirectoryPath, relativeDir)) {
|
|
91
97
|
type = "feature";
|
|
92
|
-
} else if (
|
|
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 (
|
|
105
|
+
} else if (isWithinDirectory(projectConfig.schemasDirectoryPath, relativeDir)) {
|
|
95
106
|
type = "schema";
|
|
96
|
-
} else if (
|
|
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
|
-
|
|
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,
|