milliparsec 5.0.2 → 5.1.1

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
@@ -20,7 +20,7 @@ Check out [deno-libs/parsec](https://github.com/deno-libs/parsec) for Deno port.
20
20
  - 📦 tiny package size (8KB dist size)
21
21
  - 🔥 no dependencies
22
22
  - ✨ [tinyhttp](https://github.com/tinyhttp/tinyhttp) and Express support
23
- - ⚡ 40% faster than body-parser and 20x faster than formidable
23
+ - ⚡ ~15% faster than body-parser, ~4x faster than formidable
24
24
 
25
25
  ## Install
26
26
 
package/dist/index.d.ts CHANGED
@@ -1,52 +1,36 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import type { IncomingMessage, ServerResponse as Response } from 'node:http';
3
- type NextFunction = (err?: any) => void;
4
- /**
5
- * Request extension with a body
6
- */
7
- export type ReqWithBody<T = any> = IncomingMessage & {
8
- body?: T;
9
- };
10
- export declare const hasBody: (method: string) => boolean;
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>;
2
+ import type { ServerResponse as Response } from 'node:http';
3
+ import type { LimitErrorFn, NextFunction, ParserOptions, ReqWithBody } from './types.ts';
4
+ export * from './types.ts';
5
+ export declare const p: <T = any>(fn: (body: Buffer, req: ReqWithBody<T>) => void, payloadLimit?: number, payloadLimitErrorFn?: LimitErrorFn) => (req: ReqWithBody<T>, _res: Response, next?: (err?: any) => void) => Promise<T | undefined>;
24
6
  /**
25
7
  * Parse payload with a custom function
26
8
  * @param fn
27
9
  */
28
- declare const custom: <T = any>(fn: (body: Buffer) => any) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
10
+ declare const custom: <T = any>(fn: (body: Buffer) => any, type?: ParserOptions["type"]) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
29
11
  /**
30
12
  * Parse JSON payload
31
13
  * @param options
32
14
  */
33
- declare const json: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, res: Response, next?: NextFunction) => Promise<void>;
15
+ declare const json: ({ payloadLimit, payloadLimitErrorFn, type, reviver }?: ParserOptions<{
16
+ reviver?: (this: any, key: string, value: any) => any;
17
+ }>) => (req: ReqWithBody, res: Response, next?: NextFunction) => Promise<void>;
34
18
  /**
35
19
  * Parse raw payload
36
20
  * @param options
37
21
  */
38
- declare const raw: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
22
+ declare const raw: ({ payloadLimit, payloadLimitErrorFn, type }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
39
23
  /**
40
24
  * Stringify request payload
41
25
  * @param param0
42
26
  * @returns
43
27
  */
44
- declare const text: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
28
+ declare const text: ({ payloadLimit, payloadLimitErrorFn, type }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
45
29
  /**
46
30
  * Parse urlencoded payload
47
31
  * @param options
48
32
  */
49
- declare const urlencoded: ({ payloadLimit, payloadLimitErrorFn }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
33
+ declare const urlencoded: ({ payloadLimit, payloadLimitErrorFn, type }?: ParserOptions) => (req: ReqWithBody, _res: Response, next?: NextFunction) => Promise<void>;
50
34
  type MultipartOptions = Partial<{
51
35
  /**
52
36
  * Limit number of files
@@ -67,5 +51,5 @@ type MultipartOptions = Partial<{
67
51
  * Does not restrict total payload size by default.
68
52
  * @param options
69
53
  */
70
- declare const multipart: ({ payloadLimit, payloadLimitErrorFn, ...opts }?: MultipartOptions & ParserOptions) => (req: ReqWithBody, res: Response, next?: NextFunction) => Promise<void>;
54
+ declare const multipart: ({ payloadLimit, payloadLimitErrorFn, type, ...opts }?: MultipartOptions & ParserOptions) => (req: ReqWithBody, res: Response, next?: NextFunction) => Promise<void>;
71
55
  export { custom, json, raw, text, urlencoded, multipart };
package/dist/index.js CHANGED
@@ -1,90 +1,116 @@
1
1
  import { Buffer, File } from 'node:buffer';
2
- export const hasBody = (method) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
3
- const defaultPayloadLimit = 102400; // 100KB
2
+ import { checkType, hasBody } from "./utils.js";
3
+ export * from "./types.js";
4
+ const defaultPayloadLimit = 102400; // 100KiB
4
5
  const defaultErrorFn = (payloadLimit) => new Error(`Payload too large. Limit: ${payloadLimit} bytes`);
5
6
  // Main function
6
- export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => async (req, _res, next) => {
7
- try {
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);
7
+ export const p = (fn, payloadLimit = defaultPayloadLimit, payloadLimitErrorFn = defaultErrorFn) => (req, _res, next) => new Promise((resolve) => {
8
+ const body = [];
9
+ let totalSize = 0;
10
+ req.on('data', (chunk) => {
11
+ totalSize += chunk.byteLength;
12
+ if (totalSize > payloadLimit) {
13
+ req.removeAllListeners();
14
+ next === null || next === void 0 ? void 0 : next(payloadLimitErrorFn(payloadLimit));
15
+ resolve(undefined);
16
+ }
17
+ else {
13
18
  body.push(chunk);
14
19
  }
15
- return fn(Buffer.concat(body));
16
- }
17
- catch (e) {
18
- next === null || next === void 0 ? void 0 : next(e);
19
- }
20
- };
20
+ });
21
+ req.on('end', () => {
22
+ try {
23
+ resolve(fn(body.length === 1 ? body[0] : Buffer.concat(body), req));
24
+ }
25
+ catch (e) {
26
+ next === null || next === void 0 ? void 0 : next(e);
27
+ resolve(undefined);
28
+ }
29
+ });
30
+ req.on('error', (err) => {
31
+ next === null || next === void 0 ? void 0 : next(err);
32
+ resolve(undefined);
33
+ });
34
+ });
21
35
  /**
22
36
  * Parse payload with a custom function
23
37
  * @param fn
24
38
  */
25
- const custom = (fn) => async (req, _res, next) => {
26
- if (hasBody(req.method))
27
- req.body = await p(fn)(req, _res, next);
28
- next === null || next === void 0 ? void 0 : next();
39
+ const custom = (fn, type) => {
40
+ const parse = p(fn);
41
+ return async (req, _res, next) => {
42
+ if (hasBody(req.method) && checkType(req, type))
43
+ req.body = await parse(req, _res, next);
44
+ next === null || next === void 0 ? void 0 : next();
45
+ };
29
46
  };
30
47
  /**
31
48
  * Parse JSON payload
32
49
  * @param options
33
50
  */
34
- const json = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, res, next) => {
35
- if (hasBody(req.method)) {
36
- req.body = await p((x) => {
37
- const str = td.decode(x);
38
- return str ? JSON.parse(str) : {};
39
- }, payloadLimit, payloadLimitErrorFn)(req, res, next);
40
- }
41
- next === null || next === void 0 ? void 0 : next();
51
+ const json = ({ payloadLimit, payloadLimitErrorFn, type, reviver } = {}) => {
52
+ const parse = p((x) => (x.length === 0 ? {} : JSON.parse(x.toString(), reviver)), payloadLimit, payloadLimitErrorFn);
53
+ return async (req, res, next) => {
54
+ if (hasBody(req.method) && checkType(req, type)) {
55
+ req.body = await parse(req, res, next);
56
+ }
57
+ next === null || next === void 0 ? void 0 : next();
58
+ };
42
59
  };
43
60
  /**
44
61
  * Parse raw payload
45
62
  * @param options
46
63
  */
47
- const raw = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
48
- if (hasBody(req.method)) {
49
- req.body = await p((x) => x, payloadLimit, payloadLimitErrorFn)(req, _res, next);
50
- }
51
- next === null || next === void 0 ? void 0 : next();
64
+ const raw = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
65
+ const parse = p((x) => x, payloadLimit, payloadLimitErrorFn);
66
+ return async (req, _res, next) => {
67
+ if (hasBody(req.method) && checkType(req, type)) {
68
+ req.body = await parse(req, _res, next);
69
+ }
70
+ next === null || next === void 0 ? void 0 : next();
71
+ };
52
72
  };
53
- const td = new TextDecoder();
54
73
  /**
55
74
  * Stringify request payload
56
75
  * @param param0
57
76
  * @returns
58
77
  */
59
- const text = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
60
- if (hasBody(req.method)) {
61
- req.body = await p((x) => td.decode(x), payloadLimit, payloadLimitErrorFn)(req, _res, next);
62
- }
63
- next === null || next === void 0 ? void 0 : next();
78
+ const text = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
79
+ const parse = p((x) => x.toString(), payloadLimit, payloadLimitErrorFn);
80
+ return async (req, _res, next) => {
81
+ if (hasBody(req.method) && checkType(req, type)) {
82
+ req.body = await parse(req, _res, next);
83
+ }
84
+ next === null || next === void 0 ? void 0 : next();
85
+ };
64
86
  };
87
+ const td = new TextDecoder();
65
88
  /**
66
89
  * Parse urlencoded payload
67
90
  * @param options
68
91
  */
69
- const urlencoded = ({ payloadLimit, payloadLimitErrorFn } = {}) => async (req, _res, next) => {
70
- if (hasBody(req.method)) {
71
- req.body = await p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn)(req, _res, next);
72
- }
73
- next === null || next === void 0 ? void 0 : next();
92
+ const urlencoded = ({ payloadLimit, payloadLimitErrorFn, type } = {}) => {
93
+ const parse = p((x) => Object.fromEntries(new URLSearchParams(x.toString()).entries()), payloadLimit, payloadLimitErrorFn);
94
+ return async (req, _res, next) => {
95
+ if (hasBody(req.method) && checkType(req, type)) {
96
+ req.body = await parse(req, _res, next);
97
+ }
98
+ next === null || next === void 0 ? void 0 : next();
99
+ };
74
100
  };
75
101
  const getBoundary = (contentType) => {
76
102
  const match = /boundary=(.+);?/.exec(contentType);
77
103
  return match ? `--${match[1]}` : null;
78
104
  };
79
105
  const defaultFileSizeLimitErrorFn = (limit) => new Error(`File too large. Limit: ${limit} bytes`);
80
- const defaultFileSizeLimit = 200 * 1024 * 1024;
106
+ const defaultFileSizeLimit = 200 * 1024 * 1024; // 200MiB
81
107
  const parseMultipart = (body, boundary, { fileCountLimit, fileSizeLimit = defaultFileSizeLimit, fileSizeLimitErrorFn = defaultFileSizeLimitErrorFn }) => {
82
108
  const parts = body.split(new RegExp(`${boundary}(--)?`)).filter((part) => !!part && /content-disposition/i.test(part));
83
109
  const parsedBody = {};
84
110
  if (fileCountLimit && parts.length > fileCountLimit)
85
111
  throw new Error(`Too many files. Limit: ${fileCountLimit}`);
86
- // biome-ignore lint/complexity/noForEach: for...of fails
87
112
  parts.forEach((part) => {
113
+ var _a, _b;
88
114
  const [headers, ...lines] = part.split('\r\n').filter((part) => !!part);
89
115
  const data = lines.join('\r\n').trim();
90
116
  if (data.length > fileSizeLimit)
@@ -96,11 +122,11 @@ const parseMultipart = (body, boundary, { fileCountLimit, fileSizeLimit = defaul
96
122
  const contentTypeMatch = /Content-Type: (.+)/i.exec(data);
97
123
  const fileContent = data.slice(contentTypeMatch[0].length + 2);
98
124
  const file = new File([fileContent], filename[1], { type: contentTypeMatch[1] });
99
- parsedBody[name] = parsedBody[name] ? [...parsedBody[name], file] : [file];
125
+ ((_a = parsedBody[name]) !== null && _a !== void 0 ? _a : (parsedBody[name] = [])).push(file);
100
126
  return;
101
127
  }
102
- parsedBody[name] = parsedBody[name] ? [...parsedBody[name], data] : [data];
103
- return;
128
+ ;
129
+ ((_b = parsedBody[name]) !== null && _b !== void 0 ? _b : (parsedBody[name] = [])).push(data);
104
130
  });
105
131
  return parsedBody;
106
132
  };
@@ -110,15 +136,18 @@ const parseMultipart = (body, boundary, { fileCountLimit, fileSizeLimit = defaul
110
136
  * Does not restrict total payload size by default.
111
137
  * @param options
112
138
  */
113
- const multipart = ({ payloadLimit = Number.POSITIVE_INFINITY, payloadLimitErrorFn, ...opts } = {}) => async (req, res, next) => {
114
- if (hasBody(req.method)) {
115
- req.body = await p((x) => {
116
- const boundary = getBoundary(req.headers['content-type']);
117
- if (boundary)
118
- return parseMultipart(td.decode(x), boundary, opts);
119
- return {};
120
- }, payloadLimit, payloadLimitErrorFn)(req, res, next);
121
- }
122
- next === null || next === void 0 ? void 0 : next();
139
+ const multipart = ({ payloadLimit = Number.POSITIVE_INFINITY, payloadLimitErrorFn, type, ...opts } = {}) => {
140
+ const parse = p((x, req) => {
141
+ const boundary = getBoundary(req.headers['content-type']);
142
+ if (boundary)
143
+ return parseMultipart(td.decode(x), boundary, opts);
144
+ return {};
145
+ }, payloadLimit, payloadLimitErrorFn);
146
+ return async (req, res, next) => {
147
+ if (hasBody(req.method) && checkType(req, type)) {
148
+ req.body = await parse(req, res, next);
149
+ }
150
+ next === null || next === void 0 ? void 0 : next();
151
+ };
123
152
  };
124
153
  export { custom, json, raw, text, urlencoded, multipart };
@@ -0,0 +1,24 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ /**
3
+ * Request extension with a body
4
+ */
5
+ export type ReqWithBody<T = any> = IncomingMessage & {
6
+ body?: T;
7
+ };
8
+ export type LimitErrorFn = (limit: number) => Error;
9
+ export type ParserOptions<T extends Record<string, any> = Record<string, any>> = Partial<{
10
+ /**
11
+ * Limit payload size (in bytes)
12
+ * @default 102400
13
+ */
14
+ payloadLimit: number;
15
+ /**
16
+ * Custom error function for payload limit
17
+ */
18
+ payloadLimitErrorFn: LimitErrorFn;
19
+ /**
20
+ * Middleware content type
21
+ */
22
+ type: (req: IncomingMessage) => boolean;
23
+ }> & T;
24
+ export type NextFunction = (err?: any) => void;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { ParserOptions } from './types.ts';
3
+ export declare const hasBody: (method: string) => boolean;
4
+ export declare const checkType: (req: IncomingMessage, type: ParserOptions["type"]) => boolean;
package/dist/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ const bodyMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
2
+ export const hasBody = (method) => bodyMethods.has(method);
3
+ export const checkType = (req, type) => typeof type === 'function' ? type(req) : true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "milliparsec",
3
- "version": "5.0.2",
3
+ "version": "5.1.1",
4
4
  "description": "tiniest body parser in the universe",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,25 +21,25 @@
21
21
  },
22
22
  "exports": "./dist/index.js",
23
23
  "devDependencies": {
24
- "@biomejs/biome": "1.9.3",
25
- "@tinyhttp/app": "^2.4.0",
26
- "@types/node": "^18.19.76",
24
+ "@biomejs/biome": "2.2.2",
25
+ "@tinyhttp/app": "^2.5.2",
26
+ "@types/express": "^5.0.6",
27
+ "@types/node": "^18.19.130",
27
28
  "c8": "10.1.2",
29
+ "express": "^5.2.1",
28
30
  "supertest-fetch": "^2.0.0",
29
- "tsx": "^4.19.1",
30
- "typescript": "^5.6.2"
31
+ "typescript": "^5.9.3"
31
32
  },
32
33
  "files": [
33
34
  "dist"
34
35
  ],
35
- "publishConfig": {
36
- "provenance": true
37
- },
38
36
  "scripts": {
39
- "test": "tsx --test test.ts",
37
+ "test": "node --test test.ts",
40
38
  "test:coverage": "c8 --include=src pnpm test",
41
39
  "test:report": "c8 report --reporter=text-lcov > coverage.lcov",
42
40
  "build": "tsc -p tsconfig.build.json",
41
+ "prepublishOnly": "pnpm build && pnpm test",
43
42
  "check": "biome check --write"
44
- }
45
- }
43
+ },
44
+ "packageManager": "pnpm@10.11.0"
45
+ }