refactor createPlugin and add tests. cli can now create a plugin from template
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
.idea/
|
||||
secrets.env
|
||||
.DS_Store
|
||||
cjs/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "@spotify/web-scripts/config/tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"*": ["src/*"]
|
||||
}
|
||||
|
||||
+32
-3
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user