Skip to content

Commit

Permalink
fix(plugin-js-packages): consider dynamic outdated fields for Yarn v1
Browse files Browse the repository at this point in the history
  • Loading branch information
Tlacenka committed Apr 30, 2024
1 parent 31d2301 commit 901403a
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { OutdatedDependency } from '../../runner/outdated/types';
import { Yarnv1FieldName } from './types';

export const outdatedtoFieldMapper: Record<
keyof OutdatedDependency,
Yarnv1FieldName
> = {
name: 'Package',
current: 'Current',
latest: 'Latest',
type: 'Package Type',
url: 'URL',
};

export const REQUIRED_OUTDATED_FIELDS: Yarnv1FieldName[] = [
'Package',
'Current',
'Latest',
'Package Type',
];
Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
import { fromJsonLines } from '@code-pushup/utils';
import { OutdatedResult } from '../../runner/outdated/types';
import { Yarnv1OutdatedResultJson } from './types';
import {
fromJsonLines,
objectFromEntries,
objectToEntries,
objectToKeys,
} from '@code-pushup/utils';
import {
OutdatedDependency,
OutdatedResult,
} from '../../runner/outdated/types';
import { REQUIRED_OUTDATED_FIELDS, outdatedtoFieldMapper } from './constants';
import {
Yarnv1FieldName,
Yarnv1OutdatedResultJson,
yarnv1FieldNames,
} from './types';

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

return dependencies.map(([name, current, _, latest, __, type, url]) => ({
name,
current,
latest,
type,
url,
}));
// no outdated dependencies
if (dependencies.length === 0) {
return [];
}

// map dynamic fields
validateOutdatedFields(fields);
const indexMapping = getOutdatedFieldIndexes(fields);

return dependencies.map(
dep =>
objectFromEntries(
objectToKeys(indexMapping)
.map(field => [field, dep[indexMapping[field]]] as const)
.filter(
(entry): entry is [keyof OutdatedDependency, string] =>
entry[1] != null,
),
) as OutdatedDependency,
);
}

export function validateOutdatedFields(head: string[]) {
const relevantFields = head.filter(isYarnv1FieldName);
if (hasAllRequiredFields(relevantFields)) {
return true;
}

throw new Error(
`Yarn v1 outdated: Template [${head.join(
', ',
)}] does not contain all required fields [${yarnv1FieldNames.join(', ')}]`,
);
}

function isYarnv1FieldName(value: string): value is Yarnv1FieldName {
const names: readonly string[] = yarnv1FieldNames;
return names.includes(value);
}

function hasAllRequiredFields(head: Yarnv1FieldName[]) {
return REQUIRED_OUTDATED_FIELDS.every(field => head.includes(field));
}

export function getOutdatedFieldIndexes(all: string[]) {
return objectFromEntries(
objectToEntries(outdatedtoFieldMapper).map(([outdatedField, yarnField]) => [
outdatedField,
all.indexOf(yarnField),
]),
);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { describe, expect, it } from 'vitest';
import { toJsonLines } from '@code-pushup/utils';
import { OutdatedResult } from '../../runner/outdated/types';
import { yarnv1ToOutdatedResult } from './outdated-result';
import {
OutdatedDependency,
OutdatedResult,
} from '../../runner/outdated/types';
import { REQUIRED_OUTDATED_FIELDS } from './constants';
import {
getOutdatedFieldIndexes,
validateOutdatedFields,
yarnv1ToOutdatedResult,
} from './outdated-result';
import { Yarnv1FieldName } from './types';

describe('yarnv1ToOutdatedResult', () => {
const yarnInfo = { type: 'info', data: 'Colours' };
it('should transform Yarn v1 outdated to unified outdated result', () => {
const table = {
type: 'table',
data: {
body: [
['nx', '16.8.1', '', '17.0.0', '', 'dependencies', 'https://nx.dev/'],
],
head: [
'Package',
'Current',
'Latest',
'Package Type',
'URL',
] satisfies Yarnv1FieldName[],
body: [['nx', '16.8.1', '17.0.0', 'dependencies', 'https://nx.dev/']],
},
};

Expand All @@ -28,9 +42,103 @@ describe('yarnv1ToOutdatedResult', () => {
]);
});

it('should adapt to custom fields and order', () => {
const table = {
type: 'table',
data: {
head: [
'Latest',
'Package Type',
'Package',
'Workspaces', // irrelevant
'Current',
'Wanted', // irrelevant
],
body: [
['13.6.0', 'devDependencies', 'cypress', 'cli', '11.1.1', '13.0.0'],
],
},
};

expect(
yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table])),
).toEqual<OutdatedResult>([
{
name: 'cypress',
current: '11.1.1',
latest: '13.6.0',
type: 'devDependencies',
},
]);
});

it('should transform no dependencies to empty array', () => {
const table = { type: 'table', data: { body: [] } };
const table = { type: 'table', data: { head: [], body: [] } };

expect(yarnv1ToOutdatedResult(toJsonLines([yarnInfo, table]))).toEqual([]);
});
});

describe('validateOutdatedFields', () => {
it('should consider all required fields as valid', () => {
expect(validateOutdatedFields(REQUIRED_OUTDATED_FIELDS)).toBe(true);
});

it('should consider optional fields valid', () => {
expect(validateOutdatedFields([...REQUIRED_OUTDATED_FIELDS, 'URL'])).toBe(
true,
);
});

it('should throw for missing required fields', () => {
expect(() => validateOutdatedFields(['Package', 'Current'])).toThrow(
'does not contain all required fields',
);
});
});

describe('getOutdatedFieldIndexes', () => {
it('should return relevant fields with their index', () => {
expect(
getOutdatedFieldIndexes([...REQUIRED_OUTDATED_FIELDS, 'URL']),
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
name: 0,
current: 1,
latest: 2,
type: 3,
url: 4,
});
});

it('should tag missing optional fields as -1', () => {
expect(
getOutdatedFieldIndexes(['Package', 'Current', 'Latest', 'Package Type']),
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
name: 0,
current: 1,
latest: 2,
type: 3,
url: -1,
});
});

it('should skip additional fields', () => {
expect(
getOutdatedFieldIndexes([
'Latest',
'URL',
'Package Type',
'Package',
'Workspaces', // irrelevant
'Current',
'Wanted', // irrelevant
]),
).toStrictEqual<Record<keyof OutdatedDependency, number>>({
latest: 0,
url: 1,
type: 2,
name: 3,
current: 5,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PackageAuditLevel } from '../../config';
import { DependencyGroupLong } from '../../runner/outdated/types';

// Subset of Yarn v1 audit JSON type
export type Yarnv1AuditAdvisory = {
Expand Down Expand Up @@ -35,22 +34,22 @@ export type Yarnv1AuditResultJson = [
Yarnv1AuditSummary,
];

// Subset of Yarn v1 outdated JSON type
export type Yarnv1VersionOverview = [
string, // package
string, // current
string, // wanted
string, // latest
string, // workspace
DependencyGroupLong, // package type
string, // URL
];
export const yarnv1FieldNames = [
'Package',
'Current',
'Latest',
'Package Type',
'URL',
] as const;

export type Yarnv1FieldName = (typeof yarnv1FieldNames)[number];

type Yarnv1Info = { type: 'info' };
type Yarnv1Table = {
type: 'table';
data: {
body: Yarnv1VersionOverview[];
head: string[];
body: string[][];
};
};

Expand Down
6 changes: 4 additions & 2 deletions packages/plugin-js-packages/src/lib/runner/outdated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export type DependencyGroupLong =
| 'optionalDependencies';

// Unified Outdated result type
export type OutdatedResult = {
export type OutdatedDependency = {
name: string;
current: string;
latest: string;
type: DependencyGroupLong;
url?: string;
}[];
};

export type OutdatedResult = OutdatedDependency[];

0 comments on commit 901403a

Please sign in to comment.