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 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
- - built with `async` / `await`
19
- - 🛠 JSON / raw / urlencoded data support
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
- # yarn
32
- yarn add milliparsec
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 && console.log(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 type { EventEmitter } from 'node:events';
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
- } & EventEmitter;
9
+ };
7
10
  export declare const hasBody: (method: string) => boolean;
8
- export declare const p: <T = any>(fn: (body: any) => any) => (req: ReqWithBody<T>, _res: Response, next: (err?: any) => void) => Promise<any>;
9
- declare const custom: <T = any>(fn: (body: any) => any) => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
10
- declare const json: () => (req: ReqWithBody, res: Response, next: NextFunction) => Promise<void>;
11
- declare const raw: () => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
12
- declare const text: () => (req: ReqWithBody, _res: Response, next: NextFunction) => Promise<void>;
13
- declare const urlencoded: () => (req: ReqWithBody, res: Response, next: NextFunction) => Promise<void>;
14
- export { custom, json, raw, text, urlencoded };
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
- let body = '';
6
- for await (const chunk of req)
7
- body += chunk;
8
- return fn(body);
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
- // JSON, raw, FormData
21
+ /**
22
+ * Parse payload with a custom function
23
+ * @param fn
24
+ */
15
25
  const custom = (fn) => async (req, _res, next) => {
16
- req.body = await p(fn)(req, undefined, next);
26
+ if (hasBody(req.method))
27
+ req.body = await p(fn)(req, _res, next);
17
28
  next();
18
29
  };
19
- const json = () => async (req, res, next) => {
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) => (x ? JSON.parse(x.toString()) : {}))(req, res, next);
22
- next();
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
- const raw = () => async (req, _res, next) => {
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 text = () => async (req, _res, next) => {
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) => x.toString())(req, _res, next);
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 urlencoded = () => async (req, res, next) => {
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 urlSearchParam = new URLSearchParams(x.toString());
47
- return Object.fromEntries(urlSearchParam.entries());
48
- })(req, res, next);
49
- next();
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.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "tiniest body parser in the universe",
5
- "repository": "https://github.com/talentlessguy/parsec.git",
6
- "author": "talentlessguy <pilll.PL22@gmail.com>",
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": ">=12.20"
20
+ "node": ">=20"
18
21
  },
19
22
  "exports": "./dist/index.js",
20
23
  "devDependencies": {
21
- "@biomejs/biome": "1.8.3",
22
- "@tinyhttp/app": "^2.2.4",
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
- "tsm": "^2.3.0",
27
- "typescript": "^5.5.3",
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": "uvu -r tsm",
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"