Skip to content

Commit 74241e8

Browse files
committed
refactor(plugin-js-packages): make optional dependencies opt-in, adjust weights
1 parent 94ad38a commit 74241e8

File tree

11 files changed

+156
-62
lines changed

11 files changed

+156
-62
lines changed

packages/plugin-js-packages/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ It supports the following package managers:
5959
// ...
6060
plugins: [
6161
// ...
62-
await jsPackagesPlugin({ packageManager: ['yarn'], checks: ['audit'] }),
62+
await jsPackagesPlugin({ packageManager: ['yarn-classic'], checks: ['audit'], dependencyGroups: ['prod'] }),
6363
],
6464
};
6565
```
@@ -112,11 +112,12 @@ The plugin accepts the following parameters:
112112

113113
- `packageManager`: The package manager you are using. Supported values: `npm`, `yarn-classic` (v1), `yarn-modern` (v2+), `pnpm`.
114114
- (optional) `checks`: Array of checks to be run. Supported commands: `audit`, `outdated`. Both are configured by default.
115+
- (optional) `dependencyGroups`: Array of dependency groups to be checked. `prod` and `dev` are configured by default. `optional` are opt-in.
115116
- (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`.
116117

117118
### Audits and group
118119

119-
This plugin provides a group per check for a convenient declaration in your config. Each group contains audits for all supported groups of dependencies (`prod`, `dev` and `optional`).
120+
This plugin provides a group per check for a convenient declaration in your config. Each group contains audits for all selected groups of dependencies that are supported (`prod`, `dev` or `optional`).
120121

121122
```ts
122123
// ...
@@ -144,7 +145,7 @@ This plugin provides a group per check for a convenient declaration in your conf
144145
],
145146
```
146147

147-
Each dependency group has its own audit. If you want to check only a subset of dependencies (e.g. run audit and outdated for production dependencies) or assign different weights to them, you can do so in the following way:
148+
Each dependency group has its own audit. If you want to assign different weights to the audits or record different dependency groups for different checks (the bigger set needs to be included in the plugin configuration), you can do so in the following way:
148149

149150
```ts
150151
// ...

packages/plugin-js-packages/src/lib/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models';
33
import { defaultAuditLevelMapping } from './constants';
44

55
export const dependencyGroups = ['prod', 'dev', 'optional'] as const;
6+
const dependencyGroupSchema = z.enum(dependencyGroups);
67
export type DependencyGroup = (typeof dependencyGroups)[number];
78

89
const packageCommandSchema = z.enum(['audit', 'outdated']);
@@ -51,6 +52,10 @@ export const jsPackagesPluginConfigSchema = z.object({
5152
packageManager: packageManagerIdSchema.describe(
5253
'Package manager to be used.',
5354
),
55+
dependencyGroups: z
56+
.array(dependencyGroupSchema)
57+
.min(1)
58+
.default(['prod', 'dev']),
5459
auditLevelMapping: z
5560
.record(packageAuditLevelSchema, issueSeveritySchema, {
5661
description:

packages/plugin-js-packages/src/lib/config.unit.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('jsPackagesPluginConfigSchema', () => {
1414
auditLevelMapping: { moderate: 'error' },
1515
checks: ['audit'],
1616
packageManager: 'yarn-classic',
17+
dependencyGroups: ['prod'],
1718
} satisfies JSPackagesPluginConfig),
1819
).not.toThrow();
1920
});
@@ -33,6 +34,7 @@ describe('jsPackagesPluginConfigSchema', () => {
3334
expect(config).toEqual<FinalJSPackagesPluginConfig>({
3435
checks: ['audit', 'outdated'],
3536
packageManager: 'npm',
37+
dependencyGroups: ['prod', 'dev'],
3638
auditLevelMapping: {
3739
critical: 'error',
3840
high: 'error',
@@ -51,6 +53,15 @@ describe('jsPackagesPluginConfigSchema', () => {
5153
}),
5254
).toThrow('too_small');
5355
});
56+
57+
it('should throw for no passed dependency group', () => {
58+
expect(() =>
59+
jsPackagesPluginConfigSchema.parse({
60+
packageManager: 'yarn-classic',
61+
dependencyGroups: [],
62+
}),
63+
).toThrow('too_small');
64+
});
5465
});
5566

5667
describe('fillAuditLevelMapping', () => {

packages/plugin-js-packages/src/lib/constants.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ export const dependencyGroupToLong: Record<
2222
optional: 'optionalDependencies',
2323
};
2424

25+
/* eslint-disable no-magic-numbers */
2526
export const dependencyGroupWeights: Record<DependencyGroup, number> = {
26-
// eslint-disable-next-line no-magic-numbers
27-
prod: 3,
28-
dev: 1,
29-
optional: 1,
27+
prod: 80,
28+
dev: 15,
29+
optional: 5,
3030
};
31+
/* eslint-enable no-magic-numbers */
3132

3233
export const dependencyDocs: Record<DependencyGroup, string> = {
3334
prod: 'https://classic.yarnpkg.com/docs/dependency-types#toc-dependencies',

packages/plugin-js-packages/src/lib/js-packages-plugin.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function jsPackagesPlugin(
3636
): Promise<PluginConfig> {
3737
const jsPackagesPluginConfig = jsPackagesPluginConfigSchema.parse(config);
3838
const checks = [...new Set(jsPackagesPluginConfig.checks)];
39+
const depGroups = [...new Set(jsPackagesPluginConfig.dependencyGroups)];
3940
const id = jsPackagesPluginConfig.packageManager;
4041
const pm = packageManagers[id];
4142

@@ -53,23 +54,31 @@ export async function jsPackagesPlugin(
5354
docsUrl: pm.docs.homepage,
5455
packageName: name,
5556
version,
56-
audits: createAudits(id, checks),
57-
groups: createGroups(id, checks),
57+
audits: createAudits(id, checks, depGroups),
58+
groups: createGroups(id, checks, depGroups),
5859
runner: await createRunnerConfig(runnerScriptPath, jsPackagesPluginConfig),
5960
};
6061
}
6162

62-
function createGroups(id: PackageManagerId, checks: PackageCommand[]): Group[] {
63+
function createGroups(
64+
id: PackageManagerId,
65+
checks: PackageCommand[],
66+
depGroups: DependencyGroup[],
67+
): Group[] {
6368
const pm = packageManagers[id];
6469
const supportedAuditDepGroups =
6570
pm.audit.supportedDepGroups ?? dependencyGroups;
71+
const compatibleAuditDepGroups = depGroups.filter(group =>
72+
supportedAuditDepGroups.includes(group),
73+
);
74+
6675
const groups: Record<PackageCommand, Group> = {
6776
audit: {
6877
slug: `${pm.slug}-audit`,
6978
title: `${pm.name} audit`,
7079
description: `Group containing ${pm.name} vulnerabilities.`,
7180
docsUrl: pm.docs.audit,
72-
refs: supportedAuditDepGroups.map(depGroup => ({
81+
refs: compatibleAuditDepGroups.map(depGroup => ({
7382
slug: `${pm.slug}-audit-${depGroup}`,
7483
weight: dependencyGroupWeights[depGroup],
7584
})),
@@ -79,7 +88,7 @@ function createGroups(id: PackageManagerId, checks: PackageCommand[]): Group[] {
7988
title: `${pm.name} outdated dependencies`,
8089
description: `Group containing outdated ${pm.name} dependencies.`,
8190
docsUrl: pm.docs.outdated,
82-
refs: dependencyGroups.map(depGroup => ({
91+
refs: depGroups.map(depGroup => ({
8392
slug: `${pm.slug}-outdated-${depGroup}`,
8493
weight: dependencyGroupWeights[depGroup],
8594
})),
@@ -89,15 +98,22 @@ function createGroups(id: PackageManagerId, checks: PackageCommand[]): Group[] {
8998
return checks.map(check => groups[check]);
9099
}
91100

92-
function createAudits(id: PackageManagerId, checks: PackageCommand[]): Audit[] {
101+
function createAudits(
102+
id: PackageManagerId,
103+
checks: PackageCommand[],
104+
depGroups: DependencyGroup[],
105+
): Audit[] {
93106
const { slug } = packageManagers[id];
94107
return checks.flatMap(check => {
95-
const supportedDepGroups =
108+
const supportedAuditDepGroups =
109+
packageManagers[id].audit.supportedDepGroups ?? dependencyGroups;
110+
111+
const compatibleDepGroups =
96112
check === 'audit'
97-
? packageManagers[id].audit.supportedDepGroups ?? dependencyGroups
98-
: dependencyGroups;
113+
? depGroups.filter(group => supportedAuditDepGroups.includes(group))
114+
: depGroups;
99115

100-
return supportedDepGroups.map(depGroup => ({
116+
return compatibleDepGroups.map(depGroup => ({
101117
slug: `${slug}-${check}-${depGroup}`,
102118
title: getAuditTitle(slug, check, depGroup),
103119
description: getAuditDescription(check, depGroup),

packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,53 +55,78 @@ describe('jsPackagesPlugin', () => {
5555
);
5656
});
5757

58-
it('should create an audit for each dependency group', async () => {
58+
it('should create an audit for default dependency groups', async () => {
5959
await expect(
60-
jsPackagesPlugin({ packageManager: 'yarn-classic', checks: ['audit'] }),
60+
jsPackagesPlugin({
61+
packageManager: 'yarn-classic',
62+
checks: ['audit'],
63+
}),
6164
).resolves.toStrictEqual(
6265
expect.objectContaining({
6366
audits: [
6467
expect.objectContaining({ slug: 'yarn-classic-audit-prod' }),
6568
expect.objectContaining({ slug: 'yarn-classic-audit-dev' }),
66-
expect.objectContaining({ slug: 'yarn-classic-audit-optional' }),
6769
],
6870
groups: [
6971
expect.objectContaining<Partial<Group>>({
7072
slug: 'yarn-classic-audit',
7173
refs: [
72-
{ slug: 'yarn-classic-audit-prod', weight: 3 },
73-
{ slug: 'yarn-classic-audit-dev', weight: 1 },
74-
{ slug: 'yarn-classic-audit-optional', weight: 1 },
74+
{ slug: 'yarn-classic-audit-prod', weight: 80 },
75+
{ slug: 'yarn-classic-audit-dev', weight: 15 },
7576
],
7677
}),
7778
],
7879
}),
7980
);
8081
});
8182

82-
// Note: Yarn v2 does not support audit for optional dependencies
83-
it('should omit unsupported dependency groups', async () => {
83+
it('should create an audit for selected dependency groups', async () => {
8484
await expect(
85-
jsPackagesPlugin({ packageManager: 'yarn-modern', checks: ['audit'] }),
85+
jsPackagesPlugin({
86+
packageManager: 'yarn-classic',
87+
checks: ['audit'],
88+
dependencyGroups: ['prod', 'optional'],
89+
}),
8690
).resolves.toStrictEqual(
8791
expect.objectContaining({
8892
audits: [
89-
expect.objectContaining({ slug: 'yarn-modern-audit-prod' }),
90-
expect.objectContaining({ slug: 'yarn-modern-audit-dev' }),
93+
expect.objectContaining({ slug: 'yarn-classic-audit-prod' }),
94+
expect.objectContaining({ slug: 'yarn-classic-audit-optional' }),
9195
],
9296
groups: [
9397
expect.objectContaining<Partial<Group>>({
94-
slug: 'yarn-modern-audit',
98+
slug: 'yarn-classic-audit',
9599
refs: [
96-
{ slug: 'yarn-modern-audit-prod', weight: 3 },
97-
{ slug: 'yarn-modern-audit-dev', weight: 1 },
100+
{ slug: 'yarn-classic-audit-prod', weight: 80 },
101+
{ slug: 'yarn-classic-audit-optional', weight: 5 },
98102
],
99103
}),
100104
],
101105
}),
102106
);
103107
});
104108

109+
// Note: Yarn v2 does not support audit for optional dependencies
110+
it('should omit unsupported dependency groups', async () => {
111+
await expect(
112+
jsPackagesPlugin({
113+
packageManager: 'yarn-modern',
114+
checks: ['audit'],
115+
dependencyGroups: ['prod', 'optional'],
116+
}),
117+
).resolves.toStrictEqual(
118+
expect.objectContaining({
119+
audits: [expect.objectContaining({ slug: 'yarn-modern-audit-prod' })],
120+
groups: [
121+
expect.objectContaining<Partial<Group>>({
122+
slug: 'yarn-modern-audit',
123+
refs: [{ slug: 'yarn-modern-audit-prod', weight: 80 }],
124+
}),
125+
],
126+
}),
127+
);
128+
});
129+
105130
it('should use an icon that matches the chosen package manager', async () => {
106131
await expect(
107132
jsPackagesPlugin({ packageManager: 'pnpm' }),

packages/plugin-js-packages/src/lib/package-managers/npm/npm.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { objectToKeys } from '@code-pushup/utils';
12
import { DependencyGroup } from '../../config';
2-
import { AuditResult } from '../../runner/audit/types';
33
import { filterAuditResult } from '../../runner/utils';
44
import { COMMON_AUDIT_ARGS, COMMON_OUTDATED_ARGS } from '../constants';
5-
import { PackageManager } from '../types';
5+
import { AuditResults, PackageManager } from '../types';
66
import { npmToAuditResult } from './audit-result';
77
import { npmToOutdatedResult } from './outdated-result';
88

@@ -30,11 +30,23 @@ export const npmPackageManager: PackageManager = {
3030
],
3131
unifyResult: npmToAuditResult,
3232
// prod dependencies need to be filtered out manually since v10
33-
postProcessResult: (results: Record<DependencyGroup, AuditResult>) => ({
34-
prod: results.prod,
35-
dev: filterAuditResult(results.dev, 'name', results.prod),
36-
optional: filterAuditResult(results.optional, 'name', results.prod),
37-
}),
33+
postProcessResult: (results: AuditResults) => {
34+
const depGroups = objectToKeys(results);
35+
const devFilter =
36+
results.dev && results.prod
37+
? filterAuditResult(results.dev, 'name', results.prod)
38+
: results.dev;
39+
const optionalFilter =
40+
results.optional && results.prod
41+
? filterAuditResult(results.optional, 'name', results.prod)
42+
: results.optional;
43+
44+
return {
45+
...(depGroups.includes('prod') && { prod: results.prod }),
46+
...(depGroups.includes('dev') && { dev: devFilter }),
47+
...(depGroups.includes('optional') && { optional: optionalFilter }),
48+
};
49+
},
3850
},
3951
outdated: {
4052
commandArgs: [...COMMON_OUTDATED_ARGS, '--long'],

packages/plugin-js-packages/src/lib/package-managers/pnpm/pnpm.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { objectToKeys } from '@code-pushup/utils';
12
import { DependencyGroup } from '../../config';
2-
import { AuditResult } from '../../runner/audit/types';
33
import { filterAuditResult } from '../../runner/utils';
44
import { COMMON_AUDIT_ARGS, COMMON_OUTDATED_ARGS } from '../constants';
5-
import { PackageManager } from '../types';
5+
import { AuditResults, PackageManager } from '../types';
66
import { pnpmToAuditResult } from './audit-result';
77
import { pnpmToOutdatedResult } from './outdated-result';
88

@@ -30,15 +30,23 @@ export const pnpmPackageManager: PackageManager = {
3030
ignoreExitCode: true,
3131
unifyResult: pnpmToAuditResult,
3232
// optional dependencies don't have an exclusive option so they need duplicates filtered out
33-
postProcessResult: (results: Record<DependencyGroup, AuditResult>) => ({
34-
prod: results.prod,
35-
dev: results.dev,
36-
optional: filterAuditResult(
37-
filterAuditResult(results.optional, 'id', results.prod),
38-
'id',
39-
results.dev,
40-
),
41-
}),
33+
postProcessResult: (results: AuditResults) => {
34+
const depGroups = objectToKeys(results);
35+
const prodFilter =
36+
results.optional && results.prod
37+
? filterAuditResult(results.optional, 'id', results.prod)
38+
: results.optional;
39+
const devFilter =
40+
prodFilter && results.dev
41+
? filterAuditResult(prodFilter, 'id', results.dev)
42+
: results.optional;
43+
44+
return {
45+
...(depGroups.includes('prod') && { prod: results.prod }),
46+
...(depGroups.includes('dev') && { dev: results.dev }),
47+
...(results.optional && { optional: devFilter }),
48+
};
49+
},
4250
},
4351
outdated: {
4452
commandArgs: COMMON_OUTDATED_ARGS,

packages/plugin-js-packages/src/lib/package-managers/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DependencyGroup, PackageManagerId } from '../config';
33
import { AuditResult } from '../runner/audit/types';
44
import { OutdatedResult } from '../runner/outdated/types';
55

6+
export type AuditResults = Partial<Record<DependencyGroup, AuditResult>>;
7+
68
export type PackageManager = {
79
slug: PackageManagerId;
810
name: string;
@@ -18,9 +20,7 @@ export type PackageManager = {
1820
ignoreExitCode?: boolean; // non-zero exit code will throw by default
1921
supportedDepGroups?: DependencyGroup[]; // all are supported by default
2022
unifyResult: (output: string) => AuditResult;
21-
postProcessResult?: (
22-
result: Record<DependencyGroup, AuditResult>,
23-
) => Record<DependencyGroup, AuditResult>;
23+
postProcessResult?: (result: AuditResults) => AuditResults;
2424
};
2525
outdated: {
2626
commandArgs: string[];

0 commit comments

Comments
 (0)