fetch-har 7.0.0 → 8.0.2
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/CHANGELOG.md +32 -0
- package/README.md +35 -10
- package/dist/index.d.ts +10 -0
- package/dist/index.js +304 -0
- package/example.js +2 -3
- package/package.json +24 -10
- package/{index.js → src/index.ts} +61 -40
- package/tsconfig.json +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## <small>8.0.2 (2022-05-20)</small>
|
|
2
|
+
|
|
3
|
+
* fix: don't look for a length on `postData.text` if it's undefined (#272) ([cd70c2f](https://github.com/readmeio/fetch-har/commit/cd70c2f)), closes [#272](https://github.com/readmeio/fetch-har/issues/272)
|
|
4
|
+
* fix: pin to node 18.0 (#273) ([9ab5991](https://github.com/readmeio/fetch-har/commit/9ab5991)), closes [#273](https://github.com/readmeio/fetch-har/issues/273)
|
|
5
|
+
* ci: pin ci to node 18.0 ([6ce62ac](https://github.com/readmeio/fetch-har/commit/6ce62ac))
|
|
6
|
+
* ci: pin to node 18.1 because 18.2 has broken cookie header support ([05748b4](https://github.com/readmeio/fetch-har/commit/05748b4))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## <small>8.0.1 (2022-05-03)</small>
|
|
11
|
+
|
|
12
|
+
* fix: making har-format types a regular dep ([93dcb7d](https://github.com/readmeio/fetch-har/commit/93dcb7d))
|
|
13
|
+
* chore: don't remove a `@types/` directory that we aren't using ([c915fae](https://github.com/readmeio/fetch-har/commit/c915fae))
|
|
14
|
+
* chore(deps-dev): bump @types/node from 17.0.29 to 17.0.30 (#269) ([e6f62f3](https://github.com/readmeio/fetch-har/commit/e6f62f3)), closes [#269](https://github.com/readmeio/fetch-har/issues/269)
|
|
15
|
+
* chore(deps-dev): bump ts-loader from 7.0.5 to 9.3.0 (#268) ([34877c5](https://github.com/readmeio/fetch-har/commit/34877c5)), closes [#268](https://github.com/readmeio/fetch-har/issues/268)
|
|
16
|
+
* chore(deps-dev): bump typescript from 4.6.3 to 4.6.4 (#267) ([826dcac](https://github.com/readmeio/fetch-har/commit/826dcac)), closes [#267](https://github.com/readmeio/fetch-har/issues/267)
|
|
17
|
+
* chore(deps): bump actions/cache from 3.0.1 to 3.0.2 (#270) ([55e45a6](https://github.com/readmeio/fetch-har/commit/55e45a6)), closes [#270](https://github.com/readmeio/fetch-har/issues/270)
|
|
18
|
+
* chore(deps): bump github/codeql-action from 1 to 2 (#271) ([e29ddce](https://github.com/readmeio/fetch-har/commit/e29ddce)), closes [#271](https://github.com/readmeio/fetch-har/issues/271)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## 8.0.0 (2022-04-28)
|
|
23
|
+
|
|
24
|
+
> **BREAKING CHANGE**
|
|
25
|
+
>
|
|
26
|
+
> This library is now written in TypeScript and as a result the main export dist is now contained within a `default` object. If you load it into your codebase with `import` you don't need to do anything, but if you're using `require()` you'll need to do `require('fetch-har').default`.
|
|
27
|
+
|
|
28
|
+
* feat: adding test for common mocking libraries (#266) ([1cdb0bf](https://github.com/readmeio/fetch-har/commit/1cdb0bf)), closes [#266](https://github.com/readmeio/fetch-har/issues/266)
|
|
29
|
+
* feat: rewriting the library in typescript, adding support for node 18 (#265) ([209cb20](https://github.com/readmeio/fetch-har/commit/209cb20)), closes [#265](https://github.com/readmeio/fetch-har/issues/265)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
1
33
|
## 7.0.0 (2022-04-26)
|
|
2
34
|
|
|
3
35
|
> **BREAKING CHANGE**
|
package/README.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# fetch-har
|
|
2
|
-
[](https://github.com/readmeio/fetch-har)
|
|
3
|
-
|
|
4
2
|
Make a [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) request from a HAR definition.
|
|
5
3
|
|
|
4
|
+
[](https://github.com/readmeio/fetch-har/)
|
|
5
|
+
[](https://npm.im/fetch-har)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
6
8
|
[](https://readme.io)
|
|
7
9
|
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Supports Node 14+ (including the native `fetch` implementation in Node 18!).
|
|
13
|
+
- Natively works in all browsers that support [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) without having to use any polyfils.
|
|
14
|
+
- [Tested](https://github.com/readmeio/fetch-har/actions) across Chrome, Safari, Firefox on Mac, Windows, and Linux.
|
|
15
|
+
- Requests can be mocked with [nock](https://npm.im/nock) or [fetch-mock](https://npm.im/fetch-mock).
|
|
16
|
+
|
|
8
17
|
## Installation
|
|
9
18
|
|
|
10
19
|
```
|
|
@@ -15,10 +24,11 @@ npm install --save fetch-har
|
|
|
15
24
|
```js
|
|
16
25
|
require('isomorphic-fetch');
|
|
17
26
|
|
|
18
|
-
// If executing from an environment that doesn't normally provide `fetch()`
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
const
|
|
27
|
+
// If executing from an environment that doesn't normally provide `fetch()` we'll automatically
|
|
28
|
+
// polyfill in the `Blob`, `File`, and `FormData` APIs with the optional `formdata-node` package
|
|
29
|
+
// (provided you've installed it).
|
|
30
|
+
const fetchHAR = require('fetch-har').default;
|
|
31
|
+
// import fetchHAR from 'fetch-har'); // Or if you're in an ESM codebase.
|
|
22
32
|
|
|
23
33
|
const har = {
|
|
24
34
|
log: {
|
|
@@ -51,7 +61,7 @@ const har = {
|
|
|
51
61
|
},
|
|
52
62
|
};
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
fetchHAR(har)
|
|
55
65
|
.then(res => res.json())
|
|
56
66
|
.then(console.log);
|
|
57
67
|
```
|
|
@@ -68,14 +78,14 @@ Though we recommend either [formdata-node](https://npm.im/formdata-node) or [for
|
|
|
68
78
|
A custom `User-Agent` header to apply to your request. Please note that browsers have their own handling for these headers in `fetch()` calls so it may not work everywhere; it will always be sent in Node however.
|
|
69
79
|
|
|
70
80
|
```js
|
|
71
|
-
await
|
|
81
|
+
await fetchHAR(har, { userAgent: 'my-client/1.0' });
|
|
72
82
|
```
|
|
73
83
|
|
|
74
84
|
##### files
|
|
75
85
|
An optional object map you can supply to use for `multipart/form-data` file uploads in leu of relying on if the HAR you have has [data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). It supports Node file buffers and the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) API.
|
|
76
86
|
|
|
77
87
|
```js
|
|
78
|
-
await
|
|
88
|
+
await fetchHAR(har, { files: {
|
|
79
89
|
'owlbert.png': await fs.readFile('./owlbert.png'),
|
|
80
90
|
'file.txt': document.querySelector('#some-file-input').files[0],
|
|
81
91
|
} });
|
|
@@ -93,7 +103,22 @@ We recommend [form-data-encoder](https://npm.im/form-data-encoder).
|
|
|
93
103
|
```js
|
|
94
104
|
const { FormDataEncoder } = require('form-data-encoder');
|
|
95
105
|
|
|
96
|
-
await
|
|
106
|
+
await fetchHAR(har, { multipartEncoder: FormDataEncoder });
|
|
97
107
|
```
|
|
98
108
|
|
|
99
109
|
You do **not**, and shouldn't, need to use this option in browser environments.
|
|
110
|
+
|
|
111
|
+
##### init
|
|
112
|
+
This optional argument lets you supply any option that's available to supply to the [Request constructor](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request).
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
await fetchHAR(har, {
|
|
116
|
+
init: {
|
|
117
|
+
headers: new Headers({
|
|
118
|
+
'x-custom-header': 'buster',
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> ❗ Note that if you supply `body` or `credentials` to this option they may be overridden by what your HAR requires.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { Har } from 'har-format';
|
|
3
|
+
declare type FetchHAROptions = {
|
|
4
|
+
userAgent?: string;
|
|
5
|
+
files?: Record<string, Blob | Buffer>;
|
|
6
|
+
multipartEncoder?: any;
|
|
7
|
+
init?: RequestInit;
|
|
8
|
+
};
|
|
9
|
+
export default function fetchHAR(har: Har, opts?: FetchHAROptions): Promise<Response>;
|
|
10
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
exports.__esModule = true;
|
|
17
|
+
var readable_stream_1 = require("readable-stream");
|
|
18
|
+
var parse_data_url_1 = __importDefault(require("parse-data-url"));
|
|
19
|
+
if (!globalThis.Blob) {
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
22
|
+
globalThis.Blob = require('formdata-node').Blob;
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
throw new Error('Since you do not have the Blob API available in this environment you must install the optional `formdata-node` dependency.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!globalThis.File) {
|
|
29
|
+
try {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
31
|
+
globalThis.File = require('formdata-node').File;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
throw new Error('Since you do not have the File API available in this environment you must install the optional `formdata-node` dependency.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!globalThis.FormData) {
|
|
38
|
+
try {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
40
|
+
globalThis.FormData = require('formdata-node').FormData;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
throw new Error('Since you do not have the FormData API available in this environment you must install the optional `formdata-node` dependency.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isBrowser() {
|
|
47
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
48
|
+
}
|
|
49
|
+
function isBuffer(value) {
|
|
50
|
+
return typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
|
|
51
|
+
}
|
|
52
|
+
function isFile(value) {
|
|
53
|
+
if (value instanceof File) {
|
|
54
|
+
// The `Blob` polyfill on Node comes back as being an instanceof `File`. Because passing a Blob into
|
|
55
|
+
// a File will end up with a corrupted file we want to prevent this.
|
|
56
|
+
//
|
|
57
|
+
// This object identity crisis does not happen in the browser.
|
|
58
|
+
return value.constructor.name === 'File';
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @license MIT
|
|
64
|
+
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFunction.ts}
|
|
65
|
+
*/
|
|
66
|
+
function isFunction(value) {
|
|
67
|
+
return typeof value === 'function';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* We're loading this library in here instead of loading it from `form-data-encoder` because that uses lookbehind
|
|
71
|
+
* regex in its main encoder that Safari doesn't support so it throws a fatal page exception.
|
|
72
|
+
*
|
|
73
|
+
* @license MIT
|
|
74
|
+
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFormData.ts}
|
|
75
|
+
*/
|
|
76
|
+
function isFormData(value) {
|
|
77
|
+
return (value &&
|
|
78
|
+
isFunction(value.constructor) &&
|
|
79
|
+
value[Symbol.toStringTag] === 'FormData' &&
|
|
80
|
+
isFunction(value.append) &&
|
|
81
|
+
isFunction(value.getAll) &&
|
|
82
|
+
isFunction(value.entries) &&
|
|
83
|
+
isFunction(value[Symbol.iterator]));
|
|
84
|
+
}
|
|
85
|
+
function fetchHAR(har, opts) {
|
|
86
|
+
var _a;
|
|
87
|
+
if (opts === void 0) { opts = {}; }
|
|
88
|
+
if (!har)
|
|
89
|
+
throw new Error('Missing HAR definition');
|
|
90
|
+
if (!har.log || !har.log.entries || !har.log.entries.length)
|
|
91
|
+
throw new Error('Missing log.entries array');
|
|
92
|
+
var request = har.log.entries[0].request;
|
|
93
|
+
var url = request.url;
|
|
94
|
+
var querystring = '';
|
|
95
|
+
var options = __assign(__assign({}, (opts.init ? opts.init : {})), { method: request.method });
|
|
96
|
+
if (!options.headers) {
|
|
97
|
+
options.headers = new Headers();
|
|
98
|
+
}
|
|
99
|
+
else if (typeof options.headers === 'object' && !(options.headers instanceof Headers) && options.headers !== null) {
|
|
100
|
+
options.headers = new Headers(options.headers);
|
|
101
|
+
}
|
|
102
|
+
var headers = options.headers;
|
|
103
|
+
if ('headers' in request && request.headers.length) {
|
|
104
|
+
// eslint-disable-next-line consistent-return
|
|
105
|
+
request.headers.forEach(function (header) {
|
|
106
|
+
try {
|
|
107
|
+
return headers.append(header.name, header.value);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// `Headers.append()` will throw errors if the header name is not a legal HTTP header name, like
|
|
111
|
+
// `X-API-KEY (Header)`. If that happens instead of tossing the error back out, we should silently just ignore
|
|
112
|
+
// it.
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if ('cookies' in request && request.cookies.length) {
|
|
117
|
+
// As the browser fetch API can't set custom cookies for requests, they instead need to be defined on the document
|
|
118
|
+
// and passed into the request via `credentials: include`. Since this is a browser-specific quirk, that should only
|
|
119
|
+
// happen in browsers!
|
|
120
|
+
if (isBrowser()) {
|
|
121
|
+
request.cookies.forEach(function (cookie) {
|
|
122
|
+
document.cookie = "".concat(encodeURIComponent(cookie.name), "=").concat(encodeURIComponent(cookie.value));
|
|
123
|
+
});
|
|
124
|
+
options.credentials = 'include';
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
headers.append('cookie', request.cookies
|
|
128
|
+
.map(function (cookie) { return "".concat(encodeURIComponent(cookie.name), "=").concat(encodeURIComponent(cookie.value)); })
|
|
129
|
+
.join('; '));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ('postData' in request) {
|
|
133
|
+
if ('params' in request.postData) {
|
|
134
|
+
if (!('mimeType' in request.postData)) {
|
|
135
|
+
// @ts-expect-error HAR spec requires that `mimeType` is always present but it might not be.
|
|
136
|
+
request.postData.mimeType = 'application/octet-stream';
|
|
137
|
+
}
|
|
138
|
+
switch (request.postData.mimeType) {
|
|
139
|
+
case 'application/x-www-form-urlencoded':
|
|
140
|
+
// Since the content we're handling here is to be encoded as `application/x-www-form-urlencoded`, this should
|
|
141
|
+
// override any other Content-Type headers that are present in the HAR. This is how Postman handles this case
|
|
142
|
+
// when building code snippets!
|
|
143
|
+
//
|
|
144
|
+
// https://github.com/github/fetch/issues/263#issuecomment-209530977
|
|
145
|
+
headers.set('Content-Type', request.postData.mimeType);
|
|
146
|
+
var encodedParams_1 = new URLSearchParams();
|
|
147
|
+
request.postData.params.forEach(function (param) { return encodedParams_1.set(param.name, param.value); });
|
|
148
|
+
options.body = encodedParams_1.toString();
|
|
149
|
+
break;
|
|
150
|
+
case 'multipart/alternative':
|
|
151
|
+
case 'multipart/form-data':
|
|
152
|
+
case 'multipart/mixed':
|
|
153
|
+
case 'multipart/related':
|
|
154
|
+
// If there's a Content-Type header set remove it. We're doing this because when we pass the form data object
|
|
155
|
+
// into `fetch` that'll set a proper `Content-Type` header for this request that also includes the boundary
|
|
156
|
+
// used on the content.
|
|
157
|
+
//
|
|
158
|
+
// If we don't do this, then consumers won't be able to parse out the payload because they won't know what
|
|
159
|
+
// the boundary to split on it.
|
|
160
|
+
if (headers.has('Content-Type')) {
|
|
161
|
+
headers["delete"]('Content-Type');
|
|
162
|
+
}
|
|
163
|
+
var form_1 = new FormData();
|
|
164
|
+
if (!isFormData(form_1)) {
|
|
165
|
+
// The `form-data` NPM module returns one of two things: a native `FormData` API or its own polyfill.
|
|
166
|
+
// Unfortunately this polyfill does not support the full API of the native FormData object so when you load
|
|
167
|
+
// `form-data` within a browser environment you'll have two major differences in API:
|
|
168
|
+
//
|
|
169
|
+
// * The `.append()` API in `form-data` requires that the third argument is an object containing various,
|
|
170
|
+
// undocumented, options. In the browser, `.append()`'s third argument should only be present when the
|
|
171
|
+
// second is a `Blob` or `USVString`, and when it is present, it should be a filename string.
|
|
172
|
+
// * `form-data` does not expose an `.entries()` API, so the only way to retrieve data out of it for
|
|
173
|
+
// construction of boundary-separated payload content is to use its `.pipe()` API. Since the browser
|
|
174
|
+
// doesn't have this API, you'll be unable to retrieve data out of it.
|
|
175
|
+
//
|
|
176
|
+
// Now since the native `FormData` API is iterable, and has the `.entries()` iterator, we can easily detect
|
|
177
|
+
// if we have a native copy of the FormData API. It's for all of these reasons that we're opting to hard
|
|
178
|
+
// crash here because supporting this non-compliant API is more trouble than its worth.
|
|
179
|
+
//
|
|
180
|
+
// https://github.com/form-data/form-data/issues/124
|
|
181
|
+
throw new Error("We've detected you're using a non-spec compliant FormData library. We recommend polyfilling FormData with https://npm.im/formdata-node");
|
|
182
|
+
}
|
|
183
|
+
request.postData.params.forEach(function (param) {
|
|
184
|
+
if ('fileName' in param) {
|
|
185
|
+
if (opts.files && param.fileName in opts.files) {
|
|
186
|
+
var fileContents = opts.files[param.fileName];
|
|
187
|
+
// If the file we've got available to us is a Buffer then we need to convert it so that the FormData
|
|
188
|
+
// API can use it.
|
|
189
|
+
if (isBuffer(fileContents)) {
|
|
190
|
+
form_1.set(param.name, new File([fileContents], param.fileName, {
|
|
191
|
+
type: param.contentType || null
|
|
192
|
+
}), param.fileName);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
else if (isFile(fileContents)) {
|
|
196
|
+
form_1.set(param.name, fileContents, param.fileName);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throw new TypeError('An unknown object has been supplied into the `files` config for use. We only support instances of the File API and Node Buffer objects.');
|
|
200
|
+
}
|
|
201
|
+
else if ('value' in param) {
|
|
202
|
+
var paramBlob = void 0;
|
|
203
|
+
var parsed = (0, parse_data_url_1["default"])(param.value);
|
|
204
|
+
if (parsed) {
|
|
205
|
+
// If we were able to parse out this data URL we don't need to transform its data into a buffer for
|
|
206
|
+
// `Blob` because that supports data URLs already.
|
|
207
|
+
paramBlob = new Blob([param.value], { type: parsed.contentType || param.contentType || null });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
paramBlob = new Blob([param.value], { type: param.contentType || null });
|
|
211
|
+
}
|
|
212
|
+
form_1.append(param.name, paramBlob, param.fileName);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
throw new Error("The supplied HAR has a postData parameter with `fileName`, but neither `value` content within the HAR or any file buffers were supplied with the `files` option. Since this library doesn't have access to the filesystem, it can't fetch that file.");
|
|
216
|
+
}
|
|
217
|
+
form_1.append(param.name, param.value);
|
|
218
|
+
});
|
|
219
|
+
// If a the `fetch` polyfill that's being used here doesn't have spec-compliant handling for the `FormData`
|
|
220
|
+
// API (like `node-fetch@2`), then you should pass in a handler (like the `form-data-encoder` library) to
|
|
221
|
+
// transform its contents into something that can be used with the `Request` object.
|
|
222
|
+
//
|
|
223
|
+
// https://www.npmjs.com/package/formdata-node
|
|
224
|
+
if (opts.multipartEncoder) {
|
|
225
|
+
// eslint-disable-next-line new-cap
|
|
226
|
+
var encoder_1 = new opts.multipartEncoder(form_1);
|
|
227
|
+
Object.keys(encoder_1.headers).forEach(function (header) {
|
|
228
|
+
headers.set(header, encoder_1.headers[header]);
|
|
229
|
+
});
|
|
230
|
+
// @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
|
|
231
|
+
options.body = readable_stream_1.Readable.from(encoder_1);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
options.body = form_1;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
default:
|
|
238
|
+
var formBody_1 = {};
|
|
239
|
+
request.postData.params.map(function (param) {
|
|
240
|
+
try {
|
|
241
|
+
formBody_1[param.name] = JSON.parse(param.value);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
formBody_1[param.name] = param.value;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
options.body = JSON.stringify(formBody_1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if ((_a = request.postData.text) === null || _a === void 0 ? void 0 : _a.length) {
|
|
252
|
+
// If we've got `files` map content present, and this post data content contains a valid data URL then we can
|
|
253
|
+
// substitute the payload with that file instead of the using data URL.
|
|
254
|
+
if (opts.files) {
|
|
255
|
+
var parsed = (0, parse_data_url_1["default"])(request.postData.text);
|
|
256
|
+
if (parsed) {
|
|
257
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.name) && parsed.name in opts.files) {
|
|
258
|
+
var fileContents = opts.files[parsed.name];
|
|
259
|
+
if (isBuffer(fileContents)) {
|
|
260
|
+
options.body = fileContents;
|
|
261
|
+
}
|
|
262
|
+
else if (isFile(fileContents)) {
|
|
263
|
+
// `Readable.from` isn't available in browsers but the browser `Request` object can handle `File` objects
|
|
264
|
+
// just fine without us having to mold it into shape.
|
|
265
|
+
if (isBrowser()) {
|
|
266
|
+
options.body = fileContents;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
|
|
270
|
+
options.body = readable_stream_1.Readable.from(fileContents.stream());
|
|
271
|
+
// Supplying a polyfilled `File` stream into `Request.body` doesn't automatically add `Content-Length`.
|
|
272
|
+
if (!headers.has('content-length')) {
|
|
273
|
+
headers.set('content-length', String(fileContents.size));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (typeof options.body === 'undefined') {
|
|
281
|
+
options.body = request.postData.text;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// We automaticaly assume that the HAR that we have already has query parameters encoded within it so we do **not**
|
|
286
|
+
// use the `URLSearchParams` API here for composing the query string.
|
|
287
|
+
if ('queryString' in request && request.queryString.length) {
|
|
288
|
+
var urlObj = new URL(url);
|
|
289
|
+
var queryParams_1 = Array.from(urlObj.searchParams).map(function (_a) {
|
|
290
|
+
var k = _a[0], v = _a[1];
|
|
291
|
+
return "".concat(k, "=").concat(v);
|
|
292
|
+
});
|
|
293
|
+
request.queryString.forEach(function (q) {
|
|
294
|
+
queryParams_1.push("".concat(q.name, "=").concat(q.value));
|
|
295
|
+
});
|
|
296
|
+
querystring = queryParams_1.join('&');
|
|
297
|
+
}
|
|
298
|
+
if (opts.userAgent) {
|
|
299
|
+
headers.append('User-Agent', opts.userAgent);
|
|
300
|
+
}
|
|
301
|
+
options.headers = headers;
|
|
302
|
+
return fetch("".concat(url.split('?')[0]).concat(querystring ? "?".concat(querystring) : ''), options);
|
|
303
|
+
}
|
|
304
|
+
exports["default"] = fetchHAR;
|
package/example.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/* eslint-disable import/no-extraneous-dependencies, no-console */
|
|
2
1
|
require('isomorphic-fetch');
|
|
3
2
|
|
|
4
3
|
// If executing from an environment that doesn't normally provide `fetch()`
|
|
5
4
|
// we'll automatically polyfill in the `Blob`, `File`, and `FormData` APIs
|
|
6
5
|
// with the optional `formdata-node` package (provided you've installed it).
|
|
7
|
-
const
|
|
6
|
+
const fetchHAR = require('.').default;
|
|
8
7
|
|
|
9
8
|
const har = {
|
|
10
9
|
log: {
|
|
@@ -37,6 +36,6 @@ const har = {
|
|
|
37
36
|
},
|
|
38
37
|
};
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
fetchHAR(har)
|
|
41
40
|
.then(res => res.json())
|
|
42
41
|
.then(console.log);
|
package/package.json
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fetch-har",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.2",
|
|
4
4
|
"description": "Make a fetch request from a HAR definition",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"engines": {
|
|
7
8
|
"node": ">=14"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"lint": "eslint . --ext .js,.ts",
|
|
13
|
+
"prebuild": "rm -rf dist/",
|
|
14
|
+
"prepack": "npm run build",
|
|
11
15
|
"pretest": "npm run lint",
|
|
12
|
-
"prettier": "prettier --list-different --write \"
|
|
16
|
+
"prettier": "prettier --list-different --write \"./**/**.{js,ts}\"",
|
|
13
17
|
"release": "npx conventional-changelog-cli -i CHANGELOG.md -s",
|
|
14
|
-
"serve": "node __tests__/server.js",
|
|
15
18
|
"test:browser": "karma start --single-run",
|
|
16
19
|
"test:browser:chrome": "karma start --browsers=Chrome --single-run=false",
|
|
17
20
|
"test:browser:debug": "karma start --single-run=false",
|
|
18
|
-
"test": "nyc mocha"
|
|
21
|
+
"test": "nyc mocha \"test/**/*.test.ts\""
|
|
19
22
|
},
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
},
|
|
28
31
|
"homepage": "https://github.com/readmeio/fetch-har#readme",
|
|
29
32
|
"dependencies": {
|
|
33
|
+
"@types/har-format": "^1.2.8",
|
|
30
34
|
"parse-data-url": "^4.0.1",
|
|
31
35
|
"readable-stream": "^3.6.0"
|
|
32
36
|
},
|
|
@@ -36,19 +40,29 @@
|
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@jsdevtools/host-environment": "^2.1.2",
|
|
38
42
|
"@jsdevtools/karma-config": "^3.1.7",
|
|
39
|
-
"@readme/eslint-config": "^8.7.
|
|
43
|
+
"@readme/eslint-config": "^8.7.3",
|
|
44
|
+
"@types/chai": "^4.3.1",
|
|
45
|
+
"@types/mocha": "^9.1.1",
|
|
46
|
+
"@types/node": "^17.0.29",
|
|
47
|
+
"@types/parse-data-url": "^3.0.0",
|
|
48
|
+
"@types/readable-stream": "^2.3.13",
|
|
40
49
|
"chai": "^4.3.4",
|
|
41
50
|
"eslint": "^8.14.0",
|
|
42
|
-
"
|
|
51
|
+
"fetch-mock": "^9.11.0",
|
|
43
52
|
"form-data": "^4.0.0",
|
|
44
53
|
"form-data-encoder": "^1.7.1",
|
|
45
54
|
"formdata-node": "^4.3.2",
|
|
46
|
-
"har-examples": "^3.1.
|
|
55
|
+
"har-examples": "^3.1.1",
|
|
47
56
|
"isomorphic-fetch": "^3.0.0",
|
|
48
57
|
"mocha": "^9.2.2",
|
|
58
|
+
"nock": "^13.2.4",
|
|
49
59
|
"node-fetch": "^2.6.0",
|
|
50
60
|
"nyc": "^15.1.0",
|
|
51
|
-
"prettier": "^2.6.2"
|
|
61
|
+
"prettier": "^2.6.2",
|
|
62
|
+
"ts-loader": "^9.3.0",
|
|
63
|
+
"ts-node": "^10.7.0",
|
|
64
|
+
"typescript": "^4.6.3",
|
|
65
|
+
"undici": "^5.0.0"
|
|
52
66
|
},
|
|
53
67
|
"browserslist": [
|
|
54
68
|
"last 2 versions"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { Har } from 'har-format';
|
|
2
|
+
import { Readable } from 'readable-stream';
|
|
3
|
+
import parseDataUrl from 'parse-data-url';
|
|
3
4
|
|
|
4
5
|
if (!globalThis.Blob) {
|
|
5
6
|
try {
|
|
6
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
7
8
|
globalThis.Blob = require('formdata-node').Blob;
|
|
8
9
|
} catch (e) {
|
|
9
10
|
throw new Error(
|
|
@@ -14,7 +15,7 @@ if (!globalThis.Blob) {
|
|
|
14
15
|
|
|
15
16
|
if (!globalThis.File) {
|
|
16
17
|
try {
|
|
17
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
18
19
|
globalThis.File = require('formdata-node').File;
|
|
19
20
|
} catch (e) {
|
|
20
21
|
throw new Error(
|
|
@@ -25,7 +26,7 @@ if (!globalThis.File) {
|
|
|
25
26
|
|
|
26
27
|
if (!globalThis.FormData) {
|
|
27
28
|
try {
|
|
28
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
|
|
29
30
|
globalThis.FormData = require('formdata-node').FormData;
|
|
30
31
|
} catch (e) {
|
|
31
32
|
throw new Error(
|
|
@@ -38,11 +39,11 @@ function isBrowser() {
|
|
|
38
39
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function isBuffer(value) {
|
|
42
|
+
function isBuffer(value: any) {
|
|
42
43
|
return typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function isFile(value) {
|
|
46
|
+
function isFile(value: any) {
|
|
46
47
|
if (value instanceof File) {
|
|
47
48
|
// The `Blob` polyfill on Node comes back as being an instanceof `File`. Because passing a Blob into
|
|
48
49
|
// a File will end up with a corrupted file we want to prevent this.
|
|
@@ -58,7 +59,7 @@ function isFile(value) {
|
|
|
58
59
|
* @license MIT
|
|
59
60
|
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFunction.ts}
|
|
60
61
|
*/
|
|
61
|
-
function isFunction(value) {
|
|
62
|
+
function isFunction(value: any) {
|
|
62
63
|
return typeof value === 'function';
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -69,19 +70,32 @@ function isFunction(value) {
|
|
|
69
70
|
* @license MIT
|
|
70
71
|
* @see {@link https://github.com/octet-stream/form-data-encoder/blob/master/lib/util/isFormData.ts}
|
|
71
72
|
*/
|
|
72
|
-
function isFormData(value) {
|
|
73
|
+
function isFormData(value: any) {
|
|
73
74
|
return (
|
|
74
75
|
value &&
|
|
75
76
|
isFunction(value.constructor) &&
|
|
76
|
-
value[Symbol.toStringTag] === 'FormData' &&
|
|
77
|
+
value[Symbol.toStringTag] === 'FormData' &&
|
|
77
78
|
isFunction(value.append) &&
|
|
78
79
|
isFunction(value.getAll) &&
|
|
79
80
|
isFunction(value.entries) &&
|
|
80
|
-
isFunction(value[Symbol.iterator])
|
|
81
|
+
isFunction(value[Symbol.iterator])
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
type FetchHAROptions = {
|
|
86
|
+
userAgent?: string;
|
|
87
|
+
files?: Record<string, Blob | Buffer>;
|
|
88
|
+
multipartEncoder?: any; // form-data-encoder
|
|
89
|
+
init?: RequestInit;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type DataURL = parseDataUrl.DataUrl & {
|
|
93
|
+
// `parse-data-url` doesn't explicitly support `name` in data URLs but if it's there it'll be
|
|
94
|
+
// returned back to us.
|
|
95
|
+
name?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default function fetchHAR(har: Har, opts: FetchHAROptions = {}) {
|
|
85
99
|
if (!har) throw new Error('Missing HAR definition');
|
|
86
100
|
if (!har.log || !har.log.entries || !har.log.entries.length) throw new Error('Missing log.entries array');
|
|
87
101
|
|
|
@@ -89,11 +103,20 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
89
103
|
const { url } = request;
|
|
90
104
|
let querystring = '';
|
|
91
105
|
|
|
92
|
-
const
|
|
93
|
-
|
|
106
|
+
const options: RequestInit = {
|
|
107
|
+
// If we have custom options for the `Request` API we need to add them in here now before we
|
|
108
|
+
// fill it in with everything we need from the HAR.
|
|
109
|
+
...(opts.init ? opts.init : {}),
|
|
94
110
|
method: request.method,
|
|
95
111
|
};
|
|
96
112
|
|
|
113
|
+
if (!options.headers) {
|
|
114
|
+
options.headers = new Headers();
|
|
115
|
+
} else if (typeof options.headers === 'object' && !(options.headers instanceof Headers) && options.headers !== null) {
|
|
116
|
+
options.headers = new Headers(options.headers);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const headers = options.headers as Headers;
|
|
97
120
|
if ('headers' in request && request.headers.length) {
|
|
98
121
|
// eslint-disable-next-line consistent-return
|
|
99
122
|
request.headers.forEach(header => {
|
|
@@ -130,6 +153,7 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
130
153
|
if ('postData' in request) {
|
|
131
154
|
if ('params' in request.postData) {
|
|
132
155
|
if (!('mimeType' in request.postData)) {
|
|
156
|
+
// @ts-expect-error HAR spec requires that `mimeType` is always present but it might not be.
|
|
133
157
|
request.postData.mimeType = 'application/octet-stream';
|
|
134
158
|
}
|
|
135
159
|
|
|
@@ -203,7 +227,7 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
203
227
|
|
|
204
228
|
return;
|
|
205
229
|
} else if (isFile(fileContents)) {
|
|
206
|
-
form.set(param.name, fileContents, param.fileName);
|
|
230
|
+
form.set(param.name, fileContents as Blob, param.fileName);
|
|
207
231
|
return;
|
|
208
232
|
}
|
|
209
233
|
|
|
@@ -245,6 +269,7 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
245
269
|
headers.set(header, encoder.headers[header]);
|
|
246
270
|
});
|
|
247
271
|
|
|
272
|
+
// @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
|
|
248
273
|
options.body = Readable.from(encoder);
|
|
249
274
|
} else {
|
|
250
275
|
options.body = form;
|
|
@@ -252,7 +277,7 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
252
277
|
break;
|
|
253
278
|
|
|
254
279
|
default:
|
|
255
|
-
const formBody = {};
|
|
280
|
+
const formBody: Record<string, unknown> = {};
|
|
256
281
|
request.postData.params.map(param => {
|
|
257
282
|
try {
|
|
258
283
|
formBody[param.name] = JSON.parse(param.value);
|
|
@@ -265,26 +290,29 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
265
290
|
|
|
266
291
|
options.body = JSON.stringify(formBody);
|
|
267
292
|
}
|
|
268
|
-
} else if (request.postData.text
|
|
293
|
+
} else if (request.postData.text?.length) {
|
|
269
294
|
// If we've got `files` map content present, and this post data content contains a valid data URL then we can
|
|
270
295
|
// substitute the payload with that file instead of the using data URL.
|
|
271
296
|
if (opts.files) {
|
|
272
|
-
const parsed = parseDataUrl(request.postData.text);
|
|
273
|
-
if (parsed
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
} else if (isFile(fileContents)) {
|
|
278
|
-
// `Readable.from` isn't available in browsers but the browser `Request` object can handle `File` objects
|
|
279
|
-
// just fine without us having to mold it into shape.
|
|
280
|
-
if (isBrowser()) {
|
|
297
|
+
const parsed = parseDataUrl(request.postData.text) as DataURL;
|
|
298
|
+
if (parsed) {
|
|
299
|
+
if (parsed?.name && parsed.name in opts.files) {
|
|
300
|
+
const fileContents = opts.files[parsed.name];
|
|
301
|
+
if (isBuffer(fileContents)) {
|
|
281
302
|
options.body = fileContents;
|
|
282
|
-
} else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
303
|
+
} else if (isFile(fileContents)) {
|
|
304
|
+
// `Readable.from` isn't available in browsers but the browser `Request` object can handle `File` objects
|
|
305
|
+
// just fine without us having to mold it into shape.
|
|
306
|
+
if (isBrowser()) {
|
|
307
|
+
options.body = fileContents;
|
|
308
|
+
} else {
|
|
309
|
+
// @ts-expect-error "Property 'from' does not exist on type 'typeof Readable'." but it does!
|
|
310
|
+
options.body = Readable.from((fileContents as File).stream());
|
|
311
|
+
|
|
312
|
+
// Supplying a polyfilled `File` stream into `Request.body` doesn't automatically add `Content-Length`.
|
|
313
|
+
if (!headers.has('content-length')) {
|
|
314
|
+
headers.set('content-length', String((fileContents as File).size));
|
|
315
|
+
}
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
}
|
|
@@ -316,12 +344,5 @@ function constructRequest(har, opts = { userAgent: false, files: false, multipar
|
|
|
316
344
|
|
|
317
345
|
options.headers = headers;
|
|
318
346
|
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function fetchHar(har, opts = { userAgent: false, files: false, multipartEncoder: false }) {
|
|
323
|
-
return fetch(constructRequest(har, opts));
|
|
347
|
+
return fetch(`${url.split('?')[0]}${querystring ? `?${querystring}` : ''}`, options);
|
|
324
348
|
}
|
|
325
|
-
|
|
326
|
-
module.exports = fetchHar;
|
|
327
|
-
module.exports.constructRequest = constructRequest;
|