-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathschemas.ts
More file actions
239 lines (215 loc) · 6.88 KB
/
schemas.ts
File metadata and controls
239 lines (215 loc) · 6.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import { MATERIAL_ICONS } from 'vscode-material-icons';
import {
ZodError,
type ZodIssue,
type ZodObject,
type ZodOptional,
type ZodString,
z,
} from 'zod';
import {
MAX_DESCRIPTION_LENGTH,
MAX_SLUG_LENGTH,
MAX_TITLE_LENGTH,
} from './limits.js';
import { filenameRegex, slugRegex } from './utils.js';
export const tableCellValueSchema = z
.union([z.string(), z.number(), z.boolean(), z.null()])
.default(null);
export type TableCellValue = z.infer<typeof tableCellValueSchema>;
/**
* Schema for execution meta date
*/
export function executionMetaSchema(
options: {
descriptionDate: string;
descriptionDuration: string;
} = {
descriptionDate: 'Execution start date and time',
descriptionDuration: 'Execution duration in ms',
},
) {
return z.object({
date: z.string().describe(options.descriptionDate),
duration: z.number().describe(options.descriptionDuration),
});
}
/** Schema for a slug of a categories, plugins or audits. */
export const slugSchema = z
.string()
.regex(slugRegex, {
message:
'The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug',
})
.max(MAX_SLUG_LENGTH, {
message: `The slug can be max ${MAX_SLUG_LENGTH} characters long`,
})
.describe('Unique ID (human-readable, URL-safe)');
/** Schema for a general description property */
export const descriptionSchema = z
.string()
.max(MAX_DESCRIPTION_LENGTH)
.describe('Description (markdown)')
.optional();
/* Schema for a URL */
export const urlSchema = z.string().url();
/** Schema for a docsUrl */
export const docsUrlSchema = urlSchema
.optional()
.or(z.literal('')) // allow empty string (no URL validation)
// eslint-disable-next-line unicorn/prefer-top-level-await, unicorn/catch-error-name
.catch(ctx => {
// if only URL validation fails, supress error since this metadata is optional anyway
if (
ctx.issues.length === 1 &&
(ctx.issues[0]?.errors as ZodIssue[][])
.flat()
.some(
error => error.code === 'invalid_format' && error.format === 'url',
)
) {
console.warn(`Ignoring invalid docsUrl: ${ctx.value}`);
return '';
}
throw new ZodError(ctx.error.issues);
})
.describe('Documentation site');
/** Schema for a title of a plugin, category and audit */
export const titleSchema = z
.string()
.max(MAX_TITLE_LENGTH)
.describe('Descriptive name');
/** Schema for score of audit, category or group */
export const scoreSchema = z
.number()
.min(0)
.max(1)
.describe('Value between 0 and 1');
/** Schema for a property indicating whether an entity is filtered out */
export const isSkippedSchema = z.boolean().optional();
/**
* Used for categories, plugins and audits
* @param options
*/
export function metaSchema(options?: {
titleDescription?: string;
descriptionDescription?: string;
docsUrlDescription?: string;
description?: string;
isSkippedDescription?: string;
}) {
const {
descriptionDescription,
titleDescription,
docsUrlDescription,
description,
isSkippedDescription,
} = options ?? {};
const meta = z.object({
title: titleDescription
? titleSchema.describe(titleDescription)
: titleSchema,
description: descriptionDescription
? descriptionSchema.describe(descriptionDescription)
: descriptionSchema,
docsUrl: docsUrlDescription
? docsUrlSchema.describe(docsUrlDescription)
: docsUrlSchema,
isSkipped: isSkippedDescription
? isSkippedSchema.describe(isSkippedDescription)
: isSkippedSchema,
});
return description ? meta.describe(description) : meta;
}
/** Schema for a generalFilePath */
export const filePathSchema = z
.string()
.trim()
.min(1, { message: 'The path is invalid' });
/** Schema for a fileNameSchema */
export const fileNameSchema = z
.string()
.trim()
.regex(filenameRegex, {
message: `The filename has to be valid`,
})
.min(1, { message: 'The file name is invalid' });
/** Schema for a positiveInt */
export const positiveIntSchema = z.number().int().positive();
export const nonnegativeNumberSchema = z.number().nonnegative();
export function packageVersionSchema<
TRequired extends boolean = false,
>(options?: { versionDescription?: string; required?: TRequired }) {
const { versionDescription = 'NPM version of the package', required } =
options ?? {};
const packageSchema = z.string().describe('NPM package name');
const versionSchema = z.string().describe(versionDescription);
return z
.object({
packageName: required ? packageSchema : packageSchema.optional(),
version: required ? versionSchema : versionSchema.optional(),
})
.describe(
'NPM package name and version of a published package',
) as ZodObject<{
packageName: TRequired extends true ? ZodString : ZodOptional<ZodString>;
version: TRequired extends true ? ZodString : ZodOptional<ZodString>;
}>;
}
/** Schema for a binary score threshold */
export const scoreTargetSchema = nonnegativeNumberSchema
.max(1)
.describe('Pass/fail score threshold (0-1)')
.optional();
/** Schema for a weight */
export const weightSchema = nonnegativeNumberSchema.describe(
'Coefficient for the given score (use weight 0 if only for display)',
);
export function weightedRefSchema(
description: string,
slugDescription: string,
) {
return z
.object({
slug: slugSchema.describe(slugDescription),
weight: weightSchema.describe('Weight used to calculate score'),
})
.describe(description);
}
export type WeightedRef = z.infer<ReturnType<typeof weightedRefSchema>>;
export function scorableSchema<T extends ReturnType<typeof weightedRefSchema>>(
description: string,
refSchema: T,
duplicateCheckFn: z.core.CheckFn<z.infer<T>[]>,
) {
return z
.object({
slug: slugSchema.describe('Human-readable unique ID, e.g. "performance"'),
refs: z
.array(refSchema)
.min(1, { message: 'In a category, there has to be at least one ref' })
// refs are unique
.check(duplicateCheckFn)
// category weights are correct
.refine(hasNonZeroWeightedRef, {
error: 'A category must have at least 1 ref with weight > 0.',
}),
})
.describe(description);
}
export const materialIconSchema = z
.enum(MATERIAL_ICONS)
.describe('Icon from VSCode Material Icons extension');
export type MaterialIcon = z.infer<typeof materialIconSchema>;
type Ref = { weight: number };
function hasNonZeroWeightedRef(refs: Ref[]) {
return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
}
export const filePositionSchema = z
.object({
startLine: positiveIntSchema.describe('Start line'),
startColumn: positiveIntSchema.describe('Start column').optional(),
endLine: positiveIntSchema.describe('End line').optional(),
endColumn: positiveIntSchema.describe('End column').optional(),
})
.describe('Location in file');