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.
Files changed (44) hide show
  1. package/cli/init.js +9 -13
  2. package/lib/browser/index.js +12 -8
  3. package/lib/browser/index.js.map +1 -1
  4. package/lib/browser/index.mjs +12 -8
  5. package/lib/browser/index.mjs.map +1 -1
  6. package/lib/core/HttpResponse.js +14 -1
  7. package/lib/core/HttpResponse.js.map +1 -1
  8. package/lib/core/HttpResponse.mjs +14 -1
  9. package/lib/core/HttpResponse.mjs.map +1 -1
  10. package/lib/core/SetupApi.d.mts +15 -3
  11. package/lib/core/SetupApi.d.ts +15 -3
  12. package/lib/core/SetupApi.js +26 -8
  13. package/lib/core/SetupApi.js.map +1 -1
  14. package/lib/core/SetupApi.mjs +26 -8
  15. package/lib/core/SetupApi.mjs.map +1 -1
  16. package/lib/iife/index.js +48 -17
  17. package/lib/iife/index.js.map +1 -1
  18. package/lib/mockServiceWorker.js +1 -1
  19. package/lib/native/index.d.mts +10 -5
  20. package/lib/native/index.d.ts +10 -5
  21. package/lib/native/index.js +9 -9
  22. package/lib/native/index.js.map +1 -1
  23. package/lib/native/index.mjs +9 -9
  24. package/lib/native/index.mjs.map +1 -1
  25. package/lib/node/index.d.mts +26 -4
  26. package/lib/node/index.d.ts +26 -4
  27. package/lib/node/index.js +62 -13
  28. package/lib/node/index.js.map +1 -1
  29. package/lib/node/index.mjs +62 -13
  30. package/lib/node/index.mjs.map +1 -1
  31. package/package.json +15 -7
  32. package/src/browser/setupWorker/glossary.ts +1 -1
  33. package/src/browser/setupWorker/setupWorker.ts +3 -11
  34. package/src/browser/setupWorker/start/createFallbackRequestListener.ts +1 -1
  35. package/src/browser/setupWorker/start/createRequestListener.ts +1 -1
  36. package/src/browser/setupWorker/start/createResponseListener.ts +13 -0
  37. package/src/core/HttpResponse.test.ts +34 -3
  38. package/src/core/HttpResponse.ts +24 -1
  39. package/src/core/SetupApi.ts +33 -9
  40. package/src/native/index.ts +5 -5
  41. package/src/node/SetupServerApi.ts +64 -95
  42. package/src/node/SetupServerCommonApi.ts +116 -0
  43. package/src/node/glossary.ts +17 -3
  44. package/src/node/setupServer.ts +3 -10
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/node/SetupServerApi.ts","../../src/node/setupServer.ts"],"sourcesContent":["import {\n BatchInterceptor,\n HttpRequestEventMap,\n Interceptor,\n InterceptorReadyState,\n} from '@mswjs/interceptors'\nimport { invariant } from 'outvariant'\nimport { SetupApi } from '~/core/SetupApi'\nimport { RequestHandler } from '~/core/handlers/RequestHandler'\nimport { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions'\nimport { RequiredDeep } from '~/core/typeUtils'\nimport { handleRequest } from '~/core/utils/handleRequest'\nimport { devUtils } from '~/core/utils/internal/devUtils'\nimport { mergeRight } from '~/core/utils/internal/mergeRight'\nimport { SetupServer } from './glossary'\n\nconst DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {\n onUnhandledRequest: 'warn',\n}\n\nexport class SetupServerApi\n extends SetupApi<LifeCycleEventsMap>\n implements SetupServer\n{\n protected readonly interceptor: BatchInterceptor<\n Array<Interceptor<HttpRequestEventMap>>,\n HttpRequestEventMap\n >\n private resolvedOptions: RequiredDeep<SharedOptions>\n\n constructor(\n interceptors: Array<{\n new (): Interceptor<HttpRequestEventMap>\n }>,\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 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.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 { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'\nimport { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'\nimport { FetchInterceptor } from '@mswjs/interceptors/fetch'\nimport { RequestHandler } from '~/core/handlers/RequestHandler'\nimport { SetupServerApi } from './SetupServerApi'\nimport { SetupServer } from './glossary'\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): SetupServer => {\n return new SetupServerApi(\n [ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],\n ...handlers,\n )\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EAGA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB;AAIzB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAG3B,IAAM,yBAAsD;AAAA,EAC1D,oBAAoB;AACtB;AAEO,IAAM,iBAAN,cACG,SAEV;AAAA,EACqB;AAAA,EAIX;AAAA,EAER,YACE,iBAGG,UACH;AACA,UAAM,GAAG,QAAQ;AAEjB,SAAK,cAAc,IAAI,iBAAiB;AAAA,MACtC,MAAM;AAAA,MACN,cAAc,aAAa,IAAI,CAACA,iBAAgB,IAAIA,aAAY,CAAC;AAAA,IACnE,CAAC;AACD,SAAK,kBAAkB,CAAC;AAExB,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA,EAKQ,OAAa;AACnB,SAAK,YAAY,GAAG,WAAW,OAAO,EAAE,SAAS,UAAU,MAAM;AAC/D,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MACP;AAEA,UAAI,UAAU;AACZ,gBAAQ,YAAY,QAAQ;AAAA,MAC9B;AAEA;AAAA,IACF,CAAC;AAED,SAAK,YAAY;AAAA,MACf;AAAA,MACA,CAAC,EAAE,UAAU,kBAAkB,SAAS,UAAU,MAAM;AACtD,aAAK,QAAQ;AAAA,UACX,mBAAmB,oBAAoB;AAAA,UACvC;AAAA,YACE;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEO,OAAO,UAAkC,CAAC,GAAS;AACxD,SAAK,kBAAkB;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AAGA,SAAK,YAAY,MAAM;AAEvB,SAAK,cAAc,KAAK,MAAM;AAC5B,WAAK,YAAY,QAAQ;AAAA,IAC3B,CAAC;AAKD;AAAA,MACE,CAAC,sBAAsB,UAAU,sBAAsB,OAAO,EAAE;AAAA,QAC9D,KAAK,YAAY;AAAA,MACnB;AAAA,MACA,SAAS;AAAA,QACP;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEO,QAAc;AACnB,SAAK,QAAQ;AAAA,EACf;AACF;;;AChHA,SAAS,gCAAgC;AACzC,SAAS,iCAAiC;AAC1C,SAAS,wBAAwB;AAW1B,IAAM,cAAc,IACtB,aACa;AAChB,SAAO,IAAI;AAAA,IACT,CAAC,0BAA0B,2BAA2B,gBAAgB;AAAA,IACtE,GAAG;AAAA,EACL;AACF;","names":["Interceptor"]}
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.1.7",
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
- requestHandlers: Array<RequestHandler>
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)
@@ -24,7 +24,7 @@ export function createFallbackRequestListener(
24
24
  const response = await handleRequest(
25
25
  request,
26
26
  requestId,
27
- context.requestHandlers,
27
+ context.getRequestHandlers(),
28
28
  options,
29
29
  context.emitter,
30
30
  {
@@ -43,7 +43,7 @@ export const createRequestListener = (
43
43
  await handleRequest(
44
44
  request,
45
45
  requestId,
46
- context.requestHandlers,
46
+ context.getRequestHandlers(),
47
47
  options,
48
48
  context.emitter,
49
49
  {
@@ -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()', () => {
@@ -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
- JSON.stringify(body),
103
+ responseText,
81
104
  responseInit,
82
105
  ) as StrictResponse<BodyType>
83
106
  }
@@ -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 initialHandlers: ReadonlyArray<RequestHandler>
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.initialHandlers = toReadonlyArray(initialHandlers)
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.currentHandlers.unshift(...runtimeHandlers)
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.currentHandlers =
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> {
@@ -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 { SetupServerApi } from '../node/SetupServerApi'
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
- ): SetupServerApi {
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 SetupServerApi(
17
+ return new SetupServerCommonApi(
18
18
  [FetchInterceptor, XMLHttpRequestInterceptor],
19
- ...handlers,
19
+ handlers,
20
20
  )
21
21
  }
@@ -1,113 +1,82 @@
1
- import {
2
- BatchInterceptor,
3
- HttpRequestEventMap,
4
- Interceptor,
5
- InterceptorReadyState,
6
- } from '@mswjs/interceptors'
7
- import { invariant } from 'outvariant'
8
- import { SetupApi } from '~/core/SetupApi'
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 DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {
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
- constructor(
32
- interceptors: Array<{
33
- new (): Interceptor<HttpRequestEventMap>
34
- }>,
35
- ...handlers: Array<RequestHandler>
36
- ) {
37
- super(...handlers)
12
+ type RequestHandlersContext = {
13
+ initialHandlers: Array<RequestHandler>
14
+ handlers: Array<RequestHandler>
15
+ }
38
16
 
39
- this.interceptor = new BatchInterceptor({
40
- name: 'setup-server',
41
- interceptors: interceptors.map((Interceptor) => new Interceptor()),
42
- })
43
- this.resolvedOptions = {} as RequiredDeep<SharedOptions>
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
- this.init()
25
+ constructor(initialHandlers: Array<RequestHandler>) {
26
+ this.rootContext = { initialHandlers, handlers: [] }
46
27
  }
47
28
 
48
- /**
49
- * Subscribe to all requests that are using the interceptor object
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
- return
66
- })
33
+ public prepend(runtimeHandlers: Array<RequestHandler>) {
34
+ this.context.handlers.unshift(...runtimeHandlers)
35
+ }
67
36
 
68
- this.interceptor.on(
69
- 'response',
70
- ({ response, isMockedResponse, request, requestId }) => {
71
- this.emitter.emit(
72
- isMockedResponse ? 'response:mocked' : 'response:bypass',
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 listen(options: Partial<SharedOptions> = {}): void {
84
- this.resolvedOptions = mergeRight(
85
- DEFAULT_LISTEN_OPTIONS,
86
- options,
87
- ) as RequiredDeep<SharedOptions>
44
+ public currentHandlers(): Array<RequestHandler> {
45
+ const { initialHandlers, handlers } = this.context
46
+ return handlers.concat(initialHandlers)
47
+ }
48
+ }
88
49
 
89
- // Apply the interceptor when starting the server.
90
- this.interceptor.apply()
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.subscriptions.push(() => {
93
- this.interceptor.dispose()
94
- })
60
+ this.handlersController = new AsyncHandlersController(handlers)
61
+ }
95
62
 
96
- // Assert that the interceptor has been applied successfully.
97
- // Also guards us from forgetting to call "interceptor.apply()"
98
- // as a part of the "listen" method.
99
- invariant(
100
- [InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes(
101
- this.interceptor.readyState,
102
- ),
103
- devUtils.formatMessage(
104
- 'Failed to start "setupServer": the interceptor failed to apply. This is likely an issue with the library and you should report it at "%s".',
105
- ),
106
- 'https://github.com/mswjs/msw/issues/new/choose',
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
- this.dispose()
79
+ super.close()
80
+ store.disable()
112
81
  }
113
82
  }