Skip to content

Commit baaf17f

Browse files
committed
Initial implementation
0 parents  commit baaf17f

File tree

13 files changed

+6788
-0
lines changed

13 files changed

+6788
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.log
2+
.DS_Store
3+
node_modules
4+
.rts2_cache_cjs
5+
.rts2_cache_esm
6+
.rts2_cache_umd
7+
.rts2_cache_system
8+
dist
9+
test/temp.png

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.tabSize": 2
3+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Sean Matheson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# zeit-node-server
2+
3+
An unofficial package allowing you to create Node Server instances of your Zeit `@now/node` lambdas.
4+
5+
Doing so allows you to write unit/integration tests for your routes.
6+
7+
## Installation
8+
9+
```bash
10+
npm install zeit-node-server
11+
```
12+
13+
### Example Jest Test
14+
15+
```javascript
16+
import createServer from 'zeit-now-node-server';
17+
import listen from 'test-listen';
18+
import axios from 'axios';
19+
import routeUnderTest from './api/hello-world';
20+
21+
it('should allow me to test my node lambdas' async () => {
22+
const server = createServer(routeUnderTest);
23+
const url = await listen(server);
24+
const response = await axios.get(url);
25+
expect(response.data).toBe('Hello world');
26+
server.close();
27+
});
28+
```

jest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
3+
const path = require('path');
4+
5+
module.exports = {
6+
preset: 'ts-jest',
7+
testEnvironment: 'node',
8+
testMatch: ['<rootDir>/test/**/*.test.ts'],
9+
};

myBinaryFile

48.2 KB
Binary file not shown.

package.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "zeit-now-node-server",
3+
"version": "0.1.0",
4+
"description": "Create a server for your Zeit @now/node lambdas in order to test them",
5+
"license": "MIT",
6+
"author": "Sean Matheson",
7+
"main": "dist/index.js",
8+
"module": "dist/zeit-now-node-server.esm.js",
9+
"typings": "dist/index.d.ts",
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/ctrlplusb/zeit-now-node-server.git"
13+
},
14+
"files": [
15+
"dist"
16+
],
17+
"scripts": {
18+
"start": "tsdx watch",
19+
"build": "tsdx build",
20+
"test": "tsdx test",
21+
"lint": "tsdx lint"
22+
},
23+
"peerDependencies": {},
24+
"husky": {
25+
"hooks": {
26+
"pre-commit": "tsdx lint"
27+
}
28+
},
29+
"prettier": {
30+
"printWidth": 80,
31+
"semi": true,
32+
"singleQuote": true,
33+
"trailingComma": "es5"
34+
},
35+
"devDependencies": {
36+
"@now/node": "^1.0.2",
37+
"@types/content-type": "^1.1.3",
38+
"@types/cookie": "^0.3.3",
39+
"@types/jest": "^24.0.21",
40+
"@types/micro": "^7.3.3",
41+
"@types/test-listen": "^1.1.0",
42+
"axios": "^0.19.0",
43+
"form-data": "^2.5.1",
44+
"husky": "^3.0.9",
45+
"jest": "^24.9.0",
46+
"test-listen": "^1.1.0",
47+
"ts-jest": "^24.1.0",
48+
"tsdx": "^0.11.0",
49+
"tslib": "^1.10.0",
50+
"typescript": "^3.7.2"
51+
},
52+
"dependencies": {
53+
"content-type": "^1.0.4",
54+
"cookie": "^0.4.0",
55+
"micro": "^9.3.4",
56+
"querystring": "^0.2.0"
57+
}
58+
}

src/index.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
NowRequestCookies,
3+
NowRequestQuery,
4+
NowRequestBody,
5+
NowRequest,
6+
NowResponse,
7+
} from '@now/node';
8+
import { IncomingMessage, ServerResponse } from 'http';
9+
import { parse } from 'cookie';
10+
import { parse as parseContentType } from 'content-type';
11+
import { parse as parseQS } from 'querystring';
12+
import { URL } from 'url';
13+
import micro, { buffer, send } from 'micro';
14+
15+
export class ApiError extends Error {
16+
readonly statusCode: number;
17+
18+
constructor(statusCode: number, message: string) {
19+
super(message);
20+
this.statusCode = statusCode;
21+
}
22+
}
23+
24+
function getBodyParser(req: IncomingMessage, body: Buffer) {
25+
return function parseBody(): NowRequestBody {
26+
if (!req.headers['content-type']) {
27+
return undefined;
28+
}
29+
30+
const { type } = parseContentType(req.headers['content-type']);
31+
32+
if (type === 'application/json') {
33+
try {
34+
return JSON.parse(body.toString());
35+
} catch (error) {
36+
throw new ApiError(400, 'Invalid JSON');
37+
}
38+
}
39+
40+
if (type === 'application/octet-stream') {
41+
return body;
42+
}
43+
44+
if (type === 'application/x-www-form-urlencoded') {
45+
// note: querystring.parse does not produce an iterable object
46+
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
47+
return parseQS(body.toString());
48+
}
49+
50+
if (type === 'text/plain') {
51+
return body.toString();
52+
}
53+
54+
return undefined;
55+
};
56+
}
57+
58+
function getQueryParser({ url = '/' }: IncomingMessage) {
59+
return function parseQuery(): NowRequestQuery {
60+
// we provide a placeholder base url because we only want searchParams
61+
const params = new URL(url, 'https://n').searchParams;
62+
63+
const query: { [key: string]: string | string[] } = {};
64+
params.forEach((value, name) => {
65+
query[name] = value;
66+
});
67+
return query;
68+
};
69+
}
70+
71+
function getCookieParser(req: IncomingMessage) {
72+
return function parseCookie(): NowRequestCookies {
73+
const header: undefined | string | string[] = req.headers.cookie;
74+
if (!header) {
75+
return {};
76+
}
77+
return parse(Array.isArray(header) ? header.join(';') : header);
78+
};
79+
}
80+
81+
const nowNodeServer = (
82+
route: (req: NowRequest, res: NowResponse) => any | Promise<any>
83+
) =>
84+
micro(async (req: IncomingMessage, res: ServerResponse) => {
85+
const bufferOrString = await buffer(req);
86+
const nowReq = Object.assign(req, {
87+
body:
88+
typeof bufferOrString === 'string'
89+
? bufferOrString
90+
: getBodyParser(req, bufferOrString)(),
91+
cookies: getCookieParser(req)(),
92+
query: getQueryParser(req)(),
93+
});
94+
let _status: number;
95+
const nowRes = Object.assign(res, {
96+
status: (status: number) => {
97+
_status = status;
98+
return nowRes;
99+
},
100+
json: (jsonBody: any) => {
101+
send(nowRes, _status || 200, jsonBody);
102+
return nowRes;
103+
},
104+
send: (body: string | object | Buffer) => {
105+
send(nowRes, _status || 200, body);
106+
return nowRes;
107+
},
108+
text: (body: string) => {
109+
send(nowRes, _status || 200, body);
110+
return nowRes;
111+
},
112+
});
113+
return await route(nowReq, nowRes);
114+
});
115+
116+
export default nowNodeServer;

test/avatar.png

48 KB
Loading

0 commit comments

Comments
 (0)