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