From e318b75754f04149b70a632247e16015dd87645e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:02:09 +0200 Subject: [PATCH 01/15] packages: add cli-node Signed-off-by: Patrik Oldsberg --- packages/cli-node/.eslintrc.js | 1 + packages/cli-node/README.md | 8 ++++++++ packages/cli-node/package.json | 30 +++++++++++++++++++++++++++++ packages/cli-node/src/index.ts | 21 ++++++++++++++++++++ packages/cli-node/src/setupTests.ts | 16 +++++++++++++++ yarn.lock | 8 ++++++++ 6 files changed, 84 insertions(+) create mode 100644 packages/cli-node/.eslintrc.js create mode 100644 packages/cli-node/README.md create mode 100644 packages/cli-node/package.json create mode 100644 packages/cli-node/src/index.ts create mode 100644 packages/cli-node/src/setupTests.ts diff --git a/packages/cli-node/.eslintrc.js b/packages/cli-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-node/README.md b/packages/cli-node/README.md new file mode 100644 index 0000000000..d3f7e509da --- /dev/null +++ b/packages/cli-node/README.md @@ -0,0 +1,8 @@ +# @backstage/monorepo-node + +This library provides utilities for building CLI tools for Backstage. + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json new file mode 100644 index 0000000000..a0d4327d94 --- /dev/null +++ b/packages/cli-node/package.json @@ -0,0 +1,30 @@ +{ + "name": "@backstage/cli-node", + "description": "Node.js library for Backstage CLIs", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts new file mode 100644 index 0000000000..fcd851e2ed --- /dev/null +++ b/packages/cli-node/src/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 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. + */ + +/** + * Node.js library for Backstage CLIs + * + * @packageDocumentation + */ diff --git a/packages/cli-node/src/setupTests.ts b/packages/cli-node/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/packages/cli-node/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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 {}; diff --git a/yarn.lock b/yarn.lock index 085a76bb6a..a1f21dde5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3694,6 +3694,14 @@ __metadata: languageName: unknown linkType: soft +"@backstage/cli-node@workspace:packages/cli-node": + version: 0.0.0-use.local + resolution: "@backstage/cli-node@workspace:packages/cli-node" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@backstage/cli@workspace:packages/cli" From c9321bea993ed7b5c56cb418d9699c4f3b3fcced Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:12:42 +0200 Subject: [PATCH 02/15] cli: forklift monorepo utils to cli-node Signed-off-by: Patrik Oldsberg --- .../src}/monorepo/PackageGraph.test.ts | 0 .../src}/monorepo/PackageGraph.ts | 0 .../src}/monorepo/entryPoints.ts | 0 .../lib => cli-node/src}/monorepo/index.ts | 0 .../src}/monorepo/isMonoRepo.ts | 0 .../src}/monorepo/isMonorepo.test.ts | 0 packages/cli-node/src/paths.ts | 20 +++++++++++++++++++ .../src/role/PackageRoles.test.ts} | 2 +- .../src/role/PackageRoles.ts} | 0 .../src/lib => cli-node/src}/role/index.ts | 2 +- .../src/lib => cli-node/src}/role/types.ts | 0 .../src}/versioning/Lockfile.test.ts | 0 .../src}/versioning/Lockfile.ts | 0 .../lib => cli-node/src}/versioning/index.ts | 0 .../src}/versioning/packages.test.ts | 0 .../src}/versioning/packages.ts | 0 16 files changed, 22 insertions(+), 2 deletions(-) rename packages/{cli/src/lib => cli-node/src}/monorepo/PackageGraph.test.ts (100%) rename packages/{cli/src/lib => cli-node/src}/monorepo/PackageGraph.ts (100%) rename packages/{cli/src/lib => cli-node/src}/monorepo/entryPoints.ts (100%) rename packages/{cli/src/lib => cli-node/src}/monorepo/index.ts (100%) rename packages/{cli/src/lib => cli-node/src}/monorepo/isMonoRepo.ts (100%) rename packages/{cli/src/lib => cli-node/src}/monorepo/isMonorepo.test.ts (100%) create mode 100644 packages/cli-node/src/paths.ts rename packages/{cli/src/lib/role/packageRoles.test.ts => cli-node/src/role/PackageRoles.test.ts} (99%) rename packages/{cli/src/lib/role/packageRoles.ts => cli-node/src/role/PackageRoles.ts} (100%) rename packages/{cli/src/lib => cli-node/src}/role/index.ts (96%) rename packages/{cli/src/lib => cli-node/src}/role/types.ts (100%) rename packages/{cli/src/lib => cli-node/src}/versioning/Lockfile.test.ts (100%) rename packages/{cli/src/lib => cli-node/src}/versioning/Lockfile.ts (100%) rename packages/{cli/src/lib => cli-node/src}/versioning/index.ts (100%) rename packages/{cli/src/lib => cli-node/src}/versioning/packages.test.ts (100%) rename packages/{cli/src/lib => cli-node/src}/versioning/packages.ts (100%) diff --git a/packages/cli/src/lib/monorepo/PackageGraph.test.ts b/packages/cli-node/src/monorepo/PackageGraph.test.ts similarity index 100% rename from packages/cli/src/lib/monorepo/PackageGraph.test.ts rename to packages/cli-node/src/monorepo/PackageGraph.test.ts diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts similarity index 100% rename from packages/cli/src/lib/monorepo/PackageGraph.ts rename to packages/cli-node/src/monorepo/PackageGraph.ts diff --git a/packages/cli/src/lib/monorepo/entryPoints.ts b/packages/cli-node/src/monorepo/entryPoints.ts similarity index 100% rename from packages/cli/src/lib/monorepo/entryPoints.ts rename to packages/cli-node/src/monorepo/entryPoints.ts diff --git a/packages/cli/src/lib/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts similarity index 100% rename from packages/cli/src/lib/monorepo/index.ts rename to packages/cli-node/src/monorepo/index.ts diff --git a/packages/cli/src/lib/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts similarity index 100% rename from packages/cli/src/lib/monorepo/isMonoRepo.ts rename to packages/cli-node/src/monorepo/isMonoRepo.ts diff --git a/packages/cli/src/lib/monorepo/isMonorepo.test.ts b/packages/cli-node/src/monorepo/isMonorepo.test.ts similarity index 100% rename from packages/cli/src/lib/monorepo/isMonorepo.test.ts rename to packages/cli-node/src/monorepo/isMonorepo.test.ts diff --git a/packages/cli-node/src/paths.ts b/packages/cli-node/src/paths.ts new file mode 100644 index 0000000000..2c658c27b3 --- /dev/null +++ b/packages/cli-node/src/paths.ts @@ -0,0 +1,20 @@ +/* + * 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 { findPaths } from '@backstage/cli-common'; + +/* eslint-disable-next-line no-restricted-syntax */ +export const paths = findPaths(__dirname); diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli-node/src/role/PackageRoles.test.ts similarity index 99% rename from packages/cli/src/lib/role/packageRoles.test.ts rename to packages/cli-node/src/role/PackageRoles.test.ts index 2cd1e29368..4157c42bfc 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli-node/src/role/PackageRoles.test.ts @@ -21,7 +21,7 @@ import { getRoleFromPackage, findRoleFromCommand, detectRoleFromPackage, -} from './packageRoles'; +} from './PackageRoles'; describe('getRoleInfo', () => { it('provides role info by role', () => { diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli-node/src/role/PackageRoles.ts similarity index 100% rename from packages/cli/src/lib/role/packageRoles.ts rename to packages/cli-node/src/role/PackageRoles.ts diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli-node/src/role/index.ts similarity index 96% rename from packages/cli/src/lib/role/index.ts rename to packages/cli-node/src/role/index.ts index 4e1047a628..5c668e87dc 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli-node/src/role/index.ts @@ -25,4 +25,4 @@ export { getRoleFromPackage, findRoleFromCommand, detectRoleFromPackage, -} from './packageRoles'; +} from './PackageRoles'; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli-node/src/role/types.ts similarity index 100% rename from packages/cli/src/lib/role/types.ts rename to packages/cli-node/src/role/types.ts diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli-node/src/versioning/Lockfile.test.ts similarity index 100% rename from packages/cli/src/lib/versioning/Lockfile.test.ts rename to packages/cli-node/src/versioning/Lockfile.test.ts diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli-node/src/versioning/Lockfile.ts similarity index 100% rename from packages/cli/src/lib/versioning/Lockfile.ts rename to packages/cli-node/src/versioning/Lockfile.ts diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli-node/src/versioning/index.ts similarity index 100% rename from packages/cli/src/lib/versioning/index.ts rename to packages/cli-node/src/versioning/index.ts diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli-node/src/versioning/packages.test.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.test.ts rename to packages/cli-node/src/versioning/packages.test.ts diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli-node/src/versioning/packages.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.ts rename to packages/cli-node/src/versioning/packages.ts From 65a7a7620d543de16497a9cc12dce4f47b2abfaa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:30:14 +0200 Subject: [PATCH 03/15] 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 From b396202ac67320394adbb6e52b5027588e668909 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:34:53 +0200 Subject: [PATCH 04/15] cli-node: lift over git Signed-off-by: Patrik Oldsberg --- .../src/lib => cli-node/src/git}/git.test.ts | 0 .../{cli/src/lib => cli-node/src/git}/git.ts | 2 +- packages/cli-node/src/git/index.ts | 17 ++++++++++++++++ packages/cli-node/src/run.ts | 20 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) rename packages/{cli/src/lib => cli-node/src/git}/git.test.ts (100%) rename packages/{cli/src/lib => cli-node/src/git}/git.ts (98%) create mode 100644 packages/cli-node/src/git/index.ts create mode 100644 packages/cli-node/src/run.ts diff --git a/packages/cli/src/lib/git.test.ts b/packages/cli-node/src/git/git.test.ts similarity index 100% rename from packages/cli/src/lib/git.test.ts rename to packages/cli-node/src/git/git.test.ts diff --git a/packages/cli/src/lib/git.ts b/packages/cli-node/src/git/git.ts similarity index 98% rename from packages/cli/src/lib/git.ts rename to packages/cli-node/src/git/git.ts index c0b50f5140..74127eae38 100644 --- a/packages/cli/src/lib/git.ts +++ b/packages/cli-node/src/git/git.ts @@ -17,7 +17,7 @@ import { assertError, ForwardedError } from '@backstage/errors'; import { execFile as execFileCb } from 'child_process'; import { promisify } from 'util'; -import { paths } from './paths'; +import { paths } from '../paths'; const execFile = promisify(execFileCb); diff --git a/packages/cli-node/src/git/index.ts b/packages/cli-node/src/git/index.ts new file mode 100644 index 0000000000..d962f57258 --- /dev/null +++ b/packages/cli-node/src/git/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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 { listChangedFiles, readFileAtRef } from './git'; diff --git a/packages/cli-node/src/run.ts b/packages/cli-node/src/run.ts new file mode 100644 index 0000000000..e924b5c5b8 --- /dev/null +++ b/packages/cli-node/src/run.ts @@ -0,0 +1,20 @@ +/* + * 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 { execFile as execFileCb } from 'child_process'; +import { promisify } from 'util'; + +export const execFile = promisify(execFileCb); From 43ccc06c5db9f47774882cd5a9520eb761ecea85 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:35:25 +0200 Subject: [PATCH 05/15] cli-node: add missing deps Signed-off-by: Patrik Oldsberg --- packages/cli-node/package.json | 13 ++++++++++++- yarn.lock | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index a0d4327d94..4f044e9710 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -21,8 +21,19 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/types": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@yarnpkg/parsers": "^3.0.0-rc.4", + "fs-extra": "10.1.0", + "semver": "^7.3.2", + "zod": "^3.21.4" + }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "mock-fs": "^5.2.0" }, "files": [ "dist" diff --git a/yarn.lock b/yarn.lock index a1f21dde5c..c4570f3dce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3699,6 +3699,15 @@ __metadata: resolution: "@backstage/cli-node@workspace:packages/cli-node" dependencies: "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": ^1.1.3 + "@yarnpkg/parsers": ^3.0.0-rc.4 + fs-extra: 10.1.0 + mock-fs: ^5.2.0 + semver: ^7.3.2 + zod: ^3.21.4 languageName: unknown linkType: soft From 9b3b4e6aef488586a0934e3c78c9a2f948cc32da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:38:56 +0200 Subject: [PATCH 06/15] cli-node: refactor git into GitUtils Signed-off-by: Patrik Oldsberg --- .../src/git/{git.test.ts => GitUtils.test.ts} | 10 ++- .../cli-node/src/git/{git.ts => GitUtils.ts} | 88 ++++++++++--------- packages/cli-node/src/git/index.ts | 2 +- packages/cli-node/src/index.ts | 2 + .../src/monorepo/PackageGraph.test.ts | 12 +-- .../cli-node/src/monorepo/PackageGraph.ts | 6 +- 6 files changed, 64 insertions(+), 56 deletions(-) rename packages/cli-node/src/git/{git.test.ts => GitUtils.test.ts} (85%) rename packages/cli-node/src/git/{git.ts => GitUtils.ts} (50%) diff --git a/packages/cli-node/src/git/git.test.ts b/packages/cli-node/src/git/GitUtils.test.ts similarity index 85% rename from packages/cli-node/src/git/git.test.ts rename to packages/cli-node/src/git/GitUtils.test.ts index 2e5d464796..e1ebe5513c 100644 --- a/packages/cli-node/src/git/git.test.ts +++ b/packages/cli-node/src/git/GitUtils.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { runGit, listChangedFiles } from './git'; +import { runGit, GitUtils } from './GitUtils'; describe('runGit', () => { it('runs a git command', async () => { @@ -43,10 +43,14 @@ describe('runGit', () => { describe('listChangedFiles', () => { it('requires a ref', async () => { - await expect(listChangedFiles('')).rejects.toThrow('ref is required'); + await expect(GitUtils.listChangedFiles('')).rejects.toThrow( + 'ref is required', + ); }); it('should return something', async () => { - await expect(listChangedFiles('HEAD')).resolves.toEqual(expect.any(Array)); + await expect(GitUtils.listChangedFiles('HEAD')).resolves.toEqual( + expect.any(Array), + ); }); }); diff --git a/packages/cli-node/src/git/git.ts b/packages/cli-node/src/git/GitUtils.ts similarity index 50% rename from packages/cli-node/src/git/git.ts rename to packages/cli-node/src/git/GitUtils.ts index 74127eae38..df04b1c6a2 100644 --- a/packages/cli-node/src/git/git.ts +++ b/packages/cli-node/src/git/GitUtils.ts @@ -15,11 +15,8 @@ */ import { assertError, ForwardedError } from '@backstage/errors'; -import { execFile as execFileCb } from 'child_process'; -import { promisify } from 'util'; import { paths } from '../paths'; - -const execFile = promisify(execFileCb); +import { execFile } from '../run'; /** * Run a git command, trimming the output splitting it into lines. @@ -43,44 +40,55 @@ export async function runGit(...args: string[]) { } /** - * Returns a sorted list of all files that have changed since the merge base - * of the provided `ref` and HEAD, as well as all files that are not tracked by git. + * Utilities for working with git. + * + * @public */ -export async function listChangedFiles(ref: string) { - if (!ref) { - throw new Error('ref is required'); +export class GitUtils { + /** + * Returns a sorted list of all files that have changed since the merge base + * of the provided `ref` and HEAD, as well as all files that are not tracked by git. + */ + static async listChangedFiles(ref: string) { + if (!ref) { + throw new Error('ref is required'); + } + + let diffRef = ref; + try { + const [base] = await runGit('merge-base', 'HEAD', ref); + diffRef = base; + } catch { + // silently fall back to using the ref directly if merge base is not available + } + + const tracked = await runGit('diff', '--name-only', diffRef); + const untracked = await runGit( + 'ls-files', + '--others', + '--exclude-standard', + ); + + return Array.from(new Set([...tracked, ...untracked])); } - let diffRef = ref; - try { - const [base] = await runGit('merge-base', 'HEAD', ref); - diffRef = base; - } catch { - // silently fall back to using the ref directly if merge base is not available + /** + * Returns the contents of a file at a specific ref. + */ + static async readFileAtRef(path: string, ref: string) { + let showRef = ref; + try { + const [base] = await runGit('merge-base', 'HEAD', ref); + showRef = base; + } catch { + // silently fall back to using the ref directly if merge base is not available + } + + const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], { + shell: true, + cwd: paths.targetRoot, + maxBuffer: 1024 * 1024 * 50, + }); + return stdout; } - - const tracked = await runGit('diff', '--name-only', diffRef); - const untracked = await runGit('ls-files', '--others', '--exclude-standard'); - - return Array.from(new Set([...tracked, ...untracked])); -} - -/** - * Returns the contents of a file at a specific ref. - */ -export async function readFileAtRef(path: string, ref: string) { - let showRef = ref; - try { - const [base] = await runGit('merge-base', 'HEAD', ref); - showRef = base; - } catch { - // silently fall back to using the ref directly if merge base is not available - } - - const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], { - shell: true, - cwd: paths.targetRoot, - maxBuffer: 1024 * 1024 * 50, - }); - return stdout; } diff --git a/packages/cli-node/src/git/index.ts b/packages/cli-node/src/git/index.ts index d962f57258..b4dd156ed2 100644 --- a/packages/cli-node/src/git/index.ts +++ b/packages/cli-node/src/git/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { listChangedFiles, readFileAtRef } from './git'; +export { GitUtils } from './GitUtils'; diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index fcd851e2ed..51913e9b9e 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -19,3 +19,5 @@ * * @packageDocumentation */ + +export * from './git'; diff --git a/packages/cli-node/src/monorepo/PackageGraph.test.ts b/packages/cli-node/src/monorepo/PackageGraph.test.ts index 8f34999f93..9bc9cbf72b 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.test.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.test.ts @@ -18,16 +18,10 @@ import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from './PackageGraph'; import { Lockfile } from './Lockfile'; -import { listChangedFiles, readFileAtRef } from '../git'; +import { GitUtils } from '../git'; -jest.mock('../git'); - -const mockListChangedFiles = listChangedFiles as jest.MockedFunction< - typeof listChangedFiles ->; -const mockReadFileAtRef = readFileAtRef as jest.MockedFunction< - typeof readFileAtRef ->; +const mockListChangedFiles = jest.spyOn(GitUtils, 'listChangedFiles'); +const mockReadFileAtRef = jest.spyOn(GitUtils, 'readFileAtRef'); jest.mock('../paths', () => ({ paths: { diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 704b35d531..2ab0edb68a 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -18,7 +18,7 @@ import path from 'path'; import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; import { PackageRole } from '../role'; -import { listChangedFiles, readFileAtRef } from '../git'; +import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; @@ -214,7 +214,7 @@ export class PackageGraph extends Map { ref: string; analyzeLockfile?: boolean; }) { - const changedFiles = await listChangedFiles(options.ref); + const changedFiles = await GitUtils.listChangedFiles(options.ref); const dirMap = new Map( Array.from(this.values()).map(pkg => [ @@ -265,7 +265,7 @@ export class PackageGraph extends Map { paths.resolveTargetRoot('yarn.lock'), ); otherLockfile = Lockfile.parse( - await readFileAtRef('yarn.lock', options.ref), + await GitUtils.readFileAtRef('yarn.lock', options.ref), ); } catch (error) { console.warn( From 011217c8e00222f57570acbb28ef5d37a18ea2f4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 11:57:07 +0200 Subject: [PATCH 07/15] cli-node: refactor role utils into PackageRoles + document, move stuff back Signed-off-by: Patrik Oldsberg --- packages/cli-node/src/index.ts | 1 + .../cli-node/src/monorepo/PackageGraph.ts | 2 +- packages/cli-node/src/role/PackageRoles.ts | 188 ------------------ .../src/{role => roles}/PackageRoles.test.ts | 79 ++------ packages/cli-node/src/roles/PackageRoles.ts | 186 +++++++++++++++++ .../cli-node/src/{role => roles}/index.ts | 7 +- .../cli-node/src/{role => roles}/types.ts | 21 ++ packages/cli/src/lib/role.test.ts | 57 ++++++ packages/cli/src/lib/role.ts | 34 ++++ 9 files changed, 318 insertions(+), 257 deletions(-) delete mode 100644 packages/cli-node/src/role/PackageRoles.ts rename packages/cli-node/src/{role => roles}/PackageRoles.test.ts (82%) create mode 100644 packages/cli-node/src/roles/PackageRoles.ts rename packages/cli-node/src/{role => roles}/index.ts (85%) rename packages/cli-node/src/{role => roles}/types.ts (75%) create mode 100644 packages/cli/src/lib/role.test.ts create mode 100644 packages/cli/src/lib/role.ts diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 51913e9b9e..ef43d7bff7 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -21,3 +21,4 @@ */ export * from './git'; +export * from './roles'; diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 2ab0edb68a..c01d76c11f 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -17,7 +17,7 @@ import path from 'path'; import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; -import { PackageRole } from '../role'; +import { PackageRole } from '../roles'; import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; diff --git a/packages/cli-node/src/role/PackageRoles.ts b/packages/cli-node/src/role/PackageRoles.ts deleted file mode 100644 index 685848fd26..0000000000 --- a/packages/cli-node/src/role/PackageRoles.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2022 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 { z } from 'zod'; -import fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { paths } from '../paths'; -import { PackageRole, PackageRoleInfo } from './types'; - -const packageRoleInfos: PackageRoleInfo[] = [ - { - role: 'frontend', - platform: 'web', - output: ['bundle'], - }, - { - role: 'backend', - platform: 'node', - output: ['bundle'], - }, - { - role: 'cli', - platform: 'node', - output: ['cjs'], - }, - { - role: 'web-library', - platform: 'web', - output: ['types', 'esm'], - }, - { - role: 'node-library', - platform: 'node', - output: ['types', 'cjs'], - }, - { - role: 'common-library', - platform: 'common', - output: ['types', 'esm', 'cjs'], - }, - { - role: 'frontend-plugin', - platform: 'web', - output: ['types', 'esm'], - }, - { - role: 'frontend-plugin-module', - platform: 'web', - output: ['types', 'esm'], - }, - { - role: 'backend-plugin', - platform: 'node', - output: ['types', 'cjs'], - }, - { - role: 'backend-plugin-module', - platform: 'node', - output: ['types', 'cjs'], - }, -]; - -export function getRoleInfo(role: string): PackageRoleInfo { - const roleInfo = packageRoleInfos.find(r => r.role === role); - if (!roleInfo) { - throw new Error(`Unknown package role '${role}'`); - } - return roleInfo; -} - -const readSchema = z.object({ - name: z.string().optional(), - backstage: z - .object({ - role: z.string().optional(), - }) - .optional(), -}); - -export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { - const pkg = readSchema.parse(pkgJson); - - // If there's an explicit role, use that. - if (pkg.backstage) { - const { role } = pkg.backstage; - if (!role) { - throw new Error( - `Package ${pkg.name} must specify a role in the "backstage" field`, - ); - } - - return getRoleInfo(role).role; - } - - return undefined; -} - -export async function findRoleFromCommand( - opts: OptionValues, -): Promise { - if (opts.role) { - return getRoleInfo(opts.role)?.role; - } - - const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const info = getRoleFromPackage(pkg); - if (!info) { - throw new Error(`Target package must have 'backstage.role' set`); - } - return info; -} - -const detectionSchema = z.object({ - name: z.string().optional(), - scripts: z - .object({ - start: z.string().optional(), - build: z.string().optional(), - }) - .optional(), - publishConfig: z - .object({ - main: z.string().optional(), - types: z.string().optional(), - module: z.string().optional(), - }) - .optional(), - main: z.string().optional(), - types: z.string().optional(), - module: z.string().optional(), -}); - -export function detectRoleFromPackage( - pkgJson: unknown, -): PackageRole | undefined { - const pkg = detectionSchema.parse(pkgJson); - - if (pkg.scripts?.start?.includes('app:serve')) { - return 'frontend'; - } - if (pkg.scripts?.build?.includes('backend:bundle')) { - return 'backend'; - } - if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { - return 'backend-plugin-module'; - } - if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { - return 'frontend-plugin-module'; - } - if (pkg.scripts?.start?.includes('plugin:serve')) { - return 'frontend-plugin'; - } - if (pkg.scripts?.start?.includes('backend:dev')) { - return 'backend-plugin'; - } - - const mainEntry = pkg.publishConfig?.main || pkg.main; - const moduleEntry = pkg.publishConfig?.module || pkg.module; - const typesEntry = pkg.publishConfig?.types || pkg.types; - if (typesEntry) { - if (mainEntry && moduleEntry) { - return 'common-library'; - } - if (moduleEntry || mainEntry?.endsWith('.esm.js')) { - return 'web-library'; - } - if (mainEntry) { - return 'node-library'; - } - } else if (mainEntry) { - return 'cli'; - } - - return undefined; -} diff --git a/packages/cli-node/src/role/PackageRoles.test.ts b/packages/cli-node/src/roles/PackageRoles.test.ts similarity index 82% rename from packages/cli-node/src/role/PackageRoles.test.ts rename to packages/cli-node/src/roles/PackageRoles.test.ts index 4157c42bfc..80e320f1f4 100644 --- a/packages/cli-node/src/role/PackageRoles.test.ts +++ b/packages/cli-node/src/roles/PackageRoles.test.ts @@ -14,30 +14,23 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; -import { Command } from 'commander'; -import { - getRoleInfo, - getRoleFromPackage, - findRoleFromCommand, - detectRoleFromPackage, -} from './PackageRoles'; +import { PackageRoles } from './PackageRoles'; describe('getRoleInfo', () => { it('provides role info by role', () => { - expect(getRoleInfo('web-library')).toEqual({ + expect(PackageRoles.getRoleInfo('web-library')).toEqual({ role: 'web-library', platform: 'web', output: ['types', 'esm'], }); - expect(getRoleInfo('frontend')).toEqual({ + expect(PackageRoles.getRoleInfo('frontend')).toEqual({ role: 'frontend', platform: 'web', output: ['bundle'], }); - expect(() => getRoleInfo('invalid')).toThrow( + expect(() => PackageRoles.getRoleInfo('invalid')).toThrow( `Unknown package role 'invalid'`, ); }); @@ -46,7 +39,7 @@ describe('getRoleInfo', () => { describe('getRoleFromPackage', () => { it('reads explicit package roles', () => { expect( - getRoleFromPackage({ + PackageRoles.getRoleFromPackage({ backstage: { role: 'web-library', }, @@ -54,7 +47,7 @@ describe('getRoleFromPackage', () => { ).toEqual('web-library'); expect( - getRoleFromPackage({ + PackageRoles.getRoleFromPackage({ backstage: { role: 'frontend', }, @@ -62,14 +55,14 @@ describe('getRoleFromPackage', () => { ).toEqual('frontend'); expect(() => - getRoleFromPackage({ + PackageRoles.getRoleFromPackage({ name: 'test', backstage: {}, }), ).toThrow('Package test must specify a role in the "backstage" field'); expect(() => - getRoleFromPackage({ + PackageRoles.getRoleFromPackage({ name: 'test', backstage: { role: 'invalid' }, }), @@ -77,48 +70,10 @@ describe('getRoleFromPackage', () => { }); }); -describe('findRoleFromCommand', () => { - function mkCommand(args: string) { - const parsed = new Command() - .option('--role ', 'test role') - .parse(['node', 'entry.js', ...args.split(' ')]) as Command; - return parsed.opts(); - } - - beforeEach(() => { - mockFs({ - 'package.json': JSON.stringify({ - name: 'test', - backstage: { - role: 'web-library', - }, - }), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('provides role info by role', async () => { - await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( - 'web-library', - ); - - await expect( - findRoleFromCommand(mkCommand('--role node-library')), - ).resolves.toEqual('node-library'); - - await expect( - findRoleFromCommand(mkCommand('--role invalid')), - ).rejects.toThrow(`Unknown package role 'invalid'`); - }); -}); - describe('detectRoleFromPackage', () => { it('detects the role of example-app', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: 'example-app', private: true, bundled: true, @@ -141,7 +96,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of example-backend', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: 'example-backend', main: 'dist/index.cjs.js', types: 'src/index.ts', @@ -160,7 +115,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-catalog', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-catalog', main: 'src/index.ts', types: 'src/index.ts', @@ -185,7 +140,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-catalog-backend', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend', main: 'src/index.ts', types: 'src/index.ts', @@ -209,7 +164,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-catalog-react', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-catalog-react', main: 'src/index.ts', types: 'src/index.ts', @@ -232,7 +187,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-catalog-common', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-catalog-common', main: 'src/index.ts', types: 'src/index.ts', @@ -256,7 +211,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend-module-ldap', main: 'src/index.ts', types: 'src/index.ts', @@ -279,7 +234,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-permission-node', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-permission-node', main: 'src/index.ts', types: 'src/index.ts', @@ -303,7 +258,7 @@ describe('detectRoleFromPackage', () => { it('detects the role of @backstage/plugin-analytics-module-ga', () => { expect( - detectRoleFromPackage({ + PackageRoles.detectRoleFromPackage({ name: '@backstage/plugin-analytics-module-ga', main: 'src/index.ts', types: 'src/index.ts', diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts new file mode 100644 index 0000000000..401568ad6d --- /dev/null +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -0,0 +1,186 @@ +/* + * Copyright 2022 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 { z } from 'zod'; +import { PackageRole, PackageRoleInfo } from './types'; + +const packageRoleInfos: PackageRoleInfo[] = [ + { + role: 'frontend', + platform: 'web', + output: ['bundle'], + }, + { + role: 'backend', + platform: 'node', + output: ['bundle'], + }, + { + role: 'cli', + platform: 'node', + output: ['cjs'], + }, + { + role: 'web-library', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'node-library', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'common-library', + platform: 'common', + output: ['types', 'esm', 'cjs'], + }, + { + role: 'frontend-plugin', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'frontend-plugin-module', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'backend-plugin', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'backend-plugin-module', + platform: 'node', + output: ['types', 'cjs'], + }, +]; + +const readSchema = z.object({ + name: z.string().optional(), + backstage: z + .object({ + role: z.string().optional(), + }) + .optional(), +}); + +const detectionSchema = z.object({ + name: z.string().optional(), + scripts: z + .object({ + start: z.string().optional(), + build: z.string().optional(), + }) + .optional(), + publishConfig: z + .object({ + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), + }) + .optional(), + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), +}); + +/** + * Utilities for working with Backstage package roles. + * + * @public + */ +export class PackageRoles { + /** + * Get the associated info for a package role. + */ + static getRoleInfo(role: string): PackageRoleInfo { + const roleInfo = packageRoleInfos.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown package role '${role}'`); + } + return roleInfo; + } + + /** + * Given package JSON data, get the package role. + */ + static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { + const pkg = readSchema.parse(pkgJson); + + if (pkg.backstage) { + const { role } = pkg.backstage; + if (!role) { + throw new Error( + `Package ${pkg.name} must specify a role in the "backstage" field`, + ); + } + + return this.getRoleInfo(role).role; + } + + return undefined; + } + + /** + * Attempt to detect the role of a package from its package.json. + */ + static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined { + const pkg = detectionSchema.parse(pkgJson); + + if (pkg.scripts?.start?.includes('app:serve')) { + return 'frontend'; + } + if (pkg.scripts?.build?.includes('backend:bundle')) { + return 'backend'; + } + if ( + pkg.name?.includes('plugin-') && + pkg.name?.includes('-backend-module-') + ) { + return 'backend-plugin-module'; + } + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { + return 'frontend-plugin-module'; + } + if (pkg.scripts?.start?.includes('plugin:serve')) { + return 'frontend-plugin'; + } + if (pkg.scripts?.start?.includes('backend:dev')) { + return 'backend-plugin'; + } + + const mainEntry = pkg.publishConfig?.main || pkg.main; + const moduleEntry = pkg.publishConfig?.module || pkg.module; + const typesEntry = pkg.publishConfig?.types || pkg.types; + if (typesEntry) { + if (mainEntry && moduleEntry) { + return 'common-library'; + } + if (moduleEntry || mainEntry?.endsWith('.esm.js')) { + return 'web-library'; + } + if (mainEntry) { + return 'node-library'; + } + } else if (mainEntry) { + return 'cli'; + } + + return undefined; + } +} diff --git a/packages/cli-node/src/role/index.ts b/packages/cli-node/src/roles/index.ts similarity index 85% rename from packages/cli-node/src/role/index.ts rename to packages/cli-node/src/roles/index.ts index 5c668e87dc..93022ba986 100644 --- a/packages/cli-node/src/role/index.ts +++ b/packages/cli-node/src/roles/index.ts @@ -20,9 +20,4 @@ export type { PackageOutputType, PackageRole, } from './types'; -export { - getRoleInfo, - getRoleFromPackage, - findRoleFromCommand, - detectRoleFromPackage, -} from './PackageRoles'; +export { PackageRoles } from './PackageRoles'; diff --git a/packages/cli-node/src/role/types.ts b/packages/cli-node/src/roles/types.ts similarity index 75% rename from packages/cli-node/src/role/types.ts rename to packages/cli-node/src/roles/types.ts index a3961544f4..a913ff29b6 100644 --- a/packages/cli-node/src/role/types.ts +++ b/packages/cli-node/src/roles/types.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * Backstage package role, see {@link https://backstage.io/docs/local-dev/cli-build-system#package-roles | docs}. + * + * @public + */ export type PackageRole = | 'frontend' | 'backend' @@ -26,9 +31,25 @@ export type PackageRole = | 'backend-plugin' | 'backend-plugin-module'; +/** + * A type of platform that a package can be built for. + * + * @public + */ export type PackagePlatform = 'node' | 'web' | 'common'; + +/** + * The type of output that a package can produce. + * + * @public + */ export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; +/** + * Information about a package role. + * + * @public + */ export interface PackageRoleInfo { role: PackageRole; platform: PackagePlatform; diff --git a/packages/cli/src/lib/role.test.ts b/packages/cli/src/lib/role.test.ts new file mode 100644 index 0000000000..adca2d70b8 --- /dev/null +++ b/packages/cli/src/lib/role.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2022 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 mockFs from 'mock-fs'; +import { Command } from 'commander'; +import { findRoleFromCommand } from './role'; + +describe('findRoleFromCommand', () => { + function mkCommand(args: string) { + const parsed = new Command() + .option('--role ', 'test role') + .parse(['node', 'entry.js', ...args.split(' ')]) as Command; + return parsed.opts(); + } + + beforeEach(() => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'test', + backstage: { + role: 'web-library', + }, + }), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('provides role info by role', async () => { + await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( + 'web-library', + ); + + await expect( + findRoleFromCommand(mkCommand('--role node-library')), + ).resolves.toEqual('node-library'); + + await expect( + findRoleFromCommand(mkCommand('--role invalid')), + ).rejects.toThrow(`Unknown package role 'invalid'`); + }); +}); diff --git a/packages/cli/src/lib/role.ts b/packages/cli/src/lib/role.ts new file mode 100644 index 0000000000..2f77f0d880 --- /dev/null +++ b/packages/cli/src/lib/role.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 { OptionValues } from 'commander'; +import { paths } from './paths'; + +export async function findRoleFromCommand( + opts: OptionValues, +): Promise { + if (opts.role) { + return getRoleInfo(opts.role)?.role; + } + + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const info = getRoleFromPackage(pkg); + if (!info) { + throw new Error(`Target package must have 'backstage.role' set`); + } + return info; +} From 7240ec3232f5ca9b4c5d13ef02ad3ca80cd4c687 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 14:56:44 +0200 Subject: [PATCH 08/15] cli-node: document and refactor PackageGraph Signed-off-by: Patrik Oldsberg --- packages/cli-node/src/index.ts | 1 + .../cli-node/src/monorepo/PackageGraph.ts | 65 ++++++++++++++++--- packages/cli-node/src/monorepo/entryPoints.ts | 4 +- packages/cli-node/src/monorepo/index.ts | 4 +- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index ef43d7bff7..1d3f3ec2ed 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -21,4 +21,5 @@ */ export * from './git'; +export * from './monorepo'; export * from './roles'; diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index c01d76c11f..421c49f9b9 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -22,9 +22,16 @@ import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; import { JsonValue } from '@backstage/types'; -type PackageJSON = Package['packageJson']; +/** + * Known fields in Backstage package.json files. + * + * @public + */ +export interface BackstagePackageJson { + name: string; + version: string; + private?: boolean; -export interface ExtendedPackageJSON extends PackageJSON { main?: string; module?: string; types?: string; @@ -52,20 +59,43 @@ export interface ExtendedPackageJSON extends PackageJSON { alphaTypes?: string; betaTypes?: string; }; + + dependencies?: { + [key: string]: string; + }; + peerDependencies?: { + [key: string]: string; + }; + devDependencies?: { + [key: string]: string; + }; + optionalDependencies?: { + [key: string]: string; + }; } -export type ExtendedPackage = { +/** + * A local Backstage monorepo package + * + * @public + */ +export type BackstagePackage = { dir: string; - packageJson: ExtendedPackageJSON; + packageJson: BackstagePackageJson; }; +/** + * A local package in the monorepo package graph. + * + * @public + */ export type PackageGraphNode = { /** The name of the package */ name: string; /** The directory of the package */ dir: string; /** The package data of the package itself */ - packageJson: ExtendedPackageJSON; + packageJson: BackstagePackageJson; /** All direct local dependencies of the package */ allLocalDependencies: Map; @@ -90,12 +120,23 @@ export type PackageGraphNode = { localOptionalDependents: Map; }; +/** + * Represents a local Backstage monorepo package graph. + * + * @public + */ export class PackageGraph extends Map { - static async listTargetPackages(): Promise { + /** + * Lists all local packages in a monorepo. + */ + static async listTargetPackages(): Promise { const { packages } = await getPackages(paths.targetDir); - return packages as ExtendedPackage[]; + return packages as BackstagePackage[]; } + /** + * Creates a package graph from a list of local packages. + */ static fromPackages(packages: Package[]): PackageGraph { const graph = new PackageGraph(); @@ -112,7 +153,7 @@ export class PackageGraph extends Map { graph.set(name, { name, dir: pkg.dir, - packageJson: pkg.packageJson as ExtendedPackageJSON, + packageJson: pkg.packageJson as BackstagePackageJson, allLocalDependencies: new Map(), publishedLocalDependencies: new Map(), @@ -210,6 +251,14 @@ export class PackageGraph extends Map { return targets; } + /** + * Lists all packages that have changed since a given git ref. + * + * @remarks + * + * If the `analyzeLockfile` option is set to true, the change detection will + * also consider changes to the dependency management lockfile. + */ async listChangedPackages(options: { ref: string; analyzeLockfile?: boolean; diff --git a/packages/cli-node/src/monorepo/entryPoints.ts b/packages/cli-node/src/monorepo/entryPoints.ts index 1e1fd7ed27..0ccbea8708 100644 --- a/packages/cli-node/src/monorepo/entryPoints.ts +++ b/packages/cli-node/src/monorepo/entryPoints.ts @@ -15,7 +15,7 @@ */ import { extname } from 'path'; -import { ExtendedPackageJSON } from './PackageGraph'; +import { BackstagePackageJson } from './PackageGraph'; export interface EntryPoint { mount: string; @@ -47,7 +47,7 @@ function parseEntryPoint(mount: string, path: string): EntryPoint { return { mount, path, name, ext: extname(path) }; } -export function readEntryPoints(pkg: ExtendedPackageJSON): Array { +export function readEntryPoints(pkg: BackstagePackageJson): Array { const exp = pkg.exports; if (typeof exp === 'string') { return [defaultIndex]; diff --git a/packages/cli-node/src/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts index 7156975c07..8a3eae3508 100644 --- a/packages/cli-node/src/monorepo/index.ts +++ b/packages/cli-node/src/monorepo/index.ts @@ -17,6 +17,6 @@ export { PackageGraph } from './PackageGraph'; export type { PackageGraphNode, - ExtendedPackage, - ExtendedPackageJSON, + BackstagePackage, + BackstagePackageJson, } from './PackageGraph'; From fc12d96b1df66527c0aa82c400611ead541dcfe6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:02:03 +0200 Subject: [PATCH 09/15] cli-node: document and export Lockfile Signed-off-by: Patrik Oldsberg --- packages/cli-node/src/monorepo/Lockfile.ts | 38 +++++++++++++++++++--- packages/cli-node/src/monorepo/index.ts | 15 ++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/cli-node/src/monorepo/Lockfile.ts b/packages/cli-node/src/monorepo/Lockfile.ts index efdf1e48d2..8a481c77ad 100644 --- a/packages/cli-node/src/monorepo/Lockfile.ts +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -19,6 +19,7 @@ import fs from 'fs-extra'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; +/** @internal */ type LockfileData = { [entry: string]: { version: string; @@ -30,18 +31,29 @@ type LockfileData = { }; }; +/** @internal */ type LockfileQueryEntry = { range: string; version: string; dataKey: string; }; -type LockfileDiffEntry = { +/** + * An entry for a single difference between two {@link Lockfile}s. + * + * @public + */ +export type LockfileDiffEntry = { name: string; range: string; }; -type LockfileDiff = { +/** + * Represents the difference between two {@link Lockfile}s. + * + * @public + */ +export type LockfileDiff = { added: LockfileDiffEntry[]; changed: LockfileDiffEntry[]; removed: LockfileDiffEntry[]; @@ -60,13 +72,26 @@ const SPECIAL_OBJECT_KEYS = [ `binaries`, ]; +/** + * Represents a package manager lockfile. + * + * @public + */ export class Lockfile { - static async load(path: string) { + /** + * Load a {@link Lockfile} from a file path. + */ + static async load(path: string): Promise { const lockfileContents = await fs.readFile(path, 'utf8'); return Lockfile.parse(lockfileContents); } - static parse(content: string) { + /** + * Parse lockfile contents into a {@link Lockfile}. + * + * @public + */ + static parse(content: string): Lockfile { let data: LockfileData; try { data = parseSyml(content); @@ -108,6 +133,11 @@ export class Lockfile { private readonly data: LockfileData, ) {} + /** + * 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 + * across all versions. + */ createSimplifiedDependencyGraph(): Map> { const graph = new Map>(); diff --git a/packages/cli-node/src/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts index 8a3eae3508..5071849116 100644 --- a/packages/cli-node/src/monorepo/index.ts +++ b/packages/cli-node/src/monorepo/index.ts @@ -14,9 +14,14 @@ * limitations under the License. */ -export { PackageGraph } from './PackageGraph'; -export type { - PackageGraphNode, - BackstagePackage, - BackstagePackageJson, +export { + PackageGraph, + type PackageGraphNode, + type BackstagePackage, + type BackstagePackageJson, } from './PackageGraph'; +export { + Lockfile, + type LockfileDiff, + type LockfileDiffEntry, +} from './Lockfile'; From 3e593913963239880738bbea1406124fe19a992c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:02:53 +0200 Subject: [PATCH 10/15] cli-node: document and export isMonoRepo Signed-off-by: Patrik Oldsberg --- packages/cli-node/src/monorepo/index.ts | 1 + packages/cli-node/src/monorepo/isMonoRepo.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/cli-node/src/monorepo/index.ts b/packages/cli-node/src/monorepo/index.ts index 5071849116..fdb8395c16 100644 --- a/packages/cli-node/src/monorepo/index.ts +++ b/packages/cli-node/src/monorepo/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { isMonoRepo } from './isMonoRepo'; export { PackageGraph, type PackageGraphNode, diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index 9b1ab93762..da78c8dd53 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -17,6 +17,11 @@ import { paths } from '../paths'; import fs from 'fs-extra'; +/** + * Returns try if the current project is a monorepo. + * + * @public + */ export async function isMonoRepo(): Promise { const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); try { From 6438c324b12cc01316a4f87872d156d38f46fdd3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:04:13 +0200 Subject: [PATCH 11/15] cli-node: move back entry point helper Signed-off-by: Patrik Oldsberg --- packages/{cli-node/src/monorepo => cli/src/lib}/entryPoints.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/{cli-node/src/monorepo => cli/src/lib}/entryPoints.ts (100%) diff --git a/packages/cli-node/src/monorepo/entryPoints.ts b/packages/cli/src/lib/entryPoints.ts similarity index 100% rename from packages/cli-node/src/monorepo/entryPoints.ts rename to packages/cli/src/lib/entryPoints.ts From 92abbb3d722f1bdd5bc7d01e3960dac2dedcef87 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:05:41 +0200 Subject: [PATCH 12/15] cli-node: generate API report Signed-off-by: Patrik Oldsberg --- packages/cli-node/api-report.md | 167 ++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 packages/cli-node/api-report.md diff --git a/packages/cli-node/api-report.md b/packages/cli-node/api-report.md new file mode 100644 index 0000000000..91148596b3 --- /dev/null +++ b/packages/cli-node/api-report.md @@ -0,0 +1,167 @@ +## API Report File for "@backstage/cli-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { JsonValue } from '@backstage/types'; +import { Package } from '@manypkg/get-packages'; + +// @public +export type BackstagePackage = { + dir: string; + packageJson: BackstagePackageJson; +}; + +// @public +export interface BackstagePackageJson { + // (undocumented) + backstage?: { + role?: PackageRole; + }; + // (undocumented) + bundled?: boolean; + // (undocumented) + dependencies?: { + [key: string]: string; + }; + // (undocumented) + devDependencies?: { + [key: string]: string; + }; + // (undocumented) + exports?: JsonValue; + // (undocumented) + files?: string[]; + // (undocumented) + main?: string; + // (undocumented) + module?: string; + // (undocumented) + name: string; + // (undocumented) + optionalDependencies?: { + [key: string]: string; + }; + // (undocumented) + peerDependencies?: { + [key: string]: string; + }; + // (undocumented) + private?: boolean; + // (undocumented) + publishConfig?: { + access?: 'public' | 'restricted'; + directory?: string; + registry?: string; + alphaTypes?: string; + betaTypes?: string; + }; + // (undocumented) + scripts?: { + [key: string]: string; + }; + // (undocumented) + types?: string; + // (undocumented) + typesVersions?: Record>; + // (undocumented) + version: string; +} + +// @public +export class GitUtils { + static listChangedFiles(ref: string): Promise; + static readFileAtRef(path: string, ref: string): Promise; +} + +// @public +export function isMonoRepo(): Promise; + +// @public +export class Lockfile { + createSimplifiedDependencyGraph(): Map>; + diff(otherLockfile: Lockfile): LockfileDiff; + static load(path: string): Promise; + static parse(content: string): Lockfile; +} + +// @public +export type LockfileDiff = { + added: LockfileDiffEntry[]; + changed: LockfileDiffEntry[]; + removed: LockfileDiffEntry[]; +}; + +// @public +export type LockfileDiffEntry = { + name: string; + range: string; +}; + +// @public +export class PackageGraph extends Map { + collectPackageNames( + startingPackageNames: string[], + collectFn: (pkg: PackageGraphNode) => Iterable | undefined, + ): Set; + static fromPackages(packages: Package[]): PackageGraph; + listChangedPackages(options: { + ref: string; + analyzeLockfile?: boolean; + }): Promise; + static listTargetPackages(): Promise; +} + +// @public +export type PackageGraphNode = { + name: string; + dir: string; + packageJson: BackstagePackageJson; + allLocalDependencies: Map; + publishedLocalDependencies: Map; + localDependencies: Map; + localDevDependencies: Map; + localOptionalDependencies: Map; + allLocalDependents: Map; + publishedLocalDependents: Map; + localDependents: Map; + localDevDependents: Map; + localOptionalDependents: Map; +}; + +// @public +export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; + +// @public +export type PackagePlatform = 'node' | 'web' | 'common'; + +// @public +export type PackageRole = + | 'frontend' + | 'backend' + | 'cli' + | 'web-library' + | 'node-library' + | 'common-library' + | 'frontend-plugin' + | 'frontend-plugin-module' + | 'backend-plugin' + | 'backend-plugin-module'; + +// @public +export interface PackageRoleInfo { + // (undocumented) + output: PackageOutputType[]; + // (undocumented) + platform: PackagePlatform; + // (undocumented) + role: PackageRole; +} + +// @public +export class PackageRoles { + static detectRoleFromPackage(pkgJson: unknown): PackageRole | undefined; + static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined; + static getRoleInfo(role: string): PackageRoleInfo; +} +``` From 5578c3de6354bf9ef65aaac50fe73b890b0ad0e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:30:43 +0200 Subject: [PATCH 13/15] cli: refactor to use @backstage/cli-node Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 1 + packages/cli/src/commands/build/command.ts | 5 +++-- packages/cli/src/commands/fix.ts | 2 +- .../cli/src/commands/migrate/packageExports.ts | 4 ++-- .../src/commands/migrate/packageLintConfigs.ts | 2 +- .../cli/src/commands/migrate/packageRole.ts | 6 +++--- .../cli/src/commands/migrate/packageScripts.ts | 7 +++---- .../cli/src/commands/migrate/reactRouterDeps.ts | 5 ++--- packages/cli/src/commands/new/new.ts | 2 +- packages/cli/src/commands/repo/build.ts | 17 ++++++++++------- packages/cli/src/commands/repo/clean.ts | 2 +- packages/cli/src/commands/repo/lint.ts | 6 +++--- .../cli/src/commands/repo/list-deprecations.ts | 2 +- packages/cli/src/commands/repo/test.ts | 2 +- .../cli/src/commands/start/startFrontend.ts | 4 ++-- packages/cli/src/commands/versions/bump.ts | 2 +- packages/cli/src/commands/versions/lint.ts | 2 +- packages/cli/src/lib/builder/config.ts | 6 +++--- packages/cli/src/lib/builder/packager.ts | 4 ++-- packages/cli/src/lib/builder/types.ts | 4 ++-- packages/cli/src/lib/bundler/config.ts | 6 +++--- packages/cli/src/lib/config.ts | 2 +- packages/cli/src/lib/entryPoints.ts | 2 +- .../cli/src/lib/packager/createDistWorkspace.ts | 9 ++++++--- packages/cli/src/lib/packager/productionPack.ts | 10 +++++----- packages/cli/src/lib/role.ts | 5 +++-- .../cli/src/lib/versioning/Lockfile.test.ts | 4 ++-- packages/cli/src/lib/versioning/Lockfile.ts | 4 ++-- yarn.lock | 3 ++- 29 files changed, 69 insertions(+), 61 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 5974116843..429cb85e6c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 90b76545ce..507085e0a0 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -16,7 +16,8 @@ import { OptionValues } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; +import { findRoleFromCommand } from '../../lib/role'; +import { PackageRoles } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; import { buildFrontend } from './buildFrontend'; import { buildBackend } from './buildBackend'; @@ -47,7 +48,7 @@ export async function command(opts: OptionValues): Promise { }); } - const roleInfo = getRoleInfo(role); + const roleInfo = PackageRoles.getRoleInfo(role); const outputs = new Set(); diff --git a/packages/cli/src/commands/fix.ts b/packages/cli/src/commands/fix.ts index e4cdc4dc84..263d171fd6 100644 --- a/packages/cli/src/commands/fix.ts +++ b/packages/cli/src/commands/fix.ts @@ -19,7 +19,7 @@ import { ESLint } from 'eslint'; import { join as joinPath, basename } from 'path'; import fs from 'fs-extra'; import { isChildPath } from '@backstage/cli-common'; -import { PackageGraph } from '../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; function isTestPath(filePath: string) { if (!isChildPath(joinPath(paths.targetDir, 'src'), filePath)) { diff --git a/packages/cli/src/commands/migrate/packageExports.ts b/packages/cli/src/commands/migrate/packageExports.ts index ff01f939f9..82d0b73ac2 100644 --- a/packages/cli/src/commands/migrate/packageExports.ts +++ b/packages/cli/src/commands/migrate/packageExports.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { ExtendedPackageJSON, PackageGraph } from '../../lib/monorepo'; +import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; function trimRelative(path: string): string { if (path.startsWith('./')) { @@ -94,7 +94,7 @@ export async function command() { newPackageJson = Object.fromEntries( newPkgEntries, - ) as ExtendedPackageJSON; + ) as BackstagePackageJson; changed = true; } diff --git a/packages/cli/src/commands/migrate/packageLintConfigs.ts b/packages/cli/src/commands/migrate/packageLintConfigs.ts index c0007ef59d..03d42846e8 100644 --- a/packages/cli/src/commands/migrate/packageLintConfigs.ts +++ b/packages/cli/src/commands/migrate/packageLintConfigs.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { PackageGraph } from '../../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; import { runPlain } from '../../lib/run'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts index e8bfb8e7c8..e7abbd3dcd 100644 --- a/packages/cli/src/commands/migrate/packageRole.ts +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -17,8 +17,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; +import { PackageRoles } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; -import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role'; export default async () => { const { packages } = await getPackages(paths.targetDir); @@ -26,12 +26,12 @@ export default async () => { await Promise.all( packages.map(async ({ dir, packageJson: pkg }) => { const { name } = pkg; - const existingRole = getRoleFromPackage(pkg); + const existingRole = PackageRoles.getRoleFromPackage(pkg); if (existingRole) { return; } - const detectedRole = detectRoleFromPackage(pkg); + const detectedRole = PackageRoles.detectRoleFromPackage(pkg); if (!detectedRole) { console.error(`No role detected for package ${name}`); return; diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index c7dab9466b..1552208a5f 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -16,8 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { PackageGraph } from '../../lib/monorepo'; -import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role'; +import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node'; const configArgPattern = /--config[=\s][^\s$]+/; @@ -28,12 +27,12 @@ export async function command() { await Promise.all( packages.map(async ({ dir, packageJson }) => { - const role = getRoleFromPackage(packageJson); + const role = PackageRoles.getRoleFromPackage(packageJson); if (!role) { return; } - const roleInfo = getRoleInfo(role); + const roleInfo = PackageRoles.getRoleInfo(role); const hasStart = !noStartRoles.includes(role); const needsPack = !(roleInfo.output.includes('bundle') || role === 'cli'); const scripts = packageJson.scripts ?? {}; diff --git a/packages/cli/src/commands/migrate/reactRouterDeps.ts b/packages/cli/src/commands/migrate/reactRouterDeps.ts index 3c1ddd293d..1617c855ee 100644 --- a/packages/cli/src/commands/migrate/reactRouterDeps.ts +++ b/packages/cli/src/commands/migrate/reactRouterDeps.ts @@ -16,8 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { PackageGraph } from '../../lib/monorepo'; -import { getRoleFromPackage } from '../../lib/role'; +import { PackageGraph, PackageRoles } from '@backstage/cli-node'; const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom']; const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0'; @@ -27,7 +26,7 @@ export async function command() { await Promise.all( packages.map(async ({ dir, packageJson }) => { - const role = getRoleFromPackage(packageJson); + const role = PackageRoles.getRoleFromPackage(packageJson); if (role === 'frontend') { console.log(`Skipping ${packageJson.name}`); return; diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index 8aaa671614..61e27af3c5 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -19,10 +19,10 @@ import fs from 'fs-extra'; import { join as joinPath } from 'path'; import { OptionValues } from 'commander'; import { FactoryRegistry } from '../../lib/new/FactoryRegistry'; +import { isMonoRepo } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; import { assertError } from '@backstage/errors'; import { Task } from '../../lib/tasks'; -import { isMonoRepo } from '../../lib/monorepo/isMonoRepo'; function parseOptions(optionStrings: string[]): Record { const options: Record = {}; diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index d5602a4008..71e58c6201 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -18,11 +18,13 @@ import chalk from 'chalk'; import { Command, OptionValues } from 'commander'; import { relative as relativePath } from 'path'; import { buildPackages, getOutputsForRole } from '../../lib/builder'; -import { PackageGraph } from '../../lib/monorepo'; -import { ExtendedPackage } from '../../lib/monorepo/PackageGraph'; -import { runParallelWorkers } from '../../lib/parallel'; import { paths } from '../../lib/paths'; -import { detectRoleFromPackage } from '../../lib/role'; +import { + PackageGraph, + BackstagePackage, + PackageRoles, +} from '@backstage/cli-node'; +import { runParallelWorkers } from '../../lib/parallel'; import { buildFrontend } from '../build/buildFrontend'; import { buildBackend } from '../build/buildBackend'; @@ -93,14 +95,15 @@ export async function command(opts: OptionValues, cmd: Command): Promise { packages = Array.from(withDevDependents).map(name => graph.get(name)!); } - const apps = new Array(); - const backends = new Array(); + const apps = new Array(); + const backends = new Array(); const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']); const options = packages.flatMap(pkg => { const role = - pkg.packageJson.backstage?.role ?? detectRoleFromPackage(pkg.packageJson); + pkg.packageJson.backstage?.role ?? + PackageRoles.detectRoleFromPackage(pkg.packageJson); if (!role) { console.warn(`Ignored ${pkg.packageJson.name} because it has no role`); return []; diff --git a/packages/cli/src/commands/repo/clean.ts b/packages/cli/src/commands/repo/clean.ts index 323746e089..13ff061423 100644 --- a/packages/cli/src/commands/repo/clean.ts +++ b/packages/cli/src/commands/repo/clean.ts @@ -18,7 +18,7 @@ import { execFile as execFileCb } from 'child_process'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { promisify } from 'util'; -import { PackageGraph } from '../../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; const execFile = promisify(execFileCb); diff --git a/packages/cli/src/commands/repo/lint.ts b/packages/cli/src/commands/repo/lint.ts index 8e8ec20ff8..05fbf7b87e 100644 --- a/packages/cli/src/commands/repo/lint.ts +++ b/packages/cli/src/commands/repo/lint.ts @@ -17,11 +17,11 @@ import chalk from 'chalk'; import { OptionValues } from 'commander'; import { relative as relativePath } from 'path'; -import { PackageGraph, ExtendedPackageJSON } from '../../lib/monorepo'; -import { runWorkerQueueThreads } from '../../lib/parallel'; +import { PackageGraph, BackstagePackageJson } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; +import { runWorkerQueueThreads } from '../../lib/parallel'; -function depCount(pkg: ExtendedPackageJSON) { +function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; const devDeps = pkg.devDependencies ? Object.keys(pkg.devDependencies).length diff --git a/packages/cli/src/commands/repo/list-deprecations.ts b/packages/cli/src/commands/repo/list-deprecations.ts index 97fa384561..69fe51f78e 100644 --- a/packages/cli/src/commands/repo/list-deprecations.ts +++ b/packages/cli/src/commands/repo/list-deprecations.ts @@ -18,8 +18,8 @@ import chalk from 'chalk'; import { ESLint } from 'eslint'; import { OptionValues } from 'commander'; import { join as joinPath, relative as relativePath } from 'path'; +import { PackageGraph } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; -import { PackageGraph } from '../../lib/monorepo'; export async function command(opts: OptionValues) { const packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/commands/repo/test.ts b/packages/cli/src/commands/repo/test.ts index 2dd778677a..1ca0da5495 100644 --- a/packages/cli/src/commands/repo/test.ts +++ b/packages/cli/src/commands/repo/test.ts @@ -16,7 +16,7 @@ import os from 'os'; import { Command, OptionValues } from 'commander'; -import { PackageGraph } from '../../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; import { paths } from '../../lib/paths'; import { runCheck } from '../../lib/run'; diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 991439c010..e066a40e7a 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -19,10 +19,10 @@ import chalk from 'chalk'; import uniq from 'lodash/uniq'; import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; -import { paths } from '../../lib/paths'; +import { PackageGraph } from '@backstage/cli-node'; import { Lockfile } from '../../lib/versioning'; import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; -import { PackageGraph } from '../../lib/monorepo'; +import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 45b966a52d..997e5f8e60 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -39,7 +39,7 @@ import { ReleaseManifest, } from '@backstage/release-manifests'; import 'global-agent/bootstrap'; -import { PackageGraph } from '../../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; const DEP_TYPES = [ 'dependencies', diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index 8de38548a9..9466fd0ab7 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -18,7 +18,7 @@ import { OptionValues } from 'commander'; import { Lockfile } from '../../lib/versioning'; import { paths } from '../../lib/paths'; import partition from 'lodash/partition'; -import { PackageGraph } from '../../lib/monorepo'; +import { PackageGraph } from '@backstage/cli-node'; // Packages that we try to avoid duplicates for const INCLUDED = [/^@backstage\//]; diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 7bd10cfcc3..8035796cba 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -30,9 +30,9 @@ import { RollupOptions, OutputOptions, RollupWarning } from 'rollup'; import { forwardFileImports } from './plugins'; import { BuildOptions, Output } from './types'; import { paths } from '../paths'; +import { BackstagePackageJson } from '@backstage/cli-node'; import { svgrTemplate } from '../svgrTemplate'; -import { ExtendedPackageJSON } from '../monorepo'; -import { readEntryPoints } from '../monorepo/entryPoints'; +import { readEntryPoints } from '../entryPoints'; const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; @@ -58,7 +58,7 @@ export async function makeRollupConfigs( let targetPkg = options.packageJson; if (!targetPkg) { const packagePath = resolvePath(targetDir, 'package.json'); - targetPkg = (await fs.readJson(packagePath)) as ExtendedPackageJSON; + targetPkg = (await fs.readJson(packagePath)) as BackstagePackageJson; } const onwarn = ({ code, message }: RollupWarning) => { diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index c2256c8417..780f9c2cfe 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -22,7 +22,7 @@ import { paths } from '../paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; import { buildTypeDefinitions } from './buildTypeDefinitions'; -import { getRoleInfo } from '../role'; +import { PackageRoles } from '@backstage/cli-node'; import { runParallelWorkers } from '../parallel'; export function formatErrorMessage(error: any) { @@ -152,7 +152,7 @@ export const buildPackages = async (options: BuildOptions[]) => { export function getOutputsForRole(role: string): Set { const outputs = new Set(); - for (const output of getRoleInfo(role).output) { + for (const output of PackageRoles.getRoleInfo(role).output) { if (output === 'cjs') { outputs.add(Output.cjs); } diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index de7e9b10eb..330419235f 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ExtendedPackageJSON } from '../monorepo'; +import { BackstagePackageJson } from '@backstage/cli-node'; export enum Output { esm, @@ -25,7 +25,7 @@ export enum Output { export type BuildOptions = { logPrefix?: string; targetDir?: string; - packageJson?: ExtendedPackageJSON; + packageJson?: BackstagePackageJson; outputs: Set; minify?: boolean; useApiExtractor?: boolean; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 6fc7482dce..94b2a1c561 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -32,12 +32,12 @@ import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import { BundlingOptions, BackendBundlingOptions } from './types'; import { version } from '../../lib/version'; import { paths as cliPaths } from '../../lib/paths'; +import { BackstagePackage } from '@backstage/cli-node'; import { runPlain } from '../run'; import ESLintPlugin from 'eslint-webpack-plugin'; import pickBy from 'lodash/pickBy'; import yn from 'yn'; -import { readEntryPoints } from '../monorepo/entryPoints'; -import { ExtendedPackage } from '../monorepo'; +import { readEntryPoints } from '../entryPoints'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; @@ -235,7 +235,7 @@ export async function createBackendConfig( // Find all local monorepo packages and their node_modules, and mark them as external. const { packages } = await getPackages(cliPaths.targetDir); const localPackageEntryPoints = packages.flatMap(p => { - const entryPoints = readEntryPoints((p as ExtendedPackage).packageJson); + const entryPoints = readEntryPoints((p as BackstagePackage).packageJson); return entryPoints.map(e => posixPath.join(p.packageJson.name, e.mount)); }); const moduleDirs = packages.map(p => resolvePath(p.dir, 'node_modules')); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index a76571c3f6..b34e1280c0 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -23,7 +23,7 @@ import { ConfigReader } from '@backstage/config'; import { paths } from './paths'; import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; -import { PackageGraph } from './monorepo'; +import { PackageGraph } from '@backstage/cli-node'; type Options = { args: string[]; diff --git a/packages/cli/src/lib/entryPoints.ts b/packages/cli/src/lib/entryPoints.ts index 0ccbea8708..1f6e2feec2 100644 --- a/packages/cli/src/lib/entryPoints.ts +++ b/packages/cli/src/lib/entryPoints.ts @@ -15,7 +15,7 @@ */ import { extname } from 'path'; -import { BackstagePackageJson } from './PackageGraph'; +import { BackstagePackageJson } from '@backstage/cli-node'; export interface EntryPoint { mount: string; diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index 601f442a73..ab8b20919f 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -30,7 +30,6 @@ import { dependencies as cliDependencies, devDependencies as cliDevDependencies, } from '../../../package.json'; -import { PackageGraph, PackageGraphNode } from '../monorepo'; import { BuildOptions, buildPackages, @@ -38,7 +37,11 @@ import { Output, } from '../builder'; import { productionPack } from './productionPack'; -import { getRoleInfo } from '../role'; +import { + PackageRoles, + PackageGraph, + PackageGraphNode, +} from '@backstage/cli-node'; import { runParallelWorkers } from '../parallel'; // These packages aren't safe to pack in parallel since the CLI depends on them @@ -172,7 +175,7 @@ export async function createDistWorkspace( continue; } - if (getRoleInfo(role).output.includes('bundle')) { + if (PackageRoles.getRoleInfo(role).output.includes('bundle')) { console.warn( `Building ${pkg.packageJson.name} separately because it is a bundled package`, ); diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/lib/packager/productionPack.ts index 30f4545b24..aea5df649a 100644 --- a/packages/cli/src/lib/packager/productionPack.ts +++ b/packages/cli/src/lib/packager/productionPack.ts @@ -17,8 +17,8 @@ import fs from 'fs-extra'; import npmPackList from 'npm-packlist'; import { resolve as resolvePath, posix as posixPath } from 'path'; -import { ExtendedPackageJSON } from '../monorepo'; -import { readEntryPoints } from '../monorepo/entryPoints'; +import { BackstagePackageJson } from '@backstage/cli-node'; +import { readEntryPoints } from '../entryPoints'; const PKG_PATH = 'package.json'; const PKG_BACKUP_PATH = 'package.json-prepack'; @@ -35,7 +35,7 @@ export async function productionPack(options: ProductionPackOptions) { const { packageDir, targetDir } = options; const pkgPath = resolvePath(packageDir, PKG_PATH); const pkgContent = await fs.readFile(pkgPath, 'utf8'); - const pkg = JSON.parse(pkgContent) as ExtendedPackageJSON; + const pkg = JSON.parse(pkgContent) as BackstagePackageJson; // If we're making the update in-line, back up the package.json if (!targetDir) { @@ -147,7 +147,7 @@ function resolveEntrypoint(pkg: any, name: string) { // Writes e.g. alpha/package.json async function writeReleaseStageEntrypoint( - pkg: ExtendedPackageJSON, + pkg: BackstagePackageJson, stage: 'alpha' | 'beta', targetDir: string, ) { @@ -178,7 +178,7 @@ const EXPORT_MAP = { * entry points for importers that don't support exports. */ async function prepareExportsEntryPoints( - pkg: ExtendedPackageJSON, + pkg: BackstagePackageJson, packageDir: string, ) { const distPath = resolvePath(packageDir, 'dist'); diff --git a/packages/cli/src/lib/role.ts b/packages/cli/src/lib/role.ts index 2f77f0d880..49f39f789c 100644 --- a/packages/cli/src/lib/role.ts +++ b/packages/cli/src/lib/role.ts @@ -17,16 +17,17 @@ import fs from 'fs-extra'; import { OptionValues } from 'commander'; import { paths } from './paths'; +import { PackageRoles, PackageRole } from '@backstage/cli-node'; export async function findRoleFromCommand( opts: OptionValues, ): Promise { if (opts.role) { - return getRoleInfo(opts.role)?.role; + return PackageRoles.getRoleInfo(opts.role)?.role; } const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const info = getRoleFromPackage(pkg); + const info = PackageRoles.getRoleFromPackage(pkg); if (!info) { throw new Error(`Target package must have 'backstage.role' set`); } diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index 62bab92134..909f8be9aa 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { ExtendedPackage } from '../monorepo'; +import { BackstagePackage } from '@backstage/cli-node'; import { Lockfile } from './Lockfile'; const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. @@ -290,7 +290,7 @@ describe('New Lockfile', () => { 'b', { packageJson: { version: '2.0.1' }, - } as ExtendedPackage, + } as BackstagePackage, ], ]), }); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index b502272776..50d0a77cfd 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import semver from 'semver'; import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; -import { ExtendedPackage } from '../monorepo'; +import { BackstagePackage } from '@backstage/cli-node'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; @@ -156,7 +156,7 @@ export class Lockfile { /** Analyzes the lockfile to identify possible actions and warnings for the entries */ analyze(options: { filter?: (name: string) => boolean; - localPackages: Map; + localPackages: Map; }): AnalyzeResult { const { filter, localPackages } = options; const result: AnalyzeResult = { diff --git a/yarn.lock b/yarn.lock index c4570f3dce..e07a02b7e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3694,7 +3694,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-node@workspace:packages/cli-node": +"@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": version: 0.0.0-use.local resolution: "@backstage/cli-node@workspace:packages/cli-node" dependencies: @@ -3717,6 +3717,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/core-app-api": "workspace:^" From 3d3e338efe2dbcaff86cc7e75c7f0acc8f799c7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:32:55 +0200 Subject: [PATCH 14/15] cli-node: update README with note about difference to cli-common Signed-off-by: Patrik Oldsberg --- packages/cli-node/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli-node/README.md b/packages/cli-node/README.md index d3f7e509da..7988ac1e52 100644 --- a/packages/cli-node/README.md +++ b/packages/cli-node/README.md @@ -1,7 +1,9 @@ -# @backstage/monorepo-node +# @backstage/cli-node This library provides utilities for building CLI tools for Backstage. +The difference between this library and `@backstage/cli-common` is that this library is more feature rich with a larger dependency tree, with less concern for bundle size and installation speed. The `@backstage/cli-common` package on the other hand is intended to be extremely slim and only provide minimal features for use in tools like `@backstage/create-app`. + ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) From 3156b0d85dcdb4b1e24b84b6a8be9ba7c25f98e3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Apr 2023 15:35:20 +0200 Subject: [PATCH 15/15] changesets: added changeset for cli-node refactor Signed-off-by: Patrik Oldsberg --- .changeset/bright-hats-compare.md | 5 +++++ .changeset/forty-adults-divide.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/bright-hats-compare.md create mode 100644 .changeset/forty-adults-divide.md diff --git a/.changeset/bright-hats-compare.md b/.changeset/bright-hats-compare.md new file mode 100644 index 0000000000..148eaa29ce --- /dev/null +++ b/.changeset/bright-hats-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': minor +--- + +Introduced the new `@backstage/cli-node` package, which provides utilities for use across Backstage CLIs. diff --git a/.changeset/forty-adults-divide.md b/.changeset/forty-adults-divide.md new file mode 100644 index 0000000000..55d1e3f5d8 --- /dev/null +++ b/.changeset/forty-adults-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor to move many internal utilities to the new `@backstage/cli-node` package.