UNPKG

@backstage/cli

Version:

CLI for developing Backstage plugins and apps

438 lines (429 loc) • 13.9 kB
'use strict'; var fs = require('fs-extra'); var chalk = require('chalk'); var differ = require('diff'); var path = require('path'); var inquirer = require('inquirer'); var handlebars = require('handlebars'); var recursive = require('recursive-readdir'); var index = require('./index-ce56dce5.cjs.js'); require('commander'); require('semver'); require('@backstage/cli-common'); require('@backstage/errors'); 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 handlebars__default = /*#__PURE__*/_interopDefaultLegacy(handlebars); var recursive__default = /*#__PURE__*/_interopDefaultLegacy(recursive); function sortObjectKeys(obj) { const sortedKeys = Object.keys(obj).sort(); for (const key of sortedKeys) { const value = obj[key]; delete obj[key]; obj[key] = value; } } class PackageJsonHandler { constructor(writeFunc, prompt, pkg, targetPkg, variant) { this.writeFunc = writeFunc; this.prompt = prompt; this.pkg = pkg; this.targetPkg = targetPkg; this.variant = variant; } static async handler({ path, write, missing, targetContents, templateContents }, prompt, variant) { console.log("Checking package.json"); if (missing) { throw new Error(`${path} doesn't exist`); } const pkg = JSON.parse(templateContents); const targetPkg = JSON.parse(targetContents); const handler = new PackageJsonHandler( write, prompt, pkg, targetPkg, variant ); await handler.handle(); } static async appHandler(file, prompt) { return PackageJsonHandler.handler(file, prompt, "app"); } async handle() { await this.syncField("main"); if (this.variant !== "app") { await this.syncField("main:src"); } await this.syncField("types"); await this.syncFiles(); await this.syncScripts(); await this.syncPublishConfig(); await this.syncDependencies("dependencies"); await this.syncDependencies("peerDependencies", true); await this.syncDependencies("devDependencies"); await this.syncReactDeps(); } // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. async syncField(fieldName, obj = this.pkg, targetObj = this.targetPkg, prefix, sort, optional) { const fullFieldName = chalk__default["default"].cyan( prefix ? `${prefix}[${fieldName}]` : fieldName ); const newValue = obj[fieldName]; const coloredNewValue = chalk__default["default"].cyan(JSON.stringify(newValue)); if (fieldName in targetObj) { const oldValue = targetObj[fieldName]; if (JSON.stringify(oldValue) === JSON.stringify(newValue)) { return; } const coloredOldValue = chalk__default["default"].cyan(JSON.stringify(oldValue)); const msg = `package.json has mismatched field, ${fullFieldName}, change from ${coloredOldValue} to ${coloredNewValue}?`; if (await this.prompt(msg)) { targetObj[fieldName] = newValue; if (sort) { sortObjectKeys(targetObj); } await this.write(); } } else if (fieldName in obj && optional !== true) { if (await this.prompt( `package.json is missing field ${fullFieldName}, set to ${coloredNewValue}?` )) { targetObj[fieldName] = newValue; if (sort) { sortObjectKeys(targetObj); } await this.write(); } } } async syncFiles() { const { configSchema } = this.targetPkg; const hasSchemaFile = typeof configSchema === "string"; if (!this.targetPkg.files) { const expected = hasSchemaFile ? ["dist", configSchema] : ["dist"]; if (await this.prompt( `package.json is missing field "files", set to ${JSON.stringify( expected )}?` )) { this.targetPkg.files = expected; await this.write(); } } else { const missing = []; if (!this.targetPkg.files.includes("dist")) { missing.push("dist"); } if (hasSchemaFile && !this.targetPkg.files.includes(configSchema)) { missing.push(configSchema); } if (missing.length) { if (await this.prompt( `package.json is missing ${JSON.stringify( missing )} in the "files" field, add?` )) { this.targetPkg.files.push(...missing); await this.write(); } } } } async syncScripts() { const pkgScripts = this.pkg.scripts; const targetScripts = this.targetPkg.scripts = this.targetPkg.scripts || {}; if (!pkgScripts) { return; } const hasNewScript = Object.values(targetScripts).some( (script) => String(script).includes("backstage-cli package ") ); if (hasNewScript) { return; } for (const key of Object.keys(pkgScripts)) { await this.syncField(key, pkgScripts, targetScripts, "scripts"); } } async syncPublishConfig() { const pkgPublishConf = this.pkg.publishConfig; const targetPublishConf = this.targetPkg.publishConfig; if (!pkgPublishConf) { return; } if (!targetPublishConf) { if (await this.prompt("Missing publishConfig, do you want to add it?")) { this.targetPkg.publishConfig = pkgPublishConf; await this.write(); } return; } for (const key of Object.keys(pkgPublishConf)) { if (!["access", "registry"].includes(key)) { await this.syncField( key, pkgPublishConf, targetPublishConf, "publishConfig" ); } } } async syncDependencies(fieldName, required = false) { const pkgDeps = this.pkg[fieldName]; const targetDeps = this.targetPkg[fieldName] = this.targetPkg[fieldName] || {}; if (!pkgDeps && !required) { return; } await this.syncField("@backstage/core", {}, targetDeps, fieldName, true); await this.syncField( "@backstage/core-api", {}, targetDeps, fieldName, true ); for (const key of Object.keys(pkgDeps)) { if (this.variant === "app" && key.startsWith("plugin-")) { continue; } await this.syncField( key, pkgDeps, targetDeps, fieldName, true, !required ); } } async syncReactDeps() { const targetDeps = this.targetPkg.dependencies = this.targetPkg.dependencies || {}; await this.syncField("react", {}, targetDeps, "dependencies"); await this.syncField("react-dom", {}, targetDeps, "dependencies"); } async write() { await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)} `); } } async function exactMatchHandler({ path, write, missing, targetContents, templateContents }, prompt) { console.log(`Checking ${path}`); const coloredPath = chalk__default["default"].cyan(path); if (missing) { if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { await write(templateContents); } return; } if (targetContents === templateContents) { return; } const diffs = differ.diffLines(targetContents, templateContents); for (const diff of diffs) { if (diff.added) { process.stdout.write(chalk__default["default"].green(`+${diff.value}`)); } else if (diff.removed) { process.stdout.write(chalk__default["default"].red(`-${diff.value}`)); } else { process.stdout.write(` ${diff.value}`); } } if (await prompt( `Outdated ${coloredPath}, do you want to apply the above patch?` )) { await write(templateContents); } } async function existsHandler({ path, write, missing, templateContents }, prompt) { console.log(`Making sure ${path} exists`); const coloredPath = chalk__default["default"].cyan(path); if (missing) { if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { await write(templateContents); } return; } } async function skipHandler({ path }) { console.log(`Skipping ${path}`); } const handlers = { skip: skipHandler, exists: existsHandler, exactMatch: exactMatchHandler, packageJson: PackageJsonHandler.handler, appPackageJson: PackageJsonHandler.appHandler }; async function handleAllFiles(fileHandlers, files, promptFunc) { for (const file of files) { const path$1 = file.path.split(path.sep).join(path.posix.sep); const fileHandler = fileHandlers.find( (handler) => handler.patterns.some( (pattern) => typeof pattern === "string" ? pattern === path$1 : pattern.test(path$1) ) ); if (fileHandler) { await fileHandler.handler(file, promptFunc); } else { throw new Error(`No template file handler found for ${path$1}`); } } } const inquirerPromptFunc = async (msg) => { const { result } = await inquirer__default["default"].prompt({ type: "confirm", name: "result", message: chalk__default["default"].blue(msg) }); return result; }; const makeCheckPromptFunc = () => { let failed = false; const promptFunc = async (msg) => { failed = true; console.log(chalk__default["default"].red(`[Check Failed] ${msg}`)); return false; }; const finalize = () => { if (failed) { throw new Error( "Check failed, the plugin is not in sync with the latest template" ); } }; return [promptFunc, finalize]; }; const yesPromptFunc = async (msg) => { console.log(`Accepting: "${msg}"`); return true; }; async function readTemplateFile(templateFile, templateVars) { const contents = await fs__default["default"].readFile(templateFile, "utf8"); if (!templateFile.endsWith(".hbs")) { return contents; } const packageVersionProvider = index.createPackageVersionProvider(void 0); return handlebars__default["default"].compile(contents)(templateVars, { helpers: { versionQuery(name, hint) { return packageVersionProvider( name, typeof hint === "string" ? hint : void 0 ); } } }); } async function readTemplate(templateDir, templateVars) { const templateFilePaths = await recursive__default["default"](templateDir).catch((error) => { throw new Error(`Failed to read template directory: ${error.message}`); }); const templatedFiles = new Array(); for (const templateFile of templateFilePaths) { const path$1 = path.relative(templateDir, templateFile).replace(/\.hbs$/, ""); const contents = await readTemplateFile(templateFile, templateVars); templatedFiles.push({ path: path$1, contents }); } return templatedFiles; } async function diffTemplatedFiles(targetDir, templatedFiles) { const fileDiffs = new Array(); for (const { path: path$1, contents: templateContents } of templatedFiles) { const targetPath = path.resolve(targetDir, path$1); const targetExists = await fs__default["default"].pathExists(targetPath); const write = async (contents) => { await fs__default["default"].ensureDir(path.dirname(targetPath)); await fs__default["default"].writeFile(targetPath, contents, "utf8"); }; if (targetExists) { const targetContents = await fs__default["default"].readFile(targetPath, "utf8"); fileDiffs.push({ path: path$1, write, missing: false, targetContents, templateContents }); } else { fileDiffs.push({ path: path$1, write, missing: true, targetContents: "", templateContents }); } } return fileDiffs; } async function diffTemplateFiles(template, templateData) { const templateDir = index.paths.resolveOwn("templates", template); const templatedFiles = await readTemplate(templateDir, templateData); const fileDiffs = await diffTemplatedFiles(index.paths.targetDir, templatedFiles); return fileDiffs; } const fileHandlers = [ { patterns: ["package.json"], handler: handlers.packageJson }, { // make sure files in 1st level of src/ and dev/ exist patterns: [".eslintrc.js"], handler: handlers.exists }, { patterns: ["README.md", "tsconfig.json", /^src\//, /^dev\//], handler: handlers.skip } ]; var diff = async (opts) => { let promptFunc = inquirerPromptFunc; let finalize = () => { }; if (opts.check) { [promptFunc, finalize] = makeCheckPromptFunc(); } else if (opts.yes) { promptFunc = yesPromptFunc; } const data = await readPluginData(); const templateFiles = await diffTemplateFiles("default-plugin", data); await handleAllFiles(fileHandlers, templateFiles, promptFunc); finalize(); }; async function readPluginData() { let name; let privatePackage; let pluginVersion; let npmRegistry; try { const pkg = require(index.paths.resolveTarget("package.json")); name = pkg.name; privatePackage = pkg.private; pluginVersion = pkg.version; const scope = name.split("/")[0]; if (`${scope}:registry` in pkg.publishConfig) { const registryURL = pkg.publishConfig[`${scope}:registry`]; npmRegistry = `"${scope}:registry" : "${registryURL}"`; } else npmRegistry = ""; } catch (error) { throw new Error(`Failed to read target package, ${error}`); } const pluginTsContents = await fs__default["default"].readFile( index.paths.resolveTarget("src/plugin.ts"), "utf8" ); const pluginIdMatch = pluginTsContents.match(/id: ['"`](.+?)['"`]/); if (!pluginIdMatch) { throw new Error(`Failed to parse plugin.ts, no plugin ID found`); } const id = pluginIdMatch[1]; return { id, name, privatePackage, pluginVersion, npmRegistry }; } exports["default"] = diff; //# sourceMappingURL=diff-92123077.cjs.js.map