msw 2.1.7 → 2.2.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/cli/init.js +9 -13
- package/lib/browser/index.js +12 -8
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/index.mjs +12 -8
- package/lib/browser/index.mjs.map +1 -1
- package/lib/core/HttpResponse.js +14 -1
- package/lib/core/HttpResponse.js.map +1 -1
- package/lib/core/HttpResponse.mjs +14 -1
- package/lib/core/HttpResponse.mjs.map +1 -1
- package/lib/core/SetupApi.d.mts +15 -3
- package/lib/core/SetupApi.d.ts +15 -3
- package/lib/core/SetupApi.js +26 -8
- package/lib/core/SetupApi.js.map +1 -1
- package/lib/core/SetupApi.mjs +26 -8
- package/lib/core/SetupApi.mjs.map +1 -1
- package/lib/iife/index.js +48 -17
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +1 -1
- package/lib/native/index.d.mts +10 -5
- package/lib/native/index.d.ts +10 -5
- package/lib/native/index.js +9 -9
- package/lib/native/index.js.map +1 -1
- package/lib/native/index.mjs +9 -9
- package/lib/native/index.mjs.map +1 -1
- package/lib/node/index.d.mts +26 -4
- package/lib/node/index.d.ts +26 -4
- package/lib/node/index.js +62 -13
- package/lib/node/index.js.map +1 -1
- package/lib/node/index.mjs +62 -13
- package/lib/node/index.mjs.map +1 -1
- package/package.json +15 -7
- package/src/browser/setupWorker/glossary.ts +1 -1
- package/src/browser/setupWorker/setupWorker.ts +3 -11
- package/src/browser/setupWorker/start/createFallbackRequestListener.ts +1 -1
- package/src/browser/setupWorker/start/createRequestListener.ts +1 -1
- package/src/browser/setupWorker/start/createResponseListener.ts +13 -0
- package/src/core/HttpResponse.test.ts +34 -3
- package/src/core/HttpResponse.ts +24 -1
- package/src/core/SetupApi.ts +33 -9
- package/src/native/index.ts +5 -5
- package/src/node/SetupServerApi.ts +64 -95
- package/src/node/SetupServerCommonApi.ts +116 -0
- package/src/node/glossary.ts +17 -3
- package/src/node/setupServer.ts +3 -10
package/lib/node/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/node/SetupServerApi.ts","../../src/node/setupServer.ts"],"sourcesContent":["import {\
|
|
1
|
+
{"version":3,"sources":["../../src/node/SetupServerApi.ts","../../src/node/SetupServerCommonApi.ts","../../src/node/setupServer.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'\nimport { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'\nimport { FetchInterceptor } from '@mswjs/interceptors/fetch'\nimport { HandlersController } from '~/core/SetupApi'\nimport type { RequestHandler } from '~/core/handlers/RequestHandler'\nimport type { SetupServer } from './glossary'\nimport { SetupServerCommonApi } from './SetupServerCommonApi'\n\nconst store = new AsyncLocalStorage<RequestHandlersContext>()\n\ntype RequestHandlersContext = {\n initialHandlers: Array<RequestHandler>\n handlers: Array<RequestHandler>\n}\n\n/**\n * A handlers controller that utilizes `AsyncLocalStorage` in Node.js\n * to prevent the request handlers list from being a shared state\n * across mutliple tests.\n */\nclass AsyncHandlersController implements HandlersController {\n private rootContext: RequestHandlersContext\n\n constructor(initialHandlers: Array<RequestHandler>) {\n this.rootContext = { initialHandlers, handlers: [] }\n }\n\n get context(): RequestHandlersContext {\n return store.getStore() || this.rootContext\n }\n\n public prepend(runtimeHandlers: Array<RequestHandler>) {\n this.context.handlers.unshift(...runtimeHandlers)\n }\n\n public reset(nextHandlers: Array<RequestHandler>) {\n const context = this.context\n context.handlers = []\n context.initialHandlers =\n nextHandlers.length > 0 ? nextHandlers : context.initialHandlers\n }\n\n public currentHandlers(): Array<RequestHandler> {\n const { initialHandlers, handlers } = this.context\n return handlers.concat(initialHandlers)\n }\n}\n\nexport class SetupServerApi\n extends SetupServerCommonApi\n implements SetupServer\n{\n constructor(handlers: Array<RequestHandler>) {\n super(\n [ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],\n handlers,\n )\n\n this.handlersController = new AsyncHandlersController(handlers)\n }\n\n public boundary<Fn extends (...args: Array<any>) => unknown>(\n callback: Fn,\n ): (...args: Parameters<Fn>) => ReturnType<Fn> {\n return (...args: Parameters<Fn>): ReturnType<Fn> => {\n return store.run<any, any>(\n {\n initialHandlers: this.handlersController.currentHandlers(),\n handlers: [],\n },\n callback,\n ...args,\n )\n }\n }\n\n public close(): void {\n super.close()\n store.disable()\n }\n}\n","/**\n * @note This API is extended by both \"msw/node\" and \"msw/native\"\n * so be minding about the things you import!\n */\nimport type { RequiredDeep } from 'type-fest'\nimport { invariant } from 'outvariant'\nimport {\n BatchInterceptor,\n InterceptorReadyState,\n type HttpRequestEventMap,\n type Interceptor,\n} from '@mswjs/interceptors'\nimport type { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions'\nimport { SetupApi } from '~/core/SetupApi'\nimport { handleRequest } from '~/core/utils/handleRequest'\nimport type { RequestHandler } from '~/core/handlers/RequestHandler'\nimport { mergeRight } from '~/core/utils/internal/mergeRight'\nimport { devUtils } from '~/core/utils/internal/devUtils'\nimport type { SetupServerCommon } from './glossary'\n\nexport const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {\n onUnhandledRequest: 'warn',\n}\n\nexport class SetupServerCommonApi\n extends SetupApi<LifeCycleEventsMap>\n implements SetupServerCommon\n{\n protected readonly interceptor: BatchInterceptor<\n Array<Interceptor<HttpRequestEventMap>>,\n HttpRequestEventMap\n >\n private resolvedOptions: RequiredDeep<SharedOptions>\n\n constructor(\n interceptors: Array<{ new (): Interceptor<HttpRequestEventMap> }>,\n handlers: Array<RequestHandler>,\n ) {\n super(...handlers)\n\n this.interceptor = new BatchInterceptor({\n name: 'setup-server',\n interceptors: interceptors.map((Interceptor) => new Interceptor()),\n })\n\n this.resolvedOptions = {} as RequiredDeep<SharedOptions>\n\n this.init()\n }\n\n /**\n * Subscribe to all requests that are using the interceptor object\n */\n private init(): void {\n this.interceptor.on('request', async ({ request, requestId }) => {\n const response = await handleRequest(\n request,\n requestId,\n this.handlersController.currentHandlers(),\n this.resolvedOptions,\n this.emitter,\n )\n\n if (response) {\n request.respondWith(response)\n }\n\n return\n })\n\n this.interceptor.on(\n 'response',\n ({ response, isMockedResponse, request, requestId }) => {\n this.emitter.emit(\n isMockedResponse ? 'response:mocked' : 'response:bypass',\n {\n response,\n request,\n requestId,\n },\n )\n },\n )\n }\n\n public listen(options: Partial<SharedOptions> = {}): void {\n this.resolvedOptions = mergeRight(\n DEFAULT_LISTEN_OPTIONS,\n options,\n ) as RequiredDeep<SharedOptions>\n\n // Apply the interceptor when starting the server.\n this.interceptor.apply()\n\n this.subscriptions.push(() => {\n this.interceptor.dispose()\n })\n\n // Assert that the interceptor has been applied successfully.\n // Also guards us from forgetting to call \"interceptor.apply()\"\n // as a part of the \"listen\" method.\n invariant(\n [InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes(\n this.interceptor.readyState,\n ),\n devUtils.formatMessage(\n 'Failed to start \"setupServer\": the interceptor failed to apply. This is likely an issue with the library and you should report it at \"%s\".',\n ),\n 'https://github.com/mswjs/msw/issues/new/choose',\n )\n }\n\n public close(): void {\n this.dispose()\n }\n}\n","import type { RequestHandler } from '~/core/handlers/RequestHandler'\nimport { SetupServerApi } from './SetupServerApi'\n\n/**\n * Sets up a requests interception in Node.js with the given request handlers.\n * @param {RequestHandler[]} handlers List of request handlers.\n *\n * @see {@link https://mswjs.io/docs/api/setup-server `setupServer()` API reference}\n */\nexport const setupServer = (\n ...handlers: Array<RequestHandler>\n): SetupServerApi => {\n return new SetupServerApi(handlers)\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "msw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
"import": "./lib/native/index.mjs",
|
|
35
35
|
"default": "./lib/native/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./core/http": {
|
|
38
|
+
"types": "./lib/core/http.d.ts",
|
|
39
|
+
"require": "./lib/core/http.js",
|
|
40
|
+
"import": "./lib/core/http.mjs",
|
|
41
|
+
"default": "./lib/core/http.js"
|
|
42
|
+
},
|
|
43
|
+
"./core/graphql": {
|
|
44
|
+
"types": "./lib/core/graphql.d.ts",
|
|
45
|
+
"require": "./lib/core/graphql.js",
|
|
46
|
+
"import": "./lib/core/graphql.mjs",
|
|
47
|
+
"default": "./lib/core/graphql.js"
|
|
48
|
+
},
|
|
37
49
|
"./mockServiceWorker.js": "./lib/mockServiceWorker.js",
|
|
38
50
|
"./package.json": "./package.json"
|
|
39
51
|
},
|
|
@@ -94,16 +106,15 @@
|
|
|
94
106
|
"dependencies": {
|
|
95
107
|
"@bundled-es-modules/cookie": "^2.0.0",
|
|
96
108
|
"@bundled-es-modules/statuses": "^1.0.1",
|
|
109
|
+
"@inquirer/confirm": "^3.0.0",
|
|
97
110
|
"@mswjs/cookies": "^1.1.0",
|
|
98
111
|
"@mswjs/interceptors": "^0.25.16",
|
|
99
112
|
"@open-draft/until": "^2.1.0",
|
|
100
113
|
"@types/cookie": "^0.6.0",
|
|
101
114
|
"@types/statuses": "^2.0.4",
|
|
102
115
|
"chalk": "^4.1.2",
|
|
103
|
-
"chokidar": "^3.4.2",
|
|
104
116
|
"graphql": "^16.8.1",
|
|
105
117
|
"headers-polyfill": "^4.0.2",
|
|
106
|
-
"inquirer": "^8.2.0",
|
|
107
118
|
"is-node-process": "^1.2.0",
|
|
108
119
|
"outvariant": "^1.4.2",
|
|
109
120
|
"path-to-regexp": "^6.2.0",
|
|
@@ -167,9 +178,6 @@
|
|
|
167
178
|
"optional": true
|
|
168
179
|
}
|
|
169
180
|
},
|
|
170
|
-
"resolutions": {
|
|
171
|
-
"chokidar": "3.4.1"
|
|
172
|
-
},
|
|
173
181
|
"config": {
|
|
174
182
|
"commitizen": {
|
|
175
183
|
"path": "./node_modules/cz-conventional-changelog"
|
|
@@ -189,7 +197,7 @@
|
|
|
189
197
|
"check:exports": "node \"./config/scripts/validate-esm.js\"",
|
|
190
198
|
"test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native",
|
|
191
199
|
"test:unit": "vitest",
|
|
192
|
-
"test:node": "vitest --config=./test/node/vitest.config.ts",
|
|
200
|
+
"test:node": "vitest run --config=./test/node/vitest.config.ts",
|
|
193
201
|
"test:native": "vitest --config=./test/native/vitest.config.ts",
|
|
194
202
|
"test:browser": "playwright test -c ./test/browser/playwright.config.ts",
|
|
195
203
|
"test:modules:node": "vitest --config=./test/modules/node/vitest.config.ts",
|
|
@@ -102,7 +102,7 @@ export interface SetupWorkerInternalContext {
|
|
|
102
102
|
startOptions: RequiredDeep<StartOptions>
|
|
103
103
|
worker: ServiceWorker | null
|
|
104
104
|
registration: ServiceWorkerRegistration | null
|
|
105
|
-
|
|
105
|
+
getRequestHandlers(): Array<RequestHandler>
|
|
106
106
|
requests: Map<string, Request>
|
|
107
107
|
emitter: Emitter<LifeCycleEventsMap>
|
|
108
108
|
keepAliveInterval?: number
|
|
@@ -58,8 +58,10 @@ export class SetupWorkerApi
|
|
|
58
58
|
isMockingEnabled: false,
|
|
59
59
|
startOptions: null as any,
|
|
60
60
|
worker: null,
|
|
61
|
+
getRequestHandlers: () => {
|
|
62
|
+
return this.handlersController.currentHandlers()
|
|
63
|
+
},
|
|
61
64
|
registration: null,
|
|
62
|
-
requestHandlers: this.currentHandlers,
|
|
63
65
|
requests: new Map(),
|
|
64
66
|
emitter: this.emitter,
|
|
65
67
|
workerChannel: {
|
|
@@ -151,16 +153,6 @@ export class SetupWorkerApi
|
|
|
151
153
|
},
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
/**
|
|
155
|
-
* @todo Not sure I like this but "this.currentHandlers"
|
|
156
|
-
* updates never bubble to "this.context.requestHandlers".
|
|
157
|
-
*/
|
|
158
|
-
Object.defineProperties(context, {
|
|
159
|
-
requestHandlers: {
|
|
160
|
-
get: () => this.currentHandlers,
|
|
161
|
-
},
|
|
162
|
-
})
|
|
163
|
-
|
|
164
156
|
this.startHandler = context.supports.serviceWorkerApi
|
|
165
157
|
? createFallbackStart(context)
|
|
166
158
|
: createStartHandler(context)
|
|
@@ -48,6 +48,19 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
|
|
48
48
|
responseJson,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Set response URL if it's not set already.
|
|
53
|
+
* @see https://github.com/mswjs/msw/issues/2030
|
|
54
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Response/url
|
|
55
|
+
*/
|
|
56
|
+
if (!response.url) {
|
|
57
|
+
Object.defineProperty(response, 'url', {
|
|
58
|
+
value: request.url,
|
|
59
|
+
enumerable: true,
|
|
60
|
+
writable: false,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
context.emitter.emit(
|
|
52
65
|
responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass',
|
|
53
66
|
{
|
|
@@ -22,15 +22,14 @@ describe('HttpResponse.text()', () => {
|
|
|
22
22
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
23
23
|
expect(await response.text()).toBe('hello world')
|
|
24
24
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
25
|
+
'content-length': '11',
|
|
25
26
|
'content-type': 'text/plain',
|
|
26
27
|
})
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
it('allows overriding the "Content-Type" response header', async () => {
|
|
30
31
|
const response = HttpResponse.text('hello world', {
|
|
31
|
-
headers: {
|
|
32
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
33
|
-
},
|
|
32
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
34
33
|
})
|
|
35
34
|
|
|
36
35
|
expect(response.status).toBe(200)
|
|
@@ -38,9 +37,21 @@ describe('HttpResponse.text()', () => {
|
|
|
38
37
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
39
38
|
expect(await response.text()).toBe('hello world')
|
|
40
39
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
40
|
+
'content-length': '11',
|
|
41
41
|
'content-type': 'text/plain; charset=utf-8',
|
|
42
42
|
})
|
|
43
43
|
})
|
|
44
|
+
|
|
45
|
+
it('allows overriding the "Content-Length" response header', async () => {
|
|
46
|
+
const response = HttpResponse.text('hello world', {
|
|
47
|
+
headers: { 'Content-Length': '32' },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
51
|
+
'content-length': '32',
|
|
52
|
+
'content-type': 'text/plain',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
44
55
|
})
|
|
45
56
|
|
|
46
57
|
describe('HttpResponse.json()', () => {
|
|
@@ -52,6 +63,7 @@ describe('HttpResponse.json()', () => {
|
|
|
52
63
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
53
64
|
expect(await response.json()).toEqual({ firstName: 'John' })
|
|
54
65
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
66
|
+
'content-length': '20',
|
|
55
67
|
'content-type': 'application/json',
|
|
56
68
|
})
|
|
57
69
|
})
|
|
@@ -64,6 +76,7 @@ describe('HttpResponse.json()', () => {
|
|
|
64
76
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
65
77
|
expect(await response.json()).toEqual([1, 2, 3])
|
|
66
78
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
79
|
+
'content-length': '7',
|
|
67
80
|
'content-type': 'application/json',
|
|
68
81
|
})
|
|
69
82
|
})
|
|
@@ -76,6 +89,7 @@ describe('HttpResponse.json()', () => {
|
|
|
76
89
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
77
90
|
expect(await response.json()).toBe(`"hello"`)
|
|
78
91
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
92
|
+
'content-length': '11',
|
|
79
93
|
'content-type': 'application/json',
|
|
80
94
|
})
|
|
81
95
|
})
|
|
@@ -88,6 +102,7 @@ describe('HttpResponse.json()', () => {
|
|
|
88
102
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
89
103
|
expect(await response.json()).toBe(123)
|
|
90
104
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
105
|
+
'content-length': '3',
|
|
91
106
|
'content-type': 'application/json',
|
|
92
107
|
})
|
|
93
108
|
})
|
|
@@ -112,6 +127,7 @@ describe('HttpResponse.json()', () => {
|
|
|
112
127
|
// into a plain object.
|
|
113
128
|
expect(await response.json()).toEqual({})
|
|
114
129
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
130
|
+
'content-length': '2',
|
|
115
131
|
'content-type': 'application/json',
|
|
116
132
|
})
|
|
117
133
|
})
|
|
@@ -131,9 +147,24 @@ describe('HttpResponse.json()', () => {
|
|
|
131
147
|
expect(response.body).toBeInstanceOf(ReadableStream)
|
|
132
148
|
expect(await response.json()).toEqual({ a: 1 })
|
|
133
149
|
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
150
|
+
'content-length': '7',
|
|
134
151
|
'content-type': 'application/hal+json',
|
|
135
152
|
})
|
|
136
153
|
})
|
|
154
|
+
|
|
155
|
+
it('allows overriding the "Content-Length" response header', async () => {
|
|
156
|
+
const response = HttpResponse.json(
|
|
157
|
+
{ a: 1 },
|
|
158
|
+
{
|
|
159
|
+
headers: { 'Content-Length': '32' },
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
expect(Object.fromEntries(response.headers.entries())).toEqual({
|
|
164
|
+
'content-length': '32',
|
|
165
|
+
'content-type': 'application/json',
|
|
166
|
+
})
|
|
167
|
+
})
|
|
137
168
|
})
|
|
138
169
|
|
|
139
170
|
describe('HttpResponse.xml()', () => {
|
package/src/core/HttpResponse.ts
CHANGED
|
@@ -57,6 +57,16 @@ export class HttpResponse extends Response {
|
|
|
57
57
|
responseInit.headers.set('Content-Type', 'text/plain')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Automatically set the "Content-Length" response header
|
|
61
|
+
// for non-empty text responses. This enforces consistency and
|
|
62
|
+
// brings mocked responses closer to production.
|
|
63
|
+
if (!responseInit.headers.has('Content-Length')) {
|
|
64
|
+
responseInit.headers.set(
|
|
65
|
+
'Content-Length',
|
|
66
|
+
body ? body.length.toString() : '0',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
return new HttpResponse(body, responseInit) as StrictResponse<BodyType>
|
|
61
71
|
}
|
|
62
72
|
|
|
@@ -76,8 +86,21 @@ export class HttpResponse extends Response {
|
|
|
76
86
|
responseInit.headers.set('Content-Type', 'application/json')
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @note TypeScript is incorrect here.
|
|
91
|
+
* Stringifying undefined will return undefined.
|
|
92
|
+
*/
|
|
93
|
+
const responseText = JSON.stringify(body) as string | undefined
|
|
94
|
+
|
|
95
|
+
if (!responseInit.headers.has('Content-Length')) {
|
|
96
|
+
responseInit.headers.set(
|
|
97
|
+
'Content-Length',
|
|
98
|
+
responseText ? responseText.length.toString() : '0',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
return new HttpResponse(
|
|
80
|
-
|
|
103
|
+
responseText,
|
|
81
104
|
responseInit,
|
|
82
105
|
) as StrictResponse<BodyType>
|
|
83
106
|
}
|
package/src/core/SetupApi.ts
CHANGED
|
@@ -10,12 +10,38 @@ import { pipeEvents } from './utils/internal/pipeEvents'
|
|
|
10
10
|
import { toReadonlyArray } from './utils/internal/toReadonlyArray'
|
|
11
11
|
import { Disposable } from './utils/internal/Disposable'
|
|
12
12
|
|
|
13
|
+
export abstract class HandlersController {
|
|
14
|
+
abstract prepend(runtimeHandlers: Array<RequestHandler>): void
|
|
15
|
+
abstract reset(nextHandles: Array<RequestHandler>): void
|
|
16
|
+
abstract currentHandlers(): Array<RequestHandler>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class InMemoryHandlersController implements HandlersController {
|
|
20
|
+
private handlers: Array<RequestHandler>
|
|
21
|
+
|
|
22
|
+
constructor(private initialHandlers: Array<RequestHandler>) {
|
|
23
|
+
this.handlers = [...initialHandlers]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public prepend(runtimeHandles: Array<RequestHandler>): void {
|
|
27
|
+
this.handlers.unshift(...runtimeHandles)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public reset(nextHandlers: Array<RequestHandler>): void {
|
|
31
|
+
this.handlers =
|
|
32
|
+
nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public currentHandlers(): Array<RequestHandler> {
|
|
36
|
+
return this.handlers
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
/**
|
|
14
41
|
* Generic class for the mock API setup.
|
|
15
42
|
*/
|
|
16
43
|
export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
|
|
17
|
-
protected
|
|
18
|
-
protected currentHandlers: Array<RequestHandler>
|
|
44
|
+
protected handlersController: HandlersController
|
|
19
45
|
protected readonly emitter: Emitter<EventsMap>
|
|
20
46
|
protected readonly publicEmitter: Emitter<EventsMap>
|
|
21
47
|
|
|
@@ -31,8 +57,7 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
|
|
|
31
57
|
),
|
|
32
58
|
)
|
|
33
59
|
|
|
34
|
-
this.
|
|
35
|
-
this.currentHandlers = [...initialHandlers]
|
|
60
|
+
this.handlersController = new InMemoryHandlersController(initialHandlers)
|
|
36
61
|
|
|
37
62
|
this.emitter = new Emitter<EventsMap>()
|
|
38
63
|
this.publicEmitter = new Emitter<EventsMap>()
|
|
@@ -59,24 +84,23 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
|
|
|
59
84
|
),
|
|
60
85
|
)
|
|
61
86
|
|
|
62
|
-
this.
|
|
87
|
+
this.handlersController.prepend(runtimeHandlers)
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
public restoreHandlers(): void {
|
|
66
|
-
this.currentHandlers.forEach((handler) => {
|
|
91
|
+
this.handlersController.currentHandlers().forEach((handler) => {
|
|
67
92
|
handler.isUsed = false
|
|
68
93
|
})
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
public resetHandlers(...nextHandlers: Array<RequestHandler>): void {
|
|
72
|
-
this.
|
|
73
|
-
nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers]
|
|
97
|
+
this.handlersController.reset(nextHandlers)
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
public listHandlers(): ReadonlyArray<
|
|
77
101
|
RequestHandler<RequestHandlerDefaultInfo, any, any>
|
|
78
102
|
> {
|
|
79
|
-
return toReadonlyArray(this.currentHandlers)
|
|
103
|
+
return toReadonlyArray(this.handlersController.currentHandlers())
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
private createLifeCycleEvents(): LifeCycleEventEmitter<EventsMap> {
|
package/src/native/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
|
|
2
2
|
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
|
|
3
|
-
import { RequestHandler } from '~/core/handlers/RequestHandler'
|
|
4
|
-
import {
|
|
3
|
+
import type { RequestHandler } from '~/core/handlers/RequestHandler'
|
|
4
|
+
import { SetupServerCommonApi } from '../node/SetupServerCommonApi'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Sets up a requests interception in React Native with the given request handlers.
|
|
@@ -11,11 +11,11 @@ import { SetupServerApi } from '../node/SetupServerApi'
|
|
|
11
11
|
*/
|
|
12
12
|
export function setupServer(
|
|
13
13
|
...handlers: Array<RequestHandler>
|
|
14
|
-
):
|
|
14
|
+
): SetupServerCommonApi {
|
|
15
15
|
// Provision request interception via patching the `XMLHttpRequest` class only
|
|
16
16
|
// in React Native. There is no `http`/`https` modules in that environment.
|
|
17
|
-
return new
|
|
17
|
+
return new SetupServerCommonApi(
|
|
18
18
|
[FetchInterceptor, XMLHttpRequestInterceptor],
|
|
19
|
-
|
|
19
|
+
handlers,
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -1,113 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { RequestHandler } from '~/core/handlers/RequestHandler'
|
|
10
|
-
import { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions'
|
|
11
|
-
import { RequiredDeep } from '~/core/typeUtils'
|
|
12
|
-
import { handleRequest } from '~/core/utils/handleRequest'
|
|
13
|
-
import { devUtils } from '~/core/utils/internal/devUtils'
|
|
14
|
-
import { mergeRight } from '~/core/utils/internal/mergeRight'
|
|
15
|
-
import { SetupServer } from './glossary'
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'
|
|
3
|
+
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
|
|
4
|
+
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
|
|
5
|
+
import { HandlersController } from '~/core/SetupApi'
|
|
6
|
+
import type { RequestHandler } from '~/core/handlers/RequestHandler'
|
|
7
|
+
import type { SetupServer } from './glossary'
|
|
8
|
+
import { SetupServerCommonApi } from './SetupServerCommonApi'
|
|
16
9
|
|
|
17
|
-
const
|
|
18
|
-
onUnhandledRequest: 'warn',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class SetupServerApi
|
|
22
|
-
extends SetupApi<LifeCycleEventsMap>
|
|
23
|
-
implements SetupServer
|
|
24
|
-
{
|
|
25
|
-
protected readonly interceptor: BatchInterceptor<
|
|
26
|
-
Array<Interceptor<HttpRequestEventMap>>,
|
|
27
|
-
HttpRequestEventMap
|
|
28
|
-
>
|
|
29
|
-
private resolvedOptions: RequiredDeep<SharedOptions>
|
|
10
|
+
const store = new AsyncLocalStorage<RequestHandlersContext>()
|
|
30
11
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
...handlers: Array<RequestHandler>
|
|
36
|
-
) {
|
|
37
|
-
super(...handlers)
|
|
12
|
+
type RequestHandlersContext = {
|
|
13
|
+
initialHandlers: Array<RequestHandler>
|
|
14
|
+
handlers: Array<RequestHandler>
|
|
15
|
+
}
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
/**
|
|
18
|
+
* A handlers controller that utilizes `AsyncLocalStorage` in Node.js
|
|
19
|
+
* to prevent the request handlers list from being a shared state
|
|
20
|
+
* across mutliple tests.
|
|
21
|
+
*/
|
|
22
|
+
class AsyncHandlersController implements HandlersController {
|
|
23
|
+
private rootContext: RequestHandlersContext
|
|
44
24
|
|
|
45
|
-
|
|
25
|
+
constructor(initialHandlers: Array<RequestHandler>) {
|
|
26
|
+
this.rootContext = { initialHandlers, handlers: [] }
|
|
46
27
|
}
|
|
47
28
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
private init(): void {
|
|
52
|
-
this.interceptor.on('request', async ({ request, requestId }) => {
|
|
53
|
-
const response = await handleRequest(
|
|
54
|
-
request,
|
|
55
|
-
requestId,
|
|
56
|
-
this.currentHandlers,
|
|
57
|
-
this.resolvedOptions,
|
|
58
|
-
this.emitter,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if (response) {
|
|
62
|
-
request.respondWith(response)
|
|
63
|
-
}
|
|
29
|
+
get context(): RequestHandlersContext {
|
|
30
|
+
return store.getStore() || this.rootContext
|
|
31
|
+
}
|
|
64
32
|
|
|
65
|
-
|
|
66
|
-
|
|
33
|
+
public prepend(runtimeHandlers: Array<RequestHandler>) {
|
|
34
|
+
this.context.handlers.unshift(...runtimeHandlers)
|
|
35
|
+
}
|
|
67
36
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
response,
|
|
75
|
-
request,
|
|
76
|
-
requestId,
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
},
|
|
80
|
-
)
|
|
37
|
+
public reset(nextHandlers: Array<RequestHandler>) {
|
|
38
|
+
const context = this.context
|
|
39
|
+
context.handlers = []
|
|
40
|
+
context.initialHandlers =
|
|
41
|
+
nextHandlers.length > 0 ? nextHandlers : context.initialHandlers
|
|
81
42
|
}
|
|
82
43
|
|
|
83
|
-
public
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
44
|
+
public currentHandlers(): Array<RequestHandler> {
|
|
45
|
+
const { initialHandlers, handlers } = this.context
|
|
46
|
+
return handlers.concat(initialHandlers)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
88
49
|
|
|
89
|
-
|
|
90
|
-
|
|
50
|
+
export class SetupServerApi
|
|
51
|
+
extends SetupServerCommonApi
|
|
52
|
+
implements SetupServer
|
|
53
|
+
{
|
|
54
|
+
constructor(handlers: Array<RequestHandler>) {
|
|
55
|
+
super(
|
|
56
|
+
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],
|
|
57
|
+
handlers,
|
|
58
|
+
)
|
|
91
59
|
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
})
|
|
60
|
+
this.handlersController = new AsyncHandlersController(handlers)
|
|
61
|
+
}
|
|
95
62
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
63
|
+
public boundary<Fn extends (...args: Array<any>) => unknown>(
|
|
64
|
+
callback: Fn,
|
|
65
|
+
): (...args: Parameters<Fn>) => ReturnType<Fn> {
|
|
66
|
+
return (...args: Parameters<Fn>): ReturnType<Fn> => {
|
|
67
|
+
return store.run<any, any>(
|
|
68
|
+
{
|
|
69
|
+
initialHandlers: this.handlersController.currentHandlers(),
|
|
70
|
+
handlers: [],
|
|
71
|
+
},
|
|
72
|
+
callback,
|
|
73
|
+
...args,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
108
76
|
}
|
|
109
77
|
|
|
110
78
|
public close(): void {
|
|
111
|
-
|
|
79
|
+
super.close()
|
|
80
|
+
store.disable()
|
|
112
81
|
}
|
|
113
82
|
}
|