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 b87a2079c6..e36a59b8e2 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..278778b6a6 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,53 @@ 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; + +// 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'); + const legacy = LEGACY_REGEX.test(lockfileContents); const lockfile = parseLockfile(lockfileContents); if (lockfile.type !== 'success') { throw new Error(`Failed yarn.lock parse with ${lockfile.type}`); @@ -78,6 +120,8 @@ 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) { throw new Error(`Failed to parse yarn.lock entry '${key}'`); @@ -91,13 +135,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 */ @@ -267,6 +312,6 @@ export class Lockfile { } toString() { - return stringifyLockfile(this.data); + return stringifyLockfile(this.data, this.legacy); } } diff --git a/yarn.lock b/yarn.lock index 00534f1a33..ac124a1fb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6989,6 +6989,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" @@ -15470,7 +15478,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== @@ -23943,7 +23951,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==