From 3c6bbd86353549f7829ab3904d71257c21505a8f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 31 Mar 2020 20:29:12 +0200 Subject: [PATCH 1/6] cli/commands/create-plugin: add task helper and make everything async --- .../commands/create-plugin/createPlugin.ts | 399 +++++++----------- packages/cli/src/helpers/tasks.ts | 53 +++ 2 files changed, 200 insertions(+), 252 deletions(-) create mode 100644 packages/cli/src/helpers/tasks.ts diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index da695bee95..e4b67ef3ce 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -16,86 +16,50 @@ import fs from 'fs-extra'; import path from 'path'; +import { promisify } from 'util'; import handlebars from 'handlebars'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; import recursive from 'recursive-readdir'; -import { execSync } from 'child_process'; +import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; -import { realpathSync, existsSync } from 'fs'; import os from 'os'; -import ora from 'ora'; import { parseOwnerIds, addCodeownersEntry, getCodeownersFilePath, } from './lib/codeowners'; +import { Task } from '../../helpers/tasks'; +const exec = promisify(execCb); -const MARKER_SUCCESS = chalk.green(` ✔︎\n`); -const MARKER_FAILURE = chalk.red(` ✘\n`); +async function checkExists(rootDir: string, id: string) { + Task.section('Checking if the plugin ID is available'); -const checkExists = (rootDir: string, id: string) => { - console.log(); - console.log(chalk.green(' Checking if the plugin already exists:')); + await Task.forItem('checking', id, async () => { + const destination = path.join(rootDir, 'plugins', id); - const destination = path.join(rootDir, 'plugins', id); - - if (fs.existsSync(destination)) { - console.log( - chalk.red( - ` plugin ID\t${chalk.cyan(id)} already exists${MARKER_FAILURE}`, - ), - ); - throw new Error( - `A plugin with the same name already exists: ${chalk.cyan( - destination.replace(`${rootDir}/`, ''), - )}\nPlease try again with a different plugin ID`, - ); - } - console.log( - chalk.green(` plugin ID\t${chalk.cyan(id)} is available${MARKER_SUCCESS}`), - ); -}; - -export const createTemporaryPluginFolder = (tempDir: string) => { - console.log(); - console.log(chalk.green(' Creating a temporary plugin directory:')); - - process.stdout.write( - chalk.green(` creating\t${chalk.cyan('temporary')} directory`), - ); - try { - fs.mkdirSync(tempDir); - process.stdout.write(MARKER_SUCCESS); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error( - `Failed to create temporary plugin directory: ${e.message}`, - ); - } -}; - -export const createFileFromTemplate = ( - source: string, - destination: string, - answers: Answers, - version: string, -) => { - const template = fs.readFileSync(source); - const compiled = handlebars.compile(template.toString()); - const contents = compiled({ - name: path.basename(destination), - version, - ...answers, + if (await fs.pathExists(destination)) { + const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); + throw new Error( + `A plugin with the same name already exists: ${existing}\nPlease try again with a different plugin ID`, + ); + } }); - try { - fs.writeFileSync(destination, contents); - process.stdout.write(MARKER_SUCCESS); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error(`Failed to create file: ${destination}: ${e.message}`); - } -}; +} + +export async function createTemporaryPluginFolder(tempDir: string) { + Task.section('Creating a temporary plugin directory'); + + await Task.forItem('creating', 'temporary directory', async () => { + try { + await fs.mkdir(tempDir); + } catch (error) { + throw new Error( + `Failed to create temporary plugin directory: ${error.message}`, + ); + } + }); +} const sortObjectByKeys = (obj: { [name in string]: string }) => { return Object.keys(obj) @@ -109,9 +73,9 @@ const sortObjectByKeys = (obj: { [name in string]: string }) => { const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); -const addExportStatement = (file: string, exportStatement: string) => { - const newContents = fs - .readFileSync(file, 'utf8') +async function addExportStatement(file: string, exportStatement: string) { + const contents = await fs.readFile(file, 'utf8'); + const newContents = contents .split('\n') .filter(Boolean) // get rid of empty lines .concat([exportStatement]) @@ -119,28 +83,22 @@ const addExportStatement = (file: string, exportStatement: string) => { .concat(['']) // newline at end of file .join('\n'); - fs.writeFileSync(file, newContents, 'utf8'); -}; + await fs.writeFile(file, newContents, 'utf8'); +} -export const addPluginDependencyToApp = ( +export async function addPluginDependencyToApp( rootDir: string, pluginName: string, version: string, -) => { - console.log(); - console.log(chalk.green(' Adding plugin as dependency in app:')); +) { + Task.section('Adding plugin as dependency in app'); const pluginPackage = `@backstage/plugin-${pluginName}`; - const packageFile = path.join(rootDir, 'packages', 'app', 'package.json'); + const packageFilePath = 'packages/app/package.json'; + const packageFile = path.join(rootDir, packageFilePath); - process.stdout.write( - chalk.green( - ` processing\t${chalk.cyan(packageFile.replace(`${rootDir}/`, ''))}`, - ), - ); - - try { - const packageFileContent = fs.readFileSync(packageFile, 'utf-8'); + await Task.forItem('processing', packageFilePath, async () => { + const packageFileContent = await fs.readFile(packageFile, 'utf-8'); const packageFileJson = JSON.parse(packageFileContent); const dependencies = packageFileJson.dependencies; @@ -152,24 +110,18 @@ export const addPluginDependencyToApp = ( dependencies[pluginPackage] = `^${version}`; packageFileJson.dependencies = sortObjectByKeys(dependencies); - fs.writeFileSync( - packageFile, - `${JSON.stringify(packageFileJson, null, 2)}\n`, - 'utf-8', - ); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error( - `Failed to add plugin as dependency in app: ${packageFile}: ${e.message}`, - ); - } + const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; - process.stdout.write(MARKER_SUCCESS); -}; + await fs.writeFile(packageFile, newContents, 'utf-8').catch(error => { + throw new Error( + `Failed to add plugin as dependency to app: ${packageFile}: ${error.message}`, + ); + }); + }); +} -export const addPluginToApp = (rootDir: string, pluginName: string) => { - console.log(); - console.log(chalk.green(' Import plugin in app:')); +export async function addPluginToApp(rootDir: string, pluginName: string) { + Task.section('Import plugin in app'); const pluginPackage = `@backstage/plugin-${pluginName}`; const pluginNameCapitalized = pluginName @@ -177,30 +129,17 @@ export const addPluginToApp = (rootDir: string, pluginName: string) => { .map(name => capitalize(name)) .join(''); const pluginExport = `export { default as ${pluginNameCapitalized} } from '${pluginPackage}';`; - const pluginsFile = path.join( - rootDir, - 'packages', - 'app', - 'src', - 'plugins.ts', - ); - process.stdout.write( - chalk.green( - ` processing\t${chalk.cyan(pluginsFile.replace(`${rootDir}/`, ''))}`, - ), - ); + const pluginsFilePath = 'packages/app/src/plugins.ts'; + const pluginsFile = resolvePath(rootDir, pluginsFilePath); - try { - addExportStatement(pluginsFile, pluginExport); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error( - `Failed to import plugin in app: ${pluginsFile}: ${e.message}`, - ); - } - - process.stdout.write(MARKER_SUCCESS); -}; + await Task.forItem('processing', pluginsFilePath, async () => { + await addExportStatement(pluginsFile, pluginExport).catch(error => { + throw new Error( + `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, + ); + }); + }); +} export const createFromTemplateDir = async ( templateFolder: string, @@ -208,131 +147,91 @@ export const createFromTemplateDir = async ( answers: Answers, version: string, ) => { - console.log(); - console.log(chalk.green(' Reading template files:')); - - let files = []; - - try { - files = await recursive(templateFolder); - console.log( - chalk.green( - ` reading\t${chalk.cyan(`${files.length} files`)}${MARKER_SUCCESS}`, - ), - ); - } catch (e) { - console.log( - chalk.red(` reading\t${chalk.cyan('0')} files${MARKER_FAILURE}`), - ); - throw new Error(`Failed to read files in template directory: ${e.message}`); - } - - console.log(); - console.log(chalk.green(' Setting up the plugin files:')); - files.forEach(file => { - process.stdout.write( - chalk.green(` processing\t${chalk.cyan(path.basename(file))}`), - ); - fs.ensureDirSync( - file - .replace(templateFolder, destinationFolder) - .replace(path.basename(file), ''), - ); - if (file.endsWith('hbs')) { - createFileFromTemplate( - file, - file.replace(templateFolder, destinationFolder).replace(/\.hbs$/, ''), - answers, - version, - ); - } else { - try { - fs.copyFileSync(file, file.replace(templateFolder, destinationFolder)); - process.stdout.write(MARKER_SUCCESS); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error( - `Failed to copy file: ${file.replace( - templateFolder, - destinationFolder, - )}: ${e.message}`, - ); - } - } + Task.section('Setting up plugin files'); + const files = await recursive(templateFolder).catch(error => { + throw new Error(`Failed to read template directory: ${error.message}`); }); -}; -const cleanUp = async (tempDir: string, id: string) => { - console.log( - chalk.green( - `It seems that something went wrong when creating the plugin 🤔 `, - ), - ); - console.log( - chalk.green('We are going to clean up, and then you can try again.'), - ); - const spinner = ora({ - prefixText: chalk.green(` Cleaning up\t${chalk.cyan(id)}`), - spinner: 'arc', - color: 'green', - }).start(); - try { - await fs.remove(tempDir); - spinner.succeed(); - } catch (e) { - spinner.fail(); - console.log(chalk.red(`Failed to cleanup: ${e.message}`)); + for (const file of files) { + const destinationFile = file.replace(templateFolder, destinationFolder); + path.dirname(file); + await fs.ensureDir(path.dirname(file)); + + if (file.endsWith('.hbs')) { + await Task.forItem('templating', file, async () => { + const destination = destinationFile.replace(/\.hbs$/, ''); + + const template = await fs.readFile(path.basename(file)); + const compiled = handlebars.compile(template.toString()); + const contents = compiled({ + name: path.basename(destination), + version, + ...answers, + }); + + await fs.writeFile(destination, contents).catch(error => { + throw new Error( + `Failed to create file: ${destination}: ${error.message}`, + ); + }); + }); + } else { + await Task.forItem('copying', file, async () => { + await fs.copyFile(file, destinationFile).catch(error => { + const destination = destinationFile; + throw new Error( + `Failed to copy file to ${destination} : ${error.message}`, + ); + }); + }); + } } }; -const buildPlugin = async (pluginFolder: string) => { - console.log(); - console.log(chalk.green(` Building the plugin:`)); +async function cleanUp(tempDir: string, id: string) { + Task.log('It seems that something went wrong when creating the plugin 🤔'); + Task.log('We are going to clean up, and then you can try again.'); + + await Task.forItem('Cleaning up', id, async () => { + await fs.remove(tempDir); + }); +} + +async function buildPlugin(pluginFolder: string) { + Task.section('Building the plugin'); const commands = ['yarn install', 'yarn build']; for (const command of commands) { - const spinner = ora({ - prefixText: chalk.green(` executing\t${chalk.cyan(command)}`), - spinner: 'arc', - color: 'green', - }).start(); - try { + await Task.forItem('executing', command, async () => { process.chdir(pluginFolder); - execSync(command, { timeout: 60000, stdio: 'pipe' }); - spinner.succeed(); - } catch (e) { - spinner.fail(); - process.stdout.write(e.stderr); - process.stdout.write(e.stdout); - throw new Error(`Could not execute command ${chalk.cyan(command)}`); - } - } -}; -export const movePlugin = ( + await exec(command).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(command)}`); + }); + }); + } +} + +export async function movePlugin( tempDir: string, destination: string, id: string, -) => { - console.log(); - console.log(chalk.green(` Moving the plugin:`)); +) { + Task.section('Moving the plugin to final location'); - process.stdout.write( - chalk.green(` moving\t${chalk.cyan(id)} to final location`), - ); - try { - fs.moveSync(tempDir, destination); - process.stdout.write(MARKER_SUCCESS); - } catch (e) { - process.stdout.write(MARKER_FAILURE); - throw new Error( - `Failed to move plugin from ${tempDir} to ${destination}: ${e.message}`, - ); - } -}; + await Task.forItem('moving', id, async () => { + await fs.move(tempDir, destination).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`, + ); + }); + }); +} -const createPlugin = async () => { - const rootDir = realpathSync(process.cwd()); +export default async () => { + const rootDir = await fs.realpath(process.cwd()); const codeownersPath = await getCodeownersFilePath(rootDir); const questions: Question[] = [ @@ -387,19 +286,19 @@ const createPlugin = async () => { const version = require(resolvePath(cliPackage, 'package.json')).version; const ownerIds = parseOwnerIds(answers.owner); - console.log(); - console.log(chalk.green('Creating the plugin...')); + Task.log(); + Task.log('Creating the plugin...'); try { - checkExists(rootDir, answers.id); - createTemporaryPluginFolder(tempDir); + await checkExists(rootDir, answers.id); + await createTemporaryPluginFolder(tempDir); await createFromTemplateDir(templateFolder, tempDir, answers, version); - movePlugin(tempDir, pluginDir, answers.id); + await movePlugin(tempDir, pluginDir, answers.id); await buildPlugin(pluginDir); - if (existsSync(appPackage)) { - addPluginDependencyToApp(rootDir, answers.id, version); - addPluginToApp(rootDir, answers.id); + if (await fs.pathExists(appPackage)) { + await addPluginDependencyToApp(rootDir, answers.id, version); + await addPluginToApp(rootDir, answers.id); } if (ownerIds && ownerIds.length) { @@ -410,18 +309,16 @@ const createPlugin = async () => { ); } - console.log(); - console.log( - chalk.green( - `🥇 Successfully created ${chalk.cyan( - `@backstage/plugin-${answers.id}`, - )}`, - ), + Task.log(); + Task.log( + `🥇 Successfully created ${chalk.cyan( + `@backstage/plugin-${answers.id}`, + )}`, ); + Task.log(); + } catch (error) { console.log(); - } catch (e) { - console.log(); - console.log(`${chalk.red(e.message)}`); + console.log(`${chalk.red(error.message)}`); console.log(); await cleanUp(tempDir, answers.id); console.log(); @@ -429,5 +326,3 @@ const createPlugin = async () => { console.log(); } }; - -export default createPlugin; diff --git a/packages/cli/src/helpers/tasks.ts b/packages/cli/src/helpers/tasks.ts new file mode 100644 index 0000000000..65ef169d18 --- /dev/null +++ b/packages/cli/src/helpers/tasks.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 chalk from 'chalk'; +import ora from 'ora'; + +const TASK_NAME_MAX_LENGTH = 14; + +export class Task { + static log(name: string = '') { + process.stdout.write(`${chalk.green(name)}\n`); + } + + static section(name: string) { + const title = chalk.green(`${name}:`); + process.stdout.write(`\n ${title}\n`); + } + + static async forItem( + task: string, + item: string, + taskFunc: () => Promise, + ): Promise { + const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH)); + + const spinner = ora({ + prefixText: chalk.green(` ${paddedTask}${chalk.cyan(item)}`), + spinner: 'arc', + color: 'green', + }).start(); + + try { + await taskFunc(); + spinner.succeed(); + } catch (error) { + spinner.fail(); + throw error; + } + } +} From 580cead761bd4aafa052ab2120ba6a065a117104 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 31 Mar 2020 21:02:45 +0200 Subject: [PATCH 2/6] cli/helpers/errors: make exit error message a bit more visible --- packages/cli/src/helpers/errors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/helpers/errors.ts b/packages/cli/src/helpers/errors.ts index 38ac305277..a1eab4c9e5 100644 --- a/packages/cli/src/helpers/errors.ts +++ b/packages/cli/src/helpers/errors.ts @@ -37,10 +37,10 @@ export class ExitCodeError extends CustomError { export function exitWithError(error: Error): never { if (error instanceof ExitCodeError) { - process.stderr.write(`${chalk.red(error.message)}\n`); + process.stderr.write(`\n${chalk.red(error.message)}\n\n`); process.exit(error.code); } else { - process.stderr.write(`${chalk.red(`${error}`)}\n`); + process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`); process.exit(1); } } From 841a38741773494f12b65adc4e18464871c48af2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2020 01:10:43 +0200 Subject: [PATCH 3/6] cli/commands/create-plugin: move templating to tasks helper and fix it + separate out sections --- .../commands/create-plugin/createPlugin.ts | 101 +++++------------- packages/cli/src/helpers/tasks.ts | 48 +++++++++ 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index e4b67ef3ce..1b35d99a59 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -17,10 +17,8 @@ import fs from 'fs-extra'; import path from 'path'; import { promisify } from 'util'; -import handlebars from 'handlebars'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; -import recursive from 'recursive-readdir'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import os from 'os'; @@ -29,12 +27,10 @@ import { addCodeownersEntry, getCodeownersFilePath, } from './lib/codeowners'; -import { Task } from '../../helpers/tasks'; +import { Task, templatingTask } from '../../helpers/tasks'; const exec = promisify(execCb); async function checkExists(rootDir: string, id: string) { - Task.section('Checking if the plugin ID is available'); - await Task.forItem('checking', id, async () => { const destination = path.join(rootDir, 'plugins', id); @@ -48,8 +44,6 @@ async function checkExists(rootDir: string, id: string) { } export async function createTemporaryPluginFolder(tempDir: string) { - Task.section('Creating a temporary plugin directory'); - await Task.forItem('creating', 'temporary directory', async () => { try { await fs.mkdir(tempDir); @@ -91,8 +85,6 @@ export async function addPluginDependencyToApp( pluginName: string, version: string, ) { - Task.section('Adding plugin as dependency in app'); - const pluginPackage = `@backstage/plugin-${pluginName}`; const packageFilePath = 'packages/app/package.json'; const packageFile = path.join(rootDir, packageFilePath); @@ -121,8 +113,6 @@ export async function addPluginDependencyToApp( } export async function addPluginToApp(rootDir: string, pluginName: string) { - Task.section('Import plugin in app'); - const pluginPackage = `@backstage/plugin-${pluginName}`; const pluginNameCapitalized = pluginName .split('-') @@ -141,65 +131,13 @@ export async function addPluginToApp(rootDir: string, pluginName: string) { }); } -export const createFromTemplateDir = async ( - templateFolder: string, - destinationFolder: string, - answers: Answers, - version: string, -) => { - Task.section('Setting up plugin files'); - const files = await recursive(templateFolder).catch(error => { - throw new Error(`Failed to read template directory: ${error.message}`); - }); - - for (const file of files) { - const destinationFile = file.replace(templateFolder, destinationFolder); - path.dirname(file); - await fs.ensureDir(path.dirname(file)); - - if (file.endsWith('.hbs')) { - await Task.forItem('templating', file, async () => { - const destination = destinationFile.replace(/\.hbs$/, ''); - - const template = await fs.readFile(path.basename(file)); - const compiled = handlebars.compile(template.toString()); - const contents = compiled({ - name: path.basename(destination), - version, - ...answers, - }); - - await fs.writeFile(destination, contents).catch(error => { - throw new Error( - `Failed to create file: ${destination}: ${error.message}`, - ); - }); - }); - } else { - await Task.forItem('copying', file, async () => { - await fs.copyFile(file, destinationFile).catch(error => { - const destination = destinationFile; - throw new Error( - `Failed to copy file to ${destination} : ${error.message}`, - ); - }); - }); - } - } -}; - -async function cleanUp(tempDir: string, id: string) { - Task.log('It seems that something went wrong when creating the plugin 🤔'); - Task.log('We are going to clean up, and then you can try again.'); - - await Task.forItem('Cleaning up', id, async () => { +async function cleanUp(tempDir: string) { + await Task.forItem('remove', 'temporary directory', async () => { await fs.remove(tempDir); }); } async function buildPlugin(pluginFolder: string) { - Task.section('Building the plugin'); - const commands = ['yarn install', 'yarn build']; for (const command of commands) { await Task.forItem('executing', command, async () => { @@ -219,8 +157,6 @@ export async function movePlugin( destination: string, id: string, ) { - Task.section('Moving the plugin to final location'); - await Task.forItem('moving', id, async () => { await fs.move(tempDir, destination).catch(error => { throw new Error( @@ -280,7 +216,7 @@ export default async () => { const appPackage = resolvePath(rootDir, 'packages', 'app'); const cliPackage = resolvePath(__dirname, '..', '..', '..'); - const templateFolder = resolvePath(cliPackage, 'templates', 'default-plugin'); + const templateDir = resolvePath(cliPackage, 'templates', 'default-plugin'); const tempDir = path.join(os.tmpdir(), answers.id); const pluginDir = path.join(rootDir, 'plugins', answers.id); const version = require(resolvePath(cliPackage, 'package.json')).version; @@ -290,14 +226,26 @@ export default async () => { Task.log('Creating the plugin...'); try { + Task.section('Checking if the plugin ID is available'); await checkExists(rootDir, answers.id); + + Task.section('Creating a temporary plugin directory'); await createTemporaryPluginFolder(tempDir); - await createFromTemplateDir(templateFolder, tempDir, answers, version); + + Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, { ...answers, version }); + + Task.section('Moving to final location'); await movePlugin(tempDir, pluginDir, answers.id); + + Task.section('Building the plugin'); await buildPlugin(pluginDir); if (await fs.pathExists(appPackage)) { + Task.section('Adding plugin as dependency in app'); await addPluginDependencyToApp(rootDir, answers.id, version); + + Task.section('Import plugin in app'); await addPluginToApp(rootDir, answers.id); } @@ -317,12 +265,13 @@ export default async () => { ); Task.log(); } catch (error) { - console.log(); - console.log(`${chalk.red(error.message)}`); - console.log(); - await cleanUp(tempDir, answers.id); - console.log(); - console.log(`🔥 ${chalk.red('Failed to create plugin!')}`); - console.log(); + Task.error(error.message); + + Task.log('It seems that something went wrong when creating the plugin 🤔'); + 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 plugin!'); } }; diff --git a/packages/cli/src/helpers/tasks.ts b/packages/cli/src/helpers/tasks.ts index 65ef169d18..3cba4d262f 100644 --- a/packages/cli/src/helpers/tasks.ts +++ b/packages/cli/src/helpers/tasks.ts @@ -15,7 +15,11 @@ */ import chalk from 'chalk'; +import fs from 'fs-extra'; +import handlebars from 'handlebars'; import ora from 'ora'; +import { basename, dirname } from 'path'; +import recursive from 'recursive-readdir'; const TASK_NAME_MAX_LENGTH = 14; @@ -24,6 +28,10 @@ export class Task { process.stdout.write(`${chalk.green(name)}\n`); } + static error(message: string = '') { + process.stdout.write(`\n${chalk.red(message)}\n\n`); + } + static section(name: string) { const title = chalk.green(`${name}:`); process.stdout.write(`\n ${title}\n`); @@ -51,3 +59,43 @@ export class Task { } } } + +export async function templatingTask( + templateDir: string, + destinationDir: string, + context: any, +) { + const files = await recursive(templateDir).catch(error => { + throw new Error(`Failed to read template directory: ${error.message}`); + }); + + for (const file of files) { + const destinationFile = file.replace(templateDir, destinationDir); + await fs.ensureDir(dirname(destinationFile)); + + if (file.endsWith('.hbs')) { + await Task.forItem('templating', basename(file), async () => { + const destination = destinationFile.replace(/\.hbs$/, ''); + + const template = await fs.readFile(file); + const compiled = handlebars.compile(template.toString()); + const contents = compiled({ name: basename(destination), ...context }); + + await fs.writeFile(destination, contents).catch(error => { + throw new Error( + `Failed to create file: ${destination}: ${error.message}`, + ); + }); + }); + } else { + await Task.forItem('copying', basename(file), async () => { + await fs.copyFile(file, destinationFile).catch(error => { + const destination = destinationFile; + throw new Error( + `Failed to copy file to ${destination} : ${error.message}`, + ); + }); + }); + } + } +} From 111f163d4105bdda296c2cb71caf0b3e09a71689 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 24 Mar 2020 10:23:43 +0100 Subject: [PATCH 4/6] cli: add create-app --- .../cli/src/commands/create-app/createApp.ts | 150 ++++++++++++++++++ packages/cli/src/index.ts | 6 + packages/cli/templates/default-app/README.md | 3 + packages/cli/templates/default-app/lerna.json | 6 + .../templates/default-app/package.json.hbs | 24 +++ .../default-app/packages/app/package.json.hbs | 39 +++++ .../app/public/android-chrome-192x192.png | Bin 0 -> 4352 bytes .../packages/app/public/apple-touch-icon.png | Bin 0 -> 4311 bytes .../packages/app/public/favicon-16x16.png | Bin 0 -> 890 bytes .../packages/app/public/favicon-32x32.png | Bin 0 -> 1296 bytes .../packages/app/public/favicon.ico | Bin 0 -> 15086 bytes .../packages/app/public/index.html | 93 +++++++++++ .../packages/app/public/manifest.json | 15 ++ .../packages/app/public/robots.txt | 2 + .../packages/app/public/safari-pinned-tab.svg | 28 ++++ .../default-app/packages/app/src/App.test.tsx | 10 ++ .../default-app/packages/app/src/App.tsx | 42 +++++ .../default-app/packages/app/src/index.tsx | 5 + .../default-app/packages/app/src/plugins.ts | 1 + .../packages/app/src/setupTests.ts | 1 + .../default-app/packages/app/tsconfig.json | 20 +++ .../default-app/plugins/welcome/README.md | 3 + .../plugins/welcome/package.json.hbs | 20 +++ .../welcome/src/components/Timer/Timer.tsx | 58 +++++++ .../welcome/src/components/Timer/index.ts | 1 + .../WelcomePage/WelcomePage.test.tsx | 16 ++ .../components/WelcomePage/WelcomePage.tsx | 106 +++++++++++++ .../src/components/WelcomePage/index.ts | 1 + .../default-app/plugins/welcome/src/index.ts | 1 + .../plugins/welcome/src/plugin.test.ts | 7 + .../default-app/plugins/welcome/src/plugin.ts | 9 ++ .../plugins/welcome/src/setupTests.ts | 1 + .../default-app/plugins/welcome/tsconfig.json | 4 + .../templates/default-app/prettier.config.js | 1 + .../cli/templates/default-app/tsconfig.json | 10 ++ 35 files changed, 683 insertions(+) create mode 100644 packages/cli/src/commands/create-app/createApp.ts create mode 100644 packages/cli/templates/default-app/README.md create mode 100644 packages/cli/templates/default-app/lerna.json create mode 100644 packages/cli/templates/default-app/package.json.hbs create mode 100644 packages/cli/templates/default-app/packages/app/package.json.hbs create mode 100644 packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png create mode 100644 packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png create mode 100644 packages/cli/templates/default-app/packages/app/public/favicon-16x16.png create mode 100644 packages/cli/templates/default-app/packages/app/public/favicon-32x32.png create mode 100644 packages/cli/templates/default-app/packages/app/public/favicon.ico create mode 100644 packages/cli/templates/default-app/packages/app/public/index.html create mode 100644 packages/cli/templates/default-app/packages/app/public/manifest.json create mode 100644 packages/cli/templates/default-app/packages/app/public/robots.txt create mode 100644 packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg create mode 100644 packages/cli/templates/default-app/packages/app/src/App.test.tsx create mode 100644 packages/cli/templates/default-app/packages/app/src/App.tsx create mode 100644 packages/cli/templates/default-app/packages/app/src/index.tsx create mode 100644 packages/cli/templates/default-app/packages/app/src/plugins.ts create mode 100644 packages/cli/templates/default-app/packages/app/src/setupTests.ts create mode 100644 packages/cli/templates/default-app/packages/app/tsconfig.json create mode 100644 packages/cli/templates/default-app/plugins/welcome/README.md create mode 100644 packages/cli/templates/default-app/plugins/welcome/package.json.hbs create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/index.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/plugin.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts create mode 100644 packages/cli/templates/default-app/plugins/welcome/tsconfig.json create mode 100644 packages/cli/templates/default-app/prettier.config.js create mode 100644 packages/cli/templates/default-app/tsconfig.json diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts new file mode 100644 index 0000000000..71e8ec50e9 --- /dev/null +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 path from 'path'; +import { promisify } from 'util'; +import chalk from 'chalk'; +import inquirer, { Answers, Question } from 'inquirer'; +import { exec as execCb } from 'child_process'; +import { resolve as resolvePath } from 'path'; +import { realpathSync } from 'fs'; +import os from 'os'; +import { Task, templatingTask } from '../../helpers/tasks'; +const exec = promisify(execCb); + +async function checkExists(rootDir: string, name: string) { + await Task.forItem('checking', name, async () => { + const destination = path.join(rootDir, 'plugins', name); + + if (await fs.pathExists(destination)) { + const existing = chalk.cyan(destination.replace(`${rootDir}/`, '')); + throw new Error( + `A directory with the same name already exists: ${existing}\nPlease try again with a different app name`, + ); + } + }); +} + +export async function createTemporaryAppFolder(tempDir: string) { + await Task.forItem('creating', 'temporary directory', async () => { + try { + await fs.mkdir(tempDir); + } catch (error) { + throw new Error( + `Failed to create temporary app directory: ${error.message}`, + ); + } + }); +} + +async function cleanUp(tempDir: string) { + await Task.forItem('remove', 'temporary directory', async () => { + await fs.remove(tempDir); + }); +} + +async function buildApp(pluginFolder: string) { + const commands = ['yarn install', 'yarn build']; + for (const command of commands) { + await Task.forItem('executing', command, async () => { + process.chdir(pluginFolder); + + await exec(command).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(command)}`); + }); + }); + } +} + +export async function moveApp( + tempDir: string, + destination: string, + id: string, +) { + await Task.forItem('moving', id, async () => { + await fs.move(tempDir, destination).catch(error => { + throw new Error( + `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`, + ); + }); + }); +} + +export default async () => { + const questions: Question[] = [ + { + type: 'input', + name: 'name', + message: chalk.blue('Enter a name for the app [required]'), + validate: (value: any) => { + if (!value) { + return chalk.red('Please enter a name for the app'); + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return chalk.red( + 'Plugin name must be kebab-cased and contain only letters, digits, and dashes.', + ); + } + return true; + }, + }, + ]; + const answers: Answers = await inquirer.prompt(questions); + + const rootDir = realpathSync(process.cwd()); + const cliPackage = resolvePath(__dirname, '../../..'); + const templateDir = resolvePath(cliPackage, 'templates', 'default-app'); + const tempDir = path.join(os.tmpdir(), answers.name); + const appDir = path.join(rootDir, answers.name); + const version = require(resolvePath(cliPackage, 'package.json')).version; + + Task.log(); + Task.log('Creating the app...'); + + try { + Task.section('Checking if the directory is available'); + await checkExists(rootDir, answers.name); + + Task.section('Creating a temporary app directory'); + await createTemporaryAppFolder(tempDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, { ...answers, version }); + + Task.section('Moving to final location'); + await moveApp(tempDir, appDir, answers.name); + + Task.section('Building the app'); + await buildApp(appDir); + + Task.log(); + Task.log( + chalk.green(`🥇 Successfully created ${chalk.cyan(answers.name)}`), + ); + Task.log(); + } catch (error) { + 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!'); + } +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5f24190237..3eafc6dc6b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,6 +17,7 @@ import program from 'commander'; import chalk from 'chalk'; import fs from 'fs'; +import createAppCommand from './commands/create-app/createApp'; import createPluginCommand from './commands/create-plugin/createPlugin'; import watch from './commands/watch-deps'; import lintCommand from './commands/lint'; @@ -32,6 +33,11 @@ const main = (argv: string[]) => { program.name('backstage-cli').version(packageJson.version ?? '0.0.0'); + program + .command('create-app') + .description('Creates a new app in a new directory') + .action(actionHandler(createAppCommand)); + program .command('app:build') .description('Build an app for a production release') diff --git a/packages/cli/templates/default-app/README.md b/packages/cli/templates/default-app/README.md new file mode 100644 index 0000000000..b2594d1e53 --- /dev/null +++ b/packages/cli/templates/default-app/README.md @@ -0,0 +1,3 @@ +# [Backstage](https://backstage.io) + +This is your newly scaffolded Backstage App, Good Luck! diff --git a/packages/cli/templates/default-app/lerna.json b/packages/cli/templates/default-app/lerna.json new file mode 100644 index 0000000000..322929db1d --- /dev/null +++ b/packages/cli/templates/default-app/lerna.json @@ -0,0 +1,6 @@ +{ + "packages": ["packages/*", "plugins/*"], + "npmClient": "yarn", + "useWorkspaces": true, + "version": "0.1.0" +} diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs new file mode 100644 index 0000000000..def7b3178b --- /dev/null +++ b/packages/cli/templates/default-app/package.json.hbs @@ -0,0 +1,24 @@ +{ + "name": "root", + "private": true, + "scripts": { + "start": "yarn build && yarn workspace app start", + "build": "lerna run build", + "test": "cross-env CI=true lerna run test -- --coverage", + "create-plugin": "backstage-cli create-plugin", + "lint": "lerna run lint" + }, + "workspaces": { + "packages": [ + "packages/*", + "plugins/*" + ] + }, + "version": "1.0.0", + "devDependencies": { + "@backstage/cli": "^{{version}}", + "cross-env": "^7.0.0", + "lerna": "^3.20.2", + "prettier": "^1.19.1" + } +} diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs new file mode 100644 index 0000000000..fd0a19fed0 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -0,0 +1,39 @@ +{ + "name": "app", + "version": "0.0.0", + "private": true, + "dependencies": { + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@backstage/cli": "^{{version}}", + "@backstage/core": "^{{version}}", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "@types/react-router-dom": "^5.1.3", + "plugin-welcome": "0.0.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-router-dom": "^5.1.2" + }, + "scripts": { + "start": "backstage-cli app:serve", + "build": "backstage-cli app:build", + "test": "backstage-cli test", + "lint": "backstage-cli lint" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png b/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..3d56edbb0dce7b1c12f644090e3522d9b992fd1d GIT binary patch literal 4352 zcmai1c|26@+dng71~ayplBH0HERA&tL$*kEWz9B*7RywFk!>a#M930S;*lj~YYL%e zdMpX0)$-UTvXh~VDTCj6-uJKfkKgCL@6S2cxwg-Bu5+L7b=`NGlfy}2q%;x$0AX7j zOBawb|Gfkd;N4{`lpiE~At&rl06=3FiWdL}d3n5zi#-5bQw9J^G64JmMU;5}h{OQE zqCWr}F9ZNd;=NX9V^Dzzv^!}D?EX7TUsmOU5`L1cy%qlyNfm2a{FdgxOkkN6DY=H_F9wGx<$YHa6A(A-N+gxUtYM8@$OXmVtZ7E$=8F0&F?C5a%)MtFB;j>IqnzDMid5kOPkzekF~=3w-|#OzN8 z;iW&r_!y=JGOG@Ekj}vRD2-Cg7P+Cw;rEWdb&vBoYxl~5{aeCc%|wX?4a8+eqF`wH zcy;TiW2a-@L2&RmK?8#*{NgEyO0zGo|Jq)RDahGCs%dzt8dU<1eHf0WEE+($NOse( z@FNL$sh0?BXRqmD8xc?*qd`<9x-HYbP@zutCt;E>9nfJBRCo!+@vW-n=&J%>^MORf zJ?S=uGnIhbs2d9!cf97=2}t#;7-sB;kdb`{o?YX*8Vh_n1TQ^^U~f^|XUVI2Xv!ah zy=45dwc9PIlN_DeT>dCu!x>m42olXeVH(uwSUr3~x^2%HmxAI-AhhD=->$TQoJ*P8 z-p}Pym2tVt(r(e`eHO(jl2lQM=!X&A0cD&*{2P5?1GIs7;;^p7eEN3qrax{%?b-1t zi<(kRhz&J`t(_cWN3rGF&n{71*?uE1l{u*?pdEWYa+&6KLdul+^euax#z2H4CivB% zI3(|A)O1vts0msI3onfL|jz&?Vpr~_rY;eE2u4k|hniBVr=}sMR$^7xgcAh%iq5}Mug1pKoNX@8~95#(Q zTw@058KN@@4rnuLon#h%Dqba<0L9XuQxhWiEY!7IX2F=bYryTn>V-wga zESGk4`XNB7OsgN;(PI_%gn4ESsVqLjE&5hyR_wv=XI_H7IIpe+hbIj6&0Z%A`@HxMAh*ugAI-Vj5bUhBf!v54Od ztF{A_S?QhI2k(y$Em!lIM1~gA5=Z7Tw|BKCbjEK?CM6FGj}+T?q0Hf7=f+F;98$Nx zxqC__ehi9h0wkR?Pm%H~05?~pkBH9=&G_Sg0d9^+A3+}>5Qc>{u0V|vtPZiW@7CtP z4XD$BQMUN9oI=98vVW@Wu;<6apg4Y?o7UH6L3}b&D2@QuDYFZE5nSVX4Q3>_MklK? z{Pa0hU`9uXC$>7vJLo&@iu?=6v!D%$Z>zHHS^Idce#lB7uLBWxp<{5P6vnd8hN zE-_S96<#_VhGK{L<7SFdT~X3VXW?A7YT*zhk@_C-!WpQUF=IVgJ6LXk$5#GZB8q@} zNdxdg$u_MsRe;-&H#-J$q%+(mAFK>fXkQV+We{8WP^b75h#zy{f6G9E1j7(#48qbH zQk{jT*s(X>2Hitbt{K9)mwKJ+8pXk9t#tI2;*(#JWK{Hi~&*7Qh2ja<$EmFJn zJad@gdL{0D`wGej;K_P`!DG{h@l<{1&|@~DI~5O%ILBAJj?@`bhZfRI&d$$xBYjf4 zg(L09VGU~bECCjtOqgBSGr3#WbcU=Ny>R`d7GSg=gCd)<8GEu`*EKP+NT-jMKN)H~ zBhH>ldzG3sx~nMNn5Q!#fg~OM!B!+Acurw1fZE&Y_4Y z`kU_rH01|Z4;QXdY4H)W;O+z}4Ot(1!U|ERGvnoZts(Q7D_p0QLQY3YRG$NAJhK1y zMpfy}EA^!S$#GdPKK7&12Q?L7{IksO4H>Qwb|ek@!Q}HHM#A-h(A@i92oA&%y|v$+ zS_D!IX>obR&LevFXMYj)#bizpqFczkFmpsvz}jDe-TwNH*x$4=$Z{NSm+nbJ!afd&WaL(WsES~n+U!u081 zb2+v@M5zyio_O#iPyxaoO~Iax0sp2YHpTCs2ZdoS{Wfn3IXq}{7xr$ags?vZhzJ@; zVk=J{J}U}w(8e1Lmlux9LWe}3c(jY}_iTGsC=X_L8M>qJM%Nr-;!LkMDeCS_Fu!Ym zKl|5xLhvd)^Wk>Y`Fv9xr3vN@ok*FgaFy2eY*}eRbpo)aePN-RD%| zG7Y%IesuRhF+EUA$~IRq1{+-P`TidZ3F>C?$czGtUsrkVr;|5C#X*jEkUcvrES1}b z`~}0RD`#XEu8A}ZOyM(HDtk0yUl-oF;WI2RqNYtEBaV*T~IRC|S zNK`AvwK?9S>$|>*%Mb$9<4~m#Mi#LiG5_`5OH?)T_f?_KZuoG8(MuXG1qt zka;J9r8`c2Wd~|Cn07CZHH3Lu8o2=Lkr-dJ+FNg#i8H$SS`a5!IH}RIhnes=Kbx% zi*iFsz&7&%OX*X(he9x7VmunN^Vl?h@ic~k52#YMqDTsmN2VEmv|aVK6QRgtrqy=j`R@@Js-`#ee12RwBY`qD!Zg!3#L~I z=^^lC=#;-zNSi=LMuu*6H}cn3pzQfUuOs)sUM{t>#Aqsi$%hFQ#~k6vNT=XfdV_(- zNaw<@&r~D`#XX*W$*o`D>)rH^z}7y$P>Wl30v6|DSXa5+-OP-0_P@j!5Bta~pQrX_ zYf>-Hd$9(MqtMV~GwYM%)#vh`UqgWAGbiFOzM8IbZ(f}qT`<>6yBDhPY##((iqi)Y zPnKNDAxJ4CE5zY0kH7^SoBy0dnwf;caImqQ)$m)o_RXKZIW1~RA*n~a4*Ue0gdZ)- za$DQb?=gSh{K^W7`r%{HUCo$Y)-I^70o=S1aS|=r`qt(@-nJ5UZTGpbfAbL-LVv6T z0xg7LY8`qYY|?t+pnH#GKJ8 zzUe1dTxvmDf2_tc zbPHAtcboRnNa9jZQdQ(mA0z2Ku+qmCJ<_!lkvZjf0Sk4!74~>->CZ{F@4a{2z{uy? zbF?nAc`drcyX81u3NbLHO_*7p%-#TYGe`bp6NH*Z#$Oo4z)K}PfPUqaGd4%%J7R-r zYsEtWyuMwbi3rnVzfXA;jCLZu&X1XFyVVdzKJR6I#khNfnZ}hW%h%4aU|J|77gLs= zl74B~<%AMFw@ggxD?{@$DiE`E9_DvHK2U6*Q19nALioA*EBglgCAs_K7RwYZvv#C^ z^M+UgT3}OH)Pzx1Rh3oszWwosrlWvMH7wlA@~+os_0#a;3z!4+1anM781s%9S$g_K z|JG*5AKCgfE6e|PJUqT5cSBgQU;Mfar_kmNC0h&mVt(rmWGz0 zo0i^jJ)Pq^T51@r;}}d?u&USpaR`eDx)2=m-yJ>}-JS&<+^@NLkX!FMn6l`|e3J)YoKYc)Vr{&O;%2dPY~#l z6bQ8P00dIV1cA7Gvp*Rr0Sy-&?`x`q&i)*)n~Kta8ZcH{?;dy#!oe!bx255b2LiDM zX{)Q622HF}LJUpZray0``TMo}^Y>%6&yY9FJfl6Ov2&}8TDAAfwjLe5=`jO~i}omd zt0i|cT%A~{)?dZ0;ICR2udf+`M>EbbN>W7d`V?DH(?+=X73=!AC$Ck!Z7%yMjw?@X zd6khCw_*CdhheAsd^w22EZBg&d^~jzCk^7e=!QEF$Ev~}U+y?}<3Db=4im0vf1-F7 zj>bA%iK8Op+XWb+mJ^lA2T4QUzG2tKq*JfGu)U(nro=}eQMC(<>ut(SZ#p?Sjd;{p zm6?^8va_?>NF3F~bMHzHe*XNK%RY2=D=5=$BH95q^|7z_Cc4w-swJ)nW&&CktUAAW+<(@WO9eqT$=qRLfI(h5o90cogWP}#8V9? zFR%5FUX!%-$V+;sKYMT)tBHq6^CasU#7nb*3nC*Uqrt3;i;Ev? zYNSzIt|~CXTtEXaFR#C!9}f?Y#Q`TB=D4!IzyJ97I4LQKZG1=ZA)F=mX>u|rCui8v za*;rtKLO1aPZWdptP)$ok9OT9A4>FJ!W^_Z-+KP+S+;US&T?Y!<%D+S_V#vRVPRWa zTU`TZITcv=LoY9s-~89!UM4a}-qjBFgm>@Wefp$O=%%WeAjBs3ay^)!suzXaaR|n47p)+zeMD6R>aADyK>mL9Ib%{@* zYe{tNQqOg20ypi@xddO`{Glu9Mf_C~EKKdgWHOhyRgAAn1BEYrDZNsfrN-)HKFvT7 z1_X*4%pnS%@Lbef5cX^OUyhk}P@Lyey7mu0fBqfgQ)D5=(sRG{!)=yWwMgHWreaX# zW-e$DeR^jk%^I$tkhOeaD5?NsAqI`ne8|DU@xqQx+=55bz<~7~ibF8vhJZlipC^(r zNErV6cQe!c64RRp^9=kzd@xW4J!&u*j4m+|>iIPP<45SMC3W?C83$wRpQ`sy^2^HB z=eyF1HJ&_u+S}XfjMKw9n8Seh@@d?<*e0}QfZF?(#3>3mY-{lsiP8{gAGSBr*{NYl zw;j~zHTf}7U9iwxgUhUAb`M9iYs$*VFgG!Y{xvKrA|i4w`}p|ErFNn7%8Oe;N56kR z^zc9*{mgG|RgDqaIXp&%(9|_FZZ)a%#5%z2h@-AmlzRWA?`76*134$#J(A29>9+5# zSXx?I*Lu6)%61v|kXbKZhBc~!wNuGtGW5w8#|kDUCTw6J0goTXCO#h<8=ISRthedk z?YT=dBv7Y-*Y&@0!aR1TZEmX!h#|rP;JQcx| z`K6_d^z^uWyX4$xWO!?(OOIX1dd=F}nlEbihyg%_%*@R7$yzQb6us5Kp*s-E%|?o5 zh|<#4ovL;xToFJo(pf8nRg0X9R&YC=457CSXoVw$?dygO5C7a7(p%7*}z~xakDX> zyo^4aPg`DgFLb+I3dh?o?%{l<8xMim-J~>R2nSl#gXeyY^!DnyxcouC@hZ2bwYoH} zj@euqjRq``skwQTgDB_RugSr|!TXX;rbb5Wv@jJ|b{aJc?x)TXMgmb!hG4`l}?v;KKdHHx^*T3Ed z0PnwVvoJI7(P+~R6&J(IfBy!)R?d5>n7o`BUNQ*TLw+x}eVLofKjyEU3cw7Ehv$>u zf=*c2$*6sVSB+K5z@TFV428o|K0VSYNS)<^)cBrSwPI`}G9K1U0EAKj;9*{#!~O=v zxa6-x{!iHD%Wb(o;_$wBDyhQ}V`XI({HN4w z4pwE**%QG&*B!vyOgoNSo&=%m*ERACU-Ni*c-+kPR_Uc7W7GuNuC7RAK$^|k!13*> zLCJi?Q3&tb+4g1Z#CkvsodS%da5P4@n7^{E|!oQSwX;!Jiqh4@JS;!pCd01K>>U=hUyYZ2<)putGuGB?npQVSzKfy-GoKlug ztNNT~ENx9#!#-C}q8Fv?@0=NqMCgm!@7iym+2wrcIuG%_?48DLRkwLW@b zNHRIFamWt+cLVSI%%ijoa-5821kz>ab>nwMUg`&{GB7LO`*-iu(3KkbDP7KJ^q3SX z)Z#4TM^7DzDr~^re>~#b`9z@MJZN>U4nx&YRaLdLGqGP5Jrx%*@Q11itk(renw{`(KP-Rz^z0LVv{$twuDQ z$ZpF&mG4D-TFvO5Q)T!ii+}bed4I%EB&lfGh^f7RN?!f!EgIy zc3Mu{il%M^PR;~k-j}(rO^V4zDTc@3V03hJ8Itz2)%V`Dlk2`nK+pX6@gO9m!H&4O zx+=qyjJ3xjL`5I=XUjJ$ZCm><6<|6h+xEv%_@lfCXLTYw+c7*IKzl&0{ma^Vb<{*F z72813FG_2kA)RI$m-KdbZ*R4p)rEL!kS zHY-a&fTeI@e*U0YB*`H&8Y?)JsO{28#iq1gXTZ?k%O7go?iM{C`FX#H4HgwW6wb%h zKTCgR4#$56vTS%5fQyWyeX*R(_Pi693^qY@E5wftkY@C%Ym>E-2n1;cp#fP;x}=PD zKc9cOpOd3?I*@6O!7v~o0Kj9y(teA6jTX1~FLzB3sr0Xl9I6p$0D;ERIjoKjxbK{cgy1IjQR%NCG8ESJ7 zrdqdJ4&~FqSRKyTwmBWunI(on9=#egKAZMu+@>e67=ry8mn_!N&+qkYVRv4BY! zJ_6#o&VpYs7K;TE4Il_$i=>fnR63$dskiG)J0@I5SeBd=mo8Rr#cjqO%W!L`WVzgam>W-JvZduxx1 zG~bttAeI;X7%rJ#5zlT=my77jemmlS`(|G1#_DesSY8iRxQG(a;MTb=e2Jx>6l`3c zKp2>&u>o7p*ugsz461u&pn zQqo9qX=!n3c~faw1z8yd87WaIX$2`MFuUN!|LNf6isLB-HGxqIkM{ftL1Kjgi>$Q#qXoZU%yR9bs92f$meNSJ#T14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>bCLfvgo}k(8*fF!X_4rCyA}qqt8F+F!NSBPz&MHTnhDTc#w2fd zm&jS0zDNK$>?NMQuI!Jvh51e8KDs3?1PU$oba4!kxSX7DfFb9NjLe*vJ2nN3_X-3a zJQjE0;NaryZfWoCI>>rNRb}at!>JVym^KyheByf8HK~A6HB~g!w3Ii*`LO9$0j8{5 zR=cXQez9>F^GH_o9O}I}fj8BF^ZH~1Q{(LkESwQhk+*NO-M(pU;bviZnD=RMVo`5L zL2-J*hYukkAzway`?)FK#IZ0z|cb1&`{UVGQ`l-%GB7(*g)IB(8|DoT~O&AiiX_$l+3hB z+#1S!W`X0RDkP#LD6w3jpeR2rGbdG{q_QAYA+w+)nSr5V&f`x!9ED*T8mIhEpYePe z#K5e~t(VL#tSsz3S%g_w!KK0Ea0;{X<`9L`H?EvGa^{H45%$v!9t*tm7+#4BmV9zD RoeFdlgQu&X%Q~loCID><@&Nz< literal 0 HcmV?d00001 diff --git a/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png b/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..e2707f2d1e6f17270c8728efcb7230310aefdefc GIT binary patch literal 1296 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LL6ppAc7|0tRMAnGIgd zg6z^8JOwj#q}O=JZuFK~=P9+;LuS2~^m;G3O_H2?+zGJ^OOSp?2T3kG1g zVM@V8fDE8|G%ltHTmhQlXeyDF0onim|F2@Y_Zg^Ya!HV1Fsfcg9$t3QwWm%T+c7yr zSo+SdBOWgTFUhZ5wf@Za>?9@@!B2}5I5ux*o4bZt_Iy>fhPHmdzk9D@mA3D z-b1QO^Xm4#vs8O5aif`C$5xTay+V5KtFQMexN-zUf-kVn{F#3FqLkW`ecY)gA1(-e zZIZmSj3pxNvo(KzUwghIYv`tv3oUuil%F>NI*T#M+udaFdh=h+CN7 zRPLi&;zFRbr>Bc!h{fsTgau3w9*Z|H)j8VbaJ96!x+psM#5e_U1vhRvlfp7#>g4B# zS0{W*VRdEVQq$H<4pmsRXw|ZHjA08qLmS1KxAm=CCfBj@e7kQWV@GBNOOn*=2O$R% zxRPegd>oRH$e9$CSlYa`ckSH08<{6O&FBtooHu!Mw|2+FXMOfEHa1&7YJd0?c5FS+ z0{QqgTz^dd{$*x5xbA}Zk(a)V`4h~WnhM!NJTyAq_D4HLSair9Ja9rpQdCy>bjv~o zKAGr`JU*PAo+mX|X8LfjzLcCfQ!MhP!m`!2#eVNZ6i%E@PioZW2-VUkXj#&JNa@-m zp$SczOH+L!4mmEK)3|n#(3%CSq`I4CMLDxA+f}upsm+at)wIa>KtfVdejekgRmjRy*V;jCKX z8c~vxSdwa$T$Bo=7>o=IEp!bHbqy^;3{9;}jjfCgv<(cc3=G%>mENIf$jwj5OsmAL zq0DC%s6Yy@3W+EQN-S3>D9TUE%t=)!sVqoU$Sf#HW?-n8^Y{}FM`4(T#wq{PXFQ(< zF)%B0>m_pwD+_y17GV}vaA`0(oWiWUIYi;~jVmXPoH-(Mg#C1b#{w@shF9W(C7+y3 Rrvj~D@O1TaS?83{1OQGQaIF9U literal 0 HcmV?d00001 diff --git a/packages/cli/templates/default-app/packages/app/public/favicon.ico b/packages/cli/templates/default-app/packages/app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5b582704a10d20240034385f14c210db96d51603 GIT binary patch literal 15086 zcmeI2d2AF_9LJ{=UG5^DSg*Ti8pU{EyyAuN zh&M`6142+i#ef%xF&Z`4phlws8xbwKw0^$3^S0C1Id*4fh=1%a`RqG?@AsYS&6}BZ zoGPc<88ylwt#@8L$Z?K#9H+kCEe~{@P1Fq}Rr}1Ij&nO5M$iWrXDX;ZXT$PI<+rj2 zDtn-^2h!XF6BpN3pWZyA_r&HQ%t$5KNP16!OJgt@pL1a>)IkPHGBgi@Y48HP3yWYj z>{sMK*VC^9+GCQ%?@#z1mcR@c43UP$VelsW3mqPIl7Al#j~v8tjDBrH5@>rqg^OW# z@HMp6z!LE4w)1Ofuk{_EvodM@W*_L7+y;F+Lv`lRH3@b^=-2jK+v(+-OH04D;~!{( zZr3Eb9xFn-^e^L@4!f?>)UR#$9nOG)G1v4&SJb&<&ZUp(TT^iEo=IE3wqZH+H=!pi z8eD~@C&6e8OINV}dl_G!G)n(cd@h4&9;TC@3s1l%kj5~!(LN3ehW5+Ao_GAVDM*TZ5~16J}?t>-(3Z-!CZ)%_Z3r? zlz!cJ!}PD9y_V^&8rAkKqEAgrxI1nd`n7g$hGL#S%H6!A z^=mye1$vh&tVsW)_3t3=2QU%RnD6b)N?O0UCVnN*C3dhB%2E11C+6dzwX)$k^4iCm zlZ;-!bTjQ~&HD_Uvnc(3>*p#w`@u=D8vL}Ix|eD11!-9v{a!5xQ_vi5^xCQJy~lVR z63&;aUvlmOTer7-b;6px!zH2rstElLdIKh$PeT7~5&EA>ShIJya`g{kuus5yzS#MP zy#W)>$JM_ZIyOa8bDeBmC3SzmvENAg$*vvsr2A*E5^0k%1;iifNMiv*=t8dRMxu z*8loGpku9P@KX63pRMpUyaduc78?PdqoRbJHd_ZLsR|{W7h2zf7Bz+Hnf;OdLBkaZPH)XEWFi zeu_#*aBS=0L@4}EYggPQiC3}z*B;;-R|fh+*u76{?;AjC!FwanbIM#82YZ085pOrR z5MBp;Cwv-?^X=?7`VRLUaF!R1*4-b#B-QL zcbNNw!}!DD$AI|nP~TVmJQfuHO8RK#+RJ4q>G`k$ys?$nJLz-4nb=&bbMH}~`1&u+ z73yPIQ2eP@KU17Q<`1wnG@1^?sfC5`D{KdydyUY?^zo#n#@`R$ufdM5@8bm{mahFn z;CMI?dK4PFW=f4e9Cw`?cKkVAhb_8PO8owedJov&<@AgaG`~a%35u_G@UubNC2j(J z%hb7MZ+ki4FN5OSF@nnPq30loYjNZMOh6rry10Q|zqs+g!T&r6I)L{a?D`T2Op@9|x5;61XmIaQw=N{|o--mJwU7apM1jpVq&pLEPhx zYtHuO_f^pGUK_%Pw7ms-?|K332}zjr_k;cssE^kAgNHiAQ-#eF># z-c#65Mzkel8o8H+&ZTZ^2iD1bU?6zUv&r;*2UdDmLH=pb8bo86XP;(#^}pUps&j1~ zY0&)j7j2E|htJJmo)z`Kay|RVjK9_(55YBXKWKfTaW})TGv6FK3!khz#!^yUQ!?ZK zC;73tn*7h*B=T>A@iSV7_r+%p%!fnrU2?x({}bH$0i@4>{I|kU5M;(*-#=EvB3J{8 ztGY)($0x{)zu!J=#%Z4*N4MViYu?nlUf)`mgJQSB6bLip@7;g<;;;dvX(fbx&oTab zUohV7dkm@8Zt`CX+MhvY{CCpfOIQa#f!QDTk@udj6uX%5H~mejb5Hm7`Yye@7j$gr s!c{JFNN2*y5T={v@+|-E({Wa9gK6CKM)ODZG0;F+H04p*TW9?K1Dt;TIRF3v literal 0 HcmV?d00001 diff --git a/packages/cli/templates/default-app/packages/app/public/index.html b/packages/cli/templates/default-app/packages/app/public/index.html new file mode 100644 index 0000000000..ac2e9a6a73 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/public/index.html @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + Backstage + + + +
+ + + diff --git a/packages/cli/templates/default-app/packages/app/public/manifest.json b/packages/cli/templates/default-app/packages/app/public/manifest.json new file mode 100644 index 0000000000..4a7c1b4ec4 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Backstage", + "name": "Backstage", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/cli/templates/default-app/packages/app/public/robots.txt b/packages/cli/templates/default-app/packages/app/public/robots.txt new file mode 100644 index 0000000000..01b0f9a107 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg b/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg new file mode 100644 index 0000000000..3b0f666390 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/public/safari-pinned-tab.svg @@ -0,0 +1,28 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/packages/cli/templates/default-app/packages/app/src/App.test.tsx b/packages/cli/templates/default-app/packages/app/src/App.test.tsx new file mode 100644 index 0000000000..0074416375 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/src/App.test.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import App from './App'; + +describe('App', () => { + it('should render', () => { + const rendered = render(); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx new file mode 100644 index 0000000000..14a07870b2 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/src/App.tsx @@ -0,0 +1,42 @@ +import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core'; +import { BackstageTheme, createApp } from '@backstage/core'; +import React, { FC } from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import * as plugins from './plugins'; + +const useStyles = makeStyles(theme => ({ + '@global': { + html: { + height: '100%', + fontFamily: theme.typography.fontFamily, + }, + body: { + height: '100%', + fontFamily: theme.typography.fontFamily, + 'overscroll-behavior-y': 'none', + }, + a: { + color: 'inherit', + textDecoration: 'none', + }, + }, +})); + +const app = createApp(); +app.registerPlugin(...Object.values(plugins)); +const AppComponent = app.build(); + +const App: FC<{}> = () => { + useStyles(); + return ( + + + + + + + + ); +}; + +export default App; diff --git a/packages/cli/templates/default-app/packages/app/src/index.tsx b/packages/cli/templates/default-app/packages/app/src/index.tsx new file mode 100644 index 0000000000..b597a44232 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/src/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/cli/templates/default-app/packages/app/src/plugins.ts b/packages/cli/templates/default-app/packages/app/src/plugins.ts new file mode 100644 index 0000000000..6639319bbd --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/src/plugins.ts @@ -0,0 +1 @@ +export { default as WelcomePlugin } from 'plugin-welcome'; diff --git a/packages/cli/templates/default-app/packages/app/src/setupTests.ts b/packages/cli/templates/default-app/packages/app/src/setupTests.ts new file mode 100644 index 0000000000..666127af39 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; diff --git a/packages/cli/templates/default-app/packages/app/tsconfig.json b/packages/cli/templates/default-app/packages/app/tsconfig.json new file mode 100644 index 0000000000..0457810645 --- /dev/null +++ b/packages/cli/templates/default-app/packages/app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@spotify/web-scripts/config/tsconfig.json", + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "incremental": false + }, + "include": ["src"] +} diff --git a/packages/cli/templates/default-app/plugins/welcome/README.md b/packages/cli/templates/default-app/plugins/welcome/README.md new file mode 100644 index 0000000000..e9b9dd0edf --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/README.md @@ -0,0 +1,3 @@ +# Title + +Welcome to the welcome plugin! diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs new file mode 100644 index 0000000000..1aa7f2c5c7 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -0,0 +1,20 @@ +{ + "name": "plugin-welcome", + "version": "0.0.0", + "main": "dist/cjs/index.js", + "types": "dist/cjs/index.d.ts", + "private": true, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test" + }, + "devDependencies": { + "@backstage/core": "^{{version}}", + "@backstage/cli": "^{{version}}", + "@types/testing-library__jest-dom": "5.0.2" + }, + "dependencies": { + "@material-ui/lab": "4.0.0-alpha.45" + } +} diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx new file mode 100644 index 0000000000..24c79f91ee --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/Timer.tsx @@ -0,0 +1,58 @@ +import React, { FC } from 'react'; +import { HeaderLabel } from '@backstage/core'; + +const timeFormat = { hour: '2-digit', minute: '2-digit' }; +const utcOptions = { timeZone: 'UTC', ...timeFormat }; +const nycOptions = { timeZone: 'America/New_York', ...timeFormat }; +const tyoOptions = { timeZone: 'Asia/Tokyo', ...timeFormat }; +const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat }; + +const defaultTimes = { + timeNY: '', + timeUTC: '', + timeTYO: '', + timeSTO: '', +}; + +function getTimes() { + const d = new Date(); + const lang = window.navigator.language; + + // Using the browser native toLocaleTimeString instead of huge moment-tz + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString + const timeNY = d.toLocaleTimeString(lang, nycOptions); + const timeUTC = d.toLocaleTimeString(lang, utcOptions); + const timeTYO = d.toLocaleTimeString(lang, tyoOptions); + const timeSTO = d.toLocaleTimeString(lang, stoOptions); + + return { timeNY, timeUTC, timeTYO, timeSTO }; +} + +const HomePageTimer: FC<{}> = () => { + const [{ timeNY, timeUTC, timeTYO, timeSTO }, setTimes] = React.useState( + defaultTimes, + ); + + React.useEffect(() => { + setTimes(getTimes()); + + const intervalId = setInterval(() => { + setTimes(getTimes()); + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, []); + + return ( + <> + + + + + + ); +}; + +export default HomePageTimer; diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts new file mode 100644 index 0000000000..f1fc55bfe9 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/Timer/index.ts @@ -0,0 +1 @@ +export { default } from './Timer'; diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx new file mode 100644 index 0000000000..4b9b12913d --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import WelcomePage from './WelcomePage'; +import { ThemeProvider } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/core'; + +describe('WelcomePage', () => { + it('should render', () => { + const rendered = render( + + + , + ); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx new file mode 100644 index 0000000000..19f9ee55e2 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -0,0 +1,106 @@ +import React, { FC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { + Typography, + Grid, + List, + ListItem, + ListItemText, + Link, +} from '@material-ui/core'; +import Timer from '../Timer'; +import { + Content, + InfoCard, + Header, + Page, + pageTheme, + ContentHeader, + SupportButton, +} from '@backstage/core'; + +const WelcomePage: FC<{}> = () => { + const profile = { givenName: '' }; + + return ( + +
+ +
+ + + + + + + + + You now have a running instance of Backstage! + + 🎉 + + Let's make sure you get the most out of this platform by walking + you through the basics. + + + The Setup + + + Backstage is put together from three base concepts: the core, + the app and the plugins. + + + + + + + + + + + + + + Try It Out + + + We suggest you either check out the documentation for{' '} + + creating a plugin + {' '} + or have a look in the code for the{' '} + + Home Page + {' '} + in the directory "plugins/home-page/src". + + + + + + Quick Links + + + backstage.io + + + + Create a plugin + + + + + + + +
+ ); +}; + +export default WelcomePage; diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts new file mode 100644 index 0000000000..b031301e7e --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/index.ts @@ -0,0 +1 @@ +export { default } from './WelcomePage'; diff --git a/packages/cli/templates/default-app/plugins/welcome/src/index.ts b/packages/cli/templates/default-app/plugins/welcome/src/index.ts new file mode 100644 index 0000000000..b68aea57f9 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts b/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts new file mode 100644 index 0000000000..f61dee5690 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/plugin.test.ts @@ -0,0 +1,7 @@ +import plugin from './plugin'; + +describe('welcome', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts b/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts new file mode 100644 index 0000000000..35ceddd65f --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/plugin.ts @@ -0,0 +1,9 @@ +import { createPlugin } from '@backstage/core'; +import WelcomePage from './components/WelcomePage'; + +export default createPlugin({ + id: 'welcome', + register({ router }) { + router.registerRoute('/', WelcomePage); + }, +}); diff --git a/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts b/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts new file mode 100644 index 0000000000..666127af39 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; diff --git a/packages/cli/templates/default-app/plugins/welcome/tsconfig.json b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json new file mode 100644 index 0000000000..596e2cf729 --- /dev/null +++ b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/packages/cli/templates/default-app/prettier.config.js b/packages/cli/templates/default-app/prettier.config.js new file mode 100644 index 0000000000..93df970dd6 --- /dev/null +++ b/packages/cli/templates/default-app/prettier.config.js @@ -0,0 +1 @@ +module.exports = require('@spotify/web-scripts/config/prettier.config.js'); diff --git a/packages/cli/templates/default-app/tsconfig.json b/packages/cli/templates/default-app/tsconfig.json new file mode 100644 index 0000000000..25f61cc933 --- /dev/null +++ b/packages/cli/templates/default-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@spotify/web-scripts/config/tsconfig.json", + "exclude": ["**/*.test.*"], + "compilerOptions": { + "allowJs": true, + "noEmit": false, + "declarationMap": true, + "incremental": true + } +} From d21d38e947a5c2015008595521e6cc5ac9f5b90e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2020 11:21:01 +0200 Subject: [PATCH 5/6] cli: make package.json reading absolute --- packages/cli/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3eafc6dc6b..08f14d3c00 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,6 +17,7 @@ import program from 'commander'; import chalk from 'chalk'; import fs from 'fs'; +import { resolve as resolvePath } from 'path'; import createAppCommand from './commands/create-app/createApp'; import createPluginCommand from './commands/create-plugin/createPlugin'; import watch from './commands/watch-deps'; @@ -29,7 +30,9 @@ import pluginServe from './commands/plugin/serve'; import { exitWithError } from './helpers/errors'; const main = (argv: string[]) => { - const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')); + const packageJson = JSON.parse( + fs.readFileSync(resolvePath(__dirname, '../package.json'), 'utf-8'), + ); program.name('backstage-cli').version(packageJson.version ?? '0.0.0'); From e96e95df6650ef0dc4b91e2bfa28182932b9761a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2020 11:51:28 +0200 Subject: [PATCH 6/6] cli/commands/create-plugin: update tests --- .../create-plugin/createPlugin.test.ts | 96 ++++--------------- packages/cli/src/helpers/tasks.test.ts | 53 ++++++++++ 2 files changed, 71 insertions(+), 78 deletions(-) create mode 100644 packages/cli/src/helpers/tasks.test.ts diff --git a/packages/cli/src/commands/create-plugin/createPlugin.test.ts b/packages/cli/src/commands/create-plugin/createPlugin.test.ts index 9bf50027e3..57fa33d173 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.test.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.test.ts @@ -14,115 +14,55 @@ * limitations under the License. */ -import fs from 'fs'; +import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import del from 'del'; -import { - createFileFromTemplate, - createFromTemplateDir, - createTemporaryPluginFolder, - movePlugin, -} from './createPlugin'; +import { createTemporaryPluginFolder, movePlugin } from './createPlugin'; describe('createPlugin', () => { describe('createPluginFolder', () => { - it('should create a temporary plugin directory in the correct place', () => { + it('should create a temporary plugin directory in the correct place', async () => { const id = 'testPlugin'; const tempDir = path.join(os.tmpdir(), id); try { - createTemporaryPluginFolder(tempDir); - expect(fs.existsSync(tempDir)).toBe(true); + await createTemporaryPluginFolder(tempDir); + await expect(fs.pathExists(tempDir)).resolves.toBe(true); expect(tempDir).toMatch(id); } finally { - del.sync(tempDir, { force: true }); + await del(tempDir, { force: true }); } }); - it('should not create a temporary plugin directory if it already exists', () => { + it('should not create a temporary plugin directory if it already exists', async () => { const id = 'testPlugin'; const tempDir = path.join(os.tmpdir(), id); try { - createTemporaryPluginFolder(tempDir); - expect(fs.existsSync(tempDir)).toBe(true); - expect(() => createTemporaryPluginFolder(tempDir)).toThrowError( + await createTemporaryPluginFolder(tempDir); + await expect(fs.pathExists(tempDir)).resolves.toBe(true); + await expect(createTemporaryPluginFolder(tempDir)).rejects.toThrow( /Failed to create temporary plugin directory/, ); } finally { - del.sync(tempDir, { force: true }); - } - }); - }); - - describe('createFileFromTemplate', () => { - it('should generate a valid output with inserted values', () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); - try { - const sourceData = - '{"name": "@backstage/{{id}}", "version": "{{version}}"}'; - const targetData = '{"name": "@backstage/foo", "version": "0.0.0"}'; - const sourcePath = path.join(tempDir, 'in.hbs'); - const targetPath = path.join(tempDir, 'out.json'); - fs.writeFileSync(sourcePath, sourceData); - - createFileFromTemplate(sourcePath, targetPath, { id: 'foo' }, '0.0.0'); - - expect(fs.existsSync(targetPath)).toBe(true); - expect(fs.readFileSync(targetPath).toString()).toBe(targetData); - } finally { - del.sync(tempDir, { force: true }); - } - }); - }); - - describe('createFromTemplateDir', () => { - it('should create sub-directories and files', async () => { - const templateRootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); - const templateSubDir = fs.mkdtempSync(path.join(templateRootDir, 'sub-')); - fs.writeFileSync(path.join(templateSubDir, 'test.txt'), 'testing'); - - const destinationRootDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'test-'), - ); - const subDir = path.join( - destinationRootDir, - path.basename(templateSubDir), - ); - const testFile = path.join( - destinationRootDir, - path.basename(templateSubDir), - 'test.txt', - ); - try { - await createFromTemplateDir( - templateRootDir, - destinationRootDir, - {}, - '0.0.0', - ); - expect(fs.existsSync(subDir)).toBe(true); - expect(fs.existsSync(testFile)).toBe(true); - } finally { - await del(templateRootDir, { force: true }); - await del(destinationRootDir, { force: true }); + await del(tempDir, { force: true }); } }); }); describe('movePlugin', () => { - it('should move the temporary plugin directory to its final place', () => { + it('should move the temporary plugin directory to its final place', async () => { const id = 'testPlugin'; const tempDir = path.join(os.tmpdir(), id); - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-')); const pluginDir = path.join(rootDir, 'plugins', id); try { - createTemporaryPluginFolder(tempDir); - movePlugin(tempDir, pluginDir, id); - expect(fs.existsSync(pluginDir)).toBe(true); + await createTemporaryPluginFolder(tempDir); + await movePlugin(tempDir, pluginDir, id); + await expect(fs.pathExists(pluginDir)).resolves.toBe(true); expect(pluginDir).toMatch(`/plugins\/${id}`); } finally { - del.sync(tempDir, { force: true }); - del.sync(rootDir, { force: true }); + await del(tempDir, { force: true }); + await del(rootDir, { force: true }); } }); }); diff --git a/packages/cli/src/helpers/tasks.test.ts b/packages/cli/src/helpers/tasks.test.ts new file mode 100644 index 0000000000..871d5a4d37 --- /dev/null +++ b/packages/cli/src/helpers/tasks.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 os from 'os'; +import del from 'del'; +import { templatingTask } from './tasks'; + +describe('templatingTask', () => { + it('should template a directory with mix of regular files and templates', async () => { + // Set up a testing template directory + const tmplDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'test-')); + await fs.ensureDir(resolvePath(tmplDir, 'sub')); + await fs.writeFile(resolvePath(tmplDir, 'test.txt'), 'testing'); + await fs.writeFile( + resolvePath(tmplDir, 'sub/version.txt.hbs'), + 'version: {{version}}', + ); + + // Set up a temporary dest dir to write the template to + const destDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'test-')); + + try { + await templatingTask(tmplDir, destDir, { + version: '0.0.0', + }); + + await expect( + fs.readFile(resolvePath(destDir, 'test.txt'), 'utf8'), + ).resolves.toBe('testing'); + await expect( + fs.readFile(resolvePath(destDir, 'sub/version.txt'), 'utf8'), + ).resolves.toBe('version: 0.0.0'); + } finally { + await del(tmplDir, { force: true }); + await del(destDir, { force: true }); + } + }); +});