@mswjs/interceptors 0.12.6 → 0.13.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/README.md +64 -15
- package/lib/createInterceptor.d.ts +7 -0
- package/lib/createInterceptor.js.map +1 -1
- package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +38 -0
- package/lib/interceptors/ClientRequest/NodeClientRequest.js +420 -0
- package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -0
- package/lib/interceptors/ClientRequest/http.get.d.ts +5 -0
- package/lib/interceptors/ClientRequest/http.get.js +47 -0
- package/lib/interceptors/ClientRequest/http.get.js.map +1 -0
- package/lib/interceptors/ClientRequest/http.request.d.ts +5 -0
- package/lib/interceptors/ClientRequest/http.request.js +44 -0
- package/lib/interceptors/ClientRequest/http.request.js.map +1 -0
- package/lib/interceptors/ClientRequest/index.d.ts +5 -1
- package/lib/interceptors/ClientRequest/index.js +50 -84
- package/lib/interceptors/ClientRequest/index.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.d.ts +7 -0
- package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js +77 -0
- package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js.map +1 -0
- package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.d.ts +1 -1
- package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +21 -12
- package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.d.ts +17 -0
- package/lib/interceptors/ClientRequest/utils/{normalizeHttpRequestParams.js → normalizeClientRequestArgs.js} +65 -39
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js.map +1 -0
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.d.ts +15 -0
- package/lib/interceptors/ClientRequest/utils/{normalizeHttpRequestEndParams.js → normalizeClientRequestEndArgs.js} +5 -6
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.js.map +1 -0
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.d.ts +13 -0
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js +20 -0
- package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js.map +1 -0
- package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +3 -2
- package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
- package/lib/interceptors/fetch/index.js +6 -4
- package/lib/interceptors/fetch/index.js.map +1 -1
- package/lib/utils/getUrlByRequestOptions.d.ts +5 -4
- package/lib/utils/getUrlByRequestOptions.js.map +1 -1
- package/package.json +33 -18
- package/src/createInterceptor.ts +100 -0
- package/src/index.ts +5 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +283 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +377 -0
- package/src/interceptors/ClientRequest/http.get.ts +32 -0
- package/src/interceptors/ClientRequest/http.request.ts +29 -0
- package/src/interceptors/ClientRequest/index.ts +61 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +16 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +7 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +20 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +41 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +13 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +10 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +44 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +38 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +336 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +205 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +40 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +51 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +35 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +36 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +565 -0
- package/src/interceptors/XMLHttpRequest/index.ts +34 -0
- package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
- package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +11 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +16 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +27 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
- package/src/interceptors/fetch/index.ts +89 -0
- package/src/presets/browser.ts +8 -0
- package/src/presets/node.ts +8 -0
- package/src/remote.ts +176 -0
- package/src/utils/cloneObject.test.ts +93 -0
- package/src/utils/cloneObject.ts +34 -0
- package/src/utils/getCleanUrl.test.ts +31 -0
- package/src/utils/getCleanUrl.ts +6 -0
- package/src/utils/getRequestOptionsByUrl.ts +29 -0
- package/src/utils/getUrlByRequestOptions.test.ts +140 -0
- package/src/utils/getUrlByRequestOptions.ts +108 -0
- package/src/utils/isObject.test.ts +19 -0
- package/src/utils/isObject.ts +6 -0
- package/src/utils/parseJson.test.ts +9 -0
- package/src/utils/parseJson.ts +12 -0
- package/src/utils/toIsoResponse.test.ts +39 -0
- package/src/utils/toIsoResponse.ts +14 -0
- package/src/utils/uuid.ts +7 -0
- package/lib/interceptors/ClientRequest/ClientRequest.glossary.d.ts +0 -6
- package/lib/interceptors/ClientRequest/ClientRequest.glossary.js +0 -3
- package/lib/interceptors/ClientRequest/ClientRequest.glossary.js.map +0 -1
- package/lib/interceptors/ClientRequest/createClientRequestOverride.d.ts +0 -12
- package/lib/interceptors/ClientRequest/createClientRequestOverride.js +0 -340
- package/lib/interceptors/ClientRequest/createClientRequestOverride.js.map +0 -1
- package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.d.ts +0 -49
- package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.js +0 -118
- package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.js.map +0 -1
- package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.d.ts +0 -3
- package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.js +0 -34
- package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.js.map +0 -1
- package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestEndParams.d.ts +0 -17
- package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestEndParams.js.map +0 -1
- package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestParams.d.ts +0 -11
- package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestParams.js.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { bufferFrom } from './bufferFrom'
|
|
5
|
+
|
|
6
|
+
test('returns the same Uint8Array instance as Buffer.from', () => {
|
|
7
|
+
const init = 'hello world'
|
|
8
|
+
const buffer = bufferFrom(init)
|
|
9
|
+
const rawBuffer = Buffer.from(init)
|
|
10
|
+
expect(Buffer.compare(buffer, rawBuffer)).toBe(0)
|
|
11
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a given string into a `Uint8Array`.
|
|
3
|
+
* We don't use `TextEncoder` because it's unavailable in some environments.
|
|
4
|
+
*/
|
|
5
|
+
export function bufferFrom(init: string): Uint8Array {
|
|
6
|
+
const encodedString = encodeURIComponent(init)
|
|
7
|
+
const binaryString = encodedString.replace(/%([0-9A-F]{2})/g, (_, char) => {
|
|
8
|
+
return String.fromCharCode(('0x' + char) as any)
|
|
9
|
+
})
|
|
10
|
+
const buffer = new Uint8Array(binaryString.length)
|
|
11
|
+
Array.prototype.forEach.call(binaryString, (char, index) => {
|
|
12
|
+
buffer[index] = char.charCodeAt(0)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return buffer
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { createEvent } from './createEvent'
|
|
5
|
+
import { EventPolyfill } from '../polyfills/EventPolyfill'
|
|
6
|
+
|
|
7
|
+
const request = new XMLHttpRequest()
|
|
8
|
+
request.open('POST', '/user')
|
|
9
|
+
|
|
10
|
+
test('returns an EventPolyfill instance with the given target set', () => {
|
|
11
|
+
const event = createEvent(request, 'my-event')
|
|
12
|
+
const target = event.target as XMLHttpRequest
|
|
13
|
+
|
|
14
|
+
expect(event).toBeInstanceOf(EventPolyfill)
|
|
15
|
+
expect(target).toBeInstanceOf(XMLHttpRequest)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('returns the ProgressEvent instance', () => {
|
|
19
|
+
const event = createEvent(request, 'load', {
|
|
20
|
+
loaded: 100,
|
|
21
|
+
total: 500,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(event).toBeInstanceOf(ProgressEvent)
|
|
25
|
+
expect(event.loaded).toBe(100)
|
|
26
|
+
expect(event.total).toBe(500)
|
|
27
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventPolyfill } from '../polyfills/EventPolyfill'
|
|
2
|
+
import { ProgressEventPolyfill } from '../polyfills/ProgressEventPolyfill'
|
|
3
|
+
|
|
4
|
+
const SUPPORTS_PROGRESS_EVENT = typeof ProgressEvent !== 'undefined'
|
|
5
|
+
|
|
6
|
+
export function createEvent(
|
|
7
|
+
target: XMLHttpRequest,
|
|
8
|
+
type: string,
|
|
9
|
+
init?: ProgressEventInit
|
|
10
|
+
): EventPolyfill {
|
|
11
|
+
const progressEvents = [
|
|
12
|
+
'error',
|
|
13
|
+
'progress',
|
|
14
|
+
'loadstart',
|
|
15
|
+
'loadend',
|
|
16
|
+
'load',
|
|
17
|
+
'timeout',
|
|
18
|
+
'abort',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `ProgressEvent` is not supported in React Native.
|
|
23
|
+
* @see https://github.com/mswjs/interceptors/issues/40
|
|
24
|
+
*/
|
|
25
|
+
const ProgressEventClass = SUPPORTS_PROGRESS_EVENT
|
|
26
|
+
? ProgressEvent
|
|
27
|
+
: ProgressEventPolyfill
|
|
28
|
+
|
|
29
|
+
const event = progressEvents.includes(type)
|
|
30
|
+
? new ProgressEventClass(type, {
|
|
31
|
+
lengthComputable: true,
|
|
32
|
+
loaded: init?.loaded || 0,
|
|
33
|
+
total: init?.total || 0,
|
|
34
|
+
})
|
|
35
|
+
: new EventPolyfill(type, {
|
|
36
|
+
target,
|
|
37
|
+
currentTarget: target,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return event
|
|
41
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Headers,
|
|
3
|
+
headersToObject,
|
|
4
|
+
objectToHeaders,
|
|
5
|
+
flattenHeadersObject,
|
|
6
|
+
} from 'headers-utils'
|
|
7
|
+
import {
|
|
8
|
+
Interceptor,
|
|
9
|
+
IsomorphicRequest,
|
|
10
|
+
IsomorphicResponse,
|
|
11
|
+
} from '../../createInterceptor'
|
|
12
|
+
import { toIsoResponse } from '../../utils/toIsoResponse'
|
|
13
|
+
import { uuidv4 } from '../../utils/uuid'
|
|
14
|
+
|
|
15
|
+
const debug = require('debug')('fetch')
|
|
16
|
+
|
|
17
|
+
export const interceptFetch: Interceptor = (observer, resolver) => {
|
|
18
|
+
const pureFetch = window.fetch
|
|
19
|
+
|
|
20
|
+
debug('replacing "window.fetch"...')
|
|
21
|
+
|
|
22
|
+
window.fetch = async (input, init) => {
|
|
23
|
+
const ref = new Request(input, init)
|
|
24
|
+
const url = typeof input === 'string' ? input : input.url
|
|
25
|
+
const method = init?.method || 'GET'
|
|
26
|
+
|
|
27
|
+
debug('[%s] %s', method, url)
|
|
28
|
+
|
|
29
|
+
const isoRequest: IsomorphicRequest = {
|
|
30
|
+
id: uuidv4(),
|
|
31
|
+
url: new URL(url, location.origin),
|
|
32
|
+
method: method,
|
|
33
|
+
headers: new Headers(init?.headers || {}),
|
|
34
|
+
credentials: init?.credentials || 'same-origin',
|
|
35
|
+
body: await ref.text(),
|
|
36
|
+
}
|
|
37
|
+
debug('isomorphic request', isoRequest)
|
|
38
|
+
observer.emit('request', isoRequest)
|
|
39
|
+
|
|
40
|
+
debug('awaiting for the mocked response...')
|
|
41
|
+
const response = await resolver(isoRequest, ref)
|
|
42
|
+
debug('mocked response', response)
|
|
43
|
+
|
|
44
|
+
if (response) {
|
|
45
|
+
const isomorphicResponse = toIsoResponse(response)
|
|
46
|
+
debug('derived isomorphic response', isomorphicResponse)
|
|
47
|
+
|
|
48
|
+
observer.emit('response', isoRequest, isomorphicResponse)
|
|
49
|
+
|
|
50
|
+
return new Response(response.body, {
|
|
51
|
+
...isomorphicResponse,
|
|
52
|
+
// `Response.headers` cannot be instantiated with the `Headers` polyfill.
|
|
53
|
+
// Apparently, it halts if the `Headers` class contains unknown properties
|
|
54
|
+
// (i.e. the internal `Headers.map`).
|
|
55
|
+
headers: flattenHeadersObject(response.headers || {}),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
debug('no mocked response found, bypassing...')
|
|
60
|
+
|
|
61
|
+
return pureFetch(input, init).then(async (response) => {
|
|
62
|
+
const cloneResponse = response.clone()
|
|
63
|
+
debug('original fetch performed', cloneResponse)
|
|
64
|
+
|
|
65
|
+
observer.emit(
|
|
66
|
+
'response',
|
|
67
|
+
isoRequest,
|
|
68
|
+
await normalizeFetchResponse(cloneResponse)
|
|
69
|
+
)
|
|
70
|
+
return response
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
debug('restoring modules...')
|
|
76
|
+
window.fetch = pureFetch
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function normalizeFetchResponse(
|
|
81
|
+
response: Response
|
|
82
|
+
): Promise<IsomorphicResponse> {
|
|
83
|
+
return {
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
headers: objectToHeaders(headersToObject(response.headers)),
|
|
87
|
+
body: await response.text(),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { interceptXMLHttpRequest } from '../interceptors/XMLHttpRequest'
|
|
2
|
+
import { interceptFetch } from '../interceptors/fetch'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The default preset provisions the interception of requests
|
|
6
|
+
* regardless of their type (fetch/XMLHttpRequest).
|
|
7
|
+
*/
|
|
8
|
+
export default [interceptXMLHttpRequest, interceptFetch]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { interceptClientRequest } from '../interceptors/ClientRequest'
|
|
2
|
+
import { interceptXMLHttpRequest } from '../interceptors/XMLHttpRequest'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The default preset provisions the interception of requests
|
|
6
|
+
* regardless of their type (http/https/XMLHttpRequest).
|
|
7
|
+
*/
|
|
8
|
+
export default [interceptClientRequest, interceptXMLHttpRequest]
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ChildProcess, Serializable } from 'child_process'
|
|
2
|
+
import { Headers } from 'headers-utils'
|
|
3
|
+
import { invariant } from 'outvariant'
|
|
4
|
+
import { StrictEventEmitter } from 'strict-event-emitter'
|
|
5
|
+
import {
|
|
6
|
+
createInterceptor,
|
|
7
|
+
InterceptorApi,
|
|
8
|
+
InterceptorEventsMap,
|
|
9
|
+
InterceptorOptions,
|
|
10
|
+
IsomorphicRequest,
|
|
11
|
+
Resolver,
|
|
12
|
+
} from './createInterceptor'
|
|
13
|
+
import { toIsoResponse } from './utils/toIsoResponse'
|
|
14
|
+
|
|
15
|
+
type ProcessEventListener = (message: Serializable, ...args: any[]) => void
|
|
16
|
+
|
|
17
|
+
export type CreateRemoteInterceptorOptions = Omit<
|
|
18
|
+
InterceptorOptions,
|
|
19
|
+
'resolver'
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export type RemoteResolverApi = Pick<InterceptorApi, 'on'>
|
|
23
|
+
|
|
24
|
+
export interface CreateRemoteResolverOptions {
|
|
25
|
+
process: ChildProcess
|
|
26
|
+
resolver: Resolver
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requestReviver(key: string, value: any) {
|
|
30
|
+
switch (key) {
|
|
31
|
+
case 'url':
|
|
32
|
+
return new URL(value)
|
|
33
|
+
|
|
34
|
+
case 'headers':
|
|
35
|
+
return new Headers(value)
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a remote request interceptor that delegates
|
|
44
|
+
* the mocked response resolution to the parent process.
|
|
45
|
+
* The parent process must establish a remote resolver
|
|
46
|
+
* by calling `createRemoteResolver` function.
|
|
47
|
+
*/
|
|
48
|
+
export function createRemoteInterceptor(
|
|
49
|
+
options: CreateRemoteInterceptorOptions
|
|
50
|
+
): InterceptorApi {
|
|
51
|
+
invariant(
|
|
52
|
+
process.connected,
|
|
53
|
+
`Failed to create a remote interceptor: the current process (%s) does not have a parent. Please make sure you're spawning this process as a child process in order to use remote request interception.`,
|
|
54
|
+
process.pid
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (typeof process.send === 'undefined') {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`\
|
|
60
|
+
Failed to create a remote interceptor: the current process (${process.pid}) does not have the IPC enabled. Please make sure you're spawning this process with the "ipc" stdio value set:
|
|
61
|
+
|
|
62
|
+
spawn('node', ['module.js'], { stdio: ['ipc'] })\
|
|
63
|
+
`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let handleParentMessage: ProcessEventListener
|
|
68
|
+
|
|
69
|
+
const interceptor = createInterceptor({
|
|
70
|
+
...options,
|
|
71
|
+
resolver(request) {
|
|
72
|
+
const serializedRequest = JSON.stringify(request)
|
|
73
|
+
process.send?.(`request:${serializedRequest}`)
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
handleParentMessage = (message: Serializable) => {
|
|
77
|
+
if (typeof message !== 'string') {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (message.startsWith(`response:${request.id}`)) {
|
|
82
|
+
const [, responseString] =
|
|
83
|
+
message.match(/^response:.+?:(.+)$/) || []
|
|
84
|
+
|
|
85
|
+
if (!responseString) {
|
|
86
|
+
return resolve()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mockedResponse = JSON.parse(responseString)
|
|
90
|
+
|
|
91
|
+
return resolve(mockedResponse)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
process.addListener('message', handleParentMessage)
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...interceptor,
|
|
102
|
+
restore() {
|
|
103
|
+
interceptor.restore()
|
|
104
|
+
process.removeListener('message', handleParentMessage)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a response resolver function attached to the given `ChildProcess`.
|
|
111
|
+
* The child process must establish a remote interceptor by calling `createRemoteInterceptor` function.
|
|
112
|
+
*/
|
|
113
|
+
export function createRemoteResolver(
|
|
114
|
+
options: CreateRemoteResolverOptions
|
|
115
|
+
): RemoteResolverApi {
|
|
116
|
+
const observer = new StrictEventEmitter<InterceptorEventsMap>()
|
|
117
|
+
|
|
118
|
+
const handleChildMessage: ProcessEventListener = async (message) => {
|
|
119
|
+
if (typeof message !== 'string') {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (message.startsWith('request:')) {
|
|
124
|
+
const [, requestString] = message.match(/^request:(.+)$/) || []
|
|
125
|
+
|
|
126
|
+
if (!requestString) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isoRequest: IsomorphicRequest = JSON.parse(
|
|
131
|
+
requestString,
|
|
132
|
+
requestReviver
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
observer.emit('request', isoRequest)
|
|
136
|
+
|
|
137
|
+
// Retrieve the mocked response.
|
|
138
|
+
const mockedResponse = await options.resolver(
|
|
139
|
+
isoRequest,
|
|
140
|
+
undefined as any
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Send the mocked response to the child process.
|
|
144
|
+
const serializedResponse = JSON.stringify(mockedResponse)
|
|
145
|
+
options.process.send(
|
|
146
|
+
`response:${isoRequest.id}:${serializedResponse}`,
|
|
147
|
+
(error) => {
|
|
148
|
+
if (error) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (mockedResponse) {
|
|
153
|
+
// Emit an optimisting "response" event at this point,
|
|
154
|
+
// not to rely on the back-and-forth signaling for the sake of the event.
|
|
155
|
+
observer.emit('response', isoRequest, toIsoResponse(mockedResponse))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
options.process.removeListener('message', handleChildMessage)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
options.process.addListener('message', handleChildMessage)
|
|
167
|
+
options.process.addListener('disconnect', cleanup)
|
|
168
|
+
options.process.addListener('error', cleanup)
|
|
169
|
+
options.process.addListener('exit', cleanup)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
on(event, listener) {
|
|
173
|
+
observer.addListener(event, listener)
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cloneObject } from './cloneObject'
|
|
2
|
+
|
|
3
|
+
test('clones a shallow object', () => {
|
|
4
|
+
const original = { a: 1, b: 2, c: [1, 2, 3] }
|
|
5
|
+
const clone = cloneObject(original)
|
|
6
|
+
|
|
7
|
+
expect(clone).toEqual(original)
|
|
8
|
+
|
|
9
|
+
clone.a = 5
|
|
10
|
+
clone.b = 6
|
|
11
|
+
clone.c = [5, 6, 7]
|
|
12
|
+
|
|
13
|
+
expect(clone).toHaveProperty('a', 5)
|
|
14
|
+
expect(clone).toHaveProperty('b', 6)
|
|
15
|
+
expect(clone).toHaveProperty('c', [5, 6, 7])
|
|
16
|
+
expect(original).toHaveProperty('a', 1)
|
|
17
|
+
expect(original).toHaveProperty('b', 2)
|
|
18
|
+
expect(original).toHaveProperty('c', [1, 2, 3])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('clones a nested object', () => {
|
|
22
|
+
const original = { a: { b: 1 }, c: { d: { e: 2 } } }
|
|
23
|
+
const clone = cloneObject(original)
|
|
24
|
+
|
|
25
|
+
expect(clone).toEqual(original)
|
|
26
|
+
|
|
27
|
+
clone.a.b = 10
|
|
28
|
+
clone.c.d.e = 20
|
|
29
|
+
|
|
30
|
+
expect(clone).toHaveProperty(['a', 'b'], 10)
|
|
31
|
+
expect(clone).toHaveProperty(['c', 'd', 'e'], 20)
|
|
32
|
+
expect(original).toHaveProperty(['a', 'b'], 1)
|
|
33
|
+
expect(original).toHaveProperty(['c', 'd', 'e'], 2)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('clones a class instance', () => {
|
|
37
|
+
class Car {
|
|
38
|
+
public manufacturer: string
|
|
39
|
+
constructor() {
|
|
40
|
+
this.manufacturer = 'Audi'
|
|
41
|
+
}
|
|
42
|
+
getManufacturer() {
|
|
43
|
+
return this.manufacturer
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const car = new Car()
|
|
48
|
+
const clone = cloneObject(car)
|
|
49
|
+
|
|
50
|
+
expect(clone).toHaveProperty('manufacturer', 'Audi')
|
|
51
|
+
expect(clone).toHaveProperty('getManufacturer')
|
|
52
|
+
expect(clone.getManufacturer).toBeInstanceOf(Function)
|
|
53
|
+
expect(clone.getManufacturer()).toEqual('Audi')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('ignores nested class instances', () => {
|
|
57
|
+
class Car {
|
|
58
|
+
name: string
|
|
59
|
+
constructor(name: string) {
|
|
60
|
+
this.name = name
|
|
61
|
+
}
|
|
62
|
+
getName() {
|
|
63
|
+
return this.name
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const original = {
|
|
67
|
+
a: 1,
|
|
68
|
+
car: new Car('Audi'),
|
|
69
|
+
}
|
|
70
|
+
const clone = cloneObject(original)
|
|
71
|
+
|
|
72
|
+
expect(clone).toEqual(original)
|
|
73
|
+
expect(clone.car).toBeInstanceOf(Car)
|
|
74
|
+
expect(clone.car.getName()).toEqual('Audi')
|
|
75
|
+
|
|
76
|
+
clone.car = new Car('BMW')
|
|
77
|
+
|
|
78
|
+
expect(clone.car).toBeInstanceOf(Car)
|
|
79
|
+
expect(clone.car.getName()).toEqual('BMW')
|
|
80
|
+
expect(original.car).toBeInstanceOf(Car)
|
|
81
|
+
expect(original.car.getName()).toEqual('Audi')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('clones an object with null prototype', () => {
|
|
85
|
+
const original = {
|
|
86
|
+
key: Object.create(null),
|
|
87
|
+
}
|
|
88
|
+
const clone = cloneObject(original)
|
|
89
|
+
|
|
90
|
+
expect(clone).toEqual({
|
|
91
|
+
key: {},
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const debug = require('debug')('cloneObject')
|
|
2
|
+
|
|
3
|
+
function isPlainObject(obj?: Record<string, any>): boolean {
|
|
4
|
+
debug('is plain object?', obj)
|
|
5
|
+
|
|
6
|
+
if (obj == null || !obj.constructor?.name) {
|
|
7
|
+
debug('given object is undefined, not a plain object...')
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
debug('checking the object constructor:', obj.constructor.name)
|
|
12
|
+
return obj.constructor.name === 'Object'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function cloneObject<ObjectType extends Record<string, any>>(
|
|
16
|
+
obj: ObjectType
|
|
17
|
+
): ObjectType {
|
|
18
|
+
debug('cloning object:', obj)
|
|
19
|
+
|
|
20
|
+
const enumerableProperties = Object.entries(obj).reduce<Record<string, any>>(
|
|
21
|
+
(acc, [key, value]) => {
|
|
22
|
+
debug('analyzing key-value pair:', key, value)
|
|
23
|
+
|
|
24
|
+
// Recursively clone only plain objects, omitting class instances.
|
|
25
|
+
acc[key] = isPlainObject(value) ? cloneObject(value) : value
|
|
26
|
+
return acc
|
|
27
|
+
},
|
|
28
|
+
{}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return isPlainObject(obj)
|
|
32
|
+
? enumerableProperties
|
|
33
|
+
: Object.assign(Object.getPrototypeOf(obj), enumerableProperties)
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getCleanUrl } from './getCleanUrl'
|
|
2
|
+
|
|
3
|
+
describe('getCleanUrl', () => {
|
|
4
|
+
describe('given a URL without query parameters', () => {
|
|
5
|
+
test('should return url href as-is', () => {
|
|
6
|
+
const url = new URL('https://github.com')
|
|
7
|
+
expect(getCleanUrl(url)).toEqual('https://github.com/')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('given a URL with query parameters', () => {
|
|
12
|
+
test('should return url without parameters', () => {
|
|
13
|
+
const url = new URL('https://github.com/mswjs/?userId=abc-123')
|
|
14
|
+
expect(getCleanUrl(url)).toEqual('https://github.com/mswjs/')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('given a URL with a hash', () => {
|
|
19
|
+
test('should return a url without hash', () => {
|
|
20
|
+
const url = new URL('https://github.com/mswjs/#hello-world')
|
|
21
|
+
expect(getCleanUrl(url)).toEqual('https://github.com/mswjs/')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('given an absolute URL ', () => {
|
|
26
|
+
test('should return a clean relative URL', () => {
|
|
27
|
+
const url = new URL('/login?query=value', 'https://github.com')
|
|
28
|
+
expect(getCleanUrl(url, false)).toEqual('/login')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RequestOptions } from 'http'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a URL instance into the RequestOptions object expected by
|
|
5
|
+
* the `ClientRequest` class.
|
|
6
|
+
* @see https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/internal/url.js#L1257
|
|
7
|
+
*/
|
|
8
|
+
export function getRequestOptionsByUrl(url: URL): RequestOptions {
|
|
9
|
+
const options: RequestOptions = {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
protocol: url.protocol,
|
|
12
|
+
hostname:
|
|
13
|
+
typeof url.hostname === 'string' && url.hostname.startsWith('[')
|
|
14
|
+
? url.hostname.slice(1, -1)
|
|
15
|
+
: url.hostname,
|
|
16
|
+
host: url.host,
|
|
17
|
+
path: `${url.pathname}${url.search || ''}`,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!!url.port) {
|
|
21
|
+
options.port = Number(url.port)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (url.username || url.password) {
|
|
25
|
+
options.auth = `${url.username}:${url.password}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return options
|
|
29
|
+
}
|