From 901403a9fdfeb15e5bfb5e1fdde141b7c987d8b7 Mon Sep 17 00:00:00 2001 From: Katerina Pilatova Date: Thu, 18 Apr 2024 19:00:55 +0200 Subject: [PATCH] fix(plugin-js-packages): consider dynamic outdated fields for Yarn v1 --- .../yarn-classic/constants.ts | 20 +++ .../yarn-classic/outdated-result.ts | 78 ++++++++++-- .../yarn-classic/outdated-result.unit.test.ts | 120 +++++++++++++++++- .../package-managers/yarn-classic/types.ts | 23 ++-- .../src/lib/runner/outdated/types.ts | 6 +- 5 files changed, 217 insertions(+), 30 deletions(-) create mode 100644 packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts new file mode 100644 index 000000000..4aac4f717 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/constants.ts @@ -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', +]; diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts index 5aad47206..11300b2e8 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.ts @@ -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(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), + ]), + ); } diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts index f956695b6..a6a836973 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/outdated-result.unit.test.ts @@ -1,7 +1,16 @@ 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' }; @@ -9,9 +18,14 @@ describe('yarnv1ToOutdatedResult', () => { 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/']], }, }; @@ -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([ + { + 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>({ + 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>({ + 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>({ + latest: 0, + url: 1, + type: 2, + name: 3, + current: 5, + }); + }); +}); diff --git a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts index 50bf27fad..1c1f54ce8 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/yarn-classic/types.ts @@ -1,5 +1,4 @@ import { PackageAuditLevel } from '../../config'; -import { DependencyGroupLong } from '../../runner/outdated/types'; // Subset of Yarn v1 audit JSON type export type Yarnv1AuditAdvisory = { @@ -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[][]; }; }; diff --git a/packages/plugin-js-packages/src/lib/runner/outdated/types.ts b/packages/plugin-js-packages/src/lib/runner/outdated/types.ts index c06d32139..7bafea1c1 100644 --- a/packages/plugin-js-packages/src/lib/runner/outdated/types.ts +++ b/packages/plugin-js-packages/src/lib/runner/outdated/types.ts @@ -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[];