├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | dist 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | #IDE Stuff 32 | **/.idea 33 | 34 | #OS STUFF 35 | .DS_Store 36 | .tmp 37 | 38 | #SERVERLESS STUFF 39 | admin.env 40 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serverless Optimizer Plugin 2 | ============================= 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | 5 | Browserifies, minifies your Serverless Node.js Functions on deployment, and more! 6 | 7 | Reducing the file size of your AWS Lambda Functions allows AWS to provision them more quickly, speeding up the response time of your Lambdas. Smaller Lambda sizes also helps you develop faster because you can upload them faster. This Severless Plugin is absolutely recommended for every project including Lambdas with Node.js. 8 | 9 | **Note:** Requires Serverless *v0.5.0* or higher. 10 | 11 | ###Setup 12 | 13 | * Install via npm in the root of your Serverless Project: 14 | ``` 15 | npm install serverless-optimizer-plugin --save 16 | ``` 17 | 18 | * Add the plugin to the `plugins` array in your Serverless Project's `s-project.json`, like this: 19 | 20 | ``` 21 | plugins: [ 22 | "serverless-optimizer-plugin" 23 | ] 24 | ``` 25 | 26 | * In the `custom` property of your `s-function.json` add an optimize property. 27 | 28 | ``` 29 | "custom": { 30 | "optimize": true 31 | } 32 | ``` 33 | 34 | * If you rely on the **aws-sdk**, be sure to read the **Common Pitfalls** section. 35 | 36 | * All done! 37 | 38 | ### Configuration Options 39 | 40 | Configuration options can be used by setting the `optimize` property to an object instead of a boolean value. The following options are available: 41 | 42 | * **disable** - When set to `true`, this will disable optimizer. This is effectively the same as setting the `optimize` property to `false`, but it does not require the deletion of any other configuration values within the `optimize` object. This is a good option for temporarily disabling while debugging. 43 | 44 | ``` 45 | "custom": { 46 | "optimize": { 47 | "disable": true 48 | } 49 | } 50 | ``` 51 | 52 | * **excludeStage** - When set to a `string` or `[string]`, optimizer will be disabled for the specified stage(s). This is beneficial if you do not want optimizer to run on a specific stage to aid in debugging. 53 | 54 | ``` 55 | "custom": { 56 | "optimize": { 57 | "excludeStage": ["dev", "test"] 58 | } 59 | } 60 | ``` 61 | 62 | * **excludeRegion** - When set to a `string` or `[string]`, optimizer will be disabled for the specified region(s). 63 | 64 | ``` 65 | "custom": { 66 | "optimize": { 67 | "excludeRegion": ["us-east-1"] 68 | } 69 | } 70 | ``` 71 | 72 | ### Browserify Options 73 | 74 | Browserify options can be included as normal configuration options to the `optimize` object. The following options are supported: 75 | 76 | * handlerExt 77 | * includePaths 78 | * requires 79 | * plugins 80 | * transforms 81 | * exclude 82 | * ignore 83 | * extensions 84 | 85 | For more information on these options, please visit the [Browserify Documentaton](https://github.com/substack/node-browserify#usage). 86 | 87 | ### Common Pitfalls 88 | 89 | * **aws-sdk** does not play well with Browserify. If the aws-sdk is used anywhere in your code, even if it is not within node_modules or package.json, you may receive an error similar to: 90 | 91 | `Uncaught {"errorMessage":"Cannot find module '/usr/lib/node_modules/aws-sdk/apis/metadata.json'"...` 92 | 93 | To fix this, the aws-sdk should be excluded by using the `exclude` Browserify option. Since the aws-sdk is always available to an AWS Lambda, it should never need to be included. 94 | 95 | ``` 96 | "custom": { 97 | "optimize": { 98 | "exclude": ["aws-sdk"] 99 | } 100 | } 101 | ``` 102 | 103 | ## ES6 with Babel and Babelify 104 | 105 | Bundles are packaged with Browserify, and can be transformed to support ES6 features with Babelify. 106 | 107 | 108 | Install babelify within the root context of your project: 109 | 110 | npm install babelify --save 111 | 112 | npm install babel-preset-es2015 --save 113 | 114 | 115 | Add the babelify transform to `s-function.json`: 116 | 117 | ```javascript 118 | { 119 | "name": "myfunc", 120 | "runtime": "nodejs", 121 | "custom": { 122 | "optimize": { 123 | "exclude": [ "aws-sdk" ], 124 | "transforms": [ 125 | { 126 | "name": "babelify", 127 | "opts": { 128 | "presets": [ 129 | "es2015" 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | } 136 | } 137 | 138 | ``` 139 | 140 | We're currently working on adding support for Typescript. Check back for updates! 141 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Serverless Optimizer Plugin 5 | */ 6 | 7 | module.exports = function(S) { 8 | 9 | const path = require('path'), 10 | _ = require('lodash'), 11 | browserify = require('browserify'), 12 | UglifyJS = require('uglify-js'), 13 | BbPromise = require('bluebird'), 14 | fs = BbPromise.promisifyAll(require("fs-extra")); 15 | 16 | /** 17 | * ServerlessOptimizer 18 | */ 19 | 20 | class ServerlessOptimizer extends S.classes.Plugin { 21 | 22 | /** 23 | * Constructor 24 | */ 25 | 26 | constructor() { 27 | super(); 28 | } 29 | 30 | /** 31 | * Define your plugins name 32 | */ 33 | 34 | static getName() { 35 | return 'com.serverless.' + ServerlessOptimizer.name; 36 | } 37 | 38 | /** 39 | * Register Hooks 40 | */ 41 | 42 | registerHooks() { 43 | 44 | S.addHook(this._optimize.bind(this), { 45 | action: 'codeDeployLambda', 46 | event: 'pre' 47 | }); 48 | 49 | return BbPromise.resolve(); 50 | } 51 | 52 | /** 53 | * Optimize 54 | */ 55 | 56 | _optimize(evt) { 57 | 58 | // Validate: Check Serverless version 59 | // TODO: Use a full x.x.x version string. Consider using semver: https://github.com/npm/node-semver 60 | if (parseInt(S._version.split('.')[1]) < 5) { 61 | console.log("WARNING: This version of the Serverless Optimizer Plugin will not work with a version of Serverless that is less than v0.5"); 62 | } 63 | 64 | // Get function 65 | let func = S.getProject().getFunction(evt.options.name), 66 | optimizer; 67 | 68 | // Skip if no optimization is set on function 69 | if (!func.custom || !func.custom.optimize) { 70 | return BbPromise.resolve(evt); 71 | } 72 | 73 | // Skip if disable is true in function 74 | if (func.custom && func.custom.optimize && func.custom.optimize.disable) { 75 | return BbPromise.resolve(evt); 76 | } 77 | 78 | // If component/function has an excludeStage value matching the current, skip 79 | let excludeStage = []; 80 | 81 | // Cycle through function and combine values 82 | 83 | // If excludeStage was set 84 | if (_.has(func, "custom.optimize.excludeStage")) { 85 | // If excludeStage is a string or array, combine with exclude array. Function will overwrite component settings. 86 | if (_.isString(func.custom.optimize.excludeStage) || _.isArray(func.custom.optimize.excludeStage)) { 87 | excludeStage = _.concat([], func.custom.optimize.excludeStage); 88 | } 89 | // If excludeStage is bool and false, clear the array. 90 | if (_.isBoolean(func.custom.optimize.excludeStage) && func.custom.optimize.excludeStage === false) { 91 | excludeStage = []; 92 | } 93 | } 94 | 95 | // If current stage was excluded, skip 96 | if (_.includes(excludeStage, evt.options.stage)) { 97 | return BbPromise.resolve(evt); 98 | } 99 | 100 | // If function has an excludeRegion value matching the current, skip 101 | let excludeRegion = []; 102 | 103 | // If excludeRegion was set 104 | if (_.has(func, "custom.optimize.excludeRegion")) { 105 | // If excludeRegion is a string or array, combine with exclude array. Function will overwrite component settings. 106 | if (_.isString(func.custom.optimize.excludeRegion) || _.isArray(func.custom.optimize.excludeRegion)) { 107 | excludeRegion = _.concat([], func.custom.optimize.excludeRegion); 108 | } 109 | // If excludeRegion is bool and false, clear the array. 110 | if (_.isBoolean(func.custom.optimize.excludeRegion) && func.custom.optimize.excludeRegion === false) { 111 | excludeRegion = []; 112 | } 113 | } 114 | 115 | // If current region was excluded, skip 116 | if (_.includes(excludeRegion, evt.options.region)) { 117 | return BbPromise.resolve(evt); 118 | } 119 | 120 | // Optimize: Nodejs 121 | let runtimeName = func.getRuntime().getName(); 122 | if (-1 !== runtimeName.indexOf('nodejs')) { 123 | S.utils.sDebug(`Beginning optimization. Runtime: ${runtimeName}`); 124 | optimizer = new OptimizeNodejs(S, evt, func); 125 | return optimizer.optimize() 126 | .then(function(evt) { 127 | return evt; 128 | }); 129 | } 130 | 131 | // Otherwise, skip plugin 132 | return BbPromise.resolve(evt); 133 | } 134 | } 135 | 136 | /** 137 | * Optimize Nodejs 138 | * - Separate class allows this Hook to be run concurrently safely. 139 | */ 140 | 141 | class OptimizeNodejs { 142 | 143 | constructor(S, evt, func) { 144 | this.evt = evt; 145 | this.function = func; 146 | } 147 | 148 | optimize() { 149 | 150 | let _this = this; 151 | 152 | _this.config = { 153 | handlerExt: 'js', 154 | includePaths: [], 155 | requires: [], 156 | plugins: [], 157 | transforms: [], 158 | exclude: [], 159 | ignore: [], 160 | extensions: [] 161 | }; 162 | _this.config = _.merge( 163 | _this.config, 164 | _this.function.custom.optimize ? _this.function.custom.optimize === true ? {} : _this.function.custom.optimize : {} 165 | ); 166 | 167 | return _this.browserify() 168 | .then(() => { 169 | return _this.evt; 170 | }); 171 | } 172 | 173 | /** 174 | * Browserify 175 | * - Options: transform, exclude, minify, ignore 176 | */ 177 | 178 | browserify() { 179 | 180 | let _this = this, 181 | uglyOptions = { 182 | mangle: true, //@see http://lisperator.net/uglifyjs/compress 183 | compress: {} 184 | }; 185 | 186 | const handlerName = this.function.getHandler(), 187 | bundleBaseDir = fs.realpathSync(_this.evt.options.pathDist), 188 | bundleEntryPt = handlerName.split('.')[0] + '.' + _this.config.handlerExt; 189 | 190 | let b = browserify({ 191 | basedir: bundleBaseDir, 192 | entries: [bundleEntryPt], 193 | standalone: 'lambda', 194 | extensions: _this.config.extensions, 195 | browserField: false, // Setup for node app (copy logic of --node in bin/args.js) 196 | builtins: false, 197 | commondir: false, 198 | ignoreMissing: true, // Do not fail on missing optional dependencies 199 | detectGlobals: true, // Default for bare in cli is true, but we don't care if its slower 200 | insertGlobalVars: { // Handle process https://github.com/substack/node-browserify/issues/1277 201 | //__filename: insertGlobals.lets.__filename, 202 | //__dirname: insertGlobals.lets.__dirname, 203 | process: function() { 204 | } 205 | } 206 | }); 207 | 208 | // browserify.require 209 | _this.config.requires.map(req => { 210 | if (typeof(req) === typeof('')) req = {name: req}; 211 | b.require(req.name, req.opts); 212 | }); 213 | 214 | // browserify.plugin 215 | _this.config.plugins.map(plug => { 216 | if (typeof(plug) === typeof('')) plug = {name: plug}; 217 | b.plugin(require(plug.name), plug.opts); 218 | }); 219 | 220 | // browserify.transform 221 | _this.config.transforms.map(transform => { 222 | if (typeof(transform) === typeof('')) transform = {name: transform}; 223 | b.transform(require(transform.name), transform.opts); 224 | }); 225 | 226 | // browserify.exclude 227 | _this.config.exclude.forEach(file => b.exclude(file)); 228 | 229 | // browserify.ignore 230 | _this.config.ignore.forEach(file => b.ignore(file)); 231 | 232 | // Perform Bundle 233 | return new BbPromise((resolve, reject) => { 234 | 235 | S.utils.sDebug(`Bundling starting at ${bundleBaseDir}/${bundleEntryPt}`); 236 | 237 | b.bundle((err, bundledBuf) => { 238 | 239 | // Reset pathDist 240 | _this.optimizedDistPath = path.join(_this.evt.options.pathDist, 'optimized'); 241 | S.utils.sDebug('optimizedDistPath:', _this.optimizedDistPath); 242 | 243 | // Set path of optimized file 244 | let optimizedFile = path.join(_this.optimizedDistPath, _this.function.getHandler().split('.')[0] + '.js'); 245 | 246 | if (err) { 247 | console.error('Error running browserify bundle'); 248 | reject(err); 249 | } else { 250 | 251 | S.utils.sDebug(`Writing bundled file`); 252 | S.utils.writeFileSync(optimizedFile, bundledBuf); 253 | 254 | if (_this.config.minify !== false) { 255 | 256 | S.utils.sDebug(`Minifying bundled file ${optimizedFile}`); 257 | 258 | let result; 259 | 260 | try { 261 | result = UglifyJS.minify(optimizedFile, uglyOptions); 262 | } catch (e) { 263 | console.error(`Error uglifying ${optimizedFile}`); 264 | console.error(e); 265 | return reject(e); 266 | } 267 | 268 | if (!result || !result.code) { 269 | return reject(new SError(`Problem uglifying code ${optimizedFile}`)); 270 | } 271 | 272 | S.utils.sDebug(`Writing minified file`); 273 | S.utils.writeFileSync(optimizedFile, result.code); 274 | } 275 | 276 | resolve(optimizedFile); 277 | } 278 | }); 279 | }) 280 | .then(optimizedFile => { 281 | 282 | let includePaths = _this.function.custom.optimize.includePaths || [], 283 | deferredCopies = []; 284 | 285 | includePaths.forEach(p => { 286 | let destPath = path.join(_this.optimizedDistPath, p), 287 | srcPath = path.join(_this.evt.options.pathDist, p), 288 | destDir = (fs.lstatSync(p).isDirectory()) ? destPath : path.dirname(destPath); 289 | 290 | fs.mkdirsSync(destDir, '0777'); 291 | deferredCopies.push( 292 | fs.copyAsync(srcPath, destPath, {clobber: true, dereference: true}) 293 | ); 294 | }); 295 | 296 | _this.evt.options.pathDist = _this.optimizedDistPath; 297 | 298 | return BbPromise.all(deferredCopies); 299 | }); 300 | } 301 | } 302 | 303 | return ServerlessOptimizer; 304 | }; 305 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-optimizer-plugin", 3 | "version": "2.5.1", 4 | "engines": { 5 | "node": ">=4.0" 6 | }, 7 | "description": "Serverless Optimizer Plugin - Significantly reduces Lambda file size and improves performance", 8 | "author": "serverless.com", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/serverless/serverless-optimizer-plugin" 13 | }, 14 | "keywords": [ 15 | "serverless optimizer plugin", 16 | "serverless optimizer", 17 | "serverless framework plugin", 18 | "serverless applications", 19 | "serverless plugins", 20 | "lambda browserify", 21 | "lambda minify", 22 | "api gateway", 23 | "lambda", 24 | "aws", 25 | "aws lambda", 26 | "amazon", 27 | "amazon web services", 28 | "serverless.com" 29 | ], 30 | "main": "index.js", 31 | "bin": {}, 32 | "scripts": { 33 | "test": "mocha tests/all" 34 | }, 35 | "devDependencies": { 36 | "chai": "^3.4.1", 37 | "mocha": "^2.3.4" 38 | }, 39 | "dependencies": { 40 | "bluebird": "^3.1.1", 41 | "browserify": "^13.0.0", 42 | "lodash": "^4.0.0", 43 | "uglify-js": "^2.6.1", 44 | "fs-extra": "~0.26.7" 45 | } 46 | } 47 | --------------------------------------------------------------------------------