diff --git a/package-lock.json b/package-lock.json index 53ab456d8..13e3212ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@code-pushup/portal-client": "^0.15.0", + "@code-pushup/portal-client": "^0.16.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "21.4.1", "@poppinss/cliui": "6.4.1", @@ -2319,9 +2319,9 @@ } }, "node_modules/@code-pushup/portal-client": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.15.0.tgz", - "integrity": "sha512-y3ZPaNdd1zK4lYu70S0sDC1Z5Qd1NIQWECt/8O4r0A6UTrQLYht+pgNFDFuCDvHnQrLkwmR7ib8xPbJJ74W+3w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.16.0.tgz", + "integrity": "sha512-JlMRcTKkJygVfLS+IWQxDRDnvF64p4q+QDLIXzQPep6X99C1OH3MnA9jbfGAOew/3xqOILCrifn0y54fyRs8Qg==", "license": "MIT", "dependencies": { "graphql": "^16.6.0", diff --git a/package.json b/package.json index ba7596460..2d82efb62 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "node": ">=22.14" }, "dependencies": { - "@code-pushup/portal-client": "^0.15.0", + "@code-pushup/portal-client": "^0.16.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "21.4.1", "@poppinss/cliui": "6.4.1", diff --git a/packages/ci/package.json b/packages/ci/package.json index f7f91c084..e74566f0c 100644 --- a/packages/ci/package.json +++ b/packages/ci/package.json @@ -27,7 +27,7 @@ "type": "module", "dependencies": { "@code-pushup/models": "0.78.0", - "@code-pushup/portal-client": "^0.15.0", + "@code-pushup/portal-client": "^0.16.0", "@code-pushup/utils": "0.78.0", "glob": "^11.0.1", "simple-git": "^3.20.0", diff --git a/packages/ci/src/lib/portal/transform.ts b/packages/ci/src/lib/portal/transform.ts index 1f33862f1..e93300a30 100644 --- a/packages/ci/src/lib/portal/transform.ts +++ b/packages/ci/src/lib/portal/transform.ts @@ -62,6 +62,7 @@ function transformGQLCategory(category: CategoryFragment): CategoryConfig { slug: category.slug, title: category.title, ...(category.description && { description: category.description }), + ...(category.scoreTarget != null && { scoreTarget: category.scoreTarget }), refs: category.refs.map( ({ target, weight }): CategoryRef => ({ type: lowercase(target.__typename), @@ -70,9 +71,6 @@ function transformGQLCategory(category: CategoryFragment): CategoryConfig { weight, }), ), - // TODO: Portal API migration - convert isBinary to scoreTarget for backward compatibility - // Remove this conversion when Portal API supports scoreTarget (#713) - ...(category.isBinary && { scoreTarget: 1 }), }; } diff --git a/packages/ci/src/lib/portal/transform.unit.test.ts b/packages/ci/src/lib/portal/transform.unit.test.ts index 07212b147..5ae819fca 100644 --- a/packages/ci/src/lib/portal/transform.unit.test.ts +++ b/packages/ci/src/lib/portal/transform.unit.test.ts @@ -25,7 +25,6 @@ describe('transformGQLReport', () => { { slug: 'code-style', title: 'Code style', - isBinary: false, score: 0.5, refs: [ { @@ -41,7 +40,6 @@ describe('transformGQLReport', () => { { slug: 'bundle-size', title: 'Bundle size', - isBinary: false, score: 0.75, refs: [ { diff --git a/packages/core/package.json b/packages/core/package.json index 0cfc46458..25222b673 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,7 @@ "ansis": "^3.3.0" }, "peerDependencies": { - "@code-pushup/portal-client": "^0.15.0" + "@code-pushup/portal-client": "^0.16.0" }, "peerDependenciesMeta": { "@code-pushup/portal-client": { diff --git a/packages/core/src/lib/implementation/report-to-gql.ts b/packages/core/src/lib/implementation/report-to-gql.ts index 6c0dab792..f70bc248d 100644 --- a/packages/core/src/lib/implementation/report-to-gql.ts +++ b/packages/core/src/lib/implementation/report-to-gql.ts @@ -199,9 +199,7 @@ function categoryToGQL(category: CategoryConfig): PortalCategory { slug: category.slug, title: category.title, description: category.description, - // TODO: Portal API migration - convert scoreTarget to isBinary for backward compatibility - // Remove this conversion when Portal API supports scoreTarget (#713) - isBinary: category.scoreTarget === 1, + ...(category.scoreTarget != null && { scoreTarget: category.scoreTarget }), refs: category.refs.map(ref => ({ plugin: ref.plugin, type: categoryRefTypeToGQL(ref.type), diff --git a/packages/models/docs/models-reference.md b/packages/models/docs/models-reference.md index c2aa5bac8..9965cc111 100644 --- a/packages/models/docs/models-reference.md +++ b/packages/models/docs/models-reference.md @@ -1290,21 +1290,21 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | -| `packageName` | NPM package name | `string` | -| `version` | NPM version of the package | `string` | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `description` | Description (markdown) | `string` (_max length: 65536_) | -| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `''` | -| `isSkipped` | | `boolean` | -| **`slug`** (\*) | Unique plugin slug within core config | [Slug](#slug) | -| **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) | -| **`runner`** (\*) | | [RunnerConfig](#runnerconfig) _or_ [RunnerFunction](#runnerfunction) | -| **`audits`** (\*) | List of audits maintained in a plugin | _Array of at least 1 [Audit](#audit) items_ | -| `groups` | List of groups | _Array of [Group](#group) items_ | -| `scoreTargets` | Score targets that trigger a perfect score. Number for all audits or record { slug: target } for specific audits | `number` (_≥0, ≤1_) (_optional_) _or_ _Object with dynamic keys of type_ `string` _and values of type_ `number` (_≥0, ≤1_) | -| `context` | Plugin-specific context data for helpers | [PluginContext](#plugincontext) | +| Property | Description | Type | +| :---------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- | +| `packageName` | NPM package name | `string` | +| `version` | NPM version of the package | `string` | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `description` | Description (markdown) | `string` (_max length: 65536_) | +| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `''` | +| `isSkipped` | | `boolean` | +| **`slug`** (\*) | Unique plugin slug within core config | [Slug](#slug) | +| **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) | +| **`runner`** (\*) | | [RunnerConfig](#runnerconfig) _or_ [RunnerFunction](#runnerfunction) | +| **`audits`** (\*) | List of audits maintained in a plugin | _Array of at least 1 [Audit](#audit) items_ | +| `groups` | List of groups | _Array of [Group](#group) items_ | +| `scoreTargets` | Score targets that trigger a perfect score. Number for all audits or record { slug: target } for specific audits | [PluginScoreTargets](#pluginscoretargets) | +| `context` | Plugin-specific context data for helpers | [PluginContext](#plugincontext) | _(\*) Required._ @@ -1356,6 +1356,16 @@ _Object containing the following properties:_ _(\*) Required._ +## PluginScoreTargets + +Score targets that trigger a perfect score. Number for all audits or record { slug: target } for specific audits + +_Union of the following possible types:_ + +- `number` (_≥0, ≤1_) (_optional_) +- _Object with dynamic keys of type_ `string` _and values of type_ `number` (_≥0, ≤1_) + (_optional_) + ## Report Collect output data diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 7e64d5403..370d64e7c 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -86,6 +86,7 @@ export { pluginConfigSchema, pluginContextSchema, pluginMetaSchema, + pluginScoreTargetsSchema, type PluginConfig, type PluginContext, type PluginMeta, diff --git a/packages/models/src/lib/plugin-config.unit.test.ts b/packages/models/src/lib/plugin-config.unit.test.ts index 69ccaa9ff..3c6fe2d74 100644 --- a/packages/models/src/lib/plugin-config.unit.test.ts +++ b/packages/models/src/lib/plugin-config.unit.test.ts @@ -40,7 +40,7 @@ describe('pluginConfigSchema', () => { ).not.toThrow(); }); - it('should accept a valid plugin configuration with a score target', () => { + it('should accept a valid plugin configuration with score targets', () => { expect(() => pluginConfigSchema.parse({ slug: 'lighthouse', @@ -60,7 +60,7 @@ describe('pluginConfigSchema', () => { displayValue: '180 ms', }, ], - scoreTarget: { 'total-blocking-time': 0.9 }, + scoreTargets: { 'total-blocking-time': 0.9 }, audits: [ { slug: 'first-contentful-paint', title: 'First Contentful Paint' }, { slug: 'total-blocking-time', title: 'Total Blocking Time' }, diff --git a/packages/plugin-coverage/README.md b/packages/plugin-coverage/README.md index dec029c3a..cfa6fd5e6 100644 --- a/packages/plugin-coverage/README.md +++ b/packages/plugin-coverage/README.md @@ -128,7 +128,7 @@ The plugin accepts the following parameters: - For a single project, providing paths to results as strings is enough. - If you have a monorepo, both path to results (`resultsPath`) and path from the root to project the results belong to (`pathToProject`) need to be provided for the LCOV format. For Nx monorepos, you can use our helper function `getNxCoveragePaths` to get the path information automatically. - (optional) `coverageToolCommand`: If you wish to run your coverage tool to generate the results first, you may define it here. -- (optional) `perfectScoreThreshold`: If your coverage goal is not 100%, you may define it here in range 0-1. Any score above the defined threshold will be given the perfect score. The value will stay unaffected. +- (optional) `scoreTargets`: If your coverage goal is not 100%, you may define it here in range 0-1. Any score above the defined threshold will be given the perfect score. The value will stay unaffected. ### Audits and group diff --git a/packages/plugin-coverage/src/lib/config.ts b/packages/plugin-coverage/src/lib/config.ts index 4db726ed0..daf51c182 100644 --- a/packages/plugin-coverage/src/lib/config.ts +++ b/packages/plugin-coverage/src/lib/config.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { pluginScoreTargetsSchema } from '@code-pushup/models'; export const coverageTypeSchema = z.enum(['function', 'branch', 'line']); export type CoverageType = z.infer; @@ -50,14 +51,7 @@ export const coveragePluginConfigSchema = z.object({ .describe( 'Path to all code coverage report files. Only LCOV format is supported for now.', ), - perfectScoreThreshold: z - .number() - .gt(0) - .max(1) - .describe( - 'Score will be 1 (perfect) for this coverage and above. Score range is 0 - 1.', - ) - .optional(), + scoreTargets: pluginScoreTargetsSchema, }); export type CoveragePluginConfig = z.input; export type FinalCoveragePluginConfig = z.infer< diff --git a/packages/plugin-coverage/src/lib/config.unit.test.ts b/packages/plugin-coverage/src/lib/config.unit.test.ts index 517f8231a..765ab447e 100644 --- a/packages/plugin-coverage/src/lib/config.unit.test.ts +++ b/packages/plugin-coverage/src/lib/config.unit.test.ts @@ -20,7 +20,7 @@ describe('coveragePluginConfigSchema', () => { command: 'npx nx run-many', args: ['-t', 'test', '--coverage'], }, - perfectScoreThreshold: 0.85, + scoreTargets: 0.85, } satisfies CoveragePluginConfig), ).not.toThrow(); }); @@ -33,6 +33,24 @@ describe('coveragePluginConfigSchema', () => { ).not.toThrow(); }); + it('accepts number scoreTargets', () => { + expect(() => + coveragePluginConfigSchema.parse({ + reports: ['coverage/cli/lcov.info'], + scoreTargets: 0.8, + } satisfies CoveragePluginConfig), + ).not.toThrow(); + }); + + it('accepts object scoreTargets', () => { + expect(() => + coveragePluginConfigSchema.parse({ + reports: ['coverage/cli/lcov.info'], + scoreTargets: { 'function-coverage': 0.9 }, + } satisfies CoveragePluginConfig), + ).not.toThrow(); + }); + it('replaces undefined coverage with all available types', () => { const config = { reports: ['coverage/cli/lcov.info'], @@ -86,12 +104,12 @@ describe('coveragePluginConfigSchema', () => { ).toThrow('invalid_type'); }); - it('throws for invalid score threshold', () => { + it('throws for invalid score targets', () => { expect(() => coveragePluginConfigSchema.parse({ coverageTypes: ['line'], reports: ['coverage/cli/lcov.info'], - perfectScoreThreshold: 1.1, + scoreTargets: 1.1, } satisfies CoveragePluginConfig), ).toThrow('too_big'); }); diff --git a/packages/plugin-coverage/src/lib/coverage-plugin.ts b/packages/plugin-coverage/src/lib/coverage-plugin.ts index 72e037d0b..2727f3f73 100644 --- a/packages/plugin-coverage/src/lib/coverage-plugin.ts +++ b/packages/plugin-coverage/src/lib/coverage-plugin.ts @@ -65,6 +65,8 @@ export async function coveragePlugin( '../../package.json', ) as typeof import('../../package.json'); + const scoreTargets = coverageConfig.scoreTargets; + return { slug: 'coverage', title: 'Code coverage', @@ -76,5 +78,6 @@ export async function coveragePlugin( audits, groups: [group], runner: await createRunnerConfig(runnerScriptPath, coverageConfig), + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts index cfa60083d..6967a58f1 100644 --- a/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts +++ b/packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import type { RunnerConfig } from '@code-pushup/models'; +import { type RunnerConfig, pluginConfigSchema } from '@code-pushup/models'; import { coveragePlugin } from './coverage-plugin.js'; vi.mock('./runner/index.ts', () => ({ @@ -81,4 +81,15 @@ describe('coveragePlugin', () => { }), ); }); + + it('should pass scoreTargets to PluginConfig when provided', async () => { + const scoreTargets = { 'function-coverage': 0.9, 'line-coverage': 0.8 }; + const pluginConfig = await coveragePlugin({ + reports: [LCOV_PATH], + scoreTargets, + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); }); diff --git a/packages/plugin-coverage/src/lib/runner/index.ts b/packages/plugin-coverage/src/lib/runner/index.ts index 6f28d9940..f22ed5476 100644 --- a/packages/plugin-coverage/src/lib/runner/index.ts +++ b/packages/plugin-coverage/src/lib/runner/index.ts @@ -1,11 +1,7 @@ import { bold } from 'ansis'; import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { - AuditOutputs, - RunnerConfig, - RunnerFilesPaths, -} from '@code-pushup/models'; +import type { RunnerConfig, RunnerFilesPaths } from '@code-pushup/models'; import { ProcessError, createRunnerFiles, @@ -17,7 +13,6 @@ import { ui, } from '@code-pushup/utils'; import type { FinalCoveragePluginConfig } from '../config.js'; -import { applyMaxScoreAboveThreshold } from '../utils.js'; import { lcovResultsToAuditOutputs } from './lcov/lcov-runner.js'; export async function executeRunner({ @@ -68,8 +63,6 @@ export async function createRunnerConfig( JSON.stringify(config), ); - const threshold = config.perfectScoreThreshold; - return { command: 'node', args: [ @@ -78,9 +71,5 @@ export async function createRunnerConfig( ], configFile: runnerConfigPath, outputFile: runnerOutputPath, - ...(threshold != null && { - outputTransform: outputs => - applyMaxScoreAboveThreshold(outputs as AuditOutputs, threshold), - }), }; } diff --git a/packages/plugin-coverage/src/lib/runner/runner.int.test.ts b/packages/plugin-coverage/src/lib/runner/runner.int.test.ts index 17dcda01e..91c2579b7 100644 --- a/packages/plugin-coverage/src/lib/runner/runner.int.test.ts +++ b/packages/plugin-coverage/src/lib/runner/runner.int.test.ts @@ -11,7 +11,7 @@ describe('createRunnerConfig', () => { const runnerConfig = await createRunnerConfig('executeRunner.ts', { reports: ['coverage/lcov.info'], coverageTypes: ['branch'], - perfectScoreThreshold: 85, + scoreTargets: 0.85, continueOnCommandFail: true, }); expect(runnerConfig).toStrictEqual({ @@ -21,7 +21,6 @@ describe('createRunnerConfig', () => { expect.stringContaining('plugin-config.json'), expect.stringContaining('runner-output.json'), ], - outputTransform: expect.any(Function), outputFile: expect.stringContaining('runner-output.json'), configFile: expect.stringContaining('plugin-config.json'), }); @@ -32,7 +31,7 @@ describe('createRunnerConfig', () => { coverageTypes: ['line'], reports: ['coverage/lcov.info'], coverageToolCommand: { command: 'npm', args: ['run', 'test'] }, - perfectScoreThreshold: 85, + scoreTargets: 0.85, continueOnCommandFail: true, }; diff --git a/packages/plugin-coverage/src/lib/utils.ts b/packages/plugin-coverage/src/lib/utils.ts index 371f36a63..79d7badac 100644 --- a/packages/plugin-coverage/src/lib/utils.ts +++ b/packages/plugin-coverage/src/lib/utils.ts @@ -1,4 +1,3 @@ -import type { AuditOutputs } from '@code-pushup/models'; import type { CoverageType } from './config.js'; export const coverageDescription: Record = { @@ -8,21 +7,6 @@ export const coverageDescription: Record = { function: 'Measures how many functions were called in at least one test.', }; -/** - * Since more code coverage does not necessarily mean better score, this optional override allows for defining custom coverage goals. - * @param outputs original results - * @param threshold threshold above which the score is to be 1 - * @returns Outputs with overriden score (not value) to 1 if it reached a defined threshold. - */ -export function applyMaxScoreAboveThreshold( - outputs: AuditOutputs, - threshold: number, -): AuditOutputs { - return outputs.map(output => - output.score >= threshold ? { ...output, score: 1 } : output, - ); -} - export const coverageTypeWeightMapper: Record = { /* eslint-disable @typescript-eslint/no-magic-numbers */ function: 6, diff --git a/packages/plugin-coverage/src/lib/utils.unit.test.ts b/packages/plugin-coverage/src/lib/utils.unit.test.ts deleted file mode 100644 index d1ccedc1b..000000000 --- a/packages/plugin-coverage/src/lib/utils.unit.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { AuditOutput } from '@code-pushup/models'; -import { applyMaxScoreAboveThreshold } from './utils.js'; - -describe('applyMaxScoreAboveThreshold', () => { - it('should transform score above threshold to maximum', () => { - expect( - applyMaxScoreAboveThreshold( - [ - { - slug: 'branch-coverage', - value: 75, - score: 0.75, - }, - ], - 0.7, - ), - ).toEqual([ - { - slug: 'branch-coverage', - value: 75, - score: 1, - }, - ]); - }); - - it('should leave score below threshold untouched', () => { - expect( - applyMaxScoreAboveThreshold( - [ - { - slug: 'line-coverage', - value: 60, - score: 0.6, - }, - ], - 0.7, - ), - ).toEqual([ - { - slug: 'line-coverage', - value: 60, - score: 0.6, - }, - ]); - }); -}); diff --git a/packages/plugin-eslint/src/lib/config.ts b/packages/plugin-eslint/src/lib/config.ts index 6a539b502..e49e14990 100644 --- a/packages/plugin-eslint/src/lib/config.ts +++ b/packages/plugin-eslint/src/lib/config.ts @@ -1,5 +1,8 @@ import { z } from 'zod'; -import { pluginArtifactOptionsSchema } from '@code-pushup/models'; +import { + pluginArtifactOptionsSchema, + pluginScoreTargetsSchema, +} from '@code-pushup/models'; import { toArray } from '@code-pushup/utils'; const patternsSchema = z @@ -63,5 +66,6 @@ export type CustomGroup = z.infer; export const eslintPluginOptionsSchema = z.object({ groups: z.array(customGroupSchema).optional(), artifacts: pluginArtifactOptionsSchema.optional(), + scoreTargets: pluginScoreTargetsSchema, }); export type ESLintPluginOptions = z.infer; diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.ts b/packages/plugin-eslint/src/lib/eslint-plugin.ts index 49f6ea228..2c5617b41 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.ts @@ -40,7 +40,11 @@ export async function eslintPlugin( schemaType: 'ESLint plugin config', }); - const { groups: customGroups, artifacts } = options + const { + groups: customGroups, + artifacts, + scoreTargets, + } = options ? parseSchema(eslintPluginOptionsSchema, options, { schemaType: 'ESLint plugin options', }) @@ -69,5 +73,6 @@ export async function eslintPlugin( targets, ...(artifacts ? { artifacts } : {}), }), + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts b/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts new file mode 100644 index 000000000..4d8386dfd --- /dev/null +++ b/packages/plugin-eslint/src/lib/eslint-plugin.unit.test.ts @@ -0,0 +1,28 @@ +import { pluginConfigSchema } from '@code-pushup/models'; +import { eslintPlugin } from './eslint-plugin.js'; +import * as metaModule from './meta/index.js'; + +describe('eslintPlugin', () => { + const listAuditsAndGroupsSpy = vi.spyOn(metaModule, 'listAuditsAndGroups'); + + beforeAll(() => { + listAuditsAndGroupsSpy.mockResolvedValue({ + audits: [{ slug: 'type-safety', title: 'Type Safety' }], + groups: [], + }); + }); + + it('should pass scoreTargets to PluginConfig when provided', async () => { + const scoreTargets = { 'type-safety': 0.9 }; + const pluginConfig = await eslintPlugin( + { + eslintrc: 'eslint.config.js', + patterns: ['src/**/*.js'], + }, + { scoreTargets }, + ); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); +}); diff --git a/packages/plugin-js-packages/src/lib/config.ts b/packages/plugin-js-packages/src/lib/config.ts index 37f79b687..62f086d9b 100644 --- a/packages/plugin-js-packages/src/lib/config.ts +++ b/packages/plugin-js-packages/src/lib/config.ts @@ -1,5 +1,9 @@ import { z } from 'zod'; -import { type IssueSeverity, issueSeveritySchema } from '@code-pushup/models'; +import { + type IssueSeverity, + issueSeveritySchema, + pluginScoreTargetsSchema, +} from '@code-pushup/models'; import { defaultAuditLevelMapping } from './constants.js'; export const dependencyGroups = ['prod', 'dev', 'optional'] as const; @@ -74,6 +78,7 @@ export const jsPackagesPluginConfigSchema = z.object({ 'Mapping of audit levels to issue severity. Custom mapping or overrides may be entered manually, otherwise has a default preset.', ), packageJsonPath: packageJsonPathSchema, + scoreTargets: pluginScoreTargetsSchema, }); export type JSPackagesPluginConfig = z.input< @@ -81,5 +86,5 @@ export type JSPackagesPluginConfig = z.input< >; export type FinalJSPackagesPluginConfig = Required< - z.infer + Omit, 'scoreTargets'> >; diff --git a/packages/plugin-js-packages/src/lib/config.unit.test.ts b/packages/plugin-js-packages/src/lib/config.unit.test.ts index 0b80823af..9ea26f3b3 100644 --- a/packages/plugin-js-packages/src/lib/config.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/config.unit.test.ts @@ -88,4 +88,13 @@ describe('fillAuditLevelMapping', () => { info: 'info', }); }); + + it('should accept valid score targets', () => { + expect(() => + jsPackagesPluginConfigSchema.parse({ + packageManager: 'npm', + scoreTargets: { 'npm-outdated-dev': 0.9 }, + }), + ).not.toThrow(); + }); }); diff --git a/packages/plugin-js-packages/src/lib/js-packages-plugin.ts b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts index 2744f2b27..8fe2491cd 100644 --- a/packages/plugin-js-packages/src/lib/js-packages-plugin.ts +++ b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts @@ -34,8 +34,13 @@ import { normalizeConfig } from './utils.js'; export async function jsPackagesPlugin( config?: JSPackagesPluginConfig, ): Promise { - const { packageManager, checks, depGroups, ...jsPackagesPluginConfigRest } = - await normalizeConfig(config); + const { + packageManager, + checks, + depGroups, + scoreTargets, + ...jsPackagesPluginConfigRest + } = await normalizeConfig(config); const runnerScriptPath = path.join( fileURLToPath(path.dirname(import.meta.url)), @@ -64,6 +69,7 @@ export async function jsPackagesPlugin( packageManager: packageManager.slug, dependencyGroups: depGroups, }), + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts b/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts index 7bb2a682a..2b8b90dcb 100644 --- a/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts @@ -1,6 +1,11 @@ import { vol } from 'memfs'; import { describe, expect, it } from 'vitest'; -import type { Group, PluginConfig, RunnerConfig } from '@code-pushup/models'; +import { + type Group, + type PluginConfig, + type RunnerConfig, + pluginConfigSchema, +} from '@code-pushup/models'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { jsPackagesPlugin } from './js-packages-plugin.js'; @@ -179,4 +184,15 @@ describe('jsPackagesPlugin', () => { }), ); }); + + it('should pass scoreTargets to PluginConfig when provided', async () => { + const scoreTargets = { 'npm-outdated-dev': 0.9 }; + const pluginConfig = await jsPackagesPlugin({ + packageManager: 'npm', + scoreTargets, + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); }); diff --git a/packages/plugin-jsdocs/src/lib/config.ts b/packages/plugin-jsdocs/src/lib/config.ts index cd139c50c..d746bf922 100644 --- a/packages/plugin-jsdocs/src/lib/config.ts +++ b/packages/plugin-jsdocs/src/lib/config.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { pluginScoreTargetsSchema } from '@code-pushup/models'; const patternsSchema = z .union([z.string(), z.array(z.string()).min(1)]) @@ -19,6 +20,7 @@ const jsDocsTargetObjectSchema = z 'List of audit slugs to evaluate. When specified, only these audits will be evaluated.', ), patterns: patternsSchema, + scoreTargets: pluginScoreTargetsSchema, }) .refine(data => !(data.skipAudits && data.onlyAudits), { message: "You can't define 'skipAudits' and 'onlyAudits' simultaneously", diff --git a/packages/plugin-jsdocs/src/lib/config.unit.test.ts b/packages/plugin-jsdocs/src/lib/config.unit.test.ts index d77dd19ff..6f90ac5d5 100644 --- a/packages/plugin-jsdocs/src/lib/config.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/config.unit.test.ts @@ -142,4 +142,15 @@ describe('JsDocsPlugin Configuration', () => { ).toThrow('Invalid input'); }); }); + + describe('scoreTargets', () => { + it('should accept valid score targets', () => { + expect(() => + jsDocsPluginConfigSchema.parse({ + patterns: ['src/**/*.ts'], + scoreTargets: { 'functions-coverage': 0.9 }, + }), + ).not.toThrow(); + }); + }); }); diff --git a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts index 694d0c135..8de4c3410 100644 --- a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts +++ b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.ts @@ -32,6 +32,7 @@ export const PLUGIN_DOCS_URL = */ export function jsDocsPlugin(config: JsDocsPluginConfig): PluginConfig { const jsDocsConfig = jsDocsPluginConfigSchema.parse(config); + const scoreTargets = jsDocsConfig.scoreTargets; return { slug: PLUGIN_SLUG, @@ -42,5 +43,6 @@ export function jsDocsPlugin(config: JsDocsPluginConfig): PluginConfig { groups: filterGroupsByOnlyAudits(groups, jsDocsConfig), audits: filterAuditsByPluginConfig(jsDocsConfig), runner: createRunnerFunction(jsDocsConfig), + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts index e6ec0b74f..19b6f9b0f 100644 --- a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; +import { pluginConfigSchema } from '@code-pushup/models'; import { PLUGIN_SLUG, groups } from './constants.js'; import { PLUGIN_DESCRIPTION, @@ -13,8 +14,24 @@ import { } from './utils.js'; vi.mock('./utils.js', () => ({ - filterAuditsByPluginConfig: vi.fn().mockReturnValue(['mockAudit']), - filterGroupsByOnlyAudits: vi.fn().mockReturnValue(['mockGroup']), + filterAuditsByPluginConfig: vi.fn().mockReturnValue([ + { + slug: 'mock-audit', + title: 'Mock Audit', + }, + ]), + filterGroupsByOnlyAudits: vi.fn().mockReturnValue([ + { + slug: 'mock-group', + title: 'Mock Group', + refs: [ + { + slug: 'mock-audit', + weight: 1, + }, + ], + }, + ]), })); vi.mock('./runner/runner.js', () => ({ @@ -68,4 +85,15 @@ describe('jsDocsPlugin', () => { expect(createRunnerFunction).toHaveBeenCalledWith(config); }); + + it('should pass scoreTargets to PluginConfig when provided', () => { + const scoreTargets = { 'functions-coverage': 0.9 }; + const pluginConfig = jsDocsPlugin({ + patterns: ['src/**/*.ts'], + scoreTargets, + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); }); diff --git a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts index cb5184a81..14ecb1176 100644 --- a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts +++ b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts @@ -10,8 +10,13 @@ export function lighthousePlugin( urls: LighthouseUrls, flags?: LighthouseOptions, ): PluginConfig { - const { skipAudits, onlyAudits, onlyCategories, ...unparsedFlags } = - normalizeFlags(flags ?? {}); + const { + skipAudits, + onlyAudits, + onlyCategories, + scoreTargets, + ...unparsedFlags + } = normalizeFlags(flags ?? {}); const { urls: normalizedUrls, context } = normalizeUrlInput(urls); @@ -40,5 +45,6 @@ export function lighthousePlugin( ...unparsedFlags, }), context, + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts index 8ea1735b1..753630bf8 100644 --- a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.unit.test.ts @@ -120,4 +120,14 @@ describe('lighthousePlugin-config-object', () => { }), ); }); + + it('should pass scoreTargets to PluginConfig when provided', () => { + const scoreTargets = { 'first-contentful-paint': 0.9 }; + const pluginConfig = lighthousePlugin('https://code-pushup-portal.com', { + scoreTargets, + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); }); diff --git a/packages/plugin-lighthouse/src/lib/types.ts b/packages/plugin-lighthouse/src/lib/types.ts index 2933287ab..3e1b2bb6a 100644 --- a/packages/plugin-lighthouse/src/lib/types.ts +++ b/packages/plugin-lighthouse/src/lib/types.ts @@ -1,4 +1,5 @@ import type { CliFlags } from 'lighthouse'; +import type { PluginScoreTargets } from '@code-pushup/models'; import type { ExcludeNullableProps } from '@code-pushup/utils'; import type { LIGHTHOUSE_GROUP_SLUGS } from './constants.js'; @@ -26,6 +27,7 @@ export type LighthouseOptions = ExcludeNullableProps< onlyGroups?: string | string[]; onlyAudits?: string | string[]; skipAudits?: string | string[]; + scoreTargets?: PluginScoreTargets; }; export type LighthouseGroupSlug = (typeof LIGHTHOUSE_GROUP_SLUGS)[number]; diff --git a/packages/plugin-typescript/src/lib/schema.ts b/packages/plugin-typescript/src/lib/schema.ts index ceb9c8959..ac4b9da48 100644 --- a/packages/plugin-typescript/src/lib/schema.ts +++ b/packages/plugin-typescript/src/lib/schema.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { pluginScoreTargetsSchema } from '@code-pushup/models'; import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js'; import type { AuditSlug } from './types.js'; @@ -15,6 +16,7 @@ export const typescriptPluginConfigSchema = z.object({ .array(z.enum(auditSlugs)) .describe('Filters TypeScript compiler errors by diagnostic codes') .optional(), + scoreTargets: pluginScoreTargetsSchema, }); export type TypescriptPluginOptions = z.input< diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts index c16f8fed9..dea7114cc 100644 --- a/packages/plugin-typescript/src/lib/schema.unit.test.ts +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -70,4 +70,13 @@ describe('typescriptPluginConfigSchema', () => { String.raw`Invalid option: expected one of \"syntax-errors\"|\"semantic-errors\"|`, ); }); + + it('should accept valid score targets', () => { + expect(() => + typescriptPluginConfigSchema.parse({ + tsconfig, + scoreTargets: { 'no-implicit-any-errors': 0.9 }, + }), + ).not.toThrow(); + }); }); diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index 588ce4d60..80c95f21a 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -17,9 +17,11 @@ const packageJson = createRequire(import.meta.url)( export async function typescriptPlugin( options?: TypescriptPluginOptions, ): Promise { - const { tsconfig = DEFAULT_TS_CONFIG, onlyAudits } = parseOptions( - options ?? {}, - ); + const { + tsconfig = DEFAULT_TS_CONFIG, + onlyAudits, + scoreTargets, + } = parseOptions(options ?? {}); const filteredAudits = getAudits({ onlyAudits }); const filteredGroups = getGroups({ onlyAudits }); @@ -40,6 +42,7 @@ export async function typescriptPlugin( tsconfig, expectedAudits: filteredAudits, }), + ...(scoreTargets && { scoreTargets }), }; } diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts index 838298b7d..f3425389a 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -37,4 +37,14 @@ describe('typescriptPlugin-config-object', () => { } as unknown as TypescriptPluginOptions), ).rejects.toThrow(/invalid_type/); }); + + it('should pass scoreTargets to PluginConfig when provided', async () => { + const scoreTargets = { 'no-implicit-any-errors': 0.9 }; + const pluginConfig = await typescriptPlugin({ + scoreTargets, + }); + + expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); + expect(pluginConfig.scoreTargets).toStrictEqual(scoreTargets); + }); });