refactor createPlugin and add tests. cli can now create a plugin from template

This commit is contained in:
Marcus Eide
2020-02-20 14:19:48 +01:00
parent 1b76e07910
commit 5b539a3e14
8 changed files with 198 additions and 183 deletions
+1
View File
@@ -1,3 +1,4 @@
.idea/
secrets.env
.DS_Store
cjs/
+11 -1
View File
@@ -21,8 +21,18 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"commander": "^4.1.1"
"@types/inquirer": "^6.5.0",
"commander": "^4.1.1",
"dashify": "^2.0.0",
"handlebars": "^4.7.3",
"inquirer": "^7.0.4",
"replace-in-file": "^5.0.2"
},
"files": [
"templates",
"bin",
"cjs"
],
"nodemonConfig": {
"watch": "./src",
"exec": "ts-node",
@@ -0,0 +1,50 @@
import fs from 'fs';
import path from 'path';
import { createPluginFolder, createFileFromTemplate } from './createPlugin';
describe('createPlugin', () => {
describe('createPluginFolder', () => {
it('should create a plugin directory in the correct place', () => {
const tempDir = fs.mkdtempSync('createPluginTest');
try {
const pluginFolder = createPluginFolder(tempDir, 'foo');
expect(fs.existsSync(pluginFolder)).toBe(true);
} finally {
fs.rmdirSync(tempDir, { recursive: true });
}
});
it('should not create a plugin directory if it already exists', () => {
const tempDir = fs.mkdtempSync('createPluginTest');
try {
const pluginFolder = createPluginFolder(tempDir, 'foo');
expect(fs.existsSync(pluginFolder)).toBe(true);
expect(() => createPluginFolder(tempDir, 'foo')).toThrowError(
/A plugin with the same name already exists/,
);
} finally {
fs.rmdirSync(tempDir, { recursive: true });
}
});
});
describe('createFileFromTemplate', () => {
it('should generate a valid output with inserted values', () => {
const tempDir = fs.mkdtempSync('createFileFromTemplate');
try {
const sourceData = '{"name": "@spotify-backstage/{{id}}"}';
const targetData = '{"name": "@spotify-backstage/foo"}';
const sourcePath = path.join(tempDir, 'in.hbs');
const targetPath = path.join(tempDir, 'out.json');
fs.writeFileSync(sourcePath, sourceData);
createFileFromTemplate(sourcePath, targetPath, { id: 'foo' });
expect(fs.existsSync(targetPath)).toBe(true);
expect(fs.readFileSync(targetPath).toString()).toBe(targetData);
} finally {
fs.rmdirSync(tempDir, { recursive: true });
}
});
});
});
@@ -1,187 +1,83 @@
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const replace = require('replace-in-file');
const dashify = require('dashify');
const inquirer = require('inquirer');
const handlebars = require('handlebars');
import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
import inquirer from 'inquirer';
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const lowercaseFirstLetter = str => str.charAt(0).toLowerCase() + str.slice(1);
export const createPluginFolder = (rootDir: string, id: string): string => {
const destination = path.join(rootDir, 'packages', 'plugins', id);
const appendTextFile = (file, textToAppend) => {
const originalContents = fs.readFileSync(file, 'utf8');
// Make sure that there is a newline before and after the new text
if (originalContents && !originalContents.endsWith('\n')) {
textToAppend = `\n${textToAppend}`;
}
if (textToAppend && !textToAppend.endsWith('\n')) {
textToAppend = `${textToAppend}\n`;
if (fs.existsSync(destination)) {
throw new Error(
`A plugin with the same name already exists: ${destination}`,
);
}
fs.appendFileSync(file, textToAppend);
try {
fs.mkdirSync(destination, { recursive: true });
return destination;
} catch (e) {
throw new Error(
`Failed to create plugin directory: ${destination}: ${e.message}`,
);
}
};
const questions = [
{
type: 'input',
name: 'id',
message:
"Enter an ID for the plugin (Please capitalize) e.g. 'MyAwesomePlugin' [required]",
validate: value => (value ? true : 'Please enter an ID for the plugin'),
},
{
type: 'confirm',
name: 'ts',
message: 'Use Typescript?',
},
{
type: 'input',
name: 'owner',
message: 'Which squad will own this plugin? [required]',
validate: value =>
value ? true : 'Please enter a squad that will own the plugin',
},
{
type: 'input',
name: 'title',
message: "Enter a title for the plugin e.g. 'My Awesome Plugin' [optional]",
default: '',
},
{
type: 'input',
name: 'desc',
message:
"Enter a description for the plugin e.g. 'This Plugin does all awesome things!' [optional]",
default: '',
},
{
type: 'input',
name: 'author',
message: "Author's slack handle e.g. alund [optional]",
default: '',
},
{
type: 'input',
name: 'support_channel',
message:
'Slack channel to contact for support on this plugin e.g. data-support [optional]',
default: '',
},
];
const createPluginCommand = () => {
inquirer.prompt(questions).then(answers => {
const pluginId = capitalize(answers.id);
const pluginFolderName = lowercaseFirstLetter(
pluginId.replace(/Plugin/g, ''),
);
answers.folder = pluginFolderName;
const pluginRoute = `/${dashify(pluginFolderName)}`;
answers.route = pluginRoute;
const pluginRoot = path.join(__dirname, '../src/plugins/');
const source = path.join(pluginRoot, 'scaffold');
const destination = path.join(pluginRoot, pluginFolderName);
const pluginImportText = `import { ${pluginId} } from 'plugins/${pluginFolderName}';\n// @@import`;
const pluginDefinitionText = `${pluginId},\n // @@definition`;
const pluginManagerBootstrap = path.join(
pluginRoot,
'pluginManagerBootstrap.js',
);
if (fs.existsSync(destination)) {
return console.error(
`Uh Oh! Looks like another plugin exists with the same name.\nPlease check ${destination} directory.`,
);
}
try {
fs.mkdirSync(destination);
const compileFileAndWrite = (sourceFile, destFile) => {
const hbFile = fs.readFileSync(path.join(source, sourceFile));
const template = handlebars.compile(hbFile.toString());
const contents = template({ name: destFile, ...answers });
fs.writeFile(path.join(destination, destFile), contents, err => {
if (err) {
return console.log(
'Error writing file ',
path.join(destination, destFile),
);
}
console.log('Wrote ', path.join(destination, destFile));
});
};
['Page', 'Plugin'].forEach(fileType => {
compileFileAndWrite(
`Scaffold${fileType}.hbs`,
`${pluginId}${fileType}${answers.ts ? '.tsx' : '.js'}`,
);
});
compileFileAndWrite('ScaffoldPage.test.hbs', `${pluginId}Page.test.js`);
compileFileAndWrite('plugin-info.hbs', 'plugin-info.yaml');
compileFileAndWrite('index.hbs', 'index.js');
appendTextFile(
'.github/CODEOWNERS',
`/src/plugins/${pluginFolderName}/ @backstage/${answers.owner}`,
);
// import and add plugin to bootstrap
const options = {
files: pluginManagerBootstrap,
from: [/\/\/ @@import/g, /\/\/ @@definition/],
to: [pluginImportText, pluginDefinitionText],
};
replace(options, err => {
if (err) {
return console.error(err);
}
console.log('');
console.log(
'Added plugin to bootstrap, and the plugin directory to CODEOWNERS.',
);
console.log(
`You are all set! Check http://localhost:5678${pluginRoute}`,
);
console.log('');
console.log(
'NOTE: First thing to do now should be to create a Pull Request and wait for the',
);
console.log(
'current goalie of the Tools squad to review and merge it. After that is done,',
);
console.log(
'through the use of the CODEOWNERS feature, you should be able to continue',
);
console.log('development inside your plugin directory on your own.');
console.log('');
console.log('Hack away!');
});
} catch (e) {
console.error(e);
fsExtra.removeSync(destination);
const options = {
files: pluginManagerBootstrap,
from: [pluginImportText, pluginImportText],
to: [/\/\/ @@import/g, /\/\/ @@definition/],
};
replace(options, err => {
if (err) {
return console.error(err);
}
console.log('Cleaned up');
});
}
export const createFileFromTemplate = (
sourcePath: string,
destinationPath: string,
answers: { [key: string]: string },
) => {
const template = fs.readFileSync(sourcePath);
const compiled = handlebars.compile(template.toString());
const contents = compiled({
name: path.basename(destinationPath),
...answers,
});
try {
fs.writeFileSync(destinationPath, contents);
} catch (e) {
throw new Error(`Failed to create file: ${destinationPath}: ${e.message}`);
}
};
export default createPluginCommand;
const createPlugin = async (): Promise<any> => {
const currentDir = process.argv[1];
const questions = [
{
type: 'input',
name: 'id',
message: 'Enter an ID for the plugin [required]',
validate: (value: any) =>
value ? true : 'Please enter an ID for the plugin',
},
];
const answers: { [key: string]: string } = await inquirer.prompt(questions);
const destinationFolder = createPluginFolder(
path.join(currentDir, '..', '..', '..'),
answers.id,
);
const templateFolder = path.join(
currentDir,
'..',
'..',
'@spotify-backstage',
'cli',
'templates',
'default-plugin',
);
const files = [{ input: 'package.json.hbs', output: 'package.json' }];
files.forEach(file => {
createFileFromTemplate(
path.join(templateFolder, file.input),
path.join(destinationFolder, file.output),
answers,
);
});
return destinationFolder;
};
export default createPlugin;
+1 -1
View File
@@ -19,4 +19,4 @@ const main = (argv: string[]) => {
};
// main(process.argv);
main([process.argv[0], process.argv[1], 'create-plugin', '--bla']);
main([process.argv[0], process.argv[1], 'create-plugin']);
@@ -0,0 +1,28 @@
{
"name": "@spotify-backstage/{{id}}",
"version": "{{version}}",
"main": "src/index.ts",
"main:src": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"scripts": {
"build": "web-scripts build",
"lint": "web-scripts lint",
"test": "web-scripts test",
"start": "nodemon ."
},
"devDependencies": {
"@spotify/web-scripts": "^6.0.0",
"@types/node": "^13.7.2",
"nodemon": "^2.0.2",
"ts-node": "^8.6.2"
},
"bin": {
"backstage-{{id}}": "bin/backstage-{{id}}"
},
"nodemonConfig": {
"watch": "./src",
"exec": "ts-node",
"ext": "ts"
}
}
+1
View File
@@ -2,6 +2,7 @@
"extends": "@spotify/web-scripts/config/tsconfig.json",
"include": ["src"],
"compilerOptions": {
"baseUrl": "src",
"paths": {
"*": ["src/*"]
}
+32 -3
View File
@@ -2860,6 +2860,14 @@
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
"@types/inquirer@^6.5.0":
version "6.5.0"
resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==
dependencies:
"@types/through" "*"
rxjs "^6.4.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.1"
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -3009,6 +3017,13 @@
"@types/react-dom" "*"
"@types/testing-library__dom" "*"
"@types/through@*":
version "0.0.30"
resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895"
integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*":
version "15.0.0"
resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@@ -5646,6 +5661,11 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
dashify@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz#fff270ca2868ca427fee571de35691d6e437a648"
integrity sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==
data-urls@^1.0.0, data-urls@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
@@ -7676,7 +7696,7 @@ handle-thing@^2.0.0:
resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
handlebars@^4.4.0:
handlebars@^4.4.0, handlebars@^4.7.3:
version "4.7.3"
resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee"
integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==
@@ -8288,7 +8308,7 @@ inquirer@6.5.0:
strip-ansi "^5.1.0"
through "^2.3.6"
inquirer@7.0.4, inquirer@^7.0.0:
inquirer@7.0.4, inquirer@^7.0.0, inquirer@^7.0.4:
version "7.0.4"
resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
integrity sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==
@@ -14054,6 +14074,15 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
replace-in-file@^5.0.2:
version "5.0.2"
resolved "https://registry.npmjs.org/replace-in-file/-/replace-in-file-5.0.2.tgz#bd26203b66dfb5b8112ae36a2d2cf928ea4cfe12"
integrity sha512-1Vc7Sbr/rTuHgU1PZuBb7tGsFx3D4NKdhV4BpEF2MuN/6+SoXcFtx+dZ1Zz+5Dq4k5x9js87Y+gXQYPTQ9ppkA==
dependencies:
chalk "^3.0.0"
glob "^7.1.6"
yargs "^15.0.2"
request-promise-core@1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
@@ -16978,7 +17007,7 @@ yargs@^14.2.2:
y18n "^4.0.0"
yargs-parser "^15.0.0"
yargs@^15.0.0, yargs@^15.0.1:
yargs@^15.0.0, yargs@^15.0.1, yargs@^15.0.2:
version "15.1.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219"
integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==