cli: refactor and fully implement starting packages via repo start
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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[]) {
|
||||
|
||||
Reference in New Issue
Block a user