@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +1410 -11
  3. package/coverage/coverage-final.json +21 -2
  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 +80 -20
  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 +775 -0
  23. package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
  24. package/coverage/lcov-report/linter/groupSchema.ts.html +226 -0
  25. package/coverage/lcov-report/linter/index.html +266 -0
  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 +1471 -0
  29. package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
  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 +2798 -7
  43. package/lib/builder/buildDatafile.js +15 -1
  44. package/lib/builder/buildDatafile.js.map +1 -1
  45. package/lib/config/projectConfig.d.ts +2 -0
  46. package/lib/config/projectConfig.js +3 -1
  47. package/lib/config/projectConfig.js.map +1 -1
  48. package/lib/datasource/datasource.d.ts +6 -1
  49. package/lib/datasource/datasource.js +16 -0
  50. package/lib/datasource/datasource.js.map +1 -1
  51. package/lib/datasource/filesystemAdapter.js +10 -0
  52. package/lib/datasource/filesystemAdapter.js.map +1 -1
  53. package/lib/generate-code/typescript.js +280 -46
  54. package/lib/generate-code/typescript.js.map +1 -1
  55. package/lib/linter/conditionSchema.spec.d.ts +1 -0
  56. package/lib/linter/conditionSchema.spec.js +331 -0
  57. package/lib/linter/conditionSchema.spec.js.map +1 -0
  58. package/lib/linter/featureSchema.d.ts +129 -20
  59. package/lib/linter/featureSchema.js +489 -48
  60. package/lib/linter/featureSchema.js.map +1 -1
  61. package/lib/linter/featureSchema.spec.d.ts +1 -0
  62. package/lib/linter/featureSchema.spec.js +978 -0
  63. package/lib/linter/featureSchema.spec.js.map +1 -0
  64. package/lib/linter/lintProject.d.ts +17 -1
  65. package/lib/linter/lintProject.js +228 -169
  66. package/lib/linter/lintProject.js.map +1 -1
  67. package/lib/linter/lintProject.spec.d.ts +1 -0
  68. package/lib/linter/lintProject.spec.js +86 -0
  69. package/lib/linter/lintProject.spec.js.map +1 -0
  70. package/lib/linter/printError.d.ts +7 -0
  71. package/lib/linter/printError.js +30 -15
  72. package/lib/linter/printError.js.map +1 -1
  73. package/lib/linter/schema.d.ts +42 -0
  74. package/lib/linter/schema.js +417 -0
  75. package/lib/linter/schema.js.map +1 -0
  76. package/lib/linter/schema.spec.d.ts +1 -0
  77. package/lib/linter/schema.spec.js +483 -0
  78. package/lib/linter/schema.spec.js.map +1 -0
  79. package/lib/linter/segmentSchema.spec.d.ts +1 -0
  80. package/lib/linter/segmentSchema.spec.js +231 -0
  81. package/lib/linter/segmentSchema.spec.js.map +1 -0
  82. package/lib/tester/testFeature.js +5 -3
  83. package/lib/tester/testFeature.js.map +1 -1
  84. package/lib/utils/git.js +3 -0
  85. package/lib/utils/git.js.map +1 -1
  86. package/package.json +5 -5
  87. package/src/builder/buildDatafile.ts +17 -1
  88. package/src/config/projectConfig.ts +3 -0
  89. package/src/datasource/datasource.ts +23 -0
  90. package/src/datasource/filesystemAdapter.ts +7 -0
  91. package/src/generate-code/typescript.ts +330 -49
  92. package/src/linter/conditionSchema.spec.ts +446 -0
  93. package/src/linter/featureSchema.spec.ts +1218 -0
  94. package/src/linter/featureSchema.ts +671 -69
  95. package/src/linter/lintProject.spec.ts +115 -0
  96. package/src/linter/lintProject.ts +303 -217
  97. package/src/linter/printError.ts +40 -16
  98. package/src/linter/schema.spec.ts +617 -0
  99. package/src/linter/schema.ts +462 -0
  100. package/src/linter/segmentSchema.spec.ts +273 -0
  101. package/src/tester/testFeature.ts +5 -3
  102. package/src/utils/git.ts +2 -0
  103. package/lib/linter/propertySchema.d.ts +0 -5
  104. package/lib/linter/propertySchema.js +0 -43
  105. package/lib/linter/propertySchema.js.map +0 -1
  106. 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<boolean> {
63
+ ): Promise<LintResult> {
41
64
  const { projectConfig, datasource } = deps;
42
65
 
43
- let hasError = false;
66
+ const isJsonMode = options.json === true;
67
+ const errors: LintErrorItem[] = [];
44
68
 
45
- 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) {
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 === "test") {
58
- fullPath = path.join(projectConfig.testsDirectoryPath, fileName);
87
+ } else if (type === "schema") {
88
+ fullPath = path.join(projectConfig.schemasDirectoryPath, fileName);
59
89
  } else {
60
- throw new Error(`Unknown type: ${type}`);
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
- console.log("");
74
- console.log(`Linting only keys matching pattern: ${keyPattern}`);
75
- console.log("");
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
- console.log(`Linting ${filteredKeys.length} attributes...\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
96
-
97
- if (options.authors) {
98
- const authors = await getAuthorsOfEntity(datasource, "attribute", key);
99
- console.log(` Authors: ${authors.join(", ")}\n`);
100
- }
101
-
102
- console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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 (e) {
128
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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
- console.log(`Linting ${filteredKeys.length} segments...\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
165
-
166
- if (options.authors) {
167
- const authors = await getAuthorsOfEntity(datasource, "segment", key);
168
- console.log(` Authors: ${authors.join(", ")}\n`);
169
- }
170
-
171
- console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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 (e) {
197
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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
- console.log(`Linting ${filteredKeys.length} features...\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
234
-
235
- if (options.authors) {
236
- const authors = await getAuthorsOfEntity(datasource, "feature", key);
237
- console.log(` Authors: ${authors.join(", ")}\n`);
238
- }
239
-
240
- console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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
- console.log("");
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 (e) {
285
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
286
-
287
- if (options.authors) {
288
- const authors = await getAuthorsOfEntity(datasource, "feature", key);
289
- console.log(` Authors: ${authors.join(", ")}\n`);
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
- console.log(`Linting ${filteredKeys.length} groups...\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
316
- console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
317
-
318
- if (options.authors) {
319
- const authors = await getAuthorsOfEntity(datasource, "group", key);
320
- console.log(` Authors: ${authors.join(", ")}\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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
- console.log("");
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 (e) {
367
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
368
- console.log(CLI_FORMAT_RED, ` => Error: ${e.message}`);
369
- 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
+ });
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
- console.log(`Linting ${filteredKeys.length} tests...\n`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
398
-
399
- if (options.authors) {
400
- const authors = await getAuthorsOfEntity(datasource, "test", key);
401
- console.log(` Authors: ${authors.join(", ")}\n`);
402
- }
403
-
404
- console.log(CLI_FORMAT_RED, ` => Error: Invalid name: "${key}"`);
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
- console.log(CLI_FORMAT_BOLD_UNDERLINE, fullPath);
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
- console.log("");
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 hasError;
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 hasError = await lintProject(
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 (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) {
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
  };