UNPKG

@code-pushup/models

Version:

Model definitions and validators for the Code PushUp CLI

206 lines 7.34 kB
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