got 8.2.0 → 9.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/package.json +17 -41
- package/readme.md +263 -57
- package/source/as-promise.js +112 -0
- package/source/as-stream.js +115 -0
- package/source/create.js +56 -0
- package/source/deep-freeze.js +12 -0
- package/{errors.js → source/errors.js} +9 -0
- package/source/get-body-size.js +40 -0
- package/source/get-response.js +60 -0
- package/source/index.js +49 -0
- package/source/is-form-data.js +4 -0
- package/source/is-retry-on-network-error-allowed.js +17 -0
- package/source/merge.js +32 -0
- package/source/normalize-arguments.js +230 -0
- package/source/progress.js +71 -0
- package/source/request-as-event-emitter.js +185 -0
- package/source/timed-out.js +160 -0
- package/source/url-to-options.js +25 -0
- package/index.js +0 -622
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const EventEmitter = require('events');
|
|
3
|
+
const getStream = require('get-stream');
|
|
4
|
+
const is = require('@sindresorhus/is');
|
|
5
|
+
const PCancelable = require('p-cancelable');
|
|
6
|
+
const requestAsEventEmitter = require('./request-as-event-emitter');
|
|
7
|
+
const {HTTPError, ParseError, ReadError} = require('./errors');
|
|
8
|
+
|
|
9
|
+
module.exports = options => {
|
|
10
|
+
const proxy = new EventEmitter();
|
|
11
|
+
|
|
12
|
+
const cancelable = new PCancelable((resolve, reject, onCancel) => {
|
|
13
|
+
const emitter = requestAsEventEmitter(options);
|
|
14
|
+
let cancelOnRequest = false;
|
|
15
|
+
|
|
16
|
+
onCancel(() => {
|
|
17
|
+
cancelOnRequest = true;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
emitter.on('request', request => {
|
|
21
|
+
if (cancelOnRequest) {
|
|
22
|
+
request.abort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
proxy.emit('request', request);
|
|
26
|
+
|
|
27
|
+
const uploadComplete = () => {
|
|
28
|
+
request.emit('upload-complete');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
onCancel(() => {
|
|
32
|
+
request.abort();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (is.nodeStream(options.body)) {
|
|
36
|
+
options.body.once('end', uploadComplete);
|
|
37
|
+
options.body.pipe(request);
|
|
38
|
+
options.body = undefined;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
request.end(options.body, uploadComplete);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
emitter.on('response', async response => {
|
|
46
|
+
proxy.emit('response', response);
|
|
47
|
+
|
|
48
|
+
const stream = is.null(options.encoding) ? getStream.buffer(response) : getStream(response, options);
|
|
49
|
+
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = await stream;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
reject(new ReadError(error, options));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {statusCode} = response;
|
|
59
|
+
const limitStatusCode = options.followRedirect ? 299 : 399;
|
|
60
|
+
|
|
61
|
+
response.body = data;
|
|
62
|
+
|
|
63
|
+
if (options.json && response.body) {
|
|
64
|
+
try {
|
|
65
|
+
response.body = JSON.parse(response.body);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
68
|
+
const parseError = new ParseError(error, statusCode, options, data);
|
|
69
|
+
Object.defineProperty(parseError, 'response', {value: response});
|
|
70
|
+
reject(parseError);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
|
|
76
|
+
const error = new HTTPError(statusCode, response.statusMessage, response.headers, options);
|
|
77
|
+
Object.defineProperty(error, 'response', {value: response});
|
|
78
|
+
emitter.emit('retry', error, retried => {
|
|
79
|
+
if (!retried) {
|
|
80
|
+
if (options.throwHttpErrors) {
|
|
81
|
+
reject(error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resolve(response);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resolve(response);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
emitter.once('error', reject);
|
|
95
|
+
[
|
|
96
|
+
'redirect',
|
|
97
|
+
'uploadProgress',
|
|
98
|
+
'downloadProgress'
|
|
99
|
+
].forEach(event => emitter.on(event, (...args) => proxy.emit(event, ...args)));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const promise = cancelable;
|
|
103
|
+
|
|
104
|
+
promise.cancel = cancelable.cancel.bind(cancelable);
|
|
105
|
+
|
|
106
|
+
promise.on = (name, fn) => {
|
|
107
|
+
proxy.on(name, fn);
|
|
108
|
+
return promise;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return promise;
|
|
112
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {PassThrough} = require('stream');
|
|
3
|
+
const duplexer3 = require('duplexer3');
|
|
4
|
+
const is = require('@sindresorhus/is');
|
|
5
|
+
const requestAsEventEmitter = require('./request-as-event-emitter');
|
|
6
|
+
const {HTTPError, ReadError} = require('./errors');
|
|
7
|
+
|
|
8
|
+
module.exports = options => {
|
|
9
|
+
const input = new PassThrough();
|
|
10
|
+
const output = new PassThrough();
|
|
11
|
+
const proxy = duplexer3(input, output);
|
|
12
|
+
const piped = new Set();
|
|
13
|
+
let isFinished = false;
|
|
14
|
+
|
|
15
|
+
options.gotRetry.retries = () => 0;
|
|
16
|
+
|
|
17
|
+
if (options.body) {
|
|
18
|
+
proxy.write = () => {
|
|
19
|
+
throw new Error('Got\'s stream is not writable when the `body` option is used');
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const emitter = requestAsEventEmitter(options);
|
|
24
|
+
|
|
25
|
+
emitter.on('request', request => {
|
|
26
|
+
proxy.emit('request', request);
|
|
27
|
+
const uploadComplete = () => {
|
|
28
|
+
request.emit('upload-complete');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (is.nodeStream(options.body)) {
|
|
32
|
+
options.body.once('end', uploadComplete);
|
|
33
|
+
options.body.pipe(request);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.body) {
|
|
38
|
+
request.end(options.body, uploadComplete);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') {
|
|
43
|
+
input.once('end', uploadComplete);
|
|
44
|
+
input.pipe(request);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
request.end(uploadComplete);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
emitter.on('response', response => {
|
|
52
|
+
const {statusCode} = response;
|
|
53
|
+
|
|
54
|
+
response.on('error', error => {
|
|
55
|
+
proxy.emit('error', new ReadError(error, options));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (options.throwHttpErrors && statusCode !== 304 && (statusCode < 200 || statusCode > 299)) {
|
|
59
|
+
proxy.emit('error', new HTTPError(statusCode, response.statusMessage, response.headers, options), null, response);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
isFinished = true;
|
|
64
|
+
|
|
65
|
+
response.pipe(output);
|
|
66
|
+
|
|
67
|
+
for (const destination of piped) {
|
|
68
|
+
if (destination.headersSent) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
73
|
+
// Got gives *uncompressed* data. Overriding `content-encoding` header would result in an error.
|
|
74
|
+
// It's not possible to decompress uncompressed data, is it?
|
|
75
|
+
const allowed = options.decompress ? key !== 'content-encoding' : true;
|
|
76
|
+
if (allowed) {
|
|
77
|
+
destination.setHeader(key, value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
destination.statusCode = response.statusCode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
proxy.emit('response', response);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
[
|
|
88
|
+
'error',
|
|
89
|
+
'redirect',
|
|
90
|
+
'uploadProgress',
|
|
91
|
+
'downloadProgress'
|
|
92
|
+
].forEach(event => emitter.on(event, (...args) => proxy.emit(event, ...args)));
|
|
93
|
+
|
|
94
|
+
const pipe = proxy.pipe.bind(proxy);
|
|
95
|
+
const unpipe = proxy.unpipe.bind(proxy);
|
|
96
|
+
proxy.pipe = (destination, options) => {
|
|
97
|
+
if (isFinished) {
|
|
98
|
+
throw new Error('Failed to pipe. The response has been emitted already.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = pipe(destination, options);
|
|
102
|
+
|
|
103
|
+
if (Reflect.has(destination, 'setHeader')) {
|
|
104
|
+
piped.add(destination);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
proxy.unpipe = stream => {
|
|
110
|
+
piped.delete(stream);
|
|
111
|
+
return unpipe(stream);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return proxy;
|
|
115
|
+
};
|
package/source/create.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const errors = require('./errors');
|
|
3
|
+
const asStream = require('./as-stream');
|
|
4
|
+
const asPromise = require('./as-promise');
|
|
5
|
+
const normalizeArguments = require('./normalize-arguments');
|
|
6
|
+
const merge = require('./merge');
|
|
7
|
+
const deepFreeze = require('./deep-freeze');
|
|
8
|
+
|
|
9
|
+
const next = options => options.stream ? asStream(options) : asPromise(options);
|
|
10
|
+
const mergeOptions = (defaults, options = {}) => merge({}, defaults, options);
|
|
11
|
+
|
|
12
|
+
const create = defaults => {
|
|
13
|
+
defaults = merge({}, defaults);
|
|
14
|
+
if (!defaults.handler) {
|
|
15
|
+
defaults.handler = next;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function got(url, options) {
|
|
19
|
+
try {
|
|
20
|
+
options = mergeOptions(defaults.options, options);
|
|
21
|
+
return defaults.handler(normalizeArguments(url, options, defaults), next);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return Promise.reject(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
got.create = create;
|
|
28
|
+
got.extend = (options = {}) => create({
|
|
29
|
+
options: mergeOptions(defaults.options, options),
|
|
30
|
+
methods: defaults.methods,
|
|
31
|
+
handler: defaults.handler
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
got.stream = (url, options) => {
|
|
35
|
+
options = mergeOptions(defaults.options, options);
|
|
36
|
+
options.stream = true;
|
|
37
|
+
return defaults.handler(normalizeArguments(url, options, defaults), next);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (const method of defaults.methods) {
|
|
41
|
+
got[method] = (url, options) => got(url, {...options, method});
|
|
42
|
+
got.stream[method] = (url, options) => got.stream(url, {...options, method});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Object.assign(got, {...errors, mergeOptions});
|
|
46
|
+
Object.defineProperty(got, 'defaults', {
|
|
47
|
+
value: deepFreeze(defaults),
|
|
48
|
+
writable: false,
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return got;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = create;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const is = require('@sindresorhus/is');
|
|
3
|
+
|
|
4
|
+
module.exports = function deepFreeze(object) {
|
|
5
|
+
for (const [key, value] of Object.entries(object)) {
|
|
6
|
+
if (is.object(value)) {
|
|
7
|
+
deepFreeze(object[key]);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return Object.freeze(object);
|
|
12
|
+
};
|
|
@@ -19,6 +19,7 @@ class GotError extends Error {
|
|
|
19
19
|
hostname: opts.hostname,
|
|
20
20
|
method: opts.method,
|
|
21
21
|
path: opts.path,
|
|
22
|
+
socketPath: opts.socketPath,
|
|
22
23
|
protocol: opts.protocol,
|
|
23
24
|
url: opts.href
|
|
24
25
|
});
|
|
@@ -89,4 +90,12 @@ module.exports.UnsupportedProtocolError = class extends GotError {
|
|
|
89
90
|
}
|
|
90
91
|
};
|
|
91
92
|
|
|
93
|
+
module.exports.TimeoutError = class extends GotError {
|
|
94
|
+
constructor(threshold, event, opts) {
|
|
95
|
+
super(`Timeout awaiting '${event}' for ${threshold}ms`, {code: 'ETIMEDOUT'}, opts);
|
|
96
|
+
this.name = 'TimeoutError';
|
|
97
|
+
this.event = event;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
92
101
|
module.exports.CancelError = PCancelable.CancelError;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const util = require('util');
|
|
4
|
+
const is = require('@sindresorhus/is');
|
|
5
|
+
const isFormData = require('./is-form-data');
|
|
6
|
+
|
|
7
|
+
module.exports = async options => {
|
|
8
|
+
const {body} = options;
|
|
9
|
+
|
|
10
|
+
if (options.headers['content-length']) {
|
|
11
|
+
return Number(options.headers['content-length']);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!body && !options.stream) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (is.string(body)) {
|
|
19
|
+
return Buffer.byteLength(body);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (is.buffer(body)) {
|
|
23
|
+
return body.length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isFormData(body)) {
|
|
27
|
+
return util.promisify(body.getLength.bind(body))();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (body instanceof fs.ReadStream) {
|
|
31
|
+
const {size} = await util.promisify(fs.stat)(body.path);
|
|
32
|
+
return size;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (is.nodeStream(body) && is.buffer(body._buffer)) {
|
|
36
|
+
return body._buffer.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {Transform} = require('stream');
|
|
3
|
+
const decompressResponse = require('decompress-response');
|
|
4
|
+
const is = require('@sindresorhus/is');
|
|
5
|
+
const mimicResponse = require('mimic-response');
|
|
6
|
+
|
|
7
|
+
module.exports = (response, options, emitter, redirects) => {
|
|
8
|
+
const downloadBodySize = Number(response.headers['content-length']) || null;
|
|
9
|
+
let downloaded = 0;
|
|
10
|
+
|
|
11
|
+
const progressStream = new Transform({
|
|
12
|
+
transform(chunk, encoding, callback) {
|
|
13
|
+
downloaded += chunk.length;
|
|
14
|
+
|
|
15
|
+
const percent = downloadBodySize ? downloaded / downloadBodySize : 0;
|
|
16
|
+
|
|
17
|
+
// Let `flush()` be responsible for emitting the last event
|
|
18
|
+
if (percent < 1) {
|
|
19
|
+
emitter.emit('downloadProgress', {
|
|
20
|
+
percent,
|
|
21
|
+
transferred: downloaded,
|
|
22
|
+
total: downloadBodySize
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
callback(null, chunk);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
flush(callback) {
|
|
30
|
+
emitter.emit('downloadProgress', {
|
|
31
|
+
percent: 1,
|
|
32
|
+
transferred: downloaded,
|
|
33
|
+
total: downloadBodySize
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
callback();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
mimicResponse(response, progressStream);
|
|
41
|
+
progressStream.redirectUrls = redirects;
|
|
42
|
+
|
|
43
|
+
const newResponse = options.decompress === true &&
|
|
44
|
+
is.function(decompressResponse) &&
|
|
45
|
+
options.method !== 'HEAD' ? decompressResponse(progressStream) : progressStream;
|
|
46
|
+
|
|
47
|
+
if (!options.decompress && ['gzip', 'deflate'].includes(response.headers['content-encoding'])) {
|
|
48
|
+
options.encoding = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
emitter.emit('response', newResponse);
|
|
52
|
+
|
|
53
|
+
emitter.emit('downloadProgress', {
|
|
54
|
+
percent: 0,
|
|
55
|
+
transferred: 0,
|
|
56
|
+
total: downloadBodySize
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
response.pipe(progressStream);
|
|
60
|
+
};
|
package/source/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const pkg = require('../package.json');
|
|
3
|
+
const create = require('./create');
|
|
4
|
+
|
|
5
|
+
const defaults = {
|
|
6
|
+
methods: [
|
|
7
|
+
'get',
|
|
8
|
+
'post',
|
|
9
|
+
'put',
|
|
10
|
+
'patch',
|
|
11
|
+
'head',
|
|
12
|
+
'delete'
|
|
13
|
+
],
|
|
14
|
+
options: {
|
|
15
|
+
retry: {
|
|
16
|
+
retries: 2,
|
|
17
|
+
methods: [
|
|
18
|
+
'get',
|
|
19
|
+
'put',
|
|
20
|
+
'head',
|
|
21
|
+
'delete',
|
|
22
|
+
'options',
|
|
23
|
+
'trace'
|
|
24
|
+
],
|
|
25
|
+
statusCodes: [
|
|
26
|
+
408,
|
|
27
|
+
413,
|
|
28
|
+
429,
|
|
29
|
+
502,
|
|
30
|
+
503,
|
|
31
|
+
504
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
cache: false,
|
|
35
|
+
decompress: true,
|
|
36
|
+
useElectronNet: false,
|
|
37
|
+
throwHttpErrors: true,
|
|
38
|
+
headers: {
|
|
39
|
+
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
|
|
40
|
+
},
|
|
41
|
+
hooks: {
|
|
42
|
+
beforeRequest: []
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const got = create(defaults);
|
|
48
|
+
|
|
49
|
+
module.exports = got;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WHITELIST = new Set([
|
|
4
|
+
'ETIMEDOUT',
|
|
5
|
+
'ECONNRESET',
|
|
6
|
+
'EADDRINUSE',
|
|
7
|
+
'ECONNREFUSED',
|
|
8
|
+
'EPIPE'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
module.exports = error => {
|
|
12
|
+
if (error && WHITELIST.has(error.code)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return false;
|
|
17
|
+
};
|
package/source/merge.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {URL} = require('url');
|
|
3
|
+
const is = require('@sindresorhus/is');
|
|
4
|
+
|
|
5
|
+
const merge = (target, ...sources) => {
|
|
6
|
+
for (const source of sources) {
|
|
7
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
8
|
+
if (is.undefined(sourceValue)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const targetValue = target[key];
|
|
13
|
+
if (is.urlInstance(targetValue) && (is.urlInstance(sourceValue) || is.string(sourceValue))) {
|
|
14
|
+
target[key] = new URL(sourceValue, targetValue);
|
|
15
|
+
} else if (is.plainObject(sourceValue)) {
|
|
16
|
+
if (is.plainObject(targetValue)) {
|
|
17
|
+
target[key] = merge({}, targetValue, sourceValue);
|
|
18
|
+
} else {
|
|
19
|
+
target[key] = merge({}, sourceValue);
|
|
20
|
+
}
|
|
21
|
+
} else if (is.array(sourceValue)) {
|
|
22
|
+
target[key] = merge([], sourceValue);
|
|
23
|
+
} else {
|
|
24
|
+
target[key] = sourceValue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports = merge;
|