Skip to content

Commit ca7905a

Browse files
committed
feat(plugin-js-packages): introduce yarn v1 outdated, unify type
1 parent a4b10a2 commit ca7905a

File tree

9 files changed

+370
-138
lines changed

9 files changed

+370
-138
lines changed

.eslintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
}
6464
]
6565
}
66+
],
67+
"@typescript-eslint/no-unused-vars": [
68+
"error",
69+
{
70+
"argsIgnorePattern": "^_"
71+
}
6672
]
6773
}
6874
},

packages/plugin-js-packages/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ Each dependency group has its own audit. If you want to check only a subset of d
159159
],
160160
```
161161

162+
## Adding new package managers
163+
164+
In order to add a support for a new package manager, one needs to do the following.
165+
166+
1. Expand `packageManagerSchema` in `config.ts`.
167+
2. Expand `<command>Args` in `runner/<command>/constants.ts` with a set of arguments to be run for a given package manager command.
168+
3. Create a custom type in `runner/<command>/types.ts` with relevant properties based on expected command JSON output.
169+
4. Create a function in `runner/<command>/unify-type.ts` that will transform JSON output into a normalized type `OutdatedResult` or `AuditResult` and add it to `normalized<command>Mapper` in `runner/<command>/constants.ts`.
170+
162171
## Score calculation
163172

164173
Audit output score is a numeric value in the range 0-1.

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
import { auditResultToAuditOutput } from './audit/transform';
2323
import { NpmAuditResultJson } from './audit/types';
2424
import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH } from './constants';
25+
import { normalizeOutdatedMapper, outdatedArgs } from './outdated/constants';
2526
import { outdatedResultToAuditOutput } from './outdated/transform';
26-
import { NpmOutdatedResultJson } from './outdated/types';
2727

2828
export async function createRunnerConfig(
2929
scriptPath: string,
@@ -59,13 +59,14 @@ export async function executeRunner(): Promise<void> {
5959
async function processOutdated(packageManager: PackageManager) {
6060
const { stdout } = await executeProcess({
6161
command: packageManager,
62-
args: ['outdated', '--json', '--long'],
62+
args: outdatedArgs[packageManager],
63+
cwd: process.cwd(),
6364
alwaysResolve: true, // npm outdated returns exit code 1 when outdated dependencies are found
6465
});
6566

66-
const outdatedResult = JSON.parse(stdout) as NpmOutdatedResultJson;
67+
const normalizedResult = normalizeOutdatedMapper[packageManager](stdout);
6768
return dependencyGroups.map(dep =>
68-
outdatedResultToAuditOutput(outdatedResult, dep),
69+
outdatedResultToAuditOutput(normalizedResult, dep),
6970
);
7071
}
7172

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
11
import { IssueSeverity } from '@code-pushup/models';
2-
import { VersionType } from './types';
2+
import { PackageManager } from '../../config';
3+
import { OutdatedResult, VersionType } from './types';
4+
import { npmToOutdatedResult, yarnv1ToOutdatedResult } from './unify-type';
35

46
export const outdatedSeverity: Record<VersionType, IssueSeverity> = {
57
major: 'error',
68
minor: 'warning',
79
patch: 'info',
810
};
11+
12+
export const outdatedArgs: Record<PackageManager, string[]> = {
13+
npm: ['outdated', '--json', '--long'],
14+
'yarn-classic': ['outdated', '--json', '|', 'jq', '-s'],
15+
'yarn-modern': [],
16+
pnpm: [],
17+
};
18+
19+
export const normalizeOutdatedMapper: Record<
20+
PackageManager,
21+
(_: string) => OutdatedResult
22+
> = {
23+
npm: npmToOutdatedResult,
24+
'yarn-classic': yarnv1ToOutdatedResult,
25+
// eslint-disable-next-line @typescript-eslint/naming-convention
26+
'yarn-modern': _ => [],
27+
pnpm: _ => [],
28+
};

packages/plugin-js-packages/src/lib/runner/outdated/transform.ts

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,50 @@
11
import { Issue } from '@code-pushup/models';
2-
import { objectToEntries } from '@code-pushup/utils';
2+
import { pluralize } from '@code-pushup/utils';
33
import { DependencyGroup } from '../../config';
44
import { outdatedSeverity } from './constants';
55
import {
6-
NormalizedOutdatedEntries,
7-
NormalizedVersionOverview,
8-
NpmOutdatedResultJson,
6+
OutdatedResult,
97
PackageVersion,
108
VersionType,
9+
versionType,
1110
} from './types';
1211

1312
export function outdatedResultToAuditOutput(
14-
result: NpmOutdatedResultJson,
15-
dependenciesType: DependencyGroup,
13+
result: OutdatedResult,
14+
dependencyGroup: DependencyGroup,
1615
) {
1716
// current might be missing in some cases
1817
// 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 === `${dependenciesType}Dependencies`,
28-
);
29-
const outdatedDependencies = validDependencies.filter(
30-
([, versions]) => versions.current !== versions.wanted,
18+
const relevantDependencies: OutdatedResult = result.filter(dep =>
19+
dependencyGroup === 'prod'
20+
? dep.type === 'dependencies'
21+
: dep.type === `${dependencyGroup}Dependencies`,
22+
);
23+
const outdatedDependencies = relevantDependencies.filter(
24+
dep => dep.current !== dep.latest,
3125
);
3226

33-
const majorOutdatedAmount = outdatedDependencies.filter(
34-
([, versions]) =>
35-
getOutdatedLevel(versions.current, versions.wanted) === 'major',
36-
).length;
27+
const outdatedStats = outdatedDependencies.reduce(
28+
(acc, dep) => {
29+
const outdatedLevel = getOutdatedLevel(dep.current, dep.latest);
30+
return { ...acc, [outdatedLevel]: acc[outdatedLevel] + 1 };
31+
},
32+
{ major: 0, minor: 0, patch: 0 },
33+
);
3734

3835
const issues =
3936
outdatedDependencies.length === 0
4037
? []
4138
: outdatedToIssues(outdatedDependencies);
4239

4340
return {
44-
slug: `npm-outdated-${dependenciesType}`,
41+
slug: `npm-outdated-${dependencyGroup}`,
4542
score: calculateOutdatedScore(
46-
majorOutdatedAmount,
47-
validDependencies.length,
43+
outdatedStats.major,
44+
relevantDependencies.length,
4845
),
4946
value: outdatedDependencies.length,
50-
displayValue: outdatedToDisplayValue(
51-
majorOutdatedAmount,
52-
outdatedDependencies.length,
53-
),
47+
displayValue: outdatedToDisplayValue(outdatedStats),
5448
...(issues.length > 0 && { details: { issues } }),
5549
};
5650
}
@@ -62,52 +56,59 @@ export function calculateOutdatedScore(
6256
return totalDeps > 0 ? (totalDeps - majorOutdated) / totalDeps : 1;
6357
}
6458

65-
export function outdatedToDisplayValue(
66-
majorOutdated: number,
67-
totalOutdated: number,
68-
) {
69-
return totalOutdated === 0
70-
? 'all dependencies are up to date'
71-
: majorOutdated > 0
72-
? `${majorOutdated} out of ${totalOutdated} outdated dependencies require major update`
73-
: `${totalOutdated} outdated ${
74-
totalOutdated === 1 ? 'dependency' : 'dependencies'
75-
}`;
59+
export function outdatedToDisplayValue(stats: Record<VersionType, number>) {
60+
const total = stats.major + stats.minor + stats.patch;
61+
62+
const versionBreakdown = versionType
63+
.map(version => (stats[version] > 0 ? `${stats[version]} ${version}` : ''))
64+
.filter(text => text !== '');
65+
66+
if (versionBreakdown.length === 0) {
67+
return 'all dependencies are up to date';
68+
}
69+
70+
if (versionBreakdown.length > 1) {
71+
return `${total} outdated package versions (${versionBreakdown.join(
72+
', ',
73+
)})`;
74+
}
75+
76+
return `${versionBreakdown[0]} outdated package ${pluralize(
77+
'version',
78+
total,
79+
)}`;
7680
}
7781

78-
export function outdatedToIssues(
79-
dependencies: NormalizedOutdatedEntries,
80-
): Issue[] {
81-
return dependencies.map<Issue>(([name, versions]) => {
82-
const outdatedLevel = getOutdatedLevel(versions.current, versions.wanted);
82+
export function outdatedToIssues(dependencies: OutdatedResult): Issue[] {
83+
return dependencies.map<Issue>(dep => {
84+
const { name, current, latest, url, project } = dep;
85+
const outdatedLevel = getOutdatedLevel(current, latest);
8386
const packageReference =
84-
versions.homepage == null
85-
? `\`${name}\``
86-
: `[\`${name}\`](${versions.homepage})`;
87+
url == null ? `\`${name}\`` : `[\`${name}\`](${url})`;
8788

8889
return {
89-
message: `Package ${packageReference} requires a **${outdatedLevel}** update from **${versions.current}** to **${versions.wanted}**.`,
90+
message: `${project}'s dependency ${packageReference} requires a **${outdatedLevel}** update from **${current}** to **${latest}**.`,
9091
severity: outdatedSeverity[outdatedLevel],
9192
};
9293
});
9394
}
9495

9596
export function getOutdatedLevel(
9697
currentFullVersion: string,
97-
wantedFullVersion: string,
98+
latestFullVersion: string,
9899
): VersionType {
99100
const current = splitPackageVersion(currentFullVersion);
100-
const wanted = splitPackageVersion(wantedFullVersion);
101+
const latest = splitPackageVersion(latestFullVersion);
101102

102-
if (current.major < wanted.major) {
103+
if (current.major < latest.major) {
103104
return 'major';
104105
}
105106

106-
if (current.minor < wanted.minor) {
107+
if (current.minor < latest.minor) {
107108
return 'minor';
108109
}
109110

110-
if (current.patch < wanted.patch) {
111+
if (current.patch < latest.patch) {
111112
return 'patch';
112113
}
113114

0 commit comments

Comments
 (0)