msw 2.1.3 → 2.1.5
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/lib/browser/index.js +1775 -32
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/index.mjs +1769 -28
- package/lib/browser/index.mjs.map +1 -1
- package/lib/core/handlers/GraphQLHandler.js +2 -2
- package/lib/core/handlers/GraphQLHandler.js.map +1 -1
- package/lib/core/handlers/GraphQLHandler.mjs +2 -2
- package/lib/core/handlers/GraphQLHandler.mjs.map +1 -1
- package/lib/core/handlers/HttpHandler.js +2 -2
- package/lib/core/handlers/HttpHandler.js.map +1 -1
- package/lib/core/handlers/HttpHandler.mjs +2 -2
- package/lib/core/handlers/HttpHandler.mjs.map +1 -1
- package/lib/core/sharedOptions.d.mts +0 -2
- package/lib/core/sharedOptions.d.ts +0 -2
- package/lib/core/utils/handleRequest.js +1 -1
- package/lib/core/utils/handleRequest.js.map +1 -1
- package/lib/core/utils/handleRequest.mjs +1 -1
- package/lib/core/utils/handleRequest.mjs.map +1 -1
- package/lib/core/utils/internal/parseGraphQLRequest.js +2 -2
- package/lib/core/utils/internal/parseGraphQLRequest.js.map +1 -1
- package/lib/core/utils/internal/parseGraphQLRequest.mjs +2 -2
- package/lib/core/utils/internal/parseGraphQLRequest.mjs.map +1 -1
- package/lib/core/utils/request/onUnhandledRequest.d.mts +1 -4
- package/lib/core/utils/request/onUnhandledRequest.d.ts +1 -4
- package/lib/core/utils/request/onUnhandledRequest.js +14 -115
- package/lib/core/utils/request/onUnhandledRequest.js.map +1 -1
- package/lib/core/utils/request/onUnhandledRequest.mjs +14 -107
- package/lib/core/utils/request/onUnhandledRequest.mjs.map +1 -1
- package/lib/core/utils/request/toPublicUrl.d.mts +7 -0
- package/lib/core/utils/request/toPublicUrl.d.ts +7 -0
- package/lib/core/utils/request/{getPublicUrlFromRequest.js → toPublicUrl.js} +9 -9
- package/lib/core/utils/request/toPublicUrl.js.map +1 -0
- package/lib/core/utils/request/toPublicUrl.mjs +11 -0
- package/lib/core/utils/request/toPublicUrl.mjs.map +1 -0
- package/lib/iife/index.js +46 -233
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +1 -1
- package/package.json +5 -8
- package/src/browser/setupWorker/glossary.ts +1 -0
- package/src/browser/setupWorker/setupWorker.ts +1 -0
- package/src/browser/setupWorker/start/createRequestListener.ts +9 -0
- package/src/browser/setupWorker/start/createResponseListener.ts +9 -7
- package/src/core/handlers/GraphQLHandler.ts +2 -2
- package/src/core/handlers/HttpHandler.ts +2 -2
- package/src/core/utils/handleRequest.ts +1 -1
- package/src/core/utils/internal/parseGraphQLRequest.ts +2 -2
- package/src/core/utils/request/onUnhandledRequest.test.ts +5 -101
- package/src/core/utils/request/onUnhandledRequest.ts +16 -185
- package/src/core/utils/request/toPublicUrl.test.ts +18 -0
- package/src/core/utils/request/toPublicUrl.ts +15 -0
- package/lib/core/utils/request/getPublicUrlFromRequest.d.mts +0 -7
- package/lib/core/utils/request/getPublicUrlFromRequest.d.ts +0 -7
- package/lib/core/utils/request/getPublicUrlFromRequest.js.map +0 -1
- package/lib/core/utils/request/getPublicUrlFromRequest.mjs +0 -11
- package/lib/core/utils/request/getPublicUrlFromRequest.mjs.map +0 -1
- package/src/core/utils/request/getPublicUrlFromRequest.test.ts +0 -26
- package/src/core/utils/request/getPublicUrlFromRequest.ts +0 -15
package/lib/mockServiceWorker.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "msw",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
|
|
5
5
|
"main": "./lib/core/index.js",
|
|
6
6
|
"module": "./lib/core/index.mjs",
|
|
@@ -93,13 +93,11 @@
|
|
|
93
93
|
"sideEffects": false,
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"@bundled-es-modules/cookie": "^2.0.0",
|
|
96
|
-
"@bundled-es-modules/js-levenshtein": "^2.0.1",
|
|
97
96
|
"@bundled-es-modules/statuses": "^1.0.1",
|
|
98
97
|
"@mswjs/cookies": "^1.1.0",
|
|
99
|
-
"@mswjs/interceptors": "^0.25.
|
|
98
|
+
"@mswjs/interceptors": "^0.25.15",
|
|
100
99
|
"@open-draft/until": "^2.1.0",
|
|
101
100
|
"@types/cookie": "^0.6.0",
|
|
102
|
-
"@types/js-levenshtein": "^1.1.3",
|
|
103
101
|
"@types/statuses": "^2.0.4",
|
|
104
102
|
"chalk": "^4.1.2",
|
|
105
103
|
"chokidar": "^3.4.2",
|
|
@@ -107,7 +105,6 @@
|
|
|
107
105
|
"headers-polyfill": "^4.0.2",
|
|
108
106
|
"inquirer": "^8.2.0",
|
|
109
107
|
"is-node-process": "^1.2.0",
|
|
110
|
-
"js-levenshtein": "^1.1.6",
|
|
111
108
|
"outvariant": "^1.4.2",
|
|
112
109
|
"path-to-regexp": "^6.2.0",
|
|
113
110
|
"strict-event-emitter": "^0.5.1",
|
|
@@ -192,10 +189,10 @@
|
|
|
192
189
|
"check:exports": "node \"./config/scripts/validate-esm.js\"",
|
|
193
190
|
"test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native",
|
|
194
191
|
"test:unit": "vitest",
|
|
195
|
-
"test:node": "vitest
|
|
196
|
-
"test:native": "vitest
|
|
192
|
+
"test:node": "vitest --config=./test/node/vitest.config.ts",
|
|
193
|
+
"test:native": "vitest --config=./test/native/vitest.config.ts",
|
|
197
194
|
"test:browser": "playwright test -c ./test/browser/playwright.config.ts",
|
|
198
|
-
"test:modules:node": "vitest
|
|
195
|
+
"test:modules:node": "vitest --config=./test/modules/node/vitest.config.ts",
|
|
199
196
|
"test:modules:browser": "playwright test -c ./test/modules/browser/playwright.config.ts",
|
|
200
197
|
"test:ts": "ts-node test/typings/run.ts",
|
|
201
198
|
"release": "release publish",
|
|
@@ -103,6 +103,7 @@ export interface SetupWorkerInternalContext {
|
|
|
103
103
|
worker: ServiceWorker | null
|
|
104
104
|
registration: ServiceWorkerRegistration | null
|
|
105
105
|
requestHandlers: Array<RequestHandler>
|
|
106
|
+
requests: Map<string, Request>
|
|
106
107
|
emitter: Emitter<LifeCycleEventsMap>
|
|
107
108
|
keepAliveInterval?: number
|
|
108
109
|
workerChannel: {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
WorkerChannel,
|
|
9
9
|
} from './utils/createMessageChannel'
|
|
10
10
|
import { parseWorkerRequest } from '../../utils/parseWorkerRequest'
|
|
11
|
+
import { RequestHandler } from '~/core/handlers/RequestHandler'
|
|
11
12
|
import { handleRequest } from '~/core/utils/handleRequest'
|
|
12
13
|
import { RequiredDeep } from '~/core/typeUtils'
|
|
13
14
|
import { devUtils } from '~/core/utils/internal/devUtils'
|
|
@@ -30,6 +31,14 @@ export const createRequestListener = (
|
|
|
30
31
|
const request = parseWorkerRequest(message.payload)
|
|
31
32
|
const requestCloneForLogs = request.clone()
|
|
32
33
|
|
|
34
|
+
// Make this the first requets clone before the
|
|
35
|
+
// request resolution pipeline even starts.
|
|
36
|
+
// Store the clone in cache so the first matching
|
|
37
|
+
// request handler would skip the cloning phase.
|
|
38
|
+
const requestClone = request.clone()
|
|
39
|
+
RequestHandler.cache.set(request, requestClone)
|
|
40
|
+
context.requests.set(requestId, requestClone)
|
|
41
|
+
|
|
33
42
|
try {
|
|
34
43
|
await handleRequest(
|
|
35
44
|
request,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
ServiceWorkerIncomingEventsMap,
|
|
3
3
|
SetupWorkerInternalContext,
|
|
4
4
|
} from '../glossary'
|
|
5
|
-
import { ServiceWorkerMessage } from './utils/createMessageChannel'
|
|
5
|
+
import type { ServiceWorkerMessage } from './utils/createMessageChannel'
|
|
6
6
|
import { isResponseWithoutBody } from '@mswjs/interceptors'
|
|
7
7
|
|
|
8
8
|
export function createResponseListener(context: SetupWorkerInternalContext) {
|
|
@@ -15,6 +15,12 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
|
|
15
15
|
) => {
|
|
16
16
|
const { payload: responseJson } = message
|
|
17
17
|
|
|
18
|
+
// Get the Request instance reference stored in the
|
|
19
|
+
// request listener.
|
|
20
|
+
const { requestId } = responseJson
|
|
21
|
+
const request = context.requests.get(requestId)!
|
|
22
|
+
context.requests.delete(requestId)
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* CORS requests with `mode: "no-cors"` result in "opaque" responses.
|
|
20
26
|
* That kind of responses cannot be manipulated in JavaScript due
|
|
@@ -46,11 +52,7 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
|
|
46
52
|
responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass',
|
|
47
53
|
{
|
|
48
54
|
response,
|
|
49
|
-
|
|
50
|
-
* @todo @fixme In this context, we don't know anything about
|
|
51
|
-
* the request.
|
|
52
|
-
*/
|
|
53
|
-
request: null as any,
|
|
55
|
+
request,
|
|
54
56
|
requestId: responseJson.requestId,
|
|
55
57
|
},
|
|
56
58
|
)
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
parseGraphQLRequest,
|
|
18
18
|
parseDocumentNode,
|
|
19
19
|
} from '../utils/internal/parseGraphQLRequest'
|
|
20
|
-
import {
|
|
20
|
+
import { toPublicUrl } from '../utils/request/toPublicUrl'
|
|
21
21
|
import { devUtils } from '../utils/internal/devUtils'
|
|
22
22
|
import { getAllRequestCookies } from '../utils/request/getRequestCookies'
|
|
23
23
|
|
|
@@ -200,7 +200,7 @@ export class GraphQLHandler extends RequestHandler<
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
if (!args.parsedResult.operationName && this.info.operationType !== 'all') {
|
|
203
|
-
const publicUrl =
|
|
203
|
+
const publicUrl = toPublicUrl(args.request.url)
|
|
204
204
|
|
|
205
205
|
devUtils.warn(`\
|
|
206
206
|
Failed to intercept a GraphQL request at "${args.request.method} ${publicUrl}": anonymous GraphQL operations are not supported.
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Path,
|
|
12
12
|
PathParams,
|
|
13
13
|
} from '../utils/matching/matchRequestUrl'
|
|
14
|
-
import {
|
|
14
|
+
import { toPublicUrl } from '../utils/request/toPublicUrl'
|
|
15
15
|
import { getAllRequestCookies } from '../utils/request/getRequestCookies'
|
|
16
16
|
import { cleanUrl, getSearchParams } from '../utils/url/cleanUrl'
|
|
17
17
|
import {
|
|
@@ -147,7 +147,7 @@ export class HttpHandler extends RequestHandler<
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
async log(args: { request: Request; response: Response }) {
|
|
150
|
-
const publicUrl =
|
|
150
|
+
const publicUrl = toPublicUrl(args.request.url)
|
|
151
151
|
const loggedRequest = await serializeRequest(args.request)
|
|
152
152
|
const loggedResponse = await serializeResponse(args.response)
|
|
153
153
|
const statusColor = getStatusCodeColor(loggedResponse.status)
|
|
@@ -82,7 +82,7 @@ export async function handleRequest(
|
|
|
82
82
|
// If the handler lookup returned nothing, no request handler was found
|
|
83
83
|
// matching this request. Report the request as unhandled.
|
|
84
84
|
if (!lookupResult.data) {
|
|
85
|
-
await onUnhandledRequest(request,
|
|
85
|
+
await onUnhandledRequest(request, options.onUnhandledRequest)
|
|
86
86
|
emitter.emit('request:unhandled', { request, requestId })
|
|
87
87
|
emitter.emit('request:end', { request, requestId })
|
|
88
88
|
handleRequestOptions?.onPassthroughResponse?.(request)
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from 'graphql'
|
|
6
6
|
import { parse } from 'graphql'
|
|
7
7
|
import type { GraphQLVariables } from '../../handlers/GraphQLHandler'
|
|
8
|
-
import {
|
|
8
|
+
import { toPublicUrl } from '../request/toPublicUrl'
|
|
9
9
|
import { devUtils } from './devUtils'
|
|
10
10
|
import { jsonParse } from './jsonParse'
|
|
11
11
|
import { parseMultipartData } from './parseMultipartData'
|
|
@@ -184,7 +184,7 @@ export async function parseGraphQLRequest(
|
|
|
184
184
|
const parsedResult = parseQuery(query)
|
|
185
185
|
|
|
186
186
|
if (parsedResult instanceof Error) {
|
|
187
|
-
const requestPublicUrl =
|
|
187
|
+
const requestPublicUrl = toPublicUrl(request.url)
|
|
188
188
|
|
|
189
189
|
throw new Error(
|
|
190
190
|
devUtils.formatMessage(
|
|
@@ -5,10 +5,6 @@ import {
|
|
|
5
5
|
onUnhandledRequest,
|
|
6
6
|
UnhandledRequestCallback,
|
|
7
7
|
} from './onUnhandledRequest'
|
|
8
|
-
import { HttpHandler, HttpMethods } from '../../handlers/HttpHandler'
|
|
9
|
-
import { ResponseResolver } from '../../handlers/RequestHandler'
|
|
10
|
-
|
|
11
|
-
const resolver: ResponseResolver = () => void 0
|
|
12
8
|
|
|
13
9
|
const fixtures = {
|
|
14
10
|
warningWithoutSuggestions: `\
|
|
@@ -24,18 +20,6 @@ Read more: https://mswjs.io/docs/getting-started/mocks`,
|
|
|
24
20
|
|
|
25
21
|
• GET /api
|
|
26
22
|
|
|
27
|
-
If you still wish to intercept this unhandled request, please create a request handler for it.
|
|
28
|
-
Read more: https://mswjs.io/docs/getting-started/mocks`,
|
|
29
|
-
|
|
30
|
-
warningWithSuggestions: (suggestions: string) => `\
|
|
31
|
-
[MSW] Warning: intercepted a request without a matching request handler:
|
|
32
|
-
|
|
33
|
-
• GET /api
|
|
34
|
-
|
|
35
|
-
Did you mean to request one of the following resources instead?
|
|
36
|
-
|
|
37
|
-
${suggestions}
|
|
38
|
-
|
|
39
23
|
If you still wish to intercept this unhandled request, please create a request handler for it.
|
|
40
24
|
Read more: https://mswjs.io/docs/getting-started/mocks`,
|
|
41
25
|
}
|
|
@@ -52,7 +36,6 @@ afterEach(() => {
|
|
|
52
36
|
test('supports the "bypass" request strategy', async () => {
|
|
53
37
|
await onUnhandledRequest(
|
|
54
38
|
new Request(new URL('http://localhost/api')),
|
|
55
|
-
[],
|
|
56
39
|
'bypass',
|
|
57
40
|
)
|
|
58
41
|
|
|
@@ -61,22 +44,14 @@ test('supports the "bypass" request strategy', async () => {
|
|
|
61
44
|
})
|
|
62
45
|
|
|
63
46
|
test('supports the "warn" request strategy', async () => {
|
|
64
|
-
await onUnhandledRequest(
|
|
65
|
-
new Request(new URL('http://localhost/api')),
|
|
66
|
-
[],
|
|
67
|
-
'warn',
|
|
68
|
-
)
|
|
47
|
+
await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn')
|
|
69
48
|
|
|
70
49
|
expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions)
|
|
71
50
|
})
|
|
72
51
|
|
|
73
52
|
test('supports the "error" request strategy', async () => {
|
|
74
53
|
await expect(
|
|
75
|
-
onUnhandledRequest(
|
|
76
|
-
new Request(new URL('http://localhost/api')),
|
|
77
|
-
[],
|
|
78
|
-
'error',
|
|
79
|
-
),
|
|
54
|
+
onUnhandledRequest(new Request(new URL('http://localhost/api')), 'error'),
|
|
80
55
|
).rejects.toThrow(
|
|
81
56
|
'[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.',
|
|
82
57
|
)
|
|
@@ -89,7 +64,7 @@ test('supports a custom callback function', async () => {
|
|
|
89
64
|
console.warn(`callback: ${request.method} ${request.url}`)
|
|
90
65
|
})
|
|
91
66
|
const request = new Request(new URL('/user', 'http://localhost:3000'))
|
|
92
|
-
await onUnhandledRequest(request,
|
|
67
|
+
await onUnhandledRequest(request, callback)
|
|
93
68
|
|
|
94
69
|
expect(callback).toHaveBeenCalledTimes(1)
|
|
95
70
|
expect(callback).toHaveBeenCalledWith(request, {
|
|
@@ -111,7 +86,7 @@ test('supports calling default strategies from the custom callback function', as
|
|
|
111
86
|
},
|
|
112
87
|
)
|
|
113
88
|
const request = new Request(new URL('http://localhost/api'))
|
|
114
|
-
await expect(onUnhandledRequest(request,
|
|
89
|
+
await expect(onUnhandledRequest(request, callback)).rejects.toThrow(
|
|
115
90
|
`[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.`,
|
|
116
91
|
)
|
|
117
92
|
|
|
@@ -126,86 +101,15 @@ test('supports calling default strategies from the custom callback function', as
|
|
|
126
101
|
})
|
|
127
102
|
|
|
128
103
|
test('does not print any suggestions given no handlers to suggest', async () => {
|
|
129
|
-
await onUnhandledRequest(
|
|
130
|
-
new Request(new URL('http://localhost/api')),
|
|
131
|
-
[],
|
|
132
|
-
'warn',
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('does not print any suggestions given no handlers are similar', async () => {
|
|
139
|
-
await onUnhandledRequest(
|
|
140
|
-
new Request(new URL('http://localhost/api')),
|
|
141
|
-
[
|
|
142
|
-
// None of the defined request handlers match the actual request URL
|
|
143
|
-
// to be used as suggestions.
|
|
144
|
-
new HttpHandler(HttpMethods.GET, 'https://api.github.com', resolver),
|
|
145
|
-
new HttpHandler(HttpMethods.GET, 'https://api.stripe.com', resolver),
|
|
146
|
-
],
|
|
147
|
-
'warn',
|
|
148
|
-
)
|
|
104
|
+
await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn')
|
|
149
105
|
|
|
150
106
|
expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions)
|
|
151
107
|
})
|
|
152
108
|
|
|
153
|
-
test('respects RegExp as a request handler method', async () => {
|
|
154
|
-
await onUnhandledRequest(
|
|
155
|
-
new Request(new URL('http://localhost/api')),
|
|
156
|
-
[new HttpHandler(/^GE/, 'http://localhost/api', resolver)],
|
|
157
|
-
'warn',
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
test('sorts the suggestions by relevance', async () => {
|
|
164
|
-
await onUnhandledRequest(
|
|
165
|
-
new Request(new URL('http://localhost/api')),
|
|
166
|
-
[
|
|
167
|
-
new HttpHandler(HttpMethods.GET, '/', resolver),
|
|
168
|
-
new HttpHandler(HttpMethods.GET, 'https://api.example.com/api', resolver),
|
|
169
|
-
new HttpHandler(HttpMethods.POST, '/api', resolver),
|
|
170
|
-
],
|
|
171
|
-
'warn',
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
175
|
-
fixtures.warningWithSuggestions(`\
|
|
176
|
-
• POST /api
|
|
177
|
-
• GET /`),
|
|
178
|
-
)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
test('does not print more than 4 suggestions', async () => {
|
|
182
|
-
await onUnhandledRequest(
|
|
183
|
-
new Request(new URL('http://localhost/api')),
|
|
184
|
-
[
|
|
185
|
-
new HttpHandler(HttpMethods.GET, '/ap', resolver),
|
|
186
|
-
new HttpHandler(HttpMethods.GET, '/api', resolver),
|
|
187
|
-
new HttpHandler(HttpMethods.GET, '/api-1', resolver),
|
|
188
|
-
new HttpHandler(HttpMethods.GET, '/api-2', resolver),
|
|
189
|
-
new HttpHandler(HttpMethods.GET, '/api-3', resolver),
|
|
190
|
-
new HttpHandler(HttpMethods.GET, '/api-4', resolver),
|
|
191
|
-
],
|
|
192
|
-
'warn',
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
196
|
-
fixtures.warningWithSuggestions(`\
|
|
197
|
-
• GET /api
|
|
198
|
-
• GET /ap
|
|
199
|
-
• GET /api-1
|
|
200
|
-
• GET /api-2`),
|
|
201
|
-
)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
109
|
test('throws an exception given unknown request strategy', async () => {
|
|
205
110
|
await expect(
|
|
206
111
|
onUnhandledRequest(
|
|
207
112
|
new Request(new URL('http://localhost/api')),
|
|
208
|
-
[],
|
|
209
113
|
// @ts-expect-error Intentional unknown strategy.
|
|
210
114
|
'invalid-strategy',
|
|
211
115
|
),
|
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { RequestHandler } from '../../handlers/RequestHandler'
|
|
3
|
-
import { HttpHandler } from '../../handlers/HttpHandler'
|
|
4
|
-
import { GraphQLHandler } from '../../handlers/GraphQLHandler'
|
|
5
|
-
import {
|
|
6
|
-
ParsedGraphQLQuery,
|
|
7
|
-
ParsedGraphQLRequest,
|
|
8
|
-
parseGraphQLRequest,
|
|
9
|
-
} from '../internal/parseGraphQLRequest'
|
|
10
|
-
import { getPublicUrlFromRequest } from './getPublicUrlFromRequest'
|
|
11
|
-
import { isStringEqual } from '../internal/isStringEqual'
|
|
1
|
+
import { toPublicUrl } from './toPublicUrl'
|
|
12
2
|
import { devUtils } from '../internal/devUtils'
|
|
13
3
|
|
|
14
|
-
const getStringMatchScore = jsLevenshtein
|
|
15
|
-
|
|
16
|
-
const MAX_MATCH_SCORE = 3
|
|
17
|
-
const MAX_SUGGESTION_COUNT = 4
|
|
18
|
-
const TYPE_MATCH_DELTA = 0.5
|
|
19
|
-
|
|
20
4
|
export interface UnhandledRequestPrint {
|
|
21
5
|
warning(): void
|
|
22
6
|
error(): void
|
|
@@ -33,183 +17,20 @@ export type UnhandledRequestStrategy =
|
|
|
33
17
|
| 'error'
|
|
34
18
|
| UnhandledRequestCallback
|
|
35
19
|
|
|
36
|
-
interface RequestHandlerGroups {
|
|
37
|
-
http: Array<HttpHandler>
|
|
38
|
-
graphql: Array<GraphQLHandler>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function groupHandlersByType(
|
|
42
|
-
handlers: Array<RequestHandler>,
|
|
43
|
-
): RequestHandlerGroups {
|
|
44
|
-
return handlers.reduce<RequestHandlerGroups>(
|
|
45
|
-
(groups, handler) => {
|
|
46
|
-
if (handler instanceof HttpHandler) {
|
|
47
|
-
groups.http.push(handler)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (handler instanceof GraphQLHandler) {
|
|
51
|
-
groups.graphql.push(handler)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return groups
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
http: [],
|
|
58
|
-
graphql: [],
|
|
59
|
-
},
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type RequestHandlerSuggestion = [number, RequestHandler]
|
|
64
|
-
|
|
65
|
-
type ScoreGetterFn<RequestHandlerType extends RequestHandler> = (
|
|
66
|
-
request: Request,
|
|
67
|
-
handler: RequestHandlerType,
|
|
68
|
-
) => number
|
|
69
|
-
|
|
70
|
-
function getHttpHandlerScore(): ScoreGetterFn<HttpHandler> {
|
|
71
|
-
return (request, handler) => {
|
|
72
|
-
const { path, method } = handler.info
|
|
73
|
-
|
|
74
|
-
if (path instanceof RegExp || method instanceof RegExp) {
|
|
75
|
-
return Infinity
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const hasSameMethod = isStringEqual(request.method, method)
|
|
79
|
-
|
|
80
|
-
// Always treat a handler with the same method as a more similar one.
|
|
81
|
-
const methodScoreDelta = hasSameMethod ? TYPE_MATCH_DELTA : 0
|
|
82
|
-
const requestPublicUrl = getPublicUrlFromRequest(request)
|
|
83
|
-
const score = getStringMatchScore(requestPublicUrl, path)
|
|
84
|
-
|
|
85
|
-
return score - methodScoreDelta
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getGraphQLHandlerScore(
|
|
90
|
-
parsedQuery: ParsedGraphQLQuery,
|
|
91
|
-
): ScoreGetterFn<GraphQLHandler> {
|
|
92
|
-
return (_, handler) => {
|
|
93
|
-
if (typeof parsedQuery.operationName === 'undefined') {
|
|
94
|
-
return Infinity
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const { operationType, operationName } = handler.info
|
|
98
|
-
|
|
99
|
-
if (typeof operationName !== 'string') {
|
|
100
|
-
return Infinity
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const hasSameOperationType = parsedQuery.operationType === operationType
|
|
104
|
-
// Always treat a handler with the same operation type as a more similar one.
|
|
105
|
-
const operationTypeScoreDelta = hasSameOperationType ? TYPE_MATCH_DELTA : 0
|
|
106
|
-
const score = getStringMatchScore(parsedQuery.operationName, operationName)
|
|
107
|
-
|
|
108
|
-
return score - operationTypeScoreDelta
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getSuggestedHandler(
|
|
113
|
-
request: Request,
|
|
114
|
-
handlers: Array<HttpHandler> | Array<GraphQLHandler>,
|
|
115
|
-
getScore: ScoreGetterFn<HttpHandler> | ScoreGetterFn<GraphQLHandler>,
|
|
116
|
-
): Array<RequestHandler> {
|
|
117
|
-
const suggestedHandlers = (handlers as Array<RequestHandler>)
|
|
118
|
-
.reduce<Array<RequestHandlerSuggestion>>((suggestions, handler) => {
|
|
119
|
-
const score = getScore(request, handler as any)
|
|
120
|
-
return suggestions.concat([[score, handler]])
|
|
121
|
-
}, [])
|
|
122
|
-
.sort(([leftScore], [rightScore]) => leftScore - rightScore)
|
|
123
|
-
.filter(([score]) => score <= MAX_MATCH_SCORE)
|
|
124
|
-
.slice(0, MAX_SUGGESTION_COUNT)
|
|
125
|
-
.map(([, handler]) => handler)
|
|
126
|
-
|
|
127
|
-
return suggestedHandlers
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getSuggestedHandlersMessage(handlers: RequestHandler[]) {
|
|
131
|
-
if (handlers.length > 1) {
|
|
132
|
-
return `\
|
|
133
|
-
Did you mean to request one of the following resources instead?
|
|
134
|
-
|
|
135
|
-
${handlers.map((handler) => ` • ${handler.info.header}`).join('\n')}`
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return `Did you mean to request "${handlers[0].info.header}" instead?`
|
|
139
|
-
}
|
|
140
|
-
|
|
141
20
|
export async function onUnhandledRequest(
|
|
142
21
|
request: Request,
|
|
143
|
-
handlers: Array<RequestHandler>,
|
|
144
22
|
strategy: UnhandledRequestStrategy = 'warn',
|
|
145
23
|
): Promise<void> {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
const publicUrl = getPublicUrlFromRequest(request)
|
|
150
|
-
|
|
151
|
-
function generateHandlerSuggestion(): string {
|
|
152
|
-
/**
|
|
153
|
-
* @note Ignore exceptions during GraphQL request parsing because at this point
|
|
154
|
-
* we cannot assume the unhandled request is a valid GraphQL request.
|
|
155
|
-
* If the GraphQL parsing fails, just don't treat it as a GraphQL request.
|
|
156
|
-
*/
|
|
157
|
-
const handlerGroups = groupHandlersByType(handlers)
|
|
158
|
-
const relevantHandlers = parsedGraphQLQuery
|
|
159
|
-
? handlerGroups.graphql
|
|
160
|
-
: handlerGroups.http
|
|
24
|
+
const url = new URL(request.url)
|
|
25
|
+
const publicUrl = toPublicUrl(url)
|
|
161
26
|
|
|
162
|
-
|
|
163
|
-
request,
|
|
164
|
-
relevantHandlers,
|
|
165
|
-
parsedGraphQLQuery
|
|
166
|
-
? getGraphQLHandlerScore(parsedGraphQLQuery)
|
|
167
|
-
: getHttpHandlerScore(),
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return suggestedHandlers.length > 0
|
|
171
|
-
? getSuggestedHandlersMessage(suggestedHandlers)
|
|
172
|
-
: ''
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function getGraphQLRequestHeader(
|
|
176
|
-
parsedGraphQLRequest: ParsedGraphQLRequest<any>,
|
|
177
|
-
): string {
|
|
178
|
-
if (!parsedGraphQLRequest?.operationName) {
|
|
179
|
-
return `anonymous ${parsedGraphQLRequest?.operationType} (${request.method} ${publicUrl})`
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return `${parsedGraphQLRequest.operationType} ${parsedGraphQLRequest.operationName} (${request.method} ${publicUrl})`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function generateUnhandledRequestMessage(): string {
|
|
186
|
-
const requestHeader = parsedGraphQLQuery
|
|
187
|
-
? getGraphQLRequestHeader(parsedGraphQLQuery)
|
|
188
|
-
: `${request.method} ${publicUrl}`
|
|
189
|
-
const handlerSuggestion = generateHandlerSuggestion()
|
|
190
|
-
|
|
191
|
-
const messageTemplate = [
|
|
192
|
-
`intercepted a request without a matching request handler:`,
|
|
193
|
-
` \u2022 ${requestHeader}`,
|
|
194
|
-
handlerSuggestion,
|
|
195
|
-
`\
|
|
196
|
-
If you still wish to intercept this unhandled request, please create a request handler for it.
|
|
197
|
-
Read more: https://mswjs.io/docs/getting-started/mocks\
|
|
198
|
-
`,
|
|
199
|
-
].filter(Boolean)
|
|
200
|
-
return messageTemplate.join('\n\n')
|
|
201
|
-
}
|
|
27
|
+
const unhandledRequestMessage = `intercepted a request without a matching request handler:\n\n \u2022 ${request.method} ${publicUrl}\n\nIf you still wish to intercept this unhandled request, please create a request handler for it.\nRead more: https://mswjs.io/docs/getting-started/mocks`
|
|
202
28
|
|
|
203
29
|
function applyStrategy(strategy: UnhandledRequestStrategy) {
|
|
204
|
-
// Generate handler suggestions only when applying the strategy.
|
|
205
|
-
// This saves bandwidth for scenarios when developers opt-out
|
|
206
|
-
// from the default unhandled request handling strategy.
|
|
207
|
-
const message = generateUnhandledRequestMessage()
|
|
208
|
-
|
|
209
30
|
switch (strategy) {
|
|
210
31
|
case 'error': {
|
|
211
32
|
// Print a developer-friendly error.
|
|
212
|
-
devUtils.error('Error: %s',
|
|
33
|
+
devUtils.error('Error: %s', unhandledRequestMessage)
|
|
213
34
|
|
|
214
35
|
// Throw an exception to halt request processing and not perform the original request.
|
|
215
36
|
throw new Error(
|
|
@@ -220,7 +41,7 @@ Read more: https://mswjs.io/docs/getting-started/mocks\
|
|
|
220
41
|
}
|
|
221
42
|
|
|
222
43
|
case 'warn': {
|
|
223
|
-
devUtils.warn('Warning: %s',
|
|
44
|
+
devUtils.warn('Warning: %s', unhandledRequestMessage)
|
|
224
45
|
break
|
|
225
46
|
}
|
|
226
47
|
|
|
@@ -245,5 +66,15 @@ Read more: https://mswjs.io/docs/getting-started/mocks\
|
|
|
245
66
|
return
|
|
246
67
|
}
|
|
247
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @note Ignore "file://" requests.
|
|
71
|
+
* Those often are an implementation detail of modern tooling
|
|
72
|
+
* that fetches modules via HTTP. Developers don't issue those
|
|
73
|
+
* requests and so they mustn't be warned about them.
|
|
74
|
+
*/
|
|
75
|
+
if (url.protocol === 'file:') {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
248
79
|
applyStrategy(strategy)
|
|
249
80
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { toPublicUrl } from './toPublicUrl'
|
|
5
|
+
|
|
6
|
+
test('returns an absolute request URL withouth search params', () => {
|
|
7
|
+
expect(toPublicUrl(new URL('https://test.mswjs.io/path'))).toBe(
|
|
8
|
+
'https://test.mswjs.io/path',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
expect(toPublicUrl(new URL('http://localhost/path'))).toBe('/path')
|
|
12
|
+
|
|
13
|
+
expect(toPublicUrl(new URL('http://localhost/path?foo=bar'))).toBe('/path')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns a relative URL given the request to the same origin', () => {
|
|
17
|
+
expect(toPublicUrl('http://localhost/user')).toBe('/user')
|
|
18
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a relative URL if the given request URL is relative
|
|
3
|
+
* to the current origin. Otherwise returns an absolute URL.
|
|
4
|
+
*/
|
|
5
|
+
export function toPublicUrl(url: string | URL): string {
|
|
6
|
+
if (typeof location === 'undefined') {
|
|
7
|
+
return url.toString()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const urlInstance = url instanceof URL ? url : new URL(url)
|
|
11
|
+
|
|
12
|
+
return urlInstance.origin === location.origin
|
|
13
|
+
? urlInstance.pathname
|
|
14
|
+
: urlInstance.origin + urlInstance.pathname
|
|
15
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/core/utils/request/getPublicUrlFromRequest.ts"],"sourcesContent":["/**\n * Returns a relative URL if the given request URL is relative to the current origin.\n * Otherwise returns an absolute URL.\n */\nexport function getPublicUrlFromRequest(request: Request): string {\n if (typeof location === 'undefined') {\n return request.url\n }\n\n const url = new URL(request.url)\n\n return url.origin === location.origin\n ? url.pathname\n : url.origin + url.pathname\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,SAAS,wBAAwB,SAA0B;AAChE,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO,QAAQ;AAAA,EACjB;AAEA,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAE/B,SAAO,IAAI,WAAW,SAAS,SAC3B,IAAI,WACJ,IAAI,SAAS,IAAI;AACvB;","names":[]}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
function getPublicUrlFromRequest(request) {
|
|
2
|
-
if (typeof location === "undefined") {
|
|
3
|
-
return request.url;
|
|
4
|
-
}
|
|
5
|
-
const url = new URL(request.url);
|
|
6
|
-
return url.origin === location.origin ? url.pathname : url.origin + url.pathname;
|
|
7
|
-
}
|
|
8
|
-
export {
|
|
9
|
-
getPublicUrlFromRequest
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=getPublicUrlFromRequest.mjs.map
|