From df00decfbc0b5b5150426dc5f8aa88601404cb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lib=C3=A9?= Date: Wed, 17 Feb 2016 13:38:22 +0100 Subject: [PATCH] v0.1.0 --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- index.js | 53 +++++------------------- package.json | 5 +-- 3 files changed, 121 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 24cb0e617..5e3a5469e 100644 --- a/README.md +++ b/README.md @@ -1 +1,110 @@ -# serverless-offline \ No newline at end of file +# Serverless Offline Plugin + +When developing with Serverless deploying functions to AWS after each change might be annoying. This plugin allows you to simulate API Gateway locally, so all function calls can be done on localhost. + +### Differences with Serverless-serve-plugin + +See 'Credits and inspiration'. + +### Requierements + +Node v4 and over. + +### Installation + +In your Serverless project: + +``` +npm install serverless-offline +``` + +Then in `s-project.json` add following entry to the plugins array: `serverless-offline` + +Like this: +``` + "plugins": ["serverless-offline"] +``` + +And in main project root do: + +``` +sls offline start +``` + +### Command line options + +`--prefix` `-p`: Add prefix to the URLs, so your clients will not use `http://localhost:3000/` but `http://localhost:3000/prefix/` instead. Default: empty + +`--port` `-P`: Port to listen on. Default: `3000` + + +### Usage + +Just send your requests to `http://localhost:3000/` as it would be API Gateway. + +Using this plugin with [Nodemon](https://github.com/remy/nodemon) is advised to reload your local code after every change. + +### Usage with Babel + +Optionaly, your handlers can be required with `babel-register`. +To do so, in your `s-project.json` file, set options to be passed to babel-register like this: +``` +{ + /* ... */ + "custom": { + "serverless-offline": { + "babelOptions": { + /* Your own options, example: */ + presets: ["es2015", "stage-2"] + } + } + }, + "plugins": ["serverless-offline", /* ... */] +} +``` +To view the full list of babel-register options, click [here](https://babeljs.io/docs/usage/require/) + +### Simulation quality + +This plugin simulates API Gateway for many practical purposes, good enough for development - but is not a perfect simulator. Specifically, Lambda currently runs on Node v0.10.13, whereas *offline* runs on your own runtime where no timeout or memory limits are enforced. Mapping templates are not simulated, so are security checks. You will probably find other differences. + +### Credits and inspiration + +This plugin is a fork of [Nopik](https://github.com/Nopik/)'s [Serverless-serve](https://github.com/Nopik/serverless-serve), the differences are: + +- Under the hood, *Serve* uses Express, *Offline* uses Hapi. +- *Serve*'s `event` object (passed to your handlers) is undocumented ad often empty. *Offline*'s `event` object is defined by: `Object.assign({ isServerlessOffline: true }, request);` where `request` is [Hapi's request object](http://hapijs.com/api#request-object). This allows you to quickly access properties like the request's params or payload in your lambda handler: +```javascript +module.exports.handler = function(event, context) { + + var params; + + + + if (event.isServerlessOffline) { // Offline + + params = event.params; + + } else { // Online + + /* Define your event object using a template in your s-function.json file */ + + params = event.customKeyDefinedInTemplate; + + } + +}; +``` +- *Serve* will pick the first non `default` response of an endpoint if `errorPattern` is undefined. Doing so, it neglects the `default` answer (so it does not work out of the box with `serverless project create`). This causes new projects to answer 400 using *Serve*. +Example : +```javascript +"responses": { + "400": { + "statusCode": "400" // errorPattern is undefined: Serve will always answer 400 + }, + "default": { + "statusCode": "200", + "responseParameters": {}, + "responseModels": {}, + "responseTemplates": { + "application/json": "" + } + } +} +``` +- *Offline* dropped support for *Serve*'s optional init script for now. + +### Licence + +MIT diff --git a/index.js b/index.js index 8c4b24e06..a8253dcf3 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ module.exports = function(ServerlessPlugin, serverlessPath) { const path = require('path'); const context = require(path.join(serverlessPath, 'utils', 'context')); const SCli = require(path.join(serverlessPath, 'utils', 'cli')); + const SUtils = require(path.join(serverlessPath, 'utils')); const Hapi = require('hapi'); return class Offline extends ServerlessPlugin { @@ -49,49 +50,18 @@ module.exports = function(ServerlessPlugin, serverlessPath) { port: this.port }); - let prefix = this.evt.prefix || ''; - // if( !this.evt.prefix ){ - // this.evt.prefix = ""; - // } + let prefix = this.evt.prefix || '/'; - if (prefix && prefix.endsWith('/')) prefix = prefix.slice(0, -1); + if (!prefix.startsWith('/')) prefix = '/' + prefix; + if (!prefix.endsWith('/')) prefix += '/'; this.prefix = prefix; - // if( (this.evt.prefix.length > 0) && (this.evt.prefix[this.evt.prefix.length-1] != '/') ) { - // this.evt.prefix = this.evt.prefix + "/"; - // } - - // this.app.get( '/__quit', function(req, res, next){ - // SCli.log('Quit request received, quitting.'); - // res.send({ok: true}); - // _this.server.close(); - // }); - - // this.app.use( function(req, res, next) { - // res.header('Access-Control-Allow-Origin', '*'); - // next(); - // }); - - // this.app.use(bodyParser.json({ limit: '5mb' })); - - // this.app.use( function(req, res, next){ - // res.header( 'Access-Control-Allow-Methods', 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS' ); - // res.header( 'Access-Control-Allow-Headers', 'Authorization,Content-Type,x-amz-date,x-amz-security-token' ); - - // if( req.method != 'OPTIONS' ) { - // next(); - // } else { - // res.status(200).end(); - // } - // }); - return Promise.resolve(); } _registerLambdas() { const functions = this.S.state.getFunctions(); - const handlers = {}; functions.forEach(fun => { @@ -100,23 +70,18 @@ module.exports = function(ServerlessPlugin, serverlessPath) { const handlerParts = fun.handler.split('/').pop().split('.'); const handlerPath = path.join(fun._config.fullPath, handlerParts[0] + '.js'); - handlers[fun.handler] = { - path: handlerPath, - handler: handlerParts[1], - definition: fun - }; fun.endpoints.forEach(endpoint => { // const { method, path } = endpoint; const method = endpoint.method; - const path = endpoint.path; - const finalPath = this.prefix + (path.startsWith('/') ? path : '/' + path); + const epath = endpoint.path; + const path = this.prefix + (epath.startsWith('/') ? epath.slice(1) : epath); - if(process.env.DEBUG) SCli.log(`Route: ${method} ${finalPath}`); + if(process.env.DEBUG) SCli.log(`Route: ${method} ${path}`); this.server.route({ method, - path: finalPath, + path, config: { cors: true }, handler: (request, reply) => { SCli.log(`Serving: ${method} ${request.url.path}`); @@ -131,7 +96,7 @@ module.exports = function(ServerlessPlugin, serverlessPath) { throw err ; } - const event = {}; + const event = Object.assign({ isServerlessOffline: true }, request); handler(event, context(fun.name, (err, result) => { let finalResponse; diff --git a/package.json b/package.json index ffeee611e..d8cd6f4d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "0.0.0", + "version": "0.1.0", "description": "Simulates your API Gateway ressources locally to call your lambda functions offline", "main": "index.js", "scripts": { @@ -23,7 +23,6 @@ "homepage": "https://github.com/dherault/serverless-offline#readme", "dependencies": { "babel-register": "^6.5.2", - "hapi": "^13.0.0", - "velocity": "^0.7.2" + "hapi": "^13.0.0" } }