Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dherault committed Feb 17, 2016
1 parent 5a6054c commit df00dec
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 48 deletions.
111 changes: 110 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,110 @@
# serverless-offline
# 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
53 changes: 9 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 => {

Expand All @@ -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}`);
Expand All @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
}
}

0 comments on commit df00dec

Please sign in to comment.