Skip to content

Commit e2ce3f6

Browse files
committed
feat(plugin-js-packages): implement plugin schema and configuration flow
1 parent 4020267 commit e2ce3f6

File tree

10 files changed

+495
-1
lines changed

10 files changed

+495
-1
lines changed

packages/plugin-js-packages/README.md

+130-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,135 @@
44
[![downloads](https://img.shields.io/npm/dm/%40code-pushup%2Fjs-packages-plugin)](https://npmtrends.com/@code-pushup/js-packages-plugin)
55
[![dependencies](https://img.shields.io/librariesio/release/npm/%40code-pushup/js-packages-plugin)](https://www.npmjs.com/package/@code-pushup/js-packages-plugin?activeTab=dependencies)
66

7-
🧪 **Code PushUp plugin for JavaScript packages.**
7+
📦 **Code PushUp plugin for JavaScript packages.** 🛡
88

99
This plugin allows you to list outdated dependencies and run audit for known vulnerabilities.
10+
It supports the following package managers: npm, yarn, yarn berry, pnpm.
11+
12+
## Getting started
13+
14+
1. If you haven't already, install [@code-pushup/cli](../cli/README.md) and create a configuration file.
15+
16+
2. Insert plugin configuration. By default, npm audit and npm outdated commands will be run.
17+
18+
Default configuration will look as follows:
19+
20+
```js
21+
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';
22+
23+
export default {
24+
// ...
25+
plugins: [
26+
// ...
27+
await jsPackagesPlugin(),
28+
],
29+
};
30+
```
31+
32+
You may run this plugin with a custom configuration for any supported package manager or command.
33+
34+
A custom configuration will look similarly to the following:
35+
36+
```js
37+
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';
38+
39+
export default {
40+
// ...
41+
plugins: [
42+
// ...
43+
await jsPackagesPlugin({ packageManager: ['yarn'], features: ['audit'] }),
44+
],
45+
};
46+
```
47+
48+
3. (Optional) Reference individual audits or the provided plugin group which you wish to include in custom categories (use `npx code-pushup print-config` to list audits and groups).
49+
50+
💡 Assign weights based on what influence each command should have on the overall category score (assign weight 0 to only include as extra info, without influencing category score).
51+
52+
```js
53+
export default {
54+
// ...
55+
categories: [
56+
{
57+
slug: 'dependencies',
58+
title: 'Package dependencies',
59+
refs: [
60+
{
61+
type: 'group',
62+
plugin: 'npm-package-manager', // replace prefix with your package manager
63+
slug: 'js-packages',
64+
weight: 1,
65+
},
66+
],
67+
},
68+
// ...
69+
],
70+
};
71+
```
72+
73+
4. Run the CLI with `npx code-pushup collect` and view or upload report (refer to [CLI docs](../cli/README.md)).
74+
75+
## Plugin architecture
76+
77+
### Plugin configuration specification
78+
79+
The plugin accepts the following parameters:
80+
81+
- (optional) `packageManager`: The package manager you are using. Supported values: `npm`, `yarn` (v1), `yarn-berry` (v2+), `pnpm`. Default is `npm`.
82+
- (optional) `features`: Array of commands to be run. Supported commands: `audit`, `outdated`. Both are configured by default.
83+
- (optional) `auditLevelMapping`: If you wish to set a custom level of issue severity based on audit vulnerability level, you may do so here. Any omitted values will be filled in by defaults. Audit levels are: `critical`, `high`, `moderate`, `low` and `info`. Issue severities are: `error`, `warn` and `info`. By default the mapping is as follows: `critical` and `high``error`; `moderate` and `low``warning`; `info``info`.
84+
85+
> [!NOTE]
86+
> All parameters are optional so the plugin can be called with no arguments in the default setting.
87+
88+
### Audits and group
89+
90+
This plugin provides a group for convenient declaration in your config. When defined this way, all measured coverage type audits have the same weight.
91+
92+
```ts
93+
// ...
94+
categories: [
95+
{
96+
slug: 'dependencies',
97+
title: 'Package dependencies',
98+
refs: [
99+
{
100+
type: 'group',
101+
plugin: 'js-packages',
102+
slug: 'npm-package-manager', // replace prefix with your package manager
103+
weight: 1,
104+
},
105+
// ...
106+
],
107+
},
108+
// ...
109+
],
110+
```
111+
112+
Each package manager command still has its own audit. So when you want to include a subset of commands or assign different weights to them, you can do so in the following way:
113+
114+
```ts
115+
// ...
116+
categories: [
117+
{
118+
slug: 'dependencies',
119+
title: 'Package dependencies',
120+
refs: [
121+
{
122+
type: 'audit',
123+
plugin: 'js-packages',
124+
slug: 'npm-audit', // replace prefix with your package manager
125+
weight: 2,
126+
},
127+
{
128+
type: 'audit',
129+
plugin: 'js-packages',
130+
slug: 'npm-outdated', // replace prefix with your package manager
131+
weight: 1,
132+
},
133+
// ...
134+
],
135+
},
136+
// ...
137+
],
138+
```

packages/plugin-js-packages/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.26.1",
44
"dependencies": {
55
"@code-pushup/models": "*",
6+
"@code-pushup/utils": "*",
67
"zod": "^3.22.4"
78
}
89
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { jsPackagesPlugin } from './lib/js-packages-plugin';
2+
3+
export default jsPackagesPlugin;
4+
export type { JSPackagesPluginConfig } from './lib/config';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { z } from 'zod';
2+
import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models';
3+
4+
const packageCommandSchema = z.enum(['audit', 'outdated']);
5+
export type PackageCommand = z.infer<typeof packageCommandSchema>;
6+
7+
const packageManagerSchema = z.enum(['npm', 'yarn', 'yarn-berry', 'pnpm']);
8+
export type PackageManager = z.infer<typeof packageManagerSchema>;
9+
10+
const packageAuditLevelSchema = z.enum([
11+
'info',
12+
'low',
13+
'moderate',
14+
'high',
15+
'critical',
16+
]);
17+
export type PackageAuditLevel = z.infer<typeof packageAuditLevelSchema>;
18+
19+
const defaultAuditLevelMapping: Record<PackageAuditLevel, IssueSeverity> = {
20+
critical: 'error',
21+
high: 'error',
22+
moderate: 'warning',
23+
low: 'warning',
24+
info: 'info',
25+
};
26+
27+
export function fillAuditLevelMapping(
28+
mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
29+
): Record<PackageAuditLevel, IssueSeverity> {
30+
return {
31+
critical: mapping.critical ?? defaultAuditLevelMapping.critical,
32+
high: mapping.high ?? defaultAuditLevelMapping.high,
33+
moderate: mapping.moderate ?? defaultAuditLevelMapping.moderate,
34+
low: mapping.low ?? defaultAuditLevelMapping.low,
35+
info: mapping.info ?? defaultAuditLevelMapping.info,
36+
};
37+
}
38+
39+
// TODO how?
40+
// export function objectKeys<T extends object>(obj: T): (keyof T)[] {
41+
// return Object.keys(obj) as (keyof T)[];
42+
// }
43+
44+
// function newFillAuditLevelMapping(
45+
// mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
46+
// ): Record<PackageAuditLevel, IssueSeverity> {
47+
// return Object.fromEntries(
48+
// objectKeys(defaultAuditLevelMapping).map<
49+
// [PackageAuditLevel, IssueSeverity]
50+
// >(auditLevel => [
51+
// auditLevel,
52+
// mapping[auditLevel] ?? defaultAuditLevelMapping[auditLevel],
53+
// ]),
54+
// );
55+
// }
56+
57+
export const jsPackagesPluginConfigSchema = z.object({
58+
features: z
59+
.array(packageCommandSchema, {
60+
description:
61+
'Package manager commands to be run. Defaults to both audit and outdated.',
62+
})
63+
.min(1)
64+
.default(['audit', 'outdated']),
65+
packageManager: packageManagerSchema
66+
.describe('Package manager to be used. Defaults to npm')
67+
.default('npm'),
68+
auditLevelMapping: z
69+
.record(packageAuditLevelSchema, issueSeveritySchema, {
70+
description:
71+
'Mapping of audit levels to issue severity. Custom mapping or overrides may be entered manually, otherwise has a default preset.',
72+
})
73+
.default(defaultAuditLevelMapping)
74+
.transform(fillAuditLevelMapping),
75+
});
76+
77+
export type JSPackagesPluginConfig = z.input<
78+
typeof jsPackagesPluginConfigSchema
79+
>;
80+
81+
export type FinalJSPackagesPluginConfig = z.infer<
82+
typeof jsPackagesPluginConfigSchema
83+
>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { IssueSeverity } from '@code-pushup/models';
3+
import {
4+
FinalJSPackagesPluginConfig,
5+
JSPackagesPluginConfig,
6+
PackageAuditLevel,
7+
fillAuditLevelMapping,
8+
jsPackagesPluginConfigSchema,
9+
} from './config';
10+
11+
describe('jsPackagesPluginConfigSchema', () => {
12+
it('should accept a JS package configuration with all entities', () => {
13+
expect(() =>
14+
jsPackagesPluginConfigSchema.parse({
15+
auditLevelMapping: { moderate: 'error' },
16+
features: ['audit'],
17+
packageManager: 'yarn',
18+
} satisfies JSPackagesPluginConfig),
19+
).not.toThrow();
20+
});
21+
22+
it('should accept a minimal JS package configuration', () => {
23+
expect(() => jsPackagesPluginConfigSchema.parse({})).not.toThrow();
24+
});
25+
26+
it('should fill in default values', () => {
27+
const config = jsPackagesPluginConfigSchema.parse({});
28+
expect(config).toEqual<FinalJSPackagesPluginConfig>({
29+
features: ['audit', 'outdated'],
30+
packageManager: 'npm',
31+
auditLevelMapping: {
32+
critical: 'error',
33+
high: 'error',
34+
moderate: 'warning',
35+
low: 'warning',
36+
info: 'info',
37+
},
38+
});
39+
});
40+
41+
it('should throw for no features', () => {
42+
expect(() => jsPackagesPluginConfigSchema.parse({ features: [] })).toThrow(
43+
'too_small',
44+
);
45+
});
46+
});
47+
48+
describe('fillAuditLevelMapping', () => {
49+
it('should fill in defaults', () => {
50+
expect(fillAuditLevelMapping({})).toEqual<
51+
Record<PackageAuditLevel, IssueSeverity>
52+
>({
53+
critical: 'error',
54+
high: 'error',
55+
moderate: 'warning',
56+
low: 'warning',
57+
info: 'info',
58+
});
59+
});
60+
61+
it('should override mapping for given values', () => {
62+
expect(fillAuditLevelMapping({ high: 'warning', low: 'info' })).toEqual<
63+
Record<PackageAuditLevel, IssueSeverity>
64+
>({
65+
critical: 'error',
66+
high: 'warning',
67+
moderate: 'warning',
68+
low: 'info',
69+
info: 'info',
70+
});
71+
});
72+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { dirname, join } from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import { Audit, Group, PluginConfig } from '@code-pushup/models';
4+
import { name, version } from '../../package.json';
5+
import {
6+
JSPackagesPluginConfig,
7+
PackageCommand,
8+
jsPackagesPluginConfigSchema,
9+
} from './config';
10+
import { createRunnerConfig } from './runner';
11+
import { auditDocs, outdatedDocs, pkgManagerDocs } from './utils';
12+
13+
/**
14+
* Instantiates Code PushUp JS packages plugin for core config.
15+
*
16+
* @example
17+
* import coveragePlugin from '@code-pushup/js-packages-plugin'
18+
*
19+
* export default {
20+
* // ... core config ...
21+
* plugins: [
22+
* // ... other plugins ...
23+
* await jsPackagesPlugin()
24+
* ]
25+
* }
26+
*
27+
* @returns Plugin configuration.
28+
*/
29+
export async function jsPackagesPlugin(
30+
config: JSPackagesPluginConfig = {},
31+
): Promise<PluginConfig> {
32+
const jsPackagesPluginConfig = jsPackagesPluginConfigSchema.parse(config);
33+
const pkgManager = jsPackagesPluginConfig.packageManager;
34+
const features = [...new Set(jsPackagesPluginConfig.features)];
35+
36+
const runnerScriptPath = join(
37+
fileURLToPath(dirname(import.meta.url)),
38+
'bin.js',
39+
);
40+
41+
const audits: Record<PackageCommand, Audit> = {
42+
audit: {
43+
slug: `${pkgManager}-audit`,
44+
title: `${pkgManager} audit`,
45+
description: `Lists ${pkgManager} audit vulnerabilities.`,
46+
docsUrl: auditDocs[pkgManager],
47+
},
48+
outdated: {
49+
slug: `${pkgManager}-outdated`,
50+
title: `${pkgManager} outdated dependencies`,
51+
description: `Lists ${pkgManager} outdated dependencies.`,
52+
docsUrl: outdatedDocs[pkgManager],
53+
},
54+
};
55+
56+
const group: Group = {
57+
slug: `${pkgManager}-package-manager`,
58+
title: `${pkgManager} package manager`,
59+
description: `Group containing both audit and dependencies command audits for the ${pkgManager} package manager.`,
60+
docsUrl: pkgManagerDocs[pkgManager],
61+
refs: features.map(feature => ({
62+
slug: `${pkgManager}-${feature}`,
63+
weight: 1,
64+
})),
65+
};
66+
67+
return {
68+
slug: 'js-packages',
69+
title: 'Plugin for JS packages',
70+
icon:
71+
pkgManager === 'npm' ? 'npm' : pkgManager === 'pnpm' ? 'pnpm' : 'yarn',
72+
description:
73+
'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic and berry, pnpm package managers.',
74+
docsUrl: pkgManagerDocs[pkgManager],
75+
packageName: name,
76+
version,
77+
audits: features.map(feature => audits[feature]),
78+
groups: [group],
79+
runner: await createRunnerConfig(runnerScriptPath, jsPackagesPluginConfig),
80+
};
81+
}

0 commit comments

Comments
 (0)