@featurevisor/core 2.11.0 → 2.13.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 +1410 -11
- package/coverage/coverage-final.json +21 -2
- 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/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +116 -0
- package/coverage/lcov-report/config/projectConfig.ts.html +676 -0
- package/coverage/lcov-report/datasource/adapter.ts.html +235 -0
- package/coverage/lcov-report/datasource/datasource.ts.html +862 -0
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +1354 -0
- package/coverage/lcov-report/datasource/index.html +161 -0
- package/coverage/lcov-report/datasource/index.ts.html +94 -0
- package/coverage/lcov-report/index.html +80 -20
- package/coverage/lcov-report/linter/attributeSchema.ts.html +175 -0
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +220 -0
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +268 -0
- package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
- package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
- package/coverage/lcov-report/linter/groupSchema.ts.html +226 -0
- package/coverage/lcov-report/linter/index.html +266 -0
- package/coverage/lcov-report/linter/lintProject.ts.html +1840 -0
- package/coverage/lcov-report/linter/printError.ts.html +238 -0
- package/coverage/lcov-report/linter/schema.ts.html +1471 -0
- package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
- package/coverage/lcov-report/linter/testSchema.ts.html +550 -0
- 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 +20 -5
- package/coverage/lcov-report/parsers/index.ts.html +151 -0
- 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 +103 -0
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +20 -5
- package/coverage/lcov-report/utils/git.ts.html +481 -0
- package/coverage/lcov-report/utils/index.html +116 -0
- package/coverage/lcov.info +2798 -7
- package/lib/builder/buildDatafile.js +15 -1
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/config/projectConfig.d.ts +2 -0
- package/lib/config/projectConfig.js +3 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/datasource.d.ts +6 -1
- package/lib/datasource/datasource.js +16 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.js +10 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/generate-code/typescript.js +280 -46
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/conditionSchema.spec.d.ts +1 -0
- package/lib/linter/conditionSchema.spec.js +331 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +129 -20
- package/lib/linter/featureSchema.js +489 -48
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.d.ts +1 -0
- package/lib/linter/featureSchema.spec.js +978 -0
- package/lib/linter/featureSchema.spec.js.map +1 -0
- package/lib/linter/lintProject.d.ts +17 -1
- package/lib/linter/lintProject.js +228 -169
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.d.ts +1 -0
- package/lib/linter/lintProject.spec.js +86 -0
- package/lib/linter/lintProject.spec.js.map +1 -0
- package/lib/linter/printError.d.ts +7 -0
- package/lib/linter/printError.js +30 -15
- package/lib/linter/printError.js.map +1 -1
- package/lib/linter/schema.d.ts +42 -0
- package/lib/linter/schema.js +417 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/schema.spec.d.ts +1 -0
- package/lib/linter/schema.spec.js +483 -0
- package/lib/linter/schema.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.d.ts +1 -0
- package/lib/linter/segmentSchema.spec.js +231 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -0
- package/lib/tester/testFeature.js +5 -3
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/utils/git.js +3 -0
- package/lib/utils/git.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.ts +17 -1
- package/src/config/projectConfig.ts +3 -0
- package/src/datasource/datasource.ts +23 -0
- package/src/datasource/filesystemAdapter.ts +7 -0
- package/src/generate-code/typescript.ts +330 -49
- package/src/linter/conditionSchema.spec.ts +446 -0
- package/src/linter/featureSchema.spec.ts +1218 -0
- package/src/linter/featureSchema.ts +671 -69
- package/src/linter/lintProject.spec.ts +115 -0
- package/src/linter/lintProject.ts +303 -217
- package/src/linter/printError.ts +40 -16
- package/src/linter/schema.spec.ts +617 -0
- package/src/linter/schema.ts +462 -0
- package/src/linter/segmentSchema.spec.ts +273 -0
- package/src/tester/testFeature.ts +5 -3
- package/src/utils/git.ts +2 -0
- package/lib/linter/propertySchema.d.ts +0 -5
- package/lib/linter/propertySchema.js +0 -43
- package/lib/linter/propertySchema.js.map +0 -1
- package/src/linter/propertySchema.ts +0 -47
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
// for use in node only
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
+
import type { Schema } from "@featurevisor/types";
|
|
5
|
+
import type { ZodError } from "zod";
|
|
6
|
+
|
|
4
7
|
import { getAttributeZodSchema } from "./attributeSchema";
|
|
5
8
|
import { getConditionsZodSchema } from "./conditionSchema";
|
|
6
9
|
import { getSegmentZodSchema } from "./segmentSchema";
|
|
7
10
|
import { getGroupZodSchema } from "./groupSchema";
|
|
8
11
|
import { getFeatureZodSchema } from "./featureSchema";
|
|
12
|
+
import { getSchemaZodSchema } from "./schema";
|
|
9
13
|
import { getTestsZodSchema } from "./testSchema";
|
|
10
14
|
|
|
11
15
|
import { checkForCircularDependencyInRequired } from "./checkCircularDependency";
|
|
12
16
|
import { checkForFeatureExceedingGroupSlotPercentage } from "./checkPercentageExceedingSlot";
|
|
13
|
-
import { printZodError } from "./printError";
|
|
17
|
+
import { getLintIssuesFromZodError, printZodError } from "./printError";
|
|
14
18
|
import { Dependencies } from "../dependencies";
|
|
15
19
|
import { CLI_FORMAT_RED, CLI_FORMAT_BOLD_UNDERLINE } from "../tester/cliFormat";
|
|
16
20
|
import { Plugin } from "../cli";
|
|
17
21
|
|
|
22
|
+
export type LintEntityType = "attribute" | "segment" | "feature" | "group" | "schema" | "test";
|
|
23
|
+
|
|
18
24
|
export interface LintProjectOptions {
|
|
19
25
|
keyPattern?: string;
|
|
20
26
|
entityType?: string;
|
|
21
27
|
authors?: boolean;
|
|
28
|
+
json?: boolean;
|
|
29
|
+
pretty?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LintErrorItem {
|
|
33
|
+
filePath: string;
|
|
34
|
+
entityType: LintEntityType;
|
|
35
|
+
key: string;
|
|
36
|
+
message: string;
|
|
37
|
+
path: (string | number)[];
|
|
38
|
+
code?: string;
|
|
39
|
+
value?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LintResult {
|
|
43
|
+
hasError: boolean;
|
|
44
|
+
errors: LintErrorItem[];
|
|
22
45
|
}
|
|
23
46
|
|
|
24
47
|
const ENTITY_NAME_REGEX = /^[a-zA-Z0-9_\-./]+$/;
|
|
@@ -37,12 +60,19 @@ async function getAuthorsOfEntity(datasource, entityType, entityKey): Promise<st
|
|
|
37
60
|
export async function lintProject(
|
|
38
61
|
deps: Dependencies,
|
|
39
62
|
options: LintProjectOptions = {},
|
|
40
|
-
): Promise<
|
|
63
|
+
): Promise<LintResult> {
|
|
41
64
|
const { projectConfig, datasource } = deps;
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
const isJsonMode = options.json === true;
|
|
67
|
+
const errors: LintErrorItem[] = [];
|
|
44
68
|
|
|
45
|
-
function
|
|
69
|
+
function log(...args: unknown[]) {
|
|
70
|
+
if (!isJsonMode) {
|
|
71
|
+
console.log(...args);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getFullPathFromKey(type: LintEntityType, key: string, relative = false) {
|
|
46
76
|
const fileName = `${key}.${datasource.getExtension()}`;
|
|
47
77
|
let fullPath = "";
|
|
48
78
|
|
|
@@ -54,10 +84,10 @@ export async function lintProject(
|
|
|
54
84
|
fullPath = path.join(projectConfig.featuresDirectoryPath, fileName);
|
|
55
85
|
} else if (type === "group") {
|
|
56
86
|
fullPath = path.join(projectConfig.groupsDirectoryPath, fileName);
|
|
57
|
-
} else if (type === "
|
|
58
|
-
fullPath = path.join(projectConfig.
|
|
87
|
+
} else if (type === "schema") {
|
|
88
|
+
fullPath = path.join(projectConfig.schemasDirectoryPath, fileName);
|
|
59
89
|
} else {
|
|
60
|
-
|
|
90
|
+
fullPath = path.join(projectConfig.testsDirectoryPath, fileName);
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
if (relative) {
|
|
@@ -67,12 +97,118 @@ export async function lintProject(
|
|
|
67
97
|
return fullPath;
|
|
68
98
|
}
|
|
69
99
|
|
|
100
|
+
async function printEntityHeader(entityType: LintEntityType, key: string, fullPath: string) {
|
|
101
|
+
if (isJsonMode) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
|
|
106
|
+
|
|
107
|
+
if (options.authors) {
|
|
108
|
+
const authors = await getAuthorsOfEntity(datasource, entityType, key);
|
|
109
|
+
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function recordError(error: LintErrorItem) {
|
|
114
|
+
errors.push(error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function reportSimpleError({
|
|
118
|
+
entityType,
|
|
119
|
+
key,
|
|
120
|
+
fullPath,
|
|
121
|
+
message,
|
|
122
|
+
detail,
|
|
123
|
+
pathParts = [],
|
|
124
|
+
code,
|
|
125
|
+
value,
|
|
126
|
+
}: {
|
|
127
|
+
entityType: LintEntityType;
|
|
128
|
+
key: string;
|
|
129
|
+
fullPath: string;
|
|
130
|
+
message: string;
|
|
131
|
+
detail?: string;
|
|
132
|
+
pathParts?: (string | number)[];
|
|
133
|
+
code?: string;
|
|
134
|
+
value?: unknown;
|
|
135
|
+
}) {
|
|
136
|
+
recordError({
|
|
137
|
+
filePath: path.relative(process.cwd(), fullPath),
|
|
138
|
+
entityType,
|
|
139
|
+
key,
|
|
140
|
+
message,
|
|
141
|
+
path: pathParts,
|
|
142
|
+
code,
|
|
143
|
+
value,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!isJsonMode) {
|
|
147
|
+
await printEntityHeader(entityType, key, fullPath);
|
|
148
|
+
console.log(CLI_FORMAT_RED, ` => Error: ${message}`);
|
|
149
|
+
if (detail) {
|
|
150
|
+
console.log(CLI_FORMAT_RED, ` ${detail}`);
|
|
151
|
+
}
|
|
152
|
+
console.log("");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function reportThrownError(
|
|
157
|
+
entityType: LintEntityType,
|
|
158
|
+
key: string,
|
|
159
|
+
fullPath: string,
|
|
160
|
+
error: unknown,
|
|
161
|
+
) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
|
|
164
|
+
recordError({
|
|
165
|
+
filePath: path.relative(process.cwd(), fullPath),
|
|
166
|
+
entityType,
|
|
167
|
+
key,
|
|
168
|
+
message,
|
|
169
|
+
path: [],
|
|
170
|
+
code: error instanceof Error ? error.name : undefined,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!isJsonMode) {
|
|
174
|
+
await printEntityHeader(entityType, key, fullPath);
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log(error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function reportZodValidationError(
|
|
181
|
+
entityType: LintEntityType,
|
|
182
|
+
key: string,
|
|
183
|
+
fullPath: string,
|
|
184
|
+
error: ZodError,
|
|
185
|
+
) {
|
|
186
|
+
const issues = getLintIssuesFromZodError(error);
|
|
187
|
+
|
|
188
|
+
issues.forEach((issue) => {
|
|
189
|
+
recordError({
|
|
190
|
+
filePath: path.relative(process.cwd(), fullPath),
|
|
191
|
+
entityType,
|
|
192
|
+
key,
|
|
193
|
+
message: issue.message,
|
|
194
|
+
path: issue.path,
|
|
195
|
+
code: issue.code,
|
|
196
|
+
value: issue.value,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!isJsonMode) {
|
|
201
|
+
await printEntityHeader(entityType, key, fullPath);
|
|
202
|
+
printZodError(error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
70
206
|
const keyPattern = options.keyPattern ? new RegExp(options.keyPattern) : null;
|
|
71
207
|
|
|
72
208
|
if (keyPattern) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
209
|
+
log("");
|
|
210
|
+
log(`Linting only keys matching pattern: ${keyPattern}`);
|
|
211
|
+
log("");
|
|
76
212
|
}
|
|
77
213
|
|
|
78
214
|
// lint attributes
|
|
@@ -85,57 +221,32 @@ export async function lintProject(
|
|
|
85
221
|
: attributes.filter((key) => keyPattern.test(key));
|
|
86
222
|
|
|
87
223
|
if (filteredKeys.length > 0) {
|
|
88
|
-
|
|
224
|
+
log(`Linting ${filteredKeys.length} attributes...\n`);
|
|
89
225
|
}
|
|
90
226
|
|
|
91
227
|
for (const key of filteredKeys) {
|
|
92
228
|
const fullPath = getFullPathFromKey("attribute", key);
|
|
93
229
|
|
|
94
230
|
if (!ATTRIBUTE_NAME_REGEX.test(key)) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(CLI_FORMAT_RED, ` ${ATTRIBUTE_NAME_REGEX_ERROR}`);
|
|
104
|
-
console.log("");
|
|
105
|
-
hasError = true;
|
|
231
|
+
await reportSimpleError({
|
|
232
|
+
entityType: "attribute",
|
|
233
|
+
key,
|
|
234
|
+
fullPath,
|
|
235
|
+
message: `Invalid name: "${key}"`,
|
|
236
|
+
detail: ATTRIBUTE_NAME_REGEX_ERROR,
|
|
237
|
+
code: "invalid_name",
|
|
238
|
+
});
|
|
106
239
|
}
|
|
107
240
|
|
|
108
241
|
try {
|
|
109
242
|
const parsed = await datasource.readAttribute(key);
|
|
110
|
-
|
|
111
243
|
const result = attributeZodSchema.safeParse(parsed);
|
|
112
244
|
|
|
113
|
-
if (!result.success) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (options.authors) {
|
|
117
|
-
const authors = await getAuthorsOfEntity(datasource, "attribute", key);
|
|
118
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if ("error" in result) {
|
|
122
|
-
printZodError(result.error);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
hasError = true;
|
|
245
|
+
if (!result.success && "error" in result) {
|
|
246
|
+
await reportZodValidationError("attribute", key, fullPath, result.error);
|
|
126
247
|
}
|
|
127
|
-
} catch (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (options.authors) {
|
|
131
|
-
const authors = await getAuthorsOfEntity(datasource, "attribute", key);
|
|
132
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
console.log("");
|
|
136
|
-
console.log(e);
|
|
137
|
-
|
|
138
|
-
hasError = true;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await reportThrownError("attribute", key, fullPath, error);
|
|
139
250
|
}
|
|
140
251
|
}
|
|
141
252
|
}
|
|
@@ -154,61 +265,47 @@ export async function lintProject(
|
|
|
154
265
|
const filteredKeys = !keyPattern ? segments : segments.filter((key) => keyPattern.test(key));
|
|
155
266
|
|
|
156
267
|
if (filteredKeys.length > 0) {
|
|
157
|
-
|
|
268
|
+
log(`Linting ${filteredKeys.length} segments...\n`);
|
|
158
269
|
}
|
|
159
270
|
|
|
160
271
|
for (const key of filteredKeys) {
|
|
161
272
|
const fullPath = getFullPathFromKey("segment", key);
|
|
162
273
|
|
|
163
274
|
if (!ENTITY_NAME_REGEX.test(key)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.log(CLI_FORMAT_RED, ` ${ENTITY_NAME_REGEX_ERROR}`);
|
|
173
|
-
console.log("");
|
|
174
|
-
hasError = true;
|
|
275
|
+
await reportSimpleError({
|
|
276
|
+
entityType: "segment",
|
|
277
|
+
key,
|
|
278
|
+
fullPath,
|
|
279
|
+
message: `Invalid name: "${key}"`,
|
|
280
|
+
detail: ENTITY_NAME_REGEX_ERROR,
|
|
281
|
+
code: "invalid_name",
|
|
282
|
+
});
|
|
175
283
|
}
|
|
176
284
|
|
|
177
285
|
try {
|
|
178
286
|
const parsed = await datasource.readSegment(key);
|
|
179
|
-
|
|
180
287
|
const result = segmentZodSchema.safeParse(parsed);
|
|
181
288
|
|
|
182
|
-
if (!result.success) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (options.authors) {
|
|
186
|
-
const authors = await getAuthorsOfEntity(datasource, "segment", key);
|
|
187
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if ("error" in result) {
|
|
191
|
-
printZodError(result.error);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
hasError = true;
|
|
289
|
+
if (!result.success && "error" in result) {
|
|
290
|
+
await reportZodValidationError("segment", key, fullPath, result.error);
|
|
195
291
|
}
|
|
196
|
-
} catch (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (options.authors) {
|
|
200
|
-
const authors = await getAuthorsOfEntity(datasource, "segment", key);
|
|
201
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
console.log("");
|
|
205
|
-
console.log(e);
|
|
206
|
-
|
|
207
|
-
hasError = true;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
await reportThrownError("segment", key, fullPath, error);
|
|
208
294
|
}
|
|
209
295
|
}
|
|
210
296
|
}
|
|
211
297
|
|
|
298
|
+
// List schemas and load parsed schemas for feature variable validation (schema references)
|
|
299
|
+
const schemas = await datasource.listSchemas();
|
|
300
|
+
const schemasByKey: Record<string, Schema> = {};
|
|
301
|
+
for (const key of schemas) {
|
|
302
|
+
try {
|
|
303
|
+
schemasByKey[key] = await datasource.readSchema(key);
|
|
304
|
+
} catch {
|
|
305
|
+
// Schema file may be invalid; skip for feature variable resolution
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
212
309
|
// lint features
|
|
213
310
|
const features = await datasource.listFeatures();
|
|
214
311
|
const featureZodSchema = getFeatureZodSchema(
|
|
@@ -217,30 +314,29 @@ export async function lintProject(
|
|
|
217
314
|
flattenedAttributes as [string, ...string[]],
|
|
218
315
|
segments as [string, ...string[]],
|
|
219
316
|
features as [string, ...string[]],
|
|
317
|
+
schemas,
|
|
318
|
+
schemasByKey,
|
|
220
319
|
);
|
|
221
320
|
|
|
222
321
|
if (!options.entityType || options.entityType === "feature") {
|
|
223
322
|
const filteredKeys = !keyPattern ? features : features.filter((key) => keyPattern.test(key));
|
|
224
323
|
|
|
225
324
|
if (filteredKeys.length > 0) {
|
|
226
|
-
|
|
325
|
+
log(`Linting ${filteredKeys.length} features...\n`);
|
|
227
326
|
}
|
|
228
327
|
|
|
229
328
|
for (const key of filteredKeys) {
|
|
230
329
|
const fullPath = getFullPathFromKey("feature", key);
|
|
231
330
|
|
|
232
331
|
if (!ENTITY_NAME_REGEX.test(key)) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(CLI_FORMAT_RED, ` ${ENTITY_NAME_REGEX_ERROR}`);
|
|
242
|
-
console.log("");
|
|
243
|
-
hasError = true;
|
|
332
|
+
await reportSimpleError({
|
|
333
|
+
entityType: "feature",
|
|
334
|
+
key,
|
|
335
|
+
fullPath,
|
|
336
|
+
message: `Invalid name: "${key}"`,
|
|
337
|
+
detail: ENTITY_NAME_REGEX_ERROR,
|
|
338
|
+
code: "invalid_name",
|
|
339
|
+
});
|
|
244
340
|
}
|
|
245
341
|
|
|
246
342
|
let parsed;
|
|
@@ -250,48 +346,24 @@ export async function lintProject(
|
|
|
250
346
|
|
|
251
347
|
const result = featureZodSchema.safeParse(parsed);
|
|
252
348
|
|
|
253
|
-
if (!result.success) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (options.authors) {
|
|
257
|
-
const authors = await getAuthorsOfEntity(datasource, "feature", key);
|
|
258
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if ("error" in result) {
|
|
262
|
-
printZodError(result.error);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
hasError = true;
|
|
266
|
-
}
|
|
267
|
-
} catch (e) {
|
|
268
|
-
console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
|
|
269
|
-
|
|
270
|
-
if (options.authors) {
|
|
271
|
-
const authors = await getAuthorsOfEntity(datasource, "feature", key);
|
|
272
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
349
|
+
if (!result.success && "error" in result) {
|
|
350
|
+
await reportZodValidationError("feature", key, fullPath, result.error);
|
|
273
351
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.log(e);
|
|
277
|
-
|
|
278
|
-
hasError = true;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
await reportThrownError("feature", key, fullPath, error);
|
|
279
354
|
}
|
|
280
355
|
|
|
281
356
|
if (parsed && parsed.required) {
|
|
282
357
|
try {
|
|
283
358
|
await checkForCircularDependencyInRequired(datasource, key, parsed.required);
|
|
284
|
-
} catch (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.log(CLI_FORMAT_RED, ` => Error: ${e.message}`);
|
|
293
|
-
|
|
294
|
-
hasError = true;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
await reportSimpleError({
|
|
361
|
+
entityType: "feature",
|
|
362
|
+
key,
|
|
363
|
+
fullPath,
|
|
364
|
+
message: error instanceof Error ? error.message : String(error),
|
|
365
|
+
code: error instanceof Error ? error.name : "error",
|
|
366
|
+
});
|
|
295
367
|
}
|
|
296
368
|
}
|
|
297
369
|
}
|
|
@@ -305,24 +377,21 @@ export async function lintProject(
|
|
|
305
377
|
const filteredKeys = !keyPattern ? groups : groups.filter((key) => keyPattern.test(key));
|
|
306
378
|
|
|
307
379
|
if (filteredKeys.length > 0) {
|
|
308
|
-
|
|
380
|
+
log(`Linting ${filteredKeys.length} groups...\n`);
|
|
309
381
|
}
|
|
310
382
|
|
|
311
383
|
for (const key of filteredKeys) {
|
|
312
384
|
const fullPath = getFullPathFromKey("group", key);
|
|
313
385
|
|
|
314
386
|
if (!ENTITY_NAME_REGEX.test(key)) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
console.log(CLI_FORMAT_RED, ` ${ENTITY_NAME_REGEX_ERROR}`);
|
|
324
|
-
console.log("");
|
|
325
|
-
hasError = true;
|
|
387
|
+
await reportSimpleError({
|
|
388
|
+
entityType: "group",
|
|
389
|
+
key,
|
|
390
|
+
fullPath,
|
|
391
|
+
message: `Invalid name: "${key}"`,
|
|
392
|
+
detail: ENTITY_NAME_REGEX_ERROR,
|
|
393
|
+
code: "invalid_name",
|
|
394
|
+
});
|
|
326
395
|
}
|
|
327
396
|
|
|
328
397
|
let parsed;
|
|
@@ -332,41 +401,24 @@ export async function lintProject(
|
|
|
332
401
|
|
|
333
402
|
const result = groupZodSchema.safeParse(parsed);
|
|
334
403
|
|
|
335
|
-
if (!result.success) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (options.authors) {
|
|
339
|
-
const authors = await getAuthorsOfEntity(datasource, "group", key);
|
|
340
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if ("error" in result) {
|
|
344
|
-
printZodError(result.error);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
hasError = true;
|
|
348
|
-
}
|
|
349
|
-
} catch (e) {
|
|
350
|
-
console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
|
|
351
|
-
|
|
352
|
-
if (options.authors) {
|
|
353
|
-
const authors = await getAuthorsOfEntity(datasource, "group", key);
|
|
354
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
404
|
+
if (!result.success && "error" in result) {
|
|
405
|
+
await reportZodValidationError("group", key, fullPath, result.error);
|
|
355
406
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
console.log(e);
|
|
359
|
-
|
|
360
|
-
hasError = true;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
await reportThrownError("group", key, fullPath, error);
|
|
361
409
|
}
|
|
362
410
|
|
|
363
411
|
if (parsed) {
|
|
364
412
|
try {
|
|
365
413
|
await checkForFeatureExceedingGroupSlotPercentage(datasource, parsed, features);
|
|
366
|
-
} catch (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
414
|
+
} catch (error) {
|
|
415
|
+
await reportSimpleError({
|
|
416
|
+
entityType: "group",
|
|
417
|
+
key,
|
|
418
|
+
fullPath,
|
|
419
|
+
message: error instanceof Error ? error.message : String(error),
|
|
420
|
+
code: error instanceof Error ? error.name : "error",
|
|
421
|
+
});
|
|
370
422
|
}
|
|
371
423
|
}
|
|
372
424
|
}
|
|
@@ -374,6 +426,44 @@ export async function lintProject(
|
|
|
374
426
|
|
|
375
427
|
// @TODO: feature cannot exist in multiple groups
|
|
376
428
|
|
|
429
|
+
// lint schemas (schemas and schemasByKey already loaded above for feature linting)
|
|
430
|
+
const schemaZodSchema = getSchemaZodSchema(schemas);
|
|
431
|
+
|
|
432
|
+
if (!options.entityType || options.entityType === "schema") {
|
|
433
|
+
const filteredKeys = !keyPattern ? schemas : schemas.filter((key) => keyPattern.test(key));
|
|
434
|
+
|
|
435
|
+
if (filteredKeys.length > 0) {
|
|
436
|
+
log(`Linting ${filteredKeys.length} schemas...\n`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const key of filteredKeys) {
|
|
440
|
+
const fullPath = getFullPathFromKey("schema", key);
|
|
441
|
+
|
|
442
|
+
if (!ENTITY_NAME_REGEX.test(key)) {
|
|
443
|
+
await reportSimpleError({
|
|
444
|
+
entityType: "schema",
|
|
445
|
+
key,
|
|
446
|
+
fullPath,
|
|
447
|
+
message: `Invalid name: "${key}"`,
|
|
448
|
+
detail: ENTITY_NAME_REGEX_ERROR,
|
|
449
|
+
code: "invalid_name",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const parsed = await datasource.readSchema(key);
|
|
455
|
+
|
|
456
|
+
const result = schemaZodSchema.safeParse(parsed);
|
|
457
|
+
|
|
458
|
+
if (!result.success && "error" in result) {
|
|
459
|
+
await reportZodValidationError("schema", key, fullPath, result.error);
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
await reportThrownError("schema", key, fullPath, error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
377
467
|
// lint tests
|
|
378
468
|
const tests = await datasource.listTests();
|
|
379
469
|
|
|
@@ -387,24 +477,21 @@ export async function lintProject(
|
|
|
387
477
|
const filteredKeys = !keyPattern ? tests : tests.filter((key) => keyPattern.test(key));
|
|
388
478
|
|
|
389
479
|
if (filteredKeys.length > 0) {
|
|
390
|
-
|
|
480
|
+
log(`Linting ${filteredKeys.length} tests...\n`);
|
|
391
481
|
}
|
|
392
482
|
|
|
393
483
|
for (const key of filteredKeys) {
|
|
394
484
|
const fullPath = getFullPathFromKey("test", key);
|
|
395
485
|
|
|
396
486
|
if (!ENTITY_NAME_REGEX.test(key)) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log(CLI_FORMAT_RED, ` ${ENTITY_NAME_REGEX_ERROR}`);
|
|
406
|
-
console.log("");
|
|
407
|
-
hasError = true;
|
|
487
|
+
await reportSimpleError({
|
|
488
|
+
entityType: "test",
|
|
489
|
+
key,
|
|
490
|
+
fullPath,
|
|
491
|
+
message: `Invalid name: "${key}"`,
|
|
492
|
+
detail: ENTITY_NAME_REGEX_ERROR,
|
|
493
|
+
code: "invalid_name",
|
|
494
|
+
});
|
|
408
495
|
}
|
|
409
496
|
|
|
410
497
|
try {
|
|
@@ -412,39 +499,19 @@ export async function lintProject(
|
|
|
412
499
|
|
|
413
500
|
const result = testsZodSchema.safeParse(parsed);
|
|
414
501
|
|
|
415
|
-
if (!result.success) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (options.authors) {
|
|
419
|
-
const authors = await getAuthorsOfEntity(datasource, "test", key);
|
|
420
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if ("error" in result) {
|
|
424
|
-
printZodError(result.error);
|
|
425
|
-
|
|
426
|
-
process.exit(1);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
hasError = true;
|
|
430
|
-
}
|
|
431
|
-
} catch (e) {
|
|
432
|
-
console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
|
|
433
|
-
|
|
434
|
-
if (options.authors) {
|
|
435
|
-
const authors = await getAuthorsOfEntity(datasource, "test", key);
|
|
436
|
-
console.log(` Authors: ${authors.join(", ")}\n`);
|
|
502
|
+
if (!result.success && "error" in result) {
|
|
503
|
+
await reportZodValidationError("test", key, fullPath, result.error);
|
|
437
504
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(e);
|
|
441
|
-
|
|
442
|
-
hasError = true;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
await reportThrownError("test", key, fullPath, error);
|
|
443
507
|
}
|
|
444
508
|
}
|
|
445
509
|
}
|
|
446
510
|
|
|
447
|
-
return
|
|
511
|
+
return {
|
|
512
|
+
hasError: errors.length > 0,
|
|
513
|
+
errors,
|
|
514
|
+
};
|
|
448
515
|
}
|
|
449
516
|
|
|
450
517
|
export const lintPlugin: Plugin = {
|
|
@@ -452,7 +519,7 @@ export const lintPlugin: Plugin = {
|
|
|
452
519
|
handler: async function (options) {
|
|
453
520
|
const { rootDirectoryPath, projectConfig, datasource, parsed } = options;
|
|
454
521
|
|
|
455
|
-
const
|
|
522
|
+
const result = await lintProject(
|
|
456
523
|
{
|
|
457
524
|
rootDirectoryPath,
|
|
458
525
|
projectConfig,
|
|
@@ -463,10 +530,17 @@ export const lintPlugin: Plugin = {
|
|
|
463
530
|
keyPattern: parsed.keyPattern,
|
|
464
531
|
entityType: parsed.entityType,
|
|
465
532
|
authors: parsed.authors,
|
|
533
|
+
json: parsed.json,
|
|
534
|
+
pretty: parsed.pretty,
|
|
466
535
|
},
|
|
467
536
|
);
|
|
468
537
|
|
|
469
|
-
if (
|
|
538
|
+
if (parsed.json) {
|
|
539
|
+
const payload = { errors: result.errors };
|
|
540
|
+
console.log(parsed.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (result.hasError) {
|
|
470
544
|
return false;
|
|
471
545
|
}
|
|
472
546
|
},
|
|
@@ -487,6 +561,10 @@ export const lintPlugin: Plugin = {
|
|
|
487
561
|
command: "lint --entityType=group",
|
|
488
562
|
description: "lint only groups",
|
|
489
563
|
},
|
|
564
|
+
{
|
|
565
|
+
command: "lint --entityType=schema",
|
|
566
|
+
description: "lint only schemas",
|
|
567
|
+
},
|
|
490
568
|
{
|
|
491
569
|
command: "lint --entityType=test",
|
|
492
570
|
description: "lint only tests",
|
|
@@ -495,5 +573,13 @@ export const lintPlugin: Plugin = {
|
|
|
495
573
|
command: 'lint --keyPattern="abc"',
|
|
496
574
|
description: `lint only entities with keys containing "abc"`,
|
|
497
575
|
},
|
|
576
|
+
{
|
|
577
|
+
command: "lint --json",
|
|
578
|
+
description: "print lint errors as JSON",
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
command: "lint --json --pretty",
|
|
582
|
+
description: "print lint errors as pretty JSON",
|
|
583
|
+
},
|
|
498
584
|
],
|
|
499
585
|
};
|