diff --git a/.changeset/six-camels-perform.md b/.changeset/six-camels-perform.md new file mode 100644 index 0000000000..b77c46b800 --- /dev/null +++ b/.changeset/six-camels-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Enable specifying an external application template when using the `create-app` CLI command. diff --git a/packages/create-app/cli-report.md b/packages/create-app/cli-report.md index 1244432c6a..20aa695993 100644 --- a/packages/create-app/cli-report.md +++ b/packages/create-app/cli-report.md @@ -11,5 +11,6 @@ Options: -V, --version --path [directory] --skip-install + --template-path [directory] -h, --help ``` diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index f455efa6e6..47c62520e0 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -20,6 +20,8 @@ import path from 'path'; import { Command } from 'commander'; import * as tasks from './lib/tasks'; import createApp from './createApp'; +import { findPaths } from '@backstage/cli-common'; +import { tmpdir } from 'os'; jest.mock('./lib/tasks'); @@ -74,6 +76,18 @@ describe('command entrypoint', () => { expect(createTemporaryAppFolderMock).toHaveBeenCalled(); expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); + expect(templatingMock.mock.lastCall?.[0]).toEqual( + findPaths(__dirname).resolveTarget( + 'packages', + 'create-app', + 'src', + 'templates', + 'default-app', + ), + ); + expect(templatingMock.mock.lastCall?.[1]).toEqual( + path.join(tmpdir(), 'MyApp'), + ); expect(moveAppMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); }); @@ -84,6 +98,48 @@ describe('command entrypoint', () => { expect(checkPathExistsMock).toHaveBeenCalled(); expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); + expect(templatingMock.mock.lastCall?.[0]).toEqual( + findPaths(__dirname).resolveTarget( + 'packages', + 'create-app', + 'src', + 'templates', + 'default-app', + ), + ); + expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); + expect(buildAppMock).toHaveBeenCalled(); + }); + + it('should call expected tasks with relative --template-path option', async () => { + const cmd = { + path: 'myDirectory', + templatePath: 'templateDirectory', + } as unknown as Command; + await createApp(cmd); + expect(checkPathExistsMock).toHaveBeenCalled(); + expect(tryInitGitRepositoryMock).toHaveBeenCalled(); + expect(templatingMock).toHaveBeenCalled(); + expect(templatingMock.mock.lastCall?.[0]).toEqual( + findPaths(__dirname).resolveTarget('templateDirectory'), + ); + expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); + expect(buildAppMock).toHaveBeenCalled(); + }); + + it('should call expected tasks with absolute --template-path option', async () => { + const cmd = { + path: 'myDirectory', + templatePath: path.resolve('somewhere', 'templateDirectory'), + } as unknown as Command; + await createApp(cmd); + expect(checkPathExistsMock).toHaveBeenCalled(); + expect(tryInitGitRepositoryMock).toHaveBeenCalled(); + expect(templatingMock).toHaveBeenCalled(); + expect(templatingMock.mock.lastCall?.[0]).toEqual( + path.resolve('somewhere', 'templateDirectory'), + ); + expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); expect(buildAppMock).toHaveBeenCalled(); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 87fad319be..50facb714d 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -65,7 +65,9 @@ export default async (opts: OptionValues): Promise => { }, ]); - const templateDir = paths.resolveOwn('templates/default-app'); + const templateDir = opts.templatePath + ? resolvePath(paths.targetDir, opts.templatePath) + : paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); // Use `--path` argument as application directory when specified, otherwise @@ -132,11 +134,15 @@ export default async (opts: OptionValues): Promise => { if (!opts.skipInstall) { Task.log( ` Install the dependencies: ${chalk.cyan( - `cd ${answers.name} && yarn install`, + `cd ${opts.path ?? answers.name} && yarn install`, )}`, ); } - Task.log(` Run the app: ${chalk.cyan(`cd ${answers.name} && yarn dev`)}`); + Task.log( + ` Run the app: ${chalk.cyan( + `cd ${opts.path ?? answers.name} && yarn dev`, + )}`, + ); Task.log( ' Set up the software catalog: https://backstage.io/docs/features/software-catalog/configuration', ); diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index fa235e6175..d9b1226848 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -39,6 +39,10 @@ const main = (argv: string[]) => { '--skip-install', 'Skip the install and builds steps after creating the app', ) + .option( + '--template-path [directory]', + 'Use an external application template instead of the default template', + ) .action(cmd => createApp(cmd)); program.parse(argv);