From d2ad8ca3af5fc6d4f8c5306e76c0bb930841d4c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 12 Dec 2022 18:56:56 +0100 Subject: [PATCH] cli: add Lockfile.diff Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/versioning/Lockfile.test.ts | 236 +++++++++++++++++- packages/cli/src/lib/versioning/Lockfile.ts | 61 ++++- 2 files changed, 288 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index e2842b4c95..8209ef1f23 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -78,10 +78,12 @@ describe('Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1' }, - { range: '^2', version: '2.0.0' }, + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, ]); expect(lockfile.toString()).toBe(mockA); }); @@ -234,11 +236,13 @@ describe('New Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1' }, - { range: '^2.0.1', version: '2.0.1' }, - { range: '^2', version: '2.0.0' }, + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, ]); expect(lockfile.toString()).toBe(mockANew); }); @@ -315,4 +319,222 @@ describe('New Lockfile', () => { mockANewLocalDedup, ); }); + + describe('diff', () => { + const lockfileLegacyA = Lockfile.parse(`${HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@3: + version "3.0.1" + integrity sha512-abc1 + +b@2.0.x: + version "2.0.1" + integrity sha512-abc2 + +b@^2: + version "2.0.0" + integrity sha512-abc3 + +c@^1: + version "1.0.1" + integrity x +`); + + const lockfileLegacyB = Lockfile.parse(`${HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz-other + dependencies: + b "^2" + +b@2.0.x, b@^2: + version "2.0.0" + integrity sha512-abc3 + +b@4: + version "4.0.0" + integrity sha512-abc + +d@^1: + version "1.0.1" + integrity x +`); + + const lockfileModernA = Lockfile.parse(`${HEADER} +"a@^1": + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + checksum sha512-xyz + dependencies: + b "^2" + +"b@3": + version "3.0.1" + checksum sha512-abc1 + +"b@2.0.x": + version "2.0.1" + checksum sha512-abc2 + +"b@^2": + version "2.0.0" + checksum sha512-abc3 + +"c@^1": + version "1.0.1" + checksum x +`); + + const lockfileModernB = Lockfile.parse(`${HEADER} +"a@^1": + version "1.0.1" + resolution "a@npm:1.0.1" + checksum sha512-xyz-other + dependencies: + b "^2" + +"b@2.0.x, b@^2": + version "2.0.0" + checksum sha512-abc3 + +"b@4": + version "4.0.0" + checksum sha512-abc + +"d@^1": + version "1.0.1" + checksum x +`); + + it('should diff two legacy lockfiles', async () => { + expect(lockfileLegacyA.diff(lockfileLegacyB)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + expect(lockfileLegacyB.diff(lockfileLegacyA)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + }); + + it('should diff two modern lockfiles', async () => { + expect(lockfileModernA.diff(lockfileModernB)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + expect(lockfileModernB.diff(lockfileModernA)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + }); + + it('should diff legacy and modern lockfiles', async () => { + expect(lockfileLegacyA.diff(lockfileModernB)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + expect(lockfileLegacyB.diff(lockfileModernA)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + }); + + it('should diff modern and legacy lockfiles', async () => { + expect(lockfileModernA.diff(lockfileLegacyB)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + expect(lockfileModernB.diff(lockfileLegacyA)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + }); + }); }); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index faafb465a4..d156c9498d 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -26,7 +26,8 @@ type LockfileData = { [entry: string]: { version: string; resolved?: string; - integrity?: string; + integrity?: string /* old */; + checksum?: string /* new */; dependencies?: { [name: string]: string }; }; }; @@ -34,6 +35,7 @@ type LockfileData = { type LockfileQueryEntry = { range: string; version: string; + dataKey: string; }; /** Entries that have an invalid version range, for example an npm tag */ @@ -127,7 +129,7 @@ export class Lockfile { if (range.startsWith('npm:')) { range = range.slice('npm:'.length); } - queries.push({ range, version: value.version }); + queries.push({ range, version: value.version, dataKey: key }); } } @@ -326,6 +328,61 @@ export class Lockfile { } } + diff(newLockfile: Lockfile) { + const diff = { + added: new Array<{ name: string; range: string }>(), + removed: new Array<{ name: string; range: string }>(), + changed: new Array<{ name: string; range: string }>(), + }; + + // Keeps track of packages that only exist in the old lockfile + const remainingOldNames = new Set(this.packages.keys()); + + for (const [name, newQueries] of newLockfile.packages) { + remainingOldNames.delete(name); + + const oldQueries = this.packages.get(name); + // If the packages doesn't exist in the old lockfile, add all entries + if (!oldQueries) { + diff.added.push(...newQueries.map(q => ({ name, range: q.range }))); + continue; + } + + const remainingOldRanges = new Set(oldQueries.map(q => q.range)); + + for (const newQuery of newQueries) { + remainingOldRanges.delete(newQuery.range); + + const oldQuery = oldQueries.find(q => q.range === newQuery.range); + if (!oldQuery) { + diff.added.push({ name, range: newQuery.range }); + continue; + } + + const newPkg = newLockfile.data[newQuery.dataKey]; + const oldPkg = this.data[oldQuery.dataKey]; + if (newPkg && oldPkg) { + const oldCheck = oldPkg.integrity || oldPkg.checksum; + const newCheck = newPkg.integrity || newPkg.checksum; + if (!oldCheck || !newCheck || oldCheck !== newCheck) { + diff.changed.push({ name, range: newQuery.range }); + } + } + } + + for (const oldRange of remainingOldRanges) { + diff.removed.push({ name, range: oldRange }); + } + } + + for (const name of remainingOldNames) { + const queries = this.packages.get(name) ?? []; + diff.removed.push(...queries.map(q => ({ name, range: q.range }))); + } + + return diff; + } + async save(path: string) { await fs.writeFile(path, this.toString(), 'utf8'); }