@code-pushup/models
Version:
Model definitions and validators for the Code PushUp CLI
206 lines • 7.34 kB
JavaScript
import { MATERIAL_ICONS } from 'vscode-material-icons';
import { ZodError, 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)
.meta({ title: 'TableCellValue' });
/**
* Schema for execution meta date
*/
export function executionMetaSchema(options = {
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`,
})
.meta({
title: 'Slug',
description: 'Unique ID (human-readable, URL-safe)',
});
/** Schema for a general description property */
export const descriptionSchema = z
.string()
.max(MAX_DESCRIPTION_LENGTH)
.meta({
title: 'Description',
description: 'Description (markdown)',
})
.optional();
/* Schema for a URL */
export const urlSchema = z.string().url().meta({ title: '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)
.flat()
.some(error => error.code === 'invalid_format' && error.format === 'url')) {
console.warn(`Ignoring invalid docsUrl: ${ctx.value}`);
return '';
}
throw new ZodError(ctx.error.issues);
})
.meta({ title: 'DocsUrl', description: 'Documentation site' });
/** Schema for a title of a plugin, category and audit */
export const titleSchema = z.string().max(MAX_TITLE_LENGTH).meta({
title: 'Title',
description: 'Descriptive name',
});
/** Schema for score of audit, category or group */
export const scoreSchema = z.number().min(0).max(1).meta({
title: 'Score',
description: 'Value between 0 and 1',
});
/** Schema for a property indicating whether an entity is filtered out */
export const isSkippedSchema = z
.boolean()
.optional()
.meta({ title: 'IsSkipped' });
/**
* Used for categories, plugins and audits
* @param options
*/
export function metaSchema(options) {
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)
.meta({ title: 'FilePath' });
/**
* Regex for glob patterns - validates file paths and glob patterns
* Allows normal paths and paths with glob metacharacters: *, **, {}, [], !, ?
* Excludes invalid path characters: <>"|
*/
const globRegex = /^!?[^<>"|]+$/;
export const globPathSchema = z.string().trim().min(1).regex(globRegex).meta({
title: 'GlobPath',
description: 'Schema for a glob pattern (supports wildcards like *, **, {}, !, etc.)',
});
/** Schema for a fileNameSchema */
export const fileNameSchema = z
.string()
.trim()
.regex(filenameRegex)
.min(1)
.meta({ title: 'FileName' });
/** Schema for a positiveInt */
export const positiveIntSchema = z
.number()
.int()
.positive()
.meta({ title: 'PositiveInt' });
export const nonnegativeNumberSchema = z
.number()
.nonnegative()
.meta({ title: 'NonnegativeNumber' });
export function packageVersionSchema(options) {
const { versionDescription = 'NPM version of the package', required } = options ?? {};
const packageSchema = z.string().meta({ description: 'NPM package name' });
const versionSchema = z.string().meta({ description: versionDescription });
return z
.object({
packageName: required ? packageSchema : packageSchema.optional(),
version: required ? versionSchema : versionSchema.optional(),
})
.meta({
description: 'NPM package name and version of a published package',
});
}
/** Schema for a binary score threshold */
export const scoreTargetSchema = nonnegativeNumberSchema
.max(1)
.meta({
title: 'ScoreTarget',
description: 'Pass/fail score threshold (0-1)',
})
.optional();
/** Schema for a weight */
export const weightSchema = nonnegativeNumberSchema.meta({
title: 'Weight',
description: 'Coefficient for the given score (use weight 0 if only for display)',
});
export function weightedRefSchema(description, slugDescription) {
return z
.object({
slug: slugSchema.meta({ description: slugDescription }),
weight: weightSchema.meta({
description: 'Weight used to calculate score',
}),
})
.meta({ description });
}
export function scorableSchema(description, refSchema, duplicateCheckFn) {
return z
.object({
slug: slugSchema.meta({
description: '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).meta({
title: 'MaterialIcon',
description: 'Icon from VSCode Material Icons extension',
});
function hasNonZeroWeightedRef(refs) {
return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
}
export const filePositionSchema = z
.object({
startLine: positiveIntSchema.meta({ description: 'Start line' }),
startColumn: positiveIntSchema
.meta({ description: 'Start column' })
.optional(),
endLine: positiveIntSchema.meta({ description: 'End line' }).optional(),
endColumn: positiveIntSchema.meta({ description: 'End column' }).optional(),
})
.meta({
title: 'FilePosition',
description: 'Location in file',
});
//# sourceMappingURL=schemas.js.map