Merge pull request #29489 from backstage/rugvip/start

cli: add new 'repo start' command
This commit is contained in:
Patrik Oldsberg
2025-04-08 15:52:07 +02:00
committed by GitHub
45 changed files with 1061 additions and 78 deletions
+7
View File
@@ -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`.
+16
View File
@@ -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\""
}
}
```
+30
View File
@@ -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
View File
@@ -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).
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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!
+2 -2
View File
@@ -116,11 +116,11 @@ If this fails on the `yarn install` step, it's likely that you will need to inst
## 2. Run the Backstage app
Your Backstage app is fully installed and ready to be run! Now that the installation is complete, you can go to the application directory and start the app using the `yarn dev` command. The `yarn dev` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window.
Your Backstage app is fully installed and ready to be run! Now that the installation is complete, you can go to the application directory and start the app using the `yarn start` command. The `yarn start` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window.
```bash
cd my-backstage-app # your app name
yarn dev
yarn start
```
![Screenshot of the command output, with the message web pack compiled successfully](../assets/getting-started/startup.png)
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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 };`
+1 -1
View File
@@ -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
+33 -6
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+16
View File
@@ -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`
```
+7 -22
View File
@@ -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);
+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);
}
}
+5 -2
View File
@@ -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) => {
+99
View File
@@ -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,
},
},
];
});
}
+69
View File
@@ -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.
+1 -1
View File
@@ -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
#
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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