Skip to content

Commit 901403a

Browse files
committed
fix(plugin-js-packages): consider dynamic outdated fields for Yarn v1
1 parent 31d2301 commit 901403a

File tree

5 files changed

+217
-30
lines changed

5 files changed

+217
-30
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { OutdatedDependency } from '../../runner/outdated/types';
2+
import { Yarnv1FieldName } from './types';
3+
4+
export const outdatedtoFieldMapper: Record<
5+
keyof OutdatedDependency,
6+
Yarnv1FieldName
7+
> = {
8+
name: 'Package',
9+
current: 'Current',
10+
latest: 'Latest',
11+
type: 'Package Type',
12+
url: 'URL',
13+
};
14+
15+
export const REQUIRED_OUTDATED_FIELDS: Yarnv1FieldName[] = [
16+
'Package',
17+
'Current',
18+
'Latest',
19+
'Package Type',
20+
];
Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,74 @@
1-
import { fromJsonLines } from '@code-pushup/utils';
2-
import { OutdatedResult } from '../../runner/outdated/types';
3-
import { Yarnv1OutdatedResultJson } from './types';
1+
import {
2+
fromJsonLines,
3+
objectFromEntries,
4+
objectToEntries,
5+
objectToKeys,
6+
} from '@code-pushup/utils';
7+
import {
8+
OutdatedDependency,
9+
OutdatedResult,
10+
} from '../../runner/outdated/types';
11+
import { REQUIRED_OUTDATED_FIELDS, outdatedtoFieldMapper } from './constants';
12+
import {
13+
Yarnv1FieldName,
14+
Yarnv1OutdatedResultJson,
15+
yarnv1FieldNames,
16+
} from './types';
417

518
export function yarnv1ToOutdatedResult(output: string): OutdatedResult {
619
const yarnv1Outdated = fromJsonLines<Yarnv1OutdatedResultJson>(output);
20+
const fields = yarnv1Outdated[1].data.head;
721
const dependencies = yarnv1Outdated[1].data.body;
822

9-
return dependencies.map(([name, current, _, latest, __, type, url]) => ({
10-
name,
11-
current,
12-
latest,
13-
type,
14-
url,
15-
}));
23+
// no outdated dependencies
24+
if (dependencies.length === 0) {
25+
return [];
26+
}
27+
28+
// map dynamic fields
29+
validateOutdatedFields(fields);
30+
const indexMapping = getOutdatedFieldIndexes(fields);
31+
32+
return dependencies.map(
33+
dep =>
34+
objectFromEntries(
35+
objectToKeys(indexMapping)
36+
.map(field => [field, dep[indexMapping[field]]] as const)
37+
.filter(
38+
(entry): entry is [keyof OutdatedDependency, string] =>
39+
entry[1] != null,
40+
),
41+
) as OutdatedDependency,
42+
);
43+
}
44+
45+
export function validateOutdatedFields(head: string[]) {
46+
const relevantFields = head.filter(isYarnv1FieldName);
47+
if (hasAllRequiredFields(relevantFields)) {
48+
return true;
49+
}
50+
51+
throw new Error(
52+
`Yarn v1 outdated: Template [${head.join(
53+
', ',
54+
)}] does not contain all required fields [${yarnv1FieldNames.join(', ')}]`,
55+
);
56+
}
57+
58+
function isYarnv1FieldName(value: string): value is Yarnv1FieldName {
59+
const names: readonly string[] = yarnv1FieldNames;
60+
return names.includes(value);
61+
}
62+
63+
function hasAllRequiredFields(head: Yarnv1FieldName[]) {
64+
return REQUIRED_OUTDATED_FIELDS.every(field => head.includes(field));
65+
}
66+
67+
export function getOutdatedFieldIndexes(all: string[]) {
68+
return objectFromEntries(
69+
objectToEntries(outdatedtoFieldMapper).map(([outdatedField, yarnField]) => [
70+
outdatedField,
71+
all.indexOf(yarnField),
72+
]),
73+
);
1674
}

packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import { describe, expect, it } from 'vitest';
22
import { toJsonLines } from '@code-pushup/utils';
3-
import { OutdatedResult } from '../../runner/outdated/types';
4-
import { yarnv1ToOutdatedResult } from './outdated-result';
3+
import {
4+
OutdatedDependency,
5+
OutdatedResult,
6+
} from '../../runner/outdated/types';
7+
import { REQUIRED_OUTDATED_FIELDS } from './constants';
8+
import {
9+
getOutdatedFieldIndexes,
10+
validateOutdatedFields,
11+
yarnv1ToOutdatedResult,
12+
} from './outdated-result';
13+
import { Yarnv1FieldName } from './types';
514

615
describe('yarnv1ToOutdatedResult', () => {
716
const yarnInfo = { type: 'info', data: 'Colours' };
817
it('should transform Yarn v1 outdated to unified outdated result', () => {
918
const table = {
1019
type: 'table',
1120
data: {
12-
body: [
13-
['nx', '16.8.1', '', '17.0.0', '', 'dependencies', 'https://nx.dev/'],
14-
],
21+
head: [
22+
'Package',
23+
'Current',
24+
'Latest',
25+
'Package Type',
26+
'URL',
27+
] satisfies Yarnv1FieldName[],
28+
body: [['nx', '16.8.1', '17.0.0', 'dependencies', 'https://nx.dev/']],
1529
},
1630
};
1731

@@ -28,9 +42,103 @@ describe('yarnv1ToOutdatedResult', () => {
2842
]);
2943
});
3044

45+
it('should adapt to custom fields and order', () => {
46+
const table = {
47+
type: 'table',
48+
data: {
49+
head: [
50+
'Latest',
51+
'Package Type',
52+
'Package',
53+
'Workspaces', // irrelevant
54+
'Current',
55+
'Wanted', // irrelevant
56+
],
57+
body: [
58+
['13.6.0', 'devDependencies', 'cypress', 'cli', '11.1.1', '13.0.0'],
59+
],
60+
},
61+
};
62+
63+
expect(
64+
yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table])),
65+
).toEqual<OutdatedResult>([
66+
{
67+
name: 'cypress',
68+
current: '11.1.1',
69+
latest: '13.6.0',
70+
type: 'devDependencies',
71+
},
72+
]);
73+
});
74+
3175
it('should transform no dependencies to empty array', () => {
32-
const table = { type: 'table', data: { body: [] } };
76+
const table = { type: 'table', data: { head: [], body: [] } };
3377

3478
expect(yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table]))).toEqual([]);
3579
});
3680
});
81+
82+
describe('validateOutdatedFields', () => {
83+
it('should consider all required fields as valid', () => {
84+
expect(validateOutdatedFields(REQUIRED_OUTDATED_FIELDS)).toBe(true);
85+
});
86+
87+
it('should consider optional fields valid', () => {
88+
expect(validateOutdatedFields([...REQUIRED_OUTDATED_FIELDS, 'URL'])).toBe(
89+
true,
90+
);
91+
});
92+
93+
it('should throw for missing required fields', () => {
94+
expect(() => validateOutdatedFields(['Package', 'Current'])).toThrow(
95+
'does not contain all required fields',
96+
);
97+
});
98+
});
99+
100+
describe('getOutdatedFieldIndexes', () => {
101+
it('should return relevant fields with their index', () => {
102+
expect(
103+
getOutdatedFieldIndexes([...REQUIRED_OUTDATED_FIELDS, 'URL']),
104+
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
105+
name: 0,
106+
current: 1,
107+
latest: 2,
108+
type: 3,
109+
url: 4,
110+
});
111+
});
112+
113+
it('should tag missing optional fields as -1', () => {
114+
expect(
115+
getOutdatedFieldIndexes(['Package', 'Current', 'Latest', 'Package Type']),
116+
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
117+
name: 0,
118+
current: 1,
119+
latest: 2,
120+
type: 3,
121+
url: -1,
122+
});
123+
});
124+
125+
it('should skip additional fields', () => {
126+
expect(
127+
getOutdatedFieldIndexes([
128+
'Latest',
129+
'URL',
130+
'Package Type',
131+
'Package',
132+
'Workspaces', // irrelevant
133+
'Current',
134+
'Wanted', // irrelevant
135+
]),
136+
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
137+
latest: 0,
138+
url: 1,
139+
type: 2,
140+
name: 3,
141+
current: 5,
142+
});
143+
});
144+
});

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { PackageAuditLevel } from '../../config';
2-
import { DependencyGroupLong } from '../../runner/outdated/types';
32

43
// Subset of Yarn v1 audit JSON type
54
export type Yarnv1AuditAdvisory = {
@@ -35,22 +34,22 @@ export type Yarnv1AuditResultJson = [
3534
Yarnv1AuditSummary,
3635
];
3736

38-
// Subset of Yarn v1 outdated JSON type
39-
export type Yarnv1VersionOverview = [
40-
string, // package
41-
string, // current
42-
string, // wanted
43-
string, // latest
44-
string, // workspace
45-
DependencyGroupLong, // package type
46-
string, // URL
47-
];
37+
export const yarnv1FieldNames = [
38+
'Package',
39+
'Current',
40+
'Latest',
41+
'Package Type',
42+
'URL',
43+
] as const;
44+
45+
export type Yarnv1FieldName = (typeof yarnv1FieldNames)[number];
4846

4947
type Yarnv1Info = { type: 'info' };
5048
type Yarnv1Table = {
5149
type: 'table';
5250
data: {
53-
body: Yarnv1VersionOverview[];
51+
head: string[];
52+
body: string[][];
5453
};
5554
};
5655

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ export type DependencyGroupLong =
77
| 'optionalDependencies';
88

99
// Unified Outdated result type
10-
export type OutdatedResult = {
10+
export type OutdatedDependency = {
1111
name: string;
1212
current: string;
1313
latest: string;
1414
type: DependencyGroupLong;
1515
url?: string;
16-
}[];
16+
};
17+
18+
export type OutdatedResult = OutdatedDependency[];

0 commit comments

Comments
 (0)