Repository: mswjs/msw
Branch: main
Commit: 002f3e7d86dd
Files: 584
Total size: 1021.2 KB
Directory structure:
gitextract_krzcz2ns/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-issue-browser.yml
│ │ ├── 02-issue-nodejs.yml
│ │ └── 03-feature.yml
│ └── workflows/
│ ├── auto.yml
│ ├── ci.yml
│ ├── compat.yml
│ ├── lock-closed-issues.yml
│ ├── release-preview.yml
│ ├── release.yml
│ ├── smoke-test.yml
│ └── typescript-nightly.yml
├── .gitignore
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── browser/
│ └── package.json
├── cli/
│ ├── index.js
│ ├── init.js
│ ├── invariant.js
│ └── package.json
├── commitlint.config.js
├── config/
│ ├── constants.js
│ ├── copyServiceWorker.ts
│ ├── package.json
│ ├── plugins/
│ │ └── esbuild/
│ │ ├── copyWorkerPlugin.ts
│ │ ├── forceEsmExtensionsPlugin.ts
│ │ ├── graphQLImportPlugin.ts
│ │ └── resolveCoreImportsPlugin.ts
│ ├── polyfills-node.ts
│ ├── replaceCoreImports.js
│ └── scripts/
│ ├── patch-ts.js
│ ├── postinstall.js
│ └── smoke-test.sh
├── decisions/
│ ├── jest-support.md
│ ├── linting-worker-script.md
│ ├── releases.md
│ └── typescript-versioning.md
├── eslint.config.mjs
├── global.d.ts
├── knip.json
├── native/
│ └── package.json
├── node/
│ └── package.json
├── package.json
├── release.config.json
├── src/
│ ├── browser/
│ │ ├── global.browser.d.ts
│ │ ├── index.ts
│ │ ├── setupWorker/
│ │ │ ├── glossary.ts
│ │ │ ├── setupWorker.node.test.ts
│ │ │ ├── setupWorker.ts
│ │ │ ├── start/
│ │ │ │ ├── createFallbackRequestListener.ts
│ │ │ │ ├── createRequestListener.ts
│ │ │ │ ├── createResponseListener.ts
│ │ │ │ ├── createStartHandler.ts
│ │ │ │ └── utils/
│ │ │ │ ├── enableMocking.ts
│ │ │ │ ├── getWorkerByRegistration.ts
│ │ │ │ ├── getWorkerInstance.ts
│ │ │ │ ├── prepareStartHandler.test.ts
│ │ │ │ ├── prepareStartHandler.ts
│ │ │ │ ├── printStartMessage.test.ts
│ │ │ │ ├── printStartMessage.ts
│ │ │ │ └── validateWorkerScope.ts
│ │ │ └── stop/
│ │ │ └── utils/
│ │ │ ├── printStopMessage.test.ts
│ │ │ └── printStopMessage.ts
│ │ ├── tsconfig.browser.build.json
│ │ ├── tsconfig.browser.json
│ │ └── utils/
│ │ ├── checkWorkerIntegrity.ts
│ │ ├── deserializeRequest.ts
│ │ ├── getAbsoluteWorkerUrl.test.ts
│ │ ├── getAbsoluteWorkerUrl.ts
│ │ ├── pruneGetRequestBody.test.ts
│ │ ├── pruneGetRequestBody.ts
│ │ ├── supports.ts
│ │ └── workerChannel.ts
│ ├── core/
│ │ ├── HttpResponse.test.ts
│ │ ├── HttpResponse.ts
│ │ ├── SetupApi.ts
│ │ ├── bypass.test.ts
│ │ ├── bypass.ts
│ │ ├── delay.ts
│ │ ├── getResponse.test.ts
│ │ ├── getResponse.ts
│ │ ├── graphql.test.ts
│ │ ├── graphql.ts
│ │ ├── handlers/
│ │ │ ├── GraphQLHandler.test.ts
│ │ │ ├── GraphQLHandler.ts
│ │ │ ├── HttpHandler.test.ts
│ │ │ ├── HttpHandler.ts
│ │ │ ├── RequestHandler.ts
│ │ │ ├── WebSocketHandler.test.ts
│ │ │ ├── WebSocketHandler.ts
│ │ │ └── common.ts
│ │ ├── http.test.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── isCommonAssetRequest.ts
│ │ ├── passthrough.test.ts
│ │ ├── passthrough.ts
│ │ ├── sharedOptions.ts
│ │ ├── sse.ts
│ │ ├── typeUtils.ts
│ │ ├── utils/
│ │ │ ├── HttpResponse/
│ │ │ │ └── decorators.ts
│ │ │ ├── cookieStore.ts
│ │ │ ├── executeHandlers.ts
│ │ │ ├── handleRequest.test.ts
│ │ │ ├── handleRequest.ts
│ │ │ ├── internal/
│ │ │ │ ├── Disposable.ts
│ │ │ │ ├── checkGlobals.ts
│ │ │ │ ├── devUtils.test.ts
│ │ │ │ ├── devUtils.ts
│ │ │ │ ├── getCallFrame.test.ts
│ │ │ │ ├── getCallFrame.ts
│ │ │ │ ├── hasRefCounted.test.ts
│ │ │ │ ├── hasRefCounted.ts
│ │ │ │ ├── isHandlerKind.test.ts
│ │ │ │ ├── isHandlerKind.ts
│ │ │ │ ├── isIterable.test.ts
│ │ │ │ ├── isIterable.ts
│ │ │ │ ├── isObject.test.ts
│ │ │ │ ├── isObject.ts
│ │ │ │ ├── isStringEqual.test.ts
│ │ │ │ ├── isStringEqual.ts
│ │ │ │ ├── jsonParse.test.ts
│ │ │ │ ├── jsonParse.ts
│ │ │ │ ├── mergeRight.test.ts
│ │ │ │ ├── mergeRight.ts
│ │ │ │ ├── parseGraphQLRequest.test.ts
│ │ │ │ ├── parseGraphQLRequest.ts
│ │ │ │ ├── parseMultipartData.test.ts
│ │ │ │ ├── parseMultipartData.ts
│ │ │ │ ├── pipeEvents.test.ts
│ │ │ │ ├── pipeEvents.ts
│ │ │ │ ├── requestHandlerUtils.ts
│ │ │ │ ├── toReadonlyArray.test.ts
│ │ │ │ ├── toReadonlyArray.ts
│ │ │ │ ├── tryCatch.test.ts
│ │ │ │ └── tryCatch.ts
│ │ │ ├── logging/
│ │ │ │ ├── getStatusCodeColor.test.ts
│ │ │ │ ├── getStatusCodeColor.ts
│ │ │ │ ├── getTimestamp.test.ts
│ │ │ │ ├── getTimestamp.ts
│ │ │ │ ├── serializeRequest.test.ts
│ │ │ │ ├── serializeRequest.ts
│ │ │ │ ├── serializeResponse.test.ts
│ │ │ │ └── serializeResponse.ts
│ │ │ ├── matching/
│ │ │ │ ├── matchRequestUrl.test.ts
│ │ │ │ ├── matchRequestUrl.ts
│ │ │ │ ├── normalizePath.node.test.ts
│ │ │ │ ├── normalizePath.test.ts
│ │ │ │ └── normalizePath.ts
│ │ │ ├── request/
│ │ │ │ ├── getAllAcceptedMimeTypes.test.ts
│ │ │ │ ├── getAllAcceptedMimeTypes.ts
│ │ │ │ ├── getRequestCookies.ts
│ │ │ │ ├── onUnhandledRequest.node.test.ts
│ │ │ │ ├── onUnhandledRequest.test.ts
│ │ │ │ ├── onUnhandledRequest.ts
│ │ │ │ ├── storeResponseCookies.ts
│ │ │ │ ├── toPublicUrl.node.test.ts
│ │ │ │ ├── toPublicUrl.test.ts
│ │ │ │ └── toPublicUrl.ts
│ │ │ ├── toResponseInit.ts
│ │ │ └── url/
│ │ │ ├── cleanUrl.test.ts
│ │ │ ├── cleanUrl.ts
│ │ │ ├── getAbsoluteUrl.node.test.ts
│ │ │ ├── getAbsoluteUrl.test.ts
│ │ │ ├── getAbsoluteUrl.ts
│ │ │ ├── isAbsoluteUrl.test.ts
│ │ │ └── isAbsoluteUrl.ts
│ │ ├── ws/
│ │ │ ├── WebSocketClientManager.test.ts
│ │ │ ├── WebSocketClientManager.ts
│ │ │ ├── WebSocketClientStore.ts
│ │ │ ├── WebSocketIndexedDBClientStore.ts
│ │ │ ├── WebSocketMemoryClientStore.ts
│ │ │ ├── handleWebSocketEvent.ts
│ │ │ ├── utils/
│ │ │ │ ├── attachWebSocketLogger.ts
│ │ │ │ ├── getMessageLength.test.ts
│ │ │ │ ├── getMessageLength.ts
│ │ │ │ ├── getPublicData.test.ts
│ │ │ │ ├── getPublicData.ts
│ │ │ │ ├── truncateMessage.test.ts
│ │ │ │ └── truncateMessage.ts
│ │ │ └── webSocketInterceptor.ts
│ │ ├── ws.test.ts
│ │ └── ws.ts
│ ├── iife/
│ │ └── index.ts
│ ├── mockServiceWorker.js
│ ├── native/
│ │ └── index.ts
│ ├── node/
│ │ ├── SetupServerApi.ts
│ │ ├── SetupServerCommonApi.ts
│ │ ├── glossary.ts
│ │ ├── index.ts
│ │ └── setupServer.ts
│ ├── package.json
│ ├── shims/
│ │ ├── cookie.ts
│ │ └── statuses.ts
│ ├── tsconfig.core.build.json
│ ├── tsconfig.node.build.json
│ ├── tsconfig.node.json
│ ├── tsconfig.src.json
│ └── tsconfig.worker.json
├── test/
│ ├── README.md
│ ├── browser/
│ │ ├── graphql-api/
│ │ │ ├── anonymous-operation.mocks.ts
│ │ │ ├── anonymous-operation.test.ts
│ │ │ ├── cookies.mocks.ts
│ │ │ ├── cookies.test.ts
│ │ │ ├── custom-predicate.mocks.ts
│ │ │ ├── custom-predicate.test.ts
│ │ │ ├── document-node.mocks.ts
│ │ │ ├── errors.mocks.ts
│ │ │ ├── errors.test.ts
│ │ │ ├── extensions.mocks.ts
│ │ │ ├── extensions.test.ts
│ │ │ ├── link.mocks.ts
│ │ │ ├── link.test.ts
│ │ │ ├── logging.mocks.ts
│ │ │ ├── logging.test.ts
│ │ │ ├── multipart-data.mocks.ts
│ │ │ ├── multipart-data.test.ts
│ │ │ ├── mutation.mocks.ts
│ │ │ ├── mutation.test.ts
│ │ │ ├── operation-reference.mocks.ts
│ │ │ ├── operation-reference.test.ts
│ │ │ ├── operation.mocks.ts
│ │ │ ├── operation.test.ts
│ │ │ ├── query.mocks.ts
│ │ │ ├── query.test.ts
│ │ │ ├── response-patching.mocks.ts
│ │ │ ├── response-patching.test.ts
│ │ │ ├── variables.mocks.ts
│ │ │ └── variables.test.ts
│ │ ├── msw-api/
│ │ │ ├── context/
│ │ │ │ ├── delay.mocks.ts
│ │ │ │ └── delay.test.ts
│ │ │ ├── distribution/
│ │ │ │ ├── iife.mocks.js
│ │ │ │ └── iife.test.ts
│ │ │ ├── exception-handling.mocks.ts
│ │ │ ├── exception-handling.test.ts
│ │ │ ├── hard-reload.mocks.ts
│ │ │ ├── hard-reload.test.ts
│ │ │ ├── integrity-check-invalid.mocks.ts
│ │ │ ├── integrity-check-valid.mocks.ts
│ │ │ ├── integrity-check.test.ts
│ │ │ ├── regression/
│ │ │ │ ├── 2129-worker-use.mocks.ts
│ │ │ │ ├── 2129-worker-use.test.ts
│ │ │ │ ├── handle-stream.mocks.ts
│ │ │ │ ├── handle-stream.test.ts
│ │ │ │ ├── null-body.mocks.ts
│ │ │ │ └── null-body.test.ts
│ │ │ ├── req/
│ │ │ │ ├── passthrough.mocks.ts
│ │ │ │ └── passthrough.test.ts
│ │ │ ├── res/
│ │ │ │ ├── network-error.mocks.ts
│ │ │ │ └── network-error.test.ts
│ │ │ ├── setup-worker/
│ │ │ │ ├── fallback-mode/
│ │ │ │ │ ├── fallback-mode.mocks.ts
│ │ │ │ │ └── fallback-mode.test.ts
│ │ │ │ ├── input-validation.mocks.ts
│ │ │ │ ├── input-validation.test.ts
│ │ │ │ ├── life-cycle-events/
│ │ │ │ │ ├── on.mocks.ts
│ │ │ │ │ ├── on.test.ts
│ │ │ │ │ ├── removeAllListeners.test.ts
│ │ │ │ │ └── removeListener.test.ts
│ │ │ │ ├── listHandlers.mocks.ts
│ │ │ │ ├── listHandlers.test.ts
│ │ │ │ ├── resetHandlers.test.ts
│ │ │ │ ├── response-logging.test.ts
│ │ │ │ ├── restoreHandlers.test.ts
│ │ │ │ ├── scenarios/
│ │ │ │ │ ├── custom-transformers.mocks.ts
│ │ │ │ │ ├── custom-transformers.test.ts
│ │ │ │ │ ├── errors/
│ │ │ │ │ │ ├── internal-error.mocks.ts
│ │ │ │ │ │ ├── internal-error.test.ts
│ │ │ │ │ │ ├── network-error.mocks.ts
│ │ │ │ │ │ └── network-error.test.ts
│ │ │ │ │ ├── fall-through.mocks.ts
│ │ │ │ │ ├── fall-through.test.ts
│ │ │ │ │ ├── iframe/
│ │ │ │ │ │ ├── app.html
│ │ │ │ │ │ ├── iframe.mocks.ts
│ │ │ │ │ │ ├── iframe.test.ts
│ │ │ │ │ │ ├── multiple-workers/
│ │ │ │ │ │ │ ├── child.mocks.ts
│ │ │ │ │ │ │ ├── iframe-multiple-workers.test.ts
│ │ │ │ │ │ │ └── parent.mocks.ts
│ │ │ │ │ │ ├── page-in-iframe.html
│ │ │ │ │ │ └── page-in-nested-iframe.html
│ │ │ │ │ ├── iframe-isolated-response/
│ │ │ │ │ │ ├── app.html
│ │ │ │ │ │ ├── iframe-isolated-response.mocks.ts
│ │ │ │ │ │ ├── iframe-isolated-response.test.ts
│ │ │ │ │ │ ├── one.html
│ │ │ │ │ │ └── two.html
│ │ │ │ │ ├── scope/
│ │ │ │ │ │ ├── scope-nested-quiet.mocks.ts
│ │ │ │ │ │ ├── scope-nested.mocks.ts
│ │ │ │ │ │ ├── scope-root.mocks.ts
│ │ │ │ │ │ └── scope-validation.test.ts
│ │ │ │ │ ├── shared-worker/
│ │ │ │ │ │ ├── shared-worker.mocks.ts
│ │ │ │ │ │ ├── shared-worker.test.ts
│ │ │ │ │ │ └── worker.js
│ │ │ │ │ ├── text-event-stream.mocks.ts
│ │ │ │ │ └── text-event-stream.test.ts
│ │ │ │ ├── start/
│ │ │ │ │ ├── error.mocks.ts
│ │ │ │ │ ├── error.test.ts
│ │ │ │ │ ├── find-worker.error.mocks.ts
│ │ │ │ │ ├── find-worker.mocks.ts
│ │ │ │ │ ├── find-worker.test.ts
│ │ │ │ │ ├── on-unhandled-request/
│ │ │ │ │ │ ├── bypass.mocks.ts
│ │ │ │ │ │ ├── bypass.test.ts
│ │ │ │ │ │ ├── callback-print.mocks.ts
│ │ │ │ │ │ ├── callback-print.test.ts
│ │ │ │ │ │ ├── callback-throws.mocks.ts
│ │ │ │ │ │ ├── callback.mocks.ts
│ │ │ │ │ │ ├── callback.test.ts
│ │ │ │ │ │ ├── default.mocks.ts
│ │ │ │ │ │ ├── default.test.ts
│ │ │ │ │ │ ├── warn.mocks.ts
│ │ │ │ │ │ └── warn.test.ts
│ │ │ │ │ ├── options-sw-scope.mocks.ts
│ │ │ │ │ ├── options-sw-scope.test.ts
│ │ │ │ │ ├── quiet.mocks.ts
│ │ │ │ │ ├── quiet.test.ts
│ │ │ │ │ ├── start.mocks.ts
│ │ │ │ │ ├── start.test.ts
│ │ │ │ │ ├── warn-on-wait-until-ready.mocks.ts
│ │ │ │ │ ├── warn-on-wait-until-ready.test.ts
│ │ │ │ │ └── worker.delayed.js
│ │ │ │ ├── stop/
│ │ │ │ │ ├── in-flight-request.mocks.ts
│ │ │ │ │ ├── in-flight-request.test.ts
│ │ │ │ │ ├── quiet.mocks.ts
│ │ │ │ │ ├── quiet.test.ts
│ │ │ │ │ ├── removes-all-listeners.mocks.ts
│ │ │ │ │ └── removes-all-listeners.test.ts
│ │ │ │ ├── stop.mocks.ts
│ │ │ │ ├── stop.test.ts
│ │ │ │ ├── use.mocks.ts
│ │ │ │ ├── use.test.ts
│ │ │ │ ├── worker-passthrough-header.mocks.ts
│ │ │ │ └── worker-passthrough-header.test.ts
│ │ │ ├── unregister.mocks.ts
│ │ │ └── unregister.test.ts
│ │ ├── playwright.config.ts
│ │ ├── playwright.extend.ts
│ │ ├── rest-api/
│ │ │ ├── 204-response.test.ts
│ │ │ ├── 206-response.mocks.ts
│ │ │ ├── 206-response.test.ts
│ │ │ ├── basic.mocks.ts
│ │ │ ├── basic.test.ts
│ │ │ ├── body.mocks.ts
│ │ │ ├── body.test.ts
│ │ │ ├── context.mocks.ts
│ │ │ ├── context.test.ts
│ │ │ ├── cors.mocks.ts
│ │ │ ├── cors.test.ts
│ │ │ ├── generator.mocks.ts
│ │ │ ├── generator.test.ts
│ │ │ ├── headers-multiple.mocks.ts
│ │ │ ├── headers-multiple.test.ts
│ │ │ ├── logging.test.ts
│ │ │ ├── params.mocks.ts
│ │ │ ├── params.test.ts
│ │ │ ├── plain-response.mocks.ts
│ │ │ ├── plain-response.test.ts
│ │ │ ├── query-params-warning.mocks.ts
│ │ │ ├── query-params-warning.test.ts
│ │ │ ├── query.mocks.ts
│ │ │ ├── query.test.ts
│ │ │ ├── redirect.mocks.ts
│ │ │ ├── redirect.test.ts
│ │ │ ├── request/
│ │ │ │ ├── body/
│ │ │ │ │ ├── body-arraybuffer-range.mocks.ts
│ │ │ │ │ ├── body-arraybuffer-range.test.ts
│ │ │ │ │ ├── body-arraybuffer.test.ts
│ │ │ │ │ ├── body-form-data.page.html
│ │ │ │ │ ├── body-form-data.test.ts
│ │ │ │ │ ├── body-json.test.ts
│ │ │ │ │ ├── body-text.test.ts
│ │ │ │ │ └── body.mocks.ts
│ │ │ │ ├── matching/
│ │ │ │ │ ├── all.mocks.ts
│ │ │ │ │ ├── all.test.ts
│ │ │ │ │ ├── custom-predicate.mocks.ts
│ │ │ │ │ ├── custom-predicate.test.ts
│ │ │ │ │ ├── method.mocks.ts
│ │ │ │ │ ├── method.test.ts
│ │ │ │ │ ├── path-params-decode.mocks.ts
│ │ │ │ │ ├── path-params-decode.test.ts
│ │ │ │ │ ├── uri.mocks.ts
│ │ │ │ │ └── uri.test.ts
│ │ │ │ ├── request-cookies.mocks.ts
│ │ │ │ └── request-cookies.test.ts
│ │ │ ├── response/
│ │ │ │ ├── body/
│ │ │ │ │ ├── body-binary.mocks.ts
│ │ │ │ │ ├── body-binary.test.ts
│ │ │ │ │ ├── body-blob.mocks.ts
│ │ │ │ │ ├── body-blob.test.ts
│ │ │ │ │ ├── body-formdata.mocks.ts
│ │ │ │ │ ├── body-formdata.test.ts
│ │ │ │ │ ├── body-html.mocks.ts
│ │ │ │ │ ├── body-html.test.ts
│ │ │ │ │ ├── body-json.mocks.ts
│ │ │ │ │ ├── body-json.test.ts
│ │ │ │ │ ├── body-stream.mocks.ts
│ │ │ │ │ ├── body-stream.test.ts
│ │ │ │ │ ├── body-text.mocks.ts
│ │ │ │ │ ├── body-text.test.ts
│ │ │ │ │ ├── body-xml.mocks.ts
│ │ │ │ │ └── body-xml.test.ts
│ │ │ │ ├── response-cookies.mocks.ts
│ │ │ │ ├── response-cookies.test.ts
│ │ │ │ ├── response-error.mocks.ts
│ │ │ │ ├── response-error.test.ts
│ │ │ │ ├── throw-response.mocks.ts
│ │ │ │ └── throw-response.test.ts
│ │ │ ├── response-patching.mocks.ts
│ │ │ ├── response-patching.test.ts
│ │ │ ├── send-beacon.mocks.ts
│ │ │ ├── send-beacon.test.ts
│ │ │ ├── status.mocks.ts
│ │ │ ├── status.test.ts
│ │ │ ├── xhr.mocks.ts
│ │ │ └── xhr.test.ts
│ │ ├── setup/
│ │ │ ├── webpackHttpServer.ts
│ │ │ └── workerConsole.ts
│ │ ├── sse-api/
│ │ │ ├── sse.client.send.extraneous.test.ts
│ │ │ ├── sse.client.send.multiline.test.ts
│ │ │ ├── sse.client.send.test.ts
│ │ │ ├── sse.mocks.ts
│ │ │ ├── sse.quiet.test.ts
│ │ │ ├── sse.retry.test.ts
│ │ │ ├── sse.server.connect.test.ts
│ │ │ ├── sse.use.test.ts
│ │ │ └── sse.with-credentials.test.ts
│ │ ├── third-party/
│ │ │ ├── axios-upload.browser.test.ts
│ │ │ └── axios-upload.runtime.js
│ │ └── ws-api/
│ │ ├── ws.apply.browser.test.ts
│ │ ├── ws.client.send.test.ts
│ │ ├── ws.clients.browser.test.ts
│ │ ├── ws.intercept.client.browser.test.ts
│ │ ├── ws.intercept.server.browser.test.ts
│ │ ├── ws.logging.browser.test.ts
│ │ ├── ws.runtime.js
│ │ ├── ws.server.connect.browser.test.ts
│ │ └── ws.use.browser.test.ts
│ ├── e2e/
│ │ ├── auto-update-worker.node.test.ts
│ │ ├── cli-init.node.test.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── vitest.d.ts
│ │ └── vitest.global.setup.ts
│ ├── global.d.ts
│ ├── modules/
│ │ ├── browser/
│ │ │ ├── esm-browser.test.ts
│ │ │ └── playwright.config.ts
│ │ ├── module-utils.ts
│ │ └── node/
│ │ ├── esm-node.test.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── native/
│ │ ├── node-events.native.test.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ └── vitest.setup.ts
│ ├── node/
│ │ ├── graphql-api/
│ │ │ ├── anonymous-operations.test.ts
│ │ │ ├── batched-queries.apollo.test.ts
│ │ │ ├── batched-queries.batched-execute.test.ts
│ │ │ ├── compatibility.node.test.ts
│ │ │ ├── content-type.test.ts
│ │ │ ├── cookies.test.ts
│ │ │ ├── custom-predicate.node.test.ts
│ │ │ ├── extensions.node.test.ts
│ │ │ ├── response-patching.node.test.ts
│ │ │ ├── typed-document-node.test.ts
│ │ │ └── typed-document-string.test.ts
│ │ ├── msw-api/
│ │ │ ├── context/
│ │ │ │ ├── delay-infinite.fixture.js
│ │ │ │ └── delay.node.test.ts
│ │ │ ├── req/
│ │ │ │ └── passthrough.node.test.ts
│ │ │ ├── res/
│ │ │ │ └── network-error.node.test.ts
│ │ │ └── setup-server/
│ │ │ ├── boundary/
│ │ │ │ ├── boundary.args.test.ts
│ │ │ │ ├── boundary.concurrency.test.ts
│ │ │ │ └── boundary.handlers.test.ts
│ │ │ ├── input-validation.node.test.ts
│ │ │ ├── life-cycle-events/
│ │ │ │ ├── on.node.test.ts
│ │ │ │ ├── removeAllListeners.node.test.ts
│ │ │ │ └── removeListener.node.test.ts
│ │ │ ├── listHandlers.node.test.ts
│ │ │ ├── resetHandlers.node.test.ts
│ │ │ ├── restoreHandlers.node.test.ts
│ │ │ ├── scenarios/
│ │ │ │ ├── cookies-request.node.test.ts
│ │ │ │ ├── custom-interceptors.node.test.ts
│ │ │ │ ├── custom-transformers.node.test.ts
│ │ │ │ ├── fake-timers.node.test.ts
│ │ │ │ ├── fall-through.node.test.ts
│ │ │ │ ├── fetch.node.test.ts
│ │ │ │ ├── generator.node.test.ts
│ │ │ │ ├── graphql.node.test.ts
│ │ │ │ ├── http.node.test.ts
│ │ │ │ ├── https.node.test.ts
│ │ │ │ ├── on-unhandled-request/
│ │ │ │ │ ├── bypass.node.test.ts
│ │ │ │ │ ├── callback-throws.node.test.ts
│ │ │ │ │ ├── callback.node.test.ts
│ │ │ │ │ ├── default.node.test.ts
│ │ │ │ │ ├── error.node.test.ts
│ │ │ │ │ └── warn.node.test.ts
│ │ │ │ ├── relative-url.node.test.ts
│ │ │ │ ├── response-patching.node.test.ts
│ │ │ │ └── xhr.node.test.ts
│ │ │ └── use.node.test.ts
│ │ ├── regressions/
│ │ │ ├── 2129-server-use.test.ts
│ │ │ ├── 2370-listen-after-close.test.ts
│ │ │ ├── many-request-handlers-jsdom.test.ts
│ │ │ ├── many-request-handlers.test.ts
│ │ │ ├── miniflare.node.test.ts
│ │ │ └── mixed-graphql-http-with-query-in-body.node.test.ts
│ │ ├── rest-api/
│ │ │ ├── cookies-inheritance.node.test.ts
│ │ │ ├── https.node.test.ts
│ │ │ ├── request/
│ │ │ │ ├── body/
│ │ │ │ │ ├── body-arraybuffer.node.test.ts
│ │ │ │ │ ├── body-blob.node.test.ts
│ │ │ │ │ ├── body-form-data.node.test.ts
│ │ │ │ │ ├── body-json.node.test.ts
│ │ │ │ │ ├── body-protobuf.node.test.ts
│ │ │ │ │ ├── body-text.node.test.ts
│ │ │ │ │ └── body-used.node.test.ts
│ │ │ │ └── matching/
│ │ │ │ ├── all.node.test.ts
│ │ │ │ ├── custom-predicate.node.test.ts
│ │ │ │ ├── path-params-decode.node.test.ts
│ │ │ │ ├── path-params-optional.node.test.ts
│ │ │ │ └── relative-url.node.test.ts
│ │ │ ├── response/
│ │ │ │ ├── body-binary.node.test.ts
│ │ │ │ ├── body-html.node.test.ts
│ │ │ │ ├── body-json.node.test.ts
│ │ │ │ ├── body-stream.node.test.ts
│ │ │ │ ├── body-text.node.test.ts
│ │ │ │ ├── body-xml.node.test.ts
│ │ │ │ ├── generator.test.ts
│ │ │ │ ├── response-cookies.test.ts
│ │ │ │ ├── response-error.test.ts
│ │ │ │ └── throw-response.node.test.ts
│ │ │ └── response-patching.node.test.ts
│ │ ├── third-party/
│ │ │ ├── axios-error-response.test.ts
│ │ │ ├── axios-timeout.node.test.ts
│ │ │ └── axios-upload.node.test.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ └── ws-api/
│ │ ├── on-unhandled-request/
│ │ │ ├── callback.test.ts
│ │ │ ├── error.test.ts
│ │ │ └── warn.test.ts
│ │ ├── ws.apply.test.ts
│ │ ├── ws.event-patching.test.ts
│ │ ├── ws.intercept.client.test.ts
│ │ ├── ws.intercept.server.test.ts
│ │ ├── ws.server.connect.test.ts
│ │ ├── ws.stop-propagation.test.ts
│ │ ├── ws.unhandled-exception.test.ts
│ │ └── ws.use.test.ts
│ ├── package.json
│ ├── support/
│ │ ├── WebSocketServer.ts
│ │ ├── alias.ts
│ │ ├── environments/
│ │ │ └── vitest-environment-node-websocket.ts
│ │ ├── graphql.ts
│ │ ├── msw-esm/
│ │ │ └── package.json
│ │ ├── utils.ts
│ │ └── waitFor.ts
│ ├── tsconfig.json
│ └── typings/
│ ├── custom-handler.test-d.ts
│ ├── custom-resolver.test-d.ts
│ ├── graphql-custom-predicate.test-d.ts
│ ├── graphql-typed-document-node.test-d.ts
│ ├── graphql-typed-document-string.test-d.ts
│ ├── graphql.test-d.ts
│ ├── http-custom-predicate.test-d.ts
│ ├── http.test-d.ts
│ ├── regressions/
│ │ ├── default-resolver-type.test-d.ts
│ │ ├── request-handler-type.test-d.ts
│ │ └── response-body-type.test-d.ts
│ ├── resolver-generator.test-d.ts
│ ├── server.boundary.test-d.ts
│ ├── setup-server.test-d.ts
│ ├── setup-worker.test-d.ts
│ ├── sse.test-d.ts
│ ├── tsconfig.5.0.json
│ ├── tsconfig.5.1.json
│ ├── tsconfig.5.2.json
│ ├── tsconfig.5.3.json
│ ├── tsconfig.5.4.json
│ ├── tsconfig.5.5.json
│ ├── tsconfig.json
│ ├── vitest.config.ts
│ └── ws.test-d.ts
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.test.unit.json
├── tsup.config.ts
└── vitest.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: mswjs
open_collective: mswjs
================================================
FILE: .github/ISSUE_TEMPLATE/01-issue-browser.yml
================================================
name: 'Bug report (Browser)'
description: I experience unexpected behavior using the library in a browser.
labels: ['bug', 'scope:browser', 'needs:triage']
body:
- type: markdown
attributes:
value: Thank you for reporting an issue to Mock Service Worker! Please fill in the template below to help our team tackle it in the most efficient way.
- type: checkboxes
attributes:
label: Prerequisites
description: Before we begin, let's make sure your issue hasn't been solved already.
options:
- label: I confirm my issue is not in the [opened issues](https://github.com/mswjs/msw/issues)
required: true
- label: I confirm the [Frequently Asked Questions](https://mswjs.io/docs/faq) didn't contain the answer to my issue
required: true
- type: checkboxes
attributes:
label: Environment check
options:
- label: I'm using the [latest](https://github.com/mswjs/msw/releases/latest) `msw` version
required: true
- label: I'm using Node.js version 20 or higher
required: true
- type: dropdown
attributes:
label: Browsers
description: Select in which browser(s) you're experiencing the issue.
multiple: true
options:
- 'Chromium (Chrome, Brave, etc.)'
- 'Firefox'
- 'Safari'
- type: input
attributes:
label: Reproduction repository
description: A link to the repository where your issue can be reproduced. You can clone one of [our examples](https://github.com/mswjs/examples) to create a reproduction repository much faster. **Issues without a reproduction repository will be closed**.
placeholder: i.e. https://github.com/you/msw-issue
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Include any steps necessary to reproduce the issue in the repository above.
placeholder: i.e. "npm test"
validations:
required: true
- type: textarea
attributes:
label: Current behavior
description: Share any details on what behavior you're experiencing (error messages, logs, context).
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: What do you expect to happen instead?
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/02-issue-nodejs.yml
================================================
name: 'Bug report (Node.js)'
description: I experience unexpected behavior using the library in Node.js (Vitest/React Native/Express/etc.).
labels: ['bug', 'scope:node', 'needs:triage']
body:
- type: markdown
attributes:
value: Thank you for reporting an issue to Mock Service Worker! Please fill in the template below to help our team tackle it in the most efficient way.
- type: checkboxes
attributes:
label: Prerequisites
description: Before we begin, let's make sure your issue hasn't been solved already.
options:
- label: I confirm my issue is not in the [opened issues](https://github.com/mswjs/msw/issues)
required: true
- label: I confirm the [Frequently Asked Questions](https://mswjs.io/docs/faq) didn't contain the answer to my issue
required: true
- type: checkboxes
attributes:
label: Environment check
description: Next, let's make sure you're using the library in the officially supported environments.
options:
- label: I'm using the [latest](https://github.com/mswjs/msw/releases/latest) `msw` version
required: true
- label: I'm using Node.js version 20 or higher
required: true
- type: input
attributes:
label: Node.js version
description: Specify which Node.js version you're using (`node -v`).
placeholder: i.e. v20.18.0
validations:
required: true
- type: input
attributes:
label: Reproduction repository
description: A link to the repository where your issue can be reproduced. You can clone one of [our examples](https://github.com/mswjs/examples) to create a reproduction repository much faster. **Issues without a reproduction repository will be closed**.
placeholder: i.e. https://github.com/you/msw-issue
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Include any steps necessary to reproduce the issue in the repository above.
placeholder: i.e. "npm test"
validations:
required: true
- type: textarea
attributes:
label: Current behavior
description: Share any details on what behavior you're experiencing (error messages, logs, context).
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: What do you expect to happen instead?
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/03-feature.yml
================================================
name: 'Feature request'
description: Propose a feature or improvement.
labels: [feature]
body:
- type: markdown
attributes:
value: Thank you for deciding to make Mock Service Worker better! Please fill in the form below so our team could understand your feature better.
- type: dropdown
attributes:
label: Scope
multiple: false
options:
- 'Adds a new behavior'
- 'Improves an existing behavior'
validations:
required: true
- type: checkboxes
attributes:
label: Compatibility
description: Check if your proposal is a breaking change. Leave this field unchecked if not sure.
options:
- label: This is a breaking change
required: false
- type: textarea
attributes:
label: Feature description
description: "Describe the feature you're suggesting. Be as detailed as possible: include behavior description, pseudo-code snippets, comparison to similar features."
validations:
required: true
================================================
FILE: .github/workflows/auto.yml
================================================
name: auto
on: [push]
jobs:
cancel-previous-workflows:
runs-on: macos-latest
timeout-minutes: 3
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
workflow_id: ci.yml
access_token: ${{ github.token }}
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
build-20:
name: build (20)
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
- name: Cache build output
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-20-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-20
build-22:
name: build (22)
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
- name: Cache build output
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-22-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-22
test-unit:
name: test (unit)
needs: build-20
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore build cache
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-20-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-20-build-
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps --only-shell
- name: Unit tests
run: pnpm test:unit
test-node-20:
name: test (node.js) (20)
needs: build-20
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore build cache
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-20-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-20-build-
- name: Install dependencies
run: pnpm install
- name: Node.js tests
run: pnpm test:node
test-node-22:
name: test (node.js) (22)
needs: build-22
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Restore build cache
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-22-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-22-build-
- name: Install dependencies
run: pnpm install
- name: Node.js tests
run: pnpm test:node
test-e2e:
name: test (e2e) (20)
needs: build-20
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: E2E tests
run: pnpm test:e2e
test-browser:
name: test (browser)
needs: build-20
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore build cache
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-20-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-20-build-
- name: Install dependencies
run: pnpm install
- name: Playwright install
run: pnpm exec playwright install chromium --with-deps --only-shell
- name: Browser tests
run: pnpm test:browser
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: test/browser/test-results
test-native:
name: test (react-native)
needs: build-20
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore build cache
uses: actions/cache@v4
with:
path: ./lib
key: ${{ runner.os }}-node-20-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-20-build-
- name: Install dependencies
run: pnpm install
- name: React Native tests
run: pnpm test:native
================================================
FILE: .github/workflows/compat.yml
================================================
name: compat
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
# Validate the package.json exports and emitted CJS/ESM bundles.
exports:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps --only-shell
- name: Build
run: pnpm build
- name: Lint package
run: pnpm publint
- name: Test modules (Node.js)
run: pnpm test:modules:node
- name: Test modules (browser)
run: pnpm test:modules:browser
# Checks the library's compatibility with different
# TypeScript versions to discover type regressions.
typescript:
runs-on: macos-latest
# Skip TypeScript compatibility check on "main".
# A merged pull request implies passing "typescript" job.
if: github.ref != 'refs/heads/main'
strategy:
fail-fast: false
matrix:
# As a general rule, when adding a new version, remove the oldest version
# Check if the oldest version is EOL or not first.
ts: ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Install TypeScript ${{ matrix.ts }}
run: pnpm add typescript@${{ matrix.ts }}
- name: Typings tests
run: |
pnpm tsc --version
pnpm test:ts
================================================
FILE: .github/workflows/lock-closed-issues.yml
================================================
name: locked-closed-issues
on:
schedule:
- cron: '0 0 * * *'
permissions:
issues: write
jobs:
action:
runs-on: macos-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14'
issue-lock-reason: ''
process-only: 'issues'
================================================
FILE: .github/workflows/release-preview.yml
================================================
name: release-preview
on:
push:
branches-ignore:
- 'main'
tags:
- '!**'
workflow_dispatch:
jobs:
publish:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps --only-shell
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
- name: Tests
run: pnpm test
- name: Publish preview
run: pnpm dlx pkg-pr-new@0.0 publish --compact --pnpm --comment=update
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
schedule:
- cron: '0 1 * * *'
workflow_dispatch:
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ADMIN_TOKEN }}
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps --only-shell
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build
- name: Tests
run: pnpm test
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: ./lib
release:
needs: [build]
runs-on: macos-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_ADMIN_TOKEN }}
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
# Trusted Publishing only works on Node.js v24.
node-version: 24
cache: 'pnpm'
- name: Setup Git
run: |
git config --local user.name "Artem Zakharchenko"
git config --local user.email "kettanaito@gmail.com"
- name: Install dependencies
run: pnpm install
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
path: ./lib
- name: Release
run: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GH_ADMIN_TOKEN }}
================================================
FILE: .github/workflows/smoke-test.yml
================================================
name: smoke-test
on:
# Always run smoke tests upon a successful
# "ci" job completion on "main".
workflow_run:
workflows: ['ci']
branches: [main]
types: [completed]
workflow_dispatch:
jobs:
examples:
if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || github.event_name == 'workflow_dispatch' }}
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Install dependencies
run: pnpm install
- name: Test examples
run: ./config/scripts/smoke-test.sh
================================================
FILE: .github/workflows/typescript-nightly.yml
================================================
name: typescript-nightly
on:
schedule:
# Schedule to run nightly at midnight
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
compare:
runs-on: macos-latest
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Get latest TypeScript version
id: get-versions
run: |
# Fetch the latest published version of TypeScript on npm
latest_version=$(npm show typescript dist-tags.latest)
# Get the currently installed version
rc_version=$(npm show typescript dist-tags.rc)
latest_major_minor=$(echo "$latest_version" | cut -d '.' -f 1,2)
rc_major_minor=$(echo "$rc_version" | cut -d '.' -f 1,2)
echo "latest_version=$latest_major_minor" >> $GITHUB_OUTPUT
echo "rc_version=$rc_major_minor" >> $GITHUB_OUTPUT
test:
runs-on: macos-latest
# Skip TypeScript compatibility check on "main".
# A merged pull request implies passing "typescript" job.
if: ${{ needs.compare.outputs.latest_version != needs.compare.outputs.rc_version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Install TypeScript RC
run: pnpm add typescript@rc
- name: Write tsconfig
run: |
cp test/typings/tsconfig.5.2.json test/typings/tsconfig.${needs.compare.outputs.rc_version}.json
- name: Typings tests
run: |
pnpm tsc --version
pnpm test:ts
================================================
FILE: .gitignore
================================================
__*
.DS_*
node_modules
/lib
tmp
*-error.log
/package-lock.json
/yarn.lock
stats.html
.vscode
.idea
msw-*.tgz
.husky/_
.env
**/test-results
/test/modules/node/node-esm-tests
# Smoke test temporary files.
/package.json.copy
/examples
/test/modules/node/node-esm-tests
*.vitest-temp.json
================================================
FILE: .nvmrc
================================================
v20
================================================
FILE: .prettierrc
================================================
{
"arrowParens": "always",
"bracketSpacing": true,
"semi": false,
"useTabs": false,
"trailingComma": "all",
"singleQuote": true
}
================================================
FILE: CHANGELOG.md
================================================
# Change Log
This project adheres to [Semantic Versioning](https://semver.org/).
Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/mswjs/msw/releases) page.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Hey! Thank you for deciding to contribute to Mock Service Worker! This page will help you land your first contribution by giving you a better understanding about the project's structure, dependencies, and development workflow.
## Tools
Getting yourself familiar with the tools below will substantially ease your contribution experience.
- [TypeScript](https://www.typescriptlang.org/)
- [Vitest](https://vitest.dev/)
- [Playwright](https://playwright.dev/)
## Dependencies
Mock Service Worker depends on multiple other libraries.
| Library name | Purpose |
| ------------------------------------------------------- | ------------------------------------------------------------------------ |
| [cookies](https://github.com/mswjs/cookies) | Enables cookies persistence and inference between environments. |
| [headers-utils](https://github.com/mswjs/headers-utils) | `Headers` polyfill to manage request/response headers. |
| [interceptors](https://github.com/mswjs/interceptors) | Provisions request interception in Node.js (internals of `setupServer`). |
There are cases when an issue originates from one of the said dependencies. Don't hesitate to address it in the respective repository, as they all are governed by the same team.
## Getting started
### Fork the repository
Please use the GitHub UI to [fork this repository](https://github.com/mswjs/msw) (_read more about [Forking a repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo)_). Mock Service Worker has forked builds enabled in the CI, so you will see the build status of your fork's branch.
### Install
```bash
$ cd msw
$ pnpm install
$ pnpm start
```
> Please use [PNPM][pnpm-url] version 8.15 while working on this project.
> Guide on how to install a specific PNPM version can be [found here][pnpm-install-guide-url].
## Git workflow
```bash
# Checkout the default branch and ensure it's up-to-date
$ git checkout main
$ git pull --rebase
# Create a feature branch
$ git checkout -b feature/graphql-subscriptions
# Commit the changes
$ git add .
$ git commit
# Follow the interactive prompt to compose a commit message
# Push
$ git push -u origin feature/graphql-subscriptions
```
We are using [Conventional Commits](https://conventionalcommits.org/) naming convention. It helps us automate library releases and ensure clean and standardized commit tree. Please take a moment to read about the said convention before you name your commits.
> **Tip:** running `git commit` will open an interactive prompt in your terminal. Follow the prompt to compose a valid commit message.
Once you have pushed the changes to your remote feature branch, [create a pull request](https://github.com/open-draft/msw/compare) on GitHub. Undergo the process of code review, where the maintainers of the library will help you get the changes from good to great, and enjoy your implementation merged to the default branch.
> Please be respectful when requesting and going through the code review. Everyone on the team is interested in merging quality and well tested code, and we're hopeful that you have the same goal. It may take some time and iterations to get it right, and we will assist you throughout the process.
## Build
Build the library with the following command:
```bash
$ pnpm build
```
## Tests
### Testing levels
There are two levels of tests on the project:
- [Unit tests](#unit-tests), cover small independent functions.
- [Integration tests](#integration-tests), test in-browser usage scenarios.
**Always begin your implementation from tests**. When tackling an issue, a test for it must be missing, or is incomplete. When working on a feature, starting with a test allows you to model the feature's usage before diving into implementation.
### Unit tests
#### Writing a unit test
Unit tests are placed next to the tested code. For example, if you're testing a newly added `multiply` function, create a `multiply.test.ts` file next to where the function is located:
```bash
$ touch src/utils/multiply.test.ts
```
Proceed by writing a unit test that resembles the usage of the function. Make sure to cover all the scenarios
```ts
// src/utils/multiply.test.ts
import { multiply } from './multiply'
test('multiplies two given numbers', () => {
expect(multiply(2, 3)).toEqual(6)
})
```
> Please [avoid nesting](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing/) while you're testing.
#### Running a single unit test
Once your test is written, run it in isolation.
```bash
$ pnpm test:unit src/utils/multiply.test.ts
```
At this point, the actual implementation is not ready yet, so you can expect your test to fail. **That's perfect**. Add the necessary modules and logic, and gradually see your test cases pass.
#### Running all unit tests
```bash
$ pnpm test:unit
```
### Integration tests
We follow an example-driven testing paradigm, meaning that each integration test represents a _usage example_. Mock Service Worker can be used in different environments (browser, Node.js), making such usage examples different.
> **Make sure that you [build the library](#build) before running the integration tests**. It's a good idea to keep the build running (`pnpm start`) while working on the tests. Keeping both compiler and test runner in watch mode boosts your productivity.
#### Browser integration tests
You can find all the browser integration tests under `./test/browser`. Those tests are run with Playwright and usually consist of two parts:
- `[test-name].mocks.ts`, the usage example of MSW;
- `[test-name].test.ts`, the test suite that loads the usage example, does actions and performs assertions.
It's also a great idea to get familiar with our Playwright configuration and extensions:
- [**Playwright configuration file**](./test/browser/playwright.config.ts)
- [Playwright extensions](./test/browser/playwright.extend.ts)
Let's write an example integration test that asserts the interception of a GET request. First, start with the `*.mocks.ts` file:
```js
// test/browser/example.mocks.ts
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'
const worker = setupWorker(
http.get('/books', () => {
return HttpResponse.json([
{
id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da',
title: 'The Lord of the Rings',
publishedAt: -486867600,
},
])
}),
)
worker.start()
```
> Notice how there's nothing test-specific in the example? The `example.mocks.ts` file is a copy-paste example of intercepting the `GET /books` request. This allows to share these mocks with the users as a legitimate example, because it is!
Once the `*.mocks.ts` file is written, proceed by creating a test file:
```ts
// test/browser/example.test.ts
import * as path from 'path'
import { test, expect } from './playwright.extend'
test('returns a mocked response', async ({ loadExample, fetch }) => {
// Compile the given usage example on runtime.
await loadExample(new URL('./example.mocks.ts', import.meta.url))
// Perform the "GET /books" request in the browser.
const res = await fetch('/books')
// Assert the returned response body.
expect(await res.json()).toEqual([
{
id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da',
title: 'The Lord of the Rings',
publishedAt: -486867600,
},
])
})
```
##### Running all browser tests
Make sure Playwright chromium has been installed before running browser tests.
```sh
pnpm test:browser
```
#### Running a single browser test
```sh
pnpm test:browser ./test/browser/example.test.ts
```
#### Node.js integration test
Integration tests showcase a usage example in Node.js and are often placed next to the in-browser tests. Node.js integration tests reside in the `./test/node` directory.
Similar to the browser tests, these are going to contain a usage example and the assertions over it. However, for Node.js tests there is no need to create a separate `*.mocks.ts` file. Instead, keep the usage example in the test file directly.
Let's replicate the same `GET /books` integration test in Node.js.
```ts
// test/node/example.test.ts
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('/books', () => {
return HttpResponse.json([
{
id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da',
title: 'The Lord of the Rings',
publishedAt: -486867600,
},
])
}),
)
beforeAll(() => server.listen())
afterAll(() => server.close())
test('returns a mocked response', async () => {
const res = await fetch('/books')
expect(await res.json()).toEqual([
{
id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da',
title: 'The Lord of the Rings',
publishedAt: -486867600,
},
])
})
```
##### Running all Node.js tests
```sh
pnpm test:node
```
##### Running a single Node.js test
```sh
pnpm test:node ./test/node/example.test.ts
```
## Build
Build the library with the following command:
```bash
$ pnpm build
```
[pnpm-url]: https://pnpm.io/
[page-with-url]: https://github.com/kettanaito/page-with
[pnpm-install-guide-url]: https://pnpm.io/8.x/installation#installing-a-specific-version
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2018–present Artem Zakharchenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Mock Service Worker
Industry standard API mocking for JavaScript.
Join our Discord server
## Features
- **Seamless**. A dedicated layer of requests interception at your disposal. Keep your application's code and tests unaware of whether something is mocked or not.
- **Deviation-free**. Request the same production resources and test the actual behavior of your app. Augment an existing API, or design it as you go when there is none.
- **Familiar & Powerful**. Use [Express](https://github.com/expressjs/express)-like routing syntax to intercept requests. Use parameters, wildcards, and regular expressions to match requests, and respond with necessary status codes, headers, cookies, delays, or completely custom resolvers.
---
> "_I found MSW and was thrilled that not only could I still see the mocked responses in my DevTools, but that the mocks didn't have to be written in a Service Worker and could instead live alongside the rest of my app. This made it silly easy to adopt. The fact that I can use it for testing as well makes MSW a huge productivity booster._"
>
> — [Kent C. Dodds](https://twitter.com/kentcdodds)
## Documentation
This README will give you a brief overview of the library, but there's no better place to start with Mock Service Worker than its official documentation.
- [Documentation](https://mswjs.io/docs)
- [**Quick start**](https://mswjs.io/docs/quick-start)
- [FAQ](https://mswjs.io/docs/faq)
## Examples
- See the list of [**Usage examples**](https://github.com/mswjs/examples)
## Courses
We've partnered with Egghead to bring you quality paid materials to learn the best practices of API mocking on the web. Please give them a shot! The royalties earned from them help sustain the project's development. Thank you.
- 🚀 [**Mocking REST and GraphQL APIs with Mock Service Worker**](https://egghead.io/courses/mock-rest-and-graphql-apis-with-mock-service-worker-8d471ece?af=8mci9b)
- 🔌 [Mocking (and testing) WebSocket APIs with Mock Service Worker](https://egghead.io/courses/mocking-websocket-apis-with-mock-service-worker-9933b7f5)
## Browser
- [Learn more about using MSW in a browser](https://mswjs.io/docs/integrations/browser)
- [`setupWorker` API](https://mswjs.io/docs/api/setup-worker)
### How does it work?
In-browser usage is what sets Mock Service Worker apart from other tools. Utilizing the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which can intercept requests for the purpose of caching, Mock Service Worker responds to intercepted requests with your mock definition on the network level. This way your application knows nothing about the mocking.
**Take a look at this quick presentation on how Mock Service Worker functions in a browser:**
[](https://youtu.be/HcQCqboatZk)
### How is it different?
- This library intercepts requests on the network level, which means _after_ they have been performed and "left" your application. As a result, the entirety of your code runs, giving you more confidence when mocking;
- Imagine your application as a box. Every API mocking library out there opens your box and removes the part that does the request, placing a blackbox in its stead. Mock Service Worker leaves your box intact, 1-1 as it is in production. Instead, MSW lives in a separate box next to yours;
- No more stubbing of `fetch`, `axios`, `react-query`, you-name-it;
- You can reuse the same mock definition for the unit, integration, and E2E testing. Did we mention local development and debugging? Yep. All running against the same network description without the need for adapters or bloated configurations.
### Usage example
```js
// 1. Import the library.
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'
// 2. Describe network behavior with request handlers.
const worker = setupWorker(
http.get('https://github.com/octocat', ({ request, params, cookies }) => {
return HttpResponse.json(
{
message: 'Mocked response',
},
{
status: 202,
statusText: 'Mocked status',
},
)
}),
)
// 3. Start mocking by starting the Service Worker.
await worker.start()
```
Performing a `GET https://github.com/octocat` request in your application will result into a mocked response that you can inspect in your browser's "Network" tab:

> **Tip:** Did you know that although Service Worker runs in a separate thread, your request handlers execute entirely on the client? This way you can use the same languages, like TypeScript, third-party libraries, and internal logic to create the mocks you need.
## Node.js
- [Learn more about using MSW in Node.js](https://mswjs.io/docs/integrations/node)
- [`setupServer` API](https://mswjs.io/docs/api/setup-server)
### How does it work?
There's no such thing as Service Workers in Node.js. Instead, MSW implements a [low-level interception algorithm](https://github.com/mswjs/interceptors) that can utilize the very same request handlers you have for the browser. This blends the boundary between environments, allowing you to focus on your network behaviors.
### How is it different?
- Does not stub `fetch`, `axios`, etc. As a result, your tests know _nothing_ about mocking;
- You can reuse the same request handlers for local development and debugging, as well as for testing. Truly a single source of truth for your network behavior across all environments and all tools.
### Usage example
Here's an example of using Mock Service Worker while developing your Express server:
```js
import express from 'express'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const app = express()
const server = setupServer()
app.get(
'/checkout/session',
server.boundary((req, res) => {
// Describe the network for this Express route.
server.use(
http.get(
'https://api.stripe.com/v1/checkout/sessions/:id',
({ params }) => {
return HttpResponse.json({
id: params.id,
mode: 'payment',
status: 'open',
})
},
),
)
// Continue with processing the checkout session.
handleSession(req, res)
}),
)
```
> This example showcases [`server.boundary()`](https://mswjs.io/docs/api/setup-server/boundary) to scope request interception to a particular closure, which is extremely handy!
## Sponsors
Mock Service Worker is trusted by hundreds of thousands of engineers around the globe. It's used by companies like Google, Microsoft, Spotify, Amazon, Netflix, and countless others. Despite that, it remains a hobby project maintained in a spare time and has no opportunity to financially support even a single full-time contributor.
**You can change that!** Consider [sponsoring the effort](https://github.com/sponsors/mswjs) behind one of the most innovative approaches around API mocking. Raise a topic of open source sponsorships with your boss and colleagues. Let's build sustainable open source together!
### Golden sponsors
> Become our _golden sponsor_ and get featured right here, enjoying other perks like issue prioritization and a personal consulting session with us.
>
> **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
### Silver sponsors
> Become our _silver sponsor_ and get your profile image and link featured right here.
>
> **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
### Bronze sponsors
> Become our _bronze sponsor_ and get your profile image and link featured in this section.
>
> **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
## Awards & mentions
We've been extremely humbled to receive awards and mentions from the community for all the innovation and reach Mock Service Worker brings to the JavaScript ecosystem.
================================================
FILE: browser/package.json
================================================
{
"type": "module",
"main": "../lib/browser/index.js",
"module": "../lib/browser/index.mjs",
"types": "../lib/browser/index.d.ts",
"exports": {
".": {
"module-sync": {
"types": "./../lib/browser/index.d.mts",
"default": "./../lib/browser/index.mjs"
},
"module": {
"types": "./../lib/browser/index.d.mts",
"default": "./../lib/browser/index.mjs"
},
"browser": {
"types": "./../lib/browser/index.d.mts",
"default": "./../lib/browser/index.mjs"
},
"import": {
"types": "./../lib/browser/index.d.mts",
"default": "./../lib/browser/index.mjs"
},
"default": {
"types": "./../lib/browser/index.d.ts",
"default": "./../lib/browser/index.js"
}
},
"./package.json": "./package.json"
}
}
================================================
FILE: cli/index.js
================================================
#!/usr/bin/env node
import yargs from 'yargs'
import { init } from './init.js'
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
yargs(process.argv.slice(2))
.usage('$0 [args]')
.command(
'init',
'Initializes Mock Service Worker at the specified directory',
(yargs) => {
yargs
.positional('publicDir', {
type: 'string',
description: 'Relative path to the public directory',
demandOption: false,
normalize: true,
})
.option('save', {
type: 'boolean',
description: 'Save the worker directory in your package.json',
})
.option('cwd', {
type: 'string',
description: 'Custom current worker directory',
normalize: true,
})
.example('msw init')
.example('msw init ./public')
.example('msw init ./static --save')
},
init,
)
.demandCommand()
.help().argv
================================================
FILE: cli/init.js
================================================
import fs from 'node:fs'
import path from 'node:path'
import colors from 'picocolors'
import confirm from '@inquirer/confirm'
import { invariant } from './invariant.js'
import { SERVICE_WORKER_BUILD_PATH } from '../config/constants.js'
export async function init(args) {
const CWD = args.cwd || process.cwd()
const publicDir = args._[1] ? normalizePath(args._[1]) : undefined
const packageJsonPath = path.resolve(CWD, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const savedWorkerDirectories = Array.prototype
.concat((packageJson.msw && packageJson.msw.workerDirectory) || [])
.map(normalizePath)
if (publicDir) {
// If the public directory was provided, copy the worker script
// to that directory only. Even if there are paths stored in "msw.workerDirectory",
// those will not be touched.
await copyWorkerScript(publicDir, CWD)
const relativePublicDir = path.relative(CWD, publicDir)
printSuccessMessage([publicDir])
if (args.save) {
// Only save the public path if it's not already saved in "package.json".
if (!savedWorkerDirectories.includes(relativePublicDir)) {
saveWorkerDirectory(packageJsonPath, relativePublicDir)
}
}
// Explicitly check if "save" was not provided (was null).
// You can also provide the "--no-save" option, and then "args.save"
// will equal to false.
else if (args.save == null) {
// eslint-disable-next-line no-console
console.log(`\
${colors.cyan(
'INFO',
)} In order to ease the future updates to the worker script,
we recommend saving the path to the worker directory in your package.json.`)
// If the "--save" flag was not provided, prompt to save
// the public path.
promptWorkerDirectoryUpdate(
`Do you wish to save "${relativePublicDir}" as the worker directory?`,
packageJsonPath,
relativePublicDir,
)
}
return
}
// Calling "init" without a public directory but with the "--save" flag is a no-op.
invariant(
args.save == null,
'Failed to copy the worker script: cannot call the "init" command without a public directory but with the "--save" flag. Either drop the "--save" flag to copy the worker script to all paths listed in "msw.workerDirectory", or add an explicit public directory to the command, like "npx msw init ./public".',
)
// If the public directory was not provided, check any existing
// paths in "msw.workerDirectory". When called without the public
// directory, the "init" command must copy the worker script
// to all the paths stored in "msw.workerDirectory".
if (savedWorkerDirectories.length > 0) {
const copyResults = await Promise.allSettled(
savedWorkerDirectories.map((destination) => {
return copyWorkerScript(destination, CWD).catch((error) => {
// Inject the absolute destination path onto the copy function rejections
// so it's available in the failed paths array below.
throw [toAbsolutePath(destination, CWD), error]
})
}),
)
const successfulPaths = copyResults
.filter((result) => result.status === 'fulfilled')
.map((result) => result.value)
const failedPathsWithErrors = copyResults
.filter((result) => result.status === 'rejected')
.map((result) => result.reason)
// Notify about failed copies, if any.
if (failedPathsWithErrors.length > 0) {
printFailureMessage(failedPathsWithErrors)
}
// Notify about successful copies, if any.
if (successfulPaths.length > 0) {
printSuccessMessage(successfulPaths)
}
}
}
/**
* @param {string} maybeAbsolutePath
* @param {string} cwd
* @returns {string}
*/
function toAbsolutePath(maybeAbsolutePath, cwd) {
return path.isAbsolute(maybeAbsolutePath)
? maybeAbsolutePath
: path.resolve(cwd, maybeAbsolutePath)
}
/**
* @param {string} destination
* @param {string} cwd
* @returns {Promise}
*/
async function copyWorkerScript(destination, cwd) {
// When running as a part of "postinstall" script, "cwd" equals the library's directory.
// The "postinstall" script resolves the right absolute public directory path.
const absolutePublicDir = toAbsolutePath(destination, cwd)
if (!fs.existsSync(absolutePublicDir)) {
await fs.promises
.mkdir(absolutePublicDir, { recursive: true })
.catch((error) => {
throw new Error(
invariant(
false,
'Failed to copy the worker script at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the public directory of your application.\n\nSee the original error below:\n\n%s',
absolutePublicDir,
error,
),
)
})
}
// eslint-disable-next-line no-console
console.log('Copying the worker script at "%s"...', absolutePublicDir)
const workerFilename = path.basename(SERVICE_WORKER_BUILD_PATH)
const workerDestinationPath = path.resolve(absolutePublicDir, workerFilename)
fs.copyFileSync(SERVICE_WORKER_BUILD_PATH, workerDestinationPath)
return workerDestinationPath
}
/**
* @param {Array} paths
*/
function printSuccessMessage(paths) {
// eslint-disable-next-line no-console
console.log(`
${colors.green('Worker script successfully copied!')}
${paths.map((path) => colors.gray(` - ${path}\n`))}
Continue by describing the network in your application:
${colors.red(colors.bold('https://mswjs.io/docs/quick-start'))}
`)
}
function printFailureMessage(pathsWithErrors) {
// eslint-disable-next-line no-console
console.error(`\
${colors.red('Copying the worker script failed at following paths:')}
${pathsWithErrors
.map(([path, error]) => colors.gray(` - ${path}`) + '\n' + ` ${error}`)
.join('\n\n')}
`)
}
/**
* @param {string} packageJsonPath
* @param {string} publicDir
*/
function saveWorkerDirectory(packageJsonPath, publicDir) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
// eslint-disable-next-line no-console
console.log(
colors.gray('Updating "msw.workerDirectory" at "%s"...'),
packageJsonPath,
)
const prevWorkerDirectory = Array.prototype.concat(
(packageJson.msw && packageJson.msw.workerDirectory) || [],
)
const nextWorkerDirectory = Array.from(
new Set(prevWorkerDirectory).add(publicDir),
)
const nextPackageJson = Object.assign({}, packageJson, {
msw: {
workerDirectory: nextWorkerDirectory,
},
})
fs.writeFileSync(
packageJsonPath,
JSON.stringify(nextPackageJson, null, 2),
'utf8',
)
}
/**
* @param {string} message
* @param {string} packageJsonPath
* @param {string} publicDir
* @returns {void}
*/
function promptWorkerDirectoryUpdate(message, packageJsonPath, publicDir) {
return confirm({
theme: {
prefix: colors.yellowBright('?'),
},
message,
}).then((answer) => {
if (answer) {
saveWorkerDirectory(packageJsonPath, publicDir)
}
})
}
/**
* Normalizes the given path, replacing ambiguous path separators
* with the platform-specific path separator.
* @param {string} input Path to normalize.
* @returns {string}
*/
function normalizePath(input) {
return input.replace(/[\\|\/]+/g, path.sep)
}
================================================
FILE: cli/invariant.js
================================================
import colors from 'picocolors'
export function invariant(predicate, message, ...args) {
if (!predicate) {
// eslint-disable-next-line no-console
console.error(colors.red(message), ...args)
process.exit(1)
}
}
================================================
FILE: cli/package.json
================================================
{
"type": "module"
}
================================================
FILE: commitlint.config.js
================================================
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-max-line-length': [0, 'always', Infinity],
'footer-max-line-length': [1, 'always'],
},
}
================================================
FILE: config/constants.js
================================================
import url from 'node:url'
import path from 'node:path'
export const SERVICE_WORKER_SOURCE_PATH = url.fileURLToPath(
new URL('../src/mockServiceWorker.js', import.meta.url),
)
export const SERVICE_WORKER_BUILD_PATH = url.fileURLToPath(
new URL(
path.join('../lib', path.basename(SERVICE_WORKER_SOURCE_PATH)),
import.meta.url,
),
)
================================================
FILE: config/copyServiceWorker.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import { until } from 'until-async'
/**
* Copies the given Service Worker source file into the destination.
* Injects the integrity checksum into the destination file.
*/
export default async function copyServiceWorker(
sourceFilePath: string,
destFilePath: string,
checksum: string,
): Promise {
// eslint-disable-next-line no-console
console.log('Compiling Service Worker...')
const [readFileError, readFileResult] = await until(() =>
fs.promises.readFile(sourceFilePath, 'utf8'),
)
if (readFileError) {
throw new Error('Failed to read file.\n${readError.message}')
}
const destFileDirectory = path.dirname(destFilePath)
// eslint-disable-next-line no-console
console.log('Checking if "%s" path exists...', destFileDirectory)
if (!fs.existsSync(destFileDirectory)) {
// eslint-disable-next-line no-console
console.log('Destination directory does not exist, creating...')
await fs.promises.mkdir(destFileDirectory, { recursive: true })
}
const packageJson = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
)
const nextFileContent = readFileResult
.replace('', checksum)
.replace('', packageJson.version)
const [writeFileError] = await until(() =>
fs.promises.writeFile(destFilePath, nextFileContent),
)
if (writeFileError) {
throw new Error(`Failed to write file.\n${writeFileError.message}`)
}
// eslint-disable-next-line no-console
console.log('Service Worker copied to: %s', destFilePath)
}
================================================
FILE: config/package.json
================================================
{
"type": "module"
}
================================================
FILE: config/plugins/esbuild/copyWorkerPlugin.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'crypto'
import minify from 'babel-minify'
import { invariant } from 'outvariant'
import type { Plugin } from 'esbuild'
import copyServiceWorker from '../../copyServiceWorker.js'
const SERVICE_WORKER_ENTRY_PATH = path.resolve(
process.cwd(),
'./src/mockServiceWorker.js',
)
const SERVICE_WORKER_OUTPUT_PATH = path.resolve(
process.cwd(),
'./lib/mockServiceWorker.js',
)
function getChecksum(contents: string): string {
const { code } = minify(contents, {}, { comments: false })
return crypto.createHash('md5').update(code, 'utf8').digest('hex')
}
export function getWorkerChecksum(): string {
const workerContents = fs.readFileSync(SERVICE_WORKER_ENTRY_PATH, 'utf8')
return getChecksum(workerContents)
}
export function copyWorkerPlugin(checksum: string): Plugin {
return {
name: 'copyWorkerPlugin',
async setup(build) {
invariant(
SERVICE_WORKER_ENTRY_PATH,
'Failed to locate the worker script source file',
)
if (fs.existsSync(SERVICE_WORKER_OUTPUT_PATH)) {
console.warn(
'Skipped copying the worker script to "%s": already exists',
SERVICE_WORKER_OUTPUT_PATH,
)
return
}
// Generate the checksum from the worker script's contents.
// const workerContents = await fs.readFile(workerSourcePath, 'utf8')
// const checksum = getChecksum(workerContents)
build.onLoad({ filter: /mockServiceWorker\.js$/ }, async () => {
return {
// Prevent the worker script from being transpiled.
// But, generally, the worker script is not in the entrypoints.
contents: '',
}
})
build.onEnd(() => {
// eslint-disable-next-line no-console
console.log('worker script checksum:', checksum)
// Copy the worker script on the next tick.
process.nextTick(async () => {
await copyServiceWorker(
SERVICE_WORKER_ENTRY_PATH,
SERVICE_WORKER_OUTPUT_PATH,
checksum,
)
})
})
},
}
}
================================================
FILE: config/plugins/esbuild/forceEsmExtensionsPlugin.ts
================================================
import { type Plugin } from 'esbuild'
export const ESM_EXTENSION = '.mjs'
export const CJS_EXTENSION = '.js'
export function forceEsmExtensionsPlugin(): Plugin {
return {
name: 'forceEsmExtensionsPlugin',
setup(build) {
const isEsm = build.initialOptions.format === 'esm'
build.onEnd(async (result) => {
if (result.errors.length > 0) {
return
}
for (const outputFile of result.outputFiles || []) {
// Only target CJS/ESM files.
// This ignores additional files emitted, like sourcemaps ("*.js.map").
if (
!(
outputFile.path.endsWith(ESM_EXTENSION) ||
outputFile.path.endsWith('.mjs')
)
) {
continue
}
const fileContents = outputFile.text
const nextFileContents = modifyRelativeImports(fileContents, isEsm)
outputFile.contents = Buffer.from(nextFileContents)
}
})
},
}
}
const CJS_RELATIVE_IMPORT_EXP = /require\(["'](\..+)["']\)(;)?/gm
const ESM_RELATIVE_IMPORT_EXP = /from ["'](\..+)["'](;)?/gm
function modifyRelativeImports(contents: string, isEsm: boolean): string {
const extension = isEsm ? ESM_EXTENSION : CJS_EXTENSION
const importExpression = isEsm
? ESM_RELATIVE_IMPORT_EXP
: CJS_RELATIVE_IMPORT_EXP
return contents.replace(
importExpression,
(_, importPath, maybeSemicolon = '') => {
if (importPath.endsWith('.') || importPath.endsWith('/')) {
return isEsm
? `from '${importPath}/index${extension}'${maybeSemicolon}`
: `require("${importPath}/index${extension}")${maybeSemicolon}`
}
if (importPath.endsWith(extension)) {
return isEsm
? `from '${importPath}'${maybeSemicolon}`
: `require("${importPath}")${maybeSemicolon}`
}
return isEsm
? `from '${importPath}${extension}'${maybeSemicolon}`
: `require("${importPath}${extension}")${maybeSemicolon}`
},
)
}
================================================
FILE: config/plugins/esbuild/graphQLImportPlugin.ts
================================================
import fs from 'node:fs'
import type { Plugin } from 'esbuild'
/**
* A plugin to replace `require('graphql')` statements with `await import('graphql')`
* only for ESM bundles. This makes the GraphQL module to be imported lazily
* while maintaining the CommonJS compatibility.
* @see https://github.com/mswjs/msw/issues/2254
*/
export function graphqlImportPlugin(): Plugin {
return {
name: 'graphql-import-plugin',
setup(build) {
if (build.initialOptions.format !== 'esm') {
return
}
build.onLoad({ filter: /\.ts$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf-8')
const match = /require\(['"]graphql['"]\)/g.exec(contents)
if (match) {
return {
loader: 'ts',
contents:
contents.slice(0, match.index - 1) +
`await import('graphql').catch((error) => {console.error('[MSW] Failed to parse a GraphQL query: cannot import the "graphql" module. Please make sure you install it if you wish to intercept GraphQL requests. See the original import error below.'); throw error})` +
contents.slice(match.index + match[0].length),
}
}
})
},
}
}
================================================
FILE: config/plugins/esbuild/resolveCoreImportsPlugin.ts
================================================
import { Plugin } from 'esbuild'
import { replaceCoreImports } from '../../replaceCoreImports.js'
import { ESM_EXTENSION } from './forceEsmExtensionsPlugin.js'
export function resolveCoreImportsPlugin(): Plugin {
return {
name: 'resolveCoreImportsPlugin',
setup(build) {
build.onEnd(async (result) => {
if (result.errors.length > 0) {
return
}
for (const outputFile of result.outputFiles || []) {
const isEsm = outputFile.path.endsWith(ESM_EXTENSION)
const fileContents = outputFile.text
const nextFileContents = replaceCoreImports(
outputFile.path,
fileContents,
isEsm,
)
outputFile.contents = Buffer.from(nextFileContents)
}
})
},
}
}
================================================
FILE: config/polyfills-node.ts
================================================
import { setTimeout as nodeSetTimeout } from 'timers'
// Polyfill the global "setTimeout" so MSW could be used
// with "jest.useFakeTimers()". MSW response handling
// is wrapped in "setTimeout", and without this polyfill
// you'd have to manually advance the timers for the response
// to finally resolve.
export const setTimeout = nodeSetTimeout
================================================
FILE: config/replaceCoreImports.js
================================================
const CORE_ESM_IMPORT_PATTERN = /from ["'](~\/core(.*))["'](;)?/gm
const CORE_CJS_IMPORT_PATTERN = /require\(["'](~\/core(.*))["']\)(;)?/gm
function getCoreImportPattern(isEsm) {
return isEsm ? CORE_ESM_IMPORT_PATTERN : CORE_CJS_IMPORT_PATTERN
}
export function hasCoreImports(fileContents, isEsm) {
return getCoreImportPattern(isEsm).test(fileContents)
}
export function replaceCoreImports(moduleFilePath, fileContents, isEsm) {
return fileContents.replace(
getCoreImportPattern(isEsm),
(_, __, maybeSubmodulePath, maybeSemicolon) => {
const submodulePath = maybeSubmodulePath || '/index'
/**
* @note Although all .d.ts are considered ESM, append different
* file extension for d.mts files.
*/
const extension = moduleFilePath.endsWith('.d.mts') ? '.mjs' : ''
const semicolon = maybeSemicolon || ''
return isEsm
? `from "../core${submodulePath}${extension}"${semicolon}`
: `require("../core${submodulePath}")${semicolon}`
},
)
}
================================================
FILE: config/scripts/patch-ts.js
================================================
import fs from 'node:fs'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { invariant } from 'outvariant'
import * as glob from 'glob'
import { hasCoreImports, replaceCoreImports } from '../replaceCoreImports.js'
const execAsync = promisify(exec)
const BUILD_DIR = new URL('../../lib/', import.meta.url)
async function patchTypeDefs() {
const typeDefsPaths = glob.sync('**/*.d.{ts,mts}', {
cwd: BUILD_DIR,
absolute: true,
})
const typeDefsWithCoreImports = typeDefsPaths
.map((modulePath) => {
const fileContents = fs.readFileSync(modulePath, 'utf8')
/**
* @note Treat all type definition files as ESM because even
* CJS .d.ts use `import` statements.
*/
if (hasCoreImports(fileContents, true)) {
return [modulePath, fileContents]
}
})
.filter(Boolean)
if (typeDefsWithCoreImports.length === 0) {
console.log(
'Found no .d.ts modules containing the "~/core" import, skipping...',
)
return process.exit(0)
}
console.log(
'Found %d module(s) with the "~/core" import, resolving...',
typeDefsWithCoreImports.length,
)
for (const [typeDefsPath, fileContents] of typeDefsWithCoreImports) {
// Treat ".d.ts" files as ESM to replace "import" statements.
// Force no extension on the ".d.ts" imports.
const nextFileContents = replaceCoreImports(
typeDefsPath,
fileContents,
true,
)
fs.writeFileSync(typeDefsPath, nextFileContents, 'utf8')
console.log('Successfully patched "%s"!', typeDefsPath)
}
console.log(
'Imports resolved in %d file(s), verifying...',
typeDefsWithCoreImports.length,
)
// Next, validate that we left no "~/core" imports unresolved.
const result = await execAsync(
`grep "~/core" ./**/*.d.{ts,mts} -R -l || exit 0`,
{
cwd: BUILD_DIR,
shell: '/bin/bash',
},
)
invariant(
result.stderr === '',
'Failed to validate the .d.ts modules for the presence of the "~/core" import. See the original error below.',
result.stderr,
)
if (result.stdout !== '') {
const modulesWithUnresolvedImports = result.stdout
.split('\n')
.filter(Boolean)
console.error(
`Found .d.ts modules containing unresolved "~/core" import after the patching:
${modulesWithUnresolvedImports.map((path) => ` - ${new URL(path, BUILD_DIR).pathname}`).join('\n')}
`,
)
return process.exit(1)
}
// Ensure that the .d.ts files compile without errors after resolving the "~/core" imports.
console.log('Compiling the .d.ts modules with tsc...')
const tscCompilation = await execAsync(
`tsc --noEmit --skipLibCheck ${typeDefsPaths.join(' ')}`,
{
cwd: BUILD_DIR,
},
)
if (tscCompilation.stderr !== '') {
console.error(
'Failed to compile the .d.ts modules with tsc. See the original error below.',
tscCompilation.stderr,
)
return process.exit(1)
}
// Ensure that CJS .d.ts file never reference .mjs files.
const mjsInCjsResult = await execAsync(
`grep ".mjs" ./**/*.d.ts -R -l || exit 0`,
{
cwd: BUILD_DIR,
shell: '/bin/bash',
},
)
invariant(
mjsInCjsResult.stderr === '',
'Failed to validate the .d.ts modules not referencing ".mjs" files. See the original error below.',
mjsInCjsResult.stderr,
)
if (mjsInCjsResult.stdout !== '') {
const modulesWithUnresolvedImports = mjsInCjsResult.stdout
.split('\n')
.filter(Boolean)
console.error(
`Found .d.ts modules referencing ".mjs" files after patching:
${modulesWithUnresolvedImports.map((path) => ` - ${new URL(path, BUILD_DIR).pathname}`).join('\n')}
`,
)
return process.exit(1)
}
console.log(
'The "~/core" imports resolved successfully in %d .d.ts modules! 🎉',
typeDefsWithCoreImports.length,
)
}
patchTypeDefs()
================================================
FILE: config/scripts/postinstall.js
================================================
import fs from 'node:fs'
import path from 'node:path'
import { execFileSync } from 'node:child_process'
// When executing the "postinstall" script, the "process.cwd" equals
// the package directory, not the parent project where the package is installed.
// NPM stores the parent project directory in the "INIT_CWD" env variable.
const parentPackageCwd = process.env.INIT_CWD
function postInstall() {
const packageJson = JSON.parse(
fs.readFileSync(path.resolve(parentPackageCwd, 'package.json'), 'utf8'),
)
if (!packageJson.msw || !packageJson.msw.workerDirectory) {
return
}
const cliExecutable = path.resolve(process.cwd(), 'cli/index.js')
try {
/**
* @note Call the "init" command directly. It will now copy the worker script
* to all saved paths in "msw.workerDirectory"
*/
execFileSync(process.execPath, [cliExecutable, 'init'], {
cwd: parentPackageCwd,
})
} catch (error) {
console.error(
`[MSW] Failed to automatically update the worker script.\n\n${error}`,
)
}
}
postInstall()
================================================
FILE: config/scripts/smoke-test.sh
================================================
#!/bin/bash
set -e
COMMIT_HASH=$(git rev-parse HEAD)
MSW_VERSION="0.0.0-$COMMIT_HASH"
echo "Latest commit: $COMMIT_HASH"
echo "In-progress MSW version: $MSW_VERSION"
PKG_JSON_COPY="package.json.copy"
cp package.json $PKG_JSON_COPY
pnpm version $MSW_VERSION --no-git-tag-version --allow-same-version
echo ""
echo "Packing MSW..."
pnpm pack
EXAMPLES_REPO=https://github.com/mswjs/examples.git
EXAMPLES_DIR=./examples
echo ""
echo "Cloning the examples from "$EXAMPLES_REPO"..."
if [[ -d "$EXAMPLES_DIR" ]]; then
echo "Examples already cloned, skipping..."
else
git clone $EXAMPLES_REPO $EXAMPLES_DIR
fi
echo ""
echo "Installing dependencies..."
cd $EXAMPLES_DIR
pnpm install
echo ""
echo "Linking MSW..."
pnpm add msw --filter="with-*" file:../../../msw-$MSW_VERSION.tgz
pnpm ls msw
echo ""
echo "Running tests..."
CI=1 pnpm test ; (cd ../ && mv $PKG_JSON_COPY ./package.json)
================================================
FILE: decisions/jest-support.md
================================================
# Jest support
With the introduction of [Mock Service Worker 2.0](https://mswjs.io/blog/introducing-msw-2.0), the library has made a significant step forward in the effort of embracing and promoting web standards. Since that release, its contributors have reported multiple issues with Node.js simply because MSW exposed developers to using standard Node.js APIs.
Betting on the web standards is one of the goals behind this project. One of such standards is ESM. It's the present and the future of JavaScript, and we are planning on switching to ESM-only in the years to come. For that transition to happen, we need to prioritize and, at times, make hard decisions.
**MSW offers no official support for Jest.** It doesn't mean MSW cannot be used in Jest. We maintain usage examples of both [Jest](https://github.com/mswjs/examples/tree/main/examples/with-jest) and [Jest+JSDOM](https://github.com/mswjs/examples/tree/main/examples/with-jest-jsdom) to attest to that. Although it's necessary to mention that those examples require additional setup to tackle underlying Jest or JSDOM issues.
What this means is that **we are not going to address any issues specific to Jest or JSDOM**. Those pose a significant time investment just to uncover another inconsistency between the browser and JSDOM, or the lacking features in Jest, like proper ESM support. That is not a reasonable use of the limited contributors' time. You will have a far better chance of getting your issue solved by reporting it to the Jest or JSDOM repo for the respective teams to address it.
## What's next?
> [!IMPORTANT]
> If you are experiencing issues with using MSW in Jest, **please verify them outside of Jest before reporting them**.
You can verify the issue in a plain Node.js script, or by copying your problematic test to [Vitest](https://vitest.dev/). Please note that we do not support issue reports from non-standard environments, like Deno or Bun either.
You can use one of our existing [Usage examples](https://github.com/mswjs/examples) as a template project to reproduce your issue.
================================================
FILE: decisions/linting-worker-script.md
================================================
# Linting the worker script
When linting your application, you may encounter warnings or errors originating from the `mockServiceWorker.js` script. Please refrain from opening pull requests to add the `ignore` pragma comments to the script itself.
## Solution
**Make sure that the worker script is ignored by your linting tools**. The worker script isn't a part of your application's code but a static asset. It must be ignored during linting and prettifying in the same way all your static assets in `/public` are ignored. Please configure your tools respectively.
If there are warnings/errors originating from the worker script, it's likely your public directory is not ignored by your linting tools. You may consider ignoring the entire public directory if that suits your project's conventions.
================================================
FILE: decisions/releases.md
================================================
# Releases
MSW uses the [Release](https://github.com/ossjs/release) library to automate its releases to NPM.
## Release schedule
The next version of **the library releases automatically every day**.
We do our best to choose the optimal release window to accumulate multiple changes under a single release. We do deviate from the release schedule in emergency cases, like when a critical issue has been fixed and needs an immediate release.
> [!IMPORTANT]
> Please do not ping the library maintainers to release a new version of MSW. Be patient and wait for the automated release to happen. Subscribe to any issue or pull request you are interested in, and you will be notified whenever it gets released.
## Preview releases
This repository is configured to **release work-in-progress pull requests** using [okg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). Once the pull request in question is approved, it will automatically be published to a temporary registry. Follow the instructions in the automated comment to install the work-in-progress version of the package.
================================================
FILE: decisions/typescript-versioning.md
================================================
# TypeScript versioning
## Strict TypeScript peer dependency version
In the past, MSW set the explicit upper version range for the `typescript` peer dependency (`>= 4.4.x <= 5.1.x`). While that provides better stability and predictability, we acknowledge that it negatively impacts our users, forcing them to wait for MSW to update the TypeScript version range in order to migrate to newest versions of TypeScript in their projects.
> Context: TypeScript is [not distributed according to semver](https://github.com/microsoft/TypeScript/issues/14116). Instead, it's `{marketing}.{major}.{minor}` versioning pattern. This means that it may and does contain breaking changes across what consumers perceive as _minor_ versions. I've had numerous fights with this as it's not uncommon for TypeScript to exhibit different behavior across minor versions (the same types compile on 4.6, break on 4.7, then compile without issue on 4.8).
With this in mind, _we are removing the upper range_ of the `typescript` peer dependency. We have an [automated job](../.github/workflows/typescript-nightly.yml) in place that validates the latest build of MSW against the nightly releases of TypeScript, which should ensure early issue detection and help us resolve those issues before they happen.
## TypeScript version compliance
Every version within the `typescript` peer dependency version range must have a corresponding TypeScript compliance test to guarantee the library's operatbility on that version.
A compliance test is represented as a regular _typings test_ compiled using a specific version of TypeScript. You can learn more in the [typings tests](/test/typings).
The typings test will automatically attempt to look up `tsconfig.{major}.{minor}.json` file corresponding to the currently installed version of TypeScript. The compliance is then achieved by installing different TypeScript versions from the supported range and running the existing typings tests on that version.
================================================
FILE: eslint.config.mjs
================================================
// @ts-check
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import eslintConfigPrettier from 'eslint-config-prettier'
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
export default tseslint.config(
eslint.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
},
ignores: ['/lib', '/node', '/native', '/config', '/test'],
rules: {
'no-console': [
'error',
{
allow: ['warn', 'error', 'group', 'groupCollapsed', 'groupEnd'],
},
],
'no-async-promise-executor': 'off',
'require-yield': 'off',
'no-empty-pattern': 'off',
'no-control-regex': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-namespace': [
'error',
{
allowDeclarations: true,
},
],
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
},
],
},
},
// Unused variables are useful in test files, and type test files
{
files: [
'**/*.test.ts',
'**/*.test-d.ts',
'**/*.mocks.ts',
'**/*.setup.ts',
'**/*.config.ts',
],
rules: {
'no-console': 'off',
'@typescript-eslint/prefer-ts-expect-error': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
{
files: ['**/*.test-d.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off',
},
},
eslintPluginPrettier,
)
================================================
FILE: global.d.ts
================================================
declare module 'babel-minify' {
export default function babelMinify(
code: string,
opts: Record,
babelOpts: Record,
): { code: string }
}
================================================
FILE: knip.json
================================================
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"src/{core,browser,node,native}/index.ts!",
"src/mockServiceWorker.js!",
"cli/index.js!",
"config/**/*.{js,ts}!",
"test/**/*ts"
],
"project": ["src/**/*.ts!", "cli/**/*.js!", "config/**/*.{js,ts}!"]
}
================================================
FILE: native/package.json
================================================
{
"type": "commonjs",
"browser": null,
"main": "../lib/native/index.js",
"module": "../lib/native/index.mjs",
"types": "../lib/native/index.d.ts",
"exports": {
".": {
"react-native": {
"import": {
"types": "./../lib/native/index.d.mts",
"default": "./../lib/native/index.mjs"
},
"default": {
"types": "./../lib/native/index.d.ts",
"default": "./../lib/native/index.js"
}
},
"browser": null,
"import": {
"types": "./../lib/native/index.d.mts",
"default": "./../lib/native/index.mjs"
},
"default": {
"types": "./../lib/native/index.d.ts",
"default": "./../lib/native/index.js"
}
}
}
}
================================================
FILE: node/package.json
================================================
{
"type": "commonjs",
"browser": null,
"main": "../lib/node/index.js",
"module": "../lib/node/index.mjs",
"types": "../lib/node/index.d.ts",
"exports": {
".": {
"module-sync": {
"types": "./../lib/node/index.d.mts",
"default": "./../lib/node/index.mjs"
},
"module": {
"types": "./../lib/node/index.d.mts",
"default": "./../lib/node/index.mjs"
},
"node": {
"require": "./../lib/node/index.js",
"import": "./../lib/node/index.mjs"
},
"import": {
"types": "./../lib/node/index.d.mts",
"default": "./../lib/node/index.mjs"
},
"browser": null,
"react-native": null,
"default": {
"types": "./../lib/node/index.d.ts",
"default": "./../lib/node/index.js"
}
}
}
}
================================================
FILE: package.json
================================================
{
"name": "msw",
"version": "2.12.13",
"description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
"type": "commonjs",
"main": "./lib/core/index.js",
"module": "./lib/core/index.mjs",
"types": "./lib/core/index.d.ts",
"packageManager": "pnpm@9.15.0",
"exports": {
".": {
"module-sync": {
"types": "./lib/core/index.d.mts",
"default": "./lib/core/index.mjs"
},
"module": {
"types": "./lib/core/index.d.mts",
"default": "./lib/core/index.mjs"
},
"react-native": {
"import": {
"types": "./lib/core/index.d.mts",
"default": "./lib/core/index.mjs"
},
"default": {
"types": "./lib/core/index.d.ts",
"default": "./lib/core/index.js"
}
},
"import": {
"types": "./lib/core/index.d.mts",
"default": "./lib/core/index.mjs"
},
"default": {
"types": "./lib/core/index.d.ts",
"default": "./lib/core/index.js"
}
},
"./browser": {
"module-sync": {
"types": "./lib/browser/index.d.mts",
"default": "./lib/browser/index.mjs"
},
"module": {
"types": "./lib/browser/index.d.mts",
"default": "./lib/browser/index.mjs"
},
"browser": {
"types": "./lib/browser/index.d.mts",
"default": "./lib/browser/index.mjs"
},
"import": {
"types": "./lib/browser/index.d.mts",
"default": "./lib/browser/index.mjs"
},
"node": null,
"react-native": null,
"default": {
"types": "./lib/browser/index.d.ts",
"default": "./lib/browser/index.js"
}
},
"./node": {
"module-sync": {
"types": "./lib/node/index.d.mts",
"default": "./lib/node/index.mjs"
},
"module": {
"types": "./lib/node/index.d.mts",
"default": "./lib/node/index.mjs"
},
"node": {
"require": "./lib/node/index.js",
"import": "./lib/node/index.mjs"
},
"import": {
"types": "./lib/node/index.d.mts",
"default": "./lib/node/index.mjs"
},
"browser": null,
"react-native": null,
"default": {
"types": "./lib/node/index.d.ts",
"default": "./lib/node/index.js"
}
},
"./native": {
"browser": null,
"react-native": {
"import": {
"types": "./lib/native/index.d.mts",
"default": "./lib/native/index.mjs"
},
"default": {
"types": "./lib/native/index.d.ts",
"default": "./lib/native/index.js"
}
},
"import": {
"types": "./lib/native/index.d.mts",
"default": "./lib/native/index.mjs"
},
"default": {
"types": "./lib/native/index.d.ts",
"default": "./lib/native/index.js"
}
},
"./core/http": {
"module-sync": {
"types": "./lib/core/http.d.mts",
"default": "./lib/core/http.mjs"
},
"module": {
"types": "./lib/core/http.d.mts",
"default": "./lib/core/http.mjs"
},
"import": {
"types": "./lib/core/http.d.mts",
"default": "./lib/core/http.mjs"
},
"default": {
"types": "./lib/core/http.d.ts",
"default": "./lib/core/http.js"
}
},
"./core/graphql": {
"module-sync": {
"types": "./lib/core/graphql.d.mts",
"default": "./lib/core/graphql.mjs"
},
"module": {
"types": "./lib/core/graphql.d.mts",
"default": "./lib/core/graphql.mjs"
},
"import": {
"types": "./lib/core/graphql.d.mts",
"default": "./lib/core/graphql.mjs"
},
"default": {
"types": "./lib/core/graphql.d.ts",
"default": "./lib/core/graphql.js"
}
},
"./core/ws": {
"module-sync": {
"types": "./lib/core/ws.d.mts",
"default": "./lib/core/ws.mjs"
},
"module": {
"types": "./lib/core/ws.d.mts",
"default": "./lib/core/ws.mjs"
},
"import": {
"types": "./lib/core/ws.d.mts",
"default": "./lib/core/ws.mjs"
},
"default": {
"types": "./lib/core/ws.d.ts",
"default": "./lib/core/ws.js"
}
},
"./mockServiceWorker.js": "./lib/mockServiceWorker.js",
"./package.json": "./package.json"
},
"bin": {
"msw": "cli/index.js"
},
"engines": {
"node": ">=18"
},
"scripts": {
"start": "tsup --watch",
"clean": "rimraf ./lib",
"lint": "eslint \"{cli,src,test}/**/*.ts\"",
"build": "pnpm clean && cross-env NODE_ENV=production tsup && pnpm patch:dts",
"patch:dts": "node \"./config/scripts/patch-ts.js\"",
"publint": "publint",
"test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native",
"test:unit": "vitest",
"test:node": "vitest --config=./test/node/vitest.config.ts",
"test:native": "vitest --config=./test/native/vitest.config.ts",
"test:browser": "playwright test -c ./test/browser/playwright.config.ts",
"test:modules:node": "vitest run --config=./test/modules/node/vitest.config.ts",
"test:modules:browser": "playwright test -c ./test/modules/browser/playwright.config.ts",
"test:e2e": "vitest run --config=./test/e2e/vitest.config.ts",
"test:ts": "vitest --config=./test/typings/vitest.config.ts",
"prepare": "pnpm simple-git-hooks init",
"prepack": "pnpm build",
"release": "release publish",
"postinstall": "node -e \"import('./config/scripts/postinstall.js').catch(() => void 0)\"",
"knip": "knip"
},
"lint-staged": {
"**/*.ts": [
"eslint --fix"
],
"**/*.{ts,json}": [
"prettier --write"
]
},
"homepage": "https://mswjs.io",
"repository": {
"type": "git",
"url": "git+https://github.com/mswjs/msw.git"
},
"author": {
"name": "Artem Zakharchenko",
"url": "https://github.com/kettanaito"
},
"license": "MIT",
"funding": "https://github.com/sponsors/mswjs",
"files": [
"config/package.json",
"config/constants.js",
"config/scripts/postinstall.js",
"cli",
"lib",
"src",
"browser",
"node",
"native",
"LICENSE.md",
"README.md"
],
"keywords": [
"api",
"mock",
"mocking",
"worker",
"prototype",
"server",
"service",
"handler",
"testing",
"front-end",
"back-end"
],
"sideEffects": false,
"dependencies": {
"@inquirer/confirm": "^5.0.0",
"@mswjs/interceptors": "^0.41.2",
"@open-draft/deferred-promise": "^2.2.0",
"@types/statuses": "^2.0.6",
"cookie": "^1.0.2",
"graphql": "^16.12.0",
"headers-polyfill": "^4.0.2",
"is-node-process": "^1.2.0",
"outvariant": "^1.4.3",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"rettime": "^0.10.1",
"statuses": "^2.0.2",
"strict-event-emitter": "^0.5.1",
"tough-cookie": "^6.0.0",
"type-fest": "^5.2.0",
"until-async": "^3.0.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@epic-web/test-server": "^0.1.6",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.1",
"@fastify/websocket": "^11.2.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@open-draft/test-server": "^0.4.2",
"@ossjs/release": "^0.10.1",
"@playwright/test": "^1.50.1",
"@types/express": "^5.0.5",
"@types/json-bigint": "^1.0.4",
"@types/node": "~20.19.25",
"@types/serviceworker": "^0.0.167",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"@web/dev-server": "^0.4.6",
"axios": "^1.13.5",
"babel-minify": "^0.5.1",
"commitizen": "^4.3.1",
"cross-env": "^10.1.0",
"cross-fetch": "^4.1.0",
"cz-conventional-changelog": "3.3.0",
"esbuild": "^0.27.0",
"esbuild-loader": "^4.4.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"express": "^5.1.0",
"fastify": "^5.6.2",
"fs-teardown": "^0.3.0",
"glob": "^13.0.0",
"jsdom": "^25.0.1",
"json-bigint": "^1.0.0",
"knip": "^5.70.1",
"lint-staged": "^15.2.10",
"msw": "workspace:*",
"page-with": "^0.6.1",
"prettier": "^3.6.2",
"publint": "^0.3.15",
"regenerator-runtime": "^0.14.1",
"rimraf": "^6.1.2",
"simple-git-hooks": "^2.13.1",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.47.0",
"undici": "^7.16.0",
"url-loader": "^4.1.1",
"vitest": "^4.0.13",
"vitest-environment-miniflare": "^2.14.4",
"webpack": "^5.95.0",
"webpack-http-server": "^0.5.0"
},
"peerDependencies": {
"typescript": ">= 4.8.x"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
"prepare-commit-msg": "grep -qE '^[^#]' .git/COMMIT_EDITMSG || (exec < /dev/tty && pnpm cz --hook || true)",
"commit-msg": "pnpm commitlint --edit $1"
}
}
================================================
FILE: release.config.json
================================================
{
"$schema": "./node_modules/@ossjs/release/schema.json",
"profiles": [
{
"name": "latest",
"use": "NPM_CONFIG_PROVENANCE=true pnpm publish --no-git-checks"
}
]
}
================================================
FILE: src/browser/global.browser.d.ts
================================================
declare const SERVICE_WORKER_CHECKSUM: string
================================================
FILE: src/browser/index.ts
================================================
export { setupWorker } from './setupWorker/setupWorker'
export type { SetupWorker, StartOptions } from './setupWorker/glossary'
export { SetupWorkerApi } from './setupWorker/setupWorker'
================================================
FILE: src/browser/setupWorker/glossary.ts
================================================
import { Emitter } from 'strict-event-emitter'
import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors'
import type { DeferredPromise } from '@open-draft/deferred-promise'
import {
LifeCycleEventEmitter,
LifeCycleEventsMap,
SharedOptions,
} from '~/core/sharedOptions'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import type { RequiredDeep } from '~/core/typeUtils'
import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
import type { WorkerChannel } from '../utils/workerChannel'
export interface StringifiedResponse extends ResponseInit {
body: string | ArrayBuffer | ReadableStream | null
}
export type SetupWorkerInternalContext = {
isMockingEnabled: boolean
workerStoppedAt?: number
startOptions: RequiredDeep
workerPromise: DeferredPromise
registration: ServiceWorkerRegistration | undefined
getRequestHandlers: () => Array
emitter: Emitter
keepAliveInterval?: number
workerChannel: WorkerChannel
fallbackInterceptor?: Interceptor
}
export type ServiceWorkerInstanceTuple = [
ServiceWorker | null,
ServiceWorkerRegistration,
]
export type FindWorker = (
scriptUrl: string,
mockServiceWorkerUrl: string,
) => boolean
export interface StartOptions extends SharedOptions {
/**
* Service Worker registration options.
*/
serviceWorker?: {
/**
* Custom url to the worker script.
* @default "/mockServiceWorker.js"
*/
url?: string
options?: RegistrationOptions
}
/**
* Disables the logging of the intercepted requests
* into browser's console.
* @default false
*/
quiet?: boolean
/**
* Defers any network requests until the Service Worker
* instance is activated.
* @default true
*/
waitUntilReady?: boolean
/**
* A custom lookup function to find a Mock Service Worker in the list
* of all registered Service Workers on the page.
*/
findWorker?: FindWorker
}
export type StartReturnType = Promise
export type StartHandler = (
options: RequiredDeep,
initialOptions: StartOptions,
) => StartReturnType
export type StopHandler = () => void
export interface SetupWorker {
/**
* Registers and activates the mock Service Worker.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/start `worker.start()` API reference}
*/
start: (options?: StartOptions) => StartReturnType
/**
* Stops requests interception for the current client.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/stop `worker.stop()` API reference}
*/
stop: StopHandler
/**
* Prepends given request handlers to the list of existing handlers.
* @param {RequestHandler[]} handlers List of runtime request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/use `worker.use()` API reference}
*/
use: (...handlers: Array) => void
/**
* Marks all request handlers that respond using `res.once()` as unused.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/restore-handlers `worker.restoreHandlers()` API reference}
*/
restoreHandlers: () => void
/**
* Resets request handlers to the initial list given to the `setupWorker` call, or to the explicit next request handlers list, if given.
* @param {RequestHandler[]} nextHandlers List of the new initial request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/reset-handlers `worker.resetHandlers()` API reference}
*/
resetHandlers: (
...nextHandlers: Array
) => void
/**
* Returns a readonly list of currently active request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()` API reference}
*/
listHandlers(): ReadonlyArray
/**
* Life-cycle events.
* Life-cycle events allow you to subscribe to the internal library events occurring during the request/response handling.
*
* @see {@link https://mswjs.io/docs/api/life-cycle-events Life-cycle Events API reference}
*/
events: LifeCycleEventEmitter
}
================================================
FILE: src/browser/setupWorker/setupWorker.node.test.ts
================================================
/**
* @vitest-environment node
*/
import { setupWorker } from './setupWorker'
test('returns an error when run in a Node.js environment', () => {
expect(setupWorker).toThrow(
'[MSW] Failed to execute `setupWorker` in a non-browser environment',
)
})
================================================
FILE: src/browser/setupWorker/setupWorker.ts
================================================
import { invariant } from 'outvariant'
import { isNodeProcess } from 'is-node-process'
import { DeferredPromise } from '@open-draft/deferred-promise'
import type {
SetupWorkerInternalContext,
StartReturnType,
StartOptions,
SetupWorker,
} from './glossary'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler'
import { createStartHandler } from './start/createStartHandler'
import { devUtils } from '~/core/utils/internal/devUtils'
import { SetupApi } from '~/core/SetupApi'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import type { LifeCycleEventsMap } from '~/core/sharedOptions'
import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent'
import { attachWebSocketLogger } from '~/core/ws/utils/attachWebSocketLogger'
import { WorkerChannel } from '../utils/workerChannel'
import { createFallbackRequestListener } from './start/createFallbackRequestListener'
import { printStartMessage } from './start/utils/printStartMessage'
import { printStopMessage } from './stop/utils/printStopMessage'
import { supportsServiceWorker } from '../utils/supports'
export class SetupWorkerApi
extends SetupApi
implements SetupWorker
{
private context: SetupWorkerInternalContext
constructor(...handlers: Array) {
super(...handlers)
invariant(
!isNodeProcess(),
devUtils.formatMessage(
'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.',
),
)
this.context = this.createWorkerContext()
}
private createWorkerContext(): SetupWorkerInternalContext {
const workerPromise = new DeferredPromise()
return {
// Mocking is not considered enabled until the worker
// signals back the successful activation event.
isMockingEnabled: false,
startOptions: null as any,
workerPromise,
registration: undefined,
getRequestHandlers: () => {
return this.handlersController.currentHandlers()
},
emitter: this.emitter,
workerChannel: new WorkerChannel({
worker: workerPromise,
}),
}
}
public async start(options: StartOptions = {}): StartReturnType {
if ('waitUntilReady' in options) {
devUtils.warn(
'The "waitUntilReady" option has been deprecated. Please remove it from this "worker.start()" call. Follow the recommended Browser integration (https://mswjs.io/docs/integrations/browser) to eliminate any race conditions between the Service Worker registration and any requests made by your application on initial render.',
)
}
// Warn the developer on multiple "worker.start()" calls.
// While this will not affect the worker in any way,
// it likely indicates an issue with the developer's code.
if (this.context.isMockingEnabled) {
devUtils.warn(
`Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.`,
)
return this.context.registration
}
this.context.workerStoppedAt = undefined
this.context.startOptions = mergeRight(
DEFAULT_START_OPTIONS,
options,
) as SetupWorkerInternalContext['startOptions']
// Enable the WebSocket interception.
handleWebSocketEvent({
getUnhandledRequestStrategy: () => {
return this.context.startOptions.onUnhandledRequest
},
getHandlers: () => {
return this.handlersController.currentHandlers()
},
onMockedConnection: (connection) => {
if (!this.context.startOptions.quiet) {
// Attach the logger for mocked connections since
// those won't be visible in the browser's devtools.
attachWebSocketLogger(connection)
}
},
onPassthroughConnection() {},
})
webSocketInterceptor.apply()
this.subscriptions.push(() => {
webSocketInterceptor.dispose()
})
// Use a fallback interception algorithm in the environments
// where the Service Worker API isn't supported.
if (!supportsServiceWorker()) {
const fallbackInterceptor = createFallbackRequestListener(
this.context,
this.context.startOptions,
)
this.subscriptions.push(() => {
fallbackInterceptor.dispose()
})
this.context.isMockingEnabled = true
printStartMessage({
message: 'Mocking enabled (fallback mode).',
quiet: this.context.startOptions.quiet,
})
return undefined
}
const startHandler = createStartHandler(this.context)
const registration = await startHandler(this.context.startOptions, options)
this.context.isMockingEnabled = true
return registration
}
public stop(): void {
super.dispose()
if (!this.context.isMockingEnabled) {
devUtils.warn(
'Found a redundant "worker.stop()" call. Notice that stopping the worker after it has already been stopped has no effect. Consider removing this "worker.stop()" call.',
)
return
}
this.context.isMockingEnabled = false
this.context.workerStoppedAt = Date.now()
this.context.emitter.removeAllListeners()
if (supportsServiceWorker()) {
this.context.workerChannel.removeAllListeners('RESPONSE')
window.clearInterval(this.context.keepAliveInterval)
}
// Post the internal stop message on the window
// to let any logic know when the worker has stopped.
// E.g. the WebSocket client manager needs this to know
// when to clear its in-memory clients list.
window.postMessage({ type: 'msw/worker:stop' })
printStopMessage({
quiet: this.context.startOptions?.quiet,
})
}
}
/**
* Sets up a requests interception in the browser with the given request handlers.
* @param {RequestHandler[]} handlers List of request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker()` API reference}
*/
export function setupWorker(
...handlers: Array
): SetupWorker {
return new SetupWorkerApi(...handlers)
}
================================================
FILE: src/browser/setupWorker/start/createFallbackRequestListener.ts
================================================
import {
Interceptor,
BatchInterceptor,
HttpRequestEventMap,
} from '@mswjs/interceptors'
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
import { SetupWorkerInternalContext, StartOptions } from '../glossary'
import type { RequiredDeep } from '~/core/typeUtils'
import { handleRequest } from '~/core/utils/handleRequest'
import { isHandlerKind } from '~/core/utils/internal/isHandlerKind'
export function createFallbackRequestListener(
context: SetupWorkerInternalContext,
options: RequiredDeep,
): Interceptor {
const interceptor = new BatchInterceptor({
name: 'fallback',
interceptors: [new FetchInterceptor(), new XMLHttpRequestInterceptor()],
})
interceptor.on('request', async ({ request, requestId, controller }) => {
const requestCloneForLogs = request.clone()
const response = await handleRequest(
request,
requestId,
context.getRequestHandlers().filter(isHandlerKind('RequestHandler')),
options,
context.emitter,
{
resolutionContext: {
quiet: options.quiet,
},
onMockedResponse(_, { handler, parsedResult }) {
if (!options.quiet) {
context.emitter.once('response:mocked', ({ response }) => {
handler.log({
request: requestCloneForLogs,
response,
parsedResult,
})
})
}
},
},
)
if (response) {
controller.respondWith(response)
}
})
interceptor.on(
'response',
({ response, isMockedResponse, request, requestId }) => {
context.emitter.emit(
isMockedResponse ? 'response:mocked' : 'response:bypass',
{
response,
request,
requestId,
},
)
},
)
interceptor.apply()
return interceptor
}
================================================
FILE: src/browser/setupWorker/start/createRequestListener.ts
================================================
import { Emitter } from 'rettime'
import { StartOptions, SetupWorkerInternalContext } from '../glossary'
import { deserializeRequest } from '../../utils/deserializeRequest'
import { supportsReadableStreamTransfer } from '../../utils/supports'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { handleRequest } from '~/core/utils/handleRequest'
import { RequiredDeep } from '~/core/typeUtils'
import { devUtils } from '~/core/utils/internal/devUtils'
import { toResponseInit } from '~/core/utils/toResponseInit'
import { isHandlerKind } from '~/core/utils/internal/isHandlerKind'
const SUPPORTS_READABLE_STREAM_TRANSFER = supportsReadableStreamTransfer()
export const createRequestListener = (
context: SetupWorkerInternalContext,
options: RequiredDeep,
): Emitter.ListenerType => {
return async (event) => {
// Treat any incoming requests from the worker as passthrough
// if `worker.stop()` has been called for this client.
if (
!context.isMockingEnabled &&
context.workerStoppedAt &&
event.data.interceptedAt > context.workerStoppedAt
) {
event.postMessage('PASSTHROUGH')
return
}
const requestId = event.data.id
const request = deserializeRequest(event.data)
const requestCloneForLogs = request.clone()
// Make this the first request clone before the
// request resolution pipeline even starts.
// Store the clone in cache so the first matching
// request handler would skip the cloning phase.
const requestClone = request.clone()
RequestHandler.cache.set(request, requestClone)
try {
await handleRequest(
request,
requestId,
context.getRequestHandlers().filter(isHandlerKind('RequestHandler')),
options,
context.emitter,
{
resolutionContext: {
quiet: options.quiet,
},
onPassthroughResponse() {
event.postMessage('PASSTHROUGH')
},
async onMockedResponse(response, { handler, parsedResult }) {
// Clone the mocked response so its body could be read
// to buffer to be sent to the worker and also in the
// ".log()" method of the request handler.
const responseClone = response.clone()
const responseCloneForLogs = response.clone()
const responseInit = toResponseInit(response)
/**
* @note Safari doesn't support transferring a "ReadableStream".
* Check that the browser supports that before sending it to the worker.
*/
if (SUPPORTS_READABLE_STREAM_TRANSFER) {
const responseStreamOrNull = response.body
event.postMessage(
'MOCK_RESPONSE',
{
...responseInit,
body: responseStreamOrNull,
},
responseStreamOrNull ? [responseStreamOrNull] : undefined,
)
} else {
/**
* @note If we are here, this means the current environment doesn't
* support "ReadableStream" as transferable. In that case,
* attempt to read the non-empty response body as ArrayBuffer, if it's not empty.
* @see https://github.com/mswjs/msw/issues/1827
*/
const responseBufferOrNull =
response.body === null
? null
: await responseClone.arrayBuffer()
event.postMessage('MOCK_RESPONSE', {
...responseInit,
body: responseBufferOrNull,
})
}
if (!options.quiet) {
context.emitter.once('response:mocked', () => {
handler.log({
request: requestCloneForLogs,
response: responseCloneForLogs,
parsedResult,
})
})
}
},
},
)
} catch (error) {
if (error instanceof Error) {
devUtils.error(
`Uncaught exception in the request handler for "%s %s":
%s
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/http/mocking-responses/error-responses`,
request.method,
request.url,
error.stack ?? error,
)
// Treat all other exceptions in a request handler as unintended,
// alerting that there is a problem that needs fixing.
event.postMessage('MOCK_RESPONSE', {
status: 500,
statusText: 'Request Handler Error',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: error.name,
message: error.message,
stack: error.stack,
}),
})
}
}
}
}
================================================
FILE: src/browser/setupWorker/start/createResponseListener.ts
================================================
import { FetchResponse } from '@mswjs/interceptors'
import type { Emitter } from 'rettime'
import type { SetupWorkerInternalContext } from '../glossary'
import { deserializeRequest } from '../../utils/deserializeRequest'
export function createResponseListener(
context: SetupWorkerInternalContext,
): Emitter.ListenerType {
return (event) => {
const responseMessage = event.data
const request = deserializeRequest(responseMessage.request)
/**
* CORS requests with `mode: "no-cors"` result in "opaque" responses.
* That kind of responses cannot be manipulated in JavaScript due
* to the security considerations.
* @see https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
* @see https://github.com/mswjs/msw/issues/529
*/
if (responseMessage.response.type?.includes('opaque')) {
return
}
const response =
responseMessage.response.status === 0
? Response.error()
: new FetchResponse(
/**
* Responses may be streams here, but when we create a response object
* with null-body status codes, like 204, 205, 304 Response will
* throw when passed a non-null body, so ensure it's null here
* for those codes
*/
FetchResponse.isResponseWithBody(responseMessage.response.status)
? responseMessage.response.body
: null,
{
...responseMessage.response,
/**
* Set response URL if it's not set already.
* @see https://github.com/mswjs/msw/issues/2030
* @see https://developer.mozilla.org/en-US/docs/Web/API/Response/url
*/
url: request.url,
},
)
context.emitter.emit(
responseMessage.isMockedResponse ? 'response:mocked' : 'response:bypass',
{
requestId: responseMessage.request.id,
request,
response,
},
)
}
}
================================================
FILE: src/browser/setupWorker/start/createStartHandler.ts
================================================
import { devUtils } from '~/core/utils/internal/devUtils'
import { getWorkerInstance } from './utils/getWorkerInstance'
import { enableMocking } from './utils/enableMocking'
import type { SetupWorkerInternalContext, StartHandler } from '../glossary'
import { createRequestListener } from './createRequestListener'
import { checkWorkerIntegrity } from '../../utils/checkWorkerIntegrity'
import { createResponseListener } from './createResponseListener'
import { validateWorkerScope } from './utils/validateWorkerScope'
import { DeferredPromise } from '@open-draft/deferred-promise'
export const createStartHandler = (
context: SetupWorkerInternalContext,
): StartHandler => {
return function start(options, customOptions) {
const startWorkerInstance = async () => {
// Remove all previously existing event listeners.
// This way none of the listeners persists between Fast refresh
// of the application's code.
context.workerChannel.removeAllListeners()
// Handle requests signaled by the worker.
context.workerChannel.on(
'REQUEST',
createRequestListener(context, options),
)
// Handle responses signaled by the worker.
context.workerChannel.on('RESPONSE', createResponseListener(context))
const instance = await getWorkerInstance(
options.serviceWorker.url,
options.serviceWorker.options,
options.findWorker,
)
const [worker, registration] = instance
if (!worker) {
const missingWorkerMessage = customOptions?.findWorker
? devUtils.formatMessage(
`Failed to locate the Service Worker registration using a custom "findWorker" predicate.
Please ensure that the custom predicate properly locates the Service Worker registration at "%s".
More details: https://mswjs.io/docs/api/setup-worker/start#findworker
`,
options.serviceWorker.url,
)
: devUtils.formatMessage(
`Failed to locate the Service Worker registration.
This most likely means that the worker script URL "%s" cannot resolve against the actual public hostname (%s). This may happen if your application runs behind a proxy, or has a dynamic hostname.
Please consider using a custom "serviceWorker.url" option to point to the actual worker script location, or a custom "findWorker" option to resolve the Service Worker registration manually. More details: https://mswjs.io/docs/api/setup-worker/start`,
options.serviceWorker.url,
location.host,
)
throw new Error(missingWorkerMessage)
}
context.workerPromise.resolve(worker)
context.registration = registration
window.addEventListener('beforeunload', () => {
if (worker.state !== 'redundant') {
// Notify the Service Worker that this client has closed.
// Internally, it's similar to disabling the mocking, only
// client close event has a handler that self-terminates
// the Service Worker when there are no open clients.
context.workerChannel.postMessage('CLIENT_CLOSED')
}
// Make sure we're always clearing the interval - there are reports that not doing this can
// cause memory leaks in headless browser environments.
window.clearInterval(context.keepAliveInterval)
// Notify others about this client disconnecting.
// E.g. this will purge the in-memory WebSocket clients since
// starting the worker again will assign them new IDs.
window.postMessage({ type: 'msw/worker:stop' })
})
// Check if the active Service Worker has been generated
// by the currently installed version of MSW.
await checkWorkerIntegrity(context).catch((error) => {
devUtils.error(
'Error while checking the worker script integrity. Please report this on GitHub (https://github.com/mswjs/msw/issues) and include the original error below.',
)
console.error(error)
})
context.keepAliveInterval = window.setInterval(
() => context.workerChannel.postMessage('KEEPALIVE_REQUEST'),
5000,
)
// Warn the user when loading the page that lies outside
// of the worker's scope.
validateWorkerScope(registration, context.startOptions)
return registration
}
const workerRegistration = startWorkerInstance().then(
async (registration) => {
const pendingInstance = registration.installing || registration.waiting
if (pendingInstance) {
const activationPromise = new DeferredPromise()
pendingInstance.addEventListener('statechange', () => {
if (pendingInstance.state === 'activated') {
activationPromise.resolve()
}
})
// Wait until the worker is activated.
// Assume the worker is already activated if there's no pending registration
// (i.e. when reloading the page after a successful activation).
await activationPromise
}
// Print the activation message only after the worker has been activated.
await enableMocking(context, options).catch((error) => {
devUtils.error(
'Failed to enable mocking. Please report this on GitHub (https://github.com/mswjs/msw/issues) and include the original error below.',
)
throw error
})
return registration
},
)
return workerRegistration
}
}
================================================
FILE: src/browser/setupWorker/start/utils/enableMocking.ts
================================================
import { DeferredPromise } from '@open-draft/deferred-promise'
import type { StartOptions, SetupWorkerInternalContext } from '../../glossary'
import { printStartMessage } from './printStartMessage'
/**
* Signals the worker to enable the interception of requests.
*/
export function enableMocking(
context: SetupWorkerInternalContext,
options: StartOptions,
): Promise {
const mockingEnabledPromise = new DeferredPromise()
context.workerChannel.postMessage('MOCK_ACTIVATE')
context.workerChannel.once('MOCKING_ENABLED', async (event) => {
context.isMockingEnabled = true
const worker = await context.workerPromise
printStartMessage({
quiet: options.quiet,
workerScope: context.registration?.scope,
workerUrl: worker.scriptURL,
client: event.data.client,
})
mockingEnabledPromise.resolve(true)
})
return mockingEnabledPromise
}
================================================
FILE: src/browser/setupWorker/start/utils/getWorkerByRegistration.ts
================================================
import { FindWorker } from '../../glossary'
/**
* Attempts to resolve a Service Worker instance from a given registration,
* regardless of its state (active, installing, waiting).
*/
export function getWorkerByRegistration(
registration: ServiceWorkerRegistration,
absoluteWorkerUrl: string,
findWorker: FindWorker,
): ServiceWorker | null {
const allStates = [
registration.active,
registration.installing,
registration.waiting,
]
const relevantStates = allStates.filter((state): state is ServiceWorker => {
return state != null
})
const worker = relevantStates.find((worker) => {
return findWorker(worker.scriptURL, absoluteWorkerUrl)
})
return worker || null
}
================================================
FILE: src/browser/setupWorker/start/utils/getWorkerInstance.ts
================================================
import { until } from 'until-async'
import { devUtils } from '~/core/utils/internal/devUtils'
import { getAbsoluteWorkerUrl } from '../../../utils/getAbsoluteWorkerUrl'
import { getWorkerByRegistration } from './getWorkerByRegistration'
import { ServiceWorkerInstanceTuple, FindWorker } from '../../glossary'
/**
* Returns an active Service Worker instance.
* When not found, registers a new Service Worker.
*/
export const getWorkerInstance = async (
url: string,
options: RegistrationOptions = {},
findWorker: FindWorker,
): Promise => {
// Resolve the absolute Service Worker URL.
const absoluteWorkerUrl = getAbsoluteWorkerUrl(url)
const mockRegistrations = await navigator.serviceWorker
.getRegistrations()
.then((registrations) =>
registrations.filter((registration) =>
getWorkerByRegistration(registration, absoluteWorkerUrl, findWorker),
),
)
if (!navigator.serviceWorker.controller && mockRegistrations.length > 0) {
// Reload the page when it has associated workers, but no active controller.
// The absence of a controller can mean either:
// - page has no Service Worker associated with it
// - page has been hard-reloaded and its workers won't be used until the next reload.
// Since we've checked that there are registrations associated with this page,
// at this point we are sure it's hard reload that falls into this clause.
location.reload()
}
const [existingRegistration] = mockRegistrations
if (existingRegistration) {
// Schedule the worker update in the background.
// Update ensures the existing worker is up-to-date.
existingRegistration.update()
// Return the worker reference immediately.
return [
getWorkerByRegistration(
existingRegistration,
absoluteWorkerUrl,
findWorker,
),
existingRegistration,
]
}
// When the Service Worker wasn't found, register it anew and return the reference.
const [registrationError, registrationResult] = await until<
Error,
ServiceWorkerInstanceTuple
>(async () => {
const registration = await navigator.serviceWorker.register(url, options)
return [
// Compare existing worker registration by its worker URL,
// to prevent irrelevant workers to resolve here (such as Codesandbox worker).
getWorkerByRegistration(registration, absoluteWorkerUrl, findWorker),
registration,
]
})
// Handle Service Worker registration errors.
if (registrationError) {
const isWorkerMissing = registrationError.message.includes('(404)')
// Produce a custom error message when given a non-existing Service Worker url.
// Suggest developers to check their setup.
if (isWorkerMissing) {
const scopeUrl = new URL(options?.scope || '/', location.href)
throw new Error(
devUtils.formatMessage(`\
Failed to register a Service Worker for scope ('${scopeUrl.href}') with script ('${absoluteWorkerUrl}'): Service Worker script does not exist at the given path.
Did you forget to run "npx msw init "?
Learn more about creating the Service Worker script: https://mswjs.io/docs/cli/init`),
)
}
// Fallback error message for any other registration errors.
throw new Error(
devUtils.formatMessage(
'Failed to register the Service Worker:\n\n%s',
registrationError.message,
),
)
}
return registrationResult
}
================================================
FILE: src/browser/setupWorker/start/utils/prepareStartHandler.test.ts
================================================
import { SetupWorkerInternalContext, StartOptions } from '../../glossary'
import {
DEFAULT_START_OPTIONS,
resolveStartOptions,
prepareStartHandler,
} from './prepareStartHandler'
describe('resolveStartOptions', () => {
test('returns default options given no custom start options', () => {
expect(resolveStartOptions()).toEqual(DEFAULT_START_OPTIONS)
expect(resolveStartOptions(undefined)).toEqual(DEFAULT_START_OPTIONS)
expect(resolveStartOptions({})).toEqual(DEFAULT_START_OPTIONS)
})
test('deeply merges the default and custom start options', () => {
expect(
resolveStartOptions({
quiet: true,
serviceWorker: {
url: './custom.js',
},
}),
).toEqual({
...DEFAULT_START_OPTIONS,
quiet: true,
serviceWorker: {
url: './custom.js',
options: null,
},
})
})
})
describe('prepareStartHandler', () => {
test('exposes resolved start options to the generated star handler', () => {
const createStartHandler = vi.fn()
const context: SetupWorkerInternalContext = {} as any
const startHandler = prepareStartHandler(createStartHandler, context)
expect(startHandler).toBeInstanceOf(Function)
const initialOptions: StartOptions = {
quiet: true,
serviceWorker: {
url: './custom.js',
},
}
const resolvedOptions = resolveStartOptions(initialOptions)
startHandler(initialOptions)
// Calls the handler creator with both resolved and initial options.
expect(createStartHandler).toHaveBeenCalledWith(
resolvedOptions,
initialOptions,
)
// Sets the resolved options on the internal context.
expect(context).toHaveProperty('startOptions', resolvedOptions)
})
})
================================================
FILE: src/browser/setupWorker/start/utils/prepareStartHandler.ts
================================================
import { RequiredDeep } from '~/core/typeUtils'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import {
SetupWorker,
SetupWorkerInternalContext,
StartHandler,
StartOptions,
} from '../../glossary'
export const DEFAULT_START_OPTIONS: RequiredDeep = {
serviceWorker: {
url: '/mockServiceWorker.js',
options: null as any,
},
quiet: false,
waitUntilReady: true,
onUnhandledRequest: 'warn',
findWorker(scriptURL, mockServiceWorkerUrl) {
return scriptURL === mockServiceWorkerUrl
},
}
/**
* Returns resolved worker start options, merging the default options
* with the given custom options.
*/
export function resolveStartOptions(
initialOptions?: StartOptions,
): RequiredDeep {
return mergeRight(
DEFAULT_START_OPTIONS,
initialOptions || {},
) as RequiredDeep
}
export function prepareStartHandler(
handler: StartHandler,
context: SetupWorkerInternalContext,
): SetupWorker['start'] {
return (initialOptions) => {
context.startOptions = resolveStartOptions(initialOptions)
return handler(context.startOptions, initialOptions || {})
}
}
================================================
FILE: src/browser/setupWorker/start/utils/printStartMessage.test.ts
================================================
import { printStartMessage } from './printStartMessage'
beforeEach(() => {
vi.spyOn(console, 'groupCollapsed').mockImplementation(() => void 0)
vi.spyOn(console, 'log').mockImplementation(() => void 0)
})
afterEach(() => {
vi.restoreAllMocks()
})
test('prints out a default start message into console', () => {
printStartMessage({
workerScope: 'http://localhost:3000/',
workerUrl: 'http://localhost:3000/worker.js',
})
expect(console.groupCollapsed).toHaveBeenCalledWith(
'%c[MSW] Mocking enabled.',
expect.anything(),
)
// Includes a link to the documentation.
expect(console.log).toHaveBeenCalledWith(
'%cDocumentation: %chttps://mswjs.io/docs',
expect.anything(),
expect.anything(),
)
// Includes a link to the GitHub issues page.
expect(console.log).toHaveBeenCalledWith(
'Found an issue? https://github.com/mswjs/msw/issues',
)
// Includes service worker scope.
expect(console.log).toHaveBeenCalledWith(
'Worker scope:',
'http://localhost:3000/',
)
// Includes service worker script location.
expect(console.log).toHaveBeenCalledWith(
'Worker script URL:',
'http://localhost:3000/worker.js',
)
})
test('supports printing a custom start message', () => {
printStartMessage({ message: 'Custom start message' })
expect(console.groupCollapsed).toHaveBeenCalledWith(
'%c[MSW] Custom start message',
expect.anything(),
)
})
test('does not print any messages when log level is quiet', () => {
printStartMessage({ quiet: true })
expect(console.groupCollapsed).not.toHaveBeenCalled()
expect(console.log).not.toHaveBeenCalled()
})
test('prints a worker scope in the start message', () => {
printStartMessage({
workerScope: 'http://localhost:3000/user',
})
expect(console.log).toHaveBeenCalledWith(
'Worker scope:',
'http://localhost:3000/user',
)
})
test('prints a worker script url in the start message', () => {
printStartMessage({
workerUrl: 'http://localhost:3000/mockServiceWorker.js',
})
expect(console.log).toHaveBeenCalledWith(
'Worker script URL:',
'http://localhost:3000/mockServiceWorker.js',
)
})
================================================
FILE: src/browser/setupWorker/start/utils/printStartMessage.ts
================================================
import type { ServiceWorkerIncomingEventsMap } from '../../glossary'
import { devUtils } from '~/core/utils/internal/devUtils'
interface PrintStartMessageArgs {
quiet?: boolean
message?: string
workerUrl?: string
workerScope?: string
client?: ServiceWorkerIncomingEventsMap['MOCKING_ENABLED']['client']
}
/**
* Prints a worker activation message in the browser's console.
*/
export function printStartMessage(args: PrintStartMessageArgs = {}) {
if (args.quiet) {
return
}
const message = args.message || 'Mocking enabled.'
console.groupCollapsed(
`%c${devUtils.formatMessage(message)}`,
'color:orangered;font-weight:bold;',
)
// eslint-disable-next-line no-console
console.log(
'%cDocumentation: %chttps://mswjs.io/docs',
'font-weight:bold',
'font-weight:normal',
)
// eslint-disable-next-line no-console
console.log('Found an issue? https://github.com/mswjs/msw/issues')
if (args.workerUrl) {
// eslint-disable-next-line no-console
console.log('Worker script URL:', args.workerUrl)
}
if (args.workerScope) {
// eslint-disable-next-line no-console
console.log('Worker scope:', args.workerScope)
}
if (args.client) {
// eslint-disable-next-line no-console
console.log('Client ID: %s (%s)', args.client.id, args.client.frameType)
}
console.groupEnd()
}
================================================
FILE: src/browser/setupWorker/start/utils/validateWorkerScope.ts
================================================
import { devUtils } from '~/core/utils/internal/devUtils'
import { StartOptions } from '../../glossary'
export function validateWorkerScope(
registration: ServiceWorkerRegistration,
options?: StartOptions,
): void {
if (!options?.quiet && !location.href.startsWith(registration.scope)) {
devUtils.warn(
`\
Cannot intercept requests on this page because it's outside of the worker's scope ("${registration.scope}"). If you wish to mock API requests on this page, you must resolve this scope issue.
- (Recommended) Register the worker at the root level ("/") of your application.
- Set the "Service-Worker-Allowed" response header to allow out-of-scope workers.\
`,
)
}
}
================================================
FILE: src/browser/setupWorker/stop/utils/printStopMessage.test.ts
================================================
import { printStopMessage } from './printStopMessage'
beforeAll(() => {
vi.spyOn(global.console, 'log').mockImplementation(() => void 0)
})
afterEach(() => {
vi.resetAllMocks()
})
afterAll(() => {
vi.restoreAllMocks()
})
test('prints a stop message to the console', () => {
printStopMessage()
expect(console.log).toHaveBeenCalledWith(
'%c[MSW] Mocking disabled.',
'color:orangered;font-weight:bold;',
)
})
test('does not print any message when log level is quiet', () => {
printStopMessage({ quiet: true })
expect(console.log).not.toHaveBeenCalled()
})
================================================
FILE: src/browser/setupWorker/stop/utils/printStopMessage.ts
================================================
import { devUtils } from '~/core/utils/internal/devUtils'
export function printStopMessage(args: { quiet?: boolean } = {}): void {
if (args.quiet) {
return
}
// eslint-disable-next-line no-console
console.log(
`%c${devUtils.formatMessage('Mocking disabled.')}`,
'color:orangered;font-weight:bold;',
)
}
================================================
FILE: src/browser/tsconfig.browser.build.json
================================================
{
"extends": "./tsconfig.browser.json",
"compilerOptions": {
"composite": false
}
}
================================================
FILE: src/browser/tsconfig.browser.json
================================================
{
"extends": "../tsconfig.src.json",
"compilerOptions": {
// Expose browser-specific libraries only for the
// source code under the "src/browser" directory.
"lib": ["DOM", "WebWorker", "DOM.Iterable"]
},
"include": ["../../global.d.ts", "./global.browser.d.ts", "./**/*.ts"]
}
================================================
FILE: src/browser/utils/checkWorkerIntegrity.ts
================================================
import { devUtils } from '~/core/utils/internal/devUtils'
import type { SetupWorkerInternalContext } from '../setupWorker/glossary'
import { DeferredPromise } from '@open-draft/deferred-promise'
/**
* Check whether the registered Service Worker has been
* generated by the installed version of the library.
* Prints a warning message if the worker scripts mismatch.
*/
export function checkWorkerIntegrity(
context: SetupWorkerInternalContext,
): Promise {
const integrityCheckPromise = new DeferredPromise()
// Request the integrity checksum from the registered worker.
context.workerChannel.postMessage('INTEGRITY_CHECK_REQUEST')
context.workerChannel.once('INTEGRITY_CHECK_RESPONSE', (event) => {
const { checksum, packageVersion } = event.data
// Compare the response from the Service Worker and the
// global variable set during the build.
// The integrity is validated based on the worker script's checksum
// that's derived from its minified content during the build.
// The "SERVICE_WORKER_CHECKSUM" global variable is injected by the build.
if (checksum !== SERVICE_WORKER_CHECKSUM) {
devUtils.warn(
`The currently registered Service Worker has been generated by a different version of MSW (${packageVersion}) and may not be fully compatible with the installed version.
It's recommended you update your worker script by running this command:
\u2022 npx msw init
You can also automate this process and make the worker script update automatically upon the library installations. Read more: https://mswjs.io/docs/cli/init.`,
)
}
integrityCheckPromise.resolve()
})
return integrityCheckPromise
}
================================================
FILE: src/browser/utils/deserializeRequest.ts
================================================
import { pruneGetRequestBody } from './pruneGetRequestBody'
import type { ServiceWorkerIncomingRequest } from '../setupWorker/glossary'
/**
* Converts a given request received from the Service Worker
* into a Fetch `Request` instance.
*/
export function deserializeRequest(
serializedRequest: ServiceWorkerIncomingRequest,
): Request {
return new Request(serializedRequest.url, {
...serializedRequest,
body: pruneGetRequestBody(serializedRequest),
})
}
================================================
FILE: src/browser/utils/getAbsoluteWorkerUrl.test.ts
================================================
// @vitest-environment jsdom
import { getAbsoluteWorkerUrl } from './getAbsoluteWorkerUrl'
const rawLocation = window.location
afterAll(() => {
Object.defineProperty(window, 'location', {
value: rawLocation,
})
})
it('returns absolute worker url relatively to the root', () => {
expect(getAbsoluteWorkerUrl('./worker.js')).toBe('http://localhost/worker.js')
})
it('returns an absolute worker url relatively to the current path', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/path/to/page',
},
})
expect(getAbsoluteWorkerUrl('./worker.js')).toBe(
'http://localhost/path/to/worker.js',
)
// Leading slash must still resolve to the root.
expect(getAbsoluteWorkerUrl('/worker.js')).toBe('http://localhost/worker.js')
})
================================================
FILE: src/browser/utils/getAbsoluteWorkerUrl.ts
================================================
/**
* Returns an absolute Service Worker URL based on the given
* relative URL (known during the registration).
*/
export function getAbsoluteWorkerUrl(workerUrl: string): string {
return new URL(workerUrl, location.href).href
}
================================================
FILE: src/browser/utils/pruneGetRequestBody.test.ts
================================================
/**
* @vitest-environment jsdom
*/
import { TextEncoder } from 'util'
import { pruneGetRequestBody } from './pruneGetRequestBody'
test('sets empty GET request body to undefined', () => {
expect(
pruneGetRequestBody({
method: 'GET',
}),
).toBeUndefined()
expect(
pruneGetRequestBody({
method: 'GET',
// There's no such thing as a GET request with a body.
body: new ArrayBuffer(5),
}),
).toBeUndefined()
})
test('sets HEAD request body to undefined', () => {
expect(
pruneGetRequestBody({
method: 'HEAD',
}),
).toBeUndefined()
expect(
pruneGetRequestBody({
method: 'HEAD',
body: new ArrayBuffer(5),
}),
).toBeUndefined()
})
test('ignores requests of the other methods than GET', () => {
const body = new TextEncoder().encode('hello world')
expect(
pruneGetRequestBody({
method: 'POST',
body,
}),
).toEqual(body)
expect(
pruneGetRequestBody({
method: 'PUT',
body,
}),
).toEqual(body)
})
================================================
FILE: src/browser/utils/pruneGetRequestBody.ts
================================================
import type { ServiceWorkerIncomingRequest } from '../setupWorker/glossary'
type Input = Pick
/**
* Ensures that an empty GET request body is always represented as `undefined`.
*/
export function pruneGetRequestBody(
request: Input,
): ServiceWorkerIncomingRequest['body'] {
// Force HEAD/GET request body to always be empty.
// The worker reads any request's body as ArrayBuffer,
// and you cannot re-construct a GET/HEAD Request
// with an ArrayBuffer, even if empty. Also note that
// "request.body" is always undefined in the worker.
if (['HEAD', 'GET'].includes(request.method)) {
return undefined
}
return request.body
}
================================================
FILE: src/browser/utils/supports.ts
================================================
/**
* Checks if the Service Worker API is supproted and available
* in the current browsing context.
*/
export function supportsServiceWorker(): boolean {
return (
typeof navigator !== 'undefined' &&
'serviceWorker' in navigator &&
typeof location !== 'undefined' &&
location.protocol !== 'file:'
)
}
/**
* Returns a boolean indicating whether the current browser
* supports `ReadableStream` as a `Transferable` when posting
* messages.
*/
export function supportsReadableStreamTransfer() {
try {
const stream = new ReadableStream({
start: (controller) => controller.close(),
})
const message = new MessageChannel()
message.port1.postMessage(stream, [stream])
return true
} catch {
return false
}
}
================================================
FILE: src/browser/utils/workerChannel.ts
================================================
import { invariant } from 'outvariant'
import { Emitter, TypedEvent } from 'rettime'
import { isObject } from '~/core/utils/internal/isObject'
import type { StringifiedResponse } from '../setupWorker/glossary'
import { supportsServiceWorker } from '../utils/supports'
export interface WorkerChannelOptions {
worker: Promise
}
export type WorkerChannelEventMap = {
REQUEST: WorkerEvent
RESPONSE: WorkerEvent
MOCKING_ENABLED: WorkerEvent<{
client: {
id: string
frameType: string
}
}>
INTEGRITY_CHECK_RESPONSE: WorkerEvent<{
packageVersion: string
checksum: string
}>
KEEPALIVE_RESPONSE: TypedEvent
}
/**
* Request representation received from the worker message event.
*/
export interface IncomingWorkerRequest
extends Omit<
Request,
| 'text'
| 'body'
| 'json'
| 'blob'
| 'arrayBuffer'
| 'formData'
| 'clone'
| 'signal'
| 'isHistoryNavigation'
| 'isReloadNavigation'
> {
/**
* Unique ID of the request generated once the request is
* intercepted by the "fetch" event in the Service Worker.
*/
id: string
interceptedAt: number
body?: ArrayBuffer | null
}
type IncomingWorkerResponse = {
isMockedResponse: boolean
request: IncomingWorkerRequest
response: Pick<
Response,
'type' | 'ok' | 'status' | 'statusText' | 'body' | 'headers' | 'redirected'
>
}
export type WorkerEventResponse = {
MOCK_RESPONSE: [
data: StringifiedResponse,
transfer?: [ReadableStream],
]
PASSTHROUGH: []
}
const SUPPORTS_SERVICE_WORKER = supportsServiceWorker()
export class WorkerEvent<
DataType,
ReturnType = any,
EventType extends string = string,
> extends TypedEvent {
#workerEvent: MessageEvent
constructor(workerEvent: MessageEvent) {
const type = workerEvent.data.type as EventType
const data = workerEvent.data.payload as DataType
/**
* @note This is the only place we're mapping { type, payload }
* message structure of the worker. The client references the
* payload via `event.data`.
*/
super(
// @ts-expect-error Troublesome `TypedEvent` extension.
type,
{ data },
)
this.#workerEvent = workerEvent
}
get ports() {
return this.#workerEvent.ports
}
/**
* Reply directly to this event using its `MessagePort`.
*/
public postMessage(
type: Type,
...rest: WorkerEventResponse[Type]
): void {
this.#workerEvent.ports[0].postMessage(
{ type, data: rest[0] },
{ transfer: rest[1] },
)
}
}
/**
* Map of the events that can be sent to the Service Worker
* from any execution context.
*/
type OutgoingWorkerEvents =
| 'MOCK_ACTIVATE'
| 'INTEGRITY_CHECK_REQUEST'
| 'KEEPALIVE_REQUEST'
| 'CLIENT_CLOSED'
export class WorkerChannel extends Emitter {
constructor(protected readonly options: WorkerChannelOptions) {
super()
if (!SUPPORTS_SERVICE_WORKER) {
return
}
navigator.serviceWorker.addEventListener('message', async (event) => {
const worker = await this.options.worker
if (event.source != null && event.source !== worker) {
return
}
if (event.data && isObject(event.data) && 'type' in event.data) {
this.emit(new WorkerEvent(event))
}
})
}
/**
* Send data to the Service Worker controlling this client.
* This triggers the `message` event listener on ServiceWorkerGlobalScope.
*/
public postMessage(type: OutgoingWorkerEvents): void {
invariant(
SUPPORTS_SERVICE_WORKER,
'Failed to post message on a WorkerChannel: the Service Worker API is unavailable in this context. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues',
)
this.options.worker.then((worker) => {
worker.postMessage(type)
})
}
}
================================================
FILE: src/core/HttpResponse.test.ts
================================================
/**
* @vitest-environment node
*/
import { TextEncoder } from 'util'
import { HttpResponse, kDefaultContentType } from './HttpResponse'
it('creates a plain response', async () => {
const response = new HttpResponse(null, { status: 301 })
expect(response.status).toBe(301)
expect(response.statusText).toBe('Moved Permanently')
expect(response.body).toBe(null)
await expect(response.text()).resolves.toBe('')
expect(Object.fromEntries(response.headers.entries())).toEqual({})
})
it('supports non-configurable status codes', () => {
expect(new HttpResponse(null, { status: 101 })).toHaveProperty('status', 101)
})
describe('HttpResponse.text()', () => {
it('creates a text response', async () => {
const response = HttpResponse.text('hello world', { status: 201 })
expect(response.status).toBe(201)
expect(response.statusText).toBe('Created')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe('hello world')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'text/plain',
})
expect(kDefaultContentType in response).toBe(true)
})
it('creates a text response with special characters', async () => {
const response = HttpResponse.text('안녕 세상', { status: 201 })
expect(response.status).toBe(201)
expect(response.statusText).toBe('Created')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe('안녕 세상')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '13',
'content-type': 'text/plain',
})
})
it('allows overriding the "Content-Type" response header', async () => {
const response = HttpResponse.text('hello world', {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe('hello world')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'text/plain; charset=utf-8',
})
expect(kDefaultContentType in response).toBe(false)
})
it('allows overriding the "Content-Length" response header', async () => {
const response = HttpResponse.text('hello world', {
headers: { 'Content-Length': '32' },
})
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '32',
'content-type': 'text/plain',
})
})
})
describe('HttpResponse.json()', () => {
it('creates a json response given an object', async () => {
const response = HttpResponse.json({ firstName: 'John' })
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toEqual({ firstName: 'John' })
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '20',
'content-type': 'application/json',
})
expect(kDefaultContentType in response).toBe(true)
})
it('creates a json response given an object with special characters', async () => {
const response = HttpResponse.json({ firstName: '제로' })
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toEqual({ firstName: '제로' })
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '22',
'content-type': 'application/json',
})
})
it('creates a json response given an array', async () => {
const response = HttpResponse.json([1, 2, 3])
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toEqual([1, 2, 3])
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '7',
'content-type': 'application/json',
})
})
it('creates a json response given a plain string', async () => {
const response = HttpResponse.json(`"hello"`)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toBe(`"hello"`)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'application/json',
})
})
it('creates a json response given a number', async () => {
const response = HttpResponse.json(123)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toBe(123)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '3',
'content-type': 'application/json',
})
})
it('creates a json response given a json ReadableStream', async () => {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(`{"firstName`))
controller.enqueue(encoder.encode(`":"John`))
controller.enqueue(encoder.encode(`"}`))
controller.close()
},
})
const response = HttpResponse.json(stream)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
// A ReadableStream instance is not a valid body init
// for the "Response.json()" static method. It gets serialized
// into a plain object.
expect(await response.json()).toEqual({})
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '2',
'content-type': 'application/json',
})
})
it('allows overriding the "Content-Type" response header', async () => {
const response = HttpResponse.json(
{ a: 1 },
{
headers: {
'Content-Type': 'application/hal+json',
},
},
)
expect(kDefaultContentType in response).toBe(false)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
expect(await response.json()).toEqual({ a: 1 })
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '7',
'content-type': 'application/hal+json',
})
})
it('allows overriding the "Content-Length" response header', async () => {
const response = HttpResponse.json(
{ a: 1 },
{
headers: { 'Content-Length': '32' },
},
)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '32',
'content-type': 'application/json',
})
})
})
describe('HttpResponse.xml()', () => {
it('creates an xml response', async () => {
const response = HttpResponse.xml('')
expect(kDefaultContentType in response).toBe(true)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe('')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-type': 'text/xml',
})
expect(kDefaultContentType in response).toBe(true)
})
it('allows overriding the "Content-Type" response header', async () => {
const response = HttpResponse.xml('', {
headers: {
'Content-Type': 'text/xml; charset=utf-8',
},
})
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe('')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-type': 'text/xml; charset=utf-8',
})
expect(kDefaultContentType in response).toBe(false)
})
})
describe('HttpResponse.html()', () => {
it('creates an html response', async () => {
const response = HttpResponse.html('Jane Doe
')
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe(
'Jane Doe
',
)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-type': 'text/html',
})
expect(kDefaultContentType in response).toBe(true)
})
it('allows overriding the "Content-Type" response header', async () => {
const response = HttpResponse.html('Jane Doe
', {
headers: {
'Content-Type': 'text/html; charset=utf-8',
},
})
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
await expect(response.text()).resolves.toBe(
'Jane Doe
',
)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-type': 'text/html; charset=utf-8',
})
expect(kDefaultContentType in response).toBe(false)
})
})
describe('HttpResponse.arrayBuffer()', () => {
it('creates an array buffer response', async () => {
const buffer = new TextEncoder().encode('hello world')
const response = HttpResponse.arrayBuffer(buffer)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
const responseData = await response.arrayBuffer()
expect(responseData).toEqual(buffer.buffer)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'application/octet-stream',
})
expect(kDefaultContentType in response).toBe(true)
})
it('allows overriding the "Content-Type" response header', async () => {
const buffer = new TextEncoder().encode('hello world')
const response = HttpResponse.arrayBuffer(buffer, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
const responseData = await response.arrayBuffer()
expect(responseData).toEqual(buffer.buffer)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'text/plain; charset=utf-8',
})
expect(kDefaultContentType in response).toBe(false)
})
it('creates an array buffer response from a shared array buffer', async () => {
const arrayBuffer = new TextEncoder().encode('hello world')
// Copy the data from the array buffer to a shared array buffer
const sharedBuffer = new SharedArrayBuffer(11)
const sharedView = new Uint8Array(sharedBuffer)
sharedView.set(arrayBuffer)
const response = HttpResponse.arrayBuffer(sharedBuffer)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
const responseData = await response.arrayBuffer()
expect(responseData).toEqual(arrayBuffer.buffer)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'application/octet-stream',
})
})
it('allows overriding the "Content-Type" response header for shared array buffers', async () => {
const arrayBuffer = new TextEncoder().encode('hello world')
// Copy the data from the array buffer to a shared array buffer
const sharedBuffer = new SharedArrayBuffer(11)
const sharedView = new Uint8Array(sharedBuffer)
sharedView.set(arrayBuffer)
const response = HttpResponse.arrayBuffer(sharedBuffer, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
})
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
const responseData = await response.arrayBuffer()
expect(responseData).toEqual(arrayBuffer.buffer)
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-length': '11',
'content-type': 'text/plain; charset=utf-8',
})
expect(kDefaultContentType in response).toBe(false)
})
})
it('creates a form data response', async () => {
const formData = new FormData()
formData.append('firstName', 'John')
const response = HttpResponse.formData(formData)
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.body).toBeInstanceOf(ReadableStream)
const responseData = await response.formData()
expect(responseData.get('firstName')).toBe('John')
expect(Object.fromEntries(response.headers.entries())).toEqual({
'content-type': expect.stringContaining(
'multipart/form-data; boundary=----',
),
})
})
================================================
FILE: src/core/HttpResponse.ts
================================================
import { FetchResponse } from '@mswjs/interceptors'
import type { DefaultBodyType, JsonBodyType } from './handlers/RequestHandler'
import type { NoInfer } from './typeUtils'
import {
decorateResponse,
normalizeResponseInit,
} from './utils/HttpResponse/decorators'
export interface HttpResponseInit extends ResponseInit {
type?: ResponseType
}
export const bodyType: unique symbol = Symbol('bodyType')
export type DefaultUnsafeFetchResponse = Response & {
[bodyType]?: never
}
export interface StrictRequest extends Request {
json(): Promise
clone(): StrictRequest
}
/**
* Opaque `Response` type that supports strict body type.
*
* @deprecated Please use {@link HttpResponse} instead.
*/
export type StrictResponse =
HttpResponse
export const kDefaultContentType = Symbol.for('kDefaultContentType')
/**
* A drop-in replacement for the standard `Response` class
* to allow additional features, like mocking the response `Set-Cookie` header.
*
* @example
* new HttpResponse('Hello world', { status: 201 })
* HttpResponse.json({ name: 'John' })
* HttpResponse.formData(form)
*
* @see {@link https://mswjs.io/docs/api/http-response `HttpResponse` API reference}
*/
export class HttpResponse<
BodyType extends DefaultBodyType,
> extends FetchResponse {
readonly [bodyType]: BodyType = null as any
constructor(body?: NoInfer | null, init?: HttpResponseInit) {
const responseInit = normalizeResponseInit(init)
super(body as BodyInit, responseInit)
decorateResponse(this, responseInit)
}
static error(): HttpResponse {
return super.error() as HttpResponse
}
/**
* Create a `Response` with a `Content-Type: "text/plain"` body.
* @example
* HttpResponse.text('hello world')
* HttpResponse.text('Error', { status: 500 })
*/
static text(
body?: NoInfer | null,
init?: HttpResponseInit,
): HttpResponse {
const responseInit = normalizeResponseInit(init)
const hasExplicitContentType = responseInit.headers.has('Content-Type')
if (!hasExplicitContentType) {
responseInit.headers.set('Content-Type', 'text/plain')
}
// Automatically set the "Content-Length" response header
// for non-empty text responses. This enforces consistency and
// brings mocked responses closer to production.
if (!responseInit.headers.has('Content-Length')) {
responseInit.headers.set(
'Content-Length',
body ? new Blob([body]).size.toString() : '0',
)
}
const response = new HttpResponse(body, responseInit)
if (!hasExplicitContentType) {
Object.defineProperty(response, kDefaultContentType, {
value: true,
enumerable: false,
})
}
return response
}
/**
* Create a `Response` with a `Content-Type: "application/json"` body.
* @example
* HttpResponse.json({ firstName: 'John' })
* HttpResponse.json({ error: 'Not Authorized' }, { status: 401 })
*/
static json(
body?: NoInfer | null | undefined,
init?: HttpResponseInit,
): HttpResponse {
const responseInit = normalizeResponseInit(init)
const hasExplicitContentType = responseInit.headers.has('Content-Type')
if (!hasExplicitContentType) {
responseInit.headers.set('Content-Type', 'application/json')
}
/**
* @note TypeScript is incorrect here.
* Stringifying undefined will return undefined.
*/
const responseText = JSON.stringify(body) as string | undefined
if (!responseInit.headers.has('Content-Length')) {
responseInit.headers.set(
'Content-Length',
responseText ? new Blob([responseText]).size.toString() : '0',
)
}
const response = new HttpResponse(responseText, responseInit)
if (!hasExplicitContentType) {
Object.defineProperty(response, kDefaultContentType, {
value: true,
enumerable: false,
})
}
return response as HttpResponse
}
/**
* Create a `Response` with a `Content-Type: "application/xml"` body.
* @example
* HttpResponse.xml(``)
* HttpResponse.xml(``, { status: 201 })
*/
static xml(
body?: BodyType | null,
init?: HttpResponseInit,
): HttpResponse {
const responseInit = normalizeResponseInit(init)
const hasExplicitContentType = responseInit.headers.has('Content-Type')
if (!hasExplicitContentType) {
responseInit.headers.set('Content-Type', 'text/xml')
}
const response = new HttpResponse(body, responseInit)
if (!hasExplicitContentType) {
Object.defineProperty(response, kDefaultContentType, {
value: true,
enumerable: false,
})
}
return response as HttpResponse
}
/**
* Create a `Response` with a `Content-Type: "text/html"` body.
* @example
* HttpResponse.html(`Jane Doe
`)
* HttpResponse.html(`Main text`, { status: 201 })
*/
static html(
body?: BodyType | null,
init?: HttpResponseInit,
): HttpResponse {
const responseInit = normalizeResponseInit(init)
const hasExplicitContentType = responseInit.headers.has('Content-Type')
if (!hasExplicitContentType) {
responseInit.headers.set('Content-Type', 'text/html')
}
const response = new HttpResponse(body, responseInit)
if (!hasExplicitContentType) {
Object.defineProperty(response, kDefaultContentType, {
value: true,
enumerable: false,
})
}
return response as HttpResponse
}
/**
* Create a `Response` with an `ArrayBuffer` body.
* @example
* const buffer = new ArrayBuffer(3)
* const view = new Uint8Array(buffer)
* view.set([1, 2, 3])
*
* HttpResponse.arrayBuffer(buffer)
*/
static arrayBuffer(
body?: BodyType,
init?: HttpResponseInit,
): HttpResponse {
const responseInit = normalizeResponseInit(init)
const hasExplicitContentType = responseInit.headers.has('Content-Type')
if (!hasExplicitContentType) {
responseInit.headers.set('Content-Type', 'application/octet-stream')
}
if (body && !responseInit.headers.has('Content-Length')) {
responseInit.headers.set('Content-Length', body.byteLength.toString())
}
const response = new HttpResponse(body, responseInit)
if (!hasExplicitContentType) {
Object.defineProperty(response, kDefaultContentType, {
value: true,
enumerable: false,
})
}
return response as HttpResponse
}
/**
* Create a `Response` with a `FormData` body.
* @example
* const data = new FormData()
* data.set('name', 'Alice')
*
* HttpResponse.formData(data)
*/
static formData(
body?: FormData,
init?: HttpResponseInit,
): HttpResponse {
return new HttpResponse(body, normalizeResponseInit(init))
}
}
================================================
FILE: src/core/SetupApi.ts
================================================
import { invariant } from 'outvariant'
import { EventMap, Emitter } from 'strict-event-emitter'
import { RequestHandler } from './handlers/RequestHandler'
import { LifeCycleEventEmitter } from './sharedOptions'
import { devUtils } from './utils/internal/devUtils'
import { pipeEvents } from './utils/internal/pipeEvents'
import { toReadonlyArray } from './utils/internal/toReadonlyArray'
import { Disposable } from './utils/internal/Disposable'
import type { WebSocketHandler } from './handlers/WebSocketHandler'
export abstract class HandlersController {
abstract prepend(
runtimeHandlers: Array,
): void
abstract reset(nextHandles: Array): void
abstract currentHandlers(): Array
}
export class InMemoryHandlersController implements HandlersController {
private handlers: Array
constructor(
private initialHandlers: Array,
) {
this.handlers = [...initialHandlers]
}
public prepend(
runtimeHandles: Array,
): void {
this.handlers.unshift(...runtimeHandles)
}
public reset(nextHandlers: Array): void {
this.handlers =
nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers]
}
public currentHandlers(): Array {
return this.handlers
}
}
/**
* Generic class for the mock API setup.
*/
export abstract class SetupApi extends Disposable {
protected handlersController: HandlersController
protected readonly emitter: Emitter
protected readonly publicEmitter: Emitter
public readonly events: LifeCycleEventEmitter
constructor(...initialHandlers: Array) {
super()
invariant(
this.validateHandlers(initialHandlers),
devUtils.formatMessage(
`Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?`,
),
)
this.handlersController = new InMemoryHandlersController(initialHandlers)
this.emitter = new Emitter()
this.publicEmitter = new Emitter()
pipeEvents(this.emitter, this.publicEmitter)
this.events = this.createLifeCycleEvents()
this.subscriptions.push(() => {
this.emitter.removeAllListeners()
this.publicEmitter.removeAllListeners()
})
}
private validateHandlers(handlers: ReadonlyArray): boolean {
// Guard against incorrect call signature of the setup API.
return handlers.every((handler) => !Array.isArray(handler))
}
public use(
...runtimeHandlers: Array
): void {
invariant(
this.validateHandlers(runtimeHandlers),
devUtils.formatMessage(
`Failed to call "use()" with the given request handlers: invalid input. Did you forget to spread the array of request handlers?`,
),
)
this.handlersController.prepend(runtimeHandlers)
}
public restoreHandlers(): void {
this.handlersController.currentHandlers().forEach((handler) => {
if ('isUsed' in handler) {
handler.isUsed = false
}
})
}
public resetHandlers(
...nextHandlers: Array
): void {
this.handlersController.reset(nextHandlers)
}
public listHandlers(): ReadonlyArray {
return toReadonlyArray(this.handlersController.currentHandlers())
}
private createLifeCycleEvents(): LifeCycleEventEmitter {
return {
on: (...args: any[]) => {
return (this.publicEmitter.on as any)(...args)
},
removeListener: (...args: any[]) => {
return (this.publicEmitter.removeListener as any)(...args)
},
removeAllListeners: (...args: any[]) => {
return this.publicEmitter.removeAllListeners(...args)
},
}
}
}
================================================
FILE: src/core/bypass.test.ts
================================================
/**
* @vitest-environment jsdom
*/
import { bypass } from './bypass'
it('returns bypassed request given a request url string', async () => {
const request = bypass('https://api.example.com/resource')
// Relative URLs are rebased against the current location.
expect(request.method).toBe('GET')
expect(request.url).toBe('https://api.example.com/resource')
expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']])
})
it('returns bypassed request given a request url', async () => {
const request = bypass(new URL('/resource', 'https://api.example.com'))
expect(request.url).toBe('https://api.example.com/resource')
expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']])
})
it('returns bypassed request given request instance', async () => {
const original = new Request('http://localhost/resource', {
method: 'POST',
headers: {
accept: '*/*',
'X-My-Header': 'value',
},
body: 'hello world',
})
const request = bypass(original)
expect(request.method).toBe('POST')
expect(request.url).toBe('http://localhost/resource')
const bypassedRequestBody = await request.text()
expect(original.bodyUsed).toBe(false)
expect(bypassedRequestBody).toEqual(await original.text())
expect(Array.from(request.headers)).toEqual([
['accept', '*/*, msw/passthrough'],
['content-type', 'text/plain;charset=UTF-8'],
['x-my-header', 'value'],
])
})
it('allows modifying the bypassed request instance', async () => {
const original = new Request('http://localhost/resource', {
method: 'POST',
body: 'hello world',
})
const request = bypass(original, {
method: 'PUT',
headers: { 'x-modified-header': 'yes' },
})
expect(request.method).toBe('PUT')
expect(Array.from(request.headers)).toEqual([
['accept', 'msw/passthrough'],
['x-modified-header', 'yes'],
])
expect(original.bodyUsed).toBe(false)
expect(request.bodyUsed).toBe(false)
expect(await request.text()).toBe('hello world')
expect(original.bodyUsed).toBe(false)
})
it('supports bypassing "keepalive: true" requests', async () => {
const original = new Request('http://localhost/resource', {
method: 'POST',
keepalive: true,
})
const request = bypass(original)
expect(request.method).toBe('POST')
expect(request.url).toBe('http://localhost/resource')
expect(request.body).toBeNull()
expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']])
})
================================================
FILE: src/core/bypass.ts
================================================
import { invariant } from 'outvariant'
export type BypassRequestInput = string | URL | Request
/**
* Creates a `Request` instance that will always be ignored by MSW.
*
* @example
* import { bypass } from 'msw'
*
* fetch(bypass('/resource'))
* fetch(bypass(new URL('/resource', 'https://example.com)))
* fetch(bypass(new Request('https://example.com/resource')))
*
* @see {@link https://mswjs.io/docs/api/bypass `bypass()` API reference}
*/
export function bypass(input: BypassRequestInput, init?: RequestInit): Request {
// Always create a new Request instance.
// This way, the "init" modifications will propagate
// to the bypass request instance automatically.
const request = new Request(
// If given a Request instance, clone it not to exhaust
// the original request's body.
input instanceof Request ? input.clone() : input,
init,
)
invariant(
!request.bodyUsed,
'Failed to create a bypassed request to "%s %s": given request instance already has its body read. Make sure to clone the intercepted request if you wish to read its body before bypassing it.',
request.method,
request.url,
)
const requestClone = request.clone()
/**
* Send the internal request header that would instruct MSW
* to perform this request as-is, ignoring any matching handlers.
* @note Use the `accept` header to support scenarios when the
* request cannot have headers (e.g. `sendBeacon` requests).
*/
requestClone.headers.append('accept', 'msw/passthrough')
return requestClone
}
================================================
FILE: src/core/delay.ts
================================================
import { isNodeProcess } from 'is-node-process'
import { hasRefCounted } from './utils/internal/hasRefCounted'
export const SET_TIMEOUT_MAX_ALLOWED_INT = 2147483647
export const MIN_SERVER_RESPONSE_TIME = 100
export const MAX_SERVER_RESPONSE_TIME = 400
export const NODE_SERVER_RESPONSE_TIME = 5
function getRealisticResponseTime(): number {
if (isNodeProcess()) {
return NODE_SERVER_RESPONSE_TIME
}
return Math.floor(
Math.random() * (MAX_SERVER_RESPONSE_TIME - MIN_SERVER_RESPONSE_TIME) +
MIN_SERVER_RESPONSE_TIME,
)
}
export type DelayMode = 'real' | 'infinite'
/**
* Delays the response by the given duration (ms).
*
* @example
* await delay() // emulate realistic server response time
* await delay(1200) // delay response by 1200ms
* await delay('infinite') // delay response infinitely
*
* @see {@link https://mswjs.io/docs/api/delay `delay()` API reference}
*/
export async function delay(
durationOrMode?: DelayMode | number,
): Promise {
let delayTime: number
if (typeof durationOrMode === 'string') {
switch (durationOrMode) {
case 'infinite': {
// Using `Infinity` as a delay value executes the response timeout immediately.
// Instead, use the maximum allowed integer for `setTimeout`.
delayTime = SET_TIMEOUT_MAX_ALLOWED_INT
break
}
case 'real': {
delayTime = getRealisticResponseTime()
break
}
default: {
throw new Error(
`Failed to delay a response: unknown delay mode "${durationOrMode}". Please make sure you provide one of the supported modes ("real", "infinite") or a number.`,
)
}
}
} else if (typeof durationOrMode === 'undefined') {
// Use random realistic server response time when no explicit delay duration was provided.
delayTime = getRealisticResponseTime()
} else {
// Guard against passing values like `Infinity` or `Number.MAX_VALUE`
// as the response delay duration. They don't produce the result you may expect.
if (durationOrMode > SET_TIMEOUT_MAX_ALLOWED_INT) {
throw new Error(
`Failed to delay a response: provided delay duration (${durationOrMode}) exceeds the maximum allowed duration for "setTimeout" (${SET_TIMEOUT_MAX_ALLOWED_INT}). This will cause the response to be returned immediately. Please use a number within the allowed range to delay the response by exact duration, or consider the "infinite" delay mode to delay the response indefinitely.`,
)
}
delayTime = durationOrMode
}
return new Promise((resolve) => {
const timeoutId = setTimeout(resolve, delayTime)
if (
delayTime === SET_TIMEOUT_MAX_ALLOWED_INT &&
isNodeProcess() &&
hasRefCounted(timeoutId)
) {
// Prevent the process from hanging if this is the only active ref.
timeoutId.unref()
}
})
}
================================================
FILE: src/core/getResponse.test.ts
================================================
// @vitest-environment node
import { http } from './http'
import { getResponse } from './getResponse'
it('returns undefined given empty headers array', async () => {
await expect(
getResponse([], new Request('http://localhost/')),
).resolves.toBeUndefined()
})
it('returns undefined given no matching handlers', async () => {
await expect(
getResponse(
[http.get('/product', () => void 0)],
new Request('http://localhost/user'),
),
).resolves.toBeUndefined()
})
it('returns undefined given a matching handler that returned no response', async () => {
await expect(
getResponse(
[http.get('*/user', () => void 0)],
new Request('http://localhost/user'),
),
).resolves.toBeUndefined()
})
it('returns undefined given a matching handler that returned explicit undefined', async () => {
await expect(
getResponse(
[http.get('*/user', () => undefined)],
new Request('http://localhost/user'),
),
).resolves.toBeUndefined()
})
it('returns the response returned from a matching handler', async () => {
const response = await getResponse(
[http.get('*/user', () => Response.json({ name: 'John' }))],
new Request('http://localhost/user'),
)
expect(response?.status).toBe(200)
expect(response?.headers.get('Content-Type')).toBe('application/json')
await expect(response?.json()).resolves.toEqual({ name: 'John' })
})
it('returns the response from the first matching handler if multiple match', async () => {
const response = await getResponse(
[
http.get('*/user', () => Response.json({ name: 'John' })),
http.get('*/user', () => Response.json({ name: 'Kate' })),
],
new Request('http://localhost/user'),
)
expect(response?.status).toBe(200)
expect(response?.headers.get('Content-Type')).toBe('application/json')
await expect(response?.json()).resolves.toEqual({ name: 'John' })
})
it('supports custom base url', async () => {
const response = await getResponse(
[http.get('/resource', () => new Response('hello world'))],
new Request('https://localhost:3000/resource'),
{
baseUrl: 'https://localhost:3000/',
},
)
expect(response?.status).toBe(200)
await expect(response?.text()).resolves.toBe('hello world')
})
================================================
FILE: src/core/getResponse.ts
================================================
import { createRequestId } from '@mswjs/interceptors'
import type { RequestHandler } from './handlers/RequestHandler'
import {
executeHandlers,
type ResponseResolutionContext,
} from './utils/executeHandlers'
/**
* Finds a response for the given request instance
* in the array of request handlers.
* @param handlers The array of request handlers.
* @param request The `Request` instance.
* @param resolutionContext Request resolution options.
* @returns {Response} A mocked response, if any.
*/
export const getResponse = async (
handlers: Array,
request: Request,
resolutionContext?: ResponseResolutionContext,
): Promise => {
const result = await executeHandlers({
request,
requestId: createRequestId(),
handlers,
resolutionContext,
})
return result?.response
}
================================================
FILE: src/core/graphql.test.ts
================================================
import { graphql } from './graphql'
test('exports supported GraphQL operation types', () => {
expect(graphql).toBeDefined()
expect(Object.keys(graphql)).toEqual([
'query',
'mutation',
'operation',
'link',
])
})
================================================
FILE: src/core/graphql.ts
================================================
import type { OperationTypeNode } from 'graphql'
import {
ResponseResolver,
RequestHandlerOptions,
} from './handlers/RequestHandler'
import {
GraphQLHandler,
GraphQLVariables,
GraphQLOperationType,
GraphQLResolverExtras,
GraphQLResponseBody,
GraphQLQuery,
GraphQLPredicate,
} from './handlers/GraphQLHandler'
import type { Path } from './utils/matching/matchRequestUrl'
export type GraphQLRequestHandler = <
Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables,
>(
predicate: GraphQLPredicate,
resolver: GraphQLResponseResolver<
[Query] extends [never] ? GraphQLQuery : Query,
Variables
>,
options?: RequestHandlerOptions,
) => GraphQLHandler
export type GraphQLOperationHandler = <
Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables,
>(
resolver: GraphQLResponseResolver<
[Query] extends [never] ? GraphQLQuery : Query,
Variables
>,
options?: RequestHandlerOptions,
) => GraphQLHandler
export type GraphQLResponseResolver<
Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables,
> = ResponseResolver<
GraphQLResolverExtras,
null,
GraphQLResponseBody<[Query] extends [never] ? GraphQLQuery : Query>
>
function createScopedGraphQLHandler(
operationType: GraphQLOperationType,
url: Path,
): GraphQLRequestHandler {
return (predicate, resolver, options = {}) => {
return new GraphQLHandler(operationType, predicate, url, resolver, options)
}
}
function createGraphQLOperationHandler(url: Path): GraphQLOperationHandler {
return (resolver, options) => {
return new GraphQLHandler('all', new RegExp('.*'), url, resolver, options)
}
}
export interface GraphQLLinkHandlers {
query: GraphQLRequestHandler
mutation: GraphQLRequestHandler
operation: GraphQLOperationHandler
}
/**
* A namespace to intercept and mock GraphQL operations
*
* @example
* graphql.query('GetUser', resolver)
* graphql.mutation('DeletePost', resolver)
*
* @see {@link https://mswjs.io/docs/api/graphql `graphql` API reference}
*/
export const graphql = {
/**
* Intercepts a GraphQL query by a given name.
*
* @example
* graphql.query('GetUser', () => {
* return HttpResponse.json({ data: { user: { name: 'John' } } })
* })
*
* @see {@link https://mswjs.io/docs/api/graphql#graphqlqueryqueryname-resolver `graphql.query()` API reference}
*/
query: createScopedGraphQLHandler('query' as OperationTypeNode, '*'),
/**
* Intercepts a GraphQL mutation by its name.
*
* @example
* graphql.mutation('SavePost', () => {
* return HttpResponse.json({ data: { post: { id: 'abc-123 } } })
* })
*
* @see {@link https://mswjs.io/docs/api/graphql#graphqlmutationmutationname-resolver `graphql.query()` API reference}
*
*/
mutation: createScopedGraphQLHandler('mutation' as OperationTypeNode, '*'),
/**
* Intercepts any GraphQL operation, regardless of its type or name.
*
* @example
* graphql.operation(() => {
* return HttpResponse.json({ data: { name: 'John' } })
* })
*
* @see {@link https://mswjs.io/docs/api/graphql#graphqloperationresolver `graphql.operation()` API reference}
*/
operation: createGraphQLOperationHandler('*'),
/**
* Intercepts GraphQL operations scoped by the given URL.
*
* @example
* const github = graphql.link('https://api.github.com/graphql')
* github.query('GetRepo', resolver)
*
* @see {@link https://mswjs.io/docs/api/graphql#graphqllinkurl `graphql.link()` API reference}
*/
link(url: Path): GraphQLLinkHandlers {
return {
operation: createGraphQLOperationHandler(url),
query: createScopedGraphQLHandler('query' as OperationTypeNode, url),
mutation: createScopedGraphQLHandler(
'mutation' as OperationTypeNode,
url,
),
}
},
}
================================================
FILE: src/core/handlers/GraphQLHandler.test.ts
================================================
// @vitest-environment jsdom
import { createRequestId, encodeBuffer } from '@mswjs/interceptors'
import { OperationTypeNode, parse } from 'graphql'
import {
GraphQLHandler,
GraphQLRequestBody,
GraphQLResolverExtras,
isDocumentNode,
} from './GraphQLHandler'
import { HttpResponse } from '../HttpResponse'
import { ResponseResolver } from './RequestHandler'
const resolver: ResponseResolver> = ({
variables,
}) => {
return HttpResponse.json({
data: {
user: {
id: variables.userId,
},
},
})
}
function createGetGraphQLRequest(
body: GraphQLRequestBody,
graphqlEndpoint = 'https://example.com',
) {
const requestUrl = new URL(graphqlEndpoint)
requestUrl.searchParams.set('query', body?.query)
requestUrl.searchParams.set('variables', JSON.stringify(body?.variables))
return new Request(requestUrl)
}
function createPostGraphQLRequest(
body: GraphQLRequestBody,
graphqlEndpoint = 'https://example.com',
) {
return new Request(new URL(graphqlEndpoint), {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: encodeBuffer(JSON.stringify(body)),
})
}
const GET_USER = `
query GetUser {
user {
id
}
}
`
const LOGIN = `
mutation Login {
user {
id
}
}
`
describe('info', () => {
it('exposes request handler information for query', () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
expect(handler.info.header).toEqual('query GetUser (origin: *)')
expect(handler.info.operationType).toEqual('query')
expect(handler.info.operationName).toEqual('GetUser')
})
it('exposes request handler information for mutation', () => {
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
'Login',
'*',
resolver,
)
expect(handler.info.header).toEqual('mutation Login (origin: *)')
expect(handler.info.operationType).toEqual('mutation')
expect(handler.info.operationName).toEqual('Login')
})
it('parses a query operation name from a given DocumentNode', () => {
const node = parse(`
query GetUser {
user {
firstName
}
}
`)
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
node,
'*',
resolver,
)
expect(handler.info).toHaveProperty('header', 'query GetUser (origin: *)')
expect(handler.info).toHaveProperty('operationType', 'query')
expect(handler.info).toHaveProperty('operationName', 'GetUser')
})
it('parses a mutation operation name from a given DocumentNode', () => {
const node = parse(`
mutation Login {
user {
id
}
}
`)
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
node,
'*',
resolver,
)
expect(handler.info).toHaveProperty('header', 'mutation Login (origin: *)')
expect(handler.info).toHaveProperty('operationType', 'mutation')
expect(handler.info).toHaveProperty('operationName', 'Login')
})
it('throws an exception given a DocumentNode with a mismatched operation type', () => {
const node = parse(`
mutation CreateUser {
user {
firstName
}
}
`)
expect(
() => new GraphQLHandler(OperationTypeNode.QUERY, node, '*', resolver),
).toThrow(
'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "query" but got "mutation").',
)
})
})
describe('parse', () => {
describe('query', () => {
it('parses a query without variables (GET)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createGetGraphQLRequest({
query: GET_USER,
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: undefined,
})
})
it('parses a query with variables (GET)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createGetGraphQLRequest({
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
})
it('parses a query without variables (POST)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: undefined,
})
})
it('parses a query with variables (POST)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
})
})
describe('mutation', () => {
it('parses a mutation without variables (GET)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
'GetUser',
'*',
resolver,
)
const request = createGetGraphQLRequest({
query: LOGIN,
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'mutation',
operationName: 'Login',
query: LOGIN,
variables: undefined,
})
})
it('parses a mutation with variables (GET)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
'GetUser',
'*',
resolver,
)
const request = createGetGraphQLRequest({
query: LOGIN,
variables: {
userId: 'abc-123',
},
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'mutation',
operationName: 'Login',
query: LOGIN,
variables: {
userId: 'abc-123',
},
})
})
it('parses a mutation without variables (POST)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: LOGIN,
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'mutation',
operationName: 'Login',
query: LOGIN,
variables: undefined,
})
})
it('parses a mutation with variables (POST)', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.MUTATION,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: LOGIN,
variables: {
userId: 'abc-123',
},
})
expect(await handler.parse({ request })).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'mutation',
operationName: 'Login',
query: LOGIN,
variables: {
userId: 'abc-123',
},
})
})
})
describe('with endpoint configuration', () => {
it('parses the request and parses grapqhl properties from it when the graphql.link endpoint matches', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'https://mswjs.com/graphql',
resolver,
)
await expect(
handler.parse({
request: createGetGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://mswjs.com/graphql',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: true,
params: {},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
await expect(
handler.parse({
request: createPostGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://mswjs.com/graphql',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: true,
params: {},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
})
it('parses a request but does not parse graphql properties from it graphql.link hostname does not match', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'https://mswjs.com/graphql',
resolver,
)
await expect(
handler.parse({
request: createGetGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://example.com/graphql',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: false,
params: {},
},
})
await expect(
handler.parse({
request: createPostGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://example.com/graphql',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: false,
params: {},
},
})
})
it('parses a request but does not parse graphql properties from it graphql.link pathname does not match', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'https://mswjs.com/graphql',
resolver,
)
await expect(
handler.parse({
request: createGetGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://mswjs.com/some/other/endpoint',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: false,
params: {},
},
})
await expect(
handler.parse({
request: createPostGraphQLRequest(
{
query: GET_USER,
variables: {
userId: 'abc-123',
},
},
'https://mswjs.com/some/other/endpoint',
),
}),
).resolves.toEqual({
cookies: {},
match: {
matches: false,
params: {},
},
})
})
})
})
describe('predicate', () => {
it('respects operation type', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
})
const alienRequest = createPostGraphQLRequest({
query: LOGIN,
})
expect(
await handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).toBe(true)
expect(
await handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
).toBe(false)
})
it('respects operation name', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
})
const alienRequest = createPostGraphQLRequest({
query: `
query GetAllUsers {
user {
id
}
}
`,
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
await expect(
handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
).resolves.toBe(false)
})
it('allows anonymous GraphQL operations when using "all" expected operation type', async () => {
const handler = new GraphQLHandler('all', new RegExp('.*'), '*', resolver)
const request = createPostGraphQLRequest({
query: `
query {
anonymousQuery {
query
variables
}
}
`,
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
})
it('respects custom endpoint', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'https://api.github.com/graphql',
resolver,
)
const request = createPostGraphQLRequest(
{
query: GET_USER,
},
'https://api.github.com/graphql',
)
const alienRequest = createPostGraphQLRequest({
query: GET_USER,
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
await expect(
handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
).resolves.toBe(false)
})
it('supports custom predicate function', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
({ query }) => {
return query.includes('password')
},
/.+/,
resolver,
)
{
const request = createPostGraphQLRequest({
query: `query GetUser { user { password } }`,
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
}
{
const request = createPostGraphQLRequest({
query: `query GetUser { user { nonMatching } }`,
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(false)
}
})
})
describe('test', () => {
it('respects operation type', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
})
const alienRequest = createPostGraphQLRequest({
query: LOGIN,
})
expect(await handler.test({ request })).toBe(true)
expect(await handler.test({ request: alienRequest })).toBe(false)
})
it('respects operation name', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
})
const alienRequest = createPostGraphQLRequest({
query: `
query GetAllUsers {
user {
id
}
}
`,
})
await expect(handler.test({ request })).resolves.toBe(true)
await expect(handler.test({ request: alienRequest })).resolves.toBe(false)
})
it('respects custom endpoint', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'https://api.github.com/graphql',
resolver,
)
const request = createPostGraphQLRequest(
{
query: GET_USER,
},
'https://api.github.com/graphql',
)
const alienRequest = createPostGraphQLRequest({
query: GET_USER,
})
await expect(handler.test({ request })).resolves.toBe(true)
await expect(handler.test({ request: alienRequest })).resolves.toBe(false)
})
})
describe('run', () => {
it('returns a mocked response given a matching query', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
const requestId = createRequestId()
const result = await handler.run({ request, requestId })
expect(result!.handler).toEqual(handler)
expect(result!.parsedResult).toEqual({
cookies: {},
match: {
matches: true,
params: {
'0': 'https://example.com/',
},
},
operationType: 'query',
operationName: 'GetUser',
query: GET_USER,
variables: {
userId: 'abc-123',
},
})
expect(result!.request.method).toBe('POST')
expect(result!.request.url).toBe('https://example.com/')
await expect(result!.request.json()).resolves.toEqual({
query: GET_USER,
variables: { userId: 'abc-123' },
})
expect(result!.response?.status).toBe(200)
expect(result!.response?.statusText).toBe('OK')
await expect(result!.response?.json()).resolves.toEqual({
data: { user: { id: 'abc-123' } },
})
})
it('returns null given a non-matching query', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'GetUser',
'*',
resolver,
)
const request = createPostGraphQLRequest({
query: LOGIN,
})
const requestId = createRequestId()
const result = await handler.run({ request, requestId })
expect(result).toBeNull()
})
})
describe('isDocumentNode', () => {
it('returns true given a valid DocumentNode', () => {
const node = parse(`
query GetUser {
user {
login
}
}
`)
expect(isDocumentNode(node)).toEqual(true)
})
it('returns false given an arbitrary input', () => {
expect(isDocumentNode(null)).toEqual(false)
expect(isDocumentNode(undefined)).toEqual(false)
expect(isDocumentNode('')).toEqual(false)
expect(isDocumentNode('value')).toEqual(false)
expect(isDocumentNode(/value/)).toEqual(false)
})
})
describe('request', () => {
it('has parsed operationName', async () => {
const matchAllResolver = vi.fn()
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
/.*/,
'*',
matchAllResolver,
)
const request = createPostGraphQLRequest({
query: `
query GetAllUsers {
user {
id
}
}
`,
})
const requestId = createRequestId()
await handler.run({ request, requestId })
expect(matchAllResolver).toHaveBeenCalledTimes(1)
expect(matchAllResolver.mock.calls[0][0]).toHaveProperty(
'operationName',
'GetAllUsers',
)
})
})
================================================
FILE: src/core/handlers/GraphQLHandler.ts
================================================
import { invariant } from 'outvariant'
import {
parse,
type DocumentNode,
type GraphQLError,
type OperationTypeNode,
} from 'graphql'
import {
DefaultBodyType,
RequestHandler,
RequestHandlerDefaultInfo,
RequestHandlerExecutionResult,
RequestHandlerOptions,
ResponseResolver,
} from './RequestHandler'
import { getTimestamp } from '../utils/logging/getTimestamp'
import { getStatusCodeColor } from '../utils/logging/getStatusCodeColor'
import { serializeRequest } from '../utils/logging/serializeRequest'
import { serializeResponse } from '../utils/logging/serializeResponse'
import { Match, matchRequestUrl, Path } from '../utils/matching/matchRequestUrl'
import {
ParsedGraphQLRequest,
GraphQLMultipartRequestBody,
parseGraphQLRequest,
parseDocumentNode,
ParsedGraphQLQuery,
} from '../utils/internal/parseGraphQLRequest'
import { toPublicUrl } from '../utils/request/toPublicUrl'
import { devUtils } from '../utils/internal/devUtils'
import { getAllRequestCookies } from '../utils/request/getRequestCookies'
import { ResponseResolutionContext } from 'src/iife'
import { kDefaultContentType, StrictRequest } from '../HttpResponse'
import { getAllAcceptedMimeTypes } from '../utils/request/getAllAcceptedMimeTypes'
export interface DocumentTypeDecoration<
Result = { [key: string]: any },
Variables = { [key: string]: any },
> {
__apiType?: (variables: Variables) => Result
__resultType?: Result
__variablesType?: Variables
}
export type GraphQLOperationType = OperationTypeNode | 'all'
export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string
export type GraphQLQuery = Record | null
export type GraphQLVariables = Record
export interface GraphQLHandlerInfo extends RequestHandlerDefaultInfo {
operationType: GraphQLOperationType
operationName: GraphQLHandlerNameSelector | GraphQLCustomPredicate
}
export type GraphQLRequestParsedResult = {
match: Match
cookies: Record
} & (
| ParsedGraphQLRequest
/**
* An empty version of the ParsedGraphQLRequest
* which simplifies the return type of the resolver
* when the request is to a non-matching endpoint
*/
| {
operationType?: undefined
operationName?: undefined
query?: undefined
variables?: undefined
}
)
export type GraphQLResolverExtras = {
query: string
operationName: string
variables: Variables
cookies: Record
}
export type GraphQLRequestBody =
| GraphQLJsonRequestBody
| GraphQLMultipartRequestBody
| Record
| undefined
export interface GraphQLJsonRequestBody {
query: string
variables?: Variables
}
export type GraphQLResponseBody =
| {
data?: BodyType | null
errors?: readonly Partial[] | null
extensions?: Record
}
| null
| undefined
export type GraphQLCustomPredicate = (args: {
request: Request
query: string
operationType: GraphQLOperationType
operationName: string
variables: GraphQLVariables
cookies: Record
}) => GraphQLCustomPredicateResult | Promise
export type GraphQLCustomPredicateResult = boolean | { matches: boolean }
export type GraphQLPredicate =
| GraphQLHandlerNameSelector
| DocumentTypeDecoration
| GraphQLCustomPredicate
export function isDocumentNode(
value: DocumentNode | any,
): value is DocumentNode {
if (value == null) {
return false
}
return typeof value === 'object' && 'kind' in value && 'definitions' in value
}
function isDocumentTypeDecoration(
value: unknown,
): value is DocumentTypeDecoration {
return value instanceof String
}
export class GraphQLHandler extends RequestHandler<
GraphQLHandlerInfo,
GraphQLRequestParsedResult,
GraphQLResolverExtras
> {
private endpoint: Path
static parsedRequestCache = new WeakMap<
Request,
ParsedGraphQLRequest
>()
static #parseOperationName(
predicate: GraphQLPredicate,
operationType: GraphQLOperationType,
): GraphQLHandlerInfo['operationName'] {
const getOperationName = (node: ParsedGraphQLQuery): string => {
invariant(
node.operationType === operationType,
'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "%s" but got "%s").',
operationType,
node.operationType,
)
invariant(
node.operationName,
'Failed to create a GraphQL handler: provided a DocumentNode without operation name',
)
return node.operationName
}
if (isDocumentNode(predicate)) {
return getOperationName(parseDocumentNode(predicate))
}
if (isDocumentTypeDecoration(predicate)) {
const documentNode = parse(predicate.toString())
invariant(
isDocumentNode(documentNode),
'Failed to create a GraphQL handler: given TypedDocumentString (%s) does not produce a valid DocumentNode',
predicate,
)
return getOperationName(parseDocumentNode(documentNode))
}
return predicate
}
constructor(
operationType: GraphQLOperationType,
predicate: GraphQLPredicate,
endpoint: Path,
resolver: ResponseResolver, any, any>,
options?: RequestHandlerOptions,
) {
const operationName = GraphQLHandler.#parseOperationName(
predicate,
operationType,
)
const displayOperationName =
typeof operationName === 'function' ? '[custom predicate]' : operationName
const header =
operationType === 'all'
? `${operationType} (origin: ${endpoint.toString()})`
: `${operationType}${displayOperationName ? ` ${displayOperationName}` : ''} (origin: ${endpoint.toString()})`
super({
info: {
header,
operationType,
operationName: GraphQLHandler.#parseOperationName(
predicate,
operationType,
),
},
resolver,
options,
})
this.endpoint = endpoint
}
/**
* Parses the request body, once per request, cached across all
* GraphQL handlers. This is done to avoid multiple parsing of the
* request body, which each requires a clone of the request.
*/
async parseGraphQLRequestOrGetFromCache(
request: Request,
): Promise> {
if (!GraphQLHandler.parsedRequestCache.has(request)) {
GraphQLHandler.parsedRequestCache.set(
request,
await parseGraphQLRequest(request).catch((error) => {
console.error(error)
return undefined
}),
)
}
return GraphQLHandler.parsedRequestCache.get(request)
}
async parse(args: { request: Request }): Promise {
/**
* If the request doesn't match a specified endpoint, there's no
* need to parse it since there's no case where we would handle this
*/
const match = matchRequestUrl(new URL(args.request.url), this.endpoint)
const cookies = getAllRequestCookies(args.request)
if (!match.matches) {
return {
match,
cookies,
}
}
const parsedResult = await this.parseGraphQLRequestOrGetFromCache(
args.request,
)
if (typeof parsedResult === 'undefined') {
return {
match,
cookies,
}
}
return {
match,
cookies,
query: parsedResult.query,
operationType: parsedResult.operationType,
operationName: parsedResult.operationName,
variables: parsedResult.variables,
}
}
async predicate(args: {
request: Request
parsedResult: GraphQLRequestParsedResult
}): Promise {
if (args.parsedResult.operationType === undefined) {
return false
}
if (!args.parsedResult.operationName && this.info.operationType !== 'all') {
const publicUrl = toPublicUrl(args.request.url)
devUtils.warn(`\
Failed to intercept a GraphQL request at "${args.request.method} ${publicUrl}": anonymous GraphQL operations are not supported.
Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`)
return false
}
const hasMatchingOperationType =
this.info.operationType === 'all' ||
args.parsedResult.operationType === this.info.operationType
/**
* Check if the operation name matches the outgoing GraphQL request.
* @note Unlike the HTTP handler, the custom predicate functions are invoked
* during predicate, not parsing, because GraphQL request parsing happens first,
* and non-GraphQL requests are filtered out automatically.
*/
const hasMatchingOperationName = await this.matchOperationName({
request: args.request,
parsedResult: args.parsedResult,
})
return (
args.parsedResult.match.matches &&
hasMatchingOperationType &&
hasMatchingOperationName
)
}
public async run(args: {
request: StrictRequest
requestId: string
resolutionContext?: ResponseResolutionContext
}): Promise | null> {
const result = await super.run(args)
if (result?.response == null) {
return result
}
if (!(kDefaultContentType in result.response)) {
return result
}
const acceptedMimeTypes = getAllAcceptedMimeTypes(
args.request.headers.get('accept'),
)
if (acceptedMimeTypes.length === 0) {
return result
}
const graphqlResponseIndex = acceptedMimeTypes.indexOf(
'application/graphql-response+json',
)
const jsonIndex = acceptedMimeTypes.indexOf('application/json')
/**
* Use the "application/graphql-response+json" response content type
* only when the client accepts it AND prefers it over "application/json"
* (i.e. it appears earlier in the precedence-sorted list, or "application/json"
* is not listed at all).
* @see https://github.com/graphql/graphql-over-http/blob/4d1df1fb829ec2dd3ecbf3c6aa4025bd356c270d/spec/GraphQLOverHTTP.md#accept
*/
if (
graphqlResponseIndex !== -1 &&
(jsonIndex === -1 || graphqlResponseIndex <= jsonIndex)
) {
result.response.headers.set(
'content-type',
'application/graphql-response+json',
)
}
return result
}
private async matchOperationName(args: {
request: Request
parsedResult: GraphQLRequestParsedResult
}): Promise {
if (typeof this.info.operationName === 'function') {
const customPredicateResult = await this.info.operationName({
request: args.request,
...this.extendResolverArgs({
request: args.request,
parsedResult: args.parsedResult,
}),
})
/**
* @note Keep the { matches } signature in case we decide to support path parameters
* in GraphQL handlers. If that happens, the custom predicate would have to be moved
* to the parsing phase, the same as we have for the HttpHandler, and the user will
* have a possibility to return parsed path parameters from the custom predicate.
*/
return typeof customPredicateResult === 'boolean'
? customPredicateResult
: customPredicateResult.matches
}
if (this.info.operationName instanceof RegExp) {
return this.info.operationName.test(args.parsedResult.operationName || '')
}
return args.parsedResult.operationName === this.info.operationName
}
protected extendResolverArgs(args: {
request: Request
parsedResult: GraphQLRequestParsedResult
}) {
return {
query: args.parsedResult.query || '',
operationType: args.parsedResult.operationType!,
operationName: args.parsedResult.operationName || '',
variables: args.parsedResult.variables || {},
cookies: args.parsedResult.cookies,
}
}
async log(args: {
request: Request
response: Response
parsedResult: GraphQLRequestParsedResult
}) {
const loggedRequest = await serializeRequest(args.request)
const loggedResponse = await serializeResponse(args.response)
const statusColor = getStatusCodeColor(loggedResponse.status)
const requestInfo = args.parsedResult.operationName
? `${args.parsedResult.operationType} ${args.parsedResult.operationName}`
: `anonymous ${args.parsedResult.operationType}`
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp()} ${requestInfo} (%c${loggedResponse.status} ${
loggedResponse.statusText
}%c)`,
),
`color:${statusColor}`,
'color:inherit',
)
// eslint-disable-next-line no-console
console.log('Request:', loggedRequest)
// eslint-disable-next-line no-console
console.log('Handler:', this)
// eslint-disable-next-line no-console
console.log('Response:', loggedResponse)
console.groupEnd()
}
}
================================================
FILE: src/core/handlers/HttpHandler.test.ts
================================================
// @vitest-environment jsdom
import { createRequestId } from '@mswjs/interceptors'
import { HttpHandler, HttpRequestResolverExtras } from './HttpHandler'
import { HttpResponse } from '..'
import { ResponseResolver } from './RequestHandler'
const resolver: ResponseResolver<
HttpRequestResolverExtras<{ userId: string }>
> = ({ params }) => {
return HttpResponse.json({ userId: params.userId })
}
describe('info', () => {
it('exposes request handler information', () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
expect(handler.info.header).toEqual('GET /user/:userId')
expect(handler.info.method).toEqual('GET')
expect(handler.info.path).toEqual('/user/:userId')
expect(handler.isUsed).toBe(false)
})
})
describe('parse', () => {
it('parses a URL given a matching request', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const request = new Request(new URL('/user/abc-123', location.href))
expect(await handler.parse({ request })).toEqual({
match: {
matches: true,
params: {
userId: 'abc-123',
},
},
cookies: {},
})
})
it('parses a URL and ignores the request method', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const request = new Request(new URL('/user/def-456', location.href), {
method: 'POST',
})
expect(await handler.parse({ request })).toEqual({
match: {
matches: true,
params: {
userId: 'def-456',
},
},
cookies: {},
})
})
it('returns negative match result given a non-matching request', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const request = new Request(new URL('/login', location.href))
expect(await handler.parse({ request })).toEqual({
match: {
matches: false,
params: {},
},
cookies: {},
})
})
})
describe('predicate', () => {
it('returns true given a matching request', async () => {
const handler = new HttpHandler('POST', '/login', resolver)
const request = new Request(new URL('/login', location.href), {
method: 'POST',
})
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
})
it('supports RegExp as the request method', async () => {
const handler = new HttpHandler(/.+/, '/login', resolver)
const requests = [
new Request(new URL('/login', location.href)),
new Request(new URL('/login', location.href), { method: 'POST' }),
new Request(new URL('/login', location.href), { method: 'DELETE' }),
]
for (const request of requests) {
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
}
})
it('returns false given a non-matching request', async () => {
const handler = new HttpHandler('POST', '/login', resolver)
const request = new Request(new URL('/user/abc-123', location.href))
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(false)
})
it('supports custom predicate function', async () => {
const handler = new HttpHandler(
'GET',
({ request }) => {
return new URL(request.url).searchParams.get('a') === '1'
},
resolver,
)
{
const request = new Request(new URL('/login?a=1', location.href))
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(true)
}
{
const request = new Request(new URL('/login', location.href))
await expect(
handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).resolves.toBe(false)
}
})
})
describe('test', () => {
it('returns true given a matching request', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const firstTest = await handler.test({
request: new Request(new URL('/user/abc-123', location.href)),
})
const secondTest = await handler.test({
request: new Request(new URL('/user/def-456', location.href)),
})
expect(firstTest).toBe(true)
expect(secondTest).toBe(true)
})
it('returns false given a non-matching request', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const firstTest = await handler.test({
request: new Request(new URL('/login', location.href)),
})
const secondTest = await handler.test({
request: new Request(new URL('/user/', location.href)),
})
const thirdTest = await handler.test({
request: new Request(new URL('/user/abc-123/extra', location.href)),
})
expect(firstTest).toBe(false)
expect(secondTest).toBe(false)
expect(thirdTest).toBe(false)
})
})
describe('run', () => {
it('returns a mocked response given a matching request', async () => {
const handler = new HttpHandler('GET', '/user/:userId', resolver)
const request = new Request(new URL('/user/abc-123', location.href))
const requestId = createRequestId()
const result = await handler.run({ request, requestId })
expect(result!.handler).toEqual(handler)
expect(result!.parsedResult).toEqual({
match: {
matches: true,
params: {
userId: 'abc-123',
},
},
cookies: {},
})
expect(result!.request.method).toBe('GET')
expect(result!.request.url).toBe('http://localhost/user/abc-123')
expect(result!.response?.status).toBe(200)
expect(result!.response?.statusText).toBe('OK')
await expect(result?.response?.json()).resolves.toEqual({
userId: 'abc-123',
})
})
it('returns null given a non-matching request', async () => {
const handler = new HttpHandler('POST', '/login', resolver)
const result = await handler.run({
request: new Request(new URL('/users', location.href)),
requestId: createRequestId(),
})
expect(result).toBeNull()
})
it('returns an empty "params" object given request with no URL parameters', async () => {
const handler = new HttpHandler('GET', '/users', resolver)
const result = await handler.run({
request: new Request(new URL('/users', location.href)),
requestId: createRequestId(),
})
expect(result?.parsedResult?.match?.params).toEqual({})
})
it('exhausts resolver until its generator completes', async () => {
const handler = new HttpHandler('GET', '/users', function* () {
let count = 0
while (count < 5) {
count += 1
yield HttpResponse.text('pending')
}
return HttpResponse.text('complete')
})
const run = async () => {
const result = await handler.run({
request: new Request(new URL('/users', location.href)),
requestId: createRequestId(),
})
return result?.response?.text()
}
await expect(run()).resolves.toBe('pending')
await expect(run()).resolves.toBe('pending')
await expect(run()).resolves.toBe('pending')
await expect(run()).resolves.toBe('pending')
await expect(run()).resolves.toBe('pending')
await expect(run()).resolves.toBe('complete')
await expect(run()).resolves.toBe('complete')
})
})
================================================
FILE: src/core/handlers/HttpHandler.ts
================================================
import { ResponseResolutionContext } from '../utils/executeHandlers'
import { devUtils } from '../utils/internal/devUtils'
import { isStringEqual } from '../utils/internal/isStringEqual'
import { getStatusCodeColor } from '../utils/logging/getStatusCodeColor'
import { getTimestamp } from '../utils/logging/getTimestamp'
import { serializeRequest } from '../utils/logging/serializeRequest'
import { serializeResponse } from '../utils/logging/serializeResponse'
import {
matchRequestUrl,
Match,
Path,
PathParams,
} from '../utils/matching/matchRequestUrl'
import { toPublicUrl } from '../utils/request/toPublicUrl'
import { getAllRequestCookies } from '../utils/request/getRequestCookies'
import { cleanUrl } from '../utils/url/cleanUrl'
import {
RequestHandler,
RequestHandlerDefaultInfo,
RequestHandlerOptions,
ResponseResolver,
} from './RequestHandler'
export type HttpHandlerMethod = string | RegExp
export interface HttpHandlerInfo extends RequestHandlerDefaultInfo {
method: HttpHandlerMethod
path: HttpRequestPredicate
}
export enum HttpMethods {
HEAD = 'HEAD',
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
OPTIONS = 'OPTIONS',
DELETE = 'DELETE',
}
export type RequestQuery = {
[queryName: string]: string
}
export type HttpRequestParsedResult = {
match: Match
cookies: Record
}
export type HttpRequestResolverExtras = {
params: Params
cookies: Record
}
export type HttpCustomPredicate = (args: {
request: Request
cookies: Record
}) =>
| HttpCustomPredicateResult
| Promise>
export type HttpCustomPredicateResult =
| boolean
| {
matches: boolean
params: Params
}
export type HttpRequestPredicate =
| Path
| HttpCustomPredicate
/**
* Request handler for HTTP requests.
* Provides request matching based on method and URL.
*/
export class HttpHandler extends RequestHandler<
HttpHandlerInfo,
HttpRequestParsedResult,
HttpRequestResolverExtras
> {
constructor(
method: HttpHandlerMethod,
predicate: HttpRequestPredicate,
resolver: ResponseResolver, any, any>,
options?: RequestHandlerOptions,
) {
const displayPath =
typeof predicate === 'function' ? '[custom predicate]' : predicate
super({
info: {
header: `${method}${displayPath ? ` ${displayPath}` : ''}`,
path: predicate,
method,
},
resolver,
options,
})
this.checkRedundantQueryParameters()
}
private checkRedundantQueryParameters() {
const { method, path } = this.info
if (!path || path instanceof RegExp || typeof path === 'function') {
return
}
const url = cleanUrl(path)
// Bypass request handler URLs that have no redundant characters.
if (url === path) {
return
}
devUtils.warn(
`Found a redundant usage of query parameters in the request handler URL for "${method} ${path}". Please match against a path instead and access query parameters using "new URL(request.url).searchParams" instead. Learn more: https://mswjs.io/docs/http/intercepting-requests#querysearch-parameters`,
)
}
async parse(args: {
request: Request
resolutionContext?: ResponseResolutionContext
}) {
const url = new URL(args.request.url)
const cookies = getAllRequestCookies(args.request)
/**
* Handle custom predicate functions.
* @note Invoke this during parsing so the user can parse the path parameters
* manually. Otherwise, `params` is always an empty object, which isn't nice.
*/
if (typeof this.info.path === 'function') {
const customPredicateResult = await this.info.path({
request: args.request,
cookies,
})
const match =
typeof customPredicateResult === 'boolean'
? {
matches: customPredicateResult,
params: {},
}
: customPredicateResult
return {
match,
cookies,
}
}
const match = this.info.path
? matchRequestUrl(url, this.info.path, args.resolutionContext?.baseUrl)
: { matches: false, params: {} }
return {
match,
cookies,
}
}
async predicate(args: {
request: Request
parsedResult: HttpRequestParsedResult
resolutionContext?: ResponseResolutionContext
}) {
const hasMatchingMethod = this.matchMethod(args.request.method)
const hasMatchingUrl = args.parsedResult.match.matches
return hasMatchingMethod && hasMatchingUrl
}
private matchMethod(actualMethod: string): boolean {
return this.info.method instanceof RegExp
? this.info.method.test(actualMethod)
: isStringEqual(this.info.method, actualMethod)
}
protected extendResolverArgs(args: {
request: Request
parsedResult: HttpRequestParsedResult
}) {
return {
params: args.parsedResult.match?.params || {},
cookies: args.parsedResult.cookies,
}
}
async log(args: { request: Request; response: Response }) {
const publicUrl = toPublicUrl(args.request.url)
const loggedRequest = await serializeRequest(args.request)
const loggedResponse = await serializeResponse(args.response)
const statusColor = getStatusCodeColor(loggedResponse.status)
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp()} ${args.request.method} ${publicUrl} (%c${
loggedResponse.status
} ${loggedResponse.statusText}%c)`,
),
`color:${statusColor}`,
'color:inherit',
)
// eslint-disable-next-line no-console
console.log('Request', loggedRequest)
// eslint-disable-next-line no-console
console.log('Handler:', this)
// eslint-disable-next-line no-console
console.log('Response', loggedResponse)
console.groupEnd()
}
}
================================================
FILE: src/core/handlers/RequestHandler.ts
================================================
import { getCallFrame } from '../utils/internal/getCallFrame'
import {
AsyncIterable,
Iterable,
isIterable,
} from '../utils/internal/isIterable'
import type { ResponseResolutionContext } from '../utils/executeHandlers'
import type { MaybePromise } from '../typeUtils'
import {
StrictRequest,
HttpResponse,
DefaultUnsafeFetchResponse,
} from '../HttpResponse'
import type { HandlerKind } from './common'
import type { GraphQLRequestBody } from './GraphQLHandler'
export type DefaultRequestMultipartBody = Record<
string,
string | File | Array
>
export type DefaultBodyType =
| Record
| DefaultRequestMultipartBody
| string
| number
| boolean
| null
| undefined
export type JsonBodyType =
| Record
| string
| number
| boolean
| null
| undefined
export interface RequestHandlerDefaultInfo {
header: string
}
export interface RequestHandlerInternalInfo {
callFrame?: string
}
export type ResponseResolverReturnType<
ResponseBodyType extends DefaultBodyType = undefined,
> =
// If ResponseBodyType is a union and one of the types is `undefined`,
// allow plain Response as the type.
| ([ResponseBodyType] extends [undefined]
? Response
: /**
* Treat GraphQL response body type as a special case.
* For esome reason, making the default HttpResponse | DefaultUnsafeFetchResponse
* union breaks the body type inference for HTTP requests.
* @see https://github.com/mswjs/msw/issues/2130
*/
ResponseBodyType extends GraphQLRequestBody
? HttpResponse | DefaultUnsafeFetchResponse
: HttpResponse)
| undefined
| void
export type MaybeAsyncResponseResolverReturnType<
ResponseBodyType extends DefaultBodyType,
> = MaybePromise>
export type AsyncResponseResolverReturnType<
ResponseBodyType extends DefaultBodyType,
> = MaybePromise<
| ResponseResolverReturnType
| Iterable<
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType
>
| AsyncIterable<
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType
>
>
export type ResponseResolverInfo<
ResolverExtraInfo extends Record,
RequestBodyType extends DefaultBodyType = DefaultBodyType,
> = {
request: StrictRequest
requestId: string
} & ResolverExtraInfo
export type ResponseResolver<
ResolverExtraInfo extends Record = Record,
RequestBodyType extends DefaultBodyType = DefaultBodyType,
ResponseBodyType extends DefaultBodyType = undefined,
> = (
info: ResponseResolverInfo,
) => AsyncResponseResolverReturnType
export interface RequestHandlerArgs<
HandlerInfo,
HandlerOptions extends RequestHandlerOptions,
> {
info: HandlerInfo
resolver: ResponseResolver
options?: HandlerOptions
}
export interface RequestHandlerOptions {
once?: boolean
}
export interface RequestHandlerExecutionResult<
ParsedResult extends object | undefined,
> {
handler: RequestHandler
parsedResult?: ParsedResult
request: Request
requestId: string
response?: Response
}
export abstract class RequestHandler<
HandlerInfo extends RequestHandlerDefaultInfo = RequestHandlerDefaultInfo,
ParsedResult extends Record | undefined = any,
ResolverExtras extends Record = any,
HandlerOptions extends RequestHandlerOptions = RequestHandlerOptions,
> {
static cache = new WeakMap<
StrictRequest,
StrictRequest
>()
private readonly __kind: HandlerKind
public info: HandlerInfo & RequestHandlerInternalInfo
/**
* Indicates whether this request handler has been used
* (its resolver has successfully executed).
*/
public isUsed: boolean
protected resolver: ResponseResolver
private resolverIterator?:
| Iterator<
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType
>
| AsyncIterator<
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType,
MaybeAsyncResponseResolverReturnType
>
private resolverIteratorResult?: Response | HttpResponse
private options?: HandlerOptions
constructor(args: RequestHandlerArgs) {
this.resolver = args.resolver
this.options = args.options
const callFrame = getCallFrame(new Error())
this.info = {
...args.info,
callFrame,
}
this.isUsed = false
this.__kind = 'RequestHandler'
}
/**
* Determine if the intercepted request should be mocked.
*/
abstract predicate(args: {
request: Request
parsedResult: ParsedResult
resolutionContext?: ResponseResolutionContext
}): boolean | Promise
/**
* Print out the successfully handled request.
*/
abstract log(args: {
request: Request
response: Response
parsedResult: ParsedResult
}): void
/**
* Parse the intercepted request to extract additional information from it.
* Parsed result is then exposed to other methods of this request handler.
*/
async parse(_args: {
request: Request
resolutionContext?: ResponseResolutionContext
}): Promise {
return {} as ParsedResult
}
/**
* Test if this handler matches the given request.
*
* This method is not used internally but is exposed
* as a convenience method for consumers writing custom
* handlers.
*/
public async test(args: {
request: Request
resolutionContext?: ResponseResolutionContext
}): Promise {
const parsedResult = await this.parse({
request: args.request,
resolutionContext: args.resolutionContext,
})
return this.predicate({
request: args.request,
parsedResult,
resolutionContext: args.resolutionContext,
})
}
protected extendResolverArgs(_args: {
request: Request
parsedResult: ParsedResult
}): ResolverExtras {
return {} as ResolverExtras
}
// Clone the request instance before it's passed to the handler phases
// and the response resolver so we can always read it for logging.
// We only clone it once per request to avoid unnecessary overhead.
private cloneRequestOrGetFromCache(
request: StrictRequest,
): StrictRequest {
const existingClone = RequestHandler.cache.get(request)
if (typeof existingClone !== 'undefined') {
return existingClone
}
const clonedRequest = request.clone()
RequestHandler.cache.set(request, clonedRequest)
return clonedRequest
}
/**
* Execute this request handler and produce a mocked response
* using the given resolver function.
*/
public async run(args: {
request: StrictRequest
requestId: string
resolutionContext?: ResponseResolutionContext
}): Promise | null> {
if (this.isUsed && this.options?.once) {
return null
}
// Clone the request.
// If this is the first time MSW handles this request, a fresh clone
// will be created and cached. Upon further handling of the same request,
// the request clone from the cache will be reused to prevent abundant
// "abort" listeners and save up resources on cloning.
const requestClone = this.cloneRequestOrGetFromCache(args.request)
const parsedResult = await this.parse({
request: args.request,
resolutionContext: args.resolutionContext,
})
const shouldInterceptRequest = await this.predicate({
request: args.request,
parsedResult,
resolutionContext: args.resolutionContext,
})
if (!shouldInterceptRequest) {
return null
}
// Re-check isUsed, in case another request hit this handler while we were
// asynchronously parsing the request.
if (this.isUsed && this.options?.once) {
return null
}
// Preemptively mark the handler as used.
// Generators will undo this because only when the resolver reaches the
// "done" state of the generator that it considers the handler used.
this.isUsed = true
// Create a response extraction wrapper around the resolver
// since it can be both an async function and a generator.
const executeResolver = this.wrapResolver(this.resolver)
const resolverExtras = this.extendResolverArgs({
request: args.request,
parsedResult,
})
const mockedResponsePromise = (
executeResolver({
...resolverExtras,
requestId: args.requestId,
request: args.request,
}) as Promise
).catch((errorOrResponse) => {
// Allow throwing a Response instance in a response resolver.
if (errorOrResponse instanceof Response) {
return errorOrResponse
}
// Otherwise, throw the error as-is.
throw errorOrResponse
})
const mockedResponse = await mockedResponsePromise
const executionResult = this.createExecutionResult({
// Pass the cloned request to the result so that logging
// and other consumers could read its body once more.
request: requestClone,
requestId: args.requestId,
response: mockedResponse,
parsedResult,
})
return executionResult
}
private wrapResolver(
resolver: ResponseResolver,
): ResponseResolver {
return async (info): Promise> => {
if (!this.resolverIterator) {
const result = await resolver(info)
if (!isIterable(result)) {
return result
}
this.resolverIterator =
Symbol.iterator in result
? result[Symbol.iterator]()
: result[Symbol.asyncIterator]()
}
// Opt-out from marking this handler as used.
this.isUsed = false
const { done, value } = await this.resolverIterator.next()
const nextResponse = await value
if (nextResponse) {
this.resolverIteratorResult = nextResponse.clone()
}
if (done) {
// A one-time generator resolver stops affecting the network
// only after it's been completely exhausted.
this.isUsed = true
// Clone the previously stored response so it can be read
// when receiving it repeatedly from the "done" generator.
return this.resolverIteratorResult?.clone()
}
return nextResponse
}
}
private createExecutionResult(args: {
request: Request
requestId: string
parsedResult: ParsedResult
response?: Response
}): RequestHandlerExecutionResult {
return {
handler: this,
request: args.request,
requestId: args.requestId,
response: args.response,
parsedResult: args.parsedResult,
}
}
}
================================================
FILE: src/core/handlers/WebSocketHandler.test.ts
================================================
import { WebSocketHandler } from './WebSocketHandler'
describe('parse', () => {
it('matches an exact url', () => {
expect(
new WebSocketHandler('ws://localhost:3000').parse({
url: new URL('ws://localhost:3000'),
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
})
it('ignores trailing slash', () => {
expect(
new WebSocketHandler('ws://localhost:3000').parse({
url: new URL('ws://localhost:3000/'),
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
expect(
new WebSocketHandler('ws://localhost:3000/').parse({
url: new URL('ws://localhost:3000/'),
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
})
it('supports path parameters', () => {
expect(
new WebSocketHandler('ws://localhost:3000/:serviceName').parse({
url: new URL('ws://localhost:3000/auth'),
}),
).toEqual({
match: {
matches: true,
params: {
serviceName: 'auth',
},
},
})
})
it('ignores "/socket.io/" prefix in the client url', () => {
expect(
new WebSocketHandler('ws://localhost:3000').parse({
url: new URL(
'ws://localhost:3000/socket.io/?EIO=4&transport=websocket',
),
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
expect(
new WebSocketHandler('ws://localhost:3000/non-matching').parse({
url: new URL(
'ws://localhost:3000/socket.io/?EIO=4&transport=websocket',
),
}),
).toEqual({
match: {
matches: false,
params: {},
},
})
})
it('preserves non-prefix "/socket.io/" path segment', () => {
/**
* @note It is highly unlikely but we still shouldn't modify the
* WebSocket client URL if it contains a user-defined "socket.io" segment.
*/
expect(
new WebSocketHandler('ws://localhost:3000/clients/socket.io/123').parse({
url: new URL('ws://localhost:3000/clients/socket.io/123'),
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
expect(
new WebSocketHandler('ws://localhost:3000').parse({
url: new URL('ws://localhost:3000/clients/socket.io/123'),
}),
).toEqual({
match: {
matches: false,
params: {},
},
})
})
it('supports a custom resolution context (base url)', () => {
expect(
new WebSocketHandler('/api/ws').parse({
url: new URL('ws://localhost:3000/api/ws'),
resolutionContext: {
baseUrl: 'ws://localhost:3000/',
},
}),
).toEqual({
match: {
matches: true,
params: {},
},
})
})
})
================================================
FILE: src/core/handlers/WebSocketHandler.ts
================================================
import { Emitter } from 'strict-event-emitter'
import { createRequestId, resolveWebSocketUrl } from '@mswjs/interceptors'
import type {
WebSocketClientConnectionProtocol,
WebSocketConnectionData,
WebSocketServerConnectionProtocol,
} from '@mswjs/interceptors/WebSocket'
import {
type Match,
type Path,
type PathParams,
matchRequestUrl,
} from '../utils/matching/matchRequestUrl'
import { getCallFrame } from '../utils/internal/getCallFrame'
import type { HandlerKind } from './common'
type WebSocketHandlerParsedResult = {
match: Match
}
export type WebSocketHandlerEventMap = {
connection: [args: WebSocketHandlerConnection]
}
export interface WebSocketHandlerConnection {
client: WebSocketClientConnectionProtocol
server: WebSocketServerConnectionProtocol
info: WebSocketConnectionData['info']
params: PathParams
}
export interface WebSocketResolutionContext {
baseUrl?: string
}
export const kEmitter = Symbol('kEmitter')
export const kSender = Symbol('kSender')
const kStopPropagationPatched = Symbol('kStopPropagationPatched')
const KOnStopPropagation = Symbol('KOnStopPropagation')
export class WebSocketHandler {
private readonly __kind: HandlerKind
public id: string
public callFrame?: string
protected [kEmitter]: Emitter
constructor(protected readonly url: Path) {
this.id = createRequestId()
this[kEmitter] = new Emitter()
this.callFrame = getCallFrame(new Error())
this.__kind = 'EventHandler'
}
public parse(args: {
url: URL
resolutionContext?: WebSocketResolutionContext
}): WebSocketHandlerParsedResult {
const clientUrl = new URL(args.url)
// Resolve the WebSocket handler path:
// - Plain string URLs resolved as per the specification (via Interceptors).
// - String URLs starting with a wildcard are preserved (prepending a scheme there will break them).
// - RegExp paths are preserved.
const resolvedHandlerUrl =
this.url instanceof RegExp || this.url.startsWith('*')
? this.url
: this.#resolveWebSocketUrl(this.url, args.resolutionContext?.baseUrl)
/**
* @note Remove the Socket.IO path prefix from the WebSocket
* client URL. This is an exception to keep the users from
* including the implementation details in their handlers.
*/
clientUrl.pathname = clientUrl.pathname.replace(/^\/socket.io\//, '/')
const match = matchRequestUrl(
clientUrl,
resolvedHandlerUrl,
args.resolutionContext?.baseUrl,
)
return {
match,
}
}
public predicate(args: {
url: URL
parsedResult: WebSocketHandlerParsedResult
}): boolean {
return args.parsedResult.match.matches
}
public async run(
connection: Omit,
resolutionContext?: WebSocketResolutionContext,
): Promise {
const parsedResult = this.parse({
url: connection.client.url,
resolutionContext,
})
if (!this.predicate({ url: connection.client.url, parsedResult })) {
return false
}
const resolvedConnection: WebSocketHandlerConnection = {
...connection,
params: parsedResult.match.params || {},
}
return this.connect(resolvedConnection)
}
protected connect(connection: WebSocketHandlerConnection): boolean {
// Support `event.stopPropagation()` for various client/server events.
connection.client.addEventListener(
'message',
createStopPropagationListener(this),
)
connection.client.addEventListener(
'close',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'open',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'message',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'error',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'close',
createStopPropagationListener(this),
)
// Emit the connection event on the handler.
// This is what the developer adds listeners for.
return this[kEmitter].emit('connection', connection)
}
#resolveWebSocketUrl(url: string, baseUrl?: string): string {
const resolvedUrl = resolveWebSocketUrl(
baseUrl
? /**
* @note Resolve against the base URL preemtively because `resolveWebSocketUrl` only
* resolves against `location.href`, which is missing in Node.js. Base URL allows
* the handler to accept a relative URL in Node.js.
*/
new URL(url, baseUrl)
: url,
)
/**
* @note Omit the trailing slash.
* While the browser always produces a trailing slash at the end of a WebSocket URL,
* having it in as the handler's predicate would mean it is *required* in the actual URL.
*/
return resolvedUrl.replace(/\/$/, '')
}
}
function createStopPropagationListener(handler: WebSocketHandler) {
return function stopPropagationListener(event: Event) {
const propagationStoppedAt = Reflect.get(event, 'kPropagationStoppedAt') as
| string
| undefined
if (propagationStoppedAt && handler.id !== propagationStoppedAt) {
event.stopImmediatePropagation()
return
}
Object.defineProperty(event, KOnStopPropagation, {
value(this: WebSocketHandler) {
Object.defineProperty(event, 'kPropagationStoppedAt', {
value: handler.id,
})
},
configurable: true,
})
// Since the same event instance is shared between all client/server objects,
// make sure to patch its `stopPropagation` method only once.
if (!Reflect.get(event, kStopPropagationPatched)) {
event.stopPropagation = new Proxy(event.stopPropagation, {
apply: (target, thisArg, args) => {
Reflect.get(event, KOnStopPropagation)?.call(handler)
return Reflect.apply(target, thisArg, args)
},
})
Object.defineProperty(event, kStopPropagationPatched, {
value: true,
// If something else attempts to redefine this, throw.
configurable: false,
})
}
}
}
================================================
FILE: src/core/handlers/common.ts
================================================
export type HandlerKind = 'RequestHandler' | 'EventHandler'
================================================
FILE: src/core/http.test.ts
================================================
import { http } from './http'
test('exports all REST API methods', () => {
expect(http).toBeDefined()
expect(Object.keys(http)).toEqual([
'all',
'head',
'get',
'post',
'put',
'delete',
'patch',
'options',
])
})
================================================
FILE: src/core/http.ts
================================================
import {
DefaultBodyType,
RequestHandlerOptions,
ResponseResolver,
} from './handlers/RequestHandler'
import {
HttpMethods,
HttpHandler,
HttpRequestResolverExtras,
HttpRequestPredicate,
} from './handlers/HttpHandler'
import type { PathParams } from './utils/matching/matchRequestUrl'
export type HttpRequestHandler = <
Params extends PathParams = PathParams,
RequestBodyType extends DefaultBodyType = DefaultBodyType,
// Response body type MUST be undefined by default.
// This is how we can distinguish between a handler that
// returns plain "Response" and the one returning "HttpResponse"
// to enforce a stricter response body type.
ResponseBodyType extends DefaultBodyType = undefined,
>(
predicate: HttpRequestPredicate,
resolver: HttpResponseResolver,
options?: RequestHandlerOptions,
) => HttpHandler
export type HttpResponseResolver<
Params extends PathParams = PathParams,
RequestBodyType extends DefaultBodyType = DefaultBodyType,
ResponseBodyType extends DefaultBodyType = DefaultBodyType,
> = ResponseResolver<
HttpRequestResolverExtras,
RequestBodyType,
ResponseBodyType
>
function createHttpHandler(
method: Method,
): HttpRequestHandler {
return (predicate, resolver, options = {}) => {
return new HttpHandler(method, predicate, resolver, options)
}
}
/**
* A namespace to intercept and mock HTTP requests.
*
* @example
* http.get('/user', resolver)
* http.post('/post/:id', resolver)
*
* @see {@link https://mswjs.io/docs/api/http `http` API reference}
*/
export const http = {
all: createHttpHandler(/.+/),
head: createHttpHandler(HttpMethods.HEAD),
get: createHttpHandler(HttpMethods.GET),
post: createHttpHandler(HttpMethods.POST),
put: createHttpHandler(HttpMethods.PUT),
delete: createHttpHandler(HttpMethods.DELETE),
patch: createHttpHandler(HttpMethods.PATCH),
options: createHttpHandler(HttpMethods.OPTIONS),
}
================================================
FILE: src/core/index.ts
================================================
import { checkGlobals } from './utils/internal/checkGlobals'
export { SetupApi } from './SetupApi'
/* HTTP handlers */
export { RequestHandler } from './handlers/RequestHandler'
export { http } from './http'
export { HttpHandler, HttpMethods } from './handlers/HttpHandler'
export { graphql } from './graphql'
export { GraphQLHandler } from './handlers/GraphQLHandler'
/* WebSocket handler */
export { ws, type WebSocketLink } from './ws'
export {
WebSocketHandler,
type WebSocketHandlerEventMap,
type WebSocketHandlerConnection,
} from './handlers/WebSocketHandler'
/* Server-Sent Events */
export {
sse,
type ServerSentEventRequestHandler,
type ServerSentEventResolver,
type ServerSentEventResolverExtras,
type ServerSentEventMessage,
} from './sse'
import type { HttpHandler } from './handlers/HttpHandler'
import type { GraphQLHandler } from './handlers/GraphQLHandler'
import type { WebSocketHandler } from './handlers/WebSocketHandler'
export type AnyHandler = HttpHandler | GraphQLHandler | WebSocketHandler
/* Utils */
export { matchRequestUrl } from './utils/matching/matchRequestUrl'
export { handleRequest, type HandleRequestOptions } from './utils/handleRequest'
export {
onUnhandledRequest,
type UnhandledRequestStrategy,
type UnhandledRequestCallback,
} from './utils/request/onUnhandledRequest'
export { getResponse } from './getResponse'
export { cleanUrl } from './utils/url/cleanUrl'
/**
* Type definitions.
*/
export type { SharedOptions, LifeCycleEventsMap } from './sharedOptions'
export type {
ResponseResolver,
ResponseResolverReturnType,
AsyncResponseResolverReturnType,
RequestHandlerOptions,
DefaultBodyType,
DefaultRequestMultipartBody,
JsonBodyType,
ResponseResolverInfo,
} from './handlers/RequestHandler'
export type {
RequestQuery,
HttpRequestParsedResult,
HttpHandlerInfo,
HttpRequestResolverExtras,
HttpHandlerMethod,
HttpCustomPredicate,
} from './handlers/HttpHandler'
export type { HttpRequestHandler, HttpResponseResolver } from './http'
export type {
GraphQLQuery,
GraphQLVariables,
GraphQLRequestBody,
GraphQLResponseBody,
GraphQLJsonRequestBody,
GraphQLOperationType,
GraphQLCustomPredicate,
} from './handlers/GraphQLHandler'
export type {
GraphQLRequestHandler,
GraphQLOperationHandler,
GraphQLResponseResolver,
GraphQLLinkHandlers,
} from './graphql'
export type { WebSocketData, WebSocketEventListener } from './ws'
export type { Path, PathParams, Match } from './utils/matching/matchRequestUrl'
export type { ParsedGraphQLRequest } from './utils/internal/parseGraphQLRequest'
export type { ResponseResolutionContext } from './utils/executeHandlers'
export {
HttpResponse,
type HttpResponseInit,
type StrictRequest,
type StrictResponse,
} from './HttpResponse'
export { delay, type DelayMode } from './delay'
export { bypass } from './bypass'
export { passthrough } from './passthrough'
export { isCommonAssetRequest } from './isCommonAssetRequest'
// Validate environmental globals before executing any code.
// This ensures that the library gives user-friendly errors
// when ran in the environments that require additional polyfills
// from the end user.
checkGlobals()
================================================
FILE: src/core/isCommonAssetRequest.ts
================================================
/**
* Determines if the given request is a static asset request.
* Useful when deciding which unhandled requests to ignore.
* @note Despite being ignored, you can still intercept and mock
* static assets by creating request handlers for them.
*
* @example
* import { isCommonAssetRequest } from 'msw'
*
* await worker.start({
* onUnhandledRequest(request, print) {
* if (!isCommonAssetRequest(request)) {
* print.warning()
* }
* }
* })
*/
export function isCommonAssetRequest(request: Request): boolean {
const url = new URL(request.url)
// Ignore certain protocols.
if (url.protocol === 'file:') {
return true
}
// Ignore static assets hosts.
if (/(fonts\.googleapis\.com)/.test(url.hostname)) {
return true
}
// Ignore node modules served over HTTP.
if (/node_modules/.test(url.pathname)) {
return true
}
// Ignore internal Vite requests, like "/@vite/client".
if (url.pathname.includes('@vite')) {
return true
}
// Ignore common static assets.
return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(
url.pathname,
)
}
================================================
FILE: src/core/passthrough.test.ts
================================================
// @vitest-environment node
import { passthrough } from './passthrough'
it('creates a 302 response with the intention header', () => {
const response = passthrough()
expect(response).toBeInstanceOf(Response)
expect(response.status).toBe(302)
expect(response.statusText).toBe('Passthrough')
expect(response.headers.get('x-msw-intention')).toBe('passthrough')
})
================================================
FILE: src/core/passthrough.ts
================================================
import type { HttpResponse } from './HttpResponse'
/**
* Performs the intercepted request as-is.
*
* This stops request handler lookup so no other handlers
* can affect this request past this point.
* Unlike `bypass()`, this will not trigger an additional request.
*
* @example
* http.get('/resource', () => {
* return passthrough()
* })
*
* @see {@link https://mswjs.io/docs/api/passthrough `passthrough()` API reference}
*/
export function passthrough(): HttpResponse {
return new Response(null, {
status: 302,
statusText: 'Passthrough',
headers: {
'x-msw-intention': 'passthrough',
},
}) as HttpResponse
}
================================================
FILE: src/core/sharedOptions.ts
================================================
import type { Emitter } from 'strict-event-emitter'
import type { UnhandledRequestStrategy } from './utils/request/onUnhandledRequest'
export interface SharedOptions {
/**
* Specifies how to react to a request that has no corresponding
* request handler. Warns on unhandled requests by default.
*
* @example worker.start({ onUnhandledRequest: 'bypass' })
* @example worker.start({ onUnhandledRequest: 'warn' })
* @example server.listen({ onUnhandledRequest: 'error' })
*/
onUnhandledRequest?: UnhandledRequestStrategy
}
export type LifeCycleEventsMap = {
'request:start': [
args: {
request: Request
requestId: string
},
]
'request:match': [
args: {
request: Request
requestId: string
},
]
'request:unhandled': [
args: {
request: Request
requestId: string
},
]
'request:end': [
args: {
request: Request
requestId: string
},
]
'response:mocked': [
args: {
response: Response
request: Request
requestId: string
},
]
'response:bypass': [
args: {
response: Response
request: Request
requestId: string
},
]
unhandledException: [
args: {
error: Error
request: Request
requestId: string
},
]
}
export type LifeCycleEventEmitter<
EventsMap extends Record,
> = Pick, 'on' | 'removeListener' | 'removeAllListeners'>
================================================
FILE: src/core/sse.ts
================================================
import { invariant } from 'outvariant'
import { Emitter } from 'strict-event-emitter'
import type { ResponseResolver } from './handlers/RequestHandler'
import {
HttpHandler,
type HttpRequestResolverExtras,
type HttpRequestParsedResult,
} from './handlers/HttpHandler'
import type { ResponseResolutionContext } from '~/core/utils/executeHandlers'
import type { Path, PathParams } from './utils/matching/matchRequestUrl'
import { delay } from './delay'
import { getTimestamp } from './utils/logging/getTimestamp'
import { devUtils } from './utils/internal/devUtils'
import { colors } from './ws/utils/attachWebSocketLogger'
import { toPublicUrl } from './utils/request/toPublicUrl'
type EventMapConstraint = {
message?: unknown
[key: string]: unknown
[key: symbol | number]: never
}
export type ServerSentEventResolverExtras<
EventMap extends EventMapConstraint,
Params extends PathParams,
> = HttpRequestResolverExtras & {
client: ServerSentEventClient
server: ServerSentEventServer
}
export type ServerSentEventResolver<
EventMap extends EventMapConstraint,
Params extends PathParams,
> = ResponseResolver, any, any>
export type ServerSentEventRequestHandler = <
EventMap extends EventMapConstraint = { message: unknown },
Params extends PathParams = PathParams,
RequestPath extends Path = Path,
>(
path: RequestPath,
resolver: ServerSentEventResolver,
) => HttpHandler
export type ServerSentEventMessage<
EventMap extends EventMapConstraint = { message: unknown },
> =
| ToEventDiscriminatedUnion
| {
id?: never
event?: never
data?: never
retry: number
}
/**
* Intercept Server-Sent Events (SSE).
*
* @example
* sse('http://localhost:4321', ({ client }) => {
* client.send({ data: 'hello world' })
* })
*
* @see {@link https://mswjs.io/docs/sse/ Mocking Server-Sent Events}
* @see {@link https://mswjs.io/docs/api/sse `sse()` API reference}
*/
export const sse: ServerSentEventRequestHandler = (path, resolver) => {
return new ServerSentEventHandler(path, resolver)
}
const SSE_RESPONSE_INIT: ResponseInit = {
headers: {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
},
}
class ServerSentEventHandler<
EventMap extends EventMapConstraint,
> extends HttpHandler {
#emitter: Emitter
constructor(path: Path, resolver: ServerSentEventResolver) {
invariant(
typeof EventSource !== 'undefined',
'Failed to construct a Server-Sent Event handler for path "%s": the EventSource API is not supported in this environment',
path,
)
super('GET', path, async (info) => {
const stream = new ReadableStream({
start: async (controller) => {
const client = new ServerSentEventClient({
controller,
emitter: this.#emitter,
})
const server = new ServerSentEventServer({
request: info.request,
client,
})
await resolver({
...info,
client,
server,
})
},
})
return new Response(stream, SSE_RESPONSE_INIT)
})
this.#emitter = new Emitter()
}
async predicate(args: {
request: Request
parsedResult: HttpRequestParsedResult
resolutionContext?: ResponseResolutionContext
}) {
if (args.request.headers.get('accept') !== 'text/event-stream') {
return false
}
const matches = await super.predicate(args)
if (matches && !args.resolutionContext?.quiet) {
/**
* @note Log the intercepted request early.
* Normally, the `this.log()` method is called when the handler returns a response.
* For SSE, call that method earlier so the logs are in correct order.
*/
await super.log({
request: args.request,
/**
* @note Construct a placeholder response since SSE response
* is being streamed and cannot be cloned/consumed for logging.
*/
response: new Response('[streaming]', SSE_RESPONSE_INIT),
})
this.#attachClientLogger(args.request, this.#emitter)
}
return matches
}
async log(_args: { request: Request; response: Response }): Promise {
/**
* @note Skip the default `this.log()` logic so that when this handler is logged
* upon handling the request, nothing is printed (we log SSE requests early).
*/
return
}
#attachClientLogger(
request: Request,
emitter: Emitter,
): void {
const publicUrl = toPublicUrl(request.url)
/* eslint-disable no-console */
emitter.on('message', (payload) => {
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp()} SSE %s %c⇣%c ${payload.event}`,
),
publicUrl,
`color:${colors.mocked}`,
'color:inherit',
)
console.log(payload.frames)
console.groupEnd()
})
emitter.on('error', () => {
console.groupCollapsed(
devUtils.formatMessage(`${getTimestamp()} SSE %s %c\u00D7%c error`),
publicUrl,
`color: ${colors.system}`,
'color:inherit',
)
console.log('Handler:', this)
console.groupEnd()
})
emitter.on('close', () => {
console.groupCollapsed(
devUtils.formatMessage(`${getTimestamp()} SSE %s %c■%c close`),
publicUrl,
`colors:${colors.system}`,
'color:inherit',
)
console.log('Handler:', this)
console.groupEnd()
})
/* eslint-enable no-console */
}
}
type Values = T[keyof T]
type Identity = { [K in keyof T]: T[K] } & unknown
type ToEventDiscriminatedUnion = Values<{
[K in keyof T]: Identity<
(K extends 'message'
? {
id?: string
event?: K
data?: T[K]
retry?: never
}
: {
id?: string
event: K
data?: T[K]
retry?: never
}) &
// Make the `data` field conditionally required through an intersection.
(undefined extends T[K] ? unknown : { data: unknown })
>
}>
type ServerSentEventClientEventMap = {
message: [
payload: {
id?: string
event: string
data?: unknown
frames: Array
},
]
error: []
close: []
}
class ServerSentEventClient<
EventMap extends EventMapConstraint = { message: unknown },
> {
#encoder: TextEncoder
#controller: ReadableStreamDefaultController
#emitter: Emitter
constructor(args: {
controller: ReadableStreamDefaultController
emitter: Emitter
}) {
this.#encoder = new TextEncoder()
this.#controller = args.controller
this.#emitter = args.emitter
}
/**
* Sends the given payload to the intercepted `EventSource`.
*/
public send(payload: ServerSentEventMessage): void {
if ('retry' in payload && payload.retry != null) {
this.#sendRetry(payload.retry)
return
}
this.#sendMessage({
id: payload.id,
event: payload.event,
data:
typeof payload.data === 'object'
? JSON.stringify(payload.data)
: payload.data,
})
}
/**
* Dispatches the given event on the intercepted `EventSource`.
*/
public dispatchEvent(event: Event) {
if (event instanceof MessageEvent) {
/**
* @note Use the internal send mechanism to skip normalization
* of the message data (already normalized by the server).
*/
this.#sendMessage({
id: event.lastEventId || undefined,
event: event.type === 'message' ? undefined : event.type,
data: event.data,
})
return
}
if (event.type === 'error') {
this.error()
return
}
if (event.type === 'close') {
this.close()
return
}
}
/**
* Errors the underlying `EventSource`, closing the connection with an error.
* This is equivalent to aborting the connection and will produce a `TypeError: Failed to fetch`
* error.
*/
public error(): void {
this.#controller.error()
this.#emitter.emit('error')
}
/**
* Closes the underlying `EventSource`, closing the connection.
*/
public close(): void {
this.#controller.close()
this.#emitter.emit('close')
}
#sendRetry(retry: number): void {
if (typeof retry === 'number') {
this.#controller.enqueue(this.#encoder.encode(`retry:${retry}\n\n`))
}
}
#sendMessage(message: {
id?: string
event?: unknown
data: unknown | undefined
}): void {
const frames: Array = []
if (message.id) {
frames.push(`id:${message.id}`)
}
if (message.event) {
frames.push(`event:${message.event.toString()}`)
}
if (message.data != null) {
/**
* Split data on line terminators (LF, CR, or CRLF) and translate them to individual frames.
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
*/
for (const line of message.data.toString().split(/\r\n|\r|\n/)) {
frames.push(`data:${line}`)
}
}
frames.push('', '')
this.#controller.enqueue(this.#encoder.encode(frames.join('\n')))
this.#emitter.emit('message', {
id: message.id,
event: message.event?.toString() || 'message',
data: message.data,
frames,
})
}
}
class ServerSentEventServer {
#request: Request
#client: ServerSentEventClient
constructor(args: { request: Request; client: ServerSentEventClient }) {
this.#request = args.request
this.#client = args.client
}
/**
* Establishes the actual connection for this SSE request
* and returns the `EventSource` instance.
*/
public connect(): EventSource {
const source = new ObservableEventSource(this.#request.url, {
withCredentials: this.#request.credentials === 'include',
headers: {
/**
* @note Mark this request as passthrough so it doesn't trigger
* an infinite loop matching against the existing request handler.
*/
accept: 'msw/passthrough',
},
})
source[kOnAnyMessage] = (event) => {
Object.defineProperties(event, {
target: {
value: this,
enumerable: true,
writable: true,
configurable: true,
},
})
// Schedule the server-to-client forwarding for the next tick
// so the user can prevent the message event.
queueMicrotask(() => {
if (!event.defaultPrevented) {
this.#client.dispatchEvent(event)
}
})
}
// Forward stream errors from the actual server to the client.
source.addEventListener('error', (event) => {
Object.defineProperties(event, {
target: {
value: this,
enumerable: true,
writable: true,
configurable: true,
},
})
queueMicrotask(() => {
// Allow the user to opt-out from this forwarding.
if (!event.defaultPrevented) {
this.#client.dispatchEvent(event)
}
})
})
return source
}
}
interface ObservableEventSourceInit extends EventSourceInit {
headers?: HeadersInit
}
type EventHandler = (
this: EventSource,
event: EventType,
) => any
const kRequest = Symbol('kRequest')
const kReconnectionTime = Symbol('kReconnectionTime')
const kLastEventId = Symbol('kLastEventId')
const kAbortController = Symbol('kAbortController')
const kOnOpen = Symbol('kOnOpen')
const kOnMessage = Symbol('kOnMessage')
const kOnAnyMessage = Symbol('kOnAnyMessage')
const kOnError = Symbol('kOnError')
class ObservableEventSource extends EventTarget implements EventSource {
static readonly CONNECTING = 0
static readonly OPEN = 1
static readonly CLOSED = 2
public readonly CONNECTING = ObservableEventSource.CONNECTING
public readonly OPEN = ObservableEventSource.OPEN
public readonly CLOSED = ObservableEventSource.CLOSED
public readyState: number
public url: string
public withCredentials: boolean
private [kRequest]: Request
private [kReconnectionTime]: number
private [kLastEventId]: string
private [kAbortController]: AbortController
private [kOnOpen]: EventHandler | null = null
private [kOnMessage]: EventHandler | null = null
private [kOnAnyMessage]: EventHandler | null = null
private [kOnError]: EventHandler | null = null
constructor(url: string | URL, init?: ObservableEventSourceInit) {
super()
this.url = new URL(url).href
this.withCredentials = init?.withCredentials ?? false
this.readyState = this.CONNECTING
// Support custom request init.
const headers = new Headers(init?.headers || {})
headers.append('accept', 'text/event-stream')
this[kAbortController] = new AbortController()
this[kReconnectionTime] = 2000
this[kLastEventId] = ''
this[kRequest] = new Request(this.url, {
method: 'GET',
headers,
credentials: this.withCredentials ? 'include' : 'omit',
signal: this[kAbortController].signal,
})
this.connect()
}
get onopen(): EventHandler | null {
return this[kOnOpen]
}
set onopen(handler: EventHandler) {
if (this[kOnOpen]) {
this.removeEventListener('open', this[kOnOpen])
}
this[kOnOpen] = handler.bind(this)
this.addEventListener('open', this[kOnOpen])
}
get onmessage(): EventHandler | null {
return this[kOnMessage]
}
set onmessage(handler: EventHandler) {
if (this[kOnMessage]) {
this.removeEventListener('message', { handleEvent: this[kOnMessage] })
}
this[kOnMessage] = handler.bind(this)
this.addEventListener('message', { handleEvent: this[kOnMessage] })
}
get onerror(): EventHandler | null {
return this[kOnError]
}
set oneerror(handler: EventHandler) {
if (this[kOnError]) {
this.removeEventListener('error', { handleEvent: this[kOnError] })
}
this[kOnError] = handler.bind(this)
this.addEventListener('error', { handleEvent: this[kOnError] })
}
public addEventListener(
type: K,
listener: EventHandler,
options?: boolean | AddEventListenerOptions,
): void
public addEventListener(
type: string,
listener: EventHandler,
options?: boolean | AddEventListenerOptions,
): void
public addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void
public addEventListener(
type: string,
listener: EventHandler | EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void {
super.addEventListener(
type,
listener as EventListenerOrEventListenerObject,
options,
)
}
public removeEventListener(
type: K,
listener: (this: EventSource, ev: EventSourceEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void
public removeEventListener(
type: string,
listener: (this: EventSource, event: MessageEvent) => any,
options?: boolean | EventListenerOptions,
): void
public removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void
public removeEventListener(
type: string,
listener: EventHandler | EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void {
super.removeEventListener(
type,
listener as EventListenerOrEventListenerObject,
options,
)
}
public dispatchEvent(event: Event): boolean {
return super.dispatchEvent(event)
}
public close(): void {
this[kAbortController].abort()
this.readyState = this.CLOSED
}
private async connect() {
await fetch(this[kRequest])
.then((response) => {
this.processResponse(response)
})
.catch(() => {
// Fail the connection on request errors instead of
// throwing a generic "Failed to fetch" error.
this.failConnection()
})
}
private processResponse(response: Response): void {
if (!response.body) {
this.failConnection()
return
}
if (isNetworkError(response)) {
this.reestablishConnection()
return
}
if (
response.status !== 200 ||
response.headers.get('content-type') !== 'text/event-stream'
) {
this.failConnection()
return
}
this.announceConnection()
this.interpretResponseBody(response)
}
private announceConnection(): void {
queueMicrotask(() => {
if (this.readyState !== this.CLOSED) {
this.readyState = this.OPEN
this.dispatchEvent(new Event('open'))
}
})
}
private interpretResponseBody(response: Response): void {
const parsingStream = new EventSourceParsingStream({
message: (message) => {
if (message.id) {
this[kLastEventId] = message.id
}
if (message.retry) {
this[kReconnectionTime] = message.retry
}
const messageEvent = new MessageEvent(
message.event ? message.event : 'message',
{
data: message.data,
origin: this[kRequest].url,
lastEventId: this[kLastEventId],
cancelable: true,
},
)
this[kOnAnyMessage]?.(messageEvent)
this.dispatchEvent(messageEvent)
},
abort: () => {
throw new Error('Stream abort is not implemented')
},
close: () => {
this.failConnection()
},
})
response
.body!.pipeTo(parsingStream)
.then(() => {
this.processResponseEndOfBody(response)
})
.catch(() => {
this.failConnection()
})
}
private processResponseEndOfBody(response: Response): void {
if (!isNetworkError(response)) {
this.reestablishConnection()
}
}
private async reestablishConnection(): Promise {
queueMicrotask(() => {
if (this.readyState === this.CLOSED) {
return
}
this.readyState = this.CONNECTING
this.dispatchEvent(new Event('error'))
})
await delay(this[kReconnectionTime])
queueMicrotask(async () => {
if (this.readyState !== this.CONNECTING) {
return
}
if (this[kLastEventId] !== '') {
this[kRequest].headers.set('last-event-id', this[kLastEventId])
}
await this.connect()
})
}
private failConnection(): void {
queueMicrotask(() => {
if (this.readyState !== this.CLOSED) {
this.readyState = this.CLOSED
this.dispatchEvent(new Event('error'))
}
})
}
}
/**
* Checks if the given `Response` instance is a network error.
* @see https://fetch.spec.whatwg.org/#concept-network-error
*/
function isNetworkError(response: Response): boolean {
return (
response.type === 'error' &&
response.status === 0 &&
response.statusText === '' &&
Array.from(response.headers.entries()).length === 0 &&
response.body === null
)
}
const enum ControlCharacters {
NewLine = 10,
CarriageReturn = 13,
Space = 32,
Colon = 58,
}
interface EventSourceMessage {
id?: string
event?: string
data?: string
retry?: number
}
class EventSourceParsingStream extends WritableStream {
private decoder: TextDecoder
private buffer?: Uint8Array
private position: number
private fieldLength?: number
private discardTrailingNewline = false
private message: EventSourceMessage = {
id: undefined,
event: undefined,
data: undefined,
retry: undefined,
}
constructor(
private underlyingSink: {
message: (message: EventSourceMessage) => void
abort?: (reason: any) => void
close?: () => void
},
) {
super({
write: (chunk) => {
this.processResponseBodyChunk(chunk)
},
abort: (reason) => {
this.underlyingSink.abort?.(reason)
},
close: () => {
this.underlyingSink.close?.()
},
})
this.decoder = new TextDecoder()
this.position = 0
}
private resetMessage(): void {
this.message = {
id: undefined,
event: undefined,
data: undefined,
retry: undefined,
}
}
private processResponseBodyChunk(chunk: Uint8Array): void {
if (this.buffer == null) {
this.buffer = chunk
this.position = 0
this.fieldLength = -1
} else {
const nextBuffer = new Uint8Array(this.buffer.length + chunk.length)
nextBuffer.set(this.buffer)
nextBuffer.set(chunk, this.buffer.length)
this.buffer = nextBuffer
}
const bufferLength = this.buffer.length
let lineStart = 0
while (this.position < bufferLength) {
if (this.discardTrailingNewline) {
if (this.buffer[this.position] === ControlCharacters.NewLine) {
lineStart = ++this.position
}
this.discardTrailingNewline = false
}
let lineEnd = -1
for (; this.position < bufferLength && lineEnd === -1; ++this.position) {
switch (this.buffer[this.position]) {
case ControlCharacters.Colon: {
if (this.fieldLength === -1) {
this.fieldLength = this.position - lineStart
}
break
}
case ControlCharacters.CarriageReturn: {
this.discardTrailingNewline = true
break
}
case ControlCharacters.NewLine: {
lineEnd = this.position
break
}
}
}
if (lineEnd === -1) {
break
}
this.processLine(
this.buffer.subarray(lineStart, lineEnd),
this.fieldLength!,
)
lineStart = this.position
this.fieldLength = -1
}
if (lineStart === bufferLength) {
this.buffer = undefined
} else if (lineStart !== 0) {
this.buffer = this.buffer.subarray(lineStart)
this.position -= lineStart
}
}
private processLine(line: Uint8Array, fieldLength: number): void {
// New line indicates the end of the message. Dispatch it.
if (line.length === 0) {
// Prevent dispatching the message if the data is an empty string.
// That is a no-op per spec.
if (this.message.data === undefined) {
this.message.event = undefined
return
}
this.underlyingSink.message(this.message)
this.resetMessage()
return
}
// Otherwise, keep accumulating message fields until the new line.
if (fieldLength > 0) {
const field = this.decoder.decode(line.subarray(0, fieldLength))
const valueOffset =
fieldLength +
(line[fieldLength + 1] === ControlCharacters.Space ? 2 : 1)
const value = this.decoder.decode(line.subarray(valueOffset))
switch (field) {
case 'data': {
this.message.data = this.message.data
? this.message.data + '\n' + value
: value
break
}
case 'event': {
this.message.event = value
break
}
case 'id': {
this.message.id = value
break
}
case 'retry': {
const retry = parseInt(value, 10)
if (!isNaN(retry)) {
this.message.retry = retry
}
break
}
}
}
}
}
================================================
FILE: src/core/typeUtils.ts
================================================
type Fn = (...arg: any[]) => any
export type MaybePromise = T | Promise
export type RequiredDeep<
Type,
U extends Record | Fn | undefined = undefined,
> = Type extends Fn
? Type
: /**
* @note The "Fn" type satisfies the predicate below.
* It must always come first, before the Record check.
*/
Type extends Record
? {
[Key in keyof Type]-?: NonNullable extends NonNullable
? NonNullable
: RequiredDeep, U>
}
: Type
/**
* @fixme Remove this once TS 5.4 is the lowest supported version.
* Because "NoInfer" is a built-in type utility there.
*/
export type NoInfer = [T][T extends any ? 0 : never]
================================================
FILE: src/core/utils/HttpResponse/decorators.ts
================================================
import statuses from '../../../shims/statuses'
import { Headers as HeadersPolyfill } from 'headers-polyfill'
import type { HttpResponseInit } from '../../HttpResponse'
const { message } = statuses
export const kSetCookie = Symbol('kSetCookie')
export interface HttpResponseDecoratedInit extends HttpResponseInit {
status: number
statusText: string
headers: Headers
}
export function normalizeResponseInit(
init: HttpResponseInit = {},
): HttpResponseDecoratedInit {
const status = init?.status || 200
const statusText = init?.statusText || message[status] || ''
const headers = new Headers(init?.headers)
return {
...init,
headers,
status,
statusText,
}
}
export function decorateResponse(
response: Response,
init: HttpResponseDecoratedInit,
): Response {
// Allow to mock the response type.
if (init.type) {
Object.defineProperty(response, 'type', {
value: init.type,
enumerable: true,
writable: false,
})
}
const responseCookies = init.headers.get('set-cookie')
if (responseCookies) {
// Record the raw "Set-Cookie" response header provided
// in the HeadersInit. This is later used to store these cookies
// in cookie jar and return the right cookies in the "cookies"
// response resolver argument.
Object.defineProperty(response, kSetCookie, {
value: responseCookies,
enumerable: false,
writable: false,
})
// Cookie forwarding is only relevant in the browser.
if (typeof document !== 'undefined') {
// Write the mocked response cookies to the document.
// Use `headers-polyfill` to get the Set-Cookie header value correctly.
// This is an alternative until TypeScript 5.2
// and Node.js v20 become the minimum supported version
// and getSetCookie in Headers can be used directly.
const responseCookiePairs = HeadersPolyfill.prototype.getSetCookie.call(
init.headers,
)
for (const cookieString of responseCookiePairs) {
// No need to parse the cookie headers because it's defined
// as the valid cookie string to begin with.
document.cookie = cookieString
}
}
}
return response
}
================================================
FILE: src/core/utils/cookieStore.ts
================================================
import { isNodeProcess } from 'is-node-process'
import { invariant } from 'outvariant'
import {
Cookie,
CookieJar,
MemoryCookieStore,
type MemoryCookieStoreIndex,
} from 'tough-cookie'
import { jsonParse } from './internal/jsonParse'
class CookieStore {
#storageKey = '__msw-cookie-store__'
#jar: CookieJar
#memoryStore: MemoryCookieStore
constructor() {
if (!isNodeProcess()) {
invariant(
typeof localStorage !== 'undefined',
'Failed to create a CookieStore: `localStorage` is not available in this environment. This is likely an issue with your environment, which has been detected as browser (or browser-like) environment and must implement global browser APIs correctly.',
)
}
this.#memoryStore = new MemoryCookieStore()
this.#memoryStore.idx = this.getCookieStoreIndex()
this.#jar = new CookieJar(this.#memoryStore)
}
public getCookies(url: string): Array