Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 1 addition & 3 deletions packages/ci/src/lib/portal/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 }),
};
}

Expand Down
2 changes: 0 additions & 2 deletions packages/ci/src/lib/portal/transform.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('transformGQLReport', () => {
{
slug: 'code-style',
title: 'Code style',
isBinary: false,
score: 0.5,
refs: [
{
Expand All @@ -41,7 +40,6 @@ describe('transformGQLReport', () => {
{
slug: 'bundle-size',
title: 'Bundle size',
isBinary: false,
score: 0.75,
refs: [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/lib/implementation/report-to-gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
40 changes: 25 additions & 15 deletions packages/models/docs/models-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export {
pluginConfigSchema,
pluginContextSchema,
pluginMetaSchema,
pluginScoreTargetsSchema,
type PluginConfig,
type PluginContext,
type PluginMeta,
Expand Down
4 changes: 2 additions & 2 deletions packages/models/src/lib/plugin-config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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' },
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 2 additions & 8 deletions packages/plugin-coverage/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -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<typeof coverageTypeSchema>;
Expand Down Expand Up @@ -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<typeof coveragePluginConfigSchema>;
export type FinalCoveragePluginConfig = z.infer<
Expand Down
24 changes: 21 additions & 3 deletions packages/plugin-coverage/src/lib/config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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'],
Expand Down Expand Up @@ -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');
});
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-coverage/src/lib/coverage-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -76,5 +78,6 @@ export async function coveragePlugin(
audits,
groups: [group],
runner: await createRunnerConfig(runnerScriptPath, coverageConfig),
...(scoreTargets && { scoreTargets }),
};
}
13 changes: 12 additions & 1 deletion packages/plugin-coverage/src/lib/coverage-plugin.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => ({
Expand Down Expand Up @@ -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);
});
});
13 changes: 1 addition & 12 deletions packages/plugin-coverage/src/lib/runner/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -68,8 +63,6 @@ export async function createRunnerConfig(
JSON.stringify(config),
);

const threshold = config.perfectScoreThreshold;

return {
command: 'node',
args: [
Expand All @@ -78,9 +71,5 @@ export async function createRunnerConfig(
],
configFile: runnerConfigPath,
outputFile: runnerOutputPath,
...(threshold != null && {
outputTransform: outputs =>
applyMaxScoreAboveThreshold(outputs as AuditOutputs, threshold),
}),
};
}
5 changes: 2 additions & 3 deletions packages/plugin-coverage/src/lib/runner/runner.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RunnerConfig>({
Expand All @@ -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'),
});
Expand All @@ -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,
};

Expand Down
16 changes: 0 additions & 16 deletions packages/plugin-coverage/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { AuditOutputs } from '@code-pushup/models';
import type { CoverageType } from './config.js';

export const coverageDescription: Record<CoverageType, string> = {
Expand All @@ -8,21 +7,6 @@ export const coverageDescription: Record<CoverageType, string> = {
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<CoverageType, number> = {
/* eslint-disable @typescript-eslint/no-magic-numbers */
function: 6,
Expand Down
Loading
Loading