Skip to content

feat: update to zod v4 #1038

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c918b9a
refactor(models): use forward-compatible schema descriptions
matejchalk Jul 14, 2025
faff148
feat(models): update to zod v4
matejchalk Jul 14, 2025
91df108
feat(plugin-jsdocs): update to zod v4
matejchalk Jul 15, 2025
39febf5
feat(plugin-coverage): update to zod v4
matejchalk Jul 16, 2025
fa9c18f
feat(plugin-js-packages): update to zod v4
matejchalk Jul 16, 2025
d6fae1a
feat(plugin-typescript): update to zod v4
matejchalk Jul 16, 2025
b3be1ac
feat(utils): update to zod v4, replace zod-validation-error with z.pr…
matejchalk Jul 16, 2025
9ca6f26
feat(plugin-eslint): update zod to v4
matejchalk Jul 16, 2025
3698f4a
fix(models): use implementAsync for async z.function occurrences
matejchalk Jul 27, 2025
1b3cb16
fix(cli): adapt format schema check to zod v4
matejchalk Jul 27, 2025
b9a322a
feat(ci): update to zod v4
matejchalk Jul 27, 2025
0b146e5
refactor(models): fix missing descriptions in generated docs
matejchalk Jul 27, 2025
d99f8d2
refactor(models): simplify recursive tree schemas
matejchalk Jul 27, 2025
b9fce04
docs(models): update zod2md and re-generate docs
matejchalk Jul 27, 2025
7ee2f1b
test(models): unit test z.check and z.function helpers
matejchalk Jul 28, 2025
795f835
feat(nx-plugin): update to zod v4
matejchalk Jul 28, 2025
9c10784
refactor(utils): fix compiler errors from recursive schema types
matejchalk Jul 28, 2025
dd81d8a
test(core,ci): fix zod error patterns in assertions
matejchalk Jul 28, 2025
49ab990
revert(utils): "refactor(utils): fix compiler errors from recursive s…
matejchalk Jul 29, 2025
51f634d
refactor(models): restructure recursive types to prevent weird compil…
matejchalk Jul 29, 2025
e61558b
refactor(models): fix lint errors
matejchalk Jul 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
704 changes: 156 additions & 548 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@
"vscode-material-icons": "^0.1.1",
"yaml": "^2.5.1",
"yargs": "^17.7.2",
"zod": "^3.23.8",
"zod-validation-error": "^3.4.0"
"zod": "^4.0.5"
},
"devDependencies": {
"@beaussan/nx-knip": "^0.0.5-15",
Expand Down Expand Up @@ -118,7 +117,7 @@
"verdaccio": "^5.32.2",
"vite": "^5.4.8",
"vitest": "1.3.1",
"zod2md": "^0.1.7"
"zod2md": "^0.2.4"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.19.12",
Expand Down
2 changes: 1 addition & 1 deletion packages/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"glob": "^11.0.1",
"simple-git": "^3.20.0",
"yaml": "^2.5.1",
"zod": "^3.22.1"
"zod": "^4.0.5"
}
}
2 changes: 1 addition & 1 deletion packages/ci/src/lib/cli/persist.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('parsePersistConfig', () => {
await expect(
parsePersistConfig({ persist: { format: ['json', 'html'] } }),
).rejects.toThrow(
/^Invalid persist config - ZodError:.*Invalid enum value. Expected 'json' \| 'md', received 'html'/s,
/^Invalid persist config - ZodError:.*Invalid option: expected one of \\"json\\"\|\\"md\\"/s,
);
});
});
6 changes: 3 additions & 3 deletions packages/cli/src/lib/yargs-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ function validatePersistFormat(persist: PersistConfig) {
return true;
} catch {
throw new Error(
`Invalid persist.format option. Valid options are: ${Object.values(
formatSchema.Values,
).join(', ')}`,
`Invalid persist.format option. Valid options are: ${formatSchema.options.join(
', ',
)}`,
);
}
}
20 changes: 5 additions & 15 deletions packages/core/src/lib/implementation/execute-plugin.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,11 @@ describe('executePlugins', () => {
] satisfies PluginConfig[],
{ progress: false },
),
).rejects.toThrow(`Executing 1 plugin failed.\n\nError: - Plugin ${bold(
title,
)} (${bold(slug)}) produced the following error:
- Audit output is invalid: [
{
"validation": "regex",
"code": "invalid_string",
"message": "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug",
"path": [
0,
"slug"
]
}
]
`);
).rejects.toThrow(
`Executing 1 plugin failed.\n\nError: - Plugin ${bold(
title,
)} (${bold(slug)}) produced the following error:\n - Audit output is invalid`,
);
});

it('should throw for one failing plugin', async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/implementation/read-rc-file.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ describe('readRcByPath', () => {
it('should throw if the configuration is empty', async () => {
await expect(
readRcByPath(path.join(configDirPath, 'code-pushup.empty.config.js')),
).rejects.toThrow(/invalid_type/);
).rejects.toThrow('Invalid input');
});

it('should throw if the configuration is invalid', async () => {
await expect(
readRcByPath(path.join(configDirPath, 'code-pushup.invalid.config.ts')),
).rejects.toThrow(/refs are duplicates/);
).rejects.toThrow('has duplicate references');
});
});
202 changes: 108 additions & 94 deletions packages/models/docs/models-reference.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/models/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"type": "module",
"dependencies": {
"zod": "^3.22.1",
"zod": "^4.0.5",
"vscode-material-icons": "^0.1.0"
}
}
56 changes: 19 additions & 37 deletions packages/models/src/lib/audit-output.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,49 @@
import { z } from 'zod';
import { createDuplicateSlugsCheck } from './implementation/checks.js';
import {
nonnegativeNumberSchema,
scoreSchema,
slugSchema,
} from './implementation/schemas.js';
import { errorItems, hasDuplicateStrings } from './implementation/utils.js';
import { issueSchema } from './issue.js';
import { tableSchema } from './table.js';
import { treeSchema } from './tree.js';

export const auditValueSchema =
nonnegativeNumberSchema.describe('Raw numeric value');
export const auditDisplayValueSchema = z
.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" })
.optional();
.string()
.optional()
.describe("Formatted value (e.g. '0.9 s', '2.1 MB')");

export const auditDetailsSchema = z.object(
{
issues: z
.array(issueSchema, { description: 'List of findings' })
.optional(),
export const auditDetailsSchema = z
.object({
issues: z.array(issueSchema).describe('List of findings').optional(),
table: tableSchema('Table of related findings').optional(),
trees: z
.array(treeSchema, { description: 'Findings in tree structure' })
.array(treeSchema)
.describe('Findings in tree structure')
.optional(),
},
{ description: 'Detailed information' },
);
})
.describe('Detailed information');
export type AuditDetails = z.infer<typeof auditDetailsSchema>;

export const auditOutputSchema = z.object(
{
export const auditOutputSchema = z
.object({
slug: slugSchema.describe('Reference to audit'),
displayValue: auditDisplayValueSchema,
value: auditValueSchema,
score: scoreSchema,
details: auditDetailsSchema.optional(),
},
{ description: 'Audit information' },
);
})
.describe('Audit information');

export type AuditOutput = z.infer<typeof auditOutputSchema>;

export const auditOutputsSchema = z
.array(auditOutputSchema, {
description:
'List of JSON formatted audit output emitted by the runner process of a plugin',
})
// audit slugs are unique
.refine(
audits => !getDuplicateSlugsInAudits(audits),
audits => ({ message: duplicateSlugsInAuditsErrorMsg(audits) }),
.array(auditOutputSchema)
.check(createDuplicateSlugsCheck('Audit'))
.describe(
'List of JSON formatted audit output emitted by the runner process of a plugin',
);
export type AuditOutputs = z.infer<typeof auditOutputsSchema>;

// helper for validator: audit slugs are unique
function duplicateSlugsInAuditsErrorMsg(audits: AuditOutput[]) {
const duplicateRefs = getDuplicateSlugsInAudits(audits);
return `In plugin audits the slugs are not unique: ${errorItems(
duplicateRefs,
)}`;
}

function getDuplicateSlugsInAudits(audits: AuditOutput[]) {
return hasDuplicateStrings(audits.map(({ slug }) => slug));
}
4 changes: 3 additions & 1 deletion packages/models/src/lib/audit-output.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ describe('auditOutputsSchema', () => {
score: 0.75,
},
] satisfies AuditOutputs),
).toThrow('slugs are not unique: total-blocking-time');
).toThrow(
String.raw`Audit slugs must be unique, but received duplicates: \"total-blocking-time\"`,
);
});
});
29 changes: 4 additions & 25 deletions packages/models/src/lib/audit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { createDuplicateSlugsCheck } from './implementation/checks.js';
import { metaSchema, slugSchema } from './implementation/schemas.js';
import { errorItems, hasDuplicateStrings } from './implementation/utils.js';

export const auditSchema = z
.object({
Expand All @@ -18,28 +18,7 @@ export const auditSchema = z

export type Audit = z.infer<typeof auditSchema>;
export const pluginAuditsSchema = z
.array(auditSchema, {
description: 'List of audits maintained in a plugin',
})
.array(auditSchema)
.min(1)
// audit slugs are unique
.refine(
auditMetadata => !getDuplicateSlugsInAudits(auditMetadata),
auditMetadata => ({
message: duplicateSlugsInAuditsErrorMsg(auditMetadata),
}),
);

// =======================

// helper for validator: audit slugs are unique
function duplicateSlugsInAuditsErrorMsg(audits: Audit[]) {
const duplicateRefs = getDuplicateSlugsInAudits(audits);
return `In plugin audits the following slugs are not unique: ${errorItems(
duplicateRefs,
)}`;
}

function getDuplicateSlugsInAudits(audits: Audit[]) {
return hasDuplicateStrings(audits.map(({ slug }) => slug));
}
.check(createDuplicateSlugsCheck('Audit'))
.describe('List of audits maintained in a plugin');
4 changes: 3 additions & 1 deletion packages/models/src/lib/audit.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ describe('pluginAuditsSchema', () => {
title: 'Jest unit tests results.',
},
] satisfies Audit[]),
).toThrow('slugs are not unique: jest-unit-test-results');
).toThrow(
String.raw`Audit slugs must be unique, but received duplicates: \"jest-unit-test-results\"`,
);
});
});
77 changes: 36 additions & 41 deletions packages/models/src/lib/category-config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { z } from 'zod';
import {
createDuplicateSlugsCheck,
createDuplicatesCheck,
} from './implementation/checks.js';
import {
metaSchema,
scorableSchema,
slugSchema,
weightedRefSchema,
} from './implementation/schemas.js';
import { errorItems, hasDuplicateStrings } from './implementation/utils.js';
import { formatRef } from './implementation/utils.js';

export const categoryRefSchema = weightedRefSchema(
'Weighted references to audits and/or groups for the category',
'Slug of an audit or group (depending on `type`)',
).merge(
z.object({
type: z.enum(['audit', 'group'], {
description:
type: z
.enum(['audit', 'group'])
.describe(
'Discriminant for reference kind, affects where `slug` is looked up',
}),
),
plugin: slugSchema.describe(
'Plugin slug (plugin should contain referenced audit or group)',
),
Expand All @@ -26,8 +31,11 @@ export type CategoryRef = z.infer<typeof categoryRefSchema>;
export const categoryConfigSchema = scorableSchema(
'Category with a score calculated from audits and groups from various plugins',
categoryRefSchema,
getDuplicateRefsInCategoryMetrics,
duplicateRefsInCategoryMetricsErrorMsg,
createDuplicatesCheck(
serializeCategoryRefTarget,
duplicates =>
`Category has duplicate references: ${formatSerializedCategoryRefTargets(duplicates)}`,
),
)
.merge(
metaSchema({
Expand All @@ -40,49 +48,36 @@ export const categoryConfigSchema = scorableSchema(
.merge(
z.object({
isBinary: z
.boolean({
description:
'Is this a binary category (i.e. only a perfect score considered a "pass")?',
})
.boolean()
.describe(
'Is this a binary category (i.e. only a perfect score considered a "pass")?',
)
.optional(),
}),
);

export type CategoryConfig = z.infer<typeof categoryConfigSchema>;

// helper for validator: categories have unique refs to audits or groups
export function duplicateRefsInCategoryMetricsErrorMsg(metrics: CategoryRef[]) {
const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics);
return `In the categories, the following audit or group refs are duplicates: ${errorItems(
duplicateRefs,
)}`;
}
const CATEGORY_REF_SEP = '||';

function getDuplicateRefsInCategoryMetrics(metrics: CategoryRef[]) {
return hasDuplicateStrings(
metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`),
);
function serializeCategoryRefTarget(ref: CategoryRef): string {
return [ref.type, ref.plugin, ref.slug].join(CATEGORY_REF_SEP);
}

export const categoriesSchema = z
.array(categoryConfigSchema, {
description: 'Categorization of individual audits',
})
.refine(
categoryCfg => !getDuplicateSlugCategories(categoryCfg),
categoryCfg => ({
message: duplicateSlugCategoriesErrorMsg(categoryCfg),
}),
);

// helper for validator: categories slugs are unique
function duplicateSlugCategoriesErrorMsg(categories: CategoryConfig[]) {
const duplicateStringSlugs = getDuplicateSlugCategories(categories);
return `In the categories, the following slugs are duplicated: ${errorItems(
duplicateStringSlugs,
)}`;
function formatSerializedCategoryRefTargets(keys: string[]): string {
return keys
.map(key => {
const [type, plugin, slug] = key.split(CATEGORY_REF_SEP) as [
'group' | 'audit',
string,
string,
];
return formatRef({ type, plugin, slug });
})
.join(', ');
}

function getDuplicateSlugCategories(categories: CategoryConfig[]) {
return hasDuplicateStrings(categories.map(({ slug }) => slug));
}
export const categoriesSchema = z
.array(categoryConfigSchema)
.check(createDuplicateSlugsCheck('Category'))
.describe('Categorization of individual audits');
Loading
Loading