From bd3943b7ae976ea4f2304cfafc8ae8726c0fe837 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Apr 2025 21:19:43 +0100 Subject: [PATCH 01/14] cli: migrate start command to module Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 29 ++---- packages/cli/src/modules/start/alpha.ts | 67 ++++++++++++++ .../start/commands/package/start/command.ts | 90 +++++++++++++++++++ .../start/commands/package/start/index.ts | 17 ++++ .../commands/package/start/startBackend.ts | 61 +++++++++++++ .../commands/package/start/startFrontend.ts | 57 ++++++++++++ packages/cli/src/modules/start/index.ts | 54 +++++++++++ 7 files changed, 353 insertions(+), 22 deletions(-) create mode 100644 packages/cli/src/modules/start/alpha.ts create mode 100644 packages/cli/src/modules/start/commands/package/start/command.ts create mode 100644 packages/cli/src/modules/start/commands/package/start/index.ts create mode 100644 packages/cli/src/modules/start/commands/package/start/startBackend.ts create mode 100644 packages/cli/src/modules/start/commands/package/start/startFrontend.ts create mode 100644 packages/cli/src/modules/start/index.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 99f2bc99ef..6dd2017a71 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -16,15 +16,16 @@ import { Command } from 'commander'; import { lazy } from '../lib/lazy'; -import { - configOption, - registerCommands as registerConfigCommands, -} from '../modules/config'; +import { registerCommands as registerConfigCommands } from '../modules/config'; import { registerPackageCommands as registerPackageBuildCommands, registerRepoCommands as registerRepoBuildCommands, registerCommands as registerBuildCommands, } from '../modules/build'; +import { + registerPackageCommands as registerPackageStartCommands, + registerRepoCommands as registerRepoStartCommands, +} from '../modules/start'; import { registerCommands as registerInfoCommands } from '../modules/info'; import { registerCommands as registerMigrateCommand } from '../modules/migrate'; import { @@ -45,6 +46,7 @@ export function registerRepoCommand(program: Command) { .command('repo [command]') .description('Command that run across an entire Backstage project'); + registerRepoStartCommands(command); registerRepoBuildCommands(command); registerRepoTestCommands(command); registerRepoLintCommands(command); @@ -56,24 +58,7 @@ export function registerScriptCommand(program: Command) { .command('package [command]') .description('Lifecycle scripts for individual packages'); - 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 [host]', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .option( - '--require ', - 'Add a --require argument to the node process', - ) - .option('--link ', 'Link an external workspace for module resolution') - .action(lazy(() => import('./start'), 'command')); - + registerPackageStartCommands(command); registerPackageBuildCommands(command); registerPackageTestCommands(command); registerMaintenancePackageCommands(command); diff --git a/packages/cli/src/modules/start/alpha.ts b/packages/cli/src/modules/start/alpha.ts new file mode 100644 index 0000000000..cbd9ae3a28 --- /dev/null +++ b/packages/cli/src/modules/start/alpha.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 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 { createCliPlugin } from '../../wiring/factory'; +import { lazy } from '../../lib/lazy'; +import { configOption } from '../config'; + +export const buildPlugin = createCliPlugin({ + pluginId: 'build', + init: async reg => { + reg.addCommand({ + path: ['package', 'start'], + description: 'Start a package for local development', + execute: async ({ args }) => { + const command = new Command(); + + const defaultCommand = command + .option(...configOption) + .option( + '--role ', + 'Run the command with an explicit package role', + ) + .option('--check', 'Enable type checking and linting if available') + .option('--inspect [host]', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .option( + '--require ', + 'Add a --require argument to the node process', + ) + .option( + '--link ', + 'Link an external workspace for module resolution', + ) + .action(lazy(() => import('./commands/package/start'), 'command')); + + await defaultCommand.parseAsync(args, { from: 'user' }); + }, + }); + + reg.addCommand({ + path: ['repo', 'start'], + description: 'Starts packages in the repo for local development', + execute: async () => { + throw new Error('Not implemented'); + }, + }); + }, +}); + +export default buildPlugin; diff --git a/packages/cli/src/modules/start/commands/package/start/command.ts b/packages/cli/src/modules/start/commands/package/start/command.ts new file mode 100644 index 0000000000..5a08949bf9 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/command.ts @@ -0,0 +1,90 @@ +/* + * 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 { OptionValues } from 'commander'; +import { resolve as resolvePath } from 'node:path'; +import { PackageRole } from '@backstage/cli-node'; +import { findRoleFromCommand } from '../../../../../lib/role'; +import { startBackend, startBackendPlugin } from './startBackend'; +import { startFrontend } from './startFrontend'; +import { ForwardedError } from '@backstage/errors'; + +export async function command(opts: OptionValues): Promise { + const role = await findRoleFromCommand(opts); + + if (opts.link) { + const dir = resolvePath(opts.link); + if (!fs.pathExistsSync(dir)) { + throw new Error( + `Invalid workspace link, directory does not exist: ${dir}`, + ); + } + const pkgJson = await fs + .readJson(resolvePath(dir, 'package.json')) + .catch(error => { + throw new ForwardedError( + 'Failed to read package.json in linked workspace', + error, + ); + }); + + if (!pkgJson.workspaces) { + throw new Error( + `Invalid workspace link, directory is not a workspace: ${dir}`, + ); + } + } + + const options = { + configPaths: opts.config as string[], + checksEnabled: Boolean(opts.check), + linkedWorkspace: opts.link, + inspectEnabled: opts.inspect, + inspectBrkEnabled: opts.inspectBrk, + require: opts.require, + }; + + switch (role) { + case 'backend': + return startBackend(options); + case 'backend-plugin': + case 'backend-plugin-module': + case 'node-library': + return startBackendPlugin(options); + case 'frontend': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + return startFrontend({ entry: 'dev/index', ...options }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); + default: + throw new Error( + `Start command is not supported for package role '${role}'`, + ); + } +} diff --git a/packages/cli/src/modules/start/commands/package/start/index.ts b/packages/cli/src/modules/start/commands/package/start/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/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/modules/start/commands/package/start/startBackend.ts b/packages/cli/src/modules/start/commands/package/start/startBackend.ts new file mode 100644 index 0000000000..7c89a6d887 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/startBackend.ts @@ -0,0 +1,61 @@ +/* + * 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 { runBackend } from '../../../../../lib/runner'; + +interface StartBackendOptions { + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; + linkedWorkspace?: string; + require?: string; +} + +export async function startBackend(options: StartBackendOptions) { + const waitForExit = await runBackend({ + entry: 'src/index', + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + linkedWorkspace: options.linkedWorkspace, + require: options.require, + }); + + await waitForExit(); +} + +export async function startBackendPlugin(options: StartBackendOptions) { + const hasDevIndexEntry = await fs.pathExists( + paths.resolveTarget('dev', 'index.ts'), + ); + if (!hasDevIndexEntry) { + console.warn( + `The 'dev' directory is missing. Please create a proper dev/index.ts in order to start the plugin.`, + ); + return; + } + + const waitForExit = await runBackend({ + entry: 'dev/index', + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + require: options.require, + linkedWorkspace: options.linkedWorkspace, + }); + + await waitForExit(); +} diff --git a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts new file mode 100644 index 0000000000..8abdf451b5 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts @@ -0,0 +1,57 @@ +/* + * 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 { readJson } from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { + getModuleFederationOptions, + serveBundle, +} from '../../../../build/lib/bundler'; +import { paths } from '../../../../../lib/paths'; +import { BackstagePackageJson } from '@backstage/cli-node'; + +interface StartAppOptions { + verifyVersions?: boolean; + entry: string; + + checksEnabled: boolean; + configPaths: string[]; + skipOpenBrowser?: boolean; + isModuleFederationRemote?: boolean; + linkedWorkspace?: string; +} + +export async function startFrontend(options: StartAppOptions) { + const packageJson = (await readJson( + paths.resolveTarget('package.json'), + )) as BackstagePackageJson; + + const waitForExit = await serveBundle({ + entry: options.entry, + checksEnabled: options.checksEnabled, + configPaths: options.configPaths, + verifyVersions: options.verifyVersions, + skipOpenBrowser: options.skipOpenBrowser, + linkedWorkspace: options.linkedWorkspace, + moduleFederation: await getModuleFederationOptions( + packageJson, + resolvePath(paths.targetDir), + options.isModuleFederationRemote, + ), + }); + + await waitForExit(); +} diff --git a/packages/cli/src/modules/start/index.ts b/packages/cli/src/modules/start/index.ts new file mode 100644 index 0000000000..6724fb2af8 --- /dev/null +++ b/packages/cli/src/modules/start/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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 { lazy } from '../../lib/lazy'; +import { configOption } from '../config'; + +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', + ), + ); +} + +export function registerPackageCommands(command: Command) { + 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 [host]', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .option( + '--require ', + 'Add a --require argument to the node process', + ) + .option('--link ', 'Link an external workspace for module resolution') + .action(lazy(() => import('./commands/package/start'), 'command')); +} From efd6a048d51569c6a762b564759db6ee9bbc1fdb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Apr 2025 22:05:51 +0100 Subject: [PATCH 02/14] cli: initial repo start implementation with package selection Signed-off-by: Patrik Oldsberg --- packages/cli/src/modules/start/alpha.ts | 24 +++- .../src/modules/start/commands/repo/start.ts | 115 ++++++++++++++++++ packages/cli/src/modules/start/index.ts | 23 ++-- 3 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/modules/start/commands/repo/start.ts 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) { From 944af983d9e6adbaab278c0af353f47753884729 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Apr 2025 13:44:12 +0200 Subject: [PATCH 03/14] cli: refactor and fully implement starting packages via repo start Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/errors.ts | 2 +- packages/cli/src/lib/runner/runBackend.ts | 3 + .../src/modules/build/lib/bundler/server.ts | 5 +- .../src/modules/build/lib/bundler/types.ts | 1 + .../start/commands/package/start/command.ts | 72 +++---------------- .../package/start/resolveLinkedWorkspace.ts | 47 ++++++++++++ .../commands/package/start/startBackend.ts | 6 +- .../commands/package/start/startFrontend.ts | 2 + .../commands/package/start/startPackage.ts | 63 ++++++++++++++++ .../src/modules/start/commands/repo/start.ts | 21 +++++- 10 files changed, 154 insertions(+), 68 deletions(-) create mode 100644 packages/cli/src/modules/start/commands/package/start/resolveLinkedWorkspace.ts create mode 100644 packages/cli/src/modules/start/commands/package/start/startPackage.ts diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index dd3955a10a..ef97270569 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -40,7 +40,7 @@ export function exitWithError(error: Error): never { process.stderr.write(`\n${chalk.red(error.message)}\n\n`); process.exit(error.code); } else { - process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`); + process.stderr.write(`\n${chalk.red(`${error.stack}`)}\n\n`); process.exit(1); } } diff --git a/packages/cli/src/lib/runner/runBackend.ts b/packages/cli/src/lib/runner/runBackend.ts index c085e183f1..9f52968067 100644 --- a/packages/cli/src/lib/runner/runBackend.ts +++ b/packages/cli/src/lib/runner/runBackend.ts @@ -32,6 +32,8 @@ const loaderArgs = [ ]; export type RunBackendOptions = { + /** The directory to run the backend process in, defaults to cwd */ + targetDir?: string; /** relative entry point path without extension, e.g. 'src/index' */ entry: string; /** Whether to forward the --inspect flag to the node process */ @@ -122,6 +124,7 @@ export async function runBackend(options: RunBackendOptions) { [...loaderArgs, ...optionArgs, options.entry, ...userArgs], { stdio: ['ignore', 'inherit', 'inherit', 'ipc'], + cwd: options.targetDir, env: { ...process.env, BACKSTAGE_CLI_LINKED_WORKSPACE: options.linkedWorkspace, diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli/src/modules/build/lib/bundler/server.ts index 2af8d7a921..4541ecf12c 100644 --- a/packages/cli/src/modules/build/lib/bundler/server.ts +++ b/packages/cli/src/modules/build/lib/bundler/server.ts @@ -17,6 +17,7 @@ import { AppConfig } from '@backstage/config'; import chalk from 'chalk'; import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import openBrowser from 'react-dev-utils/openBrowser'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; @@ -50,7 +51,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be checkReactVersion(); - const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); + const { name } = await fs.readJson( + resolvePath(options.targetDir ?? libPaths.targetDir, 'package.json'), + ); let webpackServer: WebpackDevServer | undefined = undefined; diff --git a/packages/cli/src/modules/build/lib/bundler/types.ts b/packages/cli/src/modules/build/lib/bundler/types.ts index e44236a602..a018ccad93 100644 --- a/packages/cli/src/modules/build/lib/bundler/types.ts +++ b/packages/cli/src/modules/build/lib/bundler/types.ts @@ -49,6 +49,7 @@ export type BundlingOptions = { }; export type ServeOptions = BundlingPathsOptions & { + targetDir?: string; checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; diff --git a/packages/cli/src/modules/start/commands/package/start/command.ts b/packages/cli/src/modules/start/commands/package/start/command.ts index 5a08949bf9..05fc668356 100644 --- a/packages/cli/src/modules/start/commands/package/start/command.ts +++ b/packages/cli/src/modules/start/commands/package/start/command.ts @@ -14,77 +14,21 @@ * limitations under the License. */ -import fs from 'fs-extra'; import { OptionValues } from 'commander'; -import { resolve as resolvePath } from 'node:path'; -import { PackageRole } from '@backstage/cli-node'; +import { startPackage } from './startPackage'; +import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; import { findRoleFromCommand } from '../../../../../lib/role'; -import { startBackend, startBackendPlugin } from './startBackend'; -import { startFrontend } from './startFrontend'; -import { ForwardedError } from '@backstage/errors'; +import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { - const role = await findRoleFromCommand(opts); - - if (opts.link) { - const dir = resolvePath(opts.link); - if (!fs.pathExistsSync(dir)) { - throw new Error( - `Invalid workspace link, directory does not exist: ${dir}`, - ); - } - const pkgJson = await fs - .readJson(resolvePath(dir, 'package.json')) - .catch(error => { - throw new ForwardedError( - 'Failed to read package.json in linked workspace', - error, - ); - }); - - if (!pkgJson.workspaces) { - throw new Error( - `Invalid workspace link, directory is not a workspace: ${dir}`, - ); - } - } - - const options = { + await startPackage({ + role: await findRoleFromCommand(opts), + targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), - linkedWorkspace: opts.link, + linkedWorkspace: await resolveLinkedWorkspace(opts.link), inspectEnabled: opts.inspect, inspectBrkEnabled: opts.inspectBrk, require: opts.require, - }; - - switch (role) { - case 'backend': - return startBackend(options); - case 'backend-plugin': - case 'backend-plugin-module': - case 'node-library': - return startBackendPlugin(options); - case 'frontend': - return startFrontend({ - ...options, - entry: 'src/index', - verifyVersions: true, - }); - case 'web-library': - case 'frontend-plugin': - case 'frontend-plugin-module': - return startFrontend({ entry: 'dev/index', ...options }); - case 'frontend-dynamic-container' as PackageRole: // experimental - return startFrontend({ - entry: 'src/index', - ...options, - skipOpenBrowser: true, - isModuleFederationRemote: true, - }); - default: - throw new Error( - `Start command is not supported for package role '${role}'`, - ); - } + }); } diff --git a/packages/cli/src/modules/start/commands/package/start/resolveLinkedWorkspace.ts b/packages/cli/src/modules/start/commands/package/start/resolveLinkedWorkspace.ts new file mode 100644 index 0000000000..814f29212d --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/resolveLinkedWorkspace.ts @@ -0,0 +1,47 @@ +/* + * 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 { ForwardedError } from '@backstage/errors'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path/posix'; + +export async function resolveLinkedWorkspace( + linkPath: string | undefined, +): Promise { + if (!linkPath) { + return undefined; + } + const dir = resolvePath(linkPath); + if (!fs.pathExistsSync(dir)) { + throw new Error(`Invalid workspace link, directory does not exist: ${dir}`); + } + const pkgJson = await fs + .readJson(resolvePath(dir, 'package.json')) + .catch(error => { + throw new ForwardedError( + 'Failed to read package.json in linked workspace', + error, + ); + }); + + if (!pkgJson.workspaces) { + throw new Error( + `Invalid workspace link, directory is not a workspace: ${dir}`, + ); + } + + return dir; +} diff --git a/packages/cli/src/modules/start/commands/package/start/startBackend.ts b/packages/cli/src/modules/start/commands/package/start/startBackend.ts index 7c89a6d887..16b47d309a 100644 --- a/packages/cli/src/modules/start/commands/package/start/startBackend.ts +++ b/packages/cli/src/modules/start/commands/package/start/startBackend.ts @@ -15,10 +15,12 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { paths } from '../../../../../lib/paths'; import { runBackend } from '../../../../../lib/runner'; interface StartBackendOptions { + targetDir: string; checksEnabled: boolean; inspectEnabled: boolean; inspectBrkEnabled: boolean; @@ -28,6 +30,7 @@ interface StartBackendOptions { export async function startBackend(options: StartBackendOptions) { const waitForExit = await runBackend({ + targetDir: options.targetDir, entry: 'src/index', inspectEnabled: options.inspectEnabled, inspectBrkEnabled: options.inspectBrkEnabled, @@ -40,7 +43,7 @@ export async function startBackend(options: StartBackendOptions) { export async function startBackendPlugin(options: StartBackendOptions) { const hasDevIndexEntry = await fs.pathExists( - paths.resolveTarget('dev', 'index.ts'), + resolvePath(options.targetDir ?? paths.targetDir, 'dev/index.ts'), ); if (!hasDevIndexEntry) { console.warn( @@ -50,6 +53,7 @@ export async function startBackendPlugin(options: StartBackendOptions) { } const waitForExit = await runBackend({ + targetDir: options.targetDir, entry: 'dev/index', inspectEnabled: options.inspectEnabled, inspectBrkEnabled: options.inspectBrkEnabled, diff --git a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts index 8abdf451b5..23aa46d7f7 100644 --- a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts +++ b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts @@ -26,6 +26,7 @@ import { BackstagePackageJson } from '@backstage/cli-node'; interface StartAppOptions { verifyVersions?: boolean; entry: string; + targetDir?: string; checksEnabled: boolean; configPaths: string[]; @@ -41,6 +42,7 @@ export async function startFrontend(options: StartAppOptions) { const waitForExit = await serveBundle({ entry: options.entry, + targetDir: options.targetDir, checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, diff --git a/packages/cli/src/modules/start/commands/package/start/startPackage.ts b/packages/cli/src/modules/start/commands/package/start/startPackage.ts new file mode 100644 index 0000000000..d6ca3c068f --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/startPackage.ts @@ -0,0 +1,63 @@ +/* + * 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 { PackageRole } from '@backstage/cli-node'; +import { startBackend, startBackendPlugin } from './startBackend'; +import { startFrontend } from './startFrontend'; + +export async function startPackage(options: { + role: PackageRole; + targetDir: string; + configPaths: string[]; + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; + linkedWorkspace?: string; + require?: string; +}): Promise { + switch (options.role) { + case 'backend': + return startBackend(options); + case 'backend-plugin': + case 'backend-plugin-module': + case 'node-library': + return startBackendPlugin(options); + case 'frontend': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + return startFrontend({ + entry: 'dev/index', + ...options, + }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); + default: + throw new Error( + `Start command is not supported for package role '${options.role}'`, + ); + } +} diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index 2b10a9c16c..ef51a8e89d 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -21,6 +21,8 @@ import { } from '@backstage/cli-node'; import { relative as relativePath } from 'path'; import { paths } from '../../../../lib/paths'; +import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; +import { startPackage } from '../package/start/startPackage'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -31,12 +33,29 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ export async function command( packageNames: string[], - options: { plugin: string[]; config: string[] }, + options: { plugin: string[]; config: string[]; link?: string }, ) { const targetPackages = await findTargetPackages(packageNames, options.plugin); console.log( `Starting ${targetPackages.map(p => p.packageJson.name).join(', ')}`, ); + + // Blocking + await Promise.all( + targetPackages.map(async pkg => { + const opts = { config: [], require: undefined }; + return startPackage({ + role: pkg.packageJson.backstage?.role!, + targetDir: pkg.dir, + configPaths: opts.config as string[], + checksEnabled: false, + linkedWorkspace: await resolveLinkedWorkspace(options.link), + inspectEnabled: false, + inspectBrkEnabled: false, + require: opts.require, + }); + }), + ); } async function findTargetPackages(packageNames: string[], pluginIds: string[]) { From c36ac9742b2101c89df86b208b70eef23c56f215 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 11:31:02 +0200 Subject: [PATCH 04/14] cli: repo start partial parsing of script options Signed-off-by: Patrik Oldsberg --- .../src/modules/build/lib/bundler/server.ts | 1 + packages/cli/src/modules/config/lib/config.ts | 8 +++-- .../commands/package/start/startFrontend.ts | 2 +- .../src/modules/start/commands/repo/start.ts | 36 ++++++++++++++++--- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli/src/modules/build/lib/bundler/server.ts index 4541ecf12c..9d4af5c0fe 100644 --- a/packages/cli/src/modules/build/lib/bundler/server.ts +++ b/packages/cli/src/modules/build/lib/bundler/server.ts @@ -77,6 +77,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const cliConfig = await loadCliConfig({ args: options.configPaths, + targetDir: options.targetDir, fromPackage: name, withFilteredKeys: true, watch(appConfigs) { diff --git a/packages/cli/src/modules/config/lib/config.ts b/packages/cli/src/modules/config/lib/config.ts index afed53af6f..a8d137c60a 100644 --- a/packages/cli/src/modules/config/lib/config.ts +++ b/packages/cli/src/modules/config/lib/config.ts @@ -19,9 +19,11 @@ import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from '../../../lib/paths'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; +import { resolve as resolvePath } from 'path'; type Options = { args: string[]; + targetDir?: string; fromPackage?: string; mockEnv?: boolean; withFilteredKeys?: boolean; @@ -32,8 +34,10 @@ type Options = { }; export async function loadCliConfig(options: Options) { + const targetDir = options.targetDir ?? paths.targetDir; + // Consider all packages in the monorepo when loading in config - const { packages } = await getPackages(paths.targetDir); + const { packages } = await getPackages(targetDir); let localPackageNames; if (options.fromPackage) { @@ -70,7 +74,7 @@ export async function loadCliConfig(options: Options) { : undefined, watch: Boolean(options.watch), rootDir: paths.targetRoot, - argv: options.args.flatMap(t => ['--config', paths.resolveTarget(t)]), + argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]), }); const appConfigs = await new Promise((resolve, reject) => { diff --git a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts index 23aa46d7f7..d4b2f6a1cd 100644 --- a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts +++ b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts @@ -37,7 +37,7 @@ interface StartAppOptions { export async function startFrontend(options: StartAppOptions) { const packageJson = (await readJson( - paths.resolveTarget('package.json'), + resolvePath(options.targetDir ?? paths.targetDir, 'package.json'), )) as BackstagePackageJson; const waitForExit = await serveBundle({ diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index ef51a8e89d..6ca204e169 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -23,6 +23,7 @@ import { relative as relativePath } from 'path'; import { paths } from '../../../../lib/paths'; import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; import { startPackage } from '../package/start/startPackage'; +import { parseArgs } from 'util'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -40,19 +41,46 @@ export async function command( `Starting ${targetPackages.map(p => p.packageJson.name).join(', ')}`, ); - // Blocking + // Each of these block until interrupt by user await Promise.all( targetPackages.map(async pkg => { - const opts = { config: [], require: undefined }; + const startScript = pkg.packageJson.scripts?.start; + if (!startScript) { + console.log( + `No start script found for package ${pkg.packageJson.name}, skipping...`, + ); + return undefined; + } + + // Grab and parse --config and --require options from the start scripts, the rest are ignored + // TODO(Rugvip): Prolly switch over to completely different arg parsing to avoid this duplication + const { values: parsedOpts } = parseArgs({ + args: startScript.split(' '), + strict: false, + options: { + config: { + type: 'string', + multiple: true, + }, + require: { + type: 'string', + }, + }, + }); + const parsedRequire = + typeof parsedOpts.require === 'string' ? parsedOpts.require : undefined; + const parsedConfig = + parsedOpts.config?.filter(c => typeof c === 'string') ?? []; + return startPackage({ role: pkg.packageJson.backstage?.role!, targetDir: pkg.dir, - configPaths: opts.config as string[], + configPaths: options.config.length > 0 ? options.config : parsedConfig, checksEnabled: false, linkedWorkspace: await resolveLinkedWorkspace(options.link), inspectEnabled: false, inspectBrkEnabled: false, - require: opts.require, + require: parsedRequire, }); }), ); From c70f89ff38ce3171f14509db2451372dc75f8c71 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 11:40:24 +0200 Subject: [PATCH 05/14] cli: repo start refactor and more accurate logging of started packages Signed-off-by: Patrik Oldsberg --- .../src/modules/start/commands/repo/start.ts | 111 +++++++++++------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index 6ca204e169..54d53c51d6 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -37,53 +37,22 @@ export async function command( options: { plugin: string[]; config: string[]; link?: string }, ) { const targetPackages = await findTargetPackages(packageNames, options.plugin); + + const packageOptions = await resolvePackageOptions(targetPackages, options); + + if (packageOptions.length === 0) { + console.log('No packages found to start'); + return; + } + console.log( - `Starting ${targetPackages.map(p => p.packageJson.name).join(', ')}`, + `Starting ${packageOptions + .map(({ pkg }) => pkg.packageJson.name) + .join(', ')}`, ); - // Each of these block until interrupt by user - await Promise.all( - targetPackages.map(async pkg => { - const startScript = pkg.packageJson.scripts?.start; - if (!startScript) { - console.log( - `No start script found for package ${pkg.packageJson.name}, skipping...`, - ); - return undefined; - } - - // Grab and parse --config and --require options from the start scripts, the rest are ignored - // TODO(Rugvip): Prolly switch over to completely different arg parsing to avoid this duplication - const { values: parsedOpts } = parseArgs({ - args: startScript.split(' '), - strict: false, - options: { - config: { - type: 'string', - multiple: true, - }, - require: { - type: 'string', - }, - }, - }); - const parsedRequire = - typeof parsedOpts.require === 'string' ? parsedOpts.require : undefined; - const parsedConfig = - parsedOpts.config?.filter(c => typeof c === 'string') ?? []; - - return startPackage({ - role: pkg.packageJson.backstage?.role!, - targetDir: pkg.dir, - configPaths: options.config.length > 0 ? options.config : parsedConfig, - checksEnabled: false, - linkedWorkspace: await resolveLinkedWorkspace(options.link), - inspectEnabled: false, - inspectBrkEnabled: false, - require: parsedRequire, - }); - }), - ); + // Each of these block until interrupted by user + await Promise.all(packageOptions.map(entry => startPackage(entry.options))); } async function findTargetPackages(packageNames: string[], pluginIds: string[]) { @@ -160,3 +129,57 @@ async function findTargetPackages(packageNames: string[], pluginIds: string[]) { } return targetPackages; } + +async function resolvePackageOptions( + targetPackages: BackstagePackage[], + options: { plugin: string[]; config: string[]; link?: string }, +) { + const linkedWorkspace = await resolveLinkedWorkspace(options.link); + + return targetPackages.flatMap(pkg => { + const startScript = pkg.packageJson.scripts?.start; + if (!startScript) { + console.log( + `No start script found for package ${pkg.packageJson.name}, skipping...`, + ); + return []; + } + + // Grab and parse --config and --require options from the start scripts, the rest are ignored + // TODO(Rugvip): Prolly switch over to completely different arg parsing to avoid this duplication + const { values: parsedOpts } = parseArgs({ + args: startScript.split(' '), + strict: false, + options: { + config: { + type: 'string', + multiple: true, + }, + require: { + type: 'string', + }, + }, + }); + const parsedRequire = + typeof parsedOpts.require === 'string' ? parsedOpts.require : undefined; + const parsedConfig = + parsedOpts.config?.filter(c => typeof c === 'string') ?? []; + + return [ + { + pkg, + options: { + role: pkg.packageJson.backstage?.role!, + targetDir: pkg.dir, + configPaths: + options.config.length > 0 ? options.config : parsedConfig, + checksEnabled: false, + linkedWorkspace, + inspectEnabled: false, + inspectBrkEnabled: false, + require: parsedRequire, + }, + }, + ]; + }); +} From 1b27c4d4349e122d92ec9ad5822c47beea48ea71 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 11:49:06 +0200 Subject: [PATCH 06/14] cli: add repo start fallback for running plugins if app/backend are missing Signed-off-by: Patrik Oldsberg --- .../src/modules/start/commands/repo/start.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index 54d53c51d6..fc1b87d3ac 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -95,7 +95,7 @@ async function findTargetPackages(packageNames: string[], pluginIds: string[]) { return targetPackages; } - // If on package names are provided, default to expect a single frontend and/or backend package + // If no 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, @@ -122,12 +122,31 @@ async function findTargetPackages(packageNames: string[], pluginIds: string[]) { targetPackages.push(...matchingPackages); } - if (targetPackages.length === 0) { - throw new Error( - `Unable to find any packages with role 'frontend' or 'backend'`, - ); + if (targetPackages.length > 0) { + return targetPackages; } - return targetPackages; + + // If no app or backend packages are found, fall back to expecting single plugin packages + for (const role of ['frontend-plugin', 'backend-plugin']) { + const matchingPackages = packages.filter( + pkg => pkg.packageJson.backstage?.role === role, + ); + if (matchingPackages.length > 1) { + throw new Error( + `Found multiple packages with role '${role}', please choose which packages you want` + + `to run by passing the package names explicitly as arguments, for example ` + + `'yarn backstage-cli repo start my-plugin my-plugin-backend'.`, + ); + } + targetPackages.push(...matchingPackages); + } + if (targetPackages.length > 0) { + return targetPackages; + } + + throw new Error( + `Unable to find any packages with role 'frontend', 'backend', 'frontend-plugin', or 'backend-plugin'.`, + ); } async function resolvePackageOptions( From b8a675df4bff18430aa3a8779930c68487cd1c42 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 11:59:44 +0200 Subject: [PATCH 07/14] docs/tooling: add docs for new repo start command Signed-off-by: Patrik Oldsberg --- docs/tooling/cli/03-commands.md | 36 +++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index ed94bb5efb..5fa01f931b 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -48,12 +48,14 @@ help [command] display help for command The `repo` command category, `yarn backstage-cli repo --help`: ```text -build [options] Build packages in the project, excluding bundled app and backend packages. -lint [options] Lint all packages in the project -clean Delete cache and output directories -list-deprecations [options] List deprecations -test [options] Run tests, forwarding args to Jest, defaulting to watch mode -help [command] display help for command +start [options] [packageName...] Starts packages in the repo for local development +build [options] Build packages in the project, excluding bundled app and backend packages. +test [options] Run tests, forwarding args to Jest, defaulting to watch mode +lint [options] Lint all packages in the project +fix [options] Automatically fix packages in the project +clean Delete cache and output directories +list-deprecations [options] List deprecations +help [command] display help for command ``` The `migrate` command category, `yarn backstage-cli migrate --help`: @@ -67,6 +69,28 @@ react-router-deps Migrates the react-router dependencies for all packages to help [command] display help for command ``` +## repo start + +Start a set of packages in the project for local development. If no explicit packages are listed via arguments or options, packages will instead be selected based on their [package role](./02-build-system.md#package-roles). If a single set of frontend and/or backend packages are found, they will be started. If there are multiple matches the directories 'packages/app' and 'packages/backend' will be preferred. If no matches are found the command will fall back to expecting a single plugin frontend and/or backend package to start instead. + +Any `--config` options in the `start` script in `package.json` of the selected packages will be picked up and used, unless a `--config` option is provided to this command, in which case it will be used instead. + +Any `--require` option in the `start` script in `package.json` of the selected backend package will be picked up and used. + +```text +Usage: backstage-cli repo start [options] [packageName...] + +Starts packages in the repo for local development + +Arguments: + packageName Run the specified package instead of the defaults. + +Options: + --config Config files to load instead of app-config.yaml (default: []) + --plugin Start the dev entry-point for any matching plugin package in the repo (default: []) + --link Link an external workspace for module resolution +``` + ## repo build Builds all packages in the project, excluding bundled packages by default, i.e. ones From fcf87f67b99448a24e905b216f6138ae271f9e8e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 12:00:00 +0200 Subject: [PATCH 08/14] cli: update CLI report for repo start Signed-off-by: Patrik Oldsberg --- packages/cli/cli-report.backstage-cli.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/cli/cli-report.backstage-cli.md b/packages/cli/cli-report.backstage-cli.md index 03c2c387a9..2e6b34850e 100644 --- a/packages/cli/cli-report.backstage-cli.md +++ b/packages/cli/cli-report.backstage-cli.md @@ -400,6 +400,7 @@ Options: -h, --help Commands: + start [options] [packageName...] build [options] clean fix [options] @@ -467,6 +468,18 @@ Options: -h, --help ``` +### `backstage-cli repo start` + +``` +Usage: backstage-cli repo start [options] [packageName...] + +Options: + --config + --plugin + --link + -h, --help +``` + ### `backstage-cli repo test` ``` From edabbd6d52b752639ea5b174b2116c65bc397f38 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 12:27:29 +0200 Subject: [PATCH 09/14] root: update start scripts and remove dev Signed-off-by: Patrik Oldsberg --- .changeset/four-drinks-begin.md | 16 ++++++++++++++++ lighthouserc.js | 2 +- package.json | 13 ++++++------- .../templates/default-app/package.json.hbs | 4 +--- 4 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 .changeset/four-drinks-begin.md diff --git a/.changeset/four-drinks-begin.md b/.changeset/four-drinks-begin.md new file mode 100644 index 0000000000..aca60e2999 --- /dev/null +++ b/.changeset/four-drinks-begin.md @@ -0,0 +1,16 @@ +--- +'@backstage/create-app': patch +--- + +Updated the root `package.json` in the template to use the new `backstage-cli repo start` command. + +The `yarn dev` command is now redundant and has been removed from the template. We recommend existing projects to add these or similar scripts to help redirect users: + +```json +{ + "scripts": { + "dev": "echo \"Use 'yarn start' instead\"", + "start-backend": "echo \"Use 'yarn start backend' instead\"" + } +} +``` diff --git a/lighthouserc.js b/lighthouserc.js index 49595068ca..396bff5dc7 100644 --- a/lighthouserc.js +++ b/lighthouserc.js @@ -52,7 +52,7 @@ module.exports = { outputPath: './.lighthouseci/reports', preset: 'desktop', }, - startServerCommand: 'yarn start:lighthouse', + startServerCommand: 'yarn start', startServerReadyPattern: 'webpack compiled successfully', startServerReadyTimeout: 600000, numberOfRuns: 1, diff --git a/package.json b/package.json index a2dad21782..26c7844580 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "build:plugins-report": "node ./scripts/build-plugins-report", "clean": "backstage-cli repo clean", "create-plugin": "echo \"use 'yarn new' instead\"", - "dev": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start", - "dev:next": "yarn workspaces foreach -A --include example-backend --include example-app-next --parallel --jobs unlimited -v -i run start", + "dev": "echo \"use 'yarn start' instead\"", + "dev:next": "echo \"use 'yarn start:next' instead\"", "docker-build": "yarn tsc && yarn workspace example-backend build && yarn workspace example-backend build-image", "fix": "backstage-cli repo fix --publish", "postinstall": "husky || true", @@ -50,12 +50,11 @@ "release": "node scripts/prepare-release.js && changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable", "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", "snyk:test:package": "yarn snyk:test --include", - "start": "yarn workspace example-app start", - "start-backend": "yarn workspace example-backend start", - "start-backend:legacy": "yarn workspace example-backend-legacy start", - "start:lighthouse": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start", + "start": "backstage-cli repo start", + "start-backend": "echo \"Use 'yarn start example-backend' instead\"", + "start-backend:legacy": "echo \"Use 'yarn start example-backend-legacy' instead\"", "start:microsite": "cd microsite/ && yarn start", - "start:next": "yarn workspace example-app-next start", + "start:next": "yarn start example-app-next example-backend", "storybook": "yarn ./storybook run storybook", "techdocs-cli": "node scripts/techdocs-cli.js", "techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js", diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 83659e2a02..8399c6505a 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -6,9 +6,7 @@ "node": "20 || 22" }, "scripts": { - "dev": "yarn workspaces foreach -A --include backend --include app --parallel --jobs unlimited -v -i run start", - "start": "yarn workspace app start", - "start-backend": "yarn workspace backend start", + "start": "backstage-cli repo start", "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", "build-image": "yarn workspace backend build-image", From d2091c6fe0ebc298f7afd6530c74354b15b73320 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 12:35:48 +0200 Subject: [PATCH 10/14] changesets: add changeset for new repo start command Signed-off-by: Patrik Oldsberg --- .changeset/twelve-hornets-smell.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .changeset/twelve-hornets-smell.md diff --git a/.changeset/twelve-hornets-smell.md b/.changeset/twelve-hornets-smell.md new file mode 100644 index 0000000000..a9845e2547 --- /dev/null +++ b/.changeset/twelve-hornets-smell.md @@ -0,0 +1,30 @@ +--- +'@backstage/cli': patch +--- + +Added a new `repo start` command to replace the existing pattern of using `yarn dev` scripts. The `repo start` command runs the app and/or backend package in the repo by default, but will also fall back to running other individual frontend or backend packages or even plugin dev entry points if the can be uniquely selected. + +The goal of this change is to reduce the number of different necessary scripts and align on `yarn start` being the only command needed for local development, similar to how `repo test` handles testing in the repo. It also opens up for more powerful options, like the `--plugin ` flag that runs the dev entry point of the selected plugin. + +The new script is installed as follows, replacing the existing `yarn start` script: + +```json +{ + "scripts": { + "start": "backstage-cli repo start" + } +} +``` + +In order to help users migrate in existing projects, it is recommended to add the following scripts to the root `package.json`: + +```json +{ + "scripts": { + "dev": "echo \"Use 'yarn start' instead\"", + "start-backend": "echo \"Use 'yarn start backend' instead\"" + } +} +``` + +For more information, run `yarn start --help` once the new command is installed. From fd4fddad77b0e651eede3af7542632502f39562a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 6 Apr 2025 12:58:19 +0200 Subject: [PATCH 11/14] cli: enable forwarding of backend flags for repo start Signed-off-by: Patrik Oldsberg --- docs/tooling/cli/03-commands.md | 11 ++++++---- packages/cli/cli-report.backstage-cli.md | 5 ++++- packages/cli/src/lib/runner/runBackend.ts | 4 ++-- packages/cli/src/modules/start/alpha.ts | 20 +++++++++++++---- .../commands/package/start/startBackend.ts | 4 ++-- .../commands/package/start/startPackage.ts | 4 ++-- .../src/modules/start/commands/repo/start.ts | 22 ++++++++++++------- packages/cli/src/modules/start/index.ts | 14 +++++++++++- 8 files changed, 60 insertions(+), 24 deletions(-) diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 5fa01f931b..6ca63da44e 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -83,12 +83,15 @@ Usage: backstage-cli repo start [options] [packageName...] Starts packages in the repo for local development Arguments: - packageName Run the specified package instead of the defaults. + packageName Run the specified package instead of the defaults. Options: - --config Config files to load instead of app-config.yaml (default: []) - --plugin Start the dev entry-point for any matching plugin package in the repo (default: []) - --link Link an external workspace for module resolution + --plugin Start the dev entry-point for any matching plugin package in the repo (default: []) + --config Config files to load instead of app-config.yaml (default: []) + --inspect [host] Enable debugger in Node.js environments. Applies to backend package only + --inspect-brk [host] Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only + --require Add a --require argument to the node process. Applies to backend package only + --link Link an external workspace for module resolution ``` ## repo build diff --git a/packages/cli/cli-report.backstage-cli.md b/packages/cli/cli-report.backstage-cli.md index 2e6b34850e..3747d6acc4 100644 --- a/packages/cli/cli-report.backstage-cli.md +++ b/packages/cli/cli-report.backstage-cli.md @@ -474,8 +474,11 @@ Options: Usage: backstage-cli repo start [options] [packageName...] Options: - --config --plugin + --config + --inspect [host] + --inspect-brk [host] + --require --link -h, --help ``` diff --git a/packages/cli/src/lib/runner/runBackend.ts b/packages/cli/src/lib/runner/runBackend.ts index 9f52968067..ebb1d535b3 100644 --- a/packages/cli/src/lib/runner/runBackend.ts +++ b/packages/cli/src/lib/runner/runBackend.ts @@ -37,9 +37,9 @@ export type RunBackendOptions = { /** relative entry point path without extension, e.g. 'src/index' */ entry: string; /** Whether to forward the --inspect flag to the node process */ - inspectEnabled: boolean; + inspectEnabled?: boolean | string; /** Whether to forward the --inspect-brk flag to the node process */ - inspectBrkEnabled: boolean; + inspectBrkEnabled?: boolean | string; /** Additional module to require via the --require flag to the node process */ require?: string | string[]; /** An external linked workspace to override module resolution towards */ diff --git a/packages/cli/src/modules/start/alpha.ts b/packages/cli/src/modules/start/alpha.ts index 860753fb85..0d2b6f661e 100644 --- a/packages/cli/src/modules/start/alpha.ts +++ b/packages/cli/src/modules/start/alpha.ts @@ -19,8 +19,8 @@ import { createCliPlugin } from '../../wiring/factory'; import { lazy } from '../../lib/lazy'; import { configOption } from '../config'; -export const buildPlugin = createCliPlugin({ - pluginId: 'build', +export const startPlugin = createCliPlugin({ + pluginId: 'start', init: async reg => { reg.addCommand({ path: ['package', 'start'], @@ -65,13 +65,25 @@ export const buildPlugin = createCliPlugin({ '[...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(...configOption) + .option( + '--inspect [host]', + 'Enable debugger in Node.js environments. Applies to backend package only', + ) + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', + ) + .option( + '--require ', + 'Add a --require argument to the node process. Applies to backend package only', + ) .option( '--link ', 'Link an external workspace for module resolution', @@ -84,4 +96,4 @@ export const buildPlugin = createCliPlugin({ }, }); -export default buildPlugin; +export default startPlugin; diff --git a/packages/cli/src/modules/start/commands/package/start/startBackend.ts b/packages/cli/src/modules/start/commands/package/start/startBackend.ts index 16b47d309a..417398e9f9 100644 --- a/packages/cli/src/modules/start/commands/package/start/startBackend.ts +++ b/packages/cli/src/modules/start/commands/package/start/startBackend.ts @@ -22,8 +22,8 @@ import { runBackend } from '../../../../../lib/runner'; interface StartBackendOptions { targetDir: string; checksEnabled: boolean; - inspectEnabled: boolean; - inspectBrkEnabled: boolean; + inspectEnabled?: boolean | string; + inspectBrkEnabled?: boolean | string; linkedWorkspace?: string; require?: string; } diff --git a/packages/cli/src/modules/start/commands/package/start/startPackage.ts b/packages/cli/src/modules/start/commands/package/start/startPackage.ts index d6ca3c068f..10f705ba4d 100644 --- a/packages/cli/src/modules/start/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/start/commands/package/start/startPackage.ts @@ -23,8 +23,8 @@ export async function startPackage(options: { targetDir: string; configPaths: string[]; checksEnabled: boolean; - inspectEnabled: boolean; - inspectBrkEnabled: boolean; + inspectEnabled?: boolean | string; + inspectBrkEnabled?: boolean | string; linkedWorkspace?: string; require?: string; }): Promise { diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index fc1b87d3ac..7a4704ea3e 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -32,10 +32,16 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ 'backend-plugin', ]; -export async function command( - packageNames: string[], - options: { plugin: string[]; config: string[]; link?: string }, -) { +type CommandOptions = { + plugin: string[]; + config: string[]; + inspect?: boolean | string; + inspectBrk?: boolean | string; + require?: string; + link?: string; +}; + +export async function command(packageNames: string[], options: CommandOptions) { const targetPackages = await findTargetPackages(packageNames, options.plugin); const packageOptions = await resolvePackageOptions(targetPackages, options); @@ -151,7 +157,7 @@ async function findTargetPackages(packageNames: string[], pluginIds: string[]) { async function resolvePackageOptions( targetPackages: BackstagePackage[], - options: { plugin: string[]; config: string[]; link?: string }, + options: CommandOptions, ) { const linkedWorkspace = await resolveLinkedWorkspace(options.link); @@ -194,9 +200,9 @@ async function resolvePackageOptions( options.config.length > 0 ? options.config : parsedConfig, checksEnabled: false, linkedWorkspace, - inspectEnabled: false, - inspectBrkEnabled: false, - require: parsedRequire, + inspectEnabled: options.inspect, + inspectBrkEnabled: options.inspectBrk, + require: options.require ?? parsedRequire, }, }, ]; diff --git a/packages/cli/src/modules/start/index.ts b/packages/cli/src/modules/start/index.ts index 627336e3f4..f1dd1eeb71 100644 --- a/packages/cli/src/modules/start/index.ts +++ b/packages/cli/src/modules/start/index.ts @@ -25,13 +25,25 @@ export function registerRepoCommands(command: Command) { '[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(...configOption) + .option( + '--inspect [host]', + 'Enable debugger in Node.js environments. Applies to backend package only', + ) + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', + ) + .option( + '--require ', + 'Add a --require argument to the node process. Applies to backend package only', + ) .option('--link ', 'Link an external workspace for module resolution') .action(lazy(() => import('./commands/repo/start'), 'command')); } From fbb84fbc8cca73bf8bfabc7c5e26c12aadcdfb74 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Apr 2025 01:23:20 +0200 Subject: [PATCH 12/14] cli: add support for passing package paths to repo start Signed-off-by: Patrik Oldsberg --- docs/tooling/cli/03-commands.md | 4 ++-- packages/cli/cli-report.backstage-cli.md | 8 +++---- packages/cli/src/modules/start/alpha.ts | 2 +- .../src/modules/start/commands/repo/start.ts | 22 ++++++++++++------- packages/cli/src/modules/start/index.ts | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 6ca63da44e..d176278655 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -78,12 +78,12 @@ Any `--config` options in the `start` script in `package.json` of the selected p Any `--require` option in the `start` script in `package.json` of the selected backend package will be picked up and used. ```text -Usage: backstage-cli repo start [options] [packageName...] +Usage: backstage-cli repo start [options] [packageNameOrPath...] Starts packages in the repo for local development Arguments: - packageName Run the specified package instead of the defaults. + packageNameOrPath Run the specified packages instead of the defaults. Options: --plugin Start the dev entry-point for any matching plugin package in the repo (default: []) diff --git a/packages/cli/cli-report.backstage-cli.md b/packages/cli/cli-report.backstage-cli.md index 3747d6acc4..ed6ae9049e 100644 --- a/packages/cli/cli-report.backstage-cli.md +++ b/packages/cli/cli-report.backstage-cli.md @@ -400,13 +400,13 @@ Options: -h, --help Commands: - start [options] [packageName...] build [options] clean fix [options] help [command] lint [options] list-deprecations [options] + start [options] [packageNameOrPath...] test [options] ``` @@ -471,15 +471,15 @@ Options: ### `backstage-cli repo start` ``` -Usage: backstage-cli repo start [options] [packageName...] +Usage: backstage-cli repo start [options] [packageNameOrPath...] Options: - --plugin --config --inspect [host] --inspect-brk [host] - --require --link + --plugin + --require -h, --help ``` diff --git a/packages/cli/src/modules/start/alpha.ts b/packages/cli/src/modules/start/alpha.ts index 0d2b6f661e..a5875df335 100644 --- a/packages/cli/src/modules/start/alpha.ts +++ b/packages/cli/src/modules/start/alpha.ts @@ -62,7 +62,7 @@ export const startPlugin = createCliPlugin({ const defaultCommand = command .argument( - '[...packageName]', + '[...packageNameOrPath]', 'Run the specified package instead of the defaults.', ) .option( diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index 7a4704ea3e..b25054bbfc 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -41,8 +41,8 @@ type CommandOptions = { link?: string; }; -export async function command(packageNames: string[], options: CommandOptions) { - const targetPackages = await findTargetPackages(packageNames, options.plugin); +export async function command(namesOrPaths: string[], options: CommandOptions) { + const targetPackages = await findTargetPackages(namesOrPaths, options.plugin); const packageOptions = await resolvePackageOptions(targetPackages, options); @@ -61,7 +61,7 @@ export async function command(packageNames: string[], options: CommandOptions) { await Promise.all(packageOptions.map(entry => startPackage(entry.options))); } -async function findTargetPackages(packageNames: string[], pluginIds: string[]) { +async function findTargetPackages(namesOrPaths: string[], pluginIds: string[]) { const targetPackages = new Array(); const packages = await PackageGraph.listTargetPackages(); @@ -87,12 +87,18 @@ async function findTargetPackages(packageNames: string[], pluginIds: string[]) { } // 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; - }); + for (const nameOrPath of namesOrPaths) { + let matchingPackage = packages.find( + pkg => nameOrPath === pkg.packageJson.name, + ); if (!matchingPackage) { - throw new Error(`Unable to find package by name '${packageName}'`); + const absPath = paths.resolveTargetRoot(nameOrPath); + matchingPackage = packages.find( + pkg => relativePath(pkg.dir, absPath) === '', + ); + } + if (!matchingPackage) { + throw new Error(`Unable to find package by name '${nameOrPath}'`); } targetPackages.push(matchingPackage); } diff --git a/packages/cli/src/modules/start/index.ts b/packages/cli/src/modules/start/index.ts index f1dd1eeb71..17225820f7 100644 --- a/packages/cli/src/modules/start/index.ts +++ b/packages/cli/src/modules/start/index.ts @@ -22,7 +22,7 @@ export function registerRepoCommands(command: Command) { .command('start') .description('Starts packages in the repo for local development') .argument( - '[packageName...]', + '[packageNameOrPath...]', 'Run the specified package instead of the defaults.', ) .option( From e655f626add5efa9967a6bd8a42b405ee7f09104 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Apr 2025 01:30:11 +0200 Subject: [PATCH 13/14] update existing references to yarn dev to use yarn start instead Signed-off-by: Patrik Oldsberg --- .changeset/chubby-tables-tie.md | 7 +++++++ CONTRIBUTING.md | 2 +- contrib/docker/devops/makefile | 16 ++++++++-------- docs/getting-started/config/database.md | 2 +- docs/getting-started/index.md | 4 ++-- docs/getting-started/logging-in.md | 2 +- docs/permissions/custom-rules.md | 2 +- docs/permissions/getting-started.md | 4 ++-- docs/plugins/create-a-plugin.md | 2 +- docs/plugins/integrating-search-into-plugins.md | 2 +- docs/tooling/local-dev/debugging.md | 2 +- docs/tooling/local-dev/profiling.md | 2 +- docs/tutorials/setup-opentelemetry.md | 2 +- ...020-04-30-how-to-quickly-set-up-backstage.mdx | 2 +- .../cli/templates/backend-plugin/README.md.hbs | 2 +- packages/create-app/src/createApp.ts | 2 +- .../create-app/templates/default-app/README.md | 2 +- .../templates/default-app/playwright.config.ts | 2 +- .../examples/docker-compose.oauth2-proxy.yaml | 4 ++-- plugins/catalog/README.md | 2 +- plugins/devtools/README.md | 6 +++--- plugins/search/README.md | 2 +- 22 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 .changeset/chubby-tables-tie.md diff --git a/.changeset/chubby-tables-tie.md b/.changeset/chubby-tables-tie.md new file mode 100644 index 0000000000..7aa5bc915b --- /dev/null +++ b/.changeset/chubby-tables-tie.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-devtools': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-search': patch +--- + +Updated `README.md` to use `yarn start` instead of `yarn dev`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58f894447b..bc67fba8c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ yarn tsc # does a first run of type generation and checks Open a terminal window and start the web app by using the following command from the project root. Make sure you have run the above mentioned commands first. ```bash -yarn dev +yarn start ``` This is going to start two things, the frontend (:3000) and the backend (:7007). diff --git a/contrib/docker/devops/makefile b/contrib/docker/devops/makefile index 048edc8886..4c376bd052 100644 --- a/contrib/docker/devops/makefile +++ b/contrib/docker/devops/makefile @@ -192,11 +192,11 @@ check: check-code check-docs check-type-dependencies check-styles # run development instance # BUG: the frontend seems to run on "$(backend_port)" (7007 default). -# The documentation states "This is going to start two things, +# The documentation states "This is going to start two things, # the frontend (:3000) and the backend (:7007)." # However, the frontend seems to end up running on 7007. .PHONY: dev -dev: build +start: build @docker run --rm -it \ --name $(docker_name_timestamp_prefix)-$@ \ -p $(frontend_port):$(frontend_host_port) \ @@ -206,12 +206,12 @@ dev: build -w /app \ --entrypoint "" \ $(docker_tag) \ - yarn dev + yarn start -# convenience: dev alias -.PHONY: start -start: dev +# convenience: start alias +.PHONY: dev +dev: start -# convenience: dev alias +# convenience: start alias .PHONY: run -run: dev +run: start diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index ecf461f02a..e068361966 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -111,7 +111,7 @@ If you opt for the second option of replacing the entire string, take care to no [Start the Backstage app](../index.md#2-run-the-backstage-app): ```shell -yarn dev +yarn start ``` After the Backstage frontend launches, you should notice that nothing has changed. This is a good sign. If everything is setup correctly above, this means that the data is flowing from the demo data files directly into your database! diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 7aee835910..cd86aa3343 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -116,11 +116,11 @@ If this fails on the `yarn install` step, it's likely that you will need to inst ## 2. Run the Backstage app -Your Backstage app is fully installed and ready to be run! Now that the installation is complete, you can go to the application directory and start the app using the `yarn dev` command. The `yarn dev` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window. +Your Backstage app is fully installed and ready to be run! Now that the installation is complete, you can go to the application directory and start the app using the `yarn start` command. The `yarn start` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window. ```bash cd my-backstage-app # your app name -yarn dev +yarn start ``` ![Screenshot of the command output, with the message web pack compiled successfully](../assets/getting-started/startup.png) diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index 16596865a2..337cc27ac7 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -16,7 +16,7 @@ You should have already [have a standalone app](./index.md) and completed the Gi ## 1. Login to Backstage -Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. +Run your Backstage app with `yarn start`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 41a1966a34..bd35f5de8e 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -187,7 +187,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer backend.add(import('./extensions/catalogPermissionRules')); ``` -5. Now when you run you Backstage instance - `yarn dev` - the rule will be added to the catalog plugin. +5. Now when you run you Backstage instance - `yarn start` - the rule will be added to the catalog plugin. The updated policy will allow catalog entity resource permissions if any of the following are true: diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 14058bccb0..cb0d29ef82 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -103,10 +103,10 @@ Now lets test end to end that the permissions framework is setup and configured enabled: true ``` -2. Now run `yarn dev`, Backstage should load up in your browser +2. Now run `yarn start`, Backstage should load up in your browser 3. You should see that you have entities in your Catalog, pretty simple 4. Let's change this line in our Test Permission Policy `return { result: AuthorizeResult.ALLOW };` to be `return { result: AuthorizeResult.DENY };` -5. Run `yarn dev` once again, Backstage should load up in your browser +5. Run `yarn start` once again, Backstage should load up in your browser 6. This time you should not see any entities in your Catalog, if you do then something went wrong along the way and you'll need to review the steps above 7. Revert the change we made in step 4 so that the line looks like this: `return { result: AuthorizeResult.ALLOW };` diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 3810495707..ea0b088af7 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -25,7 +25,7 @@ And then select `frontend-plugin`. This will create a new Backstage Plugin based on the ID that was provided. It will be built and added to the Backstage App automatically. -> If the Backstage App is already running (with `yarn start` or `yarn dev`) you +> If the Backstage App is already running (with `yarn start`) you > should be able to see the default page for your new plugin directly by > navigating to `http://localhost:3000/my-plugin`. diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index c8cb0810f7..374a8aeb8a 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -245,7 +245,7 @@ You can also check out the documentation on [how to test Backstage plugin module #### 9. Running the collator locally -Run `yarn dev` in the root folder of your Backstage project and look for logs like these: +Run `yarn start` in the root folder of your Backstage project and look for logs like these: ```sh [backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Task worker starting: search_index_faq_snippets, {"version":2,"cadence":"PT10M","initialDelayDuration":"PT3S","timeoutAfterDuration":"PT15M"} task=search_index_faq_snippets diff --git a/docs/tooling/local-dev/debugging.md b/docs/tooling/local-dev/debugging.md index 65354442b2..3f0103eaf4 100644 --- a/docs/tooling/local-dev/debugging.md +++ b/docs/tooling/local-dev/debugging.md @@ -19,7 +19,7 @@ Changing the level can be done by setting the `LOG_LEVEL` environment variable. For example, to turn on debug logs when running the app locally, you can run: ```shell -LOG_LEVEL=debug yarn dev +LOG_LEVEL=debug yarn start ``` The resulting log should now have more information available for debugging: diff --git a/docs/tooling/local-dev/profiling.md b/docs/tooling/local-dev/profiling.md index b1c904d6d0..8d88fc1769 100644 --- a/docs/tooling/local-dev/profiling.md +++ b/docs/tooling/local-dev/profiling.md @@ -60,6 +60,6 @@ See more command options in the AutoCannon documentation. Profiling the frontend can be done by using the `React DevTools` extension for Chrome or Firefox. The extension is available for download from the Chrome Web Store or the Firefox Add-ons website. -To start profiling, start the application with `yarn dev` and open inspector in the browser. In the +To start profiling, start the application with `yarn start` and open inspector in the browser. In the `Profiler` tab (far to the right), click the `Start profiling` button to start recording. After you have recorded some data by navigating through the page, click the `Stop profiling` button to stop the recording. diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index c7687f1ab9..3d7cb4bb4b 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -62,7 +62,7 @@ For local development, you can add the required flag in your `packages/backend/p ... ``` -You can now start your Backstage instance as usual, using `yarn dev`. +You can now start your Backstage instance as usual, using `yarn start`. ## Production Setup diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx index b59f943245..d01bd496d1 100644 --- a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.mdx @@ -45,7 +45,7 @@ The only thing you need to do is to start the app: ```bash cd my-app -yarn dev +yarn start ``` And you are good to go! 👍 diff --git a/packages/cli/templates/backend-plugin/README.md.hbs b/packages/cli/templates/backend-plugin/README.md.hbs index 76189c07c2..6b6b2a97bb 100644 --- a/packages/cli/templates/backend-plugin/README.md.hbs +++ b/packages/cli/templates/backend-plugin/README.md.hbs @@ -25,4 +25,4 @@ This plugin backend can be started in a standalone mode from directly in this package with `yarn start`. It is a limited setup that is most convenient when developing the plugin backend itself. -If you want to run the entire project, including the frontend, run `yarn dev` from the root directory. +If you want to run the entire project, including the frontend, run `yarn start` from the root directory. diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 22b84ba5cd..d4d9dda18a 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -155,7 +155,7 @@ export default async (opts: OptionValues): Promise => { } Task.log( ` Run the app: ${chalk.cyan( - `cd ${opts.path ?? answers.name} && yarn dev`, + `cd ${opts.path ?? answers.name} && yarn start`, )}`, ); Task.log( diff --git a/packages/create-app/templates/default-app/README.md b/packages/create-app/templates/default-app/README.md index 8c7c4373fe..041c4fbe55 100644 --- a/packages/create-app/templates/default-app/README.md +++ b/packages/create-app/templates/default-app/README.md @@ -6,5 +6,5 @@ To start the app, run: ```sh yarn install -yarn dev +yarn start ``` diff --git a/packages/create-app/templates/default-app/playwright.config.ts b/packages/create-app/templates/default-app/playwright.config.ts index 733be130bd..37c7fb14c7 100644 --- a/packages/create-app/templates/default-app/playwright.config.ts +++ b/packages/create-app/templates/default-app/playwright.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ ? [] : [ { - command: 'yarn dev', + command: 'yarn start', port: 3000, reuseExistingServer: true, timeout: 60_000, diff --git a/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml b/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml index f00e2e0987..1a8dfa5049 100755 --- a/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml +++ b/plugins/auth-backend/examples/docker-compose.oauth2-proxy.yaml @@ -10,10 +10,10 @@ # # # You also need to switch out the baseUrl and listen port of both the frontend and -# backend, but we can do that through env vars when running `yarn dev`: +# backend, but we can do that through env vars when running `yarn start`: # # APP_CONFIG_app_baseUrl=http://localhost APP_CONFIG_app_listen_port=3000 \ -# APP_CONFIG_backend_baseUrl=http://localhost APP_CONFIG_backend_listen_port=7007 yarn dev +# APP_CONFIG_backend_baseUrl=http://localhost APP_CONFIG_backend_listen_port=7007 yarn start # # Once done, you can run the following from the root and then navigate to http://localhost # diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index 9375c14a4a..e8de1118ac 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -93,7 +93,7 @@ To evaluate the catalog and have a greater amount of functionality available, run the entire Backstage example application from the root folder: ```bash -yarn dev +yarn start ``` This will launch both frontend and backend in the same window, populated with diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index fef51626ad..080e8edfc6 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -95,7 +95,7 @@ To setup the DevTools frontend you'll need to do the following steps: ``` -8. Now run `yarn dev` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info) +8. Now run `yarn start` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info) ## Customizing @@ -151,7 +151,7 @@ The DevTools plugin has been designed so that you can customize the tabs to suit + ``` -6. Now run `yarn dev` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies) +6. Now run `yarn start` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies) With this setup you can add or remove the tabs as you'd like or add your own simply by editing your `CustomDevToolsPage.tsx` file @@ -190,7 +190,7 @@ Here's how to add the Catalog Unprocessed Entities tab: ``` -4. Now run `yarn dev` and navigate to the DevTools you'll see a new tab for Unprocessed Entities +4. Now run `yarn start` and navigate to the DevTools you'll see a new tab for Unprocessed Entities ## Permissions diff --git a/plugins/search/README.md b/plugins/search/README.md index 0aab198994..beab698b2e 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -6,7 +6,7 @@ Development is ongoing. You can follow the progress and contribute at the Backst ## Getting started -Run `yarn dev` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin. +Run `yarn start` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin. ### Optional Settings From 64810eea28669a1c67d6e09bfed6f87fd1f1c591 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Apr 2025 14:08:11 +0200 Subject: [PATCH 14/14] cli: tests + fixes for repo start package selection Signed-off-by: Patrik Oldsberg --- .../modules/start/commands/repo/start.test.ts | 221 ++++++++++++++++++ .../src/modules/start/commands/repo/start.ts | 9 +- 2 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 packages/cli/src/modules/start/commands/repo/start.test.ts diff --git a/packages/cli/src/modules/start/commands/repo/start.test.ts b/packages/cli/src/modules/start/commands/repo/start.test.ts new file mode 100644 index 0000000000..2950552209 --- /dev/null +++ b/packages/cli/src/modules/start/commands/repo/start.test.ts @@ -0,0 +1,221 @@ +/* + * 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 { PackageGraph } from '@backstage/cli-node'; +import { findTargetPackages } from './start'; +import { posix } from 'path'; +import { paths } from '../../../../lib/paths'; + +const mocks = { + app: { + packageJson: { + name: 'app', + version: '0', + backstage: { role: 'frontend' }, + }, + dir: '/root/packages/app', + }, + backend: { + packageJson: { + name: 'backend', + version: '0', + backstage: { role: 'backend' }, + }, + dir: '/root/packages/backend', + }, + appNext: { + packageJson: { + name: 'app-next', + version: '0', + backstage: { role: 'frontend' }, + }, + dir: '/root/packages/app-next', + }, + backendNext: { + packageJson: { + name: 'backend-next', + version: '0', + backstage: { role: 'backend' }, + }, + dir: '/root/packages/backend-next', + }, + otherApp: { + packageJson: { + name: 'other-app', + version: '0', + backstage: { role: 'frontend' }, + }, + dir: '/root/packages/other-app', + }, + pluginX: { + packageJson: { + name: 'plugin-x', + version: '0', + backstage: { role: 'frontend-plugin', pluginId: 'x' }, + }, + dir: '/root/plugins/plugin-x', + }, + pluginXBackend: { + packageJson: { + name: 'plugin-x-backend', + version: '0', + backstage: { role: 'backend-plugin', pluginId: 'x' }, + }, + dir: '/root/plugins/plugin-x-backend', + }, + pluginY: { + packageJson: { + name: 'plugin-y', + version: '0', + backstage: { role: 'frontend-plugin', pluginId: 'y' }, + }, + dir: '/root/plugins/plugin-y', + }, + pluginYBackend: { + packageJson: { + name: 'plugin-y-backend', + version: '0', + backstage: { role: 'backend-plugin', pluginId: 'y' }, + }, + dir: '/root/plugins/plugin-y-backend', + }, +} as const; + +describe('findTargetPackages', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...parts: string[]) => { + return posix.resolve('/root', ...parts); + }); + }); + + it('should select default packages', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + const result = await findTargetPackages([], []); + expect(result).toEqual([mocks.app, mocks.backend]); + }); + + it('should select packages by plugin ID', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + const result = await findTargetPackages([], ['x']); + expect(result).toEqual([mocks.pluginX, mocks.pluginXBackend]); + }); + + it('should throw an error if no packages match the plugin ID', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + await expect( + findTargetPackages([], ['nonexistent-plugin']), + ).rejects.toThrow( + "Unable to find any plugin packages with plugin ID 'nonexistent-plugin'. Make sure backstage.pluginId is set in your package.json files by running 'yarn fix --publish'.", + ); + }); + + it('should select packages by explicit names', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + const result = await findTargetPackages(['other-app'], []); + expect(result).toEqual([mocks.otherApp]); + }); + + it('should throw an error if no package matches the explicit name', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + await expect( + findTargetPackages(['nonexistent-package'], []), + ).rejects.toThrow("Unable to find package by name 'nonexistent-package'"); + }); + + it('should select packages by relative path', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + const result = await findTargetPackages( + ['packages/app', 'packages/backend-next'], + [], + ); + expect(result).toEqual([mocks.app, mocks.backendNext]); + }); + + it('should throw an error if no package matches the relative path', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue(Object.values(mocks)); + await expect(findTargetPackages(['nonexistent/path'], [])).rejects.toThrow( + "Unable to find package by name 'nonexistent/path'", + ); + }); + + it('should select a single frontend or backend package if no arguments are provided', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.app]); + const result = await findTargetPackages([], []); + expect(result).toEqual([mocks.app]); + }); + + it('should throw an error if multiple frontend packages other than packages/app are found without explicit selection', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.otherApp, mocks.appNext]); + await expect(findTargetPackages([], [])).rejects.toThrow( + "Found multiple packages with role 'frontend' but none of the use the default path '/root/packages/app',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'.", + ); + }); + + it('should select a single plugin package if no app or backend packages are found', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.pluginX]); + const result = await findTargetPackages([], []); + expect(result).toEqual([mocks.pluginX]); + }); + + it('should select a pair of plugin packages if no app or backend packages are found', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.pluginX, mocks.pluginXBackend]); + const result = await findTargetPackages([], []); + expect(result).toEqual([mocks.pluginX, mocks.pluginXBackend]); + }); + + // Right now we're not validating this because it requires backstage.pluginId to be set, and it's a strange case anyway + it('should select a pair of plugin packages even if they are from different plugins', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.pluginX, mocks.pluginYBackend]); + const result = await findTargetPackages([], []); + expect(result).toEqual([mocks.pluginX, mocks.pluginYBackend]); + }); + + it('should throw an error if multiple plugin packages are found without explicit selection', async () => { + jest + .spyOn(PackageGraph, 'listTargetPackages') + .mockResolvedValue([mocks.pluginX, mocks.pluginY]); + await expect(findTargetPackages([], [])).rejects.toThrow( + "Found multiple packages with role 'frontend-plugin', please choose which packages you want to run by passing the package names explicitly as arguments, for example 'yarn backstage-cli repo start my-plugin my-plugin-backend'.", + ); + }); +}); diff --git a/packages/cli/src/modules/start/commands/repo/start.ts b/packages/cli/src/modules/start/commands/repo/start.ts index b25054bbfc..b0bb8f5c01 100644 --- a/packages/cli/src/modules/start/commands/repo/start.ts +++ b/packages/cli/src/modules/start/commands/repo/start.ts @@ -61,7 +61,10 @@ export async function command(namesOrPaths: string[], options: CommandOptions) { await Promise.all(packageOptions.map(entry => startPackage(entry.options))); } -async function findTargetPackages(namesOrPaths: string[], pluginIds: string[]) { +export async function findTargetPackages( + namesOrPaths: string[], + pluginIds: string[], +) { const targetPackages = new Array(); const packages = await PackageGraph.listTargetPackages(); @@ -75,7 +78,7 @@ async function findTargetPackages(namesOrPaths: string[], pluginIds: string[]) { ACCEPTED_PACKAGE_ROLES.includes(pkg.packageJson.backstage.role) ); }); - if (!matchingPackages) { + if (matchingPackages.length === 0) { 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'.`, ); @@ -145,7 +148,7 @@ async function findTargetPackages(namesOrPaths: string[], pluginIds: string[]) { ); if (matchingPackages.length > 1) { throw new Error( - `Found multiple packages with role '${role}', please choose which packages you want` + + `Found multiple packages with role '${role}', please choose which packages you want ` + `to run by passing the package names explicitly as arguments, for example ` + `'yarn backstage-cli repo start my-plugin my-plugin-backend'.`, );