@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,152 @@
|
|
|
1
|
+
import { Agent } from 'http'
|
|
2
|
+
import { RequestOptions, Agent as HttpsAgent } from 'https'
|
|
3
|
+
import { Logger } from '@open-draft/logger'
|
|
4
|
+
|
|
5
|
+
const logger = new Logger('utils getUrlByRequestOptions')
|
|
6
|
+
|
|
7
|
+
// Request instance constructed by the "request" library
|
|
8
|
+
// has a "self" property that has a "uri" field. This is
|
|
9
|
+
// reproducible by performing a "XMLHttpRequest" request in JSDOM.
|
|
10
|
+
export interface RequestSelf {
|
|
11
|
+
uri?: URL
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ResolvedRequestOptions = RequestOptions & RequestSelf
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_PATH = '/'
|
|
17
|
+
const DEFAULT_PROTOCOL = 'http:'
|
|
18
|
+
const DEFAULT_HOSTNAME = 'localhost'
|
|
19
|
+
const SSL_PORT = 443
|
|
20
|
+
|
|
21
|
+
function getAgent(
|
|
22
|
+
options: ResolvedRequestOptions
|
|
23
|
+
): Agent | HttpsAgent | undefined {
|
|
24
|
+
return options.agent instanceof Agent ? options.agent : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getProtocolByRequestOptions(options: ResolvedRequestOptions): string {
|
|
28
|
+
if (options.protocol) {
|
|
29
|
+
return options.protocol
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const agent = getAgent(options)
|
|
33
|
+
const agentProtocol = (agent as RequestOptions)?.protocol
|
|
34
|
+
|
|
35
|
+
if (agentProtocol) {
|
|
36
|
+
return agentProtocol
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const port = getPortByRequestOptions(options)
|
|
40
|
+
const isSecureRequest = options.cert || port === SSL_PORT
|
|
41
|
+
|
|
42
|
+
return isSecureRequest ? 'https:' : options.uri?.protocol || DEFAULT_PROTOCOL
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getPortByRequestOptions(
|
|
46
|
+
options: ResolvedRequestOptions
|
|
47
|
+
): number | undefined {
|
|
48
|
+
// Use the explicitly provided port.
|
|
49
|
+
if (options.port) {
|
|
50
|
+
return Number(options.port)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Otherwise, try to resolve port from the agent.
|
|
54
|
+
const agent = getAgent(options)
|
|
55
|
+
|
|
56
|
+
if ((agent as HttpsAgent)?.options.port) {
|
|
57
|
+
return Number((agent as HttpsAgent).options.port)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ((agent as RequestOptions)?.defaultPort) {
|
|
61
|
+
return Number((agent as RequestOptions).defaultPort)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Lastly, return undefined indicating that the port
|
|
65
|
+
// must inferred from the protocol. Do not infer it here.
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface RequestAuth {
|
|
70
|
+
username: string
|
|
71
|
+
password: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getAuthByRequestOptions(
|
|
75
|
+
options: ResolvedRequestOptions
|
|
76
|
+
): RequestAuth | undefined {
|
|
77
|
+
if (options.auth) {
|
|
78
|
+
const [username, password] = options.auth.split(':')
|
|
79
|
+
return { username, password }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns true if host looks like an IPv6 address without surrounding brackets
|
|
85
|
+
* It assumes any host containing `:` is definitely not IPv4 and probably IPv6,
|
|
86
|
+
* but note that this could include invalid IPv6 addresses as well.
|
|
87
|
+
*/
|
|
88
|
+
function isRawIPv6Address(host: string): boolean {
|
|
89
|
+
return host.includes(':') && !host.startsWith('[') && !host.endsWith(']')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getHostname(options: ResolvedRequestOptions): string | undefined {
|
|
93
|
+
let host = options.hostname || options.host
|
|
94
|
+
|
|
95
|
+
if (host) {
|
|
96
|
+
if (isRawIPv6Address(host)) {
|
|
97
|
+
host = `[${host}]`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check the presence of the port, and if it's present,
|
|
101
|
+
// remove it from the host, returning a hostname.
|
|
102
|
+
return new URL(`http://${host}`).hostname
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return DEFAULT_HOSTNAME
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a `URL` instance from a given `RequestOptions` object.
|
|
110
|
+
*/
|
|
111
|
+
export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL {
|
|
112
|
+
logger.info('request options', options)
|
|
113
|
+
|
|
114
|
+
if (options.uri) {
|
|
115
|
+
logger.info(
|
|
116
|
+
'constructing url from explicitly provided "options.uri": %s',
|
|
117
|
+
options.uri
|
|
118
|
+
)
|
|
119
|
+
return new URL(options.uri.href)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.info('figuring out url from request options...')
|
|
123
|
+
|
|
124
|
+
const protocol = getProtocolByRequestOptions(options)
|
|
125
|
+
logger.info('protocol', protocol)
|
|
126
|
+
|
|
127
|
+
const port = getPortByRequestOptions(options)
|
|
128
|
+
logger.info('port', port)
|
|
129
|
+
|
|
130
|
+
const hostname = getHostname(options)
|
|
131
|
+
logger.info('hostname', hostname)
|
|
132
|
+
|
|
133
|
+
const path = options.path || DEFAULT_PATH
|
|
134
|
+
logger.info('path', path)
|
|
135
|
+
|
|
136
|
+
const credentials = getAuthByRequestOptions(options)
|
|
137
|
+
logger.info('credentials', credentials)
|
|
138
|
+
|
|
139
|
+
const authString = credentials
|
|
140
|
+
? `${credentials.username}:${credentials.password}@`
|
|
141
|
+
: ''
|
|
142
|
+
logger.info('auth string:', authString)
|
|
143
|
+
|
|
144
|
+
const portString = typeof port !== 'undefined' ? `:${port}` : ''
|
|
145
|
+
const url = new URL(`${protocol}//${hostname}${portString}${path}`)
|
|
146
|
+
url.username = credentials?.username || ''
|
|
147
|
+
url.password = credentials?.password || ''
|
|
148
|
+
|
|
149
|
+
logger.info('created url:', url)
|
|
150
|
+
|
|
151
|
+
return url
|
|
152
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { it, expect } from 'vitest'
|
|
2
|
+
import { getValueBySymbol } from './getValueBySymbol'
|
|
3
|
+
|
|
4
|
+
it('returns undefined given a non-existing symbol', () => {
|
|
5
|
+
expect(getValueBySymbol('non-existing', {})).toBeUndefined()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('returns value behind the given symbol', () => {
|
|
9
|
+
const symbol = Symbol('kInternal')
|
|
10
|
+
|
|
11
|
+
expect(getValueBySymbol('kInternal', { [symbol]: null })).toBe(null)
|
|
12
|
+
expect(getValueBySymbol('kInternal', { [symbol]: true })).toBe(true)
|
|
13
|
+
expect(getValueBySymbol('kInternal', { [symbol]: 'value' })).toBe('value')
|
|
14
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the value behind the symbol with the given name.
|
|
3
|
+
*/
|
|
4
|
+
export function getValueBySymbol<T>(
|
|
5
|
+
symbolName: string,
|
|
6
|
+
source: object
|
|
7
|
+
): T | undefined {
|
|
8
|
+
const ownSymbols = Object.getOwnPropertySymbols(source)
|
|
9
|
+
|
|
10
|
+
const symbol = ownSymbols.find((symbol) => {
|
|
11
|
+
return symbol.description === symbolName
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
if (symbol) {
|
|
15
|
+
return Reflect.get(source, symbol)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return
|
|
19
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Emitter } from 'strict-event-emitter'
|
|
2
|
+
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
3
|
+
import { until } from '@open-draft/until'
|
|
4
|
+
import type { HttpRequestEventMap } from '../glossary'
|
|
5
|
+
import { emitAsync } from './emitAsync'
|
|
6
|
+
import { RequestController } from '../RequestController'
|
|
7
|
+
import {
|
|
8
|
+
createServerErrorResponse,
|
|
9
|
+
isResponseError,
|
|
10
|
+
isResponseLike,
|
|
11
|
+
} from './responseUtils'
|
|
12
|
+
import { InterceptorError } from '../InterceptorError'
|
|
13
|
+
import { isNodeLikeError } from './isNodeLikeError'
|
|
14
|
+
import { isObject } from './isObject'
|
|
15
|
+
|
|
16
|
+
interface HandleRequestOptions {
|
|
17
|
+
requestId: string
|
|
18
|
+
request: Request
|
|
19
|
+
emitter: Emitter<HttpRequestEventMap>
|
|
20
|
+
controller: RequestController
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function handleRequest(
|
|
24
|
+
options: HandleRequestOptions
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const handleResponse = async (
|
|
27
|
+
response: Response | Error | Record<string, any>
|
|
28
|
+
) => {
|
|
29
|
+
if (response instanceof Error) {
|
|
30
|
+
await options.controller.errorWith(response)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle "Response.error()" instances.
|
|
35
|
+
if (isResponseError(response)) {
|
|
36
|
+
await options.controller.respondWith(response)
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle normal responses or response-like objects.
|
|
42
|
+
* @note This must come before the arbitrary object check
|
|
43
|
+
* since Response instances are, in fact, objects.
|
|
44
|
+
*/
|
|
45
|
+
if (isResponseLike(response)) {
|
|
46
|
+
await options.controller.respondWith(response)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle arbitrary objects provided to `.errorWith(reason)`.
|
|
51
|
+
if (isObject(response)) {
|
|
52
|
+
await options.controller.errorWith(response)
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleResponseError = async (error: unknown): Promise<boolean> => {
|
|
60
|
+
// Forward the special interceptor error instances
|
|
61
|
+
// to the developer. These must not be handled in any way.
|
|
62
|
+
if (error instanceof InterceptorError) {
|
|
63
|
+
throw result.error
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Support mocking Node.js-like errors.
|
|
67
|
+
if (isNodeLikeError(error)) {
|
|
68
|
+
await options.controller.errorWith(error)
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle thrown responses.
|
|
73
|
+
if (error instanceof Response) {
|
|
74
|
+
return await handleResponse(error)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add the last "request" listener to check if the request
|
|
81
|
+
// has been handled in any way. If it hasn't, resolve the
|
|
82
|
+
// response promise with undefined.
|
|
83
|
+
// options.emitter.once('request', async ({ requestId: pendingRequestId }) => {
|
|
84
|
+
// if (
|
|
85
|
+
// pendingRequestId === options.requestId &&
|
|
86
|
+
// options.controller.readyState === RequestController.PENDING
|
|
87
|
+
// ) {
|
|
88
|
+
// await options.controller.passthrough()
|
|
89
|
+
// }
|
|
90
|
+
// })
|
|
91
|
+
|
|
92
|
+
const requestAbortPromise = new DeferredPromise<void, unknown>()
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @note `signal` is not always defined in React Native.
|
|
96
|
+
*/
|
|
97
|
+
if (options.request.signal) {
|
|
98
|
+
if (options.request.signal.aborted) {
|
|
99
|
+
await options.controller.errorWith(options.request.signal.reason)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
options.request.signal.addEventListener(
|
|
104
|
+
'abort',
|
|
105
|
+
() => {
|
|
106
|
+
requestAbortPromise.reject(options.request.signal.reason)
|
|
107
|
+
},
|
|
108
|
+
{ once: true }
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await until(async () => {
|
|
113
|
+
// Emit the "request" event and wait until all the listeners
|
|
114
|
+
// for that event are finished (e.g. async listeners awaited).
|
|
115
|
+
// By the end of this promise, the developer cannot affect the
|
|
116
|
+
// request anymore.
|
|
117
|
+
const requestListenersPromise = emitAsync(options.emitter, 'request', {
|
|
118
|
+
requestId: options.requestId,
|
|
119
|
+
request: options.request,
|
|
120
|
+
controller: options.controller,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
await Promise.race([
|
|
124
|
+
// Short-circuit the request handling promise if the request gets aborted.
|
|
125
|
+
requestAbortPromise,
|
|
126
|
+
requestListenersPromise,
|
|
127
|
+
options.controller.handled,
|
|
128
|
+
])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Handle the request being aborted while waiting for the request listeners.
|
|
132
|
+
if (requestAbortPromise.state === 'rejected') {
|
|
133
|
+
await options.controller.errorWith(requestAbortPromise.rejectionReason)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (result.error) {
|
|
138
|
+
// Handle the error during the request listener execution.
|
|
139
|
+
// These can be thrown responses or request errors.
|
|
140
|
+
if (await handleResponseError(result.error)) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If the developer has added "unhandledException" listeners,
|
|
145
|
+
// allow them to handle the error. They can translate it to a
|
|
146
|
+
// mocked response, network error, or forward it as-is.
|
|
147
|
+
if (options.emitter.listenerCount('unhandledException') > 0) {
|
|
148
|
+
// Create a new request controller just for the unhandled exception case.
|
|
149
|
+
// This is needed because the original controller might have been already
|
|
150
|
+
// interacted with (e.g. "respondWith" or "errorWith" called on it).
|
|
151
|
+
const unhandledExceptionController = new RequestController(
|
|
152
|
+
options.request,
|
|
153
|
+
{
|
|
154
|
+
/**
|
|
155
|
+
* @note Intentionally empty passthrough handle.
|
|
156
|
+
* This controller is created within another controller and we only need
|
|
157
|
+
* to know if `unhandledException` listeners handled the request.
|
|
158
|
+
*/
|
|
159
|
+
passthrough() {},
|
|
160
|
+
async respondWith(response) {
|
|
161
|
+
await handleResponse(response)
|
|
162
|
+
},
|
|
163
|
+
async errorWith(reason) {
|
|
164
|
+
/**
|
|
165
|
+
* @note Handle the result of the unhandled controller
|
|
166
|
+
* in the same way as the original request controller.
|
|
167
|
+
* The exception here is that thrown errors within the
|
|
168
|
+
* "unhandledException" event do NOT result in another
|
|
169
|
+
* emit of the same event. They are forwarded as-is.
|
|
170
|
+
*/
|
|
171
|
+
await options.controller.errorWith(reason)
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
await emitAsync(options.emitter, 'unhandledException', {
|
|
177
|
+
error: result.error,
|
|
178
|
+
request: options.request,
|
|
179
|
+
requestId: options.requestId,
|
|
180
|
+
controller: unhandledExceptionController,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// If all the "unhandledException" listeners have finished
|
|
184
|
+
// but have not handled the request in any way, passthrough.
|
|
185
|
+
if (
|
|
186
|
+
unhandledExceptionController.readyState !== RequestController.PENDING
|
|
187
|
+
) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.
|
|
193
|
+
await options.controller.respondWith(
|
|
194
|
+
createServerErrorResponse(result.error)
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If the request hasn't been handled by this point, passthrough.
|
|
200
|
+
if (options.controller.readyState === RequestController.PENDING) {
|
|
201
|
+
return await options.controller.passthrough()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return options.controller.handled
|
|
205
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { vi, beforeAll, afterEach, afterAll, it, expect } from 'vitest'
|
|
2
|
+
import { hasConfigurableGlobal } from './hasConfigurableGlobal'
|
|
3
|
+
|
|
4
|
+
beforeAll(() => {
|
|
5
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.clearAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
vi.restoreAllMocks()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns true if the global property exists and is configurable', () => {
|
|
17
|
+
Object.defineProperty(global, '_existsAndConfigurable', {
|
|
18
|
+
value: 'something',
|
|
19
|
+
configurable: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(hasConfigurableGlobal('_existsAndConfigurable')).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns false if the global property does not exist', () => {
|
|
26
|
+
expect(hasConfigurableGlobal('_non-existing')).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns false for existing global with undefined as a value', () => {
|
|
30
|
+
Object.defineProperty(global, '_existsAndUndefined', {
|
|
31
|
+
value: undefined,
|
|
32
|
+
configurable: true,
|
|
33
|
+
})
|
|
34
|
+
expect(hasConfigurableGlobal('_existsAndUndefined')).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns false for existing global with null as a value', () => {
|
|
38
|
+
Object.defineProperty(global, '_existsAndNull', {
|
|
39
|
+
value: null,
|
|
40
|
+
configurable: true,
|
|
41
|
+
})
|
|
42
|
+
expect(hasConfigurableGlobal('_existsAndNull')).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns false for existing global with a getter that returns undefined', () => {
|
|
46
|
+
Object.defineProperty(global, '_existsGetterUndefined', {
|
|
47
|
+
get: () => undefined,
|
|
48
|
+
configurable: true,
|
|
49
|
+
})
|
|
50
|
+
expect(hasConfigurableGlobal('_existsGetterUndefined')).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns false and prints an error for implicitly non-configurable global property', () => {
|
|
54
|
+
Object.defineProperty(global, '_implicitlyNonConfigurable', {
|
|
55
|
+
value: 'something',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(hasConfigurableGlobal('_implicitlyNonConfigurable')).toBe(false)
|
|
59
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
60
|
+
'[MSW] Failed to apply interceptor: the global `_implicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns false and prints an error for explicitly non-configurable global property', () => {
|
|
65
|
+
Object.defineProperty(global, '_explicitlyNonConfigurable', {
|
|
66
|
+
value: 'something',
|
|
67
|
+
configurable: false,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(hasConfigurableGlobal('_explicitlyNonConfigurable')).toBe(false)
|
|
71
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
72
|
+
'[MSW] Failed to apply interceptor: the global `_explicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns false and prints an error for global property that only has a getter', () => {
|
|
77
|
+
Object.defineProperty(global, '_onlyGetter', { get: () => 'something' })
|
|
78
|
+
|
|
79
|
+
expect(hasConfigurableGlobal('_onlyGetter')).toBe(false)
|
|
80
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
81
|
+
'[MSW] Failed to apply interceptor: the global `_onlyGetter` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a boolean indicating whether the given global property
|
|
3
|
+
* is defined and is configurable.
|
|
4
|
+
*/
|
|
5
|
+
export function hasConfigurableGlobal(propertyName: string): boolean {
|
|
6
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, propertyName)
|
|
7
|
+
|
|
8
|
+
// The property is not set at all.
|
|
9
|
+
if (typeof descriptor === 'undefined') {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The property is set to a getter that returns undefined.
|
|
14
|
+
if (
|
|
15
|
+
typeof descriptor.get === 'function' &&
|
|
16
|
+
typeof descriptor.get() === 'undefined'
|
|
17
|
+
) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The property is set to a value equal to undefined.
|
|
22
|
+
if (typeof descriptor.get === 'undefined' && descriptor.value == null) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof descriptor.set === 'undefined' && !descriptor.configurable) {
|
|
27
|
+
console.error(
|
|
28
|
+
`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`
|
|
29
|
+
)
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { it, expect } from 'vitest'
|
|
2
|
+
import { isObject } from './isObject'
|
|
3
|
+
|
|
4
|
+
it('returns true given an object', () => {
|
|
5
|
+
expect(isObject({})).toBe(true)
|
|
6
|
+
expect(isObject({ a: 1 })).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns false given an object-like instance', () => {
|
|
10
|
+
expect(isObject([1])).toBe(false)
|
|
11
|
+
expect(isObject(function () {})).toBe(false)
|
|
12
|
+
expect(isObject(new Response())).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns false given a non-object instance', () => {
|
|
16
|
+
expect(isObject(null)).toBe(false)
|
|
17
|
+
expect(isObject(undefined)).toBe(false)
|
|
18
|
+
expect(isObject(false)).toBe(false)
|
|
19
|
+
expect(isObject(123)).toBe(false)
|
|
20
|
+
expect(isObject(Symbol('object Object'))).toBe(false)
|
|
21
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines if a given value is an instance of object.
|
|
3
|
+
*/
|
|
4
|
+
export function isObject<T>(value: any, loose = false): value is T {
|
|
5
|
+
return loose
|
|
6
|
+
? Object.prototype.toString.call(value).startsWith('[object ')
|
|
7
|
+
: Object.prototype.toString.call(value) === '[object Object]'
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that validates if property access is possible on an object
|
|
3
|
+
* without throwing. It returns `true` if the property access is possible
|
|
4
|
+
* and `false` otherwise.
|
|
5
|
+
*
|
|
6
|
+
* Environments like miniflare will throw on property access on certain objects
|
|
7
|
+
* like Request and Response, for unimplemented properties.
|
|
8
|
+
*/
|
|
9
|
+
export function isPropertyAccessible<Obj extends Record<string, any>>(
|
|
10
|
+
obj: Obj,
|
|
11
|
+
key: keyof Obj
|
|
12
|
+
) {
|
|
13
|
+
try {
|
|
14
|
+
obj[key]
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ClientRequest } from 'node:http'
|
|
2
|
+
import { Readable } from 'node:stream'
|
|
3
|
+
import { invariant } from 'outvariant'
|
|
4
|
+
import { getRawRequest } from '../../getRawRequest'
|
|
5
|
+
|
|
6
|
+
const kRawRequestBodyStream = Symbol('kRawRequestBodyStream')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the request body stream of the given request.
|
|
10
|
+
* @note This is only relevant in the context of `http.ClientRequest`.
|
|
11
|
+
* This function will throw if the given `request` wasn't created based on
|
|
12
|
+
* the `http.ClientRequest` instance.
|
|
13
|
+
* You must rely on the web stream consumers for other request clients.
|
|
14
|
+
*/
|
|
15
|
+
export function getClientRequestBodyStream(request: Request): Readable {
|
|
16
|
+
const rawRequest = getRawRequest(request)
|
|
17
|
+
|
|
18
|
+
invariant(
|
|
19
|
+
rawRequest instanceof ClientRequest,
|
|
20
|
+
`Failed to retrieve raw request body stream: request is not an instance of "http.ClientRequest". Note that you can only use the "getClientRequestBodyStream" function with the requests issued by "http.clientRequest".`
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const requestBodyStream = Reflect.get(request, kRawRequestBodyStream)
|
|
24
|
+
|
|
25
|
+
invariant(
|
|
26
|
+
requestBodyStream instanceof Readable,
|
|
27
|
+
'Failed to retrieve raw request body stream: corrupted stream (%s)',
|
|
28
|
+
typeof requestBodyStream
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return requestBodyStream
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function setRawRequestBodyStream(
|
|
35
|
+
request: Request,
|
|
36
|
+
stream: Readable
|
|
37
|
+
): void {
|
|
38
|
+
Reflect.set(request, kRawRequestBodyStream, stream)
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { it, expect } from 'vitest'
|
|
2
|
+
import { parseJson } from './parseJson'
|
|
3
|
+
|
|
4
|
+
it('parses a given string into JSON', () => {
|
|
5
|
+
expect(parseJson('{"id":1}')).toEqual({ id: 1 })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('returns null given invalid JSON string', () => {
|
|
9
|
+
expect(parseJson('{"o:2\'')).toBeNull()
|
|
10
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a given string into JSON.
|
|
3
|
+
* Gracefully handles invalid JSON by returning `null`.
|
|
4
|
+
*/
|
|
5
|
+
export function parseJson(data: string): Record<string, unknown> | null {
|
|
6
|
+
try {
|
|
7
|
+
const json = JSON.parse(data)
|
|
8
|
+
return json
|
|
9
|
+
} catch (_) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|