From 65a7a7620d543de16497a9cc12dce4f47b2abfaa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:30:14 +0200 Subject: [PATCH] cli-node: only copy the lockfile logic that is needed, revert rest Signed-off-by: Patrik Oldsberg --- .../{versioning => monorepo}/Lockfile.test.ts | 285 ---------------- packages/cli-node/src/monorepo/Lockfile.ts | 188 ++++++++++ .../src/monorepo/PackageGraph.test.ts | 2 +- .../cli-node/src/monorepo/PackageGraph.ts | 2 +- .../cli/src/lib/versioning/Lockfile.test.ts | 322 ++++++++++++++++++ .../src/lib}/versioning/Lockfile.ts | 89 ----- .../src => cli/src/lib}/versioning/index.ts | 0 .../src/lib}/versioning/packages.test.ts | 0 .../src/lib}/versioning/packages.ts | 0 9 files changed, 512 insertions(+), 376 deletions(-) rename packages/cli-node/src/{versioning => monorepo}/Lockfile.test.ts (65%) create mode 100644 packages/cli-node/src/monorepo/Lockfile.ts create mode 100644 packages/cli/src/lib/versioning/Lockfile.test.ts rename packages/{cli-node/src => cli/src/lib}/versioning/Lockfile.ts (80%) rename packages/{cli-node/src => cli/src/lib}/versioning/index.ts (100%) rename packages/{cli-node/src => cli/src/lib}/versioning/packages.test.ts (100%) rename packages/{cli-node/src => cli/src/lib}/versioning/packages.ts (100%) diff --git a/packages/cli-node/src/versioning/Lockfile.test.ts b/packages/cli-node/src/monorepo/Lockfile.test.ts similarity index 65% rename from packages/cli-node/src/versioning/Lockfile.test.ts rename to packages/cli-node/src/monorepo/Lockfile.test.ts index 0275c5ee50..01574779b2 100644 --- a/packages/cli-node/src/versioning/Lockfile.test.ts +++ b/packages/cli-node/src/monorepo/Lockfile.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { ExtendedPackage } from '../monorepo'; import { Lockfile } from './Lockfile'; const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. @@ -32,294 +30,11 @@ __metadata: cacheKey: 8 `; -const mockA = `${LEGACY_HEADER} -a@^1: - version "1.0.1" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz - dependencies: - b "^2" - -b@2.0.x: - version "2.0.1" - -b@^2: - version "2.0.0" -`; - -const mockADedup = `${LEGACY_HEADER} -a@^1: - version "1.0.1" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz - dependencies: - b "^2" - -b@2.0.x, b@^2: - version "2.0.1" -`; - -const mockB = `${LEGACY_HEADER} -"@s/a@*", "@s/a@1 || 2", "@s/a@^1": - version "1.0.1" - -"@s/a@^2.0.x": - version "2.0.0" -`; - -const mockBDedup = `${LEGACY_HEADER} -"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x": - version "2.0.0" - -"@s/a@^1": - version "1.0.1" -`; - -describe('Lockfile', () => { - afterEach(() => { - mockFs.restore(); - }); - - it('should load and serialize mockA', async () => { - mockFs({ - '/yarn.lock': mockA, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - 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', dataKey: 'b@2.0.x' }, - { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, - ]); - expect(lockfile.toString()).toBe(mockA); - }); - - it('should deduplicate and save mockA', async () => { - mockFs({ - '/yarn.lock': mockA, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '2.0.1', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockA); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockADedup); - - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA); - await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup); - }); - - it('should deduplicate mockB', async () => { - mockFs({ - '/yarn.lock': mockB, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [ - { - name: '@s/a', - oldRange: '^1', - newRange: '^2.0.x', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - ], - newVersions: [ - { - name: '@s/a', - range: '*', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - { - name: '@s/a', - range: '1 || 2', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockB); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockBDedup); - }); -}); - -const mockANew = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 2.0.1 - -b@^2: - version: 2.0.0 -`; - -const mockANewDedup = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 2.0.1 - -b@^2: - version: 2.0.1 -`; - -const mockANewLocal = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 0.0.0-use.local - -b@^2: - version: 2.0.0 -`; - -const mockANewLocalDedup = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 0.0.0-use.local - -b@^2: - version: 0.0.0-use.local -`; - describe('New Lockfile', () => { afterEach(() => { mockFs.restore(); }); - it('should load and serialize mockANew', async () => { - mockFs({ - '/yarn.lock': mockANew, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - 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', 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); - }); - - it('should deduplicate and save mockANew', async () => { - mockFs({ - '/yarn.lock': mockANew, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '2.0.1', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockANew); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockANewDedup); - - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew); - await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( - mockANewDedup, - ); - }); - - it('should deduplicate and save mockANewLocal', async () => { - mockFs({ - '/yarn.lock': mockANewLocal, - }); - - const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze({ - localPackages: new Map([ - [ - 'b', - { - packageJson: { version: '2.0.1' }, - } as ExtendedPackage, - ], - ]), - }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '0.0.0-use.local', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockANewLocal); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockANewLocalDedup); - - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( - mockANewLocal, - ); - await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); - await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( - mockANewLocalDedup, - ); - }); - describe('diff', () => { const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER} a@^1: diff --git a/packages/cli-node/src/monorepo/Lockfile.ts b/packages/cli-node/src/monorepo/Lockfile.ts new file mode 100644 index 0000000000..efdf1e48d2 --- /dev/null +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseSyml } from '@yarnpkg/parsers'; +import fs from 'fs-extra'; + +const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; + +type LockfileData = { + [entry: string]: { + version: string; + resolved?: string; + integrity?: string /* old */; + checksum?: string /* new */; + dependencies?: { [name: string]: string }; + peerDependencies?: { [name: string]: string }; + }; +}; + +type LockfileQueryEntry = { + range: string; + version: string; + dataKey: string; +}; + +type LockfileDiffEntry = { + name: string; + range: string; +}; + +type LockfileDiff = { + added: LockfileDiffEntry[]; + changed: LockfileDiffEntry[]; + removed: LockfileDiffEntry[]; +}; + +// these are special top level yarn keys. +// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9 +const SPECIAL_OBJECT_KEYS = [ + `__metadata`, + `version`, + `resolution`, + `dependencies`, + `peerDependencies`, + `dependenciesMeta`, + `peerDependenciesMeta`, + `binaries`, +]; + +export class Lockfile { + static async load(path: string) { + const lockfileContents = await fs.readFile(path, 'utf8'); + return Lockfile.parse(lockfileContents); + } + + static parse(content: string) { + let data: LockfileData; + try { + data = parseSyml(content); + } catch (err) { + throw new Error(`Failed yarn.lock parse, ${err}`); + } + + const packages = new Map(); + + for (const [key, value] of Object.entries(data)) { + if (SPECIAL_OBJECT_KEYS.includes(key)) continue; + + const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? []; + if (!name) { + throw new Error(`Failed to parse yarn.lock entry '${key}'`); + } + + let queries = packages.get(name); + if (!queries) { + queries = []; + packages.set(name, queries); + } + for (let range of ranges.split(/\s*,\s*/)) { + if (range.startsWith(`${name}@`)) { + range = range.slice(`${name}@`.length); + } + if (range.startsWith('npm:')) { + range = range.slice('npm:'.length); + } + queries.push({ range, version: value.version, dataKey: key }); + } + } + + return new Lockfile(packages, data); + } + + private constructor( + private readonly packages: Map, + private readonly data: LockfileData, + ) {} + + createSimplifiedDependencyGraph(): Map> { + const graph = new Map>(); + + for (const [name, entries] of this.packages) { + const dependencies = new Set( + entries.flatMap(e => { + const data = this.data[e.dataKey]; + return [ + ...Object.keys(data?.dependencies ?? {}), + ...Object.keys(data?.peerDependencies ?? {}), + ]; + }), + ); + graph.set(name, dependencies); + } + + return graph; + } + + /** + * Diff with another lockfile, returning entries that have been + * added, changed, and removed compared to the other lockfile. + */ + diff(otherLockfile: Lockfile): LockfileDiff { + const diff = { + added: new Array<{ name: string; range: string }>(), + changed: new Array<{ name: string; range: string }>(), + removed: new Array<{ name: string; range: string }>(), + }; + + // Keeps track of packages that only exist in this lockfile + const remainingOldNames = new Set(this.packages.keys()); + + for (const [name, otherQueries] of otherLockfile.packages) { + remainingOldNames.delete(name); + + const thisQueries = this.packages.get(name); + // If the packages doesn't exist in this lockfile, add all entries + if (!thisQueries) { + diff.removed.push(...otherQueries.map(q => ({ name, range: q.range }))); + continue; + } + + const remainingOldRanges = new Set(thisQueries.map(q => q.range)); + + for (const otherQuery of otherQueries) { + remainingOldRanges.delete(otherQuery.range); + + const thisQuery = thisQueries.find(q => q.range === otherQuery.range); + if (!thisQuery) { + diff.removed.push({ name, range: otherQuery.range }); + continue; + } + + const otherPkg = otherLockfile.data[otherQuery.dataKey]; + const thisPkg = this.data[thisQuery.dataKey]; + if (otherPkg && thisPkg) { + const thisCheck = thisPkg.integrity || thisPkg.checksum; + const otherCheck = otherPkg.integrity || otherPkg.checksum; + if (thisCheck !== otherCheck) { + diff.changed.push({ name, range: otherQuery.range }); + } + } + } + + for (const thisRange of remainingOldRanges) { + diff.added.push({ name, range: thisRange }); + } + } + + for (const name of remainingOldNames) { + const queries = this.packages.get(name) ?? []; + diff.added.push(...queries.map(q => ({ name, range: q.range }))); + } + + return diff; + } +} diff --git a/packages/cli-node/src/monorepo/PackageGraph.test.ts b/packages/cli-node/src/monorepo/PackageGraph.test.ts index d8edff9c81..8f34999f93 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.test.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.test.ts @@ -17,7 +17,7 @@ import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from './PackageGraph'; -import { Lockfile } from '../versioning/Lockfile'; +import { Lockfile } from './Lockfile'; import { listChangedFiles, readFileAtRef } from '../git'; jest.mock('../git'); diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index bb203c01a5..704b35d531 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -19,7 +19,7 @@ import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; import { PackageRole } from '../role'; import { listChangedFiles, readFileAtRef } from '../git'; -import { Lockfile } from '../versioning'; +import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; type PackageJSON = Package['packageJson']; diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts new file mode 100644 index 0000000000..62bab92134 --- /dev/null +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -0,0 +1,322 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { ExtendedPackage } from '../monorepo'; +import { Lockfile } from './Lockfile'; + +const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +`; + +const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 +`; + +const mockA = `${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x: + version "2.0.1" + +b@^2: + version "2.0.0" +`; + +const mockADedup = `${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x, b@^2: + version "2.0.1" +`; + +const mockB = `${LEGACY_HEADER} +"@s/a@*", "@s/a@1 || 2", "@s/a@^1": + version "1.0.1" + +"@s/a@^2.0.x": + version "2.0.0" +`; + +const mockBDedup = `${LEGACY_HEADER} +"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x": + version "2.0.0" + +"@s/a@^1": + version "1.0.1" +`; + +describe('Lockfile', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should load and serialize mockA', async () => { + mockFs({ + '/yarn.lock': mockA, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + 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', dataKey: 'b@2.0.x' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, + ]); + expect(lockfile.toString()).toBe(mockA); + }); + + it('should deduplicate and save mockA', async () => { + mockFs({ + '/yarn.lock': mockA, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze({ localPackages: new Map() }); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [], + newVersions: [ + { + name: 'b', + range: '^2', + oldVersion: '2.0.0', + newVersion: '2.0.1', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockA); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockADedup); + + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup); + }); + + it('should deduplicate mockB', async () => { + mockFs({ + '/yarn.lock': mockB, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze({ localPackages: new Map() }); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [ + { + name: '@s/a', + oldRange: '^1', + newRange: '^2.0.x', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + ], + newVersions: [ + { + name: '@s/a', + range: '*', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + { + name: '@s/a', + range: '1 || 2', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockB); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockBDedup); + }); +}); + +const mockANew = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 2.0.1 + +b@^2: + version: 2.0.0 +`; + +const mockANewDedup = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 2.0.1 + +b@^2: + version: 2.0.1 +`; + +const mockANewLocal = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 0.0.0-use.local + +b@^2: + version: 2.0.0 +`; + +const mockANewLocalDedup = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 0.0.0-use.local + +b@^2: + version: 0.0.0-use.local +`; + +describe('New Lockfile', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should load and serialize mockANew', async () => { + mockFs({ + '/yarn.lock': mockANew, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + 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', 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); + }); + + it('should deduplicate and save mockANew', async () => { + mockFs({ + '/yarn.lock': mockANew, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze({ localPackages: new Map() }); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [], + newVersions: [ + { + name: 'b', + range: '^2', + oldVersion: '2.0.0', + newVersion: '2.0.1', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockANew); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockANewDedup); + + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewDedup, + ); + }); + + it('should deduplicate and save mockANewLocal', async () => { + mockFs({ + '/yarn.lock': mockANewLocal, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze({ + localPackages: new Map([ + [ + 'b', + { + packageJson: { version: '2.0.1' }, + } as ExtendedPackage, + ], + ]), + }); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [], + newVersions: [ + { + name: 'b', + range: '^2', + oldVersion: '2.0.0', + newVersion: '0.0.0-use.local', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockANewLocal); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockANewLocalDedup); + + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewLocal, + ); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewLocalDedup, + ); + }); +}); diff --git a/packages/cli-node/src/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts similarity index 80% rename from packages/cli-node/src/versioning/Lockfile.ts rename to packages/cli/src/lib/versioning/Lockfile.ts index 4eadaf55f9..b502272776 100644 --- a/packages/cli-node/src/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -39,17 +39,6 @@ type LockfileQueryEntry = { dataKey: string; }; -type LockfileDiffEntry = { - name: string; - range: string; -}; - -type LockfileDiff = { - added: LockfileDiffEntry[]; - changed: LockfileDiffEntry[]; - removed: LockfileDiffEntry[]; -}; - /** Entries that have an invalid version range, for example an npm tag */ type AnalyzeResultInvalidRange = { name: string; @@ -340,84 +329,6 @@ export class Lockfile { } } - createSimplifiedDependencyGraph(): Map> { - const graph = new Map>(); - - for (const [name, entries] of this.packages) { - const dependencies = new Set( - entries.flatMap(e => { - const data = this.data[e.dataKey]; - return [ - ...Object.keys(data?.dependencies ?? {}), - ...Object.keys(data?.peerDependencies ?? {}), - ]; - }), - ); - graph.set(name, dependencies); - } - - return graph; - } - - /** - * Diff with another lockfile, returning entries that have been - * added, changed, and removed compared to the other lockfile. - */ - diff(otherLockfile: Lockfile): LockfileDiff { - const diff = { - added: new Array<{ name: string; range: string }>(), - changed: new Array<{ name: string; range: string }>(), - removed: new Array<{ name: string; range: string }>(), - }; - - // Keeps track of packages that only exist in this lockfile - const remainingOldNames = new Set(this.packages.keys()); - - for (const [name, otherQueries] of otherLockfile.packages) { - remainingOldNames.delete(name); - - const thisQueries = this.packages.get(name); - // If the packages doesn't exist in this lockfile, add all entries - if (!thisQueries) { - diff.removed.push(...otherQueries.map(q => ({ name, range: q.range }))); - continue; - } - - const remainingOldRanges = new Set(thisQueries.map(q => q.range)); - - for (const otherQuery of otherQueries) { - remainingOldRanges.delete(otherQuery.range); - - const thisQuery = thisQueries.find(q => q.range === otherQuery.range); - if (!thisQuery) { - diff.removed.push({ name, range: otherQuery.range }); - continue; - } - - const otherPkg = otherLockfile.data[otherQuery.dataKey]; - const thisPkg = this.data[thisQuery.dataKey]; - if (otherPkg && thisPkg) { - const thisCheck = thisPkg.integrity || thisPkg.checksum; - const otherCheck = otherPkg.integrity || otherPkg.checksum; - if (thisCheck !== otherCheck) { - diff.changed.push({ name, range: otherQuery.range }); - } - } - } - - for (const thisRange of remainingOldRanges) { - diff.added.push({ name, range: thisRange }); - } - } - - for (const name of remainingOldNames) { - const queries = this.packages.get(name) ?? []; - diff.added.push(...queries.map(q => ({ name, range: q.range }))); - } - - return diff; - } - async save(path: string) { await fs.writeFile(path, this.toString(), 'utf8'); } diff --git a/packages/cli-node/src/versioning/index.ts b/packages/cli/src/lib/versioning/index.ts similarity index 100% rename from packages/cli-node/src/versioning/index.ts rename to packages/cli/src/lib/versioning/index.ts diff --git a/packages/cli-node/src/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts similarity index 100% rename from packages/cli-node/src/versioning/packages.test.ts rename to packages/cli/src/lib/versioning/packages.test.ts diff --git a/packages/cli-node/src/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts similarity index 100% rename from packages/cli-node/src/versioning/packages.ts rename to packages/cli/src/lib/versioning/packages.ts