From 7f03d85d91fdcb8d6406f07d1971e97ea0beb113 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 May 2020 17:53:43 +0200 Subject: [PATCH 01/11] packages/cli: added placeholder plugin:diff command --- .../cli/src/commands/plugin/diff/index.ts | 19 +++++++++++++++++++ packages/cli/src/index.ts | 5 +++++ 2 files changed, 24 insertions(+) create mode 100644 packages/cli/src/commands/plugin/diff/index.ts diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts new file mode 100644 index 0000000000..47db3f9519 --- /dev/null +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export default async () => { + console.log(`DEBUG: Diff!`); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1c3f66bce7..bdb0efff8f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -62,6 +62,11 @@ const main = (argv: string[]) => { .description('Serves the dev/ folder of a plugin') .action(actionHandler(() => require('commands/plugin/serve'))); + program + .command('plugin:diff') + .description('Diff an existing plugin with the creation template') + .action(actionHandler(() => require('commands/plugin/diff'))); + program .command('lint') .option('--fix', 'Attempt to automatically fix violations') From 4423aa931522dd01aac286685890c95c0787cbd9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 May 2020 17:54:39 +0200 Subject: [PATCH 02/11] packages/cli: added basic logic for diff to parse out existing plugin state --- .../cli/src/commands/plugin/diff/index.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 47db3f9519..e9b5be783b 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -14,6 +14,42 @@ * limitations under the License. */ -export default async () => { - console.log(`DEBUG: Diff!`); +import fs from 'fs-extra'; +import { paths } from 'lib/paths'; +import { version } from 'lib/version'; + +type PluginInfo = { + id: string; + name: string; +}; + +// Reads info from the existing plugin +async function readPluginInfo(): Promise { + let name: string; + try { + const pkg = require(paths.resolveTarget('package.json')); + name = pkg.name; + } catch (error) { + throw new Error(`Failed to read target package, ${error}`); + } + + const pluginTsContents = await fs.readFile( + paths.resolveTarget('src/plugin.ts'), + 'utf8', + ); + // TODO: replace with some proper parsing logic or plugin metadata file + 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 }; +} + +export default async () => { + const pluginInfo = await readPluginInfo(); + const templateVars = { version, ...pluginInfo }; + console.log('DEBUG: templateVars =', templateVars); }; From c348444fa60401474db63dc4b67e75b55ef3b14c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 2 May 2020 12:16:32 +0200 Subject: [PATCH 03/11] packages/cli: read template and target files for diffing --- .../cli/src/commands/plugin/diff/index.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index e9b5be783b..7b7a36d17c 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -15,6 +15,9 @@ */ import fs from 'fs-extra'; +import { relative as relativePath } from 'path'; +import handlebars from 'handlebars'; +import recursiveReadDir from 'recursive-readdir'; import { paths } from 'lib/paths'; import { version } from 'lib/version'; @@ -48,8 +51,84 @@ async function readPluginInfo(): Promise { return { id, name }; } +type TemplateFile = { + // Relative path within the target directory + targetPath: string; + // Contents of the compiled template file + templateContents: string; +} & ( + | { + // Whether the template file exists in the target directory + targetExists: true; + // Contents of the file in the target directory, if it exists + targetContents: string; + } + | { + // Whether the template file exists in the target directory + targetExists: false; + } +); + +async function readTemplateFile( + templateFile: string, + templateVars: any, +): Promise { + const contents = await fs.readFile(templateFile, 'utf8'); + + if (!templateFile.endsWith('.hbs')) { + return contents; + } + + return handlebars.compile(contents)(templateVars); +} + +async function readTemplate( + templateDir: string, + templateVars: any, +): Promise { + const templateFilePaths = await recursiveReadDir(templateDir).catch(error => { + throw new Error(`Failed to read template directory: ${error.message}`); + }); + + const templateFiles = new Array(); + for (const templateFile of templateFilePaths) { + // Target file inside the target dir without template extension + const targetFile = templateFile + .replace(templateDir, paths.targetDir) + .replace(/\.hbs$/, ''); + const targetPath = relativePath(paths.targetDir, targetFile); + + const templateContents = await readTemplateFile(templateFile, templateVars); + + const targetExists = await fs.pathExists(targetFile); + if (targetExists) { + const targetContents = await fs.readFile(targetFile, 'utf8'); + templateFiles.push({ + targetPath, + targetExists, + targetContents, + templateContents, + }); + } else { + templateFiles.push({ + targetPath, + targetExists, + templateContents, + }); + } + } + + return templateFiles; +} + export default async () => { const pluginInfo = await readPluginInfo(); const templateVars = { version, ...pluginInfo }; console.log('DEBUG: templateVars =', templateVars); + + const templateDir = paths.resolveOwn('templates/default-plugin'); + console.log('DEBUG: templateDir =', templateDir); + + const templateFiles = await readTemplate(templateDir, templateVars); + console.log('DEBUG: templateFiles =', templateFiles); }; From 323f6b715a238e4c6a0ad224bd12b46372e95d79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 12:04:17 +0200 Subject: [PATCH 04/11] packages/cli: separate handlers for different template files in diff --- packages/cli/package.json | 2 + .../cli/src/commands/plugin/diff/index.ts | 83 ++++++++++++++++++- yarn.lock | 7 +- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index bc1a349aa3..418e5f9fe2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "backstage-cli": "bin/backstage-cli" }, "devDependencies": { + "@types/diff": "^4.0.2", "@types/fs-extra": "^8.1.0", "@types/html-webpack-plugin": "^3.2.2", "@types/inquirer": "^6.5.0", @@ -58,6 +59,7 @@ "chokidar": "^3.3.1", "commander": "^4.1.1", "dashify": "^2.0.0", + "diff": "^4.0.2", "eslint-plugin-import": "^2.20.2", "eslint-plugin-monorepo": "^0.2.1", "fork-ts-checker-webpack-plugin": "^4.0.5", diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 7b7a36d17c..3eaa6c0a08 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -16,10 +16,12 @@ import fs from 'fs-extra'; import { relative as relativePath } from 'path'; -import handlebars from 'handlebars'; +import { diffLines } from 'diff'; +import handlebars, { Template } from 'handlebars'; import recursiveReadDir from 'recursive-readdir'; import { paths } from 'lib/paths'; import { version } from 'lib/version'; +import chalk from 'chalk'; type PluginInfo = { id: string; @@ -121,14 +123,87 @@ async function readTemplate( return templateFiles; } +async function packageJsonHandler(file: TemplateFile) { + if (!file.targetExists) { + throw new Error(`${file.targetPath} doesn't exist`); + } + + console.log(`pkg.json handler: ${file.targetPath}`); + const pkg = JSON.parse(file.templateContents); + console.log('DEBUG: pkg =', pkg); + const targetPkg = JSON.parse(file.targetContents); + console.log('DEBUG: targetPkg =', targetPkg); +} + +async function diffHandler(file: TemplateFile) { + if (!file.targetExists) { + // TODO: prompt to write template file + return; + } + if (file.targetContents === file.templateContents) { + return; + } + + const diffs = diffLines(file.targetContents, file.templateContents); + + for (const diff of diffs) { + if (diff.added) { + process.stdout.write(chalk.green(`+${diff.value}`)); + } else if (diff.removed) { + process.stdout.write(chalk.red(`-${diff.value}`)); + } else { + process.stdout.write(` ${diff.value}`); + } + } +} + +async function skipHandler(file: TemplateFile) { + console.log(`Skipping ${file.targetPath}`); +} + +type FileHandler = { + patterns: Array; + handler: (file: TemplateFile) => Promise; +}; + +const fileHandlers: FileHandler[] = [ + { + patterns: ['package.json'], + handler: packageJsonHandler, + }, + { + patterns: ['.eslintrc.js', 'tsconfig.json'], + handler: diffHandler, + }, + { + patterns: ['README.md', /^^src\//], + handler: skipHandler, + }, +]; + export default async () => { const pluginInfo = await readPluginInfo(); const templateVars = { version, ...pluginInfo }; - console.log('DEBUG: templateVars =', templateVars); const templateDir = paths.resolveOwn('templates/default-plugin'); - console.log('DEBUG: templateDir =', templateDir); const templateFiles = await readTemplate(templateDir, templateVars); - console.log('DEBUG: templateFiles =', templateFiles); + + for (const templateFile of templateFiles) { + const { targetPath } = templateFile; + const fileHandler = fileHandlers.find(handler => + handler.patterns.some(pattern => + typeof pattern === 'string' + ? pattern === targetPath + : pattern.test(targetPath), + ), + ); + if (fileHandler) { + await fileHandler.handler(templateFile); + } else { + throw new Error(`No template file handler found for ${targetPath}`); + } + } + + console.log(`DEBUG: done!`); }; diff --git a/yarn.lock b/yarn.lock index 79eac2cb21..d788ed448a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3915,6 +3915,11 @@ resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== +"@types/diff@^4.0.2": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" + integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -8114,7 +8119,7 @@ diff-sequences@^25.1.0: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== -diff@^4.0.1: +diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== From 8ebb45e990c98f803502fd8b36fcd40fcdfcf89b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 13:44:24 +0200 Subject: [PATCH 05/11] packages/cli: functional diff command with prompt for updates --- .../cli/src/commands/plugin/diff/index.ts | 154 ++++++++++++++++-- 1 file changed, 137 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 3eaa6c0a08..89e262ff38 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -15,13 +15,14 @@ */ import fs from 'fs-extra'; -import { relative as relativePath } from 'path'; +import { relative as relativePath, dirname } from 'path'; import { diffLines } from 'diff'; -import handlebars, { Template } from 'handlebars'; +import handlebars from 'handlebars'; import recursiveReadDir from 'recursive-readdir'; import { paths } from 'lib/paths'; import { version } from 'lib/version'; import chalk from 'chalk'; +import inquirer from 'inquirer'; type PluginInfo = { id: string; @@ -123,29 +124,138 @@ async function readTemplate( return templateFiles; } -async function packageJsonHandler(file: TemplateFile) { - if (!file.targetExists) { - throw new Error(`${file.targetPath} doesn't exist`); +async function writeTargetFile(targetPath: string, contents: string) { + const path = paths.resolveTarget(targetPath); + await fs.ensureDir(dirname(path)); + await fs.writeFile(path, contents, 'utf8'); +} + +class PackageJsonHandler { + static async handler(file: TemplateFile) { + console.log('Checking package.json'); + + if (!file.targetExists) { + throw new Error(`${file.targetPath} doesn't exist`); + } + + const pkg = JSON.parse(file.templateContents); + const targetPkg = JSON.parse(file.targetContents); + + const handler = new PackageJsonHandler(file, pkg, targetPkg); + await handler.handle(); } - console.log(`pkg.json handler: ${file.targetPath}`); - const pkg = JSON.parse(file.templateContents); - console.log('DEBUG: pkg =', pkg); - const targetPkg = JSON.parse(file.targetContents); - console.log('DEBUG: targetPkg =', targetPkg); + private changed: boolean = false; + + constructor( + private readonly file: TemplateFile, + private readonly pkg: any, + private readonly targetPkg: any, + ) {} + + async handle() { + await this.syncField('main'); + await this.syncField('types'); + await this.syncField('files'); + await this.syncScripts(); + await this.syncDependencies('dependencies'); + await this.syncDependencies('devDependencies'); + } + + private async syncField( + fieldName: string, + obj: any = this.pkg, + targetObj: any = this.targetPkg, + prefix?: string, + ) { + const fullFieldName = chalk.cyan( + prefix ? `${prefix}[${fieldName}]` : prefix, + ); + const newValue = obj[fieldName]; + + if (fieldName in targetObj) { + const oldValue = targetObj[fieldName]; + if (JSON.stringify(oldValue) === JSON.stringify(newValue)) { + return; + } + + const { addField } = await inquirer.prompt({ + type: 'confirm', + name: 'addField', + message: chalk.blue( + `Outdated field, ${fullFieldName}, change from ` + + `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`, + ), + }); + if (addField) { + targetObj[fieldName] = newValue; + await this.write(); + } + } else { + const { updateField } = await inquirer.prompt({ + type: 'confirm', + name: 'updateField', + message: chalk.blue( + `Missing field ${fullFieldName}, set to ${chalk.cyan(newValue)}?`, + ), + }); + if (updateField) { + targetObj[fieldName] = newValue; + await this.write(); + } + } + } + + private async syncScripts() { + const pkgScripts = this.pkg.scripts; + const targetScripts = (this.targetPkg.scripts = + this.targetPkg.scripts || {}); + + for (const key of Object.keys(pkgScripts)) { + await this.syncField(key, pkgScripts, targetScripts, 'scripts'); + } + } + + private async syncDependencies(fieldName: string) { + const pkgDeps = this.pkg[fieldName]; + const targetDeps = (this.targetPkg[fieldName] = + this.targetPkg[fieldName] || {}); + + for (const key of Object.keys(pkgDeps)) { + await this.syncField(key, pkgDeps, targetDeps, fieldName); + } + } + + private async write() { + await fs.writeFile( + paths.resolveTarget(this.file.targetPath), + JSON.stringify(this.targetPkg, null, 2), + ); + } } async function diffHandler(file: TemplateFile) { + console.log(`Checking ${file.targetPath}`); + + const { targetPath, templateContents } = file; + const coloredPath = chalk.cyan(targetPath); + if (!file.targetExists) { - // TODO: prompt to write template file + const { addFile } = await inquirer.prompt({ + type: 'confirm', + name: 'addFile', + message: chalk.blue(`Missing ${coloredPath}, do you want to add it?`), + }); + if (addFile) { + await writeTargetFile(targetPath, templateContents); + } return; } - if (file.targetContents === file.templateContents) { + if (file.targetContents === templateContents) { return; } - const diffs = diffLines(file.targetContents, file.templateContents); - + const diffs = diffLines(file.targetContents, templateContents); for (const diff of diffs) { if (diff.added) { process.stdout.write(chalk.green(`+${diff.value}`)); @@ -155,6 +265,18 @@ async function diffHandler(file: TemplateFile) { process.stdout.write(` ${diff.value}`); } } + + const { applyPatch } = await inquirer.prompt({ + type: 'confirm', + name: 'applyPatch', + message: chalk.blue( + `Outdated ${coloredPath}, do you want to apply the above patch?`, + ), + }); + + if (applyPatch) { + await writeTargetFile(targetPath, templateContents); + } } async function skipHandler(file: TemplateFile) { @@ -169,7 +291,7 @@ type FileHandler = { const fileHandlers: FileHandler[] = [ { patterns: ['package.json'], - handler: packageJsonHandler, + handler: PackageJsonHandler.handler, }, { patterns: ['.eslintrc.js', 'tsconfig.json'], @@ -204,6 +326,4 @@ export default async () => { throw new Error(`No template file handler found for ${targetPath}`); } } - - console.log(`DEBUG: done!`); }; From acb6212b49080c84ee4446ed7e450fbcfa216f73 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 13:53:19 +0200 Subject: [PATCH 06/11] packages/cli: add add handler for diff command to ensure files exist --- .../cli/src/commands/plugin/diff/index.ts | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 89e262ff38..b204924805 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -234,7 +234,8 @@ class PackageJsonHandler { } } -async function diffHandler(file: TemplateFile) { +// Make sure the file is an exact match of the template +async function exactMatchHandler(file: TemplateFile) { console.log(`Checking ${file.targetPath}`); const { targetPath, templateContents } = file; @@ -279,6 +280,26 @@ async function diffHandler(file: TemplateFile) { } } +// Adds the file if it is missing, but doesn't check existing files +async function addOnlyHandler(file: TemplateFile) { + console.log(`Making sure ${file.targetPath} exists`); + + const { targetPath, templateContents } = file; + const coloredPath = chalk.cyan(targetPath); + + if (!file.targetExists) { + const { addFile } = await inquirer.prompt({ + type: 'confirm', + name: 'addFile', + message: chalk.blue(`Missing ${coloredPath}, do you want to add it?`), + }); + if (addFile) { + await writeTargetFile(targetPath, templateContents); + } + return; + } +} + async function skipHandler(file: TemplateFile) { console.log(`Skipping ${file.targetPath}`); } @@ -294,11 +315,16 @@ const fileHandlers: FileHandler[] = [ handler: PackageJsonHandler.handler, }, { - patterns: ['.eslintrc.js', 'tsconfig.json'], - handler: diffHandler, + patterns: ['tsconfig.json'], + handler: exactMatchHandler, }, { - patterns: ['README.md', /^^src\//], + // make sure files in 1st level of src/ and dev/ exist + patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], + handler: addOnlyHandler, + }, + { + patterns: ['README.md', /^src\//], handler: skipHandler, }, ]; From 5e5bb5a523a04f8e4a8f355b21f00a56ee3b4740 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 14:02:31 +0200 Subject: [PATCH 07/11] packages/cli: make it possible to override prompt func in diff --- .../cli/src/commands/plugin/diff/index.ts | 77 ++++++++----------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index b204924805..6a3343215b 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -131,7 +131,7 @@ async function writeTargetFile(targetPath: string, contents: string) { } class PackageJsonHandler { - static async handler(file: TemplateFile) { + static async handler(file: TemplateFile, prompt: PromptFunc) { console.log('Checking package.json'); if (!file.targetExists) { @@ -141,14 +141,13 @@ class PackageJsonHandler { const pkg = JSON.parse(file.templateContents); const targetPkg = JSON.parse(file.targetContents); - const handler = new PackageJsonHandler(file, pkg, targetPkg); + const handler = new PackageJsonHandler(file, prompt, pkg, targetPkg); await handler.handle(); } - private changed: boolean = false; - constructor( private readonly file: TemplateFile, + private readonly prompt: PromptFunc, private readonly pkg: any, private readonly targetPkg: any, ) {} @@ -162,6 +161,7 @@ class PackageJsonHandler { await this.syncDependencies('devDependencies'); } + // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. private async syncField( fieldName: string, obj: any = this.pkg, @@ -179,27 +179,19 @@ class PackageJsonHandler { return; } - const { addField } = await inquirer.prompt({ - type: 'confirm', - name: 'addField', - message: chalk.blue( - `Outdated field, ${fullFieldName}, change from ` + - `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`, - ), - }); - if (addField) { + const msg = + `Outdated field, ${fullFieldName}, change from ` + + `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`; + if (await this.prompt(msg)) { targetObj[fieldName] = newValue; await this.write(); } } else { - const { updateField } = await inquirer.prompt({ - type: 'confirm', - name: 'updateField', - message: chalk.blue( + if ( + await this.prompt( `Missing field ${fullFieldName}, set to ${chalk.cyan(newValue)}?`, - ), - }); - if (updateField) { + ) + ) { targetObj[fieldName] = newValue; await this.write(); } @@ -235,19 +227,14 @@ class PackageJsonHandler { } // Make sure the file is an exact match of the template -async function exactMatchHandler(file: TemplateFile) { +async function exactMatchHandler(file: TemplateFile, prompt: PromptFunc) { console.log(`Checking ${file.targetPath}`); const { targetPath, templateContents } = file; const coloredPath = chalk.cyan(targetPath); if (!file.targetExists) { - const { addFile } = await inquirer.prompt({ - type: 'confirm', - name: 'addFile', - message: chalk.blue(`Missing ${coloredPath}, do you want to add it?`), - }); - if (addFile) { + if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { await writeTargetFile(targetPath, templateContents); } return; @@ -267,33 +254,24 @@ async function exactMatchHandler(file: TemplateFile) { } } - const { applyPatch } = await inquirer.prompt({ - type: 'confirm', - name: 'applyPatch', - message: chalk.blue( + if ( + await prompt( `Outdated ${coloredPath}, do you want to apply the above patch?`, - ), - }); - - if (applyPatch) { + ) + ) { await writeTargetFile(targetPath, templateContents); } } // Adds the file if it is missing, but doesn't check existing files -async function addOnlyHandler(file: TemplateFile) { +async function addOnlyHandler(file: TemplateFile, prompt: PromptFunc) { console.log(`Making sure ${file.targetPath} exists`); const { targetPath, templateContents } = file; const coloredPath = chalk.cyan(targetPath); if (!file.targetExists) { - const { addFile } = await inquirer.prompt({ - type: 'confirm', - name: 'addFile', - message: chalk.blue(`Missing ${coloredPath}, do you want to add it?`), - }); - if (addFile) { + if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { await writeTargetFile(targetPath, templateContents); } return; @@ -304,9 +282,20 @@ async function skipHandler(file: TemplateFile) { console.log(`Skipping ${file.targetPath}`); } +type PromptFunc = (msg: string) => Promise; + +const inquirerPromptFunc: PromptFunc = async msg => { + const { result } = await inquirer.prompt({ + type: 'confirm', + name: 'result', + message: chalk.blue(msg), + }); + return result; +}; + type FileHandler = { patterns: Array; - handler: (file: TemplateFile) => Promise; + handler: (file: TemplateFile, prompt: PromptFunc) => Promise; }; const fileHandlers: FileHandler[] = [ @@ -347,7 +336,7 @@ export default async () => { ), ); if (fileHandler) { - await fileHandler.handler(templateFile); + await fileHandler.handler(templateFile, inquirerPromptFunc); } else { throw new Error(`No template file handler found for ${targetPath}`); } From 7d02e1d15c73188310d4b8ee6a0c8f9eed6cfa5e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 14:21:20 +0200 Subject: [PATCH 08/11] packages/cli: split diff command implementation into multiple files --- .../cli/src/commands/plugin/diff/handlers.ts | 209 +++++++++++ .../cli/src/commands/plugin/diff/index.ts | 337 ++---------------- packages/cli/src/commands/plugin/diff/read.ts | 110 ++++++ .../cli/src/commands/plugin/diff/types.ts | 50 +++ 4 files changed, 393 insertions(+), 313 deletions(-) create mode 100644 packages/cli/src/commands/plugin/diff/handlers.ts create mode 100644 packages/cli/src/commands/plugin/diff/read.ts create mode 100644 packages/cli/src/commands/plugin/diff/types.ts diff --git a/packages/cli/src/commands/plugin/diff/handlers.ts b/packages/cli/src/commands/plugin/diff/handlers.ts new file mode 100644 index 0000000000..ec39733adf --- /dev/null +++ b/packages/cli/src/commands/plugin/diff/handlers.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import chalk from 'chalk'; +import { dirname } from 'path'; +import { diffLines } from 'diff'; +import { paths } from 'lib/paths'; +import { TemplateFile, PromptFunc, FileHandler } from './types'; + +export async function writeTargetFile(targetPath: string, contents: string) { + const path = paths.resolveTarget(targetPath); + await fs.ensureDir(dirname(path)); + await fs.writeFile(path, contents, 'utf8'); +} + +class PackageJsonHandler { + static async handler(file: TemplateFile, prompt: PromptFunc) { + console.log('Checking package.json'); + + if (!file.targetExists) { + throw new Error(`${file.targetPath} doesn't exist`); + } + + const pkg = JSON.parse(file.templateContents); + const targetPkg = JSON.parse(file.targetContents); + + const handler = new PackageJsonHandler(file, prompt, pkg, targetPkg); + await handler.handle(); + } + + constructor( + private readonly file: TemplateFile, + private readonly prompt: PromptFunc, + private readonly pkg: any, + private readonly targetPkg: any, + ) {} + + async handle() { + await this.syncField('main'); + await this.syncField('types'); + await this.syncField('files'); + await this.syncScripts(); + await this.syncDependencies('dependencies'); + await this.syncDependencies('devDependencies'); + } + + // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. + private async syncField( + fieldName: string, + obj: any = this.pkg, + targetObj: any = this.targetPkg, + prefix?: string, + ) { + const fullFieldName = chalk.cyan( + prefix ? `${prefix}[${fieldName}]` : prefix, + ); + const newValue = obj[fieldName]; + + if (fieldName in targetObj) { + const oldValue = targetObj[fieldName]; + if (JSON.stringify(oldValue) === JSON.stringify(newValue)) { + return; + } + + const msg = + `Outdated field, ${fullFieldName}, change from ` + + `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`; + if (await this.prompt(msg)) { + targetObj[fieldName] = newValue; + await this.write(); + } + } else { + if ( + await this.prompt( + `Missing field ${fullFieldName}, set to ${chalk.cyan(newValue)}?`, + ) + ) { + targetObj[fieldName] = newValue; + await this.write(); + } + } + } + + private async syncScripts() { + const pkgScripts = this.pkg.scripts; + const targetScripts = (this.targetPkg.scripts = + this.targetPkg.scripts || {}); + + for (const key of Object.keys(pkgScripts)) { + await this.syncField(key, pkgScripts, targetScripts, 'scripts'); + } + } + + private async syncDependencies(fieldName: string) { + const pkgDeps = this.pkg[fieldName]; + const targetDeps = (this.targetPkg[fieldName] = + this.targetPkg[fieldName] || {}); + + for (const key of Object.keys(pkgDeps)) { + await this.syncField(key, pkgDeps, targetDeps, fieldName); + } + } + + private async write() { + await fs.writeFile( + paths.resolveTarget(this.file.targetPath), + JSON.stringify(this.targetPkg, null, 2), + ); + } +} + +// Make sure the file is an exact match of the template +async function exactMatchHandler(file: TemplateFile, prompt: PromptFunc) { + console.log(`Checking ${file.targetPath}`); + + const { targetPath, templateContents } = file; + const coloredPath = chalk.cyan(targetPath); + + if (!file.targetExists) { + if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { + await writeTargetFile(targetPath, templateContents); + } + return; + } + if (file.targetContents === templateContents) { + return; + } + + const diffs = diffLines(file.targetContents, templateContents); + for (const diff of diffs) { + if (diff.added) { + process.stdout.write(chalk.green(`+${diff.value}`)); + } else if (diff.removed) { + process.stdout.write(chalk.red(`-${diff.value}`)); + } else { + process.stdout.write(` ${diff.value}`); + } + } + + if ( + await prompt( + `Outdated ${coloredPath}, do you want to apply the above patch?`, + ) + ) { + await writeTargetFile(targetPath, templateContents); + } +} + +// Adds the file if it is missing, but doesn't check existing files +async function existsHandler(file: TemplateFile, prompt: PromptFunc) { + console.log(`Making sure ${file.targetPath} exists`); + + const { targetPath, templateContents } = file; + const coloredPath = chalk.cyan(targetPath); + + if (!file.targetExists) { + if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { + await writeTargetFile(targetPath, templateContents); + } + return; + } +} + +async function skipHandler(file: TemplateFile) { + console.log(`Skipping ${file.targetPath}`); +} + +export const handlers = { + skip: skipHandler, + exists: existsHandler, + exactMatch: exactMatchHandler, + packageJson: PackageJsonHandler.handler, +}; + +export async function handleAllFiles( + fileHandlers: FileHandler[], + files: TemplateFile[], + promptFunc: PromptFunc, +) { + for (const file of files) { + const { targetPath } = file; + const fileHandler = fileHandlers.find(handler => + handler.patterns.some(pattern => + typeof pattern === 'string' + ? pattern === targetPath + : pattern.test(targetPath), + ), + ); + if (fileHandler) { + await fileHandler.handler(file, promptFunc); + } else { + throw new Error(`No template file handler found for ${targetPath}`); + } + } +} diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 6a3343215b..87e8628ee4 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -14,275 +14,31 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { relative as relativePath, dirname } from 'path'; -import { diffLines } from 'diff'; -import handlebars from 'handlebars'; -import recursiveReadDir from 'recursive-readdir'; -import { paths } from 'lib/paths'; -import { version } from 'lib/version'; import chalk from 'chalk'; import inquirer from 'inquirer'; +import { readTemplateFiles } from './read'; +import { handlers, handleAllFiles } from './handlers'; +import { PromptFunc } from './types'; -type PluginInfo = { - id: string; - name: string; -}; - -// Reads info from the existing plugin -async function readPluginInfo(): Promise { - let name: string; - try { - const pkg = require(paths.resolveTarget('package.json')); - name = pkg.name; - } catch (error) { - throw new Error(`Failed to read target package, ${error}`); - } - - const pluginTsContents = await fs.readFile( - paths.resolveTarget('src/plugin.ts'), - 'utf8', - ); - // TODO: replace with some proper parsing logic or plugin metadata file - 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 }; -} - -type TemplateFile = { - // Relative path within the target directory - targetPath: string; - // Contents of the compiled template file - templateContents: string; -} & ( - | { - // Whether the template file exists in the target directory - targetExists: true; - // Contents of the file in the target directory, if it exists - targetContents: string; - } - | { - // Whether the template file exists in the target directory - targetExists: false; - } -); - -async function readTemplateFile( - templateFile: string, - templateVars: any, -): Promise { - const contents = await fs.readFile(templateFile, 'utf8'); - - if (!templateFile.endsWith('.hbs')) { - return contents; - } - - return handlebars.compile(contents)(templateVars); -} - -async function readTemplate( - templateDir: string, - templateVars: any, -): Promise { - const templateFilePaths = await recursiveReadDir(templateDir).catch(error => { - throw new Error(`Failed to read template directory: ${error.message}`); - }); - - const templateFiles = new Array(); - for (const templateFile of templateFilePaths) { - // Target file inside the target dir without template extension - const targetFile = templateFile - .replace(templateDir, paths.targetDir) - .replace(/\.hbs$/, ''); - const targetPath = relativePath(paths.targetDir, targetFile); - - const templateContents = await readTemplateFile(templateFile, templateVars); - - const targetExists = await fs.pathExists(targetFile); - if (targetExists) { - const targetContents = await fs.readFile(targetFile, 'utf8'); - templateFiles.push({ - targetPath, - targetExists, - targetContents, - templateContents, - }); - } else { - templateFiles.push({ - targetPath, - targetExists, - templateContents, - }); - } - } - - return templateFiles; -} - -async function writeTargetFile(targetPath: string, contents: string) { - const path = paths.resolveTarget(targetPath); - await fs.ensureDir(dirname(path)); - await fs.writeFile(path, contents, 'utf8'); -} - -class PackageJsonHandler { - static async handler(file: TemplateFile, prompt: PromptFunc) { - console.log('Checking package.json'); - - if (!file.targetExists) { - throw new Error(`${file.targetPath} doesn't exist`); - } - - const pkg = JSON.parse(file.templateContents); - const targetPkg = JSON.parse(file.targetContents); - - const handler = new PackageJsonHandler(file, prompt, pkg, targetPkg); - await handler.handle(); - } - - constructor( - private readonly file: TemplateFile, - private readonly prompt: PromptFunc, - private readonly pkg: any, - private readonly targetPkg: any, - ) {} - - async handle() { - await this.syncField('main'); - await this.syncField('types'); - await this.syncField('files'); - await this.syncScripts(); - await this.syncDependencies('dependencies'); - await this.syncDependencies('devDependencies'); - } - - // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. - private async syncField( - fieldName: string, - obj: any = this.pkg, - targetObj: any = this.targetPkg, - prefix?: string, - ) { - const fullFieldName = chalk.cyan( - prefix ? `${prefix}[${fieldName}]` : prefix, - ); - const newValue = obj[fieldName]; - - if (fieldName in targetObj) { - const oldValue = targetObj[fieldName]; - if (JSON.stringify(oldValue) === JSON.stringify(newValue)) { - return; - } - - const msg = - `Outdated field, ${fullFieldName}, change from ` + - `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`; - if (await this.prompt(msg)) { - targetObj[fieldName] = newValue; - await this.write(); - } - } else { - if ( - await this.prompt( - `Missing field ${fullFieldName}, set to ${chalk.cyan(newValue)}?`, - ) - ) { - targetObj[fieldName] = newValue; - await this.write(); - } - } - } - - private async syncScripts() { - const pkgScripts = this.pkg.scripts; - const targetScripts = (this.targetPkg.scripts = - this.targetPkg.scripts || {}); - - for (const key of Object.keys(pkgScripts)) { - await this.syncField(key, pkgScripts, targetScripts, 'scripts'); - } - } - - private async syncDependencies(fieldName: string) { - const pkgDeps = this.pkg[fieldName]; - const targetDeps = (this.targetPkg[fieldName] = - this.targetPkg[fieldName] || {}); - - for (const key of Object.keys(pkgDeps)) { - await this.syncField(key, pkgDeps, targetDeps, fieldName); - } - } - - private async write() { - await fs.writeFile( - paths.resolveTarget(this.file.targetPath), - JSON.stringify(this.targetPkg, null, 2), - ); - } -} - -// Make sure the file is an exact match of the template -async function exactMatchHandler(file: TemplateFile, prompt: PromptFunc) { - console.log(`Checking ${file.targetPath}`); - - const { targetPath, templateContents } = file; - const coloredPath = chalk.cyan(targetPath); - - if (!file.targetExists) { - if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { - await writeTargetFile(targetPath, templateContents); - } - return; - } - if (file.targetContents === templateContents) { - return; - } - - const diffs = diffLines(file.targetContents, templateContents); - for (const diff of diffs) { - if (diff.added) { - process.stdout.write(chalk.green(`+${diff.value}`)); - } else if (diff.removed) { - process.stdout.write(chalk.red(`-${diff.value}`)); - } else { - process.stdout.write(` ${diff.value}`); - } - } - - if ( - await prompt( - `Outdated ${coloredPath}, do you want to apply the above patch?`, - ) - ) { - await writeTargetFile(targetPath, templateContents); - } -} - -// Adds the file if it is missing, but doesn't check existing files -async function addOnlyHandler(file: TemplateFile, prompt: PromptFunc) { - console.log(`Making sure ${file.targetPath} exists`); - - const { targetPath, templateContents } = file; - const coloredPath = chalk.cyan(targetPath); - - if (!file.targetExists) { - if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { - await writeTargetFile(targetPath, templateContents); - } - return; - } -} - -async function skipHandler(file: TemplateFile) { - console.log(`Skipping ${file.targetPath}`); -} - -type PromptFunc = (msg: string) => Promise; +const fileHandlers = [ + { + patterns: ['package.json'], + handler: handlers.packageJson, + }, + { + patterns: ['tsconfig.json'], + handler: handlers.exactMatch, + }, + { + // make sure files in 1st level of src/ and dev/ exist + patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], + handler: handlers.exists, + }, + { + patterns: ['README.md', /^src\//], + handler: handlers.skip, + }, +]; const inquirerPromptFunc: PromptFunc = async msg => { const { result } = await inquirer.prompt({ @@ -293,52 +49,7 @@ const inquirerPromptFunc: PromptFunc = async msg => { return result; }; -type FileHandler = { - patterns: Array; - handler: (file: TemplateFile, prompt: PromptFunc) => Promise; -}; - -const fileHandlers: FileHandler[] = [ - { - patterns: ['package.json'], - handler: PackageJsonHandler.handler, - }, - { - patterns: ['tsconfig.json'], - handler: exactMatchHandler, - }, - { - // make sure files in 1st level of src/ and dev/ exist - patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], - handler: addOnlyHandler, - }, - { - patterns: ['README.md', /^src\//], - handler: skipHandler, - }, -]; - export default async () => { - const pluginInfo = await readPluginInfo(); - const templateVars = { version, ...pluginInfo }; - - const templateDir = paths.resolveOwn('templates/default-plugin'); - - const templateFiles = await readTemplate(templateDir, templateVars); - - for (const templateFile of templateFiles) { - const { targetPath } = templateFile; - const fileHandler = fileHandlers.find(handler => - handler.patterns.some(pattern => - typeof pattern === 'string' - ? pattern === targetPath - : pattern.test(targetPath), - ), - ); - if (fileHandler) { - await fileHandler.handler(templateFile, inquirerPromptFunc); - } else { - throw new Error(`No template file handler found for ${targetPath}`); - } - } + const templateFiles = await readTemplateFiles('default-plugin'); + await handleAllFiles(fileHandlers, templateFiles, inquirerPromptFunc); }; diff --git a/packages/cli/src/commands/plugin/diff/read.ts b/packages/cli/src/commands/plugin/diff/read.ts new file mode 100644 index 0000000000..ef981a0081 --- /dev/null +++ b/packages/cli/src/commands/plugin/diff/read.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { relative as relativePath } from 'path'; +import handlebars from 'handlebars'; +import recursiveReadDir from 'recursive-readdir'; +import { paths } from 'lib/paths'; +import { version } from 'lib/version'; +import { PluginInfo, TemplateFile } from './types'; + +// Reads info from the existing plugin +export async function readPluginInfo(): Promise { + let name: string; + try { + const pkg = require(paths.resolveTarget('package.json')); + name = pkg.name; + } catch (error) { + throw new Error(`Failed to read target package, ${error}`); + } + + const pluginTsContents = await fs.readFile( + paths.resolveTarget('src/plugin.ts'), + 'utf8', + ); + // TODO: replace with some proper parsing logic or plugin metadata file + 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 }; +} + +export async function readTemplateFile( + templateFile: string, + templateVars: any, +): Promise { + const contents = await fs.readFile(templateFile, 'utf8'); + + if (!templateFile.endsWith('.hbs')) { + return contents; + } + + return handlebars.compile(contents)(templateVars); +} + +export async function readTemplate( + templateDir: string, + templateVars: any, +): Promise { + const templateFilePaths = await recursiveReadDir(templateDir).catch(error => { + throw new Error(`Failed to read template directory: ${error.message}`); + }); + + const templateFiles = new Array(); + for (const templateFile of templateFilePaths) { + // Target file inside the target dir without template extension + const targetFile = templateFile + .replace(templateDir, paths.targetDir) + .replace(/\.hbs$/, ''); + const targetPath = relativePath(paths.targetDir, targetFile); + + const templateContents = await readTemplateFile(templateFile, templateVars); + + const targetExists = await fs.pathExists(targetFile); + if (targetExists) { + const targetContents = await fs.readFile(targetFile, 'utf8'); + templateFiles.push({ + targetPath, + targetExists, + targetContents, + templateContents, + }); + } else { + templateFiles.push({ + targetPath, + targetExists, + templateContents, + }); + } + } + + return templateFiles; +} + +// Read all template files for a given template, along with all matching files in the target dir +export async function readTemplateFiles(template: string) { + const pluginInfo = await readPluginInfo(); + const templateVars = { version, ...pluginInfo }; + + const templateDir = paths.resolveOwn('templates', template); + + return await readTemplate(templateDir, templateVars); +} diff --git a/packages/cli/src/commands/plugin/diff/types.ts b/packages/cli/src/commands/plugin/diff/types.ts new file mode 100644 index 0000000000..7a04cf8d2e --- /dev/null +++ b/packages/cli/src/commands/plugin/diff/types.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +export type PluginInfo = { + id: string; + name: string; +}; + +export type TemplateFile = { + // Relative path within the target directory + targetPath: string; + // Contents of the compiled template file + templateContents: string; +} & ( + | { + // Whether the template file exists in the target directory + targetExists: true; + // Contents of the file in the target directory, if it exists + targetContents: string; + } + | { + // Whether the template file exists in the target directory + targetExists: false; + } +); + +export type PromptFunc = (msg: string) => Promise; + +export type HandlerFunc = ( + file: TemplateFile, + prompt: PromptFunc, +) => Promise; + +export type FileHandler = { + patterns: Array; + handler: HandlerFunc; +}; From 56ff5d7d97a199572739232dfe748c12c7e4c348 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 14:25:36 +0200 Subject: [PATCH 09/11] packages/cli: add --check and --yes flags to plugin:diff --- .../cli/src/commands/plugin/diff/index.ts | 23 ++++++++++++++++--- packages/cli/src/index.ts | 2 ++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/index.ts b/packages/cli/src/commands/plugin/diff/index.ts index 87e8628ee4..116ba9e633 100644 --- a/packages/cli/src/commands/plugin/diff/index.ts +++ b/packages/cli/src/commands/plugin/diff/index.ts @@ -16,6 +16,7 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; +import { Command } from 'commander'; import { readTemplateFiles } from './read'; import { handlers, handleAllFiles } from './handlers'; import { PromptFunc } from './types'; @@ -49,7 +50,23 @@ const inquirerPromptFunc: PromptFunc = async msg => { return result; }; -export default async () => { - const templateFiles = await readTemplateFiles('default-plugin'); - await handleAllFiles(fileHandlers, templateFiles, inquirerPromptFunc); +const checkPromptFunc: PromptFunc = async msg => { + throw new Error(`Check failed, the following change was needed: ${msg}`); +}; +const yesPromptFunc: PromptFunc = async msg => { + console.log(`Accepting: "${msg}"`); + return true; +}; + +export default async (cmd: Command) => { + let promptFunc = inquirerPromptFunc; + + if (cmd.check) { + promptFunc = checkPromptFunc; + } else if (cmd.yes) { + promptFunc = yesPromptFunc; + } + + const templateFiles = await readTemplateFiles('default-plugin'); + await handleAllFiles(fileHandlers, templateFiles, promptFunc); }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index bdb0efff8f..6c6aec3776 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -64,6 +64,8 @@ const main = (argv: string[]) => { program .command('plugin:diff') + .option('--check', 'Fail if changes are required') + .option('--yes', 'Apply all changes') .description('Diff an existing plugin with the creation template') .action(actionHandler(() => require('commands/plugin/diff'))); From 8e920cada86f8d38d2a16223772d94b124cf8f8c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 14:26:29 +0200 Subject: [PATCH 10/11] packages/cli: add newline at the end of package.json during diff --- packages/cli/src/commands/plugin/diff/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/plugin/diff/handlers.ts b/packages/cli/src/commands/plugin/diff/handlers.ts index ec39733adf..11af6ff42c 100644 --- a/packages/cli/src/commands/plugin/diff/handlers.ts +++ b/packages/cli/src/commands/plugin/diff/handlers.ts @@ -118,7 +118,7 @@ class PackageJsonHandler { private async write() { await fs.writeFile( paths.resolveTarget(this.file.targetPath), - JSON.stringify(this.targetPkg, null, 2), + `${JSON.stringify(this.targetPkg, null, 2)}\n`, ); } } From 41d879931d57f818b6e4efe3d1ea5385f52e2a4c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 15:28:06 +0200 Subject: [PATCH 11/11] packages/cli: clearer plugin:diff output for package.json + fix --- packages/cli/src/commands/plugin/diff/handlers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/plugin/diff/handlers.ts b/packages/cli/src/commands/plugin/diff/handlers.ts index 11af6ff42c..df7ca081de 100644 --- a/packages/cli/src/commands/plugin/diff/handlers.ts +++ b/packages/cli/src/commands/plugin/diff/handlers.ts @@ -66,9 +66,10 @@ class PackageJsonHandler { prefix?: string, ) { const fullFieldName = chalk.cyan( - prefix ? `${prefix}[${fieldName}]` : prefix, + prefix ? `${prefix}[${fieldName}]` : fieldName, ); const newValue = obj[fieldName]; + const coloredNewValue = chalk.cyan(JSON.stringify(newValue)); if (fieldName in targetObj) { const oldValue = targetObj[fieldName]; @@ -76,9 +77,8 @@ class PackageJsonHandler { return; } - const msg = - `Outdated field, ${fullFieldName}, change from ` + - `${chalk.cyan(oldValue)} to ${chalk.cyan(newValue)}?`; + const coloredOldValue = chalk.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; await this.write(); @@ -86,7 +86,7 @@ class PackageJsonHandler { } else { if ( await this.prompt( - `Missing field ${fullFieldName}, set to ${chalk.cyan(newValue)}?`, + `package.json is missing field ${fullFieldName}, set to ${coloredNewValue}?`, ) ) { targetObj[fieldName] = newValue;