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[]) {