cli: refactor and fully implement starting packages via repo start

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-04-05 13:44:12 +02:00
parent efd6a048d5
commit 944af983d9
10 changed files with 154 additions and 68 deletions
+1 -1
View File
@@ -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);
}
}
@@ -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,
@@ -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;
@@ -49,6 +49,7 @@ export type BundlingOptions = {
};
export type ServeOptions = BundlingPathsOptions & {
targetDir?: string;
checksEnabled: boolean;
configPaths: string[];
verifyVersions?: boolean;
@@ -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<void> {
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}'`,
);
}
});
}
@@ -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<string | undefined> {
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;
}
@@ -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,
@@ -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,
@@ -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<void> {
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}'`,
);
}
}
@@ -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<PackageRole | undefined> = [
'frontend',
@@ -31,12 +33,29 @@ const ACCEPTED_PACKAGE_ROLES: Array<PackageRole | undefined> = [
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[]) {