Merge pull request #29489 from backstage/rugvip/start
cli: add new 'repo start' command
This commit is contained in:
@@ -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`.
|
||||
@@ -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\""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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 <pluginId>` 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.
|
||||
+1
-1
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 };`
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,31 @@ 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] [packageNameOrPath...]
|
||||
|
||||
Starts packages in the repo for local development
|
||||
|
||||
Arguments:
|
||||
packageNameOrPath Run the specified packages instead of the defaults.
|
||||
|
||||
Options:
|
||||
--plugin <pluginId> Start the dev entry-point for any matching plugin package in the repo (default: [])
|
||||
--config <path> 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 <path...> Add a --require argument to the node process. Applies to backend package only
|
||||
--link <path> Link an external workspace for module resolution
|
||||
```
|
||||
|
||||
## repo build
|
||||
|
||||
Builds all packages in the project, excluding bundled packages by default, i.e. ones
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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! 👍
|
||||
|
||||
+6
-7
@@ -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",
|
||||
|
||||
@@ -406,6 +406,7 @@ Commands:
|
||||
help [command]
|
||||
lint [options]
|
||||
list-deprecations [options]
|
||||
start [options] [packageNameOrPath...]
|
||||
test [options]
|
||||
```
|
||||
|
||||
@@ -467,6 +468,21 @@ Options:
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli repo start`
|
||||
|
||||
```
|
||||
Usage: backstage-cli repo start [options] [packageNameOrPath...]
|
||||
|
||||
Options:
|
||||
--config <path>
|
||||
--inspect [host]
|
||||
--inspect-brk [host]
|
||||
--link <path>
|
||||
--plugin <pluginId>
|
||||
--require <path...>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli repo test`
|
||||
|
||||
```
|
||||
|
||||
@@ -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 <name>', '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 <path...>',
|
||||
'Add a --require argument to the node process',
|
||||
)
|
||||
.option('--link <path>', 'Link an external workspace for module resolution')
|
||||
.action(lazy(() => import('./start'), 'command'));
|
||||
|
||||
registerPackageStartCommands(command);
|
||||
registerPackageBuildCommands(command);
|
||||
registerPackageTestCommands(command);
|
||||
registerMaintenancePackageCommands(command);
|
||||
|
||||
@@ -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,12 +32,14 @@ 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 */
|
||||
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 */
|
||||
@@ -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;
|
||||
|
||||
@@ -74,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) {
|
||||
|
||||
@@ -49,6 +49,7 @@ export type BundlingOptions = {
|
||||
};
|
||||
|
||||
export type ServeOptions = BundlingPathsOptions & {
|
||||
targetDir?: string;
|
||||
checksEnabled: boolean;
|
||||
configPaths: string[];
|
||||
verifyVersions?: boolean;
|
||||
|
||||
@@ -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<AppConfig[]>((resolve, reject) => {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 startPlugin = createCliPlugin({
|
||||
pluginId: 'start',
|
||||
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 <name>',
|
||||
'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 <path...>',
|
||||
'Add a --require argument to the node process',
|
||||
)
|
||||
.option(
|
||||
'--link <path>',
|
||||
'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 ({ args }) => {
|
||||
const command = new Command();
|
||||
|
||||
const defaultCommand = command
|
||||
.argument(
|
||||
'[...packageNameOrPath]',
|
||||
'Run the specified package instead of the defaults.',
|
||||
)
|
||||
.option(
|
||||
'--plugin <pluginId>',
|
||||
'Start the dev entry-point for any matching plugin package in the repo',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
)
|
||||
.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 <path...>',
|
||||
'Add a --require argument to the node process. Applies to backend package only',
|
||||
)
|
||||
.option(
|
||||
'--link <path>',
|
||||
'Link an external workspace for module resolution',
|
||||
)
|
||||
.action(lazy(() => import('./commands/repo/start'), 'command'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default startPlugin;
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { OptionValues } from 'commander';
|
||||
import { startPackage } from './startPackage';
|
||||
import { resolveLinkedWorkspace } from './resolveLinkedWorkspace';
|
||||
import { findRoleFromCommand } from '../../../../../lib/role';
|
||||
import { paths } from '../../../../../lib/paths';
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
await startPackage({
|
||||
role: await findRoleFromCommand(opts),
|
||||
targetDir: paths.targetDir,
|
||||
configPaths: opts.config as string[],
|
||||
checksEnabled: Boolean(opts.check),
|
||||
linkedWorkspace: await resolveLinkedWorkspace(opts.link),
|
||||
inspectEnabled: opts.inspect,
|
||||
inspectBrkEnabled: opts.inspectBrk,
|
||||
require: opts.require,
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 { resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../../../../lib/paths';
|
||||
import { runBackend } from '../../../../../lib/runner';
|
||||
|
||||
interface StartBackendOptions {
|
||||
targetDir: string;
|
||||
checksEnabled: boolean;
|
||||
inspectEnabled?: boolean | string;
|
||||
inspectBrkEnabled?: boolean | string;
|
||||
linkedWorkspace?: string;
|
||||
require?: string;
|
||||
}
|
||||
|
||||
export async function startBackend(options: StartBackendOptions) {
|
||||
const waitForExit = await runBackend({
|
||||
targetDir: options.targetDir,
|
||||
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(
|
||||
resolvePath(options.targetDir ?? paths.targetDir, '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({
|
||||
targetDir: options.targetDir,
|
||||
entry: 'dev/index',
|
||||
inspectEnabled: options.inspectEnabled,
|
||||
inspectBrkEnabled: options.inspectBrkEnabled,
|
||||
require: options.require,
|
||||
linkedWorkspace: options.linkedWorkspace,
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
targetDir?: string;
|
||||
|
||||
checksEnabled: boolean;
|
||||
configPaths: string[];
|
||||
skipOpenBrowser?: boolean;
|
||||
isModuleFederationRemote?: boolean;
|
||||
linkedWorkspace?: string;
|
||||
}
|
||||
|
||||
export async function startFrontend(options: StartAppOptions) {
|
||||
const packageJson = (await readJson(
|
||||
resolvePath(options.targetDir ?? paths.targetDir, 'package.json'),
|
||||
)) as BackstagePackageJson;
|
||||
|
||||
const waitForExit = await serveBundle({
|
||||
entry: options.entry,
|
||||
targetDir: options.targetDir,
|
||||
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();
|
||||
}
|
||||
@@ -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 | string;
|
||||
inspectBrkEnabled?: boolean | string;
|
||||
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}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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';
|
||||
import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace';
|
||||
import { startPackage } from '../package/start/startPackage';
|
||||
import { parseArgs } from 'util';
|
||||
|
||||
const ACCEPTED_PACKAGE_ROLES: Array<PackageRole | undefined> = [
|
||||
'frontend',
|
||||
'backend',
|
||||
'frontend-plugin',
|
||||
'backend-plugin',
|
||||
];
|
||||
|
||||
type CommandOptions = {
|
||||
plugin: string[];
|
||||
config: string[];
|
||||
inspect?: boolean | string;
|
||||
inspectBrk?: boolean | string;
|
||||
require?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export async function command(namesOrPaths: string[], options: CommandOptions) {
|
||||
const targetPackages = await findTargetPackages(namesOrPaths, options.plugin);
|
||||
|
||||
const packageOptions = await resolvePackageOptions(targetPackages, options);
|
||||
|
||||
if (packageOptions.length === 0) {
|
||||
console.log('No packages found to start');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Starting ${packageOptions
|
||||
.map(({ pkg }) => pkg.packageJson.name)
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
// Each of these block until interrupted by user
|
||||
await Promise.all(packageOptions.map(entry => startPackage(entry.options)));
|
||||
}
|
||||
|
||||
export async function findTargetPackages(
|
||||
namesOrPaths: string[],
|
||||
pluginIds: string[],
|
||||
) {
|
||||
const targetPackages = new Array<BackstagePackage>();
|
||||
|
||||
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.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'.`,
|
||||
);
|
||||
}
|
||||
targetPackages.push(...matchingPackages);
|
||||
}
|
||||
if (targetPackages.length > 0) {
|
||||
return targetPackages;
|
||||
}
|
||||
|
||||
// Next check if explicit package names are provided, use them in that case.
|
||||
for (const nameOrPath of namesOrPaths) {
|
||||
let matchingPackage = packages.find(
|
||||
pkg => nameOrPath === pkg.packageJson.name,
|
||||
);
|
||||
if (!matchingPackage) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (targetPackages.length > 0) {
|
||||
return targetPackages;
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
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) {
|
||||
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(
|
||||
targetPackages: BackstagePackage[],
|
||||
options: CommandOptions,
|
||||
) {
|
||||
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: options.inspect,
|
||||
inspectBrkEnabled: options.inspectBrk,
|
||||
require: options.require ?? parsedRequire,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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')
|
||||
.argument(
|
||||
'[packageNameOrPath...]',
|
||||
'Run the specified package instead of the defaults.',
|
||||
)
|
||||
.option(
|
||||
'--plugin <pluginId>',
|
||||
'Start the dev entry-point for any matching plugin package in the repo',
|
||||
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
|
||||
Array<string>(),
|
||||
)
|
||||
.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 <path...>',
|
||||
'Add a --require argument to the node process. Applies to backend package only',
|
||||
)
|
||||
.option('--link <path>', 'Link an external workspace for module resolution')
|
||||
.action(lazy(() => import('./commands/repo/start'), 'command'));
|
||||
}
|
||||
|
||||
export function registerPackageCommands(command: Command) {
|
||||
command
|
||||
.command('start')
|
||||
.description('Start a package for local development')
|
||||
.option(...configOption)
|
||||
.option('--role <name>', '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 <path...>',
|
||||
'Add a --require argument to the node process',
|
||||
)
|
||||
.option('--link <path>', 'Link an external workspace for module resolution')
|
||||
.action(lazy(() => import('./commands/package/start'), 'command'));
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -155,7 +155,7 @@ export default async (opts: OptionValues): Promise<void> => {
|
||||
}
|
||||
Task.log(
|
||||
` Run the app: ${chalk.cyan(
|
||||
`cd ${opts.path ?? answers.name} && yarn dev`,
|
||||
`cd ${opts.path ?? answers.name} && yarn start`,
|
||||
)}`,
|
||||
);
|
||||
Task.log(
|
||||
|
||||
@@ -6,5 +6,5 @@ To start the app, run:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn dev
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineConfig({
|
||||
? []
|
||||
: [
|
||||
{
|
||||
command: 'yarn dev',
|
||||
command: 'yarn start',
|
||||
port: 3000,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60_000,
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
# <ProxiedSignInPage {...props} provider="myproxy" />
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,7 +95,7 @@ To setup the DevTools frontend you'll need to do the following steps:
|
||||
<SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
|
||||
```
|
||||
|
||||
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
|
||||
+ </Route>
|
||||
```
|
||||
|
||||
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:
|
||||
</DevToolsLayout>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user