Skip to content

Commit 5efe94a

Browse files
committed
feat(core): enhance config validation
1 parent 98ad90e commit 5efe94a

File tree

11 files changed

+91
-91
lines changed

11 files changed

+91
-91
lines changed

packages/core/src/lib/implementation/read-rc-file.ts

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,17 @@
1-
import { bold, red } from 'ansis';
1+
import { bold } from 'ansis';
22
import path, { join } from 'node:path';
3-
import {
4-
type MessageBuilder,
5-
fromError,
6-
isZodErrorLike,
7-
} from 'zod-validation-error';
3+
import { fromError, isZodErrorLike } from 'zod-validation-error';
84
import {
95
CONFIG_FILE_NAME,
106
type CoreConfig,
117
SUPPORTED_CONFIG_FILE_FORMATS,
128
coreConfigSchema,
139
} from '@code-pushup/models';
14-
import { fileExists, importModule } from '@code-pushup/utils';
15-
16-
function formatErrorPath(errorPath: (string | number)[]): string {
17-
return errorPath
18-
.map((key, index) => {
19-
if (typeof key === 'number') {
20-
return `[${key}]`;
21-
}
22-
return index > 0 ? `.${key}` : key;
23-
})
24-
.join('');
25-
}
26-
27-
const coreConfigMessageBuilder: MessageBuilder = issues =>
28-
issues
29-
.map(issue => {
30-
const formattedMessage = red(`${bold(issue.code)}: ${issue.message}`);
31-
const formattedPath = formatErrorPath(issue.path);
32-
if (formattedPath) {
33-
return `Validation error at ${bold(formattedPath)}\n${formattedMessage}\n`;
34-
}
35-
return `${formattedMessage}\n`;
36-
})
37-
.join('\n');
10+
import {
11+
coreConfigMessageBuilder,
12+
fileExists,
13+
importModule,
14+
} from '@code-pushup/utils';
3815

3916
export class ConfigPathError extends Error {
4017
constructor(configPath: string) {

packages/models/src/lib/category-config.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,21 @@ export const categoryRefSchema = weightedRefSchema(
2323
);
2424
export type CategoryRef = z.infer<typeof categoryRefSchema>;
2525

26-
export const categoryConfigSchema = z
27-
.intersection(
28-
scorableSchema(
29-
'Category with a score calculated from audits and groups from various plugins',
30-
categoryRefSchema,
31-
getDuplicateRefsInCategoryMetrics,
32-
duplicateRefsInCategoryMetricsErrorMsg,
33-
),
26+
export const categoryConfigSchema = scorableSchema(
27+
'Category with a score calculated from audits and groups from various plugins',
28+
categoryRefSchema,
29+
getDuplicateRefsInCategoryMetrics,
30+
duplicateRefsInCategoryMetricsErrorMsg,
31+
)
32+
.merge(
3433
metaSchema({
3534
titleDescription: 'Category Title',
3635
docsUrlDescription: 'Category docs URL',
3736
descriptionDescription: 'Category description',
3837
description: 'Meta info for category',
3938
}),
4039
)
41-
.and(
40+
.merge(
4241
z.object({
4342
isBinary: z
4443
.boolean({

packages/models/src/lib/category-config.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe('categoryConfigSchema', () => {
129129
title: 'This category is empty for now',
130130
refs: [],
131131
} satisfies CategoryConfig),
132-
).toThrow('In category in-progress, there has to be at least one ref');
132+
).toThrow('In a category, there has to be at least one ref');
133133
});
134134

135135
it('should throw for duplicate category references', () => {
@@ -176,7 +176,7 @@ describe('categoryConfigSchema', () => {
176176
],
177177
} satisfies CategoryConfig),
178178
).toThrow(
179-
'In category informational, there has to be at least one ref with weight > 0. Affected refs: functional/immutable-data, lighthouse-experimental',
179+
/In a category, there has to be at least one ref with weight > 0. Affected refs: \\"functional\/immutable-data\\", \\"lighthouse-experimental\\"/,
180180
);
181181
});
182182
});

packages/models/src/lib/group.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,14 @@ export const groupMetaSchema = metaSchema({
2525
});
2626
export type GroupMeta = z.infer<typeof groupMetaSchema>;
2727

28-
export const groupSchema = z.intersection(
29-
scorableSchema(
30-
'A group aggregates a set of audits into a single score which can be referenced from a category. ' +
31-
'E.g. the group slug "performance" groups audits and can be referenced in a category',
32-
groupRefSchema,
33-
getDuplicateRefsInGroups,
34-
duplicateRefsInGroupsErrorMsg,
35-
),
36-
groupMetaSchema,
37-
);
28+
export const groupSchema = scorableSchema(
29+
'A group aggregates a set of audits into a single score which can be referenced from a category. ' +
30+
'E.g. the group slug "performance" groups audits and can be referenced in a category',
31+
groupRefSchema,
32+
getDuplicateRefsInGroups,
33+
duplicateRefsInGroupsErrorMsg,
34+
).merge(groupMetaSchema);
35+
3836
export type Group = z.infer<typeof groupSchema>;
3937

4038
export const groupsSchema = z

packages/models/src/lib/group.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('groupSchema', () => {
6969
title: 'Empty group',
7070
refs: [],
7171
} satisfies Group),
72-
).toThrow('In category empty-group, there has to be at least one ref');
72+
).toThrow('In a category, there has to be at least one ref');
7373
});
7474

7575
it('should throw for duplicate group references', () => {

packages/models/src/lib/implementation/schemas.ts

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -167,43 +167,28 @@ export function scorableSchema<T extends ReturnType<typeof weightedRefSchema>>(
167167
duplicateCheckFn: (metrics: z.infer<T>[]) => false | string[],
168168
duplicateMessageFn: (metrics: z.infer<T>[]) => string,
169169
) {
170-
return (
171-
z
172-
.object(
173-
{
174-
slug: slugSchema.describe(
175-
'Human-readable unique ID, e.g. "performance"',
176-
),
177-
refs: z
178-
.array(refSchema)
179-
.min(1)
180-
// refs are unique
181-
.refine(
182-
refs => !duplicateCheckFn(refs),
183-
refs => ({
184-
message: duplicateMessageFn(refs),
185-
}),
186-
),
187-
},
188-
{ description },
189-
)
190-
// category weights are correct
191-
.superRefine(({ slug, refs }, ctx) => {
192-
if (refs.length === 0) {
193-
ctx.addIssue({
194-
code: z.ZodIssueCode.custom,
195-
message: `In category ${slug}, there has to be at least one ref`,
196-
path: ['refs'],
197-
});
198-
} else if (!hasNonZeroWeightedRef(refs)) {
199-
const affectedRefs = refs.map(ref => ref.slug).join(', ');
200-
ctx.addIssue({
201-
code: z.ZodIssueCode.custom,
202-
message: `In category ${slug}, there has to be at least one ref with weight > 0. Affected refs: ${affectedRefs}`,
203-
path: ['refs'],
204-
});
205-
}
206-
})
170+
return z.object(
171+
{
172+
slug: slugSchema.describe('Human-readable unique ID, e.g. "performance"'),
173+
refs: z
174+
.array(refSchema)
175+
.min(1, { message: 'In a category, there has to be at least one ref' })
176+
// refs are unique
177+
.refine(
178+
refs => !duplicateCheckFn(refs),
179+
refs => ({
180+
message: duplicateMessageFn(refs),
181+
}),
182+
)
183+
// category weights are correct
184+
.refine(hasNonZeroWeightedRef, refs => {
185+
const affectedRefs = refs.map(ref => `"${ref.slug}"`).join(', ');
186+
return {
187+
message: `In a category, there has to be at least one ref with weight > 0. Affected refs: ${affectedRefs}`,
188+
};
189+
}),
190+
},
191+
{ description },
207192
);
208193
}
209194

packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('lighthousePlugin-config-object', () => {
5757
});
5858

5959
expect(() => pluginConfigSchema.parse(pluginConfig)).toThrow(
60-
'In category best-practices, there has to be at least one ref with weight > 0. Affected refs: csp-xss',
60+
/In a category, there has to be at least one ref with weight > 0. Affected refs: \\"csp-xss\\"/,
6161
);
6262
});
6363
});

packages/utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"esbuild": "^0.19.2",
3434
"multi-progress-bars": "^5.0.3",
3535
"semver": "^7.6.0",
36-
"simple-git": "^3.20.0"
36+
"simple-git": "^3.20.0",
37+
"zod-validation-error": "^3.4.0"
3738
}
3839
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,4 @@ export type {
122122
WithRequired,
123123
} from './lib/types.js';
124124
export { verboseUtils } from './lib/verbose-utils.js';
125+
export { coreConfigMessageBuilder } from './lib/zod-validation.js';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { bold, red } from 'ansis';
2+
import type { MessageBuilder } from 'zod-validation-error';
3+
4+
export function formatErrorPath(errorPath: (string | number)[]): string {
5+
return errorPath
6+
.map((key, index) => {
7+
if (typeof key === 'number') {
8+
return `[${key}]`;
9+
}
10+
return index > 0 ? `.${key}` : key;
11+
})
12+
.join('');
13+
}
14+
15+
export const coreConfigMessageBuilder: MessageBuilder = issues =>
16+
issues
17+
.map(issue => {
18+
const formattedMessage = red(`${bold(issue.code)}: ${issue.message}`);
19+
const formattedPath = formatErrorPath(issue.path);
20+
if (formattedPath) {
21+
return `Validation error at ${bold(formattedPath)}\n${formattedMessage}\n`;
22+
}
23+
return `${formattedMessage}\n`;
24+
})
25+
.join('\n');

0 commit comments

Comments
 (0)