cli: remove old diff implementation
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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<string, unknown>) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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<string> {
|
||||
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<TemplatedFile[]> {
|
||||
const templateFilePaths = await recursiveReadDir(templateDir).catch(error => {
|
||||
throw new Error(`Failed to read template directory: ${error.message}`);
|
||||
});
|
||||
|
||||
const templatedFiles = new Array<TemplatedFile>();
|
||||
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<FileDiff[]> {
|
||||
const fileDiffs = new Array<FileDiff>();
|
||||
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;
|
||||
}
|
||||
@@ -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<void>;
|
||||
|
||||
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<boolean>;
|
||||
|
||||
export type HandlerFunc = (file: FileDiff, prompt: PromptFunc) => Promise<void>;
|
||||
|
||||
export type FileHandler = {
|
||||
patterns: Array<string | RegExp>;
|
||||
handler: HandlerFunc;
|
||||
};
|
||||
Reference in New Issue
Block a user