From 5fbc348f344aaba16f55a3f5ce830c12b55d1181 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Thu, 19 Mar 2020 13:45:45 -0500 Subject: [PATCH 01/14] Updated purpleBlue in PageThemeProvider --- packages/core/src/layout/Page/PageThemeProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/layout/Page/PageThemeProvider.ts b/packages/core/src/layout/Page/PageThemeProvider.ts index 64c90b78a1..64f8a9f8a7 100644 --- a/packages/core/src/layout/Page/PageThemeProvider.ts +++ b/packages/core/src/layout/Page/PageThemeProvider.ts @@ -45,7 +45,7 @@ export const gradients: Record = { colors: ['#F13DA2', '#FF8A48'], }, purpleBlue: { - colors: ['#4100F4', '#AF2996'], + colors: ['#2D00AA', '#C769B5'], }, tealGreen: { colors: ['#19E68C', '#1D7F6E'], From 557880a36f17e81848a6f1d4dc0c119665b1baf2 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Tue, 31 Mar 2020 12:53:58 -0600 Subject: [PATCH 02/14] Added removePlugin --- package.json | 1 + .../remove-plugin/removePlugin.test.ts | 47 +++++ .../commands/remove-plugin/removePlugin.ts | 189 ++++++++++++++++++ packages/cli/src/index.ts | 6 + 4 files changed, 243 insertions(+) create mode 100644 packages/cli/src/commands/remove-plugin/removePlugin.test.ts create mode 100644 packages/cli/src/commands/remove-plugin/removePlugin.ts diff --git a/package.json b/package.json index 8a862e04b5..828b18c88d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "lerna run build", "test": "cross-env CI=true lerna run test --since origin/master -- --coverage", "create-plugin": "backstage-cli create-plugin", + "remove-plugin": "backstage-cli remove-plugin", "release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push; fi", "lint": "cross-env CI=true lerna run lint --since origin/master --", "storybook": "yarn workspace storybook start" diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts new file mode 100644 index 0000000000..5253fdf301 --- /dev/null +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -0,0 +1,47 @@ +// Test Suite for removePlugin command. + +import fse from 'fs-extra' +import path from 'path' +import {// removeExportStatementFromPlugins, + removePluginDependencyFromApp, + //removePluginDirectory, +} from './removePlugin' + +const rootDir = fse.realpathSync(process.cwd().replace('/cli', '')); + +// test remove export statement +describe('removePlugin', () => { + describe('Remove Plugin Dependencies', () => { + // Set up test + // Copy contents of package file for test + const packageFile = path.join(rootDir, 'app', 'package.json'); + const testFilePath = path.join(rootDir, 'app', 'test.json'); + const testPluginName = 'yarn-test-package'; + const testPluginPackage = `@spotify-backstage/plugin-${testPluginName}`; + + let packageFileContents = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + packageFileContents.dependencies[testPluginPackage] = "0.1.0"; + + it('should remove plugin dependency from /packages/app/package.json', async () => { + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContents, null, 2)}\n`, + 'utf8'); + console.log(JSON.parse(fse.readFileSync(testFilePath, 'utf8'))); + try { + await removePluginDependencyFromApp(testFilePath, testPluginName); + expect(JSON.parse(fse.readFileSync(testFilePath, 'utf8')).hasOwnProperty(testPluginPackage)).toBe(false); + } finally { + fse.removeSync(testFilePath); + } + }); + }); +}); + + +// test remove plugin dependency from app + +// remove plugin from directory + +// remove symlink from lerna scope \ No newline at end of file diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts new file mode 100644 index 0000000000..86444ab640 --- /dev/null +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -0,0 +1,189 @@ +/* + * 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 fse from 'fs-extra'; +import path from 'path'; +// 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 { resolve as resolvePath } from 'path'; +import { realpathSync, /*existsSync*/ } from 'fs'; +// import os from 'os'; +import ora from 'ora'; + +const MARKER_SUCCESS = chalk.green(` ✔︎`); +const MARKER_FAILURE = chalk.red(` ✘`); + +export const checkExists = async (rootDir: string, pluginName: string) => { + const destination = path.join(rootDir, 'plugins', pluginName); + const spinner = ora({ + prefixText: `Checking plugin exists.`, + spinner: 'arc', + color: 'green', + }).start(); + try { + let pathExist = await fse.pathExists(destination); + if (pathExist) { + spinner.succeed(); + console.log(chalk.green(` Plugin ID ${chalk.cyan(pluginName)} exists at: ${destination.replace(`${rootDir}`, '')} ${MARKER_SUCCESS}`)); + } else { + throw new Error(chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`)); + } + } catch (e) { + spinner.fail(); + throw new Error(chalk.red(` There was an error removing plugin ${chalk.cyan(pluginName)}: ${e.message}`)); + } +} + +export const removePluginDirectory = async (destination: string, pluginName: string) => { + console.log(` Removing plugin files ${chalk.cyan(destination)}.`); + try { + await fse.remove(destination); + console.log(chalk.green(` Plugin files removed successfully. ${MARKER_SUCCESS}`)); + } catch (e) { + throw Error(` Could not remove Plugin\t${pluginName}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`) + } +} + +export const removeSymLink = async (destination: string) => { + console.log(` Removing symbolic link if it exists at:\t${chalk.cyan(destination)}.`) + const symLinkExists = fse.pathExists(destination); + if (symLinkExists) { + try { + await fse.remove(destination); + console.log(chalk.green(` Symbolic link successfully removed. ${MARKER_SUCCESS}`)) + } catch (e) { + throw Error(` Could not remove symbolic link\t${destination}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`) + } + + } +} + +export const removeExportStatementFromPlugins = async (pluginsFile: string, pluginName: string) => { + const pluginNameCapitalized = pluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + + console.log(` Removing export statement from ${chalk.cyan(pluginsFile.replace(pluginsFile.split('/app', 1)[0], ''))}`); // remove long path + try { + let originalContent = await fse.readFile(pluginsFile, 'utf8'); + const contentAfterRemoval = originalContent + .split('\n') + .filter(Boolean) // get rid of empty lines + .filter(statement => { return !statement.includes(`${pluginNameCapitalized}`) }) // get rid of lines with pluginName + .sort() + .concat(['']) // newline at end of line + .join('\n'); + await fse.writeFile(pluginsFile, contentAfterRemoval, 'utf8'); + const finalContent = await fse.readFile(pluginsFile, 'utf8'); + if (finalContent === originalContent) + throw new Error(`File was not modified.`); + console.log(chalk.green(` Successfully removed export statement from /app/src/plugin.ts ${MARKER_SUCCESS}`)); + } catch (e) { + throw new Error(chalk.red(` There was an error removing export statement for plugin ${chalk.cyan(pluginNameCapitalized)} ${MARKER_FAILURE} ${e.message}`)); + } +}; + +export const removePluginDependencyFromApp = async ( + packageFile: string, + pluginName: string, +) => { + + const pluginPackage = `@spotify-backstage/plugin-${pluginName}`; + + console.log(` Removing plugin from app dependencies ${chalk.cyan(packageFile.replace(`${packageFile}/packages`, ''))}:`); + + try { + const packageFileContent = await fse.readFile(packageFile, 'utf-8'); + const packageFileContentJSON = JSON.parse(packageFileContent); + const dependencies = packageFileContentJSON.dependencies; + + if (!dependencies[pluginPackage]) { + throw new Error( + chalk.red(` Plugin ${chalk.cyan(pluginPackage)} does not exist in ${chalk.yellow(packageFile)}`), + ); + } + + delete dependencies[pluginPackage]; + await fse.writeFile( + packageFile, + `${JSON.stringify(packageFileContentJSON, null, 2)}\n`, + 'utf-8', + ); + + console.log(chalk.green(` Successfully removed plugin from app dependencies. ${MARKER_SUCCESS}`)); + } catch (e) { + throw new Error( + `${chalk.red(` Failed to remove plugin as dependency in app: ${chalk.cyan(packageFile)}:`)} ${e.message}`, + ); + } +} + +const capitalize = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); + +const removePlugin = async () => { + const questions: Question[] = [ + { + type: 'input', + name: 'pluginName', + message: chalk.blue('Enter the ID of the plugin to be removed [required]'), + validate: (value: any) => { + if (!value) { + return chalk.red('Please enter an ID for the plugin'); + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return chalk.red( + 'Plugin IDs must be kehbab-cased and contain only letters, digits and dashes.' + ); + } + return true; + }, + }, + ]; + + const answers: Answers = await inquirer.prompt(questions); + + const rootDir = realpathSync(process.cwd()); + const pluginName: string = answers.pluginName; + const packageFile = path.join(rootDir, 'packages', 'app', 'package.json'); + const pluginsFile = path.join(rootDir, 'packages', 'app', 'src', 'plugins.ts'); + const pluginDirectory = path.join(rootDir, 'plugins', pluginName); + const pluginScopedDirectory = path.join(rootDir, `node_modules/@spotify-backstage/plugin-${pluginName}`); + + console.log(pluginScopedDirectory); + try { + + await checkExists(rootDir, pluginName); + await removeExportStatementFromPlugins(pluginsFile, pluginName); + await removePluginDependencyFromApp(packageFile, pluginName); + await removePluginDirectory(pluginDirectory, pluginName); + await removeSymLink(pluginScopedDirectory); + + console.log(chalk.green(`Successfully removed plugin ${chalk.cyan(pluginName)} from app.`)); + + } catch (e) { + // If error, restore files + console.log(e); + throw new Error( + chalk.red(`Failed to remove plugin: ${chalk.cyan(pluginName)}: ${e.message}`), + ); + } + +}; + +export default removePlugin; \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 997c05c18c..44e30fa2c8 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -18,6 +18,7 @@ import program from 'commander'; import chalk from 'chalk'; import fs from 'fs'; import createPluginCommand from './commands/create-plugin/createPlugin'; +import removePluginCommand from './commands/remove-plugin/removePlugin'; import watch from './commands/watch-deps'; import buildCache from './commands/build-cache'; import lintCommand from './commands/lint'; @@ -48,6 +49,11 @@ const main = (argv: string[]) => { .description('Creates a new plugin in the current repository') .action(actionHandler(createPluginCommand)); + program + .command('remove-plugin') + .description('Removes plugin in the current repository') + .action(actionHandler(removePluginCommand)); + program .command('plugin:build') .option('--watch', 'Enable watch mode') From ecc2dbe6584736405955309f61cb725fee15a860 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Tue, 31 Mar 2020 14:19:36 -0600 Subject: [PATCH 03/14] remove-plugin removes codeowners statement --- .../commands/remove-plugin/removePlugin.ts | 390 +++++++++++------- 1 file changed, 249 insertions(+), 141 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 86444ab640..25553fa96e 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -21,169 +21,277 @@ import inquirer, { Answers, Question } from 'inquirer'; // import recursive from 'recursive-readdir'; // import { execSync } from 'child_process'; // import { resolve as resolvePath } from 'path'; -import { realpathSync, /*existsSync*/ } from 'fs'; +import { realpathSync /*existsSync*/ } from 'fs'; // import os from 'os'; import ora from 'ora'; const MARKER_SUCCESS = chalk.green(` ✔︎`); const MARKER_FAILURE = chalk.red(` ✘`); +const BACKSTAGE = '@backstage'; export const checkExists = async (rootDir: string, pluginName: string) => { - const destination = path.join(rootDir, 'plugins', pluginName); - const spinner = ora({ - prefixText: `Checking plugin exists.`, - spinner: 'arc', - color: 'green', - }).start(); - try { - let pathExist = await fse.pathExists(destination); - if (pathExist) { - spinner.succeed(); - console.log(chalk.green(` Plugin ID ${chalk.cyan(pluginName)} exists at: ${destination.replace(`${rootDir}`, '')} ${MARKER_SUCCESS}`)); - } else { - throw new Error(chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`)); - } - } catch (e) { - spinner.fail(); - throw new Error(chalk.red(` There was an error removing plugin ${chalk.cyan(pluginName)}: ${e.message}`)); + const destination = path.join(rootDir, 'plugins', pluginName); + const spinner = ora({ + prefixText: ` Checking plugin exists.`, + spinner: 'arc', + color: 'green', + }).start(); + try { + let pathExist = await fse.pathExists(destination); + if (pathExist) { + spinner.succeed(); + console.log( + chalk.green( + ` Plugin ID ${chalk.cyan( + pluginName, + )} exists at: ${destination.replace( + `${rootDir}`, + '', + )} ${MARKER_SUCCESS}`, + ), + ); + } else { + throw new Error( + chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`), + ); } -} + } catch (e) { + spinner.fail(); + throw new Error( + chalk.red( + ` There was an error removing plugin ${chalk.cyan(pluginName)}: ${ + e.message + }`, + ), + ); + } +}; -export const removePluginDirectory = async (destination: string, pluginName: string) => { - console.log(` Removing plugin files ${chalk.cyan(destination)}.`); - try { - await fse.remove(destination); - console.log(chalk.green(` Plugin files removed successfully. ${MARKER_SUCCESS}`)); - } catch (e) { - throw Error(` Could not remove Plugin\t${pluginName}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`) - } -} +export const removePluginDirectory = async ( + destination: string, + pluginName: string, +) => { + console.log(` Removing plugin files ${chalk.cyan(destination)}.`); + try { + await fse.remove(destination); + console.log( + chalk.green(` Plugin files removed successfully. ${MARKER_SUCCESS}`), + ); + } catch (e) { + throw Error( + ` Could not remove Plugin\t${pluginName}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`, + ); + } +}; export const removeSymLink = async (destination: string) => { - console.log(` Removing symbolic link if it exists at:\t${chalk.cyan(destination)}.`) - const symLinkExists = fse.pathExists(destination); - if (symLinkExists) { - try { - await fse.remove(destination); - console.log(chalk.green(` Symbolic link successfully removed. ${MARKER_SUCCESS}`)) - } catch (e) { - throw Error(` Could not remove symbolic link\t${destination}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`) - } - - } -} - -export const removeExportStatementFromPlugins = async (pluginsFile: string, pluginName: string) => { - const pluginNameCapitalized = pluginName - .split('-') - .map(name => capitalize(name)) - .join(''); - - console.log(` Removing export statement from ${chalk.cyan(pluginsFile.replace(pluginsFile.split('/app', 1)[0], ''))}`); // remove long path + console.log( + ` Removing symbolic link if it exists at:\t${chalk.cyan(destination)}.`, + ); + const symLinkExists = fse.pathExists(destination); + if (symLinkExists) { try { - let originalContent = await fse.readFile(pluginsFile, 'utf8'); - const contentAfterRemoval = originalContent - .split('\n') - .filter(Boolean) // get rid of empty lines - .filter(statement => { return !statement.includes(`${pluginNameCapitalized}`) }) // get rid of lines with pluginName - .sort() - .concat(['']) // newline at end of line - .join('\n'); - await fse.writeFile(pluginsFile, contentAfterRemoval, 'utf8'); - const finalContent = await fse.readFile(pluginsFile, 'utf8'); - if (finalContent === originalContent) - throw new Error(`File was not modified.`); - console.log(chalk.green(` Successfully removed export statement from /app/src/plugin.ts ${MARKER_SUCCESS}`)); + await fse.remove(destination); + console.log( + chalk.green(` Symbolic link successfully removed. ${MARKER_SUCCESS}`), + ); } catch (e) { - throw new Error(chalk.red(` There was an error removing export statement for plugin ${chalk.cyan(pluginNameCapitalized)} ${MARKER_FAILURE} ${e.message}`)); + throw Error( + ` Could not remove symbolic link\t${destination}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`, + ); } + } +}; + +export const removeStatementContainingID = async (file: string, ID: string) => { + const originalContent = await fse.readFile(file, 'utf8'); + const contentAfterRemoval = originalContent + .split('\n') + .filter(Boolean) // get rid of empty lines + .filter(statement => { + return !statement.includes(`${ID}`); + }) // get rid of lines with pluginName + .sort() + .concat(['']) // newline at end of line + .join('\n'); + await fse.writeFile(file, contentAfterRemoval, 'utf8'); + const finalContent = await fse.readFile(file, 'utf8'); + if (finalContent === originalContent) + throw new Error(`File was not modified.`); +}; + +export const removeExportStatementFromPlugins = async ( + pluginsFile: string, + pluginName: string, +) => { + const pluginNameCapitalized = pluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + console.log( + ` Removing export statement from ${chalk.cyan( + pluginsFile.replace(pluginsFile.split('/app', 1)[0], ''), + )}`, + ); // remove long path + try { + await removeStatementContainingID(pluginsFile, pluginNameCapitalized); + console.log( + chalk.green( + ` Successfully removed export statement from /app/src/plugin.ts ${MARKER_SUCCESS}`, + ), + ); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing export statement for plugin ${chalk.cyan( + pluginNameCapitalized, + )} ${MARKER_FAILURE} ${e.message}`, + ), + ); + } +}; + +export const removePluginFromCodeOwners = async ( + codeOwnersFile: string, + pluginName: string, +) => { + console.log( + ` Removing teams and owners from ${chalk.cyan( + codeOwnersFile.replace(codeOwnersFile.split('/.git', 1)[0], ''), + )}`, + ); // remove long path + try { + await removeStatementContainingID(codeOwnersFile, pluginName); + console.log( + chalk.green( + ` Successfully removed codeowners statement from /.git/CODEOWNERS ${MARKER_SUCCESS}`, + ), + ); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing code owners statement for plugin ${chalk.cyan( + pluginName, + )} ${MARKER_FAILURE} ${e.message}`, + ), + ); + } }; export const removePluginDependencyFromApp = async ( - packageFile: string, - pluginName: string, + packageFile: string, + pluginName: string, ) => { + const pluginPackage = `${BACKSTAGE}/plugin-${pluginName}`; - const pluginPackage = `@spotify-backstage/plugin-${pluginName}`; + console.log( + ` Removing plugin from app dependencies ${chalk.cyan( + packageFile.replace(`${packageFile}/packages`, ''), + )}:`, + ); - console.log(` Removing plugin from app dependencies ${chalk.cyan(packageFile.replace(`${packageFile}/packages`, ''))}:`); + try { + const packageFileContent = await fse.readFile(packageFile, 'utf-8'); + const packageFileContentJSON = JSON.parse(packageFileContent); + const dependencies = packageFileContentJSON.dependencies; - try { - const packageFileContent = await fse.readFile(packageFile, 'utf-8'); - const packageFileContentJSON = JSON.parse(packageFileContent); - const dependencies = packageFileContentJSON.dependencies; - - if (!dependencies[pluginPackage]) { - throw new Error( - chalk.red(` Plugin ${chalk.cyan(pluginPackage)} does not exist in ${chalk.yellow(packageFile)}`), - ); - } - - delete dependencies[pluginPackage]; - await fse.writeFile( - packageFile, - `${JSON.stringify(packageFileContentJSON, null, 2)}\n`, - 'utf-8', - ); - - console.log(chalk.green(` Successfully removed plugin from app dependencies. ${MARKER_SUCCESS}`)); - } catch (e) { - throw new Error( - `${chalk.red(` Failed to remove plugin as dependency in app: ${chalk.cyan(packageFile)}:`)} ${e.message}`, - ); - } -} - -const capitalize = (str: string): string => - str.charAt(0).toUpperCase() + str.slice(1); - -const removePlugin = async () => { - const questions: Question[] = [ - { - type: 'input', - name: 'pluginName', - message: chalk.blue('Enter the ID of the plugin to be removed [required]'), - validate: (value: any) => { - if (!value) { - return chalk.red('Please enter an ID for the plugin'); - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return chalk.red( - 'Plugin IDs must be kehbab-cased and contain only letters, digits and dashes.' - ); - } - return true; - }, - }, - ]; - - const answers: Answers = await inquirer.prompt(questions); - - const rootDir = realpathSync(process.cwd()); - const pluginName: string = answers.pluginName; - const packageFile = path.join(rootDir, 'packages', 'app', 'package.json'); - const pluginsFile = path.join(rootDir, 'packages', 'app', 'src', 'plugins.ts'); - const pluginDirectory = path.join(rootDir, 'plugins', pluginName); - const pluginScopedDirectory = path.join(rootDir, `node_modules/@spotify-backstage/plugin-${pluginName}`); - - console.log(pluginScopedDirectory); - try { - - await checkExists(rootDir, pluginName); - await removeExportStatementFromPlugins(pluginsFile, pluginName); - await removePluginDependencyFromApp(packageFile, pluginName); - await removePluginDirectory(pluginDirectory, pluginName); - await removeSymLink(pluginScopedDirectory); - - console.log(chalk.green(`Successfully removed plugin ${chalk.cyan(pluginName)} from app.`)); - - } catch (e) { - // If error, restore files - console.log(e); - throw new Error( - chalk.red(`Failed to remove plugin: ${chalk.cyan(pluginName)}: ${e.message}`), - ); + if (!dependencies[pluginPackage]) { + throw new Error( + chalk.red( + ` Plugin ${chalk.cyan( + pluginPackage, + )} does not exist in ${chalk.yellow(packageFile)}`, + ), + ); } + delete dependencies[pluginPackage]; + await fse.writeFile( + packageFile, + `${JSON.stringify(packageFileContentJSON, null, 2)}\n`, + 'utf-8', + ); + + console.log( + chalk.green( + ` Successfully removed plugin from app dependencies. ${MARKER_SUCCESS}`, + ), + ); + } catch (e) { + throw new Error( + `${chalk.red( + ` Failed to remove plugin as dependency in app: ${chalk.cyan( + packageFile, + )}:`, + )} ${e.message}`, + ); + } }; -export default removePlugin; \ No newline at end of file +const capitalize = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); + +const removePlugin = async () => { + const questions: Question[] = [ + { + type: 'input', + name: 'pluginName', + message: chalk.blue( + 'Enter the ID of the plugin to be removed [required]', + ), + validate: (value: any) => { + if (!value) { + return chalk.red('Please enter an ID for the plugin'); + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return chalk.red( + 'Plugin IDs must be kehbab-cased and contain only letters, digits and dashes.', + ); + } + return true; + }, + }, + ]; + + const answers: Answers = await inquirer.prompt(questions); + + const rootDir = realpathSync(process.cwd()); + const codeOwnersFile = path.join(rootDir, '.github', 'CODEOWNERS'); + const pluginName: string = answers.pluginName; + const packageFile = path.join(rootDir, 'packages', 'app', 'package.json'); + const pluginsFile = path.join( + rootDir, + 'packages', + 'app', + 'src', + 'plugins.ts', + ); + const pluginDirectory = path.join(rootDir, 'plugins', pluginName); + const pluginScopedDirectory = path.join( + rootDir, + `node_modules/${BACKSTAGE}/plugin-${pluginName}`, + ); + console.log(pluginScopedDirectory); + try { + await checkExists(rootDir, pluginName); + await removeExportStatementFromPlugins(pluginsFile, pluginName); + await removePluginDependencyFromApp(packageFile, pluginName); + await removePluginDirectory(pluginDirectory, pluginName); + await removeSymLink(pluginScopedDirectory); + await removePluginFromCodeOwners(codeOwnersFile, pluginName); + console.log( + chalk.green( + `Successfully removed plugin ${chalk.cyan(pluginName)} from app.`, + ), + ); + } catch (e) { + // If error, restore files + console.log(e); + throw new Error( + chalk.red( + `Failed to remove plugin: ${chalk.cyan(pluginName)}: ${e.message}`, + ), + ); + } +}; + +export default removePlugin; From 0a70a0a26baaadf6b9c36151d3639a166d9be261 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Tue, 31 Mar 2020 14:27:54 -0600 Subject: [PATCH 04/14] Removed unnecessary comments --- packages/cli/src/commands/remove-plugin/removePlugin.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 25553fa96e..b1fa469be2 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -15,15 +15,11 @@ */ import fse from 'fs-extra'; import path from 'path'; -// 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 { resolve as resolvePath } from 'path'; -import { realpathSync /*existsSync*/ } from 'fs'; -// import os from 'os'; +import { realpathSync } from 'fs'; import ora from 'ora'; +// import os from 'os'; const MARKER_SUCCESS = chalk.green(` ✔︎`); const MARKER_FAILURE = chalk.red(` ✘`); From 7217b56825aef89088c8d66f65e084b4e53ebf3f Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Tue, 31 Mar 2020 15:13:31 -0600 Subject: [PATCH 05/14] Fixed @bakcstage name in test --- .../remove-plugin/removePlugin.test.ts | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 5253fdf301..51a910c8a5 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -1,47 +1,53 @@ // Test Suite for removePlugin command. -import fse from 'fs-extra' -import path from 'path' -import {// removeExportStatementFromPlugins, - removePluginDependencyFromApp, - //removePluginDirectory, -} from './removePlugin' +import fse from 'fs-extra'; +import path from 'path'; +import { + // removeExportStatementFromPlugins, + removePluginDependencyFromApp, + //removePluginDirectory, +} from './removePlugin'; const rootDir = fse.realpathSync(process.cwd().replace('/cli', '')); +const BACKSTAGE = `@backstage`; // test remove export statement describe('removePlugin', () => { - describe('Remove Plugin Dependencies', () => { - // Set up test - // Copy contents of package file for test - const packageFile = path.join(rootDir, 'app', 'package.json'); - const testFilePath = path.join(rootDir, 'app', 'test.json'); - const testPluginName = 'yarn-test-package'; - const testPluginPackage = `@spotify-backstage/plugin-${testPluginName}`; + describe('Remove Plugin Dependencies', () => { + // Set up test + // Copy contents of package file for test + const packageFile = path.join(rootDir, 'app', 'package.json'); + const testFilePath = path.join(rootDir, 'app', 'test.json'); + const testPluginName = 'yarn-test-package'; + const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; - let packageFileContents = JSON.parse(fse.readFileSync(packageFile, 'utf8')); - packageFileContents.dependencies[testPluginPackage] = "0.1.0"; + let packageFileContents = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + packageFileContents.dependencies[testPluginPackage] = '0.1.0'; - it('should remove plugin dependency from /packages/app/package.json', async () => { - fse.createFileSync(testFilePath); - fse.writeFileSync( - testFilePath, - `${JSON.stringify(packageFileContents, null, 2)}\n`, - 'utf8'); - console.log(JSON.parse(fse.readFileSync(testFilePath, 'utf8'))); - try { - await removePluginDependencyFromApp(testFilePath, testPluginName); - expect(JSON.parse(fse.readFileSync(testFilePath, 'utf8')).hasOwnProperty(testPluginPackage)).toBe(false); - } finally { - fse.removeSync(testFilePath); - } - }); + it('should remove plugin dependency from /packages/app/package.json', async () => { + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContents, null, 2)}\n`, + 'utf8', + ); + console.log(JSON.parse(fse.readFileSync(testFilePath, 'utf8'))); + try { + await removePluginDependencyFromApp(testFilePath, testPluginName); + expect( + JSON.parse(fse.readFileSync(testFilePath, 'utf8')).hasOwnProperty( + testPluginPackage, + ), + ).toBe(false); + } finally { + fse.removeSync(testFilePath); + } }); + }); }); - // test remove plugin dependency from app // remove plugin from directory -// remove symlink from lerna scope \ No newline at end of file +// remove symlink from lerna scope From f7967c6d1784bfa6bc11b519362377b951034577 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Tue, 31 Mar 2020 15:24:46 -0600 Subject: [PATCH 06/14] Fixed yarn lint errors --- .../remove-plugin/removePlugin.test.ts | 30 ++++++++++++------- .../commands/remove-plugin/removePlugin.ts | 8 ++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 51a910c8a5..c801aaaa02 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -1,12 +1,22 @@ -// Test Suite for removePlugin command. +/* + * 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 fse from 'fs-extra'; import path from 'path'; -import { - // removeExportStatementFromPlugins, - removePluginDependencyFromApp, - //removePluginDirectory, -} from './removePlugin'; +import { removePluginDependencyFromApp } from './removePlugin'; const rootDir = fse.realpathSync(process.cwd().replace('/cli', '')); const BACKSTAGE = `@backstage`; @@ -21,7 +31,9 @@ describe('removePlugin', () => { const testPluginName = 'yarn-test-package'; const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; - let packageFileContents = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + const packageFileContents = JSON.parse( + fse.readFileSync(packageFile, 'utf8'), + ); packageFileContents.dependencies[testPluginPackage] = '0.1.0'; it('should remove plugin dependency from /packages/app/package.json', async () => { @@ -45,9 +57,7 @@ describe('removePlugin', () => { }); }); }); - +// Still to implement // test remove plugin dependency from app - // remove plugin from directory - // remove symlink from lerna scope diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index b1fa469be2..51f10fbc7a 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -33,7 +33,7 @@ export const checkExists = async (rootDir: string, pluginName: string) => { color: 'green', }).start(); try { - let pathExist = await fse.pathExists(destination); + const pathExist = await fse.pathExists(destination); if (pathExist) { spinner.succeed(); console.log( @@ -116,6 +116,9 @@ export const removeStatementContainingID = async (file: string, ID: string) => { throw new Error(`File was not modified.`); }; +const capitalize = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); + export const removeExportStatementFromPlugins = async ( pluginsFile: string, pluginName: string, @@ -224,9 +227,6 @@ export const removePluginDependencyFromApp = async ( } }; -const capitalize = (str: string): string => - str.charAt(0).toUpperCase() + str.slice(1); - const removePlugin = async () => { const questions: Question[] = [ { From e67bfd134936d62b9fad53df342d1d0ccc9d5145 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 13:13:26 -0500 Subject: [PATCH 07/14] Merge branch 'master' of https://github.com/spotify/backstage into remove_plugin_wip --- docs/design.md | 10 ++++++++++ docs/designheader.png | Bin 0 -> 124655 bytes lerna.json | 2 +- packages/app/package.json | 12 ++++++------ packages/cli/package.json | 2 +- .../cli/src/commands/create-app/createApp.ts | 8 ++++---- .../default-app/plugins/welcome/tsconfig.json | 5 ++++- .../cli/templates/default-plugin/tsconfig.json | 5 ++++- packages/core/package.json | 8 ++++---- .../src/api/apis/definitions/featureFlags.ts | 2 +- packages/core/src/api/app/AppBuilder.tsx | 8 ++++---- .../core/src/api/app/FeatureFlags.test.tsx | 2 +- packages/core/src/api/app/FeatureFlags.tsx | 4 ++-- .../core/src/api/app/LoginPage/LoginPage.tsx | 10 +++++----- packages/core/src/api/app/types.ts | 2 +- packages/core/src/api/plugin/Plugin.tsx | 4 ++-- packages/core/src/api/plugin/types.ts | 2 +- .../src/api/widgetView/WidgetViewBuilder.tsx | 6 +++--- .../DefaultWidgetView/DefaultWidgetView.tsx | 2 +- packages/core/src/components/ProgressCard.tsx | 4 ++-- packages/core/src/icons/icons.tsx | 2 +- packages/core/src/layout/Header/Header.tsx | 2 +- packages/core/src/layout/Header/Waves.test.tsx | 2 +- packages/core/src/layout/Header/Waves.tsx | 2 +- .../src/layout/HeaderLabel/OwnerHeaderLabel.js | 2 +- packages/core/src/layout/InfoCard/InfoCard.tsx | 2 +- packages/core/tsconfig.json | 1 + packages/storybook/.storybook/main.js | 3 +++ packages/storybook/package.json | 2 +- packages/test-utils/package.json | 6 +++--- packages/test-utils/tsconfig.json | 5 ++++- packages/theme/package.json | 4 ++-- packages/theme/tsconfig.json | 5 ++++- plugins/home-page/package.json | 8 ++++---- .../src/components/HomePage/HomePage.tsx | 2 +- plugins/home-page/src/plugin.ts | 2 +- plugins/home-page/tsconfig.json | 5 ++++- plugins/welcome/package.json | 8 ++++---- .../src/components/WelcomePage/WelcomePage.tsx | 2 +- plugins/welcome/src/plugin.ts | 2 +- plugins/welcome/tsconfig.json | 5 ++++- 41 files changed, 101 insertions(+), 69 deletions(-) create mode 100644 docs/designheader.png diff --git a/docs/design.md b/docs/design.md index e1a2af84b1..a75227b0cd 100644 --- a/docs/design.md +++ b/docs/design.md @@ -19,11 +19,21 @@ There are a lot of exciting things coming up and we want to keep you in the loop ## 🛠 Our Practice The chart below details how we work. ***Stay tuned***: We are currently in the process of securing a Figma workspace for Backstage Open Source, and we plan on referencing Figma documents to share specs and prototypes with the community. +### Creating a New Design Component | Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 | |:---|:---|:---|:---|:---|:---| | Platform design team submits an issue to **spotify/Backstage GitHub** with a potential component. | Backstage community offers feedback or approval on **spotify/Backstage GitHub**. | Platform design team adjusts accordingly (as they see fit) and update the Figma DLS document. | Designed component is added to **spotify/Backstage GitHub** as an issue. | External or internal Backstage open source contributors build the component. | External or internal contributors add the component to the **Backstage Storybook**. 🎉 | +### Building for Backstage +| Step 1 | Step 2 | Step 3 | Step 4 | +|:---|:---|:---|:---| +| External or internal contributors use Backstage and come up with an idea of an entity to build for Backstage. |External or internal contributors refer to the Backstage Open Source design system documentation in the Figma DLS document. | External or internal contributors leverage the components and tokens from the Backstage Storybook. | External or internal contributors build their Backstage entity. | + +| Step 5 | Step 6 | Step 7 | Step 8 | +|:---|:---|:---|:---| +| External or internal contributors make a pull request for their entity on spotify/Backstage GitHub for review. | Platform designers and devs review the entity and submit feedback or approval on spotify/Backstage GitHub. | External or internal contributors make the changes, pull request is approved and the entity is merged. It’s live on Backstage! 🎉 | If the entity happens to be or include a UX component, it’s added to Backstage Storybook as well. | + The following diagram shows the relationship between the Backstage Design System and our foundation, which comprises of [Material UI](https://material-ui.com/) that is shaped by user experience and user interface decisions made by our Backstage Design Team. Also note, we encourage you to take the core experience we’ve crafted and add custom theming to better represent your organization! diff --git a/docs/designheader.png b/docs/designheader.png new file mode 100644 index 0000000000000000000000000000000000000000..e9ace5c2e7074fbcbc716571b75e1a217daf528e GIT binary patch literal 124655 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

uc_d`U20&n=&=JUdnimE=7xQ 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

+xwm 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 YWEar~{rMFq3B7n%K}|kW_Oajp2cor%zyJUM literal 0 HcmV?d00001 diff --git a/lerna.json b/lerna.json index 7a9ee6a4cb..2cc439d190 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.2" + "version": "0.1.1-alpha.3" } diff --git a/packages/app/package.json b/packages/app/package.json index e24ac9688c..0d7d29e480 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,13 +1,13 @@ { "name": "example-app", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "private": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.2", - "@backstage/core": "^0.1.1-alpha.2", - "@backstage/theme": "^0.1.1-alpha.2", - "@backstage/plugin-home-page": "^0.1.1-alpha.2", - "@backstage/plugin-welcome": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", + "@backstage/core": "^0.1.1-alpha.3", + "@backstage/plugin-home-page": "^0.1.1-alpha.3", + "@backstage/plugin-welcome": "^0.1.1-alpha.3", + "@backstage/theme": "^0.1.1-alpha.3", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", diff --git a/packages/cli/package.json b/packages/cli/package.json index 40b5d78040..1764d19bf2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "private": false, "publishConfig": { "access": "public" diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index 826f9866d2..5863d9e705 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -57,11 +57,11 @@ async function cleanUp(tempDir: string) { }); } -async function buildApp(pluginFolder: string) { +async function buildApp(appFolder: string) { const commands = ['yarn install', 'yarn build']; for (const command of commands) { await Task.forItem('executing', command, async () => { - process.chdir(pluginFolder); + process.chdir(appFolder); await exec(command).catch(error => { process.stdout.write(error.stderr); @@ -80,7 +80,7 @@ export async function moveApp( 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}`, + `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); }); }); @@ -97,7 +97,7 @@ export default async () => { 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.', + 'App name must be kebab-cased and contain only letters, digits, and dashes.', ); } return true; diff --git a/packages/cli/templates/default-app/plugins/welcome/tsconfig.json b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/packages/cli/templates/default-app/plugins/welcome/tsconfig.json +++ b/packages/cli/templates/default-app/plugins/welcome/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } diff --git a/packages/cli/templates/default-plugin/tsconfig.json b/packages/cli/templates/default-plugin/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/packages/cli/templates/default-plugin/tsconfig.json +++ b/packages/cli/templates/default-plugin/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } diff --git a/packages/core/package.json b/packages/core/package.json index 85054eba14..cc30222cf4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "private": false, "publishConfig": { "access": "public" @@ -41,9 +41,9 @@ "recompose": "0.30.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.2", - "@backstage/test-utils": "^0.1.1-alpha.2", - "@backstage/theme": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", + "@backstage/test-utils": "^0.1.1-alpha.3", + "@backstage/theme": "^0.1.1-alpha.3", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/packages/core/src/api/apis/definitions/featureFlags.ts b/packages/core/src/api/apis/definitions/featureFlags.ts index 926fe091d5..5d6a134f84 100644 --- a/packages/core/src/api/apis/definitions/featureFlags.ts +++ b/packages/core/src/api/apis/definitions/featureFlags.ts @@ -19,7 +19,7 @@ import { UserFlags, FeatureFlagsRegistry, FeatureFlagsRegistryItem, -} from '../../app/FeatureFlags'; +} from 'api/app/FeatureFlags'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/AppBuilder.tsx index 23d0e17672..ccc1303ccb 100644 --- a/packages/core/src/api/app/AppBuilder.tsx +++ b/packages/core/src/api/app/AppBuilder.tsx @@ -18,16 +18,16 @@ import React, { ComponentType } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; import { App } from './types'; -import BackstagePlugin from '../plugin/Plugin'; +import BackstagePlugin from 'api/plugin/Plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; -import { featureFlagsApiRef } from '../apis/definitions/featureFlags'; +import { featureFlagsApiRef } from 'api/apis/definitions/featureFlags'; import { IconComponent, SystemIcons, SystemIconKey, defaultSystemIcons, -} from '../../icons'; -import { ApiHolder, ApiProvider } from '../apis'; +} from 'icons'; +import { ApiHolder, ApiProvider } from 'api/apis'; import LoginPage from './LoginPage'; class AppImpl implements App { diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx index b5bbb4d68e..cc09ebc61d 100644 --- a/packages/core/src/api/app/FeatureFlags.test.tsx +++ b/packages/core/src/api/app/FeatureFlags.test.tsx @@ -15,7 +15,7 @@ */ import { FeatureFlags as FeatureFlagsImpl } from './FeatureFlags'; -import { FeatureFlagState } from '../apis/definitions/featureFlags'; +import { FeatureFlagState } from 'api/apis/definitions/featureFlags'; describe('FeatureFlags', () => { beforeEach(() => { diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx index 8e5a06b209..fcbefedb3e 100644 --- a/packages/core/src/api/app/FeatureFlags.tsx +++ b/packages/core/src/api/app/FeatureFlags.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { FeatureFlagName } from '../plugin/types'; +import { FeatureFlagName } from 'api/plugin/types'; import { FeatureFlagState, FeatureFlagsApi, -} from '../apis/definitions/featureFlags'; +} from 'api/apis/definitions/featureFlags'; /** * Helper method for validating compatibility and flag name. diff --git a/packages/core/src/api/app/LoginPage/LoginPage.tsx b/packages/core/src/api/app/LoginPage/LoginPage.tsx index 7352600f63..9176c4c522 100644 --- a/packages/core/src/api/app/LoginPage/LoginPage.tsx +++ b/packages/core/src/api/app/LoginPage/LoginPage.tsx @@ -16,10 +16,10 @@ import React, { FC, useState } from 'react'; import { GitHub as GitHubIcon } from '@material-ui/icons'; -import Page from '../../../layout/Page'; -import Header from '../../../layout/Header'; -import Content from '../../../layout/Content/Content'; -import ContentHeader from '../../../layout/ContentHeader/ContentHeader'; +import Page from 'layout/Page'; +import Header from 'layout/Header'; +import Content from 'layout/Content/Content'; +import ContentHeader from 'layout/ContentHeader/ContentHeader'; import { Grid, Typography, @@ -29,7 +29,7 @@ import { ListItem, Link, } from '@material-ui/core'; -import InfoCard from '../../../layout/InfoCard/InfoCard'; +import InfoCard from 'layout/InfoCard/InfoCard'; enum AuthType { GitHub, diff --git a/packages/core/src/api/app/types.ts b/packages/core/src/api/app/types.ts index 4a6efe66fb..cc54cef606 100644 --- a/packages/core/src/api/app/types.ts +++ b/packages/core/src/api/app/types.ts @@ -15,7 +15,7 @@ */ import { ComponentType } from 'react'; -import { IconComponent, SystemIconKey } from '../../icons'; +import { IconComponent, SystemIconKey } from 'icons'; export type App = { getSystemIcon(key: SystemIconKey): IconComponent; diff --git a/packages/core/src/api/plugin/Plugin.tsx b/packages/core/src/api/plugin/Plugin.tsx index aade5d0092..47f8331e10 100644 --- a/packages/core/src/api/plugin/Plugin.tsx +++ b/packages/core/src/api/plugin/Plugin.tsx @@ -21,8 +21,8 @@ import { RouteOptions, FeatureFlagName, } from './types'; -import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags'; -import { Widget } from '../widgetView/types'; +import { validateBrowserCompat, validateFlagName } from 'api/app/FeatureFlags'; +import { Widget } from 'api/widgetView/types'; export type PluginConfig = { id: string; diff --git a/packages/core/src/api/plugin/types.ts b/packages/core/src/api/plugin/types.ts index b006e06d9a..3192dfd095 100644 --- a/packages/core/src/api/plugin/types.ts +++ b/packages/core/src/api/plugin/types.ts @@ -15,7 +15,7 @@ */ import { ComponentType } from 'react'; -import { Widget } from '../widgetView/types'; +import { Widget } from 'api/widgetView/types'; export type RouteOptions = { // Whether the route path must match exactly, defaults to true. diff --git a/packages/core/src/api/widgetView/WidgetViewBuilder.tsx b/packages/core/src/api/widgetView/WidgetViewBuilder.tsx index fb26eadbe3..0bf873df86 100644 --- a/packages/core/src/api/widgetView/WidgetViewBuilder.tsx +++ b/packages/core/src/api/widgetView/WidgetViewBuilder.tsx @@ -15,10 +15,10 @@ */ import React, { ComponentType } from 'react'; -import { AppComponentBuilder } from '../app/types'; +import { AppComponentBuilder } from 'api/app/types'; import { Widget } from './types'; -import BackstagePlugin from '../plugin/Plugin'; -import DefaultWidgetView from '../../components/DefaultWidgetView'; +import BackstagePlugin from 'api/plugin/Plugin'; +import DefaultWidgetView from 'components/DefaultWidgetView'; type WidgetViewRegistration = | { diff --git a/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx b/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx index 26246d5341..990ecb0e92 100644 --- a/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx +++ b/packages/core/src/components/DefaultWidgetView/DefaultWidgetView.tsx @@ -16,7 +16,7 @@ import React, { FC } from 'react'; import { Grid, Paper, makeStyles, Theme } from '@material-ui/core'; -import { WidgetViewProps } from '../../api/widgetView/types'; +import { WidgetViewProps } from 'api/widgetView/types'; const useStyles = makeStyles(theme => ({ root: { diff --git a/packages/core/src/components/ProgressCard.tsx b/packages/core/src/components/ProgressCard.tsx index ef84c40499..1964f382e9 100644 --- a/packages/core/src/components/ProgressCard.tsx +++ b/packages/core/src/components/ProgressCard.tsx @@ -17,8 +17,8 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; -import InfoCard from '../layout/InfoCard'; -import { Props as BottomLinkProps } from '../layout/InfoCard/BottomLink'; +import InfoCard from 'layout/InfoCard'; +import { Props as BottomLinkProps } from 'layout/InfoCard/BottomLink'; import CircleProgress from './CircleProgress'; type Props = { diff --git a/packages/core/src/icons/icons.tsx b/packages/core/src/icons/icons.tsx index 454a10c264..a8debf56b4 100644 --- a/packages/core/src/icons/icons.tsx +++ b/packages/core/src/icons/icons.tsx @@ -18,7 +18,7 @@ import { SvgIconProps } from '@material-ui/core'; import PeopleIcon from '@material-ui/icons/People'; import PersonIcon from '@material-ui/icons/Person'; import React, { FC } from 'react'; -import { useApp } from '../api/app/AppContext'; +import { useApp } from 'api/app/AppContext'; import { IconComponent, SystemIconKey, SystemIcons } from './types'; export const defaultSystemIcons: SystemIcons = { diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 6af4019d13..5ed929d9a0 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -19,7 +19,7 @@ import Helmet from 'react-helmet'; import { Typography, Tooltip, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -import { Theme } from '../Page/Page'; +import { Theme } from 'layout/Page/Page'; // import { Link } from 'shared/components'; import Waves from './Waves'; diff --git a/packages/core/src/layout/Header/Waves.test.tsx b/packages/core/src/layout/Header/Waves.test.tsx index 2c7d463052..02ea8da634 100644 --- a/packages/core/src/layout/Header/Waves.test.tsx +++ b/packages/core/src/layout/Header/Waves.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { pageTheme } from '../Page/PageThemeProvider'; +import { pageTheme } from 'layout/Page/PageThemeProvider'; import Waves from './Waves'; describe('', () => { diff --git a/packages/core/src/layout/Header/Waves.tsx b/packages/core/src/layout/Header/Waves.tsx index f37ecf7b12..001f3d61c4 100644 --- a/packages/core/src/layout/Header/Waves.tsx +++ b/packages/core/src/layout/Header/Waves.tsx @@ -16,7 +16,7 @@ import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core'; -import { PageTheme } from '../Page'; +import { PageTheme } from 'layout/Page'; const useStyles = makeStyles({ wave: { diff --git a/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js b/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js index d13f0bd07a..abe0ee8b33 100644 --- a/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js +++ b/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js @@ -18,7 +18,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Tooltip, Link, withStyles } from '@material-ui/core'; -import { StatusError } from '../../components/Status'; +import { StatusError } from 'components/Status'; import HeaderLabel from './HeaderLabel'; const style = theme => ({ diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index 10f9f60da0..caa5f16513 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -24,7 +24,7 @@ import { withStyles, makeStyles, } from '@material-ui/core'; -import ErrorBoundary from '../ErrorBoundary/ErrorBoundary'; +import ErrorBoundary from 'layout/ErrorBoundary/ErrorBoundary'; import BottomLink, { Props as BottomLinkProps } from './BottomLink'; const useStyles = makeStyles(theme => ({ diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index ad43a0b986..5c5c42c25f 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { + "baseUrl": "src", "noImplicitAny": false } } diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index afed6723d8..55d731c8e6 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { stories: [ '../../core/src/layout/**/*.stories.tsx', @@ -5,6 +7,7 @@ module.exports = { ], addons: ['@storybook/addon-actions', '@storybook/addon-links'], webpackFinal: async config => { + config.resolve.modules.push(path.resolve(__dirname, '../../core/src')); config.module.rules.push( { test: /\.(ts|tsx)$/, diff --git a/packages/storybook/package.json b/packages/storybook/package.json index fc15da6207..4dbf2990fa 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "description": "Storybook build for core package", "private": true, "scripts": { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ce2861353f..98b2dcd749 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "private": false, "publishConfig": { "access": "public" @@ -24,8 +24,8 @@ "test": "backstage-cli test" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.2", - "@backstage/theme": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", + "@backstage/theme": "^0.1.1-alpha.3", "@material-ui/core": "^4.9.1", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } diff --git a/packages/theme/package.json b/packages/theme/package.json index 450f8f6f86..b08014e9d6 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "private": false, "publishConfig": { "access": "public" @@ -23,7 +23,7 @@ "lint": "backstage-cli lint" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", "@material-ui/core": "^4.9.1" }, "peerDependencies": { diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/packages/theme/tsconfig.json +++ b/packages/theme/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index 73e208b737..e5d90de209 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-page", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "license": "Apache-2.0", @@ -11,9 +11,9 @@ "test": "backstage-cli test" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.2", - "@backstage/core": "^0.1.1-alpha.2", - "@backstage/theme": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", + "@backstage/core": "^0.1.1-alpha.3", + "@backstage/theme": "^0.1.1-alpha.3", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^4.2.4", diff --git a/plugins/home-page/src/components/HomePage/HomePage.tsx b/plugins/home-page/src/components/HomePage/HomePage.tsx index c3f0250d71..e9e71c3b0b 100644 --- a/plugins/home-page/src/components/HomePage/HomePage.tsx +++ b/plugins/home-page/src/components/HomePage/HomePage.tsx @@ -16,7 +16,7 @@ import React, { FC } from 'react'; import { Typography, Link, Grid } from '@material-ui/core'; -import HomePageTimer from '../HomepageTimer'; +import HomePageTimer from 'components/HomepageTimer'; import { Content, InfoCard, Header, Page, pageTheme } from '@backstage/core'; import SquadTechHealth from './SquadTechHealth'; import Table from '@material-ui/core/Table'; diff --git a/plugins/home-page/src/plugin.ts b/plugins/home-page/src/plugin.ts index 17eb89eb35..c3b4d30fe3 100644 --- a/plugins/home-page/src/plugin.ts +++ b/plugins/home-page/src/plugin.ts @@ -15,7 +15,7 @@ */ import { createPlugin } from '@backstage/core'; -import HomePage from './components/HomePage'; +import HomePage from 'components/HomePage'; export default createPlugin({ id: 'home-page', diff --git a/plugins/home-page/tsconfig.json b/plugins/home-page/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/plugins/home-page/tsconfig.json +++ b/plugins/home-page/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index f4f33d6265..0d31b0d635 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.2", + "version": "0.1.1-alpha.3", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "private": true, @@ -11,9 +11,9 @@ "test": "backstage-cli test" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.2", - "@backstage/core": "^0.1.1-alpha.2", - "@backstage/theme": "^0.1.1-alpha.2", + "@backstage/cli": "^0.1.1-alpha.3", + "@backstage/core": "^0.1.1-alpha.3", + "@backstage/theme": "^0.1.1-alpha.3", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index eea582ab55..307369f5fd 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -24,7 +24,7 @@ import { ListItemText, Link, } from '@material-ui/core'; -import Timer from '../Timer'; +import Timer from 'components/Timer'; import { Content, InfoCard, diff --git a/plugins/welcome/src/plugin.ts b/plugins/welcome/src/plugin.ts index e7755862ff..740c6d2da3 100644 --- a/plugins/welcome/src/plugin.ts +++ b/plugins/welcome/src/plugin.ts @@ -15,7 +15,7 @@ */ import { createPlugin } from '@backstage/core'; -import WelcomePage from './components/WelcomePage'; +import WelcomePage from 'components/WelcomePage'; export default createPlugin({ id: 'welcome', diff --git a/plugins/welcome/tsconfig.json b/plugins/welcome/tsconfig.json index 596e2cf729..7b73db2f0f 100644 --- a/plugins/welcome/tsconfig.json +++ b/plugins/welcome/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src"] + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } } From 083f104946483f982387457038029d4ff4fe15b0 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 23:30:26 -0500 Subject: [PATCH 08/14] removePlugin.ts now uses Task helper --- .../commands/remove-plugin/removePlugin.ts | 330 ++++++++---------- 1 file changed, 147 insertions(+), 183 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 51f10fbc7a..76b8606973 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -17,89 +17,70 @@ import fse from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; -import { realpathSync } from 'fs'; -import ora from 'ora'; +import { getCodeownersFilePath } from '../create-plugin/lib/codeowners'; +import { paths } from 'helpers/paths'; +import { Task } from 'helpers/tasks'; // import os from 'os'; -const MARKER_SUCCESS = chalk.green(` ✔︎`); -const MARKER_FAILURE = chalk.red(` ✘`); const BACKSTAGE = '@backstage'; export const checkExists = async (rootDir: string, pluginName: string) => { - const destination = path.join(rootDir, 'plugins', pluginName); - const spinner = ora({ - prefixText: ` Checking plugin exists.`, - spinner: 'arc', - color: 'green', - }).start(); - try { - const pathExist = await fse.pathExists(destination); - if (pathExist) { - spinner.succeed(); - console.log( - chalk.green( - ` Plugin ID ${chalk.cyan( - pluginName, - )} exists at: ${destination.replace( - `${rootDir}`, - '', - )} ${MARKER_SUCCESS}`, + await Task.forItem('checking', pluginName, async () => { + try { + const destination = path.join(rootDir, 'plugins', pluginName); + const pathExist = await fse.pathExists(destination); + + if (!pathExist) { + throw new Error( + chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`), + ); + } + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing plugin ${chalk.cyan(pluginName)}: ${ + e.message + }`, ), ); - } else { - throw new Error( - chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`), - ); } - } catch (e) { - spinner.fail(); - throw new Error( - chalk.red( - ` There was an error removing plugin ${chalk.cyan(pluginName)}: ${ - e.message - }`, - ), - ); - } + }); }; -export const removePluginDirectory = async ( - destination: string, - pluginName: string, -) => { - console.log(` Removing plugin files ${chalk.cyan(destination)}.`); - try { - await fse.remove(destination); - console.log( - chalk.green(` Plugin files removed successfully. ${MARKER_SUCCESS}`), - ); - } catch (e) { - throw Error( - ` Could not remove Plugin\t${pluginName}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`, - ); - } +export const removePluginDirectory = async (destination: string) => { + await Task.forItem('removing', 'plugin files', async () => { + try { + await fse.remove(destination); + } catch (e) { + throw Error( + chalk.red( + ` There was a problem removing the plugin directory: ${e.message}`, + ), + ); + } + }); }; export const removeSymLink = async (destination: string) => { - console.log( - ` Removing symbolic link if it exists at:\t${chalk.cyan(destination)}.`, - ); - const symLinkExists = fse.pathExists(destination); - if (symLinkExists) { - try { - await fse.remove(destination); - console.log( - chalk.green(` Symbolic link successfully removed. ${MARKER_SUCCESS}`), - ); - } catch (e) { - throw Error( - ` Could not remove symbolic link\t${destination}. ${MARKER_FAILURE} \n Please try again. Error: ${e.message}`, - ); + await Task.forItem('removing', 'symbolic link', async () => { + const symLinkExists = fse.pathExists(destination); + if (symLinkExists) { + try { + await fse.remove(destination); + } catch (e) { + throw Error( + chalk.red( + ` Could not remove symbolic link\t${chalk.cyan(destination)}: ${ + e.message + }`, + ), + ); + } } - } + }); }; -export const removeStatementContainingID = async (file: string, ID: string) => { +const removeAllStatementsContainingID = async (file: string, ID: string) => { const originalContent = await fse.readFile(file, 'utf8'); const contentAfterRemoval = originalContent .split('\n') @@ -107,7 +88,6 @@ export const removeStatementContainingID = async (file: string, ID: string) => { .filter(statement => { return !statement.includes(`${ID}`); }) // get rid of lines with pluginName - .sort() .concat(['']) // newline at end of line .join('\n'); await fse.writeFile(file, contentAfterRemoval, 'utf8'); @@ -119,7 +99,7 @@ export const removeStatementContainingID = async (file: string, ID: string) => { const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); -export const removeExportStatementFromPlugins = async ( +export const removeReferencesFromPluginsFile = async ( pluginsFile: string, pluginName: string, ) => { @@ -127,107 +107,82 @@ export const removeExportStatementFromPlugins = async ( .split('-') .map(name => capitalize(name)) .join(''); - console.log( - ` Removing export statement from ${chalk.cyan( - pluginsFile.replace(pluginsFile.split('/app', 1)[0], ''), - )}`, - ); // remove long path - try { - await removeStatementContainingID(pluginsFile, pluginNameCapitalized); - console.log( - chalk.green( - ` Successfully removed export statement from /app/src/plugin.ts ${MARKER_SUCCESS}`, - ), - ); - } catch (e) { - throw new Error( - chalk.red( - ` There was an error removing export statement for plugin ${chalk.cyan( - pluginNameCapitalized, - )} ${MARKER_FAILURE} ${e.message}`, - ), - ); - } + + await Task.forItem('removing', 'export references', async () => { + try { + await removeAllStatementsContainingID(pluginsFile, pluginNameCapitalized); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing export statement for plugin ${chalk.cyan( + pluginNameCapitalized, + )}: ${e.message}`, + ), + ); + } + }); }; export const removePluginFromCodeOwners = async ( codeOwnersFile: string, pluginName: string, ) => { - console.log( - ` Removing teams and owners from ${chalk.cyan( - codeOwnersFile.replace(codeOwnersFile.split('/.git', 1)[0], ''), - )}`, - ); // remove long path - try { - await removeStatementContainingID(codeOwnersFile, pluginName); - console.log( - chalk.green( - ` Successfully removed codeowners statement from /.git/CODEOWNERS ${MARKER_SUCCESS}`, - ), - ); - } catch (e) { - throw new Error( - chalk.red( - ` There was an error removing code owners statement for plugin ${chalk.cyan( - pluginName, - )} ${MARKER_FAILURE} ${e.message}`, - ), - ); - } + await Task.forItem('removing', 'codeowners references', async () => { + try { + await removeAllStatementsContainingID(codeOwnersFile, pluginName); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing code owners statement for plugin ${chalk.cyan( + pluginName, + )}: ${e.message}`, + ), + ); + } + }); }; -export const removePluginDependencyFromApp = async ( - packageFile: string, +export const removeReferencesFromAppPackage = async ( + appPackageFile: string, pluginName: string, ) => { const pluginPackage = `${BACKSTAGE}/plugin-${pluginName}`; - console.log( - ` Removing plugin from app dependencies ${chalk.cyan( - packageFile.replace(`${packageFile}/packages`, ''), - )}:`, - ); + await Task.forItem('removing', 'plugin app dependency', async () => { + try { + const appPackageFileContent = await fse.readFile(appPackageFile, 'utf-8'); + const appPackageFileContentJSON = JSON.parse(appPackageFileContent); + const dependencies = appPackageFileContentJSON.dependencies; - try { - const packageFileContent = await fse.readFile(packageFile, 'utf-8'); - const packageFileContentJSON = JSON.parse(packageFileContent); - const dependencies = packageFileContentJSON.dependencies; + if (!dependencies[pluginPackage]) { + throw new Error( + chalk.red( + ` Plugin ${chalk.cyan( + pluginPackage, + )} does not exist in ${chalk.cyan(appPackageFile)}`, + ), + ); + } - if (!dependencies[pluginPackage]) { + delete dependencies[pluginPackage]; + await fse.writeFile( + appPackageFile, + `${JSON.stringify(appPackageFileContentJSON, null, 2)}\n`, + 'utf-8', + ); + } catch (e) { throw new Error( chalk.red( - ` Plugin ${chalk.cyan( - pluginPackage, - )} does not exist in ${chalk.yellow(packageFile)}`, + ` Failed to remove plugin as dependency in app: ${chalk.cyan( + appPackageFile, + )}: ${e.message}`, ), ); } - - delete dependencies[pluginPackage]; - await fse.writeFile( - packageFile, - `${JSON.stringify(packageFileContentJSON, null, 2)}\n`, - 'utf-8', - ); - - console.log( - chalk.green( - ` Successfully removed plugin from app dependencies. ${MARKER_SUCCESS}`, - ), - ); - } catch (e) { - throw new Error( - `${chalk.red( - ` Failed to remove plugin as dependency in app: ${chalk.cyan( - packageFile, - )}:`, - )} ${e.message}`, - ); - } + }); }; -const removePlugin = async () => { +export default async () => { const questions: Question[] = [ { type: 'input', @@ -249,45 +204,54 @@ const removePlugin = async () => { ]; const answers: Answers = await inquirer.prompt(questions); - - const rootDir = realpathSync(process.cwd()); - const codeOwnersFile = path.join(rootDir, '.github', 'CODEOWNERS'); const pluginName: string = answers.pluginName; - const packageFile = path.join(rootDir, 'packages', 'app', 'package.json'); - const pluginsFile = path.join( - rootDir, - 'packages', - 'app', - 'src', - 'plugins.ts', - ); - const pluginDirectory = path.join(rootDir, 'plugins', pluginName); - const pluginScopedDirectory = path.join( - rootDir, - `node_modules/${BACKSTAGE}/plugin-${pluginName}`, + const appPackage = paths.resolveTargetRoot('packages/app'); + const pluginDir = paths.resolveTargetRoot('plugins', answers.pluginName); + const codeOwnersFile = await getCodeownersFilePath(paths.targetRoot); + const appPackageFile = path.join(appPackage, 'package.json'); + const appPluginsFile = path.join(appPackage, 'src', 'plugins.ts'); + const pluginScopedDirectory = paths.resolveTargetRoot( + 'node_modules', + BACKSTAGE, + `plugin-${pluginName}`, ); + + Task.log(); + Task.log('Removing the plugin...'); + console.log(pluginScopedDirectory); try { - await checkExists(rootDir, pluginName); - await removeExportStatementFromPlugins(pluginsFile, pluginName); - await removePluginDependencyFromApp(packageFile, pluginName); - await removePluginDirectory(pluginDirectory, pluginName); + Task.section('Checking the plugin exists.'); + await checkExists(paths.targetRoot, pluginName); + + Task.section('Removing plugin files.'); + await removePluginDirectory(pluginDir); + + Task.section('Removing symbolic link from @backstage.'); await removeSymLink(pluginScopedDirectory); - await removePluginFromCodeOwners(codeOwnersFile, pluginName); - console.log( - chalk.green( - `Successfully removed plugin ${chalk.cyan(pluginName)} from app.`, - ), - ); - } catch (e) { - // If error, restore files - console.log(e); - throw new Error( - chalk.red( - `Failed to remove plugin: ${chalk.cyan(pluginName)}: ${e.message}`, - ), + + if (await fse.pathExists(appPackage)) { + Task.section('Removing references from plugins.ts.'); + await removeReferencesFromPluginsFile(appPluginsFile, pluginName); + + Task.section('Removing plugin dependency from app.'); + await removeReferencesFromAppPackage(appPackageFile, pluginName); + } + + if (codeOwnersFile) { + Task.section('Removing codeowners reference.'); + await removePluginFromCodeOwners(codeOwnersFile, pluginName); + } + + Task.log(); + Task.log( + `🥇 Successfully removed ${chalk.cyan( + `@backstage/plugin-${answers.id}`, + )}`, ); + Task.log(); + } catch (error) { + Task.error(error.message); + Task.log('It seems that something went wrong when removing the plugin 🤔'); } }; - -export default removePlugin; From 169ecbe2f0cb55828bfd709aa5a430b2fd872990 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 23:31:13 -0500 Subject: [PATCH 09/14] Added unit tests for remove-plugin --- .../remove-plugin/removePlugin.test.ts | 179 ++++++++++++++---- 1 file changed, 146 insertions(+), 33 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index c801aaaa02..5ed8e82d13 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -16,48 +16,161 @@ import fse from 'fs-extra'; import path from 'path'; -import { removePluginDependencyFromApp } from './removePlugin'; +import { paths } from '../../helpers/paths'; +import { addExportStatement, capitalize } from '../create-plugin/createPlugin'; +import { addCodeownersEntry } from '../create-plugin/lib/codeowners'; +import { + removeReferencesFromAppPackage, + removeReferencesFromPluginsFile, + removePluginDirectory, + removeSymLink, + removePluginFromCodeOwners, +} from './removePlugin'; -const rootDir = fse.realpathSync(process.cwd().replace('/cli', '')); const BACKSTAGE = `@backstage`; +const testPluginName = 'yarn-test-package'; +const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; -// test remove export statement describe('removePlugin', () => { describe('Remove Plugin Dependencies', () => { - // Set up test - // Copy contents of package file for test - const packageFile = path.join(rootDir, 'app', 'package.json'); - const testFilePath = path.join(rootDir, 'app', 'test.json'); - const testPluginName = 'yarn-test-package'; - const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; - - const packageFileContents = JSON.parse( - fse.readFileSync(packageFile, 'utf8'), - ); - packageFileContents.dependencies[testPluginPackage] = '0.1.0'; - - it('should remove plugin dependency from /packages/app/package.json', async () => { - fse.createFileSync(testFilePath); - fse.writeFileSync( - testFilePath, - `${JSON.stringify(packageFileContents, null, 2)}\n`, - 'utf8', - ); - console.log(JSON.parse(fse.readFileSync(testFilePath, 'utf8'))); + const appPath = paths.resolveTargetRoot('packages', 'app'); + const githubDir = paths.resolveTargetRoot('.github'); + it('removes plugin references from /packages/app/package.json', async () => { + // Set up test + const packageFilePath = path.join(appPath, 'package.json'); + const testFilePath = path.join(appPath, 'test.json'); + createTestPackageFile(testFilePath, packageFilePath); try { - await removePluginDependencyFromApp(testFilePath, testPluginName); - expect( - JSON.parse(fse.readFileSync(testFilePath, 'utf8')).hasOwnProperty( - testPluginPackage, - ), - ).toBe(false); + await removeReferencesFromAppPackage(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const packageFileContent = removeEmptyLines( + fse.readFileSync(packageFilePath, 'utf8'), + ); + expect(testFileContent === packageFileContent).toBe(true); } finally { fse.removeSync(testFilePath); } }); + it('removes plugin exports from /packages/app/src/packacge.json', async () => { + const testFilePath = path.join(appPath, 'src', 'test.ts'); + const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts'); + createTestPluginFile(testFilePath, pluginsFilePaths); + try { + await removeReferencesFromPluginsFile(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const pluginsFileContent = removeEmptyLines( + fse.readFileSync(pluginsFilePaths, 'utf8'), + ); + expect(testFileContent === pluginsFileContent).toBe(true); + } finally { + fse.removeSync(testFilePath); + } + }); + it('removes codeOwners references', async () => { + const testFilePath = path.join(githubDir, 'test'); + const codeownersPath = path.join(githubDir, 'CODEOWNERS'); + try { + fse.copySync(codeownersPath, testFilePath); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const codeOwnersFileContent = removeEmptyLines( + fse.readFileSync(codeownersPath, 'utf8'), + ); + await addCodeownersEntry(testFilePath!, `/plugins/${testPluginName}`, [ + '@thisIsAtestTeam', + 'test@gmail.com', + ]); + await removePluginFromCodeOwners(testFilePath, testPluginName); + expect(testFileContent === codeOwnersFileContent).toBeTruthy(); + } finally { + if (fse.existsSync(testFilePath)) fse.removeSync(testFilePath); + } + }); + }); + describe('Remove files', () => { + const testDirPath = path.join( + paths.resolveTargetRoot(), + 'plugins', + testPluginName, + ); + describe('Removes Plugin Directory', () => { + it('removes plugin directory from /plugins', async () => { + try { + mkTestDir(testDirPath); + expect(fse.existsSync(testDirPath)).toBeTruthy(); + await removePluginDirectory(testDirPath); + expect(fse.existsSync(testDirPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + } + }); + }); + describe('Removes System Link', () => { + it('removes system link from @backstage', async () => { + const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage'); + const testSymLinkPath = path.join( + scopedDir, + `plugin-${testPluginName}`, + ); + try { + mkTestDir(testDirPath); + fse.ensureSymlinkSync(testSymLinkPath, testDirPath); + + await removeSymLink(testSymLinkPath); + expect(fse.existsSync(testSymLinkPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + if (fse.existsSync(testSymLinkPath)) fse.removeSync(testSymLinkPath); + } + }); + }); }); }); -// Still to implement -// test remove plugin dependency from app -// remove plugin from directory -// remove symlink from lerna scope + +const removeEmptyLines = (file: string): string => + file + .split('\n') + .filter(Boolean) + .join('\n'); + +const createTestPackageFile = async ( + testFilePath: string, + packageFile: string, +) => { + // Copy contents of package file for test + const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + + packageFileContent.dependencies[testPluginPackage] = '0.1.0'; + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContent, null, 2)}\n`, + 'utf8', + ); + return; +}; +const createTestPluginFile = async ( + testFilePath: string, + pluginsFilePath: string, +) => { + // Copy contents of package file for test + fse.copyFileSync(pluginsFilePath, testFilePath); + const pluginNameCapitalized = testPluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + const exportStatement = `export {${pluginNameCapitalized}}`; + addExportStatement(testFilePath, importStatement, exportStatement); +}; + +function mkTestDir(testDirPath: string) { + fse.mkdirSync(testDirPath); + for (let i = 0; i < 50; i++) + fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); +} From 08a0f6fe8a1ce1c1f8b80d9079824ce0563caf96 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 23:32:41 -0500 Subject: [PATCH 10/14] Added unit tests for remove-plugin --- .../commands/create-plugin/createPlugin.ts | 4 +- .../remove-plugin/removePlugin.test.ts | 179 ++++++++++++++---- 2 files changed, 148 insertions(+), 35 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 7efb29d270..40c99b1571 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -65,10 +65,10 @@ const sortObjectByKeys = (obj: { [name in string]: string }) => { }, {} as { [name in string]: string }); }; -const capitalize = (str: string): string => +export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); -const addExportStatement = async ( +export const addExportStatement = async ( file: string, importStatement: string, exportStatement: string, diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index c801aaaa02..5ed8e82d13 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -16,48 +16,161 @@ import fse from 'fs-extra'; import path from 'path'; -import { removePluginDependencyFromApp } from './removePlugin'; +import { paths } from '../../helpers/paths'; +import { addExportStatement, capitalize } from '../create-plugin/createPlugin'; +import { addCodeownersEntry } from '../create-plugin/lib/codeowners'; +import { + removeReferencesFromAppPackage, + removeReferencesFromPluginsFile, + removePluginDirectory, + removeSymLink, + removePluginFromCodeOwners, +} from './removePlugin'; -const rootDir = fse.realpathSync(process.cwd().replace('/cli', '')); const BACKSTAGE = `@backstage`; +const testPluginName = 'yarn-test-package'; +const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; -// test remove export statement describe('removePlugin', () => { describe('Remove Plugin Dependencies', () => { - // Set up test - // Copy contents of package file for test - const packageFile = path.join(rootDir, 'app', 'package.json'); - const testFilePath = path.join(rootDir, 'app', 'test.json'); - const testPluginName = 'yarn-test-package'; - const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; - - const packageFileContents = JSON.parse( - fse.readFileSync(packageFile, 'utf8'), - ); - packageFileContents.dependencies[testPluginPackage] = '0.1.0'; - - it('should remove plugin dependency from /packages/app/package.json', async () => { - fse.createFileSync(testFilePath); - fse.writeFileSync( - testFilePath, - `${JSON.stringify(packageFileContents, null, 2)}\n`, - 'utf8', - ); - console.log(JSON.parse(fse.readFileSync(testFilePath, 'utf8'))); + const appPath = paths.resolveTargetRoot('packages', 'app'); + const githubDir = paths.resolveTargetRoot('.github'); + it('removes plugin references from /packages/app/package.json', async () => { + // Set up test + const packageFilePath = path.join(appPath, 'package.json'); + const testFilePath = path.join(appPath, 'test.json'); + createTestPackageFile(testFilePath, packageFilePath); try { - await removePluginDependencyFromApp(testFilePath, testPluginName); - expect( - JSON.parse(fse.readFileSync(testFilePath, 'utf8')).hasOwnProperty( - testPluginPackage, - ), - ).toBe(false); + await removeReferencesFromAppPackage(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const packageFileContent = removeEmptyLines( + fse.readFileSync(packageFilePath, 'utf8'), + ); + expect(testFileContent === packageFileContent).toBe(true); } finally { fse.removeSync(testFilePath); } }); + it('removes plugin exports from /packages/app/src/packacge.json', async () => { + const testFilePath = path.join(appPath, 'src', 'test.ts'); + const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts'); + createTestPluginFile(testFilePath, pluginsFilePaths); + try { + await removeReferencesFromPluginsFile(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const pluginsFileContent = removeEmptyLines( + fse.readFileSync(pluginsFilePaths, 'utf8'), + ); + expect(testFileContent === pluginsFileContent).toBe(true); + } finally { + fse.removeSync(testFilePath); + } + }); + it('removes codeOwners references', async () => { + const testFilePath = path.join(githubDir, 'test'); + const codeownersPath = path.join(githubDir, 'CODEOWNERS'); + try { + fse.copySync(codeownersPath, testFilePath); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const codeOwnersFileContent = removeEmptyLines( + fse.readFileSync(codeownersPath, 'utf8'), + ); + await addCodeownersEntry(testFilePath!, `/plugins/${testPluginName}`, [ + '@thisIsAtestTeam', + 'test@gmail.com', + ]); + await removePluginFromCodeOwners(testFilePath, testPluginName); + expect(testFileContent === codeOwnersFileContent).toBeTruthy(); + } finally { + if (fse.existsSync(testFilePath)) fse.removeSync(testFilePath); + } + }); + }); + describe('Remove files', () => { + const testDirPath = path.join( + paths.resolveTargetRoot(), + 'plugins', + testPluginName, + ); + describe('Removes Plugin Directory', () => { + it('removes plugin directory from /plugins', async () => { + try { + mkTestDir(testDirPath); + expect(fse.existsSync(testDirPath)).toBeTruthy(); + await removePluginDirectory(testDirPath); + expect(fse.existsSync(testDirPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + } + }); + }); + describe('Removes System Link', () => { + it('removes system link from @backstage', async () => { + const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage'); + const testSymLinkPath = path.join( + scopedDir, + `plugin-${testPluginName}`, + ); + try { + mkTestDir(testDirPath); + fse.ensureSymlinkSync(testSymLinkPath, testDirPath); + + await removeSymLink(testSymLinkPath); + expect(fse.existsSync(testSymLinkPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + if (fse.existsSync(testSymLinkPath)) fse.removeSync(testSymLinkPath); + } + }); + }); }); }); -// Still to implement -// test remove plugin dependency from app -// remove plugin from directory -// remove symlink from lerna scope + +const removeEmptyLines = (file: string): string => + file + .split('\n') + .filter(Boolean) + .join('\n'); + +const createTestPackageFile = async ( + testFilePath: string, + packageFile: string, +) => { + // Copy contents of package file for test + const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + + packageFileContent.dependencies[testPluginPackage] = '0.1.0'; + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContent, null, 2)}\n`, + 'utf8', + ); + return; +}; +const createTestPluginFile = async ( + testFilePath: string, + pluginsFilePath: string, +) => { + // Copy contents of package file for test + fse.copyFileSync(pluginsFilePath, testFilePath); + const pluginNameCapitalized = testPluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + const exportStatement = `export {${pluginNameCapitalized}}`; + addExportStatement(testFilePath, importStatement, exportStatement); +}; + +function mkTestDir(testDirPath: string) { + fse.mkdirSync(testDirPath); + for (let i = 0; i < 50; i++) + fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); +} From 6899f96db5df8b0c186aafa1f0fa62712c2de49e Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 23:41:16 -0500 Subject: [PATCH 11/14] Ficed some lint issues --- .../remove-plugin/removePlugin.test.ts | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 5ed8e82d13..aac3caece9 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -31,6 +31,49 @@ const BACKSTAGE = `@backstage`; const testPluginName = 'yarn-test-package'; const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; +const removeEmptyLines = (file: string): string => + file + .split('\n') + .filter(Boolean) + .join('\n'); + +const createTestPackageFile = async ( + testFilePath: string, + packageFile: string, +) => { + // Copy contents of package file for test + const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + + packageFileContent.dependencies[testPluginPackage] = '0.1.0'; + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContent, null, 2)}\n`, + 'utf8', + ); + return; +}; +const createTestPluginFile = async ( + testFilePath: string, + pluginsFilePath: string, +) => { + // Copy contents of package file for test + fse.copyFileSync(pluginsFilePath, testFilePath); + const pluginNameCapitalized = testPluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + const exportStatement = `export {${pluginNameCapitalized}}`; + addExportStatement(testFilePath, importStatement, exportStatement); +}; + +function mkTestDir(testDirPath: string) { + fse.mkdirSync(testDirPath); + for (let i = 0; i < 50; i++) + fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); +} + describe('removePlugin', () => { describe('Remove Plugin Dependencies', () => { const appPath = paths.resolveTargetRoot('packages', 'app'); @@ -131,46 +174,3 @@ describe('removePlugin', () => { }); }); }); - -const removeEmptyLines = (file: string): string => - file - .split('\n') - .filter(Boolean) - .join('\n'); - -const createTestPackageFile = async ( - testFilePath: string, - packageFile: string, -) => { - // Copy contents of package file for test - const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); - - packageFileContent.dependencies[testPluginPackage] = '0.1.0'; - fse.createFileSync(testFilePath); - fse.writeFileSync( - testFilePath, - `${JSON.stringify(packageFileContent, null, 2)}\n`, - 'utf8', - ); - return; -}; -const createTestPluginFile = async ( - testFilePath: string, - pluginsFilePath: string, -) => { - // Copy contents of package file for test - fse.copyFileSync(pluginsFilePath, testFilePath); - const pluginNameCapitalized = testPluginName - .split('-') - .map(name => capitalize(name)) - .join(''); - const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; - const exportStatement = `export {${pluginNameCapitalized}}`; - addExportStatement(testFilePath, importStatement, exportStatement); -}; - -function mkTestDir(testDirPath: string) { - fse.mkdirSync(testDirPath); - for (let i = 0; i < 50; i++) - fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); -} From 429b7f08cb604aad22bad8296874ec6d6c8f33d1 Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Wed, 8 Apr 2020 23:43:31 -0500 Subject: [PATCH 12/14] Fixed some lint issues --- .../remove-plugin/removePlugin.test.ts | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 5ed8e82d13..aac3caece9 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -31,6 +31,49 @@ const BACKSTAGE = `@backstage`; const testPluginName = 'yarn-test-package'; const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; +const removeEmptyLines = (file: string): string => + file + .split('\n') + .filter(Boolean) + .join('\n'); + +const createTestPackageFile = async ( + testFilePath: string, + packageFile: string, +) => { + // Copy contents of package file for test + const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + + packageFileContent.dependencies[testPluginPackage] = '0.1.0'; + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContent, null, 2)}\n`, + 'utf8', + ); + return; +}; +const createTestPluginFile = async ( + testFilePath: string, + pluginsFilePath: string, +) => { + // Copy contents of package file for test + fse.copyFileSync(pluginsFilePath, testFilePath); + const pluginNameCapitalized = testPluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + const exportStatement = `export {${pluginNameCapitalized}}`; + addExportStatement(testFilePath, importStatement, exportStatement); +}; + +function mkTestDir(testDirPath: string) { + fse.mkdirSync(testDirPath); + for (let i = 0; i < 50; i++) + fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); +} + describe('removePlugin', () => { describe('Remove Plugin Dependencies', () => { const appPath = paths.resolveTargetRoot('packages', 'app'); @@ -131,46 +174,3 @@ describe('removePlugin', () => { }); }); }); - -const removeEmptyLines = (file: string): string => - file - .split('\n') - .filter(Boolean) - .join('\n'); - -const createTestPackageFile = async ( - testFilePath: string, - packageFile: string, -) => { - // Copy contents of package file for test - const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); - - packageFileContent.dependencies[testPluginPackage] = '0.1.0'; - fse.createFileSync(testFilePath); - fse.writeFileSync( - testFilePath, - `${JSON.stringify(packageFileContent, null, 2)}\n`, - 'utf8', - ); - return; -}; -const createTestPluginFile = async ( - testFilePath: string, - pluginsFilePath: string, -) => { - // Copy contents of package file for test - fse.copyFileSync(pluginsFilePath, testFilePath); - const pluginNameCapitalized = testPluginName - .split('-') - .map(name => capitalize(name)) - .join(''); - const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; - const exportStatement = `export {${pluginNameCapitalized}}`; - addExportStatement(testFilePath, importStatement, exportStatement); -}; - -function mkTestDir(testDirPath: string) { - fse.mkdirSync(testDirPath); - for (let i = 0; i < 50; i++) - fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); -} From 6c514e1a1e0856b7d99905e9abcacb487260432c Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Thu, 16 Apr 2020 13:12:07 -0500 Subject: [PATCH 13/14] Updates test, uses temporary directory now --- .../remove-plugin/removePlugin.test.ts | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index aac3caece9..1cd47864c6 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -16,8 +16,13 @@ import fse from 'fs-extra'; import path from 'path'; +import os from 'os'; import { paths } from '../../helpers/paths'; -import { addExportStatement, capitalize } from '../create-plugin/createPlugin'; +import { + addExportStatement, + capitalize, + createTemporaryPluginFolder, +} from '../create-plugin/createPlugin'; import { addCodeownersEntry } from '../create-plugin/lib/codeowners'; import { removeReferencesFromAppPackage, @@ -27,9 +32,11 @@ import { removePluginFromCodeOwners, } from './removePlugin'; +// Some constant variables const BACKSTAGE = `@backstage`; const testPluginName = 'yarn-test-package'; const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; +const tempDir = path.join(os.tmpdir(), 'remove-plugin-test'); const removeEmptyLines = (file: string): string => file @@ -53,6 +60,7 @@ const createTestPackageFile = async ( ); return; }; + const createTestPluginFile = async ( testFilePath: string, pluginsFilePath: string, @@ -63,16 +71,20 @@ const createTestPluginFile = async ( .split('-') .map(name => capitalize(name)) .join(''); - const importStatement = `import { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; - const exportStatement = `export {${pluginNameCapitalized}}`; - addExportStatement(testFilePath, importStatement, exportStatement); + const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + addExportStatement(testFilePath, exportStatement); }; -function mkTestDir(testDirPath: string) { +const mkTestPluginDir = (testDirPath: string) => { fse.mkdirSync(testDirPath); for (let i = 0; i < 50; i++) fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); -} +}; + +beforeAll(() => { + // Create temporary directory for all tests + createTemporaryPluginFolder(tempDir); +}); describe('removePlugin', () => { describe('Remove Plugin Dependencies', () => { @@ -81,7 +93,7 @@ describe('removePlugin', () => { it('removes plugin references from /packages/app/package.json', async () => { // Set up test const packageFilePath = path.join(appPath, 'package.json'); - const testFilePath = path.join(appPath, 'test.json'); + const testFilePath = path.join(tempDir, 'test.json'); createTestPackageFile(testFilePath, packageFilePath); try { await removeReferencesFromAppPackage(testFilePath, testPluginName); @@ -97,7 +109,7 @@ describe('removePlugin', () => { } }); it('removes plugin exports from /packages/app/src/packacge.json', async () => { - const testFilePath = path.join(appPath, 'src', 'test.ts'); + const testFilePath = path.join(tempDir, 'test.ts'); const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts'); createTestPluginFile(testFilePath, pluginsFilePaths); try { @@ -114,7 +126,7 @@ describe('removePlugin', () => { } }); it('removes codeOwners references', async () => { - const testFilePath = path.join(githubDir, 'test'); + const testFilePath = path.join(tempDir, 'test'); const codeownersPath = path.join(githubDir, 'CODEOWNERS'); try { fse.copySync(codeownersPath, testFilePath); @@ -144,7 +156,7 @@ describe('removePlugin', () => { describe('Removes Plugin Directory', () => { it('removes plugin directory from /plugins', async () => { try { - mkTestDir(testDirPath); + mkTestPluginDir(testDirPath); expect(fse.existsSync(testDirPath)).toBeTruthy(); await removePluginDirectory(testDirPath); expect(fse.existsSync(testDirPath)).toBeFalsy(); @@ -161,7 +173,7 @@ describe('removePlugin', () => { `plugin-${testPluginName}`, ); try { - mkTestDir(testDirPath); + mkTestPluginDir(testDirPath); fse.ensureSymlinkSync(testSymLinkPath, testDirPath); await removeSymLink(testSymLinkPath); @@ -174,3 +186,8 @@ describe('removePlugin', () => { }); }); }); + +afterAll(() => { + // Remove temporary directory + fse.removeSync(tempDir); +}); From fd190f8bd542dd0ef073e1ca01c8c530d042b6ee Mon Sep 17 00:00:00 2001 From: Jose Balanza Martinez Date: Thu, 16 Apr 2020 13:14:38 -0500 Subject: [PATCH 14/14] Update Create plugin logic --- .../src/commands/create-plugin/createPlugin.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index ac200d01f1..1af5832818 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -70,14 +70,13 @@ export const capitalize = (str: string): string => export const addExportStatement = async ( file: string, - importStatement: string, exportStatement: string, ) => { const newContents = fs .readFileSync(file, 'utf8') .split('\n') .filter(Boolean) // get rid of empty lines - .concat([importStatement, exportStatement]) + .concat([exportStatement]) .concat(['']) // newline at end of file .join('\n'); @@ -122,19 +121,16 @@ export async function addPluginToApp(rootDir: string, pluginName: string) { .split('-') .map(name => capitalize(name)) .join(''); - const pluginImport = `import { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`; - const pluginExport = `export { ${pluginNameCapitalized} };`; + const pluginExport = `export { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`; const pluginsFilePath = 'packages/app/src/plugins.ts'; const pluginsFile = resolvePath(rootDir, pluginsFilePath); await Task.forItem('processing', pluginsFilePath, async () => { - await addExportStatement(pluginsFile, pluginImport, pluginExport).catch( - error => { - throw new Error( - `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, - ); - }, - ); + await addExportStatement(pluginsFile, pluginExport).catch(error => { + throw new Error( + `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, + ); + }); }); }