diff --git a/packages/cli/src/modules/start/alpha.ts b/packages/cli/src/modules/start/alpha.ts index cbd9ae3a28..860753fb85 100644 --- a/packages/cli/src/modules/start/alpha.ts +++ b/packages/cli/src/modules/start/alpha.ts @@ -57,8 +57,28 @@ export const buildPlugin = createCliPlugin({ reg.addCommand({ path: ['repo', 'start'], description: 'Starts packages in the repo for local development', - execute: async () => { - throw new Error('Not implemented'); + execute: async ({ args }) => { + const command = new Command(); + + const defaultCommand = command + .argument( + '[...packageName]', + 'Run the specified package instead of the defaults.', + ) + .option(...configOption) + .option( + '--plugin ', + 'Start the dev entry-point for any matching plugin package in the repo', + (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), + Array(), + ) + .option( + '--link ', + 'Link an external workspace for module resolution', + ) + .action(lazy(() => import('./commands/repo/start'), 'command')); + + await defaultCommand.parseAsync(args, { from: 'user' }); }, }); }, diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts new file mode 100644 index 0000000000..2b10a9c16c --- /dev/null +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2025 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 { + BackstagePackage, + PackageGraph, + PackageRole, +} from '@backstage/cli-node'; +import { relative as relativePath } from 'path'; +import { paths } from '../../../../lib/paths'; + +const ACCEPTED_PACKAGE_ROLES: Array = [ + 'frontend', + 'backend', + 'frontend-plugin', + 'backend-plugin', +]; + +export async function command( + packageNames: string[], + options: { plugin: string[]; config: string[] }, +) { + const targetPackages = await findTargetPackages(packageNames, options.plugin); + console.log( + `Starting ${targetPackages.map(p => p.packageJson.name).join(', ')}`, + ); +} + +async function findTargetPackages(packageNames: string[], pluginIds: string[]) { + const targetPackages = new Array(); + + const packages = await PackageGraph.listTargetPackages(); + + // Priorotize plugin options, so that the `start` script can contain a list of packages, + // but make them easy to override by running for example `yarn start --plugin catalog` + for (const pluginId of pluginIds) { + const matchingPackages = packages.filter(pkg => { + return ( + pluginId === pkg.packageJson.backstage?.pluginId && + ACCEPTED_PACKAGE_ROLES.includes(pkg.packageJson.backstage.role) + ); + }); + if (!matchingPackages) { + throw new Error( + `Unable to find any plugin packages with plugin ID '${pluginId}'. Make sure backstage.pluginId is set in your package.json files by running 'yarn fix --publish'.`, + ); + } + targetPackages.push(...matchingPackages); + } + if (targetPackages.length > 0) { + return targetPackages; + } + + // Next check if explicit package names are provided, use them in that case. + for (const packageName of packageNames) { + const matchingPackage = packages.find(pkg => { + return packageName === pkg.packageJson.name; + }); + if (!matchingPackage) { + throw new Error(`Unable to find package by name '${packageName}'`); + } + targetPackages.push(matchingPackage); + } + + if (targetPackages.length > 0) { + return targetPackages; + } + + // If on package names are provided, default to expect a single frontend and/or backend package + for (const role of ['frontend', 'backend']) { + const matchingPackages = packages.filter( + pkg => pkg.packageJson.backstage?.role === role, + ); + if (matchingPackages.length > 1) { + // Final fallback is to check for the package path within the monorepo, packages/app or packages/backend + const expectedPath = paths.resolveTargetRoot( + role === 'frontend' ? 'packages/app' : 'packages/backend', + ); + const matchByPath = matchingPackages.find( + pkg => relativePath(expectedPath, pkg.dir) === '', + ); + if (matchByPath) { + targetPackages.push(matchByPath); + continue; + } + + throw new Error( + `Found multiple packages with role '${role}' but none of the use the default path '${expectedPath}',` + + `choose which packages you want to run by passing the package names explicitly ` + + `as arguments, for example 'yarn backstage-cli repo start my-app my-backend'.`, + ); + } + + targetPackages.push(...matchingPackages); + } + if (targetPackages.length === 0) { + throw new Error( + `Unable to find any packages with role 'frontend' or 'backend'`, + ); + } + return targetPackages; +} diff --git a/packages/cli/src/modules/start/index.ts b/packages/cli/src/modules/start/index.ts index 6724fb2af8..627336e3f4 100644 --- a/packages/cli/src/modules/start/index.ts +++ b/packages/cli/src/modules/start/index.ts @@ -21,16 +21,19 @@ export function registerRepoCommands(command: Command) { command .command('start') .description('Starts packages in the repo for local development') - .action( - lazy( - async () => ({ - command: () => { - throw new Error('Not implemented'); - }, - }), - 'command', - ), - ); + .argument( + '[packageName...]', + 'Run the specified package instead of the defaults.', + ) + .option(...configOption) + .option( + '--plugin ', + 'Start the dev entry-point for any matching plugin package in the repo', + (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), + Array(), + ) + .option('--link ', 'Link an external workspace for module resolution') + .action(lazy(() => import('./commands/repo/start'), 'command')); } export function registerPackageCommands(command: Command) {