diff --git a/lib/main.d.ts b/lib/main.d.ts new file mode 100644 index 0000000..da6fb3c --- /dev/null +++ b/lib/main.d.ts @@ -0,0 +1,168 @@ +// Definitions by: Jacob Baskin +// BendingBender +// Igor Savin + +/// + +import * as http from 'http'; +import {Readable, Writable} from 'stream'; + +declare const busboy: BusboyConstructor; +export default busboy + +export interface BusboyConfig { + /** + * These are the HTTP headers of the incoming request, which are used by individual parsers. + */ + headers: BusboyHeaders; + /** + * `highWaterMark` to use for this Busboy instance. + * @default WritableStream default. + */ + highWaterMark?: number | undefined; + /** + * highWaterMark to use for file streams. + * @default ReadableStream default. + */ + fileHwm?: number | undefined; + /** + * Default character set to use when one isn't defined. + * @default 'utf8' + */ + defCharset?: string | undefined; + /** + * If paths in the multipart 'filename' field shall be preserved. + * @default false + */ + preservePath?: boolean | undefined; + /** + * Various limits on incoming data. + */ + limits?: + | { + /** + * Max field name size (in bytes) + * @default 100 bytes + */ + fieldNameSize?: number | undefined; + /** + * Max field value size (in bytes) + * @default 1MB + */ + fieldSize?: number | undefined; + /** + * Max number of non-file fields + * @default Infinity + */ + fields?: number | undefined; + /** + * For multipart forms, the max file size (in bytes) + * @default Infinity + */ + fileSize?: number | undefined; + /** + * For multipart forms, the max number of file fields + * @default Infinity + */ + files?: number | undefined; + /** + * For multipart forms, the max number of parts (fields + files) + * @default Infinity + */ + parts?: number | undefined; + /** + * For multipart forms, the max number of header key=>value pairs to parse + * @default 2000 (same as node's http) + */ + headerPairs?: number | undefined; + } + | undefined; +} + +export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders; + +export interface Busboy extends Writable { + addListener(event: Event, listener: BusboyEvents[Event]): this; + + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + + on(event: Event, listener: BusboyEvents[Event]): this; + + on(event: string | symbol, listener: (...args: any[]) => void): this; + + once(event: Event, listener: BusboyEvents[Event]): this; + + once(event: string | symbol, listener: (...args: any[]) => void): this; + + removeListener(event: Event, listener: BusboyEvents[Event]): this; + + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + + off(event: Event, listener: BusboyEvents[Event]): this; + + off(event: string | symbol, listener: (...args: any[]) => void): this; + + prependListener(event: Event, listener: BusboyEvents[Event]): this; + + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + + prependOnceListener(event: Event, listener: BusboyEvents[Event]): this; + + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; +} + +export interface BusboyEvents { + /** + * Emitted for each new file form field found. + * + * * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the + * file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), + * otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** + * incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically + * and safely discarded (these discarded files do still count towards `files` and `parts` limits). + * * If a configured file size limit was reached, `stream` will both have a boolean property `truncated` + * (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens. + * + * @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream. + * @param listener.mimeType Contains the 'Content-Type' value for the file stream. + */ + file: ( + fieldname: string, + stream: Readable, + filename: string, + transferEncoding: string, + mimeType: string, + ) => void; + /** + * Emitted for each new non-file field found. + */ + field: ( + fieldname: string, + value: string, + fieldnameTruncated: boolean, + valueTruncated: boolean, + transferEncoding: string, + mimeType: string, + ) => void; + finish: () => void; + /** + * Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted. + */ + partsLimit: () => void; + /** + * Emitted when specified `files` limit has been reached. No more 'file' events will be emitted. + */ + filesLimit: () => void; + /** + * Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted. + */ + fieldsLimit: () => void; + error: (error: unknown) => void; +} + +export interface BusboyConstructor { + (options: BusboyConfig): Busboy; + + new(options: BusboyConfig): Busboy; +} + diff --git a/package.json b/package.json index 9208130..7451e15 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,23 @@ } ], "description": "A streaming parser for HTML form data for node.js", - "main": "./lib/main", + "main": "lib/main", + "types": "lib/main.d.ts", "dependencies": { "dicer": "0.3.0" }, "devDependencies": { + "@types/node": "^16.11.10", "chai": "^4.3.4", - "mocha": "^8.4.0" + "mocha": "^8.4.0", + "tsd": "^0.19.0", + "typescript": "^4.5.2" }, "scripts": { "test:legacy": "node test/test.js", "test:mocha": "mocha test", - "test": "npm run test:mocha && npm run test:legacy" + "test:types": "tsd", + "test": "npm run test:mocha && npm run test:legacy && npm run test:types" }, "engines": { "node": ">=10.17.0" @@ -36,5 +41,13 @@ "repository": { "type": "git", "url": "https://github.com/fastify/busboy.git" + }, + "tsd": { + "directory": "test/types", + "compilerOptions": { + "esModuleInterop": false, + "module": "commonjs", + "target": "ES2017" + } } } diff --git a/test/types/main.test-d.ts b/test/types/main.test-d.ts new file mode 100644 index 0000000..e33eaf2 --- /dev/null +++ b/test/types/main.test-d.ts @@ -0,0 +1,235 @@ +import BusboyDefault, { BusboyConstructor, BusboyConfig, BusboyHeaders, Busboy, BusboyEvents } from '../..'; +import {expectError, expectType} from "tsd"; +import {Readable} from "stream"; + +// test type exports +type Constructor = BusboyConstructor; +type Config = BusboyConfig; +type Headers = BusboyHeaders; +type Events = BusboyEvents; +type BB = Busboy; + +expectError(new BusboyDefault({})); +const busboy = BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, highWaterMark: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, fileHwm: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, defCharset: 'utf8' }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, preservePath: true }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldNameSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fields: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fileSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { files: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { parts: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerPairs: 200 } }); // $ExpectType Busboy + +busboy.addListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname) + expectType(file); + expectType(filename); + expectType(encoding); + expectType(mimetype); +}); +busboy.addListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.addListener('partsLimit', () => {}); +busboy.addListener('filesLimit', () => {}); +busboy.addListener('fieldsLimit', () => {}); +busboy.addListener('error', e => { + expectType (e); +}); +busboy.addListener('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType(foo); +}); + +busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.on('partsLimit', () => {}); +busboy.on('filesLimit', () => {}); +busboy.on('fieldsLimit', () => {}); +busboy.on('error', e => { + expectType (e); +}); +busboy.on('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType (foo); +}); + +busboy.once('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.once('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.once('partsLimit', () => {}); +busboy.once('filesLimit', () => {}); +busboy.once('fieldsLimit', () => {}); +busboy.once('error', e => { + expectType (e); +}); +busboy.once('finish', () => {}); +// test fallback +busboy.once('foo', foo => { + expectType (foo); +}); +busboy.once(Symbol('foo'), foo => { + expectType (foo); +}); + +busboy.removeListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.removeListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.removeListener('partsLimit', () => {}); +busboy.removeListener('filesLimit', () => {}); +busboy.removeListener('fieldsLimit', () => {}); +busboy.removeListener('error', e => { + expectType (e); +}); +busboy.removeListener('finish', () => {}); +// test fallback +busboy.removeListener('foo', foo => { + expectType (foo); +}); +busboy.removeListener(Symbol('foo'), foo => { + expectType (foo); +}); + +busboy.off('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.off('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.off('partsLimit', () => {}); +busboy.off('filesLimit', () => {}); +busboy.off('fieldsLimit', () => {}); +busboy.off('error', e => { + expectType (e); +}); +busboy.off('finish', () => {}); +// test fallback +busboy.off('foo', foo => { + expectType (foo); +}); +busboy.off(Symbol('foo'), foo => { + expectType (foo); +}); + +busboy.prependListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.prependListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.prependListener('partsLimit', () => {}); +busboy.prependListener('filesLimit', () => {}); +busboy.prependListener('fieldsLimit', () => {}); +busboy.prependListener('error', e => { + expectType (e); +}); +busboy.prependListener('finish', () => {}); +// test fallback +busboy.prependListener('foo', foo => { + expectType (foo); +}); +busboy.prependListener(Symbol('foo'), foo => { + expectType (foo); +}); + +busboy.prependOnceListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType (fieldname); + expectType (file); + expectType (filename); + expectType (encoding); + expectType (mimetype); +}); +busboy.prependOnceListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType (fieldname); + expectType (val); + expectType (fieldnameTruncated); + expectType (valTruncated); + expectType (encoding); + expectType (mimetype); +}); +busboy.prependOnceListener('partsLimit', () => {}); +busboy.prependOnceListener('filesLimit', () => {}); +busboy.prependOnceListener('fieldsLimit', () => {}); +busboy.prependOnceListener('error', e => { + expectType (e); +}); +busboy.prependOnceListener('finish', () => {}); +// test fallback +busboy.prependOnceListener('foo', foo => { + expectType (foo); +}); +busboy.prependOnceListener(Symbol('foo'), foo => { + expectType (foo); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..55b6570 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "target": "es2015", + "sourceMap": false, + "declaration": true, + "declarationMap": false, + "types": ["node", "jest"], + "strict": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "importHelpers": true, + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": [ + "node_modules", + "test", + "dist" + ] +}