@depup/mswjs__interceptors 0.41.3-depup.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/ClientRequest/package.json +11 -0
- package/LICENSE.md +9 -0
- package/README.md +31 -0
- package/RemoteHttpInterceptor/package.json +11 -0
- package/WebSocket/package.json +12 -0
- package/XMLHttpRequest/package.json +12 -0
- package/changes.json +10 -0
- package/fetch/package.json +12 -0
- package/lib/browser/Interceptor-Deczogc8.d.cts +65 -0
- package/lib/browser/Interceptor-gqKgs-aF.d.mts +65 -0
- package/lib/browser/XMLHttpRequest-BACqefB-.cjs +761 -0
- package/lib/browser/XMLHttpRequest-BACqefB-.cjs.map +1 -0
- package/lib/browser/XMLHttpRequest-BvxZV0WU.mjs +756 -0
- package/lib/browser/XMLHttpRequest-BvxZV0WU.mjs.map +1 -0
- package/lib/browser/bufferUtils-BiiO6HZv.mjs +20 -0
- package/lib/browser/bufferUtils-BiiO6HZv.mjs.map +1 -0
- package/lib/browser/bufferUtils-Uc0eRItL.cjs +38 -0
- package/lib/browser/bufferUtils-Uc0eRItL.cjs.map +1 -0
- package/lib/browser/createRequestId-Cs4oXfa1.cjs +205 -0
- package/lib/browser/createRequestId-Cs4oXfa1.cjs.map +1 -0
- package/lib/browser/createRequestId-DQcIlohW.mjs +170 -0
- package/lib/browser/createRequestId-DQcIlohW.mjs.map +1 -0
- package/lib/browser/fetch-DdKEdDOR.mjs +248 -0
- package/lib/browser/fetch-DdKEdDOR.mjs.map +1 -0
- package/lib/browser/fetch-U3v3Y4ap.cjs +253 -0
- package/lib/browser/fetch-U3v3Y4ap.cjs.map +1 -0
- package/lib/browser/getRawRequest-BTaNLFr0.mjs +218 -0
- package/lib/browser/getRawRequest-BTaNLFr0.mjs.map +1 -0
- package/lib/browser/getRawRequest-zx8rUJL2.cjs +259 -0
- package/lib/browser/getRawRequest-zx8rUJL2.cjs.map +1 -0
- package/lib/browser/glossary-BdLS4k1H.d.cts +70 -0
- package/lib/browser/glossary-DYwOrogs.d.mts +70 -0
- package/lib/browser/handleRequest-CvX2G-Lz.cjs +189 -0
- package/lib/browser/handleRequest-CvX2G-Lz.cjs.map +1 -0
- package/lib/browser/handleRequest-D7kpTI5U.mjs +178 -0
- package/lib/browser/handleRequest-D7kpTI5U.mjs.map +1 -0
- package/lib/browser/hasConfigurableGlobal-BvCTG97d.cjs +45 -0
- package/lib/browser/hasConfigurableGlobal-BvCTG97d.cjs.map +1 -0
- package/lib/browser/hasConfigurableGlobal-npXitu1-.mjs +33 -0
- package/lib/browser/hasConfigurableGlobal-npXitu1-.mjs.map +1 -0
- package/lib/browser/index.cjs +70 -0
- package/lib/browser/index.cjs.map +1 -0
- package/lib/browser/index.d.cts +96 -0
- package/lib/browser/index.d.mts +96 -0
- package/lib/browser/index.mjs +56 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/browser/interceptors/WebSocket/index.cjs +622 -0
- package/lib/browser/interceptors/WebSocket/index.cjs.map +1 -0
- package/lib/browser/interceptors/WebSocket/index.d.cts +277 -0
- package/lib/browser/interceptors/WebSocket/index.d.mts +277 -0
- package/lib/browser/interceptors/WebSocket/index.mjs +615 -0
- package/lib/browser/interceptors/WebSocket/index.mjs.map +1 -0
- package/lib/browser/interceptors/XMLHttpRequest/index.cjs +7 -0
- package/lib/browser/interceptors/XMLHttpRequest/index.d.cts +15 -0
- package/lib/browser/interceptors/XMLHttpRequest/index.d.mts +15 -0
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +7 -0
- package/lib/browser/interceptors/fetch/index.cjs +6 -0
- package/lib/browser/interceptors/fetch/index.d.cts +13 -0
- package/lib/browser/interceptors/fetch/index.d.mts +13 -0
- package/lib/browser/interceptors/fetch/index.mjs +6 -0
- package/lib/browser/presets/browser.cjs +17 -0
- package/lib/browser/presets/browser.cjs.map +1 -0
- package/lib/browser/presets/browser.d.cts +12 -0
- package/lib/browser/presets/browser.d.mts +14 -0
- package/lib/browser/presets/browser.mjs +17 -0
- package/lib/browser/presets/browser.mjs.map +1 -0
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs +31 -0
- package/lib/browser/resolveWebSocketUrl-6K6EgqsA.cjs.map +1 -0
- package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs +25 -0
- package/lib/browser/resolveWebSocketUrl-C83-x9iE.mjs.map +1 -0
- package/lib/node/BatchInterceptor-3LnAnLTx.cjs +49 -0
- package/lib/node/BatchInterceptor-3LnAnLTx.cjs.map +1 -0
- package/lib/node/BatchInterceptor-D7mXzHcQ.d.mts +26 -0
- package/lib/node/BatchInterceptor-DFaBPilf.mjs +44 -0
- package/lib/node/BatchInterceptor-DFaBPilf.mjs.map +1 -0
- package/lib/node/BatchInterceptor-D_YqR8qU.d.cts +26 -0
- package/lib/node/ClientRequest-2rDe54Ui.cjs +1043 -0
- package/lib/node/ClientRequest-2rDe54Ui.cjs.map +1 -0
- package/lib/node/ClientRequest-Ca8Qykuv.mjs +1034 -0
- package/lib/node/ClientRequest-Ca8Qykuv.mjs.map +1 -0
- package/lib/node/Interceptor-DEazpLJd.d.mts +133 -0
- package/lib/node/Interceptor-DJ2akVWI.d.cts +133 -0
- package/lib/node/RemoteHttpInterceptor.cjs +154 -0
- package/lib/node/RemoteHttpInterceptor.cjs.map +1 -0
- package/lib/node/RemoteHttpInterceptor.d.cts +39 -0
- package/lib/node/RemoteHttpInterceptor.d.mts +39 -0
- package/lib/node/RemoteHttpInterceptor.mjs +152 -0
- package/lib/node/RemoteHttpInterceptor.mjs.map +1 -0
- package/lib/node/XMLHttpRequest-B7kJdYYI.cjs +763 -0
- package/lib/node/XMLHttpRequest-B7kJdYYI.cjs.map +1 -0
- package/lib/node/XMLHttpRequest-C8dIZpds.mjs +757 -0
- package/lib/node/XMLHttpRequest-C8dIZpds.mjs.map +1 -0
- package/lib/node/bufferUtils-DiCTqG-7.cjs +38 -0
- package/lib/node/bufferUtils-DiCTqG-7.cjs.map +1 -0
- package/lib/node/bufferUtils-_8XfKIfX.mjs +20 -0
- package/lib/node/bufferUtils-_8XfKIfX.mjs.map +1 -0
- package/lib/node/chunk-CbDLau6x.cjs +34 -0
- package/lib/node/fetch-BmXpK10r.cjs +272 -0
- package/lib/node/fetch-BmXpK10r.cjs.map +1 -0
- package/lib/node/fetch-G1DVwDKG.mjs +265 -0
- package/lib/node/fetch-G1DVwDKG.mjs.map +1 -0
- package/lib/node/fetchUtils-BaY5iWXw.cjs +419 -0
- package/lib/node/fetchUtils-BaY5iWXw.cjs.map +1 -0
- package/lib/node/fetchUtils-CoU35g3M.mjs +359 -0
- package/lib/node/fetchUtils-CoU35g3M.mjs.map +1 -0
- package/lib/node/getRawRequest-BavnMWh_.cjs +36 -0
- package/lib/node/getRawRequest-BavnMWh_.cjs.map +1 -0
- package/lib/node/getRawRequest-DnwmXyOW.mjs +24 -0
- package/lib/node/getRawRequest-DnwmXyOW.mjs.map +1 -0
- package/lib/node/glossary-BLKRyLBd.cjs +12 -0
- package/lib/node/glossary-BLKRyLBd.cjs.map +1 -0
- package/lib/node/glossary-glQBRnVD.mjs +6 -0
- package/lib/node/glossary-glQBRnVD.mjs.map +1 -0
- package/lib/node/handleRequest-Bb7Y-XLw.cjs +220 -0
- package/lib/node/handleRequest-Bb7Y-XLw.cjs.map +1 -0
- package/lib/node/handleRequest-Y97UwBbF.mjs +190 -0
- package/lib/node/handleRequest-Y97UwBbF.mjs.map +1 -0
- package/lib/node/hasConfigurableGlobal-C97fWuaA.cjs +26 -0
- package/lib/node/hasConfigurableGlobal-C97fWuaA.cjs.map +1 -0
- package/lib/node/hasConfigurableGlobal-DBJA0vjm.mjs +20 -0
- package/lib/node/hasConfigurableGlobal-DBJA0vjm.mjs.map +1 -0
- package/lib/node/index-BMbJ8FXL.d.cts +113 -0
- package/lib/node/index-C0YAQ36w.d.mts +113 -0
- package/lib/node/index.cjs +54 -0
- package/lib/node/index.cjs.map +1 -0
- package/lib/node/index.d.cts +75 -0
- package/lib/node/index.d.mts +75 -0
- package/lib/node/index.mjs +40 -0
- package/lib/node/index.mjs.map +1 -0
- package/lib/node/interceptors/ClientRequest/index.cjs +6 -0
- package/lib/node/interceptors/ClientRequest/index.d.cts +2 -0
- package/lib/node/interceptors/ClientRequest/index.d.mts +3 -0
- package/lib/node/interceptors/ClientRequest/index.mjs +6 -0
- package/lib/node/interceptors/XMLHttpRequest/index.cjs +6 -0
- package/lib/node/interceptors/XMLHttpRequest/index.d.cts +14 -0
- package/lib/node/interceptors/XMLHttpRequest/index.d.mts +14 -0
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +6 -0
- package/lib/node/interceptors/fetch/index.cjs +5 -0
- package/lib/node/interceptors/fetch/index.d.cts +12 -0
- package/lib/node/interceptors/fetch/index.d.mts +12 -0
- package/lib/node/interceptors/fetch/index.mjs +5 -0
- package/lib/node/node-DwCc6iuP.mjs +27 -0
- package/lib/node/node-DwCc6iuP.mjs.map +1 -0
- package/lib/node/node-dKdAf3tC.cjs +39 -0
- package/lib/node/node-dKdAf3tC.cjs.map +1 -0
- package/lib/node/presets/node.cjs +22 -0
- package/lib/node/presets/node.cjs.map +1 -0
- package/lib/node/presets/node.d.cts +13 -0
- package/lib/node/presets/node.d.mts +15 -0
- package/lib/node/presets/node.mjs +22 -0
- package/lib/node/presets/node.mjs.map +1 -0
- package/lib/node/utils/node/index.cjs +4 -0
- package/lib/node/utils/node/index.d.cts +16 -0
- package/lib/node/utils/node/index.d.mts +16 -0
- package/lib/node/utils/node/index.mjs +3 -0
- package/package.json +204 -0
- package/presets/browser/package.json +5 -0
- package/presets/node/package.json +11 -0
- package/src/BatchInterceptor.test.ts +255 -0
- package/src/BatchInterceptor.ts +95 -0
- package/src/Interceptor.test.ts +205 -0
- package/src/Interceptor.ts +249 -0
- package/src/InterceptorError.ts +7 -0
- package/src/RemoteHttpInterceptor.ts +251 -0
- package/src/RequestController.test.ts +104 -0
- package/src/RequestController.ts +109 -0
- package/src/createRequestId.test.ts +7 -0
- package/src/createRequestId.ts +9 -0
- package/src/getRawRequest.ts +21 -0
- package/src/glossary.ts +37 -0
- package/src/index.ts +15 -0
- package/src/interceptors/ClientRequest/MockHttpSocket.ts +724 -0
- package/src/interceptors/ClientRequest/agents.ts +110 -0
- package/src/interceptors/ClientRequest/index.test.ts +75 -0
- package/src/interceptors/ClientRequest/index.ts +193 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +54 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +45 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +427 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +268 -0
- package/src/interceptors/ClientRequest/utils/parserUtils.ts +48 -0
- package/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts +258 -0
- package/src/interceptors/ClientRequest/utils/recordRawHeaders.ts +262 -0
- package/src/interceptors/Socket/MockSocket.test.ts +264 -0
- package/src/interceptors/Socket/MockSocket.ts +58 -0
- package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +26 -0
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +52 -0
- package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +33 -0
- package/src/interceptors/WebSocket/WebSocketClassTransport.ts +116 -0
- package/src/interceptors/WebSocket/WebSocketClientConnection.ts +152 -0
- package/src/interceptors/WebSocket/WebSocketOverride.ts +252 -0
- package/src/interceptors/WebSocket/WebSocketServerConnection.ts +420 -0
- package/src/interceptors/WebSocket/WebSocketTransport.ts +39 -0
- package/src/interceptors/WebSocket/index.ts +191 -0
- package/src/interceptors/WebSocket/utils/bindEvent.test.ts +27 -0
- package/src/interceptors/WebSocket/utils/bindEvent.ts +21 -0
- package/src/interceptors/WebSocket/utils/events.test.ts +101 -0
- package/src/interceptors/WebSocket/utils/events.ts +94 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +746 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +121 -0
- package/src/interceptors/XMLHttpRequest/index.ts +61 -0
- package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
- package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
- package/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts +12 -0
- package/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts +12 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +26 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
- package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +49 -0
- package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.test.ts +164 -0
- package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts +16 -0
- package/src/interceptors/XMLHttpRequest/utils/isDomParserSupportedType.ts +14 -0
- package/src/interceptors/fetch/index.ts +214 -0
- package/src/interceptors/fetch/utils/brotli-decompress.browser.ts +14 -0
- package/src/interceptors/fetch/utils/brotli-decompress.ts +31 -0
- package/src/interceptors/fetch/utils/createNetworkError.ts +5 -0
- package/src/interceptors/fetch/utils/decompression.ts +85 -0
- package/src/interceptors/fetch/utils/followRedirect.ts +114 -0
- package/src/presets/browser.ts +11 -0
- package/src/presets/node.ts +13 -0
- package/src/utils/bufferUtils.test.ts +21 -0
- package/src/utils/bufferUtils.ts +22 -0
- package/src/utils/canParseUrl.ts +13 -0
- package/src/utils/cloneObject.test.ts +94 -0
- package/src/utils/cloneObject.ts +36 -0
- package/src/utils/createProxy.test.ts +164 -0
- package/src/utils/createProxy.ts +104 -0
- package/src/utils/emitAsync.ts +25 -0
- package/src/utils/fetchUtils.ts +119 -0
- package/src/utils/findPropertySource.test.ts +27 -0
- package/src/utils/findPropertySource.ts +20 -0
- package/src/utils/getCleanUrl.test.ts +32 -0
- package/src/utils/getCleanUrl.ts +6 -0
- package/src/utils/getUrlByRequestOptions.test.ts +163 -0
- package/src/utils/getUrlByRequestOptions.ts +152 -0
- package/src/utils/getValueBySymbol.test.ts +14 -0
- package/src/utils/getValueBySymbol.ts +19 -0
- package/src/utils/handleRequest.ts +205 -0
- package/src/utils/hasConfigurableGlobal.test.ts +83 -0
- package/src/utils/hasConfigurableGlobal.ts +34 -0
- package/src/utils/isNodeLikeError.ts +13 -0
- package/src/utils/isObject.test.ts +21 -0
- package/src/utils/isObject.ts +8 -0
- package/src/utils/isPropertyAccessible.ts +19 -0
- package/src/utils/nextTick.ts +11 -0
- package/src/utils/node/index.ts +39 -0
- package/src/utils/parseJson.test.ts +10 -0
- package/src/utils/parseJson.ts +12 -0
- package/src/utils/resolveWebSocketUrl.ts +45 -0
- package/src/utils/responseUtils.ts +59 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { invariant } from 'outvariant'
|
|
2
|
+
import { until } from '@open-draft/until'
|
|
3
|
+
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
4
|
+
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
|
5
|
+
import { Interceptor } from '../../Interceptor'
|
|
6
|
+
import { RequestController } from '../../RequestController'
|
|
7
|
+
import { emitAsync } from '../../utils/emitAsync'
|
|
8
|
+
import { handleRequest } from '../../utils/handleRequest'
|
|
9
|
+
import { canParseUrl } from '../../utils/canParseUrl'
|
|
10
|
+
import { createRequestId } from '../../createRequestId'
|
|
11
|
+
import { createNetworkError } from './utils/createNetworkError'
|
|
12
|
+
import { followFetchRedirect } from './utils/followRedirect'
|
|
13
|
+
import { decompressResponse } from './utils/decompression'
|
|
14
|
+
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
|
|
15
|
+
import { FetchResponse } from '../../utils/fetchUtils'
|
|
16
|
+
import { setRawRequest } from '../../getRawRequest'
|
|
17
|
+
import { isResponseError } from '../../utils/responseUtils'
|
|
18
|
+
|
|
19
|
+
export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
20
|
+
static symbol = Symbol('fetch')
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super(FetchInterceptor.symbol)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected checkEnvironment() {
|
|
27
|
+
return hasConfigurableGlobal('fetch')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected async setup() {
|
|
31
|
+
const pureFetch = globalThis.fetch
|
|
32
|
+
|
|
33
|
+
invariant(
|
|
34
|
+
!(pureFetch as any)[IS_PATCHED_MODULE],
|
|
35
|
+
'Failed to patch the "fetch" module: already patched.'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
globalThis.fetch = async (input, init) => {
|
|
39
|
+
const requestId = createRequestId()
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @note Resolve potentially relative request URL
|
|
43
|
+
* against the present `location`. This is mainly
|
|
44
|
+
* for native `fetch` in JSDOM.
|
|
45
|
+
* @see https://github.com/mswjs/msw/issues/1625
|
|
46
|
+
*/
|
|
47
|
+
const resolvedInput =
|
|
48
|
+
typeof input === 'string' &&
|
|
49
|
+
typeof location !== 'undefined' &&
|
|
50
|
+
!canParseUrl(input)
|
|
51
|
+
? new URL(input, location.href)
|
|
52
|
+
: input
|
|
53
|
+
|
|
54
|
+
const request = new Request(resolvedInput, init)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @note Set the raw request only if a Request instance was provided to fetch.
|
|
58
|
+
*/
|
|
59
|
+
if (input instanceof Request) {
|
|
60
|
+
setRawRequest(request, input)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const responsePromise = new DeferredPromise<Response>()
|
|
64
|
+
|
|
65
|
+
const controller = new RequestController(request, {
|
|
66
|
+
passthrough: async () => {
|
|
67
|
+
this.logger.info('request has not been handled, passthrough...')
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @note Clone the request instance right before performing it.
|
|
71
|
+
* This preserves any modifications made to the intercepted request
|
|
72
|
+
* in the "request" listener. This also allows the user to read the
|
|
73
|
+
* request body in the "response" listener (otherwise "unusable").
|
|
74
|
+
*/
|
|
75
|
+
const requestCloneForResponseEvent = request.clone()
|
|
76
|
+
|
|
77
|
+
// Perform the intercepted request as-is.
|
|
78
|
+
const { error: responseError, data: originalResponse } = await until(
|
|
79
|
+
() => pureFetch(request)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (responseError) {
|
|
83
|
+
return responsePromise.reject(responseError)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.logger.info('original fetch performed', originalResponse)
|
|
87
|
+
|
|
88
|
+
if (this.emitter.listenerCount('response') > 0) {
|
|
89
|
+
this.logger.info('emitting the "response" event...')
|
|
90
|
+
|
|
91
|
+
const responseClone = originalResponse.clone()
|
|
92
|
+
await emitAsync(this.emitter, 'response', {
|
|
93
|
+
response: responseClone,
|
|
94
|
+
isMockedResponse: false,
|
|
95
|
+
request: requestCloneForResponseEvent,
|
|
96
|
+
requestId,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolve the response promise with the original response
|
|
101
|
+
// since the `fetch()` return this internal promise.
|
|
102
|
+
responsePromise.resolve(originalResponse)
|
|
103
|
+
},
|
|
104
|
+
respondWith: async (rawResponse) => {
|
|
105
|
+
// Handle mocked `Response.error()` (i.e. request errors).
|
|
106
|
+
if (isResponseError(rawResponse)) {
|
|
107
|
+
this.logger.info('request has errored!', { response: rawResponse })
|
|
108
|
+
responsePromise.reject(createNetworkError(rawResponse))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.logger.info('received mocked response!', {
|
|
113
|
+
rawResponse,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Decompress the mocked response body, if applicable.
|
|
117
|
+
const decompressedStream = decompressResponse(rawResponse)
|
|
118
|
+
const response =
|
|
119
|
+
decompressedStream === null
|
|
120
|
+
? rawResponse
|
|
121
|
+
: new FetchResponse(decompressedStream, rawResponse)
|
|
122
|
+
|
|
123
|
+
FetchResponse.setUrl(request.url, response)
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Undici's handling of following redirect responses.
|
|
127
|
+
* Treat the "manual" redirect mode as a regular mocked response.
|
|
128
|
+
* This way, the client can manually follow the redirect it receives.
|
|
129
|
+
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1173
|
|
130
|
+
*/
|
|
131
|
+
if (FetchResponse.isRedirectResponse(response.status)) {
|
|
132
|
+
// Reject the request promise if its `redirect` is set to `error`
|
|
133
|
+
// and it receives a mocked redirect response.
|
|
134
|
+
if (request.redirect === 'error') {
|
|
135
|
+
responsePromise.reject(createNetworkError('unexpected redirect'))
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (request.redirect === 'follow') {
|
|
140
|
+
followFetchRedirect(request, response).then(
|
|
141
|
+
(response) => {
|
|
142
|
+
responsePromise.resolve(response)
|
|
143
|
+
},
|
|
144
|
+
(reason) => {
|
|
145
|
+
responsePromise.reject(reason)
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.emitter.listenerCount('response') > 0) {
|
|
153
|
+
this.logger.info('emitting the "response" event...')
|
|
154
|
+
|
|
155
|
+
// Await the response listeners to finish before resolving
|
|
156
|
+
// the response promise. This ensures all your logic finishes
|
|
157
|
+
// before the interceptor resolves the pending response.
|
|
158
|
+
await emitAsync(this.emitter, 'response', {
|
|
159
|
+
// Clone the mocked response for the "response" event listener.
|
|
160
|
+
// This way, the listener can read the response and not lock its body
|
|
161
|
+
// for the actual fetch consumer.
|
|
162
|
+
response: response.clone(),
|
|
163
|
+
isMockedResponse: true,
|
|
164
|
+
request,
|
|
165
|
+
requestId,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
responsePromise.resolve(response)
|
|
170
|
+
},
|
|
171
|
+
errorWith: (reason) => {
|
|
172
|
+
this.logger.info('request has been aborted!', { reason })
|
|
173
|
+
responsePromise.reject(reason)
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
this.logger.info('[%s] %s', request.method, request.url)
|
|
178
|
+
this.logger.info('awaiting for the mocked response...')
|
|
179
|
+
|
|
180
|
+
this.logger.info(
|
|
181
|
+
'emitting the "request" event for %s listener(s)...',
|
|
182
|
+
this.emitter.listenerCount('request')
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
await handleRequest({
|
|
186
|
+
request,
|
|
187
|
+
requestId,
|
|
188
|
+
emitter: this.emitter,
|
|
189
|
+
controller,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return responsePromise
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
|
|
196
|
+
enumerable: true,
|
|
197
|
+
configurable: true,
|
|
198
|
+
value: true,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
this.subscriptions.push(() => {
|
|
202
|
+
Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
|
|
203
|
+
value: undefined,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
globalThis.fetch = pureFetch
|
|
207
|
+
|
|
208
|
+
this.logger.info(
|
|
209
|
+
'restored native "globalThis.fetch"!',
|
|
210
|
+
globalThis.fetch.name
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class BrotliDecompressionStream extends TransformStream {
|
|
2
|
+
constructor() {
|
|
3
|
+
console.warn(
|
|
4
|
+
'[Interceptors]: Brotli decompression of response streams is not supported in the browser'
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
super({
|
|
8
|
+
transform(chunk, controller) {
|
|
9
|
+
// Keep the stream as passthrough, it does nothing.
|
|
10
|
+
controller.enqueue(chunk)
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import zlib from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
export class BrotliDecompressionStream extends TransformStream {
|
|
4
|
+
constructor() {
|
|
5
|
+
const decompress = zlib.createBrotliDecompress({
|
|
6
|
+
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
|
7
|
+
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
super({
|
|
11
|
+
async transform(chunk, controller) {
|
|
12
|
+
const buffer = Buffer.from(chunk)
|
|
13
|
+
|
|
14
|
+
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
|
15
|
+
decompress.write(buffer, (error) => {
|
|
16
|
+
if (error) reject(error)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
decompress.flush()
|
|
20
|
+
decompress.once('data', (data) => resolve(data))
|
|
21
|
+
decompress.once('error', (error) => reject(error))
|
|
22
|
+
decompress.once('end', () => controller.terminate())
|
|
23
|
+
}).catch((error) => {
|
|
24
|
+
controller.error(error)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
controller.enqueue(decompressed)
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Import from an internal alias that resolves to different modules
|
|
2
|
+
// depending on the environment. This way, we can keep the fetch interceptor
|
|
3
|
+
// intact while using different strategies for Brotli decompression.
|
|
4
|
+
import { BrotliDecompressionStream } from 'internal:brotli-decompress'
|
|
5
|
+
|
|
6
|
+
class PipelineStream extends TransformStream {
|
|
7
|
+
constructor(
|
|
8
|
+
transformStreams: Array<TransformStream>,
|
|
9
|
+
...strategies: Array<QueuingStrategy>
|
|
10
|
+
) {
|
|
11
|
+
super({}, ...strategies)
|
|
12
|
+
|
|
13
|
+
const readable = [super.readable as any, ...transformStreams].reduce(
|
|
14
|
+
(readable, transform) => readable.pipeThrough(transform)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
Object.defineProperty(this, 'readable', {
|
|
18
|
+
get() {
|
|
19
|
+
return readable
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseContentEncoding(contentEncoding: string): Array<string> {
|
|
26
|
+
return contentEncoding
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((coding) => coding.trim())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createDecompressionStream(
|
|
33
|
+
contentEncoding: string
|
|
34
|
+
): TransformStream | null {
|
|
35
|
+
if (contentEncoding === '') {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const codings = parseContentEncoding(contentEncoding)
|
|
40
|
+
|
|
41
|
+
if (codings.length === 0) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const transformers = codings.reduceRight<Array<TransformStream>>(
|
|
46
|
+
(transformers, coding) => {
|
|
47
|
+
if (coding === 'gzip' || coding === 'x-gzip') {
|
|
48
|
+
return transformers.concat(new DecompressionStream('gzip'))
|
|
49
|
+
} else if (coding === 'deflate') {
|
|
50
|
+
return transformers.concat(new DecompressionStream('deflate'))
|
|
51
|
+
} else if (coding === 'br') {
|
|
52
|
+
return transformers.concat(new BrotliDecompressionStream())
|
|
53
|
+
} else {
|
|
54
|
+
transformers.length = 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return transformers
|
|
58
|
+
},
|
|
59
|
+
[]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return new PipelineStream(transformers)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decompressResponse(
|
|
66
|
+
response: Response
|
|
67
|
+
): ReadableStream<any> | null {
|
|
68
|
+
if (response.body === null) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const decompressionStream = createDecompressionStream(
|
|
73
|
+
response.headers.get('content-encoding') || ''
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (!decompressionStream) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Use `pipeTo` and return the decompression stream's readable
|
|
81
|
+
// instead of `pipeThrough` because that will lock the original
|
|
82
|
+
// response stream, making it unusable as the input to Response.
|
|
83
|
+
response.body.pipeTo(decompressionStream.writable)
|
|
84
|
+
return decompressionStream.readable
|
|
85
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createNetworkError } from './createNetworkError'
|
|
2
|
+
|
|
3
|
+
const REQUEST_BODY_HEADERS = [
|
|
4
|
+
'content-encoding',
|
|
5
|
+
'content-language',
|
|
6
|
+
'content-location',
|
|
7
|
+
'content-type',
|
|
8
|
+
'content-length',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const kRedirectCount = Symbol('kRedirectCount')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1210
|
|
15
|
+
*/
|
|
16
|
+
export async function followFetchRedirect(
|
|
17
|
+
request: Request,
|
|
18
|
+
response: Response
|
|
19
|
+
): Promise<Response> {
|
|
20
|
+
if (response.status !== 303 && request.body != null) {
|
|
21
|
+
return Promise.reject(createNetworkError())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const requestUrl = new URL(request.url)
|
|
25
|
+
|
|
26
|
+
let locationUrl: URL
|
|
27
|
+
try {
|
|
28
|
+
// If the location is a relative URL, use the request URL as the base URL.
|
|
29
|
+
locationUrl = new URL(response.headers.get('location')!, request.url)
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return Promise.reject(createNetworkError(error))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
!(locationUrl.protocol === 'http:' || locationUrl.protocol === 'https:')
|
|
36
|
+
) {
|
|
37
|
+
return Promise.reject(
|
|
38
|
+
createNetworkError('URL scheme must be a HTTP(S) scheme')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Reflect.get(request, kRedirectCount) > 20) {
|
|
43
|
+
return Promise.reject(createNetworkError('redirect count exceeded'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Object.defineProperty(request, kRedirectCount, {
|
|
47
|
+
value: (Reflect.get(request, kRedirectCount) || 0) + 1,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
request.mode === 'cors' &&
|
|
52
|
+
(locationUrl.username || locationUrl.password) &&
|
|
53
|
+
!sameOrigin(requestUrl, locationUrl)
|
|
54
|
+
) {
|
|
55
|
+
return Promise.reject(
|
|
56
|
+
createNetworkError('cross origin not allowed for request mode "cors"')
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const requestInit: RequestInit = {}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
([301, 302].includes(response.status) && request.method === 'POST') ||
|
|
64
|
+
(response.status === 303 && !['HEAD', 'GET'].includes(request.method))
|
|
65
|
+
) {
|
|
66
|
+
requestInit.method = 'GET'
|
|
67
|
+
requestInit.body = null
|
|
68
|
+
|
|
69
|
+
REQUEST_BODY_HEADERS.forEach((headerName) => {
|
|
70
|
+
request.headers.delete(headerName)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!sameOrigin(requestUrl, locationUrl)) {
|
|
75
|
+
request.headers.delete('authorization')
|
|
76
|
+
request.headers.delete('proxy-authorization')
|
|
77
|
+
request.headers.delete('cookie')
|
|
78
|
+
request.headers.delete('host')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @note Undici "safely" extracts the request body.
|
|
83
|
+
* I suspect we cannot dispatch this request again
|
|
84
|
+
* since its body has been read and the stream is locked.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
requestInit.headers = request.headers
|
|
88
|
+
const finalResponse = await fetch(new Request(locationUrl, requestInit))
|
|
89
|
+
Object.defineProperty(finalResponse, 'redirected', {
|
|
90
|
+
value: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return finalResponse
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/util.js#L761
|
|
99
|
+
*/
|
|
100
|
+
function sameOrigin(left: URL, right: URL): boolean {
|
|
101
|
+
if (left.origin === right.origin && left.origin === 'null') {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
left.protocol === right.protocol &&
|
|
107
|
+
left.hostname === right.hostname &&
|
|
108
|
+
left.port === right.port
|
|
109
|
+
) {
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FetchInterceptor } from '../interceptors/fetch'
|
|
2
|
+
import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The default preset provisions the interception of requests
|
|
6
|
+
* regardless of their type (fetch/XMLHttpRequest).
|
|
7
|
+
*/
|
|
8
|
+
export default [
|
|
9
|
+
new FetchInterceptor(),
|
|
10
|
+
new XMLHttpRequestInterceptor(),
|
|
11
|
+
] as const
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClientRequestInterceptor } from '../interceptors/ClientRequest'
|
|
2
|
+
import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
|
|
3
|
+
import { FetchInterceptor } from '../interceptors/fetch'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The default preset provisions the interception of requests
|
|
7
|
+
* regardless of their type (http/https/XMLHttpRequest).
|
|
8
|
+
*/
|
|
9
|
+
export default [
|
|
10
|
+
new ClientRequestInterceptor(),
|
|
11
|
+
new XMLHttpRequestInterceptor(),
|
|
12
|
+
new FetchInterceptor(),
|
|
13
|
+
] as const
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { it, expect } from 'vitest'
|
|
2
|
+
import { decodeBuffer, encodeBuffer } from './bufferUtils'
|
|
3
|
+
|
|
4
|
+
it('encodes utf-8 string', () => {
|
|
5
|
+
const encoded = encodeBuffer('😁')
|
|
6
|
+
expect(new Uint8Array(encoded)).toEqual(new Uint8Array([240, 159, 152, 129]))
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('decodes utf-8 string', () => {
|
|
10
|
+
const array = new Uint8Array([240, 159, 152, 129])
|
|
11
|
+
const decoded = decodeBuffer(array.buffer)
|
|
12
|
+
expect(decoded).toEqual('😁')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('decodes string with custom encoding', () => {
|
|
16
|
+
const array = new Uint8Array([
|
|
17
|
+
207, 240, 232, 226, 229, 242, 44, 32, 236, 232, 240, 33,
|
|
18
|
+
])
|
|
19
|
+
const decoded = decodeBuffer(array.buffer, 'windows-1251')
|
|
20
|
+
expect(decoded).toEqual('Привет, мир!')
|
|
21
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const encoder = new TextEncoder()
|
|
2
|
+
|
|
3
|
+
export function encodeBuffer(text: string): Uint8Array {
|
|
4
|
+
return encoder.encode(text)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function decodeBuffer(buffer: ArrayBuffer, encoding?: string): string {
|
|
8
|
+
const decoder = new TextDecoder(encoding)
|
|
9
|
+
return decoder.decode(buffer)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create an `ArrayBuffer` from the given `Uint8Array`.
|
|
14
|
+
* Takes the byte offset into account to produce the right buffer
|
|
15
|
+
* in the case when the buffer is bigger than the data view.
|
|
16
|
+
*/
|
|
17
|
+
export function toArrayBuffer(array: Uint8Array): ArrayBuffer {
|
|
18
|
+
return array.buffer.slice(
|
|
19
|
+
array.byteOffset,
|
|
20
|
+
array.byteOffset + array.byteLength
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a boolean indicating whether the given URL string
|
|
3
|
+
* can be parsed into a `URL` instance.
|
|
4
|
+
* A substitute for `URL.canParse()` for Node.js 18.
|
|
5
|
+
*/
|
|
6
|
+
export function canParseUrl(url: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
new URL(url)
|
|
9
|
+
return true
|
|
10
|
+
} catch (_error) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { it, expect } from 'vitest'
|
|
2
|
+
import { cloneObject } from './cloneObject'
|
|
3
|
+
|
|
4
|
+
it('clones a shallow object', () => {
|
|
5
|
+
const original = { a: 1, b: 2, c: [1, 2, 3] }
|
|
6
|
+
const clone = cloneObject(original)
|
|
7
|
+
|
|
8
|
+
expect(clone).toEqual(original)
|
|
9
|
+
|
|
10
|
+
clone.a = 5
|
|
11
|
+
clone.b = 6
|
|
12
|
+
clone.c = [5, 6, 7]
|
|
13
|
+
|
|
14
|
+
expect(clone).toHaveProperty('a', 5)
|
|
15
|
+
expect(clone).toHaveProperty('b', 6)
|
|
16
|
+
expect(clone).toHaveProperty('c', [5, 6, 7])
|
|
17
|
+
expect(original).toHaveProperty('a', 1)
|
|
18
|
+
expect(original).toHaveProperty('b', 2)
|
|
19
|
+
expect(original).toHaveProperty('c', [1, 2, 3])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('clones a nested object', () => {
|
|
23
|
+
const original = { a: { b: 1 }, c: { d: { e: 2 } } }
|
|
24
|
+
const clone = cloneObject(original)
|
|
25
|
+
|
|
26
|
+
expect(clone).toEqual(original)
|
|
27
|
+
|
|
28
|
+
clone.a.b = 10
|
|
29
|
+
clone.c.d.e = 20
|
|
30
|
+
|
|
31
|
+
expect(clone).toHaveProperty(['a', 'b'], 10)
|
|
32
|
+
expect(clone).toHaveProperty(['c', 'd', 'e'], 20)
|
|
33
|
+
expect(original).toHaveProperty(['a', 'b'], 1)
|
|
34
|
+
expect(original).toHaveProperty(['c', 'd', 'e'], 2)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('clones a class instance', () => {
|
|
38
|
+
class Car {
|
|
39
|
+
public manufacturer: string
|
|
40
|
+
constructor() {
|
|
41
|
+
this.manufacturer = 'Audi'
|
|
42
|
+
}
|
|
43
|
+
getManufacturer() {
|
|
44
|
+
return this.manufacturer
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const car = new Car()
|
|
49
|
+
const clone = cloneObject(car)
|
|
50
|
+
|
|
51
|
+
expect(clone).toHaveProperty('manufacturer', 'Audi')
|
|
52
|
+
expect(clone).toHaveProperty('getManufacturer')
|
|
53
|
+
expect(clone.getManufacturer).toBeInstanceOf(Function)
|
|
54
|
+
expect(clone.getManufacturer()).toEqual('Audi')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('ignores nested class instances', () => {
|
|
58
|
+
class Car {
|
|
59
|
+
name: string
|
|
60
|
+
constructor(name: string) {
|
|
61
|
+
this.name = name
|
|
62
|
+
}
|
|
63
|
+
getName() {
|
|
64
|
+
return this.name
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const original = {
|
|
68
|
+
a: 1,
|
|
69
|
+
car: new Car('Audi'),
|
|
70
|
+
}
|
|
71
|
+
const clone = cloneObject(original)
|
|
72
|
+
|
|
73
|
+
expect(clone).toEqual(original)
|
|
74
|
+
expect(clone.car).toBeInstanceOf(Car)
|
|
75
|
+
expect(clone.car.getName()).toEqual('Audi')
|
|
76
|
+
|
|
77
|
+
clone.car = new Car('BMW')
|
|
78
|
+
|
|
79
|
+
expect(clone.car).toBeInstanceOf(Car)
|
|
80
|
+
expect(clone.car.getName()).toEqual('BMW')
|
|
81
|
+
expect(original.car).toBeInstanceOf(Car)
|
|
82
|
+
expect(original.car.getName()).toEqual('Audi')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('clones an object with null prototype', () => {
|
|
86
|
+
const original = {
|
|
87
|
+
key: Object.create(null),
|
|
88
|
+
}
|
|
89
|
+
const clone = cloneObject(original)
|
|
90
|
+
|
|
91
|
+
expect(clone).toEqual({
|
|
92
|
+
key: {},
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Logger } from '@open-draft/logger'
|
|
2
|
+
|
|
3
|
+
const logger = new Logger('cloneObject')
|
|
4
|
+
|
|
5
|
+
function isPlainObject(obj?: Record<string, any>): boolean {
|
|
6
|
+
logger.info('is plain object?', obj)
|
|
7
|
+
|
|
8
|
+
if (obj == null || !obj.constructor?.name) {
|
|
9
|
+
logger.info('given object is undefined, not a plain object...')
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
logger.info('checking the object constructor:', obj.constructor.name)
|
|
14
|
+
return obj.constructor.name === 'Object'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function cloneObject<ObjectType extends Record<string, any>>(
|
|
18
|
+
obj: ObjectType
|
|
19
|
+
): ObjectType {
|
|
20
|
+
logger.info('cloning object:', obj)
|
|
21
|
+
|
|
22
|
+
const enumerableProperties = Object.entries(obj).reduce<Record<string, any>>(
|
|
23
|
+
(acc, [key, value]) => {
|
|
24
|
+
logger.info('analyzing key-value pair:', key, value)
|
|
25
|
+
|
|
26
|
+
// Recursively clone only plain objects, omitting class instances.
|
|
27
|
+
acc[key] = isPlainObject(value) ? cloneObject(value) : value
|
|
28
|
+
return acc
|
|
29
|
+
},
|
|
30
|
+
{}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return isPlainObject(obj)
|
|
34
|
+
? enumerableProperties
|
|
35
|
+
: Object.assign(Object.getPrototypeOf(obj), enumerableProperties)
|
|
36
|
+
}
|