UNPKG

@backstage/cli

Version:

CLI for developing Backstage plugins and apps

279 lines (273 loc) • 10.9 kB
'use strict'; var fs = require('fs-extra'); var util = require('util'); var chalk = require('chalk'); var inquirer = require('inquirer'); var child_process = require('child_process'); var path = require('path'); var camelCase = require('lodash/camelCase'); var upperFirst = require('lodash/upperFirst'); var os = require('os'); var errors = require('@backstage/errors'); var tasks = require('./tasks-6771fb2c.cjs.js'); var index = require('./index-d2845aa8.cjs.js'); var Lockfile = require('./Lockfile-e5943b84.cjs.js'); require('minimatch'); require('@manypkg/get-packages'); require('./yarn-8315d2ff.cjs.js'); require('./run-eac5f3ab.cjs.js'); require('handlebars'); require('ora'); require('recursive-readdir'); require('commander'); require('semver'); require('@backstage/cli-common'); require('@yarnpkg/parsers'); require('@yarnpkg/lockfile'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var inquirer__default = /*#__PURE__*/_interopDefaultLegacy(inquirer); var camelCase__default = /*#__PURE__*/_interopDefaultLegacy(camelCase); var upperFirst__default = /*#__PURE__*/_interopDefaultLegacy(upperFirst); var os__default = /*#__PURE__*/_interopDefaultLegacy(os); const exec = util.promisify(child_process.exec); async function checkExists(destination) { await tasks.Task.forItem("checking", destination, async () => { if (await fs__default["default"].pathExists(destination)) { const existing = chalk__default["default"].cyan( destination.replace(`${index.paths.targetRoot}/`, "") ); throw new Error( `A plugin with the same name already exists: ${existing} Please try again with a different plugin ID` ); } }); } const sortObjectByKeys = (obj) => { return Object.keys(obj).sort().reduce((result, key) => { result[key] = obj[key]; return result; }, {}); }; const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); const addExportStatement = async (file, exportStatement) => { const newContents = fs__default["default"].readFileSync(file, "utf8").split("\n").filter(Boolean).concat([exportStatement]).concat([""]).join("\n"); await fs__default["default"].writeFile(file, newContents, "utf8"); }; async function addPluginDependencyToApp(rootDir, pluginPackage, versionStr) { const packageFilePath = "packages/app/package.json"; const packageFile = path.resolve(rootDir, packageFilePath); await tasks.Task.forItem("processing", packageFilePath, async () => { const packageFileContent = await fs__default["default"].readFile(packageFile, "utf-8"); const packageFileJson = JSON.parse(packageFileContent); const dependencies = packageFileJson.dependencies; if (dependencies[pluginPackage]) { throw new Error( `Plugin ${pluginPackage} already exists in ${packageFile}` ); } dependencies[pluginPackage] = `^${versionStr}`; packageFileJson.dependencies = sortObjectByKeys(dependencies); const newContents = `${JSON.stringify(packageFileJson, null, 2)} `; await fs__default["default"].writeFile(packageFile, newContents, "utf-8").catch((error) => { throw new Error( `Failed to add plugin as dependency to app: ${packageFile}: ${error.message}` ); }); }); } async function addPluginExtensionToApp(pluginId, extensionName, pluginPackage) { const pluginsFilePath = index.paths.resolveTargetRoot("packages/app/src/App.tsx"); if (!await fs__default["default"].pathExists(pluginsFilePath)) { return; } await tasks.Task.forItem("processing", pluginsFilePath, async () => { var _a; const content = await fs__default["default"].readFile(pluginsFilePath, "utf8"); const revLines = content.split("\n").reverse(); const lastImportIndex = revLines.findIndex( (line) => line.match(/ from ("|').*("|')/) ); const lastRouteIndex = revLines.findIndex( (line) => line.match(/<\/FlatRoutes/) ); if (lastImportIndex !== -1 && lastRouteIndex !== -1) { revLines.splice( lastImportIndex, 0, `import { ${extensionName} } from '${pluginPackage}';` ); const [indentation] = (_a = revLines[lastRouteIndex + 1].match(/^\s*/)) != null ? _a : []; revLines.splice( lastRouteIndex + 1, 0, `${indentation}<Route path="/${pluginId}" element={<${extensionName} />}/>` ); const newContent = revLines.reverse().join("\n"); await fs__default["default"].writeFile(pluginsFilePath, newContent, "utf8"); } }); } async function cleanUp(tempDir) { await tasks.Task.forItem("remove", "temporary directory", async () => { await fs__default["default"].remove(tempDir); }); } async function buildPlugin(pluginFolder) { const commands = [ "yarn install", "yarn lint --fix", "yarn tsc", "yarn build" ]; for (const command of commands) { try { await tasks.Task.forItem("executing", command, async () => { process.chdir(pluginFolder); await exec(command); }).catch((error) => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); throw new Error( `Warning: Could not execute command ${chalk__default["default"].cyan(command)}` ); }); } catch (error) { errors.assertError(error); tasks.Task.error(error.message); break; } } } async function movePlugin(tempDir, destination, id) { await tasks.Task.forItem("moving", id, async () => { await fs__default["default"].move(tempDir, destination).catch((error) => { throw new Error( `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}` ); }); }); } var createPlugin = async (opts) => { const codeownersPath = await tasks.getCodeownersFilePath(index.paths.targetRoot); const questions = [ { type: "input", name: "id", message: chalk__default["default"].blue("Enter an ID for the plugin [required]"), validate: (value) => { if (!value) { return chalk__default["default"].red("Please enter an ID for the plugin"); } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { return chalk__default["default"].red( "Plugin IDs must be lowercase and contain only letters, digits, and dashes." ); } return true; } } ]; if (codeownersPath) { questions.push({ type: "input", name: "owner", message: chalk__default["default"].blue( "Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]" ), validate: (value) => { if (!value) { return true; } const ownerIds = tasks.parseOwnerIds(value); if (!ownerIds) { return chalk__default["default"].red( "The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses of users (e.g. user@example.com)." ); } return true; } }); } const answers = await inquirer__default["default"].prompt(questions); const pluginId = opts.backend && !answers.id.endsWith("-backend") ? `${answers.id}-backend` : answers.id; const name = opts.scope ? `@${opts.scope.replace(/^@/, "")}/plugin-${pluginId}` : `plugin-${pluginId}`; const pluginVar = `${camelCase__default["default"](answers.id)}Plugin`; const extensionName = `${upperFirst__default["default"](camelCase__default["default"](answers.id))}Page`; const npmRegistry = opts.npmRegistry && opts.scope ? opts.npmRegistry : ""; const privatePackage = opts.private === false ? false : true; const isMonoRepo = await fs__default["default"].pathExists(index.paths.resolveTargetRoot("lerna.json")); const appPackage = index.paths.resolveTargetRoot("packages/app"); const templateDir = index.paths.resolveOwn( opts.backend ? "templates/default-backend-plugin" : "templates/default-plugin" ); const pluginDir = isMonoRepo ? index.paths.resolveTargetRoot("plugins", pluginId) : index.paths.resolveTargetRoot(pluginId); const { version: pluginVersion } = isMonoRepo ? await fs__default["default"].readJson(index.paths.resolveTargetRoot("lerna.json")) : { version: "0.1.0" }; let lockfile; try { lockfile = await Lockfile.Lockfile.load(index.paths.resolveTargetRoot("yarn.lock")); } catch (error) { console.warn(`No yarn.lock available, ${error}`); } tasks.Task.log(); tasks.Task.log("Creating the plugin..."); tasks.Task.section("Checking if the plugin ID is available"); await checkExists(pluginDir); tasks.Task.section("Creating a temporary plugin directory"); const tempDir = await fs__default["default"].mkdtemp( path.join(os__default["default"].tmpdir(), `backstage-plugin-${pluginId}`) ); try { tasks.Task.section("Preparing files"); await tasks.templatingTask( templateDir, tempDir, { ...answers, pluginVar, pluginVersion, extensionName, name, privatePackage, npmRegistry }, index.createPackageVersionProvider(lockfile), isMonoRepo ); tasks.Task.section("Moving to final location"); await movePlugin(tempDir, pluginDir, pluginId); tasks.Task.section("Building the plugin"); await buildPlugin(pluginDir); if (await fs__default["default"].pathExists(appPackage) && !opts.backend) { tasks.Task.section("Adding plugin as dependency in app"); await addPluginDependencyToApp(index.paths.targetRoot, name, pluginVersion); tasks.Task.section("Import plugin in app"); await addPluginExtensionToApp(pluginId, extensionName, name); } if (answers.owner) { await tasks.addCodeownersEntry(`/plugins/${pluginId}`, answers.owner); } tasks.Task.log(); tasks.Task.log(`\u{1F947} Successfully created ${chalk__default["default"].cyan(`${name}`)}`); tasks.Task.log(); tasks.Task.exit(); } catch (error) { errors.assertError(error); tasks.Task.error(error.message); tasks.Task.log("It seems that something went wrong when creating the plugin \u{1F914}"); tasks.Task.log("We are going to clean up, and then you can try again."); tasks.Task.section("Cleanup"); await cleanUp(tempDir); tasks.Task.error("\u{1F525} Failed to create plugin!"); tasks.Task.exit(1); } }; exports.addExportStatement = addExportStatement; exports.addPluginDependencyToApp = addPluginDependencyToApp; exports.addPluginExtensionToApp = addPluginExtensionToApp; exports.capitalize = capitalize; exports["default"] = createPlugin; exports.movePlugin = movePlugin; //# sourceMappingURL=createPlugin-563b25e0.cjs.js.map