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/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/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index da695bee95..1b35d99a59 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -16,86 +16,44 @@ import fs from 'fs-extra'; import path from 'path'; -import handlebars from 'handlebars'; +import { promisify } from 'util'; 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, templatingTask } 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) { + await Task.forItem('checking', id, async () => { + const destination = path.join(rootDir, 'plugins', id); -const checkExists = (rootDir: string, id: string) => { - console.log(); - console.log(chalk.green(' Checking if the plugin already exists:')); - - 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) { + 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 +67,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 +77,20 @@ 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:')); - +) { 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,187 +102,72 @@ 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); -}; - -export const addPluginToApp = (rootDir: string, pluginName: string) => { - console.log(); - console.log(chalk.green(' Import plugin in app:')); + await fs.writeFile(packageFile, newContents, 'utf-8').catch(error => { + throw new Error( + `Failed to add plugin as dependency to app: ${packageFile}: ${error.message}`, + ); + }); + }); +} +export async function addPluginToApp(rootDir: string, pluginName: string) { const pluginPackage = `@backstage/plugin-${pluginName}`; const pluginNameCapitalized = pluginName .split('-') .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); -}; - -export const createFromTemplateDir = async ( - templateFolder: string, - destinationFolder: string, - 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, + await Task.forItem('processing', pluginsFilePath, async () => { + await addExportStatement(pluginsFile, pluginExport).catch(error => { + throw new Error( + `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, ); - } 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}`, - ); - } - } + }); }); -}; +} -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 { +async function cleanUp(tempDir: string) { + await Task.forItem('remove', 'temporary directory', async () => { await fs.remove(tempDir); - spinner.succeed(); - } catch (e) { - spinner.fail(); - console.log(chalk.red(`Failed to cleanup: ${e.message}`)); - } -}; - -const buildPlugin = async (pluginFolder: string) => { - console.log(); - console.log(chalk.green(` Building the plugin:`)); + }); +} +async function buildPlugin(pluginFolder: string) { 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:`)); +) { + 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}`, + ); + }); + }); +} - 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}`, - ); - } -}; - -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[] = [ @@ -381,25 +216,37 @@ const createPlugin = 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; 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 createFromTemplateDir(templateFolder, tempDir, answers, version); - movePlugin(tempDir, pluginDir, answers.id); + Task.section('Checking if the plugin ID is available'); + await checkExists(rootDir, answers.id); + + Task.section('Creating a temporary plugin directory'); + await createTemporaryPluginFolder(tempDir); + + 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 (existsSync(appPackage)) { - addPluginDependencyToApp(rootDir, answers.id, version); - addPluginToApp(rootDir, answers.id); + 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); } if (ownerIds && ownerIds.length) { @@ -410,24 +257,21 @@ 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}`, + )}`, ); - console.log(); - } catch (e) { - console.log(); - console.log(`${chalk.red(e.message)}`); - console.log(); - await cleanUp(tempDir, answers.id); - console.log(); - console.log(`🔥 ${chalk.red('Failed to create plugin!')}`); - console.log(); + Task.log(); + } catch (error) { + 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!'); } }; - -export default createPlugin; 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); } } 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 }); + } + }); +}); diff --git a/packages/cli/src/helpers/tasks.ts b/packages/cli/src/helpers/tasks.ts new file mode 100644 index 0000000000..3cba4d262f --- /dev/null +++ b/packages/cli/src/helpers/tasks.ts @@ -0,0 +1,101 @@ +/* + * 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 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; + +export class Task { + static log(name: string = '') { + 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`); + } + + 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; + } + } +} + +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}`, + ); + }); + }); + } + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 997c05c18c..31cc0f95ce 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,6 +17,8 @@ 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'; import buildCache from './commands/build-cache'; @@ -29,10 +31,17 @@ 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'); + 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 0000000000..3d56edbb0d Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/android-chrome-192x192.png differ diff --git a/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png b/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png new file mode 100644 index 0000000000..0977175f6f Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/apple-touch-icon.png differ diff --git a/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png b/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png new file mode 100644 index 0000000000..a455ffac7b Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon-16x16.png differ 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 0000000000..e2707f2d1e Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon-32x32.png differ 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 0000000000..5b582704a1 Binary files /dev/null and b/packages/cli/templates/default-app/packages/app/public/favicon.ico differ 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 + } +}