diff --git a/.changeset/cli-node-lockfile-tostring.md b/.changeset/cli-node-lockfile-tostring.md new file mode 100644 index 0000000000..bf4fc31225 --- /dev/null +++ b/.changeset/cli-node-lockfile-tostring.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. diff --git a/.changeset/cli-versioning-consolidation.md b/.changeset/cli-versioning-consolidation.md new file mode 100644 index 0000000000..114b931c9b --- /dev/null +++ b/.changeset/cli-versioning-consolidation.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 7d198f0a3f..014ed56992 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -35,6 +35,7 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0", "fs-extra": "^11.2.0", "semver": "^7.5.3", @@ -43,6 +44,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/test-utils": "workspace:^" + "@backstage/test-utils": "workspace:^", + "@types/yarnpkg__lockfile": "^1.1.4" } } diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index b3d8fe5641..cf27d92894 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -111,6 +111,7 @@ export class Lockfile { keys(): IterableIterator; static load(path: string): Promise; static parse(content: string): Lockfile; + toString(): string; } // @public diff --git a/packages/cli-node/src/monorepo/Lockfile.test.ts b/packages/cli-node/src/monorepo/Lockfile.test.ts index 03b50fea90..c2d35f7352 100644 --- a/packages/cli-node/src/monorepo/Lockfile.test.ts +++ b/packages/cli-node/src/monorepo/Lockfile.test.ts @@ -15,6 +15,7 @@ */ import { Lockfile } from './Lockfile'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 @@ -29,7 +30,74 @@ __metadata: cacheKey: 8 `; -describe('New Lockfile', () => { +const mockLegacy = `${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 mockModern = `${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 +`; + +describe('Lockfile', () => { + const mockDir = createMockDirectory(); + + it('should load and serialize a legacy lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockLegacy, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('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(mockLegacy); + }); + + it('should load and serialize a modern lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockModern, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('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(mockModern); + }); +}); + +describe('Lockfile advanced', () => { 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 index ff624191af..65e22dcf13 100644 --- a/packages/cli-node/src/monorepo/Lockfile.ts +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -14,12 +14,22 @@ * limitations under the License. */ -import { parseSyml } from '@yarnpkg/parsers'; +import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; +import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; import crypto from 'node:crypto'; import fs from 'fs-extra'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; +// 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`; + +// 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; + /** @internal */ type LockfileData = { [entry: string]: { @@ -97,6 +107,8 @@ export class Lockfile { * @public */ static parse(content: string): Lockfile { + const legacy = LEGACY_REGEX.test(content); + let data: LockfileData; try { data = parseSyml(content); @@ -130,18 +142,21 @@ export class Lockfile { } } - return new Lockfile(packages, data); + return new Lockfile(packages, data, legacy); } private readonly packages: Map; private readonly data: LockfileData; + private readonly legacy: boolean; private constructor( packages: Map, data: LockfileData, + legacy: boolean = false, ) { this.packages = packages; this.data = data; + this.legacy = legacy; } /** Returns the name of all packages available in the lockfile */ @@ -154,6 +169,15 @@ export class Lockfile { return this.packages.keys(); } + /** + * Serialize the lockfile back to a string. + */ + toString(): string { + return this.legacy + ? legacyStringifyLockfile(this.data) + : NEW_HEADER + stringifySyml(this.data); + } + /** * Creates a simplified dependency graph from the lockfile data, where each * key is a package, and the value is a set of all packages that it depends on diff --git a/packages/cli/package.json b/packages/cli/package.json index 27a1b92471..9a4e690e92 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -77,8 +77,6 @@ "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "^3.0.0", "bfj": "^9.0.2", "buffer": "^6.0.3", "chalk": "^4.0.0", @@ -182,7 +180,6 @@ "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-sources": "^3.2.3", - "@types/yarnpkg__lockfile": "^1.1.4", "del": "^8.0.0", "esbuild-loader": "^4.0.0", "eslint-webpack-plugin": "^4.2.0", diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index d582c397c3..86b92e35c3 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -15,7 +15,7 @@ */ import { packageVersions, createPackageVersionProvider } from './version'; -import { Lockfile } from './versioning'; +import { Lockfile } from '@backstage/cli-node'; import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; import { createMockDirectory } from '@backstage/backend-test-utils'; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index aa8556534f..94337710d6 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import semver from 'semver'; import { findOwnPaths } from '@backstage/cli-common'; -import { Lockfile } from './versioning'; +import { Lockfile } from '@backstage/cli-node'; /* eslint-disable-next-line no-restricted-syntax */ const ownPaths = findOwnPaths(__dirname); diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts deleted file mode 100644 index d6996b9143..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 { Lockfile } from './Lockfile'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -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" -`; - -describe('Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockA', async () => { - mockDir.setContent({ - 'yarn.lock': mockA, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('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); - }); -}); - -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 -`; - -describe('New Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockANew', async () => { - mockDir.setContent({ - 'yarn.lock': mockANew, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('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); - }); -}); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts deleted file mode 100644 index 7a9a2c801a..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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 { parseSyml, stringifySyml } from '@yarnpkg/parsers'; -import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; - -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; -}; - -// 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`; - -// 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'); - return Lockfile.parse(lockfileContents); - } - - static parse(content: string) { - const legacy = LEGACY_REGEX.test(content); - - 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, legacy); - } - - private readonly packages: Map; - private readonly data: LockfileData; - private readonly legacy: boolean; - - private constructor( - packages: Map, - data: LockfileData, - legacy: boolean = false, - ) { - this.packages = packages; - this.data = data; - this.legacy = legacy; - } - - /** Get the entries for a single package in the lockfile */ - get(name: string): LockfileQueryEntry[] | undefined { - return this.packages.get(name); - } - - /** Returns the name of all packages available in the lockfile */ - keys(): IterableIterator { - return this.packages.keys(); - } - - toString() { - return this.legacy - ? legacyStringifyLockfile(this.data) - : NEW_HEADER + stringifySyml(this.data); - } -} diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli/src/lib/versioning/index.ts deleted file mode 100644 index e0b9280ee6..0000000000 --- a/packages/cli/src/lib/versioning/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -export { Lockfile } from './Lockfile'; -export { fetchPackageInfo, mapDependencies } from './packages'; -export type { YarnInfoInspectData } from './packages'; diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index c50a9da373..c6732e2af1 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -17,8 +17,11 @@ import { version as cliVersion } from '../../../../package.json'; import os from 'node:os'; import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; -import { Lockfile } from '../../../lib/versioning'; -import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { + BackstagePackageJson, + Lockfile, + PackageGraph, +} from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index cc662f9d5e..0061d07eba 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -19,7 +19,7 @@ import * as runObj from '@backstage/cli-common'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; -import { YarnInfoInspectData } from '../../../../lib/versioning/packages'; +import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { NotFoundError } from '@backstage/errors'; @@ -69,8 +69,8 @@ jest.mock('@backstage/cli-common', () => { }); const mockFetchPackageInfo = jest.fn(); -jest.mock('../../../../lib/versioning/packages', () => { - const actual = jest.requireActual('../../../../lib/versioning/packages'); +jest.mock('../../lib/versioning/packages', () => { + const actual = jest.requireActual('../../lib/versioning/packages'); return { ...actual, fetchPackageInfo: (name: string) => mockFetchPackageInfo(name), diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 4283028216..7ecd908d8a 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -31,13 +31,12 @@ import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; +import { Lockfile, runConcurrentTasks } from '@backstage/cli-node'; import { fetchPackageInfo, - Lockfile, mapDependencies, YarnInfoInspectData, -} from '../../../../lib/versioning'; -import { runConcurrentTasks } from '@backstage/cli-node'; +} from '../../lib/versioning/packages'; import { getManifestByReleaseLine, getManifestByVersion, diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/modules/migrate/lib/versioning/packages.test.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.test.ts rename to packages/cli/src/modules/migrate/lib/versioning/packages.test.ts diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/modules/migrate/lib/versioning/packages.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.ts rename to packages/cli/src/modules/migrate/lib/versioning/packages.ts diff --git a/packages/cli/src/lib/versioning/yarn.ts b/packages/cli/src/modules/migrate/lib/versioning/yarn.ts similarity index 100% rename from packages/cli/src/lib/versioning/yarn.ts rename to packages/cli/src/modules/migrate/lib/versioning/yarn.ts diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 511e656c91..45478957ac 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -24,7 +24,7 @@ import startCase from 'lodash/startCase'; import upperCase from 'lodash/upperCase'; import upperFirst from 'lodash/upperFirst'; import lowerFirst from 'lodash/lowerFirst'; -import { Lockfile } from '../../../../lib/versioning'; +import { Lockfile } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; import { createPackageVersionProvider } from '../../../../lib/version'; diff --git a/yarn.lock b/yarn.lock index 14a3f7453e..74484a0056 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3272,6 +3272,8 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" + "@types/yarnpkg__lockfile": "npm:^1.1.4" + "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:^3.0.0" fs-extra: "npm:^11.2.0" semver: "npm:^7.5.3" @@ -3343,11 +3345,8 @@ __metadata: "@types/terser-webpack-plugin": "npm:^5.0.4" "@types/webpack-env": "npm:^1.15.2" "@types/webpack-sources": "npm:^3.2.3" - "@types/yarnpkg__lockfile": "npm:^1.1.4" "@typescript-eslint/eslint-plugin": "npm:^8.17.0" "@typescript-eslint/parser": "npm:^8.16.0" - "@yarnpkg/lockfile": "npm:^1.1.0" - "@yarnpkg/parsers": "npm:^3.0.0" bfj: "npm:^9.0.2" buffer: "npm:^6.0.3" chalk: "npm:^4.0.0"