From 4f1c30c1766263e7dd1276445933db00464771b0 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Mon, 4 Oct 2021 15:36:50 -0400 Subject: [PATCH 01/11] Support optional [path] argument in create-app command-line utility When this is specified, then copy the newly created application to the specified directory, otherwise, copy the application to a folder based on the name specified name. Signed-off-by: Colton Padden --- .changeset/neat-rings-protect.md | 5 +++++ packages/create-app/src/createApp.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 .changeset/neat-rings-protect.md diff --git a/.changeset/neat-rings-protect.md b/.changeset/neat-rings-protect.md new file mode 100644 index 0000000000..16b0a5fe82 --- /dev/null +++ b/.changeset/neat-rings-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Support optional path argument when generating a project using `create-app` diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 1da3139f41..0c1cd09d5b 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -77,7 +77,7 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch(error => { + await fs.move(tempDir, destination, { overwrite: true }).catch(error => { throw new Error( `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); @@ -119,14 +119,24 @@ export default async (cmd: Command): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - const appDir = resolvePath(paths.targetDir, answers.name); + + // Use `[path]` argument as applicaiton directory when specified, otherwise + // create a directory using `answers.name` + const appDir = + cmd.args.length > 0 + ? resolvePath(cmd.args[0]) + : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); try { - Task.section('Checking if the directory is available'); - await checkExists(paths.targetDir, answers.name); + // Directory must not already exist when using consructed path from + // `answers.name` + if (cmd.args.length === 0) { + Task.section('Checking if the directory is available'); + await checkExists(paths.targetDir, answers.name); + } Task.section('Creating a temporary app directory'); await createTemporaryAppFolder(tempDir); From 448b3ed9fadcb80fefdb7cca5f4d3f26c07471d1 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 09:36:58 -0400 Subject: [PATCH 02/11] modify changeset from patch to minor as we are in a 0.x release Signed-off-by: Colton Padden --- .changeset/neat-rings-protect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/neat-rings-protect.md b/.changeset/neat-rings-protect.md index 16b0a5fe82..9242d4ce6c 100644 --- a/.changeset/neat-rings-protect.md +++ b/.changeset/neat-rings-protect.md @@ -1,5 +1,5 @@ --- -'@backstage/create-app': minor +'@backstage/create-app': patch --- Support optional path argument when generating a project using `create-app` From fe48f0ea9563687b884f38dfb2fc38ef10bb4771 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 09:41:38 -0400 Subject: [PATCH 03/11] update create-app usage to include optional [path] argument Signed-off-by: Colton Padden --- packages/create-app/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 98a3cee908..c603592207 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -26,10 +26,11 @@ import { version } from '../package.json'; import createApp from './createApp'; const main = (argv: string[]) => { - program.name('backstage-create-app').version(version); - program - .description('Creates a new app in a new directory') + .name('backstage-create-app') + .version(version) + .description('Creates a new app in a new directory or specified [path]') + .usage('[path] [options]') .option( '--skip-install', 'Skip the install and builds steps after creating the app', From 973678fd8cda95f932dc04c7abc075486c86f769 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 11:20:35 -0400 Subject: [PATCH 04/11] use --path option instead of [path] argument in create-app Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 11 +++++------ packages/create-app/src/index.ts | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 0c1cd09d5b..4f0c141d86 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -120,12 +120,11 @@ export default async (cmd: Command): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - // Use `[path]` argument as applicaiton directory when specified, otherwise + // Use `--path` argument as applicaiton directory when specified, otherwise // create a directory using `answers.name` - const appDir = - cmd.args.length > 0 - ? resolvePath(cmd.args[0]) - : resolvePath(paths.targetDir, answers.name); + const appDir = cmd.path + ? resolvePath(cmd.path) + : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); @@ -133,7 +132,7 @@ export default async (cmd: Command): Promise => { try { // Directory must not already exist when using consructed path from // `answers.name` - if (cmd.args.length === 0) { + if (!cmd.path) { Task.section('Checking if the directory is available'); await checkExists(paths.targetDir, answers.name); } diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index c603592207..1f37be56d2 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -29,8 +29,11 @@ const main = (argv: string[]) => { program .name('backstage-create-app') .version(version) - .description('Creates a new app in a new directory or specified [path]') - .usage('[path] [options]') + .description('Creates a new app in a new directory or specified path') + .option( + '--path [directory]', + 'Location to store the app defaulting to a new folder with the app name', + ) .option( '--skip-install', 'Skip the install and builds steps after creating the app', From 1d7078df932415a798fe70d851e34de040711a68 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Tue, 5 Oct 2021 16:10:07 -0400 Subject: [PATCH 05/11] validate path directory, and prevent target from being overwritten Validation now occurs to ensure that the target path exists and is a directory. Additionally, instead of moving the temporary directory to the target path, files are now recursively copied, and then cleaned up. This was to support app creation in the current directory, without clobbering existing files (for example .git/) Instead of overwriting the target `path` directory while using the move method, Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 49 ++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 4f0c141d86..732e2e84d9 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -24,10 +24,11 @@ import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; +import recursive from 'recursive-readdir'; const exec = promisify(execCb); -async function checkExists(rootDir: string, name: string) { +async function checkAppPathDoesntExist(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { const destination = resolvePath(rootDir, name); @@ -40,6 +41,22 @@ async function checkExists(rootDir: string, name: string) { }); } +async function checkPathExistsAndIsDirectory(path: string) { + await Task.forItem('checking', path, async () => { + if (await fs.pathExists(path)) { + if (!fs.lstatSync(path).isDirectory()) { + throw new Error( + 'Directory specified with --path argument is a file\nPlease ensure target path is a directory', + ); + } + } else { + throw new Error( + 'Directory specified with --path argument does not exist\nPlease try again with a different path', + ); + } + }); +} + async function createTemporaryAppFolder(tempDir: string) { await Task.forItem('creating', 'temporary directory', async () => { try { @@ -77,11 +94,18 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination, { overwrite: true }).catch(error => { - throw new Error( - `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, - ); + const tempFiles = await recursive(tempDir).catch(error => { + throw new Error(`Failed to read temporary directory: ${error.message}`); }); + + for (const tempFile of tempFiles) { + const destFile = tempFile.replace(tempDir, destination); + await fs.copy(tempFile, destFile).catch(error => { + throw new Error( + `Failed to move file from ${tempFile} to ${destFile}: ${error.message}`, + ); + }); + } }); } @@ -130,11 +154,13 @@ export default async (cmd: Command): Promise => { Task.log('Creating the app...'); try { - // Directory must not already exist when using consructed path from - // `answers.name` - if (!cmd.path) { - Task.section('Checking if the directory is available'); - await checkExists(paths.targetDir, answers.name); + Task.section('Checking if the directory is available'); + if (cmd.path) { + await checkPathExistsAndIsDirectory(appDir); + } else { + // Directory must not already exist when using consructed path from + // `answers.name` + await checkAppPathDoesntExist(paths.targetDir, answers.name); } Task.section('Creating a temporary app directory'); @@ -146,6 +172,9 @@ export default async (cmd: Command): Promise => { Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); + Task.section('Cleanup'); + await cleanUp(tempDir); + if (!cmd.skipInstall) { Task.section('Building the app'); await buildApp(appDir); From 6c93f322ca38af6113854e150eac137d2b80c6d5 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 7 Oct 2021 11:00:40 -0400 Subject: [PATCH 06/11] use resolvePath with targetDir when using cmd.path Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 732e2e84d9..bc33fd91ae 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -147,7 +147,7 @@ export default async (cmd: Command): Promise => { // Use `--path` argument as applicaiton directory when specified, otherwise // create a directory using `answers.name` const appDir = cmd.path - ? resolvePath(cmd.path) + ? resolvePath(paths.targetDir, cmd.path) : resolvePath(paths.targetDir, answers.name); Task.log(); From c80e7ffbf93a7d4f3dcf7a617556f25a0d1e4e62 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 7 Oct 2021 12:34:12 -0400 Subject: [PATCH 07/11] use relative temporary file paths in resolving app destination paths Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index bc33fd91ae..68aad6e2f4 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -20,7 +20,7 @@ import chalk from 'chalk'; import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, relative as relativePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; @@ -94,12 +94,22 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - const tempFiles = await recursive(tempDir).catch(error => { - throw new Error(`Failed to read temporary directory: ${error.message}`); - }); + // Get the temporary files relative path in relation to the `tempDir` so + // that it can be used in resolving destination file path + const relativeTempFiles = await recursive(tempDir) + .then(files => { + return files.map(file => { + return relativePath(tempDir, file); + }); + }) + .catch(error => { + throw new Error(`Failed to read temporary directory: ${error.message}`); + }); + + for (const relativeTempFile of relativeTempFiles) { + const tempFile = resolvePath(tempDir, relativeTempFile); + const destFile = resolvePath(destination, relativeTempFile); - for (const tempFile of tempFiles) { - const destFile = tempFile.replace(tempDir, destination); await fs.copy(tempFile, destFile).catch(error => { throw new Error( `Failed to move file from ${tempFile} to ${destFile}: ${error.message}`, From 10f513dd8c2bb154fadb0d552828a480e89f7a1c Mon Sep 17 00:00:00 2001 From: colton Date: Mon, 11 Oct 2021 13:48:40 -0400 Subject: [PATCH 08/11] Use standard error message in temporary app file copy catch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 68aad6e2f4..b83e3321e0 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -103,7 +103,7 @@ async function moveApp(tempDir: string, destination: string, id: string) { }); }) .catch(error => { - throw new Error(`Failed to read temporary directory: ${error.message}`); + throw new Error(`Failed to read temporary directory, ${error}`); }); for (const relativeTempFile of relativeTempFiles) { From 0a788da8731cf795d27660833edf5d822d4f9936 Mon Sep 17 00:00:00 2001 From: colton Date: Mon, 11 Oct 2021 15:01:16 -0400 Subject: [PATCH 09/11] Use standard error message in catch of copy file call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index b83e3321e0..8f054319e0 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -112,7 +112,7 @@ async function moveApp(tempDir: string, destination: string, id: string) { await fs.copy(tempFile, destFile).catch(error => { throw new Error( - `Failed to move file from ${tempFile} to ${destFile}: ${error.message}`, + `Failed to move file from ${tempFile} to ${destFile}, ${error}`, ); }); } From c671c3827939e6ffd51addac9650d98f540bf8d3 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Thu, 21 Oct 2021 14:53:29 -0400 Subject: [PATCH 10/11] Template directly to specified path otherwise use temp dir When the `--path` argument is specified, then perform the create-app templating directly to that path. Otherwise, template to a temporary directory, and move the files to a directory based on the project name. Removed the `cleanUp` method in favor of removing temporary files directly in the `moveApp` method in a `finally` block. Reverted `moveApp` method to use the move operation instead ofe recursively copying files. Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 70 +++++++++++----------------- 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 8f054319e0..d833f0c4d8 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -20,11 +20,10 @@ import chalk from 'chalk'; import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath, relative as relativePath } from 'path'; +import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; -import recursive from 'recursive-readdir'; const exec = promisify(execCb); @@ -69,12 +68,6 @@ async function createTemporaryAppFolder(tempDir: string) { }); } -async function cleanUp(tempDir: string) { - await Task.forItem('remove', 'temporary directory', async () => { - await fs.remove(tempDir); - }); -} - async function buildApp(appDir: string) { const runCmd = async (cmd: string) => { await Task.forItem('executing', cmd, async () => { @@ -94,28 +87,17 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - // Get the temporary files relative path in relation to the `tempDir` so - // that it can be used in resolving destination file path - const relativeTempFiles = await recursive(tempDir) - .then(files => { - return files.map(file => { - return relativePath(tempDir, file); - }); - }) + await fs + .move(tempDir, destination) .catch(error => { - throw new Error(`Failed to read temporary directory, ${error}`); - }); - - for (const relativeTempFile of relativeTempFiles) { - const tempFile = resolvePath(tempDir, relativeTempFile); - const destFile = resolvePath(destination, relativeTempFile); - - await fs.copy(tempFile, destFile).catch(error => { throw new Error( - `Failed to move file from ${tempFile} to ${destFile}, ${error}`, + `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); + }) + .finally(() => { + // remove temporary files on both success and failure + fs.removeSync(tempDir); }); - } }); } @@ -164,27 +146,30 @@ export default async (cmd: Command): Promise => { Task.log('Creating the app...'); try { - Task.section('Checking if the directory is available'); if (cmd.path) { + // Template directly to specified path + + Task.section('Checking that supplied path exists'); await checkPathExistsAndIsDirectory(appDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, cmd.path, answers); } else { - // Directory must not already exist when using consructed path from - // `answers.name` + // Template to temporary location, and then move files + + Task.section('Checking if the directory is available'); await checkAppPathDoesntExist(paths.targetDir, answers.name); + + Task.section('Creating a temporary app directory'); + await createTemporaryAppFolder(tempDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, answers); + + Task.section('Moving to final location'); + await moveApp(tempDir, appDir, answers.name); } - Task.section('Creating a temporary app directory'); - await createTemporaryAppFolder(tempDir); - - Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); - - Task.section('Moving to final location'); - await moveApp(tempDir, appDir, answers.name); - - Task.section('Cleanup'); - await cleanUp(tempDir); - if (!cmd.skipInstall) { Task.section('Building the app'); await buildApp(appDir); @@ -207,10 +192,7 @@ export default async (cmd: Command): Promise => { Task.error(error.message); Task.log('It seems that something went wrong when creating the app 🤔'); - Task.log('We are going to clean up, and then you can try again.'); - Task.section('Cleanup'); - await cleanUp(tempDir); Task.error('🔥 Failed to create app!'); Task.exit(1); } From c9e843547efbf73320f01aa7c3c3115dc30d4322 Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Wed, 27 Oct 2021 14:03:28 -0400 Subject: [PATCH 11/11] Create `--path` directory if it does not already exist Instead of failing when the specified path does not exist, create it using `mkdirs`. This ensures that that either the directory already exists, or it's created. Signed-off-by: Colton Padden --- packages/create-app/src/createApp.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d833f0c4d8..270ed3fd54 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -27,7 +27,7 @@ import { Task, templatingTask } from './lib/tasks'; const exec = promisify(execCb); -async function checkAppPathDoesntExist(rootDir: string, name: string) { +async function checkAppExists(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { const destination = resolvePath(rootDir, name); @@ -40,18 +40,13 @@ async function checkAppPathDoesntExist(rootDir: string, name: string) { }); } -async function checkPathExistsAndIsDirectory(path: string) { +async function checkPathExists(path: string) { await Task.forItem('checking', path, async () => { - if (await fs.pathExists(path)) { - if (!fs.lstatSync(path).isDirectory()) { - throw new Error( - 'Directory specified with --path argument is a file\nPlease ensure target path is a directory', - ); - } - } else { - throw new Error( - 'Directory specified with --path argument does not exist\nPlease try again with a different path', - ); + try { + await fs.mkdirs(path); + } catch (error) { + // will fail if a file already exists at given `path` + throw new Error(`Failed to create app directory: ${error.message}`); } }); } @@ -150,7 +145,7 @@ export default async (cmd: Command): Promise => { // Template directly to specified path Task.section('Checking that supplied path exists'); - await checkPathExistsAndIsDirectory(appDir); + await checkPathExists(appDir); Task.section('Preparing files'); await templatingTask(templateDir, cmd.path, answers); @@ -158,7 +153,7 @@ export default async (cmd: Command): Promise => { // Template to temporary location, and then move files Task.section('Checking if the directory is available'); - await checkAppPathDoesntExist(paths.targetDir, answers.name); + await checkAppExists(paths.targetDir, answers.name); Task.section('Creating a temporary app directory'); await createTemporaryAppFolder(tempDir);