@backstage/cli
Version:
CLI for developing Backstage plugins and apps
629 lines (611 loc) • 21.9 kB
JavaScript
var os = require('os');
var fs = require('fs-extra');
var path = require('path');
var chalk = require('chalk');
var inquirer = require('inquirer');
var camelCase = require('lodash/camelCase');
var upperFirst = require('lodash/upperFirst');
var index = require('./index-ce56dce5.cjs.js');
var codeowners = require('./codeowners-874aee37.cjs.js');
var tasks = require('./tasks-84de240c.cjs.js');
var Lockfile = require('./Lockfile-eced6070.cjs.js');
require('minimatch');
require('@manypkg/get-packages');
require('./yarn-6cd89e16.cjs.js');
require('./run-a0658306.cjs.js');
var partition = require('lodash/partition');
var cliNode = require('@backstage/cli-node');
var errors = require('@backstage/errors');
require('commander');
require('semver');
require('@backstage/cli-common');
require('handlebars');
require('ora');
require('util');
require('recursive-readdir');
require('child_process');
require('@yarnpkg/parsers');
require('@yarnpkg/lockfile');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
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 partition__default = /*#__PURE__*/_interopDefaultLegacy(partition);
function createFactory(config) {
return config;
}
function pluginIdPrompt() {
return {
type: "input",
name: "id",
message: "Enter the ID of the plugin [required]",
validate: (value) => {
if (!value) {
return "Please enter the ID of the plugin";
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return "Plugin IDs must be lowercase and contain only letters, digits, and dashes.";
}
return true;
}
};
}
function ownerPrompt() {
return {
type: "input",
name: "owner",
message: "Enter an owner to add to CODEOWNERS [optional]",
when: (opts) => Boolean(opts.codeOwnersPath),
validate: (value) => {
if (!value) {
return true;
}
const ownerIds = codeowners.parseOwnerIds(value);
if (!ownerIds) {
return "The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses (e.g. user@example.com).";
}
return true;
}
};
}
async function executePluginPackageTemplate(ctx, options) {
const { targetDir } = options;
let lockfile;
try {
lockfile = await Lockfile.Lockfile.load(index.paths.resolveTargetRoot("yarn.lock"));
} catch {
}
tasks.Task.section("Checking Prerequisites");
const shortPluginDir = path.relative(index.paths.targetRoot, targetDir);
await tasks.Task.forItem("availability", shortPluginDir, async () => {
if (await fs__default["default"].pathExists(targetDir)) {
throw new Error(
`A package with the same plugin ID already exists at ${chalk__default["default"].cyan(
shortPluginDir
)}. Please try again with a different ID.`
);
}
});
const tempDir = await tasks.Task.forItem("creating", "temp dir", async () => {
return await ctx.createTemporaryDirectory("backstage-create");
});
tasks.Task.section("Executing Template");
await tasks.templatingTask(
index.paths.resolveOwn("templates", options.templateName),
tempDir,
options.values,
index.createPackageVersionProvider(lockfile),
ctx.isMonoRepo
);
const pkgJsonPath = path.resolve(tempDir, "package.json");
if (await fs__default["default"].pathExists(pkgJsonPath)) {
const pkgJson = await fs__default["default"].readJson(pkgJsonPath);
await fs__default["default"].writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
}
tasks.Task.section("Installing");
await tasks.Task.forItem("moving", shortPluginDir, async () => {
await fs__default["default"].move(tempDir, targetDir).catch((error) => {
throw new Error(
`Failed to move package from ${tempDir} to ${targetDir}, ${error.message}`
);
});
});
ctx.markAsModified();
}
const frontendPlugin = createFactory({
name: "plugin",
description: "A new frontend plugin",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const name = ctx.scope ? `@${ctx.scope}/plugin-${id}` : `backstage-plugin-${id}`;
const extensionName = `${upperFirst__default["default"](camelCase__default["default"](id))}Page`;
tasks.Task.log();
tasks.Task.log(`Creating frontend plugin ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", id) : index.paths.resolveTargetRoot(`backstage-plugin-${id}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "default-plugin",
values: {
id,
name,
extensionName,
pluginVar: `${camelCase__default["default"](id)}Plugin`,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry
}
});
if (await fs__default["default"].pathExists(index.paths.resolveTargetRoot("packages/app"))) {
await tasks.Task.forItem("app", "adding dependency", async () => {
await tasks.addPackageDependency(
index.paths.resolveTargetRoot("packages/app/package.json"),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`
}
}
);
});
await tasks.Task.forItem("app", "adding import", async () => {
var _a;
const pluginsFilePath = index.paths.resolveTargetRoot(
"packages/app/src/App.tsx"
);
if (!await fs__default["default"].pathExists(pluginsFilePath)) {
return;
}
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) {
const importLine = `import { ${extensionName} } from '${name}';`;
if (!content.includes(importLine)) {
revLines.splice(lastImportIndex, 0, importLine);
}
const componentLine = `<Route path="/${id}" element={<${extensionName} />} />`;
if (!content.includes(componentLine)) {
const [indentation] = (_a = revLines[lastRouteIndex + 1].match(/^\s*/)) != null ? _a : [];
revLines.splice(lastRouteIndex + 1, 0, indentation + componentLine);
}
const newContent = revLines.reverse().join("\n");
await fs__default["default"].writeFile(pluginsFilePath, newContent, "utf8");
}
});
}
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${id}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const backendPlugin = createFactory({
name: "backend-plugin",
description: "A new backend plugin",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const pluginId = `${id}-backend`;
const name = ctx.scope ? `@${ctx.scope}/plugin-${pluginId}` : `backstage-plugin-${pluginId}`;
tasks.Task.log();
tasks.Task.log(`Creating backend plugin ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", pluginId) : index.paths.resolveTargetRoot(`backstage-plugin-${pluginId}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "default-backend-plugin",
values: {
id,
name,
pluginVar: `${camelCase__default["default"](id)}Plugin`,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry
}
});
if (await fs__default["default"].pathExists(index.paths.resolveTargetRoot("packages/backend"))) {
await tasks.Task.forItem("backend", "adding dependency", async () => {
await tasks.addPackageDependency(
index.paths.resolveTargetRoot("packages/backend/package.json"),
{
dependencies: {
[name]: `^${ctx.defaultVersion}`
}
}
);
});
}
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${id}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const webLibraryPackage = createFactory({
name: "web-library",
description: "A new web-library package",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const name = ctx.scope ? `@${ctx.scope}/${id}` : `${id}`;
tasks.Task.log();
tasks.Task.log(`Creating web-library package ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("packages", id) : index.paths.resolveTargetRoot(`${id}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "web-library-package",
values: {
id,
name,
pluginVersion: ctx.defaultVersion,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry
}
});
if (options.owner) {
await codeowners.addCodeownersEntry(`/packages/${id}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const pluginCommon = createFactory({
name: "plugin-common",
description: "A new isomorphic common plugin package",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const suffix = `${id}-common`;
const name = ctx.scope ? `@${ctx.scope}/plugin-${suffix}` : `backstage-plugin-${suffix}`;
tasks.Task.log();
tasks.Task.log(`Creating backend plugin ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", suffix) : index.paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "default-common-plugin-package",
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion
}
});
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${suffix}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const pluginNode = createFactory({
name: "plugin-node",
description: "A new Node.js library plugin package",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const suffix = `${id}-node`;
const name = ctx.scope ? `@${ctx.scope}/plugin-${suffix}` : `backstage-plugin-${suffix}`;
tasks.Task.log();
tasks.Task.log(`Creating Node.js plugin library ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", suffix) : index.paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "default-node-plugin-package",
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion
}
});
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${suffix}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const pluginWeb = createFactory({
name: "plugin-react",
description: "A new web library plugin package",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [pluginIdPrompt(), ownerPrompt()],
async create(options, ctx) {
const { id } = options;
const suffix = `${id}-react`;
const name = ctx.scope ? `@${ctx.scope}/plugin-${suffix}` : `backstage-plugin-${suffix}`;
tasks.Task.log();
tasks.Task.log(`Creating web plugin library ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", suffix) : index.paths.resolveTargetRoot(`backstage-plugin-${suffix}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "default-react-plugin-package",
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion
}
});
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${suffix}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
const scaffolderModule = createFactory({
name: "scaffolder-module",
description: "An module exporting custom actions for @backstage/plugin-scaffolder-backend",
optionsDiscovery: async () => ({
codeOwnersPath: await codeowners.getCodeownersFilePath(index.paths.targetRoot)
}),
optionsPrompts: [
{
type: "input",
name: "id",
message: "Enter the name of the module [required]",
validate: (value) => {
if (!value) {
return "Please enter the name of the module";
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
return "Module names must be lowercase and contain only letters, digits, and dashes.";
}
return true;
}
},
ownerPrompt()
],
async create(options, ctx) {
const { id } = options;
const slug = `scaffolder-backend-module-${id}`;
let name = `backstage-plugin-${slug}`;
if (ctx.scope) {
if (ctx.scope === "backstage") {
name = `@backstage/plugin-${slug}`;
} else {
name = `@${ctx.scope}/backstage-plugin-${slug}`;
}
}
tasks.Task.log();
tasks.Task.log(`Creating module ${chalk__default["default"].cyan(name)}`);
const targetDir = ctx.isMonoRepo ? index.paths.resolveTargetRoot("plugins", slug) : index.paths.resolveTargetRoot(`backstage-plugin-${slug}`);
await executePluginPackageTemplate(ctx, {
targetDir,
templateName: "scaffolder-module",
values: {
id,
name,
privatePackage: ctx.private,
npmRegistry: ctx.npmRegistry,
pluginVersion: ctx.defaultVersion
}
});
if (options.owner) {
await codeowners.addCodeownersEntry(`/plugins/${slug}`, options.owner);
}
await tasks.Task.forCommand("yarn install", { cwd: targetDir, optional: true });
await tasks.Task.forCommand("yarn lint --fix", {
cwd: targetDir,
optional: true
});
}
});
var factories = /*#__PURE__*/Object.freeze({
__proto__: null,
frontendPlugin: frontendPlugin,
backendPlugin: backendPlugin,
webLibraryPackage: webLibraryPackage,
pluginCommon: pluginCommon,
pluginNode: pluginNode,
pluginWeb: pluginWeb,
scaffolderModule: scaffolderModule
});
function applyPromptMessageTransforms(prompt, transforms) {
return {
...prompt,
message: prompt.message && (async (answers) => {
if (typeof prompt.message === "function") {
return transforms.message(await prompt.message(answers));
}
return transforms.message(await prompt.message);
}),
validate: prompt.validate && (async (...args) => {
const result = await prompt.validate(...args);
if (typeof result === "string") {
return transforms.error(result);
}
return result;
})
};
}
class FactoryRegistry {
static async interactiveSelect(preselected) {
let selected = preselected;
if (!selected) {
const answers = await inquirer__default["default"].prompt([
{
type: "list",
name: "name",
message: "What do you want to create?",
choices: Array.from(this.factoryMap.values()).map((factory2) => ({
name: `${factory2.name} - ${factory2.description}`,
value: factory2.name
}))
}
]);
selected = answers.name;
}
const factory = this.factoryMap.get(selected);
if (!factory) {
throw new Error(`Unknown selection '${selected}'`);
}
return factory;
}
static async populateOptions(factory, provided) {
let currentOptions = provided;
if (factory.optionsDiscovery) {
const discoveredOptions = await factory.optionsDiscovery();
currentOptions = {
...currentOptions,
...discoveredOptions
};
}
if (factory.optionsPrompts) {
const [hasAnswers, needsAnswers] = partition__default["default"](
factory.optionsPrompts,
(option) => option.name in currentOptions
);
for (const option of hasAnswers) {
const value = provided[option.name];
if (option.validate) {
const result = option.validate(value);
if (result !== true) {
throw new Error(`Invalid option '${option.name}'. ${result}`);
}
}
}
currentOptions = await inquirer__default["default"].prompt(
needsAnswers.map(
(option) => applyPromptMessageTransforms(option, {
message: chalk__default["default"].blue,
error: chalk__default["default"].red
})
),
currentOptions
);
}
return currentOptions;
}
}
FactoryRegistry.factoryMap = new Map(
Object.values(factories).map((factory) => [factory.name, factory])
);
function parseOptions(optionStrings) {
const options = {};
for (const str of optionStrings) {
const [key] = str.split("=", 1);
const value = str.slice(key.length + 1);
if (!key || str[key.length] !== "=") {
throw new Error(
`Invalid option '${str}', must be of the format <key>=<value>`
);
}
options[key] = value;
}
return options;
}
var _new = async (opts) => {
var _a;
const factory = await FactoryRegistry.interactiveSelect(opts.select);
const providedOptions = parseOptions(opts.option);
const options = await FactoryRegistry.populateOptions(
factory,
providedOptions
);
let defaultVersion = "0.1.0";
if (opts.baseVersion) {
defaultVersion = opts.baseVersion;
} else {
const lernaVersion = await fs__default["default"].readJson(index.paths.resolveTargetRoot("lerna.json")).then((pkg) => pkg.version).catch(() => void 0);
if (lernaVersion) {
defaultVersion = lernaVersion;
}
}
const tempDirs = new Array();
async function createTemporaryDirectory(name) {
const dir = await fs__default["default"].mkdtemp(path.join(os__default["default"].tmpdir(), name));
tempDirs.push(dir);
return dir;
}
let modified = false;
try {
await factory.create(options, {
isMonoRepo: await cliNode.isMonoRepo(),
defaultVersion,
scope: (_a = opts.scope) == null ? void 0 : _a.replace(/^@/, ""),
npmRegistry: opts.npmRegistry,
private: Boolean(opts.private),
createTemporaryDirectory,
markAsModified() {
modified = true;
}
});
tasks.Task.log();
tasks.Task.log(`\u{1F389} Successfully created ${factory.name}`);
tasks.Task.log();
} catch (error) {
errors.assertError(error);
tasks.Task.error(error.message);
if (modified) {
tasks.Task.log("It seems that something went wrong in the creation process \u{1F914}");
tasks.Task.log();
tasks.Task.log(
"We have left the changes that were made intact in case you want to"
);
tasks.Task.log(
"continue manually, but you can also revert the changes and try again."
);
tasks.Task.error(`\u{1F525} Failed to create ${factory.name}!`);
}
} finally {
for (const dir of tempDirs) {
try {
await fs__default["default"].remove(dir);
} catch (error) {
console.error(
`Failed to remove temporary directory '${dir}', ${error}`
);
}
}
}
};
exports["default"] = _new;
//# sourceMappingURL=new-b59ffab1.cjs.js.map
;