diff --git a/README.md b/README.md index 3bd7e19f3..4380f04d1 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ const snippet = new HTTPSnippet({ }); // generate Node.js: Native output -console.log(await snippet.convert('node')); +console.log(snippet.convert('node')); // generate Node.js: Native output, indent with tabs console.log( - await snippet.convert('node', { + snippet.convert('node', { indent: '\t', }), ); @@ -104,13 +104,13 @@ const snippet = new HTTPSnippet({ // generate Shell: cURL output console.log( - await snippet.convert('shell', 'curl', { + snippet.convert('shell', 'curl', { indent: '\t', }), ); // generate Node.js: Unirest output -console.log(await snippet.convert('node', 'unirest')); +console.log(snippet.convert('node', 'unirest')); ``` ### addTarget(target) diff --git a/package-lock.json b/package-lock.json index e77d930f7..53eb973e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "8.1.4", "license": "MIT", "dependencies": { - "formdata-to-string": "^2.0.2", "qs": "^6.11.2", "stringify-object": "^3.3.0" }, @@ -3871,14 +3870,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/formdata-to-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/formdata-to-string/-/formdata-to-string-2.0.2.tgz", - "integrity": "sha512-OxurQikLgzU3+AhBCb2Or7pV2+dQWMSi1r4ZmhGMZ/WxVLOfUCqB2hqK5EwTGSzN9O/dx9uw5Mln/vtG1t0XbQ==", - "engines": { - "node": ">=18" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/package.json b/package.json index 71bde5ee3..9ae9b0d96 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "test": "vitest run --coverage" }, "dependencies": { - "formdata-to-string": "^2.0.2", "qs": "^6.11.2", "stringify-object": "^3.3.0" }, diff --git a/src/fixtures/runCustomFixtures.ts b/src/fixtures/runCustomFixtures.ts index ac31a783a..faf323aa5 100644 --- a/src/fixtures/runCustomFixtures.ts +++ b/src/fixtures/runCustomFixtures.ts @@ -31,7 +31,7 @@ export const runCustomFixtures = ({ targetId, clientId, tests }: CustomFixture) } const snippet = new HTTPSnippet(request, opts); - const result = await snippet.convert(targetId, clientId, options); + const result = snippet.convert(targetId, clientId, options); const filePath = path.join(__dirname, '..', 'targets', targetId, clientId, 'fixtures', fixtureFile); if (process.env.OVERWRITE_EVERYTHING) { writeFileSync(filePath, String(result)); diff --git a/src/index.test.ts b/src/index.test.ts index 808a78b82..a5171621c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -10,24 +10,24 @@ import short from './fixtures/requests/short.cjs'; import { HTTPSnippet } from './index.js'; describe('HTTPSnippet', () => { - it('should return false if no matching target', async () => { + it('should return false if no matching target', () => { const snippet = new HTTPSnippet(short.log.entries[0].request as Request); // @ts-expect-error intentionally incorrect - const result = await snippet.convert(null); + const result = snippet.convert(null); expect(result).toBe(false); }); describe('repair malformed `postData`', () => { - it('should repair a HAR with an empty `postData` object', async () => { + it('should repair a HAR with an empty `postData` object', () => { const snippet = new HTTPSnippet({ method: 'POST', url: 'https://httpbin.org/anything', postData: {}, } as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -35,7 +35,7 @@ describe('HTTPSnippet', () => { }); }); - it('should repair a HAR with a `postData` params object missing `mimeType`', async () => { + it('should repair a HAR with a `postData` params object missing `mimeType`', () => { // @ts-expect-error Testing a malformed HAR case. const snippet = new HTTPSnippet({ method: 'POST', @@ -44,7 +44,7 @@ describe('HTTPSnippet', () => { params: [], }, } as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -53,7 +53,7 @@ describe('HTTPSnippet', () => { }); }); - it('should repair a HAR with a `postData` text object missing `mimeType`', async () => { + it('should repair a HAR with a `postData` text object missing `mimeType`', () => { const snippet = new HTTPSnippet({ method: 'POST', url: 'https://httpbin.org/anything', @@ -61,7 +61,7 @@ describe('HTTPSnippet', () => { text: '', }, } as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.postData).toStrictEqual({ @@ -71,7 +71,7 @@ describe('HTTPSnippet', () => { }); }); - it('should parse HAR file with multiple entries', async () => { + it('should parse HAR file with multiple entries', () => { const snippet = new HTTPSnippet({ log: { version: '1.2', @@ -96,7 +96,7 @@ describe('HTTPSnippet', () => { }, }); - await snippet.convert('node'); + snippet.convert('node'); expect(snippet).toHaveProperty('requests'); expect(Array.isArray(snippet.requests)).toBeTruthy(); @@ -136,18 +136,18 @@ describe('HTTPSnippet', () => { ] as { expected: string; input: keyof typeof mimetypes; - }[])('mimetype conversion of $input to $output', async ({ input, expected }) => { + }[])('mimetype conversion of $input to $output', ({ input, expected }) => { const snippet = new HTTPSnippet(mimetypes[input]); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.postData.mimeType).toStrictEqual(expected); }); }); - it('should set postData.text to empty string when postData.params is undefined in application/x-www-form-urlencoded', async () => { + it('should set postData.text to empty string when postData.params is undefined in application/x-www-form-urlencoded', () => { const snippet = new HTTPSnippet(mimetypes['application/x-www-form-urlencoded']); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.postData.text).toBe(''); @@ -155,9 +155,9 @@ describe('HTTPSnippet', () => { describe('requestExtras', () => { describe('uriObj', () => { - it('should add uriObj', async () => { + it('should add uriObj', () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; @@ -181,9 +181,9 @@ describe('HTTPSnippet', () => { }); }); - it('should fix the `path` property of uriObj to match queryString', async () => { + it('should fix the `path` property of uriObj to match queryString', () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.uriObj.path).toBe('/anything?foo=bar&foo=baz&baz=abc&key=value'); @@ -191,9 +191,9 @@ describe('HTTPSnippet', () => { }); describe('queryObj', () => { - it('should add queryObj', async () => { + it('should add queryObj', () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.queryObj).toMatchObject({ baz: 'abc', key: 'value', foo: ['bar', 'baz'] }); @@ -201,9 +201,9 @@ describe('HTTPSnippet', () => { }); describe('headersObj', () => { - it('should add headersObj', async () => { + it('should add headersObj', () => { const snippet = new HTTPSnippet(headers.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.headersObj).toMatchObject({ @@ -212,7 +212,7 @@ describe('HTTPSnippet', () => { }); }); - it('should add headersObj to source object case insensitive when HTTP/1.0', async () => { + it('should add headersObj to source object case insensitive when HTTP/1.0', () => { const snippet = new HTTPSnippet({ ...headers.log.entries[0].request, httpVersion: 'HTTP/1.1', @@ -225,7 +225,7 @@ describe('HTTPSnippet', () => { ], } as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; @@ -236,7 +236,7 @@ describe('HTTPSnippet', () => { }); }); - it('should add headersObj to source object lowercased when HTTP/2.x', async () => { + it('should add headersObj to source object lowercased when HTTP/2.x', () => { const snippet = new HTTPSnippet({ ...headers.log.entries[0].request, httpVersion: 'HTTP/2', @@ -249,7 +249,7 @@ describe('HTTPSnippet', () => { ], } as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; @@ -262,9 +262,9 @@ describe('HTTPSnippet', () => { }); describe('url', () => { - it('should modify the original url to strip query string', async () => { + it('should modify the original url to strip query string', () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.url).toBe('https://httpbin.org/anything'); @@ -272,9 +272,9 @@ describe('HTTPSnippet', () => { }); describe('fullUrl', () => { - it('adds fullURL', async () => { + it('adds fullURL', () => { const snippet = new HTTPSnippet(query.log.entries[0].request as Request); - await snippet.convert('node'); + snippet.convert('node'); const request = snippet.requests[0]; expect(request.fullUrl).toBe('https://httpbin.org/anything?foo=bar&foo=baz&baz=abc&key=value'); diff --git a/src/index.ts b/src/index.ts index 763e94ac0..040507da3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ import type { UrlWithParsedQuery } from 'node:url'; import { format as urlFormat, parse as urlParse } from 'node:url'; -import formDataToString from 'formdata-to-string'; import { stringify as queryStringify } from 'qs'; import { getHeaderName } from './helpers/headers.js'; @@ -100,12 +99,10 @@ export class HTTPSnippet { } } - async init() { + init() { this.initCalled = true; - const promises: Promise[] = []; - - this.entries.forEach(({ request }) => { + this.requests = this.entries.map(({ request }) => { // add optional properties to make validation successful const req = { bodySize: 0, @@ -125,15 +122,13 @@ export class HTTPSnippet { req.postData.mimeType = 'application/octet-stream'; } - promises.push(this.prepare(req as HarRequest, this.options)); + return this.prepare(req as HarRequest, this.options); }); - this.requests = await Promise.all(promises); - return this; } - async prepare(harRequest: HarRequest, options: HTTPSnippetOptions) { + prepare(harRequest: HarRequest, options: HTTPSnippetOptions) { const request: Request = { ...harRequest, fullUrl: '', @@ -195,24 +190,36 @@ export class HTTPSnippet { request.postData.mimeType = 'multipart/form-data'; if (request.postData?.params) { - const form = new FormData(); + const boundary = '---011000010111000001101001'; // this is binary for "api" (easter egg) + const carraige = `${boundary}--`; + const rn = '\r\n'; - request.postData?.params.forEach(param => { + const payload = [`--${boundary}`]; + request.postData?.params.forEach((param, i) => { const name = param.name; const value = param.value || ''; const filename = param.fileName || null; const contentType = param.contentType || ''; if (filename) { - form.append(name, new Blob([value], { type: contentType }), filename); + payload.push(`Content-Disposition: form-data; name="${name}"; filename="${filename}"`); + payload.push(`Content-Type: ${contentType}`); } else { - form.append(name, value); + payload.push(`Content-Disposition: form-data; name="${name}"`); + } + + payload.push(''); + payload.push(value); + + if (i !== (request.postData.params as Param[]).length - 1) { + payload.push(`--${boundary}`); } }); - const boundary = '---011000010111000001101001'; // this is binary for "api" (easter egg) + payload.push(`--${carraige}`); + request.postData.boundary = boundary; - request.postData.text = await formDataToString(form, { boundary }); + request.postData.text = payload.join(rn); // Since headers are case-sensitive we need to see if there's an existing `Content-Type` header that we can override. const contentTypeHeader = getHeaderName(request.headersObj, 'content-type') || 'content-type'; @@ -305,9 +312,9 @@ export class HTTPSnippet { }; } - async convert(targetId: TargetId, clientId?: ClientId, options?: any) { + convert(targetId: TargetId, clientId?: ClientId, options?: any) { if (!this.initCalled) { - await this.init(); + this.init(); } if (!options && clientId) { diff --git a/src/targets/index.test.ts b/src/targets/index.test.ts index 4f3f66b28..e2e3d231d 100644 --- a/src/targets/index.test.ts +++ b/src/targets/index.test.ts @@ -64,7 +64,7 @@ describe('request validation', () => { (clientId, { extname: fixtureExtension }) => { it.each(fixtures.filter(testFilter(0, fixtureFilter)))( 'request should match fixture for "%s.js"', - async (fixture, request) => { + (fixture, request) => { const expectedPath = path.join( 'src', 'targets', @@ -88,7 +88,7 @@ describe('request validation', () => { expected = readFileSync(expectedPath).toString(); const snippet = new HTTPSnippet(request, options); - result = await snippet.convert(targetId, clientId); + result = snippet.convert(targetId, clientId); if (OVERWRITE_EVERYTHING && result) { writeFileSync(expectedPath, String(result)); @@ -295,7 +295,7 @@ describe('addTargetClient', () => { delete targets.node.clientsById.custom; }); - it('should add a new custom target', async () => { + it('should add a new custom target', () => { const customClient: Client = { info: { key: 'custom', @@ -313,7 +313,7 @@ describe('addTargetClient', () => { const snippet = new HTTPSnippet(short.log.entries[0].request as Request, {}); - const result = await snippet.convert('node', 'custom'); + const result = snippet.convert('node', 'custom'); expect(result).toBe('This was generated from a custom client.'); }); @@ -324,7 +324,7 @@ describe('addClientPlugin', () => { delete targets.node.clientsById.custom; }); - it('should add a new custom target', async () => { + it('should add a new custom target', () => { const customPlugin: ClientPlugin = { target: 'node', client: { @@ -345,7 +345,7 @@ describe('addClientPlugin', () => { const snippet = new HTTPSnippet(short.log.entries[0].request as Request, {}); - const result = await snippet.convert('node', 'custom'); + const result = snippet.convert('node', 'custom'); expect(result).toBe('This was generated from a custom client.'); });