milliparsec 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -61
- package/dist/index.d.ts +65 -9
- package/dist/index.js +73 -31
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
<img src="logo.png" width="400px" />
|
|
4
4
|
<br /><br />
|
|
5
5
|
|
|
6
|
-
![Vulnerabilities][vulns-badge-url]
|
|
7
6
|
[![Version][v-badge-url]][npm-url] [![Coverage][cov-img]][cov-url] [![Github actions][gh-actions-img]][github-actions] [![Downloads][dl-badge-url]][npm-url]
|
|
8
7
|
|
|
9
8
|
</div>
|
|
@@ -15,8 +14,7 @@ Check out [deno-libs/parsec](https://github.com/deno-libs/parsec) for Deno port.
|
|
|
15
14
|
|
|
16
15
|
## Features
|
|
17
16
|
|
|
18
|
-
-
|
|
19
|
-
- 🛠 JSON / raw / urlencoded data support
|
|
17
|
+
- 🛠 JSON / raw / urlencoded / multipart support
|
|
20
18
|
- 📦 tiny package size (8KB dist size)
|
|
21
19
|
- 🔥 no dependencies
|
|
22
20
|
- ✨ [tinyhttp](https://github.com/tinyhttp/tinyhttp) and Express support
|
|
@@ -43,7 +41,7 @@ import { createServer } from 'node:http'
|
|
|
43
41
|
import { json } from 'milliparsec'
|
|
44
42
|
|
|
45
43
|
const server = createServer(async (req: ReqWithBody, res) => {
|
|
46
|
-
await json()(req, res, (err) => void err &&
|
|
44
|
+
await json()(req, res, (err) => void err && res.end(err))
|
|
47
45
|
|
|
48
46
|
res.setHeader('Content-Type', 'application/json')
|
|
49
47
|
|
|
@@ -51,67 +49,10 @@ const server = createServer(async (req: ReqWithBody, res) => {
|
|
|
51
49
|
})
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
### Web frameworks integration
|
|
55
|
-
|
|
56
|
-
#### tinyhttp
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { App } from '@tinyhttp/app'
|
|
60
|
-
import { urlencoded } from 'milliparsec'
|
|
61
|
-
|
|
62
|
-
new App()
|
|
63
|
-
.use(urlencoded())
|
|
64
|
-
.post('/', (req, res) => void res.send(req.body))
|
|
65
|
-
.listen(3000, () => console.log(`Started on http://localhost:3000`))
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## API
|
|
69
|
-
|
|
70
|
-
### `raw(req, res, cb)`
|
|
71
|
-
|
|
72
|
-
Minimal body parsing without any formatting.
|
|
73
|
-
|
|
74
|
-
### `text(req, res, cb)`
|
|
75
|
-
|
|
76
|
-
Converts request body to string.
|
|
77
|
-
|
|
78
|
-
### `urlencoded(req, res, cb)`
|
|
79
|
-
|
|
80
|
-
Parses request body using `new URLSearchParams`.
|
|
81
|
-
|
|
82
|
-
### `json(req, res, cb)`
|
|
83
|
-
|
|
84
|
-
Parses request body using `JSON.parse`.
|
|
85
|
-
|
|
86
|
-
### `multipart(req, res, cb)`
|
|
87
|
-
|
|
88
|
-
Parses request body using `multipart/form-data` content type and boundary. Supports files as well.
|
|
89
|
-
|
|
90
|
-
```js
|
|
91
|
-
// curl -F "textfield=textfield" -F "someother=textfield with text" localhost:3000
|
|
92
|
-
await multipart()(req, res, (err) => void err && console.log(err))
|
|
93
|
-
res.end(req.body) // { textfield: "textfield", someother: "textfield with text" }
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### `custom(fn)(req, res, cb)`
|
|
97
|
-
|
|
98
|
-
Custom function for `parsec`.
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
// curl -d "this text must be uppercased" localhost:3000
|
|
102
|
-
await custom(
|
|
103
|
-
req,
|
|
104
|
-
(d) => d.toUpperCase(),
|
|
105
|
-
(err) => {}
|
|
106
|
-
)
|
|
107
|
-
res.end(req.body) // "THIS TEXT MUST BE UPPERCASED"
|
|
108
|
-
```
|
|
109
|
-
|
|
110
52
|
### What is "parsec"?
|
|
111
53
|
|
|
112
54
|
The parsec is a unit of length used to measure large distances to astronomical objects outside the Solar System.
|
|
113
55
|
|
|
114
|
-
[vulns-badge-url]: https://img.shields.io/snyk/vulnerabilities/npm/milliparsec.svg?style=for-the-badge&color=25608B&label=vulns
|
|
115
56
|
[v-badge-url]: https://img.shields.io/npm/v/milliparsec.svg?style=for-the-badge&color=25608B&logo=npm&label=
|
|
116
57
|
[npm-url]: https://www.npmjs.com/package/milliparsec
|
|
117
58
|
[dl-badge-url]: https://img.shields.io/npm/dt/milliparsec?style=for-the-badge&color=25608B
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,71 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
2
|
import type { IncomingMessage, ServerResponse as Response } from 'node:http';
|
|
3
3
|
type NextFunction = (err?: any) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Request extension with a body
|
|
6
|
+
*/
|
|
4
7
|
export type ReqWithBody<T = any> = IncomingMessage & {
|
|
5
8
|
body?: T;
|
|
6
|
-
}
|
|
9
|
+
};
|
|
7
10
|
export declare const hasBody: (method: string) => boolean;
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export type LimitErrorFn = (limit: number) => Error;
|
|
12
|
+
export type ParserOptions = Partial<{
|
|
13
|
+
/**
|
|
14
|
+
* Limit payload size (in bytes)
|
|
15
|
+
* @default '100KB'
|
|
16
|
+
*/
|
|
17
|
+
payloadLimit: number;
|
|
18
|
+
/**
|
|
19
|
+
* Custom error function for payload limit
|
|
20
|
+
*/
|
|
21
|
+
payloadLimitErrorFn: LimitErrorFn;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const p: <T = any>(fn: (body: Buffer) => void, payloadLimit?: number, payloadLimitErrorFn?: LimitErrorFn) => (req: ReqWithBody<T>, _res: Response, next: (err?: any) => void) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Parse payload with a custom function
|
|
26
|
+
* @param fn
|
|
27
|
+
*/
|
|
28
|
+
declare const custom: <T = any>(fn: (body: Buffer) => any) => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Parse JSON payload
|
|
31
|
+
* @param options
|
|
32
|
+
*/
|
|
33
|
+
declare const json: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, res: Response, next: NextFunction) => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Parse raw payload
|
|
36
|
+
* @param options
|
|
37
|
+
*/
|
|
38
|
+
declare const raw: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Stringify request payload
|
|
41
|
+
* @param param0
|
|
42
|
+
* @returns
|
|
43
|
+
*/
|
|
44
|
+
declare const text: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Parse urlencoded payload
|
|
47
|
+
* @param options
|
|
48
|
+
*/
|
|
49
|
+
declare const urlencoded: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
|
|
50
|
+
type MultipartOptions = Partial<{
|
|
51
|
+
/**
|
|
52
|
+
* Limit number of files
|
|
53
|
+
*/
|
|
54
|
+
fileCountLimit: number;
|
|
55
|
+
/**
|
|
56
|
+
* Limit file size (in bytes)
|
|
57
|
+
*/
|
|
58
|
+
fileSizeLimit: number;
|
|
59
|
+
/**
|
|
60
|
+
* Custom error function for file size limit
|
|
61
|
+
*/
|
|
62
|
+
fileSizeLimitErrorFn: LimitErrorFn;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Parse multipart form data (supports files as well)
|
|
66
|
+
*
|
|
67
|
+
* Does not restrict total payload size by default.
|
|
68
|
+
* @param options
|
|
69
|
+
*/
|
|
70
|
+
declare const multipart: ({ payloadLimit, payloadLimitErrorFn, ...opts }?: MultipartOptions & ParserOptions) => (req: ReqWithBody, res: Response, next: NextFunction) => Promise<void>;
|
|
15
71
|
export { custom, json, raw, text, urlencoded, multipart };
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,77 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
1
2
|
export const hasBody = (method) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
2
|
-
|
|
3
|
+
const defaultPayloadLimit = 102400; // 100KB
|
|
4
|
+
const defaultErrorFn = (payloadLimit) => new Error(`Payload too large. Limit: ${payloadLimit} bytes`);
|
|
5
|
+
// Main function
|
|
6
|
+
export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => async (req, _res, next) => {
|
|
3
7
|
try {
|
|
4
|
-
|
|
5
|
-
for await (const chunk of req)
|
|
6
|
-
body
|
|
7
|
-
|
|
8
|
+
const body = [];
|
|
9
|
+
for await (const chunk of req) {
|
|
10
|
+
const totalSize = body.reduce((total, buffer) => total + buffer.byteLength, 0);
|
|
11
|
+
if (totalSize > payloadLimit)
|
|
12
|
+
throw payloadLimitErrorFn(payloadLimit);
|
|
13
|
+
body.push(chunk);
|
|
14
|
+
}
|
|
15
|
+
return fn(Buffer.concat(body));
|
|
8
16
|
}
|
|
9
17
|
catch (e) {
|
|
10
18
|
next(e);
|
|
11
19
|
}
|
|
12
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Parse payload with a custom function
|
|
23
|
+
* @param fn
|
|
24
|
+
*/
|
|
13
25
|
const custom = (fn) => async (req, _res, next) => {
|
|
14
|
-
|
|
26
|
+
if (hasBody(req.method))
|
|
27
|
+
req.body = await p(fn)(req, _res, next);
|
|
15
28
|
next();
|
|
16
29
|
};
|
|
17
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Parse JSON payload
|
|
32
|
+
* @param options
|
|
33
|
+
*/
|
|
34
|
+
const json = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, res, next) => {
|
|
18
35
|
if (hasBody(req.method)) {
|
|
19
|
-
req.body = await p((x) =>
|
|
20
|
-
|
|
36
|
+
req.body = await p((x) => {
|
|
37
|
+
const str = td.decode(x);
|
|
38
|
+
return str ? JSON.parse(str) : {};
|
|
39
|
+
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
|
|
21
40
|
}
|
|
22
41
|
else
|
|
23
42
|
next();
|
|
24
43
|
};
|
|
25
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Parse raw payload
|
|
46
|
+
* @param options
|
|
47
|
+
*/
|
|
48
|
+
const raw = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
|
|
26
49
|
if (hasBody(req.method)) {
|
|
27
|
-
req.body = await p((x) => x)(req, _res, next);
|
|
28
|
-
next();
|
|
50
|
+
req.body = await p((x) => x, payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
29
51
|
}
|
|
30
52
|
else
|
|
31
53
|
next();
|
|
32
54
|
};
|
|
33
|
-
const
|
|
55
|
+
const td = new TextDecoder();
|
|
56
|
+
/**
|
|
57
|
+
* Stringify request payload
|
|
58
|
+
* @param param0
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
const text = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
|
|
34
62
|
if (hasBody(req.method)) {
|
|
35
|
-
req.body = await p((x) =>
|
|
36
|
-
next();
|
|
63
|
+
req.body = await p((x) => td.decode(x), payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
37
64
|
}
|
|
38
65
|
else
|
|
39
66
|
next();
|
|
40
67
|
};
|
|
41
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Parse urlencoded payload
|
|
70
|
+
* @param options
|
|
71
|
+
*/
|
|
72
|
+
const urlencoded = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
|
|
42
73
|
if (hasBody(req.method)) {
|
|
43
|
-
req.body = await p((x) =>
|
|
44
|
-
const urlSearchParam = new URLSearchParams(x.toString());
|
|
45
|
-
return Object.fromEntries(urlSearchParam.entries());
|
|
46
|
-
})(req, res, next);
|
|
47
|
-
next();
|
|
74
|
+
req.body = await p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
48
75
|
}
|
|
49
76
|
else
|
|
50
77
|
next();
|
|
@@ -53,33 +80,48 @@ const getBoundary = (contentType) => {
|
|
|
53
80
|
const match = /boundary=(.+);?/.exec(contentType);
|
|
54
81
|
return match ? `--${match[1]}` : null;
|
|
55
82
|
};
|
|
56
|
-
const
|
|
83
|
+
const defaultFileSizeLimitErrorFn = (limit) => new Error(`File too large. Limit: ${limit} bytes`);
|
|
84
|
+
const defaultFileSizeLimit = 200 * 1024 * 1024;
|
|
85
|
+
const parseMultipart = (body, boundary, { fileCountLimit, fileSizeLimit = defaultFileSizeLimit, fileSizeLimitErrorFn = defaultFileSizeLimitErrorFn }) => {
|
|
57
86
|
const parts = body.split(new RegExp(`${boundary}(--)?`)).filter((part) => !!part && /content-disposition/i.test(part));
|
|
58
87
|
const parsedBody = {};
|
|
59
|
-
parts.
|
|
88
|
+
if (fileCountLimit && parts.length > fileCountLimit)
|
|
89
|
+
throw new Error(`Too many files. Limit: ${fileCountLimit}`);
|
|
90
|
+
// biome-ignore lint/complexity/noForEach: for...of fails
|
|
91
|
+
parts.forEach((part) => {
|
|
60
92
|
const [headers, ...lines] = part.split('\r\n').filter((part) => !!part);
|
|
61
93
|
const data = lines.join('\r\n').trim();
|
|
94
|
+
if (data.length > fileSizeLimit)
|
|
95
|
+
throw fileSizeLimitErrorFn(fileSizeLimit);
|
|
96
|
+
// Extract the name and filename from the headers
|
|
62
97
|
const name = /name="(.+?)"/.exec(headers)[1];
|
|
63
98
|
const filename = /filename="(.+?)"/.exec(headers);
|
|
64
99
|
if (filename) {
|
|
65
100
|
const contentTypeMatch = /Content-Type: (.+)/i.exec(data);
|
|
66
101
|
const fileContent = data.slice(contentTypeMatch[0].length + 2);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
const file = new File([fileContent], filename[1], { type: contentTypeMatch[1] });
|
|
103
|
+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], file] : [file];
|
|
104
|
+
return;
|
|
70
105
|
}
|
|
71
|
-
|
|
106
|
+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], data] : [data];
|
|
107
|
+
return;
|
|
72
108
|
});
|
|
73
109
|
return parsedBody;
|
|
74
110
|
};
|
|
75
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Parse multipart form data (supports files as well)
|
|
113
|
+
*
|
|
114
|
+
* Does not restrict total payload size by default.
|
|
115
|
+
* @param options
|
|
116
|
+
*/
|
|
117
|
+
const multipart = ({ payloadLimit = Number.POSITIVE_INFINITY, payloadLimitErrorFn, ...opts } = {}) => async (req, res, next) => {
|
|
76
118
|
if (hasBody(req.method)) {
|
|
77
119
|
req.body = await p((x) => {
|
|
78
120
|
const boundary = getBoundary(req.headers['content-type']);
|
|
79
121
|
if (boundary)
|
|
80
|
-
return parseMultipart(x, boundary);
|
|
81
|
-
|
|
82
|
-
next
|
|
122
|
+
return parseMultipart(td.decode(x), boundary, opts);
|
|
123
|
+
return {};
|
|
124
|
+
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
|
|
83
125
|
}
|
|
84
126
|
else
|
|
85
127
|
next();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "milliparsec",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "tiniest body parser in the universe",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,13 +21,12 @@
|
|
|
21
21
|
},
|
|
22
22
|
"exports": "./dist/index.js",
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@biomejs/biome": "1.
|
|
25
|
-
"@types/node": "^
|
|
24
|
+
"@biomejs/biome": "1.9.3",
|
|
25
|
+
"@types/node": "^22.7.4",
|
|
26
26
|
"c8": "10.1.2",
|
|
27
27
|
"supertest-fetch": "^2.0.0",
|
|
28
|
-
"
|
|
29
|
-
"typescript": "^5.
|
|
30
|
-
"uvu": "^0.5.6"
|
|
28
|
+
"tsx": "^4.19.1",
|
|
29
|
+
"typescript": "^5.6.2"
|
|
31
30
|
},
|
|
32
31
|
"files": [
|
|
33
32
|
"dist"
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
"provenance": true
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
|
-
"test": "
|
|
38
|
+
"test": "tsx --test test.ts",
|
|
40
39
|
"test:coverage": "c8 --include=src pnpm test",
|
|
41
40
|
"test:report": "c8 report --reporter=text-lcov > coverage.lcov",
|
|
42
41
|
"build": "tsc -p tsconfig.build.json"
|