Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 8 additions & 1 deletion packages/core/src/lib/implementation/execute-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
groupByStatus,
logMultipleResults,
pluralizeToken,
scoreAuditsWithTarget,
} from '@code-pushup/utils';
import {
executePluginRunner,
Expand Down Expand Up @@ -57,6 +58,7 @@ export async function executePlugin(
description,
docsUrl,
groups,
scoreTarget,
...pluginMeta
} = pluginConfig;
const { write: cacheWrite = false, read: cacheRead = false } = cache;
Expand All @@ -76,8 +78,13 @@ export async function executePlugin(
});
}

// transform audit scores to 1 when they meet/exceed scoreTarget
Comment thread
matejchalk marked this conversation as resolved.
Outdated
const transformedAudits = scoreTarget
? scoreAuditsWithTarget(audits, scoreTarget)
: audits;

// enrich `AuditOutputs` to `AuditReport`
const auditReports: AuditReport[] = audits.map(
const auditReports: AuditReport[] = transformedAudits.map(
Comment thread
matejchalk marked this conversation as resolved.
Outdated
(auditOutput: AuditOutput) => ({
...auditOutput,
...(pluginConfigAudits.find(
Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/lib/implementation/execute-plugin.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,67 @@ describe('executePlugin', () => {
MINIMAL_PLUGIN_CONFIG_MOCK,
);
});

it('should apply a single score target to all audits', async () => {
const pluginConfig: PluginConfig = {
...MINIMAL_PLUGIN_CONFIG_MOCK,
scoreTarget: 0.8,
audits: [
{
slug: 'speed-index',
title: 'Speed Index',
},
{
slug: 'total-blocking-time',
title: 'Total Blocking Time',
},
],
runner: () => [
{ slug: 'speed-index', score: 0.9, value: 1300 },
{ slug: 'total-blocking-time', score: 0.3, value: 600 },
],
};

const result = await executePlugin(pluginConfig, {
persist: { outputDir: '' },
cache: { read: false, write: false },
});

expect(result.audits).toEqual(
expect.arrayContaining([
expect.objectContaining({
slug: 'speed-index',
score: 1,
scoreTarget: 0.8,
}),
expect.objectContaining({
slug: 'total-blocking-time',
score: 0.3,
scoreTarget: 0.8,
}),
]),
);
});

it('should apply per-audit score targets', async () => {
const pluginConfig: PluginConfig = {
...MINIMAL_PLUGIN_CONFIG_MOCK, // returns node-version audit with score 0.3
scoreTarget: {
'node-version': 0.2,
},
};

const result = await executePlugin(pluginConfig, {
persist: { outputDir: '' },
cache: { read: false, write: false },
});

expect(result.audits[0]).toMatchObject({
slug: 'node-version',
score: 1,
scoreTarget: 0.2,
});
});
});

describe('executePlugins', () => {
Expand Down
31 changes: 17 additions & 14 deletions packages/models/docs/models-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ _Object containing the following properties:_
| `displayValue` | Formatted value (e.g. '0.9 s', '2.1 MB') | `string` |
| **`value`** (\*) | Raw numeric value | `number` (_≥0_) |
| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) |
| `scoreTarget` | Pass/fail score threshold (0-1) | `number` (_≥0, ≤1_) |
| `details` | Detailed information | [AuditDetails](#auditdetails) |

_(\*) Required._
Expand All @@ -73,6 +74,7 @@ _Object containing the following properties:_
| `displayValue` | Formatted value (e.g. '0.9 s', '2.1 MB') | `string` |
| **`value`** (\*) | Raw numeric value | `number` (_≥0_) |
| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) |
| `scoreTarget` | Pass/fail score threshold (0-1) | `number` (_≥0, ≤1_) |
| `details` | Detailed information | [AuditDetails](#auditdetails) |

_(\*) Required._
Expand Down Expand Up @@ -1282,20 +1284,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 | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
| **`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_ |
| `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 | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
| **`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_ |
| `scoreTarget` | 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) |

_(\*) Required._

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 @@ -80,6 +80,7 @@ export {
type PluginConfig,
type PluginContext,
type PluginMeta,
type PluginScoreTarget,
} from './lib/plugin-config.js';
export {
auditReportSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/models/src/lib/audit-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createDuplicateSlugsCheck } from './implementation/checks.js';
import {
nonnegativeNumberSchema,
scoreSchema,
scoreTargetSchema,
slugSchema,
} from './implementation/schemas.js';
import { issueSchema } from './issue.js';
Expand Down Expand Up @@ -34,6 +35,7 @@ export const auditOutputSchema = z
displayValue: auditDisplayValueSchema,
value: auditValueSchema,
score: scoreSchema,
scoreTarget: scoreTargetSchema,
details: auditDetailsSchema.optional(),
})
.describe('Audit information');
Expand Down
12 changes: 12 additions & 0 deletions packages/models/src/lib/audit-output.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ describe('auditOutputSchema', () => {
).not.toThrow();
});

it('should accept a valid audit output with a score target', () => {
expect(() =>
auditOutputSchema.parse({
slug: 'total-blocking-time',
score: 0.91,
scoreTarget: 0.9,
value: 183.5,
displayValue: '180 ms',
} satisfies AuditOutput),
).not.toThrow();
});

it('should accept a decimal value', () => {
expect(() =>
auditOutputSchema.parse({
Expand Down
4 changes: 2 additions & 2 deletions packages/models/src/lib/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export const auditSchema = z
.object({
slug: slugSchema.describe('ID (unique within plugin)'),
})
.merge(
.extend(
metaSchema({
titleDescription: 'Descriptive name',
descriptionDescription: 'Description (markdown)',
docsUrlDescription: 'Link to documentation (rationale)',
description: 'List of scorable metrics for the given plugin',
isSkippedDescription: 'Indicates whether the audit is skipped',
}),
}).shape,
);

export type Audit = z.infer<typeof auditSchema>;
Expand Down
9 changes: 2 additions & 7 deletions packages/models/src/lib/category-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
} from './implementation/checks.js';
import {
metaSchema,
nonnegativeNumberSchema,
scorableSchema,
scoreTargetSchema,
slugSchema,
weightedRefSchema,
} from './implementation/schemas.js';
Expand Down Expand Up @@ -44,12 +44,7 @@ export const categoryConfigSchema = scorableSchema(
description: 'Meta info for category',
}).shape,
)
.extend({
scoreTarget: nonnegativeNumberSchema
.max(1)
.describe('Pass/fail score threshold (0-1)')
.optional(),
});
.extend({ scoreTarget: scoreTargetSchema });

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

Expand Down
24 changes: 24 additions & 0 deletions packages/models/src/lib/category-config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ describe('categoryConfigSchema', () => {
).not.toThrow();
});

it('should accept a valid category configuration with a score target', () => {
expect(() =>
categoryConfigSchema.parse({
slug: 'core-web-vitals',
title: 'Core Web Vitals',
scoreTarget: 0.9,
refs: [
{
plugin: 'lighthouse',
slug: 'largest-contentful-paint',
type: 'audit',
weight: 3,
},
{
plugin: 'lighthouse',
slug: 'first-input-delay',
type: 'audit',
weight: 2,
},
],
} satisfies CategoryConfig),
).not.toThrow();
});

it('should throw for an empty category', () => {
expect(() =>
categoryConfigSchema.parse({
Expand Down
8 changes: 7 additions & 1 deletion packages/models/src/lib/core-config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,13 @@ describe('coreConfigSchema', () => {
slug: 'lighthouse',
title: 'Lighthouse',
icon: 'lighthouse',
runner: { command: 'npm run lint', outputFile: 'output.json' },
runner: async () => [
{
slug: 'csp-xss',
score: 1,
value: 1,
},
],
audits: [
{
slug: 'csp-xss',
Expand Down
6 changes: 6 additions & 0 deletions packages/models/src/lib/implementation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export function packageVersionSchema<
}>;
}

/** Schema for a binary score threshold */
export const scoreTargetSchema = nonnegativeNumberSchema
.max(1)
.describe('Pass/fail score threshold (0-1)')
.optional();

/** Schema for a weight */
export const weightSchema = nonnegativeNumberSchema.describe(
'Coefficient for the given score (use weight 0 if only for display)',
Expand Down
30 changes: 21 additions & 9 deletions packages/models/src/lib/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
materialIconSchema,
metaSchema,
packageVersionSchema,
scoreTargetSchema,
slugSchema,
} from './implementation/schemas.js';
import { formatSlugsList, hasMissingStrings } from './implementation/utils.js';
Expand All @@ -18,31 +19,42 @@ export const pluginContextSchema = z
export type PluginContext = z.infer<typeof pluginContextSchema>;

export const pluginMetaSchema = packageVersionSchema()
.merge(
.extend(
metaSchema({
titleDescription: 'Descriptive name',
descriptionDescription: 'Description (markdown)',
docsUrlDescription: 'Plugin documentation site',
description: 'Plugin metadata',
}),
}).shape,
)
.merge(
z.object({
slug: slugSchema.describe('Unique plugin slug within core config'),
icon: materialIconSchema,
}),
);
.extend({
slug: slugSchema.describe('Unique plugin slug within core config'),
icon: materialIconSchema,
});
export type PluginMeta = z.infer<typeof pluginMetaSchema>;

export const pluginScoreTargetSchema = z
.union([
scoreTargetSchema,
z.record(z.string(), scoreTargetSchema.nonoptional()),
])
.describe(
'Score targets that trigger a perfect score. Number for all audits or record { slug: target } for specific audits',
)
.optional();

export type PluginScoreTarget = z.infer<typeof pluginScoreTargetSchema>;

export const pluginDataSchema = z.object({
runner: z.union([runnerConfigSchema, runnerFunctionSchema]),
audits: pluginAuditsSchema,
groups: groupsSchema,
scoreTarget: pluginScoreTargetSchema,
Comment thread
matejchalk marked this conversation as resolved.
Outdated
context: pluginContextSchema,
});

export const pluginConfigSchema = pluginMetaSchema
.merge(pluginDataSchema)
.extend(pluginDataSchema.shape)
.check(createCheck(findMissingSlugsInGroupRefs));

export type PluginConfig = z.infer<typeof pluginConfigSchema>;
Expand Down
29 changes: 29 additions & 0 deletions packages/models/src/lib/plugin-config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@ describe('pluginConfigSchema', () => {
).not.toThrow();
});

it('should accept a valid plugin configuration with a score target', () => {
expect(() =>
pluginConfigSchema.parse({
slug: 'lighthouse',
title: 'Lighthouse',
icon: 'lighthouse',
runner: async () => [
{
slug: 'first-contentful-paint',
score: 0.28,
value: 3752,
displayValue: '3.8 s',
},
{
slug: 'total-blocking-time',
score: 0.91,
value: 183.5,
displayValue: '180 ms',
},
],
scoreTarget: { 'total-blocking-time': 0.9 },
audits: [
{ slug: 'first-contentful-paint', title: 'First Contentful Paint' },
{ slug: 'total-blocking-time', title: 'Total Blocking Time' },
],
} satisfies PluginConfig),
).not.toThrow();
});

it('should throw for a plugin configuration without audits', () => {
expect(() =>
pluginConfigSchema.parse({
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export {
} from './lib/reports/generate-md-reports-diff.js';
export { loadReport } from './lib/reports/load-report.js';
export { logStdoutSummary } from './lib/reports/log-stdout-summary.js';
export { scoreReport } from './lib/reports/scoring.js';
export { scoreReport, scoreAuditsWithTarget } from './lib/reports/scoring.js';
export { sortReport } from './lib/reports/sorting.js';
export type {
ScoredCategoryConfig,
Expand Down
Loading
Loading