UNPKG

mdk

Version:

Command line tool for generating Mattermost integration and plugin templates

372 lines (301 loc) 11.7 kB
const co = require('co'); const prompt = require('co-prompt'); const program = require('commander'); const inquirer = require('inquirer'); const fs = require('fs'); const os = require('os'); const denodeify = require('denodeify'); const ncp = denodeify(require('ncp').ncp) const replace = require('replace'); const chalk = require('chalk'); const WEBAPP_COMPONENTS = { ProfilePopover: 'profile_popover', Root: 'root' }; const INTEGRATION_TYPES = [ 'rest_api', 'incoming_webhook (coming soon)', 'outgoing_webhook (coming soon)', 'slash_command (coming soon)' ]; const AUTH_METHODS = [ 'personal_access_token', 'email_password (coming soon)', 'oauth2 (coming soon)' ]; ncp.limit = 16; program .arguments('<type>') .option('-n, --name <name>', 'Name of the plugin/integration') .option('-d, --description <description>', '(Plugins) Brief description of the plugin') .option('-c, --components <components>', '(Plugins) Comma separated list of components to override') .option('-p, --post-types <post-types>', '(Plugins) Comma separated list of post types') .option('-t, --type <type>', '(Integrations) Type of integration to generate') .option('-a, --auth-method <auth-method>', '(REST API) Authentication method for the integration') .option('-S, --skip-prompts', 'Skip optional user input prompts') .option('-q, --quiet', 'Suppress non-error messages') .action((type) => { let generator; switch (type) { case 'plugin': generator = plugin; break; case 'integration': generator = integration; break; default: log(`Unsupported type: ${type}`); return; } co(generator); }) .parse(process.argv); // --------------------------------------------------- // Plugin Functions // --------------------------------------------------- function* plugin() { log(chalk.underline.bold.cyan('Plugin Generation')); log(); const manifest = {}; if (typeof program.name === 'string') { log(chalk.bold('Plugin name: ') + program.name); manifest.name = program.name; } else { manifest.name = yield prompt(chalk.bold('Plugin name: ')); } manifest.id = manifest.name.toLowerCase().trim().replace(/\s+/g, '-'); if (typeof program.description === 'string') { log(chalk.bold('Description: ') + program.description); manifest.description = program.description; } else if (program.skipPrompts) { manifest.description = ''; } else { manifest.description = yield prompt(chalk.bold('Description: ')); } const optionsForWebapp = yield* webappOptions(); const homePath = `${(process.env.MDK_PLUGIN_PATH || process.cwd())}`; // Create home directory if not exists if (!fs.existsSync(homePath)) { fs.mkdirSync(homePath); } const pluginPath = `${homePath}/${manifest.id}`; if (fs.existsSync(pluginPath)) { console.error(chalk.red(`A directory already exists at ${pluginPath}. Please remove it or pick a new plugin name.`)); process.exit(1); } fs.mkdirSync(pluginPath); ncp(__dirname + '/templates/plugins', pluginPath).then(() => { replaceTemplatePlaceholders(manifest, pluginPath); webappComplete(manifest, optionsForWebapp, pluginPath).then(() => { log(); log(chalk.bold.magenta('Plugin generated at: ') + pluginPath); process.exit(0); }); }); } function* webappOptions() { log(); log(chalk.bold.cyan('Webapp')); log(); let components = []; if (typeof program.components === 'string') { components = cleanComponentInput(program.components.replace(/\s+/g, '').split(',')); log(chalk.bold('Components: ') + components); } else if (!program.skipPrompts) { const results = yield inquirer.prompt([ { type: 'checkbox', name: 'components', message: chalk.bold('Override Components: '), choices: Object.keys(WEBAPP_COMPONENTS) } ]); components = results.components; } let postTypes = []; if (typeof program.postTypes === 'string') { log(chalk.bold('Post Types: ') + program.postTypes); postTypes = program.postTypes.replace(/\s+/g, '').split(','); } else if (!program.skipPrompts) { const input = (yield prompt(chalk.bold('Post Types (comma separated, leave blank to skip): '))).replace(/\s+/g, ''); if (input) { postTypes = input.split(','); } } return {components, postTypes}; } async function webappComplete(manifest, options, path) { const webappPath = `${path}/webapp`; fs.mkdirSync(webappPath); // Build import statements for index.js const components = options.components; let componentImports = ''; components.forEach((name) => { componentImports += `import ${name} from './components/${WEBAPP_COMPONENTS[name]}';\n`; }); const postTypes = options.postTypes; let postTypeImports = ''; let postTypeComponents = []; postTypes.forEach((type) => { postTypeImports += `import PostType${toTitleCase(type)} from './components/post_type_${type}';\n`; postTypeComponents.push(`custom_${type}: PostType${toTitleCase(type)}`); }); let imports = ''; if (componentImports || postTypeImports) { imports = '\n' + componentImports + postTypeImports; } const replacements = [ {regex: '%plugin_components%', replacement: components.join(', ')}, {regex: '%plugin_post_types%', replacement: postTypeComponents.join(', ')}, {regex: '%plugin_imports%', replacement: imports} ]; // Copy all webapp template files await ncp(__dirname + '/templates/webapp', webappPath); // Remove unused component templates for (let name of Object.keys(WEBAPP_COMPONENTS).diff(components)) { const id = WEBAPP_COMPONENTS[name]; fs.unlinkSync(`${webappPath}/components/${id}/index.js`); fs.unlinkSync(`${webappPath}/components/${id}/${id}.jsx`); fs.rmdirSync(`${webappPath}/components/${id}`); } // Create post type components for (let type of postTypes) { const typePath = `${webappPath}/components/post_type_${type}`; fs.mkdirSync(typePath); await ncp(__dirname + '/templates/webapp/components/post_type_template/index.js', typePath + '/index.js'); await ncp(__dirname + '/templates/webapp/components/post_type_template/post_type_template.jsx', `${typePath}/post_type_${type}.jsx`); const replacements = [ {regex: '%type%', replacement: type}, {regex: '%Type%', replacement: toTitleCase(type)} ]; replaceTemplatePlaceholders(manifest, typePath, replacements); } fs.unlinkSync(`${webappPath}/components/post_type_template/index.js`); fs.unlinkSync(`${webappPath}/components/post_type_template/post_type_template.jsx`); fs.rmdirSync(`${webappPath}/components/post_type_template`); replaceTemplatePlaceholders(manifest, webappPath, replacements); } // --------------------------------------------------- // Integration Functions // --------------------------------------------------- function* integration() { log(chalk.underline.bold.cyan('Integration Generation')); log(); const manifest = {}; if (typeof program.name === 'string') { log(chalk.bold('Integration name: ') + program.name); manifest.name = program.name; } else { manifest.name = yield prompt(chalk.bold('Integration name: ')); } manifest.id = manifest.name.toLowerCase().trim().replace(/\s+/g, '-'); let type = ''; if (typeof program.type === 'string') { log(chalk.bold('Integration Type: ') + program.type); type = program.type; } else if (!program.skipPrompts) { const results = yield inquirer.prompt([ { type: 'list', name: 'type', message: chalk.bold('Integration Type: '), choices: INTEGRATION_TYPES } ]); type = results.type; } if (type !== 'rest_api') { console.error(chalk.red(`Unsupported type: ${type}`)); process.exit(1); } const typeOptions = yield* restApiOptions(); if (typeOptions.authMethod !== 'personal_access_token') { console.error(chalk.red(`Unsupported authentication method: ${typeOptions.authMethod}`)); process.exit(1); } const homePath = `${(process.env.MDK_PLUGIN_PATH || process.cwd())}`; // Create home directory if not exists if (!fs.existsSync(homePath)) { fs.mkdirSync(homePath); } const integrationPath = `${homePath}/${manifest.id}`; if (fs.existsSync(integrationPath)) { console.error(chalk.red(`A directory already exists at ${integrationPath}. Please remove it or pick a new integration name.`)); process.exit(1); } fs.mkdirSync(integrationPath); restApiComplete(manifest, typeOptions, integrationPath).then(() => { log(); log(chalk.bold.magenta('Plugin generated at: ') + integrationPath); process.exit(0); }); } function* restApiOptions() { log(); log(chalk.bold.cyan('REST API')); log(); let authMethod = ''; if (typeof program.authMethod === 'string') { log(chalk.bold('Authentication Method: ') + program.authMethod); authMethod = program.authMethod; } else if (!program.skipPrompts) { const results = yield inquirer.prompt([ { type: 'list', name: 'authMethod', message: chalk.bold('Authentication Method: '), choices: AUTH_METHODS } ]); authMethod = results.authMethod; } return {authMethod}; } async function restApiComplete(manifest, options, path) { // Copy all rest api template files await ncp(__dirname + '/templates/rest-api', path); replaceTemplatePlaceholders(manifest, path); } // --------------------------------------------------- // Helper Functions // --------------------------------------------------- function replaceTemplatePlaceholders(manifest, path, customReplacements = []) { const replacements = [ {regex: '%plugin_name%', replacement: manifest.name}, {regex: '%plugin_id%', replacement: manifest.id}, {regex: '%plugin_description%', replacement: manifest.description}, ...customReplacements ]; replacements.forEach((r) => { replace({ regex: r.regex, replacement: r.replacement, paths: [path], recursive: true, silent: true }); }); } function cleanComponentInput(components) { let i = components.length; const webappComponents = Object.keys(WEBAPP_COMPONENTS); while (i--) { if (!webappComponents.includes(components[i])) { log(`WARNING: Unsupported component '${components[i]}' ignored`); components.splice(i, 1); } } return components; } function toTitleCase(str) { return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); } Array.prototype.diff = function(a) { return this.filter(function(i) {return a.indexOf(i) < 0;}); }; function log(message = '') { if (!program.quiet) { console.log(message); } }