From 4f73352608f24468d23b5354193550139532d178 Mon Sep 17 00:00:00 2001 From: Mark David Avery Date: Sat, 7 May 2022 17:30:09 -0700 Subject: [PATCH 1/2] feat: updated to new crossversion lockfile parser to facilitate upgrading to yarn 3 Signed-off-by: Mark David Avery --- .changeset/fresh-items-punch.md | 5 ++ packages/cli/package.json | 1 + packages/cli/src/lib/version.test.ts | 7 +- .../cli/src/lib/versioning/Lockfile.test.ts | 89 +++++++++++++++++++ packages/cli/src/lib/versioning/Lockfile.ts | 46 ++++++++-- yarn.lock | 18 ++-- 6 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 .changeset/fresh-items-punch.md diff --git a/.changeset/fresh-items-punch.md b/.changeset/fresh-items-punch.md new file mode 100644 index 0000000000..1893dc7661 --- /dev/null +++ b/.changeset/fresh-items-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated Lockfile to support new versions of yarn as well as the legacy 1 version diff --git a/packages/cli/package.json b/packages/cli/package.json index 67af644dfa..be88386e64 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -58,6 +58,7 @@ "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "^3.0.0-rc.4", "bfj": "^7.0.2", "buffer": "^6.0.3", "chalk": "^4.0.0", diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index c8edf9382f..fb9bf83e7c 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -25,9 +25,14 @@ describe('createPackageVersionProvider', () => { mockFs.restore(); }); + const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +`; + it('should provide package versions', async () => { mockFs({ - 'yarn.lock': ` + 'yarn.lock': `${HEADER} "a@^0.1.0": version "0.1.5" diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index f182aabdad..bad7ab6719 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -153,3 +153,92 @@ describe('Lockfile', () => { expect(lockfile.toString()).toBe(mockBDedup); }); }); + +const newHeader = `# 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 mockANew = `${newHeader} +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: + version: 2.0.1 + +b@^2: + version: 2.0.0 +`; + +const mockANewDedup = `${newHeader} +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: + version: 2.0.1 + +b@^2: + version: 2.0.1 +`; + +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' }]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1' }, + { range: '^2', version: '2.0.0' }, + ]); + 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(); + 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()).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewDedup, + ); + }); +}); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index d3517e3c83..d2ad54188b 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -16,10 +16,8 @@ import fs from 'fs-extra'; import semver from 'semver'; -import { - parse as parseLockfile, - stringify as stringifyLockfile, -} from '@yarnpkg/lockfile'; +import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; +import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; @@ -66,9 +64,40 @@ type AnalyzeResult = { newRanges: AnalyzeResultNewRange[]; }; +function parseLockfile(lockfileContents: string) { + try { + return { + object: parseSyml(lockfileContents), + type: 'success', + }; + } catch (err) { + return { + object: null, + type: err, + }; + } +} + +// the new yarn header is handled out of band of the parsing +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 +const NEW_HEADER = `${[ + `# This file is generated by running "yarn install" inside your project.\n`, + `# Manual changes might be lost - proceed with caution!\n`, +].join(``)}\n`; + +function stringifyLockfile(data: LockfileData, legacy: boolean) { + return legacy + ? legacyStringifyLockfile(data) + : NEW_HEADER + stringifySyml(data); +} +// taken from yarn parser package +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 +const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; + export class Lockfile { static async load(path: string) { const lockfileContents = await fs.readFile(path, 'utf8'); + const legacy = LEGACY_REGEX.test(lockfileContents); const lockfile = parseLockfile(lockfileContents); if (lockfile.type !== 'success') { throw new Error(`Failed yarn.lock parse with ${lockfile.type}`); @@ -79,7 +108,7 @@ export class Lockfile { for (const [key, value] of Object.entries(data)) { const [, name, range] = ENTRY_PATTERN.exec(key) ?? []; - if (!name) { + if (!name && key !== '__metadata') { throw new Error(`Failed to parse yarn.lock entry '${key}'`); } @@ -91,13 +120,14 @@ export class Lockfile { queries.push({ range, version: value.version }); } - return new Lockfile(path, packages, data); + return new Lockfile(path, packages, data, legacy); } private constructor( private readonly path: string, private readonly packages: Map, private readonly data: LockfileData, + private readonly legacy: boolean = false, ) {} /** Get the entries for a single package in the lockfile */ @@ -120,7 +150,7 @@ export class Lockfile { }; for (const [name, allEntries] of this.packages) { - if (filter && !filter(name)) { + if (typeof name !== 'string' || (filter && !filter(name))) { continue; } @@ -267,6 +297,6 @@ export class Lockfile { } toString() { - return stringifyLockfile(this.data); + return stringifyLockfile(this.data, this.legacy); } } diff --git a/yarn.lock b/yarn.lock index 2754e61e43..b0e362bf07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6524,9 +6524,9 @@ integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== "@types/react-dom@*", "@types/react-dom@<18.0.0", "@types/react-dom@^17": - version "17.0.15" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.15.tgz#f2c8efde11521a4b7991e076cb9c70ba3bb0d156" - integrity sha512-Tr9VU9DvNoHDWlmecmcsE5ZZiUkYx+nKBzum4Oxe1K0yJVyBlfbq7H3eXjxXqJczBKqPGq3EgfTru4MgKb9+Yw== + version "17.0.16" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.16.tgz#7caba93cf2806c51e64d620d8dff4bae57e06cc4" + integrity sha512-DWcXf8EbMrO/gWnQU7Z88Ws/p16qxGpPyjTKTpmBSFKeE+HveVubqGO1CVK7FrwlWD5MuOcvh8gtd0/XO38NdQ== dependencies: "@types/react" "^17" @@ -7262,6 +7262,14 @@ resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +"@yarnpkg/parsers@^3.0.0-rc.4": + version "3.0.0-rc.4" + resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.4.tgz#d7b19fa22ce7ff2423e5cbf008f7be0a85e9f1e3" + integrity sha512-ScXXCUwGdx+aEIP20U8VEGtuXmxcMPFdJTb9G9a8MRpQPxIkky/GYXEL5Hf4oqJJXGyCv/DN31zfmxyj31SLKw== + dependencies: + js-yaml "^3.10.0" + tslib "^1.13.0" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -15818,7 +15826,7 @@ js-yaml@4.1.0, js-yaml@=4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6.1, js-yaml@^3.8.3: +js-yaml@^3.10.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.6.1, js-yaml@^3.8.3: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -24318,7 +24326,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== From eca26d9a3bb3c64bcb33c2bb368b5e5050a6a0e5 Mon Sep 17 00:00:00 2001 From: Mark David Avery Date: Sun, 8 May 2022 20:45:58 -0700 Subject: [PATCH 2/2] fix: pulling out entires that are special to yarn configs from pacakge deps Signed-off-by: Mark David Avery --- packages/cli/src/lib/versioning/Lockfile.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index d2ad54188b..278778b6a6 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -94,6 +94,19 @@ function stringifyLockfile(data: LockfileData, legacy: boolean) { // https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; +// 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'); @@ -107,8 +120,10 @@ export class Lockfile { const packages = new Map(); for (const [key, value] of Object.entries(data)) { + if (SPECIAL_OBJECT_KEYS.includes(key)) continue; + const [, name, range] = ENTRY_PATTERN.exec(key) ?? []; - if (!name && key !== '__metadata') { + if (!name) { throw new Error(`Failed to parse yarn.lock entry '${key}'`); } @@ -150,7 +165,7 @@ export class Lockfile { }; for (const [name, allEntries] of this.packages) { - if (typeof name !== 'string' || (filter && !filter(name))) { + if (filter && !filter(name)) { continue; }