Merge pull request #31255 from backstage/cli-package-start-entry-dir

[CLI] Package Start Entrypoint Option
This commit is contained in:
Camila Belo
2025-09-26 14:02:54 +02:00
committed by GitHub
7 changed files with 136 additions and 6 deletions
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/cli': patch
---
Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha).
**Example usage:**
Consider the following plugin dev folder structure:
```
dev/
index.tsx
alpha/
index.ts
```
- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file;
- Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file.
+6 -5
View File
@@ -177,11 +177,12 @@ Usage: backstage-cli package start [options]
Start a package for local development
Options:
--config <path> Config files to load instead of app-config.yaml (default: [])
--role <name> Run the command with an explicit package role
--check Enable type checking and linting if available
--inspect Enable debugger in Node.js environments
--inspect-brk Enable debugger in Node.js environments, breaking before code starts
--config <path> Config files to load instead of app-config.yaml (default: [])
--role <name> Run the command with an explicit package role
--check Enable type checking and linting if available
--inspect Enable debugger in Node.js environments
--inspect-brk Enable debugger in Node.js environments, breaking before code starts
--entrypoint <path> Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev"
```
## package build
+1
View File
@@ -312,6 +312,7 @@ Usage: program [options]
Options:
--check
--config <path>
--entrypoint <path>
--inspect [host]
--inspect-brk [host]
--link <path>
@@ -23,6 +23,7 @@ import { paths } from '../../../../../lib/paths';
export async function command(opts: OptionValues): Promise<void> {
await startPackage({
role: await findRoleFromCommand(opts),
entrypoint: opts.entrypoint,
targetDir: paths.targetDir,
configPaths: opts.config as string[],
checksEnabled: Boolean(opts.check),
@@ -0,0 +1,89 @@
/*
* 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 { createMockDirectory } from '@backstage/backend-test-utils';
import { resolveEntryPath } from './startPackage';
describe('resolveEntryPath', () => {
const mockDir = createMockDirectory();
afterEach(() => {
mockDir.clear();
});
it('should remove file extensions', () => {
mockDir.setContent({
'dev/custom.tsx': '// dev app code',
});
const result = resolveEntryPath('dev/custom.tsx', mockDir.path);
expect(result).toBe('dev/custom');
});
it('should remove trailing slashes', () => {
mockDir.setContent({
'dev/alpha.ts': '// dev app code',
});
const result = resolveEntryPath('dev/alpha/', mockDir.path);
expect(result).toBe('dev/alpha');
});
it('should handle multiple dots in filename', () => {
mockDir.setContent({
'index.alpha.ts': 'export const data = {};',
});
const result = resolveEntryPath('index.alpha.ts', mockDir.path);
expect(result).toBe('index.alpha');
});
it('should handle simple directory names', () => {
mockDir.setContent({
'dev/index.ts': '// dev app code',
});
const result = resolveEntryPath('dev', mockDir.path);
expect(result).toBe('dev/index');
});
it('should handle nested directory paths', () => {
mockDir.setContent({
'dev/alpha/index.ts': '// dev app code',
});
const result = resolveEntryPath('dev/alpha', mockDir.path);
expect(result).toBe('dev/alpha/index');
});
it('should return the file when there is a directory with the same name', () => {
mockDir.setContent({
'dev/alpha.ts': '// dev app code',
'dev/app-config.yaml': '// dev app config',
'dev/alpha/index.ts': '// dev app code',
'dev/alpha/app-config.yaml': '// dev app config',
});
const result = resolveEntryPath('dev/alpha', mockDir.path);
expect(result).toBe('dev/alpha');
});
});
@@ -17,9 +17,24 @@
import { PackageRole } from '@backstage/cli-node';
import { startBackend, startBackendPlugin } from './startBackend';
import { startFrontend } from './startFrontend';
import { parse, resolve, join } from 'path';
import { glob } from 'glob';
export function resolveEntryPath(
entrypoint: string = 'dev',
targetDir: string,
): string {
const { dir: entryDir, name: entryName } = parse(entrypoint);
const [entryFile] = glob.sync(`${resolve(targetDir, entryDir, entryName)}.*`);
if (entryFile) {
return join(entryDir, entryName);
}
return join(entryDir, entryName, 'index');
}
export async function startPackage(options: {
role: PackageRole;
entrypoint?: string;
targetDir: string;
configPaths: string[];
checksEnabled: boolean;
@@ -45,8 +60,8 @@ export async function startPackage(options: {
case 'frontend-plugin':
case 'frontend-plugin-module':
return startFrontend({
entry: 'dev/index',
...options,
entry: resolveEntryPath(options.entrypoint, options.targetDir),
});
case 'frontend-dynamic-container' as PackageRole: // experimental
return startFrontend({
+4
View File
@@ -137,6 +137,10 @@ export const buildPlugin = createCliPlugin({
'--link <path>',
'Link an external workspace for module resolution',
)
.option(
'--entrypoint <path>',
'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"',
)
.action(lazy(() => import('./commands/package/start'), 'command'));
await defaultCommand.parseAsync(args, { from: 'user' });