Skip to content

Commit 7cb623b

Browse files
committed
feat(plugin-js-packages): implement runner for npm outdated
1 parent 6aa55a2 commit 7cb623b

File tree

5 files changed

+399
-14
lines changed

5 files changed

+399
-14
lines changed

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

+35-14
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import {
88
} from '@code-pushup/utils';
99
import {
1010
FinalJSPackagesPluginConfig,
11+
PackageCommand,
1112
PackageDependency,
1213
packageDependencies,
1314
} from '../config';
1415
import { auditResultToAuditOutput } from './audit/transform';
1516
import { NpmAuditResultJson } from './audit/types';
1617
import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH } from './constants';
18+
import { outdatedResultToAuditOutput } from './outdated/transform';
19+
import { NpmOutdatedResultJson } from './outdated/types';
1720

1821
export async function executeRunner(): Promise<void> {
1922
const outputPath = join(
@@ -29,22 +32,28 @@ export async function executeRunner(): Promise<void> {
2932
const results = await Promise.allSettled(
3033
checks.flatMap(check =>
3134
packageDependencies.map<Promise<AuditOutput>>(async dep => {
35+
const outputFilename = `${packageManager}-${check}-${dep}.json`;
36+
3237
await executeProcess({
3338
command: 'npm',
3439
args: [
3540
check,
36-
...createAuditFlags(dep),
37-
'--json',
38-
'--audit-level=none',
39-
'>',
40-
join(outputPath, `${packageManager}-${check}-${dep}.json`),
41+
...getCommandArgs(check, dep, join(outputPath, outputFilename)),
4142
],
43+
alwaysResolve: true, // npm outdated returns exit code 1 when outdated dependencies are found
4244
});
4345

44-
const auditResult = await readJsonFile<NpmAuditResultJson>(
45-
join(outputPath, `${packageManager}-${check}-${dep}.json`),
46-
);
47-
return auditResultToAuditOutput(auditResult, dep, auditLevelMapping);
46+
if (check === 'audit') {
47+
const auditResult = await readJsonFile<NpmAuditResultJson>(
48+
join(outputPath, outputFilename),
49+
);
50+
return auditResultToAuditOutput(auditResult, dep, auditLevelMapping);
51+
} else {
52+
const outdatedResult = await readJsonFile<NpmOutdatedResultJson>(
53+
join(outputPath, outputFilename),
54+
);
55+
return outdatedResultToAuditOutput(outdatedResult, dep);
56+
}
4857
}),
4958
),
5059
);
@@ -72,13 +81,25 @@ export async function createRunnerConfig(
7281
};
7382
}
7483

75-
function createAuditFlags(currentDep: PackageDependency) {
76-
if (currentDep === 'optional') {
77-
return packageDependencies.map(dep => `--include=${dep}`);
78-
}
84+
function getCommandArgs(
85+
check: PackageCommand,
86+
dep: PackageDependency,
87+
outputPath: string,
88+
) {
89+
return check === 'audit'
90+
? [
91+
...createAuditFlags(dep),
92+
'--json',
93+
'--audit-level=none',
94+
'>',
95+
outputPath,
96+
]
97+
: ['--json', '--long', '>', outputPath];
98+
}
7999

100+
function createAuditFlags(currentDep: PackageDependency) {
80101
return [
81-
`--include${currentDep}`,
102+
`--include=${currentDep}`,
82103
...packageDependencies
83104
.filter(dep => dep !== currentDep)
84105
.map(dep => `--omit=${dep}`),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { IssueSeverity } from '@code-pushup/models';
2+
import { VersionType } from './types';
3+
4+
export const outdatedSeverity: Record<VersionType, IssueSeverity> = {
5+
major: 'error',
6+
minor: 'warning',
7+
patch: 'info',
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Issue } from '@code-pushup/models';
2+
import { objectToEntries, pluralize } from '@code-pushup/utils';
3+
import { PackageDependency } from '../../config';
4+
import { outdatedSeverity } from './constants';
5+
import {
6+
NormalizedOutdatedEntries,
7+
NormalizedVersionOverview,
8+
NpmOutdatedResultJson,
9+
PackageVersion,
10+
VersionType,
11+
} from './types';
12+
13+
export function outdatedResultToAuditOutput(
14+
result: NpmOutdatedResultJson,
15+
dependenciesType: PackageDependency,
16+
) {
17+
// current might be missing in some cases
18+
// https://stackoverflow.com/questions/42267101/npm-outdated-command-shows-missing-in-current-version
19+
const validDependencies: NormalizedOutdatedEntries = objectToEntries(result)
20+
.filter(
21+
(entry): entry is [string, NormalizedVersionOverview] =>
22+
entry[1].current != null,
23+
)
24+
.filter(([, detail]) =>
25+
dependenciesType === 'prod'
26+
? detail.type === 'dependencies'
27+
: detail.type.startsWith(dependenciesType),
28+
);
29+
const outdatedDependencies = validDependencies.filter(
30+
([, versions]) => versions.current !== versions.wanted,
31+
);
32+
33+
const issues =
34+
outdatedDependencies.length === 0
35+
? []
36+
: outdatedToIssues(outdatedDependencies);
37+
38+
return {
39+
slug: `npm-outdated-${dependenciesType}`,
40+
score: outdatedDependencies.length === 0 ? 1 : 0,
41+
value: outdatedDependencies.length,
42+
displayValue: outdatedToDisplayValue(outdatedDependencies.length),
43+
...(issues.length > 0 && { details: { issues } }),
44+
};
45+
}
46+
47+
function outdatedToDisplayValue(outdatedDeps: number) {
48+
return outdatedDeps === 0
49+
? 'passed'
50+
: `${outdatedDeps} outdated ${
51+
outdatedDeps === 1 ? 'dependency' : pluralize('dependency')
52+
}`;
53+
}
54+
55+
export function outdatedToIssues(
56+
dependencies: NormalizedOutdatedEntries,
57+
): Issue[] {
58+
return dependencies.map<Issue>(([name, versions]) => {
59+
const outdatedLevel = getOutdatedLevel(versions.current, versions.wanted);
60+
const packageDocumentation =
61+
versions.homepage == null
62+
? ''
63+
: ` Package documentation [here](${versions.homepage})`;
64+
65+
return {
66+
message: `Package ${name} requires a ${outdatedLevel} update from **${versions.current}** to **${versions.wanted}**.${packageDocumentation}`,
67+
severity: outdatedSeverity[outdatedLevel],
68+
};
69+
});
70+
}
71+
72+
export function getOutdatedLevel(
73+
currentFullVersion: string,
74+
wantedFullVersion: string,
75+
): VersionType {
76+
const current = splitPackageVersion(currentFullVersion);
77+
const wanted = splitPackageVersion(wantedFullVersion);
78+
79+
if (current.major < wanted.major) {
80+
return 'major';
81+
}
82+
83+
if (current.minor < wanted.minor) {
84+
return 'minor';
85+
}
86+
87+
if (current.patch < wanted.patch) {
88+
return 'patch';
89+
}
90+
91+
throw new Error('Package is not outdated.');
92+
}
93+
94+
export function splitPackageVersion(fullVersion: string): PackageVersion {
95+
const [major, minor, patch] = fullVersion.split('.').map(Number);
96+
97+
if (major == null || minor == null || patch == null) {
98+
throw new Error(`Invalid version description ${fullVersion}`);
99+
}
100+
101+
return { major, minor, patch };
102+
}

0 commit comments

Comments
 (0)