Merge pull request #953 from spotify/rugvip/app-diff
packages/cli: add initial app:diff
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { Command } from 'commander';
|
||||
import {
|
||||
diffTemplateFiles,
|
||||
handlers,
|
||||
handleAllFiles,
|
||||
inquirerPromptFunc,
|
||||
makeCheckPromptFunc,
|
||||
yesPromptFunc,
|
||||
} from '../../lib/diff';
|
||||
import { version } from '../../lib/version';
|
||||
|
||||
const fileHandlers = [
|
||||
{
|
||||
patterns: ['packages/app/package.json'],
|
||||
handler: handlers.appPackageJson,
|
||||
},
|
||||
{
|
||||
patterns: [/tsconfig\.json$/],
|
||||
handler: handlers.exactMatch,
|
||||
},
|
||||
{
|
||||
patterns: [
|
||||
/README\.md$/,
|
||||
/\.eslintrc\.js$/,
|
||||
// make sure files in 1st level of src/ and dev/ exist
|
||||
/^packages\/app\/(src|dev)\/[^/]+$/,
|
||||
],
|
||||
handler: handlers.exists,
|
||||
},
|
||||
{
|
||||
patterns: [
|
||||
'lerna.json',
|
||||
/^src\//,
|
||||
/^patches\//,
|
||||
/^packages\/app\/public\//,
|
||||
/^packages\/app\/cypress/,
|
||||
// Let plugin:diff take care of the plugins
|
||||
/^plugins/,
|
||||
/package\.json$/,
|
||||
],
|
||||
handler: handlers.skip,
|
||||
},
|
||||
];
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
let promptFunc = inquirerPromptFunc;
|
||||
let finalize = () => {};
|
||||
|
||||
if (cmd.check) {
|
||||
[promptFunc, finalize] = makeCheckPromptFunc();
|
||||
} else if (cmd.yes) {
|
||||
promptFunc = yesPromptFunc;
|
||||
}
|
||||
|
||||
const templateFiles = await diffTemplateFiles('default-app', { version });
|
||||
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
|
||||
await finalize();
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 { Command } from 'commander';
|
||||
import {
|
||||
diffTemplateFiles,
|
||||
handlers,
|
||||
handleAllFiles,
|
||||
inquirerPromptFunc,
|
||||
makeCheckPromptFunc,
|
||||
yesPromptFunc,
|
||||
} from '../../lib/diff';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
|
||||
export type PluginData = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
let promptFunc = inquirerPromptFunc;
|
||||
let finalize = () => {};
|
||||
|
||||
if (cmd.check) {
|
||||
[promptFunc, finalize] = makeCheckPromptFunc();
|
||||
} else if (cmd.yes) {
|
||||
promptFunc = yesPromptFunc;
|
||||
}
|
||||
|
||||
const data = await readPluginData();
|
||||
const templateFiles = await diffTemplateFiles('default-plugin', {
|
||||
version,
|
||||
...data,
|
||||
});
|
||||
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
|
||||
await finalize();
|
||||
};
|
||||
|
||||
// Reads templating data from the existing plugin
|
||||
async function readPluginData(): Promise<PluginData> {
|
||||
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 };
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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<PluginInfo> {
|
||||
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<string> {
|
||||
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<TemplateFile[]> {
|
||||
const templateFilePaths = await recursiveReadDir(templateDir).catch(
|
||||
(error) => {
|
||||
throw new Error(`Failed to read template directory: ${error.message}`);
|
||||
},
|
||||
);
|
||||
|
||||
const templateFiles = new Array<TemplateFile>();
|
||||
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);
|
||||
}
|
||||
@@ -39,6 +39,13 @@ const main = (argv: string[]) => {
|
||||
.option('--check', 'Enable type checking and linting')
|
||||
.action(actionHandler(() => require('./commands/app/serve')));
|
||||
|
||||
program
|
||||
.command('app:diff')
|
||||
.option('--check', 'Fail if changes are required')
|
||||
.option('--yes', 'Apply all changes')
|
||||
.description('Diff an existing app with the creation template')
|
||||
.action(actionHandler(() => require('./commands/app/diff')));
|
||||
|
||||
program
|
||||
.command('create-plugin')
|
||||
.description('Creates a new plugin in the current repository')
|
||||
@@ -157,7 +164,7 @@ function actionHandler<T extends readonly any[]>(
|
||||
};
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (rejection) => {
|
||||
process.on('unhandledRejection', rejection => {
|
||||
if (rejection instanceof Error) {
|
||||
exitWithError(rejection);
|
||||
} else {
|
||||
|
||||
+67
-48
@@ -14,43 +14,52 @@
|
||||
* 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');
|
||||
}
|
||||
import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types';
|
||||
|
||||
class PackageJsonHandler {
|
||||
static async handler(file: TemplateFile, prompt: PromptFunc) {
|
||||
static async handler(
|
||||
{ path, write, missing, targetContents, templateContents }: FileDiff,
|
||||
prompt: PromptFunc,
|
||||
variant?: string,
|
||||
) {
|
||||
console.log('Checking package.json');
|
||||
|
||||
if (!file.targetExists) {
|
||||
throw new Error(`${file.targetPath} doesn't exist`);
|
||||
if (missing) {
|
||||
throw new Error(`${path} doesn't exist`);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(file.templateContents);
|
||||
const targetPkg = JSON.parse(file.targetContents);
|
||||
const pkg = JSON.parse(templateContents);
|
||||
const targetPkg = JSON.parse(targetContents);
|
||||
|
||||
const handler = new PackageJsonHandler(file, prompt, pkg, targetPkg);
|
||||
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 file: TemplateFile,
|
||||
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.syncField('files');
|
||||
await this.syncScripts();
|
||||
@@ -84,7 +93,7 @@ class PackageJsonHandler {
|
||||
targetObj[fieldName] = newValue;
|
||||
await this.write();
|
||||
}
|
||||
} else {
|
||||
} else if (fieldName in obj) {
|
||||
if (
|
||||
await this.prompt(
|
||||
`package.json is missing field ${fullFieldName}, set to ${coloredNewValue}?`,
|
||||
@@ -101,6 +110,10 @@ class PackageJsonHandler {
|
||||
const targetScripts = (this.targetPkg.scripts =
|
||||
this.targetPkg.scripts || {});
|
||||
|
||||
if (!pkgScripts) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(pkgScripts)) {
|
||||
await this.syncField(key, pkgScripts, targetScripts, 'scripts');
|
||||
}
|
||||
@@ -138,37 +151,42 @@ class PackageJsonHandler {
|
||||
const targetDeps = (this.targetPkg[fieldName] =
|
||||
this.targetPkg[fieldName] || {});
|
||||
|
||||
if (!pkgDeps) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(pkgDeps)) {
|
||||
if (this.variant === 'app' && key.startsWith('plugin-')) {
|
||||
continue;
|
||||
}
|
||||
await this.syncField(key, pkgDeps, targetDeps, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
private async write() {
|
||||
await fs.writeFile(
|
||||
paths.resolveTarget(this.file.targetPath),
|
||||
`${JSON.stringify(this.targetPkg, null, 2)}\n`,
|
||||
);
|
||||
await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the file is an exact match of the template
|
||||
async function exactMatchHandler(file: TemplateFile, prompt: PromptFunc) {
|
||||
console.log(`Checking ${file.targetPath}`);
|
||||
async function exactMatchHandler(
|
||||
{ path, write, missing, targetContents, templateContents }: FileDiff,
|
||||
prompt: PromptFunc,
|
||||
) {
|
||||
console.log(`Checking ${path}`);
|
||||
const coloredPath = chalk.cyan(path);
|
||||
|
||||
const { targetPath, templateContents } = file;
|
||||
const coloredPath = chalk.cyan(targetPath);
|
||||
|
||||
if (!file.targetExists) {
|
||||
if (missing) {
|
||||
if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) {
|
||||
await writeTargetFile(targetPath, templateContents);
|
||||
await write(templateContents);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (file.targetContents === templateContents) {
|
||||
if (targetContents === templateContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diffs = diffLines(file.targetContents, templateContents);
|
||||
const diffs = diffLines(targetContents, templateContents);
|
||||
for (const diff of diffs) {
|
||||
if (diff.added) {
|
||||
process.stdout.write(chalk.green(`+${diff.value}`));
|
||||
@@ -184,27 +202,29 @@ async function exactMatchHandler(file: TemplateFile, prompt: PromptFunc) {
|
||||
`Outdated ${coloredPath}, do you want to apply the above patch?`,
|
||||
)
|
||||
) {
|
||||
await writeTargetFile(targetPath, templateContents);
|
||||
await write(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`);
|
||||
async function existsHandler(
|
||||
{ path, write, missing, templateContents }: FileDiff,
|
||||
prompt: PromptFunc,
|
||||
) {
|
||||
console.log(`Making sure ${path} exists`);
|
||||
|
||||
const { targetPath, templateContents } = file;
|
||||
const coloredPath = chalk.cyan(targetPath);
|
||||
const coloredPath = chalk.cyan(path);
|
||||
|
||||
if (!file.targetExists) {
|
||||
if (missing) {
|
||||
if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) {
|
||||
await writeTargetFile(targetPath, templateContents);
|
||||
await write(templateContents);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function skipHandler(file: TemplateFile) {
|
||||
console.log(`Skipping ${file.targetPath}`);
|
||||
async function skipHandler({ path }: FileDiff) {
|
||||
console.log(`Skipping ${path}`);
|
||||
}
|
||||
|
||||
export const handlers = {
|
||||
@@ -212,26 +232,25 @@ export const handlers = {
|
||||
exists: existsHandler,
|
||||
exactMatch: exactMatchHandler,
|
||||
packageJson: PackageJsonHandler.handler,
|
||||
appPackageJson: PackageJsonHandler.appHandler,
|
||||
};
|
||||
|
||||
export async function handleAllFiles(
|
||||
fileHandlers: FileHandler[],
|
||||
files: TemplateFile[],
|
||||
files: FileDiff[],
|
||||
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),
|
||||
const { path } = file;
|
||||
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 ${targetPath}`);
|
||||
throw new Error(`No template file handler found for ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 * from './handlers';
|
||||
export * from './prompts';
|
||||
export * from './read';
|
||||
export * from './types';
|
||||
+4
-42
@@ -16,32 +16,9 @@
|
||||
|
||||
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';
|
||||
|
||||
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) => {
|
||||
export const inquirerPromptFunc: PromptFunc = async msg => {
|
||||
const { result } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'result',
|
||||
@@ -50,10 +27,10 @@ const inquirerPromptFunc: PromptFunc = async (msg) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const makeCheck = () => {
|
||||
export const makeCheckPromptFunc = () => {
|
||||
let failed = false;
|
||||
|
||||
const promptFunc: PromptFunc = async (msg) => {
|
||||
const promptFunc: PromptFunc = async msg => {
|
||||
failed = true;
|
||||
console.log(chalk.red(`[Check Failed] ${msg}`));
|
||||
return false;
|
||||
@@ -70,22 +47,7 @@ const makeCheck = () => {
|
||||
return [promptFunc, finalize] as const;
|
||||
};
|
||||
|
||||
const yesPromptFunc: PromptFunc = async (msg) => {
|
||||
export const yesPromptFunc: PromptFunc = async msg => {
|
||||
console.log(`Accepting: "${msg}"`);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
let promptFunc = inquirerPromptFunc;
|
||||
let finalize = () => {};
|
||||
|
||||
if (cmd.check) {
|
||||
[promptFunc, finalize] = makeCheck();
|
||||
} else if (cmd.yes) {
|
||||
promptFunc = yesPromptFunc;
|
||||
}
|
||||
|
||||
const templateFiles = await readTemplateFiles('default-plugin');
|
||||
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
|
||||
await finalize();
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 {
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return handlebars.compile(contents)(templateVars);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
+11
-22
@@ -14,35 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type PluginInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
export type WriteFileFunc = (contents: string) => Promise<void>;
|
||||
|
||||
export type TemplateFile = {
|
||||
export type FileDiff = {
|
||||
// Relative path within the target directory
|
||||
targetPath: string;
|
||||
path: string;
|
||||
// Wether 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;
|
||||
} & (
|
||||
| {
|
||||
// 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;
|
||||
}
|
||||
);
|
||||
// Write new contents to the target file
|
||||
write: WriteFileFunc;
|
||||
};
|
||||
|
||||
export type PromptFunc = (msg: string) => Promise<boolean>;
|
||||
|
||||
export type HandlerFunc = (
|
||||
file: TemplateFile,
|
||||
prompt: PromptFunc,
|
||||
) => Promise<void>;
|
||||
export type HandlerFunc = (file: FileDiff, prompt: PromptFunc) => Promise<void>;
|
||||
|
||||
export type FileHandler = {
|
||||
patterns: Array<string | RegExp>;
|
||||
@@ -27,7 +27,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
"@spotify/prettier-config": "^7.0.0",
|
||||
"lerna": "^3.20.2",
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"extends": "@spotify/web-scripts/config/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"incremental": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('@spotify/web-scripts/config/prettier.config.js');
|
||||
Reference in New Issue
Block a user