From 0a2719a5ab7c0205f8e70a2ce96e82c0b6b31199 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Jan 2022 01:52:05 +0100 Subject: [PATCH] cli: add initial role types and detection Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 3 +- .../src/lib/role/detectPackageRole.test.ts | 295 ++++++++++++++++++ .../cli/src/lib/role/detectPackageRole.ts | 117 +++++++ packages/cli/src/lib/role/index.ts | 22 ++ packages/cli/src/lib/role/types.ts | 34 ++ 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/lib/role/detectPackageRole.test.ts create mode 100644 packages/cli/src/lib/role/detectPackageRole.ts create mode 100644 packages/cli/src/lib/role/index.ts create mode 100644 packages/cli/src/lib/role/types.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index aad08a21eb..41cefe6c53 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -112,7 +112,8 @@ "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yml-loader": "^2.1.0", - "yn": "^4.0.0" + "yn": "^4.0.0", + "zod": "^3.11.6" }, "devDependencies": { "@backstage/backend-common": "^0.10.6", diff --git a/packages/cli/src/lib/role/detectPackageRole.test.ts b/packages/cli/src/lib/role/detectPackageRole.test.ts new file mode 100644 index 0000000000..f513a5aa3a --- /dev/null +++ b/packages/cli/src/lib/role/detectPackageRole.test.ts @@ -0,0 +1,295 @@ +/* + * 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 { detectPackageRole } from './detectPackageRole'; + +describe('detectPackageRole', () => { + it('detects explicit package roles', () => { + expect( + detectPackageRole({ + backstage: { + role: 'web-library', + }, + }), + ).toEqual({ + role: 'web-library', + platform: 'web', + }); + + expect( + detectPackageRole({ + backstage: { + role: 'app', + }, + }), + ).toEqual({ + role: 'app', + platform: 'web', + }); + + expect(() => + detectPackageRole({ + name: 'test', + backstage: {}, + }), + ).toThrow('Package test must specify a role in the "backstage" field'); + + expect(() => + detectPackageRole({ + name: 'test', + backstage: { role: 'invalid' }, + }), + ).toThrow(`Unknown role 'invalid' in package test`); + }); + + it('detects the role of example-app', () => { + expect( + detectPackageRole({ + name: 'example-app', + private: true, + bundled: true, + scripts: { + start: 'backstage-cli app:serve', + build: 'backstage-cli app:build', + clean: 'backstage-cli clean', + test: 'backstage-cli test', + 'test:e2e': + 'start-server-and-test start http://localhost:3000 cy:dev', + 'test:e2e:ci': + 'start-server-and-test start http://localhost:3000 cy:run', + lint: 'backstage-cli lint', + 'cy:dev': 'cypress open', + 'cy:run': 'cypress run', + }, + }), + ).toEqual({ + role: 'app', + platform: 'web', + }); + }); + + it('detects the role of example-backend', () => { + expect( + detectPackageRole({ + name: 'example-backend', + main: 'dist/index.cjs.js', + types: 'src/index.ts', + scripts: { + build: 'backstage-cli backend:bundle', + 'build-image': + 'docker build ../.. -f Dockerfile --tag example-backend', + start: 'backstage-cli backend:dev', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + clean: 'backstage-cli clean', + 'migrate:create': 'knex migrate:make -x ts', + }, + }), + ).toEqual({ + role: 'backend', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-catalog', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli plugin:build', + start: 'backstage-cli plugin:serve', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + diff: 'backstage-cli plugin:diff', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-frontend', + platform: 'web', + }); + }); + + it('detects the role of @backstage/plugin-catalog-backend', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-backend', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + start: 'backstage-cli backend:dev', + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-backend', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-catalog-react', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-react', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'web-library', + platform: 'web', + }); + }); + + it('detects the role of @backstage/plugin-catalog-common', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-common', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + module: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test --passWithNoTests', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'common-library', + platform: 'common', + }); + }); + + it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-backend-module-ldap', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-backend-module', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-permission-node', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-permission-node', + main: 'src/index.ts', + types: 'src/index.ts', + homepage: 'https://backstage.io', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'node-library', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-analytics-module-ga', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-analytics-module-ga', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli plugin:build', + start: 'backstage-cli plugin:serve', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + diff: 'backstage-cli plugin:diff', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-frontend-module', + platform: 'web', + }); + }); +}); diff --git a/packages/cli/src/lib/role/detectPackageRole.ts b/packages/cli/src/lib/role/detectPackageRole.ts new file mode 100644 index 0000000000..f0941c1e42 --- /dev/null +++ b/packages/cli/src/lib/role/detectPackageRole.ts @@ -0,0 +1,117 @@ +/* + * 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 { PackageRoleInfo } from './types'; + +const packageRoles: PackageRoleInfo[] = [ + { role: 'app', platform: 'web' }, + { role: 'backend', platform: 'node' }, + { role: 'cli', platform: 'node' }, + { role: 'web-library', platform: 'web' }, + { role: 'node-library', platform: 'node' }, + { role: 'common-library', platform: 'common' }, + { role: 'plugin-frontend', platform: 'web' }, + { role: 'plugin-frontend-module', platform: 'web' }, + { role: 'plugin-backend', platform: 'node' }, + { role: 'plugin-backend-module', platform: 'node' }, +]; +const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); + +const backstagePackageSchema = z.object({ + name: z.string().optional(), + scripts: z + .object({ + start: z.string().optional(), + build: z.string().optional(), + }) + .optional(), + backstage: z + .object({ + role: 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 detectPackageRole( + pkgJson: unknown, +): PackageRoleInfo | undefined { + const pkg = backstagePackageSchema.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`, + ); + } + + const roleInfo = packageRoles.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown role '${role}' in package ${pkg.name}`); + } + return roleInfo; + } + + if (pkg.scripts?.start?.includes('app:serve')) { + return roleMap.app; + } + if (pkg.scripts?.build?.includes('backend:bundle')) { + return roleMap.backend; + } + if (pkg.name?.includes('plugin') && pkg.name?.includes('backend-module')) { + return roleMap['plugin-backend-module']; + } + if (pkg.name?.includes('plugin') && pkg.name?.includes('module')) { + return roleMap['plugin-frontend-module']; + } + if (pkg.scripts?.start?.includes('plugin:serve')) { + return roleMap['plugin-frontend']; + } + if (pkg.scripts?.start?.includes('backend:dev')) { + return roleMap['plugin-backend']; + } + + 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 roleMap['common-library']; + } + if (moduleEntry || mainEntry?.endsWith('.esm.js')) { + return roleMap['web-library']; + } + if (mainEntry) { + return roleMap['node-library']; + } + } else if (mainEntry) { + return roleMap.cli; + } + + return undefined; +} diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts new file mode 100644 index 0000000000..41d65073d4 --- /dev/null +++ b/packages/cli/src/lib/role/index.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export type { + PackageRoleInfo, + PackagePlatform, + PackageRoleName, +} from './types'; +export { detectPackageRole } from './detectPackageRole'; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts new file mode 100644 index 0000000000..1f5afe7926 --- /dev/null +++ b/packages/cli/src/lib/role/types.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +export type PackageRoleName = + | 'app' + | 'backend' + | 'cli' + | 'web-library' + | 'node-library' + | 'common-library' + | 'plugin-frontend' + | 'plugin-frontend-module' + | 'plugin-backend' + | 'plugin-backend-module'; + +export type PackagePlatform = 'node' | 'web' | 'common'; + +export interface PackageRoleInfo { + role: PackageRoleName; + platform: PackagePlatform; +}