milliparsec 3.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 +6 -58
- package/dist/index.d.ts +66 -9
- package/dist/index.js +96 -21
- package/package.json +15 -11
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,9 +14,8 @@ Check out [deno-libs/parsec](https://github.com/deno-libs/parsec) for Deno port.
|
|
|
15
14
|
|
|
16
15
|
## Features
|
|
17
16
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- 📦 tiny package size (675B)
|
|
17
|
+
- 🛠 JSON / raw / urlencoded / multipart support
|
|
18
|
+
- 📦 tiny package size (8KB dist size)
|
|
21
19
|
- 🔥 no dependencies
|
|
22
20
|
- ✨ [tinyhttp](https://github.com/tinyhttp/tinyhttp) and Express support
|
|
23
21
|
- ⚡ 30% faster than body-parser
|
|
@@ -28,11 +26,8 @@ Check out [deno-libs/parsec](https://github.com/deno-libs/parsec) for Deno port.
|
|
|
28
26
|
# pnpm
|
|
29
27
|
pnpm i milliparsec
|
|
30
28
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# npm
|
|
35
|
-
npm i milliparsec
|
|
29
|
+
# bun
|
|
30
|
+
bun i milliparsec
|
|
36
31
|
```
|
|
37
32
|
|
|
38
33
|
## Usage
|
|
@@ -42,11 +37,11 @@ npm i milliparsec
|
|
|
42
37
|
Use a middleware inside a server:
|
|
43
38
|
|
|
44
39
|
```js
|
|
45
|
-
import { createServer } from 'http'
|
|
40
|
+
import { createServer } from 'node:http'
|
|
46
41
|
import { json } from 'milliparsec'
|
|
47
42
|
|
|
48
43
|
const server = createServer(async (req: ReqWithBody, res) => {
|
|
49
|
-
await json()(req, res, (err) => void err &&
|
|
44
|
+
await json()(req, res, (err) => void err && res.end(err))
|
|
50
45
|
|
|
51
46
|
res.setHeader('Content-Type', 'application/json')
|
|
52
47
|
|
|
@@ -54,57 +49,10 @@ const server = createServer(async (req: ReqWithBody, res) => {
|
|
|
54
49
|
})
|
|
55
50
|
```
|
|
56
51
|
|
|
57
|
-
### Web frameworks integration
|
|
58
|
-
|
|
59
|
-
#### tinyhttp
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import { App } from '@tinyhttp/app'
|
|
63
|
-
import { urlencoded } from 'milliparsec'
|
|
64
|
-
|
|
65
|
-
new App()
|
|
66
|
-
.use(urlencoded())
|
|
67
|
-
.post('/', (req, res) => void res.send(req.body))
|
|
68
|
-
.listen(3000, () => console.log(`Started on http://localhost:3000`))
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## API
|
|
72
|
-
|
|
73
|
-
### `raw(req, res, cb)`
|
|
74
|
-
|
|
75
|
-
Minimal body parsing without any formatting.
|
|
76
|
-
|
|
77
|
-
### `text(req, res, cb)`
|
|
78
|
-
|
|
79
|
-
Converts request body to string.
|
|
80
|
-
|
|
81
|
-
### `urlencoded(req, res, cb)`
|
|
82
|
-
|
|
83
|
-
Parses request body using `new URLSearchParams`.
|
|
84
|
-
|
|
85
|
-
### `json(req, res, cb)`
|
|
86
|
-
|
|
87
|
-
Parses request body using `JSON.parse`.
|
|
88
|
-
|
|
89
|
-
### `custom(fn)(req, res, cb)`
|
|
90
|
-
|
|
91
|
-
Custom function for `parsec`.
|
|
92
|
-
|
|
93
|
-
```js
|
|
94
|
-
// curl -d "this text must be uppercased" localhost
|
|
95
|
-
await custom(
|
|
96
|
-
req,
|
|
97
|
-
(d) => d.toUpperCase(),
|
|
98
|
-
(err) => {}
|
|
99
|
-
)
|
|
100
|
-
res.end(req.body) // "THIS TEXT MUST BE UPPERCASED"
|
|
101
|
-
```
|
|
102
|
-
|
|
103
52
|
### What is "parsec"?
|
|
104
53
|
|
|
105
54
|
The parsec is a unit of length used to measure large distances to astronomical objects outside the Solar System.
|
|
106
55
|
|
|
107
|
-
[vulns-badge-url]: https://img.shields.io/snyk/vulnerabilities/npm/milliparsec.svg?style=for-the-badge&color=25608B&label=vulns
|
|
108
56
|
[v-badge-url]: https://img.shields.io/npm/v/milliparsec.svg?style=for-the-badge&color=25608B&logo=npm&label=
|
|
109
57
|
[npm-url]: https://www.npmjs.com/package/milliparsec
|
|
110
58
|
[dl-badge-url]: https://img.shields.io/npm/dt/milliparsec?style=for-the-badge&color=25608B
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +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>;
|
|
71
|
+
export { custom, json, raw, text, urlencoded, multipart };
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,129 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
1
2
|
export const hasBody = (method) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
3
|
+
const defaultPayloadLimit = 102400; // 100KB
|
|
4
|
+
const defaultErrorFn = (payloadLimit) => new Error(`Payload too large. Limit: ${payloadLimit} bytes`);
|
|
2
5
|
// Main function
|
|
3
|
-
export const p = (fn) => async (req, _res, next) => {
|
|
6
|
+
export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => async (req, _res, next) => {
|
|
4
7
|
try {
|
|
5
|
-
|
|
6
|
-
for await (const chunk of req)
|
|
7
|
-
body
|
|
8
|
-
|
|
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));
|
|
9
16
|
}
|
|
10
17
|
catch (e) {
|
|
11
18
|
next(e);
|
|
12
19
|
}
|
|
13
20
|
};
|
|
14
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Parse payload with a custom function
|
|
23
|
+
* @param fn
|
|
24
|
+
*/
|
|
15
25
|
const custom = (fn) => async (req, _res, next) => {
|
|
16
|
-
|
|
26
|
+
if (hasBody(req.method))
|
|
27
|
+
req.body = await p(fn)(req, _res, next);
|
|
17
28
|
next();
|
|
18
29
|
};
|
|
19
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Parse JSON payload
|
|
32
|
+
* @param options
|
|
33
|
+
*/
|
|
34
|
+
const json = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, res, next) => {
|
|
20
35
|
if (hasBody(req.method)) {
|
|
21
|
-
req.body = await p((x) =>
|
|
22
|
-
|
|
36
|
+
req.body = await p((x) => {
|
|
37
|
+
const str = td.decode(x);
|
|
38
|
+
return str ? JSON.parse(str) : {};
|
|
39
|
+
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
|
|
23
40
|
}
|
|
24
41
|
else
|
|
25
42
|
next();
|
|
26
43
|
};
|
|
27
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Parse raw payload
|
|
46
|
+
* @param options
|
|
47
|
+
*/
|
|
48
|
+
const raw = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
|
|
28
49
|
if (hasBody(req.method)) {
|
|
29
|
-
req.body = await p((x) => x)(req, _res, next);
|
|
30
|
-
next();
|
|
50
|
+
req.body = await p((x) => x, payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
31
51
|
}
|
|
32
52
|
else
|
|
33
53
|
next();
|
|
34
54
|
};
|
|
35
|
-
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) => {
|
|
36
62
|
if (hasBody(req.method)) {
|
|
37
|
-
req.body = await p((x) =>
|
|
63
|
+
req.body = await p((x) => td.decode(x), payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
64
|
+
}
|
|
65
|
+
else
|
|
38
66
|
next();
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Parse urlencoded payload
|
|
70
|
+
* @param options
|
|
71
|
+
*/
|
|
72
|
+
const urlencoded = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
|
|
73
|
+
if (hasBody(req.method)) {
|
|
74
|
+
req.body = await p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn)(req, _res, next);
|
|
39
75
|
}
|
|
40
76
|
else
|
|
41
77
|
next();
|
|
42
78
|
};
|
|
43
|
-
const
|
|
79
|
+
const getBoundary = (contentType) => {
|
|
80
|
+
const match = /boundary=(.+);?/.exec(contentType);
|
|
81
|
+
return match ? `--${match[1]}` : null;
|
|
82
|
+
};
|
|
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 }) => {
|
|
86
|
+
const parts = body.split(new RegExp(`${boundary}(--)?`)).filter((part) => !!part && /content-disposition/i.test(part));
|
|
87
|
+
const parsedBody = {};
|
|
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) => {
|
|
92
|
+
const [headers, ...lines] = part.split('\r\n').filter((part) => !!part);
|
|
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
|
|
97
|
+
const name = /name="(.+?)"/.exec(headers)[1];
|
|
98
|
+
const filename = /filename="(.+?)"/.exec(headers);
|
|
99
|
+
if (filename) {
|
|
100
|
+
const contentTypeMatch = /Content-Type: (.+)/i.exec(data);
|
|
101
|
+
const fileContent = data.slice(contentTypeMatch[0].length + 2);
|
|
102
|
+
const file = new File([fileContent], filename[1], { type: contentTypeMatch[1] });
|
|
103
|
+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], file] : [file];
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], data] : [data];
|
|
107
|
+
return;
|
|
108
|
+
});
|
|
109
|
+
return parsedBody;
|
|
110
|
+
};
|
|
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) => {
|
|
44
118
|
if (hasBody(req.method)) {
|
|
45
119
|
req.body = await p((x) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
120
|
+
const boundary = getBoundary(req.headers['content-type']);
|
|
121
|
+
if (boundary)
|
|
122
|
+
return parseMultipart(td.decode(x), boundary, opts);
|
|
123
|
+
return {};
|
|
124
|
+
}, payloadLimit, payloadLimitErrorFn)(req, res, next);
|
|
50
125
|
}
|
|
51
126
|
else
|
|
52
127
|
next();
|
|
53
128
|
};
|
|
54
|
-
export { custom, json, raw, text, urlencoded };
|
|
129
|
+
export { custom, json, raw, text, urlencoded, multipart };
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "milliparsec",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "tiniest body parser in the universe",
|
|
5
|
-
"repository":
|
|
6
|
-
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/tinyhttp/milliparsec"
|
|
8
|
+
},
|
|
9
|
+
"author": "talentlessguy <hi@v1rtl.site>",
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
9
12
|
"type": "module",
|
|
@@ -14,24 +17,25 @@
|
|
|
14
17
|
"body-parsing"
|
|
15
18
|
],
|
|
16
19
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
20
|
+
"node": ">=20"
|
|
18
21
|
},
|
|
19
22
|
"exports": "./dist/index.js",
|
|
20
23
|
"devDependencies": {
|
|
21
|
-
"@biomejs/biome": "1.
|
|
22
|
-
"@
|
|
23
|
-
"@types/node": "^20.14.9",
|
|
24
|
+
"@biomejs/biome": "1.9.3",
|
|
25
|
+
"@types/node": "^22.7.4",
|
|
24
26
|
"c8": "10.1.2",
|
|
25
27
|
"supertest-fetch": "^2.0.0",
|
|
26
|
-
"
|
|
27
|
-
"typescript": "^5.
|
|
28
|
-
"uvu": "^0.5.6"
|
|
28
|
+
"tsx": "^4.19.1",
|
|
29
|
+
"typescript": "^5.6.2"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"dist"
|
|
32
33
|
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"provenance": true
|
|
36
|
+
},
|
|
33
37
|
"scripts": {
|
|
34
|
-
"test": "
|
|
38
|
+
"test": "tsx --test test.ts",
|
|
35
39
|
"test:coverage": "c8 --include=src pnpm test",
|
|
36
40
|
"test:report": "c8 report --reporter=text-lcov > coverage.lcov",
|
|
37
41
|
"build": "tsc -p tsconfig.build.json"
|