From 742444a646df6c1ae6786dc7314798539dce04f9 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Thu, 11 Feb 2016 11:15:40 +0100 Subject: [PATCH 1/2] Refs #593 - Add plugin create functionality Add a basic 'plugin create' functionality so that users can create plugins from within the command line. Templates for the plugin are included in the templates folder. --- lib/Actions.json | 3 +- lib/actions/PluginCreate.js | 164 ++++++++++++++++++++++++++ lib/templates/plugin/README.md | 1 + lib/templates/plugin/index.js | 183 ++++++++++++++++++++++++++++++ lib/templates/plugin/package.json | 38 +++++++ lib/utils/index.js | 4 + 6 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 lib/actions/PluginCreate.js create mode 100644 lib/templates/plugin/README.md create mode 100644 lib/templates/plugin/index.js create mode 100644 lib/templates/plugin/package.json diff --git a/lib/Actions.json b/lib/Actions.json index 7f08c7767..aba527bd1 100644 --- a/lib/Actions.json +++ b/lib/Actions.json @@ -32,6 +32,7 @@ "./actions/RegionRemove.js", "./actions/StageRemove.js", "./actions/ProjectRemove.js", - "./actions/FunctionLogs.js" + "./actions/FunctionLogs.js", + "./actions/PluginCreate" ] } diff --git a/lib/actions/PluginCreate.js b/lib/actions/PluginCreate.js new file mode 100644 index 000000000..48f0ba6e0 --- /dev/null +++ b/lib/actions/PluginCreate.js @@ -0,0 +1,164 @@ +'use strict'; + +/** + * Action: Plugin create + * - validates that plugin does NOT already exists + * - validates that the plugins directory is present + * - generates plugin skeleton with the plugins name + * + * Event Options: + * - pluginName: (String) The name of your plugin + */ + +module.exports = function(SPlugin, serverlessPath) { + const path = require('path'), + SError = require(path.join(serverlessPath, 'ServerlessError')), + SCli = require(path.join(serverlessPath, 'utils/cli')), + BbPromise = require('bluebird'), + SUtils = require(path.join(serverlessPath, 'utils')), + _ = require('lodash'), + execSync = require('child_process').execSync; + + let fs = require('fs'); + + BbPromise.promisifyAll(fs); + + /** + * PluginCreate Class + */ + + class PluginCreate extends SPlugin { + + constructor(S, config) { + super(S, config); + } + + static getName() { + return 'serverless.core.' + PluginCreate.name; + } + + registerActions() { + this.S.addAction(this.pluginCreate.bind(this), { + handler: 'pluginCreate', + description: `Creates scaffolding for a new plugin. +usage: serverless plugin create `, + context: 'plugin', + contextAction: 'create', + options: [], + parameters: [ + { + parameter: 'pluginName', + description: 'The name of your plugin', + position: '0' + } + ] + }); + return BbPromise.resolve(); + } + + /** + * Action + */ + + pluginCreate(evt) { + + let _this = this; + _this.evt = evt; + + return _this._prompt() + .bind(_this) + .then(_this._createPluginSkeleton) + .then(function() { + + SCli.log('Successfully created plugin scaffold with the name: "' + _this.evt.options.pluginName + '"'); + + /** + * Return Event + */ + + return _this.evt; + + }); + } + + /** + * Prompt plugin if they're missing + */ + + _prompt() { + + let _this = this, + overrides = {}; + + // If non-interactive, skip + if (!_this.S.config.interactive) return BbPromise.resolve(); + + let prompts = { + properties: { + name: { + description: 'Enter a new plugin name: '.yellow, + message: 'Plugin name must contain only letters, numbers, hyphens, or underscores.', + required: true, + conform: function(pluginName) { + return SUtils.isPluginNameValid(pluginName); + } + } + } + }; + + return _this.cliPromptInput(prompts, overrides) + .then(function(answers) { + _this.evt.options.pluginName = answers.name; + }); + }; + + /** + * Create Plugin Skeleton + */ + + _createPluginSkeleton() { + // Name of the plugin + let pluginName = this.evt.options.pluginName; + // Paths + let projectPath = this.S.config.projectPath; + let serverlessPath = this.S.config.serverlessPath; + // Directories + let pluginsDirectory = path.join(projectPath, 'plugins'); + let pluginDirectory = path.join(pluginsDirectory, pluginName); + let pluginTemplateDirectory = path.join(serverlessPath, 'templates', 'plugin'); + // Plugin files from the serverless template directory + let indexJs = fs.readFileSync(path.join(pluginTemplateDirectory, 'index.js')); + let packageJson = fs.readFileSync(path.join(pluginTemplateDirectory, 'package.json')); + let readmeMd = fs.readFileSync(path.join(pluginTemplateDirectory, 'README.md')); + + // Create the plugins directory if it's not yet present + if (!SUtils.dirExistsSync(pluginsDirectory)) { + fs.mkdirSync(pluginsDirectory); + } + + // Create the directory for the new plugin in the plugins directory + if (!SUtils.dirExistsSync(pluginDirectory)) { + fs.mkdirSync(pluginDirectory); + } else { + throw new SError('Plugin with the name ' + pluginName + ' already exists.'); + } + + // Prepare and copy all files + let modifiedPackageJson = _.template(packageJson)({ pluginName: pluginName }); + fs.writeFileSync(path.join(pluginDirectory, 'package.json'), modifiedPackageJson); + fs.writeFileSync(path.join(pluginDirectory, 'index.js'), indexJs); + fs.writeFileSync(path.join(pluginDirectory, 'README.md'), readmeMd); + + // link the new package + execSync('cd ' + pluginDirectory + ' && npm link'); + execSync('cd ' + projectPath + ' && npm link ' + pluginName); + + // Add the newly create plugin to the plugins array of the projects s-project.json file + let sProjectJson = SUtils.readAndParseJsonSync(path.join(projectPath, 's-project.json')); + sProjectJson.plugins.push(pluginName); + fs.writeFileSync(path.join(projectPath, 's-project.json'), JSON.stringify(sProjectJson, null, 2)); + }; + } + + return( PluginCreate ); +}; diff --git a/lib/templates/plugin/README.md b/lib/templates/plugin/README.md new file mode 100644 index 000000000..c04564d1a --- /dev/null +++ b/lib/templates/plugin/README.md @@ -0,0 +1 @@ +# Serverless plugin diff --git a/lib/templates/plugin/index.js b/lib/templates/plugin/index.js new file mode 100644 index 000000000..3734338a8 --- /dev/null +++ b/lib/templates/plugin/index.js @@ -0,0 +1,183 @@ +'use strict'; + +/** + * Serverless Plugin Boilerplate + * - Useful example/starter code for writing a plugin for the Serverless Framework. + * - In a plugin, you can: + * - Create a Custom Action that can be called via the CLI or programmatically via a function handler. + * - Overwrite a Core Action that is included by default in the Serverless Framework. + * - Add a hook that fires before or after a Core Action or a Custom Action + * - All of the above at the same time :) + * + * - Setup: + * - Make a Serverless Project dedicated for plugin development, or use an existing Serverless Project + * - Make a "plugins" folder in the root of your Project and copy this codebase into it. Title it your custom plugin name with the suffix "-dev", like "myplugin-dev" + * - Run "npm link" in your plugin, then run "npm link myplugin" in the root of your project. + * - Start developing! + * + * - Good luck, serverless.com :) + */ + +module.exports = function(ServerlessPlugin) { // Always pass in the ServerlessPlugin Class + + const path = require('path'), + fs = require('fs'), + BbPromise = require('bluebird'); // Serverless uses Bluebird Promises and we recommend you do to because they provide more than your average Promise :) + + /** + * ServerlessPluginBoilerplate + */ + + class ServerlessPluginBoilerplate extends ServerlessPlugin { + + /** + * Constructor + * - Keep this and don't touch it unless you know what you're doing. + */ + + constructor(S) { + super(S); + } + + /** + * Define your plugins name + * - We recommend adding prefixing your personal domain to the name so people know the plugin author + */ + + static getName() { + return 'com.serverless.' + ServerlessPluginBoilerplate.name; + } + + /** + * Register Actions + * - If you would like to register a Custom Action or overwrite a Core Serverless Action, add this function. + * - If you would like your Action to be used programatically, include a "handler" which can be called in code. + * - If you would like your Action to be used via the CLI, include a "description", "context", "action" and any options you would like to offer. + * - Your custom Action can be called programatically and via CLI, as in the example provided below + */ + + registerActions() { + + this.S.addAction(this._customAction.bind(this), { + handler: 'customAction', + description: 'A custom action from a custom plugin', + context: 'custom', + contextAction: 'run', + options: [{ // These must be specified in the CLI like this "-option true" or "-o true" + option: 'option', + shortcut: 'o', + description: 'test option 1' + }], + parameters: [ // Use paths when you multiple values need to be input (like an array). Input looks like this: "serverless custom run module1/function1 module1/function2 module1/function3. Serverless will automatically turn this into an array and attach it to evt.options within your plugin + { + parameter: 'paths', + description: 'One or multiple paths to your function', + position: '0->' // Can be: 0, 0-2, 0-> This tells Serverless which params are which. 3-> Means that number and infinite values after it. + } + ] + }); + + return BbPromise.resolve(); + } + + /** + * Register Hooks + * - If you would like to register hooks (i.e., functions) that fire before or after a core Serverless Action or your Custom Action, include this function. + * - Make sure to identify the Action you want to add a hook for and put either "pre" or "post" to describe when it should happen. + */ + + registerHooks() { + + this.S.addHook(this._hookPre.bind(this), { + action: 'functionRunLambdaNodeJs', + event: 'pre' + }); + + this.S.addHook(this._hookPost.bind(this), { + action: 'functionRunLambdaNodeJs', + event: 'post' + }); + + return BbPromise.resolve(); + } + + /** + * Custom Action Example + * - Here is an example of a Custom Action. Include this and modify it if you would like to write your own Custom Action for the Serverless Framework. + * - Be sure to ALWAYS accept and return the "evt" object, or you will break the entire flow. + * - The "evt" object contains Action-specific data. You can add custom data to it, but if you change any data it will affect subsequent Actions and Hooks. + * - You can also access other Project-specific data @ this.S Again, if you mess with data on this object, it could break everything, so make sure you know what you're doing ;) + */ + + _customAction(evt) { + + let _this = this; + + return new BbPromise(function (resolve, reject) { + + // console.log(evt) // Contains Action Specific data + // console.log(_this.S) // Contains Project Specific data + // console.log(_this.S.state) // Contains tons of useful methods for you to use in your plugin. It's the official API for plugin developers. + + console.log('-------------------'); + console.log('YOU JUST RAN YOUR CUSTOM ACTION, NICE!'); + console.log('-------------------'); + + return resolve(evt); + + }); + } + + /** + * Your Custom PRE Hook + * - Here is an example of a Custom PRE Hook. Include this and modify it if you would like to write your a hook that fires BEFORE an Action. + * - Be sure to ALWAYS accept and return the "evt" object, or you will break the entire flow. + * - The "evt" object contains Action-specific data. You can add custom data to it, but if you change any data it will affect subsequent Actions and Hooks. + * - You can also access other Project-specific data @ this.S Again, if you mess with data on this object, it could break everything, so make sure you know what you're doing ;) + */ + + _hookPre(evt) { + + let _this = this; + + return new BbPromise(function (resolve, reject) { + + console.log('-------------------'); + console.log('YOUR SERVERLESS PLUGIN\'S CUSTOM "PRE" HOOK HAS RUN BEFORE "FunctionRunLambdaNodeJs"'); + console.log('-------------------'); + + return resolve(evt); + + }); + } + + /** + * Your Custom POST Hook + * - Here is an example of a Custom POST Hook. Include this and modify it if you would like to write your a hook that fires AFTER an Action. + * - Be sure to ALWAYS accept and return the "evt" object, or you will break the entire flow. + * - The "evt" object contains Action-specific data. You can add custom data to it, but if you change any data it will affect subsequent Actions and Hooks. + * - You can also access other Project-specific data @ this.S Again, if you mess with data on this object, it could break everything, so make sure you know what you're doing ;) + */ + + _hookPost(evt) { + + let _this = this; + + return new BbPromise(function (resolve, reject) { + + console.log('-------------------'); + console.log('YOUR SERVERLESS PLUGIN\'S CUSTOM "POST" HOOK HAS RUN AFTER "FunctionRunLambdaNodeJs"'); + console.log('-------------------'); + + return resolve(evt); + + }); + } + } + + // Export Plugin Class + return ServerlessPluginBoilerplate; + +}; + +// Godspeed! diff --git a/lib/templates/plugin/package.json b/lib/templates/plugin/package.json new file mode 100644 index 000000000..8d7bf1440 --- /dev/null +++ b/lib/templates/plugin/package.json @@ -0,0 +1,38 @@ +{ + "name": "<%= pluginName %>", + "version": "0.1.0", + "engines": { + "node": ">=4.0" + }, + "description": "Serverless plugin", + "author": "serverless.com", + "license": "MIT", + "repository": { + "type": "git", + "url": "http://github.com/" + }, + "keywords": [ + "serverless framework plugin", + "serverless applications", + "serverless plugins", + "api gateway", + "lambda", + "aws", + "aws lambda", + "amazon", + "amazon web services", + "serverless.com" + ], + "main": "index.js", + "bin": {}, + "scripts": { + "test": "mocha tests/all" + }, + "devDependencies": { + "chai": "^3.2.0", + "mocha": "^2.2.5" + }, + "dependencies": { + "bluebird": "^3.0.6" + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index d58ed8d4a..0631a61d9 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -498,6 +498,10 @@ exports.isFunctionNameValid = function(functionName) { return /^[\w-]{1,20}$/.test(functionName); }; +exports.isPluginNameValid = function(pluginName) { + return /^[\w-]+$/.test(pluginName); +}; + exports.getModulePath = function(moduleName, componentName, projectRootPath) { return path.join(projectRootPath, componentName, moduleName); }; From be4bb539ba95489c544a9780ac2430a610f9d077 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Thu, 11 Feb 2016 13:48:18 +0100 Subject: [PATCH 2/2] Refs #615 - Add plugin to projects package.json file The newly created plugin is added to the package.json file of the project. This enables package.json based plugin discovery (as proposed in #580). Adding the plugin to the s-project.json files plugins array can be removed in future releases. --- lib/actions/PluginCreate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/actions/PluginCreate.js b/lib/actions/PluginCreate.js index 48f0ba6e0..d4cd50e0d 100644 --- a/lib/actions/PluginCreate.js +++ b/lib/actions/PluginCreate.js @@ -153,10 +153,16 @@ usage: serverless plugin create `, execSync('cd ' + pluginDirectory + ' && npm link'); execSync('cd ' + projectPath + ' && npm link ' + pluginName); + // TODO: Remove in V1 because will result in breaking change // Add the newly create plugin to the plugins array of the projects s-project.json file let sProjectJson = SUtils.readAndParseJsonSync(path.join(projectPath, 's-project.json')); sProjectJson.plugins.push(pluginName); fs.writeFileSync(path.join(projectPath, 's-project.json'), JSON.stringify(sProjectJson, null, 2)); + + // Add the newly created plugin to the package.json file of the project + let projectPackageJson = SUtils.readAndParseJsonSync(path.join(projectPath, 'package.json')); + projectPackageJson.dependencies[pluginName] = JSON.parse(packageJson).version; + fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(projectPackageJson, null, 2)); }; }