From ab96bb770f57e8d7e84375047d9a9e1ed49105f4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 24 Sep 2025 09:57:46 +0200 Subject: [PATCH 1/4] feat: add package start entry dir option Signed-off-by: Camila Belo --- .changeset/modern-pugs-appear.md | 21 +++++++++++++++++++ docs/tooling/cli/03-commands.md | 11 +++++----- .../build/commands/package/start/command.ts | 1 + .../commands/package/start/startPackage.ts | 3 ++- packages/cli/src/modules/build/index.ts | 4 ++++ 5 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-pugs-appear.md diff --git a/.changeset/modern-pugs-appear.md b/.changeset/modern-pugs-appear.md new file mode 100644 index 0000000000..c9b9f56e89 --- /dev/null +++ b/.changeset/modern-pugs-appear.md @@ -0,0 +1,21 @@ +--- +'@backstage/cli': patch +--- + +Added a new `--entry-dir` option to the `package start` command, which allows you to specify a custom entry directory 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` +- Running `yarn package start --entry-dir dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` + +This enables you to maintain multiple development environments within a single plugin package. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index d176278655..da8a9650f1 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -177,11 +177,12 @@ Usage: backstage-cli package start [options] Start a package for local development Options: - --config Config files to load instead of app-config.yaml (default: []) - --role 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 Config files to load instead of app-config.yaml (default: []) + --role 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 + --entry-dir Path to the directory containing the entry point file ``` ## package build diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 05fc668356..4a65953f76 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -23,6 +23,7 @@ import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { await startPackage({ role: await findRoleFromCommand(opts), + entryDir: opts.entryDir, targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 10f705ba4d..bac7bee68f 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -20,6 +20,7 @@ import { startFrontend } from './startFrontend'; export async function startPackage(options: { role: PackageRole; + entryDir: string; targetDir: string; configPaths: string[]; checksEnabled: boolean; @@ -45,8 +46,8 @@ export async function startPackage(options: { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ - entry: 'dev/index', ...options, + entry: `${options.entryDir ?? 'dev'}/index`, }); case 'frontend-dynamic-container' as PackageRole: // experimental return startFrontend({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index fd31a6c782..552a35a316 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -137,6 +137,10 @@ export const buildPlugin = createCliPlugin({ '--link ', 'Link an external workspace for module resolution', ) + .option( + '--entry-dir ', + 'Path to the directory containing the entry point file', + ) .action(lazy(() => import('./commands/package/start'), 'command')); await defaultCommand.parseAsync(args, { from: 'user' }); From 8a16781eba4515328e5ec792b48bda4b25fd5408 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 24 Sep 2025 13:58:35 +0200 Subject: [PATCH 2/4] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/modern-pugs-appear.md | 8 +- docs/tooling/cli/03-commands.md | 12 +-- packages/cli/cli-report.md | 1 + .../build/commands/package/start/command.ts | 2 +- .../package/start/startPackage.test.ts | 89 +++++++++++++++++++ .../commands/package/start/startPackage.ts | 20 ++++- packages/cli/src/modules/build/index.ts | 4 +- 7 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/modules/build/commands/package/start/startPackage.test.ts diff --git a/.changeset/modern-pugs-appear.md b/.changeset/modern-pugs-appear.md index c9b9f56e89..8b502fb52d 100644 --- a/.changeset/modern-pugs-appear.md +++ b/.changeset/modern-pugs-appear.md @@ -2,7 +2,7 @@ '@backstage/cli': patch --- -Added a new `--entry-dir` option to the `package start` command, which allows you to specify a custom entry directory for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). +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:** @@ -15,7 +15,5 @@ dev/ index.ts ``` -- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` -- Running `yarn package start --entry-dir dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` - -This enables you to maintain multiple development environments within a single plugin package. +- 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. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index da8a9650f1..ce8f862c98 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -177,12 +177,12 @@ Usage: backstage-cli package start [options] Start a package for local development Options: - --config Config files to load instead of app-config.yaml (default: []) - --role 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 - --entry-dir Path to the directory containing the entry point file + --config Config files to load instead of app-config.yaml (default: []) + --role 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 Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev" ``` ## package build diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index e2f5bb9cc6..a8df5242ad 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -312,6 +312,7 @@ Usage: program [options] Options: --check --config + --entrypoint --inspect [host] --inspect-brk [host] --link diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 4a65953f76..b38957f488 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -23,7 +23,7 @@ import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { await startPackage({ role: await findRoleFromCommand(opts), - entryDir: opts.entryDir, + entrypoint: opts.entrypoint, targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts new file mode 100644 index 0000000000..b2aa172867 --- /dev/null +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts @@ -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({ + 'file.backup.old.ts': 'export const data = {};', + }); + + const result = resolveEntryPath('file.backup.old.ts', mockDir.path); + + expect(result).toBe('file.backup.old'); + }); + + 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'); + }); +}); diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index bac7bee68f..368a28b5df 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -17,10 +17,26 @@ import { PackageRole } from '@backstage/cli-node'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; +import { statSync, readdirSync } from 'fs'; +import { resolve, join, parse } from 'path'; + +export function resolveEntryPath( + entrypoint: string = 'dev', + targetDir: string, +): string { + const { dir: entryDir, name: entryName } = parse(entrypoint); + const entryPath = join(entryDir, entryName); + const parentPath = resolve(targetDir, entryDir); + const isFile = readdirSync(parentPath).some(base => { + const path = resolve(parentPath, base); + return statSync(path).isFile(); + }); + return isFile ? entryPath : join(entryPath, 'index'); +} export async function startPackage(options: { role: PackageRole; - entryDir: string; + entrypoint?: string; targetDir: string; configPaths: string[]; checksEnabled: boolean; @@ -47,7 +63,7 @@ export async function startPackage(options: { case 'frontend-plugin-module': return startFrontend({ ...options, - entry: `${options.entryDir ?? 'dev'}/index`, + entry: resolveEntryPath(options.entrypoint, options.targetDir), }); case 'frontend-dynamic-container' as PackageRole: // experimental return startFrontend({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 552a35a316..88706046ee 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -138,8 +138,8 @@ export const buildPlugin = createCliPlugin({ 'Link an external workspace for module resolution', ) .option( - '--entry-dir ', - 'Path to the directory containing the entry point file', + '--entrypoint ', + 'Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev"', ) .action(lazy(() => import('./commands/package/start'), 'command')); From 941613854c8db61db102afc4a572f84368be897b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Sep 2025 09:39:20 +0200 Subject: [PATCH 3/4] refactor: rephrase the flag description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Camila Belo --- packages/cli/src/modules/build/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 88706046ee..5382adfec9 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -139,7 +139,7 @@ export const buildPlugin = createCliPlugin({ ) .option( '--entrypoint ', - 'Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev"', + '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')); From d070bda0d4409d143d7bb41eefef43be178905ad Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Sep 2025 10:55:50 +0200 Subject: [PATCH 4/4] refactor: simplify the resolve entry path logic Signed-off-by: Camila Belo --- .../commands/package/start/startPackage.test.ts | 6 +++--- .../build/commands/package/start/startPackage.ts | 16 +++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts index b2aa172867..ca153adb22 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts @@ -46,12 +46,12 @@ describe('resolveEntryPath', () => { it('should handle multiple dots in filename', () => { mockDir.setContent({ - 'file.backup.old.ts': 'export const data = {};', + 'index.alpha.ts': 'export const data = {};', }); - const result = resolveEntryPath('file.backup.old.ts', mockDir.path); + const result = resolveEntryPath('index.alpha.ts', mockDir.path); - expect(result).toBe('file.backup.old'); + expect(result).toBe('index.alpha'); }); it('should handle simple directory names', () => { diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 368a28b5df..b49fd482e5 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -17,21 +17,19 @@ import { PackageRole } from '@backstage/cli-node'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; -import { statSync, readdirSync } from 'fs'; -import { resolve, join, parse } from 'path'; +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 entryPath = join(entryDir, entryName); - const parentPath = resolve(targetDir, entryDir); - const isFile = readdirSync(parentPath).some(base => { - const path = resolve(parentPath, base); - return statSync(path).isFile(); - }); - return isFile ? entryPath : join(entryPath, 'index'); + const [entryFile] = glob.sync(`${resolve(targetDir, entryDir, entryName)}.*`); + if (entryFile) { + return join(entryDir, entryName); + } + return join(entryDir, entryName, 'index'); } export async function startPackage(options: {