├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src └── index.js └── test ├── index.js ├── test-prj-2 ├── .jshintrc └── curlyFunction │ └── handler.js └── test-prj ├── invalidNodeFunction └── handler.js ├── otherValidNodeFunction └── handler.js ├── pythonFunction └── handler.py └── validNodeFunction └── handler.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | npm-debug.log 4 | /node_modules 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.2 4 | - 5.3 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.2 4 | 5 | * Support all Node runtimes 6 | 7 | ## 0.1.1 8 | 9 | * Support full and partial linting 10 | 11 | ## 0.1.0 12 | 13 | * Made compatible with Serverless v0.5 14 | * Accept multiple names as arguments 15 | 16 | ## 0.0.2 17 | 18 | * Support for custom configuration file (.jshintrc) 19 | 20 | ## 0.0.1 21 | 22 | * Initial release 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Joost Farla 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless JSHint Plugin 2 | 3 | A Serverless Plugin for the [Serverless Framework](http://www.serverless.com) which 4 | adds support for [JSHint](http://jshint.com/) linting. 5 | 6 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 7 | [![npm version](https://badge.fury.io/js/serverless-jshint-plugin.svg)](https://badge.fury.io/js/serverless-jshint-plugin) 8 | [![Build Status](https://travis-ci.org/joostfarla/serverless-jshint-plugin.svg?branch=develop)](https://travis-ci.org/joostfarla/serverless-jshint-plugin) 9 | [![Dependencies Status](https://david-dm.org/joostfarla/serverless-jshint-plugin.svg)](https://david-dm.org/joostfarla/serverless-jshint-plugin) 10 | [![DevDependencies Status](https://david-dm.org/joostfarla/serverless-jshint-plugin/dev-status.svg)](https://david-dm.org/joostfarla/serverless-jshint-plugin#info=devDependencies) 11 | 12 | **THIS PLUGIN REQUIRES SERVERLESS V0.5 OR HIGHER!** 13 | 14 | ## Introduction 15 | 16 | This plugins adds capabilities to lint your Lambda functions before deploying. It also 17 | saves you from deploying ES6 syntax by accident. 18 | 19 | ## Installation 20 | 21 | In your project root, run: 22 | 23 | ```bash 24 | npm install --save serverless-jshint-plugin 25 | ``` 26 | 27 | Add the plugin to `s-project.json`: 28 | 29 | ```json 30 | "plugins": [ 31 | "serverless-jshint-plugin" 32 | ] 33 | ``` 34 | 35 | ## Usage 36 | 37 | Run the *jshint* action to check one or multiple functions for errors: 38 | 39 | ``` 40 | serverless function jshint someFunction someOtherFunction 41 | ``` 42 | 43 | When no function names are provided, it will check all functions in the current 44 | working directory. You can also check the full project by passing the `--all` / 45 | `-a` flag. 46 | 47 | To apply custom configuration, add a `.jshintrc` file in the project root. 48 | 49 | ## Roadmap 50 | 51 | * Improve documentation 52 | * Add hooks to automate linting upon run or deployment 53 | 54 | ## License 55 | 56 | ISC License. See the [LICENSE](LICENSE) file. 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-jshint-plugin", 3 | "version": "0.1.2", 4 | "description": "Serverless JSHint Plugin - Detect errors and potential problems in your Lambda functions", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/joostfarla/serverless-jshint-plugin.git" 12 | }, 13 | "keywords": [ 14 | "serverless", 15 | "aws", 16 | "lambda", 17 | "jshint", 18 | "lint" 19 | ], 20 | "author": "Joost Farla ", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/joostfarla/serverless-jshint-plugin/issues" 24 | }, 25 | "homepage": "https://github.com/joostfarla/serverless-jshint-plugin#readme", 26 | "dependencies": { 27 | "bluebird": "^3.3.4", 28 | "chalk": "^1.1.3", 29 | "jshint": "^2.9.1", 30 | "lodash": "^4.7.0" 31 | }, 32 | "devDependencies": { 33 | "chai": "^3.5.0", 34 | "chai-as-promised": "^5.3.0", 35 | "mocha": "^2.4.5", 36 | "serverless": "^0.5.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Serverless CORS Plugin 5 | */ 6 | module.exports = function(S) { 7 | const _ = require('lodash'), 8 | path = require('path'), 9 | Promise = require('bluebird'), 10 | SCli = require(S.getServerlessPath('utils/cli')), 11 | SError = require(S.getServerlessPath('Error')), 12 | chalk = require('chalk'), 13 | jshint = require('jshint').JSHINT, 14 | readFile = Promise.promisify(require('fs').readFile), 15 | util = require('util'); 16 | 17 | class ServerlessJSHint extends S.classes.Plugin { 18 | constructor(config) { 19 | super(); 20 | if (!config) config = {}; 21 | this.log = config.logger || SCli.log; 22 | } 23 | 24 | static getName() { 25 | return 'com.joostfarla.' + ServerlessJSHint.name; 26 | } 27 | 28 | registerActions() { 29 | S.addAction(this.functionJSHint.bind(this), { 30 | handler: 'functionJSHint', 31 | description: 'Detects errors and potential problems in your Lambda function', 32 | context: 'function', 33 | contextAction: 'jshint', 34 | options: [ 35 | { 36 | option: 'all', 37 | shortcut: 'a', 38 | description: 'Optional - Deploy all Functions' 39 | } 40 | ], 41 | parameters: [ 42 | { 43 | parameter: 'names', 44 | description: 'One or multiple function names', 45 | position: '0->' 46 | } 47 | ] 48 | }); 49 | 50 | return Promise.resolve(); 51 | } 52 | 53 | functionJSHint(evt) { 54 | return this._validateAndPrepare(evt.options.names,evt.options) 55 | .then(func => { 56 | return this._lint(func) 57 | .then(() => { 58 | this.log(chalk.bold.green('Success! - No linting errors found.')); 59 | }) 60 | .catch(err => { 61 | this.log(chalk.bold.red('Error! - Linting errors found.')); 62 | jshint.errors.forEach(err => { 63 | this.log(chalk.red(util.format('Line %d: %s', err.line, err.reason))); 64 | }); 65 | }); 66 | }); 67 | } 68 | 69 | _getFuncs(names, options) { 70 | // user passed the --all option 71 | if (options.all) { 72 | return S.getProject().getAllFunctions().filter(function(func) { 73 | return func.runtime.substring(0, 6) === 'nodejs'; 74 | }); 75 | } 76 | 77 | // no names or options so use cwd behavior 78 | // will return all functions if none in cwd 79 | if (S.cli && names.length === 0) { 80 | return S.utils.getFunctionsByCwd(S.getProject().getAllFunctions()).filter(function(func) { 81 | return func.runtime.substring(0, 6) === 'nodejs'; 82 | }); 83 | } 84 | 85 | // return by passed name(s) 86 | return _.map(names, name => { 87 | const func = S.getProject().getFunction(name); 88 | if (!func) throw new SError(`Function ${name} does not exist in your project`); 89 | if (func.runtime.substring(0, 6) !== 'nodejs') throw new SError(`JSHint doesn't support runtimes other than "nodejs".`); 90 | return func; 91 | }); 92 | } 93 | 94 | _validateAndPrepare(names, options) { 95 | try { 96 | const funcs = this._getFuncs(names, options); 97 | if (funcs.length == 0) throw new SError("No nodejs functions found by jshint"); 98 | return Promise.resolve(funcs); 99 | } catch (err) { 100 | return Promise.reject(err); 101 | } 102 | } 103 | 104 | _lint(functions) { 105 | return Promise.each(functions, func => { 106 | const file = func.getRootPath(func.handler.split('/').pop().split('.')[0] + '.js'); 107 | 108 | return this._getConfig() 109 | .then(config => { 110 | return readFile(file, 'utf-8') 111 | .then(data => { 112 | jshint(data, _.merge({ 113 | node: true 114 | }, config)); 115 | 116 | if (jshint.errors.length > 0) { 117 | return Promise.reject(jshint.errors); 118 | } 119 | 120 | return Promise.resolve(); 121 | }); 122 | }); 123 | }); 124 | } 125 | 126 | _getConfig() { 127 | return readFile(S.getProject().getRootPath('.jshintrc'), 'utf-8') 128 | .then(config => { 129 | return JSON.parse(config); 130 | }) 131 | .catch(err => { 132 | return {}; 133 | }); 134 | } 135 | } 136 | 137 | return ServerlessJSHint; 138 | }; 139 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | chai = require('chai'), 5 | should = chai.should(), 6 | chaiAsPromised = require('chai-as-promised'), 7 | Serverless = require('serverless'); 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | let s, plugin, logs, JSHintPlugin; 12 | 13 | const logger = function(log) { 14 | logs.push(log); 15 | }; 16 | 17 | describe('ServerlessJSHint', function() { 18 | beforeEach(function(done) { 19 | this.timeout(0); 20 | 21 | s = new Serverless(); 22 | logs = []; 23 | 24 | s.init().then(function() { 25 | JSHintPlugin = require('..')(s); 26 | plugin = new JSHintPlugin({ logger: logger }); 27 | 28 | s.addPlugin(plugin); 29 | s.config.projectPath = path.join(__dirname, 'test-prj'); 30 | s.setProject(new s.classes.Project({ 31 | stages: { 32 | dev: { regions: { 'eu-west-1': {} }} 33 | }, 34 | variables: { 35 | project: 'serverless-project', 36 | stage: 'dev', 37 | region: 'eu-west-1' 38 | } 39 | })); 40 | 41 | done(); 42 | }); 43 | }); 44 | 45 | describe('#getName()', function() { 46 | it('should return the correct name', function() { 47 | JSHintPlugin.getName().should.equal('com.joostfarla.ServerlessJSHint'); 48 | }); 49 | }); 50 | 51 | describe('#registerActions()', function() { 52 | it('should register actions', function() { 53 | s.actions.should.have.property('functionJSHint'); 54 | s.actions.functionJSHint.should.be.a('function'); 55 | }); 56 | }); 57 | 58 | // different function lookup methods 59 | describe('#_validateAndPrepare', function() { 60 | beforeEach(function() { 61 | s.cli = true; 62 | _bootstrapFunction('validNodeFunction', 'nodejs'); 63 | _bootstrapFunction('otherValidNodeFunction', 'nodejs'); 64 | _bootstrapFunction('validPythonFunction', 'python2.7'); 65 | 66 | var me = this; 67 | 68 | process.cwd = function() { 69 | return me.tmpCwd || s.config.projectPath; 70 | }; 71 | }); 72 | 73 | afterEach(function() { 74 | this.tmpCwd = null; 75 | }); 76 | 77 | it('should do all functions with --all flag', function() { 78 | return plugin._validateAndPrepare([],{all: true}).should.be.fulfilled.then(function(funcs) { 79 | funcs.should.lengthOf(2); 80 | }); 81 | }); 82 | 83 | it('should do function in working dir', function() { 84 | this.tmpCwd = `${s.config.projectPath}/otherValidNodeFunction`; 85 | return plugin._validateAndPrepare([],{}).should.be.fulfilled.then(function(funcs) { 86 | funcs.should.lengthOf(1); 87 | funcs[0].getName().should.equal('otherValidNodeFunction'); 88 | }); 89 | }); 90 | 91 | it('should do all functions if no function in working dir', function() { 92 | return plugin._validateAndPrepare([],{}).should.be.fulfilled.then(function(funcs) { 93 | funcs.should.lengthOf(2); 94 | }); 95 | }); 96 | 97 | it('should do function with provided name', function() { 98 | return plugin._validateAndPrepare(['validNodeFunction'],{}).should.be.fulfilled.then(function(funcs) { 99 | funcs.should.lengthOf(1); 100 | funcs[0].getName().should.equal('validNodeFunction'); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('#functionJSHint()', function() { 106 | it('should fail for non-existing functions', function() { 107 | return plugin.functionJSHint({ options: { names: ['someFunction'] }}).should.be.rejected.then(function() { 108 | logs.should.be.empty; 109 | }); 110 | }); 111 | 112 | it('should error when no functions to lint', function() { 113 | return plugin.functionJSHint({ options: { names: [] }}).should.be.rejected; 114 | }); 115 | 116 | it('should succeed for all functions', function() { 117 | _bootstrapFunction('validNodeFunction', 'nodejs'); 118 | 119 | return plugin.functionJSHint({ options: { names: [], all: true }}).should.be.fulfilled.then(function() { 120 | logs[0].should.contain('Success!'); 121 | }); 122 | }); 123 | 124 | it('should report errors for invalid functions', function() { 125 | _bootstrapFunction('invalidNodeFunction', 'nodejs'); 126 | 127 | return plugin.functionJSHint({ options: { names: ['invalidNodeFunction'] }}).should.be.fulfilled.then(function() { 128 | logs[0].should.contain('Error!'); 129 | }); 130 | }); 131 | 132 | it('should apply a custom configuration file', function() { 133 | s.config.projectPath = path.join(__dirname, 'test-prj-2'); 134 | _bootstrapFunction('curlyFunction', 'nodejs'); 135 | 136 | return plugin.functionJSHint({ options: { names: ['curlyFunction'] }}).should.be.fulfilled.then(function() { 137 | logs[0].should.contain('Error!'); 138 | }); 139 | }); 140 | 141 | it('should fail on non-Node components', function() { 142 | _bootstrapFunction('pythonFunction', 'python2.7'); 143 | 144 | return plugin.functionJSHint({ options: { names: ['pythonFunction'] }}).should.be.rejected; 145 | }); 146 | }); 147 | }); 148 | 149 | function _bootstrapFunction(name, runtime) { 150 | const func = new s.classes.Function({ 151 | name: name, 152 | runtime: runtime 153 | }, path.join(s.config.projectPath, name, 's-function.json')); 154 | 155 | s.getProject().setFunction(func); 156 | 157 | return func; 158 | } 159 | -------------------------------------------------------------------------------- /test/test-prj-2/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true 3 | } 4 | -------------------------------------------------------------------------------- /test/test-prj-2/curlyFunction/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = function(event, context) { 4 | var response = {}; 5 | if (response) response.foo = 'bar'; // Not so curly here! 6 | 7 | context.done(null, response); 8 | }; 9 | -------------------------------------------------------------------------------- /test/test-prj/invalidNodeFunction/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = function(event, context) { 4 | const response = {}; // Invalid ES5 syntax 5 | context.done(null, response); 6 | }; 7 | -------------------------------------------------------------------------------- /test/test-prj/otherValidNodeFunction/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = function(event, context) { 4 | var response = {}; 5 | if (response) response.foo = 'bar'; 6 | 7 | context.done(null, response); 8 | }; 9 | -------------------------------------------------------------------------------- /test/test-prj/pythonFunction/handler.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joostfarla/serverless-jshint-plugin/62d03857916307e3eb5c60c660705e4da2f891b4/test/test-prj/pythonFunction/handler.py -------------------------------------------------------------------------------- /test/test-prj/validNodeFunction/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = function(event, context) { 4 | var response = {}; 5 | if (response) response.foo = 'bar'; 6 | 7 | context.done(null, response); 8 | }; 9 | --------------------------------------------------------------------------------