@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.
Files changed (100) hide show
  1. package/README.md +64 -15
  2. package/lib/createInterceptor.d.ts +7 -0
  3. package/lib/createInterceptor.js.map +1 -1
  4. package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +38 -0
  5. package/lib/interceptors/ClientRequest/NodeClientRequest.js +420 -0
  6. package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -0
  7. package/lib/interceptors/ClientRequest/http.get.d.ts +5 -0
  8. package/lib/interceptors/ClientRequest/http.get.js +47 -0
  9. package/lib/interceptors/ClientRequest/http.get.js.map +1 -0
  10. package/lib/interceptors/ClientRequest/http.request.d.ts +5 -0
  11. package/lib/interceptors/ClientRequest/http.request.js +44 -0
  12. package/lib/interceptors/ClientRequest/http.request.js.map +1 -0
  13. package/lib/interceptors/ClientRequest/index.d.ts +5 -1
  14. package/lib/interceptors/ClientRequest/index.js +50 -84
  15. package/lib/interceptors/ClientRequest/index.js.map +1 -1
  16. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.d.ts +7 -0
  17. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js +77 -0
  18. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js.map +1 -0
  19. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.d.ts +1 -1
  20. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +21 -12
  21. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
  22. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.d.ts +17 -0
  23. package/lib/interceptors/ClientRequest/utils/{normalizeHttpRequestParams.js → normalizeClientRequestArgs.js} +65 -39
  24. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js.map +1 -0
  25. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.d.ts +15 -0
  26. package/lib/interceptors/ClientRequest/utils/{normalizeHttpRequestEndParams.js → normalizeClientRequestEndArgs.js} +5 -6
  27. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.js.map +1 -0
  28. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.d.ts +13 -0
  29. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js +20 -0
  30. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js.map +1 -0
  31. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +3 -2
  32. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
  33. package/lib/interceptors/fetch/index.js +6 -4
  34. package/lib/interceptors/fetch/index.js.map +1 -1
  35. package/lib/utils/getUrlByRequestOptions.d.ts +5 -4
  36. package/lib/utils/getUrlByRequestOptions.js.map +1 -1
  37. package/package.json +33 -18
  38. package/src/createInterceptor.ts +100 -0
  39. package/src/index.ts +5 -0
  40. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +283 -0
  41. package/src/interceptors/ClientRequest/NodeClientRequest.ts +377 -0
  42. package/src/interceptors/ClientRequest/http.get.ts +32 -0
  43. package/src/interceptors/ClientRequest/http.request.ts +29 -0
  44. package/src/interceptors/ClientRequest/index.ts +61 -0
  45. package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +16 -0
  46. package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +7 -0
  47. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +20 -0
  48. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +41 -0
  49. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +13 -0
  50. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +10 -0
  51. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +44 -0
  52. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +38 -0
  53. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +336 -0
  54. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +205 -0
  55. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +40 -0
  56. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +51 -0
  57. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +35 -0
  58. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +36 -0
  59. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +565 -0
  60. package/src/interceptors/XMLHttpRequest/index.ts +34 -0
  61. package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
  62. package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
  63. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +11 -0
  64. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +16 -0
  65. package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +27 -0
  66. package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
  67. package/src/interceptors/fetch/index.ts +89 -0
  68. package/src/presets/browser.ts +8 -0
  69. package/src/presets/node.ts +8 -0
  70. package/src/remote.ts +176 -0
  71. package/src/utils/cloneObject.test.ts +93 -0
  72. package/src/utils/cloneObject.ts +34 -0
  73. package/src/utils/getCleanUrl.test.ts +31 -0
  74. package/src/utils/getCleanUrl.ts +6 -0
  75. package/src/utils/getRequestOptionsByUrl.ts +29 -0
  76. package/src/utils/getUrlByRequestOptions.test.ts +140 -0
  77. package/src/utils/getUrlByRequestOptions.ts +108 -0
  78. package/src/utils/isObject.test.ts +19 -0
  79. package/src/utils/isObject.ts +6 -0
  80. package/src/utils/parseJson.test.ts +9 -0
  81. package/src/utils/parseJson.ts +12 -0
  82. package/src/utils/toIsoResponse.test.ts +39 -0
  83. package/src/utils/toIsoResponse.ts +14 -0
  84. package/src/utils/uuid.ts +7 -0
  85. package/lib/interceptors/ClientRequest/ClientRequest.glossary.d.ts +0 -6
  86. package/lib/interceptors/ClientRequest/ClientRequest.glossary.js +0 -3
  87. package/lib/interceptors/ClientRequest/ClientRequest.glossary.js.map +0 -1
  88. package/lib/interceptors/ClientRequest/createClientRequestOverride.d.ts +0 -12
  89. package/lib/interceptors/ClientRequest/createClientRequestOverride.js +0 -340
  90. package/lib/interceptors/ClientRequest/createClientRequestOverride.js.map +0 -1
  91. package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.d.ts +0 -49
  92. package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.js +0 -118
  93. package/lib/interceptors/ClientRequest/polyfills/SocketPolyfill.js.map +0 -1
  94. package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.d.ts +0 -3
  95. package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.js +0 -34
  96. package/lib/interceptors/ClientRequest/utils/inheritRequestHeaders.js.map +0 -1
  97. package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestEndParams.d.ts +0 -17
  98. package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestEndParams.js.map +0 -1
  99. package/lib/interceptors/ClientRequest/utils/normalizeHttpRequestParams.d.ts +0 -11
  100. 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,6 @@
1
+ /**
2
+ * Removes query parameters and hashes from a given URL.
3
+ */
4
+ export function getCleanUrl(url: URL, isAbsolute: boolean = true): string {
5
+ return [isAbsolute && url.origin, url.pathname].filter(Boolean).join('')
6
+ }
@@ -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
+ }