diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts deleted file mode 100644 index 55e9cddd3d..0000000000 --- a/packages/cli/src/lib/diff/handlers.ts +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 chalk from 'chalk'; -import { diffLines } from 'diff'; -import { sep, posix } from 'path'; -import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types'; - -function sortObjectKeys(obj: Record) { - const sortedKeys = Object.keys(obj).sort(); - for (const key of sortedKeys) { - const value = obj[key]; - delete obj[key]; - obj[key] = value; - } -} - -class PackageJsonHandler { - static async handler( - { path, write, missing, targetContents, templateContents }: FileDiff, - prompt: PromptFunc, - variant?: string, - ) { - 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: FileDiff, prompt: PromptFunc) { - return PackageJsonHandler.handler(file, prompt, 'app'); - } - - constructor( - private readonly writeFunc: WriteFileFunc, - private readonly prompt: PromptFunc, - private readonly pkg: any, - private readonly targetPkg: any, - private readonly variant?: string, - ) {} - - 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. - private async syncField( - fieldName: string, - obj: any = this.pkg, - targetObj: any = this.targetPkg, - prefix?: string, - sort?: boolean, - optional?: boolean, - ) { - const fullFieldName = chalk.cyan( - prefix ? `${prefix}[${fieldName}]` : fieldName, - ); - const newValue = obj[fieldName]; - const coloredNewValue = chalk.cyan(JSON.stringify(newValue)); - - if (fieldName in targetObj) { - const oldValue = targetObj[fieldName]; - if (JSON.stringify(oldValue) === JSON.stringify(newValue)) { - return; - } - - 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; - 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(); - } - } - } - - private 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(); - } - } - } - } - - private async syncScripts() { - const pkgScripts = this.pkg.scripts; - const targetScripts = (this.targetPkg.scripts = - this.targetPkg.scripts || {}); - - if (!pkgScripts) { - return; - } - - // Skip diffing package scripts that have been migrated to the new commands - 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'); - } - } - - private async syncPublishConfig() { - const pkgPublishConf = this.pkg.publishConfig; - const targetPublishConf = this.targetPkg.publishConfig; - - // If template doesn't have a publish config we're done - if (!pkgPublishConf) { - return; - } - - // Publish config can be removed the the target, skip in that case - 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)) { - // Don't want to mess with peoples internal setup - if (!['access', 'registry'].includes(key)) { - await this.syncField( - key, - pkgPublishConf, - targetPublishConf, - 'publishConfig', - ); - } - } - } - - private async syncDependencies(fieldName: string, required: boolean = false) { - const pkgDeps = this.pkg[fieldName]; - const targetDeps = (this.targetPkg[fieldName] = - this.targetPkg[fieldName] || {}); - - if (!pkgDeps && !required) { - return; - } - - // Hardcoded removal of these during migration - 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, - ); - } - } - - private async syncReactDeps() { - const targetDeps = (this.targetPkg.dependencies = - this.targetPkg.dependencies || {}); - - // Remove these from from deps since they're now in peerDeps - await this.syncField('react', {}, targetDeps, 'dependencies'); - await this.syncField('react-dom', {}, targetDeps, 'dependencies'); - } - - private async write() { - await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)}\n`); - } -} - -// Make sure the file is an exact match of the template -async function exactMatchHandler( - { path, write, missing, targetContents, templateContents }: FileDiff, - prompt: PromptFunc, -) { - console.log(`Checking ${path}`); - const coloredPath = chalk.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 = diffLines(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 write(templateContents); - } -} - -// Adds the file if it is missing, but doesn't check existing files -async function existsHandler( - { path, write, missing, templateContents }: FileDiff, - prompt: PromptFunc, -) { - console.log(`Making sure ${path} exists`); - - const coloredPath = chalk.cyan(path); - - if (missing) { - if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) { - await write(templateContents); - } - return; - } -} - -async function skipHandler({ path }: FileDiff) { - console.log(`Skipping ${path}`); -} - -export const handlers = { - skip: skipHandler, - exists: existsHandler, - exactMatch: exactMatchHandler, - packageJson: PackageJsonHandler.handler, - appPackageJson: PackageJsonHandler.appHandler, -}; - -export async function handleAllFiles( - fileHandlers: FileHandler[], - files: FileDiff[], - promptFunc: PromptFunc, -) { - for (const file of files) { - const path = file.path.split(sep).join(posix.sep); - const fileHandler = fileHandlers.find(handler => - handler.patterns.some(pattern => - typeof pattern === 'string' ? pattern === path : pattern.test(path), - ), - ); - if (fileHandler) { - await fileHandler.handler(file, promptFunc); - } else { - throw new Error(`No template file handler found for ${path}`); - } - } -} diff --git a/packages/cli/src/lib/diff/index.ts b/packages/cli/src/lib/diff/index.ts deleted file mode 100644 index 79a55f023e..0000000000 --- a/packages/cli/src/lib/diff/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 * from './handlers'; -export * from './prompts'; -export * from './read'; -export * from './types'; diff --git a/packages/cli/src/lib/diff/prompts.ts b/packages/cli/src/lib/diff/prompts.ts deleted file mode 100644 index d2ddca5fc0..0000000000 --- a/packages/cli/src/lib/diff/prompts.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 chalk from 'chalk'; -import inquirer from 'inquirer'; -import { PromptFunc } from './types'; - -export const inquirerPromptFunc: PromptFunc = async msg => { - const { result } = await inquirer.prompt({ - type: 'confirm', - name: 'result', - message: chalk.blue(msg), - }); - return result; -}; - -export const makeCheckPromptFunc = () => { - let failed = false; - - const promptFunc: PromptFunc = async msg => { - failed = true; - console.log(chalk.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] as const; -}; - -export const yesPromptFunc: PromptFunc = async msg => { - console.log(`Accepting: "${msg}"`); - return true; -}; diff --git a/packages/cli/src/lib/diff/read.ts b/packages/cli/src/lib/diff/read.ts deleted file mode 100644 index 95b0bed43d..0000000000 --- a/packages/cli/src/lib/diff/read.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { - dirname, - resolve as resolvePath, - relative as relativePath, -} from 'path'; -import handlebars from 'handlebars'; -import recursiveReadDir from 'recursive-readdir'; -import { paths } from '../paths'; -import { FileDiff } from './types'; -import { createPackageVersionProvider } from '../../lib/version'; - -export type TemplatedFile = { - path: string; - contents: string; -}; - -async function readTemplateFile( - templateFile: string, - templateVars: any, -): Promise { - const contents = await fs.readFile(templateFile, 'utf8'); - - if (!templateFile.endsWith('.hbs')) { - return contents; - } - const packageVersionProvider = createPackageVersionProvider(undefined); - - return handlebars.compile(contents)(templateVars, { - helpers: { - versionQuery(name: string, hint: string | unknown) { - return packageVersionProvider( - name, - typeof hint === 'string' ? hint : undefined, - ); - }, - }, - }); -} - -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 templatedFiles = new Array(); - for (const templateFile of templateFilePaths) { - const path = relativePath(templateDir, templateFile).replace(/\.hbs$/, ''); - const contents = await readTemplateFile(templateFile, templateVars); - - templatedFiles.push({ path, contents }); - } - - return templatedFiles; -} - -async function diffTemplatedFiles( - targetDir: string, - templatedFiles: TemplatedFile[], -): Promise { - const fileDiffs = new Array(); - for (const { path, contents: templateContents } of templatedFiles) { - const targetPath = resolvePath(targetDir, path); - const targetExists = await fs.pathExists(targetPath); - - const write = async (contents: string) => { - await fs.ensureDir(dirname(targetPath)); - await fs.writeFile(targetPath, contents, 'utf8'); - }; - - if (targetExists) { - const targetContents = await fs.readFile(targetPath, 'utf8'); - fileDiffs.push({ - path, - write, - missing: false, - targetContents, - templateContents, - }); - } else { - fileDiffs.push({ - path, - write, - missing: true, - targetContents: '', - templateContents, - }); - } - } - - return fileDiffs; -} - -// Read all template files for a given template, along with all matching files in the target dir -export async function diffTemplateFiles(template: string, templateData: any) { - const templateDir = paths.resolveOwn('templates', template); - - const templatedFiles = await readTemplate(templateDir, templateData); - const fileDiffs = await diffTemplatedFiles(paths.targetDir, templatedFiles); - return fileDiffs; -} diff --git a/packages/cli/src/lib/diff/types.ts b/packages/cli/src/lib/diff/types.ts deleted file mode 100644 index ba649f66ff..0000000000 --- a/packages/cli/src/lib/diff/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 WriteFileFunc = (contents: string) => Promise; - -export type FileDiff = { - // Relative path within the target directory - path: string; - // Whether the target file exists in the target directory. - missing: boolean; - // Contents of the file in the target directory, or an empty string if the file is missing. - targetContents: string; - // Contents of the compiled template file - templateContents: string; - // Write new contents to the target file - write: WriteFileFunc; -}; - -export type PromptFunc = (msg: string) => Promise; - -export type HandlerFunc = (file: FileDiff, prompt: PromptFunc) => Promise; - -export type FileHandler = { - patterns: Array; - handler: HandlerFunc; -};