From 0a2719a5ab7c0205f8e70a2ce96e82c0b6b31199 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Jan 2022 01:52:05 +0100 Subject: [PATCH 01/19] 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; +} From 4e62242eb1945cd9c9f969144a4e72a451953aa7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 14:03:49 +0100 Subject: [PATCH 02/19] cli: split role detection into read and detect Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/index.ts | 2 +- ...ckageRole.test.ts => packageRoles.test.ts} | 16 +++--- .../{detectPackageRole.ts => packageRoles.ts} | 56 +++++++++++-------- 3 files changed, 43 insertions(+), 31 deletions(-) rename packages/cli/src/lib/role/{detectPackageRole.test.ts => packageRoles.test.ts} (97%) rename packages/cli/src/lib/role/{detectPackageRole.ts => packageRoles.ts} (92%) diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 41d65073d4..45ddbc8823 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -19,4 +19,4 @@ export type { PackagePlatform, PackageRoleName, } from './types'; -export { detectPackageRole } from './detectPackageRole'; +export { detectPackageRole, readPackageRole } from './packageRoles'; diff --git a/packages/cli/src/lib/role/detectPackageRole.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts similarity index 97% rename from packages/cli/src/lib/role/detectPackageRole.test.ts rename to packages/cli/src/lib/role/packageRoles.test.ts index f513a5aa3a..555dc9ce9c 100644 --- a/packages/cli/src/lib/role/detectPackageRole.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { detectPackageRole } from './detectPackageRole'; +import { readPackageRole, detectPackageRole } from './packageRoles'; -describe('detectPackageRole', () => { - it('detects explicit package roles', () => { +describe('readPackageRole', () => { + it('reads explicit package roles', () => { expect( - detectPackageRole({ + readPackageRole({ backstage: { role: 'web-library', }, @@ -30,7 +30,7 @@ describe('detectPackageRole', () => { }); expect( - detectPackageRole({ + readPackageRole({ backstage: { role: 'app', }, @@ -41,20 +41,22 @@ describe('detectPackageRole', () => { }); expect(() => - detectPackageRole({ + readPackageRole({ name: 'test', backstage: {}, }), ).toThrow('Package test must specify a role in the "backstage" field'); expect(() => - detectPackageRole({ + readPackageRole({ name: 'test', backstage: { role: 'invalid' }, }), ).toThrow(`Unknown role 'invalid' in package test`); }); +}); +describe('detectPackageRole', () => { it('detects the role of example-app', () => { expect( detectPackageRole({ diff --git a/packages/cli/src/lib/role/detectPackageRole.ts b/packages/cli/src/lib/role/packageRoles.ts similarity index 92% rename from packages/cli/src/lib/role/detectPackageRole.ts rename to packages/cli/src/lib/role/packageRoles.ts index f0941c1e42..3525cbd314 100644 --- a/packages/cli/src/lib/role/detectPackageRole.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -31,7 +31,38 @@ const packageRoles: PackageRoleInfo[] = [ ]; const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); -const backstagePackageSchema = z.object({ +const readSchema = z.object({ + name: z.string().optional(), + backstage: z + .object({ + role: z.string().optional(), + }) + .optional(), +}); + +export function readPackageRole(pkgJson: unknown): PackageRoleInfo | 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`, + ); + } + + const roleInfo = packageRoles.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown role '${role}' in package ${pkg.name}`); + } + return roleInfo; + } + + return undefined; +} + +const detectionSchema = z.object({ name: z.string().optional(), scripts: z .object({ @@ -39,11 +70,6 @@ const backstagePackageSchema = z.object({ build: z.string().optional(), }) .optional(), - backstage: z - .object({ - role: z.string().optional(), - }) - .optional(), publishConfig: z .object({ main: z.string().optional(), @@ -59,23 +85,7 @@ const backstagePackageSchema = z.object({ 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; - } + const pkg = detectionSchema.parse(pkgJson); if (pkg.scripts?.start?.includes('app:serve')) { return roleMap.app; From 8263cac40e2b9e7b90b7146b0e73b9cb0b98b33c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 14:53:20 +0100 Subject: [PATCH 03/19] cli: bit more specific matching of module roles Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/packageRoles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 3525cbd314..76467d990e 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -93,10 +93,10 @@ export function detectPackageRole( if (pkg.scripts?.build?.includes('backend:bundle')) { return roleMap.backend; } - if (pkg.name?.includes('plugin') && pkg.name?.includes('backend-module')) { + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { return roleMap['plugin-backend-module']; } - if (pkg.name?.includes('plugin') && pkg.name?.includes('module')) { + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { return roleMap['plugin-frontend-module']; } if (pkg.scripts?.start?.includes('plugin:serve')) { From 29260100446370472eaa10cbf2af4e11b5ff8936 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 15:36:06 +0100 Subject: [PATCH 04/19] cli: add new role-based bundle command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/bundle/bundleApp.ts | 39 ++++++++++ .../cli/src/commands/bundle/bundleBackend.ts | 75 +++++++++++++++++++ packages/cli/src/commands/bundle/command.ts | 46 ++++++++++++ packages/cli/src/commands/bundle/index.ts | 17 +++++ packages/cli/src/commands/index.ts | 14 ++++ 5 files changed, 191 insertions(+) create mode 100644 packages/cli/src/commands/bundle/bundleApp.ts create mode 100644 packages/cli/src/commands/bundle/bundleBackend.ts create mode 100644 packages/cli/src/commands/bundle/command.ts create mode 100644 packages/cli/src/commands/bundle/index.ts diff --git a/packages/cli/src/commands/bundle/bundleApp.ts b/packages/cli/src/commands/bundle/bundleApp.ts new file mode 100644 index 0000000000..1ebd1f044e --- /dev/null +++ b/packages/cli/src/commands/bundle/bundleApp.ts @@ -0,0 +1,39 @@ +/* + * 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 { buildBundle } from '../../lib/bundler'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { loadCliConfig } from '../../lib/config'; +import { paths } from '../../lib/paths'; + +interface BundleAppOptions { + writeStats: boolean; + configPaths: string[]; +} + +export async function bundleApp(options: BundleAppOptions) { + const { name } = await fs.readJson(paths.resolveTarget('package.json')); + await buildBundle({ + entry: 'src/index', + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + statsJsonEnabled: options.writeStats, + ...(await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + })), + }); +} diff --git a/packages/cli/src/commands/bundle/bundleBackend.ts b/packages/cli/src/commands/bundle/bundleBackend.ts new file mode 100644 index 0000000000..0f240d87dc --- /dev/null +++ b/packages/cli/src/commands/bundle/bundleBackend.ts @@ -0,0 +1,75 @@ +/* + * 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 os from 'os'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import tar, { CreateOptions } from 'tar'; +import { createDistWorkspace } from '../../lib/packager'; +import { paths } from '../../lib/paths'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { buildPackage, Output } from '../../lib/builder'; + +const BUNDLE_FILE = 'bundle.tar.gz'; +const SKELETON_FILE = 'skeleton.tar.gz'; + +interface BundleBackendOptions { + skipBuildDependencies: boolean; +} + +export async function bundleBackend(options: BundleBackendOptions) { + const targetDir = paths.resolveTarget('dist'); + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + + // We build the target package without generating type declarations. + await buildPackage({ outputs: new Set([Output.cjs]) }); + + const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle')); + try { + await createDistWorkspace([pkg.name], { + targetDir: tmpDir, + buildDependencies: !options.skipBuildDependencies, + buildExcludes: [pkg.name], + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + skeleton: SKELETON_FILE, + }); + + // We built the target backend package using the regular build process, but the result of + // that has now been packed into the dist workspace, so clean up the dist dir. + await fs.remove(targetDir); + await fs.mkdir(targetDir); + + // Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice. + await fs.move( + resolvePath(tmpDir, SKELETON_FILE), + resolvePath(targetDir, SKELETON_FILE), + ); + + // Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache. + await tar.create( + { + file: resolvePath(targetDir, BUNDLE_FILE), + cwd: tmpDir, + portable: true, + noMtime: true, + gzip: true, + } as CreateOptions & { noMtime: boolean }, + [''], + ); + } finally { + await fs.remove(tmpDir); + } +} diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts new file mode 100644 index 0000000000..a13faeee62 --- /dev/null +++ b/packages/cli/src/commands/bundle/command.ts @@ -0,0 +1,46 @@ +/* + * 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 { Command } from 'commander'; +import { paths } from '../../lib/paths'; +import { readPackageRole } from '../../lib/role/packageRoles'; +import { bundleApp } from './bundleApp'; +import { bundleBackend } from './bundleBackend'; + +export async function command(cmd: Command): Promise { + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const roleInfo = readPackageRole(pkg); + if (!roleInfo) { + throw new Error(`Target package must have 'backstage.role' set`); + } + + const options = { + configPaths: cmd.config as string[], + writeStats: Boolean(cmd.stats), + skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + }; + + if (roleInfo.role === 'app') { + return bundleApp(options); + } else if (roleInfo.role === 'backend') { + return bundleBackend(options); + } + + throw new Error( + `Bundle command is not supported for package role '${roleInfo.role}'`, + ); +} diff --git a/packages/cli/src/commands/bundle/index.ts b/packages/cli/src/commands/bundle/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/bundle/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 362771f8f2..e834e0e494 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -133,6 +133,20 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./build').then(m => m.default))); + program + .command('bundle') + .description('Bundle a package for deployment') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .option(...configOption) + .action(lazy(() => import('./bundle').then(m => m.command))); + program .command('lint') .option( From 17a9f90efcd0a4d9cfc9c93d24755b94ca71437c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 16:06:56 +0100 Subject: [PATCH 05/19] cli: add util to get role info by name Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/index.ts | 6 ++++- .../cli/src/lib/role/packageRoles.test.ts | 26 +++++++++++++++++-- packages/cli/src/lib/role/packageRoles.ts | 14 ++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 45ddbc8823..5a1f836036 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -19,4 +19,8 @@ export type { PackagePlatform, PackageRoleName, } from './types'; -export { detectPackageRole, readPackageRole } from './packageRoles'; +export { + getRoleInfo, + detectPackageRole, + readPackageRole, +} from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index 555dc9ce9c..d41cd08488 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,7 +14,29 @@ * limitations under the License. */ -import { readPackageRole, detectPackageRole } from './packageRoles'; +import { + getRoleInfo, + readPackageRole, + detectPackageRole, +} from './packageRoles'; + +describe('getRoleInfo', () => { + it('provides role info by role', () => { + expect(getRoleInfo('web-library')).toEqual({ + role: 'web-library', + platform: 'web', + }); + + expect(getRoleInfo('app')).toEqual({ + role: 'app', + platform: 'web', + }); + + expect(() => getRoleInfo('invalid')).toThrow( + `Unknown package role 'invalid'`, + ); + }); +}); describe('readPackageRole', () => { it('reads explicit package roles', () => { @@ -52,7 +74,7 @@ describe('readPackageRole', () => { name: 'test', backstage: { role: 'invalid' }, }), - ).toThrow(`Unknown role 'invalid' in package test`); + ).toThrow(`Unknown package role 'invalid'`); }); }); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 76467d990e..9d0ec501c6 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -31,6 +31,14 @@ const packageRoles: PackageRoleInfo[] = [ ]; const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); +export function getRoleInfo(role: string): PackageRoleInfo { + const roleInfo = packageRoles.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 @@ -52,11 +60,7 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { ); } - const roleInfo = packageRoles.find(r => r.role === role); - if (!roleInfo) { - throw new Error(`Unknown role '${role}' in package ${pkg.name}`); - } - return roleInfo; + return getRoleInfo(role); } return undefined; From f0ee50cfabd467c357ed955dc1ce19684a9abcb2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 16:39:48 +0100 Subject: [PATCH 06/19] cli: add utility for passing explicit role option Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/bundle/command.ts | 10 +---- packages/cli/src/commands/index.ts | 3 +- packages/cli/src/lib/role/index.ts | 3 +- .../cli/src/lib/role/packageRoles.test.ts | 44 +++++++++++++++++++ packages/cli/src/lib/role/packageRoles.ts | 18 ++++++++ 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts index a13faeee62..44b39e6797 100644 --- a/packages/cli/src/commands/bundle/command.ts +++ b/packages/cli/src/commands/bundle/command.ts @@ -14,19 +14,13 @@ * limitations under the License. */ -import fs from 'fs-extra'; import { Command } from 'commander'; -import { paths } from '../../lib/paths'; -import { readPackageRole } from '../../lib/role/packageRoles'; import { bundleApp } from './bundleApp'; import { bundleBackend } from './bundleBackend'; +import { readRoleForCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const roleInfo = readPackageRole(pkg); - if (!roleInfo) { - throw new Error(`Target package must have 'backstage.role' set`); - } + const roleInfo = await readRoleForCommand(cmd); const options = { configPaths: cmd.config as string[], diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index e834e0e494..860cd0fd62 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -136,6 +136,8 @@ export function registerCommands(program: CommanderStatic) { program .command('bundle') .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') .option( '--skip-build-dependencies', 'Skip the automatic building of local dependencies', @@ -144,7 +146,6 @@ export function registerCommands(program: CommanderStatic) { '--stats', 'If bundle stats are available, write them to the output directory', ) - .option(...configOption) .action(lazy(() => import('./bundle').then(m => m.command))); program diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 5a1f836036..9becfa367b 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -21,6 +21,7 @@ export type { } from './types'; export { getRoleInfo, - detectPackageRole, readPackageRole, + readRoleForCommand, + detectPackageRole, } from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index d41cd08488..f3302d3256 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import mockFs from 'mock-fs'; +import { Command } from 'commander'; import { getRoleInfo, readPackageRole, + readRoleForCommand, detectPackageRole, } from './packageRoles'; @@ -78,6 +81,47 @@ describe('readPackageRole', () => { }); }); +describe('readRoleForCommand', () => { + function mkCommand(args: string) { + return new Command() + .option('--role ', 'test role') + .parse(['node', 'entry.js', ...args.split(' ')]) as Command; + } + + beforeEach(() => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'test', + backstage: { + role: 'web-library', + }, + }), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('provides role info by role', async () => { + await expect(readRoleForCommand(mkCommand(''))).resolves.toEqual({ + role: 'web-library', + platform: 'web', + }); + + await expect( + readRoleForCommand(mkCommand('--role node-library')), + ).resolves.toEqual({ + role: 'node-library', + platform: 'node', + }); + + await expect( + readRoleForCommand(mkCommand('--role invalid')), + ).rejects.toThrow(`Unknown package role 'invalid'`); + }); +}); + describe('detectPackageRole', () => { it('detects the role of example-app', () => { expect( diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 9d0ec501c6..67bee0e64f 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -15,6 +15,9 @@ */ import { z } from 'zod'; +import fs from 'fs-extra'; +import { Command } from 'commander'; +import { paths } from '../paths'; import { PackageRoleInfo } from './types'; const packageRoles: PackageRoleInfo[] = [ @@ -66,6 +69,21 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { return undefined; } +export async function readRoleForCommand( + cmd: Command, +): Promise { + if (cmd.role) { + return getRoleInfo(cmd.role); + } + + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const info = readPackageRole(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 From 9227753a7c30f57c7988dfa0a0e0b8098129aba1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 17:17:23 +0100 Subject: [PATCH 07/19] cli: added role-based start command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 13 ++++ packages/cli/src/commands/start/command.ts | 53 +++++++++++++ packages/cli/src/commands/start/index.ts | 17 +++++ .../cli/src/commands/start/startBackend.ts | 41 ++++++++++ .../cli/src/commands/start/startFrontend.ts | 76 +++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 packages/cli/src/commands/start/command.ts create mode 100644 packages/cli/src/commands/start/index.ts create mode 100644 packages/cli/src/commands/start/startBackend.ts create mode 100644 packages/cli/src/commands/start/startFrontend.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 860cd0fd62..cecbab82e5 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -148,6 +148,19 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./bundle').then(m => m.command))); + program + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + program .command('lint') .option( diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts new file mode 100644 index 0000000000..ff4d3b7c0b --- /dev/null +++ b/packages/cli/src/commands/start/command.ts @@ -0,0 +1,53 @@ +/* + * 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 { Command } from 'commander'; +import { startBackend } from './startBackend'; +import { startFrontend } from './startFrontend'; +import { readRoleForCommand } from '../../lib/role'; + +export async function command(cmd: Command): Promise { + const roleInfo = await readRoleForCommand(cmd); + + const options = { + configPaths: cmd.config as string[], + checksEnabled: Boolean(cmd.check), + inspectEnabled: Boolean(cmd.inspect), + inspectBrkEnabled: Boolean(cmd.inspectBrk), + }; + + switch (roleInfo.role) { + case 'backend': + case 'plugin-backend': + case 'plugin-backend-module': + case 'node-library': + return startBackend(options); + case 'app': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'plugin-frontend': + case 'plugin-frontend-module': + return startFrontend({ entry: 'dev/index', ...options }); + default: + throw new Error( + `Start command is not supported for package role '${roleInfo.role}'`, + ); + } +} diff --git a/packages/cli/src/commands/start/index.ts b/packages/cli/src/commands/start/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/start/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { command } from './command'; diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts new file mode 100644 index 0000000000..61ada7a291 --- /dev/null +++ b/packages/cli/src/commands/start/startBackend.ts @@ -0,0 +1,41 @@ +/* + * 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 { paths } from '../../lib/paths'; +import { serveBackend } from '../../lib/bundler'; + +interface StartBackendOptions { + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; +} + +export async function startBackend(options: StartBackendOptions) { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); + + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); +} diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts new file mode 100644 index 0000000000..e5500c310b --- /dev/null +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -0,0 +1,76 @@ +/* + * 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 chalk from 'chalk'; +import uniq from 'lodash/uniq'; +import { serveBundle } from '../../lib/bundler'; +import { loadCliConfig } from '../../lib/config'; +import { paths } from '../../lib/paths'; +import { Lockfile } from '../../lib/versioning'; +import { includedFilter } from '../versions/lint'; + +interface StartAppOptions { + verifyVersions?: boolean; + entry: string; + + checksEnabled: boolean; + configPaths: string[]; +} + +export async function startFrontend(options: StartAppOptions) { + if (options.verifyVersions) { + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const result = lockfile.analyze({ + filter: includedFilter, + }); + const problemPackages = [...result.newVersions, ...result.newRanges].map( + ({ name }) => name, + ); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `⚠️ Some of the following packages may be outdated or have duplicate installations: + + ${uniq(problemPackages).join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `⚠️ This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + } + + const { name } = await fs.readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ + entry: options.entry, + checksEnabled: options.checksEnabled, + ...(await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + withFilteredKeys: true, + })), + }); + + await waitForExit(); +} From c7169dc440f0560d945be95ef8e541f9ea6b4f50 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 17:52:30 +0100 Subject: [PATCH 08/19] cli: add migrate:package-role command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 5 ++ .../cli/src/commands/migrate/packageRole.ts | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/cli/src/commands/migrate/packageRole.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index cecbab82e5..61ded0741c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -233,6 +233,11 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); + program + .command('migrate:package-role') + .description(`Add package role field to packages that don't have it`) + .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + program .command('versions:bump') .option( diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts new file mode 100644 index 0000000000..86898a6012 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -0,0 +1,69 @@ +/* + * 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 { resolve as resolvePath } from 'path'; +import { getPackages } from '@manypkg/get-packages'; +import { paths } from '../../lib/paths'; +import { readPackageRole, detectPackageRole } from '../../lib/role'; + +export default async () => { + const { packages } = await getPackages(paths.targetDir); + + await Promise.all( + packages.map(async ({ dir, packageJson: pkg }) => { + const { name } = pkg; + const existingRole = readPackageRole(pkg); + if (existingRole) { + return; + } + + const detectedRole = detectPackageRole(pkg); + if (!detectedRole) { + console.error(`No role detected for package ${name}`); + return; + } + + console.log(`Detected package role of ${name} as ${detectedRole.role}`); + + let newPkg = pkg as any; + + const pkgKeys = Object.keys(pkg); + if (pkgKeys.includes('backstage')) { + newPkg.backstage = { + ...newPkg.backstage, + role: detectedRole.role, + }; + } else { + // We insert the backstage field after one of these fields, otherwise at the end + const index = + Math.max( + pkgKeys.indexOf('version'), + pkgKeys.indexOf('private'), + pkgKeys.indexOf('publishConfig'), + ) + 1 || pkgKeys.length; + + const pkgEntries = Object.entries(pkg); + pkgEntries.splice(index, 0, ['backstage', { role: detectedRole.role }]); + newPkg = Object.fromEntries(pkgEntries); + } + + await fs.writeJson(resolvePath(dir, 'package.json'), newPkg, { + spaces: 2, + }); + }), + ); +}; From 0bf983e703cef5c7fb3c98ee0dc32d9918800b6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 13:05:54 +0100 Subject: [PATCH 09/19] cli: add PackageGraph utility for listing extended packages Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/monorepo/PackageGraph.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 9860b92ef6..db7a528494 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Package } from '@manypkg/get-packages'; +import { getPackages, Package } from '@manypkg/get-packages'; +import { paths } from '../paths'; +import { PackageRoleName } from '../role'; type PackageJSON = Package['packageJson']; @@ -25,8 +27,17 @@ export interface ExtendedPackageJSON extends PackageJSON { // The `bundled` field is a field known within Backstage, it means // that the package bundles all of its dependencies in its build output. bundled?: boolean; + + backstage?: { + role?: PackageRoleName; + }; } +export type ExtendedPackage = { + dir: string; + packageJson: ExtendedPackageJSON; +}; + export type PackageGraphNode = { /** The name of the package */ name: string; @@ -47,6 +58,11 @@ export type PackageGraphNode = { }; export class PackageGraph extends Map { + static async listTargetPackages(): Promise { + const { packages } = await getPackages(paths.targetDir); + return packages as ExtendedPackage[]; + } + static fromPackages(packages: Package[]): PackageGraph { const graph = new PackageGraph(); From 189c44104c05ac3a727cd3002e3813bc746a85d7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 13:07:21 +0100 Subject: [PATCH 10/19] cli: added migrate:package-scripts Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 7 ++ .../src/commands/migrate/packageScripts.ts | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/cli/src/commands/migrate/packageScripts.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 61ded0741c..ef2e5ef96a 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -238,6 +238,13 @@ export function registerCommands(program: CommanderStatic) { .description(`Add package role field to packages that don't have it`) .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + program + .command('migrate:package-scripts') + .description('Set package scripts according to each package role') + .action( + lazy(() => import('./migrate/packageScripts').then(m => m.command)), + ); + program .command('versions:bump') .option( diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts new file mode 100644 index 0000000000..58a8481c81 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -0,0 +1,70 @@ +/* + * 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 { resolve as resolvePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import { readPackageRole, PackageRoleName } from '../../lib/role'; + +const bundledRoles: PackageRoleName[] = ['app', 'backend']; +const noStartRoles: PackageRoleName[] = ['cli', 'common-library']; + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + const roleInfo = readPackageRole(packageJson); + if (!roleInfo) { + return; + } + + const hasStart = !noStartRoles.includes(roleInfo.role); + const isBundled = bundledRoles.includes(roleInfo.role); + + const expectedScripts = { + ...(hasStart && { start: 'backstage-cli start' }), + ...(isBundled + ? { bundle: 'backstage-cli bundle' } + : { build: 'backstage-cli build' }), + lint: 'backstage-cli lint', + test: 'backstage-cli test', + clean: 'backstage-cli clean', + ...(!isBundled && { + postpack: 'backstage-cli postpack', + prepack: 'backstage-cli prepack', + }), + }; + + let changed = false; + const currentScripts = (packageJson.scripts = packageJson.scripts || {}); + + for (const [name, value] of Object.entries(expectedScripts)) { + if (currentScripts[name] !== value) { + changed = true; + currentScripts[name] = value; + } + } + + if (changed) { + console.log(`Updating scripts for ${packageJson.name}`); + await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, { + spaces: 2, + }); + } + }), + ); +} From 703314d1a53c5515f960ce43bfb2a0cac0bcfff2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 14:28:13 +0100 Subject: [PATCH 11/19] cli: move new commands into experimental sub-commands Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 72 ++++++++++--------- .../src/commands/migrate/packageScripts.ts | 4 +- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ef2e5ef96a..caeee1557c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -133,34 +133,6 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./build').then(m => m.default))); - program - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory', - ) - .action(lazy(() => import('./bundle').then(m => m.command))); - - program - .command('start') - .description('Start a package for local development') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option('--check', 'Enable type checking and linting if available') - .option('--inspect', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .action(lazy(() => import('./start').then(m => m.command))); - program .command('lint') .option( @@ -233,13 +205,49 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); - program - .command('migrate:package-role') + const script = program + .command('script [command]', { hidden: true }) + .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); + + script + .command('bundle') + .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .action(lazy(() => import('./bundle').then(m => m.command))); + + script + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + + const migrate = program + .command('migrate [command]', { hidden: true }) + .description('Migration utilities [EXPERIMENTAL]'); + + migrate + .command('package-role') .description(`Add package role field to packages that don't have it`) .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); - program - .command('migrate:package-scripts') + migrate + .command('package-scripts') .description('Set package scripts according to each package role') .action( lazy(() => import('./migrate/packageScripts').then(m => m.command)), diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 58a8481c81..ada358b56b 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -36,9 +36,9 @@ export async function command() { const isBundled = bundledRoles.includes(roleInfo.role); const expectedScripts = { - ...(hasStart && { start: 'backstage-cli start' }), + ...(hasStart && { start: 'backstage-cli script start' }), ...(isBundled - ? { bundle: 'backstage-cli bundle' } + ? { bundle: 'backstage-cli script bundle' } : { build: 'backstage-cli build' }), lint: 'backstage-cli lint', test: 'backstage-cli test', From 38963aa860feced0673f514d00d901667d3c6d9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 15:47:53 +0100 Subject: [PATCH 12/19] cli: add new role-based build command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 52 +++++++++++++++++++ packages/cli/src/commands/build/index.ts | 17 ++++++ packages/cli/src/commands/index.ts | 9 +++- .../src/commands/{build.ts => oldBuild.ts} | 0 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/build/command.ts create mode 100644 packages/cli/src/commands/build/index.ts rename packages/cli/src/commands/{build.ts => oldBuild.ts} (100%) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts new file mode 100644 index 0000000000..733faa9480 --- /dev/null +++ b/packages/cli/src/commands/build/command.ts @@ -0,0 +1,52 @@ +/* + * 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 { Command } from 'commander'; +import { buildPackage, Output } from '../../lib/builder'; +import { PackageRoleName, readRoleForCommand } from '../../lib/role'; + +const bundledRoles: PackageRoleName[] = ['app', 'backend']; + +const esmPlatforms = ['web', 'common']; +const cjsPlatforms = ['node', 'common']; + +export async function command(cmd: Command): Promise { + const roleInfo = await readRoleForCommand(cmd); + + if (bundledRoles.includes(roleInfo.role)) { + throw new Error( + `Build command is not supported for package role '${roleInfo.role}'`, + ); + } + + const outputs = new Set(); + + if (cjsPlatforms.includes(roleInfo.platform)) { + outputs.add(Output.cjs); + } + if (esmPlatforms.includes(roleInfo.platform)) { + outputs.add(Output.esm); + } + if (roleInfo.role !== 'cli') { + outputs.add(Output.types); + } + + await buildPackage({ + outputs, + minify: Boolean(cmd.minify), + useApiExtractor: Boolean(cmd.experimentalTypeBuild), + }); +} diff --git a/packages/cli/src/commands/build/index.ts b/packages/cli/src/commands/build/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/build/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index caeee1557c..ea0f7dec58 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -131,7 +131,7 @@ export function registerCommands(program: CommanderStatic) { .option('--outputs ', 'List of formats to output [types,cjs,esm]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.default))); + .action(lazy(() => import('./oldBuild').then(m => m.default))); program .command('lint') @@ -224,6 +224,13 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./bundle').then(m => m.command))); + script + .command('build') + .description('Build a package for publishing') + .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') + .action(lazy(() => import('./build').then(m => m.command))); + script .command('start') .description('Start a package for local development') diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/oldBuild.ts similarity index 100% rename from packages/cli/src/commands/build.ts rename to packages/cli/src/commands/oldBuild.ts From 036d95b99b4d9cec1ec40ebd87d2b6e5526503e0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:19:45 +0100 Subject: [PATCH 13/19] cli: refactored registration of script and migration commands Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 162 ++++++++++++++++++----------- 1 file changed, 101 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ea0f7dec58..54fe48e2cf 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -18,14 +18,106 @@ import { assertError } from '@backstage/errors'; import { CommanderStatic } from 'commander'; import { exitWithError } from '../lib/errors'; -export function registerCommands(program: CommanderStatic) { - const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => [...opts, opt], - Array(), - ] as const; +const configOption = [ + '--config ', + 'Config files to load instead of app-config.yaml', + (opt: string, opts: string[]) => [...opts, opt], + Array(), +] as const; +export function registerScriptCommand(program: CommanderStatic) { + const command = program + .command('script [command]', { hidden: true }) + .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); + + command + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + + command + .command('build') + .description('Build a package for publishing') + .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') + .action(lazy(() => import('./build').then(m => m.command))); + + command + .command('bundle') + .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .action(lazy(() => import('./bundle').then(m => m.command))); + + program + .command('lint') + .option( + '--format ', + 'Lint report output format', + 'eslint-formatter-friendly', + ) + .option('--fix', 'Attempt to automatically fix violations') + .description('Lint a package') + .action(lazy(() => import('./lint').then(m => m.default))); + + program + .command('test') + .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args + .helpOption(', --backstage-cli-help') // Let Jest handle help + .description('Run tests, forwarding args to Jest, defaulting to watch mode') + .action(lazy(() => import('./testCommand').then(m => m.default))); + + command + .command('clean') + .description('Delete cache directories') + .action(lazy(() => import('./clean/clean').then(m => m.default))); + + command + .command('prepack') + .description('Prepares a package for packaging before publishing') + .action(lazy(() => import('./pack').then(m => m.pre))); + + command + .command('postpack') + .description('Restores the changes made by the prepack command') + .action(lazy(() => import('./pack').then(m => m.post))); +} + +export function registerMigrateCommand(program: CommanderStatic) { + const command = program + .command('migrate [command]', { hidden: true }) + .description('Migration utilities [EXPERIMENTAL]'); + + command + .command('package-role') + .description(`Add package role field to packages that don't have it`) + .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + + command + .command('package-scripts') + .description('Set package scripts according to each package role') + .action( + lazy(() => import('./migrate/packageScripts').then(m => m.command)), + ); +} + +export function registerCommands(program: CommanderStatic) { program .command('app:build') .description('Build an app for a production release') @@ -205,60 +297,8 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); - const script = program - .command('script [command]', { hidden: true }) - .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); - - script - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory', - ) - .action(lazy(() => import('./bundle').then(m => m.command))); - - script - .command('build') - .description('Build a package for publishing') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.command))); - - script - .command('start') - .description('Start a package for local development') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option('--check', 'Enable type checking and linting if available') - .option('--inspect', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .action(lazy(() => import('./start').then(m => m.command))); - - const migrate = program - .command('migrate [command]', { hidden: true }) - .description('Migration utilities [EXPERIMENTAL]'); - - migrate - .command('package-role') - .description(`Add package role field to packages that don't have it`) - .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); - - migrate - .command('package-scripts') - .description('Set package scripts according to each package role') - .action( - lazy(() => import('./migrate/packageScripts').then(m => m.command)), - ); + registerScriptCommand(program); + registerMigrateCommand(program); program .command('versions:bump') From c9f8c1189f57014c13a331bfa7687c0fbd12f83d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:20:27 +0100 Subject: [PATCH 14/19] cli: mark commands for planned deprecation Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 54fe48e2cf..c9c06141c3 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -118,6 +118,7 @@ export function registerMigrateCommand(program: CommanderStatic) { } export function registerCommands(program: CommanderStatic) { + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:build') .description('Build an app for a production release') @@ -125,6 +126,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:serve') .description('Serve an app for local development') @@ -132,6 +134,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/serve').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:build') .description('Build a backend plugin') @@ -139,6 +142,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./backend/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:bundle') .description('Bundle the backend into a deployment archive') @@ -148,6 +152,7 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./backend/bundle').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:dev') .description('Start local development server with HMR for the backend') @@ -196,6 +201,7 @@ export function registerCommands(program: CommanderStatic) { lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:build') .description('Build a plugin') @@ -203,6 +209,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./plugin/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:serve') .description('Serves the dev/ folder of a plugin') @@ -217,6 +224,7 @@ export function registerCommands(program: CommanderStatic) { .description('Diff an existing plugin with the creation template') .action(lazy(() => import('./plugin/diff').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('build') .description('Build a package for publishing') @@ -225,6 +233,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./oldBuild').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('lint') .option( @@ -236,6 +245,7 @@ export function registerCommands(program: CommanderStatic) { .description('Lint a package') .action(lazy(() => import('./lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args @@ -315,16 +325,19 @@ export function registerCommands(program: CommanderStatic) { .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('prepack') .description('Prepares a package for packaging before publishing') .action(lazy(() => import('./pack').then(m => m.pre))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('postpack') .description('Restores the changes made by the prepack command') .action(lazy(() => import('./pack').then(m => m.post))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('clean') .description('Delete cache directories') From 3532a7d81c94d77b3c275685e0201a2e443d0a78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:33:46 +0100 Subject: [PATCH 15/19] cli: update package script migration to use script commands Signed-off-by: Patrik Oldsberg --- .../cli/src/commands/migrate/packageScripts.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index ada358b56b..5d0033606f 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -38,19 +38,20 @@ export async function command() { const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), ...(isBundled - ? { bundle: 'backstage-cli script bundle' } - : { build: 'backstage-cli build' }), - lint: 'backstage-cli lint', - test: 'backstage-cli test', - clean: 'backstage-cli clean', + ? { bundle: 'backstage-cli script bundle', build: undefined } + : { build: 'backstage-cli script build', bundle: undefined }), + lint: 'backstage-cli script lint', + test: 'backstage-cli script test', + clean: 'backstage-cli script clean', ...(!isBundled && { - postpack: 'backstage-cli postpack', - prepack: 'backstage-cli prepack', + postpack: 'backstage-cli script postpack', + prepack: 'backstage-cli script prepack', }), }; let changed = false; - const currentScripts = (packageJson.scripts = packageJson.scripts || {}); + const currentScripts: Record = + (packageJson.scripts = packageJson.scripts || {}); for (const [name, value] of Object.entries(expectedScripts)) { if (currentScripts[name] !== value) { From f2c5b7461773a2c9ced713c9bc3c86a83c912588 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 24 Jan 2022 00:34:20 +0100 Subject: [PATCH 16/19] cli: refactor role functions to work around simpler PackageRole Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 13 +- packages/cli/src/commands/bundle/command.ts | 12 +- .../cli/src/commands/migrate/packageRole.ts | 12 +- .../src/commands/migrate/packageScripts.ts | 14 +-- packages/cli/src/commands/start/command.ts | 8 +- packages/cli/src/lib/monorepo/PackageGraph.ts | 4 +- packages/cli/src/lib/role/index.ts | 12 +- .../cli/src/lib/role/packageRoles.test.ts | 111 ++++++------------ packages/cli/src/lib/role/packageRoles.ts | 63 +++++----- packages/cli/src/lib/role/types.ts | 5 +- 10 files changed, 106 insertions(+), 148 deletions(-) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 733faa9480..5db6baa34f 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -16,19 +16,20 @@ import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -import { PackageRoleName, readRoleForCommand } from '../../lib/role'; +import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; -const bundledRoles: PackageRoleName[] = ['app', 'backend']; +const bundledRoles: PackageRole[] = ['app', 'backend']; const esmPlatforms = ['web', 'common']; const cjsPlatforms = ['node', 'common']; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); + const roleInfo = getRoleInfo(role); - if (bundledRoles.includes(roleInfo.role)) { + if (bundledRoles.includes(role)) { throw new Error( - `Build command is not supported for package role '${roleInfo.role}'`, + `Build command is not supported for package role '${role}'`, ); } @@ -40,7 +41,7 @@ export async function command(cmd: Command): Promise { if (esmPlatforms.includes(roleInfo.platform)) { outputs.add(Output.esm); } - if (roleInfo.role !== 'cli') { + if (role !== 'cli') { outputs.add(Output.types); } diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts index 44b39e6797..4b8c718001 100644 --- a/packages/cli/src/commands/bundle/command.ts +++ b/packages/cli/src/commands/bundle/command.ts @@ -17,10 +17,10 @@ import { Command } from 'commander'; import { bundleApp } from './bundleApp'; import { bundleBackend } from './bundleBackend'; -import { readRoleForCommand } from '../../lib/role'; +import { findRoleFromCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); const options = { configPaths: cmd.config as string[], @@ -28,13 +28,11 @@ export async function command(cmd: Command): Promise { skipBuildDependencies: Boolean(cmd.skipBuildDependencies), }; - if (roleInfo.role === 'app') { + if (role === 'app') { return bundleApp(options); - } else if (roleInfo.role === 'backend') { + } else if (role === 'backend') { return bundleBackend(options); } - throw new Error( - `Bundle command is not supported for package role '${roleInfo.role}'`, - ); + throw new Error(`Bundle command is not supported for package role '${role}'`); } diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts index 86898a6012..e8bfb8e7c8 100644 --- a/packages/cli/src/commands/migrate/packageRole.ts +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; import { paths } from '../../lib/paths'; -import { readPackageRole, detectPackageRole } from '../../lib/role'; +import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role'; export default async () => { const { packages } = await getPackages(paths.targetDir); @@ -26,18 +26,18 @@ export default async () => { await Promise.all( packages.map(async ({ dir, packageJson: pkg }) => { const { name } = pkg; - const existingRole = readPackageRole(pkg); + const existingRole = getRoleFromPackage(pkg); if (existingRole) { return; } - const detectedRole = detectPackageRole(pkg); + const detectedRole = detectRoleFromPackage(pkg); if (!detectedRole) { console.error(`No role detected for package ${name}`); return; } - console.log(`Detected package role of ${name} as ${detectedRole.role}`); + console.log(`Detected package role of ${name} as ${detectedRole}`); let newPkg = pkg as any; @@ -45,7 +45,7 @@ export default async () => { if (pkgKeys.includes('backstage')) { newPkg.backstage = { ...newPkg.backstage, - role: detectedRole.role, + role: detectedRole, }; } else { // We insert the backstage field after one of these fields, otherwise at the end @@ -57,7 +57,7 @@ export default async () => { ) + 1 || pkgKeys.length; const pkgEntries = Object.entries(pkg); - pkgEntries.splice(index, 0, ['backstage', { role: detectedRole.role }]); + pkgEntries.splice(index, 0, ['backstage', { role: detectedRole }]); newPkg = Object.fromEntries(pkgEntries); } diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 5d0033606f..c54d4ebb31 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -17,23 +17,23 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { PackageGraph } from '../../lib/monorepo'; -import { readPackageRole, PackageRoleName } from '../../lib/role'; +import { getRoleFromPackage, PackageRole } from '../../lib/role'; -const bundledRoles: PackageRoleName[] = ['app', 'backend']; -const noStartRoles: PackageRoleName[] = ['cli', 'common-library']; +const bundledRoles: PackageRole[] = ['app', 'backend']; +const noStartRoles: PackageRole[] = ['cli', 'common-library']; export async function command() { const packages = await PackageGraph.listTargetPackages(); await Promise.all( packages.map(async ({ dir, packageJson }) => { - const roleInfo = readPackageRole(packageJson); - if (!roleInfo) { + const role = getRoleFromPackage(packageJson); + if (!role) { return; } - const hasStart = !noStartRoles.includes(roleInfo.role); - const isBundled = bundledRoles.includes(roleInfo.role); + const hasStart = !noStartRoles.includes(role); + const isBundled = bundledRoles.includes(role); const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index ff4d3b7c0b..6615d5fd46 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -17,10 +17,10 @@ import { Command } from 'commander'; import { startBackend } from './startBackend'; import { startFrontend } from './startFrontend'; -import { readRoleForCommand } from '../../lib/role'; +import { findRoleFromCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); const options = { configPaths: cmd.config as string[], @@ -29,7 +29,7 @@ export async function command(cmd: Command): Promise { inspectBrkEnabled: Boolean(cmd.inspectBrk), }; - switch (roleInfo.role) { + switch (role) { case 'backend': case 'plugin-backend': case 'plugin-backend-module': @@ -47,7 +47,7 @@ export async function command(cmd: Command): Promise { return startFrontend({ entry: 'dev/index', ...options }); default: throw new Error( - `Start command is not supported for package role '${roleInfo.role}'`, + `Start command is not supported for package role '${role}'`, ); } } diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index db7a528494..89d5f79cd2 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -16,7 +16,7 @@ import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; -import { PackageRoleName } from '../role'; +import { PackageRole } from '../role'; type PackageJSON = Package['packageJson']; @@ -29,7 +29,7 @@ export interface ExtendedPackageJSON extends PackageJSON { bundled?: boolean; backstage?: { - role?: PackageRoleName; + role?: PackageRole; }; } diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 9becfa367b..a01be8d11e 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -export type { - PackageRoleInfo, - PackagePlatform, - PackageRoleName, -} from './types'; +export type { PackageRoleInfo, PackagePlatform, PackageRole } from './types'; export { getRoleInfo, - readPackageRole, - readRoleForCommand, - detectPackageRole, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, } from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index f3302d3256..db79143e50 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -18,9 +18,9 @@ import mockFs from 'mock-fs'; import { Command } from 'commander'; import { getRoleInfo, - readPackageRole, - readRoleForCommand, - detectPackageRole, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, } from './packageRoles'; describe('getRoleInfo', () => { @@ -28,11 +28,13 @@ describe('getRoleInfo', () => { expect(getRoleInfo('web-library')).toEqual({ role: 'web-library', platform: 'web', + bundled: false, }); expect(getRoleInfo('app')).toEqual({ role: 'app', platform: 'web', + bundled: true, }); expect(() => getRoleInfo('invalid')).toThrow( @@ -41,39 +43,33 @@ describe('getRoleInfo', () => { }); }); -describe('readPackageRole', () => { +describe('getRoleFromPackage', () => { it('reads explicit package roles', () => { expect( - readPackageRole({ + getRoleFromPackage({ backstage: { role: 'web-library', }, }), - ).toEqual({ - role: 'web-library', - platform: 'web', - }); + ).toEqual('web-library'); expect( - readPackageRole({ + getRoleFromPackage({ backstage: { role: 'app', }, }), - ).toEqual({ - role: 'app', - platform: 'web', - }); + ).toEqual('app'); expect(() => - readPackageRole({ + getRoleFromPackage({ name: 'test', backstage: {}, }), ).toThrow('Package test must specify a role in the "backstage" field'); expect(() => - readPackageRole({ + getRoleFromPackage({ name: 'test', backstage: { role: 'invalid' }, }), @@ -81,7 +77,7 @@ describe('readPackageRole', () => { }); }); -describe('readRoleForCommand', () => { +describe('findRoleFromCommand', () => { function mkCommand(args: string) { return new Command() .option('--role ', 'test role') @@ -104,28 +100,24 @@ describe('readRoleForCommand', () => { }); it('provides role info by role', async () => { - await expect(readRoleForCommand(mkCommand(''))).resolves.toEqual({ - role: 'web-library', - platform: 'web', - }); + await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( + 'web-library', + ); await expect( - readRoleForCommand(mkCommand('--role node-library')), - ).resolves.toEqual({ - role: 'node-library', - platform: 'node', - }); + findRoleFromCommand(mkCommand('--role node-library')), + ).resolves.toEqual('node-library'); await expect( - readRoleForCommand(mkCommand('--role invalid')), + findRoleFromCommand(mkCommand('--role invalid')), ).rejects.toThrow(`Unknown package role 'invalid'`); }); }); -describe('detectPackageRole', () => { +describe('detectRoleFromPackage', () => { it('detects the role of example-app', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: 'example-app', private: true, bundled: true, @@ -143,15 +135,12 @@ describe('detectPackageRole', () => { 'cy:run': 'cypress run', }, }), - ).toEqual({ - role: 'app', - platform: 'web', - }); + ).toEqual('app'); }); it('detects the role of example-backend', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: 'example-backend', main: 'dist/index.cjs.js', types: 'src/index.ts', @@ -166,15 +155,12 @@ describe('detectPackageRole', () => { 'migrate:create': 'knex migrate:make -x ts', }, }), - ).toEqual({ - role: 'backend', - platform: 'node', - }); + ).toEqual('backend'); }); it('detects the role of @backstage/plugin-catalog', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog', main: 'src/index.ts', types: 'src/index.ts', @@ -194,15 +180,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-frontend', - platform: 'web', - }); + ).toEqual('plugin-frontend'); }); it('detects the role of @backstage/plugin-catalog-backend', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend', main: 'src/index.ts', types: 'src/index.ts', @@ -221,15 +204,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-backend', - platform: 'node', - }); + ).toEqual('plugin-backend'); }); it('detects the role of @backstage/plugin-catalog-react', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-react', main: 'src/index.ts', types: 'src/index.ts', @@ -247,15 +227,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'web-library', - platform: 'web', - }); + ).toEqual('web-library'); }); it('detects the role of @backstage/plugin-catalog-common', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-common', main: 'src/index.ts', types: 'src/index.ts', @@ -274,15 +251,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'common-library', - platform: 'common', - }); + ).toEqual('common-library'); }); it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend-module-ldap', main: 'src/index.ts', types: 'src/index.ts', @@ -300,15 +274,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-backend-module', - platform: 'node', - }); + ).toEqual('plugin-backend-module'); }); it('detects the role of @backstage/plugin-permission-node', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-permission-node', main: 'src/index.ts', types: 'src/index.ts', @@ -327,15 +298,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'node-library', - platform: 'node', - }); + ).toEqual('node-library'); }); it('detects the role of @backstage/plugin-analytics-module-ga', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-analytics-module-ga', main: 'src/index.ts', types: 'src/index.ts', @@ -355,9 +323,6 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-frontend-module', - platform: 'web', - }); + ).toEqual('plugin-frontend-module'); }); }); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 67bee0e64f..c12ca644b9 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -18,24 +18,23 @@ import { z } from 'zod'; import fs from 'fs-extra'; import { Command } from 'commander'; import { paths } from '../paths'; -import { PackageRoleInfo } from './types'; +import { PackageRole, 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 packageRoleInfos: PackageRoleInfo[] = [ + { role: 'app', bundled: true, platform: 'web' }, + { role: 'backend', bundled: true, platform: 'node' }, + { role: 'cli', bundled: false, platform: 'node' }, + { role: 'web-library', bundled: false, platform: 'web' }, + { role: 'node-library', bundled: false, platform: 'node' }, + { role: 'common-library', bundled: false, platform: 'common' }, + { role: 'plugin-frontend', bundled: false, platform: 'web' }, + { role: 'plugin-frontend-module', bundled: false, platform: 'web' }, + { role: 'plugin-backend', bundled: false, platform: 'node' }, + { role: 'plugin-backend-module', bundled: false, platform: 'node' }, ]; -const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); export function getRoleInfo(role: string): PackageRoleInfo { - const roleInfo = packageRoles.find(r => r.role === role); + const roleInfo = packageRoleInfos.find(r => r.role === role); if (!roleInfo) { throw new Error(`Unknown package role '${role}'`); } @@ -51,7 +50,7 @@ const readSchema = z.object({ .optional(), }); -export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { +export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { const pkg = readSchema.parse(pkgJson); // If there's an explicit role, use that. @@ -63,21 +62,19 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { ); } - return getRoleInfo(role); + return getRoleInfo(role).role; } return undefined; } -export async function readRoleForCommand( - cmd: Command, -): Promise { +export async function findRoleFromCommand(cmd: Command): Promise { if (cmd.role) { - return getRoleInfo(cmd.role); + return getRoleInfo(cmd.role)?.role; } const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const info = readPackageRole(pkg); + const info = getRoleFromPackage(pkg); if (!info) { throw new Error(`Target package must have 'backstage.role' set`); } @@ -104,28 +101,28 @@ const detectionSchema = z.object({ module: z.string().optional(), }); -export function detectPackageRole( +export function detectRoleFromPackage( pkgJson: unknown, -): PackageRoleInfo | undefined { +): PackageRole | undefined { const pkg = detectionSchema.parse(pkgJson); if (pkg.scripts?.start?.includes('app:serve')) { - return roleMap.app; + return 'app'; } if (pkg.scripts?.build?.includes('backend:bundle')) { - return roleMap.backend; + return 'backend'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { - return roleMap['plugin-backend-module']; + return 'plugin-backend-module'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { - return roleMap['plugin-frontend-module']; + return 'plugin-frontend-module'; } if (pkg.scripts?.start?.includes('plugin:serve')) { - return roleMap['plugin-frontend']; + return 'plugin-frontend'; } if (pkg.scripts?.start?.includes('backend:dev')) { - return roleMap['plugin-backend']; + return 'plugin-backend'; } const mainEntry = pkg.publishConfig?.main || pkg.main; @@ -133,16 +130,16 @@ export function detectPackageRole( const typesEntry = pkg.publishConfig?.types || pkg.types; if (typesEntry) { if (mainEntry && moduleEntry) { - return roleMap['common-library']; + return 'common-library'; } if (moduleEntry || mainEntry?.endsWith('.esm.js')) { - return roleMap['web-library']; + return 'web-library'; } if (mainEntry) { - return roleMap['node-library']; + return 'node-library'; } } else if (mainEntry) { - return roleMap.cli; + return 'cli'; } return undefined; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts index 1f5afe7926..efe2c99fd5 100644 --- a/packages/cli/src/lib/role/types.ts +++ b/packages/cli/src/lib/role/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export type PackageRoleName = +export type PackageRole = | 'app' | 'backend' | 'cli' @@ -29,6 +29,7 @@ export type PackageRoleName = export type PackagePlatform = 'node' | 'web' | 'common'; export interface PackageRoleInfo { - role: PackageRoleName; + role: PackageRole; + bundled: boolean; platform: PackagePlatform; } From 323562efc9236bd397034d8e24881fa15ea97065 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 25 Jan 2022 21:35:39 +0100 Subject: [PATCH 17/19] cli: switch to keeping track of outputs for each package role Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 9 +-- .../src/commands/migrate/packageScripts.ts | 6 +- packages/cli/src/lib/role/index.ts | 7 ++- .../cli/src/lib/role/packageRoles.test.ts | 4 +- packages/cli/src/lib/role/packageRoles.ts | 60 +++++++++++++++---- packages/cli/src/lib/role/types.ts | 3 +- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 5db6baa34f..b2e5925119 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -20,9 +20,6 @@ import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; const bundledRoles: PackageRole[] = ['app', 'backend']; -const esmPlatforms = ['web', 'common']; -const cjsPlatforms = ['node', 'common']; - export async function command(cmd: Command): Promise { const role = await findRoleFromCommand(cmd); const roleInfo = getRoleInfo(role); @@ -35,13 +32,13 @@ export async function command(cmd: Command): Promise { const outputs = new Set(); - if (cjsPlatforms.includes(roleInfo.platform)) { + if (roleInfo.output.includes('cjs')) { outputs.add(Output.cjs); } - if (esmPlatforms.includes(roleInfo.platform)) { + if (roleInfo.output.includes('esm')) { outputs.add(Output.esm); } - if (role !== 'cli') { + if (roleInfo.output.includes('types')) { outputs.add(Output.types); } diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index c54d4ebb31..7a02411690 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -17,9 +17,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { PackageGraph } from '../../lib/monorepo'; -import { getRoleFromPackage, PackageRole } from '../../lib/role'; +import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role'; -const bundledRoles: PackageRole[] = ['app', 'backend']; const noStartRoles: PackageRole[] = ['cli', 'common-library']; export async function command() { @@ -32,8 +31,9 @@ export async function command() { return; } + const roleInfo = getRoleInfo(role); const hasStart = !noStartRoles.includes(role); - const isBundled = bundledRoles.includes(role); + const isBundled = roleInfo.output.includes('bundle'); const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index a01be8d11e..4e1047a628 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -export type { PackageRoleInfo, PackagePlatform, PackageRole } from './types'; +export type { + PackageRoleInfo, + PackagePlatform, + PackageOutputType, + PackageRole, +} from './types'; export { getRoleInfo, getRoleFromPackage, diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index db79143e50..f6c9fb418a 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -28,13 +28,13 @@ describe('getRoleInfo', () => { expect(getRoleInfo('web-library')).toEqual({ role: 'web-library', platform: 'web', - bundled: false, + output: ['types', 'esm'], }); expect(getRoleInfo('app')).toEqual({ role: 'app', platform: 'web', - bundled: true, + output: ['bundle'], }); expect(() => getRoleInfo('invalid')).toThrow( diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index c12ca644b9..7c855ee0fd 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -21,16 +21,56 @@ import { paths } from '../paths'; import { PackageRole, PackageRoleInfo } from './types'; const packageRoleInfos: PackageRoleInfo[] = [ - { role: 'app', bundled: true, platform: 'web' }, - { role: 'backend', bundled: true, platform: 'node' }, - { role: 'cli', bundled: false, platform: 'node' }, - { role: 'web-library', bundled: false, platform: 'web' }, - { role: 'node-library', bundled: false, platform: 'node' }, - { role: 'common-library', bundled: false, platform: 'common' }, - { role: 'plugin-frontend', bundled: false, platform: 'web' }, - { role: 'plugin-frontend-module', bundled: false, platform: 'web' }, - { role: 'plugin-backend', bundled: false, platform: 'node' }, - { role: 'plugin-backend-module', bundled: false, platform: 'node' }, + { + role: 'app', + 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: 'plugin-frontend', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-frontend-module', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-backend', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'plugin-backend-module', + platform: 'node', + output: ['types', 'cjs'], + }, ]; export function getRoleInfo(role: string): PackageRoleInfo { diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts index efe2c99fd5..6a89f7bdaf 100644 --- a/packages/cli/src/lib/role/types.ts +++ b/packages/cli/src/lib/role/types.ts @@ -27,9 +27,10 @@ export type PackageRole = | 'plugin-backend-module'; export type PackagePlatform = 'node' | 'web' | 'common'; +export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; export interface PackageRoleInfo { role: PackageRole; - bundled: boolean; platform: PackagePlatform; + output: PackageOutputType[]; } From eaf67f05783da2a5c140f66ce19647daa9faf8b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 27 Jan 2022 18:43:50 +0100 Subject: [PATCH 18/19] changesets: add changset for cli role addition Signed-off-by: Patrik Oldsberg --- .changeset/brave-tools-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-tools-drop.md diff --git a/.changeset/brave-tools-drop.md b/.changeset/brave-tools-drop.md new file mode 100644 index 0000000000..02b6b7ebb1 --- /dev/null +++ b/.changeset/brave-tools-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet. From 3941ada3cfa8dcf58c9939dd9ae56dd7cffd326d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Feb 2022 13:35:53 +0100 Subject: [PATCH 19/19] cli: remove separate bundle script, using build instead Signed-off-by: Patrik Oldsberg --- .../bundleApp.ts => build/buildApp.ts} | 4 +- .../buildBackend.ts} | 4 +- packages/cli/src/commands/build/command.ts | 25 +++++++----- packages/cli/src/commands/bundle/command.ts | 38 ------------------- packages/cli/src/commands/bundle/index.ts | 17 --------- packages/cli/src/commands/index.ts | 31 ++++++++------- .../src/commands/migrate/packageScripts.ts | 4 +- 7 files changed, 39 insertions(+), 84 deletions(-) rename packages/cli/src/commands/{bundle/bundleApp.ts => build/buildApp.ts} (93%) rename packages/cli/src/commands/{bundle/bundleBackend.ts => build/buildBackend.ts} (96%) delete mode 100644 packages/cli/src/commands/bundle/command.ts delete mode 100644 packages/cli/src/commands/bundle/index.ts diff --git a/packages/cli/src/commands/bundle/bundleApp.ts b/packages/cli/src/commands/build/buildApp.ts similarity index 93% rename from packages/cli/src/commands/bundle/bundleApp.ts rename to packages/cli/src/commands/build/buildApp.ts index 1ebd1f044e..3d86d796b7 100644 --- a/packages/cli/src/commands/bundle/bundleApp.ts +++ b/packages/cli/src/commands/build/buildApp.ts @@ -20,12 +20,12 @@ import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; -interface BundleAppOptions { +interface BuildAppOptions { writeStats: boolean; configPaths: string[]; } -export async function bundleApp(options: BundleAppOptions) { +export async function buildApp(options: BuildAppOptions) { const { name } = await fs.readJson(paths.resolveTarget('package.json')); await buildBundle({ entry: 'src/index', diff --git a/packages/cli/src/commands/bundle/bundleBackend.ts b/packages/cli/src/commands/build/buildBackend.ts similarity index 96% rename from packages/cli/src/commands/bundle/bundleBackend.ts rename to packages/cli/src/commands/build/buildBackend.ts index 0f240d87dc..a4d8858cf8 100644 --- a/packages/cli/src/commands/bundle/bundleBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -26,11 +26,11 @@ import { buildPackage, Output } from '../../lib/builder'; const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; -interface BundleBackendOptions { +interface BuildBackendOptions { skipBuildDependencies: boolean; } -export async function bundleBackend(options: BundleBackendOptions) { +export async function buildBackend(options: BuildBackendOptions) { const targetDir = paths.resolveTarget('dist'); const pkg = await fs.readJson(paths.resolveTarget('package.json')); diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index b2e5925119..8c13b515d7 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -16,19 +16,26 @@ import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; - -const bundledRoles: PackageRole[] = ['app', 'backend']; +import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; +import { buildApp } from './buildApp'; +import { buildBackend } from './buildBackend'; export async function command(cmd: Command): Promise { const role = await findRoleFromCommand(cmd); - const roleInfo = getRoleInfo(role); - if (bundledRoles.includes(role)) { - throw new Error( - `Build command is not supported for package role '${role}'`, - ); + if (role === 'app') { + return buildApp({ + configPaths: cmd.config as string[], + writeStats: Boolean(cmd.stats), + }); } + if (role === 'backend') { + return buildBackend({ + skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + }); + } + + const roleInfo = getRoleInfo(role); const outputs = new Set(); @@ -42,7 +49,7 @@ export async function command(cmd: Command): Promise { outputs.add(Output.types); } - await buildPackage({ + return buildPackage({ outputs, minify: Boolean(cmd.minify), useApiExtractor: Boolean(cmd.experimentalTypeBuild), diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts deleted file mode 100644 index 4b8c718001..0000000000 --- a/packages/cli/src/commands/bundle/command.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Command } from 'commander'; -import { bundleApp } from './bundleApp'; -import { bundleBackend } from './bundleBackend'; -import { findRoleFromCommand } from '../../lib/role'; - -export async function command(cmd: Command): Promise { - const role = await findRoleFromCommand(cmd); - - const options = { - configPaths: cmd.config as string[], - writeStats: Boolean(cmd.stats), - skipBuildDependencies: Boolean(cmd.skipBuildDependencies), - }; - - if (role === 'app') { - return bundleApp(options); - } else if (role === 'backend') { - return bundleBackend(options); - } - - throw new Error(`Bundle command is not supported for package role '${role}'`); -} diff --git a/packages/cli/src/commands/bundle/index.ts b/packages/cli/src/commands/bundle/index.ts deleted file mode 100644 index 680fe9e11d..0000000000 --- a/packages/cli/src/commands/bundle/index.ts +++ /dev/null @@ -1,17 +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. - */ - -export { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index c9c06141c3..a33e7d4afe 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -45,25 +45,30 @@ export function registerScriptCommand(program: CommanderStatic) { command .command('build') - .description('Build a package for publishing') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.command))); - - command - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') + .description('Build a package for production deployment or publishing') + .option( + '--minify', + 'Minify the generated code. Does not apply to app or backend packages.', + ) + .option( + '--experimental-type-build', + 'Enable experimental type build. Does not apply to app or backend packages.', + ) .option( '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', + 'Skip the automatic building of local dependencies. Applies to backend packages only.', ) .option( '--stats', - 'If bundle stats are available, write them to the output directory', + 'If bundle stats are available, write them to the output directory. Applies to app packages only.', ) - .action(lazy(() => import('./bundle').then(m => m.command))); + .option( + '--config ', + 'Config files to load instead of app-config.yaml. Applies to app packages only.', + (opt: string, opts: string[]) => [...opts, opt], + Array(), + ) + .action(lazy(() => import('./build').then(m => m.command))); program .command('lint') diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 7a02411690..a3fdd20018 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -37,9 +37,7 @@ export async function command() { const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), - ...(isBundled - ? { bundle: 'backstage-cli script bundle', build: undefined } - : { build: 'backstage-cli script build', bundle: undefined }), + build: 'backstage-cli script build', lint: 'backstage-cli script lint', test: 'backstage-cli script test', clean: 'backstage-cli script clean',