Skip to content

Commit af32f53

Browse files
authored
Merge pull request serverless-heaven#2 from elastic-coders/add-serve-command
Add `serve` command
2 parents a0d2a52 + e78bba7 commit af32f53

File tree

6 files changed

+200
-47
lines changed

6 files changed

+200
-47
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Note that, if the `output` configuration is not set, it will automatically be
2929
generated to write bundles in the `.webpack` directory.
3030

3131
## Usage
32+
3233
### Automatic bundling
3334

3435
The normal Serverless deploy procedure will automatically bundle with Webpack:
@@ -37,6 +38,20 @@ The normal Serverless deploy procedure will automatically bundle with Webpack:
3738
- Install Serverless Webpack as above
3839
- Deploy with `serverless deploy`
3940

41+
### Simulate API Gateway locally
42+
43+
To start a local server that will act like the API Gateway use the following command.
44+
Your code will be reloaded upon change so that every request to your local server
45+
will serve the latest code.
46+
47+
```
48+
serverless webpack serve
49+
```
50+
51+
Options are:
52+
53+
- `--port` or `-p` (optional) The local server port. Defaults to `8000`
54+
4055
### Run a function locally
4156
4257
To run your bundled functions locally you can:

index.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const validate = require('./lib/validate');
66
const compile = require('./lib/compile');
77
const cleanup = require('./lib/cleanup');
88
const run = require('./lib/run');
9+
const serve = require('./lib/serve');
910

1011
class ServerlessWebpack {
1112
constructor(serverless, options) {
@@ -17,7 +18,8 @@ class ServerlessWebpack {
1718
validate,
1819
compile,
1920
cleanup,
20-
run
21+
run,
22+
serve
2123
);
2224

2325
this.commands = {
@@ -68,6 +70,18 @@ class ServerlessWebpack {
6870
},
6971
},
7072
},
73+
serve: {
74+
usage: 'Simulate the API Gateway and serves lambdas locally',
75+
lifecycleEvents: [
76+
'serve',
77+
],
78+
options: {
79+
port: {
80+
usage: 'The local server port',
81+
shortcut: 'p',
82+
},
83+
},
84+
},
7185
},
7286
},
7387
};
@@ -95,6 +109,10 @@ class ServerlessWebpack {
95109
'webpack:watch:watch': () => BbPromise.bind(this)
96110
.then(this.validate)
97111
.then(this.watch),
112+
113+
'webpack:serve:serve': () => BbPromise.bind(this)
114+
.then(this.validate)
115+
.then(this.serve),
98116
};
99117
}
100118
}

lib/run.js

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33
const BbPromise = require('bluebird');
44
const path = require('path');
55
const webpack = require('webpack');
6+
const utils = require('./utils');
67

78
module.exports = {
8-
loadFunction(stats) {
9-
const functionName = this.options.function;
9+
loadHandler(stats) {
1010
const handlerFilePath = path.join(
1111
stats.compilation.options.output.path,
1212
stats.compilation.options.output.filename
1313
);
14-
purgeCache(handlerFilePath);
15-
const handler = require(handlerFilePath);
16-
17-
return handler[functionName];
14+
utils.purgeCache(handlerFilePath);
15+
return require(handlerFilePath);
1816
},
1917

2018
getEvent() {
@@ -23,11 +21,10 @@ module.exports = {
2321
: null; // TODO use get-stdin instead
2422
},
2523

26-
getContext() {
27-
const functionName = this.options.function;
24+
getContext(functionName) {
2825
return {
29-
awsRequestId: guid(),
30-
invokeid: guid(),
26+
awsRequestId: utils.guid(),
27+
invokeid: utils.guid(),
3128
logGroupName: `/aws/lambda/${functionName}`,
3229
logStreamName: '2016/02/14/[HEAD]13370a84ca4ed8b77c427af260',
3330
functionVersion: '$LATEST',
@@ -42,9 +39,9 @@ module.exports = {
4239

4340
this.serverless.cli.log(`Run function ${functionName}...`);
4441

45-
const handler = this.loadFunction(stats);
42+
const handler = this.loadHandler(stats)[functionName];
4643
const event = this.getEvent();
47-
const context = this.getContext();
44+
const context = this.getContext(functionName);
4845

4946
return new BbPromise((resolve, reject) => handler(
5047
event,
@@ -70,9 +67,9 @@ module.exports = {
7067
throw err;
7168
}
7269
this.serverless.cli.log(`Run function ${functionName}...`);
73-
const handler = this.loadFunction(stats);
70+
const handler = this.loadHandler(stats)[functionName];
7471
const event = this.getEvent();
75-
const context = this.getContext();
72+
const context = this.getContext(functionName);
7673
handler(
7774
event,
7875
context,
@@ -89,35 +86,3 @@ module.exports = {
8986
return BbPromise.resolve();
9087
},
9188
};
92-
93-
function guid() {
94-
function s4() {
95-
return Math.floor((1 + Math.random()) * 0x10000)
96-
.toString(16)
97-
.substring(1);
98-
}
99-
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
100-
}
101-
102-
function purgeCache(moduleName) {
103-
searchCache(moduleName, function (mod) {
104-
delete require.cache[mod.id];
105-
});
106-
Object.keys(module.constructor._pathCache).forEach(function(cacheKey) {
107-
if (cacheKey.indexOf(moduleName)>0) {
108-
delete module.constructor._pathCache[cacheKey];
109-
}
110-
});
111-
}
112-
113-
function searchCache(moduleName, callback) {
114-
var mod = require.resolve(moduleName);
115-
if (mod && ((mod = require.cache[mod]) !== undefined)) {
116-
(function traverse(mod) {
117-
mod.children.forEach(function (child) {
118-
traverse(child);
119-
});
120-
callback(mod);
121-
}(mod));
122-
}
123-
}

lib/serve.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const BbPromise = require('bluebird');
4+
const webpack = require('webpack');
5+
const express = require('express');
6+
const bodyParser = require('body-parser');
7+
8+
module.exports = {
9+
serve() {
10+
this.serverless.cli.log('Serving functions...');
11+
12+
const compiler = webpack(this.webpackConfig);
13+
const app = this._newExpressApp();
14+
const port = this._getPort();
15+
16+
app.listen(port, () => {
17+
compiler.watch({}, (err, stats) => {
18+
if (err) {
19+
throw err;
20+
}
21+
this.handler = this.loadHandler(stats);
22+
});
23+
});
24+
25+
return BbPromise.resolve();
26+
},
27+
28+
_newExpressApp() {
29+
const app = express();
30+
const funcConfs = this._getFuncConfigs();
31+
32+
app.use(bodyParser.json({ limit: '5mb' }));
33+
34+
app.use((req, res, next) => {
35+
if(req.method !== 'OPTIONS') {
36+
next();
37+
} else {
38+
res.status(200).end();
39+
}
40+
});
41+
42+
for (let funcConf of funcConfs) {
43+
for (let httpEvent of funcConf.events) {
44+
const method = httpEvent.method.toLowerCase();
45+
const endpoint = `/${this.options.stage}/${httpEvent.path}`;
46+
const path = endpoint.replace(/\{(.+?)\}/g, ':$1');
47+
let handler = this._handerBase(funcConf.id);
48+
if (httpEvent.cors) {
49+
handler = this._handlerAddCors(handler);
50+
}
51+
app[method](
52+
path,
53+
handler
54+
);
55+
console.log(` ${method.toUpperCase()} - http://localhost:${this._getPort()}${endpoint}`);
56+
}
57+
}
58+
59+
return app;
60+
},
61+
62+
_getFuncConfigs() {
63+
const funcConfs = [];
64+
const inputfuncConfs = this.serverless.service.functions;
65+
for (let funcName in inputfuncConfs) {
66+
const funcConf = inputfuncConfs[funcName];
67+
const httpEvents = funcConf.events
68+
.filter(e => e.hasOwnProperty('http'))
69+
.map(e => e.http);
70+
if (httpEvents.length > 0) {
71+
funcConfs.push(Object.assign({}, funcConf, {
72+
id: funcName,
73+
events: httpEvents,
74+
}));
75+
}
76+
}
77+
return funcConfs;
78+
},
79+
80+
_getPort() {
81+
return this.options.port || 8000;
82+
},
83+
84+
_handlerAddCors(handler) {
85+
return (req, res, next) => {
86+
res.header('Access-Control-Allow-Origin', '*');
87+
res.header( 'Access-Control-Allow-Methods', 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS');
88+
res.header( 'Access-Control-Allow-Headers', 'Authorization,Content-Type,x-amz-date,x-amz-security-token');
89+
handler(req, res, next);
90+
};
91+
},
92+
93+
_handerBase(funcId) {
94+
return (req, res, next) => {
95+
const func = this.handler[funcId];
96+
const event = {
97+
method: req.method,
98+
headers: req.headers,
99+
body: req.body,
100+
path: req.params,
101+
query: req.query,
102+
// principalId,
103+
// stageVariables,
104+
};
105+
const context = this.getContext(funcId);
106+
func(event, context, (err, resp) => {
107+
if (err) {
108+
console.error(err);
109+
res.sendStatus(500);
110+
} else {
111+
res.status(200).send(resp);
112+
}
113+
});
114+
}
115+
},
116+
};

lib/utils.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
function guid() {
2+
function s4() {
3+
return Math.floor((1 + Math.random()) * 0x10000)
4+
.toString(16)
5+
.substring(1);
6+
}
7+
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
8+
}
9+
10+
function purgeCache(moduleName) {
11+
searchCache(moduleName, function (mod) {
12+
delete require.cache[mod.id];
13+
});
14+
Object.keys(module.constructor._pathCache).forEach(function(cacheKey) {
15+
if (cacheKey.indexOf(moduleName)>0) {
16+
delete module.constructor._pathCache[cacheKey];
17+
}
18+
});
19+
}
20+
21+
function searchCache(moduleName, callback) {
22+
var mod = require.resolve(moduleName);
23+
if (mod && ((mod = require.cache[mod]) !== undefined)) {
24+
(function traverse(mod) {
25+
mod.children.forEach(function (child) {
26+
traverse(child);
27+
});
28+
callback(mod);
29+
}(mod));
30+
}
31+
}
32+
33+
module.exports = {
34+
guid,
35+
purgeCache,
36+
searchCache,
37+
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"homepage": "https://github.com/elastic-coders/serverless-webpack#readme",
2424
"dependencies": {
2525
"bluebird": "^3.4.0",
26+
"body-parser": "^1.15.2",
27+
"express": "^4.14.0",
2628
"fs-extra": "^0.26.7",
2729
"webpack": "^1.13.1"
2830
}

0 commit comments

Comments
 (0)