From 2e53ae73bd734c435ac447ca2b60cdaaebc1390b Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 13 Sep 2023 21:51:04 +0200 Subject: [PATCH 01/12] fix: pin "headers-polyfill" to 3.2.5 (#1736) --- package.json | 4 ++-- pnpm-lock.yaml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index f5af84154..df3940fa7 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "sideEffects": false, "dependencies": { "@mswjs/cookies": "^0.2.2", - "@mswjs/interceptors": "^0.17.5", + "@mswjs/interceptors": "^0.17.10", "@open-draft/until": "^1.0.3", "@types/cookie": "^0.4.1", "@types/js-levenshtein": "^1.1.1", @@ -97,7 +97,7 @@ "chokidar": "^3.4.2", "cookie": "^0.4.2", "graphql": "^15.0.0 || ^16.0.0", - "headers-polyfill": "^3.2.0", + "headers-polyfill": "3.2.5", "inquirer": "^8.2.0", "is-node-process": "^1.2.0", "js-levenshtein": "^1.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f3bbf4bc..bd6d7e58c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ specifiers: '@commitlint/cli': ^16.1.0 '@commitlint/config-conventional': ^16.0.0 '@mswjs/cookies': ^0.2.2 - '@mswjs/interceptors': ^0.17.5 + '@mswjs/interceptors': ^0.17.10 '@open-draft/test-server': ^0.4.2 '@open-draft/until': ^1.0.3 '@ossjs/release': ^0.7.2 @@ -44,7 +44,7 @@ specifiers: fs-extra: ^10.0.0 fs-teardown: ^0.3.0 graphql: ^15.0.0 || ^16.0.0 - headers-polyfill: ^3.2.0 + headers-polyfill: 3.2.5 inquirer: ^8.2.0 is-node-process: ^1.2.0 jest: ^29.4.3 @@ -74,7 +74,7 @@ specifiers: dependencies: '@mswjs/cookies': 0.2.2 - '@mswjs/interceptors': 0.17.7 + '@mswjs/interceptors': 0.17.10 '@open-draft/until': 1.0.3 '@types/cookie': 0.4.1 '@types/js-levenshtein': 1.1.1 @@ -82,7 +82,7 @@ dependencies: chokidar: 3.4.1 cookie: 0.4.2 graphql: 16.6.0 - headers-polyfill: 3.2.0 + headers-polyfill: 3.2.5 inquirer: 8.2.5 is-node-process: 1.2.0 js-levenshtein: 1.1.6 @@ -2119,15 +2119,15 @@ packages: set-cookie-parser: 2.5.1 dev: false - /@mswjs/interceptors/0.17.7: - resolution: {integrity: sha512-dPInyLEF6ybLxfKGY99euI+mbT6ls4PVO9qPgGIsRk3+2VZVfT7fo9Sq6Q8eKT9W38QtUyhG74hN7xMtKWioGw==} + /@mswjs/interceptors/0.17.10: + resolution: {integrity: sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==} engines: {node: '>=14'} dependencies: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 debug: 4.3.4 - headers-polyfill: 3.2.0 + headers-polyfill: 3.2.5 outvariant: 1.4.0 strict-event-emitter: 0.2.8 web-encoding: 1.1.5 @@ -5994,8 +5994,8 @@ packages: dependencies: function-bind: 1.1.1 - /headers-polyfill/3.2.0: - resolution: {integrity: sha512-NsYkbrWFQyoPBrbX5riycJ3D4aaB/3fpx1nYgDi3JTTnoeFET3BN0rq2nOB3VUmvpQrYpbKL8zRJo1Jsmmt7nA==} + /headers-polyfill/3.2.5: + resolution: {integrity: sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==} /homedir-polyfill/1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} @@ -8103,7 +8103,7 @@ packages: '@types/uuid': 8.3.4 debug: 4.3.4 express: 4.18.2 - headers-polyfill: 3.2.0 + headers-polyfill: 3.2.5 memfs: 3.4.13 mustache: 4.2.0 playwright: 1.30.0 From 5a03078a20c0f2f9535b2f44657a007615d4737b Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 13 Sep 2023 20:02:45 +0000 Subject: [PATCH 02/12] chore(release): v1.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df3940fa7..b43d3f14c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw", - "version": "1.3.0", + "version": "1.3.1", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "main": "./lib/index.js", "types": "./lib/index.d.ts", From b225a883eccacf8d6f6e3caa23bc6b33eec5e9ca Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 16 Sep 2023 16:39:05 +0200 Subject: [PATCH 03/12] chore: update to @ossjs/release@0.8.0 (#1740) --- package.json | 2 +- pnpm-lock.yaml | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b43d3f14c..825d31a73 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "@commitlint/cli": "^16.1.0", "@commitlint/config-conventional": "^16.0.0", "@open-draft/test-server": "^0.4.2", - "@ossjs/release": "^0.7.2", + "@ossjs/release": "^0.8.0", "@playwright/test": "^1.30.0", "@swc/core": "^1.3.35", "@swc/jest": "^0.2.24", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6d7e58c..7d02f6904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ specifiers: '@mswjs/interceptors': ^0.17.10 '@open-draft/test-server': ^0.4.2 '@open-draft/until': ^1.0.3 - '@ossjs/release': ^0.7.2 + '@ossjs/release': ^0.8.0 '@playwright/test': ^1.30.0 '@swc/core': ^1.3.35 '@swc/jest': ^0.2.24 @@ -99,7 +99,7 @@ devDependencies: '@commitlint/cli': 16.3.0_@swc+core@1.3.35 '@commitlint/config-conventional': 16.2.4 '@open-draft/test-server': 0.4.2 - '@ossjs/release': 0.7.2 + '@ossjs/release': 0.8.0 '@playwright/test': 1.30.0 '@swc/core': 1.3.35 '@swc/jest': 0.2.24_@swc+core@1.3.35 @@ -2183,8 +2183,8 @@ packages: resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} dev: true - /@ossjs/release/0.7.2: - resolution: {integrity: sha512-s8w7VRC6Xf1vfpIsDG2CflWinSg9O7H/6nckxaBCiMWClkeaZ3JuXzZEIcybc6jeAFc1Rz7UilCvgZflb06Y5g==} + /@ossjs/release/0.8.0: + resolution: {integrity: sha512-vzxhYvad/Ub3j8bWWCRfdwTvFzK3HtKjm8IM5J+7njnQcZZie5iouUXX+G65OI3F1YgQSWvsozrWqHyN1x7fjQ==} hasBin: true dependencies: '@open-draft/deferred-promise': 2.1.0 @@ -2197,7 +2197,7 @@ packages: '@types/registry-auth-token': 4.2.1 '@types/semver': 7.5.1 '@types/yargs': 17.0.22 - conventional-commits-parser: 3.2.4 + conventional-commits-parser: 5.0.0 get-stream: 6.0.1 git-log-parser: 1.2.0 issue-parser: 6.0.0 @@ -4122,6 +4122,17 @@ packages: through2: 4.0.2 dev: true + /conventional-commits-parser/5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.1.0 + dev: true + /convert-source-map/1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true @@ -6534,6 +6545,13 @@ packages: text-extensions: 1.9.0 dev: true + /is-text-path/2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + dependencies: + text-extensions: 2.4.0 + dev: true + /is-typed-array/1.1.10: resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} engines: {node: '>= 0.4'} @@ -7556,6 +7574,11 @@ packages: readable-stream: 2.3.7 dev: true + /meow/12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + dev: true + /meow/8.1.2: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} @@ -9615,6 +9638,11 @@ packages: engines: {node: '>=0.10'} dev: true + /text-extensions/2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + dev: true + /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true From 0a857f67b6e0883336a790633aa9bafc0b16c7cb Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 28 Sep 2023 13:31:27 +0200 Subject: [PATCH 04/12] fix: set minimal "graphql" version to 16.18.1 (#1754) --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 825d31a73..fccf37677 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "chalk": "^4.1.1", "chokidar": "^3.4.2", "cookie": "^0.4.2", - "graphql": "^15.0.0 || ^16.0.0", + "graphql": "^16.8.1", "headers-polyfill": "3.2.5", "inquirer": "^8.2.0", "is-node-process": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d02f6904..05e02d57c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ specifiers: express: ^4.18.2 fs-extra: ^10.0.0 fs-teardown: ^0.3.0 - graphql: ^15.0.0 || ^16.0.0 + graphql: ^16.8.1 headers-polyfill: 3.2.5 inquirer: ^8.2.0 is-node-process: ^1.2.0 @@ -81,7 +81,7 @@ dependencies: chalk: 4.1.1 chokidar: 3.4.1 cookie: 0.4.2 - graphql: 16.6.0 + graphql: 16.8.1 headers-polyfill: 3.2.5 inquirer: 8.2.5 is-node-process: 1.2.0 @@ -5920,8 +5920,8 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true - /graphql/16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + /graphql/16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: false From 2f7215294cac98149757f57118c7492d31a2a8e0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 1 Oct 2023 00:26:01 +0000 Subject: [PATCH 05/12] chore(release): v1.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fccf37677..20d9c42ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw", - "version": "1.3.1", + "version": "1.3.2", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "main": "./lib/index.js", "types": "./lib/index.d.ts", From 61c220c4ea64c192e1327f478f6961240d9fc958 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 23 Oct 2023 09:07:58 +0200 Subject: [PATCH 06/12] chore: set dry run release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecff80f93..1b7796f38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: run: pnpm test - name: Release - run: pnpm release + run: pnpm release --dry-run env: GITHUB_TOKEN: ${{ secrets.GH_ADMIN_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 1033f651291f67a0ce509af965fdd0aa50fa7509 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 23 Oct 2023 09:45:04 +0200 Subject: [PATCH 07/12] feat!: adopt the global Fetch API (#1436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frederik Rabøl Co-authored-by: Christoph Fricke Co-authored-by: Piotr Co-authored-by: Kristján Oddsson Co-authored-by: thepassle Co-authored-by: Kevin Østerkilde Co-authored-by: Laryssa Rocha Co-authored-by: Matthew Costabile --- .github/workflows/ci.yml | 44 +- .github/workflows/compat.yml | 78 + .github/workflows/release.yml | 2 +- .nvmrc | 2 +- CONTRIBUTING.md | 42 +- MIGRATING.md | 659 +++++++ README.md | 61 +- browser/package.json | 5 + cli/init.js | 6 +- config/constants.js | 3 +- config/copyServiceWorker.ts | 17 +- config/plugins/esbuild/copyWorkerPlugin.ts | 80 + .../esbuild/forceEsmExtensionsPlugin.ts | 54 + .../esbuild/resolveCoreImportsPlugin.ts | 24 + config/plugins/esbuild/workerScriptPlugin.ts | 82 - config/replaceCoreImports.js | 21 + config/scripts/patch-ts.js | 29 + config/scripts/validate-esm.js | 247 +++ global.d.ts | 9 + jest.config.js | 3 + jest.setup.js | 30 +- native/package.json | 3 +- node/package.json | 3 +- package.json | 74 +- pnpm-lock.yaml | 1566 ++++++++++++----- src/browser/index.ts | 3 + src/{ => browser}/setupWorker/glossary.ts | 94 +- .../setupWorker/setupWorker.node.test.ts | 0 src/{ => browser}/setupWorker/setupWorker.ts | 119 +- .../start/createFallbackRequestListener.ts | 67 + .../setupWorker/start/createFallbackStart.ts | 0 .../start/createRequestListener.ts | 116 ++ .../start/createResponseListener.ts | 25 +- .../setupWorker/start/createStartHandler.ts | 10 +- .../start/utils/createMessageChannel.ts | 17 +- .../setupWorker/start/utils/enableMocking.ts | 2 +- .../start/utils/getWorkerByRegistration.ts | 0 .../start/utils/getWorkerInstance.ts | 14 +- .../start/utils/prepareStartHandler.test.ts | 0 .../start/utils/prepareStartHandler.ts | 4 +- .../start/utils/printStartMessage.test.ts | 0 .../start/utils/printStartMessage.ts | 2 +- .../start/utils/validateWorkerScope.ts | 2 +- .../setupWorker/stop/createFallbackStop.ts | 0 .../setupWorker/stop/createStop.ts | 2 +- .../stop/utils/printStopMessage.test.ts | 0 .../stop/utils/printStopMessage.ts | 2 +- src/browser/tsconfig.json | 7 + .../utils/deferNetworkRequestsUntil.test.ts | 2 +- .../utils/deferNetworkRequestsUntil.ts | 0 .../utils}/getAbsoluteWorkerUrl.test.ts | 0 .../utils}/getAbsoluteWorkerUrl.ts | 0 src/browser/utils/parseWorkerRequest.ts | 15 + src/browser/utils/pruneGetRequestBody.test.ts | 53 + src/browser/utils/pruneGetRequestBody.ts | 21 + .../utils}/requestIntegrityCheck.ts | 2 +- .../utils/supportsReadableStreamTransfer.ts | 17 + src/context/body.test.ts | 22 - src/context/body.ts | 19 - src/context/cookie.node.test.ts | 10 - src/context/cookie.test.ts | 39 - src/context/cookie.ts | 23 - src/context/data.test.ts | 64 - src/context/data.ts | 21 - src/context/delay.node.test.ts | 40 - src/context/delay.test.ts | 42 - src/context/delay.ts | 72 - src/context/errors.test.ts | 72 - src/context/errors.ts | 27 - src/context/extensions.test.ts | 70 - src/context/extensions.ts | 20 - src/context/fetch.test.ts | 36 - src/context/fetch.ts | 65 - src/context/field.test.ts | 117 -- src/context/field.ts | 60 - src/context/index.ts | 12 - src/context/json.test.ts | 31 - src/context/json.ts | 22 - src/context/set.test.ts | 34 - src/context/set.ts | 56 - src/context/status.test.ts | 17 - src/context/status.ts | 22 - src/context/text.test.ts | 12 - src/context/text.ts | 17 - src/context/xml.test.ts | 12 - src/context/xml.ts | 18 - src/core/HttpResponse.test.ts | 65 + src/core/HttpResponse.ts | 122 ++ src/{ => core}/SetupApi.ts | 28 +- src/core/bypass.test.ts | 47 + src/core/bypass.ts | 36 + src/core/delay.ts | 70 + src/{ => core}/graphql.test.ts | 2 +- src/core/graphql.ts | 138 ++ .../handlers/GraphQLHandler.test.ts | 196 ++- src/{ => core}/handlers/GraphQLHandler.ts | 164 +- src/core/handlers/HttpHandler.test.ts | 218 +++ src/core/handlers/HttpHandler.ts | 169 ++ src/core/handlers/RequestHandler.ts | 297 ++++ src/{rest.spec.ts => core/http.spec.ts} | 6 +- src/core/http.ts | 51 + src/core/index.ts | 55 + src/core/passthrough.test.ts | 13 + src/core/passthrough.ts | 23 + src/core/sharedOptions.ts | 66 + src/{ => core}/typeUtils.ts | 8 +- src/core/utils/HttpResponse/decorators.ts | 56 + src/core/utils/getResponse.ts | 55 + src/core/utils/handleRequest.test.ts | 344 ++++ src/{ => core}/utils/handleRequest.ts | 96 +- src/core/utils/internal/Disposable.ts | 9 + src/{ => core}/utils/internal/checkGlobals.ts | 0 src/{ => core}/utils/internal/devUtils.ts | 0 .../utils/internal/getCallFrame.test.ts | 56 +- src/{ => core}/utils/internal/getCallFrame.ts | 2 +- .../utils/internal/isIterable.test.ts | 0 src/{ => core}/utils/internal/isIterable.ts | 0 .../utils/internal/isObject.test.ts | 0 src/{ => core}/utils/internal/isObject.ts | 0 .../utils/internal/isStringEqual.test.ts | 0 .../utils/internal/isStringEqual.ts | 0 .../utils/internal/jsonParse.test.ts | 0 src/{ => core}/utils/internal/jsonParse.ts | 0 .../utils/internal/mergeRight.test.ts | 0 src/{ => core}/utils/internal/mergeRight.ts | 0 .../internal/parseGraphQLRequest.test.ts | 99 ++ .../utils/internal/parseGraphQLRequest.ts | 72 +- .../utils/internal/parseMultipartData.test.ts | 0 .../utils/internal/parseMultipartData.ts | 0 .../utils/internal/pipeEvents.test.ts | 6 +- src/{ => core}/utils/internal/pipeEvents.ts | 0 .../utils/internal/requestHandlerUtils.ts | 12 +- .../utils/internal/toReadonlyArray.test.ts | 0 .../utils/internal/toReadonlyArray.ts | 0 .../utils/internal/tryCatch.test.ts | 0 src/{ => core}/utils/internal/tryCatch.ts | 0 src/core/utils/internal/uuidv4.ts | 3 + .../utils/logging/getStatusCodeColor.test.ts | 0 .../utils/logging/getStatusCodeColor.ts | 0 .../utils/logging/getTimestamp.test.ts | 0 src/{ => core}/utils/logging/getTimestamp.ts | 0 .../utils/logging/serializeRequest.test.ts | 23 + src/core/utils/logging/serializeRequest.ts | 23 + .../utils/logging/serializeResponse.test.ts | 77 + src/core/utils/logging/serializeResponse.ts | 31 + .../utils/matching/matchRequestUrl.test.ts | 0 .../utils/matching/matchRequestUrl.ts | 2 +- .../utils/matching/normalizePath.node.test.ts | 0 .../utils/matching/normalizePath.test.ts | 0 .../utils/matching/normalizePath.ts | 0 .../request/getPublicUrlFromRequest.test.ts | 26 + .../utils/request/getPublicUrlFromRequest.ts | 15 + .../request/getRequestCookies.node.test.ts | 3 +- .../utils/request/getRequestCookies.test.ts | 11 +- src/core/utils/request/getRequestCookies.ts | 76 + .../utils/request/onUnhandledRequest.test.ts | 135 +- .../utils/request/onUnhandledRequest.ts | 76 +- .../utils/request/readResponseCookies.ts | 8 +- src/core/utils/toResponseInit.ts | 7 + src/{ => core}/utils/url/cleanUrl.test.ts | 0 src/{ => core}/utils/url/cleanUrl.ts | 0 .../utils/url/getAbsoluteUrl.node.test.ts | 0 .../utils/url/getAbsoluteUrl.test.ts | 0 src/{ => core}/utils/url/getAbsoluteUrl.ts | 0 .../utils/url/isAbsoluteUrl.test.ts | 0 src/{ => core}/utils/url/isAbsoluteUrl.ts | 0 src/graphql.ts | 110 -- src/handlers/RequestHandler.ts | 286 --- src/handlers/RestHandler.test.ts | 200 --- src/handlers/RestHandler.ts | 197 --- src/iife/index.ts | 2 + src/index.ts | 75 - src/mockServiceWorker.js | 171 +- src/native/index.ts | 7 +- src/node/SetupServerApi.ts | 144 +- src/node/glossary.ts | 50 +- src/node/index.ts | 3 +- src/node/setupServer.ts | 11 +- src/node/utils/isNodeException.ts | 10 + src/response.ts | 93 - src/rest.ts | 43 - .../start/createFallbackRequestListener.ts | 85 - .../start/createRequestListener.ts | 141 -- src/setupWorker/start/utils/streamResponse.ts | 56 - src/sharedOptions.ts | 30 - src/utils/NetworkError.ts | 6 - src/utils/getResponse.ts | 88 - src/utils/handleRequest.test.ts | 279 --- src/utils/internal/StrictBroadcastChannel.ts | 27 - src/utils/internal/compose.test.ts | 24 - src/utils/internal/compose.ts | 46 - .../internal/parseGraphQLRequest.test.ts | 86 - src/utils/internal/uuidv4.ts | 7 - src/utils/logging/prepareRequest.test.ts | 28 - src/utils/logging/prepareRequest.ts | 22 - src/utils/logging/prepareResponse.test.ts | 52 - src/utils/logging/prepareResponse.ts | 18 - src/utils/logging/serializeResponse.ts | 16 - src/utils/request/MockedRequest.test.ts | 237 --- src/utils/request/MockedRequest.ts | 180 -- .../createResponseFromIsomorphicResponse.ts | 11 - .../request/getPublicUrlFromRequest.test.ts | 19 - src/utils/request/getPublicUrlFromRequest.ts | 14 - src/utils/request/getRequestCookies.ts | 35 - src/utils/request/parseBody.test.ts | 114 -- src/utils/request/parseBody.ts | 33 - src/utils/request/parseWorkerRequest.ts | 21 - src/utils/request/pruneGetRequestBody.test.ts | 38 - src/utils/request/pruneGetRequestBody.ts | 21 - .../graphql-api/anonymous-operation.mocks.ts | 12 + .../graphql-api/anonymous-operation.test.ts | 210 +++ test/browser/graphql-api/cookies.mocks.ts | 21 +- test/browser/graphql-api/cookies.test.ts | 2 +- .../graphql-api/document-node.mocks.ts | 35 +- test/browser/graphql-api/errors.mocks.ts | 13 +- test/browser/graphql-api/extensions.mocks.ts | 25 +- test/browser/graphql-api/link.mocks.ts | 72 +- test/browser/graphql-api/link.test.ts | 4 +- test/browser/graphql-api/logging.mocks.ts | 39 +- test/browser/graphql-api/logging.test.ts | 10 +- .../graphql-api/multipart-data.mocks.ts | 65 +- .../graphql-api/multipart-data.test.ts | 22 +- test/browser/graphql-api/mutation.mocks.ts | 13 +- test/browser/graphql-api/mutation.test.ts | 2 +- .../graphql-api/operation-reference.mocks.ts | 31 +- .../graphql-api/operation-reference.test.ts | 6 +- test/browser/graphql-api/operation.mocks.ts | 17 +- test/browser/graphql-api/operation.test.ts | 14 +- test/browser/graphql-api/query.mocks.ts | 13 +- test/browser/graphql-api/query.test.ts | 2 +- .../graphql-api/response-patching.mocks.ts | 45 +- .../graphql-api/response-patching.test.ts | 32 +- test/browser/graphql-api/variables.mocks.ts | 41 +- .../async-response-transformer.mocks.ts | 41 - .../async-response-transformer.test.ts | 44 - test/browser/msw-api/context/delay.mocks.ts | 18 +- test/browser/msw-api/context/delay.test.ts | 9 +- .../msw-api/distribution/iife.mocks.js | 6 +- .../browser/msw-api/distribution/iife.test.ts | 1 - .../msw-api/exception-handling.mocks.ts | 7 +- .../msw-api/exception-handling.test.ts | 3 +- test/browser/msw-api/hard-reload.mocks.ts | 7 +- test/browser/msw-api/hard-reload.test.ts | 3 +- .../msw-api/integrity-check-invalid.mocks.ts | 9 +- .../msw-api/integrity-check-valid.mocks.ts | 7 +- test/browser/msw-api/integrity-check.test.ts | 6 +- .../msw-api/regression/handle-stream.mocks.ts | 8 +- .../msw-api/regression/null-body.mocks.ts | 7 +- .../msw-api/regression/null-body.test.ts | 8 +- test/browser/msw-api/req/passthrough.mocks.ts | 11 +- test/browser/msw-api/req/passthrough.test.ts | 39 +- .../msw-api/res/network-error.mocks.ts | 7 +- .../fallback-mode/fallback-mode.mocks.ts | 7 +- .../fallback-mode/fallback-mode.test.ts | 5 +- .../setup-worker/input-validation.mocks.ts | 7 +- .../life-cycle-events/on.mocks.ts | 45 +- .../setup-worker/life-cycle-events/on.test.ts | 11 +- .../removeAllListeners.test.ts | 2 +- .../life-cycle-events/removeListener.test.ts | 2 +- .../setup-worker/listHandlers.mocks.ts | 11 +- .../msw-api/setup-worker/listHandlers.test.ts | 11 +- .../setup-worker/printHandlers.mocks.ts | 23 - .../setup-worker/printHandlers.test.ts | 71 - .../setup-worker/resetHandlers.test.ts | 18 +- .../setup-worker/response-logging.test.ts | 8 +- .../setup-worker/restoreHandlers.test.ts | 27 +- .../scenarios/custom-transformers.mocks.ts | 23 +- .../scenarios/errors/internal-error.mocks.ts | 5 +- .../scenarios/errors/network-error.mocks.ts | 7 +- .../scenarios/errors/network-error.test.ts | 48 +- .../scenarios/fall-through.mocks.ts | 15 +- .../scenarios/iframe/iframe.mocks.ts | 7 +- .../scope/scope-nested-quiet.mocks.ts | 2 +- .../scenarios/scope/scope-nested.mocks.ts | 2 +- .../scenarios/scope/scope-root.mocks.ts | 2 +- .../shared-worker/shared-worker.mocks.ts | 2 +- .../scenarios/text-event-stream.mocks.ts | 7 +- .../msw-api/setup-worker/start/error.mocks.ts | 7 +- .../msw-api/setup-worker/start/error.test.ts | 2 +- .../start/find-worker.error.mocks.ts | 11 +- .../setup-worker/start/find-worker.mocks.ts | 13 +- .../setup-worker/start/find-worker.test.ts | 2 +- .../on-unhandled-request/bypass.mocks.ts | 7 +- .../callback-print.mocks.ts | 14 +- .../callback-print.test.ts | 4 +- .../callback-throws.mocks.ts | 11 +- .../on-unhandled-request/callback.mocks.ts | 11 +- .../on-unhandled-request/default.mocks.ts | 7 +- .../on-unhandled-request/default.test.ts | 2 +- .../start/on-unhandled-request/error.mocks.ts | 7 +- .../suggestions.graphql.test.ts | 27 +- .../on-unhandled-request/suggestions.mocks.ts | 5 +- .../suggestions.rest.test.ts | 35 +- .../start/on-unhandled-request/warn.mocks.ts | 11 +- .../start/on-unhandled-request/warn.test.ts | 8 +- .../start/options-sw-scope.mocks.ts | 7 +- .../msw-api/setup-worker/start/quiet.mocks.ts | 15 +- .../msw-api/setup-worker/start/quiet.test.ts | 8 +- .../msw-api/setup-worker/start/start.mocks.ts | 7 +- .../msw-api/setup-worker/start/start.test.ts | 2 +- .../start/wait-until-ready.error.mocks.ts | 11 +- .../start/wait-until-ready.false.mocks.ts | 11 +- .../start/wait-until-ready.mocks.ts | 11 +- .../msw-api/setup-worker/stop.mocks.ts | 7 +- .../browser/msw-api/setup-worker/stop.test.ts | 8 +- .../msw-api/setup-worker/stop/quiet.mocks.ts | 2 +- .../msw-api/setup-worker/stop/quiet.test.ts | 2 +- .../stop/removes-all-listeners.mocks.ts | 7 +- .../stop/removes-all-listeners.test.ts | 2 +- .../browser/msw-api/setup-worker/use.mocks.ts | 14 +- test/browser/msw-api/setup-worker/use.test.ts | 57 +- test/browser/msw-api/unregister.mocks.ts | 7 +- test/browser/msw-api/unregister.test.ts | 9 +- test/browser/playwright.extend.ts | 2 +- test/browser/rest-api/basic.mocks.ts | 17 +- test/browser/rest-api/basic.test.ts | 3 +- test/browser/rest-api/body.mocks.ts | 65 +- test/browser/rest-api/body.test.ts | 95 +- test/browser/rest-api/context.mocks.ts | 26 +- test/browser/rest-api/context.test.ts | 2 +- .../rest-api/cookies-inheritance.mocks.ts | 33 +- .../browser/rest-api/cookies-request.mocks.ts | 9 +- test/browser/rest-api/cookies-request.test.ts | 12 +- test/browser/rest-api/cookies.mocks.ts | 33 +- test/browser/rest-api/cookies.test.ts | 4 +- test/browser/rest-api/cors.mocks.ts | 2 +- .../rest-api/custom-request-handler.mocks.ts | 93 - .../rest-api/custom-request-handler.test.ts | 43 - test/browser/rest-api/generator.mocks.ts | 64 +- test/browser/rest-api/generator.test.ts | 4 +- .../rest-api/headers-multiple.mocks.ts | 31 +- .../browser/rest-api/headers-multiple.test.ts | 7 +- test/browser/rest-api/logging.test.ts | 6 +- test/browser/rest-api/params.mocks.ts | 24 +- test/browser/rest-api/params.test.ts | 3 +- test/browser/rest-api/plain-response.mocks.ts | 10 + test/browser/rest-api/plain-response.test.ts | 26 + .../rest-api/query-params-warning.mocks.ts | 15 +- test/browser/rest-api/query.mocks.ts | 25 +- test/browser/rest-api/query.test.ts | 6 +- test/browser/rest-api/redirect.mocks.ts | 24 +- test/browser/rest-api/redirect.test.ts | 5 +- .../request/body/body-form-data.page.html | 39 +- .../request/body/body-form-data.test.ts | 17 +- .../rest-api/request/body/body-json.test.ts | 4 +- .../rest-api/request/body/body.mocks.ts | 33 +- .../rest-api/request/matching/all.mocks.ts | 11 +- .../rest-api/request/matching/all.test.ts | 2 +- .../rest-api/request/matching/method.mocks.ts | 11 +- .../rest-api/request/matching/method.test.ts | 3 +- .../matching/path-params-decode.mocks.ts | 10 +- .../matching/path-params-decode.test.ts | 2 +- .../rest-api/request/matching/uri.mocks.ts | 46 +- .../rest-api/request/matching/uri.test.ts | 12 +- .../rest-api/response-patching.mocks.ts | 107 +- .../rest-api/response-patching.test.ts | 29 +- .../response/body/body-binary.mocks.ts | 15 +- .../response/body/body-binary.test.ts | 3 +- .../rest-api/response/body/body-blob.mocks.ts | 14 + .../rest-api/response/body/body-blob.test.ts | 13 + .../response/body/body-formdata.mocks.ts | 14 + .../response/body/body-formdata.test.ts | 18 + .../rest-api/response/body/body-json.mocks.ts | 11 +- .../response/body/body-stream.mocks.ts | 29 + .../response/body/body-stream.test.ts | 46 + .../rest-api/response/body/body-text.mocks.ts | 7 +- .../rest-api/response/body/body-xml.mocks.ts | 11 +- .../rest-api/response/response-error.mocks.ts | 10 + .../rest-api/response/response-error.test.ts | 27 + test/browser/rest-api/status.mocks.ts | 14 +- test/browser/rest-api/xhr.mocks.ts | 7 +- test/browser/setup/webpackHttpServer.ts | 4 +- test/jest.config.js | 18 +- test/modules/browser/esm-browser.test.ts | 87 + test/modules/browser/playwright.config.ts | 13 + test/modules/module-utils.ts | 52 + test/modules/node/esm-node.test.ts | 108 ++ test/modules/node/jest.config.js | 9 + .../graphql-api/anonymous-operations.test.ts | 108 ++ .../graphql-api/compatibility.node.test.ts | 14 +- test/node/graphql-api/cookies.node.test.ts | 20 +- test/node/graphql-api/extensions.node.test.ts | 22 +- .../response-patching.node.test.ts | 31 +- test/node/msw-api/context/delay.node.test.ts | 17 +- .../node/msw-api/req/passthrough.node.test.ts | 26 +- .../msw-api/res/network-error.node.test.ts | 19 +- .../input-validation.node.test.ts | 8 +- .../life-cycle-events/on.node.test.ts | 46 +- .../removeAllListeners.node.test.ts | 6 +- .../removeListener.node.test.ts | 6 +- .../setup-server/listHandlers.node.test.ts | 6 +- .../setup-server/printHandlers.node.test.ts | 96 - .../setup-server/resetHandlers.node.test.ts | 18 +- .../setup-server/restoreHandlers.node.test.ts | 16 +- .../scenarios/cookies-request.node.test.ts | 29 +- .../custom-transformers.node.test.ts | 20 +- .../scenarios/fake-timers.node.test.ts | 6 +- .../scenarios/fall-through.node.test.ts | 35 +- .../setup-server/scenarios/fetch.node.test.ts | 138 +- .../scenarios/generator.node.test.ts | 63 +- .../scenarios/graphql.node.test.ts | 26 +- .../setup-server/scenarios/http.node.test.ts | 26 +- .../setup-server/scenarios/https.node.test.ts | 28 +- .../on-unhandled-request/bypass.node.test.ts | 8 +- .../callback-throws.node.test.ts | 24 +- .../callback.node.test.ts | 8 +- .../on-unhandled-request/default.node.test.ts | 8 +- .../on-unhandled-request/error.node.test.ts | 12 +- .../on-unhandled-request/warn.node.test.ts | 8 +- .../scenarios/relative-url.node.test.ts | 11 +- .../scenarios/response-patching..node.test.ts | 85 +- .../setup-server/scenarios/xhr.node.test.ts | 20 +- .../msw-api/setup-server/use.node.test.ts | 63 +- .../many-request-handlers-jsdom.test.ts | 65 + .../regressions/many-request-handlers.test.ts | 58 + .../rest-api/cookies-inheritance.node.test.ts | 40 +- test/node/rest-api/https.node.test.ts | 46 + .../body/body-arraybuffer.node.test.ts | 15 +- .../request/body/body-form-data.node.test.ts | 24 +- .../request/body/body-json.node.test.ts | 38 +- .../request/body/body-text.node.test.ts | 9 +- .../request/body/body-used.node.test.ts | 66 + .../request/matching/all.node.test.ts | 22 +- .../matching/path-params-decode.node.test.ts | 15 +- .../{body => }/body-binary.node.test.ts | 21 +- .../rest-api/response/body-json.node.test.ts | 36 + .../response/body-stream.node.test.ts | 102 ++ .../{body => }/body-text.node.test.ts | 7 +- .../response/{body => }/body-xml.node.test.ts | 10 +- .../response/body/body-json.node.test.ts | 41 - .../rest-api/response/response-error.test.ts | 35 + test/support/graphql.ts | 10 +- test/typings/graphql.test-d.ts | 190 +- test/typings/path-params.test-d.ts | 55 - test/typings/rest.test-d.ts | 144 +- test/typings/run.ts | 2 +- test/typings/set.test-d.ts | 43 - test/typings/tsconfig.json | 6 +- tsconfig.json | 11 +- tsup.config.ts | 176 +- 440 files changed, 8971 insertions(+), 7709 deletions(-) create mode 100644 .github/workflows/compat.yml create mode 100644 MIGRATING.md create mode 100644 browser/package.json create mode 100644 config/plugins/esbuild/copyWorkerPlugin.ts create mode 100644 config/plugins/esbuild/forceEsmExtensionsPlugin.ts create mode 100644 config/plugins/esbuild/resolveCoreImportsPlugin.ts delete mode 100644 config/plugins/esbuild/workerScriptPlugin.ts create mode 100644 config/replaceCoreImports.js create mode 100644 config/scripts/patch-ts.js create mode 100644 config/scripts/validate-esm.js create mode 100644 src/browser/index.ts rename src/{ => browser}/setupWorker/glossary.ts (77%) rename src/{ => browser}/setupWorker/setupWorker.node.test.ts (100%) rename src/{ => browser}/setupWorker/setupWorker.ts (64%) create mode 100644 src/browser/setupWorker/start/createFallbackRequestListener.ts rename src/{ => browser}/setupWorker/start/createFallbackStart.ts (100%) create mode 100644 src/browser/setupWorker/start/createRequestListener.ts rename src/{ => browser}/setupWorker/start/createResponseListener.ts (62%) rename src/{ => browser}/setupWorker/start/createStartHandler.ts (94%) rename src/{ => browser}/setupWorker/start/utils/createMessageChannel.ts (62%) rename src/{ => browser}/setupWorker/start/utils/enableMocking.ts (94%) rename src/{ => browser}/setupWorker/start/utils/getWorkerByRegistration.ts (100%) rename src/{ => browser}/setupWorker/start/utils/getWorkerInstance.ts (88%) rename src/{ => browser}/setupWorker/start/utils/prepareStartHandler.test.ts (100%) rename src/{ => browser}/setupWorker/start/utils/prepareStartHandler.ts (90%) rename src/{ => browser}/setupWorker/start/utils/printStartMessage.test.ts (100%) rename src/{ => browser}/setupWorker/start/utils/printStartMessage.ts (93%) rename src/{ => browser}/setupWorker/start/utils/validateWorkerScope.ts (91%) rename src/{ => browser}/setupWorker/stop/createFallbackStop.ts (100%) rename src/{ => browser}/setupWorker/stop/createStop.ts (94%) rename src/{ => browser}/setupWorker/stop/utils/printStopMessage.test.ts (100%) rename src/{ => browser}/setupWorker/stop/utils/printStopMessage.ts (79%) create mode 100644 src/browser/tsconfig.json rename src/{ => browser}/utils/deferNetworkRequestsUntil.test.ts (94%) rename src/{ => browser}/utils/deferNetworkRequestsUntil.ts (100%) rename src/{utils/url => browser/utils}/getAbsoluteWorkerUrl.test.ts (100%) rename src/{utils/url => browser/utils}/getAbsoluteWorkerUrl.ts (100%) create mode 100644 src/browser/utils/parseWorkerRequest.ts create mode 100644 src/browser/utils/pruneGetRequestBody.test.ts create mode 100644 src/browser/utils/pruneGetRequestBody.ts rename src/{utils/internal => browser/utils}/requestIntegrityCheck.ts (90%) create mode 100644 src/browser/utils/supportsReadableStreamTransfer.ts delete mode 100644 src/context/body.test.ts delete mode 100644 src/context/body.ts delete mode 100644 src/context/cookie.node.test.ts delete mode 100644 src/context/cookie.test.ts delete mode 100644 src/context/cookie.ts delete mode 100644 src/context/data.test.ts delete mode 100644 src/context/data.ts delete mode 100644 src/context/delay.node.test.ts delete mode 100644 src/context/delay.test.ts delete mode 100644 src/context/delay.ts delete mode 100644 src/context/errors.test.ts delete mode 100644 src/context/errors.ts delete mode 100644 src/context/extensions.test.ts delete mode 100644 src/context/extensions.ts delete mode 100644 src/context/fetch.test.ts delete mode 100644 src/context/fetch.ts delete mode 100644 src/context/field.test.ts delete mode 100644 src/context/field.ts delete mode 100644 src/context/index.ts delete mode 100644 src/context/json.test.ts delete mode 100644 src/context/json.ts delete mode 100644 src/context/set.test.ts delete mode 100644 src/context/set.ts delete mode 100644 src/context/status.test.ts delete mode 100644 src/context/status.ts delete mode 100644 src/context/text.test.ts delete mode 100644 src/context/text.ts delete mode 100644 src/context/xml.test.ts delete mode 100644 src/context/xml.ts create mode 100644 src/core/HttpResponse.test.ts create mode 100644 src/core/HttpResponse.ts rename src/{ => core}/SetupApi.ts (84%) create mode 100644 src/core/bypass.test.ts create mode 100644 src/core/bypass.ts create mode 100644 src/core/delay.ts rename src/{ => core}/graphql.test.ts (100%) create mode 100644 src/core/graphql.ts rename src/{ => core}/handlers/GraphQLHandler.test.ts (72%) rename src/{ => core}/handlers/GraphQLHandler.ts (51%) create mode 100644 src/core/handlers/HttpHandler.test.ts create mode 100644 src/core/handlers/HttpHandler.ts create mode 100644 src/core/handlers/RequestHandler.ts rename src/{rest.spec.ts => core/http.spec.ts} (61%) create mode 100644 src/core/http.ts create mode 100644 src/core/index.ts create mode 100644 src/core/passthrough.test.ts create mode 100644 src/core/passthrough.ts create mode 100644 src/core/sharedOptions.ts rename src/{ => core}/typeUtils.ts (73%) create mode 100644 src/core/utils/HttpResponse/decorators.ts create mode 100644 src/core/utils/getResponse.ts create mode 100644 src/core/utils/handleRequest.test.ts rename src/{ => core}/utils/handleRequest.ts (50%) create mode 100644 src/core/utils/internal/Disposable.ts rename src/{ => core}/utils/internal/checkGlobals.ts (100%) rename src/{ => core}/utils/internal/devUtils.ts (100%) rename src/{ => core}/utils/internal/getCallFrame.test.ts (68%) rename src/{ => core}/utils/internal/getCallFrame.ts (91%) rename src/{ => core}/utils/internal/isIterable.test.ts (100%) rename src/{ => core}/utils/internal/isIterable.ts (100%) rename src/{ => core}/utils/internal/isObject.test.ts (100%) rename src/{ => core}/utils/internal/isObject.ts (100%) rename src/{ => core}/utils/internal/isStringEqual.test.ts (100%) rename src/{ => core}/utils/internal/isStringEqual.ts (100%) rename src/{ => core}/utils/internal/jsonParse.test.ts (100%) rename src/{ => core}/utils/internal/jsonParse.ts (100%) rename src/{ => core}/utils/internal/mergeRight.test.ts (100%) rename src/{ => core}/utils/internal/mergeRight.ts (100%) create mode 100644 src/core/utils/internal/parseGraphQLRequest.test.ts rename src/{ => core}/utils/internal/parseGraphQLRequest.ts (70%) rename src/{ => core}/utils/internal/parseMultipartData.test.ts (100%) rename src/{ => core}/utils/internal/parseMultipartData.ts (100%) rename src/{ => core}/utils/internal/pipeEvents.test.ts (74%) rename src/{ => core}/utils/internal/pipeEvents.ts (100%) rename src/{ => core}/utils/internal/requestHandlerUtils.ts (52%) rename src/{ => core}/utils/internal/toReadonlyArray.test.ts (100%) rename src/{ => core}/utils/internal/toReadonlyArray.ts (100%) rename src/{ => core}/utils/internal/tryCatch.test.ts (100%) rename src/{ => core}/utils/internal/tryCatch.ts (100%) create mode 100644 src/core/utils/internal/uuidv4.ts rename src/{ => core}/utils/logging/getStatusCodeColor.test.ts (100%) rename src/{ => core}/utils/logging/getStatusCodeColor.ts (100%) rename src/{ => core}/utils/logging/getTimestamp.test.ts (100%) rename src/{ => core}/utils/logging/getTimestamp.ts (100%) create mode 100644 src/core/utils/logging/serializeRequest.test.ts create mode 100644 src/core/utils/logging/serializeRequest.ts create mode 100644 src/core/utils/logging/serializeResponse.test.ts create mode 100644 src/core/utils/logging/serializeResponse.ts rename src/{ => core}/utils/matching/matchRequestUrl.test.ts (100%) rename src/{ => core}/utils/matching/matchRequestUrl.ts (96%) rename src/{ => core}/utils/matching/normalizePath.node.test.ts (100%) rename src/{ => core}/utils/matching/normalizePath.test.ts (100%) rename src/{ => core}/utils/matching/normalizePath.ts (100%) create mode 100644 src/core/utils/request/getPublicUrlFromRequest.test.ts create mode 100644 src/core/utils/request/getPublicUrlFromRequest.ts rename src/{ => core}/utils/request/getRequestCookies.node.test.ts (86%) rename src/{ => core}/utils/request/getRequestCookies.test.ts (77%) create mode 100644 src/core/utils/request/getRequestCookies.ts rename src/{ => core}/utils/request/onUnhandledRequest.test.ts (51%) rename src/{ => core}/utils/request/onUnhandledRequest.ts (77%) rename src/{ => core}/utils/request/readResponseCookies.ts (51%) create mode 100644 src/core/utils/toResponseInit.ts rename src/{ => core}/utils/url/cleanUrl.test.ts (100%) rename src/{ => core}/utils/url/cleanUrl.ts (100%) rename src/{ => core}/utils/url/getAbsoluteUrl.node.test.ts (100%) rename src/{ => core}/utils/url/getAbsoluteUrl.test.ts (100%) rename src/{ => core}/utils/url/getAbsoluteUrl.ts (100%) rename src/{ => core}/utils/url/isAbsoluteUrl.test.ts (100%) rename src/{ => core}/utils/url/isAbsoluteUrl.ts (100%) delete mode 100644 src/graphql.ts delete mode 100644 src/handlers/RequestHandler.ts delete mode 100644 src/handlers/RestHandler.test.ts delete mode 100644 src/handlers/RestHandler.ts create mode 100644 src/iife/index.ts delete mode 100644 src/index.ts create mode 100644 src/node/utils/isNodeException.ts delete mode 100644 src/response.ts delete mode 100644 src/rest.ts delete mode 100644 src/setupWorker/start/createFallbackRequestListener.ts delete mode 100644 src/setupWorker/start/createRequestListener.ts delete mode 100644 src/setupWorker/start/utils/streamResponse.ts delete mode 100644 src/sharedOptions.ts delete mode 100644 src/utils/NetworkError.ts delete mode 100644 src/utils/getResponse.ts delete mode 100644 src/utils/handleRequest.test.ts delete mode 100644 src/utils/internal/StrictBroadcastChannel.ts delete mode 100644 src/utils/internal/compose.test.ts delete mode 100644 src/utils/internal/compose.ts delete mode 100644 src/utils/internal/parseGraphQLRequest.test.ts delete mode 100644 src/utils/internal/uuidv4.ts delete mode 100644 src/utils/logging/prepareRequest.test.ts delete mode 100644 src/utils/logging/prepareRequest.ts delete mode 100644 src/utils/logging/prepareResponse.test.ts delete mode 100644 src/utils/logging/prepareResponse.ts delete mode 100644 src/utils/logging/serializeResponse.ts delete mode 100644 src/utils/request/MockedRequest.test.ts delete mode 100644 src/utils/request/MockedRequest.ts delete mode 100644 src/utils/request/createResponseFromIsomorphicResponse.ts delete mode 100644 src/utils/request/getPublicUrlFromRequest.test.ts delete mode 100644 src/utils/request/getPublicUrlFromRequest.ts delete mode 100644 src/utils/request/getRequestCookies.ts delete mode 100644 src/utils/request/parseBody.test.ts delete mode 100644 src/utils/request/parseBody.ts delete mode 100644 src/utils/request/parseWorkerRequest.ts delete mode 100644 src/utils/request/pruneGetRequestBody.test.ts delete mode 100644 src/utils/request/pruneGetRequestBody.ts create mode 100644 test/browser/graphql-api/anonymous-operation.mocks.ts create mode 100644 test/browser/graphql-api/anonymous-operation.test.ts delete mode 100644 test/browser/msw-api/context/async-response-transformer.mocks.ts delete mode 100644 test/browser/msw-api/context/async-response-transformer.test.ts delete mode 100644 test/browser/msw-api/setup-worker/printHandlers.mocks.ts delete mode 100644 test/browser/msw-api/setup-worker/printHandlers.test.ts delete mode 100644 test/browser/rest-api/custom-request-handler.mocks.ts delete mode 100644 test/browser/rest-api/custom-request-handler.test.ts create mode 100644 test/browser/rest-api/plain-response.mocks.ts create mode 100644 test/browser/rest-api/plain-response.test.ts create mode 100644 test/browser/rest-api/response/body/body-blob.mocks.ts create mode 100644 test/browser/rest-api/response/body/body-blob.test.ts create mode 100644 test/browser/rest-api/response/body/body-formdata.mocks.ts create mode 100644 test/browser/rest-api/response/body/body-formdata.test.ts create mode 100644 test/browser/rest-api/response/body/body-stream.mocks.ts create mode 100644 test/browser/rest-api/response/body/body-stream.test.ts create mode 100644 test/browser/rest-api/response/response-error.mocks.ts create mode 100644 test/browser/rest-api/response/response-error.test.ts create mode 100644 test/modules/browser/esm-browser.test.ts create mode 100644 test/modules/browser/playwright.config.ts create mode 100644 test/modules/module-utils.ts create mode 100644 test/modules/node/esm-node.test.ts create mode 100644 test/modules/node/jest.config.js create mode 100644 test/node/graphql-api/anonymous-operations.test.ts delete mode 100644 test/node/msw-api/setup-server/printHandlers.node.test.ts create mode 100644 test/node/regressions/many-request-handlers-jsdom.test.ts create mode 100644 test/node/regressions/many-request-handlers.test.ts create mode 100644 test/node/rest-api/https.node.test.ts create mode 100644 test/node/rest-api/request/body/body-used.node.test.ts rename test/node/rest-api/response/{body => }/body-binary.node.test.ts (75%) create mode 100644 test/node/rest-api/response/body-json.node.test.ts create mode 100644 test/node/rest-api/response/body-stream.node.test.ts rename test/node/rest-api/response/{body => }/body-text.node.test.ts (74%) rename test/node/rest-api/response/{body => }/body-xml.node.test.ts (83%) delete mode 100644 test/node/rest-api/response/body/body-json.node.test.ts create mode 100644 test/node/rest-api/response/response-error.test.ts delete mode 100644 test/typings/path-params.test-d.ts delete mode 100644 test/typings/set.test-d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46c36d33e..1029c106f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,17 +16,17 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - uses: pnpm/action-setup@v2 with: version: 7.12 - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install - name: Unit tests run: pnpm test:unit @@ -46,41 +46,3 @@ jobs: with: name: playwright-report path: test/browser/test-results - - # 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: - ts: ['4.4', '4.5', '4.6', '4.7', '4.8', '4.9', '5.0', '5.1', '5.2'] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 16 - - - uses: pnpm/action-setup@v2 - with: - version: 7.12 - - - name: Install dependencies - run: pnpm install - - - name: Install TypeScript ${{ matrix.ts }} - run: pnpm add typescript@${{ matrix.ts }} - - - name: Build - run: pnpm build - - - name: Typings tests - run: | - pnpm tsc --version - pnpm test:ts diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml new file mode 100644 index 000000000..2cc378ed1 --- /dev/null +++ b/.github/workflows/compat.yml @@ -0,0 +1,78 @@ +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@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Set up pnpm + uses: pnpm/action-setup@v2 + with: + version: 7.12 + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Validate package.json exports + run: pnpm check:exports + + - 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: + ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2'] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2 + with: + version: 7.12 + + - name: Install dependencies + run: pnpm install + + - name: Install TypeScript ${{ matrix.ts }} + run: pnpm add typescript@${{ matrix.ts }} + + - name: Build + run: pnpm build + + - name: Typings tests + run: | + pnpm tsc --version + pnpm test:ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b7796f38..540f3d59f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 always-auth: true registry-url: https://registry.npmjs.org diff --git a/.nvmrc b/.nvmrc index e2838c8b8..e8b25b544 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.14.0 \ No newline at end of file +v18.14.2 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1df787cf6..1d38a6d00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,19 +148,18 @@ Let's write an example integration test that asserts the interception of a GET r ```js // test/browser/example.mocks.ts -import { rest, setupWorker } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/books', (req, res, ctx) => { - return res( - ctx.json([ - { - id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', - title: 'The Lord of the Rings', - publishedAt: -486867600, - }, - ]), - ) + http.get('/books', () => { + return HttpResponse.json([ + { + id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', + title: 'The Lord of the Rings', + publishedAt: -486867600, + }, + ]) }), ) @@ -217,24 +216,23 @@ Let's replicate the same `GET /books` integration test in Node.js. ```ts // test/node/example.test.ts import fetch from 'node-fetch' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('/books', (req, res, ctx) => { - return res( - ctx.json([ - { - id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', - title: 'The Lord of the Rings', - publishedAt: -486867600, - }, - ]), - ) + 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 () => { diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 000000000..8ddc0e003 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,659 @@ +# Migration guide + +This guide will help you migrate from the latest version of MSW to the `next` release that introduces a first-class support for Fetch API primitives to the library. **This is a breaking change**. In fact, this is the biggest change to our public API since the day the library was first published. Do not fret, however, as this is precisely why this document exists. + +## Getting started + +```sh +npm install msw@next --save-dev +``` + +## Table of contents + +To help you navigate, we've structured this guide on the feature basis. You can read it top-to-bottom, or you can jump to a particular feature you have trouble migrating from. + +- [**Imports**](#imports) +- [**Response resolver**](#response-resolver) (call signature change) +- [Request changes](#request-changes) +- [req.params](#reqparams) +- [req.cookies](#request-cookies) +- [req.passthrough](#reqpassthrough) +- [res.once](#resonce) +- [res.networkError](#resnetworkerror) +- [Context utilities](#context-utilities) + - [ctx.status](#ctxstatus) + - [ctx.set](#ctxset) + - [ctx.cookie](#ctxcookie) + - [ctx.body](#ctxbody) + - [ctx.text](#ctxtext) + - [ctx.json](#ctxjson) + - [ctx.xml](#ctxxml) + - [ctx.data](#ctxdata) + - [ctx.errors](#ctxerrors) + - [ctx.delay](#ctxdelay) + - [ctx.fetch](#ctx-fetch) +- [Life-cycle events](#life-cycle-events) +- [`.printHandlers()`](#print-handlers) +- [Advanced](#advanced) +- [**What's new in this release?**](#whats-new) +- [Common issues](#common-issues) + +--- + +## Imports + +### `rest` becomes `http` + +The `rest` request handler namespace has been renamed to `http`. + +```diff +-import { rest } from 'msw' ++import { http } from 'msw' +``` + +This affects the request handlers declaration as well: + +```js +import { http } from 'msw' + +export const handlers = [ + http.get('/resource', resolver), + http.post('/resource', resolver), + http.all('*', resolver), +] +``` + +### Browser imports + +The `setupWorker` API, alongside any related type definitions, are no longer exported from the root of `msw`. Instead, import them from `msw/browser`: + +```diff +-import { setupWorker } from 'msw' ++import { setupWorker } from 'msw/browser' +``` + +> Note that the request handlers like `http` and `graphql`, as well as the utility functions like `bypass` and `passthrough` must still be imported from the root-level `msw`. + +## Response resolver + +A response resolver now exposes a single object argument instead of `(req, res, ctx)`. That argument represents resolver information and consists of properties that are always present for all handler types and extra properties specific to handler types. + +### Resolver info + +#### General + +- `request`, a Fetch API `Request` instance representing an intercepted request. +- `cookies`, a parsed cookies object based on the request cookies. + +#### REST-specific + +- `params`, an object of parsed path parameters. + +#### GraphQL-specific + +- `query`, a GraphQL query string extracted from either URL search parameters or a POST request body. +- `variables`, an object of GraphQL query variables. + +### Using a new signature + +To mock responses, you should now return a Fetch API `Response` instance from the response resolver. You no longer need to compose a response via `res()`, and all the context utilities have also [been removed](#context-utilities). + +```js +http.get('/greet/:name', ({ request, params }) => { + console.log('Intercepted %s %s', request.method, request.url) + return new Response(`hello, ${params.name}!`) +}) +``` + +Now, a more complex example for both REST and GraphQL requests. + +```js +import { http, graphql } from 'msw' + +export const handlers = [ + http.put('/user/:id', async ({ request, params, cookies }) => { + // Read request body as you'd normally do with Fetch. + const payload = await request.json() + // Access path parameters like before. + const { id } = params + // Access cookies like before. + const { sessionId } = cookies + + return new Response(null, { status: 201 }) + }), + + graphql.mutation('CreateUser', ({ request, query, variables }) => { + return new Response( + JSON.stringify({ + data: { + user: { + id: 'abc-123', + firstName: variables.firstName, + }, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }), +] +``` + +### Request changes + +Since the returned `request` is now an instance of Fetch API `Request`, there are some changes to its properties. + +#### Request URL + +The `request.url` property is a string (previously, a `URL` instance). If you wish to operate with it like a `URL`, you need to construct it manually: + +```js +http.get('/product', ({ request }) => { + // For example, this is how you would access + // request search parameters now. + const url = new URL(request.url) + const productId = url.searchParams.get('id') +}) +``` + +#### `req.params` + +Path parameters are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.params`). + +```js +http.get('/resource', ({ params }) => { + console.log('Request path parameters:', params) +}) +``` + +#### `req.cookies` + +Request cookies are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.cookies`). + +```js +http.get('/resource', ({ cookies }) => { + console.log('Request cookies:', cookies) +}) +``` + +#### Request body + +The library now does no assumptions when reading the intercepted request's body (previously, `req.body`). Instead, you are in charge to read the request body as you see appropriate. + +> Note that since the intercepted request is now represented by a Fetch API `Request` instance, its `request.body` property still exists but returns a `ReadableStream`. + +For example, this is how you would read request body: + +```js +http.post('/resource', async ({ request }) => { + const data = await request.json() + // request.formData() / request.arrayBuffer() / etc. +}) +``` + +### Convenient response declarations + +Using the Fetch API `Response` instance may get quite verbose. To give you more convenient means of declaring mocked responses while remaining specification compliant and compatible, the library now exports an `HttpResponse` object. You can use that object to construct response instances faster. + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/user', () => { + // This is synonymous to "ctx.json()": + // HttpResponse.json() stringifies the given body + // and sets the correct "Content-Type" response header + // to describe a JSON response body. + return HttpResponse.json({ firstName: 'John' }) + }), +] +``` + +> Read more on how to use `HttpResponse` to mock [REST API](#rest-response-body-utilities) and [GraphQL API](#graphql-response-body-utilities) responses. + +## Responses in Node.js + +Although MSW now respects the Fetch API specification, the older versions of Node.js do not, so you can't construct a `Response` instance because there is no such global class. + +To account for this, the library exports a `Response` class that you should use when declaring request handlers. Behind the hood, that response class is resolved to a compatible polyfill in Node.js; in the browser, it only aliases `global.Response` without introducing additional behaviors. + +```js +import { http,Response } from 'msw' + +setupServer( + http.get('/ping', () => { + return new Response('hello world) + }) +) +``` + +Relying on a single universal `Response` class will allow you to write request handlers that can run in both browser and Node.js environments. + +## `res.once` + +To create a one-time request handler, pass it an object as the third argument with `once: true` set: + +```js +import { HttpResponse, http } from 'msw' + +export const handlers = [ + http.get( + '/user', + () => { + return HttpResponse.text('hello') + }, + { once: true }, + ), +] +``` + +## `res.networkError` + +To respond to a request with a network error, use the `HttpResponse.error()` static method: + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.error() + }), +] +``` + +> Note that we are dropping support for custom network error messages to be more compliant with the standard [`Response.error()`](https://developer.mozilla.org/en-US/docs/Web/API/Response/error_static) network errors, which don't support custom error messages. + +## `req.passthrough` + +```js +import { http, passthrough } from 'msw' + +export const handlers = [ + http.get('/user', () => { + // Previously, "req.passthrough()". + return passthrough() + }), +] +``` + +--- + +## Context utilities + +Most of the context utilities you'd normally use via `ctx.*` were removed. Instead, we encourage you to set respective properties directly on the response instance: + +```js +import { HttpResponse, http } from 'msw' + +export const handlers = [ + http.post('/user', () => { + // ctx.json() + return HttpResponse.json( + { firstName: 'John' }, + { + status: 201, // ctx.status() + headers: { + 'X-Custom-Header': 'value', // ctx.set() + }, + }, + ) + }), +] +``` + +Let's go through each previously existing context utility and see how to declare its analogue using the `Response` class. + +### `ctx.status` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.text('hello', { status: 201 }) + }), +] +``` + +### `ctx.set` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.text('hello', { + headers: { + 'Content-Type': 'text/plain; charset=windows-1252', + }, + }) + }), +] +``` + +### `ctx.cookie` + +```js +import { HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.text('hello', { + headers: { + 'Set-Cookie': 'token=abc-123', + }, + }) + }), +] +``` + +When you provide an object as the `ResponseInit.headers` value, you cannot specify multiple response cookies with the same name. Instead, to support multiple response cookies, provide a `Headers` instance: + +```js +import { HttpResponse, http } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return new HttpResponse(null, { + headers: new Headers([ + // Mock a multi-value response cookie header. + ['Set-Cookie', 'sessionId=123'], + ['Set-Cookie', 'gtm=en_US'], + ]), + }) + }), +] +``` + +> This is applicable to any multi-value headers, really. + +### `ctx.body` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return new HttpResponse('any-body') + }), +] +``` + +> Do not forget to set the `Content-Type` header that represents the mocked response's body type. If using common response body types, like text or json, see the respective migration instructions for those context utilities below. + +### `ctx.text` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.text('hello') + }), +] +``` + +### `ctx.json` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.json({ firstName: 'John' }) + }), +] +``` + +### `ctx.xml` + +```js +import { http, HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.xml('') + }), +] +``` + +### `ctx.data` + +The `ctx.data` utility has been removed in favor of constructing a mocked JSON response with the "data" property in it. + +```js +import { HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.json({ + data: { + user: { + firstName: 'John', + }, + }, + }) + }), +] +``` + +### `ctx.errors` + +The `ctx.errors` utility has been removed in favor of constructing a mocked JSON response with the "errors" property in it. + +```js +import { HttpResponse } from 'msw' + +export const handlers = [ + http.get('/resource', () => { + return HttpResponse.json({ + errors: [ + { + message: 'Something went wrong', + }, + ], + }) + }), +] +``` + +### `ctx.delay` + +```js +import { http, HttpResponse, delay } from 'msw' + +export const handlers = [ + http.get('/resource', async () => { + await delay() + return HttpResponse.text('hello') + }), +] +``` + +The `delay` function has the same call signature as the `ctx.delay` context function. This means it supports the delay mode as an argument: + +```js +await delay(500) +await delay('infinite') +``` + +### `ctx.fetch` + +The `ctx.fetch()` function has been removed in favor of the `bypass()` function. You should now always perform a regular `fetch()` call and wrap the request in the `bypass()` function if you wish for it to ignore any otherwise matching request handlers. + +```js +import { http, HttpResponse, bypass } from 'msw' + +export const handlers = [ + http.get('/resource', async ({ request }) => { + // Use the regular "fetch" from your environment. + const originalResponse = await fetch(bypass(request)) + const json = await originalResponse.json() + + // ...handle the original response, maybe return a mocked one. + }), +] +``` + +The `bypass()` function also accepts `RequestInit` as the second argument to modify the bypassed request. + +```js +// Bypass the given "request" and modify its headers. +bypass(request, { + headers: { + 'X-Modified-Header': 'true', + }, +}) +``` + +--- + +## Life-cycle events + +The life-cycle events listeners now accept a single argument being an object with contextual properties. + +```diff +-server.events.on('request:start', (request, requestId) = {}) ++server.events.on('request:start', ({ request, requestId}) => {}) +``` + +The request and response instances exposed in the life-cycle API have also been updated to return Fetch API `Request` and `Response` respectively. + +The request ID is now exposed as a standalone argument (previously, `req.id`). + +```js +server.events.on('request:start', ({ request, requestId }) => { + console.log(request.method, request.url) +}) +``` + +To read a request body, make sure to clone the request first. Otherwise, it won't be performed as it would be already read. + +```js +server.events.on('request:match', async ({ request }) => { + // Make sure to clone the request so it could be + // processed further down the line. + const clone = request.clone() + const json = await clone.json() + + console.log('Performed request with body:', json) +}) +``` + +The `response:*` events now always contain the response reference, the related request, and its id in the listener arguments. + +```js +worker.events.on('response:mocked', ({ response, request, requestId }) => { + console.log('response to %s %s is:', request.method, request.url, response) +}) +``` + +--- + +## `.printHandlers() + +The `worker.prinHandlers()` and `server.printHandlers()` methods were removed. Use the `.listHandlers()` method instead: + +```diff +-worker.printHandlers() ++console.log(worker.listHandlers()) +``` + +--- + +## Advanced + +It is still possible to create custom handlers and resolvers, just make sure to account for the new [resolver call signature](#response-resolver). + +### Custom response composition + +As this release removes the concept of response composition via `res()`, you can no longer compose context utilities or abstract their partial composed state to a helper function. + +Instead, you can abstract a common response logic into a plain function that creates a new `Response` or modifies a provided instance. + +```js +// utils.js +import { HttpResponse } from 'msw' + +export function augmentResponse(json) { + const response = HttpResponse.json(json, { + // Come up with some reusable defaults here. + }) + return response +} +``` + +```js +import { http } from 'msw' +import { augmentResponse } from './utils' + +export const handlers = [ + http.get('/user', () => { + return augmentResponse({ id: 1 }) + }), +] +``` + +--- + +## What's new? + +The main benefit of this release is the adoption of Fetch API primitives—`Request` and `Response` classes. By handling requests and responses as the platform does it, you bring your API mocking setup to the next level. Less library-specific abstractions, flatter learning curve, improved compatibility with other tools. But, most importantly, specification compliance and investment into a solution that uses standard APIs that are here to stay. + +### New request body methods + +You can now read the intercepted request body as you would a regular `Request` instance. This mainly means the addition of the following methods on the `request`: + +- `request.blob()` +- `request.formData()` +- `request.arrayBuffer()` + +For example, this is how you would read the request as `Blob`: + +```js +import { http } from 'msw' + +export const handlers = [ + http.get('/resource', async ({ request }) => { + const blob = await request.blob() + }), +] +``` + +### Support `ReadableStream` mocked responses + +You can now send a `ReadableStream` as the mocked response body. This is great for mocking any kind of streaming in HTTP responses. + +```js +import { http, HttpResponse, delay } from 'msw' + +http.get('/greeting', () => { + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode('hello')) + await delay(100) + controller.enqueue(encoder.encode('world')) + await delay(100) + controller.close() + }, + }) + + return new HttpResponse(stream) +}) +``` + +--- + +## Common issues + +### `Response is not defined` + +This likely means that you are running an old version of Node.js. Please use Node.js v18.14.0 and higher with this version of MSW. Also, see [this](#responses-in-nodejs). + +### `multipart/form-data is not supported` in Node.js + +Earlier versions of Node.js 18, like v18.8.0, had no support for `request.formData()`. Please upgrade to the latest Node.js version where Undici have added the said support to resolve the issue. diff --git a/README.md b/README.md index 225664989..38edf3573 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> [!IMPORTANT]\ +> **MSW 2.0 is finally here! 🎉** Read the [Release notes](https://github.com/mswjs/msw/releases/tag/v2.0.0) and please follow the [**Migration guidelines**](https://mswjs.io/docs/migrations/1.x-to-2.x) to upgrade. If you're having any questions while upgrading, please reach out in our [Discord server](https://kettanaito.com/discord). +

@@ -25,7 +28,7 @@ - **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 capture requests. Use parameters, wildcards, and regular expressions to match requests, and respond with necessary status codes, headers, cookies, delays, or completely custom resolvers. +- **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. --- @@ -38,8 +41,7 @@ This README will give you a brief overview on the library but there's no better place to start with Mock Service Worker than its official documentation. - [Documentation](https://mswjs.io/docs) -- [**Getting started**](https://mswjs.io/docs/getting-started/install) -- [Recipes](https://mswjs.io/docs/recipes) +- [**Getting started**](https://mswjs.io/docs/getting-started) - [FAQ](https://mswjs.io/docs/faq) ## Examples @@ -48,12 +50,12 @@ This README will give you a brief overview on the library but there's no better ## Browser -- [Learn more about using MSW in a browser](https://mswjs.io/docs/getting-started/integrate/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 captured requests with your mock definition on the network level. This way your application knows nothing about the mocking. +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:** @@ -71,17 +73,20 @@ In-browser usage is what sets Mock Service Worker apart from other tools. Utiliz ```js // src/mocks.js // 1. Import the library. -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' // 2. Describe network behavior with request handlers. const worker = setupWorker( - rest.get('https://github.com/octocat', (req, res, ctx) => { - return res( - ctx.delay(1500), - ctx.status(202, 'Mocked status'), - ctx.json({ - message: 'Mocked response JSON body', - }), + http.get('https://github.com/octocat', ({ request, params, cookies }) => { + return HttpResponse.json( + { + message: 'Mocked response', + }, + { + status: 202, + statusText: 'Mocked status', + }, ) }), ) @@ -98,7 +103,7 @@ Performing a `GET https://github.com/octocat` request in your application will r ## Node.js -- [Learn more about using MSW in Node.js](https://mswjs.io/docs/getting-started/integrate/node) +- [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? @@ -118,7 +123,7 @@ Take a look at the example of an integration test in Jest that uses [React Testi // test/Dashboard.test.js import React from 'react' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { render, screen, waitFor } from '@testing-library/react' import Dashboard from '../src/components/Dashboard' @@ -127,19 +132,17 @@ const server = setupServer( // Describe network behavior with request handlers. // Tip: move the handlers into their own module and // import it across your browser and Node.js setups! - rest.get('/posts', (req, res, ctx) => { - return res( - ctx.json([ - { - id: 'f8dd058f-9006-4174-8d49-e3086bc39c21', - title: `Avoid Nesting When You're Testing`, - }, - { - id: '8ac96078-6434-4959-80ed-cc834e7fef61', - title: `How I Built A Modern Website In 2021`, - }, - ]), - ) + http.get('/posts', ({ request, params, cookies }) => { + return HttpResponse.json([ + { + id: 'f8dd058f-9006-4174-8d49-e3086bc39c21', + title: `Avoid Nesting When You're Testing`, + }, + { + id: '8ac96078-6434-4959-80ed-cc834e7fef61', + title: `How I Built A Modern Website In 2021`, + }, + ]) }), ) @@ -174,7 +177,7 @@ it('displays the list of recent posts', async () => { }) ``` -> Don't get overwhelmed! We've prepared a step-by-step [**Getting started**](https://mswjs.io/docs/getting-started/install) tutorial that you can follow to learn how to integrate Mock Service Worker into your project. +> Don't get overwhelmed! We've prepared a step-by-step [**Getting started**](https://mswjs.io/docs/getting-started) tutorial that you can follow to learn how to integrate Mock Service Worker into your project. Despite the API being called `setupServer`, there are no actual servers involved! The name was chosen for familiarity, and the API was designed to resemble operating with an actual server. diff --git a/browser/package.json b/browser/package.json new file mode 100644 index 000000000..d29733135 --- /dev/null +++ b/browser/package.json @@ -0,0 +1,5 @@ +{ + "main": "../lib/browser/index.js", + "module": "../lib/browser/index.mjs", + "types": "../lib/browser/index.d.ts" +} diff --git a/cli/init.js b/cli/init.js index 16a0f312f..aed4f8600 100755 --- a/cli/init.js +++ b/cli/init.js @@ -21,14 +21,14 @@ module.exports = async function init(args) { if (!dirExists) { // Try to create the directory if it doesn't exist - const [createDirectoryError] = await until(() => + const createDirectoryResult = await until(() => fs.promises.mkdir(absolutePublicDir, { recursive: true }), ) invariant( - createDirectoryError == null, + createDirectoryResult.error == null, 'Failed to create a Service Worker at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the root directory of your server.\n\nSee the original error below:\n%s', absolutePublicDir, - createDirectoryError, + createDirectoryResult.error, ) } diff --git a/config/constants.js b/config/constants.js index e38940945..ba32bc9af 100644 --- a/config/constants.js +++ b/config/constants.js @@ -2,8 +2,7 @@ const path = require('path') const SERVICE_WORKER_SOURCE_PATH = path.resolve( __dirname, - '../', - 'src/mockServiceWorker.js', + '../src/mockServiceWorker.js', ) const SERVICE_WORKER_BUILD_PATH = path.resolve( diff --git a/config/copyServiceWorker.ts b/config/copyServiceWorker.ts index 264ed4da2..7058b36cb 100644 --- a/config/copyServiceWorker.ts +++ b/config/copyServiceWorker.ts @@ -1,10 +1,7 @@ import * as fs from 'fs' import * as path from 'path' -import chalk from 'chalk' import { until } from '@open-draft/until' -const { cyan } = chalk - /** * Copies the given Service Worker source file into the destination. * Injects the integrity checksum into the destination file. @@ -16,11 +13,11 @@ export default async function copyServiceWorker( ): Promise { console.log('Compiling Service Worker...') - const [readError, fileContent] = await until(() => + const readFileResult = await until(() => fs.promises.readFile(sourceFilePath, 'utf8'), ) - if (readError) { + if (readFileResult.error) { throw new Error('Failed to read file.\n${readError.message}') } @@ -36,17 +33,17 @@ export default async function copyServiceWorker( fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'), ) - const nextFileContent = fileContent + const nextFileContent = readFileResult.data .replace('', checksum) .replace('', packageJson.version) - const [writeFileError] = await until(() => + const writeFileResult = await until(() => fs.promises.writeFile(destFilePath, nextFileContent), ) - if (writeFileError) { - throw new Error(`Failed to write file.\n${writeFileError.message}`) + if (writeFileResult.error) { + throw new Error(`Failed to write file.\n${writeFileResult.error.message}`) } - console.log('Service Worker copied to: %s', cyan(destFilePath)) + console.log('Service Worker copied to: %s', destFilePath) } diff --git a/config/plugins/esbuild/copyWorkerPlugin.ts b/config/plugins/esbuild/copyWorkerPlugin.ts new file mode 100644 index 000000000..720671b0c --- /dev/null +++ b/config/plugins/esbuild/copyWorkerPlugin.ts @@ -0,0 +1,80 @@ +import fs from 'fs' +import path from 'path' +import crypto from 'crypto' +import minify from 'babel-minify' +import { invariant } from 'outvariant' +import type { Plugin } from 'esbuild' +import copyServiceWorker from '../../copyServiceWorker' + +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, + {}, + { + // @ts-ignore "babel-minify" has no type definitions. + 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(() => { + 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, + ) + }) + }) + }, + } +} diff --git a/config/plugins/esbuild/forceEsmExtensionsPlugin.ts b/config/plugins/esbuild/forceEsmExtensionsPlugin.ts new file mode 100644 index 000000000..61e007409 --- /dev/null +++ b/config/plugins/esbuild/forceEsmExtensionsPlugin.ts @@ -0,0 +1,54 @@ +import { Plugin } from 'esbuild' + +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 || []) { + 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 ? '.mjs' : '.js' + 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}` + }, + ) +} diff --git a/config/plugins/esbuild/resolveCoreImportsPlugin.ts b/config/plugins/esbuild/resolveCoreImportsPlugin.ts new file mode 100644 index 000000000..1aa30ee8e --- /dev/null +++ b/config/plugins/esbuild/resolveCoreImportsPlugin.ts @@ -0,0 +1,24 @@ +import { Plugin } from 'esbuild' + +const { replaceCoreImports } = require('../../replaceCoreImports') + +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('.mjs') + const fileContents = outputFile.text + const nextFileContents = replaceCoreImports(fileContents, isEsm) + + outputFile.contents = Buffer.from(nextFileContents) + } + }) + }, + } +} diff --git a/config/plugins/esbuild/workerScriptPlugin.ts b/config/plugins/esbuild/workerScriptPlugin.ts deleted file mode 100644 index f2c5885b7..000000000 --- a/config/plugins/esbuild/workerScriptPlugin.ts +++ /dev/null @@ -1,82 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' -import crypto from 'crypto' -import minify from 'babel-minify' -import { invariant } from 'outvariant' -import type { Plugin } from 'esbuild' -import copyServiceWorker from '../../copyServiceWorker' - -function getChecksum(contents: string): string { - const { code } = minify( - contents, - {}, - { - // @ts-ignore "babel-minify" has no type definitions. - comments: false, - }, - ) - - return crypto.createHash('md5').update(code, 'utf8').digest('hex') -} - -let hasRunAlready = false - -export function workerScriptPlugin(): Plugin { - return { - name: 'workerScriptPlugin', - async setup(build) { - const workerSourcePath = path.resolve( - process.cwd(), - './src/mockServiceWorker.js', - ) - const workerOutputPath = path.resolve( - process.cwd(), - './lib/mockServiceWorker.js', - ) - - invariant( - workerSourcePath, - 'Failed to locate the worker script source file', - ) - invariant( - workerOutputPath, - 'Failed to locate the worker script output file', - ) - - // Generate the checksum from the worker script's contents. - const workerContents = await fs.readFile(workerSourcePath, 'utf8') - const checksum = getChecksum(workerContents) - - // Inject the global "SERVICE_WORKER_CHECKSUM" variable - // for runtime worker integrity check. - build.initialOptions.define = { - SERVICE_WORKER_CHECKSUM: JSON.stringify(checksum), - } - - // Prevent from copying the worker script multiple times. - // esbuild will execute this plugin for *each* format. - if (hasRunAlready) { - return - } - - hasRunAlready = true - - 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(() => { - console.log('worker script checksum:', checksum) - - // Copy the worker script on the next tick. - setTimeout(async () => { - await copyServiceWorker(workerSourcePath, workerOutputPath, checksum) - }, 100) - }) - }, - } -} diff --git a/config/replaceCoreImports.js b/config/replaceCoreImports.js new file mode 100644 index 000000000..3087485cf --- /dev/null +++ b/config/replaceCoreImports.js @@ -0,0 +1,21 @@ +function replaceCoreImports(fileContents, isEsm) { + const importPattern = isEsm + ? /from ["'](~\/core(.*))["'](;)?$/gm + : /require\(["'](~\/core(.*))["']\)(;)?/gm + + return fileContents.replace( + importPattern, + (_, __, maybeSubmodulePath, maybeSemicolon) => { + const submodulePath = maybeSubmodulePath || '/index' + const semicolon = maybeSemicolon || '' + + return isEsm + ? `from "../core${submodulePath}"${semicolon}` + : `require("../core${submodulePath}")${semicolon}` + }, + ) +} + +module.exports = { + replaceCoreImports, +} diff --git a/config/scripts/patch-ts.js b/config/scripts/patch-ts.js new file mode 100644 index 000000000..fa25c7895 --- /dev/null +++ b/config/scripts/patch-ts.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') +const { replaceCoreImports } = require('../replaceCoreImports') + +async function patchTypeDefs() { + const typeDefsPaths = [ + path.resolve(__dirname, '../..', 'lib/browser/index.d.ts'), + path.resolve(__dirname, '../..', 'lib/node/index.d.ts'), + path.resolve(__dirname, '../..', 'lib/native/index.d.ts'), + ] + + for (const typeDefsPath of typeDefsPaths) { + if (!fs.existsSync(typeDefsPath)) { + continue + } + + const fileContents = fs.readFileSync(typeDefsPath, 'utf8') + + // Treat ".d.ts" files as ESM to replace "import" statements. + // Force no extension on the ".d.ts" imports. + const nextFileContents = replaceCoreImports(fileContents, true) + + fs.writeFileSync(typeDefsPath, nextFileContents, 'utf8') + + console.log('Successfully patched at "%s"!', typeDefsPath) + } +} + +patchTypeDefs() diff --git a/config/scripts/validate-esm.js b/config/scripts/validate-esm.js new file mode 100644 index 000000000..cc473820b --- /dev/null +++ b/config/scripts/validate-esm.js @@ -0,0 +1,247 @@ +const fs = require('fs') +const path = require('path') +const { invariant } = require('outvariant') + +const ROOT_PATH = path.resolve(__dirname, '../..') + +function fromRoot(...paths) { + return path.resolve(ROOT_PATH, ...paths) +} + +const PKG_JSON_PATH = fromRoot('package.json') +const PKG_JSON = require(PKG_JSON_PATH) + +function validatePackageExports() { + const { exports } = PKG_JSON + + // Validate the "main", "browser", and "types" root fields. + invariant('main' in PKG_JSON, 'Missing "main" field in package.json') + invariant('module' in PKG_JSON, 'Missing "module" field in package.json') + invariant('types' in PKG_JSON, 'Missing "types" field in package.json') + + invariant( + fs.existsSync(fromRoot(PKG_JSON.main)), + 'The "main" field points at a non-existing path at "%s"', + PKG_JSON.main, + ) + + // The "exports" key must be present. + invariant(exports, 'package.json must have an "exports" field') + + // The "exports" must list expected paths. + const expectedExportPaths = [ + '.', + './browser', + './node', + './package.json', + './native', + ] + expectedExportPaths.forEach((exportPath) => { + invariant(exportPath in exports, 'Missing exports path "%s"', exportPath) + }) + + // Must describe the root export properly. + const rootExport = exports['.'] + + validateExportConditions(`exports['.']`, rootExport) + validateBundle(rootExport.require, false) + validateBundle(rootExport.import, true) + validateTypeDefs(rootExport.types) + + // Validate "./browser" exports. + const browserExports = exports['./browser'] + validateExportConditions(`exports['./browser']`, browserExports) + invariant( + browserExports.node === null, + 'The "browser" export must set the "node" field to null', + ) + validateBundle(browserExports.require, false) + validateBundle(browserExports.import, true) + validateTypeDefs(browserExports.types) + + // Validate "./node" exports. + const nodeExports = exports['./node'] + validateExportConditions(`exports['./node']`, nodeExports) + invariant( + nodeExports.browser === null, + 'The "node" export must set the "browser" field to null', + ) + validateBundle(nodeExports.require, false) + validateBundle(nodeExports.import, true) + validateTypeDefs(nodeExports.types) + + // Validate "./native" exports. + const nativeExports = exports['./native'] + validateExportConditions(`exports['./native']`, nativeExports) + invariant( + nativeExports.browser === null, + 'The "native" export must set the "browser" field to null', + ) + validateBundle(nativeExports.require, false) + validateBundle(nativeExports.import, true) + validateTypeDefs(nativeExports.types) + + // Validate "./package.json" exports. + validateExportConditions( + `exports['./package.json]`, + exports['./package.json'], + ) + + console.log('✅ Validated package.json exports') +} + +function validateExportConditions(pointer, conditions) { + if (typeof conditions === 'string') { + invariant( + fs.existsSync(conditions), + 'Expected a valid path at "%s" but got %s', + pointer, + conditions, + ) + return + } + + const keys = Object.keys(conditions) + + if (conditions[keys[0]] !== null) { + invariant(keys[0] === 'types', 'FS') + } + + // Ensure that paths point to existing files. + keys.forEach((key) => { + const relativeExportPath = conditions[key] + + if (relativeExportPath === null) { + return + } + + const exportPath = fromRoot(relativeExportPath) + invariant( + fs.existsSync(exportPath), + 'Expected the path at "%s" ("%s") to point at existing file but got %s', + pointer, + key, + exportPath, + ) + }) +} + +const ESM_CORE_IMPORT_EXP = /from ["'](..\/)+core(.*)["'];?$/gm +const CJS_CORE_IMPORT_EXP = /require\(["'](..\/)+core(.*)["']\);?$/gm + +function getCodeSnippetAt(contents, index) { + return contents.slice(index - 100, index + 50) +} + +function validateBundle(bundlePath, isEsm = false) { + const expectedExtension = isEsm ? '.mjs' : '.js' + + invariant( + bundlePath.endsWith(expectedExtension), + 'Failed to validate bundle: provided bundle path does not point at an ".mjs" file: %s', + bundlePath, + ) + + const absoluteBundlePath = fromRoot(bundlePath) + const contents = fs.readFileSync(absoluteBundlePath, 'utf8') + + // The "~/core" imports must be overwritten on the bundler level. + invariant( + !contents.includes('~/core'), + 'Bundle at "%s" includes unresolved "~/core" imports:\n\n%s', + bundlePath, + getCodeSnippetAt(contents, contents.indexOf('~/core')), + ) + + // The "core" imports must end with the explicit ".mjs" extension. + const coreImportsMatches = + contents.matchAll(isEsm ? ESM_CORE_IMPORT_EXP : CJS_CORE_IMPORT_EXP) || [] + + for (const match of coreImportsMatches) { + const [, backslashes, relativeImportPath] = match + + invariant( + backslashes === '../', + 'Found a "core" import with incorrect nesting level', + ) + + invariant( + relativeImportPath !== '', + 'Found a "core" import without an explicit path at "%s":\n\n%s', + absoluteBundlePath, + getCodeSnippetAt(contents, match.index), + ) + + if (isEsm) { + // Ensure that all relative imports in the ESM bundle end with ".mjs". + // This way bundlers can distinguish between the referenced modules + // since the "core" directory contains both ".js" and ".mjs" modules on the same level. + invariant( + relativeImportPath.endsWith('.mjs'), + `Found a "core" import without "${expectedExtension}" extension at "%s":\n\n%s`, + absoluteBundlePath, + getCodeSnippetAt(contents, match.index), + ) + } + } + + console.log('✅ Validated bundle at "%s"', bundlePath) +} + +function validateTypeDefs(typeDefsPath) { + const absoluteTypeDefsPath = fromRoot(typeDefsPath) + invariant( + fs.existsSync(absoluteTypeDefsPath), + 'Failed to validate type definitions at "%s": file does not exist', + absoluteTypeDefsPath, + ) + + const contents = fs.readFileSync(absoluteTypeDefsPath, 'utf8') + + // The "~/core" imports must also be replaced with relative paths on build. + invariant( + !contents.includes('~/core'), + 'Found unresolved "~/core" imports at "%s":\n\n%s', + absoluteTypeDefsPath, + getCodeSnippetAt(contents, contents.indexOf('~/core')), + ) + + console.log('✅ Validated type definitions at "%s"', typeDefsPath) +} + +function validatePackageFiles() { + const { files } = PKG_JSON + + const expectedFiles = [ + 'config/constants.js', + 'config/scripts/postinstall.js', + 'cli', + 'lib', + 'browser', + 'node', + 'native', + ] + + // Must list all the expcted files. + expectedFiles.forEach((expectedFile) => { + invariant( + files.includes(expectedFile), + '"%s" is not listed in "files" in package.json', + expectedFile, + ) + }) + + // All the listed files must exist. + expectedFiles.every((expectedFile) => { + invariant( + fs.existsSync(fromRoot(expectedFile)), + 'The file "%s" in "files" points at non-existing file', + expectedFile, + ) + }) + + console.log('✅ Validated package.json "files" field') +} + +validatePackageExports() +validatePackageFiles() diff --git a/global.d.ts b/global.d.ts index 8976bba1f..e8970504b 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1 +1,10 @@ declare const SERVICE_WORKER_CHECKSUM: string + +declare module '@bundled-es-modules/cookie' { + export * as default from 'cookie' +} + +declare module '@bundled-es-modules/statuses' { + import * as statuses from 'statuses' + export default statuses +} diff --git a/jest.config.js b/jest.config.js index 68450831a..6feb897fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,4 +7,7 @@ module.exports = { testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(j|t)sx?$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], setupFiles: ['./jest.setup.js'], + moduleNameMapper: { + '^~/core(/.*)?$': '/src/core/$1', + }, } diff --git a/jest.setup.js b/jest.setup.js index 50eb416a3..318a499b0 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,13 +1,27 @@ -const fetch = require('node-fetch') +const { TextEncoder, TextDecoder } = require('util') -if (typeof window !== 'undefined') { - // Provide "Headers" to be accessible in test cases - // since they are not, by default. - Object.defineProperty(window, 'Headers', { - writable: true, - value: fetch.Headers, - }) +/** + * @note Temporary global polyfills for Jest because it's + * ignoring Node.js defaults. + */ +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, +}) + +const { Blob } = require('buffer') +const { Request, Response, Headers, File, FormData } = require('undici') +Object.defineProperties(globalThis, { + Headers: { value: Headers }, + Request: { value: Request }, + Response: { value: Response }, + File: { value: File }, + Blob: { value: Blob }, + FormData: { value: FormData }, +}) + +if (typeof window !== 'undefined') { Object.defineProperty(navigator || {}, 'serviceWorker', { writable: false, value: { diff --git a/native/package.json b/native/package.json index bb12dd1d1..a44c5d0e4 100644 --- a/native/package.json +++ b/native/package.json @@ -1,5 +1,6 @@ { + "browser": null, "main": "../lib/native/index.js", "module": "../lib/native/index.mjs", - "typings": "../lib/native/index.d.ts" + "types": "../lib/native/index.d.ts" } diff --git a/node/package.json b/node/package.json index 3d1591115..395e07575 100644 --- a/node/package.json +++ b/node/package.json @@ -1,5 +1,6 @@ { + "browser": null, "main": "../lib/node/index.js", "module": "../lib/node/index.mjs", - "typings": "../lib/node/index.d.ts" + "types": "../lib/node/index.d.ts" } diff --git a/package.json b/package.json index 20d9c42ba..8aec4edaa 100644 --- a/package.json +++ b/package.json @@ -2,39 +2,60 @@ "name": "msw", "version": "1.3.2", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", - "main": "./lib/index.js", - "types": "./lib/index.d.ts", + "main": "./lib/core/index.js", + "module": "./lib/core/index.mjs", + "types": "./lib/core/index.d.ts", "packageManager": "pnpm@7.12.2", "exports": { ".": { - "default": "./lib/index.js" + "types": "./lib/core/index.d.ts", + "require": "./lib/core/index.js", + "import": "./lib/core/index.mjs", + "default": "./lib/core/index.js" }, - "./native": { - "types": "./lib/native/index.d.ts", - "default": "./lib/native/index.js" + "./browser": { + "node": null, + "types": "./lib/browser/index.d.ts", + "require": "./lib/browser/index.js", + "import": "./lib/browser/index.mjs", + "default": "./lib/browser/index.js" }, "./node": { + "browser": null, "types": "./lib/node/index.d.ts", "require": "./lib/node/index.js", + "import": "./lib/node/index.mjs", "default": "./lib/node/index.mjs" }, + "./native": { + "browser": null, + "types": "./lib/native/index.d.ts", + "require": "./lib/native/index.js", + "import": "./lib/native/index.mjs", + "default": "./lib/native/index.js" + }, "./package.json": "./package.json" }, "bin": { "msw": "cli/index.js" }, "engines": { - "node": ">=14" + "node": ">=18" }, "scripts": { "start": "tsup --watch", "clean": "rimraf ./lib", "lint": "eslint \"{cli,config,src,test}/**/*.ts\"", - "build": "cross-env NODE_ENV=production tsup", + "prebuild": "rimraf ./lib", + "build": "pnpm clean && cross-env NODE_ENV=production tsup && pnpm patch:dts", + "patch:dts": "node \"./config/scripts/patch-ts.js\"", + "check:exports": "node \"./config/scripts/validate-esm.js\"", "test": "pnpm test:unit && pnpm test:node && pnpm test:browser", "test:unit": "cross-env BABEL_ENV=test jest --maxWorkers=3", - "test:node": "jest --config=./test/jest.config.js", + "test:node": "jest --config=./test/jest.config.js --forceExit", "test:browser": "playwright test -c ./test/browser/playwright.config.ts", + "test:modules:node": "jest --config=./test/modules/node/jest.config.js", + "test:modules:browser": "playwright test -c ./test/modules/browser/playwright.config.ts", "test:ts": "ts-node test/typings/run.ts", "prepare": "pnpm simple-git-hooks init", "prepack": "pnpm build", @@ -68,8 +89,9 @@ "config/scripts/postinstall.js", "cli", "lib", - "native", + "browser", "node", + "native", "LICENSE.md", "README.md" ], @@ -88,23 +110,27 @@ ], "sideEffects": false, "dependencies": { - "@mswjs/cookies": "^0.2.2", - "@mswjs/interceptors": "^0.17.10", - "@open-draft/until": "^1.0.3", + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/js-levenshtein": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@mswjs/cookies": "^1.0.0", + "@mswjs/interceptors": "^0.25.1", + "@open-draft/until": "^2.1.0", "@types/cookie": "^0.4.1", "@types/js-levenshtein": "^1.1.1", - "chalk": "^4.1.1", + "@types/statuses": "^2.0.1", + "chalk": "^4.1.2", "chokidar": "^3.4.2", - "cookie": "^0.4.2", + "formdata-node": "4.4.1", "graphql": "^16.8.1", - "headers-polyfill": "3.2.5", + "headers-polyfill": "^4.0.1", "inquirer": "^8.2.0", "is-node-process": "^1.2.0", "js-levenshtein": "^1.1.6", "node-fetch": "^2.6.7", "outvariant": "^1.4.0", "path-to-regexp": "^6.2.0", - "strict-event-emitter": "^0.4.3", + "strict-event-emitter": "^0.5.0", "type-fest": "^2.19.0", "yargs": "^17.3.1" }, @@ -120,19 +146,22 @@ "@swc/jest": "^0.2.24", "@types/express": "^4.17.17", "@types/fs-extra": "^9.0.13", + "@types/glob": "^8.1.0", "@types/jest": "^29.4.0", "@types/json-bigint": "^1.0.1", - "@types/node": "^14.14.31", + "@types/node": "18.x", "@types/node-fetch": "^2.5.11", "@types/puppeteer": "^5.4.4", "@typescript-eslint/eslint-plugin": "^5.11.0", "@typescript-eslint/parser": "^5.11.0", + "@web/dev-server": "^0.1.38", "babel-loader": "^8.2.3", "babel-minify": "^0.5.1", "commitizen": "^4.2.4", "cross-env": "^7.0.3", "cross-fetch": "^3.1.5", "cz-conventional-changelog": "3.3.0", + "esbuild": "^0.17.15", "esbuild-loader": "^2.21.0", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", @@ -140,26 +169,27 @@ "express": "^4.18.2", "fs-extra": "^10.0.0", "fs-teardown": "^0.3.0", + "glob": "^9.3.4", "jest": "^29.4.3", "jest-environment-jsdom": "^29.4.3", "json-bigint": "^1.0.0", "lint-staged": "^13.0.3", - "page-with": "^0.5.0", + "page-with": "^0.6.1", "prettier": "^2.7.1", "regenerator-runtime": "^0.13.9", "rimraf": "^3.0.2", "simple-git-hooks": "^2.8.0", - "statuses": "^2.0.0", "ts-node": "^10.9.1", - "tsup": "^5.12.8", + "tsup": "^6.7.0", "typescript": "^5.0.2", + "undici": "^5.20.0", "url-loader": "^4.1.1", "webpack": "^5.68.0", "webpack-dev-server": "^3.11.2", "webpack-http-server": "^0.5.0" }, "peerDependencies": { - "typescript": ">= 4.4.x <= 5.2.x" + "typescript": ">= 4.7.x <= 5.2.x" }, "peerDependenciesMeta": { "typescript": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05e02d57c..84896c459 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,12 +6,15 @@ overrides: specifiers: '@babel/core': ^7.17.2 '@babel/preset-env': ^7.16.11 + '@bundled-es-modules/cookie': ^2.0.0 + '@bundled-es-modules/js-levenshtein': ^2.0.1 + '@bundled-es-modules/statuses': ^1.0.1 '@commitlint/cli': ^16.1.0 '@commitlint/config-conventional': ^16.0.0 - '@mswjs/cookies': ^0.2.2 - '@mswjs/interceptors': ^0.17.10 + '@mswjs/cookies': ^1.0.0 + '@mswjs/interceptors': ^0.25.1 '@open-draft/test-server': ^0.4.2 - '@open-draft/until': ^1.0.3 + '@open-draft/until': ^2.1.0 '@ossjs/release': ^0.8.0 '@playwright/test': ^1.30.0 '@swc/core': ^1.3.35 @@ -19,32 +22,37 @@ specifiers: '@types/cookie': ^0.4.1 '@types/express': ^4.17.17 '@types/fs-extra': ^9.0.13 + '@types/glob': ^8.1.0 '@types/jest': ^29.4.0 '@types/js-levenshtein': ^1.1.1 '@types/json-bigint': ^1.0.1 - '@types/node': ^14.14.31 + '@types/node': 18.x '@types/node-fetch': ^2.5.11 '@types/puppeteer': ^5.4.4 + '@types/statuses': ^2.0.1 '@typescript-eslint/eslint-plugin': ^5.11.0 '@typescript-eslint/parser': ^5.11.0 + '@web/dev-server': ^0.1.38 babel-loader: ^8.2.3 babel-minify: ^0.5.1 - chalk: ^4.1.1 + chalk: ^4.1.2 chokidar: 3.4.1 commitizen: ^4.2.4 - cookie: ^0.4.2 cross-env: ^7.0.3 cross-fetch: ^3.1.5 cz-conventional-changelog: 3.3.0 + esbuild: ^0.17.15 esbuild-loader: ^2.21.0 eslint: ^7.30.0 eslint-config-prettier: ^8.3.0 eslint-plugin-prettier: ^3.4.0 express: ^4.18.2 + formdata-node: 4.4.1 fs-extra: ^10.0.0 fs-teardown: ^0.3.0 + glob: ^9.3.4 graphql: ^16.8.1 - headers-polyfill: 3.2.5 + headers-polyfill: ^4.0.1 inquirer: ^8.2.0 is-node-process: ^1.2.0 jest: ^29.4.3 @@ -54,18 +62,18 @@ specifiers: lint-staged: ^13.0.3 node-fetch: ^2.6.7 outvariant: ^1.4.0 - page-with: ^0.5.0 + page-with: ^0.6.1 path-to-regexp: ^6.2.0 prettier: ^2.7.1 regenerator-runtime: ^0.13.9 rimraf: ^3.0.2 simple-git-hooks: ^2.8.0 - statuses: ^2.0.0 - strict-event-emitter: ^0.4.3 + strict-event-emitter: ^0.5.0 ts-node: ^10.9.1 - tsup: ^5.12.8 + tsup: ^6.7.0 type-fest: ^2.19.0 typescript: ^5.0.2 + undici: ^5.20.0 url-loader: ^4.1.1 webpack: ^5.68.0 webpack-dev-server: ^3.11.2 @@ -73,23 +81,27 @@ specifiers: yargs: ^17.3.1 dependencies: - '@mswjs/cookies': 0.2.2 - '@mswjs/interceptors': 0.17.10 - '@open-draft/until': 1.0.3 + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/js-levenshtein': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@mswjs/cookies': 1.0.0 + '@mswjs/interceptors': 0.25.1 + '@open-draft/until': 2.1.0 '@types/cookie': 0.4.1 '@types/js-levenshtein': 1.1.1 - chalk: 4.1.1 + '@types/statuses': 2.0.1 + chalk: 4.1.2 chokidar: 3.4.1 - cookie: 0.4.2 + formdata-node: 4.4.1 graphql: 16.8.1 - headers-polyfill: 3.2.5 + headers-polyfill: 4.0.1 inquirer: 8.2.5 is-node-process: 1.2.0 js-levenshtein: 1.1.6 node-fetch: 2.6.9 outvariant: 1.4.0 path-to-regexp: 6.2.1 - strict-event-emitter: 0.4.6 + strict-event-emitter: 0.5.0 type-fest: 2.19.0 yargs: 17.7.0 @@ -105,19 +117,22 @@ devDependencies: '@swc/jest': 0.2.24_@swc+core@1.3.35 '@types/express': 4.17.17 '@types/fs-extra': 9.0.13 + '@types/glob': 8.1.0 '@types/jest': 29.4.0 '@types/json-bigint': 1.0.1 - '@types/node': 14.18.36 + '@types/node': 18.17.14 '@types/node-fetch': 2.6.2 '@types/puppeteer': 5.4.7 '@typescript-eslint/eslint-plugin': 5.52.0_aaw67h7nkydj3qj4plp2jqjmxe '@typescript-eslint/parser': 5.52.0_jeuwjvsopuo23ctsjsevxmvjsi + '@web/dev-server': 0.1.38 babel-loader: 8.3.0_la66t7xldg4uecmyawueag5wkm babel-minify: 0.5.2 commitizen: 4.3.0_@swc+core@1.3.35 cross-env: 7.0.3 cross-fetch: 3.1.5 cz-conventional-changelog: 3.3.0_@swc+core@1.3.35 + esbuild: 0.17.19 esbuild-loader: 2.21.0_webpack@5.75.0 eslint: 7.32.0 eslint-config-prettier: 8.6.0_eslint@7.32.0 @@ -125,26 +140,35 @@ devDependencies: express: 4.18.2 fs-extra: 10.1.0 fs-teardown: 0.3.2 - jest: 29.4.3_nw6xvwuzmqp7vps7knduexkcvm + glob: 9.3.5 + jest: 29.4.3_v5qag4bu7yd4vl7sd6rt2doplm jest-environment-jsdom: 29.4.3 json-bigint: 1.0.0 lint-staged: 13.1.2 - page-with: 0.5.1_@swc+core@1.3.35 + page-with: 0.6.1_mtsvlg4x4u5udzh2pohivgt4x4 prettier: 2.8.4 regenerator-runtime: 0.13.11 rimraf: 3.0.2 simple-git-hooks: 2.8.1 - statuses: 2.0.1 - ts-node: 10.9.1_oe3jy5ze54sjippw2sqzxdlwem - tsup: 5.12.9_4s7jzcjqpdttwnwh3e3glkuq6y + ts-node: 10.9.1_x2vjra2lmmhd46xm3mchw7ztui + tsup: 6.7.0_4s7jzcjqpdttwnwh3e3glkuq6y typescript: 5.0.2 + undici: 5.23.0 url-loader: 4.1.1_webpack@5.75.0 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 webpack-dev-server: 3.11.3_webpack@5.75.0 - webpack-http-server: 0.5.0_@swc+core@1.3.35 + webpack-http-server: 0.5.0_mtsvlg4x4u5udzh2pohivgt4x4 packages: + /@75lb/deep-merge/1.1.1: + resolution: {integrity: sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==} + engines: {node: '>=12.17'} + dependencies: + lodash.assignwith: 4.2.0 + typical: 7.1.1 + dev: true + /@ampproject/remapping/2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} @@ -1346,6 +1370,24 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@bundled-es-modules/cookie/2.0.0: + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + dependencies: + cookie: 0.5.0 + dev: false + + /@bundled-es-modules/js-levenshtein/2.0.1: + resolution: {integrity: sha512-DERMS3yfbAljKsQc0U2wcqGKUWpdFjwqWuoMugEJlqBnKO180/n+4SR/J8MRDt1AN48X1ovgoD9KrdVXcaa3Rg==} + dependencies: + js-levenshtein: 1.1.6 + dev: false + + /@bundled-es-modules/statuses/1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: false + /@commitlint/cli/16.3.0_@swc+core@1.3.35: resolution: {integrity: sha512-P+kvONlfsuTMnxSwWE1H+ZcPMY3STFaHb2kAacsqoIkNx66O0T7sTpBxpxkMrFPyhkJiLJnJWMhk4bbvYD3BMA==} engines: {node: '>=v12'} @@ -1413,7 +1455,7 @@ packages: engines: {node: '>=v12'} dependencies: '@commitlint/types': 16.2.1 - chalk: 4.1.1 + chalk: 4.1.2 dev: true /@commitlint/is-ignored/16.2.4: @@ -1442,10 +1484,10 @@ packages: '@commitlint/execute-rule': 16.2.1 '@commitlint/resolve-extends': 16.2.1 '@commitlint/types': 16.2.1 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 cosmiconfig: 7.1.0 - cosmiconfig-typescript-loader: 2.0.2_plaptv2cv5vvro2su5yxvauvda + cosmiconfig-typescript-loader: 2.0.2_f6calhiv3qbku3gmsoec3zvctu lodash: 4.17.21 resolve-from: 5.0.0 typescript: 4.9.5 @@ -1463,15 +1505,15 @@ packages: '@commitlint/execute-rule': 17.4.0 '@commitlint/resolve-extends': 17.4.4 '@commitlint/types': 17.4.4 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 cosmiconfig: 8.0.0 - cosmiconfig-typescript-loader: 4.3.0_gg653o4cxizhrslchmhiad54ma + cosmiconfig-typescript-loader: 4.3.0_bmci5xihqld5vqu3v4tyk3r5ra lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1_plaptv2cv5vvro2su5yxvauvda + ts-node: 10.9.1_f6calhiv3qbku3gmsoec3zvctu typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' @@ -1555,14 +1597,14 @@ packages: resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==} engines: {node: '>=v12'} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 dev: true /@commitlint/types/17.4.4: resolution: {integrity: sha512-amRN8tRLYOsxRr6mTnGGGvB5EmW/4DDjLMgiwK3CCVEmN6Sr/6xePGEpWaspKkckILuUORCwe6VfDBw6uj4axQ==} engines: {node: '>=v14'} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 dev: true optional: true @@ -1582,6 +1624,15 @@ packages: dev: true optional: true + /@esbuild/android-arm/0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64/0.16.17: resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} engines: {node: '>=12'} @@ -1591,6 +1642,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64/0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64/0.16.17: resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} engines: {node: '>=12'} @@ -1600,6 +1660,15 @@ packages: dev: true optional: true + /@esbuild/android-x64/0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64/0.16.17: resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} engines: {node: '>=12'} @@ -1609,6 +1678,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64/0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64/0.16.17: resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} engines: {node: '>=12'} @@ -1618,6 +1696,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64/0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64/0.16.17: resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} engines: {node: '>=12'} @@ -1627,6 +1714,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64/0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64/0.16.17: resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} engines: {node: '>=12'} @@ -1636,6 +1732,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64/0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm/0.16.17: resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} engines: {node: '>=12'} @@ -1645,6 +1750,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm/0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64/0.16.17: resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} engines: {node: '>=12'} @@ -1654,6 +1768,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64/0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32/0.16.17: resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} engines: {node: '>=12'} @@ -1663,10 +1786,10 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.14.54: - resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + /@esbuild/linux-ia32/0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} engines: {node: '>=12'} - cpu: [loong64] + cpu: [ia32] os: [linux] requiresBuild: true dev: true @@ -1681,6 +1804,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64/0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el/0.16.17: resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} engines: {node: '>=12'} @@ -1690,6 +1822,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el/0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64/0.16.17: resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} engines: {node: '>=12'} @@ -1699,6 +1840,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64/0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64/0.16.17: resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} engines: {node: '>=12'} @@ -1708,6 +1858,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64/0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x/0.16.17: resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} engines: {node: '>=12'} @@ -1717,6 +1876,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x/0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64/0.16.17: resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} engines: {node: '>=12'} @@ -1726,6 +1894,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64/0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64/0.16.17: resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} engines: {node: '>=12'} @@ -1735,6 +1912,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64/0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64/0.16.17: resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} engines: {node: '>=12'} @@ -1744,6 +1930,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64/0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64/0.16.17: resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} engines: {node: '>=12'} @@ -1753,6 +1948,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64/0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64/0.16.17: resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} engines: {node: '>=12'} @@ -1762,6 +1966,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64/0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32/0.16.17: resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} engines: {node: '>=12'} @@ -1771,6 +1984,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32/0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64/0.16.17: resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} engines: {node: '>=12'} @@ -1780,6 +2002,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64/0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint/eslintrc/0.4.3: resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1833,8 +2064,8 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.4.3 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 jest-message-util: 29.4.3 jest-util: 29.4.3 slash: 3.0.0 @@ -1854,14 +2085,14 @@ packages: '@jest/test-result': 29.4.3 '@jest/transform': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 ansi-escapes: 4.3.2 - chalk: 4.1.1 + chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.4.3 - jest-config: 29.4.3_ghv2zugsw3zjg5rog5rhyka5ja + jest-config: 29.4.3_v5qag4bu7yd4vl7sd6rt2doplm jest-haste-map: 29.4.3 jest-message-util: 29.4.3 jest-regex-util: 29.4.3 @@ -1895,7 +2126,7 @@ packages: dependencies: '@jest/fake-timers': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-mock: 29.4.3 dev: true @@ -1922,7 +2153,7 @@ packages: dependencies: '@jest/types': 29.4.3 '@sinonjs/fake-timers': 10.0.2 - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-message-util: 29.4.3 jest-mock: 29.4.3 jest-util: 29.4.3 @@ -1955,8 +2186,8 @@ packages: '@jest/transform': 29.4.3 '@jest/types': 29.4.3 '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 glob: 7.2.3 @@ -2021,7 +2252,7 @@ packages: '@jest/types': 29.4.3 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 - chalk: 4.1.1 + chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 @@ -2042,9 +2273,9 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 16.18.12 + '@types/node': 18.17.14 '@types/yargs': 16.0.5 - chalk: 4.1.1 + chalk: 4.1.2 dev: true /@jest/types/29.4.3: @@ -2054,9 +2285,9 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 16.18.12 + '@types/node': 18.17.14 '@types/yargs': 17.0.22 - chalk: 4.1.1 + chalk: 4.1.2 dev: true /@jridgewell/gen-mapping/0.1.1: @@ -2111,28 +2342,21 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@mswjs/cookies/0.2.2: - resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} + /@mswjs/cookies/1.0.0: + resolution: {integrity: sha512-TdXoBdI+h/EDTsVLCX/34s4+9U0sWi92qFnIGUEikpMCSKLhBeujovyYVSoORNbYgsBH5ga7/tfxyWcEZAxiYA==} engines: {node: '>=14'} - dependencies: - '@types/set-cookie-parser': 2.4.2 - set-cookie-parser: 2.5.1 dev: false - /@mswjs/interceptors/0.17.10: - resolution: {integrity: sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==} - engines: {node: '>=14'} + /@mswjs/interceptors/0.25.1: + resolution: {integrity: sha512-iM/2Qp+y7zKrX1sf45sPvvE7CGly8AKSR8Ua7cXAszXCK/To5i/L8AwiheEaBSVcZ6R7Em7kTcyZWN5H2ivcEQ==} + engines: {node: '>=18'} dependencies: - '@open-draft/until': 1.0.3 - '@types/debug': 4.1.7 - '@xmldom/xmldom': 0.8.6 - debug: 4.3.4 - headers-polyfill: 3.2.5 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 outvariant: 1.4.0 - strict-event-emitter: 0.2.8 - web-encoding: 1.1.5 - transitivePeerDependencies: - - supports-color + strict-event-emitter: 0.5.0 dev: false /@nodelib/fs.scandir/2.1.5: @@ -2156,9 +2380,15 @@ packages: fastq: 1.15.0 dev: true - /@open-draft/deferred-promise/2.1.0: - resolution: {integrity: sha512-Rzd5JrXZX8zErHzgcGyngh4fmEbSHqTETdGj9rXtejlqMIgXFlyKBA7Jn1Xp0Ls0M0Y22+xHcWiEzbmdWl0BOA==} - dev: true + /@open-draft/deferred-promise/2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + /@open-draft/logger/0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.0 + dev: false /@open-draft/test-server/0.4.2: resolution: {integrity: sha512-J9wbdQkPx5WKcDNtgfnXsx5ew4UJd6BZyGr89YlHeaUkOShkO2iO5QIyCCsG4qpjIvr2ZTkEYJA9ujOXXyO6Pg==} @@ -2176,18 +2406,14 @@ packages: - utf-8-validate dev: true - /@open-draft/until/1.0.3: - resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} - /@open-draft/until/2.1.0: resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - dev: true /@ossjs/release/0.8.0: resolution: {integrity: sha512-vzxhYvad/Ub3j8bWWCRfdwTvFzK3HtKjm8IM5J+7njnQcZZie5iouUXX+G65OI3F1YgQSWvsozrWqHyN1x7fjQ==} hasBin: true dependencies: - '@open-draft/deferred-promise': 2.1.0 + '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/conventional-commits-parser': 3.0.3 '@types/issue-parser': 3.0.1 @@ -2218,10 +2444,37 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 playwright-core: 1.30.0 dev: true + /@rollup/plugin-node-resolve/13.3.0_rollup@2.79.1: + resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.42.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + '@types/resolve': 1.17.1 + deepmerge: 4.3.0 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils/3.1.0_rollup@2.79.1: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + /@sinclair/typebox/0.25.23: resolution: {integrity: sha512-VEB8ygeP42CFLWyAJhN5OklpxUliqdNEUcXb4xZ/CINqtYGTjL5ukluKdKzQ0iWdUxyQ7B0539PAUhHKrCNWSQ==} dev: true @@ -2381,6 +2634,12 @@ packages: resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} dev: true + /@types/accepts/1.3.5: + resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} + dependencies: + '@types/node': 18.17.14 + dev: true + /@types/babel__core/7.20.0: resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} dependencies: @@ -2414,34 +2673,52 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 16.18.12 + '@types/node': 18.17.14 + dev: true + + /@types/command-line-args/5.2.1: + resolution: {integrity: sha512-U2OcmS2tj36Yceu+mRuPyUV0ILfau/h5onStcSCzqTENsq0sBiAp2TmaXu1k8fY4McLcPKSYl9FRzn2hx5bI+w==} dev: true /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 + dev: true + + /@types/content-disposition/0.5.6: + resolution: {integrity: sha512-GmShTb4qA9+HMPPaV2+Up8tJafgi38geFi7vL4qAM7k8BwjoelgHZqEUKJZLvughUw22h6vD/wvwN4IUCaWpDA==} dev: true /@types/conventional-commits-parser/3.0.3: resolution: {integrity: sha512-aoUKfRQYvGMH+spFpOTX9jO4nZoz9/BKp4hlHPxL3Cj2r2Xj+jEcwlXtFIyZr5uL8bh1nbWynDEYaAota+XqPg==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 dev: true /@types/cookie/0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookies/0.7.8: + resolution: {integrity: sha512-y6KhF1GtsLERUpqOV+qZJrjUGzc0GE6UTa0b5Z/LZ7Nm2mKSdCXmS6Kdnl7fctPNnMSouHjxqEWI12/YqQfk5w==} + dependencies: + '@types/connect': 3.4.35 + '@types/express': 4.17.17 + '@types/keygrip': 1.0.2 + '@types/node': 18.17.14 + dev: true + /@types/cors/2.8.13: resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 dev: true /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: '@types/ms': 0.7.31 + dev: true /@types/eslint-scope/3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} @@ -2457,6 +2734,10 @@ packages: '@types/json-schema': 7.0.11 dev: true + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + /@types/estree/0.0.51: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} dev: true @@ -2464,7 +2745,7 @@ packages: /@types/express-serve-static-core/4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -2481,20 +2762,35 @@ packages: /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 14.18.36 + '@types/node': 18.17.14 dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 16.18.12 + '@types/node': 18.17.14 + dev: true + + /@types/glob/8.1.0: + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.17.14 dev: true /@types/graceful-fs/4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 + dev: true + + /@types/http-assert/1.5.3: + resolution: {integrity: sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==} + dev: true + + /@types/http-errors/2.0.1: + resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} dev: true /@types/issue-parser/3.0.1: @@ -2531,7 +2827,7 @@ packages: /@types/jsdom/20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -2544,6 +2840,29 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/keygrip/1.0.2: + resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==} + dev: true + + /@types/koa-compose/3.2.5: + resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==} + dependencies: + '@types/koa': 2.13.8 + dev: true + + /@types/koa/2.13.8: + resolution: {integrity: sha512-Ugmxmgk/yPRW3ptBTh9VjOLwsKWJuGbymo1uGX0qdaqqL18uJiiG1ZoV0rxCOYSaDGhvEp5Ece02Amx0iwaxQQ==} + dependencies: + '@types/accepts': 1.3.5 + '@types/content-disposition': 0.5.6 + '@types/cookies': 0.7.8 + '@types/http-assert': 1.5.3 + '@types/http-errors': 2.0.1 + '@types/keygrip': 1.0.2 + '@types/koa-compose': 3.2.5 + '@types/node': 18.17.14 + dev: true + /@types/mime/3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true @@ -2558,6 +2877,7 @@ packages: /@types/ms/0.7.31: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + dev: true /@types/mustache/4.2.2: resolution: {integrity: sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==} @@ -2566,16 +2886,17 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 14.18.36 + '@types/node': 18.17.14 form-data: 3.0.1 dev: true - /@types/node/14.18.36: - resolution: {integrity: sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==} - dev: true - /@types/node/16.18.12: resolution: {integrity: sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==} + dev: true + + /@types/node/18.17.14: + resolution: {integrity: sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==} + dev: true /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -2585,6 +2906,10 @@ packages: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true + /@types/parse5/6.0.3: + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + dev: true + /@types/prettier/2.7.2: resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} dev: true @@ -2592,7 +2917,7 @@ packages: /@types/puppeteer/5.4.7: resolution: {integrity: sha512-JdGWZZYL0vKapXF4oQTC5hLVNfOgdPrqeZ1BiQnGk5cB7HeE91EWUiTdVSdQPobRN8rIcdffjiOgCYJ/S8QrnQ==} dependencies: - '@types/node': 14.18.36 + '@types/node': 18.17.14 dev: true /@types/qs/6.9.7: @@ -2613,6 +2938,12 @@ packages: resolution: {integrity: sha512-VtTUcUaJGiJtlBKYwwFIOSvrcnuKmpPGO+x56XijNZnaDpnzKh2VwoTw5hewrOMW2BgjoU+uFbVAvSCW2FpWmA==} dev: true + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.17.14 + dev: true + /@types/semver/7.3.13: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true @@ -2625,19 +2956,17 @@ packages: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: '@types/mime': 3.0.1 - '@types/node': 16.18.12 + '@types/node': 18.17.14 dev: true - /@types/set-cookie-parser/2.4.2: - resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} - dependencies: - '@types/node': 16.18.12 - dev: false - /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/statuses/2.0.1: + resolution: {integrity: sha512-vVRgv7WXbhIZzILgr58b4Ki2uqpN/dlVCUBWCMkPbMDlV1CrQrgCl5hnIoUlMrgymGcTmdfVqbs1yWj/IRIRtQ==} + dev: false + /@types/tough-cookie/4.0.2: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} dev: true @@ -2646,6 +2975,12 @@ packages: resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} dev: true + /@types/ws/7.4.7: + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + dependencies: + '@types/node': 18.17.14 + dev: true + /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -2781,15 +3116,99 @@ packages: semver: 7.3.8 transitivePeerDependencies: - supports-color - - typescript + - typescript + dev: true + + /@typescript-eslint/visitor-keys/5.52.0: + resolution: {integrity: sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.52.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /@web/config-loader/0.1.3: + resolution: {integrity: sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==} + engines: {node: '>=10.0.0'} + dependencies: + semver: 7.5.4 + dev: true + + /@web/dev-server-core/0.4.1: + resolution: {integrity: sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==} + engines: {node: '>=10.0.0'} + dependencies: + '@types/koa': 2.13.8 + '@types/ws': 7.4.7 + '@web/parse5-utils': 1.3.1 + chokidar: 3.4.1 + clone: 2.1.2 + es-module-lexer: 1.3.0 + get-stream: 6.0.1 + is-stream: 2.0.1 + isbinaryfile: 5.0.0 + koa: 2.14.2 + koa-etag: 4.0.0 + koa-send: 5.0.1 + koa-static: 5.0.0 + lru-cache: 6.0.0 + mime-types: 2.1.35 + parse5: 6.0.1 + picomatch: 2.3.1 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /@web/dev-server-rollup/0.4.1: + resolution: {integrity: sha512-Ebsv7Ovd9MufeH3exvikBJ7GmrZA5OmHnOgaiHcwMJ2eQBJA5/I+/CbRjsLX97ICj/ZwZG//p2ITRz8W3UfSqg==} + engines: {node: '>=10.0.0'} + dependencies: + '@rollup/plugin-node-resolve': 13.3.0_rollup@2.79.1 + '@web/dev-server-core': 0.4.1 + nanocolors: 0.2.13 + parse5: 6.0.1 + rollup: 2.79.1 + whatwg-url: 11.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /@web/dev-server/0.1.38: + resolution: {integrity: sha512-WUq7Zi8KeJ5/UZmmpZ+kzUpUlFlMP/rcreJKYg9Lxiz998KYl4G5Rv24akX0piTuqXG7r6h+zszg8V/hdzjCoA==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + '@babel/code-frame': 7.18.6 + '@types/command-line-args': 5.2.1 + '@web/config-loader': 0.1.3 + '@web/dev-server-core': 0.4.1 + '@web/dev-server-rollup': 0.4.1 + camelcase: 6.3.0 + command-line-args: 5.2.1 + command-line-usage: 7.0.1 + debounce: 1.2.1 + deepmerge: 4.3.0 + ip: 1.1.8 + nanocolors: 0.2.13 + open: 8.4.2 + portfinder: 1.0.32 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate dev: true - /@typescript-eslint/visitor-keys/5.52.0: - resolution: {integrity: sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@web/parse5-utils/1.3.1: + resolution: {integrity: sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==} + engines: {node: '>=10.0.0'} dependencies: - '@typescript-eslint/types': 5.52.0 - eslint-visitor-keys: 3.3.0 + '@types/parse5': 6.0.3 + parse5: 6.0.1 dev: true /@webassemblyjs/ast/1.11.1: @@ -2898,11 +3317,6 @@ packages: '@xtuc/long': 4.2.2 dev: true - /@xmldom/xmldom/0.8.6: - resolution: {integrity: sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==} - engines: {node: '>=10.0.0'} - dev: false - /@xtuc/ieee754/1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} dev: true @@ -2911,12 +3325,6 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true - /@zxing/text-encoding/0.9.0: - resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} - requiresBuild: true - dev: false - optional: true - /JSONStream/1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -3147,6 +3555,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /array-back/3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: true + + /array-back/6.2.2: + resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} + engines: {node: '>=12.17'} + dev: true + /array-flatten/1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: true @@ -3240,6 +3658,7 @@ packages: /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + dev: true /babel-helper-evaluate-path/0.5.0: resolution: {integrity: sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==} @@ -3280,7 +3699,7 @@ packages: '@types/babel__core': 7.20.0 babel-plugin-istanbul: 6.1.1 babel-preset-jest: 29.4.3_@babel+core@7.20.12 - chalk: 4.1.1 + chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 transitivePeerDependencies: @@ -3299,7 +3718,7 @@ packages: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 dev: true /babel-minify/0.5.2: @@ -3654,6 +4073,12 @@ packages: concat-map: 0.0.1 dev: true + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + /braces/2.3.2_supports-color@6.1.0: resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} engines: {node: '>=0.10.0'} @@ -3709,16 +4134,28 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 - /bundle-require/3.1.2_esbuild@0.14.54: - resolution: {integrity: sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA==} + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /bundle-require/4.0.1_esbuild@0.17.19: + resolution: {integrity: sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: - esbuild: '>=0.13' + esbuild: '>=0.17' dependencies: - esbuild: 0.14.54 + esbuild: 0.17.19 load-tsconfig: 0.2.3 dev: true + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + /bytes/3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -3749,6 +4186,14 @@ packages: unset-value: 1.0.0 dev: true + /cache-content-type/1.0.1: + resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} + engines: {node: '>= 6.0.0'} + dependencies: + mime-types: 2.1.35 + ylru: 1.3.2 + dev: true + /cachedir/2.3.0: resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} engines: {node: '>=6'} @@ -3759,6 +4204,7 @@ packages: dependencies: function-bind: 1.1.1 get-intrinsic: 1.2.0 + dev: true /callsites/3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -3798,6 +4244,13 @@ packages: resolution: {integrity: sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA==} dev: true + /chalk-template/0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + dependencies: + chalk: 4.1.2 + dev: true + /chalk/2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3807,8 +4260,8 @@ packages: supports-color: 5.5.0 dev: true - /chalk/4.1.1: - resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 @@ -3924,6 +4377,11 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + /clone/2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: true + /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -3971,6 +4429,26 @@ packages: delayed-stream: 1.0.0 dev: true + /command-line-args/5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: true + + /command-line-usage/7.0.1: + resolution: {integrity: sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==} + engines: {node: '>=12.20.0'} + dependencies: + array-back: 6.2.2 + chalk-template: 0.4.0 + table-layout: 3.0.2 + typical: 7.1.1 + dev: true + /commander/2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true @@ -4148,10 +4626,18 @@ packages: /cookie/0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} + dev: true /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + + /cookies/0.8.0: + resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 dev: true /copy-descriptor/0.1.1: @@ -4177,23 +4663,23 @@ packages: vary: 1.1.2 dev: true - /cosmiconfig-typescript-loader/2.0.2_plaptv2cv5vvro2su5yxvauvda: + /cosmiconfig-typescript-loader/2.0.2_f6calhiv3qbku3gmsoec3zvctu: resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@types/node': '*' typescript: '>=3' dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 cosmiconfig: 7.1.0 - ts-node: 10.9.1_plaptv2cv5vvro2su5yxvauvda + ts-node: 10.9.1_f6calhiv3qbku3gmsoec3zvctu typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' dev: true - /cosmiconfig-typescript-loader/4.3.0_gg653o4cxizhrslchmhiad54ma: + /cosmiconfig-typescript-loader/4.3.0_bmci5xihqld5vqu3v4tyk3r5ra: resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -4202,9 +4688,9 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 cosmiconfig: 8.0.0 - ts-node: 10.9.1_plaptv2cv5vvro2su5yxvauvda + ts-node: 10.9.1_f6calhiv3qbku3gmsoec3zvctu typescript: 4.9.5 dev: true optional: true @@ -4321,6 +4807,10 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true + /debounce/1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: true + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -4344,6 +4834,17 @@ packages: supports-color: 6.1.0 dev: true + /debug/3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /debug/3.2.7_supports-color@6.1.0: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -4366,6 +4867,7 @@ packages: optional: true dependencies: ms: 2.1.2 + dev: true /debug/4.3.4_supports-color@6.1.0: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -4406,6 +4908,10 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /deep-equal/1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + dev: true + /deep-equal/1.1.1: resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==} dependencies: @@ -4444,6 +4950,11 @@ packages: dependencies: clone: 1.0.4 + /define-lazy-prop/2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: true + /define-properties/1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} @@ -4492,6 +5003,10 @@ packages: engines: {node: '>=0.4.0'} dev: true + /delegates/1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: true + /depd/1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -4651,7 +5166,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.13 - '@types/node': 16.18.12 + '@types/node': 18.17.14 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -4745,6 +5260,10 @@ packages: resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} dev: true + /es-module-lexer/1.3.0: + resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} + dev: true + /es-set-tostringtag/2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} @@ -4763,132 +5282,6 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild-android-64/0.14.54: - resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-android-arm64/0.14.54: - resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-64/0.14.54: - resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-arm64/0.14.54: - resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-64/0.14.54: - resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-arm64/0.14.54: - resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-32/0.14.54: - resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-64/0.14.54: - resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm/0.14.54: - resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm64/0.14.54: - resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-mips64le/0.14.54: - resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-ppc64le/0.14.54: - resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-riscv64/0.14.54: - resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-s390x/0.14.54: - resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - /esbuild-loader/2.21.0_webpack@5.75.0: resolution: {integrity: sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==} peerDependencies: @@ -4899,93 +5292,10 @@ packages: json5: 2.2.3 loader-utils: 2.0.4 tapable: 2.2.1 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 webpack-sources: 1.4.3 dev: true - /esbuild-netbsd-64/0.14.54: - resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-openbsd-64/0.14.54: - resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-sunos-64/0.14.54: - resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-32/0.14.54: - resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-64/0.14.54: - resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-arm64/0.14.54: - resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild/0.14.54: - resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/linux-loong64': 0.14.54 - esbuild-android-64: 0.14.54 - esbuild-android-arm64: 0.14.54 - esbuild-darwin-64: 0.14.54 - esbuild-darwin-arm64: 0.14.54 - esbuild-freebsd-64: 0.14.54 - esbuild-freebsd-arm64: 0.14.54 - esbuild-linux-32: 0.14.54 - esbuild-linux-64: 0.14.54 - esbuild-linux-arm: 0.14.54 - esbuild-linux-arm64: 0.14.54 - esbuild-linux-mips64le: 0.14.54 - esbuild-linux-ppc64le: 0.14.54 - esbuild-linux-riscv64: 0.14.54 - esbuild-linux-s390x: 0.14.54 - esbuild-netbsd-64: 0.14.54 - esbuild-openbsd-64: 0.14.54 - esbuild-sunos-64: 0.14.54 - esbuild-windows-32: 0.14.54 - esbuild-windows-64: 0.14.54 - esbuild-windows-arm64: 0.14.54 - dev: true - /esbuild/0.16.17: resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} engines: {node: '>=12'} @@ -5016,6 +5326,36 @@ packages: '@esbuild/win32-x64': 0.16.17 dev: true + /esbuild/0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -5126,7 +5466,7 @@ packages: '@eslint/eslintrc': 0.4.3 '@humanwhocodes/config-array': 0.5.0 ajv: 6.12.6 - chalk: 4.1.1 + chalk: 4.1.2 cross-spawn: 7.0.3 debug: 4.3.4 doctrine: 3.0.0 @@ -5205,6 +5545,10 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5222,6 +5566,7 @@ packages: /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + dev: true /eventsource/2.0.2: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} @@ -5556,6 +5901,13 @@ packages: merge: 2.1.1 dev: true + /find-replace/3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: true + /find-root/1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} dev: true @@ -5621,6 +5973,7 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 + dev: true /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -5645,6 +5998,14 @@ packages: mime-types: 2.1.35 dev: true + /formdata-node/4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -5718,6 +6079,7 @@ packages: /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true /function.prototype.name/1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -5752,6 +6114,7 @@ packages: function-bind: 1.1.1 has: 1.0.3 has-symbols: 1.0.3 + dev: true /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} @@ -5838,6 +6201,16 @@ packages: path-is-absolute: 1.0.1 dev: true + /glob/9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.10.1 + dev: true + /global-dirs/0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} engines: {node: '>=4'} @@ -5911,6 +6284,7 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.0 + dev: true /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -5961,12 +6335,14 @@ packages: /has-symbols/1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + dev: true /has-tostringtag/1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 + dev: true /has-value/0.3.1: resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} @@ -6004,9 +6380,15 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 + dev: true - /headers-polyfill/3.2.5: - resolution: {integrity: sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==} + /headers-polyfill/3.2.3: + resolution: {integrity: sha512-oj6MO8sdFQ9gQQedSVdMGh96suxTNp91vPQu7C4qx/57FqYsA5TiNr92nhIZwVQq8zygn4nu3xS1aEqpakGqdw==} + dev: true + + /headers-polyfill/4.0.1: + resolution: {integrity: sha512-CD3yq1U/nwyKZHRFIjESyveXz6Buk0ImoIwlEOEyNVNAqJLjNX3YkJkaH9Mg5rqU5JiVgTBq/6Z0jR1L6KS0Gg==} + dev: false /homedir-polyfill/1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} @@ -6050,6 +6432,14 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /http-assert/1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + dev: true + /http-deceiver/1.2.7: resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} dev: true @@ -6064,6 +6454,17 @@ packages: statuses: 1.5.0 dev: true + /http-errors/1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + dev: true + /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -6219,7 +6620,7 @@ packages: engines: {node: '>=12.0.0'} dependencies: ansi-escapes: 4.3.2 - chalk: 4.1.1 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-width: 3.0.0 external-editor: 3.1.0 @@ -6290,6 +6691,7 @@ packages: dependencies: call-bind: 1.0.2 has-tostringtag: 1.0.0 + dev: true /is-array-buffer/3.0.1: resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} @@ -6327,9 +6729,17 @@ packages: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: true + /is-builtin-module/3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + /is-callable/1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + dev: true /is-core-module/2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} @@ -6376,6 +6786,12 @@ packages: kind-of: 6.0.3 dev: true + /is-docker/2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + dev: true + /is-extendable/0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -6416,7 +6832,7 @@ packages: engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 - dev: false + dev: true /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -6428,6 +6844,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + /is-module/1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + /is-negative-zero/2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -6561,6 +6981,7 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 + dev: true /is-unicode-supported/0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} @@ -6586,10 +7007,22 @@ packages: engines: {node: '>=4'} dev: true + /is-wsl/2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + dev: true + /isarray/1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: true + /isbinaryfile/5.0.0: + resolution: {integrity: sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==} + engines: {node: '>= 14.0.0'} + dev: true + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -6679,8 +7112,8 @@ packages: '@jest/expect': 29.4.3 '@jest/test-result': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 @@ -6698,7 +7131,7 @@ packages: - supports-color dev: true - /jest-cli/29.4.3_nw6xvwuzmqp7vps7knduexkcvm: + /jest-cli/29.4.3_v5qag4bu7yd4vl7sd6rt2doplm: resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6711,11 +7144,11 @@ packages: '@jest/core': 29.4.3_ts-node@10.9.1 '@jest/test-result': 29.4.3 '@jest/types': 29.4.3 - chalk: 4.1.1 + chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.3_nw6xvwuzmqp7vps7knduexkcvm + jest-config: 29.4.3_v5qag4bu7yd4vl7sd6rt2doplm jest-util: 29.4.3 jest-validate: 29.4.3 prompts: 2.4.2 @@ -6726,47 +7159,7 @@ packages: - ts-node dev: true - /jest-config/29.4.3_ghv2zugsw3zjg5rog5rhyka5ja: - resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.12 - '@jest/test-sequencer': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 16.18.12 - babel-jest: 29.4.3_@babel+core@7.20.12 - chalk: 4.1.1 - ci-info: 3.8.0 - deepmerge: 4.3.0 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.4.3 - jest-environment-node: 29.4.3 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runner: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.4.3 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1_oe3jy5ze54sjippw2sqzxdlwem - transitivePeerDependencies: - - supports-color - dev: true - - /jest-config/29.4.3_nw6xvwuzmqp7vps7knduexkcvm: + /jest-config/29.4.3_v5qag4bu7yd4vl7sd6rt2doplm: resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -6781,9 +7174,9 @@ packages: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 14.18.36 + '@types/node': 18.17.14 babel-jest: 29.4.3_@babel+core@7.20.12 - chalk: 4.1.1 + chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.0 glob: 7.2.3 @@ -6801,7 +7194,7 @@ packages: pretty-format: 29.4.3 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_oe3jy5ze54sjippw2sqzxdlwem + ts-node: 10.9.1_x2vjra2lmmhd46xm3mchw7ztui transitivePeerDependencies: - supports-color dev: true @@ -6810,7 +7203,7 @@ packages: resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 diff-sequences: 29.4.3 jest-get-type: 29.4.3 pretty-format: 29.4.3 @@ -6828,7 +7221,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.4.3 - chalk: 4.1.1 + chalk: 4.1.2 jest-get-type: 29.4.3 jest-util: 29.4.3 pretty-format: 29.4.3 @@ -6847,7 +7240,7 @@ packages: '@jest/fake-timers': 29.4.3 '@jest/types': 29.4.3 '@types/jsdom': 20.0.1 - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-mock: 29.4.3 jest-util: 29.4.3 jsdom: 20.0.3 @@ -6864,7 +7257,7 @@ packages: '@jest/environment': 29.4.3 '@jest/fake-timers': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-mock: 29.4.3 jest-util: 29.4.3 dev: true @@ -6880,7 +7273,7 @@ packages: dependencies: '@jest/types': 29.4.3 '@types/graceful-fs': 4.1.6 - '@types/node': 16.18.12 + '@types/node': 18.17.14 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 @@ -6905,7 +7298,7 @@ packages: resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 jest-diff: 29.4.3 jest-get-type: 29.4.3 pretty-format: 29.4.3 @@ -6918,7 +7311,7 @@ packages: '@babel/code-frame': 7.18.6 '@jest/types': 29.4.3 '@types/stack-utils': 2.0.1 - chalk: 4.1.1 + chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 pretty-format: 29.4.3 @@ -6931,7 +7324,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.4.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-util: 29.4.3 dev: true @@ -6966,7 +7359,7 @@ packages: resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 graceful-fs: 4.2.10 jest-haste-map: 29.4.3 jest-pnp-resolver: 1.2.3_jest-resolve@29.4.3 @@ -6986,8 +7379,8 @@ packages: '@jest/test-result': 29.4.3 '@jest/transform': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 jest-docblock: 29.4.3 @@ -7017,8 +7410,8 @@ packages: '@jest/test-result': 29.4.3 '@jest/transform': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 @@ -7052,7 +7445,7 @@ packages: '@types/babel__traverse': 7.18.3 '@types/prettier': 2.7.2 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.12 - chalk: 4.1.1 + chalk: 4.1.2 expect: 29.4.3 graceful-fs: 4.2.10 jest-diff: 29.4.3 @@ -7073,8 +7466,8 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.4.3 - '@types/node': 16.18.12 - chalk: 4.1.1 + '@types/node': 18.17.14 + chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.10 picomatch: 2.3.1 @@ -7086,7 +7479,7 @@ packages: dependencies: '@jest/types': 29.4.3 camelcase: 6.3.0 - chalk: 4.1.1 + chalk: 4.1.2 jest-get-type: 29.4.3 leven: 3.1.0 pretty-format: 29.4.3 @@ -7098,9 +7491,9 @@ packages: dependencies: '@jest/test-result': 29.4.3 '@jest/types': 29.4.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 ansi-escapes: 4.3.2 - chalk: 4.1.1 + chalk: 4.1.2 emittery: 0.13.1 jest-util: 29.4.3 string-length: 4.0.2 @@ -7110,7 +7503,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -7119,13 +7512,13 @@ packages: resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 16.18.12 + '@types/node': 18.17.14 jest-util: 29.4.3 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest/29.4.3_nw6xvwuzmqp7vps7knduexkcvm: + /jest/29.4.3_v5qag4bu7yd4vl7sd6rt2doplm: resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -7138,7 +7531,7 @@ packages: '@jest/core': 29.4.3_ts-node@10.9.1 '@jest/types': 29.4.3 import-local: 3.1.0 - jest-cli: 29.4.3_nw6xvwuzmqp7vps7knduexkcvm + jest-cli: 29.4.3_v5qag4bu7yd4vl7sd6rt2doplm transitivePeerDependencies: - '@types/node' - supports-color @@ -7272,6 +7665,13 @@ packages: engines: {'0': node >= 0.2.0} dev: true + /keygrip/1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + dependencies: + tsscmp: 1.0.6 + dev: true + /killable/1.0.1: resolution: {integrity: sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==} dev: true @@ -7305,6 +7705,76 @@ packages: engines: {node: '>=6'} dev: true + /koa-compose/4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + dev: true + + /koa-convert/2.0.0: + resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} + engines: {node: '>= 10'} + dependencies: + co: 4.6.0 + koa-compose: 4.1.0 + dev: true + + /koa-etag/4.0.0: + resolution: {integrity: sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==} + dependencies: + etag: 1.8.1 + dev: true + + /koa-send/5.0.1: + resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} + engines: {node: '>= 8'} + dependencies: + debug: 4.3.4 + http-errors: 1.8.1 + resolve-path: 1.4.0 + transitivePeerDependencies: + - supports-color + dev: true + + /koa-static/5.0.0: + resolution: {integrity: sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==} + engines: {node: '>= 7.6.0'} + dependencies: + debug: 3.2.7 + koa-send: 5.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /koa/2.14.2: + resolution: {integrity: sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g==} + engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.8.0 + debug: 4.3.4 + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.6.3 + is-generator-function: 1.0.10 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /leven/2.1.0: resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==} engines: {node: '>=0.10.0'} @@ -7423,6 +7893,14 @@ packages: p-locate: 5.0.0 dev: true + /lodash.assignwith/4.2.0: + resolution: {integrity: sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==} + dev: true + + /lodash.camelcase/4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + /lodash.capitalize/4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} dev: true @@ -7480,7 +7958,7 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} dependencies: - chalk: 4.1.1 + chalk: 4.1.2 is-unicode-supported: 0.1.0 /log-update/4.0.0: @@ -7503,6 +7981,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /lru-cache/10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: true + /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -7695,6 +8178,13 @@ packages: brace-expansion: 1.1.11 dev: true + /minimatch/8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options/4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -7712,6 +8202,16 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /minipass/4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + dev: true + + /minipass/7.0.3: + resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mixin-deep/1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} @@ -7738,6 +8238,7 @@ packages: /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -7771,6 +8272,10 @@ packages: thenify-all: 1.6.0 dev: true + /nanocolors/0.2.13: + resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==} + dev: true + /nanomatch/1.2.13_supports-color@6.1.0: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -7811,6 +8316,11 @@ packages: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true + /node-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + /node-fetch/2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -7863,7 +8373,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.11.0 - semver: 7.3.8 + semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true @@ -8000,6 +8510,19 @@ packages: mimic-fn: 4.0.0 dev: true + /only/0.0.2: + resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + dev: true + + /open/8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + /opn/5.5.0: resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} engines: {node: '>=4'} @@ -8036,7 +8559,7 @@ packages: engines: {node: '>=10'} dependencies: bl: 4.1.0 - chalk: 4.1.1 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.7.0 is-interactive: 1.0.0 @@ -8116,22 +8639,22 @@ packages: engines: {node: '>=6'} dev: true - /page-with/0.5.1_@swc+core@1.3.35: - resolution: {integrity: sha512-830oKHY2kfhPuc3vsaaeqrwCH1FkOhopFQ0JmSPObGI4yVZQLYYpRW4frNP4hYyuxVgC0zbwMqr+Z4ESgDX5Sw==} + /page-with/0.6.1_mtsvlg4x4u5udzh2pohivgt4x4: + resolution: {integrity: sha512-5J58fSpc8CKonUWCPsh8b2LctFrNSOpXQ8O3tB+/iJvixOQf1qHp4+cDLiIVsl/WiuheXdZTzMcuR0KLQMaWcg==} dependencies: - '@open-draft/until': 1.0.3 + '@open-draft/until': 2.1.0 '@types/debug': 4.1.7 '@types/express': 4.17.17 '@types/mustache': 4.2.2 '@types/uuid': 8.3.4 debug: 4.3.4 express: 4.18.2 - headers-polyfill: 3.2.5 + headers-polyfill: 3.2.3 memfs: 3.4.13 mustache: 4.2.0 playwright: 1.30.0 uuid: 8.3.2 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 webpack-merge: 5.8.0 transitivePeerDependencies: - '@swc/core' @@ -8163,6 +8686,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /parse5/6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + dev: true + /parse5/7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -8217,6 +8744,14 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-scurry/1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.1 + minipass: 7.0.3 + dev: true + /path-to-regexp/0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} dev: true @@ -8347,6 +8882,17 @@ packages: playwright-core: 1.30.0 dev: true + /portfinder/1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + dependencies: + async: 2.6.4 + debug: 3.2.7 + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + /portfinder/1.0.32_supports-color@6.1.0: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -8376,7 +8922,7 @@ packages: optional: true dependencies: lilconfig: 2.0.6 - ts-node: 10.9.1_oe3jy5ze54sjippw2sqzxdlwem + ts-node: 10.9.1_x2vjra2lmmhd46xm3mchw7ztui yaml: 1.10.2 dev: true @@ -8733,6 +9279,14 @@ packages: global-dirs: 0.1.1 dev: true + /resolve-path/1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} + dependencies: + http-errors: 1.6.3 + path-is-absolute: 1.0.1 + dev: true + /resolve-url/0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated @@ -8800,6 +9354,14 @@ packages: fsevents: 2.3.2 dev: true + /rollup/3.29.0: + resolution: {integrity: sha512-nszM8DINnx1vSS+TpbWKMkxem0CDWk3cSit/WWCBVs9/JZ1I/XLwOsiUglYuYReaeWWSsW9kge5zE5NZtf/a4w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -9017,10 +9579,6 @@ packages: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true - /set-cookie-parser/2.5.1: - resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} - dev: false - /set-value/2.0.1: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} @@ -9370,7 +9928,6 @@ packages: /statuses/2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - dev: true /stream-combiner2/1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} @@ -9379,18 +9936,22 @@ packages: readable-stream: 2.3.7 dev: true + /stream-read-all/3.0.1: + resolution: {integrity: sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==} + engines: {node: '>=10'} + dev: true + /stream-shift/1.0.1: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} dev: true - /strict-event-emitter/0.2.8: - resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} - dependencies: - events: 3.3.0 - dev: false + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true - /strict-event-emitter/0.4.6: - resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} + /strict-event-emitter/0.5.0: + resolution: {integrity: sha512-sqnMpVJLSB3daNO6FcvsEk4Mq5IJeAwDeH80DP1S8+pgxrF6yZnE1+VeapesGled7nEcIkz1Ax87HzaIy+02kA==} dev: false /string-argv/0.3.1: @@ -9572,6 +10133,20 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true + /table-layout/3.0.2: + resolution: {integrity: sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==} + engines: {node: '>=12.17'} + hasBin: true + dependencies: + '@75lb/deep-merge': 1.1.1 + array-back: 6.2.2 + command-line-args: 5.2.1 + command-line-usage: 7.0.1 + stream-read-all: 3.0.1 + typical: 7.1.1 + wordwrapjs: 5.1.0 + dev: true + /table/6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} engines: {node: '>=10.0.0'} @@ -9588,7 +10163,7 @@ packages: engines: {node: '>=6'} dev: true - /terser-webpack-plugin/5.3.6_gwpkmym7uf5m6snr3dgsgj5rrq: + /terser-webpack-plugin/5.3.6_46rrhsymls7zkxn67al7zvwy5y: resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -9606,11 +10181,12 @@ packages: dependencies: '@jridgewell/trace-mapping': 0.3.17 '@swc/core': 1.3.35 + esbuild: 0.17.19 jest-worker: 27.5.1 schema-utils: 3.1.1 serialize-javascript: 6.0.1 terser: 5.16.4 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 dev: true /terser/5.16.4: @@ -9781,7 +10357,7 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /ts-node/10.9.1_oe3jy5ze54sjippw2sqzxdlwem: + /ts-node/10.9.1_f6calhiv3qbku3gmsoec3zvctu: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -9801,19 +10377,19 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 14.18.36 + '@types/node': 18.17.14 acorn: 8.8.2 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.0.2 + typescript: 4.9.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true - /ts-node/10.9.1_plaptv2cv5vvro2su5yxvauvda: + /ts-node/10.9.1_x2vjra2lmmhd46xm3mchw7ztui: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -9833,14 +10409,14 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 16.18.12 + '@types/node': 18.17.14 acorn: 8.8.2 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.9.5 + typescript: 5.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -9852,13 +10428,19 @@ packages: /tslib/2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - /tsup/5.12.9_4s7jzcjqpdttwnwh3e3glkuq6y: - resolution: {integrity: sha512-dUpuouWZYe40lLufo64qEhDpIDsWhRbr2expv5dHEMjwqeKJS2aXA/FPqs1dxO4T6mBojo7rvo3jP9NNzaKyDg==} + /tsscmp/1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + dev: true + + /tsup/6.7.0_4s7jzcjqpdttwnwh3e3glkuq6y: + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} hasBin: true peerDependencies: '@swc/core': ^1 postcss: ^8.4.12 - typescript: ^4.1.0 + typescript: '>=4.1.0' peerDependenciesMeta: '@swc/core': optional: true @@ -9868,17 +10450,17 @@ packages: optional: true dependencies: '@swc/core': 1.3.35 - bundle-require: 3.1.2_esbuild@0.14.54 + bundle-require: 4.0.1_esbuild@0.17.19 cac: 6.7.14 chokidar: 3.4.1 debug: 4.3.4 - esbuild: 0.14.54 + esbuild: 0.17.19 execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 postcss-load-config: 3.1.4_ts-node@10.9.1 resolve-from: 5.0.0 - rollup: 2.79.1 + rollup: 3.29.0 source-map: 0.8.0-beta.0 sucrase: 3.29.0 tree-kill: 1.2.2 @@ -9974,6 +10556,16 @@ packages: hasBin: true dev: true + /typical/4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: true + + /typical/7.1.1: + resolution: {integrity: sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==} + engines: {node: '>=12.17'} + dev: true + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -9983,6 +10575,13 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici/5.23.0: + resolution: {integrity: sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==} + engines: {node: '>=14.0'} + dependencies: + busboy: 1.6.0 + dev: true + /unicode-canonical-property-names-ecmascript/2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -10074,7 +10673,7 @@ packages: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.1.1 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 dev: true /url-parse/1.5.10: @@ -10109,16 +10708,6 @@ packages: object.getownpropertydescriptors: 2.1.5 dev: true - /util/0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.10 - which-typed-array: 1.1.9 - dev: false - /utils-merge/1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -10196,12 +10785,9 @@ packages: dependencies: defaults: 1.0.4 - /web-encoding/1.1.5: - resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} - dependencies: - util: 0.12.5 - optionalDependencies: - '@zxing/text-encoding': 0.9.0 + /web-streams-polyfill/4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} dev: false /webidl-conversions/3.0.1: @@ -10226,7 +10812,7 @@ packages: mime: 2.6.0 mkdirp: 0.5.6 range-parser: 1.2.1 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 webpack-log: 2.0.0 dev: true @@ -10270,7 +10856,7 @@ packages: strip-ansi: 3.0.1 supports-color: 6.1.0 url: 0.11.0 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 webpack-dev-middleware: 3.7.3_webpack@5.75.0 webpack-log: 2.0.0 ws: 6.2.2 @@ -10280,7 +10866,7 @@ packages: - utf-8-validate dev: true - /webpack-http-server/0.5.0_@swc+core@1.3.35: + /webpack-http-server/0.5.0_mtsvlg4x4u5udzh2pohivgt4x4: resolution: {integrity: sha512-kyewxAnzmDuZxe09fn/Bb0PeEnaDxHChYKFVsMy4oeBUs9Cyv2j1uEgzQJ7ljPFexLU8ongUS4i4O+e22CeBZQ==} dependencies: '@types/express': 4.17.17 @@ -10289,7 +10875,7 @@ packages: memfs: 3.4.13 mustache: 4.2.0 outvariant: 1.4.0 - webpack: 5.75.0_@swc+core@1.3.35 + webpack: 5.75.0_mtsvlg4x4u5udzh2pohivgt4x4 transitivePeerDependencies: - '@swc/core' - esbuild @@ -10326,7 +10912,7 @@ packages: engines: {node: '>=10.13.0'} dev: true - /webpack/5.75.0_@swc+core@1.3.35: + /webpack/5.75.0_mtsvlg4x4u5udzh2pohivgt4x4: resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} engines: {node: '>=10.13.0'} hasBin: true @@ -10357,7 +10943,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.3.6_gwpkmym7uf5m6snr3dgsgj5rrq + terser-webpack-plugin: 5.3.6_46rrhsymls7zkxn67al7zvwy5y watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -10438,6 +11024,7 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 + dev: true /which/1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -10463,6 +11050,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /wordwrapjs/5.1.0: + resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} + engines: {node: '>=12.17'} + dev: true + /wrap-ansi/5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -10515,6 +11107,19 @@ packages: async-limiter: 1.0.1 dev: true + /ws/7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /ws/8.11.0: resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} engines: {node: '>=10.0.0'} @@ -10630,6 +11235,11 @@ packages: yargs-parser: 21.1.1 dev: true + /ylru/1.3.2: + resolution: {integrity: sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==} + engines: {node: '>= 4.0.0'} + dev: true + /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} diff --git a/src/browser/index.ts b/src/browser/index.ts new file mode 100644 index 000000000..0eafbe76f --- /dev/null +++ b/src/browser/index.ts @@ -0,0 +1,3 @@ +export { setupWorker } from './setupWorker/setupWorker' +export type { SetupWorker, StartOptions } from './setupWorker/glossary' +export { SetupWorkerApi } from './setupWorker/setupWorker' diff --git a/src/setupWorker/glossary.ts b/src/browser/setupWorker/glossary.ts similarity index 77% rename from src/setupWorker/glossary.ts rename to src/browser/setupWorker/glossary.ts index d0370cff9..23017c6ec 100644 --- a/src/setupWorker/glossary.ts +++ b/src/browser/setupWorker/glossary.ts @@ -1,20 +1,17 @@ -import { FlatHeadersObject } from 'headers-polyfill' import { Emitter } from 'strict-event-emitter' import { LifeCycleEventEmitter, LifeCycleEventsMap, SharedOptions, -} from '../sharedOptions' +} from '~/core/sharedOptions' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' import { - DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo, -} from '../handlers/RequestHandler' +} from '~/core/handlers/RequestHandler' import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors' -import { Path } from '../utils/matching/matchRequestUrl' -import { RequiredDeep } from '../typeUtils' -import { MockedRequest } from '../utils/request/MockedRequest' +import { Path } from '~/core/utils/matching/matchRequestUrl' +import { RequiredDeep } from '~/core/typeUtils' export type ResolvedPath = Path | URL @@ -37,15 +34,11 @@ type RequestWithoutMethods = Omit< */ export interface ServiceWorkerIncomingRequest extends RequestWithoutMethods { /** - * Unique UUID of the request generated once the request is - * captured by the "fetch" event in the Service Worker. + * Unique ID of the request generated once the request is + * intercepted by the "fetch" event in the Service Worker. */ id: string - - /** - * Text response body. - */ - body?: string + body?: ArrayBuffer | null } export type ServiceWorkerIncomingResponse = Pick< @@ -53,6 +46,7 @@ export type ServiceWorkerIncomingResponse = Pick< 'type' | 'ok' | 'status' | 'statusText' | 'body' | 'headers' | 'redirected' > & { requestId: string + isMockedResponse: boolean } /** @@ -77,13 +71,17 @@ export type ServiceWorkerOutgoingEventTypes = | 'KEEPALIVE_REQUEST' | 'CLIENT_CLOSED' +export interface StringifiedResponse extends ResponseInit { + body: string | ArrayBuffer | ReadableStream | null +} + /** * Map of the events that can be sent to the Service Worker * only as a part of a single `fetch` event handler. */ export interface ServiceWorkerFetchEventMap { - MOCK_RESPONSE(payload: SerializedResponse): void - MOCK_RESPONSE_START(payload: SerializedResponse): void + MOCK_RESPONSE(payload: StringifiedResponse): void + MOCK_RESPONSE_START(payload: StringifiedResponse): void MOCK_NOT_FOUND(): void NETWORK_ERROR(payload: { name: string; message: string }): void @@ -95,15 +93,17 @@ export interface ServiceWorkerBroadcastChannelMessageMap { MOCK_RESPONSE_END(): void } -export type WorkerLifecycleEventsMap = LifeCycleEventsMap +export interface StrictEventListener { + (event: EventType): void +} export interface SetupWorkerInternalContext { isMockingEnabled: boolean startOptions: RequiredDeep worker: ServiceWorker | null registration: ServiceWorkerRegistration | null - requestHandlers: RequestHandler[] - emitter: Emitter + requestHandlers: Array + emitter: Emitter keepAliveInterval?: number workerChannel: { /** @@ -128,10 +128,10 @@ export interface SetupWorkerInternalContext { * Adds an event listener on the given target. * Returns a clean-up function that removes that listener. */ - addListener( + addListener( target: EventTarget, eventType: string, - listener: (event: EventType) => void, + callback: StrictEventListener, ): () => void /** * Removes all currently attached listeners. @@ -146,7 +146,10 @@ export interface SetupWorkerInternalContext { ServiceWorkerMessage > } - useFallbackMode: boolean + supports: { + serviceWorkerApi: boolean + readableStreamTransfer: boolean + } fallbackInterceptor?: Interceptor } @@ -174,7 +177,7 @@ export interface StartOptions extends SharedOptions { } /** - * Disables the logging of captured requests + * Disables the logging of the intercepted requests * into browser's console. * @default false */ @@ -194,14 +197,6 @@ export interface StartOptions extends SharedOptions { findWorker?: FindWorker } -export interface SerializedResponse { - status: number - statusText: string - headers: FlatHeadersObject - body: BodyType - delay?: number -} - export type StartReturnType = Promise export type StartHandler = ( options: RequiredDeep, @@ -212,54 +207,53 @@ 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()`} + * + * @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()`} + * + * @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()`} + * + * @see {@link https://mswjs.io/docs/api/setup-worker/use `worker.use()` API reference} */ use: (...handlers: RequestHandler[]) => 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()`} + * + * @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()`} + * + * @see {@link https://mswjs.io/docs/api/setup-worker/reset-handlers `worker.resetHandlers()` API reference} */ resetHandlers: (...nextHandlers: RequestHandler[]) => void /** * Returns a readonly list of currently active request handlers. - * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()`} + * + * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()` API reference} */ - listHandlers(): ReadonlyArray< - RequestHandler< - RequestHandlerDefaultInfo, - MockedRequest, - any, - MockedRequest - > - > + listHandlers(): ReadonlyArray> /** - * Lists all active request handlers. - * @see {@link https://mswjs.io/docs/api/setup-worker/print-handlers `worker.printHandlers()`} + * 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} */ - printHandlers: () => void - - events: LifeCycleEventEmitter + events: LifeCycleEventEmitter } diff --git a/src/setupWorker/setupWorker.node.test.ts b/src/browser/setupWorker/setupWorker.node.test.ts similarity index 100% rename from src/setupWorker/setupWorker.node.test.ts rename to src/browser/setupWorker/setupWorker.node.test.ts diff --git a/src/setupWorker/setupWorker.ts b/src/browser/setupWorker/setupWorker.ts similarity index 64% rename from src/setupWorker/setupWorker.ts rename to src/browser/setupWorker/setupWorker.ts index 3246ccbb0..b89376641 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/browser/setupWorker/setupWorker.ts @@ -3,7 +3,6 @@ import { isNodeProcess } from 'is-node-process' import { SetupWorkerInternalContext, ServiceWorkerIncomingEventsMap, - WorkerLifecycleEventsMap, StartReturnType, StopHandler, StartHandler, @@ -12,23 +11,25 @@ import { import { createStartHandler } from './start/createStartHandler' import { createStop } from './stop/createStop' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' -import { RequestHandler } from '../handlers/RequestHandler' +import { RequestHandler } from '~/core/handlers/RequestHandler' import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler' import { createFallbackStart } from './start/createFallbackStart' import { createFallbackStop } from './stop/createFallbackStop' -import { devUtils } from '../utils/internal/devUtils' -import { SetupApi } from '../SetupApi' -import { mergeRight } from '../utils/internal/mergeRight' +import { devUtils } from '~/core/utils/internal/devUtils' +import { SetupApi } from '~/core/SetupApi' +import { mergeRight } from '~/core/utils/internal/mergeRight' +import { LifeCycleEventsMap } from '~/core/sharedOptions' import { SetupWorker } from './glossary' +import { supportsReadableStreamTransfer } from '../utils/supportsReadableStreamTransfer' interface Listener { target: EventTarget eventType: string - callback: EventListener + callback: EventListenerOrEventListenerObject } export class SetupWorkerApi - extends SetupApi + extends SetupApi implements SetupWorker { private context: SetupWorkerInternalContext @@ -51,7 +52,7 @@ export class SetupWorkerApi } private createWorkerContext(): SetupWorkerInternalContext { - const context = { + const context: SetupWorkerInternalContext = { // Mocking is not considered enabled until the worker // signals back the successful activation event. isMockingEnabled: false, @@ -61,55 +62,41 @@ export class SetupWorkerApi requestHandlers: this.currentHandlers, emitter: this.emitter, workerChannel: { - on: ( - eventType: EventType, - callback: ( - event: MessageEvent, - message: ServiceWorkerMessage< - EventType, - ServiceWorkerIncomingEventsMap[EventType] - >, - ) => void, - ) => { - this.context.events.addListener( - navigator.serviceWorker, - 'message', - (event: MessageEvent) => { - // Avoid messages broadcasted from unrelated workers. - if (event.source !== this.context.worker) { - return - } + on: (eventType, callback) => { + this.context.events.addListener< + MessageEvent> + >(navigator.serviceWorker, 'message', (event) => { + // Avoid messages broadcasted from unrelated workers. + if (event.source !== this.context.worker) { + return + } - const message = event.data as ServiceWorkerMessage< - typeof eventType, - any - > + const message = event.data - if (!message) { - return - } + if (!message) { + return + } - if (message.type === eventType) { - callback(event, message) - } - }, - ) + if (message.type === eventType) { + callback(event, message) + } + }) }, - send: (type: any) => { + send: (type) => { this.context.worker?.postMessage(type) }, }, events: { - addListener: ( - target: EventTarget, - eventType: string, - callback: EventListener, - ) => { - target.addEventListener(eventType, callback) - this.listeners.push({ eventType, target, callback }) + addListener: (target, eventType, callback) => { + target.addEventListener(eventType, callback as EventListener) + this.listeners.push({ + eventType, + target, + callback: callback as EventListener, + }) return () => { - target.removeEventListener(eventType, callback) + target.removeEventListener(eventType, callback as EventListener) } }, removeAllListeners: () => { @@ -118,9 +105,7 @@ export class SetupWorkerApi } this.listeners = [] }, - once: ( - eventType: EventType, - ) => { + once: (eventType) => { const bindings: Array<() => void> = [] return new Promise< @@ -158,8 +143,11 @@ export class SetupWorkerApi }) }, }, - useFallbackMode: - !('serviceWorker' in navigator) || location.protocol === 'file:', + supports: { + serviceWorkerApi: + !('serviceWorker' in navigator) || location.protocol === 'file:', + readableStreamTransfer: supportsReadableStreamTransfer(), + }, } /** @@ -172,11 +160,11 @@ export class SetupWorkerApi }, }) - this.startHandler = context.useFallbackMode + this.startHandler = context.supports.serviceWorkerApi ? createFallbackStart(context) : createStartHandler(context) - this.stopHandler = context.useFallbackMode + this.stopHandler = context.supports.serviceWorkerApi ? createFallbackStop(context) : createStop(context) @@ -192,26 +180,6 @@ export class SetupWorkerApi return await this.startHandler(this.context.startOptions, options) } - public printHandlers(): void { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.groupCollapsed(`${pragma} ${header}`) - - if (callFrame) { - console.log(`Declaration: ${callFrame}`) - } - - console.log('Handler:', handler) - console.groupEnd() - }) - } - public stop(): void { super.dispose() this.context.events.removeAllListeners() @@ -223,7 +191,8 @@ export class SetupWorkerApi /** * 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`} + * + * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker()` API reference} */ export function setupWorker(...handlers: Array): SetupWorker { return new SetupWorkerApi(...handlers) diff --git a/src/browser/setupWorker/start/createFallbackRequestListener.ts b/src/browser/setupWorker/start/createFallbackRequestListener.ts new file mode 100644 index 000000000..c722b2772 --- /dev/null +++ b/src/browser/setupWorker/start/createFallbackRequestListener.ts @@ -0,0 +1,67 @@ +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' + +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 }) => { + const requestCloneForLogs = request.clone() + + const response = await handleRequest( + request, + requestId, + context.requestHandlers, + options, + context.emitter, + { + onMockedResponse(_, { handler, parsedResult }) { + if (!options.quiet) { + context.emitter.once('response:mocked', ({ response }) => { + handler.log({ + request: requestCloneForLogs, + response, + parsedResult, + }) + }) + } + }, + }, + ) + + if (response) { + request.respondWith(response) + } + }) + + interceptor.on( + 'response', + ({ response, isMockedResponse, request, requestId }) => { + context.emitter.emit( + isMockedResponse ? 'response:mocked' : 'response:bypass', + { + response, + request, + requestId, + }, + ) + }, + ) + + interceptor.apply() + + return interceptor +} diff --git a/src/setupWorker/start/createFallbackStart.ts b/src/browser/setupWorker/start/createFallbackStart.ts similarity index 100% rename from src/setupWorker/start/createFallbackStart.ts rename to src/browser/setupWorker/start/createFallbackStart.ts diff --git a/src/browser/setupWorker/start/createRequestListener.ts b/src/browser/setupWorker/start/createRequestListener.ts new file mode 100644 index 000000000..b6ee1e56a --- /dev/null +++ b/src/browser/setupWorker/start/createRequestListener.ts @@ -0,0 +1,116 @@ +import { + StartOptions, + SetupWorkerInternalContext, + ServiceWorkerIncomingEventsMap, +} from '../glossary' +import { + ServiceWorkerMessage, + WorkerChannel, +} from './utils/createMessageChannel' +import { parseWorkerRequest } from '../../utils/parseWorkerRequest' +import { handleRequest } from '~/core/utils/handleRequest' +import { RequiredDeep } from '~/core/typeUtils' +import { devUtils } from '~/core/utils/internal/devUtils' +import { toResponseInit } from '~/core/utils/toResponseInit' + +export const createRequestListener = ( + context: SetupWorkerInternalContext, + options: RequiredDeep, +) => { + return async ( + event: MessageEvent, + message: ServiceWorkerMessage< + 'REQUEST', + ServiceWorkerIncomingEventsMap['REQUEST'] + >, + ) => { + const messageChannel = new WorkerChannel(event.ports[0]) + + const requestId = message.payload.id + const request = parseWorkerRequest(message.payload) + const requestCloneForLogs = request.clone() + + try { + await handleRequest( + request, + requestId, + context.requestHandlers, + options, + context.emitter, + { + onPassthroughResponse() { + messageChannel.postMessage('NOT_FOUND') + }, + 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 responseInit = toResponseInit(response) + + /** + * @note Safari doesn't support transferring a "ReadableStream". + * Check that the browser supports that before sending it to the worker. + */ + if (context.supports.readableStreamTransfer) { + const responseStream = response.body + messageChannel.postMessage( + 'MOCK_RESPONSE', + { + ...responseInit, + body: responseStream, + }, + responseStream ? [responseStream] : undefined, + ) + } else { + // As a fallback, send the response body buffer to the worker. + const responseBuffer = await responseClone.arrayBuffer() + messageChannel.postMessage('MOCK_RESPONSE', { + ...responseInit, + body: responseBuffer, + }) + } + + if (!options.quiet) { + context.emitter.once('response:mocked', ({ response }) => { + handler.log({ + request: requestCloneForLogs, + response, + 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/recipes/mocking-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. + messageChannel.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, + }), + }) + } + } + } +} diff --git a/src/setupWorker/start/createResponseListener.ts b/src/browser/setupWorker/start/createResponseListener.ts similarity index 62% rename from src/setupWorker/start/createResponseListener.ts rename to src/browser/setupWorker/start/createResponseListener.ts index 6fa37ad89..7719dfe19 100644 --- a/src/setupWorker/start/createResponseListener.ts +++ b/src/browser/setupWorker/start/createResponseListener.ts @@ -1,7 +1,7 @@ import { ServiceWorkerIncomingEventsMap, SetupWorkerInternalContext, -} from '../../setupWorker/glossary' +} from '../glossary' import { ServiceWorkerMessage } from './utils/createMessageChannel' export function createResponseListener(context: SetupWorkerInternalContext) { @@ -25,13 +25,22 @@ export function createResponseListener(context: SetupWorkerInternalContext) { return } - const response = new Response(responseJson.body || null, responseJson) - const isMockedResponse = response.headers.get('x-powered-by') === 'msw' + const response = + responseJson.status === 0 + ? Response.error() + : new Response(responseJson.body, responseJson) - if (isMockedResponse) { - context.emitter.emit('response:mocked', response, responseJson.requestId) - } else { - context.emitter.emit('response:bypass', response, responseJson.requestId) - } + context.emitter.emit( + responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass', + { + response, + /** + * @todo @fixme In this context, we don't know anything about + * the request. + */ + request: null as any, + requestId: responseJson.requestId, + }, + ) } } diff --git a/src/setupWorker/start/createStartHandler.ts b/src/browser/setupWorker/start/createStartHandler.ts similarity index 94% rename from src/setupWorker/start/createStartHandler.ts rename to src/browser/setupWorker/start/createStartHandler.ts index c7a127593..2ad650604 100644 --- a/src/setupWorker/start/createStartHandler.ts +++ b/src/browser/setupWorker/start/createStartHandler.ts @@ -1,13 +1,13 @@ import { until } from '@open-draft/until' +import { devUtils } from '~/core/utils/internal/devUtils' import { getWorkerInstance } from './utils/getWorkerInstance' import { enableMocking } from './utils/enableMocking' import { SetupWorkerInternalContext, StartHandler } from '../glossary' import { createRequestListener } from './createRequestListener' -import { requestIntegrityCheck } from '../../utils/internal/requestIntegrityCheck' +import { requestIntegrityCheck } from '../../utils/requestIntegrityCheck' import { deferNetworkRequestsUntil } from '../../utils/deferNetworkRequestsUntil' import { createResponseListener } from './createResponseListener' import { validateWorkerScope } from './utils/validateWorkerScope' -import { devUtils } from '../../utils/internal/devUtils' export const createStartHandler = ( context: SetupWorkerInternalContext, @@ -76,13 +76,13 @@ Please consider using a custom "serviceWorker.url" option to point to the actual }) // Check if the active Service Worker is the latest published one - const [integrityError] = await until(() => + const integrityCheckResult = await until(() => requestIntegrityCheck(context, worker), ) - if (integrityError) { + if (integrityCheckResult.error) { devUtils.error(`\ -Detected outdated Service Worker: ${integrityError.message} +Detected outdated Service Worker: ${integrityCheckResult.error.message} The mocking is still enabled, but it's highly recommended that you update your Service Worker by running: diff --git a/src/setupWorker/start/utils/createMessageChannel.ts b/src/browser/setupWorker/start/utils/createMessageChannel.ts similarity index 62% rename from src/setupWorker/start/utils/createMessageChannel.ts rename to src/browser/setupWorker/start/utils/createMessageChannel.ts index 207ee5638..210a7c3d1 100644 --- a/src/setupWorker/start/utils/createMessageChannel.ts +++ b/src/browser/setupWorker/start/utils/createMessageChannel.ts @@ -1,5 +1,5 @@ import { - SerializedResponse, + StringifiedResponse, ServiceWorkerIncomingEventsMap, } from '../../glossary' @@ -12,9 +12,11 @@ export interface ServiceWorkerMessage< } interface WorkerChannelEventsMap { - MOCK_RESPONSE: [data: SerializedResponse, body?: [ArrayBuffer]] + MOCK_RESPONSE: [ + data: StringifiedResponse, + transfer?: [ReadableStream], + ] NOT_FOUND: [] - NETWORK_ERROR: [data: { name: string; message: string }] } export class WorkerChannel { @@ -25,6 +27,13 @@ export class WorkerChannel { ...rest: WorkerChannelEventsMap[Event] ): void { const [data, transfer] = rest - this.port.postMessage({ type: event, data }, { transfer }) + this.port.postMessage( + { type: event, data }, + { + // @ts-ignore ReadableStream can be transferred + // but TypeScript doesn't acknowledge that. + transfer, + }, + ) } } diff --git a/src/setupWorker/start/utils/enableMocking.ts b/src/browser/setupWorker/start/utils/enableMocking.ts similarity index 94% rename from src/setupWorker/start/utils/enableMocking.ts rename to src/browser/setupWorker/start/utils/enableMocking.ts index 890211b3d..c0f19f314 100644 --- a/src/setupWorker/start/utils/enableMocking.ts +++ b/src/browser/setupWorker/start/utils/enableMocking.ts @@ -1,4 +1,4 @@ -import { devUtils } from '../../../utils/internal/devUtils' +import { devUtils } from '~/core/utils/internal/devUtils' import { StartOptions, SetupWorkerInternalContext } from '../../glossary' import { printStartMessage } from './printStartMessage' diff --git a/src/setupWorker/start/utils/getWorkerByRegistration.ts b/src/browser/setupWorker/start/utils/getWorkerByRegistration.ts similarity index 100% rename from src/setupWorker/start/utils/getWorkerByRegistration.ts rename to src/browser/setupWorker/start/utils/getWorkerByRegistration.ts diff --git a/src/setupWorker/start/utils/getWorkerInstance.ts b/src/browser/setupWorker/start/utils/getWorkerInstance.ts similarity index 88% rename from src/setupWorker/start/utils/getWorkerInstance.ts rename to src/browser/setupWorker/start/utils/getWorkerInstance.ts index 56949d806..05594426b 100644 --- a/src/setupWorker/start/utils/getWorkerInstance.ts +++ b/src/browser/setupWorker/start/utils/getWorkerInstance.ts @@ -1,8 +1,8 @@ import { until } from '@open-draft/until' +import { devUtils } from '~/core/utils/internal/devUtils' +import { getAbsoluteWorkerUrl } from '../../../utils/getAbsoluteWorkerUrl' import { getWorkerByRegistration } from './getWorkerByRegistration' import { ServiceWorkerInstanceTuple, FindWorker } from '../../glossary' -import { getAbsoluteWorkerUrl } from '../../../utils/url/getAbsoluteWorkerUrl' -import { devUtils } from '../../../utils/internal/devUtils' /** * Returns an active Service Worker instance. @@ -50,7 +50,7 @@ export const getWorkerInstance = async ( } // When the Service Worker wasn't found, register it anew and return the reference. - const [error, instance] = await until( + const registrationResult = await until( async () => { const registration = await navigator.serviceWorker.register(url, options) return [ @@ -63,8 +63,8 @@ export const getWorkerInstance = async ( ) // Handle Service Worker registration errors. - if (error) { - const isWorkerMissing = error.message.includes('(404)') + if (registrationResult.error) { + const isWorkerMissing = registrationResult.error.message.includes('(404)') // Produce a custom error message when given a non-existing Service Worker url. // Suggest developers to check their setup. @@ -85,10 +85,10 @@ Learn more about creating the Service Worker script: https://mswjs.io/docs/cli/i throw new Error( devUtils.formatMessage( 'Failed to register the Service Worker:\n\n%s', - error.message, + registrationResult.error.message, ), ) } - return instance + return registrationResult.data } diff --git a/src/setupWorker/start/utils/prepareStartHandler.test.ts b/src/browser/setupWorker/start/utils/prepareStartHandler.test.ts similarity index 100% rename from src/setupWorker/start/utils/prepareStartHandler.test.ts rename to src/browser/setupWorker/start/utils/prepareStartHandler.test.ts diff --git a/src/setupWorker/start/utils/prepareStartHandler.ts b/src/browser/setupWorker/start/utils/prepareStartHandler.ts similarity index 90% rename from src/setupWorker/start/utils/prepareStartHandler.ts rename to src/browser/setupWorker/start/utils/prepareStartHandler.ts index 3828a13c1..e98fe832c 100644 --- a/src/setupWorker/start/utils/prepareStartHandler.ts +++ b/src/browser/setupWorker/start/utils/prepareStartHandler.ts @@ -1,5 +1,5 @@ -import { RequiredDeep } from '../../../typeUtils' -import { mergeRight } from '../../../utils/internal/mergeRight' +import { RequiredDeep } from '~/core/typeUtils' +import { mergeRight } from '~/core/utils/internal/mergeRight' import { SetupWorker, SetupWorkerInternalContext, diff --git a/src/setupWorker/start/utils/printStartMessage.test.ts b/src/browser/setupWorker/start/utils/printStartMessage.test.ts similarity index 100% rename from src/setupWorker/start/utils/printStartMessage.test.ts rename to src/browser/setupWorker/start/utils/printStartMessage.test.ts diff --git a/src/setupWorker/start/utils/printStartMessage.ts b/src/browser/setupWorker/start/utils/printStartMessage.ts similarity index 93% rename from src/setupWorker/start/utils/printStartMessage.ts rename to src/browser/setupWorker/start/utils/printStartMessage.ts index 9e588afaa..44ffcd353 100644 --- a/src/setupWorker/start/utils/printStartMessage.ts +++ b/src/browser/setupWorker/start/utils/printStartMessage.ts @@ -1,4 +1,4 @@ -import { devUtils } from '../../../utils/internal/devUtils' +import { devUtils } from '~/core/utils/internal/devUtils' export interface PrintStartMessageArgs { quiet?: boolean diff --git a/src/setupWorker/start/utils/validateWorkerScope.ts b/src/browser/setupWorker/start/utils/validateWorkerScope.ts similarity index 91% rename from src/setupWorker/start/utils/validateWorkerScope.ts rename to src/browser/setupWorker/start/utils/validateWorkerScope.ts index b288e0d39..0e93412c2 100644 --- a/src/setupWorker/start/utils/validateWorkerScope.ts +++ b/src/browser/setupWorker/start/utils/validateWorkerScope.ts @@ -1,4 +1,4 @@ -import { devUtils } from '../../../utils/internal/devUtils' +import { devUtils } from '~/core/utils/internal/devUtils' import { StartOptions } from '../../glossary' export function validateWorkerScope( diff --git a/src/setupWorker/stop/createFallbackStop.ts b/src/browser/setupWorker/stop/createFallbackStop.ts similarity index 100% rename from src/setupWorker/stop/createFallbackStop.ts rename to src/browser/setupWorker/stop/createFallbackStop.ts diff --git a/src/setupWorker/stop/createStop.ts b/src/browser/setupWorker/stop/createStop.ts similarity index 94% rename from src/setupWorker/stop/createStop.ts rename to src/browser/setupWorker/stop/createStop.ts index df4a2e5d1..48c37996d 100644 --- a/src/setupWorker/stop/createStop.ts +++ b/src/browser/setupWorker/stop/createStop.ts @@ -1,4 +1,4 @@ -import { devUtils } from '../../utils/internal/devUtils' +import { devUtils } from '~/core/utils/internal/devUtils' import { SetupWorkerInternalContext, StopHandler } from '../glossary' import { printStopMessage } from './utils/printStopMessage' diff --git a/src/setupWorker/stop/utils/printStopMessage.test.ts b/src/browser/setupWorker/stop/utils/printStopMessage.test.ts similarity index 100% rename from src/setupWorker/stop/utils/printStopMessage.test.ts rename to src/browser/setupWorker/stop/utils/printStopMessage.test.ts diff --git a/src/setupWorker/stop/utils/printStopMessage.ts b/src/browser/setupWorker/stop/utils/printStopMessage.ts similarity index 79% rename from src/setupWorker/stop/utils/printStopMessage.ts rename to src/browser/setupWorker/stop/utils/printStopMessage.ts index d12246fea..43a08a7a3 100644 --- a/src/setupWorker/stop/utils/printStopMessage.ts +++ b/src/browser/setupWorker/stop/utils/printStopMessage.ts @@ -1,4 +1,4 @@ -import { devUtils } from '../../../utils/internal/devUtils' +import { devUtils } from '~/core/utils/internal/devUtils' export function printStopMessage(args: { quiet?: boolean } = {}): void { if (args.quiet) { diff --git a/src/browser/tsconfig.json b/src/browser/tsconfig.json new file mode 100644 index 000000000..30d12be0c --- /dev/null +++ b/src/browser/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["dom", "WebWorker"] + }, + "include": ["./**/*.ts"] +} diff --git a/src/utils/deferNetworkRequestsUntil.test.ts b/src/browser/utils/deferNetworkRequestsUntil.test.ts similarity index 94% rename from src/utils/deferNetworkRequestsUntil.test.ts rename to src/browser/utils/deferNetworkRequestsUntil.test.ts index 8ba23be8a..c5f0e963c 100644 --- a/src/utils/deferNetworkRequestsUntil.test.ts +++ b/src/browser/utils/deferNetworkRequestsUntil.test.ts @@ -31,7 +31,7 @@ test('defers any requests that happen while a given promise is pending', async ( events.push('promise resolved') }) - // Calling this functions captures all requests that happen while + // Calling this functions intercepts all requests that happen while // the given promise is pending, and defers their execution // until the promise is resolved. deferNetworkRequestsUntil(workerPromise) diff --git a/src/utils/deferNetworkRequestsUntil.ts b/src/browser/utils/deferNetworkRequestsUntil.ts similarity index 100% rename from src/utils/deferNetworkRequestsUntil.ts rename to src/browser/utils/deferNetworkRequestsUntil.ts diff --git a/src/utils/url/getAbsoluteWorkerUrl.test.ts b/src/browser/utils/getAbsoluteWorkerUrl.test.ts similarity index 100% rename from src/utils/url/getAbsoluteWorkerUrl.test.ts rename to src/browser/utils/getAbsoluteWorkerUrl.test.ts diff --git a/src/utils/url/getAbsoluteWorkerUrl.ts b/src/browser/utils/getAbsoluteWorkerUrl.ts similarity index 100% rename from src/utils/url/getAbsoluteWorkerUrl.ts rename to src/browser/utils/getAbsoluteWorkerUrl.ts diff --git a/src/browser/utils/parseWorkerRequest.ts b/src/browser/utils/parseWorkerRequest.ts new file mode 100644 index 000000000..4160efcb8 --- /dev/null +++ b/src/browser/utils/parseWorkerRequest.ts @@ -0,0 +1,15 @@ +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 parseWorkerRequest( + incomingRequest: ServiceWorkerIncomingRequest, +): Request { + return new Request(incomingRequest.url, { + ...incomingRequest, + body: pruneGetRequestBody(incomingRequest), + }) +} diff --git a/src/browser/utils/pruneGetRequestBody.test.ts b/src/browser/utils/pruneGetRequestBody.test.ts new file mode 100644 index 000000000..eee2932a9 --- /dev/null +++ b/src/browser/utils/pruneGetRequestBody.test.ts @@ -0,0 +1,53 @@ +/** + * @jest-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) +}) diff --git a/src/browser/utils/pruneGetRequestBody.ts b/src/browser/utils/pruneGetRequestBody.ts new file mode 100644 index 000000000..b17602217 --- /dev/null +++ b/src/browser/utils/pruneGetRequestBody.ts @@ -0,0 +1,21 @@ +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 +} diff --git a/src/utils/internal/requestIntegrityCheck.ts b/src/browser/utils/requestIntegrityCheck.ts similarity index 90% rename from src/utils/internal/requestIntegrityCheck.ts rename to src/browser/utils/requestIntegrityCheck.ts index 10f1fb112..67e1b4144 100644 --- a/src/utils/internal/requestIntegrityCheck.ts +++ b/src/browser/utils/requestIntegrityCheck.ts @@ -1,4 +1,4 @@ -import { SetupWorkerInternalContext } from '../../setupWorker/glossary' +import type { SetupWorkerInternalContext } from '../setupWorker/glossary' export async function requestIntegrityCheck( context: SetupWorkerInternalContext, diff --git a/src/browser/utils/supportsReadableStreamTransfer.ts b/src/browser/utils/supportsReadableStreamTransfer.ts new file mode 100644 index 000000000..b1c5dc295 --- /dev/null +++ b/src/browser/utils/supportsReadableStreamTransfer.ts @@ -0,0 +1,17 @@ +/** + * 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 (error) { + return false + } +} diff --git a/src/context/body.test.ts b/src/context/body.test.ts deleted file mode 100644 index c6e7056a9..000000000 --- a/src/context/body.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { body } from './body' -import { set } from './set' -import { response } from '../response' - -test('sets a given body value without implicit "Content-Type" header', async () => { - const result = await response(body('Lorem ipsum')) - - expect(result).toHaveProperty('body', 'Lorem ipsum') - expect(result.headers.get('content-type')).toBeNull() -}) - -test('does not stringify raw body twice if content is string and "Content-Type" header is "json"', async () => { - const result = await response( - set('Content-Type', 'application/json'), - body(JSON.stringify('some text')), - ) - - expect(result).toHaveProperty('body', `"some text"`) -}) diff --git a/src/context/body.ts b/src/context/body.ts deleted file mode 100644 index 4a07a62ec..000000000 --- a/src/context/body.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ResponseTransformer } from '../response' - -/** - * Sets a raw response body. Does not append any `Content-Type` headers. - * @example - * res(ctx.body('Successful response')) - * res(ctx.body(JSON.stringify({ key: 'value' }))) - * @see {@link https://mswjs.io/docs/api/context/body `ctx.body()`} - */ -export const body = < - BodyType extends string | Blob | BufferSource | ReadableStream | FormData, ->( - value: BodyType, -): ResponseTransformer => { - return (res) => { - res.body = value - return res - } -} diff --git a/src/context/cookie.node.test.ts b/src/context/cookie.node.test.ts deleted file mode 100644 index 8551b706a..000000000 --- a/src/context/cookie.node.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @jest-environment node - */ -import { cookie } from './cookie' -import { response } from '../response' - -test('sets a cookie on the response headers, node environment', async () => { - const result = await response(cookie('my-cookie', 'arbitrary-value')) - expect(result.headers.get('set-cookie')).toEqual('my-cookie=arbitrary-value') -}) diff --git a/src/context/cookie.test.ts b/src/context/cookie.test.ts deleted file mode 100644 index c17cca610..000000000 --- a/src/context/cookie.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @jest-environment jsdom - */ -import * as cookieUtils from 'cookie' -import { cookie } from './cookie' -import { response } from '../response' -import { clearCookies } from '../../test/support/utils' - -beforeAll(() => { - clearCookies() -}) - -afterEach(() => { - clearCookies() -}) - -test('sets a given response cookie', async () => { - const result = await response(cookie('myCookie', 'value')) - - expect(result.headers.get('set-cookie')).toBe('myCookie=value') - - // Propagates the response cookies on the document. - const allCookies = cookieUtils.parse(document.cookie) - expect(allCookies).toEqual({ myCookie: 'value' }) -}) - -test('supports setting multiple response cookies', async () => { - const result = await response( - cookie('firstCookie', 'yes'), - cookie('secondCookie', 'no'), - ) - - expect(result.headers.get('set-cookie')).toBe( - 'secondCookie=no, firstCookie=yes', - ) - - const allCookies = cookieUtils.parse(document.cookie) - expect(allCookies).toEqual({ firstCookie: 'yes', secondCookie: 'no' }) -}) diff --git a/src/context/cookie.ts b/src/context/cookie.ts deleted file mode 100644 index 8d38b270a..000000000 --- a/src/context/cookie.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as cookieUtils from 'cookie' -import { ResponseTransformer } from '../response' - -/** - * Sets a given cookie on the mocked response. - * @example res(ctx.cookie('name', 'value')) - */ -export const cookie = ( - name: string, - value: string, - options?: cookieUtils.CookieSerializeOptions, -): ResponseTransformer => { - return (res) => { - const serializedCookie = cookieUtils.serialize(name, value, options) - res.headers.append('Set-Cookie', serializedCookie) - - if (typeof document !== 'undefined') { - document.cookie = serializedCookie - } - - return res - } -} diff --git a/src/context/data.test.ts b/src/context/data.test.ts deleted file mode 100644 index f73e0693c..000000000 --- a/src/context/data.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { data } from './data' -import { errors } from './errors' -import { response } from '../response' - -test('sets a single data on the response JSON body', async () => { - const result = await response(data({ name: 'msw' })) - - expect(result.headers.get('content-type')).toBe('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - data: { - name: 'msw', - }, - }), - ) -}) - -test('sets multiple data on the response JSON body', async () => { - const result = await response( - data({ name: 'msw' }), - data({ description: 'API mocking library' }), - ) - - expect(result.headers.get('content-type')).toBe('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - data: { - description: 'API mocking library', - name: 'msw', - }, - }), - ) -}) - -test('combines with error in the response JSON body', async () => { - const result = await response( - data({ name: 'msw' }), - errors([ - { - message: 'exceeds the limit of awesomeness', - }, - ]), - ) - - expect(result.headers.get('content-type')).toBe('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - errors: [ - { - message: 'exceeds the limit of awesomeness', - }, - ], - data: { - name: 'msw', - }, - }), - ) -}) diff --git a/src/context/data.ts b/src/context/data.ts deleted file mode 100644 index 9180cb7d9..000000000 --- a/src/context/data.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { jsonParse } from '../utils/internal/jsonParse' -import { mergeRight } from '../utils/internal/mergeRight' -import { json } from './json' -import { GraphQLPayloadContext } from '../typeUtils' - -/** - * Sets a given payload as a GraphQL response body. - * @example - * res(ctx.data({ user: { firstName: 'John' }})) - * @see {@link https://mswjs.io/docs/api/context/data `ctx.data()`} - */ -export const data: GraphQLPayloadContext> = ( - payload, -) => { - return (res) => { - const prevBody = jsonParse(res.body) || {} - const nextBody = mergeRight(prevBody, { data: payload }) - - return json(nextBody)(res) - } -} diff --git a/src/context/delay.node.test.ts b/src/context/delay.node.test.ts deleted file mode 100644 index 09d78a465..000000000 --- a/src/context/delay.node.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @jest-environment node - */ -import { delay, NODE_SERVER_RESPONSE_TIME } from './delay' -import { response } from '../response' - -test('sets a Node.js-specific response delay when not provided', async () => { - const resolvedResponse = await response(delay()) - expect(resolvedResponse).toHaveProperty('delay', NODE_SERVER_RESPONSE_TIME) -}) - -test('allows response delay duration overrides', async () => { - const resolvedResponse = await response(delay(1234)) - expect(resolvedResponse).toHaveProperty('delay', 1234) -}) - -test('throws an exception given a too large duration', async () => { - const createErrorMessage = (value: any) => { - return `Failed to delay a response: provided delay duration (${value}) exceeds the maximum allowed duration for "setTimeout" (2147483647). 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.` - } - - const exceedingValues = [ - Infinity, - Number.MAX_VALUE, - Number.MAX_SAFE_INTEGER, - 2147483648, - ] - - for (const value of exceedingValues) { - await expect(() => response(delay(value))).rejects.toThrow( - createErrorMessage(value), - ) - } -}) - -test('throws an exception given an unknown delay mode', async () => { - await expect(() => response(delay('foo' as any))).rejects.toThrow( - 'Failed to delay a response: unknown delay mode "foo". Please make sure you provide one of the supported modes ("real", "infinite") or a number to "ctx.delay".', - ) -}) diff --git a/src/context/delay.test.ts b/src/context/delay.test.ts deleted file mode 100644 index e3ea29852..000000000 --- a/src/context/delay.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @jest-environment jsdom - * - * Since jsdom also runs in Node.js, expect a Node.js-specific implicit delay. - */ -import { delay, NODE_SERVER_RESPONSE_TIME } from './delay' -import { response } from '../response' - -test('sets a Node.js-specific response delay when not provided', async () => { - const resolvedResponse = await response(delay()) - expect(resolvedResponse).toHaveProperty('delay', NODE_SERVER_RESPONSE_TIME) -}) - -test('allows response delay duration overrides', async () => { - const resolvedResponse = await response(delay(1234)) - expect(resolvedResponse).toHaveProperty('delay', 1234) -}) - -test('throws an exception given a too large duration', async () => { - const createErrorMessage = (value: any) => { - return `Failed to delay a response: provided delay duration (${value}) exceeds the maximum allowed duration for "setTimeout" (2147483647). 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.` - } - - const exceedingValues = [ - Infinity, - Number.MAX_VALUE, - Number.MAX_SAFE_INTEGER, - 2147483648, - ] - - for (const value of exceedingValues) { - await expect(() => response(delay(value))).rejects.toThrow( - createErrorMessage(value), - ) - } -}) - -test('throws an exception given an unknown delay mode', async () => { - await expect(() => response(delay('foo' as any))).rejects.toThrow( - 'Failed to delay a response: unknown delay mode "foo". Please make sure you provide one of the supported modes ("real", "infinite") or a number to "ctx.delay".', - ) -}) diff --git a/src/context/delay.ts b/src/context/delay.ts deleted file mode 100644 index 50ff9b01a..000000000 --- a/src/context/delay.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { isNodeProcess } from 'is-node-process' -import { ResponseTransformer } from '../response' - -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 - -const getRandomServerResponseTime = () => { - 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 - * res(ctx.delay(1200)) // delay response by 1200ms - * res(ctx.delay()) // emulate realistic server response time - * res(ctx.delay('infinite')) // delay response infinitely - * @see {@link https://mswjs.io/docs/api/context/delay `ctx.delay()`} - */ -export const delay = ( - durationOrMode?: DelayMode | number, -): ResponseTransformer => { - return (res) => { - 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 = getRandomServerResponseTime() - 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 to "ctx.delay".`, - ) - } - } - } else if (typeof durationOrMode === 'undefined') { - // Use random realistic server response time when no explicit delay duration was provided. - delayTime = getRandomServerResponseTime() - } 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 - } - - res.delay = delayTime - return res - } -} diff --git a/src/context/errors.test.ts b/src/context/errors.test.ts deleted file mode 100644 index ed1cd1f1f..000000000 --- a/src/context/errors.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { errors } from './errors' -import { data } from './data' -import { response } from '../response' - -test('sets a given error on the response JSON body', async () => { - const result = await response(errors([{ message: 'Error message' }])) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - errors: [ - { - message: 'Error message', - }, - ], - }), - ) -}) - -test('sets given errors on the response JSON body', async () => { - const result = await response( - errors([{ message: 'Error message' }, { message: 'Second error' }]), - ) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - errors: [ - { - message: 'Error message', - }, - { - message: 'Second error', - }, - ], - }), - ) -}) - -test('combines with data in the response JSON body', async () => { - const result = await response( - data({ name: 'msw' }), - errors([{ message: 'exceeds the limit of awesomeness' }]), - ) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - errors: [ - { - message: 'exceeds the limit of awesomeness', - }, - ], - data: { - name: 'msw', - }, - }), - ) -}) - -test('bypasses undefined errors', async () => { - const result = await response(errors(undefined), errors(null)) - - expect(result.headers.get('content-type')).not.toEqual('application/json') - expect(result).toHaveProperty('body', null) -}) diff --git a/src/context/errors.ts b/src/context/errors.ts deleted file mode 100644 index c64d97331..000000000 --- a/src/context/errors.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { GraphQLError } from 'graphql' -import { ResponseTransformer } from '../response' -import { jsonParse } from '../utils/internal/jsonParse' -import { mergeRight } from '../utils/internal/mergeRight' -import { json } from './json' - -/** - * Sets a given list of GraphQL errors on the mocked response. - * @example res(ctx.errors([{ message: 'Unauthorized' }])) - * @see {@link https://mswjs.io/docs/api/context/errors} - */ -export const errors = < - ErrorsType extends readonly Partial[] | null | undefined, ->( - errorsList: ErrorsType, -): ResponseTransformer => { - return (res) => { - if (errorsList == null) { - return res - } - - const prevBody = jsonParse(res.body) || {} - const nextBody = mergeRight(prevBody, { errors: errorsList }) - - return json(nextBody)(res as any) as any - } -} diff --git a/src/context/extensions.test.ts b/src/context/extensions.test.ts deleted file mode 100644 index b341e1598..000000000 --- a/src/context/extensions.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { errors } from './errors' -import { data } from './data' -import { extensions } from './extensions' -import { response } from '../response' - -test('sets standalone extensions on the response JSON body', async () => { - const result = await response(extensions({ tracking: { version: 1 } })) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result.body).toEqual( - JSON.stringify({ - extensions: { - tracking: { - version: 1, - }, - }, - }), - ) -}) - -test('sets given extensions on the response JSON body with data', async () => { - const result = await response( - data({ hello: 'world' }), - extensions({ tracking: { version: 1 } }), - ) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result.body).toEqual( - JSON.stringify({ - extensions: { - tracking: { - version: 1, - }, - }, - data: { - hello: 'world', - }, - }), - ) -}) - -test('sets given extensions on the response JSON body in the presence with data and errors', async () => { - const result = await response( - data({ hello: 'world' }), - extensions({ tracking: { version: 1 } }), - errors([{ message: 'Error message' }]), - ) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result.body).toEqual( - JSON.stringify({ - errors: [ - { - message: 'Error message', - }, - ], - extensions: { - tracking: { - version: 1, - }, - }, - data: { - hello: 'world', - }, - }), - ) -}) diff --git a/src/context/extensions.ts b/src/context/extensions.ts deleted file mode 100644 index 4c2925394..000000000 --- a/src/context/extensions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { jsonParse } from '../utils/internal/jsonParse' -import { mergeRight } from '../utils/internal/mergeRight' -import { json } from './json' -import { GraphQLPayloadContext } from '../typeUtils' - -/** - * Sets the GraphQL extensions on a given response. - * @example - * res(ctx.extensions({ tracing: { version: 1 }})) - * @see {@link https://mswjs.io/docs/api/context/extensions `ctx.extensions()`} - */ -export const extensions: GraphQLPayloadContext> = ( - payload, -) => { - return (res) => { - const prevBody = jsonParse(res.body) || {} - const nextBody = mergeRight(prevBody, { extensions: payload }) - return json(nextBody)(res) - } -} diff --git a/src/context/fetch.test.ts b/src/context/fetch.test.ts deleted file mode 100644 index 01c1ae13a..000000000 --- a/src/context/fetch.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { augmentRequestInit } from './fetch' - -test('augments RequestInit with the Headers instance', () => { - const result = augmentRequestInit({ - headers: new Headers({ Authorization: 'token' }), - }) - const headers = new Headers(result.headers) - - expect(headers.get('Authorization')).toEqual('token') - expect(headers.get('x-msw-bypass')).toEqual('true') -}) - -test('augments RequestInit with the string[][] headers object', () => { - const result = augmentRequestInit({ - headers: [['Authorization', 'token']], - }) - const headers = new Headers(result.headers) - - expect(headers.get('x-msw-bypass')).toEqual('true') - expect(headers.get('authorization')).toEqual('token') -}) - -test('aguments RequestInit with the Record headers', () => { - const result = augmentRequestInit({ - headers: { - Authorization: 'token', - }, - }) - const headers = new Headers(result.headers) - - expect(headers.get('x-msw-bypass')).toEqual('true') - expect(headers.get('authorization')).toEqual('token') -}) diff --git a/src/context/fetch.ts b/src/context/fetch.ts deleted file mode 100644 index 6d91a756d..000000000 --- a/src/context/fetch.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isNodeProcess } from 'is-node-process' -import { Headers } from 'headers-polyfill' -import { MockedRequest } from '../utils/request/MockedRequest' - -const useFetch: (input: RequestInfo, init?: RequestInit) => Promise = - isNodeProcess() - ? (input, init) => - import('node-fetch').then(({ default: nodeFetch }) => - (nodeFetch as unknown as typeof window.fetch)(input, init), - ) - : globalThis.fetch - -export const augmentRequestInit = (requestInit: RequestInit): RequestInit => { - const headers = new Headers(requestInit.headers) - headers.set('x-msw-bypass', 'true') - - return { - ...requestInit, - headers: headers.all(), - } -} - -const createFetchRequestParameters = (input: MockedRequest): RequestInit => { - const { body, method } = input - const requestParameters: RequestInit = { - ...input, - body: undefined, - } - - if (['GET', 'HEAD'].includes(method)) { - return requestParameters - } - - if ( - typeof body === 'object' || - typeof body === 'number' || - typeof body === 'boolean' - ) { - requestParameters.body = JSON.stringify(body) - } else { - requestParameters.body = body - } - - return requestParameters -} - -/** - * Performs a bypassed request inside a request handler. - * @example - * const originalResponse = await ctx.fetch(req) - * @see {@link https://mswjs.io/docs/api/context/fetch `ctx.fetch()`} - */ -export const fetch = ( - input: string | MockedRequest, - requestInit: RequestInit = {}, -): Promise => { - if (typeof input === 'string') { - return useFetch(input, augmentRequestInit(requestInit)) - } - - const requestParameters = createFetchRequestParameters(input) - const derivedRequestInit = augmentRequestInit(requestParameters) - - return useFetch(input.url.href, derivedRequestInit) -} diff --git a/src/context/field.test.ts b/src/context/field.test.ts deleted file mode 100644 index 48952e9d0..000000000 --- a/src/context/field.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { field } from './field' -import { response } from '../response' -import { data } from './data' -import { errors } from './errors' - -test('sets a given field value string on the response JSON body', async () => { - const result = await response(field('field', 'value')) - - expect(result.headers.get('content-type')).toBe('application/json') - expect(result).toHaveProperty('body', JSON.stringify({ field: 'value' })) -}) - -test('sets a given field value object on the response JSON body', async () => { - const result = await response( - field('metadata', { - date: new Date('2022-05-27'), - comment: 'nice metadata', - }), - ) - - expect(result.headers.get('content-type')).toBe('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - metadata: { date: new Date('2022-05-27'), comment: 'nice metadata' }, - }), - ) -}) - -test('combines with data, errors and other field in the response JSON body', async () => { - const result = await response( - data({ name: 'msw' }), - errors([{ message: 'exceeds the limit of awesomeness' }]), - field('field', { errorCode: 'value' }), - field('field2', 123), - ) - - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result).toHaveProperty( - 'body', - JSON.stringify({ - field2: 123, - field: { errorCode: 'value' }, - errors: [ - { - message: 'exceeds the limit of awesomeness', - }, - ], - data: { - name: 'msw', - }, - }), - ) -}) - -test('throws when trying to set non-serializable values', async () => { - await expect(response(field('metadata', BigInt(1)))).rejects.toThrow( - 'Do not know how to serialize a BigInt', - ) -}) - -test('throws when passing an empty string as field name', async () => { - await expect(response(field('' as string, 'value'))).rejects.toThrow( - `[MSW] Failed to set a custom field on a GraphQL response: field name cannot be empty.`, - ) -}) - -test('throws when passing an empty string (when trimmed) as field name', async () => { - await expect(response(field(' ' as string, 'value'))).rejects.toThrow( - `[MSW] Failed to set a custom field on a GraphQL response: field name cannot be empty.`, - ) -}) - -test('throws when using "data" as the field name', async () => { - await expect( - response( - field( - // @ts-expect-error Test runtime value. - 'data', - 'value', - ), - ), - ).rejects.toThrow( - '[MSW] Failed to set a custom "data" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.data()" instead?', - ) -}) - -test('throws when using "errors" as the field name', async () => { - await expect( - response( - field( - // @ts-expect-error Test runtime value. - 'errors', - 'value', - ), - ), - ).rejects.toThrow( - '[MSW] Failed to set a custom "errors" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.errors()" instead?', - ) -}) - -test('throws when using "extensions" as the field name', async () => { - await expect( - response( - field( - // @ts-expect-error Test runtime value. - 'extensions', - 'value', - ), - ), - ).rejects.toThrow( - '[MSW] Failed to set a custom "extensions" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.extensions()" instead?', - ) -}) diff --git a/src/context/field.ts b/src/context/field.ts deleted file mode 100644 index a97764607..000000000 --- a/src/context/field.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { invariant } from 'outvariant' -import { ResponseTransformer } from '../response' -import { devUtils } from '../utils/internal/devUtils' -import { jsonParse } from '../utils/internal/jsonParse' -import { mergeRight } from '../utils/internal/mergeRight' -import { json } from './json' - -type ForbiddenFieldNames = '' | 'data' | 'errors' | 'extensions' - -/** - * Set a custom field on the GraphQL mocked response. - * @example res(ctx.fields('customField', value)) - * @see {@link https://mswjs.io/docs/api/context/field} - */ -export const field = ( - fieldName: FieldNameType extends ForbiddenFieldNames ? never : FieldNameType, - fieldValue: FieldValueType, -): ResponseTransformer => { - return (res) => { - validateFieldName(fieldName) - - const prevBody = jsonParse(res.body) || {} - const nextBody = mergeRight(prevBody, { [fieldName]: fieldValue }) - - return json(nextBody)(res as any) as any - } -} - -function validateFieldName(fieldName: string) { - invariant( - fieldName.trim() !== '', - devUtils.formatMessage( - 'Failed to set a custom field on a GraphQL response: field name cannot be empty.', - ), - ) - - invariant( - fieldName !== 'data', - devUtils.formatMessage( - 'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.data()" instead?', - fieldName, - ), - ) - - invariant( - fieldName !== 'errors', - devUtils.formatMessage( - 'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.errors()" instead?', - fieldName, - ), - ) - - invariant( - fieldName !== 'extensions', - devUtils.formatMessage( - 'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.extensions()" instead?', - fieldName, - ), - ) -} diff --git a/src/context/index.ts b/src/context/index.ts deleted file mode 100644 index 6cbed148d..000000000 --- a/src/context/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { status } from './status' -export { set } from './set' -export { cookie } from './cookie' -export { body } from './body' -export { data } from './data' -export { extensions } from './extensions' -export { delay } from './delay' -export { errors } from './errors' -export { fetch } from './fetch' -export { json } from './json' -export { text } from './text' -export { xml } from './xml' diff --git a/src/context/json.test.ts b/src/context/json.test.ts deleted file mode 100644 index 4c0f2b3cf..000000000 --- a/src/context/json.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { json } from './json' -import { response } from '../response' - -test('sets response content type and body to the given JSON', async () => { - const result = await response(json({ firstName: 'John' })) - expect(result.headers.get('content-type')).toEqual('application/json') - expect(result).toHaveProperty('body', `{"firstName":"John"}`) -}) - -test('sets given Array as the response JSOn body', async () => { - const result = await response(json([1, '2', true, { ok: true }, ''])) - expect(result).toHaveProperty('body', `[1,"2",true,{"ok":true},""]`) -}) - -test('sets given string as the response JSON body', async () => { - const result = await response(json('some string')) - expect(result).toHaveProperty('body', `"some string"`) -}) - -test('sets given boolean as the response JSON body', async () => { - const result = await response(json(true)) - expect(result).toHaveProperty('body', `true`) -}) - -test('sets given date as the response JSON body', async () => { - const result = await response(json(new Date(Date.UTC(2020, 0, 1)))) - expect(result).toHaveProperty('body', `"2020-01-01T00:00:00.000Z"`) -}) diff --git a/src/context/json.ts b/src/context/json.ts deleted file mode 100644 index ae67fd45d..000000000 --- a/src/context/json.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ResponseTransformer } from '../response' - -/** - * Sets the given value as the JSON body of the response. - * Appends a `Content-Type: application/json` header on the - * mocked response. - * @example - * res(ctx.json('Some string')) - * res(ctx.json({ key: 'value' })) - * res(ctx.json([1, '2', false, { ok: true }])) - * @see {@link https://mswjs.io/docs/api/context/json `ctx.json()`} - */ -export const json = ( - body: BodyTypeJSON, -): ResponseTransformer => { - return (res) => { - res.headers.set('Content-Type', 'application/json') - res.body = JSON.stringify(body) as any - - return res - } -} diff --git a/src/context/set.test.ts b/src/context/set.test.ts deleted file mode 100644 index fa061f4f1..000000000 --- a/src/context/set.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { set } from './set' -import { response } from '../response' - -test('sets a single header', async () => { - const { headers } = await response(set('Content-Type', 'image/*')) - expect(headers.get('content-type')).toEqual('image/*') -}) - -test('sets a single header with multiple values', async () => { - const { headers } = await response( - set({ - Accept: ['application/json', 'image/png'], - }), - ) - - expect(headers.get('accept')).toEqual('application/json, image/png') -}) - -test('sets multiple headers', async () => { - const { headers } = await response( - set({ - Accept: '*/*', - 'Accept-Language': 'en', - 'Content-Type': 'application/json', - }), - ) - - expect(headers.get('accept')).toEqual('*/*') - expect(headers.get('accept-language')).toEqual('en') - expect(headers.get('content-type')).toEqual('application/json') -}) diff --git a/src/context/set.ts b/src/context/set.ts deleted file mode 100644 index 71ca89c52..000000000 --- a/src/context/set.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { objectToHeaders } from 'headers-polyfill' -import { ResponseTransformer } from '../response' - -export type HeadersObject = Record< - KeyType, - string | string[] -> - -/** - * @see https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name - */ -export type ForbiddenHeaderNames = - | 'cookie' - | 'cookie2' - | 'set-cookie' - | 'set-cookie2' - -export type ForbiddenHeaderError = - `SafeResponseHeader: the '${HeaderName}' header cannot be set on the response. Please use the 'ctx.cookie()' function instead.` - -/** - * Sets one or multiple response headers. - * @example - * ctx.set('Content-Type', 'text/plain') - * ctx.set({ - * 'Accept': 'application/javascript', - * 'Content-Type': "text/plain" - * }) - * @see {@link https://mswjs.io/docs/api/context/set `ctx.set()`} - */ -export function set( - ...args: N extends string - ? Lowercase extends ForbiddenHeaderNames - ? [ForbiddenHeaderError] - : [N, string] - : N extends HeadersObject - ? Lowercase extends ForbiddenHeaderNames - ? [ForbiddenHeaderError] - : [N] - : [N] -): ResponseTransformer { - return (res) => { - const [name, value] = args - - if (typeof name === 'string') { - res.headers.append(name, value as string) - } else { - const headers = objectToHeaders(name) - headers.forEach((value, name) => { - res.headers.append(name, value) - }) - } - - return res - } -} diff --git a/src/context/status.test.ts b/src/context/status.test.ts deleted file mode 100644 index 2bd795536..000000000 --- a/src/context/status.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { status } from './status' -import { response } from '../response' - -test('sets given status code on the response', async () => { - const result = await response(status(403)) - expect(result).toHaveProperty('status', 403) - expect(result).toHaveProperty('statusText', 'Forbidden') -}) - -test('supports custom status text', async () => { - const result = await response(status(301, 'Custom text')) - expect(result).toHaveProperty('status', 301) - expect(result).toHaveProperty('statusText', 'Custom text') -}) diff --git a/src/context/status.ts b/src/context/status.ts deleted file mode 100644 index e8113519c..000000000 --- a/src/context/status.ts +++ /dev/null @@ -1,22 +0,0 @@ -import statuses from 'statuses/codes.json' -import { ResponseTransformer } from '../response' - -/** - * Sets a response status code and text. - * @example - * res(ctx.status(301)) - * res(ctx.status(400, 'Custom status text')) - * @see {@link https://mswjs.io/docs/api/context/status `ctx.status()`} - */ -export const status = ( - statusCode: number, - statusText?: string, -): ResponseTransformer => { - return (res) => { - res.status = statusCode - res.statusText = - statusText || statuses[String(statusCode) as keyof typeof statuses] - - return res - } -} diff --git a/src/context/text.test.ts b/src/context/text.test.ts deleted file mode 100644 index aafc263b1..000000000 --- a/src/context/text.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { text } from './text' -import { response } from '../response' - -test('sets a given text as the response body', async () => { - const result = await response(text('Lorem ipsum')) - - expect(result.headers.get('content-type')).toEqual('text/plain') - expect(result).toHaveProperty('body', 'Lorem ipsum') -}) diff --git a/src/context/text.ts b/src/context/text.ts deleted file mode 100644 index 6cdbb2d1d..000000000 --- a/src/context/text.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ResponseTransformer } from '../response' - -/** - * Sets a textual response body. Appends a `Content-Type: text/plain` - * header on the mocked response. - * @example res(ctx.text('Successful response')) - * @see {@link https://mswjs.io/docs/api/context/text `ctx.text()`} - */ -export const text = ( - body: BodyType, -): ResponseTransformer => { - return (res) => { - res.headers.set('Content-Type', 'text/plain') - res.body = body - return res - } -} diff --git a/src/context/xml.test.ts b/src/context/xml.test.ts deleted file mode 100644 index 479e24817..000000000 --- a/src/context/xml.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { xml } from './xml' -import { response } from '../response' - -test('sets a given XML as the response body', async () => { - const result = await response(xml('JohnJohnContent')) - * @see {@link https://mswjs.io/docs/api/context/xml `ctx.xml()`} - */ -export const xml = ( - body: BodyType, -): ResponseTransformer => { - return (res) => { - res.headers.set('Content-Type', 'text/xml') - res.body = body - return res - } -} diff --git a/src/core/HttpResponse.test.ts b/src/core/HttpResponse.test.ts new file mode 100644 index 000000000..ef9ec2df7 --- /dev/null +++ b/src/core/HttpResponse.test.ts @@ -0,0 +1,65 @@ +/** + * @jest-environment node + */ +import { TextEncoder } from 'util' +import { HttpResponse } 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) + expect(await response.text()).toBe('') +}) + +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) + expect(await response.text()).toBe('hello world') +}) + +it('creates a json response', 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' }) +}) + +it('creates an xml response', async () => { + const response = HttpResponse.xml('') + + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + expect(response.body).toBeInstanceOf(ReadableStream) + expect(await response.text()).toBe('') +}) + +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) +}) + +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') +}) diff --git a/src/core/HttpResponse.ts b/src/core/HttpResponse.ts new file mode 100644 index 000000000..e5e51c482 --- /dev/null +++ b/src/core/HttpResponse.ts @@ -0,0 +1,122 @@ +import type { DefaultBodyType } from './handlers/RequestHandler' +import { + decorateResponse, + normalizeResponseInit, +} from './utils/HttpResponse/decorators' + +export interface HttpResponseInit extends ResponseInit { + type?: ResponseType +} + +declare const bodyType: unique symbol + +export interface StrictRequest + extends Request { + json(): Promise +} + +/** + * Opaque `Response` type that supports strict body type. + */ +export interface StrictResponse + extends Response { + readonly [bodyType]: BodyType +} + +/** + * 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 extends Response { + constructor(body?: BodyInit | null, init?: HttpResponseInit) { + const responseInit = normalizeResponseInit(init) + super(body, responseInit) + decorateResponse(this, responseInit) + } + + /** + * Create a `Response` with a `Content-Type: "text/plain"` body. + * @example + * HttpResponse.text('hello world') + * HttpResponse.text('Error', { status: 500 }) + */ + static text( + body?: BodyType | null, + init?: HttpResponseInit, + ): StrictResponse { + const responseInit = normalizeResponseInit(init) + responseInit.headers.set('Content-Type', 'text/plain') + return new HttpResponse(body, responseInit) as StrictResponse + } + + /** + * 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?: BodyType | null, + init?: HttpResponseInit, + ): StrictResponse { + const responseInit = normalizeResponseInit(init) + responseInit.headers.set('Content-Type', 'application/json') + return new HttpResponse( + JSON.stringify(body), + responseInit, + ) as StrictResponse + } + + /** + * Create a `Response` with a `Content-Type: "application/xml"` body. + * @example + * HttpResponse.xml(``) + * HttpResponse.xml(`

`, { status: 201 }) + */ + static xml( + body?: BodyType | null, + init?: HttpResponseInit, + ): Response { + const responseInit = normalizeResponseInit(init) + responseInit.headers.set('Content-Type', 'text/xml') + return new HttpResponse(body, responseInit) + } + + /** + * 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?: ArrayBuffer, init?: HttpResponseInit): Response { + const responseInit = normalizeResponseInit(init) + + if (body) { + responseInit.headers.set('Content-Length', body.byteLength.toString()) + } + + return new HttpResponse(body, responseInit) + } + + /** + * 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): Response { + return new HttpResponse(body, normalizeResponseInit(init)) + } +} diff --git a/src/SetupApi.ts b/src/core/SetupApi.ts similarity index 84% rename from src/SetupApi.ts rename to src/core/SetupApi.ts index d345c82fa..d7cae2f48 100644 --- a/src/SetupApi.ts +++ b/src/core/SetupApi.ts @@ -1,7 +1,6 @@ import { invariant } from 'outvariant' import { EventMap, Emitter } from 'strict-event-emitter' import { - DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo, } from './handlers/RequestHandler' @@ -9,12 +8,12 @@ import { LifeCycleEventEmitter } from './sharedOptions' import { devUtils } from './utils/internal/devUtils' import { pipeEvents } from './utils/internal/pipeEvents' import { toReadonlyArray } from './utils/internal/toReadonlyArray' -import { MockedRequest } from './utils/request/MockedRequest' +import { Disposable } from './utils/internal/Disposable' /** * Generic class for the mock API setup. */ -export abstract class SetupApi { +export abstract class SetupApi extends Disposable { protected initialHandlers: ReadonlyArray protected currentHandlers: Array protected readonly emitter: Emitter @@ -23,6 +22,8 @@ export abstract class SetupApi { public readonly events: LifeCycleEventEmitter constructor(...initialHandlers: Array) { + super() + this.validateHandlers(...initialHandlers) this.initialHandlers = toReadonlyArray(initialHandlers) @@ -33,6 +34,11 @@ export abstract class SetupApi { pipeEvents(this.emitter, this.publicEmitter) this.events = this.createLifeCycleEvents() + + this.subscriptions.push(() => { + this.emitter.removeAllListeners() + this.publicEmitter.removeAllListeners() + }) } private validateHandlers(...handlers: ReadonlyArray): void { @@ -48,18 +54,13 @@ export abstract class SetupApi { } } - protected dispose(): void { - this.emitter.removeAllListeners() - this.publicEmitter.removeAllListeners() - } - public use(...runtimeHandlers: Array): void { this.currentHandlers.unshift(...runtimeHandlers) } public restoreHandlers(): void { this.currentHandlers.forEach((handler) => { - handler.markAsSkipped(false) + handler.isUsed = false }) } @@ -69,12 +70,7 @@ export abstract class SetupApi { } public listHandlers(): ReadonlyArray< - RequestHandler< - RequestHandlerDefaultInfo, - MockedRequest, - any, - MockedRequest - > + RequestHandler > { return toReadonlyArray(this.currentHandlers) } @@ -92,6 +88,4 @@ export abstract class SetupApi { }, } } - - abstract printHandlers(): void } diff --git a/src/core/bypass.test.ts b/src/core/bypass.test.ts new file mode 100644 index 000000000..aca432070 --- /dev/null +++ b/src/core/bypass.test.ts @@ -0,0 +1,47 @@ +/** + * @jest-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(Object.fromEntries(request.headers.entries())).toEqual({ + 'x-msw-intention': 'bypass', + }) +}) + +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(Object.fromEntries(request.headers)).toEqual({ + 'x-msw-intention': 'bypass', + }) +}) + +it('returns bypassed request given request instance', async () => { + const original = new Request('http://localhost/resource', { + method: 'POST', + headers: { + '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(Object.fromEntries(request.headers.entries())).toEqual({ + ...Object.fromEntries(original.headers.entries()), + 'x-msw-intention': 'bypass', + }) +}) diff --git a/src/core/bypass.ts b/src/core/bypass.ts new file mode 100644 index 000000000..e467cf30c --- /dev/null +++ b/src/core/bypass.ts @@ -0,0 +1,36 @@ +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 { + const request = input instanceof Request ? input : new Request(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() + + // Set the internal header that would instruct MSW + // to bypass this request from any further request matching. + // Unlike "passthrough()", bypass is meant for performing + // additional requests within pending request resolution. + requestClone.headers.set('x-msw-intention', 'bypass') + + return requestClone +} diff --git a/src/core/delay.ts b/src/core/delay.ts new file mode 100644 index 000000000..0244567ed --- /dev/null +++ b/src/core/delay.ts @@ -0,0 +1,70 @@ +import { isNodeProcess } from 'is-node-process' + +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) => setTimeout(resolve, delayTime)) +} diff --git a/src/graphql.test.ts b/src/core/graphql.test.ts similarity index 100% rename from src/graphql.test.ts rename to src/core/graphql.test.ts index 0658b09a5..e783d0fbd 100644 --- a/src/graphql.test.ts +++ b/src/core/graphql.test.ts @@ -3,9 +3,9 @@ import { graphql } from './graphql' test('exports supported GraphQL operation types', () => { expect(graphql).toBeDefined() expect(Object.keys(graphql)).toEqual([ - 'operation', 'query', 'mutation', + 'operation', 'link', ]) }) diff --git a/src/core/graphql.ts b/src/core/graphql.ts new file mode 100644 index 000000000..b3409eb7f --- /dev/null +++ b/src/core/graphql.ts @@ -0,0 +1,138 @@ +import type { DocumentNode, OperationTypeNode } from 'graphql' +import { + ResponseResolver, + RequestHandlerOptions, +} from './handlers/RequestHandler' +import { + GraphQLHandler, + GraphQLVariables, + ExpectedOperationTypeNode, + GraphQLHandlerNameSelector, + GraphQLResolverExtras, + GraphQLResponseBody, +} from './handlers/GraphQLHandler' +import type { Path } from './utils/matching/matchRequestUrl' + +export interface TypedDocumentNode< + Result = { [key: string]: any }, + Variables = { [key: string]: any }, +> extends DocumentNode { + __apiType?: (variables: Variables) => Result + __resultType?: Result + __variablesType?: Variables +} + +function createScopedGraphQLHandler( + operationType: ExpectedOperationTypeNode, + url: Path, +) { + return < + Query extends Record, + Variables extends GraphQLVariables = GraphQLVariables, + >( + operationName: + | GraphQLHandlerNameSelector + | DocumentNode + | TypedDocumentNode, + resolver: ResponseResolver< + GraphQLResolverExtras, + null, + GraphQLResponseBody + >, + options: RequestHandlerOptions = {}, + ) => { + return new GraphQLHandler( + operationType, + operationName, + url, + resolver, + options, + ) + } +} + +function createGraphQLOperationHandler(url: Path) { + return < + Query extends Record, + Variables extends GraphQLVariables = GraphQLVariables, + >( + resolver: ResponseResolver< + GraphQLResolverExtras, + null, + GraphQLResponseBody + >, + ) => { + return new GraphQLHandler('all', new RegExp('.*'), url, resolver) + } +} + +const standardGraphQLHandlers = { + /** + * 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#graphloperationresolver `graphql.operation()` API reference} + */ + operation: createGraphQLOperationHandler('*'), +} + +function createGraphQLLink(url: Path): typeof standardGraphQLHandlers { + return { + operation: createGraphQLOperationHandler(url), + query: createScopedGraphQLHandler('query' as OperationTypeNode, url), + mutation: createScopedGraphQLHandler('mutation' as OperationTypeNode, url), + } +} + +/** + * 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 = { + ...standardGraphQLHandlers, + + /** + * 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: createGraphQLLink, +} diff --git a/src/handlers/GraphQLHandler.test.ts b/src/core/handlers/GraphQLHandler.test.ts similarity index 72% rename from src/handlers/GraphQLHandler.test.ts rename to src/core/handlers/GraphQLHandler.test.ts index 4fefa5d86..cebac6440 100644 --- a/src/handlers/GraphQLHandler.test.ts +++ b/src/core/handlers/GraphQLHandler.test.ts @@ -3,27 +3,25 @@ */ import { encodeBuffer } from '@mswjs/interceptors' import { OperationTypeNode, parse } from 'graphql' -import { Headers } from 'headers-polyfill' -import { context, MockedRequest, MockedRequestInit } from '..' -import { response } from '../response' import { - GraphQLContext, GraphQLHandler, - GraphQLRequest, GraphQLRequestBody, + GraphQLResolverExtras, isDocumentNode, } from './GraphQLHandler' +import { HttpResponse } from '../HttpResponse' import { ResponseResolver } from './RequestHandler' -const resolver: ResponseResolver< - GraphQLRequest<{ userId: string }>, - GraphQLContext -> = (req, res, ctx) => { - return res( - ctx.data({ - user: { id: req.variables.userId }, - }), - ) +const resolver: ResponseResolver> = ({ + variables, +}) => { + return HttpResponse.json({ + data: { + user: { + id: variables.userId, + }, + }, + }) } function createGetGraphQLRequest( @@ -33,17 +31,15 @@ function createGetGraphQLRequest( const requestUrl = new URL(hostname) requestUrl.searchParams.set('query', body?.query) requestUrl.searchParams.set('variables', JSON.stringify(body?.variables)) - return new MockedRequest(requestUrl) + return new Request(requestUrl) } function createPostGraphQLRequest( body: GraphQLRequestBody, hostname = 'https://example.com', - requestInit: MockedRequestInit = {}, ) { - return new MockedRequest(new URL(hostname), { + return new Request(new URL(hostname), { method: 'POST', - ...requestInit, headers: new Headers({ 'Content-Type': 'application/json' }), body: encodeBuffer(JSON.stringify(body)), }) @@ -152,7 +148,7 @@ describe('info', () => { describe('parse', () => { describe('query', () => { - test('parses a query without variables (GET)', () => { + test('parses a query without variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -163,14 +159,15 @@ describe('parse', () => { query: GET_USER, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'query', operationName: 'GetUser', + query: GET_USER, variables: undefined, }) }) - test('parses a query with variables (GET)', () => { + test('parses a query with variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -184,16 +181,17 @@ describe('parse', () => { }, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'query', operationName: 'GetUser', + query: GET_USER, variables: { userId: 'abc-123', }, }) }) - test('parses a query without variables (POST)', () => { + test('parses a query without variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -204,14 +202,15 @@ describe('parse', () => { query: GET_USER, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'query', operationName: 'GetUser', + query: GET_USER, variables: undefined, }) }) - test('parses a query with variables (POST)', () => { + test('parses a query with variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -225,9 +224,10 @@ describe('parse', () => { }, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'query', operationName: 'GetUser', + query: GET_USER, variables: { userId: 'abc-123', }, @@ -236,7 +236,7 @@ describe('parse', () => { }) describe('mutation', () => { - test('parses a mutation without variables (GET)', () => { + test('parses a mutation without variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', @@ -247,14 +247,15 @@ describe('parse', () => { query: LOGIN, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'mutation', operationName: 'Login', + query: LOGIN, variables: undefined, }) }) - test('parses a mutation with variables (GET)', () => { + test('parses a mutation with variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', @@ -268,16 +269,17 @@ describe('parse', () => { }, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'mutation', operationName: 'Login', + query: LOGIN, variables: { userId: 'abc-123', }, }) }) - test('parses a mutation without variables (POST)', () => { + test('parses a mutation without variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', @@ -288,14 +290,15 @@ describe('parse', () => { query: LOGIN, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'mutation', operationName: 'Login', + query: LOGIN, variables: undefined, }) }) - test('parses a mutation with variables (POST)', () => { + test('parses a mutation with variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', @@ -309,9 +312,10 @@ describe('parse', () => { }, }) - expect(handler.parse(request)).toEqual({ + expect(await handler.parse({ request })).toEqual({ operationType: 'mutation', operationName: 'Login', + query: LOGIN, variables: { userId: 'abc-123', }, @@ -321,7 +325,7 @@ describe('parse', () => { }) describe('predicate', () => { - test('respects operation type', () => { + test('respects operation type', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -335,13 +339,21 @@ describe('predicate', () => { query: LOGIN, }) - expect(handler.predicate(request, handler.parse(request))).toBe(true) - expect(handler.predicate(alienRequest, handler.parse(alienRequest))).toBe( - false, - ) + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) + expect( + handler.predicate({ + request: alienRequest, + parsedResult: await handler.parse({ request: alienRequest }), + }), + ).toBe(false) }) - test('respects operation name', () => { + test('respects operation name', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -361,13 +373,21 @@ describe('predicate', () => { `, }) - expect(handler.predicate(request, handler.parse(request))).toBe(true) - expect(handler.predicate(alienRequest, handler.parse(alienRequest))).toBe( - false, - ) + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) + expect( + handler.predicate({ + request: alienRequest, + parsedResult: await handler.parse({ request: alienRequest }), + }), + ).toBe(false) }) - test('allows anonymous GraphQL opertaions when using "all" expected operation type', () => { + test('allows anonymous GraphQL opertaions when using "all" expected operation type', async () => { const handler = new GraphQLHandler('all', new RegExp('.*'), '*', resolver) const request = createPostGraphQLRequest({ query: ` @@ -380,10 +400,15 @@ describe('predicate', () => { `, }) - expect(handler.predicate(request, handler.parse(request))).toBe(true) + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) }) - test('respects custom endpoint', () => { + test('respects custom endpoint', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -400,15 +425,23 @@ describe('predicate', () => { query: GET_USER, }) - expect(handler.predicate(request, handler.parse(request))).toBe(true) - expect(handler.predicate(alienRequest, handler.parse(alienRequest))).toBe( - false, - ) + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) + expect( + handler.predicate({ + request: alienRequest, + parsedResult: await handler.parse({ request: alienRequest }), + }), + ).toBe(false) }) }) describe('test', () => { - test('respects operation type', () => { + test('respects operation type', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -422,11 +455,11 @@ describe('test', () => { query: LOGIN, }) - expect(handler.test(request)).toBe(true) - expect(handler.test(alienRequest)).toBe(false) + expect(await handler.test({ request })).toBe(true) + expect(await handler.test({ request: alienRequest })).toBe(false) }) - test('respects operation name', () => { + test('respects operation name', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -446,11 +479,11 @@ describe('test', () => { `, }) - expect(handler.test(request)).toBe(true) - expect(handler.test(alienRequest)).toBe(false) + expect(await handler.test({ request })).toBe(true) + expect(await handler.test({ request: alienRequest })).toBe(false) }) - test('respects custom endpoint', () => { + test('respects custom endpoint', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', @@ -467,8 +500,8 @@ describe('test', () => { query: GET_USER, }) - expect(handler.test(request)).toBe(true) - expect(handler.test(alienRequest)).toBe(false) + expect(await handler.test({ request })).toBe(true) + expect(await handler.test({ request: alienRequest })).toBe(false) }) }) @@ -486,28 +519,27 @@ describe('run', () => { userId: 'abc-123', }, }) - const result = await handler.run(request) + const result = await handler.run({ request }) - expect(result).toMatchObject({ - handler, - request: { - ...request, - variables: { - userId: 'abc-123', - }, - }, - parsedResult: { - operationType: 'query', - operationName: 'GetUser', - variables: { - userId: 'abc-123', - }, + expect(result!.handler).toEqual(handler) + expect(result!.parsedResult).toEqual({ + operationType: 'query', + operationName: 'GetUser', + query: GET_USER, + variables: { + userId: 'abc-123', }, - response: await response( - context.data({ - user: { id: 'abc-123' }, - }), - ), + }) + expect(result!.request.method).toBe('POST') + expect(result!.request.url).toBe('https://example.com/') + expect(await result!.request.json()).toEqual({ + query: GET_USER, + variables: { userId: 'abc-123' }, + }) + expect(result!.response?.status).toBe(200) + expect(result!.response?.statusText).toBe('OK') + expect(await result!.response?.json()).toEqual({ + data: { user: { id: 'abc-123' } }, }) }) @@ -521,7 +553,7 @@ describe('run', () => { const request = createPostGraphQLRequest({ query: LOGIN, }) - const result = await handler.run(request) + const result = await handler.run({ request }) expect(result).toBeNull() }) @@ -568,7 +600,7 @@ describe('request', () => { `, }) - await handler.run(request) + await handler.run({ request }) expect(matchAllResolver).toHaveBeenCalledTimes(1) expect(matchAllResolver.mock.calls[0][0]).toHaveProperty( diff --git a/src/handlers/GraphQLHandler.ts b/src/core/handlers/GraphQLHandler.ts similarity index 51% rename from src/handlers/GraphQLHandler.ts rename to src/core/handlers/GraphQLHandler.ts index c44e6dae1..0c3e56e3b 100644 --- a/src/handlers/GraphQLHandler.ts +++ b/src/core/handlers/GraphQLHandler.ts @@ -1,22 +1,15 @@ -import type { DocumentNode, OperationTypeNode } from 'graphql' -import { SerializedResponse } from '../setupWorker/glossary' -import { data } from '../context/data' -import { extensions } from '../context/extensions' -import { errors } from '../context/errors' -import { field } from '../context/field' -import { GraphQLPayloadContext } from '../typeUtils' -import { cookie } from '../context/cookie' +import type { DocumentNode, GraphQLError, OperationTypeNode } from 'graphql' import { - defaultContext, - DefaultContext, + DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo, + RequestHandlerOptions, ResponseResolver, } from './RequestHandler' import { getTimestamp } from '../utils/logging/getTimestamp' import { getStatusCodeColor } from '../utils/logging/getStatusCodeColor' -import { prepareRequest } from '../utils/logging/prepareRequest' -import { prepareResponse } from '../utils/logging/prepareResponse' +import { serializeRequest } from '../utils/logging/serializeRequest' +import { serializeResponse } from '../utils/logging/serializeResponse' import { matchRequestUrl, Path } from '../utils/matching/matchRequestUrl' import { ParsedGraphQLRequest, @@ -25,34 +18,11 @@ import { parseDocumentNode, } from '../utils/internal/parseGraphQLRequest' import { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromRequest' -import { tryCatch } from '../utils/internal/tryCatch' import { devUtils } from '../utils/internal/devUtils' -import { MockedRequest } from '../utils/request/MockedRequest' export type ExpectedOperationTypeNode = OperationTypeNode | 'all' export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string -// GraphQL related context should contain utility functions -// useful for GraphQL. Functions like `xml()` bear no value -// in the GraphQL universe. -export type GraphQLContext> = - DefaultContext & { - data: GraphQLPayloadContext - extensions: GraphQLPayloadContext - errors: typeof errors - cookie: typeof cookie - field: typeof field - } - -export const graphqlContext: GraphQLContext = { - ...defaultContext, - data, - extensions, - errors, - cookie, - field, -} - export type GraphQLVariables = Record export interface GraphQLHandlerInfo extends RequestHandlerDefaultInfo { @@ -60,6 +30,12 @@ export interface GraphQLHandlerInfo extends RequestHandlerDefaultInfo { operationName: GraphQLHandlerNameSelector } +export type GraphQLResolverExtras = { + query: string + operationName: string + variables: Variables +} + export type GraphQLRequestBody = | GraphQLJsonRequestBody | GraphQLMultipartRequestBody @@ -71,6 +47,11 @@ export interface GraphQLJsonRequestBody { variables?: Variables } +export interface GraphQLResponseBody { + data?: BodyType + errors?: readonly Partial[] +} + export function isDocumentNode( value: DocumentNode | any, ): value is DocumentNode { @@ -81,31 +62,10 @@ export function isDocumentNode( return typeof value === 'object' && 'kind' in value && 'definitions' in value } -export class GraphQLRequest< - Variables extends GraphQLVariables, -> extends MockedRequest> { - constructor( - request: MockedRequest, - public readonly variables: Variables, - public readonly operationName: string, - ) { - super(request.url, { - ...request, - /** - * TODO(https://github.com/mswjs/msw/issues/1318): Cleanup - */ - body: request['_body'], - }) - } -} - -export class GraphQLHandler< - Request extends GraphQLRequest = GraphQLRequest, -> extends RequestHandler< +export class GraphQLHandler extends RequestHandler< GraphQLHandlerInfo, - Request, - ParsedGraphQLRequest | null, - GraphQLRequest + ParsedGraphQLRequest, + GraphQLResolverExtras > { private endpoint: Path @@ -113,7 +73,8 @@ export class GraphQLHandler< operationType: ExpectedOperationTypeNode, operationName: GraphQLHandlerNameSelector, endpoint: Path, - resolver: ResponseResolver, + resolver: ResponseResolver, any, any>, + options?: RequestHandlerOptions, ) { let resolvedOperationName = operationName @@ -146,55 +107,47 @@ export class GraphQLHandler< operationType, operationName: resolvedOperationName, }, - ctx: graphqlContext, resolver, + options, }) this.endpoint = endpoint } - parse(request: MockedRequest) { - return tryCatch( - () => parseGraphQLRequest(request), - (error) => console.error(error.message), - ) - } - - protected getPublicRequest( - request: Request, - parsedResult: ParsedGraphQLRequest, - ): GraphQLRequest { - return new GraphQLRequest( - request, - parsedResult?.variables ?? {}, - parsedResult?.operationName ?? '', - ) + async parse(args: { request: Request }) { + return parseGraphQLRequest(args.request).catch((error) => { + console.error(error) + return undefined + }) } - predicate(request: MockedRequest, parsedResult: ParsedGraphQLRequest) { - if (!parsedResult) { + predicate(args: { request: Request; parsedResult: ParsedGraphQLRequest }) { + if (!args.parsedResult) { return false } - if (!parsedResult.operationName && this.info.operationType !== 'all') { - const publicUrl = getPublicUrlFromRequest(request) + if (!args.parsedResult.operationName && this.info.operationType !== 'all') { + const publicUrl = getPublicUrlFromRequest(args.request) + devUtils.warn(`\ -Failed to intercept a GraphQL request at "${request.method} ${publicUrl}": anonymous GraphQL operations are not supported. +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/operation\ - `) +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/operation`) return false } - const hasMatchingUrl = matchRequestUrl(request.url, this.endpoint) + const hasMatchingUrl = matchRequestUrl( + new URL(args.request.url), + this.endpoint, + ) const hasMatchingOperationType = this.info.operationType === 'all' || - parsedResult.operationType === this.info.operationType + args.parsedResult.operationType === this.info.operationType const hasMatchingOperationName = this.info.operationName instanceof RegExp - ? this.info.operationName.test(parsedResult.operationName || '') - : parsedResult.operationName === this.info.operationName + ? this.info.operationName.test(args.parsedResult.operationName || '') + : args.parsedResult.operationName === this.info.operationName return ( hasMatchingUrl.matches && @@ -203,24 +156,35 @@ Consider naming this operation or using "graphql.operation()" request handler to ) } - log( - request: Request, - response: SerializedResponse, - parsedRequest: ParsedGraphQLRequest, - ) { - const loggedRequest = prepareRequest(request) - const loggedResponse = prepareResponse(response) - const statusColor = getStatusCodeColor(response.status) - const requestInfo = parsedRequest?.operationName - ? `${parsedRequest?.operationType} ${parsedRequest?.operationName}` - : `anonymous ${parsedRequest?.operationType}` + protected extendResolverArgs(args: { + request: Request + parsedResult: ParsedGraphQLRequest + }) { + return { + query: args.parsedResult?.query || '', + operationName: args.parsedResult?.operationName || '', + variables: args.parsedResult?.variables || {}, + } + } + + async log(args: { + request: Request + response: Response + parsedResult: ParsedGraphQLRequest + }) { + 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('%s %s (%c%s%c)'), getTimestamp(), `${requestInfo}`, `color:${statusColor}`, - `${response.status} ${response.statusText}`, + `${loggedResponse.status} ${loggedResponse.statusText}`, 'color:inherit', ) console.log('Request:', loggedRequest) diff --git a/src/core/handlers/HttpHandler.test.ts b/src/core/handlers/HttpHandler.test.ts new file mode 100644 index 000000000..0d0ac2d49 --- /dev/null +++ b/src/core/handlers/HttpHandler.test.ts @@ -0,0 +1,218 @@ +/** + * @jest-environment jsdom + */ +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', () => { + test('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', () => { + test('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: {}, + }) + }) + + test('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: {}, + }) + }) + + test('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', () => { + test('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', + }) + + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) + }) + + test('respects 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) { + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(true) + } + }) + + test('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)) + + expect( + handler.predicate({ + request, + parsedResult: await handler.parse({ request }), + }), + ).toBe(false) + }) +}) + +describe('test', () => { + test('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) + }) + + test('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', () => { + test('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 result = await handler.run({ request }) + + 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') + expect(await result?.response?.json()).toEqual({ userId: 'abc-123' }) + }) + + test('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)), + }) + + expect(result).toBeNull() + }) + + test('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)), + }) + + expect(result?.parsedResult?.match?.params).toEqual({}) + }) + + test('exhauses 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)), + }) + return result?.response?.text() + } + + expect(await run()).toBe('pending') + expect(await run()).toBe('pending') + expect(await run()).toBe('pending') + expect(await run()).toBe('pending') + expect(await run()).toBe('pending') + expect(await run()).toBe('complete') + expect(await run()).toBe('complete') + }) +}) diff --git a/src/core/handlers/HttpHandler.ts b/src/core/handlers/HttpHandler.ts new file mode 100644 index 000000000..963814785 --- /dev/null +++ b/src/core/handlers/HttpHandler.ts @@ -0,0 +1,169 @@ +import { ResponseResolutionContext } from '../utils/getResponse' +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 { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromRequest' +import { getAllRequestCookies } from '../utils/request/getRequestCookies' +import { cleanUrl, getSearchParams } from '../utils/url/cleanUrl' +import { + RequestHandler, + RequestHandlerDefaultInfo, + RequestHandlerOptions, + ResponseResolver, +} from './RequestHandler' + +type HttpHandlerMethod = string | RegExp + +export interface HttpHandlerInfo extends RequestHandlerDefaultInfo { + method: HttpHandlerMethod + path: Path +} + +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> +} + +/** + * Request handler for HTTP requests. + * Provides request matching based on method and URL. + */ +export class HttpHandler extends RequestHandler< + HttpHandlerInfo, + HttpRequestParsedResult, + HttpRequestResolverExtras +> { + constructor( + method: HttpHandlerMethod, + path: Path, + resolver: ResponseResolver, any, any>, + options?: RequestHandlerOptions, + ) { + super({ + info: { + header: `${method} ${path}`, + path, + method, + }, + resolver, + options, + }) + + this.checkRedundantQueryParameters() + } + + private checkRedundantQueryParameters() { + const { method, path } = this.info + + if (path instanceof RegExp) { + return + } + + const url = cleanUrl(path) + + // Bypass request handler URLs that have no redundant characters. + if (url === path) { + return + } + + const searchParams = getSearchParams(path) + const queryParams: string[] = [] + + searchParams.forEach((_, paramName) => { + queryParams.push(paramName) + }) + + 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 in the response resolver function using "req.url.searchParams".`, + ) + } + + async parse(args: { + request: Request + resolutionContext?: ResponseResolutionContext + }) { + const url = new URL(args.request.url) + const match = matchRequestUrl( + url, + this.info.path, + args.resolutionContext?.baseUrl, + ) + const cookies = getAllRequestCookies(args.request) + + return { + match, + cookies, + } + } + + predicate(args: { request: Request; parsedResult: HttpRequestParsedResult }) { + 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 = getPublicUrlFromRequest(args.request) + const loggedRequest = await serializeRequest(args.request) + const loggedResponse = await serializeResponse(args.response) + const statusColor = getStatusCodeColor(loggedResponse.status) + + console.groupCollapsed( + devUtils.formatMessage('%s %s %s (%c%s%c)'), + getTimestamp(), + args.request.method, + publicUrl, + `color:${statusColor}`, + `${loggedResponse.status} ${loggedResponse.statusText}`, + 'color:inherit', + ) + console.log('Request', loggedRequest) + console.log('Handler:', this) + console.log('Response', loggedResponse) + console.groupEnd() + } +} diff --git a/src/core/handlers/RequestHandler.ts b/src/core/handlers/RequestHandler.ts new file mode 100644 index 000000000..917d6edc4 --- /dev/null +++ b/src/core/handlers/RequestHandler.ts @@ -0,0 +1,297 @@ +import { invariant } from 'outvariant' +import { getCallFrame } from '../utils/internal/getCallFrame' +import { isIterable } from '../utils/internal/isIterable' +import type { ResponseResolutionContext } from '../utils/getResponse' +import type { MaybePromise } from '../typeUtils' +import { StrictRequest, StrictResponse } from '..//HttpResponse' + +export type DefaultRequestMultipartBody = Record< + string, + string | File | Array +> + +export type DefaultBodyType = + | Record + | DefaultRequestMultipartBody + | string + | number + | boolean + | null + | undefined + +export interface RequestHandlerDefaultInfo { + header: string +} + +export interface RequestHandlerInternalInfo { + callFrame?: string +} + +export type ResponseResolverReturnType< + BodyType extends DefaultBodyType = undefined, +> = + | (BodyType extends undefined ? Response : StrictResponse) + | undefined + | void + +export type MaybeAsyncResponseResolverReturnType< + BodyType extends DefaultBodyType, +> = MaybePromise> + +export type AsyncResponseResolverReturnType = + | MaybeAsyncResponseResolverReturnType + | Generator< + MaybeAsyncResponseResolverReturnType, + MaybeAsyncResponseResolverReturnType, + MaybeAsyncResponseResolverReturnType + > + +export type ResponseResolverInfo< + ResolverExtraInfo extends Record, + RequestBodyType extends DefaultBodyType = DefaultBodyType, +> = { + request: StrictRequest +} & 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 Record | undefined, +> { + handler: RequestHandler + parsedResult?: ParsedResult + request: Request + response?: Response +} + +export abstract class RequestHandler< + HandlerInfo extends RequestHandlerDefaultInfo = RequestHandlerDefaultInfo, + ParsedResult extends Record | undefined = any, + ResolverExtras extends Record = any, + HandlerOptions extends RequestHandlerOptions = RequestHandlerOptions, +> { + public info: HandlerInfo & RequestHandlerInternalInfo + /** + * Indicates whether this request handler has been used + * (its resolver has successfully executed). + */ + public isUsed: boolean + + protected resolver: ResponseResolver + private resolverGenerator?: Generator< + MaybeAsyncResponseResolverReturnType, + MaybeAsyncResponseResolverReturnType, + MaybeAsyncResponseResolverReturnType + > + private resolverGeneratorResult?: Response | StrictResponse + 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 + } + + /** + * Determine if the intercepted request should be mocked. + */ + abstract predicate(args: { + request: Request + parsedResult: ParsedResult + resolutionContext?: ResponseResolutionContext + }): boolean + + /** + * 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. + */ + 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 + } + + /** + * Execute this request handler and produce a mocked response + * using the given resolver function. + */ + public async run(args: { + request: StrictRequest + resolutionContext?: ResponseResolutionContext + }): Promise | null> { + if (this.isUsed && this.options?.once) { + return null + } + + // Clone the request instance before it's passed to the handler phases + // and the response resolver so we can always read it for logging. + const mainRequestRef = args.request.clone() + + // Immediately mark the handler as used. + // Can't await the resolver to be resolved because it's potentially + // asynchronous, and there may be multiple requests hitting this handler. + this.isUsed = true + + const parsedResult = await this.parse({ + request: args.request, + resolutionContext: args.resolutionContext, + }) + const shouldInterceptRequest = this.predicate({ + request: args.request, + parsedResult, + resolutionContext: args.resolutionContext, + }) + + if (!shouldInterceptRequest) { + return null + } + + // 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 mockedResponse = (await executeResolver({ + ...resolverExtras, + request: args.request, + })) as Response + + const executionResult = this.createExecutionResult({ + // Pass the cloned request to the result so that logging + // and other consumers could read its body once more. + request: mainRequestRef, + response: mockedResponse, + parsedResult, + }) + + return executionResult + } + + private wrapResolver( + resolver: ResponseResolver, + ): ResponseResolver { + return async (info): Promise> => { + const result = this.resolverGenerator || (await resolver(info)) + + if (isIterable>(result)) { + // Immediately mark this handler as unused. + // Only when the generator is done, the handler will be + // considered used. + this.isUsed = false + + const { value, done } = result[Symbol.iterator]().next() + const nextResponse = await value + + if (done) { + this.isUsed = true + } + + // If the generator is done and there is no next value, + // return the previous generator's value. + if (!nextResponse && done) { + invariant( + this.resolverGeneratorResult, + 'Failed to returned a previously stored generator response: the value is not a valid Response.', + ) + + // Clone the previously stored response from the generator + // so that it could be read again. + return this.resolverGeneratorResult.clone() + } + + if (!this.resolverGenerator) { + this.resolverGenerator = result + } + + if (nextResponse) { + // Also clone the response before storing it + // so it could be read again. + this.resolverGeneratorResult = nextResponse?.clone() + } + + return nextResponse + } + + return result + } + } + + private createExecutionResult(args: { + request: Request + parsedResult: ParsedResult + response?: Response + }): RequestHandlerExecutionResult { + return { + handler: this, + request: args.request, + response: args.response, + parsedResult: args.parsedResult, + } + } +} diff --git a/src/rest.spec.ts b/src/core/http.spec.ts similarity index 61% rename from src/rest.spec.ts rename to src/core/http.spec.ts index c44f0a85e..8bc2bdc2d 100644 --- a/src/rest.spec.ts +++ b/src/core/http.spec.ts @@ -1,8 +1,8 @@ -import { rest } from './rest' +import { http } from './http' test('exports all REST API methods', () => { - expect(rest).toBeDefined() - expect(Object.keys(rest)).toEqual([ + expect(http).toBeDefined() + expect(Object.keys(http)).toEqual([ 'all', 'head', 'get', diff --git a/src/core/http.ts b/src/core/http.ts new file mode 100644 index 000000000..d4f7c58f8 --- /dev/null +++ b/src/core/http.ts @@ -0,0 +1,51 @@ +import { + DefaultBodyType, + RequestHandlerOptions, + ResponseResolver, +} from './handlers/RequestHandler' +import { + HttpMethods, + HttpHandler, + HttpRequestResolverExtras, +} from './handlers/HttpHandler' +import type { Path, PathParams } from './utils/matching/matchRequestUrl' + +function createHttpHandler( + method: Method, +) { + return < + Params extends PathParams = PathParams, + RequestBodyType extends DefaultBodyType = DefaultBodyType, + ResponseBodyType extends DefaultBodyType = undefined, + >( + path: Path, + resolver: ResponseResolver< + HttpRequestResolverExtras, + RequestBodyType, + ResponseBodyType + >, + options: RequestHandlerOptions = {}, + ) => { + return new HttpHandler(method, path, 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), +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 000000000..d7bf7f968 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,55 @@ +import { checkGlobals } from './utils/internal/checkGlobals' + +export { SetupApi } from './SetupApi' + +/* Request 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' + +/* Utils */ +export { matchRequestUrl } from './utils/matching/matchRequestUrl' +export * from './utils/handleRequest' +export { cleanUrl } from './utils/url/cleanUrl' + +/** + * Type definitions. + */ + +export type { SharedOptions, LifeCycleEventsMap } from './sharedOptions' + +export type { + ResponseResolver, + ResponseResolverReturnType, + AsyncResponseResolverReturnType, + RequestHandlerOptions, + DefaultBodyType, + DefaultRequestMultipartBody, +} from './handlers/RequestHandler' + +export type { + RequestQuery, + HttpRequestParsedResult, +} from './handlers/HttpHandler' + +export type { + GraphQLVariables, + GraphQLRequestBody, + GraphQLJsonRequestBody, +} from './handlers/GraphQLHandler' + +export type { Path, PathParams, Match } from './utils/matching/matchRequestUrl' +export type { ParsedGraphQLRequest } from './utils/internal/parseGraphQLRequest' + +export * from './HttpResponse' +export * from './delay' +export { bypass } from './bypass' +export { passthrough } from './passthrough' + +// 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() diff --git a/src/core/passthrough.test.ts b/src/core/passthrough.test.ts new file mode 100644 index 000000000..631fcdaf3 --- /dev/null +++ b/src/core/passthrough.test.ts @@ -0,0 +1,13 @@ +/** + * @jest-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') +}) diff --git a/src/core/passthrough.ts b/src/core/passthrough.ts new file mode 100644 index 000000000..d67afdde1 --- /dev/null +++ b/src/core/passthrough.ts @@ -0,0 +1,23 @@ +/** + * 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(): Response { + return new Response(null, { + status: 302, + statusText: 'Passthrough', + headers: { + 'x-msw-intention': 'passthrough', + }, + }) +} diff --git a/src/core/sharedOptions.ts b/src/core/sharedOptions.ts new file mode 100644 index 000000000..ad7f151a2 --- /dev/null +++ b/src/core/sharedOptions.ts @@ -0,0 +1,66 @@ +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'> diff --git a/src/typeUtils.ts b/src/core/typeUtils.ts similarity index 73% rename from src/typeUtils.ts rename to src/core/typeUtils.ts index d9310465d..8e878be22 100644 --- a/src/typeUtils.ts +++ b/src/core/typeUtils.ts @@ -1,7 +1,7 @@ -import { ResponseTransformer } from './response' - type Fn = (...arg: any[]) => any +export type MaybePromise = T | Promise + export type RequiredDeep< Type, U extends Record | Fn | undefined = undefined, @@ -18,7 +18,3 @@ export type RequiredDeep< : RequiredDeep, U> } : Type - -export type GraphQLPayloadContext> = ( - payload: QueryType, -) => ResponseTransformer diff --git a/src/core/utils/HttpResponse/decorators.ts b/src/core/utils/HttpResponse/decorators.ts new file mode 100644 index 000000000..7b1bb97cc --- /dev/null +++ b/src/core/utils/HttpResponse/decorators.ts @@ -0,0 +1,56 @@ +import statuses from '@bundled-es-modules/statuses' +import type { HttpResponseInit } from '../../HttpResponse' + +const { message } = statuses + +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, + }) + } + + // Cookie forwarding is only relevant in the browser. + if (typeof document !== 'undefined') { + // Write the mocked response cookies to the document. + // Note that Fetch API Headers will concatenate multiple "Set-Cookie" + // headers into a single comma-separated string, just as it does + // with any other multi-value headers. + const responseCookies = init.headers.get('Set-Cookie')?.split(',') || [] + + for (const cookieString of responseCookies) { + // No need to parse the cookie headers because it's defined + // as the valid cookie string to begin with. + document.cookie = cookieString + } + } + + return response +} diff --git a/src/core/utils/getResponse.ts b/src/core/utils/getResponse.ts new file mode 100644 index 000000000..131804d8b --- /dev/null +++ b/src/core/utils/getResponse.ts @@ -0,0 +1,55 @@ +import { + RequestHandler, + RequestHandlerExecutionResult, +} from '../handlers/RequestHandler' + +export interface ResponseLookupResult { + handler: RequestHandler + parsedResult?: any + response?: Response +} + +export interface ResponseResolutionContext { + baseUrl?: string +} + +/** + * Returns a mocked response for a given request using following request handlers. + */ +export const getResponse = async >( + request: Request, + handlers: Handler, + resolutionContext?: ResponseResolutionContext, +): Promise => { + let matchingHandler: RequestHandler | null = null + let result: RequestHandlerExecutionResult | null = null + + for (const handler of handlers) { + result = await handler.run({ request, resolutionContext }) + + // If the handler produces some result for this request, + // it automatically becomes matching. + if (result !== null) { + matchingHandler = handler + } + + // Stop the lookup if this handler returns a mocked response. + // If it doesn't, it will still be considered the last matching + // handler until any of them returns a response. This way we can + // distinguish between fallthrough handlers without responses + // and the lack of a matching handler. + if (result?.response) { + break + } + } + + if (matchingHandler) { + return { + handler: matchingHandler, + parsedResult: result?.parsedResult, + response: result?.response, + } + } + + return null +} diff --git a/src/core/utils/handleRequest.test.ts b/src/core/utils/handleRequest.test.ts new file mode 100644 index 000000000..a89bd00f3 --- /dev/null +++ b/src/core/utils/handleRequest.test.ts @@ -0,0 +1,344 @@ +/** + * @jest-environment jsdom + */ +import { Emitter } from 'strict-event-emitter' +import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' +import { RequestHandler } from '../handlers/RequestHandler' +import { http } from '../http' +import { handleRequest, HandleRequestOptions } from './handleRequest' +import { RequiredDeep } from '../typeUtils' +import { uuidv4 } from './internal/uuidv4' +import { HttpResponse } from '../HttpResponse' +import { passthrough } from '../passthrough' + +const options: RequiredDeep = { + onUnhandledRequest: jest.fn(), +} +const callbacks: Partial> = { + onPassthroughResponse: jest.fn(), + onMockedResponse: jest.fn(), +} + +function setup() { + const emitter = new Emitter() + const listener = jest.fn() + + const createMockListener = (name: string) => { + return (...args: any) => { + listener(name, ...args) + } + } + + emitter.on('request:start', createMockListener('request:start')) + emitter.on('request:match', createMockListener('request:match')) + emitter.on('request:unhandled', createMockListener('request:unhandled')) + emitter.on('request:end', createMockListener('request:end')) + emitter.on('response:mocked', createMockListener('response:mocked')) + emitter.on('response:bypass', createMockListener('response:bypass')) + + const events = listener.mock.calls + return { emitter, events } +} + +beforeEach(() => { + jest.spyOn(global.console, 'warn').mockImplementation() +}) + +afterEach(() => { + jest.resetAllMocks() +}) + +test('returns undefined for a request with the "x-msw-intention" header equal to "bypass"', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user'), { + headers: new Headers({ + 'x-msw-intention': 'bypass', + }), + }) + const handlers: Array = [] + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + callbacks, + ) + + expect(result).toBeUndefined() + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(options.onUnhandledRequest).not.toHaveBeenCalled() + expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) + expect(callbacks.onMockedResponse).not.toHaveBeenCalled() +}) + +test('does not bypass a request with "x-msw-intention" header set to arbitrary value', async () => { + const { emitter } = setup() + + const request = new Request(new URL('http://localhost/user'), { + headers: new Headers({ + 'x-msw-intention': 'invalid', + }), + }) + const handlers: Array = [ + http.get('/user', () => { + return HttpResponse.text('hello world') + }), + ] + + const result = await handleRequest( + request, + uuidv4(), + handlers, + options, + emitter, + callbacks, + ) + + expect(result).not.toBeUndefined() + expect(options.onUnhandledRequest).not.toHaveBeenCalled() + expect(callbacks.onMockedResponse).toHaveBeenCalledTimes(1) +}) + +test('reports request as unhandled when it has no matching request handlers', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user')) + const handlers: Array = [] + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + callbacks, + ) + + expect(result).toBeUndefined() + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:unhandled', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(options.onUnhandledRequest).toHaveBeenNthCalledWith(1, request, { + warning: expect.any(Function), + error: expect.any(Function), + }) + expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) + expect(callbacks.onMockedResponse).not.toHaveBeenCalled() +}) + +test('returns undefined on a request handler that returns no response', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user')) + const handlers: Array = [ + http.get('/user', () => { + // Intentionally blank response resolver. + return + }), + ] + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + callbacks, + ) + + expect(result).toBeUndefined() + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(options.onUnhandledRequest).not.toHaveBeenCalled() + expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) + expect(callbacks.onMockedResponse).not.toHaveBeenCalled() + + /** + * @note Returning undefined from a resolver no longer prints a warning. + */ + expect(console.warn).toHaveBeenCalledTimes(0) +}) + +test('returns the mocked response for a request with a matching request handler', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user')) + const mockedResponse = HttpResponse.json({ firstName: 'John' }) + const handlers: Array = [ + http.get('/user', () => { + return mockedResponse + }), + ] + const lookupResult = { + handler: handlers[0], + response: mockedResponse, + request, + parsedResult: { + match: { matches: true, params: {} }, + cookies: {}, + }, + } + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + callbacks, + ) + + expect(result).toEqual(mockedResponse) + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:match', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled() + + expect(callbacks.onMockedResponse).toHaveBeenCalledTimes(1) + const [mockedResponseParam, lookupResultParam] = + callbacks.onMockedResponse.mock.calls[0] + + expect(mockedResponseParam.status).toBe(mockedResponse.status) + expect(mockedResponseParam.statusText).toBe(mockedResponse.statusText) + expect(Object.fromEntries(mockedResponseParam.headers.entries())).toEqual( + Object.fromEntries(mockedResponse.headers.entries()), + ) + + expect(lookupResultParam).toEqual({ + handler: lookupResult.handler, + parsedResult: lookupResult.parsedResult, + response: expect.objectContaining({ + status: lookupResult.response.status, + statusText: lookupResult.response.statusText, + }), + }) +}) + +test('returns a transformed response if the "transformResponse" option is provided', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user')) + const mockedResponse = HttpResponse.json({ firstName: 'John' }) + const handlers: Array = [ + http.get('/user', () => { + return mockedResponse + }), + ] + const transformResponseImpelemntation = (response: Response): Response => { + return new Response('transformed', response) + } + const transformResponse = jest + .fn() + .mockImplementation(transformResponseImpelemntation) + const finalResponse = transformResponseImpelemntation(mockedResponse) + const lookupResult = { + handler: handlers[0], + response: mockedResponse, + request, + parsedResult: { + match: { matches: true, params: {} }, + cookies: {}, + }, + } + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + { + ...callbacks, + transformResponse, + }, + ) + + expect(result?.status).toEqual(finalResponse.status) + expect(result?.statusText).toEqual(finalResponse.statusText) + expect(Object.fromEntries(result!.headers.entries())).toEqual( + Object.fromEntries(mockedResponse.headers.entries()), + ) + + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:match', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled() + + expect(transformResponse).toHaveBeenCalledTimes(1) + const [responseParam] = transformResponse.mock.calls[0] + + expect(responseParam.status).toBe(mockedResponse.status) + expect(responseParam.statusText).toBe(mockedResponse.statusText) + expect(Object.fromEntries(responseParam.headers.entries())).toEqual( + Object.fromEntries(mockedResponse.headers.entries()), + ) + + expect(callbacks.onMockedResponse).toHaveBeenCalledTimes(1) + const [mockedResponseParam, lookupResultParam] = + callbacks.onMockedResponse.mock.calls[0] + + expect(mockedResponseParam.status).toBe(finalResponse.status) + expect(mockedResponseParam.statusText).toBe(finalResponse.statusText) + expect(Object.fromEntries(mockedResponseParam.headers.entries())).toEqual( + Object.fromEntries(mockedResponse.headers.entries()), + ) + expect(await mockedResponseParam.text()).toBe('transformed') + + expect(lookupResultParam).toEqual({ + handler: lookupResult.handler, + parsedResult: lookupResult.parsedResult, + response: expect.objectContaining({ + status: lookupResult.response.status, + statusText: lookupResult.response.statusText, + }), + }) +}) + +it('returns undefined without warning on a passthrough request', async () => { + const { emitter, events } = setup() + + const requestId = uuidv4() + const request = new Request(new URL('http://localhost/user')) + const handlers: Array = [ + http.get('/user', () => { + return passthrough() + }), + ] + + const result = await handleRequest( + request, + requestId, + handlers, + options, + emitter, + callbacks, + ) + + expect(result).toBeUndefined() + expect(events).toEqual([ + ['request:start', { request, requestId }], + ['request:end', { request, requestId }], + ]) + expect(options.onUnhandledRequest).not.toHaveBeenCalled() + expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) + expect(callbacks.onMockedResponse).not.toHaveBeenCalled() +}) diff --git a/src/utils/handleRequest.ts b/src/core/utils/handleRequest.ts similarity index 50% rename from src/utils/handleRequest.ts rename to src/core/utils/handleRequest.ts index 71e4f2ae1..f70002179 100644 --- a/src/utils/handleRequest.ts +++ b/src/core/utils/handleRequest.ts @@ -1,17 +1,13 @@ import { until } from '@open-draft/until' import { Emitter } from 'strict-event-emitter' import { RequestHandler } from '../handlers/RequestHandler' -import { ServerLifecycleEventsMap } from '../node/glossary' -import { MockedResponse } from '../response' -import { SharedOptions } from '../sharedOptions' +import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' import { RequiredDeep } from '../typeUtils' import { ResponseLookupResult, getResponse } from './getResponse' -import { devUtils } from './internal/devUtils' -import { MockedRequest } from './request/MockedRequest' import { onUnhandledRequest } from './request/onUnhandledRequest' import { readResponseCookies } from './request/readResponseCookies' -export interface HandleRequestOptions { +export interface HandleRequestOptions { /** * Options for the response resolution process. */ @@ -23,42 +19,41 @@ export interface HandleRequestOptions { * Transforms a `MockedResponse` instance returned from a handler * to a response instance supported by the lower tooling (i.e. interceptors). */ - transformResponse?(response: MockedResponse): ResponseType + transformResponse?(response: Response): Response /** * Invoked whenever a request is performed as-is. */ - onPassthroughResponse?(request: MockedRequest): void + onPassthroughResponse?(request: Request): void /** * Invoked when the mocked response is ready to be sent. */ onMockedResponse?( - response: ResponseType, + response: Response, handler: RequiredDeep, ): void } -export async function handleRequest< - ResponseType extends Record = MockedResponse, ->( - request: MockedRequest, - handlers: RequestHandler[], +export async function handleRequest( + request: Request, + requestId: string, + handlers: Array, options: RequiredDeep, - emitter: Emitter, - handleRequestOptions?: HandleRequestOptions, -): Promise { - emitter.emit('request:start', request) + emitter: Emitter, + handleRequestOptions?: HandleRequestOptions, +): Promise { + emitter.emit('request:start', { request, requestId }) // Perform bypassed requests (i.e. issued via "ctx.fetch") as-is. - if (request.headers.get('x-msw-bypass') === 'true') { - emitter.emit('request:end', request) + if (request.headers.get('x-msw-intention') === 'bypass') { + emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } // Resolve a mocked response from the list of request handlers. - const [lookupError, lookupResult] = await until(() => { + const lookupResult = await until(() => { return getResponse( request, handlers, @@ -66,48 +61,43 @@ export async function handleRequest< ) }) - if (lookupError) { + if (lookupResult.error) { // Allow developers to react to unhandled exceptions in request handlers. - emitter.emit('unhandledException', lookupError, request) - throw lookupError + emitter.emit('unhandledException', { + error: lookupResult.error, + request, + requestId, + }) + throw lookupResult.error } - const { handler, response } = lookupResult - - // When there's no handler for the request, consider it unhandled. - // Allow the developer to react to such cases. - if (!handler) { - onUnhandledRequest(request, handlers, options.onUnhandledRequest) - emitter.emit('request:unhandled', request) - emitter.emit('request:end', request) + // If the handler lookup returned nothing, no request handler was found + // matching this request. Report the request as unhandled. + if (!lookupResult.data) { + await onUnhandledRequest(request, handlers, options.onUnhandledRequest) + emitter.emit('request:unhandled', { request, requestId }) + emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } + const { response } = lookupResult.data + // When the handled request returned no mocked response, warn the developer, // as it may be an oversight on their part. Perform the request as-is. if (!response) { - devUtils.warn( - `\ -Expected response resolver to return a mocked response Object, but got %s. The original response is going to be used instead.\ -\n - \u2022 %s - %s\ -`, - response, - handler.info.header, - handler.info.callFrame, - ) - - emitter.emit('request:end', request) + emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } - // When the developer explicitly returned "req.passthrough()" do not warn them. - // Perform the request as-is. - if (response.passthrough) { - emitter.emit('request:end', request) + // Perform the request as-is when the developer explicitly returned "req.passthrough()". + // This produces no warning as the request was handled. + if ( + response.status === 302 && + response.headers.get('x-msw-intention') === 'passthrough' + ) { + emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } @@ -115,21 +105,21 @@ Expected response resolver to return a mocked response Object, but got %s. The o // Store all the received response cookies in the virtual cookie store. readResponseCookies(request, response) - emitter.emit('request:match', request) + emitter.emit('request:match', { request, requestId }) const requiredLookupResult = - lookupResult as RequiredDeep + lookupResult.data as RequiredDeep const transformedResponse = handleRequestOptions?.transformResponse?.(response) || - (response as any as ResponseType) + (response as any as Response) handleRequestOptions?.onMockedResponse?.( transformedResponse, requiredLookupResult, ) - emitter.emit('request:end', request) + emitter.emit('request:end', { request, requestId }) return transformedResponse } diff --git a/src/core/utils/internal/Disposable.ts b/src/core/utils/internal/Disposable.ts new file mode 100644 index 000000000..ca61652ab --- /dev/null +++ b/src/core/utils/internal/Disposable.ts @@ -0,0 +1,9 @@ +export type DisposableSubscription = () => Promise | void + +export class Disposable { + protected subscriptions: Array = [] + + public async dispose() { + await Promise.all(this.subscriptions.map((subscription) => subscription())) + } +} diff --git a/src/utils/internal/checkGlobals.ts b/src/core/utils/internal/checkGlobals.ts similarity index 100% rename from src/utils/internal/checkGlobals.ts rename to src/core/utils/internal/checkGlobals.ts diff --git a/src/utils/internal/devUtils.ts b/src/core/utils/internal/devUtils.ts similarity index 100% rename from src/utils/internal/devUtils.ts rename to src/core/utils/internal/devUtils.ts diff --git a/src/utils/internal/getCallFrame.test.ts b/src/core/utils/internal/getCallFrame.test.ts similarity index 68% rename from src/utils/internal/getCallFrame.test.ts rename to src/core/utils/internal/getCallFrame.test.ts index e13c875ad..a61e30f7c 100644 --- a/src/utils/internal/getCallFrame.test.ts +++ b/src/core/utils/internal/getCallFrame.test.ts @@ -13,9 +13,9 @@ class ErrorWithStack extends Error { test('supports Node.js (Linux, MacOS) error stack', () => { const linuxError = new ErrorWithStack([ 'Error: ', - ' at getCallFrame (/Users/mock/github/msw/lib/umd/index.js:3735:22)', - ' at Object.get (/Users/mock/github/msw/lib/umd/index.js:3776:29)', - ' at Object. (/Users/mock/github/msw/test/msw-api/setup-server/printHandlers.test.ts:13:8)', // <-- this one + ' at getCallFrame (/Users/mock/github/msw/lib/node/index.js:3735:22)', + ' at Object.get (/Users/mock/github/msw/lib/node/index.js:3776:29)', + ' at Object. (/Users/mock/github/msw/test/msw-api/setup-server/listHandlers.test.ts:13:8)', // <-- this one ' at Runtime._execModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:1299:24)', ' at Runtime._loadModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:898:12)', ' at Runtime.requireModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:746:10)', @@ -24,15 +24,15 @@ test('supports Node.js (Linux, MacOS) error stack', () => { ' at runTest (/Users/mock/github/msw/node_modules/jest-runner/build/runTest.js:472:34)', ]) expect(getCallFrame(linuxError)).toEqual( - '/Users/mock/github/msw/test/msw-api/setup-server/printHandlers.test.ts:13:8', + '/Users/mock/github/msw/test/msw-api/setup-server/listHandlers.test.ts:13:8', ) const macOsError = new ErrorWithStack([ 'Error: ', - ' at getCallFrame (/Users/mock/git/msw/lib/umd/index.js:3735:22)', - ' at graphQLRequestHandler (/Users/mock/git/msw/lib/umd/index.js:7071:25)', - ' at Object.query (/Users/mock/git/msw/lib/umd/index.js:7182:18)', - ' at Object. (/Users/mock/git/msw/test/msw-api/setup-server/printHandlers.test.ts:14:11)', // <-- this one + ' at getCallFrame (/Users/mock/git/msw/lib/node/index.js:3735:22)', + ' at graphQLRequestHandler (/Users/mock/git/msw/lib/node/index.js:7071:25)', + ' at Object.query (/Users/mock/git/msw/lib/node/index.js:7182:18)', + ' at Object. (/Users/mock/git/msw/test/msw-api/setup-server/listHandlers.test.ts:14:11)', // <-- this one ' at Runtime._execModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:1299:24)', ' at Runtime._loadModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:898:12)', ' at Runtime.requireModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:746:10)', @@ -42,17 +42,17 @@ test('supports Node.js (Linux, MacOS) error stack', () => { ]) expect(getCallFrame(macOsError)).toEqual( - '/Users/mock/git/msw/test/msw-api/setup-server/printHandlers.test.ts:14:11', + '/Users/mock/git/msw/test/msw-api/setup-server/listHandlers.test.ts:14:11', ) }) test('supports Node.js (Windows) error stack', () => { const error = new ErrorWithStack([ 'Error: ', - ' at getCallFrame (C:\\Users\\mock\\git\\msw\\lib\\umd\\index.js:3735:22)', - ' at graphQLRequestHandler (C:\\Users\\mock\\git\\msw\\lib\\umd\\index.js:7071:25)', - ' at Object.query (C:\\Users\\mock\\git\\msw\\lib\\umd\\index.js:7182:18)', - ' at Object. (C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\printHandlers.test.ts:75:13)', // <-- this one + ' at getCallFrame (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:3735:22)', + ' at graphQLRequestHandler (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:7071:25)', + ' at Object.query (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:7182:18)', + ' at Object. (C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\listHandlers.test.ts:75:13)', // <-- this one ' at Object.asyncJestTest (C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\jasmineAsyncInstall.js:106:37)', ' at C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\queueRunner.js:45:12', ' at new Promise ()', @@ -61,17 +61,17 @@ test('supports Node.js (Windows) error stack', () => { ]) expect(getCallFrame(error)).toBe( - 'C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\printHandlers.test.ts:75:13', + 'C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\listHandlers.test.ts:75:13', ) }) test('supports Chrome and Edge error stack', () => { const error = new ErrorWithStack([ 'Error', - ' at getCallFrame (webpack:///./lib/esm/getCallFrame-deps.js?:272:20)', - ' at Object.eval [as get] (webpack:///./lib/esm/rest-deps.js?:1402:90)', - ' at eval (webpack:///./test/msw-api/setup-worker/printHandlers.mocks.ts?:6:113)', // <-- this one - ' at Module../test/msw-api/setup-worker/printHandlers.mocks.ts (http://localhost:59464/main.js:1376:1)', + ' at getCallFrame (webpack:///./lib/browser/getCallFrame-deps.js?:272:20)', + ' at Object.eval [as get] (webpack:///./lib/browser/rest-deps.js?:1402:90)', + ' at eval (webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113)', // <-- this one + ' at Module../test/msw-api/setup-worker/listHandlers.mocks.ts (http://localhost:59464/main.js:1376:1)', ' at __webpack_require__ (http://localhost:59464/main.js:790:30)', ' at fn (http://localhost:59464/main.js:101:20)', ' at eval (webpack:///multi_(webpack)-dev-server/client?:4:18)', @@ -81,16 +81,16 @@ test('supports Chrome and Edge error stack', () => { ]) expect(getCallFrame(error)).toBe( - 'webpack:///./test/msw-api/setup-worker/printHandlers.mocks.ts?:6:113', + 'webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', ) }) test('supports Firefox (MacOS, Windows) error stack', () => { const error = new ErrorWithStack([ - 'getCallFrame@webpack:///./lib/esm/getCallFrame-deps.js?:272:20', - 'createRestHandler/<@webpack:///./lib/esm/rest-deps.js?:1402:90', - '@webpack:///./test/msw-api/setup-worker/printHandlers.mocks.ts?:6:113', // <-- this one - './test/msw-api/setup-worker/printHandlers.mocks.ts@http://localhost:59464/main.js:1376:1', + 'getCallFrame@webpack:///./lib/browser/getCallFrame-deps.js?:272:20', + 'createRestHandler/<@webpack:///./lib/browser/rest-deps.js?:1402:90', + '@webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', // <-- this one + './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:1', '__webpack_require__@http://localhost:59464/main.js:790:30', 'fn@http://localhost:59464/main.js:101:20', '@webpack:///multi_(webpack)-dev-server/client?:4:18', @@ -100,7 +100,7 @@ test('supports Firefox (MacOS, Windows) error stack', () => { ]) expect(getCallFrame(error)).toBe( - 'webpack:///./test/msw-api/setup-worker/printHandlers.mocks.ts?:6:113', + 'webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', ) }) @@ -110,7 +110,7 @@ test('supports Safari (MacOS) error stack', () => { '', 'eval code', 'eval@[native code]', - './test/msw-api/setup-worker/printHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', // <-- this one + './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', // <-- this one '__webpack_require__@http://localhost:59464/main.js:790:34', 'fn@http://localhost:59464/main.js:101:39', 'eval code', @@ -122,7 +122,7 @@ test('supports Safari (MacOS) error stack', () => { ]) expect(getCallFrame(errorOne)).toBe( - './test/msw-api/setup-worker/printHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', + './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', ) const errorTwo = new ErrorWithStack([ @@ -130,7 +130,7 @@ test('supports Safari (MacOS) error stack', () => { 'graphQLRequestHandler', 'eval code', 'eval@[native code]', - './test/msw-api/setup-worker/printHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', // <-- this one + './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', // <-- this one '__webpack_require__@http://localhost:56460/main.js:790:34', 'fn@http://localhost:56460/main.js:101:39', 'eval code', @@ -142,7 +142,7 @@ test('supports Safari (MacOS) error stack', () => { ]) expect(getCallFrame(errorTwo)).toBe( - './test/msw-api/setup-worker/printHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', + './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', ) }) diff --git a/src/utils/internal/getCallFrame.ts b/src/core/utils/internal/getCallFrame.ts similarity index 91% rename from src/utils/internal/getCallFrame.ts rename to src/core/utils/internal/getCallFrame.ts index 4e297d2ee..bee9e70f4 100644 --- a/src/utils/internal/getCallFrame.ts +++ b/src/core/utils/internal/getCallFrame.ts @@ -2,7 +2,7 @@ const SOURCE_FRAME = /[\/\\]msw[\/\\]src[\/\\](.+)/ const BUILD_FRAME = - /(node_modules)?[\/\\]lib[\/\\](umd|esm|iief|cjs)[\/\\]|^[^\/\\]*$/ + /(node_modules)?[\/\\]lib[\/\\](core|browser|node|native|iife)[\/\\]|^[^\/\\]*$/ /** * Return the stack trace frame of a function's invocation. diff --git a/src/utils/internal/isIterable.test.ts b/src/core/utils/internal/isIterable.test.ts similarity index 100% rename from src/utils/internal/isIterable.test.ts rename to src/core/utils/internal/isIterable.test.ts diff --git a/src/utils/internal/isIterable.ts b/src/core/utils/internal/isIterable.ts similarity index 100% rename from src/utils/internal/isIterable.ts rename to src/core/utils/internal/isIterable.ts diff --git a/src/utils/internal/isObject.test.ts b/src/core/utils/internal/isObject.test.ts similarity index 100% rename from src/utils/internal/isObject.test.ts rename to src/core/utils/internal/isObject.test.ts diff --git a/src/utils/internal/isObject.ts b/src/core/utils/internal/isObject.ts similarity index 100% rename from src/utils/internal/isObject.ts rename to src/core/utils/internal/isObject.ts diff --git a/src/utils/internal/isStringEqual.test.ts b/src/core/utils/internal/isStringEqual.test.ts similarity index 100% rename from src/utils/internal/isStringEqual.test.ts rename to src/core/utils/internal/isStringEqual.test.ts diff --git a/src/utils/internal/isStringEqual.ts b/src/core/utils/internal/isStringEqual.ts similarity index 100% rename from src/utils/internal/isStringEqual.ts rename to src/core/utils/internal/isStringEqual.ts diff --git a/src/utils/internal/jsonParse.test.ts b/src/core/utils/internal/jsonParse.test.ts similarity index 100% rename from src/utils/internal/jsonParse.test.ts rename to src/core/utils/internal/jsonParse.test.ts diff --git a/src/utils/internal/jsonParse.ts b/src/core/utils/internal/jsonParse.ts similarity index 100% rename from src/utils/internal/jsonParse.ts rename to src/core/utils/internal/jsonParse.ts diff --git a/src/utils/internal/mergeRight.test.ts b/src/core/utils/internal/mergeRight.test.ts similarity index 100% rename from src/utils/internal/mergeRight.test.ts rename to src/core/utils/internal/mergeRight.test.ts diff --git a/src/utils/internal/mergeRight.ts b/src/core/utils/internal/mergeRight.ts similarity index 100% rename from src/utils/internal/mergeRight.ts rename to src/core/utils/internal/mergeRight.ts diff --git a/src/core/utils/internal/parseGraphQLRequest.test.ts b/src/core/utils/internal/parseGraphQLRequest.test.ts new file mode 100644 index 000000000..7bb624d96 --- /dev/null +++ b/src/core/utils/internal/parseGraphQLRequest.test.ts @@ -0,0 +1,99 @@ +/** + * @jest-environment jsdom + */ +import { encodeBuffer } from '@mswjs/interceptors' +import { OperationTypeNode } from 'graphql' +import { + ParsedGraphQLRequest, + parseGraphQLRequest, +} from './parseGraphQLRequest' + +test('returns true given a GraphQL-compatible request', async () => { + const getRequest = new Request( + new URL( + 'http://localhost:8080/graphql?query=mutation Login { user { id } }', + ), + ) + expect(await parseGraphQLRequest(getRequest)).toEqual< + ParsedGraphQLRequest + >({ + operationType: OperationTypeNode.MUTATION, + operationName: 'Login', + query: `mutation Login { user { id } }`, + variables: undefined, + }) + + const postRequest = new Request(new URL('http://localhost:8080/graphql'), { + method: 'POST', + headers: new Headers({ 'Content-Type': 'application/json' }), + body: encodeBuffer( + JSON.stringify({ + query: `query GetUser { user { firstName } }`, + }), + ), + }) + + expect(await parseGraphQLRequest(postRequest)).toEqual< + ParsedGraphQLRequest + >({ + operationType: OperationTypeNode.QUERY, + operationName: 'GetUser', + query: `query GetUser { user { firstName } }`, + variables: undefined, + }) +}) + +test('throws an exception given an invalid GraphQL request', async () => { + const getRequest = new Request( + new URL('http://localhost:8080/graphql?query=mutation Login() { user { {}'), + ) + + await expect(parseGraphQLRequest(getRequest)).rejects.toThrowError( + '[MSW] Failed to intercept a GraphQL request to "GET http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.', + ) + + const postRequest = new Request(new URL('http://localhost:8080/graphql'), { + method: 'POST', + headers: new Headers({ 'Content-Type': 'application/json' }), + body: encodeBuffer( + JSON.stringify({ + query: `query GetUser() { user {{}`, + }), + ), + }) + + await expect(parseGraphQLRequest(postRequest)).rejects.toThrowError( + '[MSW] Failed to intercept a GraphQL request to "POST http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.\n\nSyntax Error: Expected "$", found ")".', + ) +}) + +test('returns false given a GraphQL-incompatible request', async () => { + const getRequest = new Request(new URL('http://localhost:8080/graphql'), { + headers: new Headers({ 'Content-Type': 'application/json' }), + }) + expect(await parseGraphQLRequest(getRequest)).toBeUndefined() + + const postRequest = new Request(new URL('http://localhost:8080/graphql'), { + method: 'POST', + headers: new Headers({ 'Content-Type': 'application/json' }), + body: encodeBuffer( + JSON.stringify({ + queryUser: true, + }), + ), + }) + expect(await parseGraphQLRequest(postRequest)).toBeUndefined() +}) + +test('does not read the original request body', async () => { + const request = new Request(new URL('http://localhost/api'), { + method: 'POST', + body: JSON.stringify({ payload: 'value' }), + }) + + await parseGraphQLRequest(request) + + // Must not read the original request body because GraphQL parsing + // is an internal operation that must not lock the body stream. + expect(request.bodyUsed).toBe(false) +}) diff --git a/src/utils/internal/parseGraphQLRequest.ts b/src/core/utils/internal/parseGraphQLRequest.ts similarity index 70% rename from src/utils/internal/parseGraphQLRequest.ts rename to src/core/utils/internal/parseGraphQLRequest.ts index b3d757040..230d145f0 100644 --- a/src/utils/internal/parseGraphQLRequest.ts +++ b/src/core/utils/internal/parseGraphQLRequest.ts @@ -4,11 +4,11 @@ import type { OperationTypeNode, } from 'graphql' import { parse } from 'graphql' -import { GraphQLVariables } from '../../handlers/GraphQLHandler' +import type { GraphQLVariables } from '../../handlers/GraphQLHandler' import { getPublicUrlFromRequest } from '../request/getPublicUrlFromRequest' -import { MockedRequest } from '../request/MockedRequest' import { devUtils } from './devUtils' import { jsonParse } from './jsonParse' +import { parseMultipartData } from './parseMultipartData' interface GraphQLInput { query: string | null @@ -24,13 +24,14 @@ export type ParsedGraphQLRequest< VariablesType extends GraphQLVariables = GraphQLVariables, > = | (ParsedGraphQLQuery & { + query: string variables?: VariablesType }) | undefined export function parseDocumentNode(node: DocumentNode): ParsedGraphQLQuery { - const operationDef = node.definitions.find((def) => { - return def.kind === 'OperationDefinition' + const operationDef = node.definitions.find((definition) => { + return definition.kind === 'OperationDefinition' }) as OperationDefinitionNode return { @@ -62,6 +63,7 @@ function extractMultipartVariables( files: Record, ) { const operations = { variables } + for (const [key, pathArray] of Object.entries(map)) { if (!(key in files)) { throw new Error(`Given files do not have a key '${key}' .`) @@ -83,14 +85,16 @@ function extractMultipartVariables( target[lastPath] = files[key] } } + return operations.variables } -function getGraphQLInput(request: MockedRequest): GraphQLInput | null { +async function getGraphQLInput(request: Request): Promise { switch (request.method) { case 'GET': { - const query = request.url.searchParams.get('query') - const variables = request.url.searchParams.get('variables') || '' + const url = new URL(request.url) + const query = url.searchParams.get('query') + const variables = url.searchParams.get('variables') || '' return { query, @@ -99,19 +103,24 @@ function getGraphQLInput(request: MockedRequest): GraphQLInput | null { } case 'POST': { - if (request.body?.query) { - const { query, variables } = request.body - - return { - query, - variables, + // Clone the request so we could read its body without locking + // the body stream to the downward consumers. + const requestClone = request.clone() + + // Handle multipart body GraphQL operations. + if ( + request.headers.get('content-type')?.includes('multipart/form-data') + ) { + const responseJson = parseMultipartData( + await requestClone.text(), + request.headers, + ) + + if (!responseJson) { + return null } - } - // Handle multipart body operations. - if (request.body?.operations) { - const { operations, map, ...files } = - request.body as GraphQLMultipartRequestBody + const { operations, map, ...files } = responseJson const parsedOperations = jsonParse<{ query?: string; variables?: GraphQLVariables }>( operations, @@ -135,6 +144,22 @@ function getGraphQLInput(request: MockedRequest): GraphQLInput | null { variables, } } + + // Handle plain POST GraphQL operations. + const requestJson: { + query: string + variables?: GraphQLVariables + operations?: any /** @todo Annotate this */ + } = await requestClone.json().catch(() => null) + + if (requestJson?.query) { + const { query, variables } = requestJson + + return { + query, + variables, + } + } } default: @@ -146,13 +171,13 @@ function getGraphQLInput(request: MockedRequest): GraphQLInput | null { * Determines if a given request can be considered a GraphQL request. * Does not parse the query and does not guarantee its validity. */ -export function parseGraphQLRequest( - request: MockedRequest, -): ParsedGraphQLRequest { - const input = getGraphQLInput(request) +export async function parseGraphQLRequest( + request: Request, +): Promise { + const input = await getGraphQLInput(request) if (!input || !input.query) { - return undefined + return } const { query, variables } = input @@ -172,6 +197,7 @@ export function parseGraphQLRequest( } return { + query: input.query, operationType: parsedResult.operationType, operationName: parsedResult.operationName, variables, diff --git a/src/utils/internal/parseMultipartData.test.ts b/src/core/utils/internal/parseMultipartData.test.ts similarity index 100% rename from src/utils/internal/parseMultipartData.test.ts rename to src/core/utils/internal/parseMultipartData.test.ts diff --git a/src/utils/internal/parseMultipartData.ts b/src/core/utils/internal/parseMultipartData.ts similarity index 100% rename from src/utils/internal/parseMultipartData.ts rename to src/core/utils/internal/parseMultipartData.ts diff --git a/src/utils/internal/pipeEvents.test.ts b/src/core/utils/internal/pipeEvents.test.ts similarity index 74% rename from src/utils/internal/pipeEvents.test.ts rename to src/core/utils/internal/pipeEvents.test.ts index 727a0cdc8..d5e9e51b3 100644 --- a/src/utils/internal/pipeEvents.test.ts +++ b/src/core/utils/internal/pipeEvents.test.ts @@ -1,9 +1,9 @@ -import { EventEmitter } from 'stream' +import { Emitter } from 'strict-event-emitter' import { pipeEvents } from './pipeEvents' it('pipes events from the source emitter to the destination emitter', () => { - const source = new EventEmitter() - const destination = new EventEmitter() + const source = new Emitter() + const destination = new Emitter() pipeEvents(source, destination) const callback = jest.fn() diff --git a/src/utils/internal/pipeEvents.ts b/src/core/utils/internal/pipeEvents.ts similarity index 100% rename from src/utils/internal/pipeEvents.ts rename to src/core/utils/internal/pipeEvents.ts diff --git a/src/utils/internal/requestHandlerUtils.ts b/src/core/utils/internal/requestHandlerUtils.ts similarity index 52% rename from src/utils/internal/requestHandlerUtils.ts rename to src/core/utils/internal/requestHandlerUtils.ts index 10d18952d..2b50fa29f 100644 --- a/src/utils/internal/requestHandlerUtils.ts +++ b/src/core/utils/internal/requestHandlerUtils.ts @@ -1,21 +1,21 @@ import { RequestHandler } from '../../handlers/RequestHandler' export function use( - currentHandlers: RequestHandler[], - ...handlers: RequestHandler[] + currentHandlers: Array, + ...handlers: Array ): void { currentHandlers.unshift(...handlers) } -export function restoreHandlers(handlers: RequestHandler[]): void { +export function restoreHandlers(handlers: Array): void { handlers.forEach((handler) => { - handler.markAsSkipped(false) + handler.isUsed = false }) } export function resetHandlers( - initialHandlers: RequestHandler[], - ...nextHandlers: RequestHandler[] + initialHandlers: Array, + ...nextHandlers: Array ) { return nextHandlers.length > 0 ? [...nextHandlers] : [...initialHandlers] } diff --git a/src/utils/internal/toReadonlyArray.test.ts b/src/core/utils/internal/toReadonlyArray.test.ts similarity index 100% rename from src/utils/internal/toReadonlyArray.test.ts rename to src/core/utils/internal/toReadonlyArray.test.ts diff --git a/src/utils/internal/toReadonlyArray.ts b/src/core/utils/internal/toReadonlyArray.ts similarity index 100% rename from src/utils/internal/toReadonlyArray.ts rename to src/core/utils/internal/toReadonlyArray.ts diff --git a/src/utils/internal/tryCatch.test.ts b/src/core/utils/internal/tryCatch.test.ts similarity index 100% rename from src/utils/internal/tryCatch.test.ts rename to src/core/utils/internal/tryCatch.test.ts diff --git a/src/utils/internal/tryCatch.ts b/src/core/utils/internal/tryCatch.ts similarity index 100% rename from src/utils/internal/tryCatch.ts rename to src/core/utils/internal/tryCatch.ts diff --git a/src/core/utils/internal/uuidv4.ts b/src/core/utils/internal/uuidv4.ts new file mode 100644 index 000000000..5daf9d0cc --- /dev/null +++ b/src/core/utils/internal/uuidv4.ts @@ -0,0 +1,3 @@ +export function uuidv4(): string { + return Math.random().toString(16).slice(2) +} diff --git a/src/utils/logging/getStatusCodeColor.test.ts b/src/core/utils/logging/getStatusCodeColor.test.ts similarity index 100% rename from src/utils/logging/getStatusCodeColor.test.ts rename to src/core/utils/logging/getStatusCodeColor.test.ts diff --git a/src/utils/logging/getStatusCodeColor.ts b/src/core/utils/logging/getStatusCodeColor.ts similarity index 100% rename from src/utils/logging/getStatusCodeColor.ts rename to src/core/utils/logging/getStatusCodeColor.ts diff --git a/src/utils/logging/getTimestamp.test.ts b/src/core/utils/logging/getTimestamp.test.ts similarity index 100% rename from src/utils/logging/getTimestamp.test.ts rename to src/core/utils/logging/getTimestamp.test.ts diff --git a/src/utils/logging/getTimestamp.ts b/src/core/utils/logging/getTimestamp.ts similarity index 100% rename from src/utils/logging/getTimestamp.ts rename to src/core/utils/logging/getTimestamp.ts diff --git a/src/core/utils/logging/serializeRequest.test.ts b/src/core/utils/logging/serializeRequest.test.ts new file mode 100644 index 000000000..aac41d891 --- /dev/null +++ b/src/core/utils/logging/serializeRequest.test.ts @@ -0,0 +1,23 @@ +import { encodeBuffer } from '@mswjs/interceptors' +import { serializeRequest } from './serializeRequest' + +test('serializes given Request instance into a plain object', async () => { + const request = await serializeRequest( + new Request(new URL('http://test.mswjs.io/user'), { + method: 'POST', + headers: new Headers({ + 'Content-Type': 'text/plain', + 'X-Header': 'secret', + }), + body: encodeBuffer('text-body'), + }), + ) + + expect(request.method).toBe('POST') + expect(request.url.href).toBe('http://test.mswjs.io/user') + expect(request.headers).toEqual({ + 'content-type': 'text/plain', + 'x-header': 'secret', + }) + expect(request.body).toBe('text-body') +}) diff --git a/src/core/utils/logging/serializeRequest.ts b/src/core/utils/logging/serializeRequest.ts new file mode 100644 index 000000000..a2c2afd01 --- /dev/null +++ b/src/core/utils/logging/serializeRequest.ts @@ -0,0 +1,23 @@ +export interface LoggedRequest { + url: URL + method: string + headers: Record + body: string +} + +/** + * Formats a mocked request for introspection in browser's console. + */ +export async function serializeRequest( + request: Request, +): Promise { + const requestClone = request.clone() + const requestText = await requestClone.text() + + return { + url: new URL(request.url), + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + body: requestText, + } +} diff --git a/src/core/utils/logging/serializeResponse.test.ts b/src/core/utils/logging/serializeResponse.test.ts new file mode 100644 index 000000000..61a903286 --- /dev/null +++ b/src/core/utils/logging/serializeResponse.test.ts @@ -0,0 +1,77 @@ +/** + * @jest-environment node + */ +import { encodeBuffer } from '@mswjs/interceptors' +import { serializeResponse } from './serializeResponse' + +it('serializes response without body', async () => { + const result = await serializeResponse(new Response(null)) + + expect(result.status).toBe(200) + expect(result.statusText).toBe('OK') + expect(result.headers).toEqual({}) + expect(result.body).toBe('') +}) + +it('serializes a plain text response', async () => { + const result = await serializeResponse( + new Response('hello world', { + status: 201, + statusText: 'Created', + headers: { + 'Content-Type': 'text/plain', + }, + }), + ) + + expect(result.status).toBe(201) + expect(result.statusText).toBe('Created') + expect(result.headers).toEqual({ + 'content-type': 'text/plain', + }) + expect(result.body).toBe('hello world') +}) + +it('serializes a JSON response', async () => { + const response = new Response(JSON.stringify({ users: ['John'] }), { + headers: { + 'Content-Type': 'application/json', + }, + }) + const result = await serializeResponse(response) + + expect(result.headers).toEqual({ + 'content-type': 'application/json', + }) + expect(result.body).toBe(JSON.stringify({ users: ['John'] })) +}) + +it('serializes a ArrayBuffer response', async () => { + const data = encodeBuffer('hello world') + const response = new Response(data) + const result = await serializeResponse(response) + + expect(result.body).toBe('hello world') +}) + +it('serializes a Blob response', async () => { + const response = new Response(new Blob(['hello world'])) + const result = await serializeResponse(response) + + expect(result.body).toBe('hello world') +}) + +it('serializes a FormData response', async () => { + const data = new FormData() + data.set('firstName', 'Alice') + data.set('age', '32') + const response = new Response(data) + const result = await serializeResponse(response) + + expect(result.body).toContain( + `Content-Disposition: form-data; name="firstName"\r\n\r\nAlice`, + ) + expect(result.body).toContain( + `Content-Disposition: form-data; name="age"\r\n\r\n32`, + ) +}) diff --git a/src/core/utils/logging/serializeResponse.ts b/src/core/utils/logging/serializeResponse.ts new file mode 100644 index 000000000..754dbd32e --- /dev/null +++ b/src/core/utils/logging/serializeResponse.ts @@ -0,0 +1,31 @@ +import statuses from '@bundled-es-modules/statuses' + +const { message } = statuses + +export interface SerializedResponse { + status: number + statusText: string + headers: Record + body: string +} + +export async function serializeResponse( + response: Response, +): Promise { + const responseClone = response.clone() + const responseText = await responseClone.text() + + // Normalize the response status and status text when logging + // since the default Response instance doesn't infer status texts + // from status codes. This has no effect on the actual response instance. + const responseStatus = responseClone.status || 200 + const responseStatusText = + responseClone.statusText || message[responseStatus] || 'OK' + + return { + status: responseStatus, + statusText: responseStatusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseText, + } +} diff --git a/src/utils/matching/matchRequestUrl.test.ts b/src/core/utils/matching/matchRequestUrl.test.ts similarity index 100% rename from src/utils/matching/matchRequestUrl.test.ts rename to src/core/utils/matching/matchRequestUrl.test.ts diff --git a/src/utils/matching/matchRequestUrl.ts b/src/core/utils/matching/matchRequestUrl.ts similarity index 96% rename from src/utils/matching/matchRequestUrl.ts rename to src/core/utils/matching/matchRequestUrl.ts index 8d5d25cfc..3b9ce6ebf 100644 --- a/src/utils/matching/matchRequestUrl.ts +++ b/src/core/utils/matching/matchRequestUrl.ts @@ -1,5 +1,5 @@ import { match } from 'path-to-regexp' -import { getCleanUrl } from '@mswjs/interceptors/lib/utils/getCleanUrl.js' +import { getCleanUrl } from '@mswjs/interceptors' import { normalizePath } from './normalizePath' export type Path = string | RegExp diff --git a/src/utils/matching/normalizePath.node.test.ts b/src/core/utils/matching/normalizePath.node.test.ts similarity index 100% rename from src/utils/matching/normalizePath.node.test.ts rename to src/core/utils/matching/normalizePath.node.test.ts diff --git a/src/utils/matching/normalizePath.test.ts b/src/core/utils/matching/normalizePath.test.ts similarity index 100% rename from src/utils/matching/normalizePath.test.ts rename to src/core/utils/matching/normalizePath.test.ts diff --git a/src/utils/matching/normalizePath.ts b/src/core/utils/matching/normalizePath.ts similarity index 100% rename from src/utils/matching/normalizePath.ts rename to src/core/utils/matching/normalizePath.ts diff --git a/src/core/utils/request/getPublicUrlFromRequest.test.ts b/src/core/utils/request/getPublicUrlFromRequest.test.ts new file mode 100644 index 000000000..3ac30af83 --- /dev/null +++ b/src/core/utils/request/getPublicUrlFromRequest.test.ts @@ -0,0 +1,26 @@ +/** + * @jest-environment jsdom + */ +import { getPublicUrlFromRequest } from './getPublicUrlFromRequest' + +test('returns an absolute request URL withouth search params', () => { + expect( + getPublicUrlFromRequest(new Request(new URL('https://test.mswjs.io/path'))), + ).toBe('https://test.mswjs.io/path') + + expect( + getPublicUrlFromRequest(new Request(new URL('http://localhost/path'))), + ).toBe('/path') + + expect( + getPublicUrlFromRequest( + new Request(new URL('http://localhost/path?foo=bar')), + ), + ).toBe('/path') +}) + +it('returns a relative URL given the request to the same origin', () => { + expect(getPublicUrlFromRequest(new Request('http://localhost/user'))).toBe( + '/user', + ) +}) diff --git a/src/core/utils/request/getPublicUrlFromRequest.ts b/src/core/utils/request/getPublicUrlFromRequest.ts new file mode 100644 index 000000000..63bae1014 --- /dev/null +++ b/src/core/utils/request/getPublicUrlFromRequest.ts @@ -0,0 +1,15 @@ +/** + * Returns a relative URL if the given request URL is relative to the current origin. + * Otherwise returns an absolute URL. + */ +export function getPublicUrlFromRequest(request: Request): string { + if (typeof location === 'undefined') { + return request.url + } + + const url = new URL(request.url) + + return url.origin === location.origin + ? url.pathname + : url.origin + url.pathname +} diff --git a/src/utils/request/getRequestCookies.node.test.ts b/src/core/utils/request/getRequestCookies.node.test.ts similarity index 86% rename from src/utils/request/getRequestCookies.node.test.ts rename to src/core/utils/request/getRequestCookies.node.test.ts index 185af3264..2061c4fe0 100644 --- a/src/utils/request/getRequestCookies.node.test.ts +++ b/src/core/utils/request/getRequestCookies.node.test.ts @@ -2,7 +2,6 @@ * @jest-environment node */ import { getRequestCookies } from './getRequestCookies' -import { MockedRequest } from './MockedRequest' const prevLocation = global.location @@ -21,7 +20,7 @@ afterAll(() => { test('returns empty object when in a node environment with polyfilled location object', () => { const cookies = getRequestCookies( - new MockedRequest(new URL('/user', location.origin), { + new Request(new URL('/user', location.href), { credentials: 'include', }), ) diff --git a/src/utils/request/getRequestCookies.test.ts b/src/core/utils/request/getRequestCookies.test.ts similarity index 77% rename from src/utils/request/getRequestCookies.test.ts rename to src/core/utils/request/getRequestCookies.test.ts index 42ee3676f..59a5c5b7b 100644 --- a/src/utils/request/getRequestCookies.test.ts +++ b/src/core/utils/request/getRequestCookies.test.ts @@ -2,8 +2,7 @@ * @jest-environment jsdom */ import { getRequestCookies } from './getRequestCookies' -import { clearCookies } from '../../../test/support/utils' -import { MockedRequest } from './MockedRequest' +import { clearCookies } from '../../../../test/support/utils' beforeAll(() => { // Emulate some `document.cookie` value. @@ -17,7 +16,7 @@ afterAll(() => { test('returns all document cookies given "include" credentials', () => { const cookies = getRequestCookies( - new MockedRequest(new URL('/user', location.origin), { + new Request(new URL('/user', location.origin), { credentials: 'include', }), ) @@ -30,7 +29,7 @@ test('returns all document cookies given "include" credentials', () => { test('returns all document cookies given "same-origin" credentials and the same request origin', () => { const cookies = getRequestCookies( - new MockedRequest(new URL('/user', location.origin), { + new Request(new URL('/user', location.origin), { credentials: 'same-origin', }), ) @@ -43,7 +42,7 @@ test('returns all document cookies given "same-origin" credentials and the same test('returns an empty object given "same-origin" credentials and a different request origin', () => { const cookies = getRequestCookies( - new MockedRequest(new URL('https://test.mswjs.io/user'), { + new Request(new URL('https://test.mswjs.io/user'), { credentials: 'same-origin', }), ) @@ -53,7 +52,7 @@ test('returns an empty object given "same-origin" credentials and a different re test('returns an empty object given "omit" credentials', () => { const cookies = getRequestCookies( - new MockedRequest(new URL('/user', location.origin), { + new Request(new URL('/user', location.origin), { credentials: 'omit', }), ) diff --git a/src/core/utils/request/getRequestCookies.ts b/src/core/utils/request/getRequestCookies.ts new file mode 100644 index 000000000..749390ee2 --- /dev/null +++ b/src/core/utils/request/getRequestCookies.ts @@ -0,0 +1,76 @@ +import cookieUtils from '@bundled-es-modules/cookie' +import { store } from '@mswjs/cookies' + +function getAllDocumentCookies() { + return cookieUtils.parse(document.cookie) +} + +/** @todo Rename this to "getDocumentCookies" */ +/** + * Returns relevant document cookies based on the request `credentials` option. + */ +export function getRequestCookies(request: Request): Record { + /** + * @note No cookies persist on the document in Node.js: no document. + */ + if (typeof document === 'undefined' || typeof location === 'undefined') { + return {} + } + + switch (request.credentials) { + case 'same-origin': { + const url = new URL(request.url) + + // Return document cookies only when requested a resource + // from the same origin as the current document. + return location.origin === url.origin ? getAllDocumentCookies() : {} + } + + case 'include': { + // Return all document cookies. + return getAllDocumentCookies() + } + + default: { + return {} + } + } +} + +export function getAllRequestCookies(request: Request): Record { + const requestCookiesString = request.headers.get('cookie') + const cookiesFromHeaders = requestCookiesString + ? cookieUtils.parse(requestCookiesString) + : {} + + store.hydrate() + + const cookiesFromStore = Array.from(store.get(request)?.entries()).reduce( + (cookies, [name, { value }]) => { + return Object.assign(cookies, { [name.trim()]: value }) + }, + {}, + ) + + const cookiesFromDocument = getRequestCookies(request) + + const forwardedCookies = { + ...cookiesFromDocument, + ...cookiesFromStore, + } + + // Set the inferred cookies from the cookie store and the document + // on the request's headers. + /** + * @todo Consider making this a separate step so this function + * is pure-er. + */ + for (const [name, value] of Object.entries(forwardedCookies)) { + request.headers.append('cookie', `${name}=${value}`) + } + + return { + ...forwardedCookies, + ...cookiesFromHeaders, + } +} diff --git a/src/utils/request/onUnhandledRequest.test.ts b/src/core/utils/request/onUnhandledRequest.test.ts similarity index 51% rename from src/utils/request/onUnhandledRequest.test.ts rename to src/core/utils/request/onUnhandledRequest.test.ts index 3d6de6e75..5649ea866 100644 --- a/src/utils/request/onUnhandledRequest.test.ts +++ b/src/core/utils/request/onUnhandledRequest.test.ts @@ -1,34 +1,36 @@ +/** + * @jest-environment jsdom + */ import { onUnhandledRequest, UnhandledRequestCallback, } from './onUnhandledRequest' -import { RestHandler, RESTMethods } from '../../handlers/RestHandler' +import { HttpHandler, HttpMethods } from '../../handlers/HttpHandler' import { ResponseResolver } from '../../handlers/RequestHandler' -import { MockedRequest } from './MockedRequest' const resolver: ResponseResolver = () => void 0 const fixtures = { warningWithoutSuggestions: `\ -[MSW] Warning: captured a request without a matching request handler: +[MSW] Warning: intercepted a request without a matching request handler: - • GET http://localhost/api + • GET /api If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/getting-started/mocks`, errorWithoutSuggestions: `\ -[MSW] Error: captured a request without a matching request handler: +[MSW] Error: intercepted a request without a matching request handler: - • GET http://localhost/api + • GET /api If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/getting-started/mocks`, warningWithSuggestions: (suggestions: string) => `\ -[MSW] Warning: captured a request without a matching request handler: +[MSW] Warning: intercepted a request without a matching request handler: - • GET http://localhost/api + • GET /api Did you mean to request one of the following resources instead? @@ -47,9 +49,9 @@ afterEach(() => { jest.restoreAllMocks() }) -test('supports the "bypass" request strategy', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('supports the "bypass" request strategy', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [], 'bypass', ) @@ -58,9 +60,9 @@ test('supports the "bypass" request strategy', () => { expect(console.error).not.toHaveBeenCalled() }) -test('supports the "warn" request strategy', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('supports the "warn" request strategy', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [], 'warn', ) @@ -68,28 +70,28 @@ test('supports the "warn" request strategy', () => { expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) }) -test('supports the "error" request strategy', () => { - expect(() => +test('supports the "error" request strategy', async () => { + await expect( onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), + new Request(new URL('http://localhost/api')), [], 'error', ), - ).toThrow( + ).rejects.toThrow( '[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ) expect(console.error).toHaveBeenCalledWith(fixtures.errorWithoutSuggestions) }) -test('supports a custom callback function', () => { +test('supports a custom callback function', async () => { const callback = jest.fn>( (request) => { - console.warn(`callback: ${request.method} ${request.url.href}`) + console.warn(`callback: ${request.method} ${request.url}`) }, ) - const request = new MockedRequest(new URL('/user', 'http://localhost:3000')) - onUnhandledRequest(request, [], callback) + const request = new Request(new URL('/user', 'http://localhost:3000')) + await onUnhandledRequest(request, [], callback) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith(request, { @@ -103,17 +105,15 @@ test('supports a custom callback function', () => { ) }) -test('supports calling default strategies from the custom callback function', () => { +test('supports calling default strategies from the custom callback function', async () => { const callback = jest.fn>( (request, print) => { - console.warn(`custom callback: ${request.id}`) - // Call the default "error" strategy. print.error() }, ) - const request = new MockedRequest(new URL('http://localhost/api')) - expect(() => onUnhandledRequest(request, [], callback)).toThrow( + const request = new Request(new URL('http://localhost/api')) + await expect(onUnhandledRequest(request, [], callback)).rejects.toThrow( `[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.`, ) @@ -123,16 +123,13 @@ test('supports calling default strategies from the custom callback function', () error: expect.any(Function), }) - // Check that the custom logic in the callback was called. - expect(console.warn).toHaveBeenCalledWith(`custom callback: ${request.id}`) - // Check that the default strategy was called. expect(console.error).toHaveBeenCalledWith(fixtures.errorWithoutSuggestions) }) -test('does not print any suggestions given no handlers to suggest', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('does not print any suggestions given no handlers to suggest', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [], 'warn', ) @@ -140,14 +137,14 @@ test('does not print any suggestions given no handlers to suggest', () => { expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) }) -test('does not print any suggestions given no handlers are similar', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('does not print any suggestions given no handlers are similar', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [ // None of the defined request handlers match the actual request URL // to be used as suggestions. - new RestHandler(RESTMethods.GET, 'https://api.github.com', resolver), - new RestHandler(RESTMethods.GET, 'https://api.stripe.com', resolver), + new HttpHandler(HttpMethods.GET, 'https://api.github.com', resolver), + new HttpHandler(HttpMethods.GET, 'https://api.stripe.com', resolver), ], 'warn', ) @@ -155,66 +152,66 @@ test('does not print any suggestions given no handlers are similar', () => { expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) }) -test('respects RegExp as a request handler method', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), - [new RestHandler(/^GE/, 'http://localhost/api', resolver)], +test('respects RegExp as a request handler method', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), + [new HttpHandler(/^GE/, 'http://localhost/api', resolver)], 'warn', ) expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) }) -test('sorts the suggestions by relevance', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('sorts the suggestions by relevance', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [ - new RestHandler(RESTMethods.GET, 'http://localhost/', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost:9090/api', resolver), - new RestHandler(RESTMethods.POST, 'http://localhost/api', resolver), + new HttpHandler(HttpMethods.GET, '/', resolver), + new HttpHandler(HttpMethods.GET, 'https://api.example.com/api', resolver), + new HttpHandler(HttpMethods.POST, '/api', resolver), ], 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithSuggestions(`\ - • POST http://localhost/api - • GET http://localhost/`), + • POST /api + • GET /`), ) }) -test('does not print more than 4 suggestions', () => { - onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), +test('does not print more than 4 suggestions', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api')), [ - new RestHandler(RESTMethods.GET, 'http://localhost/ap', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost/api', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost/api-1', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost/api-2', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost/api-3', resolver), - new RestHandler(RESTMethods.GET, 'http://localhost/api-4', resolver), + new HttpHandler(HttpMethods.GET, '/ap', resolver), + new HttpHandler(HttpMethods.GET, '/api', resolver), + new HttpHandler(HttpMethods.GET, '/api-1', resolver), + new HttpHandler(HttpMethods.GET, '/api-2', resolver), + new HttpHandler(HttpMethods.GET, '/api-3', resolver), + new HttpHandler(HttpMethods.GET, '/api-4', resolver), ], 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithSuggestions(`\ - • GET http://localhost/api - • GET http://localhost/ap - • GET http://localhost/api-1 - • GET http://localhost/api-2`), + • GET /api + • GET /ap + • GET /api-1 + • GET /api-2`), ) }) -test('throws an exception given unknown request strategy', () => { - expect(() => +test('throws an exception given unknown request strategy', async () => { + await expect( onUnhandledRequest( - new MockedRequest(new URL('http://localhost/api')), + new Request(new URL('http://localhost/api')), [], // @ts-expect-error Intentional unknown strategy. - 'arbitrary-strategy', + 'invalid-strategy', ), - ).toThrow( - '[MSW] Failed to react to an unhandled request: unknown strategy "arbitrary-strategy". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', + ).rejects.toThrow( + '[MSW] Failed to react to an unhandled request: unknown strategy "invalid-strategy". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', ) }) diff --git a/src/utils/request/onUnhandledRequest.ts b/src/core/utils/request/onUnhandledRequest.ts similarity index 77% rename from src/utils/request/onUnhandledRequest.ts rename to src/core/utils/request/onUnhandledRequest.ts index 96432e150..89a571360 100644 --- a/src/utils/request/onUnhandledRequest.ts +++ b/src/core/utils/request/onUnhandledRequest.ts @@ -1,16 +1,16 @@ -import getStringMatchScore from 'js-levenshtein' +// @ts-ignore +import jsLevenshtein from '@bundled-es-modules/js-levenshtein' +import { RequestHandler, HttpHandler, GraphQLHandler } from '../..' import { ParsedGraphQLQuery, + ParsedGraphQLRequest, parseGraphQLRequest, } from '../internal/parseGraphQLRequest' import { getPublicUrlFromRequest } from './getPublicUrlFromRequest' import { isStringEqual } from '../internal/isStringEqual' -import { RestHandler } from '../../handlers/RestHandler' -import { GraphQLHandler } from '../../handlers/GraphQLHandler' -import { RequestHandler } from '../../handlers/RequestHandler' -import { tryCatch } from '../internal/tryCatch' import { devUtils } from '../internal/devUtils' -import { MockedRequest } from './MockedRequest' + +const getStringMatchScore = jsLevenshtein const MAX_MATCH_SCORE = 3 const MAX_SUGGESTION_COUNT = 4 @@ -22,7 +22,7 @@ export interface UnhandledRequestPrint { } export type UnhandledRequestCallback = ( - request: MockedRequest, + request: Request, print: UnhandledRequestPrint, ) => void @@ -33,15 +33,17 @@ export type UnhandledRequestStrategy = | UnhandledRequestCallback interface RequestHandlerGroups { - rest: RestHandler[] - graphql: GraphQLHandler[] + http: Array + graphql: Array } -function groupHandlersByType(handlers: RequestHandler[]): RequestHandlerGroups { +function groupHandlersByType( + handlers: Array, +): RequestHandlerGroups { return handlers.reduce( (groups, handler) => { - if (handler instanceof RestHandler) { - groups.rest.push(handler) + if (handler instanceof HttpHandler) { + groups.http.push(handler) } if (handler instanceof GraphQLHandler) { @@ -51,7 +53,7 @@ function groupHandlersByType(handlers: RequestHandler[]): RequestHandlerGroups { return groups }, { - rest: [], + http: [], graphql: [], }, ) @@ -60,11 +62,11 @@ function groupHandlersByType(handlers: RequestHandler[]): RequestHandlerGroups { type RequestHandlerSuggestion = [number, RequestHandler] type ScoreGetterFn = ( - request: MockedRequest, + request: Request, handler: RequestHandlerType, ) => number -function getRestHandlerScore(): ScoreGetterFn { +function getHttpHandlerScore(): ScoreGetterFn { return (request, handler) => { const { path, method } = handler.info @@ -107,12 +109,12 @@ function getGraphQLHandlerScore( } function getSuggestedHandler( - request: MockedRequest, - handlers: RestHandler[] | GraphQLHandler[], - getScore: ScoreGetterFn | ScoreGetterFn, -): RequestHandler[] { - const suggestedHandlers = (handlers as RequestHandler[]) - .reduce((suggestions, handler) => { + request: Request, + handlers: Array | Array, + getScore: ScoreGetterFn | ScoreGetterFn, +): Array { + const suggestedHandlers = (handlers as Array) + .reduce>((suggestions, handler) => { const score = getScore(request, handler as any) return suggestions.concat([[score, handler]]) }, []) @@ -135,12 +137,15 @@ ${handlers.map((handler) => ` • ${handler.info.header}`).join('\n')}` return `Did you mean to request "${handlers[0].info.header}" instead?` } -export function onUnhandledRequest( - request: MockedRequest, - handlers: RequestHandler[], +export async function onUnhandledRequest( + request: Request, + handlers: Array, strategy: UnhandledRequestStrategy = 'warn', -): void { - const parsedGraphQLQuery = tryCatch(() => parseGraphQLRequest(request)) +): Promise { + const parsedGraphQLQuery = await parseGraphQLRequest(request).catch( + () => null, + ) + const publicUrl = getPublicUrlFromRequest(request) function generateHandlerSuggestion(): string { /** @@ -151,14 +156,14 @@ export function onUnhandledRequest( const handlerGroups = groupHandlersByType(handlers) const relevantHandlers = parsedGraphQLQuery ? handlerGroups.graphql - : handlerGroups.rest + : handlerGroups.http const suggestedHandlers = getSuggestedHandler( request, relevantHandlers, parsedGraphQLQuery ? getGraphQLHandlerScore(parsedGraphQLQuery) - : getRestHandlerScore(), + : getHttpHandlerScore(), ) return suggestedHandlers.length > 0 @@ -166,15 +171,24 @@ export function onUnhandledRequest( : '' } + function getGraphQLRequestHeader( + parsedGraphQLRequest: ParsedGraphQLRequest, + ): string { + if (!parsedGraphQLRequest?.operationName) { + return `anonymous ${parsedGraphQLRequest?.operationType} (${request.method} ${publicUrl})` + } + + return `${parsedGraphQLRequest.operationType} ${parsedGraphQLRequest.operationName} (${request.method} ${publicUrl})` + } + function generateUnhandledRequestMessage(): string { - const publicUrl = getPublicUrlFromRequest(request) const requestHeader = parsedGraphQLQuery - ? `${parsedGraphQLQuery.operationType} ${parsedGraphQLQuery.operationName} (${request.method} ${publicUrl})` + ? getGraphQLRequestHeader(parsedGraphQLQuery) : `${request.method} ${publicUrl}` const handlerSuggestion = generateHandlerSuggestion() const messageTemplate = [ - `captured a request without a matching request handler:`, + `intercepted a request without a matching request handler:`, ` \u2022 ${requestHeader}`, handlerSuggestion, `\ diff --git a/src/utils/request/readResponseCookies.ts b/src/core/utils/request/readResponseCookies.ts similarity index 51% rename from src/utils/request/readResponseCookies.ts rename to src/core/utils/request/readResponseCookies.ts index e57497dae..0e01b6137 100644 --- a/src/utils/request/readResponseCookies.ts +++ b/src/core/utils/request/readResponseCookies.ts @@ -1,11 +1,9 @@ import { store } from '@mswjs/cookies' -import { MockedResponse } from '../../response' -import { MockedRequest } from './MockedRequest' export function readResponseCookies( - request: MockedRequest, - response: MockedResponse, -) { + request: Request, + response: Response, +): void { store.add({ ...request, url: request.url.toString() }, response) store.persist() } diff --git a/src/core/utils/toResponseInit.ts b/src/core/utils/toResponseInit.ts new file mode 100644 index 000000000..e7e6f9a7a --- /dev/null +++ b/src/core/utils/toResponseInit.ts @@ -0,0 +1,7 @@ +export function toResponseInit(response: Response): ResponseInit { + return { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + } +} diff --git a/src/utils/url/cleanUrl.test.ts b/src/core/utils/url/cleanUrl.test.ts similarity index 100% rename from src/utils/url/cleanUrl.test.ts rename to src/core/utils/url/cleanUrl.test.ts diff --git a/src/utils/url/cleanUrl.ts b/src/core/utils/url/cleanUrl.ts similarity index 100% rename from src/utils/url/cleanUrl.ts rename to src/core/utils/url/cleanUrl.ts diff --git a/src/utils/url/getAbsoluteUrl.node.test.ts b/src/core/utils/url/getAbsoluteUrl.node.test.ts similarity index 100% rename from src/utils/url/getAbsoluteUrl.node.test.ts rename to src/core/utils/url/getAbsoluteUrl.node.test.ts diff --git a/src/utils/url/getAbsoluteUrl.test.ts b/src/core/utils/url/getAbsoluteUrl.test.ts similarity index 100% rename from src/utils/url/getAbsoluteUrl.test.ts rename to src/core/utils/url/getAbsoluteUrl.test.ts diff --git a/src/utils/url/getAbsoluteUrl.ts b/src/core/utils/url/getAbsoluteUrl.ts similarity index 100% rename from src/utils/url/getAbsoluteUrl.ts rename to src/core/utils/url/getAbsoluteUrl.ts diff --git a/src/utils/url/isAbsoluteUrl.test.ts b/src/core/utils/url/isAbsoluteUrl.test.ts similarity index 100% rename from src/utils/url/isAbsoluteUrl.test.ts rename to src/core/utils/url/isAbsoluteUrl.test.ts diff --git a/src/utils/url/isAbsoluteUrl.ts b/src/core/utils/url/isAbsoluteUrl.ts similarity index 100% rename from src/utils/url/isAbsoluteUrl.ts rename to src/core/utils/url/isAbsoluteUrl.ts diff --git a/src/graphql.ts b/src/graphql.ts deleted file mode 100644 index 7f971bdf6..000000000 --- a/src/graphql.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { DocumentNode, OperationTypeNode } from 'graphql' -import { ResponseResolver } from './handlers/RequestHandler' -import { - GraphQLHandler, - GraphQLContext, - GraphQLRequest, - GraphQLVariables, - ExpectedOperationTypeNode, - GraphQLHandlerNameSelector, -} from './handlers/GraphQLHandler' -import { Path } from './utils/matching/matchRequestUrl' - -export interface TypedDocumentNode< - Result = { [key: string]: any }, - Variables = { [key: string]: any }, -> extends DocumentNode { - __apiType?: (variables: Variables) => Result - __resultType?: Result - __variablesType?: Variables -} - -function createScopedGraphQLHandler( - operationType: ExpectedOperationTypeNode, - url: Path, -) { - return < - Query extends Record, - Variables extends GraphQLVariables = GraphQLVariables, - >( - operationName: - | GraphQLHandlerNameSelector - | DocumentNode - | TypedDocumentNode, - resolver: ResponseResolver< - GraphQLRequest, - GraphQLContext - >, - ) => { - return new GraphQLHandler>( - operationType, - operationName, - url, - resolver, - ) - } -} - -function createGraphQLOperationHandler(url: Path) { - return < - Query extends Record, - Variables extends GraphQLVariables = GraphQLVariables, - >( - resolver: ResponseResolver< - GraphQLRequest, - GraphQLContext - >, - ) => { - return new GraphQLHandler>( - 'all', - new RegExp('.*'), - url, - resolver, - ) - } -} - -const standardGraphQLHandlers = { - /** - * Captures any GraphQL operation, regardless of its name, under the current scope. - * @example - * graphql.operation((req, res, ctx) => { - * return res(ctx.data({ name: 'John' })) - * }) - * @see {@link https://mswjs.io/docs/api/graphql/operation `graphql.operation()`} - */ - operation: createGraphQLOperationHandler('*'), - - /** - * Captures a GraphQL query by a given name. - * @example - * graphql.query('GetUser', (req, res, ctx) => { - * return res(ctx.data({ user: { name: 'John' } })) - * }) - * @see {@link https://mswjs.io/docs/api/graphql/query `graphql.query()`} - */ - query: createScopedGraphQLHandler('query' as OperationTypeNode, '*'), - - /** - * Captures a GraphQL mutation by a given name. - * @example - * graphql.mutation('SavePost', (req, res, ctx) => { - * return res(ctx.data({ post: { id: 'abc-123' } })) - * }) - * @see {@link https://mswjs.io/docs/api/graphql/mutation `graphql.mutation()`} - */ - mutation: createScopedGraphQLHandler('mutation' as OperationTypeNode, '*'), -} - -function createGraphQLLink(url: Path): typeof standardGraphQLHandlers { - return { - operation: createGraphQLOperationHandler(url), - query: createScopedGraphQLHandler('query' as OperationTypeNode, url), - mutation: createScopedGraphQLHandler('mutation' as OperationTypeNode, url), - } -} - -export const graphql = { - ...standardGraphQLHandlers, - link: createGraphQLLink, -} diff --git a/src/handlers/RequestHandler.ts b/src/handlers/RequestHandler.ts deleted file mode 100644 index 3d0e5bd10..000000000 --- a/src/handlers/RequestHandler.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Headers } from 'headers-polyfill' -import { - MaybePromise, - MockedResponse, - response, - ResponseComposition, -} from '../response' -import { getCallFrame } from '../utils/internal/getCallFrame' -import { isIterable } from '../utils/internal/isIterable' -import { status } from '../context/status' -import { set } from '../context/set' -import { delay } from '../context/delay' -import { fetch } from '../context/fetch' -import { ResponseResolutionContext } from '../utils/getResponse' -import { SerializedResponse } from '../setupWorker/glossary' -import { MockedRequest } from '../utils/request/MockedRequest' - -export type DefaultContext = { - status: typeof status - set: typeof set - delay: typeof delay - fetch: typeof fetch -} - -export const defaultContext: DefaultContext = { - status, - set, - delay, - fetch, -} - -export type DefaultRequestMultipartBody = Record< - string, - string | File | (string | File)[] -> - -export type DefaultBodyType = - | Record - | DefaultRequestMultipartBody - | string - | number - | boolean - | null - | undefined - -export interface RequestHandlerDefaultInfo { - header: string -} - -export interface RequestHandlerInternalInfo { - callFrame?: string -} - -type ContextMap = Record any> - -export type ResponseResolverReturnType = - | ReturnType - | undefined - | void - -export type MaybeAsyncResponseResolverReturnType = MaybePromise< - ResponseResolverReturnType -> - -export type AsyncResponseResolverReturnType = - | MaybeAsyncResponseResolverReturnType - | Generator< - MaybeAsyncResponseResolverReturnType, - MaybeAsyncResponseResolverReturnType, - MaybeAsyncResponseResolverReturnType - > - -export type ResponseResolver< - RequestType = MockedRequest, - ContextType = typeof defaultContext, - BodyType extends DefaultBodyType = any, -> = ( - req: RequestType, - res: ResponseComposition, - context: ContextType, -) => AsyncResponseResolverReturnType> - -export interface RequestHandlerOptions { - info: HandlerInfo - resolver: ResponseResolver - ctx?: ContextMap -} - -export interface RequestHandlerExecutionResult { - handler: RequestHandler - parsedResult: any - request: PublicRequestType - response?: MockedResponse -} - -export abstract class RequestHandler< - HandlerInfo extends RequestHandlerDefaultInfo = RequestHandlerDefaultInfo, - Request extends MockedRequest = MockedRequest, - ParsedResult = any, - PublicRequest extends MockedRequest = Request, -> { - public info: HandlerInfo & RequestHandlerInternalInfo - public shouldSkip: boolean - - private ctx: ContextMap - private resolverGenerator?: Generator< - MaybeAsyncResponseResolverReturnType, - MaybeAsyncResponseResolverReturnType, - MaybeAsyncResponseResolverReturnType - > - private resolverGeneratorResult?: MaybeAsyncResponseResolverReturnType - - protected resolver: ResponseResolver - - constructor(options: RequestHandlerOptions) { - this.shouldSkip = false - this.ctx = options.ctx || defaultContext - this.resolver = options.resolver - - const callFrame = getCallFrame(new Error()) - - this.info = { - ...options.info, - callFrame, - } - } - - /** - * Determine if the captured request should be mocked. - */ - abstract predicate( - request: MockedRequest, - parsedResult: ParsedResult, - resolutionContext?: ResponseResolutionContext, - ): boolean - - /** - * Print out the successfully handled request. - */ - abstract log( - request: Request, - response: SerializedResponse, - parsedResult: ParsedResult, - ): void - - /** - * Parse the captured request to extract additional information from it. - * Parsed result is then exposed to other methods of this request handler. - */ - parse( - _request: MockedRequest, - _resolutionContext?: ResponseResolutionContext, - ): ParsedResult { - return null as any - } - - /** - * Test if this handler matches the given request. - */ - public test( - request: MockedRequest, - resolutionContext?: ResponseResolutionContext, - ): boolean { - return this.predicate( - request, - this.parse(request, resolutionContext), - resolutionContext, - ) - } - - /** - * Derive the publicly exposed request (`req`) instance of the response resolver - * from the captured request and its parsed result. - */ - protected getPublicRequest( - request: MockedRequest, - _parsedResult: ParsedResult, - ) { - return request as PublicRequest - } - - public markAsSkipped(shouldSkip = true) { - this.shouldSkip = shouldSkip - } - - /** - * Execute this request handler and produce a mocked response - * using the given resolver function. - */ - public async run( - request: MockedRequest, - resolutionContext?: ResponseResolutionContext, - ): Promise | null> { - if (this.shouldSkip) { - return null - } - - const parsedResult = this.parse(request, resolutionContext) - const shouldIntercept = this.predicate( - request, - parsedResult, - resolutionContext, - ) - - if (!shouldIntercept) { - return null - } - - const publicRequest = this.getPublicRequest(request, parsedResult) - - // 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 mockedResponse = await executeResolver( - publicRequest, - response, - this.ctx, - ) - - return this.createExecutionResult( - parsedResult, - publicRequest, - mockedResponse, - ) - } - - private wrapResolver( - resolver: ResponseResolver, - ): ResponseResolver, any> { - return async (req, res, ctx) => { - const result = this.resolverGenerator || (await resolver(req, res, ctx)) - - if (isIterable>(result)) { - const { value, done } = result[Symbol.iterator]().next() - const nextResponse = await value - - // If the generator is done and there is no next value, - // return the previous generator's value. - if (!nextResponse && done) { - return this.resolverGeneratorResult - } - - if (!this.resolverGenerator) { - this.resolverGenerator = result - } - - this.resolverGeneratorResult = nextResponse - return nextResponse - } - - return result - } - } - - private createExecutionResult( - parsedResult: ParsedResult, - request: PublicRequest, - response: any, - ): RequestHandlerExecutionResult { - return { - handler: this, - parsedResult: parsedResult || null, - request, - response: response || null, - } - } -} - -/** - * Bypass this intercepted request. - * This will make a call to the actual endpoint requested. - */ -export function passthrough(): MockedResponse { - // Constructing a dummy "101 Continue" mocked response - // to keep the return type of the resolver consistent. - return { - status: 101, - statusText: 'Continue', - headers: new Headers(), - body: null, - // Setting "passthrough" to true will signal the response pipeline - // to perform this intercepted request as-is. - passthrough: true, - once: false, - } -} diff --git a/src/handlers/RestHandler.test.ts b/src/handlers/RestHandler.test.ts deleted file mode 100644 index 04bb5bc50..000000000 --- a/src/handlers/RestHandler.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { RestHandler, RestRequest, RestContext } from './RestHandler' -import { response } from '../response' -import { context, MockedRequest } from '..' -import { ResponseResolver } from './RequestHandler' - -const resolver: ResponseResolver< - RestRequest<{ userId: string }>, - RestContext -> = (req, res, ctx) => { - return res(ctx.json({ userId: req.params.userId })) -} - -const generatorResolver: ResponseResolver< - RestRequest<'pending' | 'complete'>, - RestContext -> = function* (req, res, ctx) { - let count = 0 - while (count < 5) { - count += 1 - yield res(ctx.body('pending')) - } - return res(ctx.body('complete')) -} - -describe('info', () => { - test('exposes request handler information', () => { - const handler = new RestHandler('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') - }) -}) - -describe('parse', () => { - test('parses a URL given a matching request', () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const request = new MockedRequest(new URL('/user/abc-123', location.href)) - - expect(handler.parse(request)).toEqual({ - matches: true, - params: { - userId: 'abc-123', - }, - }) - }) - - test('parses a URL and ignores the request method', () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const request = new MockedRequest(new URL('/user/def-456', location.href), { - method: 'POST', - }) - - expect(handler.parse(request)).toEqual({ - matches: true, - params: { - userId: 'def-456', - }, - }) - }) - - test('returns negative match result given a non-matching request', () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const request = new MockedRequest(new URL('/login', location.href)) - - expect(handler.parse(request)).toEqual({ - matches: false, - params: {}, - }) - }) -}) - -describe('predicate', () => { - test('returns true given a matching request', () => { - const handler = new RestHandler('POST', '/login', resolver) - const request = new MockedRequest(new URL('/login', location.href), { - method: 'POST', - }) - - expect(handler.predicate(request, handler.parse(request))).toBe(true) - }) - - test('respects RegExp as the request method', () => { - const handler = new RestHandler(/.+/, '/login', resolver) - const requests = [ - new MockedRequest(new URL('/login', location.href)), - new MockedRequest(new URL('/login', location.href), { method: 'POST' }), - new MockedRequest(new URL('/login', location.href), { method: 'DELETE' }), - ] - - for (const request of requests) { - expect(handler.predicate(request, handler.parse(request))).toBe(true) - } - }) - - test('returns false given a non-matching request', () => { - const handler = new RestHandler('POST', '/login', resolver) - const request = new MockedRequest(new URL('/user/abc-123', location.href)) - - expect(handler.predicate(request, handler.parse(request))).toBe(false) - }) -}) - -describe('test', () => { - test('returns true given a matching request', () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const firstTest = handler.test( - new MockedRequest(new URL('/user/abc-123', location.href)), - ) - const secondTest = handler.test( - new MockedRequest(new URL('/user/def-456', location.href)), - ) - - expect(firstTest).toBe(true) - expect(secondTest).toBe(true) - }) - - test('returns false given a non-matching request', () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const firstTest = handler.test( - new MockedRequest(new URL('/login', location.href)), - ) - const secondTest = handler.test( - new MockedRequest(new URL('/user/', location.href)), - ) - const thirdTest = handler.test( - new MockedRequest(new URL('/user/abc-123/extra', location.href)), - ) - - expect(firstTest).toBe(false) - expect(secondTest).toBe(false) - expect(thirdTest).toBe(false) - }) -}) - -describe('run', () => { - test('returns a mocked response given a matching request', async () => { - const handler = new RestHandler('GET', '/user/:userId', resolver) - const request = new MockedRequest(new URL('/user/abc-123', location.href)) - const result = await handler.run(request) - - expect(result).toEqual({ - handler, - request: { - ...request, - params: { - userId: 'abc-123', - }, - }, - parsedResult: { - matches: true, - params: { - userId: 'abc-123', - }, - }, - response: await response(context.json({ userId: 'abc-123' })), - }) - }) - - test('returns null given a non-matching request', async () => { - const handler = new RestHandler('POST', '/login', resolver) - const result = await handler.run( - new MockedRequest(new URL('/users', location.href)), - ) - - expect(result).toBeNull() - }) - - test('returns an empty object as "req.params" given request with no URL parameters', async () => { - const handler = new RestHandler('GET', '/users', resolver) - const result = await handler.run( - new MockedRequest(new URL('/users', location.href)), - ) - - expect(result?.request.params).toEqual({}) - }) -}) - -describe('run with generator', () => { - test('Resolver runs until generator completes', async () => { - const handler = new RestHandler('GET', '/users', generatorResolver) - const run = async () => { - const result = await handler.run( - new MockedRequest(new URL('/users', location.href)), - ) - return result?.response?.body - } - - expect(await run()).toBe('pending') - expect(await run()).toBe('pending') - expect(await run()).toBe('pending') - expect(await run()).toBe('pending') - expect(await run()).toBe('pending') - expect(await run()).toBe('complete') - expect(await run()).toBe('complete') - expect(handler.shouldSkip).toBe(false) - }) -}) diff --git a/src/handlers/RestHandler.ts b/src/handlers/RestHandler.ts deleted file mode 100644 index 955e26039..000000000 --- a/src/handlers/RestHandler.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { body, cookie, json, text, xml } from '../context' -import type { SerializedResponse } from '../setupWorker/glossary' -import { ResponseResolutionContext } from '../utils/getResponse' -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 { prepareRequest } from '../utils/logging/prepareRequest' -import { prepareResponse } from '../utils/logging/prepareResponse' -import { - Match, - matchRequestUrl, - Path, - PathParams, -} from '../utils/matching/matchRequestUrl' -import { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromRequest' -import { MockedRequest } from '../utils/request/MockedRequest' -import { cleanUrl, getSearchParams } from '../utils/url/cleanUrl' -import { - DefaultBodyType, - defaultContext, - DefaultContext, - RequestHandler, - RequestHandlerDefaultInfo, - ResponseResolver, -} from './RequestHandler' - -type RestHandlerMethod = string | RegExp - -export interface RestHandlerInfo extends RequestHandlerDefaultInfo { - method: RestHandlerMethod - path: Path -} - -export enum RESTMethods { - HEAD = 'HEAD', - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - PATCH = 'PATCH', - OPTIONS = 'OPTIONS', - DELETE = 'DELETE', -} - -// Declaring a context interface infers -// JSDoc description of the referenced utils. -export type RestContext = DefaultContext & { - cookie: typeof cookie - text: typeof text - body: typeof body - json: typeof json - xml: typeof xml -} - -export const restContext: RestContext = { - ...defaultContext, - cookie, - body, - text, - json, - xml, -} - -export type RequestQuery = { - [queryName: string]: string -} - -export type ParsedRestRequest = Match - -export class RestRequest< - RequestBody extends DefaultBodyType = DefaultBodyType, - RequestParams extends PathParams = PathParams, -> extends MockedRequest { - constructor( - request: MockedRequest, - public readonly params: RequestParams, - ) { - super(request.url, { - ...request, - /** - * @deprecated https://github.com/mswjs/msw/issues/1318 - * @note Use internal request body buffer as the body init - * because "request.body" is a getter that will trigger - * request body parsing at this step. - */ - body: request['_body'], - }) - this.id = request.id - } -} - -/** - * Request handler for REST API requests. - * Provides request matching based on method and URL. - */ -export class RestHandler< - RequestType extends MockedRequest = MockedRequest, -> extends RequestHandler< - RestHandlerInfo, - RequestType, - ParsedRestRequest, - RestRequest< - RequestType extends MockedRequest - ? RequestBodyType - : any, - PathParams - > -> { - constructor( - method: RestHandlerMethod, - path: Path, - resolver: ResponseResolver, - ) { - super({ - info: { - header: `${method} ${path}`, - path, - method, - }, - ctx: restContext, - resolver, - }) - - this.checkRedundantQueryParameters() - } - - private checkRedundantQueryParameters() { - const { method, path } = this.info - - if (path instanceof RegExp) { - return - } - - const url = cleanUrl(path) - - // Bypass request handler URLs that have no redundant characters. - if (url === path) { - return - } - - const searchParams = getSearchParams(path) - const queryParams: string[] = [] - - searchParams.forEach((_, paramName) => { - queryParams.push(paramName) - }) - - 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 in the response resolver function using "req.url.searchParams".`, - ) - } - - parse(request: RequestType, resolutionContext?: ResponseResolutionContext) { - return matchRequestUrl( - request.url, - this.info.path, - resolutionContext?.baseUrl, - ) - } - - protected getPublicRequest( - request: RequestType, - parsedResult: ParsedRestRequest, - ): RestRequest { - return new RestRequest(request, parsedResult.params || {}) - } - - predicate(request: RequestType, parsedResult: ParsedRestRequest) { - const matchesMethod = - this.info.method instanceof RegExp - ? this.info.method.test(request.method) - : isStringEqual(this.info.method, request.method) - - return matchesMethod && parsedResult.matches - } - - log(request: RequestType, response: SerializedResponse) { - const publicUrl = getPublicUrlFromRequest(request) - const loggedRequest = prepareRequest(request) - const loggedResponse = prepareResponse(response) - const statusColor = getStatusCodeColor(response.status) - - console.groupCollapsed( - devUtils.formatMessage('%s %s %s (%c%s%c)'), - getTimestamp(), - request.method, - publicUrl, - `color:${statusColor}`, - `${response.status} ${response.statusText}`, - 'color:inherit', - ) - console.log('Request', loggedRequest) - console.log('Handler:', this) - console.log('Response', loggedResponse) - console.groupEnd() - } -} diff --git a/src/iife/index.ts b/src/iife/index.ts new file mode 100644 index 000000000..189ebc2ec --- /dev/null +++ b/src/iife/index.ts @@ -0,0 +1,2 @@ +export * from '~/core' +export * from '../browser' diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 3bb24a9d3..000000000 --- a/src/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as context from './context' -import { checkGlobals } from './utils/internal/checkGlobals' -export { context } - -export { setupWorker } from './setupWorker/setupWorker' - -export { SetupApi } from './SetupApi' - -export { - response, - defaultResponse, - createResponseComposition, -} from './response' - -/* Request handlers */ -export { RequestHandler, defaultContext } from './handlers/RequestHandler' -export { rest } from './rest' -export { RestHandler, RESTMethods, restContext } from './handlers/RestHandler' -export { graphql } from './graphql' -export { GraphQLHandler, graphqlContext } from './handlers/GraphQLHandler' - -/* Utils */ -export { matchRequestUrl } from './utils/matching/matchRequestUrl' -export { compose } from './utils/internal/compose' -export * from './utils/handleRequest' -export { cleanUrl } from './utils/url/cleanUrl' - -/** - * Type definitions. - */ -export type { SetupWorker, StartOptions } from './setupWorker/glossary' -export { SetupWorkerApi } from './setupWorker/setupWorker' -export type { SharedOptions } from './sharedOptions' - -export * from './utils/request/MockedRequest' -export type { - ResponseResolver, - ResponseResolverReturnType, - AsyncResponseResolverReturnType, - DefaultBodyType, - DefaultRequestMultipartBody, -} from './handlers/RequestHandler' - -export type { - MockedResponse, - ResponseTransformer, - ResponseComposition, - ResponseCompositionOptions, - ResponseFunction, -} from './response' - -export type { - RestRequest, - RestContext, - RequestQuery, - ParsedRestRequest, -} from './handlers/RestHandler' - -export type { - GraphQLContext, - GraphQLVariables, - GraphQLRequest, - GraphQLRequestBody, - GraphQLJsonRequestBody, -} from './handlers/GraphQLHandler' - -export type { Path, PathParams, Match } from './utils/matching/matchRequestUrl' -export type { DelayMode } from './context/delay' -export { ParsedGraphQLRequest } from './utils/internal/parseGraphQLRequest' - -// 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() diff --git a/src/mockServiceWorker.js b/src/mockServiceWorker.js index dabd1abe0..faa67b6c8 100644 --- a/src/mockServiceWorker.js +++ b/src/mockServiceWorker.js @@ -9,6 +9,7 @@ */ const INTEGRITY_CHECKSUM = '' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() self.addEventListener('install', function () { @@ -86,12 +87,6 @@ self.addEventListener('message', async function (event) { self.addEventListener('fetch', function (event) { const { request } = event - const accept = request.headers.get('accept') || '' - - // Bypass server-sent events. - if (accept.includes('text/event-stream')) { - return - } // Bypass navigation requests. if (request.mode === 'navigate') { @@ -112,29 +107,8 @@ self.addEventListener('fetch', function (event) { } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2) - - event.respondWith( - handleRequest(event, requestId).catch((error) => { - if (error.name === 'NetworkError') { - console.warn( - '[MSW] Successfully emulated a network error for the "%s %s" request.', - request.method, - request.url, - ) - return - } - - // At this point, any exception indicates an issue with the original request/response. - console.error( - `\ -[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, - request.method, - request.url, - `${error.name}: ${error.message}`, - ) - }), - ) + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) }) async function handleRequest(event, requestId) { @@ -146,21 +120,29 @@ async function handleRequest(event, requestId) { // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { ;(async function () { - const clonedResponse = response.clone() - sendToClient(client, { - type: 'RESPONSE', - payload: { - requestId, - type: clonedResponse.type, - ok: clonedResponse.ok, - status: clonedResponse.status, - statusText: clonedResponse.statusText, - body: - clonedResponse.body === null ? null : await clonedResponse.text(), - headers: Object.fromEntries(clonedResponse.headers.entries()), - redirected: clonedResponse.redirected, + const responseClone = response.clone() + // When performing original requests, response body will + // always be a ReadableStream, even for 204 responses. + // But when creating a new Response instance on the client, + // the body for a 204 response must be null. + const responseBody = response.status === 204 ? null : responseClone.body + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseBody, + headers: Object.fromEntries(responseClone.headers.entries()), + }, }, - }) + [responseBody], + ) })() } @@ -196,20 +178,20 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId) { const { request } = event - const clonedRequest = request.clone() + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() function passthrough() { - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()) + const headers = Object.fromEntries(requestClone.headers.entries()) - // Remove MSW-specific request headers so the bypassed requests - // comply with the server's CORS preflight check. - // Operate with the headers as an object because request "Headers" - // are immutable. - delete headers['x-msw-bypass'] + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] - return fetch(clonedRequest, { headers }) + return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. @@ -227,31 +209,36 @@ async function getResponse(event, client, requestId) { // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". - if (request.headers.get('x-msw-bypass') === 'true') { + const mswIntention = request.headers.get('x-msw-intention') + if (['bypass', 'passthrough'].includes(mswIntention)) { return passthrough() } // Notify the client that a request has been intercepted. - const clientMessage = await sendToClient(client, { - type: 'REQUEST', - payload: { - id: requestId, - url: request.url, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - mode: request.mode, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: await request.text(), - bodyUsed: request.bodyUsed, - keepalive: request.keepalive, + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, }, - }) + [requestBuffer], + ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { @@ -261,21 +248,12 @@ async function getResponse(event, client, requestId) { case 'MOCK_NOT_FOUND': { return passthrough() } - - case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data - const networkError = new Error(message) - networkError.name = name - - // Rejecting a "respondWith" promise emulates a network error. - throw networkError - } } return passthrough() } -function sendToClient(client, message) { +function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -287,17 +265,28 @@ function sendToClient(client, message) { resolve(event.data) } - client.postMessage(message, [channel.port2]) + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) }) } -function sleep(timeMs) { - return new Promise((resolve) => { - setTimeout(resolve, timeMs) +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, }) -} -async function respondWithMock(response) { - await sleep(response.delay) - return new Response(response.body, response) + return mockedResponse } diff --git a/src/native/index.ts b/src/native/index.ts index c0acc4d32..245c1fb93 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,11 +1,12 @@ -import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { RequestHandler } from '../handlers/RequestHandler' +import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' +import { RequestHandler } from '~/core/handlers/RequestHandler' import { SetupServerApi } from '../node/SetupServerApi' /** * Sets up a requests interception in React Native with the given request handlers. * @param {RequestHandler[]} handlers List of request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} + * + * @see {@link https://mswjs.io/docs/api/setup-server `setupServer()` API reference} */ export function setupServer( ...handlers: Array diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts index 02cf2932d..a451f521b 100644 --- a/src/node/SetupServerApi.ts +++ b/src/node/SetupServerApi.ts @@ -1,36 +1,27 @@ -import chalk from 'chalk' +import { setMaxListeners, defaultMaxListeners } from 'node:events' import { invariant } from 'outvariant' import { BatchInterceptor, HttpRequestEventMap, Interceptor, InterceptorReadyState, - IsomorphicResponse, - MockedResponse as MockedInterceptedResponse, } from '@mswjs/interceptors' -import { SetupApi } from '../SetupApi' -import { RequestHandler } from '../handlers/RequestHandler' -import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' -import { RequiredDeep } from '../typeUtils' -import { mergeRight } from '../utils/internal/mergeRight' -import { MockedRequest } from '../utils/request/MockedRequest' -import { handleRequest } from '../utils/handleRequest' -import { devUtils } from '../utils/internal/devUtils' +import { SetupApi } from '~/core/SetupApi' +import { RequestHandler } from '~/core/handlers/RequestHandler' +import { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions' +import { RequiredDeep } from '~/core/typeUtils' +import { mergeRight } from '~/core/utils/internal/mergeRight' +import { handleRequest } from '~/core/utils/handleRequest' +import { devUtils } from '~/core/utils/internal/devUtils' import { SetupServer } from './glossary' - -/** - * @see https://github.com/mswjs/msw/pull/1399 - */ -const { bold } = chalk - -export type ServerLifecycleEventsMap = LifeCycleEventsMap +import { isNodeException } from './utils/isNodeException' const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { onUnhandledRequest: 'warn', } export class SetupServerApi - extends SetupApi + extends SetupApi implements SetupServer { protected readonly interceptor: BatchInterceptor< @@ -60,59 +51,70 @@ export class SetupServerApi * Subscribe to all requests that are using the interceptor object */ private init(): void { - this.interceptor.on('request', async (request) => { - const mockedRequest = new MockedRequest(request.url, { - ...request, - body: await request.arrayBuffer(), - }) + this.interceptor.on('request', async ({ request, requestId }) => { + /** + * @note React Native doesn't have "node:events". + */ + if (typeof setMaxListeners === 'function') { + // Bump the maximum number of event listeners on the + // request's "AbortSignal". This prepares the request + // for each request handler cloning it at least once. + // Note that cloning a request automatically appends a + // new "abort" event listener to the parent request's + // "AbortController" so if the parent aborts, all the + // clones are automatically aborted. + try { + setMaxListeners( + Math.max(defaultMaxListeners, this.currentHandlers.length), + request.signal, + ) + } catch (error: unknown) { + /** + * @note Mock environments (JSDOM, ...) are not able to implement an internal + * "kIsNodeEventTarget" Symbol that Node.js uses to identify Node.js `EventTarget`s. + * `setMaxListeners` throws an error for non-Node.js `EventTarget`s. + * At the same time, mock environments are also not able to implement the + * internal "events.maxEventTargetListenersWarned" Symbol, which results in + * "MaxListenersExceededWarning" not being printed by Node.js for those anyway. + * The main reason for using `setMaxListeners` is to suppress these warnings in Node.js, + * which won't be printed anyway if `setMaxListeners` fails. + */ + if ( + !(isNodeException(error) && error.code === 'ERR_INVALID_ARG_TYPE') + ) { + throw error + } + } + } - const response = await handleRequest< - MockedInterceptedResponse & { delay?: number } - >( - mockedRequest, + const response = await handleRequest( + request, + requestId, this.currentHandlers, this.resolvedOptions, this.emitter, - { - transformResponse(response) { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } - }, - }, ) if (response) { - // Delay Node.js responses in the listener so that - // the response lookup logic is not concerned with responding - // in any way. The same delay is implemented in the worker. - if (response.delay) { - await new Promise((resolve) => { - setTimeout(resolve, response.delay) - }) - } - request.respondWith(response) } return }) - this.interceptor.on('response', (request, response) => { - if (!request.id) { - return - } - - if (response.headers.get('x-powered-by') === 'msw') { - this.emitter.emit('response:mocked', response, request.id) - } else { - this.emitter.emit('response:bypass', response, request.id) - } - }) + this.interceptor.on( + 'response', + ({ response, isMockedResponse, request, requestId }) => { + this.emitter.emit( + isMockedResponse ? 'response:mocked' : 'response:bypass', + { + response, + request, + requestId, + }, + ) + }, + ) } public listen(options: Partial = {}): void { @@ -124,6 +126,10 @@ export class SetupServerApi // Apply the interceptor when starting the server. this.interceptor.apply() + this.subscriptions.push(() => { + this.interceptor.dispose() + }) + // Assert that the interceptor has been applied successfully. // Also guards us from forgetting to call "interceptor.apply()" // as a part of the "listen" method. @@ -138,25 +144,7 @@ export class SetupServerApi ) } - public printHandlers(): void { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.log(`\ -${bold(`${pragma} ${header}`)} - Declaration: ${callFrame} -`) - }) - } - public close(): void { - super.dispose() - this.interceptor.dispose() + this.dispose() } } diff --git a/src/node/glossary.ts b/src/node/glossary.ts index c8ed0aa85..0edda3ce8 100644 --- a/src/node/glossary.ts +++ b/src/node/glossary.ts @@ -1,68 +1,62 @@ import type { PartialDeep } from 'type-fest' -import type { IsomorphicResponse } from '@mswjs/interceptors' import { - DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo, -} from '../handlers/RequestHandler' +} from '~/core/handlers/RequestHandler' import { LifeCycleEventEmitter, LifeCycleEventsMap, SharedOptions, -} from '../sharedOptions' -import { MockedRequest } from '../utils/request/MockedRequest' - -export type ServerLifecycleEventsMap = LifeCycleEventsMap +} from '~/core/sharedOptions' export interface SetupServer { /** * Starts requests interception based on the previously provided request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server/listen `server.listen()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/listen `server.listen()` API reference} */ listen(options?: PartialDeep): void /** * Stops requests interception by restoring all augmented modules. - * @see {@link https://mswjs.io/docs/api/setup-server/close `server.close()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/close `server.close()` API reference} */ close(): void /** * Prepends given request handlers to the list of existing handlers. - * @see {@link https://mswjs.io/docs/api/setup-server/use `server.use()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/use `server.use()` API reference} */ - use(...handlers: RequestHandler[]): void + use(...handlers: Array): void /** * Marks all request handlers that respond using `res.once()` as unused. - * @see {@link https://mswjs.io/docs/api/setup-server/restore-handlers `server.restore-handlers()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/restore-handlers `server.restore-handlers()` API reference} */ restoreHandlers(): void /** * Resets request handlers to the initial list given to the `setupServer` call, or to the explicit next request handlers list, if given. - * @see {@link https://mswjs.io/docs/api/setup-server/reset-handlers `server.reset-handlers()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/reset-handlers `server.reset-handlers()` API reference} */ - resetHandlers(...nextHandlers: RequestHandler[]): void + resetHandlers(...nextHandlers: Array): void /** * Returns a readonly list of currently active request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.listHandlers()`} + * + * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.listHandlers()` API reference} */ - listHandlers(): ReadonlyArray< - RequestHandler< - RequestHandlerDefaultInfo, - MockedRequest, - any, - MockedRequest - > - > + listHandlers(): ReadonlyArray> /** - * Lists all active request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server/print-handlers `server.print-handlers()`} + * 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} */ - printHandlers(): void - - events: LifeCycleEventEmitter + events: LifeCycleEventEmitter } diff --git a/src/node/index.ts b/src/node/index.ts index 11a0dd223..d9b2ea46c 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -1,4 +1,3 @@ -export { ServerLifecycleEventsMap } from './SetupServerApi' -export { setupServer } from './setupServer' export type { SetupServer } from './glossary' export { SetupServerApi } from './SetupServerApi' +export { setupServer } from './setupServer' diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index c71e40370..06b2546af 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -1,14 +1,15 @@ -import { ClientRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/ClientRequest/index.js' -import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest/index.js' -import { FetchInterceptor } from '@mswjs/interceptors/lib/interceptors/fetch/index.js' -import { RequestHandler } from '../handlers/RequestHandler' +import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest' +import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' +import { FetchInterceptor } from '@mswjs/interceptors/fetch' +import { RequestHandler } from '~/core/handlers/RequestHandler' import { SetupServer } from './glossary' import { SetupServerApi } from './SetupServerApi' /** * Sets up a requests interception in Node.js with the given request handlers. * @param {RequestHandler[]} handlers List of request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} + * + * @see {@link https://mswjs.io/docs/api/setup-server `setupServer()` API reference} */ export const setupServer = ( ...handlers: Array diff --git a/src/node/utils/isNodeException.ts b/src/node/utils/isNodeException.ts new file mode 100644 index 000000000..268e5b8a5 --- /dev/null +++ b/src/node/utils/isNodeException.ts @@ -0,0 +1,10 @@ +/** + * Determines if the given value is a Node.js exception. + * Node.js exceptions have additional information, like + * the `code` and `errno` properties. + */ +export function isNodeException( + error: unknown, +): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error +} diff --git a/src/response.ts b/src/response.ts deleted file mode 100644 index 674ffbd42..000000000 --- a/src/response.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Headers } from 'headers-polyfill' -import { DefaultBodyType } from './handlers/RequestHandler' -import { compose } from './utils/internal/compose' -import { NetworkError } from './utils/NetworkError' - -export type MaybePromise = ValueType | Promise - -/** - * Internal representation of a mocked response instance. - */ -export interface MockedResponse { - body: BodyType - status: number - statusText: string - headers: Headers - once: boolean - passthrough: boolean - delay?: number -} - -export type ResponseTransformer< - BodyType extends TransformerBodyType = any, - TransformerBodyType extends DefaultBodyType = any, -> = ( - res: MockedResponse, -) => MaybePromise> - -export type ResponseFunction = ( - ...transformers: ResponseTransformer[] -) => MaybePromise> - -export type ResponseComposition = - ResponseFunction & { - /** - * Respond using a given mocked response to the first captured request. - * Does not affect any subsequent captured requests. - */ - once: ResponseFunction - networkError: (message: string) => void - } - -export const defaultResponse: Omit = { - status: 200, - statusText: 'OK', - body: null, - delay: 0, - once: false, - passthrough: false, -} - -export type ResponseCompositionOptions = { - defaultTransformers?: ResponseTransformer[] - mockedResponseOverrides?: Partial -} - -export const defaultResponseTransformers: ResponseTransformer[] = [] - -export function createResponseComposition( - responseOverrides?: Partial>, - defaultTransformers: ResponseTransformer[] = defaultResponseTransformers, -): ResponseFunction { - return async (...transformers) => { - const initialResponse: MockedResponse = Object.assign( - {}, - defaultResponse, - { - headers: new Headers({ - 'x-powered-by': 'msw', - }), - }, - responseOverrides, - ) - - const resolvedTransformers = [ - ...defaultTransformers, - ...transformers, - ].filter(Boolean) - - const resolvedResponse = - resolvedTransformers.length > 0 - ? compose(...resolvedTransformers)(initialResponse) - : initialResponse - - return resolvedResponse - } -} - -export const response = Object.assign(createResponseComposition(), { - once: createResponseComposition({ once: true }), - networkError(message: string) { - throw new NetworkError(message) - }, -}) diff --git a/src/rest.ts b/src/rest.ts deleted file mode 100644 index 47cc23076..000000000 --- a/src/rest.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DefaultBodyType, ResponseResolver } from './handlers/RequestHandler' -import { - RESTMethods, - RestContext, - RestHandler, - RestRequest, -} from './handlers/RestHandler' -import { Path, PathParams } from './utils/matching/matchRequestUrl' - -function createRestHandler( - method: Method, -) { - return < - RequestBodyType extends DefaultBodyType = DefaultBodyType, - Params extends PathParams = PathParams, - ResponseBody extends DefaultBodyType = DefaultBodyType, - >( - path: Path, - resolver: ResponseResolver< - RestRequest< - Method extends RESTMethods.HEAD | RESTMethods.GET - ? never - : RequestBodyType, - Params - >, - RestContext, - ResponseBody - >, - ) => { - return new RestHandler(method, path, resolver) - } -} - -export const rest = { - all: createRestHandler(/.+/), - head: createRestHandler(RESTMethods.HEAD), - get: createRestHandler(RESTMethods.GET), - post: createRestHandler(RESTMethods.POST), - put: createRestHandler(RESTMethods.PUT), - delete: createRestHandler(RESTMethods.DELETE), - patch: createRestHandler(RESTMethods.PATCH), - options: createRestHandler(RESTMethods.OPTIONS), -} diff --git a/src/setupWorker/start/createFallbackRequestListener.ts b/src/setupWorker/start/createFallbackRequestListener.ts deleted file mode 100644 index db7eb76eb..000000000 --- a/src/setupWorker/start/createFallbackRequestListener.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - Interceptor, - BatchInterceptor, - HttpRequestEventMap, -} from '@mswjs/interceptors' -import { FetchInterceptor } from '@mswjs/interceptors/lib/interceptors/fetch' -import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { - SerializedResponse, - SetupWorkerInternalContext, - StartOptions, -} from '../glossary' -import type { RequiredDeep } from '../../typeUtils' -import { handleRequest } from '../../utils/handleRequest' -import { MockedRequest } from '../../utils/request/MockedRequest' -import { serializeResponse } from '../../utils/logging/serializeResponse' -import { createResponseFromIsomorphicResponse } from '../../utils/request/createResponseFromIsomorphicResponse' - -export function createFallbackRequestListener( - context: SetupWorkerInternalContext, - options: RequiredDeep, -): Interceptor { - const interceptor = new BatchInterceptor({ - name: 'fallback', - interceptors: [new FetchInterceptor(), new XMLHttpRequestInterceptor()], - }) - - interceptor.on('request', async (request) => { - const mockedRequest = new MockedRequest(request.url, { - ...request, - body: await request.arrayBuffer(), - }) - - const response = await handleRequest( - mockedRequest, - context.requestHandlers, - options, - context.emitter, - { - transformResponse(response) { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } - }, - onMockedResponse(_, { handler, publicRequest, parsedRequest }) { - if (!options.quiet) { - context.emitter.once('response:mocked', async (response) => { - handler.log( - publicRequest, - await serializeResponse(response), - parsedRequest, - ) - }) - } - }, - }, - ) - - if (response) { - request.respondWith(response) - } - }) - - interceptor.on('response', (request, response) => { - if (!request.id) { - return - } - - const browserResponse = createResponseFromIsomorphicResponse(response) - - if (response.headers.get('x-powered-by') === 'msw') { - context.emitter.emit('response:mocked', browserResponse, request.id) - } else { - context.emitter.emit('response:bypass', browserResponse, request.id) - } - }) - - interceptor.apply() - - return interceptor -} diff --git a/src/setupWorker/start/createRequestListener.ts b/src/setupWorker/start/createRequestListener.ts deleted file mode 100644 index 090b13c46..000000000 --- a/src/setupWorker/start/createRequestListener.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - StartOptions, - SerializedResponse, - SetupWorkerInternalContext, - ServiceWorkerIncomingEventsMap, -} from '../glossary' -import { - ServiceWorkerMessage, - WorkerChannel, -} from './utils/createMessageChannel' -import { NetworkError } from '../../utils/NetworkError' -import { parseWorkerRequest } from '../../utils/request/parseWorkerRequest' -import { handleRequest } from '../../utils/handleRequest' -import { RequiredDeep } from '../../typeUtils' -import { MockedResponse } from '../../response' -import { devUtils } from '../../utils/internal/devUtils' -import { serializeResponse } from '../../utils/logging/serializeResponse' - -export const createRequestListener = ( - context: SetupWorkerInternalContext, - options: RequiredDeep, -) => { - return async ( - event: MessageEvent, - message: ServiceWorkerMessage< - 'REQUEST', - ServiceWorkerIncomingEventsMap['REQUEST'] - >, - ) => { - const messageChannel = new WorkerChannel(event.ports[0]) - const request = parseWorkerRequest(message.payload) - - try { - await handleRequest( - request, - context.requestHandlers, - options, - context.emitter, - { - transformResponse, - onPassthroughResponse() { - messageChannel.postMessage('NOT_FOUND') - }, - async onMockedResponse( - response, - { handler, publicRequest, parsedRequest }, - ) { - if (response.body instanceof ReadableStream) { - throw new Error( - devUtils.formatMessage( - 'Failed to construct a mocked response with a "ReadableStream" body: mocked streams are not supported. Follow https://github.com/mswjs/msw/issues/1336 for more details.', - ), - ) - } - - const responseInstance = new Response(response.body, response) - const responseForLogs = responseInstance.clone() - const responseBodyBuffer = await responseInstance.arrayBuffer() - - // If the mocked response has no body, keep it that way. - // Sending an empty "ArrayBuffer" to the worker will cause - // the worker constructing "new Response(new ArrayBuffer(0))" - // which will throw on responses that must have no body (i.e. 204). - const responseBody = - response.body == null ? null : responseBodyBuffer - - messageChannel.postMessage( - 'MOCK_RESPONSE', - { - ...response, - body: responseBody, - }, - [responseBodyBuffer], - ) - - if (!options.quiet) { - context.emitter.once('response:mocked', async () => { - handler.log( - publicRequest, - await serializeResponse(responseForLogs), - parsedRequest, - ) - }) - } - }, - }, - ) - } catch (error) { - if (error instanceof NetworkError) { - // Treat emulated network error differently, - // as it is an intended exception in a request handler. - messageChannel.postMessage('NETWORK_ERROR', { - name: error.name, - message: error.message, - }) - - return - } - - 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/recipes/mocking-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. - messageChannel.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, - }), - }) - } - } - } -} - -function transformResponse( - response: MockedResponse, -): SerializedResponse { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } -} diff --git a/src/setupWorker/start/utils/streamResponse.ts b/src/setupWorker/start/utils/streamResponse.ts deleted file mode 100644 index 8f8b46a5a..000000000 --- a/src/setupWorker/start/utils/streamResponse.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { invariant } from 'outvariant' -import { StrictBroadcastChannel } from '../../../utils/internal/StrictBroadcastChannel' -import { - SerializedResponse, - ServiceWorkerBroadcastChannelMessageMap, -} from '../../glossary' -import { WorkerMessageChannel } from './createMessageChannel' - -export async function streamResponse( - operationChannel: StrictBroadcastChannel, - messageChannel: WorkerMessageChannel, - mockedResponse: SerializedResponse, -): Promise { - const response = new Response(mockedResponse.body, mockedResponse) - - /** - * Delete the ReadableStream response body - * so it doesn't get sent via the message channel. - * @note Otherwise, an error: cannot clone a ReadableStream if - * it hasn't been transformed yet. - */ - delete mockedResponse.body - - // Signal the mock response stream start event on the global - // message channel because the worker expects an event in response - // to the sent "REQUEST" global event. - messageChannel.send({ - type: 'MOCK_RESPONSE_START', - payload: mockedResponse, - }) - - invariant(response.body, 'Failed to stream mocked response with no body') - - // Read the mocked response body as stream - // and pipe it to the worker. - const reader = response.body.getReader() - - while (true) { - const { done, value } = await reader.read() - - if (!done) { - operationChannel.postMessage({ - type: 'MOCK_RESPONSE_CHUNK', - payload: value, - }) - continue - } - - operationChannel.postMessage({ - type: 'MOCK_RESPONSE_END', - }) - operationChannel.close() - reader.releaseLock() - break - } -} diff --git a/src/sharedOptions.ts b/src/sharedOptions.ts deleted file mode 100644 index 2ed9f012c..000000000 --- a/src/sharedOptions.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Emitter } from 'strict-event-emitter' -import { MockedRequest } from './utils/request/MockedRequest' -import { 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 interface LifeCycleEventsMap { - 'request:start': [MockedRequest] - 'request:match': [MockedRequest] - 'request:unhandled': [MockedRequest] - 'request:end': [MockedRequest] - 'response:mocked': [response: ResponseType, requestId: string] - 'response:bypass': [response: ResponseType, requestId: string] - unhandledException: [error: Error, request: MockedRequest] - [key: string]: Array -} - -export type LifeCycleEventEmitter< - ResponseType extends Record, -> = Pick, 'on' | 'removeListener' | 'removeAllListeners'> diff --git a/src/utils/NetworkError.ts b/src/utils/NetworkError.ts deleted file mode 100644 index 50d03995e..000000000 --- a/src/utils/NetworkError.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class NetworkError extends Error { - constructor(message: string) { - super(message) - this.name = 'NetworkError' - } -} diff --git a/src/utils/getResponse.ts b/src/utils/getResponse.ts deleted file mode 100644 index e5c0927b5..000000000 --- a/src/utils/getResponse.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MockedResponse } from '../response' -import { - RequestHandler, - RequestHandlerExecutionResult, -} from '../handlers/RequestHandler' -import { MockedRequest } from './request/MockedRequest' - -export interface ResponseLookupResult { - handler?: RequestHandler - publicRequest?: any - parsedRequest?: any - response?: MockedResponse -} - -export interface ResponseResolutionContext { - baseUrl?: string -} - -/** - * Returns a mocked response for a given request using following request handlers. - */ -export const getResponse = async < - Request extends MockedRequest, - Handler extends RequestHandler[], ->( - request: Request, - handlers: Handler, - resolutionContext?: ResponseResolutionContext, -): Promise => { - const relevantHandlers = handlers.filter((handler) => { - return handler.test(request, resolutionContext) - }) - - if (relevantHandlers.length === 0) { - return { - handler: undefined, - response: undefined, - } - } - - const result = await relevantHandlers.reduce< - Promise | null> - >(async (executionResult, handler) => { - const previousResults = await executionResult - - if (!!previousResults?.response) { - return executionResult - } - - const result = await handler.run(request, resolutionContext) - - if (result === null || result.handler.shouldSkip) { - return null - } - - if (!result.response) { - return { - request: result.request, - handler: result.handler, - response: undefined, - parsedResult: result.parsedResult, - } - } - - if (result.response.once) { - handler.markAsSkipped(true) - } - - return result - }, Promise.resolve(null)) - - // Although reducing a list of relevant request handlers, it's possible - // that in the end there will be no handler associted with the request - // (i.e. if relevant handlers are fall-through). - if (!result) { - return { - handler: undefined, - response: undefined, - } - } - - return { - handler: result.handler, - publicRequest: result.request, - parsedRequest: result.parsedResult, - response: result.response, - } -} diff --git a/src/utils/handleRequest.test.ts b/src/utils/handleRequest.test.ts deleted file mode 100644 index 168c0e8ce..000000000 --- a/src/utils/handleRequest.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { Headers } from 'headers-polyfill' -import { Emitter } from 'strict-event-emitter' -import { ServerLifecycleEventsMap } from '../node/glossary' -import { SharedOptions } from '../sharedOptions' -import { RequestHandler } from '../handlers/RequestHandler' -import { rest } from '../rest' -import { handleRequest, HandleRequestOptions } from './handleRequest' -import { response } from '../response' -import { context, MockedRequest } from '..' -import { RequiredDeep } from '../typeUtils' - -const options: RequiredDeep = { - onUnhandledRequest: jest.fn(), -} -const callbacks: Partial, any>> = { - onPassthroughResponse: jest.fn(), - onMockedResponse: jest.fn(), -} - -function setup() { - const emitter = new Emitter() - const listener = jest.fn() - - const createMockListener = (name: string) => { - return (...args: any) => { - listener(name, ...args) - } - } - - emitter.on('request:start', createMockListener('request:start')) - emitter.on('request:match', createMockListener('request:match')) - emitter.on('request:unhandled', createMockListener('request:unhandled')) - emitter.on('request:end', createMockListener('request:end')) - emitter.on('response:mocked', createMockListener('response:mocked')) - emitter.on('response:bypass', createMockListener('response:bypass')) - - const events = listener.mock.calls - return { emitter, events } -} - -beforeEach(() => { - jest.spyOn(global.console, 'warn').mockImplementation() -}) - -afterEach(() => { - jest.resetAllMocks() -}) - -test('returns undefined for a request with the "x-msw-bypass" header equal to "true"', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user'), { - headers: new Headers({ - 'x-msw-bypass': 'true', - }), - }) - const handlers: Array = [] - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).toBeUndefined() - expect(events).toEqual([ - ['request:start', request], - ['request:end', request], - ]) - expect(options.onUnhandledRequest).not.toHaveBeenCalled() - expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) - expect(callbacks.onMockedResponse).not.toHaveBeenCalled() -}) - -test('does not bypass a request with "x-msw-bypass" header set to arbitrary value', async () => { - const { emitter } = setup() - - const request = new MockedRequest(new URL('http://localhost/user'), { - headers: new Headers({ - 'x-msw-bypass': 'anything', - }), - }) - const handlers: Array = [ - rest.get('/user', (req, res, ctx) => { - return res(ctx.text('hello world')) - }), - ] - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).not.toBeUndefined() - expect(options.onUnhandledRequest).not.toHaveBeenCalled() - expect(callbacks.onMockedResponse).toHaveBeenCalledTimes(1) -}) - -test('reports request as unhandled when it has no matching request handlers', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user')) - const handlers: Array = [] - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).toBeUndefined() - expect(events).toEqual([ - ['request:start', request], - ['request:unhandled', request], - ['request:end', request], - ]) - expect(options.onUnhandledRequest).toHaveBeenNthCalledWith(1, request, { - warning: expect.any(Function), - error: expect.any(Function), - }) - expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) - expect(callbacks.onMockedResponse).not.toHaveBeenCalled() -}) - -test('returns undefined and warns on a request handler that returns no response', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user')) - const handlers: Array = [ - rest.get('/user', () => { - // Intentionally blank response resolver. - return - }), - ] - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).toBeUndefined() - expect(events).toEqual([ - ['request:start', request], - ['request:end', request], - ]) - expect(options.onUnhandledRequest).not.toHaveBeenCalled() - expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) - expect(callbacks.onMockedResponse).not.toHaveBeenCalled() - - expect(console.warn).toHaveBeenCalledTimes(1) - const warning = (console.warn as unknown as jest.SpyInstance).mock.calls[0][0] - - expect(warning).toContain( - '[MSW] Expected response resolver to return a mocked response Object, but got undefined. The original response is going to be used instead.', - ) - expect(warning).toContain('GET /user') - expect(warning).toMatch(/\d+:\d+/) -}) - -test('returns the mocked response for a request with a matching request handler', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user')) - const mockedResponse = await response(context.json({ firstName: 'John' })) - const handlers: Array = [ - rest.get('/user', () => { - return mockedResponse - }), - ] - const lookupResult = { - handler: handlers[0], - response: mockedResponse, - publicRequest: { ...request, params: {} }, - parsedRequest: { matches: true, params: {} }, - } - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).toEqual(mockedResponse) - expect(events).toEqual([ - ['request:start', request], - ['request:match', request], - ['request:end', request], - ]) - expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled() - expect(callbacks.onMockedResponse).toHaveBeenNthCalledWith( - 1, - mockedResponse, - lookupResult, - ) -}) - -test('returns a transformed response if the "transformResponse" option is provided', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user')) - const mockedResponse = await response(context.json({ firstName: 'John' })) - const handlers: Array = [ - rest.get('/user', () => { - return mockedResponse - }), - ] - const transformResponse = jest.fn().mockImplementation((response) => ({ - body: response.body, - })) - const finalResponse = transformResponse(mockedResponse) - const lookupResult = { - handler: handlers[0], - response: mockedResponse, - publicRequest: { ...request, params: {} }, - parsedRequest: { matches: true, params: {} }, - } - - const result = await handleRequest(request, handlers, options, emitter, { - ...callbacks, - transformResponse, - }) - - expect(result).toEqual(finalResponse) - expect(events).toEqual([ - ['request:start', request], - ['request:match', request], - ['request:end', request], - ]) - expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled() - expect(transformResponse).toHaveBeenNthCalledWith(1, mockedResponse) - expect(callbacks.onMockedResponse).toHaveBeenNthCalledWith( - 1, - finalResponse, - lookupResult, - ) -}) - -it('returns undefined without warning on a passthrough request', async () => { - const { emitter, events } = setup() - - const request = new MockedRequest(new URL('http://localhost/user')) - const handlers: Array = [ - rest.get('/user', (req) => { - return req.passthrough() - }), - ] - - const result = await handleRequest( - request, - handlers, - options, - emitter, - callbacks, - ) - - expect(result).toBeUndefined() - expect(events).toEqual([ - ['request:start', request], - ['request:end', request], - ]) - expect(options.onUnhandledRequest).not.toHaveBeenCalled() - expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request) - expect(callbacks.onMockedResponse).not.toHaveBeenCalled() -}) diff --git a/src/utils/internal/StrictBroadcastChannel.ts b/src/utils/internal/StrictBroadcastChannel.ts deleted file mode 100644 index 15b7237e4..000000000 --- a/src/utils/internal/StrictBroadcastChannel.ts +++ /dev/null @@ -1,27 +0,0 @@ -const ParentClass = - typeof BroadcastChannel == 'undefined' - ? class UnsupportedEnvironment { - constructor() { - throw new Error( - 'Cannot construct BroadcastChannel in a non-browser environment', - ) - } - } - : BroadcastChannel - -export class StrictBroadcastChannel< - MessageMap extends Record, -> extends (ParentClass as unknown as { new (name: string): BroadcastChannel }) { - public postMessage( - message: Parameters[0] extends undefined - ? { - type: MessageType - } - : { - type: MessageType - payload: Parameters[0] - }, - ): void { - return super.postMessage(message) - } -} diff --git a/src/utils/internal/compose.test.ts b/src/utils/internal/compose.test.ts deleted file mode 100644 index 024ad7232..000000000 --- a/src/utils/internal/compose.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { compose } from './compose' - -test('composes given functions from right to left', () => { - const list: number[] = [] - const populateList = compose( - () => list.push(1), - () => list.push(7), - () => list.push(5), - ) - - populateList() - - expect(list).toEqual([5, 7, 1]) -}) - -test('composes a list of async functions from right to left', async () => { - const generateNumber = compose( - async (n: number) => n + 1, - async (n: number) => n * 5, - ) - const number = await generateNumber(5) - - expect(number).toEqual(26) -}) diff --git a/src/utils/internal/compose.ts b/src/utils/internal/compose.ts deleted file mode 100644 index a13ca1bf9..000000000 --- a/src/utils/internal/compose.ts +++ /dev/null @@ -1,46 +0,0 @@ -type ArityOneFunction = (arg: any) => any - -type LengthOfTuple = Tuple extends { length: infer L } - ? L - : never - -type DropFirstInTuple = ((...args: Tuple) => any) extends ( - arg: any, - ...rest: infer LastArg -) => any - ? LastArg - : Tuple - -type LastInTuple = Tuple[LengthOfTuple< - DropFirstInTuple ->] - -type FirstFnParameterType = Parameters< - LastInTuple ->[any] - -type LastFnParameterType = ReturnType< - Functions[0] -> - -/** - * Composes a given list of functions into a new function that - * executes from right to left. - */ -export function compose< - Functions extends ArityOneFunction[], - LeftReturnType extends FirstFnParameterType, - RightReturnType extends LastFnParameterType, ->( - ...fns: Functions -): ( - ...args: [LeftReturnType] extends [never] ? never[] : [LeftReturnType] -) => RightReturnType { - return (...args) => { - return fns.reduceRight((leftFn: any, rightFn) => { - return leftFn instanceof Promise - ? Promise.resolve(leftFn).then(rightFn) - : rightFn(leftFn) - }, args[0]) - } -} diff --git a/src/utils/internal/parseGraphQLRequest.test.ts b/src/utils/internal/parseGraphQLRequest.test.ts deleted file mode 100644 index 58e312e60..000000000 --- a/src/utils/internal/parseGraphQLRequest.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { encodeBuffer } from '@mswjs/interceptors' -import { Headers } from 'headers-polyfill' -import { MockedRequest } from '../request/MockedRequest' -import { parseGraphQLRequest } from './parseGraphQLRequest' - -test('returns true given a GraphQL-compatible request', () => { - const getRequest = new MockedRequest( - new URL( - 'http://localhost:8080/graphql?query=mutation Login { user { id } }', - ), - ) - expect(parseGraphQLRequest(getRequest)).toEqual({ - operationType: 'mutation', - operationName: 'Login', - }) - - const postRequest = new MockedRequest( - new URL('http://localhost:8080/graphql'), - { - method: 'POST', - headers: new Headers({ 'Content-Type': 'application/json' }), - body: encodeBuffer( - JSON.stringify({ - query: `query GetUser { user { firstName } }`, - }), - ), - }, - ) - - expect(parseGraphQLRequest(postRequest)).toEqual({ - operationType: 'query', - operationName: 'GetUser', - }) -}) - -test('throws an exception given an invalid GraphQL request', () => { - const getRequest = new MockedRequest( - new URL('http://localhost:8080/graphql?query=mutation Login() { user { {}'), - ) - expect(() => parseGraphQLRequest(getRequest)).toThrowError( - '[MSW] Failed to intercept a GraphQL request to "GET http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.', - ) - - const postRequest = new MockedRequest( - new URL('http://localhost:8080/graphql'), - { - method: 'POST', - headers: new Headers({ 'Content-Type': 'application/json' }), - body: encodeBuffer( - JSON.stringify({ - query: `query GetUser() { user {{}`, - }), - ), - }, - ) - expect(() => parseGraphQLRequest(postRequest)).toThrowError( - '[MSW] Failed to intercept a GraphQL request to "POST http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.\n\nSyntax Error: Expected "$", found ")".', - ) -}) - -test('returns false given a GraphQL-incompatible request', () => { - const getRequest = new MockedRequest( - new URL('http://localhost:8080/graphql'), - { - headers: new Headers({ 'Content-Type': 'application/json' }), - }, - ) - expect(parseGraphQLRequest(getRequest)).toBeUndefined() - - const postRequest = new MockedRequest( - new URL('http://localhost:8080/graphql'), - { - method: 'POST', - headers: new Headers({ 'Content-Type': 'application/json' }), - body: encodeBuffer( - JSON.stringify({ - queryUser: true, - }), - ), - }, - ) - expect(parseGraphQLRequest(postRequest)).toBeUndefined() -}) diff --git a/src/utils/internal/uuidv4.ts b/src/utils/internal/uuidv4.ts deleted file mode 100644 index 8de04232e..000000000 --- a/src/utils/internal/uuidv4.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0 - const v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) -} diff --git a/src/utils/logging/prepareRequest.test.ts b/src/utils/logging/prepareRequest.test.ts deleted file mode 100644 index 1c858ebfc..000000000 --- a/src/utils/logging/prepareRequest.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { encodeBuffer } from '@mswjs/interceptors' -import { Headers } from 'headers-polyfill' -import { MockedRequest } from '../request/MockedRequest' -import { prepareRequest } from './prepareRequest' - -test('converts request headers into an object', () => { - const request = prepareRequest( - new MockedRequest(new URL('http://test.mswjs.io/user'), { - headers: new Headers({ - 'Content-Type': 'text/plain', - 'X-Header': 'secret', - }), - body: encodeBuffer('text-body'), - }), - ) - - expect(request).toEqual( - expect.objectContaining({ - url: new URL('http://test.mswjs.io/user'), - // Converts `Headers` instance into a plain object. - headers: { - 'content-type': 'text/plain', - 'x-header': 'secret', - }, - body: 'text-body', - }), - ) -}) diff --git a/src/utils/logging/prepareRequest.ts b/src/utils/logging/prepareRequest.ts deleted file mode 100644 index b5148f279..000000000 --- a/src/utils/logging/prepareRequest.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { DefaultBodyType } from '../../handlers/RequestHandler.js' -import type { MockedRequest } from '../request/MockedRequest.js' - -export interface LoggedRequest { - id: string - url: URL - method: string - headers: Record - cookies: Record - body: DefaultBodyType -} - -/** - * Formats a mocked request for introspection in browser's console. - */ -export function prepareRequest(request: MockedRequest): LoggedRequest { - return { - ...request, - body: request.body, - headers: request.headers.all(), - } -} diff --git a/src/utils/logging/prepareResponse.test.ts b/src/utils/logging/prepareResponse.test.ts deleted file mode 100644 index c00821873..000000000 --- a/src/utils/logging/prepareResponse.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { prepareResponse } from './prepareResponse' - -test('parses a JSON response body given a "Content-Type:*/json" header', async () => { - const res = await prepareResponse({ - status: 200, - statusText: 'OK', - headers: { - 'Content-Type': 'application/json', - }, - body: `{"property":2}`, - }) - - // Preserves all the properties - expect(res.status).toEqual(200) - expect(res.statusText).toEqual('OK') - expect(res.headers).toEqual({ 'Content-Type': 'application/json' }) - - // Parses a JSON response body - expect(res.body).toEqual({ property: 2 }) -}) - -test('returns a stringified valid JSON body given a non-JSON "Content-Type" header', () => { - const res = prepareResponse({ - status: 200, - statusText: 'OK', - headers: {}, - body: `{"property":2}`, - }) - - expect(res.status).toEqual(200) - expect(res.statusText).toEqual('OK') - expect(res.headers).toEqual({}) - - // Returns a non-JSON response body as-is - expect(res.body).toEqual(`{"property":2}`) -}) - -test('returns a non-JSON response body as-is', () => { - const res = prepareResponse({ - status: 200, - statusText: 'OK', - headers: {}, - body: `text-body`, - }) - - expect(res.status).toEqual(200) - expect(res.statusText).toEqual('OK') - expect(res.headers).toEqual({}) - - // Returns a non-JSON response body as-is - expect(res.body).toEqual('text-body') -}) diff --git a/src/utils/logging/prepareResponse.ts b/src/utils/logging/prepareResponse.ts deleted file mode 100644 index 1fb206094..000000000 --- a/src/utils/logging/prepareResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { objectToHeaders } from 'headers-polyfill' -import { SerializedResponse } from '../../setupWorker/glossary' -import { parseBody } from '../request/parseBody' - -/** - * Formats a mocked response for introspection in the browser's console. - */ -export function prepareResponse(res: SerializedResponse) { - const responseHeaders = objectToHeaders(res.headers) - - // Parse a response JSON body for preview in the logs - const parsedBody = parseBody(res.body, responseHeaders) - - return { - ...res, - body: parsedBody, - } -} diff --git a/src/utils/logging/serializeResponse.ts b/src/utils/logging/serializeResponse.ts deleted file mode 100644 index c46303d75..000000000 --- a/src/utils/logging/serializeResponse.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { flattenHeadersObject, headersToObject } from 'headers-polyfill' -import type { SerializedResponse } from '../../setupWorker/glossary' - -export async function serializeResponse( - response: Response, -): Promise> { - return { - status: response.status, - statusText: response.statusText, - headers: flattenHeadersObject(headersToObject(response.headers)), - // Serialize the response body to a string - // so it's easier to process further down the chain in "prepareResponse" (browser-only) - // and "parseBody" (ambiguous). - body: await response.clone().text(), - } -} diff --git a/src/utils/request/MockedRequest.test.ts b/src/utils/request/MockedRequest.test.ts deleted file mode 100644 index d71294561..000000000 --- a/src/utils/request/MockedRequest.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { Headers } from 'headers-polyfill' -import { clearCookies } from '../../../test/support/utils' -import { MockedRequest } from './MockedRequest' - -const url = new URL('/resource', location.href) - -describe('cache', () => { - it('sets "default" as the default request cache', () => { - const request = new MockedRequest(url) - expect(request.cache).toBe('default') - }) - - it('respects custom request init cache', () => { - const request = new MockedRequest(url, { cache: 'no-cache' }) - expect(request.cache).toBe('no-cache') - }) -}) - -describe('credentials', () => { - it('sets "same-origin" as the default request credentials', () => { - const request = new MockedRequest(url) - expect(request.credentials).toBe('same-origin') - }) - - it('respects custom request init credentials', () => { - const request = new MockedRequest(url, { credentials: 'include' }) - expect(request.credentials).toBe('include') - }) -}) - -describe('destination', () => { - it('sets empty string as the default request destination', () => { - const request = new MockedRequest(url) - expect(request.destination).toBe('') - }) - - it('respects custom request init destination', () => { - const request = new MockedRequest(url, { destination: 'image' }) - expect(request.destination).toBe('image') - }) -}) - -describe('integrity', () => { - it('sets empty string as the default request integrity', () => { - const request = new MockedRequest(url) - expect(request.integrity).toBe('') - }) - - it('respects custom request init integrity', () => { - const request = new MockedRequest(url, { integrity: 'sha256-...' }) - expect(request.integrity).toBe('sha256-...') - }) -}) - -describe('keepalive', () => { - it('sets false as the default request keepalive', () => { - const request = new MockedRequest(url) - expect(request.keepalive).toBe(false) - }) - - it('respects custom request init keepalive', () => { - const request = new MockedRequest(url, { keepalive: true }) - expect(request.keepalive).toBe(true) - }) -}) - -describe('mode', () => { - it('sets "cors" as the default request mode', () => { - const request = new MockedRequest(url) - expect(request.mode).toBe('cors') - }) - - it('respects custom request init mode', () => { - const request = new MockedRequest(url, { mode: 'no-cors' }) - expect(request.mode).toBe('no-cors') - }) -}) - -describe('method', () => { - it('sets "GET" as the default request method', () => { - const request = new MockedRequest(url) - expect(request.method).toBe('GET') - }) - - it('respects custom request init method', () => { - const request = new MockedRequest(url, { method: 'POST' }) - expect(request.method).toBe('POST') - }) -}) - -describe('priority', () => { - it('sets "auto" as the default request priority', () => { - const request = new MockedRequest(url) - expect(request.priority).toBe('auto') - }) - - it('respects custom request init priority', () => { - const request = new MockedRequest(url, { priority: 'high' }) - expect(request.priority).toBe('high') - }) -}) - -describe('redirect', () => { - it('sets "follow" as the default request redirect', () => { - const request = new MockedRequest(url) - expect(request.redirect).toBe('follow') - }) - - it('respects custom request init redirect', () => { - const request = new MockedRequest(url, { redirect: 'error' }) - expect(request.redirect).toBe('error') - }) -}) - -describe('referrer', () => { - it('sets empty string as the default request referrer', () => { - const request = new MockedRequest(url) - expect(request.referrer).toBe('') - }) - - it('respects custom request init referrer', () => { - const request = new MockedRequest(url, { referrer: 'https://example.com' }) - expect(request.referrer).toBe('https://example.com') - }) -}) - -describe('referrerPolicy', () => { - it('sets "no-referrer" as the default request referrerPolicy', () => { - const request = new MockedRequest(url) - expect(request.referrerPolicy).toBe('no-referrer') - }) - - it('respects custom request init referrerPolicy', () => { - const request = new MockedRequest(url, { referrerPolicy: 'origin' }) - expect(request.referrerPolicy).toBe('origin') - }) -}) - -describe('cookies', () => { - beforeAll(() => { - clearCookies() - }) - - afterEach(() => { - clearCookies() - }) - - it('preserves request cookies when there are no document cookies to infer', () => { - const request = new MockedRequest(url, { - headers: new Headers({ Cookie: 'token=abc-123' }), - }) - - expect(request.cookies).toEqual({ token: 'abc-123' }) - expect(request.headers.get('cookie')).toBe('token=abc-123') - }) - - it('infers document cookies for request with "same-origin" credentials', () => { - document.cookie = 'documentCookie=yes' - - const request = new MockedRequest(url, { - headers: new Headers({ Cookie: 'token=abc-123' }), - credentials: 'same-origin', - }) - - expect(request.headers.get('cookie')).toEqual( - 'token=abc-123, documentCookie=yes', - ) - expect(request.cookies).toEqual({ - // Cookies present in the document must be forwarded. - documentCookie: 'yes', - token: 'abc-123', - }) - }) - - it('does not infer document cookies for request with "same-origin" credentials made to extraneous origin', () => { - document.cookie = 'documentCookie=yes' - - const request = new MockedRequest(new URL('https://example.com'), { - headers: new Headers({ Cookie: 'token=abc-123' }), - credentials: 'same-origin', - }) - - expect(request.headers.get('cookie')).toBe('token=abc-123') - expect(request.cookies).toEqual({ token: 'abc-123' }) - }) - - it('infers document cookies for request with "include" credentials', () => { - document.cookie = 'documentCookie=yes' - - const request = new MockedRequest(url, { - headers: new Headers({ Cookie: 'token=abc-123' }), - credentials: 'include', - }) - - expect(request.headers.get('cookie')).toBe( - 'token=abc-123, documentCookie=yes', - ) - expect(request.cookies).toEqual({ - // Cookies present in the document must be forwarded. - documentCookie: 'yes', - token: 'abc-123', - }) - }) - - it('infers document cookies for request with "include" credentials made to extraneous origin', () => { - document.cookie = 'documentCookie=yes' - - const request = new MockedRequest(new URL('https://example.com'), { - headers: new Headers({ Cookie: 'token=abc-123' }), - credentials: 'include', - }) - - expect(request.headers.get('cookie')).toBe( - 'token=abc-123, documentCookie=yes', - ) - expect(request.cookies).toEqual({ - // Document cookies are always included. - documentCookie: 'yes', - token: 'abc-123', - }) - }) - - it('does not infer document cookies for request with "omit" credentials', () => { - document.cookie = 'documentCookie=yes' - - const request = new MockedRequest(url, { - headers: new Headers({ Cookie: 'token=abc-123' }), - credentials: 'omit', - }) - - expect(request.headers.get('cookie')).toBe('token=abc-123') - expect(request.cookies).toEqual({ token: 'abc-123' }) - }) -}) diff --git a/src/utils/request/MockedRequest.ts b/src/utils/request/MockedRequest.ts deleted file mode 100644 index 68d3adf91..000000000 --- a/src/utils/request/MockedRequest.ts +++ /dev/null @@ -1,180 +0,0 @@ -import * as cookieUtils from 'cookie' -import { store } from '@mswjs/cookies' -import { IsomorphicRequest, RequestInit } from '@mswjs/interceptors' -import { decodeBuffer } from '@mswjs/interceptors/lib/utils/bufferUtils.js' -import { Headers } from 'headers-polyfill' -import { DefaultBodyType } from '../../handlers/RequestHandler' -import { MockedResponse } from '../../response' -import { getRequestCookies } from './getRequestCookies' -import { parseBody } from './parseBody' -import { isStringEqual } from '../internal/isStringEqual' - -export type RequestCache = - | 'default' - | 'no-store' - | 'reload' - | 'no-cache' - | 'force-cache' - | 'only-if-cached' - -export type RequestMode = 'navigate' | 'same-origin' | 'no-cors' | 'cors' - -export type RequestRedirect = 'follow' | 'error' | 'manual' - -export type RequestDestination = - | '' - | 'audio' - | 'audioworklet' - | 'document' - | 'embed' - | 'font' - | 'frame' - | 'iframe' - | 'image' - | 'manifest' - | 'object' - | 'paintworklet' - | 'report' - | 'script' - | 'sharedworker' - | 'style' - | 'track' - | 'video' - | 'xslt' - | 'worker' - -export type RequestPriority = 'high' | 'low' | 'auto' - -export type RequestReferrerPolicy = - | '' - | 'no-referrer' - | 'no-referrer-when-downgrade' - | 'origin' - | 'origin-when-cross-origin' - | 'same-origin' - | 'strict-origin' - | 'strict-origin-when-cross-origin' - | 'unsafe-url' - -export interface MockedRequestInit extends RequestInit { - id?: string - cache?: RequestCache - redirect?: RequestRedirect - integrity?: string - keepalive?: boolean - mode?: RequestMode - priority?: RequestPriority - destination?: RequestDestination - referrer?: string - referrerPolicy?: RequestReferrerPolicy - cookies?: Record -} - -export class MockedRequest< - RequestBody extends DefaultBodyType = DefaultBodyType, -> extends IsomorphicRequest { - public readonly cache: RequestCache - public readonly cookies: Record - public readonly destination: RequestDestination - public readonly integrity: string - public readonly keepalive: boolean - public readonly mode: RequestMode - public readonly priority: RequestPriority - public readonly redirect: RequestRedirect - public readonly referrer: string - public readonly referrerPolicy: RequestReferrerPolicy - - constructor(url: URL, init: MockedRequestInit = {}) { - super(url, init) - if (init.id) { - this.id = init.id - } - this.cache = init.cache || 'default' - this.destination = init.destination || '' - this.integrity = init.integrity || '' - this.keepalive = init.keepalive || false - this.mode = init.mode || 'cors' - this.priority = init.priority || 'auto' - this.redirect = init.redirect || 'follow' - this.referrer = init.referrer || '' - this.referrerPolicy = init.referrerPolicy || 'no-referrer' - this.cookies = init.cookies || this.getCookies() - } - - /** - * Get parsed request body. The type is inferred from the content type. - * - * @deprecated - Use `req.text()`, `req.json()` or `req.arrayBuffer()` - * to read the request body as a plain text, JSON, or ArrayBuffer. - */ - public get body(): RequestBody { - const text = decodeBuffer(this['_body']) - - /** - * @deprecated https://github.com/mswjs/msw/issues/1318 - * @fixme Remove this assumption and let the users read - * request body explicitly using ".json()"/".text()"/".arrayBuffer()". - */ - // Parse the request's body based on the "Content-Type" header. - const body = parseBody(text, this.headers) - - if (isStringEqual(this.method, 'GET') && body === '') { - return undefined as RequestBody - } - - return body as RequestBody - } - - /** - * Bypass the intercepted request. - * This will make a call to the actual endpoint requested. - */ - public passthrough(): MockedResponse { - return { - // Constructing a dummy "101 Continue" mocked response - // to keep the return type of the resolver consistent. - status: 101, - statusText: 'Continue', - headers: new Headers(), - body: null, - // Setting "passthrough" to true will signal the response pipeline - // to perform this intercepted request as-is. - passthrough: true, - once: false, - } - } - - private getCookies(): Record { - // Parse the cookies passed in the original request "cookie" header. - const requestCookiesString = this.headers.get('cookie') - const ownCookies = requestCookiesString - ? cookieUtils.parse(requestCookiesString) - : {} - - store.hydrate() - - const cookiesFromStore = Array.from( - store.get({ ...this, url: this.url.href })?.entries(), - ).reduce((cookies, [name, { value }]) => { - return Object.assign(cookies, { [name.trim()]: value }) - }, {}) - - // Get existing document cookies that are applicable - // to this request based on its "credentials" policy. - const cookiesFromDocument = getRequestCookies(this) - - const forwardedCookies = { - ...cookiesFromDocument, - ...cookiesFromStore, - } - - for (const [name, value] of Object.entries(forwardedCookies)) { - this.headers.append('cookie', `${name}=${value}`) - } - - return { - ...forwardedCookies, - ...ownCookies, - } - } -} diff --git a/src/utils/request/createResponseFromIsomorphicResponse.ts b/src/utils/request/createResponseFromIsomorphicResponse.ts deleted file mode 100644 index 759d5cd26..000000000 --- a/src/utils/request/createResponseFromIsomorphicResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsomorphicResponse } from '@mswjs/interceptors' - -export function createResponseFromIsomorphicResponse( - response: IsomorphicResponse, -): Response { - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }) -} diff --git a/src/utils/request/getPublicUrlFromRequest.test.ts b/src/utils/request/getPublicUrlFromRequest.test.ts deleted file mode 100644 index 16fdbb3a7..000000000 --- a/src/utils/request/getPublicUrlFromRequest.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { getPublicUrlFromRequest } from './getPublicUrlFromRequest' -import { MockedRequest } from './MockedRequest' - -test('returns an absolute URL string given its origin differs from the referrer', () => { - const request = new MockedRequest(new URL('https://test.mswjs.io/path'), { - referrer: 'http://localhost', - }) - expect(getPublicUrlFromRequest(request)).toBe('https://test.mswjs.io/path') -}) - -test('returns a relative URL string given its origin matches the referrer', () => { - const request = new MockedRequest(new URL('http://localhost/path'), { - referrer: 'http://localhost', - }) - expect(getPublicUrlFromRequest(request)).toBe('/path') -}) diff --git a/src/utils/request/getPublicUrlFromRequest.ts b/src/utils/request/getPublicUrlFromRequest.ts deleted file mode 100644 index f540a2143..000000000 --- a/src/utils/request/getPublicUrlFromRequest.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MockedRequest } from './MockedRequest' - -/** - * Returns a relative URL if the given request URL is relative to the current origin. - * Otherwise returns an absolute URL. - */ -export const getPublicUrlFromRequest = (request: MockedRequest) => { - return request.referrer.startsWith(request.url.origin) - ? request.url.pathname - : new URL( - request.url.pathname, - `${request.url.protocol}//${request.url.host}`, - ).href -} diff --git a/src/utils/request/getRequestCookies.ts b/src/utils/request/getRequestCookies.ts deleted file mode 100644 index ef85b1a70..000000000 --- a/src/utils/request/getRequestCookies.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as cookieUtils from 'cookie' -import { MockedRequest } from './MockedRequest' - -function getAllCookies() { - return cookieUtils.parse(document.cookie) -} - -/** - * Returns relevant document cookies based on the request `credentials` option. - */ -export function getRequestCookies(request: MockedRequest) { - /** - * @note No cookies persist on the document in Node.js: no document. - */ - if (typeof document === 'undefined' || typeof location === 'undefined') { - return {} - } - - switch (request.credentials) { - case 'same-origin': { - // Return document cookies only when requested a resource - // from the same origin as the current document. - return location.origin === request.url.origin ? getAllCookies() : {} - } - - case 'include': { - // Return all document cookies. - return getAllCookies() - } - - default: { - return {} - } - } -} diff --git a/src/utils/request/parseBody.test.ts b/src/utils/request/parseBody.test.ts deleted file mode 100644 index dd204baab..000000000 --- a/src/utils/request/parseBody.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { parseBody } from './parseBody' - -test('parses a body if the "Content-Type:application/json" header is set', () => { - expect( - parseBody( - `{"property":2}`, - new Headers({ 'Content-Type': 'application/json' }), - ), - ).toEqual({ - property: 2, - }) -}) - -test('parses a body for headers with letter cases', () => { - expect( - parseBody( - `{"property":2}`, - new Headers({ 'Content-Type': 'Application/JSON' }), - ), - ).toEqual({ - property: 2, - }) -}) - -test('parses a body if the "Content-Type*/json" header is set', () => { - expect( - parseBody( - `{"property":2}`, - new Headers({ 'Content-Type': 'application/hal+json' }), - ), - ).toEqual({ - property: 2, - }) -}) - -test('parses a body if the "Content-Type:application/json; charset=UTF-8" header is set', () => { - expect( - parseBody( - `{"property":2}`, - new Headers({ 'Content-Type': 'application/json; charset=UTF-8' }), - ), - ).toEqual({ - property: 2, - }) -}) - -test('returns an invalid JSON body as-is even if the "Content-Type:*/json" header is set', () => { - expect( - parseBody('text-body', new Headers({ 'Content-Type': 'application/json' })), - ).toBe('text-body') -}) - -test('parses a body if the "Content-Type: multipart/form-data" header is set', () => { - const body = `\ -------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r -Content-Disposition: form-data; name="file"; filename="file1.txt"\r -Content-Type: application/octet-stream\r -\r -file content\r -------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r -Content-Disposition: form-data; name="text"\r -\r -text content\r -------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r -Content-Disposition: form-data; name="text2"\r -\r -another text content\r -------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r -Content-Disposition: form-data; name="text2"\r -\r -another text content 2\r -------WebKitFormBoundaryvZ1cVXWyK0ilQdab--` - const headers = new Headers({ - 'content-type': - 'multipart/form-data; boundary=--WebKitFormBoundaryvZ1cVXWyK0ilQdab', - }) - const parsed = parseBody(body, headers) - - // Workaround: JSDOM does not have `Blob.text` implementation. - // see https://github.com/jsdom/jsdom/issues/2555 - expect(parsed).toHaveProperty('file.name', 'file1.txt') - - expect(parsed).toHaveProperty('text', 'text content') - expect(parsed).toHaveProperty('text2', [ - 'another text content', - 'another text content 2', - ]) -}) - -test('returns an invalid Multipart body as-is even if the "Content-Type: multipart/form-data" header is set', () => { - const headers = new Headers({ - 'content-type': - 'multipart/form-data; boundary=------WebKitFormBoundaryvZ1cVXWyK0ilQdab', - }) - expect(parseBody('text-body', headers)).toEqual('text-body') -}) - -test('parses a single stringified number as a valid "application/json" body', () => { - const headers = new Headers({ 'Content-Type': 'application/json' }) - expect(parseBody('1', headers)).toEqual(1) -}) - -test('preserves a single stringified number in a "multipart/form-data" body', () => { - const headers = new Headers({ 'Content-Type': 'multipart/form-data;' }) - expect(parseBody('1', headers)).toEqual('1') -}) - -test('returns a falsy body as-is', () => { - expect(parseBody('')).toEqual('') - expect(parseBody(undefined)).toBeUndefined() -}) diff --git a/src/utils/request/parseBody.ts b/src/utils/request/parseBody.ts deleted file mode 100644 index 221601153..000000000 --- a/src/utils/request/parseBody.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { jsonParse } from '../internal/jsonParse' -import { parseMultipartData } from '../internal/parseMultipartData' -import { MockedRequest } from './MockedRequest' - -/** - * Parses a given request/response body based on the "Content-Type" header. - */ -export function parseBody(body?: MockedRequest['body'], headers?: Headers) { - // Return whatever falsey body value is given. - if (!body) { - return body - } - - const contentType = headers?.get('content-type')?.toLowerCase() || '' - - // If the body has a Multipart Content-Type - // parse it into an object. - const hasMultipartContent = contentType.startsWith('multipart/form-data') - if (hasMultipartContent && typeof body !== 'object') { - return parseMultipartData(body.toString(), headers) || body - } - - // If the intercepted request's body has a JSON Content-Type - // parse it into an object. - const hasJsonContent = contentType.includes('json') - - if (hasJsonContent && typeof body !== 'object') { - return jsonParse(body.toString()) || body - } - - // Otherwise leave as-is. - return body -} diff --git a/src/utils/request/parseWorkerRequest.ts b/src/utils/request/parseWorkerRequest.ts deleted file mode 100644 index cc8b45fd7..000000000 --- a/src/utils/request/parseWorkerRequest.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { encodeBuffer } from '@mswjs/interceptors' -import { Headers } from 'headers-polyfill' -import { ServiceWorkerIncomingRequest } from '../../setupWorker/glossary' -import { MockedRequest } from './MockedRequest' - -/** - * Converts a given request received from the Service Worker - * into a `MockedRequest` instance. - */ -export function parseWorkerRequest( - rawRequest: ServiceWorkerIncomingRequest, -): MockedRequest { - const url = new URL(rawRequest.url) - const headers = new Headers(rawRequest.headers) - - return new MockedRequest(url, { - ...rawRequest, - body: encodeBuffer(rawRequest.body || ''), - headers, - }) -} diff --git a/src/utils/request/pruneGetRequestBody.test.ts b/src/utils/request/pruneGetRequestBody.test.ts deleted file mode 100644 index 1c08e25a4..000000000 --- a/src/utils/request/pruneGetRequestBody.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { pruneGetRequestBody } from './pruneGetRequestBody' - -test('sets empty GET request body to undefined', () => { - const body = pruneGetRequestBody({ - method: 'GET', - body: '', - }) - - expect(body).toBeUndefined() -}) - -test('preserves non-empty GET request body', () => { - const body = pruneGetRequestBody({ - method: 'GET', - body: 'text-body', - }) - - expect(body).toBe('text-body') -}) - -test('ignores requests of the other method than GET', () => { - expect( - pruneGetRequestBody({ - method: 'HEAD', - body: JSON.stringify({ a: 1 }), - }), - ).toBe(JSON.stringify({ a: 1 })) - - expect( - pruneGetRequestBody({ - method: 'POST', - body: 'text-body', - }), - ).toBe('text-body') -}) diff --git a/src/utils/request/pruneGetRequestBody.ts b/src/utils/request/pruneGetRequestBody.ts deleted file mode 100644 index d6f1e1ce7..000000000 --- a/src/utils/request/pruneGetRequestBody.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ServiceWorkerIncomingRequest } from '../../setupWorker/glossary' -import { isStringEqual } from '../internal/isStringEqual' - -type Input = Pick - -/** - * Ensures that an empty GET request body is always represented as `undefined`. - */ -export function pruneGetRequestBody( - request: Input, -): ServiceWorkerIncomingRequest['body'] { - if ( - request.method && - isStringEqual(request.method, 'GET') && - request.body === '' - ) { - return undefined - } - - return request.body -} diff --git a/test/browser/graphql-api/anonymous-operation.mocks.ts b/test/browser/graphql-api/anonymous-operation.mocks.ts new file mode 100644 index 000000000..92647efc5 --- /dev/null +++ b/test/browser/graphql-api/anonymous-operation.mocks.ts @@ -0,0 +1,12 @@ +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' + +const worker = setupWorker() +worker.start() + +// @ts-ignore +window.msw = { + worker, + graphql, + HttpResponse, +} diff --git a/test/browser/graphql-api/anonymous-operation.test.ts b/test/browser/graphql-api/anonymous-operation.test.ts new file mode 100644 index 000000000..efc6a2479 --- /dev/null +++ b/test/browser/graphql-api/anonymous-operation.test.ts @@ -0,0 +1,210 @@ +import { HttpServer } from '@open-draft/test-server/http' +import { test, expect } from '../playwright.extend' +import { gql } from '../../support/graphql' +import { waitFor } from '../../support/waitFor' + +declare namespace window { + export const msw: { + worker: import('msw/browser').SetupWorkerApi + graphql: typeof import('msw').graphql + HttpResponse: typeof import('msw').HttpResponse + } +} + +const httpServer = new HttpServer((app) => { + app.post('/graphql', (req, res) => { + res.json({ + data: { + user: { + id: 'abc-123', + }, + }, + }) + }) +}) + +test.beforeAll(async () => { + await httpServer.listen() +}) + +test.afterAll(async () => { + await httpServer.close() +}) + +test('does not warn on anonymous GraphQL operation when no GraphQL handlers are present', async ({ + loadExample, + query, + spyOnConsole, +}) => { + await loadExample(require.resolve('./anonymous-operation.mocks.ts')) + const consoleSpy = spyOnConsole() + + const endpointUrl = httpServer.http.url('/graphql') + const response = await query(endpointUrl, { + query: gql` + # Intentionally anonymous query. + query { + user { + id + } + } + `, + }) + + const json = await response.json() + + // Must get the original server response. + expect(json).toEqual({ + data: { + user: { + id: 'abc-123', + }, + }, + }) + + await waitFor(() => { + // Must print a generic unhandled GraphQL request warning. + // This has nothing to do with the operation being anonymous. + expect(consoleSpy.get('warning')).toEqual([ + `\ +[MSW] Warning: intercepted a request without a matching request handler: + + • anonymous query (POST ${endpointUrl}) + +If you still wish to intercept this unhandled request, please create a request handler for it. +Read more: https://mswjs.io/docs/getting-started/mocks`, + ]) + }) + + // // Must print the warning because anonymous operations cannot be intercepted + // // using standard "graphql.query()" and "graphql.mutation()" handlers. + // await waitFor(() => { + // expect(consoleSpy.get('warning')).toEqual( + // expect.arrayContaining([ + // `[MSW] Failed to intercept a GraphQL request at "POST ${endpointUrl}": 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/operation`, + // ]), + // ) + // }) +}) + +test('warns on handled anonymous GraphQL operation', async ({ + loadExample, + query, + spyOnConsole, + page, +}) => { + await loadExample(require.resolve('./anonymous-operation.mocks.ts')) + const consoleSpy = spyOnConsole() + + await page.evaluate(() => { + const { worker, graphql, HttpResponse } = window.msw + + worker.use( + // This handler will have no effect on the anonymous operation performed. + graphql.query('IrrelevantQuery', () => { + return HttpResponse.json({ + data: { + user: { + id: 'mocked-123', + }, + }, + }) + }), + ) + }) + + const endpointUrl = httpServer.http.url('/graphql') + const response = await query(endpointUrl, { + query: gql` + # Intentionally anonymous query. + # It will be handled in the "graphql.operation()" handler above. + query { + user { + id + } + } + `, + }) + + const json = await response.json() + + // Must get the original response because the "graphql.query()" + // handler won't match an anonymous GraphQL operation. + expect(json).toEqual({ + data: { + user: { + id: 'abc-123', + }, + }, + }) + + // Must print the warning because an anonymous operation has been performed. + await waitFor(() => { + expect(consoleSpy.get('warning')).toEqual( + expect.arrayContaining([ + `[MSW] Failed to intercept a GraphQL request at "POST ${endpointUrl}": 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/operation`, + ]), + ) + }) +}) + +test('does not print a warning on anonymous GraphQL operation handled by "graphql.operation()"', async ({ + loadExample, + spyOnConsole, + page, + query, +}) => { + await loadExample(require.resolve('./anonymous-operation.mocks.ts')) + const consoleSpy = spyOnConsole() + + await page.evaluate(() => { + const { worker, graphql, HttpResponse } = window.msw + + worker.use( + // This handler will match ANY anonymous GraphQL operation. + // It's a good idea to include some matching logic to differentiate + // between those operations. We're omitting it for testing purposes. + graphql.operation(() => { + return HttpResponse.json({ + data: { + user: { + id: 'mocked-123', + }, + }, + }) + }), + ) + }) + + const endpointUrl = httpServer.http.url('/graphql') + const response = await query(endpointUrl, { + query: gql` + # Intentionally anonymous query. + # It will be handled in the "graphql.operation()" handler above. + query { + user { + id + } + } + `, + }) + + const json = await response.json() + + // Must get the mocked response. + expect(json).toEqual({ + data: { + user: { + id: 'mocked-123', + }, + }, + }) + + // Must not print any warnings because a permissive "graphql.operation()" + // handler was used to intercept and mock the anonymous GraphQL operation. + expect(consoleSpy.get('warning')).toBeUndefined() +}) diff --git a/test/browser/graphql-api/cookies.mocks.ts b/test/browser/graphql-api/cookies.mocks.ts index 66c5460cb..7cd387da2 100644 --- a/test/browser/graphql-api/cookies.mocks.ts +++ b/test/browser/graphql-api/cookies.mocks.ts @@ -1,12 +1,19 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - graphql.query('GetUser', (req, res, ctx) => { - return res( - ctx.cookie('test-cookie', 'value'), - ctx.data({ - firstName: 'John', - }), + graphql.query('GetUser', () => { + return HttpResponse.json( + { + data: { + firstName: 'John', + }, + }, + { + headers: { + 'Set-Cookie': 'test-cookie=value', + }, + }, ) }), ) diff --git a/test/browser/graphql-api/cookies.test.ts b/test/browser/graphql-api/cookies.test.ts index a94805ad9..c451f8dcc 100644 --- a/test/browser/graphql-api/cookies.test.ts +++ b/test/browser/graphql-api/cookies.test.ts @@ -20,7 +20,7 @@ test('sets cookie on the mocked GraphQL response', async ({ const headers = await res.allHeaders() const body = await res.json() - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(headers).not.toHaveProperty('set-cookie') expect(body).toEqual({ data: { diff --git a/test/browser/graphql-api/document-node.mocks.ts b/test/browser/graphql-api/document-node.mocks.ts index 399118d0c..38d66056e 100644 --- a/test/browser/graphql-api/document-node.mocks.ts +++ b/test/browser/graphql-api/document-node.mocks.ts @@ -1,5 +1,6 @@ import { parse } from 'graphql' -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const GetUser = parse(` query GetUser { @@ -32,38 +33,38 @@ const github = graphql.link('https://api.github.com/graphql') const worker = setupWorker( // "DocumentNode" can be used as the expected query/mutation. - graphql.query(GetUser, (req, res, ctx) => { - return res( - ctx.data({ + graphql.query(GetUser, () => { + return HttpResponse.json({ + data: { // Note that inferring the query body and variables // is impossible with the native "DocumentNode". // Consider using tools like GraphQL Code Generator. user: { firstName: 'John', }, - }), - ) + }, + }) }), - graphql.mutation(Login, (req, res, ctx) => { - return res( - ctx.data({ + graphql.mutation(Login, ({ variables }) => { + return HttpResponse.json({ + data: { session: { id: 'abc-123', }, user: { - username: req.variables.username, + username: variables.username, }, - }), - ) + }, + }) }), - github.query(GetSubscription, (req, res, ctx) => { - return res( - ctx.data({ + github.query(GetSubscription, () => { + return HttpResponse.json({ + data: { subscription: { id: 123, }, - }), - ) + }, + }) }), ) diff --git a/test/browser/graphql-api/errors.mocks.ts b/test/browser/graphql-api/errors.mocks.ts index 46fe08634..18b98088a 100644 --- a/test/browser/graphql-api/errors.mocks.ts +++ b/test/browser/graphql-api/errors.mocks.ts @@ -1,9 +1,10 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - graphql.query('Login', (req, res, ctx) => { - return res( - ctx.errors([ + graphql.query('Login', () => { + return HttpResponse.json({ + errors: [ { message: 'This is a mocked error', locations: [ @@ -13,8 +14,8 @@ const worker = setupWorker( }, ], }, - ]), - ) + ], + }) }), ) diff --git a/test/browser/graphql-api/extensions.mocks.ts b/test/browser/graphql-api/extensions.mocks.ts index 28246c6db..8981bf8cb 100644 --- a/test/browser/graphql-api/extensions.mocks.ts +++ b/test/browser/graphql-api/extensions.mocks.ts @@ -1,23 +1,32 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' + +interface LoginQuery { + user: { + id: number + name: string + password: string + } +} const worker = setupWorker( - graphql.query('Login', (_req, res, ctx) => { - return res( - ctx.data({ + graphql.query('Login', () => { + return HttpResponse.json({ + data: { user: { id: 1, name: 'Joe Bloggs', password: 'HelloWorld!', }, - }), - ctx.extensions({ + }, + extensions: { message: 'This is a mocked extension', tracking: { version: '0.1.2', page: '/test/', }, - }), - ) + }, + }) }), ) diff --git a/test/browser/graphql-api/link.mocks.ts b/test/browser/graphql-api/link.mocks.ts index 8481c35b7..9e25b356e 100644 --- a/test/browser/graphql-api/link.mocks.ts +++ b/test/browser/graphql-api/link.mocks.ts @@ -1,4 +1,5 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const github = graphql.link('https://api.github.com/graphql') const stripe = graphql.link('https://api.stripe.com/graphql') @@ -24,36 +25,51 @@ interface GetUserQuery { } const worker = setupWorker( - github.query('GetUser', (req, res, ctx) => { - return res( - ctx.data({ - user: { - id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', - username: req.variables.username, + github.query( + 'GetUser', + ({ variables }) => { + return HttpResponse.json({ + data: { + user: { + id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', + username: variables.username, + }, }, - }), - ) - }), - stripe.mutation('Payment', (req, res, ctx) => { - return res( - ctx.data({ - bankAccount: { - totalFunds: 100 + req.variables.amount, + }) + }, + ), + stripe.mutation( + 'Payment', + ({ variables }) => { + return HttpResponse.json({ + data: { + bankAccount: { + totalFunds: 100 + variables.amount, + }, }, - }), - ) - }), - graphql.query('GetUser', (req, res, ctx) => { - return res( - ctx.set('x-request-handler', 'fallback'), - ctx.data({ - user: { - id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', - username: req.variables.username, + }) + }, + ), + graphql.query( + 'GetUser', + ({ variables }) => { + return HttpResponse.json( + { + data: { + user: { + id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', + username: variables.username, + }, + }, }, - }), - ) - }), + { + headers: { + 'X-Request-Handler': 'fallback', + }, + }, + ) + }, + ), ) worker.start() diff --git a/test/browser/graphql-api/link.test.ts b/test/browser/graphql-api/link.test.ts index a29a48315..58e35a15f 100644 --- a/test/browser/graphql-api/link.test.ts +++ b/test/browser/graphql-api/link.test.ts @@ -41,7 +41,7 @@ test('mocks a GraphQL query to the GitHub GraphQL API', async ({ const headers = await res.allHeaders() const body = await res.json() - expect(res.status()).toEqual(200) + expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { @@ -75,7 +75,7 @@ test('mocks a GraphQL mutation to the Stripe GraphQL API', async ({ const headers = await res.allHeaders() const body = await res.json() - expect(res.status()).toEqual(200) + expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { diff --git a/test/browser/graphql-api/logging.mocks.ts b/test/browser/graphql-api/logging.mocks.ts index 515fae03c..b24f7a270 100644 --- a/test/browser/graphql-api/logging.mocks.ts +++ b/test/browser/graphql-api/logging.mocks.ts @@ -1,4 +1,5 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' interface GetUserDetailQuery { user: { @@ -14,31 +15,35 @@ interface LoginQuery { } const worker = setupWorker( - graphql.query('GetUserDetail', (req, res, ctx) => { - return res( - ctx.data({ + graphql.query('GetUserDetail', () => { + return HttpResponse.json({ + data: { user: { firstName: 'John', lastName: 'Maverick', }, - }), - ) + }, + }) }), - graphql.mutation('Login', (req, res, ctx) => { - return res( - ctx.data({ + graphql.mutation('Login', () => { + return HttpResponse.json({ + data: { user: { id: 'abc-123', }, - }), - ) + }, + }) }), - graphql.operation((req, res, ctx) => { - return res( - ctx.status(301), - ctx.data({ - ok: true, - }), + graphql.operation(() => { + return HttpResponse.json( + { + data: { + ok: true, + }, + }, + { + status: 301, + }, ) }), ) diff --git a/test/browser/graphql-api/logging.test.ts b/test/browser/graphql-api/logging.test.ts index 99690510f..ee7d57fdd 100644 --- a/test/browser/graphql-api/logging.test.ts +++ b/test/browser/graphql-api/logging.test.ts @@ -1,4 +1,4 @@ -import { StatusCodeColor } from '../../../src/utils/logging/getStatusCodeColor' +import { StatusCodeColor } from '../../../src/core/utils/logging/getStatusCodeColor' import { waitFor } from '../../support/waitFor' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' @@ -25,7 +25,7 @@ test('prints a log for a GraphQL query', async ({ }) await waitFor(() => { - expect(consoleSpy.get('raw').get('startGroupCollapsed')).toEqual( + expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( @@ -56,7 +56,7 @@ test('prints a log for a GraphQL mutation', async ({ }) await waitFor(() => { - expect(consoleSpy.get('raw').get('startGroupCollapsed')).toEqual( + expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( @@ -87,7 +87,7 @@ test('prints a log for a GraphQL query intercepted via "graphql.operation"', asy }) await waitFor(() => { - expect(consoleSpy.get('raw').get('startGroupCollapsed')).toEqual( + expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( @@ -118,7 +118,7 @@ test('prints a log for a GraphQL mutation intercepted via "graphql.operation"', }) await waitFor(() => { - expect(consoleSpy.get('raw').get('startGroupCollapsed')).toEqual( + expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( diff --git a/test/browser/graphql-api/multipart-data.mocks.ts b/test/browser/graphql-api/multipart-data.mocks.ts index 9f971fd88..4732768e5 100644 --- a/test/browser/graphql-api/multipart-data.mocks.ts +++ b/test/browser/graphql-api/multipart-data.mocks.ts @@ -1,40 +1,37 @@ -import { setupWorker, graphql } from 'msw' - -interface UploadFileMutation { - multipart: { - file1?: string - file2?: string - files?: string[] - otherVariable?: string - } -} - -interface UploadFileVariables { - file1?: File - file2?: File - files?: File[] - otherVariable?: string -} +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - graphql.mutation( - 'UploadFile', - async (req, res, ctx) => { - const { file1, file2, files = [], otherVariable } = req.variables - const filesResponse = await Promise.all(files.map((file) => file.text())) - - return res( - ctx.data({ - multipart: { - file1: await file1?.text(), - file2: await file2?.text(), - files: filesResponse, - otherVariable, - }, - }), - ) + graphql.mutation< + { + multipart: { + file1?: string + file2?: string + files?: Array + plainText?: string + } }, - ), + { + file1?: File + file2?: File + files?: Array + plainText?: string + } + >('UploadFile', async ({ variables }) => { + const { file1, file2, files = [], plainText } = variables + const filesResponse = await Promise.all(files.map((file) => file.text())) + + return HttpResponse.json({ + data: { + multipart: { + file1: await file1?.text(), + file2: await file2?.text(), + files: filesResponse, + plainText, + }, + }, + }) + }), ) worker.start() diff --git a/test/browser/graphql-api/multipart-data.test.ts b/test/browser/graphql-api/multipart-data.test.ts index 0ead2e067..e5a48cafd 100644 --- a/test/browser/graphql-api/multipart-data.test.ts +++ b/test/browser/graphql-api/multipart-data.test.ts @@ -1,5 +1,4 @@ import { test, expect } from '../playwright.extend' -import { gql } from '../../support/graphql' test('accepts a file from a GraphQL mutation', async ({ loadExample, @@ -7,22 +6,31 @@ test('accepts a file from a GraphQL mutation', async ({ }) => { await loadExample(require.resolve('./multipart-data.mocks.ts')) - const UPLOAD_FILE_MUTATION = gql` - mutation UploadFile($file1: Upload, $file2: Upload, $plainText: String) { - multipart(file1: $file1, file2: $file2, plainText: $plainText) { + const UPLOAD_MUTATION = ` + mutation UploadFile( + $file1: Upload + $file2: Upload + $plainText: String + ) { + multipart( + file1: $file1 + file2: $file2 + plainText: $plainText + ){ file1 file2 plainText } } ` + const res = await query('/graphql', { - query: UPLOAD_FILE_MUTATION, + query: UPLOAD_MUTATION, variables: { file1: null, file2: null, files: [null, null], - otherVariable: 'value', + plainText: 'text', }, multipartOptions: { map: { @@ -42,7 +50,7 @@ test('accepts a file from a GraphQL mutation', async ({ file1: 'file1 content', file2: 'file2 content', files: ['file1 content', 'file2 content'], - otherVariable: 'value', + plainText: 'text', }, }, }) diff --git a/test/browser/graphql-api/mutation.mocks.ts b/test/browser/graphql-api/mutation.mocks.ts index e748425c1..1a6253433 100644 --- a/test/browser/graphql-api/mutation.mocks.ts +++ b/test/browser/graphql-api/mutation.mocks.ts @@ -1,4 +1,5 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' interface LogoutQuery { logout: { @@ -7,14 +8,14 @@ interface LogoutQuery { } const worker = setupWorker( - graphql.mutation('Logout', (req, res, ctx) => { - return res( - ctx.data({ + graphql.mutation('Logout', () => { + return HttpResponse.json({ + data: { logout: { userSession: false, }, - }), - ) + }, + }) }), ) diff --git a/test/browser/graphql-api/mutation.test.ts b/test/browser/graphql-api/mutation.test.ts index 791f9ccbf..4a5a9af77 100644 --- a/test/browser/graphql-api/mutation.test.ts +++ b/test/browser/graphql-api/mutation.test.ts @@ -49,7 +49,7 @@ test('sends a mocked response to a GraphQL mutation', async ({ }) }) -test('prints a warning when captured an anonymous GraphQL mutation', async ({ +test('prints a warning when intercepted an anonymous GraphQL mutation', async ({ loadExample, spyOnConsole, query, diff --git a/test/browser/graphql-api/operation-reference.mocks.ts b/test/browser/graphql-api/operation-reference.mocks.ts index f291c5c88..a0137d52c 100644 --- a/test/browser/graphql-api/operation-reference.mocks.ts +++ b/test/browser/graphql-api/operation-reference.mocks.ts @@ -1,21 +1,22 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - graphql.query('GetUser', (req, res, ctx) => { - return res( - ctx.data({ - query: req.body.query, - variables: req.body.variables, - }), - ) + graphql.query('GetUser', async ({ query, variables }) => { + return HttpResponse.json({ + data: { + query, + variables, + }, + }) }), - graphql.mutation('Login', (req, res, ctx) => { - return res( - ctx.data({ - query: req.body.query, - variables: req.body.variables, - }), - ) + graphql.mutation('Login', ({ query, variables }) => { + return HttpResponse.json({ + data: { + query, + variables, + }, + }) }), ) diff --git a/test/browser/graphql-api/operation-reference.test.ts b/test/browser/graphql-api/operation-reference.test.ts index 5c7f7663f..7e4850d66 100644 --- a/test/browser/graphql-api/operation-reference.test.ts +++ b/test/browser/graphql-api/operation-reference.test.ts @@ -23,11 +23,10 @@ test('allows referencing the request body in the GraphQL query handler', async ( id: 'abc-123', }, }) - const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: GET_USER_QUERY, @@ -58,11 +57,10 @@ test('allows referencing the request body in the GraphQL mutation handler', asyn password: 'super-secret', }, }) - const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: LOGIN_MUTATION, diff --git a/test/browser/graphql-api/operation.mocks.ts b/test/browser/graphql-api/operation.mocks.ts index d1f56b2fb..f5ecc0fc4 100644 --- a/test/browser/graphql-api/operation.mocks.ts +++ b/test/browser/graphql-api/operation.mocks.ts @@ -1,13 +1,14 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - graphql.operation((req, res, ctx) => { - return res( - ctx.data({ - query: req.body.query, - variables: req.body.variables, - }), - ) + graphql.operation(async ({ query, variables }) => { + return HttpResponse.json({ + data: { + query, + variables, + }, + }) }), ) diff --git a/test/browser/graphql-api/operation.test.ts b/test/browser/graphql-api/operation.test.ts index 193e9dcf9..2658867bc 100644 --- a/test/browser/graphql-api/operation.test.ts +++ b/test/browser/graphql-api/operation.test.ts @@ -39,11 +39,10 @@ test('intercepts and mocks a GraphQL query', async ({ id: 'abc-123', }, }) - const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: GET_USER_QUERY, @@ -87,9 +86,7 @@ test('intercepts and mocks an anonymous GraphQL query', async ({ expect(consoleSpy.get('warning')).toBeUndefined() expect(res.status()).toBe(200) - - const headers = await res.allHeaders() - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) const body = await res.json() expect(body).toEqual({ @@ -128,11 +125,10 @@ test('intercepts and mocks a GraphQL mutation', async ({ password: 'super-secret', }, }) - const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: LOGIN_MUTATION, @@ -184,12 +180,10 @@ test('bypasses seemingly compatible REST requests', async ({ const res = await query(server.http.url('/search'), { query: 'favorite books', }) - - const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) - expect(headers).not.toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ results: [1, 2, 3], }) diff --git a/test/browser/graphql-api/query.mocks.ts b/test/browser/graphql-api/query.mocks.ts index e701b1326..740a4d8f9 100644 --- a/test/browser/graphql-api/query.mocks.ts +++ b/test/browser/graphql-api/query.mocks.ts @@ -1,4 +1,5 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' interface GetUserDetailQuery { user: { @@ -8,15 +9,15 @@ interface GetUserDetailQuery { } const worker = setupWorker( - graphql.query('GetUserDetail', (req, res, ctx) => { - return res( - ctx.data({ + graphql.query('GetUserDetail', () => { + return HttpResponse.json({ + data: { user: { firstName: 'John', lastName: 'Maverick', }, - }), - ) + }, + }) }), ) diff --git a/test/browser/graphql-api/query.test.ts b/test/browser/graphql-api/query.test.ts index 44d3244b9..b1dbc6171 100644 --- a/test/browser/graphql-api/query.test.ts +++ b/test/browser/graphql-api/query.test.ts @@ -85,7 +85,7 @@ test('mocks a GraphQL query issued with a POST request', async ({ }) }) -test('prints a warning when captured an anonymous GraphQL query', async ({ +test('prints a warning when intercepted an anonymous GraphQL query', async ({ loadExample, spyOnConsole, query, diff --git a/test/browser/graphql-api/response-patching.mocks.ts b/test/browser/graphql-api/response-patching.mocks.ts index 2f7110896..5db10c3bc 100644 --- a/test/browser/graphql-api/response-patching.mocks.ts +++ b/test/browser/graphql-api/response-patching.mocks.ts @@ -1,5 +1,6 @@ -import { setupWorker, graphql } from 'msw' -// import { createGraphQLClient, gql } from '../../support/graphql' +import { graphql, bypass, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' +import { createGraphQLClient, gql } from '../../support/graphql' interface GetUserQuery { user: { @@ -9,19 +10,19 @@ interface GetUserQuery { } const worker = setupWorker( - graphql.query('GetUser', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req) + graphql.query('GetUser', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) const originalJson = await originalResponse.json() - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { firstName: 'Christian', lastName: originalJson.data?.user?.lastName, }, - }), - ctx.errors(originalJson.errors), - ) + }, + errors: originalJson.errors, + }) }), ) @@ -31,17 +32,17 @@ window.msw = { } // @ts-ignore -// window.dispatchGraphQLQuery = (uri: string) => { -// const client = createGraphQLClient({ uri }) +window.dispatchGraphQLQuery = (uri: string) => { + const client = createGraphQLClient({ uri }) -// return client({ -// query: gql` -// query GetUser { -// user { -// firstName -// lastName -// } -// } -// `, -// }) -// } + return client({ + query: gql` + query GetUser { + user { + firstName + lastName + } + } + `, + }) +} diff --git a/test/browser/graphql-api/response-patching.test.ts b/test/browser/graphql-api/response-patching.test.ts index f2994a4d3..3a175d37d 100644 --- a/test/browser/graphql-api/response-patching.test.ts +++ b/test/browser/graphql-api/response-patching.test.ts @@ -1,6 +1,6 @@ import type { ExecutionResult } from 'graphql' import { buildSchema, graphql } from 'graphql' -import { SetupWorkerApi } from 'msw' +import { SetupWorkerApi } from 'msw/browser' import { HttpServer } from '@open-draft/test-server/http' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' @@ -49,7 +49,7 @@ test.afterEach(async () => { await httpServer.close() }) -test('patches a GraphQL response', async ({ loadExample, page, query }) => { +test('patches a GraphQL response', async ({ loadExample, page }) => { await loadExample(require.resolve('./response-patching.mocks.ts')) const endpointUrl = httpServer.http.url('/graphql') @@ -57,24 +57,16 @@ test('patches a GraphQL response', async ({ loadExample, page, query }) => { return window.msw.registration }) - const res = await query(endpointUrl, { - query: gql` - query GetUser { - user { - firstName - lastName - } - } - `, - }) - const body = await res.json() - - expect(body).toEqual({ - data: { - user: { - firstName: 'Christian', - lastName: 'Maverick', - }, + const res = await page.evaluate( + ([url]) => { + return window.dispatchGraphQLQuery(url) }, + [endpointUrl], + ) + + expect(res.errors).toBeUndefined() + expect(res.data).toHaveProperty('user', { + firstName: 'Christian', + lastName: 'Maverick', }) }) diff --git a/test/browser/graphql-api/variables.mocks.ts b/test/browser/graphql-api/variables.mocks.ts index 9e7c2fc4a..ad13674f6 100644 --- a/test/browser/graphql-api/variables.mocks.ts +++ b/test/browser/graphql-api/variables.mocks.ts @@ -1,4 +1,5 @@ -import { setupWorker, graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' interface GetGitHubUserQuery { user: { @@ -28,55 +29,55 @@ interface GetActiveUserQuery { } interface GetActiveUserQueryVariables { - foo: string + userId: string } const worker = setupWorker( graphql.query( 'GetGithubUser', - (req, res, ctx) => { - const { username } = req.variables + ({ variables }) => { + const { username } = variables - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { username, firstName: 'John', }, - }), - ) + }, + }) }, ), graphql.mutation( 'DeletePost', - (req, res, ctx) => { - const { postId } = req.variables + ({ variables }) => { + const { postId } = variables - return res( - ctx.data({ + return HttpResponse.json({ + data: { deletePost: { postId, }, - }), - ) + }, + }) }, ), graphql.query( 'GetActiveUser', - (req, res, ctx) => { + ({ variables }) => { // Intentionally unused variable // eslint-disable-next-line - const { foo } = req.variables + const { userId } = variables - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { id: 1, }, - }), - ) + }, + }) }, ), ) diff --git a/test/browser/msw-api/context/async-response-transformer.mocks.ts b/test/browser/msw-api/context/async-response-transformer.mocks.ts deleted file mode 100644 index 6b45f2720..000000000 --- a/test/browser/msw-api/context/async-response-transformer.mocks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - ResponseTransformer, - setupWorker, - rest, - context, - compose, - createResponseComposition, -} from 'msw' -import base64Image from 'url-loader!../../../fixtures/image.jpg' - -async function jpeg(base64: string): Promise { - const buffer = await fetch(base64).then((res) => res.arrayBuffer()) - - return compose( - context.set('Content-Length', buffer.byteLength.toString()), - context.set('Content-Type', 'image/jpeg'), - context.body(buffer), - ) -} - -const customResponse = createResponseComposition(null, [ - async (res) => { - res.statusText = 'Custom Status Text' - return res - }, - async (res) => { - res.headers.set('x-custom', 'yes') - return res - }, -]) - -const worker = setupWorker( - rest.get('/image', async (req, res, ctx) => { - return res(ctx.status(201), await jpeg(base64Image)) - }), - rest.post('/search', (req, res, ctx) => { - return customResponse(ctx.status(301)) - }), -) - -worker.start() diff --git a/test/browser/msw-api/context/async-response-transformer.test.ts b/test/browser/msw-api/context/async-response-transformer.test.ts deleted file mode 100644 index f2c9145e8..000000000 --- a/test/browser/msw-api/context/async-response-transformer.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as fs from 'fs' -import * as path from 'path' -import { test, expect } from '../../playwright.extend' - -test('supports asynchronous response transformer', async ({ - loadExample, - fetch, -}) => { - await loadExample(require.resolve('./async-response-transformer.mocks.ts')) - - const res = await fetch('/image') - const body = await res.body() - const expectedBuffer = fs.readFileSync( - path.resolve(__dirname, '../../../fixtures/image.jpg'), - ) - const status = res.status() - const headers = await res.allHeaders() - - expect(status).toBe(201) - expect(headers).toHaveProperty('content-type', 'image/jpeg') - expect(headers).toHaveProperty( - 'content-length', - expectedBuffer.byteLength.toString(), - ) - expect(new Uint8Array(body)).toEqual(new Uint8Array(expectedBuffer)) -}) - -test('supports asynchronous default response transformer', async ({ - loadExample, - fetch, -}) => { - await loadExample(require.resolve('./async-response-transformer.mocks.ts')) - - const res = await fetch('/search', { - method: 'POST', - }) - const status = res.status() - const statusText = res.statusText() - const headers = await res.allHeaders() - - expect(status).toBe(301) - expect(statusText).toBe('Custom Status Text') - expect(headers).toHaveProperty('x-custom', 'yes') -}) diff --git a/test/browser/msw-api/context/delay.mocks.ts b/test/browser/msw-api/context/delay.mocks.ts index 8207fa8e8..f8282b54f 100644 --- a/test/browser/msw-api/context/delay.mocks.ts +++ b/test/browser/msw-api/context/delay.mocks.ts @@ -1,13 +1,15 @@ -import { setupWorker, rest, DelayMode } from 'msw' +import { http, delay, DelayMode, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/delay', (req, res, ctx) => { - const mode = req.url.searchParams.get('mode') as DelayMode - const duration = req.url.searchParams.get('duration') - return res( - ctx.delay(duration ? Number(duration) : mode || undefined), - ctx.json({ mocked: true }), - ) + http.get('/delay', async ({ request }) => { + const url = new URL(request.url) + const mode = url.searchParams.get('mode') as DelayMode + const duration = url.searchParams.get('duration') + + await delay(duration ? Number(duration) : mode || undefined) + + return HttpResponse.json({ mocked: true }) }), ) diff --git a/test/browser/msw-api/context/delay.test.ts b/test/browser/msw-api/context/delay.test.ts index 3a07d55dd..d4b6031ac 100644 --- a/test/browser/msw-api/context/delay.test.ts +++ b/test/browser/msw-api/context/delay.test.ts @@ -44,10 +44,9 @@ test('uses explicit server response delay', async ({ loadExample, fetch }) => { expect(timing.responseStart).toRoughlyEqual(1200, 250) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true }) }) @@ -64,10 +63,9 @@ test('uses realistic server response delay when no delay value is provided', asy expect(timing.responseStart).toRoughlyEqual(250, 300) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true, @@ -85,10 +83,9 @@ test('uses realistic server response delay when "real" delay mode is provided', expect(timing.responseStart).toRoughlyEqual(250, 300) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true, diff --git a/test/browser/msw-api/distribution/iife.mocks.js b/test/browser/msw-api/distribution/iife.mocks.js index e321ecf1a..60d5c1c2c 100644 --- a/test/browser/msw-api/distribution/iife.mocks.js +++ b/test/browser/msw-api/distribution/iife.mocks.js @@ -1,8 +1,8 @@ -const { setupWorker, rest } = MockServiceWorker +const { setupWorker, http, HttpResponse } = MockServiceWorker const worker = setupWorker( - rest.get('/user', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('/user', () => { + return HttpResponse.json({ firstName: 'John' }) }), ) diff --git a/test/browser/msw-api/distribution/iife.test.ts b/test/browser/msw-api/distribution/iife.test.ts index 12974d0fa..400c8c492 100644 --- a/test/browser/msw-api/distribution/iife.test.ts +++ b/test/browser/msw-api/distribution/iife.test.ts @@ -27,7 +27,6 @@ test('supports the usage of the iife bundle in a + fetch('/formData', { + method: 'POST', + body: data, + }) + } + + + diff --git a/test/browser/rest-api/request/body/body-form-data.test.ts b/test/browser/rest-api/request/body/body-form-data.test.ts index 704d8bff9..2cb9e899c 100644 --- a/test/browser/rest-api/request/body/body-form-data.test.ts +++ b/test/browser/rest-api/request/body/body-form-data.test.ts @@ -1,5 +1,11 @@ import { test, expect } from '../../../playwright.extend' +declare global { + interface Window { + makeRequest(): void + } +} + test('handles FormData as a request body', async ({ loadExample, page, @@ -9,15 +15,18 @@ test('handles FormData as a request body', async ({ markup: require.resolve('./body-form-data.page.html'), }) - page.click('button') + await page.evaluate(() => window.makeRequest()) + + await page.pause() - const res = await page.waitForResponse(makeUrl('/deprecated')) + const res = await page.waitForResponse(makeUrl('/formData')) const status = res.status() const json = await res.json() expect(status).toBe(200) expect(json).toEqual({ - username: 'john.maverick', - password: 'secret123', + name: 'Alice', + file: 'hello world', + ids: [1, 2, 3], }) }) diff --git a/test/browser/rest-api/request/body/body-json.test.ts b/test/browser/rest-api/request/body/body-json.test.ts index de3cadc28..09a6e98b0 100644 --- a/test/browser/rest-api/request/body/body-json.test.ts +++ b/test/browser/rest-api/request/body/body-json.test.ts @@ -3,7 +3,7 @@ import { test, expect } from '../../../playwright.extend' test('reads request body as json', async ({ loadExample, fetch, page }) => { await loadExample(require.resolve('./body.mocks.ts')) - const res = await fetch('/deprecated', { + const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -24,7 +24,7 @@ test('reads a single number as json request body', async ({ }) => { await loadExample(require.resolve('./body.mocks.ts')) - const res = await fetch('/deprecated', { + const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/test/browser/rest-api/request/body/body.mocks.ts b/test/browser/rest-api/request/body/body.mocks.ts index b2d052c6b..c03ab78b0 100644 --- a/test/browser/rest-api/request/body/body.mocks.ts +++ b/test/browser/rest-api/request/body/body.mocks.ts @@ -1,20 +1,29 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.post('/deprecated', (req, res, ctx) => { - return res(ctx.json(req.body)) + http.post('/text', async ({ request }) => { + return HttpResponse.text(await request.text()) }), - rest.post('/text', async (req, res, ctx) => { - const body = await req.text() - return res(ctx.body(body)) + http.post('/json', async ({ request }) => { + return HttpResponse.json(await request.json()) }), - rest.post('/json', async (req, res, ctx) => { - const json = await req.json() - return res(ctx.json(json)) + http.post('/arrayBuffer', async ({ request }) => { + return HttpResponse.arrayBuffer(await request.arrayBuffer()) }), - rest.post('/arrayBuffer', async (req, res, ctx) => { - const arrayBuffer = await req.arrayBuffer() - return res(ctx.body(arrayBuffer)) + http.post('/formData', async ({ request }) => { + const data = await request.formData() + const name = data.get('name') + const file = data.get('file') as File + const fileText = await file.text() + const ids = data.get('ids') as File + const idsJson = JSON.parse(await ids.text()) + + return HttpResponse.json({ + name, + file: fileText, + ids: idsJson, + }) }), ) diff --git a/test/browser/rest-api/request/matching/all.mocks.ts b/test/browser/rest-api/request/matching/all.mocks.ts index d606dcfa0..bab7e136a 100644 --- a/test/browser/rest-api/request/matching/all.mocks.ts +++ b/test/browser/rest-api/request/matching/all.mocks.ts @@ -1,11 +1,12 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.all('*/api/*', (req, res, ctx) => { - return res(ctx.text('hello world')) + http.all('*/api/*', () => { + return HttpResponse.text('hello world') }), - rest.all('*', (req, res, ctx) => { - return res(ctx.text('welcome to the jungle')) + http.all('*', () => { + return HttpResponse.text('welcome to the jungle') }), ) diff --git a/test/browser/rest-api/request/matching/all.test.ts b/test/browser/rest-api/request/matching/all.test.ts index 074e3197e..ab23ef155 100644 --- a/test/browser/rest-api/request/matching/all.test.ts +++ b/test/browser/rest-api/request/matching/all.test.ts @@ -41,7 +41,7 @@ test('respects custom path when matching requests', async ({ } // Mismatched request. - // There's a fallback "rest.all()" in this test that acts + // There's a fallback "http.all()" in this test that acts // as a fallback request handler for all otherwise mismatched requests. const mismatchedResponses = await forEachMethod((method) => { return fetch('http://localhost/foo', { method }) diff --git a/test/browser/rest-api/request/matching/method.mocks.ts b/test/browser/rest-api/request/matching/method.mocks.ts index 0bc778e58..08fd0ba16 100644 --- a/test/browser/rest-api/request/matching/method.mocks.ts +++ b/test/browser/rest-api/request/matching/method.mocks.ts @@ -1,12 +1,9 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.post('*/user', (req, res, ctx) => { - return res( - ctx.json({ - mocked: true, - }), - ) + http.post('*/user', () => { + return HttpResponse.json({ mocked: true }) }), ) diff --git a/test/browser/rest-api/request/matching/method.test.ts b/test/browser/rest-api/request/matching/method.test.ts index 407bd080c..e0b074a8b 100644 --- a/test/browser/rest-api/request/matching/method.test.ts +++ b/test/browser/rest-api/request/matching/method.test.ts @@ -29,11 +29,10 @@ test('sends a mocked response to a matching method and url', async ({ method: 'POST', }) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true, }) diff --git a/test/browser/rest-api/request/matching/path-params-decode.mocks.ts b/test/browser/rest-api/request/matching/path-params-decode.mocks.ts index 60ff634cf..98e71c479 100644 --- a/test/browser/rest-api/request/matching/path-params-decode.mocks.ts +++ b/test/browser/rest-api/request/matching/path-params-decode.mocks.ts @@ -1,10 +1,10 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('https://test.mswjs.io/reflect-url/:url', (req, res, ctx) => { - const { url } = req.params - - return res(ctx.json({ url })) + http.get('https://test.mswjs.io/reflect-url/:url', ({ params }) => { + const { url } = params + return HttpResponse.json({ url }) }), ) diff --git a/test/browser/rest-api/request/matching/path-params-decode.test.ts b/test/browser/rest-api/request/matching/path-params-decode.test.ts index 2d02e15bd..02ee458db 100644 --- a/test/browser/rest-api/request/matching/path-params-decode.test.ts +++ b/test/browser/rest-api/request/matching/path-params-decode.test.ts @@ -9,7 +9,7 @@ test('decodes url componets', async ({ loadExample, fetch }) => { ) expect(res.status()).toBe(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ url, }) diff --git a/test/browser/rest-api/request/matching/uri.mocks.ts b/test/browser/rest-api/request/matching/uri.mocks.ts index 7beccfc73..38262dde7 100644 --- a/test/browser/rest-api/request/matching/uri.mocks.ts +++ b/test/browser/rest-api/request/matching/uri.mocks.ts @@ -1,43 +1,27 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('https://api.github.com/made-up', (req, res, ctx) => { - return res( - ctx.json({ - mocked: true, - }), - ) + http.get('https://api.github.com/made-up', () => { + return HttpResponse.json({ mocked: true }) }), - rest.get('https://test.mswjs.io/messages/:messageId', (req, res, ctx) => { - const { messageId } = req.params - - return res( - ctx.json({ - messageId, - }), - ) + http.get('https://test.mswjs.io/messages/:messageId', ({ params }) => { + const { messageId } = params + return HttpResponse.json({ messageId }) }), - rest.get( - 'https://test.mswjs.io/messages/:messageId/items', - (req, res, ctx) => { - const { messageId } = req.params - - return res( - ctx.json({ - messageId, - }), - ) - }, - ), + http.get('https://test.mswjs.io/messages/:messageId/items', ({ params }) => { + const { messageId } = params + return HttpResponse.json({ messageId }) + }), - rest.get(/(.+?)\.google\.com\/path/, (req, res, ctx) => { - return res(ctx.json({ mocked: true })) + http.get(/(.+?)\.google\.com\/path/, () => { + return HttpResponse.json({ mocked: true }) }), - rest.get(`/resource\\('id'\\)`, (req, res, ctx) => { - return res(ctx.json({ mocked: true })) + http.get(`/resource\\('id'\\)`, () => { + return HttpResponse.json({ mocked: true }) }), ) diff --git a/test/browser/rest-api/request/matching/uri.test.ts b/test/browser/rest-api/request/matching/uri.test.ts index 362e44c57..3029fe2fb 100644 --- a/test/browser/rest-api/request/matching/uri.test.ts +++ b/test/browser/rest-api/request/matching/uri.test.ts @@ -9,7 +9,7 @@ test('matches an exact string with the same request URL with a trailing slash', const res = await fetch('https://api.github.com/made-up/') expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ mocked: true, }) @@ -36,7 +36,7 @@ test('matches an exact string with the same request URL without a trailing slash const res = await fetch('https://api.github.com/made-up') expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ mocked: true, }) @@ -63,7 +63,7 @@ test('matches a mask against a matching request URL', async ({ const res = await fetch('https://test.mswjs.io/messages/abc-123') expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ messageId: 'abc-123', }) @@ -80,7 +80,7 @@ test('ignores query parameters when matching a mask against a matching request U ) expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ messageId: 'abc-123', }) @@ -109,7 +109,7 @@ test('matches a RegExp against a matching request URL', async ({ const res = await fetch('https://mswjs.google.com/path') expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ mocked: true, }) @@ -138,7 +138,7 @@ test('supports escaped parentheses in the request URL', async ({ const res = await fetch(`/resource('id')`) expect(res.status()).toEqual(200) - expect(await res.allHeaders()).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ mocked: true, }) diff --git a/test/browser/rest-api/response-patching.mocks.ts b/test/browser/rest-api/response-patching.mocks.ts index 87c79eeff..1ca6bccfb 100644 --- a/test/browser/rest-api/response-patching.mocks.ts +++ b/test/browser/rest-api/response-patching.mocks.ts @@ -1,77 +1,108 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse, bypass } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('*/user', async (req, res, ctx) => { - /** - * @note Do not pass the entire "req" as the input to "ctx.fetch" - * because then the bypassed request will inherit the "Accept-Language" header - * that's used by tests to await for responses. It will await the incorrect - * response in that case. - */ - const originalResponse = await ctx.fetch(req.url.href) + http.get('*/user', async ({ request }) => { + const originalResponse = await fetch(bypass(request.url)) const body = await originalResponse.json() - return res( - ctx.json({ + return HttpResponse.json( + { name: body.name, location: body.location, mocked: true, - }), + }, + { + headers: { + 'X-Source': 'msw', + }, + }, ) }), - rest.get('*/repos/:owner/:repoName', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req.url.href) + http.get('*/repos/:owner/:repoName', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() - return res( - ctx.json({ + return HttpResponse.json( + { name: body.name, stargazers_count: 9999, - }), + }, + { + headers: { + 'X-Source': 'msw', + }, + }, ) }), - rest.get('*/headers', async (req, res, ctx) => { - const proxyUrl = new URL('/headers-proxy', req.url).href - const originalResponse = await ctx.fetch(proxyUrl, { - method: 'POST', - headers: req.headers.all(), - }) + http.get('*/headers', async ({ request }) => { + const proxyUrl = new URL('/headers-proxy', request.url) + const originalResponse = await fetch( + bypass(proxyUrl, { + method: 'POST', + headers: request.headers, + }), + ) const body = await originalResponse.json() - return res(ctx.json(body)) + return HttpResponse.json(body, { + headers: { + 'X-Source': 'msw', + }, + }) }), - rest.post('*/posts', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req) + http.post('*/posts', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() - return res( - ctx.set('x-custom', originalResponse.headers.get('x-custom')!), - ctx.json({ + return HttpResponse.json( + { ...body, mocked: true, - }), + }, + { + headers: { + 'X-Source': 'msw', + 'X-Custom': originalResponse.headers.get('x-custom') || '', + }, + }, ) }), - rest.get('*/posts', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req.url.href) + http.get('*/posts', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() - return res( - ctx.json({ + return HttpResponse.json( + { ...body, mocked: true, - }), + }, + { + headers: { + 'X-Source': 'msw', + }, + }, ) }), - rest.head('*/posts', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req.url.href, { method: 'HEAD' }) + http.head('*/posts', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) - return res(ctx.set('x-custom', originalResponse.headers.get('x-custom')!)) + return HttpResponse.json( + { + mocked: true, + }, + { + headers: { + 'X-Source': 'msw', + 'X-Custom': originalResponse.headers.get('x-custom') || '', + }, + }, + ) }), ) diff --git a/test/browser/rest-api/response-patching.test.ts b/test/browser/rest-api/response-patching.test.ts index 1ad2df148..6ee5d5b48 100644 --- a/test/browser/rest-api/response-patching.test.ts +++ b/test/browser/rest-api/response-patching.test.ts @@ -63,11 +63,10 @@ test('responds with a combination of the mocked and original responses', async ( const res = await fetch(httpServer.http.url('/user')) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'The Octocat', location: 'San Francisco', @@ -90,18 +89,17 @@ test('bypasses the original request when it equals the mocked request', async ({ // Await the response from MSW so that the original response // from the same URL would not interfere. matchRequestUrl(new URL(res.request().url()), res.url()).matches && - res.headers()['x-powered-by'] === 'msw' + res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'msw', stargazers_count: 9999, @@ -147,20 +145,19 @@ test('supports patching a HEAD request', async ({ loadExample, fetch }) => { const headers = res.headers() return ( - headers['x-powered-by'] === 'msw' && - headers['x-msw-bypass'] !== 'true' + headers['x-source'] === 'msw' && headers['x-msw-bypass'] !== 'true' ) }, }, ) const status = res.status() - const headers = await res.allHeaders() + const headers = res.headers() expect(status).toBe(200) expect(headers).toEqual( expect.objectContaining({ - 'x-powered-by': 'msw', + 'x-source': 'msw', 'x-custom': 'HEAD REQUEST PATCHED', }), ) @@ -185,17 +182,16 @@ test('supports patching a GET request', async ({ waitForResponse(res) { return ( matchRequestUrl(new URL(makeUrl(res.request().url())), res.url()) - .matches && res.headers()['x-powered-by'] === 'msw' + .matches && res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() - const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ id: 101, mocked: true }) }) @@ -203,7 +199,6 @@ test('supports patching a POST request', async ({ loadExample, fetch, makeUrl, - page, }) => { await loadExample(require.resolve('./response-patching.mocks.ts')) @@ -224,19 +219,17 @@ test('supports patching a POST request', async ({ waitForResponse(res) { return ( matchRequestUrl(new URL(makeUrl(res.request().url())), res.url()) - .matches && res.headers()['x-powered-by'] === 'msw' + .matches && res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() - const headers = await res.allHeaders() + const headers = res.headers() const body = await res.json() - await page.pause() - expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(headers).toHaveProperty('x-custom', 'POST REQUEST PATCHED') expect(body).toEqual({ id: 101, diff --git a/test/browser/rest-api/response/body/body-binary.mocks.ts b/test/browser/rest-api/response/body/body-binary.mocks.ts index 78459d7ab..751f7e9ec 100644 --- a/test/browser/rest-api/response/body/body-binary.mocks.ts +++ b/test/browser/rest-api/response/body/body-binary.mocks.ts @@ -1,17 +1,18 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' import base64Image from 'url-loader!../../../../fixtures/image.jpg' const worker = setupWorker( - rest.get('/images/:imageId', async (_, res, ctx) => { + http.get('/images/:imageId', async () => { const imageBuffer = await fetch(base64Image).then((res) => res.arrayBuffer(), ) - return res( - ctx.set('Content-Length', imageBuffer.byteLength.toString()), - ctx.set('Content-Type', 'image/jpeg'), - ctx.body(imageBuffer), - ) + return HttpResponse.arrayBuffer(imageBuffer, { + headers: { + 'Content-Type': 'image/jpeg', + }, + }) }), ) diff --git a/test/browser/rest-api/response/body/body-binary.test.ts b/test/browser/rest-api/response/body/body-binary.test.ts index b173104c1..186a98f0a 100644 --- a/test/browser/rest-api/response/body/body-binary.test.ts +++ b/test/browser/rest-api/response/body/body-binary.test.ts @@ -7,7 +7,6 @@ test('responds with a given binary body', async ({ loadExample, fetch }) => { const res = await fetch('/images/abc-123') const status = res.status() - const headers = await res.allHeaders() const body = await res.body() const expectedBuffer = fs.readFileSync( @@ -15,6 +14,6 @@ test('responds with a given binary body', async ({ loadExample, fetch }) => { ) expect(status).toBe(200) - expect(headers).toHaveProperty('x-powered-by', 'msw') + expect(res.fromServiceWorker()).toBe(true) expect(new Uint8Array(body)).toEqual(new Uint8Array(expectedBuffer)) }) diff --git a/test/browser/rest-api/response/body/body-blob.mocks.ts b/test/browser/rest-api/response/body/body-blob.mocks.ts new file mode 100644 index 000000000..eb7b26901 --- /dev/null +++ b/test/browser/rest-api/response/body/body-blob.mocks.ts @@ -0,0 +1,14 @@ +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' + +const worker = setupWorker( + http.get('/greeting', async () => { + const blob = new Blob(['hello world'], { + type: 'text/plain', + }) + + return new HttpResponse(blob) + }), +) + +worker.start() diff --git a/test/browser/rest-api/response/body/body-blob.test.ts b/test/browser/rest-api/response/body/body-blob.test.ts new file mode 100644 index 000000000..a6b655184 --- /dev/null +++ b/test/browser/rest-api/response/body/body-blob.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from '../.././../playwright.extend' + +test('responds to a request with a Blob', async ({ loadExample, fetch }) => { + await loadExample(require.resolve('./body-blob.mocks.ts')) + const res = await fetch('/greeting') + + const headers = await res.allHeaders() + expect(headers).toHaveProperty('content-type', 'text/plain') + expect(res.fromServiceWorker()).toBe(true) + + const text = await res.text() + expect(text).toBe('hello world') +}) diff --git a/test/browser/rest-api/response/body/body-formdata.mocks.ts b/test/browser/rest-api/response/body/body-formdata.mocks.ts new file mode 100644 index 000000000..29cf1c27c --- /dev/null +++ b/test/browser/rest-api/response/body/body-formdata.mocks.ts @@ -0,0 +1,14 @@ +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' + +const worker = setupWorker( + http.get('/user', async () => { + const data = new FormData() + data.append('name', 'Alice') + data.append('age', '32') + + return HttpResponse.formData(data) + }), +) + +worker.start() diff --git a/test/browser/rest-api/response/body/body-formdata.test.ts b/test/browser/rest-api/response/body/body-formdata.test.ts new file mode 100644 index 000000000..e4eb625ca --- /dev/null +++ b/test/browser/rest-api/response/body/body-formdata.test.ts @@ -0,0 +1,18 @@ +import { test, expect } from '../.././../playwright.extend' + +test('responds to a request with FormData', async ({ loadExample, fetch }) => { + await loadExample(require.resolve('./body-formdata.mocks.ts')) + const res = await fetch('/user') + + const headers = await res.allHeaders() + expect(headers).toHaveProperty( + 'content-type', + expect.stringContaining('multipart/form-data'), + ) + expect(res.fromServiceWorker()).toBe(true) + + const text = await res.text() + expect(text).toMatch( + /------WebKitFormBoundary.+?\r\nContent-Disposition: form-data; name="name"\r\n\r\nAlice\r\n------WebKitFormBoundary.+?\r\nContent-Disposition: form-data; name="age"\r\n\r\n32\r\n------WebKitFormBoundary.+?--/gm, + ) +}) diff --git a/test/browser/rest-api/response/body/body-json.mocks.ts b/test/browser/rest-api/response/body/body-json.mocks.ts index 44357b41f..7bf637cc2 100644 --- a/test/browser/rest-api/response/body/body-json.mocks.ts +++ b/test/browser/rest-api/response/body/body-json.mocks.ts @@ -1,11 +1,12 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/json', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('/json', () => { + return HttpResponse.json({ firstName: 'John' }) }), - rest.get('/number', (req, res, ctx) => { - return res(ctx.json(123)) + http.get('/number', () => { + return HttpResponse.json(123) }), ) diff --git a/test/browser/rest-api/response/body/body-stream.mocks.ts b/test/browser/rest-api/response/body/body-stream.mocks.ts new file mode 100644 index 000000000..cd88c669f --- /dev/null +++ b/test/browser/rest-api/response/body/body-stream.mocks.ts @@ -0,0 +1,29 @@ +import { http, HttpResponse, delay } from 'msw' +import { setupWorker } from 'msw/browser' + +const encoder = new TextEncoder() +const chunks = ['hello', 'streaming', 'world'] + +const worker = setupWorker( + http.get('/stream', () => { + const stream = new ReadableStream({ + async start(controller) { + for (const chunk of chunks) { + controller.enqueue(encoder.encode(chunk)) + await delay(250) + } + + controller.close() + }, + }) + + return new HttpResponse(stream, { + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': chunks.join('').length.toString(), + }, + }) + }), +) + +worker.start() diff --git a/test/browser/rest-api/response/body/body-stream.test.ts b/test/browser/rest-api/response/body/body-stream.test.ts new file mode 100644 index 000000000..43b76d5a1 --- /dev/null +++ b/test/browser/rest-api/response/body/body-stream.test.ts @@ -0,0 +1,46 @@ +import { test, expect } from '../../../playwright.extend' + +test('responds with a mocked ReadableStream response', async ({ + loadExample, + page, +}) => { + await loadExample(require.resolve('./body-stream.mocks.ts')) + + const chunks = await page.evaluate(() => { + return fetch('/stream').then(async (res) => { + if (res.body === null) { + return [] + } + + const decoder = new TextDecoder() + const chunks: Array<{ text: string; timestamp: number }> = [] + const reader = res.body.getReader() + + while (true) { + const { value, done } = await reader.read() + + if (done) { + return chunks + } + + chunks.push({ + text: decoder.decode(value), + timestamp: Date.now(), + }) + } + }) + }) + + // Must stream the mocked response in three chunks. + const chunksText = chunks.map((chunk) => chunk.text) + expect(chunksText).toEqual(['hello', 'streaming', 'world']) + + const chunkDeltas = chunks.map((chunk, index) => { + const prevChunk = chunks[index - 1] + return prevChunk ? chunk.timestamp - prevChunk.timestamp : 0 + }) + + expect(chunkDeltas[0]).toBe(0) + expect(chunkDeltas[1]).toBeGreaterThanOrEqual(200) + expect(chunkDeltas[2]).toBeGreaterThanOrEqual(200) +}) diff --git a/test/browser/rest-api/response/body/body-text.mocks.ts b/test/browser/rest-api/response/body/body-text.mocks.ts index 6ffc86a45..fbac4101d 100644 --- a/test/browser/rest-api/response/body/body-text.mocks.ts +++ b/test/browser/rest-api/response/body/body-text.mocks.ts @@ -1,8 +1,9 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/text', (req, res, ctx) => { - return res(ctx.text('hello world')) + http.get('/text', () => { + return HttpResponse.text('hello world') }), ) diff --git a/test/browser/rest-api/response/body/body-xml.mocks.ts b/test/browser/rest-api/response/body/body-xml.mocks.ts index 37cf076bf..e58c1bd63 100644 --- a/test/browser/rest-api/response/body/body-xml.mocks.ts +++ b/test/browser/rest-api/response/body/body-xml.mocks.ts @@ -1,15 +1,14 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/user', (req, res, ctx) => { - return res( - ctx.xml(` + http.get('/user', () => { + return HttpResponse.xml(` abc-123 John Maverick -`), - ) +`) }), ) diff --git a/test/browser/rest-api/response/response-error.mocks.ts b/test/browser/rest-api/response/response-error.mocks.ts new file mode 100644 index 000000000..c372dda35 --- /dev/null +++ b/test/browser/rest-api/response/response-error.mocks.ts @@ -0,0 +1,10 @@ +import { http } from 'msw' +import { setupWorker } from 'msw/browser' + +const worker = setupWorker( + http.get('/resource', () => { + return Response.error() + }), +) + +worker.start() diff --git a/test/browser/rest-api/response/response-error.test.ts b/test/browser/rest-api/response/response-error.test.ts new file mode 100644 index 000000000..a05b4163f --- /dev/null +++ b/test/browser/rest-api/response/response-error.test.ts @@ -0,0 +1,27 @@ +import { test, expect } from '../../playwright.extend' + +test('responds with a mocked error response using "Response.error" shorthand', async ({ + loadExample, + fetch, + page, +}) => { + await loadExample(require.resolve('./response-error.mocks.ts')) + + const responseError = await page.evaluate(() => { + return fetch('/resource') + .then(() => null) + .catch((error) => ({ + name: error.name, + message: error.message, + stack: error.stack, + cause: error.cause, + })) + }) + + // Responding with a "Response.error()" produced a "Failed to fetch" error, + // breaking the request. This is analogous to a network error. + expect(responseError?.name).toBe('TypeError') + expect(responseError?.message).toBe('Failed to fetch') + // Guard against false positives due to exceptions arising from the library. + expect(responseError?.cause).toBeUndefined() +}) diff --git a/test/browser/rest-api/status.mocks.ts b/test/browser/rest-api/status.mocks.ts index fb138b25a..44902088f 100644 --- a/test/browser/rest-api/status.mocks.ts +++ b/test/browser/rest-api/status.mocks.ts @@ -1,15 +1,19 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('/posts', (req, res, ctx) => { + http.get('/posts', () => { // Setting response status code without status text // implicitly sets the correct status text. - return res(ctx.status(403)) + return HttpResponse.text(null, { status: 403 }) }), - rest.get('/user', (req, res, ctx) => { + http.get('/user', () => { // Response status text can be overridden // to an arbitrary string value. - return res(ctx.status(401, 'Custom text')) + return HttpResponse.text(null, { + status: 401, + statusText: 'Custom text', + }) }), ) diff --git a/test/browser/rest-api/xhr.mocks.ts b/test/browser/rest-api/xhr.mocks.ts index 42dae4379..28239554b 100644 --- a/test/browser/rest-api/xhr.mocks.ts +++ b/test/browser/rest-api/xhr.mocks.ts @@ -1,8 +1,9 @@ -import { setupWorker, rest } from 'msw' +import { http, HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' const worker = setupWorker( - rest.get('https://api.github.com/users/octocat', (req, res, ctx) => { - return res(ctx.json({ mocked: true })) + http.get('https://api.github.com/users/octocat', () => { + return HttpResponse.json({ mocked: true }) }), ) diff --git a/test/browser/setup/webpackHttpServer.ts b/test/browser/setup/webpackHttpServer.ts index bfed60ae0..e24a07a4e 100644 --- a/test/browser/setup/webpackHttpServer.ts +++ b/test/browser/setup/webpackHttpServer.ts @@ -1,7 +1,7 @@ import * as fs from 'fs' import * as path from 'path' import { WebpackHttpServer } from 'webpack-http-server' -import { getWorkerScriptPatch } from './WorkerConsole' +import { getWorkerScriptPatch } from './workerConsole' const { SERVICE_WORKER_BUILD_PATH } = require('../../../config/constants.js') @@ -62,7 +62,7 @@ export async function startWebpackServer(): Promise { alias: { msw: path.resolve(__dirname, '../../..'), }, - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js', '.mjs'], }, }, }) diff --git a/test/jest.config.js b/test/jest.config.js index 4f865b000..1f8692b34 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -1,4 +1,4 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +/** @type {import('jest').Config} */ module.exports = { rootDir: './node', transform: { @@ -10,4 +10,20 @@ module.exports = { moduleNameMapper: { '^msw(.*)': '/../..$1', }, + testEnvironmentOptions: { + // Force JSDOM to use the Node module resolution because we're still in Node.js. + // Using browser resolution won't work by design because JSDOM is not a browser + // and doesn't ship with 100% compatibility with the browser APIs. + // In tests, using browser resolution will result in "ClientRequest" imports + // from "@mswjs/interceptors" to not be found because they are not exported + // by the browser bundle of that library. + customExportConditions: [''], + }, + globals: { + fetch, + Request, + Response, + TextEncoder, + TextDecoder, + }, } diff --git a/test/modules/browser/esm-browser.test.ts b/test/modules/browser/esm-browser.test.ts new file mode 100644 index 000000000..53bfbe2eb --- /dev/null +++ b/test/modules/browser/esm-browser.test.ts @@ -0,0 +1,87 @@ +import * as path from 'path' +import { invariant } from 'outvariant' +import { createTeardown } from 'fs-teardown' +import * as express from 'express' +import { HttpServer } from '@open-draft/test-server/http' +import { test, expect } from '@playwright/test' +import { spyOnConsole } from 'page-with' +import { startDevServer } from '@web/dev-server' +import { installLibrary } from '../module-utils' + +type DevServer = Awaited> + +const fsMock = createTeardown({ + rootDir: path.resolve(__dirname, 'esm-browser-tests'), + paths: { + 'package.json': JSON.stringify({ type: 'module' }), + }, +}) + +let devServer: DevServer + +function getDevServerUrl(): string { + const address = devServer.server.address() + + invariant(address, 'Failed to retrieve dev server url: null') + + if (typeof address === 'string') { + return new URL(address).href + } + + return new URL(`http://localhost:${address.port}`).href +} + +const httpServer = new HttpServer((app) => { + app.use(express.static(fsMock.resolve('.'))) +}) + +test.beforeAll(async () => { + devServer = await startDevServer({ + config: { + rootDir: fsMock.resolve('.'), + port: 0, + nodeResolve: { + exportConditions: ['browser'], + }, + }, + logStartMessage: false, + }) + + await httpServer.listen() + await fsMock.prepare() + await installLibrary(fsMock.resolve('.')) +}) + +test.afterAll(async () => { + await devServer?.stop() + await httpServer.close() + await fsMock.cleanup() +}) + +test('runs in an ESM browser project', async ({ page }) => { + await fsMock.create({ + 'entry.mjs': ` +import { http,HttpResponse } from 'msw' +import { setupWorker } from 'msw/browser' +const worker = setupWorker( + http.get('/resource', () => new Response()), + http.post('/login', () => HttpResponse.json([1, 2, 3])) +) +console.log(typeof worker.start) + `, + 'index.html': ` + + `, + }) + const consoleSpy = spyOnConsole(page) + const pageErrors: Array = [] + page.on('pageerror', (error) => + pageErrors.push(`${error.message}\n${error.stack}`), + ) + + await page.goto(getDevServerUrl(), { waitUntil: 'networkidle' }) + + expect(pageErrors).toEqual([]) + expect(consoleSpy.get('error')).toBeUndefined() + expect(consoleSpy.get('log')).toEqual(expect.arrayContaining(['function'])) +}) diff --git a/test/modules/browser/playwright.config.ts b/test/modules/browser/playwright.config.ts new file mode 100644 index 000000000..2fea97eca --- /dev/null +++ b/test/modules/browser/playwright.config.ts @@ -0,0 +1,13 @@ +import { Config } from '@playwright/test' + +const config: Config = { + testDir: __dirname, + use: { + launchOptions: { + devtools: !process.env.CI, + }, + }, + fullyParallel: true, +} + +export default config diff --git a/test/modules/module-utils.ts b/test/modules/module-utils.ts new file mode 100644 index 000000000..0ec7752e5 --- /dev/null +++ b/test/modules/module-utils.ts @@ -0,0 +1,52 @@ +import * as fs from 'fs' +import * as path from 'path' +import { spawnSync } from 'child_process' +import { invariant } from 'outvariant' + +export async function getLibraryTarball(): Promise { + const ROOT_PATH = path.resolve(__dirname, '../..') + const { version } = require(`${ROOT_PATH}/package.json`) + const packFilename = `msw-${version}.tgz` + const packPath = path.resolve(ROOT_PATH, packFilename) + + /** + * @note Beware that you need to remove the tarball after + * the test run is done. Don't want to use a stale tgarball, do you? + */ + if (fs.existsSync(packPath)) { + return packPath + } + + const out = spawnSync('pnpm', ['pack'], { cwd: ROOT_PATH }) + + if (out.error) { + console.error(out.error) + } + + invariant( + fs.existsSync(packPath), + 'Failed to pack the library at "%s"', + packPath, + ) + + return packPath +} + +export async function installLibrary(projectPath: string) { + const TARBALL_PATH = await getLibraryTarball() + + const out = spawnSync('pnpm', ['install', TARBALL_PATH], { + cwd: projectPath, + }) + + if (out.error) { + console.error(out.error) + return Promise.reject( + 'Failed to install the library. See the stderr output above.', + ) + } + + /** @todo Assert that pnpm printed success: + * + msw 0.0.0-fetch.rc-11 + */ +} diff --git a/test/modules/node/esm-node.test.ts b/test/modules/node/esm-node.test.ts new file mode 100644 index 000000000..303a19e7f --- /dev/null +++ b/test/modules/node/esm-node.test.ts @@ -0,0 +1,108 @@ +import * as path from 'path' +import { createTeardown } from 'fs-teardown' +import { installLibrary } from '../module-utils' + +const fsMock = createTeardown({ + rootDir: path.resolve(__dirname, 'node-esm-tests'), + paths: { + 'package.json': JSON.stringify({ type: 'module' }), + }, +}) + +beforeAll(async () => { + await fsMock.prepare() + await installLibrary(fsMock.resolve('.')) +}) + +afterAll(async () => { + await fsMock.cleanup() +}) + +it('runs in a ESM Node.js project', async () => { + await fsMock.create({ + 'resolve.mjs': ` +console.log('msw:', await import.meta.resolve('msw')) +console.log('msw/node:', await import.meta.resolve('msw/node')) +console.log('msw/native:', await import.meta.resolve('msw/native')) +`, + 'runtime.mjs': ` +import { http } from 'msw' +import { setupServer } from 'msw/node' +const server = setupServer( + http.get('/resource', () => new Response()) +) +console.log(typeof server.listen) +`, + }) + + const resolveStdio = await fsMock.exec( + /** + * @note Using the import meta resolve flag + * to enable the "import.meta.resolve" API to see + * what library imports resolve to in Node.js ESM. + */ + 'node --experimental-import-meta-resolve ./resolve.mjs', + ) + expect(resolveStdio.stderr).toBe('') + /** + * @todo Take these expected export paths from package.json. + * That should be the source of truth. + */ + expect(resolveStdio.stdout).toMatch( + /^msw: (.+?)\/node_modules\/msw\/lib\/core\/index\.mjs/m, + ) + expect(resolveStdio.stdout).toMatch( + /^msw\/node: (.+?)\/node_modules\/msw\/lib\/node\/index\.mjs/m, + ) + expect(resolveStdio.stdout).toMatch( + /^msw\/native: (.+?)\/node_modules\/msw\/lib\/native\/index\.mjs/m, + ) + + /** + * @todo Also test the "msw/browser" import that throws, + * saying that the "./browser" export is not defined. + * That's correct, it's exlpicitly set as "browser: null" for Node.js. + */ + + const runtimeStdio = await fsMock.exec('node ./runtime.mjs') + expect(runtimeStdio.stderr).toBe('') + expect(runtimeStdio.stdout).toMatch(/function/m) +}) + +it('runs in a CJS Node.js project', async () => { + await fsMock.create({ + 'resolve.cjs': ` +console.log('msw:', require.resolve('msw')) +console.log('msw/node:', require.resolve('msw/node')) +console.log('msw/native:', require.resolve('msw/native')) +`, + 'runtime.cjs': ` +import { http } from 'msw' +import { setupServer } from 'msw/node' +const server = setupServer( + http.get('/resource', () => new Response()) +) +console.log(typeof server.listen) +`, + }) + + const resolveStdio = await fsMock.exec('node ./resolve.cjs') + expect(resolveStdio.stderr).toBe('') + /** + * @todo Take these expected export paths from package.json. + * That should be the source of truth. + */ + expect(resolveStdio.stdout).toMatch( + /^msw: (.+?)\/node_modules\/msw\/lib\/core\/index\.js/m, + ) + expect(resolveStdio.stdout).toMatch( + /^msw\/node: (.+?)\/node_modules\/msw\/lib\/node\/index\.js/m, + ) + expect(resolveStdio.stdout).toMatch( + /^msw\/native: (.+?)\/node_modules\/msw\/lib\/native\/index\.js/m, + ) + + const runtimeStdio = await fsMock.exec('node ./runtime.mjs') + expect(runtimeStdio.stderr).toBe('') + expect(runtimeStdio.stdout).toMatch(/function/m) +}) diff --git a/test/modules/node/jest.config.js b/test/modules/node/jest.config.js new file mode 100644 index 000000000..5d31469cb --- /dev/null +++ b/test/modules/node/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('jest').Config} */ +module.exports = { + rootDir: '.', + transform: { + '^.+\\.ts$': '@swc/jest', + }, + testEnvironment: 'node', + testTimeout: 60_000, +} diff --git a/test/node/graphql-api/anonymous-operations.test.ts b/test/node/graphql-api/anonymous-operations.test.ts new file mode 100644 index 000000000..8a7643435 --- /dev/null +++ b/test/node/graphql-api/anonymous-operations.test.ts @@ -0,0 +1,108 @@ +/** + * @jest-environment node + */ +import fetch from 'node-fetch' +import { HttpServer } from '@open-draft/test-server/http' +import { HttpResponse, graphql } from 'msw' +import { setupServer } from 'msw/node' + +const httpServer = new HttpServer((app) => { + app.post('/graphql', (req, res) => { + res.json({ + data: { + user: { id: 'abc-123' }, + }, + }) + }) +}) + +const server = setupServer(graphql.query('GetUser', () => {})) + +beforeAll(async () => { + server.listen() + await httpServer.listen() + jest.spyOn(console, 'warn').mockImplementation(() => {}) +}) + +afterEach(() => { + server.resetHandlers() + jest.resetAllMocks() +}) + +afterAll(async () => { + jest.restoreAllMocks() + server.close() + await httpServer.close() +}) + +test('warns on unhandled anonymous GraphQL operations', async () => { + const endpointUrl = httpServer.http.url('/graphql') + const response = await fetch(endpointUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: ` + query { + user { + id + } + } + `, + }), + }) + const json = await response.json() + + // Must receive the original server response. + expect(json).toEqual({ + data: { user: { id: 'abc-123' } }, + }) + + // Must print a warning about the anonymous GraphQL operation. + expect(console.warn).toHaveBeenCalledWith(`\ +[MSW] Failed to intercept a GraphQL request at "POST ${endpointUrl}": 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/operation`) +}) + +test('does not print a warning when using anonymous operation with "graphql.operation()"', async () => { + server.use( + graphql.operation(async ({ query, variables }) => { + return HttpResponse.json({ + data: { + pets: [{ name: 'Tom' }, { name: 'Jerry' }], + }, + }) + }), + ) + + const endpointUrl = httpServer.http.url('/graphql') + const response = await fetch(endpointUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: ` + query { + pets { + name + } + } + `, + }), + }) + const json = await response.json() + + // Must get the mocked response. + expect(json).toEqual({ + data: { + pets: [{ name: 'Tom' }, { name: 'Jerry' }], + }, + }) + + // Must print no warnings: operation is handled and doesn't + // have to be named since we're using "graphql.operation()". + expect(console.warn).not.toHaveBeenCalled() +}) diff --git a/test/node/graphql-api/compatibility.node.test.ts b/test/node/graphql-api/compatibility.node.test.ts index 69baec2e5..e8529f49f 100644 --- a/test/node/graphql-api/compatibility.node.test.ts +++ b/test/node/graphql-api/compatibility.node.test.ts @@ -1,6 +1,6 @@ import fetch from 'cross-fetch' import { graphql as executeGraphql, buildSchema } from 'graphql' -import { graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../support/graphql' @@ -15,10 +15,10 @@ const schema = gql` ` const server = setupServer( - graphql.query('GetUser', async (req, res, ctx) => { + graphql.query('GetUser', async ({ query }) => { const executionResult = await executeGraphql({ schema: buildSchema(schema), - source: req.body.query, + source: query, rootValue: { user: { firstName: 'John', @@ -26,10 +26,10 @@ const server = setupServer( }, }) - return res( - ctx.data(executionResult.data), - ctx.errors(executionResult.errors), - ) + return HttpResponse.json({ + data: executionResult.data, + errors: executionResult.errors, + }) }), ) diff --git a/test/node/graphql-api/cookies.node.test.ts b/test/node/graphql-api/cookies.node.test.ts index b7660bf39..356872569 100644 --- a/test/node/graphql-api/cookies.node.test.ts +++ b/test/node/graphql-api/cookies.node.test.ts @@ -4,7 +4,7 @@ import * as cookieUtils from 'cookie' import fetch from 'node-fetch' import { graphql as executeGraphql, buildSchema } from 'graphql' -import { graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { gql } from '../../support/graphql' @@ -19,10 +19,10 @@ const schema = gql` ` const server = setupServer( - graphql.query('GetUser', async (req, res, ctx) => { + graphql.query('GetUser', async ({ query }) => { const { data, errors } = await executeGraphql({ schema: buildSchema(schema), - source: req.body.query, + source: query, rootValue: { user: { firstName: 'John', @@ -30,10 +30,16 @@ const server = setupServer( }, }) - return res( - ctx.cookie('test-cookie', 'value'), - ctx.data(data), - ctx.errors(errors), + return HttpResponse.json( + { + data, + errors, + }, + { + headers: { + 'Set-Cookie': 'test-cookie=value', + }, + }, ) }), ) diff --git a/test/node/graphql-api/extensions.node.test.ts b/test/node/graphql-api/extensions.node.test.ts index ce6f8ffc8..bd9884a69 100644 --- a/test/node/graphql-api/extensions.node.test.ts +++ b/test/node/graphql-api/extensions.node.test.ts @@ -1,11 +1,11 @@ /** * @jest-environment node */ +import fetch from 'node-fetch' import type { ExecutionResult } from 'graphql' import { buildSchema, graphql as executeGraphql } from 'graphql' -import { graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import fetch from 'node-fetch' import { gql } from '../../support/graphql' const schema = gql` @@ -18,10 +18,10 @@ const schema = gql` ` const server = setupServer( - graphql.query('GetUser', async (req, res, ctx) => { + graphql.query('GetUser', async ({ query }) => { const { data, errors } = await executeGraphql({ schema: buildSchema(schema), - source: req.body.query, + source: query, rootValue: { user: { firstName: 'John', @@ -29,16 +29,16 @@ const server = setupServer( }, }) - return res( - ctx.data(data), - ctx.errors(errors), - ctx.extensions({ + return HttpResponse.json({ + data, + errors, + extensions: { tracking: { version: 1, page: '/test', }, - }), - ) + }, + }) }), ) @@ -69,7 +69,7 @@ test('adds extensions to the original response data', async () => { }) const body: ExecutionResult = await res.json() - expect(res.status).toEqual(200) + expect(res.status).toBe(200) expect(body.data).toEqual({ user: { firstName: 'John', diff --git a/test/node/graphql-api/response-patching.node.test.ts b/test/node/graphql-api/response-patching.node.test.ts index ae3585e38..abf349fa7 100644 --- a/test/node/graphql-api/response-patching.node.test.ts +++ b/test/node/graphql-api/response-patching.node.test.ts @@ -1,20 +1,19 @@ /** * @jest-environment node */ -import { graphql } from 'msw' +import { bypass, graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import fetch from 'cross-fetch' import { graphql as executeGraphql, buildSchema } from 'graphql' import { HttpServer } from '@open-draft/test-server/http' import { createGraphQLClient, gql } from '../../support/graphql' const server = setupServer( - graphql.query('GetUser', async (req, res, ctx) => { - const originalResponse = await ctx.fetch(req) + graphql.query('GetUser', async ({ request }) => { + const originalResponse = await fetch(bypass(request)) const { requestHeaders, queryResult } = await originalResponse.json() - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { firstName: 'Christian', lastName: queryResult.data?.user?.lastName, @@ -22,9 +21,9 @@ const server = setupServer( // Setting the request headers on the response data on purpose // to access them in the response of the Apollo client. requestHeaders, - }), - ctx.errors(queryResult.errors), - ) + }, + errors: queryResult.errors, + }) }), ) @@ -87,7 +86,13 @@ test('patches a GraphQL response', async () => { fetch, }) - const res = await client({ + const res = await client<{ + user: { + firstName: string + lastName: string + } + requestHeaders: Record + }>({ query: gql` query GetUser { user { @@ -107,7 +112,7 @@ test('patches a GraphQL response', async () => { firstName: 'Christian', lastName: 'Maverick', }) - expect(res.data.requestHeaders).toHaveProperty('x-msw-bypass', 'true') - expect(res.data.requestHeaders).not.toHaveProperty('_headers') - expect(res.data.requestHeaders).not.toHaveProperty('_names') + expect(res.data?.requestHeaders).toHaveProperty('x-msw-intention', 'bypass') + expect(res.data?.requestHeaders).not.toHaveProperty('_headers') + expect(res.data?.requestHeaders).not.toHaveProperty('_names') }) diff --git a/test/node/msw-api/context/delay.node.test.ts b/test/node/msw-api/context/delay.node.test.ts index adc685ae1..92c813d5c 100644 --- a/test/node/msw-api/context/delay.node.test.ts +++ b/test/node/msw-api/context/delay.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { delay, HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { performance } from 'perf_hooks' @@ -31,8 +31,9 @@ async function makeRequest(url: string) { test('uses explicit server response time', async () => { server.use( - rest.get('http://localhost/user', (req, res, ctx) => { - return res(ctx.delay(500), ctx.text('john')) + http.get('http://localhost/user', async () => { + await delay(500) + return HttpResponse.text('john') }), ) @@ -44,8 +45,9 @@ test('uses explicit server response time', async () => { test('uses realistic server response time when no duration is provided', async () => { server.use( - rest.get('http://localhost/user', (req, res, ctx) => { - return res(ctx.delay(), ctx.text('john')) + http.get('http://localhost/user', async () => { + await delay() + return HttpResponse.text('john') }), ) @@ -58,8 +60,9 @@ test('uses realistic server response time when no duration is provided', async ( test('uses realistic server response time when "real" mode is provided', async () => { server.use( - rest.get('http://localhost/user', (req, res, ctx) => { - return res(ctx.delay('real'), ctx.text('john')) + http.get('http://localhost/user', async () => { + await delay('real') + return HttpResponse.text('john') }), ) diff --git a/test/node/msw-api/req/passthrough.node.test.ts b/test/node/msw-api/req/passthrough.node.test.ts index 55000e6a5..64ef800c1 100644 --- a/test/node/msw-api/req/passthrough.node.test.ts +++ b/test/node/msw-api/req/passthrough.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, passthrough, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' @@ -40,8 +40,8 @@ afterAll(async () => { it('performs request as-is when returning "req.passthrough" call in the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( - rest.post(endpointUrl, (req) => { - return req.passthrough() + http.post(endpointUrl, () => { + return passthrough() }), ) @@ -57,11 +57,11 @@ it('performs request as-is when returning "req.passthrough" call in the resolver it('does not allow fall-through when returning "req.passthrough" call in the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( - rest.post(endpointUrl, (req) => { - return req.passthrough() + http.post(endpointUrl, () => { + return passthrough() }), - rest.post(endpointUrl, (req, res, ctx) => { - return res(ctx.json({ name: 'Kate' })) + http.post(endpointUrl, () => { + return HttpResponse.json({ name: 'Kate' }) }), ) @@ -74,10 +74,10 @@ it('does not allow fall-through when returning "req.passthrough" call in the res expect(console.warn).not.toHaveBeenCalled() }) -it('prints a warning and performs a request as-is if nothing was returned from the resolver', async () => { +it('performs a request as-is if nothing was returned from the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( - rest.post(endpointUrl, () => { + http.post(endpointUrl, () => { return }), ) @@ -88,12 +88,4 @@ it('prints a warning and performs a request as-is if nothing was returned from t expect(json).toEqual({ name: 'John', }) - - const warning = (console.warn as any as jest.SpyInstance).mock.calls[0][0] - - expect(warning).toContain( - '[MSW] Expected response resolver to return a mocked response Object, but got undefined. The original response is going to be used instead.', - ) - expect(warning).toContain(`POST ${endpointUrl}`) - expect(console.warn).toHaveBeenCalledTimes(1) }) diff --git a/test/node/msw-api/res/network-error.node.test.ts b/test/node/msw-api/res/network-error.node.test.ts index 8de52578f..6caaafaa7 100644 --- a/test/node/msw-api/res/network-error.node.test.ts +++ b/test/node/msw-api/res/network-error.node.test.ts @@ -1,23 +1,20 @@ /** * @jest-environment node */ -import fetch from 'node-fetch' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -const server = setupServer() +const server = setupServer( + http.get('http://example.com/user', () => { + return HttpResponse.error() + }), +) beforeAll(() => server.listen()) afterAll(() => server.close()) test('throws a network error when used with fetch', async () => { - server.use( - rest.get('http://test.io/user', (_, res) => { - return res.networkError('Custom network error message') - }), - ) - - await expect(fetch('http://test.io/user')).rejects.toThrow( - 'Custom network error message', + await expect(fetch('http://example.com/user')).rejects.toThrow( + 'Failed to fetch', ) }) diff --git a/test/node/msw-api/setup-server/input-validation.node.test.ts b/test/node/msw-api/setup-server/input-validation.node.test.ts index 4c9d0cb16..38c0fb342 100644 --- a/test/node/msw-api/setup-server/input-validation.node.test.ts +++ b/test/node/msw-api/setup-server/input-validation.node.test.ts @@ -1,14 +1,14 @@ -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' -test('throws an error given an Array of request handlers to setupServer', async () => { +test('throws an error given an Array of request handlers to setupServer', () => { const createServer = () => { // The next line will be ignored because we want to test that an Error // should be thrown when `setupServer` parameters are not valid // @ts-ignore return setupServer([ - rest.get('https://test.mswjs.io/book/:bookId', (req, res, ctx) => { - return res(ctx.json({ title: 'Original title' })) + http.get('https://test.mswjs.io/book/:bookId', () => { + return HttpResponse.json({ title: 'Original title' }) }), ]) } diff --git a/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts b/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts index 6d49aad74..932fa2d81 100644 --- a/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts +++ b/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' import { waitFor } from '../../../../support/waitFor' @@ -30,45 +30,47 @@ beforeAll(async () => { await httpServer.listen() server.use( - rest.get(httpServer.http.url('/user'), (req, res, ctx) => { - return res(ctx.text('response-body')) + http.get(httpServer.http.url('/user'), () => { + return HttpResponse.text('response-body') }), - rest.post(httpServer.http.url('/no-response'), () => { + http.post(httpServer.http.url('/no-response'), () => { return }), - rest.get(httpServer.http.url('/unhandled-exception'), () => { + http.get(httpServer.http.url('/unhandled-exception'), () => { throw new Error('Unhandled resolver error') }), ) server.listen() - server.events.on('request:start', (req) => { - listener(`[request:start] ${req.method} ${req.url.href} ${req.id}`) + server.events.on('request:start', ({ request, requestId }) => { + listener(`[request:start] ${request.method} ${request.url} ${requestId}`) }) - server.events.on('request:match', (req) => { - listener(`[request:match] ${req.method} ${req.url.href} ${req.id}`) + server.events.on('request:match', ({ request, requestId }) => { + listener(`[request:match] ${request.method} ${request.url} ${requestId}`) }) - server.events.on('request:unhandled', (req) => { - listener(`[request:unhandled] ${req.method} ${req.url.href} ${req.id}`) + server.events.on('request:unhandled', ({ request, requestId }) => { + listener( + `[request:unhandled] ${request.method} ${request.url} ${requestId}`, + ) }) - server.events.on('request:end', (req) => { - listener(`[request:end] ${req.method} ${req.url.href} ${req.id}`) + server.events.on('request:end', ({ request, requestId }) => { + listener(`[request:end] ${request.method} ${request.url} ${requestId}`) }) - server.events.on('response:mocked', (res, requestId) => { - listener(`[response:mocked] ${res.body} ${requestId}`) + server.events.on('response:mocked', async ({ response, requestId }) => { + listener(`[response:mocked] ${await response.text()} ${requestId}`) }) - server.events.on('response:bypass', (res, requestId) => { - listener(`[response:bypass] ${res.body} ${requestId}`) + server.events.on('response:bypass', async ({ response, requestId }) => { + listener(`[response:bypass] ${await response.text()} ${requestId}`) }) - server.events.on('unhandledException', (error, req) => { + server.events.on('unhandledException', ({ error, request, requestId }) => { listener( - `[unhandledException] ${req.method} ${req.url.href} ${req.id} ${error.message}`, + `[unhandledException] ${request.method} ${request.url} ${requestId} ${error.message}`, ) }) }) @@ -93,6 +95,12 @@ test('emits events for a handler request and mocked response', async () => { await fetch(url) const requestId = getRequestId(listener) + await waitFor(() => { + expect(listener).toHaveBeenCalledWith( + expect.stringContaining('[response:mocked]'), + ) + }) + expect(listener).toHaveBeenNthCalledWith( 1, `[request:start] GET ${url} ${requestId}`, diff --git a/test/node/msw-api/setup-server/life-cycle-events/removeAllListeners.node.test.ts b/test/node/msw-api/setup-server/life-cycle-events/removeAllListeners.node.test.ts index e36722aa1..bc96068ef 100644 --- a/test/node/msw-api/setup-server/life-cycle-events/removeAllListeners.node.test.ts +++ b/test/node/msw-api/setup-server/life-cycle-events/removeAllListeners.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' @@ -18,8 +18,8 @@ beforeAll(async () => { await httpServer.listen() server.use( - rest.get(httpServer.http.url('/user'), (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get(httpServer.http.url('/user'), () => { + return HttpResponse.json({ firstName: 'John' }) }), ) server.listen() diff --git a/test/node/msw-api/setup-server/life-cycle-events/removeListener.node.test.ts b/test/node/msw-api/setup-server/life-cycle-events/removeListener.node.test.ts index a50299be6..bf40845b9 100644 --- a/test/node/msw-api/setup-server/life-cycle-events/removeListener.node.test.ts +++ b/test/node/msw-api/setup-server/life-cycle-events/removeListener.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' @@ -18,8 +18,8 @@ beforeAll(async () => { await httpServer.listen() server.use( - rest.get(httpServer.http.url('/user'), (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get(httpServer.http.url('/user'), () => { + return HttpResponse.json({ firstName: 'John' }) }), ) server.listen() diff --git a/test/node/msw-api/setup-server/listHandlers.node.test.ts b/test/node/msw-api/setup-server/listHandlers.node.test.ts index 2abe6f41f..c542579ac 100644 --- a/test/node/msw-api/setup-server/listHandlers.node.test.ts +++ b/test/node/msw-api/setup-server/listHandlers.node.test.ts @@ -1,14 +1,14 @@ /** * @jest-environment node */ -import { rest, graphql } from 'msw' +import { http, graphql } from 'msw' import { setupServer } from 'msw/node' const resolver = () => null const github = graphql.link('https://api.github.com') const server = setupServer( - rest.get('https://test.mswjs.io/book/:bookId', resolver), + http.get('https://test.mswjs.io/book/:bookId', resolver), graphql.query('GetUser', resolver), graphql.mutation('UpdatePost', resolver), graphql.operation(resolver), @@ -58,7 +58,7 @@ test('forbids from modifying the list of handlers', () => { test('includes runtime request handlers when listing handlers', () => { server.use( - rest.get('https://test.mswjs.io/book/:bookId', resolver), + http.get('https://test.mswjs.io/book/:bookId', resolver), graphql.query('GetRandomNumber', resolver), ) diff --git a/test/node/msw-api/setup-server/printHandlers.node.test.ts b/test/node/msw-api/setup-server/printHandlers.node.test.ts deleted file mode 100644 index bb812cd70..000000000 --- a/test/node/msw-api/setup-server/printHandlers.node.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @jest-environment node - */ -import { bold } from 'chalk' -import { rest, graphql } from 'msw' -import { setupServer } from 'msw/node' - -const resolver = () => void 0 - -const github = graphql.link('https://api.github.com') - -const server = setupServer( - rest.get('https://test.mswjs.io/book/:bookId', resolver), - graphql.query('GetUser', resolver), - graphql.mutation('UpdatePost', resolver), - graphql.operation(resolver), - github.query('GetRepo', resolver), - github.operation(resolver), -) - -beforeAll(() => { - server.listen() -}) - -beforeEach(() => { - jest.spyOn(global.console, 'log').mockImplementation() -}) - -afterEach(() => { - jest.restoreAllMocks() - server.resetHandlers() -}) - -afterAll(() => { - server.close() -}) - -test('lists all current request handlers', () => { - server.printHandlers() - - // Test failed here, commenting so it shows up in the PR - expect(console.log).toBeCalledTimes(6) - - expect(console.log).toBeCalledWith(`\ -${bold('[rest] GET https://test.mswjs.io/book/:bookId')} - Declaration: ${__filename}:13:8 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] query GetUser (origin: *)')} - Declaration: ${__filename}:14:11 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] mutation UpdatePost (origin: *)')} - Declaration: ${__filename}:15:11 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] all (origin: *)')} - Declaration: ${__filename}:16:11 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] query GetRepo (origin: https://api.github.com)')} - Declaration: ${__filename}:17:10 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] all (origin: https://api.github.com)')} - Declaration: ${__filename}:18:10 -`) -}) - -test('respects runtime request handlers when listing handlers', () => { - server.use( - rest.get('https://test.mswjs.io/book/:bookId', resolver), - graphql.query('GetRandomNumber', resolver), - ) - - server.printHandlers() - - // Runtime handlers are prepended to the list of handlers - // and they DON'T remove the handlers they may override. - expect(console.log).toBeCalledTimes(8) - - expect(console.log).toBeCalledWith(`\ -${bold('[rest] GET https://test.mswjs.io/book/:bookId')} - Declaration: ${__filename}:77:10 -`) - - expect(console.log).toBeCalledWith(`\ -${bold('[graphql] query GetRandomNumber (origin: *)')} - Declaration: ${__filename}:78:13 -`) -}) diff --git a/test/node/msw-api/setup-server/resetHandlers.node.test.ts b/test/node/msw-api/setup-server/resetHandlers.node.test.ts index 4173eb4fb..822da11af 100644 --- a/test/node/msw-api/setup-server/resetHandlers.node.test.ts +++ b/test/node/msw-api/setup-server/resetHandlers.node.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('https://test.mswjs.io/books', (req, res, ctx) => { - return res(ctx.json({ title: 'Original title' })) + http.get('https://test.mswjs.io/books', () => { + return HttpResponse.json({ title: 'Original title' }) }), ) @@ -23,8 +23,8 @@ afterAll(() => { test('removes all runtime request handlers when resetting without explicit next handlers', async () => { server.use( - rest.post('https://test.mswjs.io/login', (req, res, ctx) => { - return res(ctx.json({ accepted: true })) + http.post('https://test.mswjs.io/login', () => { + return HttpResponse.json({ accepted: true }) }), ) @@ -53,16 +53,16 @@ test('removes all runtime request handlers when resetting without explicit next test('replaces all handlers with the explicit next runtime handlers upon reset', async () => { server.use( - rest.post('https://test.mswjs.io/login', (req, res, ctx) => { - return res(ctx.json({ accepted: true })) + http.post('https://test.mswjs.io/login', () => { + return HttpResponse.json({ accepted: true }) }), ) // Once reset with explicit next requets handlers, // replaces all present requets handlers with those. server.resetHandlers( - rest.get('https://test.mswjs.io/products', (req, res, ctx) => { - return res(ctx.json([1, 2, 3])) + http.get('https://test.mswjs.io/products', () => { + return HttpResponse.json([1, 2, 3]) }), ) diff --git a/test/node/msw-api/setup-server/restoreHandlers.node.test.ts b/test/node/msw-api/setup-server/restoreHandlers.node.test.ts index b8862c925..bd2c6a2b8 100644 --- a/test/node/msw-api/setup-server/restoreHandlers.node.test.ts +++ b/test/node/msw-api/setup-server/restoreHandlers.node.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('https://test.mswjs.io/book/:bookId', (req, res, ctx) => { - return res(ctx.json({ title: 'Original title' })) + http.get('https://test.mswjs.io/book/:bookId', () => { + return HttpResponse.json({ title: 'Original title' }) }), ) @@ -16,9 +16,13 @@ afterAll(() => server.close()) test('returns a mocked response from the used one-time request handler when restored', async () => { server.use( - rest.get('https://test.mswjs.io/book/:bookId', (req, res, ctx) => { - return res.once(ctx.json({ title: 'Overridden title' })) - }), + http.get( + 'https://test.mswjs.io/book/:bookId', + () => { + return HttpResponse.json({ title: 'Overridden title' }) + }, + { once: true }, + ), ) const firstResponse = await fetch('https://test.mswjs.io/book/abc-123') diff --git a/test/node/msw-api/setup-server/scenarios/cookies-request.node.test.ts b/test/node/msw-api/setup-server/scenarios/cookies-request.node.test.ts index 66e573962..ea39cb68e 100644 --- a/test/node/msw-api/setup-server/scenarios/cookies-request.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/cookies-request.node.test.ts @@ -2,28 +2,21 @@ * @jest-environment node */ import https from 'https' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer, SetupServerApi } from 'msw/node' -import { HttpServer } from '@open-draft/test-server/http' +import { httpsAgent, HttpServer } from '@open-draft/test-server/http' import { waitForClientRequest } from '../../../../support/utils' -let server: SetupServerApi - const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.json({ works: false }) }) }) +const server = setupServer() + beforeAll(async () => { await httpServer.listen() - - server = setupServer( - rest.get(httpServer.https.url('/user'), (req, res, ctx) => { - return res(ctx.json({ cookies: req.cookies })) - }), - ) - server.listen() }) @@ -33,15 +26,25 @@ afterAll(async () => { }) test('has access to request cookies', async () => { - const url = new URL(httpServer.https.url('/user')) + const endpointUrl = httpServer.https.url('/user') + + server.use( + http.get(endpointUrl, ({ cookies }) => { + return HttpResponse.json({ cookies }) + }), + ) + + const url = new URL(endpointUrl) const request = https.get({ protocol: url.protocol, - host: url.host, + hostname: url.hostname, path: url.pathname, + port: url.port, headers: { Cookie: 'auth-token=abc-123', }, + agent: httpsAgent, }) const { responseText } = await waitForClientRequest(request) diff --git a/test/node/msw-api/setup-server/scenarios/custom-transformers.node.test.ts b/test/node/msw-api/setup-server/scenarios/custom-transformers.node.test.ts index 39bc1099e..f0aec6dae 100644 --- a/test/node/msw-api/setup-server/scenarios/custom-transformers.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/custom-transformers.node.test.ts @@ -1,22 +1,20 @@ import fetch from 'node-fetch' import * as JSONbig from 'json-bigint' -import { ResponseTransformer, compose, context, rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -const jsonBig = (body: Record): ResponseTransformer => { - return compose( - context.set('Content-Type', 'application/json'), - context.body(JSONbig.stringify(body)), - ) -} - const server = setupServer( - rest.get('http://test.mswjs.io/me', (req, res) => { - return res( - jsonBig({ + http.get('http://test.mswjs.io/me', () => { + return new HttpResponse( + JSONbig.stringify({ username: 'john.maverick', balance: BigInt(1597928668063727616), }), + { + headers: { + 'Content-Tpye': 'application/json', + }, + }, ) }), ) diff --git a/test/node/msw-api/setup-server/scenarios/fake-timers.node.test.ts b/test/node/msw-api/setup-server/scenarios/fake-timers.node.test.ts index a6835f560..15caee6e2 100644 --- a/test/node/msw-api/setup-server/scenarios/fake-timers.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/fake-timers.node.test.ts @@ -1,10 +1,10 @@ import fetch from 'node-fetch' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const server = setupServer( - rest.get('https://test.mswjs.io/pull', (req, res, ctx) => { - return res(ctx.json({ status: 'pulled' })) + http.get('https://test.mswjs.io/pull', () => { + return HttpResponse.json({ status: 'pulled' }) }), ) diff --git a/test/node/msw-api/setup-server/scenarios/fall-through.node.test.ts b/test/node/msw-api/setup-server/scenarios/fall-through.node.test.ts index e1f128141..26f84b4f7 100644 --- a/test/node/msw-api/setup-server/scenarios/fall-through.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/fall-through.node.test.ts @@ -2,31 +2,32 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const log = jest.fn() const server = setupServer( - rest.get('https://test.mswjs.io/*', () => log('[get] first')), - rest.get('https://test.mswjs.io/us*', () => log('[get] second')), - rest.get('https://test.mswjs.io/user', (req, res, ctx) => - res(ctx.json({ firstName: 'John' })), - ), - rest.get('https://test.mswjs.io/user', () => log('[get] third')), - - rest.post('https://test.mswjs.io/blog/*', () => log('[post] first')), - rest.post('https://test.mswjs.io/blog/article', () => log('[post] second')), + http.get('https://test.mswjs.io/*', () => log('[get] first')), + http.get('https://test.mswjs.io/us*', () => log('[get] second')), + http.get('https://test.mswjs.io/user', () => { + return HttpResponse.json({ firstName: 'John' }) + }), + http.get('https://test.mswjs.io/user', () => log('[get] third')), + + http.post('https://test.mswjs.io/blog/*', () => log('[post] first')), + http.post('https://test.mswjs.io/blog/article', () => log('[post] second')), ) beforeAll(() => { - // Supress the "Expeted mocking resolver function to return a mocked response" warnings. - jest.spyOn(global.console, 'warn').mockImplementation() server.listen() }) +afterEach(() => { + jest.resetAllMocks() +}) + afterAll(() => { - jest.restoreAllMocks() server.close() }) @@ -37,8 +38,8 @@ test('falls through all relevant request handlers until response is returned', a expect(body).toEqual({ firstName: 'John', }) - expect(log).toBeCalledWith('[get] first') - expect(log).toBeCalledWith('[get] second') + expect(log).toHaveBeenNthCalledWith(1, '[get] first') + expect(log).toHaveBeenNthCalledWith(2, '[get] second') expect(log).not.toBeCalledWith('[get] third') }) @@ -49,6 +50,6 @@ test('falls through all relevant handlers even if none return response', async ( const { status } = res expect(status).toBe(404) - expect(log).toBeCalledWith('[post] first') - expect(log).toBeCalledWith('[post] second') + expect(log).toHaveBeenNthCalledWith(1, '[post] first') + expect(log).toHaveBeenNthCalledWith(2, '[post] second') }) diff --git a/test/node/msw-api/setup-server/scenarios/fetch.node.test.ts b/test/node/msw-api/setup-server/scenarios/fetch.node.test.ts index 4abc9b184..64af804a1 100644 --- a/test/node/msw-api/setup-server/scenarios/fetch.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/fetch.node.test.ts @@ -1,94 +1,68 @@ -/** - * @jest-environment node - */ -import fetch, { Response } from 'node-fetch' -import { rest } from 'msw' +import fetch from 'node-fetch' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' -describe('setupServer / fetch', () => { - const server = setupServer( - rest.get('http://test.mswjs.io', (req, res, ctx) => { - return res( - ctx.status(401), - ctx.set('x-header', 'yes'), - ctx.json({ - firstName: 'John', - age: 32, - }), - ) - }), - rest.post('https://test.mswjs.io', (req, res, ctx) => { - return res( - ctx.status(403), - ctx.set('x-header', 'yes'), - ctx.json(req.body as Record), - ) - }), - ) - - beforeAll(() => { - server.listen() - }) - - afterAll(() => { - server.close() - }) - - describe('given I perform a GET request using fetch', () => { - let res: Response - - beforeAll(async () => { - res = await fetch('http://test.mswjs.io') - }) - - test('should return mocked status code', async () => { - expect(res.status).toEqual(401) - }) - - test('should return mocked headers', () => { - expect(res.headers.get('content-type')).toEqual('application/json') - expect(res.headers.get('x-header')).toEqual('yes') - }) - - test('should return mocked body', async () => { - const body = await res.json() - - expect(body).toEqual({ +const server = setupServer( + http.get('http://test.mswjs.io', () => { + return HttpResponse.json( + { firstName: 'John', age: 32, - }) - }) - }) - - describe('given I perform a POST request using fetch', () => { - let res: Response - - beforeAll(async () => { - res = await fetch('https://test.mswjs.io', { - method: 'POST', + }, + { + status: 401, headers: { - 'Content-Type': 'application/json', + 'X-Header': 'yes', }, - body: JSON.stringify({ - payload: 'info', - }), - }) + }, + ) + }), + http.post('https://test.mswjs.io', async ({ request }) => { + return HttpResponse.json(await request.json(), { + status: 403, + headers: { + 'X-Header': 'yes', + }, }) + }), +) - test('should return mocked status code', () => { - expect(res.status).toEqual(403) - }) +beforeAll(() => { + server.listen() +}) - test('should return mocked headers', () => { - expect(res.headers.get('content-type')).toEqual('application/json') - expect(res.headers.get('x-header')).toEqual('yes') - }) +afterAll(() => { + server.close() +}) - test('should return mocked and parsed JSON body', async () => { - const body = await res.json() - expect(body).toEqual({ - payload: 'info', - }) - }) +it('returns a mocked response to a GET request using fetch', async () => { + const res = await fetch('http://test.mswjs.io') + + expect(res.status).toEqual(401) + expect(res.headers.get('content-type')).toEqual('application/json') + expect(res.headers.get('x-header')).toEqual('yes') + + expect(await res.json()).toEqual({ + firstName: 'John', + age: 32, + }) +}) + +it('returns a mocked response to a POST request using fetch', async () => { + const res = await fetch('https://test.mswjs.io', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + payload: 'info', + }), + }) + + expect(res.status).toEqual(403) + expect(res.headers.get('content-type')).toEqual('application/json') + expect(res.headers.get('x-header')).toEqual('yes') + expect(await res.json()).toEqual({ + payload: 'info', }) }) diff --git a/test/node/msw-api/setup-server/scenarios/generator.node.test.ts b/test/node/msw-api/setup-server/scenarios/generator.node.test.ts index 974cc62c7..bc4dd8a81 100644 --- a/test/node/msw-api/setup-server/scenarios/generator.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/generator.node.test.ts @@ -2,63 +2,56 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get( + http.get<{ maxCount: string }>( 'https://example.com/polling/:maxCount', - function* (req, res, ctx) { - const maxCount = parseInt(req.params.maxCount) + function* ({ params }) { + const maxCount = parseInt(params.maxCount) let count = 0 while (count < maxCount) { count += 1 - yield res( - ctx.json({ - status: 'pending', - count, - }), - ) + yield HttpResponse.json({ + status: 'pending', + count, + }) } - return res( - ctx.json({ - status: 'complete', - count, - }), - ) + return HttpResponse.json({ + status: 'complete', + count, + }) }, ), - rest.get( + http.get<{ maxCount: string }>( 'https://example.com/polling/once/:maxCount', - function* (req, res, ctx) { - const maxCount = parseInt(req.params.maxCount) + function* ({ params }) { + const maxCount = parseInt(params.maxCount) let count = 0 while (count < maxCount) { count += 1 - yield res( - ctx.json({ - status: 'pending', - count, - }), - ) + yield HttpResponse.json({ + status: 'pending', + count, + }) } - return res.once( - ctx.json({ - status: 'complete', - count, - }), - ) + return HttpResponse.json({ + status: 'complete', + count, + }) }, + { once: true }, ), - rest.get( + http.get<{ maxCount: string }>( 'https://example.com/polling/once/:maxCount', - (req, res, ctx) => { - return res(ctx.json({ status: 'done' })) + () => { + return HttpResponse.json({ status: 'done' }) }, ), ) @@ -81,7 +74,6 @@ test('supports generator as the response resolver', async () => { const res = await fetch('https://example.com/polling/3') const body = await res.json() expect(res.status).toBe(200) - expect(res.headers.get('x-powered-by')).toEqual('msw') expect(body).toEqual(expectedBody) } @@ -108,7 +100,6 @@ test('supports one-time handlers with the generator as the response resolver', a const res = await fetch('https://example.com/polling/once/3') const body = await res.json() expect(res.status).toBe(200) - expect(res.headers.get('x-powered-by')).toEqual('msw') expect(body).toEqual(expectedBody) } diff --git a/test/node/msw-api/setup-server/scenarios/graphql.node.test.ts b/test/node/msw-api/setup-server/scenarios/graphql.node.test.ts index 5a88dc6a0..4d9d99e55 100644 --- a/test/node/msw-api/setup-server/scenarios/graphql.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/graphql.node.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetch from 'cross-fetch' -import { graphql } from 'msw' +import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../../../support/graphql' @@ -30,24 +30,24 @@ const LOGIN = gql` ` const server = setupServer( - graphql.query('GetUserDetail', (req, res, ctx) => { - const { userId } = req.variables + graphql.query('GetUserDetail', ({ variables }) => { + const { userId } = variables - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { id: userId, firstName: 'John', age: 32, }, - }), - ) + }, + }) }), - graphql.mutation('Login', (req, res, ctx) => { - const { username } = req.variables + graphql.mutation('Login', ({ variables }) => { + const { username } = variables - return res( - ctx.errors([ + return HttpResponse.json({ + errors: [ { message: `User "${username}" is not found`, locations: [ @@ -57,8 +57,8 @@ const server = setupServer( }, ], }, - ]), - ) + ], + }) }), ) diff --git a/test/node/msw-api/setup-server/scenarios/http.node.test.ts b/test/node/msw-api/setup-server/scenarios/http.node.test.ts index b0359f2df..aeca32c49 100644 --- a/test/node/msw-api/setup-server/scenarios/http.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/http.node.test.ts @@ -1,13 +1,9 @@ /** * @jest-environment node */ -/** - * @note Do not import as wildcard lest the ESM gods be displeased. - * Make sure "allowSyntheticDefaultImports" is true in tsconfig.json. - */ -import http from 'http' +import nodeHttp from 'http' import { HttpServer } from '@open-draft/test-server/http' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { waitForClientRequest } from '../../../../support/utils' @@ -26,11 +22,15 @@ beforeAll(async () => { beforeEach(() => { server.use( - rest.get(httpServer.http.url('/resource'), (req, res, ctx) => { - return res( - ctx.status(401), - ctx.set('x-header', 'yes'), - ctx.json({ firstName: 'John' }), + http.get(httpServer.http.url('/resource'), () => { + return HttpResponse.json( + { firstName: 'John' }, + { + status: 401, + headers: { + 'x-header': 'yes', + }, + }, ) }), ) @@ -46,7 +46,7 @@ afterAll(async () => { }) it('returns a mocked response to an "http.get" request', async () => { - const request = http.get(httpServer.http.url('/resource')) + const request = nodeHttp.get(httpServer.http.url('/resource')) const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) @@ -60,7 +60,7 @@ it('returns a mocked response to an "http.get" request', async () => { }) it('returns a mocked response to an "http.request" request', async () => { - const request = http.request(httpServer.http.url('/resource')) + const request = nodeHttp.request(httpServer.http.url('/resource')) request.end() const { response, responseText } = await waitForClientRequest(request) diff --git a/test/node/msw-api/setup-server/scenarios/https.node.test.ts b/test/node/msw-api/setup-server/scenarios/https.node.test.ts index c7ee22d52..32eee39ad 100644 --- a/test/node/msw-api/setup-server/scenarios/https.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/https.node.test.ts @@ -2,8 +2,8 @@ * @jest-environment node */ import https from 'https' -import { HttpServer } from '@open-draft/test-server/http' -import { rest } from 'msw' +import { HttpServer, httpsAgent } from '@open-draft/test-server/http' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { waitForClientRequest } from '../../../../support/utils' @@ -22,11 +22,17 @@ beforeAll(async () => { beforeEach(() => { server.use( - rest.get(httpServer.https.url('/resource'), (req, res, ctx) => { - return res( - ctx.status(401), - ctx.set('x-header', 'yes'), - ctx.json({ firstName: 'John' }), + http.get(httpServer.https.url('/resource'), () => { + return HttpResponse.json( + { + firstName: 'John', + }, + { + status: 401, + headers: { + 'X-Header': 'yes', + }, + }, ) }), ) @@ -42,7 +48,9 @@ afterAll(async () => { }) it('returns a mocked response to an "https.get" request', async () => { - const request = https.get(httpServer.https.url('/resource')) + const request = https.get(httpServer.https.url('/resource'), { + agent: httpsAgent, + }) const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) @@ -56,7 +64,9 @@ it('returns a mocked response to an "https.get" request', async () => { }) it('returns a mocked response to an "https.request" request', async () => { - const request = https.request(httpServer.https.url('/resource')) + const request = https.request(httpServer.https.url('/resource'), { + agent: httpsAgent, + }) request.end() const { response, responseText } = await waitForClientRequest(request) diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/bypass.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/bypass.node.test.ts index a989d8b37..d35478e09 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/bypass.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/bypass.node.test.ts @@ -4,7 +4,7 @@ import fetch from 'node-fetch' import { HttpServer } from '@open-draft/test-server/http' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const httpServer = new HttpServer((app) => { app.get('/', (req, res) => { @@ -21,8 +21,8 @@ beforeAll(async () => { await httpServer.listen() server.use( - rest.get(httpServer.http.url('/user'), (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get(httpServer.http.url('/user'), () => { + return HttpResponse.json({ firstName: 'John' }) }), ) server.listen({ onUnhandledRequest: 'bypass' }) @@ -41,7 +41,7 @@ test('bypasses unhandled requests', async () => { const res = await fetch(httpServer.http.url('/')) // Request should be performed as-is - expect(res.status).toEqual(200) + expect(res.status).toBe(200) expect(await res.text()).toEqual('root') // No warnings/errors should be printed diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts index 36507e7a1..c941910c3 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts @@ -3,28 +3,36 @@ */ import fetch from 'node-fetch' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const server = setupServer( - rest.get('https://test.mswjs.io/user', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('https://test.mswjs.io/user', () => { + return HttpResponse.json({ firstName: 'John' }) }), ) beforeAll(() => server.listen({ - onUnhandledRequest(req) { - throw new Error(`Custom error for ${req.method} ${req.url}`) + onUnhandledRequest(request) { + /** + * @fixme @todo For some reason, the exception from the "onUnhandledRequest" + * callback doesn't propagate to the intercepted request but instead is thrown + * in this test's context. + */ + throw new Error(`Custom error for ${request.method} ${request.url}`) }, }), ) -afterAll(() => server.close()) + +afterAll(() => { + server.close() +}) test('prevents a request when a custom callback throws an exception', async () => { - const getResponse = () => fetch('https://test.mswjs.io') + const getResponse = () => fetch('https://example.com') // Request should be cancelled with a fetch error, since the callback threw. await expect(getResponse()).rejects.toThrow( - 'request to https://test.mswjs.io/ failed, reason: Custom error for GET https://test.mswjs.io/', + 'request to https://example.com/ failed, reason: Custom error for GET https://example.com/', ) }) diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback.node.test.ts index 251c652a9..13799b6fe 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback.node.test.ts @@ -3,15 +3,15 @@ */ import fetch from 'node-fetch' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const server = setupServer( - rest.get('https://test.mswjs.io/user', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('https://test.mswjs.io/user', () => { + return HttpResponse.json({ firstName: 'John' }) }), ) -const logs = [] +const logs: Array = [] beforeAll(() => server.listen({ diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts index 1e7103e74..460c85e6b 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts @@ -3,11 +3,11 @@ */ import fetch from 'node-fetch' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const server = setupServer( - rest.get('https://test.mswjs.io/user', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('https://test.mswjs.io/user', () => { + return HttpResponse.json({ firstName: 'John' }) }), ) @@ -30,7 +30,7 @@ test('warns on unhandled requests by default', async () => { expect(console.error).not.toBeCalled() expect(console.warn).toBeCalledWith(`\ -[MSW] Warning: captured a request without a matching request handler: +[MSW] Warning: intercepted a request without a matching request handler: • GET https://test.mswjs.io/ diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts index 7d87e0cdb..c6bb16f9f 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts @@ -3,7 +3,7 @@ */ import fetch from 'node-fetch' import { HttpServer } from '@open-draft/test-server/http' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { @@ -23,15 +23,15 @@ beforeAll(async () => { await httpServer.listen() server.use( - rest.get(httpServer.http.url('/user'), (req, res, ctx) => { - return res(ctx.json({ mocked: true })) + http.get(httpServer.http.url('/user'), () => { + return HttpResponse.json({ mocked: true }) }), - rest.post(httpServer.http.url('/explicit-return'), () => { + http.post(httpServer.http.url('/explicit-return'), () => { // Short-circuiting in a handler makes it perform the request as-is, // but still treats this request as handled. return }), - rest.post(httpServer.http.url('/implicit-return'), () => { + http.post(httpServer.http.url('/implicit-return'), () => { // The handler that has no return also performs the request as-is, // still treating this request as handled. }), @@ -62,7 +62,7 @@ test('errors on unhandled request when using the "error" value', async () => { `request to ${endpointUrl} failed, reason: [MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.`, ) expect(console.error) - .toHaveBeenCalledWith(`[MSW] Error: captured a request without a matching request handler: + .toHaveBeenCalledWith(`[MSW] Error: intercepted a request without a matching request handler: • GET ${endpointUrl} diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/warn.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/warn.node.test.ts index f88f1472a..28fd440f9 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/warn.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/warn.node.test.ts @@ -3,11 +3,11 @@ */ import fetch from 'node-fetch' import { setupServer } from 'msw/node' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' const server = setupServer( - rest.get('https://test.mswjs.io/user', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) + http.get('https://test.mswjs.io/user', () => { + return HttpResponse.json({ firstName: 'John' }) }), ) @@ -26,7 +26,7 @@ test('warns on unhandled request when using the "warn" value', async () => { expect(res).toHaveProperty('status', 404) expect(console.warn).toBeCalledWith(`\ -[MSW] Warning: captured a request without a matching request handler: +[MSW] Warning: intercepted a request without a matching request handler: • GET https://test.mswjs.io/ diff --git a/test/node/msw-api/setup-server/scenarios/relative-url.node.test.ts b/test/node/msw-api/setup-server/scenarios/relative-url.node.test.ts index 042232a53..85b1f37fe 100644 --- a/test/node/msw-api/setup-server/scenarios/relative-url.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/relative-url.node.test.ts @@ -2,19 +2,20 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('/books', (req, res, ctx) => { - return res(ctx.json([1, 2, 3])) + http.get('/books', () => { + return HttpResponse.json([1, 2, 3]) }), - rest.get('https://api.backend.com/path', (req, res, ctx) => { - return res(ctx.json({ success: true })) + http.get('https://api.backend.com/path', () => { + return HttpResponse.json({ success: true }) }), ) beforeAll(() => server.listen()) + afterAll(() => server.close()) test('tolerates relative request handlers on the server', async () => { diff --git a/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts b/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts index 7931aed34..0c9891509 100644 --- a/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts @@ -1,9 +1,8 @@ /** * @jest-environment node */ -import fetch from 'node-fetch' import { HttpServer } from '@open-draft/test-server/http' -import { rest } from 'msw' +import { HttpResponse, http, bypass } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { @@ -21,45 +20,42 @@ interface ResponseBody { } const server = setupServer( - rest.get( - 'https://test.mswjs.io/user', - async (req, res, ctx) => { - const originalResponse = await ctx.fetch(httpServer.http.url('/user')) - const body = await originalResponse.json() - - return res( - ctx.json({ - id: body.id, - mocked: true, - }), - ) - }, - ), - rest.get( - 'https://test.mswjs.io/complex-request', - async (req, res, ctx) => { - const shouldBypass = req.url.searchParams.get('bypass') === 'true' - const performRequest = shouldBypass - ? () => - ctx - .fetch(httpServer.http.url('/user'), { method: 'POST' }) - .then((res) => res.json()) - : () => - fetch('https://httpbin.org/post', { method: 'POST' }).then((res) => - res.json(), - ) - const originalResponse = await performRequest() - - return res( - ctx.json({ - id: originalResponse.id, - mocked: true, - }), - ) - }, - ), - rest.post('https://httpbin.org/post', (req, res, ctx) => { - return res(ctx.json({ id: 303 })) + http.get('https://test.mswjs.io/user', async () => { + const originalResponse = await fetch(bypass(httpServer.http.url('/user'))) + const body = await originalResponse.json() + + return HttpResponse.json({ + id: body.id, + mocked: true, + }) + }), + http.get('https://test.mswjs.io/complex-request', async ({ request }) => { + const url = new URL(request.url) + + const shouldBypass = url.searchParams.get('bypass') === 'true' + const performRequest = shouldBypass + ? () => + fetch( + bypass( + new Request(httpServer.http.url('/user'), { + method: 'POST', + }), + ), + ).then((res) => res.json()) + : () => + fetch('https://httpbin.org/post', { method: 'POST' }).then((res) => + res.json(), + ) + + const originalResponse = await performRequest() + + return HttpResponse.json({ + id: originalResponse.id, + mocked: true, + }) + }), + http.post('https://httpbin.org/post', () => { + return HttpResponse.json({ id: 303 }) }), ) @@ -79,22 +75,20 @@ afterAll(async () => { test('returns a combination of mocked and original responses', async () => { const res = await fetch('https://test.mswjs.io/user') - const { status, headers } = res + const { status } = res const body = await res.json() expect(status).toBe(200) - expect(headers.get('x-powered-by')).toBe('msw') expect(body).toEqual({ id: 101, mocked: true, }) }) -test('bypasses a mocked request when using "ctx.fetch"', async () => { +test('bypasses a mocked request when using "bypass()"', async () => { const res = await fetch('https://test.mswjs.io/complex-request?bypass=true') expect(res.status).toBe(200) - expect(res.headers.get('x-powered-by')).toBe('msw') expect(await res.json()).toEqual({ id: 202, mocked: true, @@ -105,7 +99,6 @@ test('falls into the mocked request when using "fetch" directly', async () => { const res = await fetch('https://test.mswjs.io/complex-request') expect(res.status).toBe(200) - expect(res.headers.get('x-powered-by')).toBe('msw') expect(await res.json()).toEqual({ id: 303, mocked: true, diff --git a/test/node/msw-api/setup-server/scenarios/xhr.node.test.ts b/test/node/msw-api/setup-server/scenarios/xhr.node.test.ts index 25042d9ed..ea98d859c 100644 --- a/test/node/msw-api/setup-server/scenarios/xhr.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/xhr.node.test.ts @@ -1,19 +1,25 @@ /** * @jest-environment jsdom */ -import { rest } from 'msw' +import { http } from 'msw' import { setupServer } from 'msw/node' import { stringToHeaders } from 'headers-polyfill' const server = setupServer( - rest.get('http://test.mswjs.io', (req, res, ctx) => { - return res( - ctx.status(401), - ctx.set('x-header', 'yes'), - ctx.json({ + http.get('http://localhost:3001/resource', ({ request }) => { + return new Response( + JSON.stringify({ firstName: 'John', age: 32, }), + { + status: 401, + statusText: 'Unauthorized', + headers: { + 'Content-Type': 'application/json', + 'X-Header': 'yes', + }, + }, ) }), ) @@ -33,7 +39,7 @@ describe('given I perform an XMLHttpRequest', () => { beforeAll((done) => { const req = new XMLHttpRequest() - req.open('GET', 'http://test.mswjs.io') + req.open('GET', 'http://localhost:3001/resource') req.onload = function () { statusCode = this.status body = JSON.parse(this.response) diff --git a/test/node/msw-api/setup-server/use.node.test.ts b/test/node/msw-api/setup-server/use.node.test.ts index bbd22e610..8e74db56d 100644 --- a/test/node/msw-api/setup-server/use.node.test.ts +++ b/test/node/msw-api/setup-server/use.node.test.ts @@ -2,8 +2,8 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' -import { setupServer, SetupServerApi } from 'msw/node' +import { HttpResponse, http } from 'msw' +import { SetupServer, setupServer } from 'msw/node' import { RequestHandler as ExpressRequestHandler } from 'express' import { HttpServer } from '@open-draft/test-server/http' @@ -15,16 +15,17 @@ const httpServer = new HttpServer((app) => { app.post('/login', handler) }) -let server: SetupServerApi +let server: SetupServer beforeAll(async () => { await httpServer.listen() server = setupServer( - rest.get(httpServer.http.url('/book/:bookId'), (req, res, ctx) => { - return res(ctx.json({ title: 'Original title' })) + http.get<{ bookId: string }>(httpServer.http.url('/book/:bookId'), () => { + return HttpResponse.json({ title: 'Original title' }) }), ) + server.listen() }) @@ -39,8 +40,8 @@ afterAll(async () => { test('returns a mocked response from a runtime request handler upon match', async () => { server.use( - rest.post(httpServer.http.url('/login'), (req, res, ctx) => { - return res(ctx.json({ accepted: true })) + http.post(httpServer.http.url('/login'), () => { + return HttpResponse.json({ accepted: true }) }), ) @@ -54,15 +55,14 @@ test('returns a mocked response from a runtime request handler upon match', asyn // Other request handlers are preserved, if there are no overlaps. const bookResponse = await fetch(httpServer.http.url('/book/abc-123')) - const bookBody = await bookResponse.json() expect(bookResponse.status).toBe(200) - expect(bookBody).toEqual({ title: 'Original title' }) + expect(await bookResponse.json()).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a persistent request handler override', async () => { server.use( - rest.get(httpServer.http.url('/book/:bookId'), (req, res, ctx) => { - return res(ctx.json({ title: 'Permanent override' })) + http.get<{ bookId: string }>(httpServer.http.url('/book/:bookId'), () => { + return HttpResponse.json({ title: 'Permanent override' }) }), ) @@ -72,16 +72,21 @@ test('returns a mocked response from a persistent request handler override', asy expect(bookBody).toEqual({ title: 'Permanent override' }) const anotherBookResponse = await fetch(httpServer.http.url('/book/abc-123')) - const anotherBookBody = await anotherBookResponse.json() expect(anotherBookResponse.status).toBe(200) - expect(anotherBookBody).toEqual({ title: 'Permanent override' }) + expect(await anotherBookResponse.json()).toEqual({ + title: 'Permanent override', + }) }) test('returns a mocked response from a one-time request handler override only upon first request match', async () => { server.use( - rest.get(httpServer.http.url('/book/:bookId'), (req, res, ctx) => { - return res.once(ctx.json({ title: 'One-time override' })) - }), + http.get<{ bookId: string }>( + httpServer.http.url('/book/:bookId'), + () => { + return HttpResponse.json({ title: 'One-time override' }) + }, + { once: true }, + ), ) const bookResponse = await fetch(httpServer.http.url('/book/abc-123')) @@ -90,29 +95,35 @@ test('returns a mocked response from a one-time request handler override only up expect(bookBody).toEqual({ title: 'One-time override' }) const anotherBookResponse = await fetch(httpServer.http.url('/book/abc-123')) - const anotherBookBody = await anotherBookResponse.json() expect(anotherBookResponse.status).toBe(200) - expect(anotherBookBody).toEqual({ title: 'Original title' }) + expect(await anotherBookResponse.json()).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a one-time request handler override only upon first request match with parallel requests', async () => { server.use( - rest.get(httpServer.http.url('/book/:bookId'), (req, res, ctx) => { - const { bookId } = req.params - return res.once(ctx.json({ title: 'One-time override', bookId })) - }), + http.get<{ bookId: string }>( + httpServer.http.url('/book/:bookId'), + ({ params }) => { + return HttpResponse.json({ + title: 'One-time override', + bookId: params.bookId, + }) + }, + { once: true }, + ), ) const bookRequestPromise = fetch(httpServer.http.url('/book/abc-123')) const anotherBookRequestPromise = fetch(httpServer.http.url('/book/abc-123')) const bookResponse = await bookRequestPromise - const bookBody = await bookResponse.json() expect(bookResponse.status).toBe(200) - expect(bookBody).toEqual({ title: 'One-time override', bookId: 'abc-123' }) + expect(await bookResponse.json()).toEqual({ + title: 'One-time override', + bookId: 'abc-123', + }) const anotherBookResponse = await anotherBookRequestPromise - const anotherBookBody = await anotherBookResponse.json() expect(anotherBookResponse.status).toBe(200) - expect(anotherBookBody).toEqual({ title: 'Original title' }) + expect(await anotherBookResponse.json()).toEqual({ title: 'Original title' }) }) diff --git a/test/node/regressions/many-request-handlers-jsdom.test.ts b/test/node/regressions/many-request-handlers-jsdom.test.ts new file mode 100644 index 000000000..c2c81c2a6 --- /dev/null +++ b/test/node/regressions/many-request-handlers-jsdom.test.ts @@ -0,0 +1,65 @@ +/** + * @jest-environment jsdom + * + * @note In JSDOM, the "AbortSignal" class is polyfilled instead of + * using the Node.js global. Because of that, its instances won't + * pass the instance check of "require('event').setMaxListeners" + * (that's based on the internal Node.js symbol), resulting in + * an exception. + * @see https://github.com/mswjs/msw/pull/1779 + */ +import { graphql, http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +// Create a large number of request handlers. +const restHandlers = new Array(100).fill(null).map((_, index) => { + return http.post( + `https://example.com/resource/${index}`, + async ({ request }) => { + const text = await request.text() + return HttpResponse.text(text + index.toString()) + }, + ) +}) + +const graphqlHanlers = new Array(100).fill(null).map((_, index) => { + return graphql.query(`Get${index}`, () => { + return HttpResponse.json({ data: { index } }) + }) +}) + +const server = setupServer(...restHandlers, ...graphqlHanlers) + +beforeAll(() => { + server.listen() + jest.spyOn(process.stderr, 'write') +}) + +afterAll(() => { + server.close() + jest.restoreAllMocks() +}) + +it('does not print a memory leak warning when having many request handlers', async () => { + const httpResponse = await fetch('https://example.com/resource/42', { + method: 'POST', + body: 'request-body-', + }).then((response) => response.text()) + + const graphqlResponse = await fetch('https://example.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `query Get42 { index }`, + }), + }).then((response) => response.json()) + + // Must not print any memory leak warnings. + expect(process.stderr.write).not.toHaveBeenCalled() + + // Must return the mocked response. + expect(httpResponse).toBe('request-body-42') + expect(graphqlResponse).toEqual({ data: { index: 42 } }) +}) diff --git a/test/node/regressions/many-request-handlers.test.ts b/test/node/regressions/many-request-handlers.test.ts new file mode 100644 index 000000000..ecdd4d793 --- /dev/null +++ b/test/node/regressions/many-request-handlers.test.ts @@ -0,0 +1,58 @@ +/** + * @jest-environment node + */ +import { graphql, http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +// Create a large number of request handlers. +const restHandlers = new Array(100).fill(null).map((_, index) => { + return http.post( + `https://example.com/resource/${index}`, + async ({ request }) => { + const text = await request.text() + return HttpResponse.text(text + index.toString()) + }, + ) +}) + +const graphqlHanlers = new Array(100).fill(null).map((_, index) => { + return graphql.query(`Get${index}`, () => { + return HttpResponse.json({ data: { index } }) + }) +}) + +const server = setupServer(...restHandlers, ...graphqlHanlers) + +beforeAll(() => { + server.listen() + jest.spyOn(process.stderr, 'write') +}) + +afterAll(() => { + server.close() + jest.restoreAllMocks() +}) + +it('does not print a memory leak warning when having many request handlers', async () => { + const httpResponse = await fetch('https://example.com/resource/42', { + method: 'POST', + body: 'request-body-', + }).then((response) => response.text()) + + const graphqlResponse = await fetch('https://example.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `query Get42 { index }`, + }), + }).then((response) => response.json()) + + // Must not print any memory leak warnings. + expect(process.stderr.write).not.toHaveBeenCalled() + + // Must return the mocked response. + expect(httpResponse).toBe('request-body-42') + expect(graphqlResponse).toEqual({ data: { index: 42 } }) +}) diff --git a/test/node/rest-api/cookies-inheritance.node.test.ts b/test/node/rest-api/cookies-inheritance.node.test.ts index 30031d770..8672570f0 100644 --- a/test/node/rest-api/cookies-inheritance.node.test.ts +++ b/test/node/rest-api/cookies-inheritance.node.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' -import { setupServer, SetupServerApi } from 'msw/node' +import { HttpResponse, http } from 'msw' +import { setupServer, SetupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' import { RequestHandler as ExpressRequestHandler } from 'express' -let server: SetupServerApi +let server: SetupServer const httpServer = new HttpServer((app) => { const handler: ExpressRequestHandler = (req, res) => { @@ -21,25 +21,31 @@ beforeAll(async () => { await httpServer.listen() server = setupServer( - rest.post(httpServer.https.url('/login'), (req, res, ctx) => { - return res(ctx.cookie('authToken', 'abc-123')) + http.post(httpServer.https.url('/login'), () => { + return new HttpResponse(null, { + headers: { + 'Set-Cookie': 'authToken=abc-123', + }, + }) }), - rest.get(httpServer.https.url('/user'), (req, res, ctx) => { - if (req.cookies.authToken == null) { - return res( - ctx.status(403), - ctx.json({ + http.get< + never, + never, + { firstName: string; lastName: string } | { error: string } + >(httpServer.https.url('/user'), ({ cookies }) => { + if (cookies.authToken == null) { + return HttpResponse.json( + { error: 'Auth token not found', - }), + }, + { status: 403 }, ) } - return res( - ctx.json({ - firstName: 'John', - lastName: 'Maverick', - }), - ) + return HttpResponse.json({ + firstName: 'John', + lastName: 'Maverick', + }) }), ) diff --git a/test/node/rest-api/https.node.test.ts b/test/node/rest-api/https.node.test.ts new file mode 100644 index 000000000..72fb2a836 --- /dev/null +++ b/test/node/rest-api/https.node.test.ts @@ -0,0 +1,46 @@ +/** + * @jest-environment node + */ +import https from 'https' +import { HttpResponse, http } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer() + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +test('intercepts and mocks a request made via "https"', (done) => { + server.use( + http.get('https://api.example.com/resource', () => { + return HttpResponse.text('Hello, world!') + }), + ) + const request = https.get('https://api.example.com/resource') + + request.on('response', (response) => { + const chunks: Array = [] + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + + response.on('error', done) + response.once('end', () => { + expect(chunks).toHaveLength(1) + + const responseText = Buffer.concat(chunks).toString('utf8') + expect(responseText).toBe('Hello, world!') + + done() + }) + }) + + request.on('error', done) +}) diff --git a/test/node/rest-api/request/body/body-arraybuffer.node.test.ts b/test/node/rest-api/request/body/body-arraybuffer.node.test.ts index 4fb7580cd..dd5991223 100644 --- a/test/node/rest-api/request/body/body-arraybuffer.node.test.ts +++ b/test/node/rest-api/request/body/body-arraybuffer.node.test.ts @@ -2,14 +2,17 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { encodeBuffer } from '@mswjs/interceptors' + +function encodeBuffer(value: unknown) { + return Buffer.from(JSON.stringify(value)).buffer +} const server = setupServer( - rest.post('http://localhost/arrayBuffer', async (req, res, ctx) => { - const arrayBuffer = await req.arrayBuffer() - return res(ctx.body(arrayBuffer)) + http.post('http://localhost/arrayBuffer', async ({ request }) => { + const requestBodyBuffer = await request.arrayBuffer() + return HttpResponse.arrayBuffer(requestBodyBuffer) }), ) @@ -52,7 +55,7 @@ test('reads null request body as empty array buffer', async () => { headers: { 'Content-Type': 'application/json', }, - body: null, + body: undefined, }) const body = await res.arrayBuffer() diff --git a/test/node/rest-api/request/body/body-form-data.node.test.ts b/test/node/rest-api/request/body/body-form-data.node.test.ts index 4683408c1..f07d5026a 100644 --- a/test/node/rest-api/request/body/body-form-data.node.test.ts +++ b/test/node/rest-api/request/body/body-form-data.node.test.ts @@ -1,14 +1,13 @@ /** * @jest-environment node */ -import fetch from 'node-fetch' -import FormDataPolyfill from 'form-data' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.post('http://localhost/deprecated', (req, res, ctx) => { - return res(ctx.json(req.body)) + http.post('http://localhost/resource', async ({ request }) => { + const formData = await request.formData() + return HttpResponse.json(Array.from(formData.entries())) }), ) @@ -20,24 +19,23 @@ afterAll(() => { server.close() }) -test('handles "FormData" as a request body', async () => { +test('reads FormData request body', async () => { // Note that creating a `FormData` instance in Node/JSDOM differs // from the same instance in a real browser. Follow the instructions // of your `fetch` polyfill to learn more. - const formData = new FormDataPolyfill() + const formData = new FormData() formData.append('username', 'john.maverick') formData.append('password', 'secret123') - const res = await fetch('http://localhost/deprecated', { + const res = await fetch('http://localhost/resource', { method: 'POST', - headers: formData.getHeaders(), body: formData, }) const json = await res.json() expect(res.status).toBe(200) - expect(json).toEqual({ - username: 'john.maverick', - password: 'secret123', - }) + expect(json).toEqual([ + ['username', 'john.maverick'], + ['password', 'secret123'], + ]) }) diff --git a/test/node/rest-api/request/body/body-json.node.test.ts b/test/node/rest-api/request/body/body-json.node.test.ts index f72b49a4e..20f301b37 100644 --- a/test/node/rest-api/request/body/body-json.node.test.ts +++ b/test/node/rest-api/request/body/body-json.node.test.ts @@ -2,17 +2,13 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { encodeBuffer } from '@mswjs/interceptors' const server = setupServer( - rest.post('http://localhost/deprecated', (req, res, ctx) => { - return res(ctx.json(req.body)) - }), - rest.post('http://localhost/json', async (req, res, ctx) => { - const json = await req.json() - return res(ctx.json(json)) + http.post('http://localhost/json', async ({ request }) => { + return HttpResponse.json(await request.json()) }), ) @@ -24,34 +20,6 @@ afterAll(() => { server.close() }) -test('reads request body as json', async () => { - const res = await fetch('http://localhost/deprecated', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ firstName: 'John' }), - }) - const json = await res.json() - - expect(res.status).toBe(200) - expect(json).toEqual({ firstName: 'John' }) -}) - -test('reads a single number as json request body', async () => { - const res = await fetch('http://localhost/deprecated', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(123), - }) - const json = await res.json() - - expect(res.status).toBe(200) - expect(json).toEqual(123) -}) - test('reads request body using json() method', async () => { const res = await fetch('http://localhost/json', { method: 'POST', diff --git a/test/node/rest-api/request/body/body-text.node.test.ts b/test/node/rest-api/request/body/body-text.node.test.ts index 45ebe6c28..b3025686f 100644 --- a/test/node/rest-api/request/body/body-text.node.test.ts +++ b/test/node/rest-api/request/body/body-text.node.test.ts @@ -2,14 +2,13 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { encodeBuffer } from '@mswjs/interceptors' const server = setupServer( - rest.post('http://localhost/resource', async (req, res, ctx) => { - const body = await req.text() - return res(ctx.body(body)) + http.post('http://localhost/resource', async ({ request }) => { + return HttpResponse.text(await request.text()) }), ) @@ -63,7 +62,7 @@ test('reads array buffer request body as text', async () => { test('reads null request body as empty text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', - body: null, + body: null as any, }) const body = await res.text() diff --git a/test/node/rest-api/request/body/body-used.node.test.ts b/test/node/rest-api/request/body/body-used.node.test.ts new file mode 100644 index 000000000..cc6d77a5c --- /dev/null +++ b/test/node/rest-api/request/body/body-used.node.test.ts @@ -0,0 +1,66 @@ +/** + * @jest-environment node + */ +import { HttpResponse, http, graphql } from 'msw' +import { setupServer } from 'msw/node' +import * as express from 'express' +import { HttpServer } from '@open-draft/test-server/http' + +const httpServer = new HttpServer((app) => { + app.post('/resource', express.json(), (req, res) => { + res.json({ response: `received: ${req.body.message}` }) + }) +}) + +const server = setupServer() + +beforeAll(async () => { + server.listen() + await httpServer.listen() +}) + +afterEach(() => { + server.resetHandlers() + jest.restoreAllMocks() +}) + +afterAll(async () => { + server.close() + await httpServer.close() +}) + +it('does not read the body while parsing an unhandled request', async () => { + // Expecting an unhandled request warning in this test. + jest.spyOn(console, 'warn').mockImplementation(() => {}) + + const requestUrl = httpServer.http.url('/resource') + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: 'Hello server', + }), + }) + expect(await response.json()).toEqual({ response: `received: Hello server` }) +}) + +it('does not read the body while parsing an unhandled request', async () => { + const requestUrl = httpServer.http.url('/resource') + server.use( + http.post(requestUrl, () => { + return HttpResponse.json({ mocked: true }) + }), + ) + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: 'Hello server', + }), + }) + expect(await response.json()).toEqual({ mocked: true }) +}) diff --git a/test/node/rest-api/request/matching/all.node.test.ts b/test/node/rest-api/request/matching/all.node.test.ts index 4afeba4fb..350855cee 100644 --- a/test/node/rest-api/request/matching/all.node.test.ts +++ b/test/node/rest-api/request/matching/all.node.test.ts @@ -3,7 +3,7 @@ */ import fetch, { Response } from 'node-fetch' import { HttpServer } from '@open-draft/test-server/http' -import { RESTMethods, rest } from 'msw' +import { HttpMethods, http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { @@ -31,21 +31,21 @@ afterAll(async () => { await httpServer.close() }) -async function forEachMethod(callback: (method: RESTMethods) => unknown) { - for (const method of Object.values(RESTMethods)) { +async function forEachMethod(callback: (method: HttpMethods) => unknown) { + for (const method of Object.values(HttpMethods)) { await callback(method) } } test('matches all requests given no custom path', async () => { server.use( - rest.all('*', (req, res, ctx) => { - return res(ctx.text('welcome to the jungle')) + http.all('*', () => { + return HttpResponse.text('welcome to the jungle') }), ) const responses = await Promise.all( - Object.values(RESTMethods).reduce[]>((all, method) => { + Object.values(HttpMethods).reduce[]>((all, method) => { return all.concat( [ httpServer.http.url('/'), @@ -57,22 +57,22 @@ test('matches all requests given no custom path', async () => { ) for (const response of responses) { - expect(response.status).toEqual(200) + expect(response.status).toBe(200) expect(await response.text()).toEqual('welcome to the jungle') } }) test('respects custom path when matching requests', async () => { server.use( - rest.all(httpServer.http.url('/api/*'), (req, res, ctx) => { - return res(ctx.text('hello world')) + http.all(httpServer.http.url('/api/*'), () => { + return HttpResponse.text('hello world') }), ) // Root requests. await forEachMethod(async (method) => { const response = await fetch(httpServer.http.url('/api/'), { method }) - expect(response.status).toEqual(200) + expect(response.status).toBe(200) expect(await response.text()).toEqual('hello world') }) @@ -81,7 +81,7 @@ test('respects custom path when matching requests', async () => { const response = await fetch(httpServer.http.url('/api/foo'), { method, }) - expect(response.status).toEqual(200) + expect(response.status).toBe(200) expect(await response.text()).toEqual('hello world') }) diff --git a/test/node/rest-api/request/matching/path-params-decode.node.test.ts b/test/node/rest-api/request/matching/path-params-decode.node.test.ts index eaeda027f..f59815ffc 100644 --- a/test/node/rest-api/request/matching/path-params-decode.node.test.ts +++ b/test/node/rest-api/request/matching/path-params-decode.node.test.ts @@ -2,15 +2,16 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('https://test.mswjs.io/reflect-url/:url', (req, res, ctx) => { - const { url } = req.params - - return res(ctx.json({ url })) - }), + http.get<{ url: string }>( + 'https://test.mswjs.io/reflect-url/:url', + ({ params }) => { + return HttpResponse.json({ url: params.url }) + }, + ), ) beforeAll(() => { @@ -28,7 +29,7 @@ test('decodes url componets', async () => { `https://test.mswjs.io/reflect-url/${encodeURIComponent(url)}`, ) - expect(res.status).toEqual(200) + expect(res.status).toBe(200) expect(await res.json()).toEqual({ url, }) diff --git a/test/node/rest-api/response/body/body-binary.node.test.ts b/test/node/rest-api/response/body-binary.node.test.ts similarity index 75% rename from test/node/rest-api/response/body/body-binary.node.test.ts rename to test/node/rest-api/response/body-binary.node.test.ts index c49427d04..1c9d8b681 100644 --- a/test/node/rest-api/response/body/body-binary.node.test.ts +++ b/test/node/rest-api/response/body-binary.node.test.ts @@ -4,24 +4,23 @@ import * as path from 'path' import * as fs from 'fs' import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' function getImageBuffer() { - return fs.readFileSync( - path.resolve(__dirname, '../../../../fixtures/image.jpg'), - ) + return fs.readFileSync(path.resolve(__dirname, '../../../fixtures/image.jpg')) } const server = setupServer( - rest.get('http://test.mswjs.io/image', (_, res, ctx) => { + http.get('http://test.mswjs.io/image', () => { const imageBuffer = getImageBuffer() - return res( - ctx.set('Content-Length', imageBuffer.byteLength.toString()), - ctx.set('Content-Type', 'image/jpeg'), - ctx.body(imageBuffer), - ) + return HttpResponse.arrayBuffer(imageBuffer, { + headers: { + 'Content-Type': 'image/jpeg', + 'Content-Length': imageBuffer.byteLength.toString(), + }, + }) }), ) @@ -35,7 +34,6 @@ test('returns given buffer in the mocked response', async () => { const expectedImageBuffer = getImageBuffer() expect(status).toBe(200) - expect(headers.get('x-powered-by')).toBe('msw') expect(headers.get('content-length')).toBe( actualImageBuffer.byteLength.toString(), ) @@ -52,7 +50,6 @@ test('returns given blob in the mocked response', async () => { const expectedImageBuffer = getImageBuffer() expect(status).toBe(200) - expect(headers.get('x-powered-by')).toBe('msw') expect(blob.type).toBe('image/jpeg') expect(blob.size).toBe(Number(headers.get('content-length'))) expect(Buffer.compare(actualImageBuffer, expectedImageBuffer)).toBe(0) diff --git a/test/node/rest-api/response/body-json.node.test.ts b/test/node/rest-api/response/body-json.node.test.ts new file mode 100644 index 000000000..9c59d1b92 --- /dev/null +++ b/test/node/rest-api/response/body-json.node.test.ts @@ -0,0 +1,36 @@ +/** + * @jest-environment node + */ +import { HttpResponse, http } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer( + http.get('http://localhost/json', () => { + return HttpResponse.json({ firstName: 'John' }) + }), + http.get('http://localhost/number', () => { + return HttpResponse.json(123) + }), +) + +beforeAll(() => { + server.listen() +}) + +afterAll(() => { + server.close() +}) + +test('responds with a JSON response body', async () => { + const response = await fetch('http://localhost/json') + + expect(response.headers.get('content-type')).toBe('application/json') + expect(await response.json()).toEqual({ firstName: 'John' }) +}) + +test('responds with a single number JSON response body', async () => { + const response = await fetch('http://localhost/number') + + expect(response.headers.get('content-type')).toBe('application/json') + expect(await response.json()).toEqual(123) +}) diff --git a/test/node/rest-api/response/body-stream.node.test.ts b/test/node/rest-api/response/body-stream.node.test.ts new file mode 100644 index 000000000..2e657b5b7 --- /dev/null +++ b/test/node/rest-api/response/body-stream.node.test.ts @@ -0,0 +1,102 @@ +/** + * @jest-environment node + */ +import https from 'https' +import { HttpResponse, http, delay } from 'msw' +import { setupServer } from 'msw/node' + +const encoder = new TextEncoder() +const server = setupServer() + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +test('responds with a ReadableStream', async () => { + server.use( + http.get('https://api.example.com/stream', () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('hello')) + controller.enqueue(encoder.encode('world')) + controller.close() + }, + }) + + return new HttpResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + }), + ) + + const response = await fetch('https://api.example.com/stream') + + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + expect(response.body).toBeInstanceOf(ReadableStream) + expect(response.body!.locked).toBe(false) + + expect(await response.text()).toBe('helloworld') +}) + +test('supports delays when enqueuing chunks', (done) => { + server.use( + http.get('https://api.example.com/stream', () => { + const stream = new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode('first')) + await delay(500) + + controller.enqueue(encoder.encode('second')) + await delay(500) + + controller.enqueue(encoder.encode('third')) + await delay(500) + controller.close() + }, + }) + + return new HttpResponse(stream, { + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + }), + ) + + const request = https.get('https://api.example.com/stream', (response) => { + const chunks: Array<{ buffer: Buffer; timestamp: number }> = [] + + response.on('data', (data) => { + chunks.push({ + buffer: Buffer.from(data), + timestamp: Date.now(), + }) + }) + + response.once('end', () => { + const textChunks = chunks.map((chunk) => chunk.buffer.toString('utf8')) + expect(textChunks).toEqual(['first', 'second', 'third']) + + // Ensure that the chunks were sent over time, + // respecting the delay set in the mocked stream. + const chunkTimings = chunks.map((chunk) => chunk.timestamp) + expect(chunkTimings[1] - chunkTimings[0]).toBeGreaterThanOrEqual(490) + expect(chunkTimings[2] - chunkTimings[1]).toBeGreaterThanOrEqual(490) + + done() + }) + }) + + request.on('error', done) +}) diff --git a/test/node/rest-api/response/body/body-text.node.test.ts b/test/node/rest-api/response/body-text.node.test.ts similarity index 74% rename from test/node/rest-api/response/body/body-text.node.test.ts rename to test/node/rest-api/response/body-text.node.test.ts index b2b0c85e9..0c97edf49 100644 --- a/test/node/rest-api/response/body/body-text.node.test.ts +++ b/test/node/rest-api/response/body-text.node.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('http://localhost/text', (req, res, ctx) => { - return res(ctx.text('hello world')) + http.get('http://localhost/text', () => { + return HttpResponse.text('hello world') }), ) @@ -23,6 +23,7 @@ test('responds with a text response body', async () => { const res = await fetch('http://localhost/text') const text = await res.text() + expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('text/plain') expect(text).toBe('hello world') }) diff --git a/test/node/rest-api/response/body/body-xml.node.test.ts b/test/node/rest-api/response/body-xml.node.test.ts similarity index 83% rename from test/node/rest-api/response/body/body-xml.node.test.ts rename to test/node/rest-api/response/body-xml.node.test.ts index ebaef22a4..8862936db 100644 --- a/test/node/rest-api/response/body/body-xml.node.test.ts +++ b/test/node/rest-api/response/body-xml.node.test.ts @@ -2,19 +2,17 @@ * @jest-environment node */ import fetch from 'node-fetch' -import { rest } from 'msw' +import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( - rest.get('http://localhost/xml', (req, res, ctx) => { - return res( - ctx.xml(` + http.get('http://localhost/xml', () => { + return HttpResponse.xml(` abc-123 John Maverick -`), - ) +`) }), ) diff --git a/test/node/rest-api/response/body/body-json.node.test.ts b/test/node/rest-api/response/body/body-json.node.test.ts deleted file mode 100644 index bb60182a9..000000000 --- a/test/node/rest-api/response/body/body-json.node.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @jest-environment node - */ -import fetch from 'node-fetch' -import { rest } from 'msw' -import { setupServer } from 'msw/node' - -const server = setupServer( - rest.get('http://localhost/json', (req, res, ctx) => { - return res(ctx.json({ firstName: 'John' })) - }), - rest.get('http://localhost/number', (req, res, ctx) => { - return res(ctx.json(123)) - }), -) - -beforeAll(() => { - server.listen() -}) - -afterAll(() => { - server.close() -}) - -test('responds with a JSON response body', async () => { - const res = await fetch('http://localhost/json') - - expect(res.headers.get('content-type')).toBe('application/json') - - const json = await res.json() - expect(json).toEqual({ firstName: 'John' }) -}) - -test('responds with a single number JSON response body', async () => { - const res = await fetch('http://localhost/number') - - expect(res.headers.get('content-type')).toBe('application/json') - - const json = await res.json() - expect(json).toEqual(123) -}) diff --git a/test/node/rest-api/response/response-error.test.ts b/test/node/rest-api/response/response-error.test.ts new file mode 100644 index 000000000..443023ecb --- /dev/null +++ b/test/node/rest-api/response/response-error.test.ts @@ -0,0 +1,35 @@ +/** + * @jest-environment node + */ +import { http } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer() + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +it('responds with a mocked error response using "Response.error" shorthand', async () => { + server.use( + http.get('https://api.example.com/resource', () => { + return Response.error() + }), + ) + + const responseError = await fetch('https://api.example.com/resource') + .then(() => null) + .catch((error) => error) + + expect(responseError).toEqual(new TypeError('Failed to fetch')) + // Guard against false positives due to exceptions arising from the library. + expect(responseError.cause).toEqual(Response.error()) +}) diff --git a/test/support/graphql.ts b/test/support/graphql.ts index 766e47c5d..37de7c55f 100644 --- a/test/support/graphql.ts +++ b/test/support/graphql.ts @@ -10,7 +10,7 @@ export const gql = (str: TemplateStringsArray) => { interface GraphQLClientOPtions { uri: string - fetch?: typeof fetch + fetch?: (input: any, init?: any) => Promise } interface GraphQLOperationInput { @@ -25,8 +25,10 @@ interface GraphQLOperationInput { export function createGraphQLClient(options: GraphQLClientOPtions) { const fetchFn = options.fetch || fetch - return async (input: GraphQLOperationInput): Promise => { - const res = await fetchFn(options.uri, { + return async >( + input: GraphQLOperationInput, + ): Promise> => { + const response = await fetchFn(options.uri, { method: 'POST', headers: { accept: '*/*', @@ -38,6 +40,6 @@ export function createGraphQLClient(options: GraphQLClientOPtions) { // No need to transform the JSON into `ExecutionResult`, // because that's the responsibility of an actual server // or an MSW request handler. - return res.json() + return response.json() } } diff --git a/test/typings/graphql.test-d.ts b/test/typings/graphql.test-d.ts index 93685fae7..488b4e467 100644 --- a/test/typings/graphql.test-d.ts +++ b/test/typings/graphql.test-d.ts @@ -1,63 +1,111 @@ import { parse } from 'graphql' -import { - MockedRequest, - GraphQLRequest, - graphql, - GraphQLHandler, - GraphQLVariables, -} from 'msw' - -graphql.query<{ key: string }>('', (req, res, ctx) => { - return res( - ctx.data( - // @ts-expect-error Response data doesn't match the query type. - {}, - ), - ) +import { graphql, HttpResponse } from 'msw' + +/** + * Variables type. + */ +graphql.mutation('CreateUser', ({ variables }) => { + variables.id + variables.unknown +}) + +graphql.mutation('CreateUser', ({ variables }) => { + variables.id.toUpperCase() + // @ts-expect-error unknown variable name + variables.unknown +}) + +graphql.mutation('CreateUser', ({ variables }) => { + // @ts-expect-error + variables.id.toUpperCase() + // @ts-expect-error + variables.unknown }) graphql.query< { key: string }, // @ts-expect-error `null` is not a valid variables type. null ->('', (req, res, ctx) => { - return res(ctx.data({ key: 'pass' })) -}) - -graphql.mutation<{ key: string }>('', (req, res, ctx) => - res( - ctx.data( - // @ts-expect-error Response data doesn't match the query type. - {}, - ), - ), -) +>('', () => {}) graphql.mutation< { key: string }, // @ts-expect-error `null` is not a valid variables type. null ->('', (req, res, ctx) => { - return res(ctx.data({ key: 'pass' })) -}) - -graphql.operation<{ key: string }>((req, res, ctx) => { - return res( - ctx.data( - // @ts-expect-error Response data doesn't match the query type. - {}, - ), - ) -}) +>('', () => {}) graphql.operation< { key: string }, // @ts-expect-error `null` is not a valid variables type. null ->((req, res, ctx) => { - return res(ctx.data({ key: 'pass' })) +>(() => { + return HttpResponse.json({ data: { key: 'a' } }) }) +/** + * Response body type (GraphQL query type). + */ +// Returned mocked response body must satisfy the +// GraphQL query generic. +graphql.query<{ id: string }>('GetUser', () => { + return HttpResponse.json({ + data: { id: '2' }, + }) +}) + +graphql.query<{ id: string }>( + 'GetUser', + // @ts-expect-error "id" type is incorrect + () => { + return HttpResponse.json({ + data: { id: 123 }, + }) + }, +) + +graphql.query<{ id: string }>( + 'GetUser', + // @ts-expect-error response json is empty + () => HttpResponse.json({ data: {} }), +) + +graphql.query<{ id: string }>( + 'GetUser', + // @ts-expect-error incompatible response body type + () => HttpResponse.text('hello'), +) + +/// +/// +/// + +graphql.query<{ key: string }>( + 'GetData', + // @ts-expect-error Response data doesn't match the query type. + () => { + return HttpResponse.json({ data: {} }) + }, +) + +graphql.mutation<{ key: string }>( + 'MutateData', + // @ts-expect-error Response data doesn't match the query type. + () => { + return HttpResponse.json({ data: {} }) + }, +) + +graphql.operation<{ key: string }>( + // @ts-expect-error Response data doesn't match the query type. + () => { + return HttpResponse.json({ data: {} }) + }, +) + +/** + * Variables type. + */ + /** * Supports `DocumentNode` as the GraphQL operation name. */ @@ -68,15 +116,15 @@ const getUser = parse(` } } `) -graphql.query(getUser, (req, res, ctx) => - res( - ctx.data({ - // Cannot extract query type from the runtime `DocumentNode`. - arbitrary: true, - }), - ), -) +graphql.query(getUser, () => { + return HttpResponse.json({ + // Cannot extract query type from the runtime `DocumentNode`. + data: { arbitrary: true }, + }) +}) +// Both variable and response types can be extracted +// from a "TypedDocumentNode" value. const getUserById = parse(` query GetUserById($userId: String!) { user(id: $userId) { @@ -84,21 +132,21 @@ const getUserById = parse(` } } `) -graphql.query(getUserById, (req, res, ctx) => { - req.variables.userId +graphql.query(getUserById, ({ variables }) => { + variables.userId.toUpperCase() // Extracting variables from the native "DocumentNode" is impossible. - req.variables.foo + variables.foo - return res( - ctx.data({ + return HttpResponse.json({ + data: { user: { firstName: 'John', // Extracting a query body type from the "DocumentNode" is impossible. lastName: 'Maverick', }, - }), - ) + }, + }) }) const createUser = parse(` @@ -108,28 +156,8 @@ const createUser = parse(` } } `) -graphql.mutation(createUser, (req, res, ctx) => - res( - ctx.data({ - arbitrary: true, - }), - ), -) - -// GraphQL request variables must be inferrable -// via the variables generic. -function extractVariables( - _handler: GraphQLHandler>, -): MockedRequest> { - return null as any -} -const handlerWithVariables = graphql.query<{ data: unknown }, { id: string }>( - 'GetUser', - () => void 0, -) -const handler = extractVariables(handlerWithVariables) - -handler.body.variables.id - -// @ts-expect-error Property "foo" is not defined on the variables generic. -handler.body.variables.foo +graphql.mutation(createUser, () => { + return HttpResponse.json({ + data: { arbitrary: true }, + }) +}) diff --git a/test/typings/path-params.test-d.ts b/test/typings/path-params.test-d.ts deleted file mode 100644 index 60749efe4..000000000 --- a/test/typings/path-params.test-d.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { rest } from 'msw' - -rest.get('/user/:userId', (req) => { - req.params.userId - - // @ts-expect-error `unknown` is not defined in the request params type. - req.params.unknown -}) - -rest.get('/user/:id', (req, res, ctx) => { - const { userId } = req.params - - return res( - ctx.body( - // @ts-expect-error "userId" parameter is not annotated - // and is ambiguous (string | string[]). - userId, - ), - ) -}) - -rest.get< - never, - // @ts-expect-error Path parameters are always strings. - // Parse them to numbers in the resolver if necessary. - { id: number } ->('/posts/:id', () => null) - -/** - * Using interface as path parameters type. - */ -interface UserParamsInterface { - userId: string -} - -rest.get('/user/:userId', (req) => { - req.params.userId.toUpperCase() - - // @ts-expect-error Unknown path parameter "foo". - req.params.foo -}) - -/** - * Using type as path parameters type. - */ -type UserParamsType = { - userId: string -} - -rest.get('/user/:userId', (req) => { - req.params.userId.toUpperCase() - - // @ts-expect-error Unknown path parameter "foo". - req.params.foo -}) diff --git a/test/typings/rest.test-d.ts b/test/typings/rest.test-d.ts index dabe4dd68..7b7df59a7 100644 --- a/test/typings/rest.test-d.ts +++ b/test/typings/rest.test-d.ts @@ -1,61 +1,121 @@ -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' -rest.get('/user', (req, res, ctx) => { - // @ts-expect-error `session` property is not defined on the request body type. - req.body.session +/** + * Request path parameters. + */ +http.get<{ id: string }>('/user/:id', ({ params }) => { + params.id.toUpperCase() - res( - // @ts-expect-error JSON doesn't match given response body generic type. - ctx.json({ unknown: true }), - ) + // @ts-expect-error Unknown path parameter + params.unknown +}) - res( - // @ts-expect-error value types do not match - ctx.json({ postCount: 'this is not a number' }), - ) +http.get<{ a: string; b: string[] }>('/user/:a/:b/:b', ({ params }) => { + params.a.toUpperCase() + params.b.map((x) => x) - return res(ctx.json({ postCount: 2 })) + // @ts-expect-error Unknown path parameter + params.unknown }) -rest.post('/submit', () => null) +// Supports path parameters declaration via type. +type UserPathParams = { id: string } +http.get('/user/:id', ({ params }) => { + params.id.toUpperCase() -rest.get< - any, - // @ts-expect-error `null` is not a valid response body type. - null ->('/user', () => null) + // @ts-expect-error Unknown path parameter + params.unknown +}) -rest.get('/user', (req, res, ctx) => - // allow ResponseTransformer to contain a more specific type - res(ctx.json({ label: true })), -) +// Supports path parameters declaration via interface. +interface PostPathParameters { + id: string +} +http.get('/user/:id', ({ params }) => { + params.id.toUpperCase() -rest.get('/user', (req, res, ctx) => - // allow ResponseTransformer to return a narrower type than a given union - res(ctx.json('hello')), -) + // @ts-expect-error Unknown path parameter + params.unknown +}) -rest.head('/user', (req) => { - // @ts-expect-error GET requests cannot have body. - req.body.toString() +http.get('/user/:a/:b', ({ params }) => { + // @ts-expect-error Unknown path parameter + params.a.toUpperCase() + // @ts-expect-error Unknown path parameter + params.b.map((x) => x) }) -rest.head('/user', (req) => { - // @ts-expect-error GET requests cannot have body. - req.body.toString() +/** + * Request body generic. + */ +http.post('/user', async ({ request }) => { + const data = await request.json() + data.id + + // @ts-expect-error Unknown property + data.unknown + + const text = await request.text() + text.toUpperCase() + // @ts-expect-error Text remains plain text. + text.id }) -rest.get('/user', (req) => { - // @ts-expect-error GET requests cannot have body. - req.body.toString() +http.get('/user', async ({ request }) => { + const data = await request.json() + // @ts-expect-error Null is not an object + Object.keys(data) }) -rest.get('/user', (req) => { - // @ts-expect-error GET requests cannot have body. - req.body.toString() +/** + * Response body generic. + */ +http.get('/user', () => { + // Allows responding with a plain Response + // when no response body generic is set. + return new Response('hello') }) -rest.post<{ userId: string }>('/user', (req) => { - req.body.userId.toUpperCase() +http.get('/user', () => { + return HttpResponse.json({ id: 1 }) }) + +// Supports explicit response data declared via type. +type ResponseBodyType = { id: number } +http.get('/user', () => { + const data: ResponseBodyType = { id: 1 } + return HttpResponse.json(data) +}) + +// Supports explicit response data declared via interface. +interface ResponseBodyInterface { + id: number +} +http.get('/user', () => { + const data: ResponseBodyInterface = { id: 1 } + return HttpResponse.json(data) +}) + +http.get( + '/user', + // @ts-expect-error String not assignable to number + () => HttpResponse.json({ id: 'invalid' }), +) + +http.get( + '/user', + // @ts-expect-error Missing property "id" + () => HttpResponse.json({}), +) + +// Response resolver can return a response body of a +// narrower type than defined in the generic. +http.get('/user', () => + HttpResponse.json(['value']), +) + +// Response resolver can return a more specific type +// than provided in the response generic. +http.get('/user', () => + HttpResponse.json({ label: true }), +) diff --git a/test/typings/run.ts b/test/typings/run.ts index 86b26d7bd..fe7861751 100644 --- a/test/typings/run.ts +++ b/test/typings/run.ts @@ -2,7 +2,7 @@ import * as fs from 'fs' import * as path from 'path' import { spawnSync } from 'child_process' import { invariant } from 'outvariant' -import tsPackageJson from 'typescript/package.json' +import * as tsPackageJson from 'typescript/package.json' const tsInstalledVersion = tsPackageJson.version invariant( diff --git a/test/typings/set.test-d.ts b/test/typings/set.test-d.ts deleted file mode 100644 index ecc6b9bde..000000000 --- a/test/typings/set.test-d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { defaultContext } from 'msw' - -const { set } = defaultContext - -set('header', 'value') -set({ - one: 'value', - two: 'value', -}) - -// @ts-expect-error Forbidden response header. -set('cookie', 'secret') -// @ts-expect-error Forbidden response header. -set('Cookie', 'secret') -// @ts-expect-error Forbidden response header. -set('cookie2', 'secret') -// @ts-expect-error Forbidden response header. -set('Cookie2', 'secret') -// @ts-expect-error Forbidden response header. -set('set-cookie', 'secret') -// @ts-expect-error Forbidden response header. -set('Set-Cookie', 'secret') -// @ts-expect-error Forbidden response header. -set('set-cookie2', 'secret') -// @ts-expect-error Forbidden response header. -set('Set-Cookie2', 'secret') - -// @ts-expect-error Forbidden response header. -set({ cookie: 'secret' }) -// @ts-expect-error Forbidden response header. -set({ Cookie: 'secret' }) -// @ts-expect-error Forbidden response header. -set({ cookie2: 'secret' }) -// @ts-expect-error Forbidden response header. -set({ Cookie2: 'secret' }) -// @ts-expect-error Forbidden response header. -set({ 'set-cookie': 'secret' }) -// @ts-expect-error Forbidden response header. -set({ 'Set-Cookie': 'secret' }) -// @ts-expect-error Forbidden response header. -set({ 'set-cookie2': 'secret' }) -// @ts-expect-error Forbidden response header. -set({ 'Set-Cookie2': 'secret' }) diff --git a/test/typings/tsconfig.json b/test/typings/tsconfig.json index d149b93ad..ce1fa9209 100644 --- a/test/typings/tsconfig.json +++ b/test/typings/tsconfig.json @@ -1,15 +1,13 @@ { + "extends": "../../tsconfig", "compilerOptions": { - "strict": true, "target": "esnext", "module": "commonjs", "noEmit": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "moduleResolution": "Node", "types": ["node"], "typeRoots": ["../../node_modules/@types"], "lib": ["dom"], + "rootDir": "../..", "baseUrl": ".", "paths": { "msw": ["../.."], diff --git a/tsconfig.json b/tsconfig.json index fcf4b37ed..c5e2f59a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,13 +4,18 @@ "target": "es6", "module": "ESNext", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, + "allowSyntheticDefaultImports": false, + "esModuleInterop": false, "resolveJsonModule": true, "declaration": true, "declarationDir": "lib/types", "noEmit": true, - "lib": ["es2017", "ESNext.AsyncIterable", "dom", "webworker"] + "lib": ["DOM", "DOM.Iterable", "ESNext.AsyncIterable"], + "baseUrl": "./src", + "paths": { + "~/core": ["./core"], + "~/core/*": ["./core/*"] + } }, "include": ["global.d.ts", "src/**/*.ts"], "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] diff --git a/tsup.config.ts b/tsup.config.ts index 685e77872..609045a35 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,76 +1,112 @@ -import { defineConfig } from 'tsup' -import { workerScriptPlugin } from './config/plugins/esbuild/workerScriptPlugin' +import { defineConfig, Options } from 'tsup' +import * as glob from 'glob' +import { + getWorkerChecksum, + copyWorkerPlugin, +} from './config/plugins/esbuild/copyWorkerPlugin' +import { resolveCoreImportsPlugin } from './config/plugins/esbuild/resolveCoreImportsPlugin' +import { forceEsmExtensionsPlugin } from './config/plugins/esbuild/forceEsmExtensionsPlugin' -// Prevent from bunlding the "@mswjs/*" packages -// so that the users get the latest versions without -// having to bump them in "msw'." +// Externalize the in-house dependencies so that the user +// would get the latest published version automatically. const ecosystemDependencies = /^@mswjs\/(.+)$/ -export default defineConfig([ - { - name: 'main', - entry: ['./src/index.ts'], - outDir: './lib', - format: ['cjs'], - legacyOutput: true, - sourcemap: true, - clean: true, - bundle: true, - splitting: false, - dts: false, - esbuildPlugins: [workerScriptPlugin()], - }, - { - name: 'iife', - entry: ['./src/index.ts'], - outDir: './lib', - legacyOutput: true, - format: ['iife'], - platform: 'browser', - globalName: 'MockServiceWorker', - bundle: true, - sourcemap: true, - splitting: false, - dts: false, - esbuildPlugins: [workerScriptPlugin()], - }, - { - name: 'node', - entry: ['./src/node/index.ts'], - format: ['esm', 'cjs'], - outDir: './lib/node', - platform: 'node', - external: [ - 'http', - 'https', - 'util', - 'events', - 'tty', - 'os', - 'timers', - ecosystemDependencies, - ], - clean: true, - inject: ['./config/polyfills-node.ts'], - sourcemap: true, - dts: false, - esbuildPlugins: [workerScriptPlugin()], - }, - { - name: 'native', - entry: ['./src/native/index.ts'], - format: ['esm', 'cjs'], - outDir: './lib/native', - clean: true, - external: ['chalk', 'util', 'events', ecosystemDependencies], +// Externalize the core functionality (reused across environments) +// so that it can be shared between the environments. +const mswCore = /\/core(\/.+)?$/ + +const SERVICE_WORKER_CHECKSUM = getWorkerChecksum() + +const coreConfig: Options = { + name: 'core', + platform: 'neutral', + entry: glob.sync('./src/core/**/*.ts', { + ignore: '**/*.test.ts', + }), + external: [ecosystemDependencies], + format: ['esm', 'cjs'], + outDir: './lib/core', + bundle: false, + splitting: false, + dts: true, + esbuildPlugins: [forceEsmExtensionsPlugin()], +} + +const nodeConfig: Options = { + name: 'node', + platform: 'node', + entry: ['./src/node/index.ts'], + inject: ['./config/polyfills-node.ts'], + external: [mswCore, ecosystemDependencies], + format: ['esm', 'cjs'], + outDir: './lib/node', + sourcemap: false, + bundle: true, + splitting: false, + dts: true, + + esbuildPlugins: [resolveCoreImportsPlugin(), forceEsmExtensionsPlugin()], +} + +const browserConfig: Options = { + name: 'browser', + platform: 'browser', + entry: ['./src/browser/index.ts'], + external: [mswCore, ecosystemDependencies], + format: ['esm', 'cjs'], + outDir: './lib/browser', + bundle: true, + splitting: false, + dts: true, + define: { + SERVICE_WORKER_CHECKSUM: JSON.stringify(SERVICE_WORKER_CHECKSUM), }, - { - name: 'typedefs', - entry: ['./src/index.ts', './src/node/index.ts', './src/native/index.ts'], - outDir: './lib', - clean: false, - dts: { - only: true, - }, + esbuildPlugins: [ + resolveCoreImportsPlugin(), + forceEsmExtensionsPlugin(), + copyWorkerPlugin(SERVICE_WORKER_CHECKSUM), + ], +} + +const reactNativeConfig: Options = { + name: 'react-native', + platform: 'node', + entry: ['./src/native/index.ts'], + external: ['chalk', 'util', 'events', mswCore, ecosystemDependencies], + format: ['esm', 'cjs'], + outDir: './lib/native', + bundle: true, + splitting: false, + dts: true, + esbuildPlugins: [resolveCoreImportsPlugin(), forceEsmExtensionsPlugin()], +} + +const iifeConfig: Options = { + name: 'iife', + platform: 'browser', + globalName: 'MockServiceWorker', + entry: ['./src/iife/index.ts'], + /** + * @note Legacy output format will automatically create + * a "iife" directory under the "outDir". + */ + outDir: './lib', + format: ['iife'], + legacyOutput: true, + bundle: true, + splitting: false, + dts: false, + define: { + // Sign the IIFE build as well because any bundle containing + // the worker API must have the the integrity checksum defined. + SERVICE_WORKER_CHECKSUM: JSON.stringify(SERVICE_WORKER_CHECKSUM), }, +} + +export default defineConfig([ + coreConfig, + nodeConfig, + reactNativeConfig, + browserConfig, + iifeConfig, ]) From 3ec3cc9374a280492c4684b23b853e577e33a94c Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 23 Oct 2023 09:50:08 +0200 Subject: [PATCH 08/12] chore: remove dry release --- .github/workflows/release.yml | 2 +- MIGRATING.md | 659 ---------------------------------- 2 files changed, 1 insertion(+), 660 deletions(-) delete mode 100644 MIGRATING.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 540f3d59f..ffd36fa8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: run: pnpm test - name: Release - run: pnpm release --dry-run + run: pnpm release env: GITHUB_TOKEN: ${{ secrets.GH_ADMIN_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/MIGRATING.md b/MIGRATING.md deleted file mode 100644 index 8ddc0e003..000000000 --- a/MIGRATING.md +++ /dev/null @@ -1,659 +0,0 @@ -# Migration guide - -This guide will help you migrate from the latest version of MSW to the `next` release that introduces a first-class support for Fetch API primitives to the library. **This is a breaking change**. In fact, this is the biggest change to our public API since the day the library was first published. Do not fret, however, as this is precisely why this document exists. - -## Getting started - -```sh -npm install msw@next --save-dev -``` - -## Table of contents - -To help you navigate, we've structured this guide on the feature basis. You can read it top-to-bottom, or you can jump to a particular feature you have trouble migrating from. - -- [**Imports**](#imports) -- [**Response resolver**](#response-resolver) (call signature change) -- [Request changes](#request-changes) -- [req.params](#reqparams) -- [req.cookies](#request-cookies) -- [req.passthrough](#reqpassthrough) -- [res.once](#resonce) -- [res.networkError](#resnetworkerror) -- [Context utilities](#context-utilities) - - [ctx.status](#ctxstatus) - - [ctx.set](#ctxset) - - [ctx.cookie](#ctxcookie) - - [ctx.body](#ctxbody) - - [ctx.text](#ctxtext) - - [ctx.json](#ctxjson) - - [ctx.xml](#ctxxml) - - [ctx.data](#ctxdata) - - [ctx.errors](#ctxerrors) - - [ctx.delay](#ctxdelay) - - [ctx.fetch](#ctx-fetch) -- [Life-cycle events](#life-cycle-events) -- [`.printHandlers()`](#print-handlers) -- [Advanced](#advanced) -- [**What's new in this release?**](#whats-new) -- [Common issues](#common-issues) - ---- - -## Imports - -### `rest` becomes `http` - -The `rest` request handler namespace has been renamed to `http`. - -```diff --import { rest } from 'msw' -+import { http } from 'msw' -``` - -This affects the request handlers declaration as well: - -```js -import { http } from 'msw' - -export const handlers = [ - http.get('/resource', resolver), - http.post('/resource', resolver), - http.all('*', resolver), -] -``` - -### Browser imports - -The `setupWorker` API, alongside any related type definitions, are no longer exported from the root of `msw`. Instead, import them from `msw/browser`: - -```diff --import { setupWorker } from 'msw' -+import { setupWorker } from 'msw/browser' -``` - -> Note that the request handlers like `http` and `graphql`, as well as the utility functions like `bypass` and `passthrough` must still be imported from the root-level `msw`. - -## Response resolver - -A response resolver now exposes a single object argument instead of `(req, res, ctx)`. That argument represents resolver information and consists of properties that are always present for all handler types and extra properties specific to handler types. - -### Resolver info - -#### General - -- `request`, a Fetch API `Request` instance representing an intercepted request. -- `cookies`, a parsed cookies object based on the request cookies. - -#### REST-specific - -- `params`, an object of parsed path parameters. - -#### GraphQL-specific - -- `query`, a GraphQL query string extracted from either URL search parameters or a POST request body. -- `variables`, an object of GraphQL query variables. - -### Using a new signature - -To mock responses, you should now return a Fetch API `Response` instance from the response resolver. You no longer need to compose a response via `res()`, and all the context utilities have also [been removed](#context-utilities). - -```js -http.get('/greet/:name', ({ request, params }) => { - console.log('Intercepted %s %s', request.method, request.url) - return new Response(`hello, ${params.name}!`) -}) -``` - -Now, a more complex example for both REST and GraphQL requests. - -```js -import { http, graphql } from 'msw' - -export const handlers = [ - http.put('/user/:id', async ({ request, params, cookies }) => { - // Read request body as you'd normally do with Fetch. - const payload = await request.json() - // Access path parameters like before. - const { id } = params - // Access cookies like before. - const { sessionId } = cookies - - return new Response(null, { status: 201 }) - }), - - graphql.mutation('CreateUser', ({ request, query, variables }) => { - return new Response( - JSON.stringify({ - data: { - user: { - id: 'abc-123', - firstName: variables.firstName, - }, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }), -] -``` - -### Request changes - -Since the returned `request` is now an instance of Fetch API `Request`, there are some changes to its properties. - -#### Request URL - -The `request.url` property is a string (previously, a `URL` instance). If you wish to operate with it like a `URL`, you need to construct it manually: - -```js -http.get('/product', ({ request }) => { - // For example, this is how you would access - // request search parameters now. - const url = new URL(request.url) - const productId = url.searchParams.get('id') -}) -``` - -#### `req.params` - -Path parameters are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.params`). - -```js -http.get('/resource', ({ params }) => { - console.log('Request path parameters:', params) -}) -``` - -#### `req.cookies` - -Request cookies are now exposed directly on the [Resolver info](#resolver-info) object (previously, `req.cookies`). - -```js -http.get('/resource', ({ cookies }) => { - console.log('Request cookies:', cookies) -}) -``` - -#### Request body - -The library now does no assumptions when reading the intercepted request's body (previously, `req.body`). Instead, you are in charge to read the request body as you see appropriate. - -> Note that since the intercepted request is now represented by a Fetch API `Request` instance, its `request.body` property still exists but returns a `ReadableStream`. - -For example, this is how you would read request body: - -```js -http.post('/resource', async ({ request }) => { - const data = await request.json() - // request.formData() / request.arrayBuffer() / etc. -}) -``` - -### Convenient response declarations - -Using the Fetch API `Response` instance may get quite verbose. To give you more convenient means of declaring mocked responses while remaining specification compliant and compatible, the library now exports an `HttpResponse` object. You can use that object to construct response instances faster. - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/user', () => { - // This is synonymous to "ctx.json()": - // HttpResponse.json() stringifies the given body - // and sets the correct "Content-Type" response header - // to describe a JSON response body. - return HttpResponse.json({ firstName: 'John' }) - }), -] -``` - -> Read more on how to use `HttpResponse` to mock [REST API](#rest-response-body-utilities) and [GraphQL API](#graphql-response-body-utilities) responses. - -## Responses in Node.js - -Although MSW now respects the Fetch API specification, the older versions of Node.js do not, so you can't construct a `Response` instance because there is no such global class. - -To account for this, the library exports a `Response` class that you should use when declaring request handlers. Behind the hood, that response class is resolved to a compatible polyfill in Node.js; in the browser, it only aliases `global.Response` without introducing additional behaviors. - -```js -import { http,Response } from 'msw' - -setupServer( - http.get('/ping', () => { - return new Response('hello world) - }) -) -``` - -Relying on a single universal `Response` class will allow you to write request handlers that can run in both browser and Node.js environments. - -## `res.once` - -To create a one-time request handler, pass it an object as the third argument with `once: true` set: - -```js -import { HttpResponse, http } from 'msw' - -export const handlers = [ - http.get( - '/user', - () => { - return HttpResponse.text('hello') - }, - { once: true }, - ), -] -``` - -## `res.networkError` - -To respond to a request with a network error, use the `HttpResponse.error()` static method: - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.error() - }), -] -``` - -> Note that we are dropping support for custom network error messages to be more compliant with the standard [`Response.error()`](https://developer.mozilla.org/en-US/docs/Web/API/Response/error_static) network errors, which don't support custom error messages. - -## `req.passthrough` - -```js -import { http, passthrough } from 'msw' - -export const handlers = [ - http.get('/user', () => { - // Previously, "req.passthrough()". - return passthrough() - }), -] -``` - ---- - -## Context utilities - -Most of the context utilities you'd normally use via `ctx.*` were removed. Instead, we encourage you to set respective properties directly on the response instance: - -```js -import { HttpResponse, http } from 'msw' - -export const handlers = [ - http.post('/user', () => { - // ctx.json() - return HttpResponse.json( - { firstName: 'John' }, - { - status: 201, // ctx.status() - headers: { - 'X-Custom-Header': 'value', // ctx.set() - }, - }, - ) - }), -] -``` - -Let's go through each previously existing context utility and see how to declare its analogue using the `Response` class. - -### `ctx.status` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.text('hello', { status: 201 }) - }), -] -``` - -### `ctx.set` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.text('hello', { - headers: { - 'Content-Type': 'text/plain; charset=windows-1252', - }, - }) - }), -] -``` - -### `ctx.cookie` - -```js -import { HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.text('hello', { - headers: { - 'Set-Cookie': 'token=abc-123', - }, - }) - }), -] -``` - -When you provide an object as the `ResponseInit.headers` value, you cannot specify multiple response cookies with the same name. Instead, to support multiple response cookies, provide a `Headers` instance: - -```js -import { HttpResponse, http } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return new HttpResponse(null, { - headers: new Headers([ - // Mock a multi-value response cookie header. - ['Set-Cookie', 'sessionId=123'], - ['Set-Cookie', 'gtm=en_US'], - ]), - }) - }), -] -``` - -> This is applicable to any multi-value headers, really. - -### `ctx.body` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return new HttpResponse('any-body') - }), -] -``` - -> Do not forget to set the `Content-Type` header that represents the mocked response's body type. If using common response body types, like text or json, see the respective migration instructions for those context utilities below. - -### `ctx.text` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.text('hello') - }), -] -``` - -### `ctx.json` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.json({ firstName: 'John' }) - }), -] -``` - -### `ctx.xml` - -```js -import { http, HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.xml('') - }), -] -``` - -### `ctx.data` - -The `ctx.data` utility has been removed in favor of constructing a mocked JSON response with the "data" property in it. - -```js -import { HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.json({ - data: { - user: { - firstName: 'John', - }, - }, - }) - }), -] -``` - -### `ctx.errors` - -The `ctx.errors` utility has been removed in favor of constructing a mocked JSON response with the "errors" property in it. - -```js -import { HttpResponse } from 'msw' - -export const handlers = [ - http.get('/resource', () => { - return HttpResponse.json({ - errors: [ - { - message: 'Something went wrong', - }, - ], - }) - }), -] -``` - -### `ctx.delay` - -```js -import { http, HttpResponse, delay } from 'msw' - -export const handlers = [ - http.get('/resource', async () => { - await delay() - return HttpResponse.text('hello') - }), -] -``` - -The `delay` function has the same call signature as the `ctx.delay` context function. This means it supports the delay mode as an argument: - -```js -await delay(500) -await delay('infinite') -``` - -### `ctx.fetch` - -The `ctx.fetch()` function has been removed in favor of the `bypass()` function. You should now always perform a regular `fetch()` call and wrap the request in the `bypass()` function if you wish for it to ignore any otherwise matching request handlers. - -```js -import { http, HttpResponse, bypass } from 'msw' - -export const handlers = [ - http.get('/resource', async ({ request }) => { - // Use the regular "fetch" from your environment. - const originalResponse = await fetch(bypass(request)) - const json = await originalResponse.json() - - // ...handle the original response, maybe return a mocked one. - }), -] -``` - -The `bypass()` function also accepts `RequestInit` as the second argument to modify the bypassed request. - -```js -// Bypass the given "request" and modify its headers. -bypass(request, { - headers: { - 'X-Modified-Header': 'true', - }, -}) -``` - ---- - -## Life-cycle events - -The life-cycle events listeners now accept a single argument being an object with contextual properties. - -```diff --server.events.on('request:start', (request, requestId) = {}) -+server.events.on('request:start', ({ request, requestId}) => {}) -``` - -The request and response instances exposed in the life-cycle API have also been updated to return Fetch API `Request` and `Response` respectively. - -The request ID is now exposed as a standalone argument (previously, `req.id`). - -```js -server.events.on('request:start', ({ request, requestId }) => { - console.log(request.method, request.url) -}) -``` - -To read a request body, make sure to clone the request first. Otherwise, it won't be performed as it would be already read. - -```js -server.events.on('request:match', async ({ request }) => { - // Make sure to clone the request so it could be - // processed further down the line. - const clone = request.clone() - const json = await clone.json() - - console.log('Performed request with body:', json) -}) -``` - -The `response:*` events now always contain the response reference, the related request, and its id in the listener arguments. - -```js -worker.events.on('response:mocked', ({ response, request, requestId }) => { - console.log('response to %s %s is:', request.method, request.url, response) -}) -``` - ---- - -## `.printHandlers() - -The `worker.prinHandlers()` and `server.printHandlers()` methods were removed. Use the `.listHandlers()` method instead: - -```diff --worker.printHandlers() -+console.log(worker.listHandlers()) -``` - ---- - -## Advanced - -It is still possible to create custom handlers and resolvers, just make sure to account for the new [resolver call signature](#response-resolver). - -### Custom response composition - -As this release removes the concept of response composition via `res()`, you can no longer compose context utilities or abstract their partial composed state to a helper function. - -Instead, you can abstract a common response logic into a plain function that creates a new `Response` or modifies a provided instance. - -```js -// utils.js -import { HttpResponse } from 'msw' - -export function augmentResponse(json) { - const response = HttpResponse.json(json, { - // Come up with some reusable defaults here. - }) - return response -} -``` - -```js -import { http } from 'msw' -import { augmentResponse } from './utils' - -export const handlers = [ - http.get('/user', () => { - return augmentResponse({ id: 1 }) - }), -] -``` - ---- - -## What's new? - -The main benefit of this release is the adoption of Fetch API primitives—`Request` and `Response` classes. By handling requests and responses as the platform does it, you bring your API mocking setup to the next level. Less library-specific abstractions, flatter learning curve, improved compatibility with other tools. But, most importantly, specification compliance and investment into a solution that uses standard APIs that are here to stay. - -### New request body methods - -You can now read the intercepted request body as you would a regular `Request` instance. This mainly means the addition of the following methods on the `request`: - -- `request.blob()` -- `request.formData()` -- `request.arrayBuffer()` - -For example, this is how you would read the request as `Blob`: - -```js -import { http } from 'msw' - -export const handlers = [ - http.get('/resource', async ({ request }) => { - const blob = await request.blob() - }), -] -``` - -### Support `ReadableStream` mocked responses - -You can now send a `ReadableStream` as the mocked response body. This is great for mocking any kind of streaming in HTTP responses. - -```js -import { http, HttpResponse, delay } from 'msw' - -http.get('/greeting', () => { - const encoder = new TextEncoder() - const stream = new ReadableStream({ - async start(controller) { - controller.enqueue(encoder.encode('hello')) - await delay(100) - controller.enqueue(encoder.encode('world')) - await delay(100) - controller.close() - }, - }) - - return new HttpResponse(stream) -}) -``` - ---- - -## Common issues - -### `Response is not defined` - -This likely means that you are running an old version of Node.js. Please use Node.js v18.14.0 and higher with this version of MSW. Also, see [this](#responses-in-nodejs). - -### `multipart/form-data is not supported` in Node.js - -Earlier versions of Node.js 18, like v18.8.0, had no support for `request.formData()`. Please upgrade to the latest Node.js version where Undici have added the said support to resolve the issue. From 873b9963de2c35832ea3f96cdb7b977d4fab9d54 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 23 Oct 2023 08:05:58 +0000 Subject: [PATCH 09/12] chore(release): v2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8aec4edaa..96a756a44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw", - "version": "1.3.2", + "version": "2.0.0", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "main": "./lib/core/index.js", "module": "./lib/core/index.mjs", From a54138a62a7e89bd5f768d369e6b5bc4c46686ae Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 23 Oct 2023 12:36:33 +0200 Subject: [PATCH 10/12] chore: update minimal node version in issue template --- .github/ISSUE_TEMPLATE/02-issue-nodejs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/02-issue-nodejs.yml b/.github/ISSUE_TEMPLATE/02-issue-nodejs.yml index 1f50812c6..9d82472c0 100644 --- a/.github/ISSUE_TEMPLATE/02-issue-nodejs.yml +++ b/.github/ISSUE_TEMPLATE/02-issue-nodejs.yml @@ -23,14 +23,14 @@ body: 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 14 or higher + - label: I'm using Node.js version 18 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. v16.14.0 + placeholder: i.e. v18.14.0 validations: required: true From 48d8fdf0f416504def82a47f2e69dc8b4541aced Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 26 Oct 2023 11:47:31 +0200 Subject: [PATCH 11/12] docs: add codacy to gold sponsors (#1797) --- README.md | 5 +++++ media/sponsors/codacy.svg | 45 +++++++++++++++++++++++++++++++++++++++ media/sponsors/github.svg | 5 +++++ 3 files changed, 55 insertions(+) create mode 100644 media/sponsors/codacy.svg diff --git a/README.md b/README.md index 38edf3573..da26ec1f0 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,11 @@ Mock Service Worker is trusted by hundreds of thousands of engineers around the + + + Codacy + + diff --git a/media/sponsors/codacy.svg b/media/sponsors/codacy.svg new file mode 100644 index 000000000..5ad5f930c --- /dev/null +++ b/media/sponsors/codacy.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/media/sponsors/github.svg b/media/sponsors/github.svg index a8d117404..dde07a89a 100644 --- a/media/sponsors/github.svg +++ b/media/sponsors/github.svg @@ -1,3 +1,8 @@ + From 1d1fbca6a39b0d89c7e18bc50cab73ddac7ea5f0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 26 Oct 2023 12:41:43 +0200 Subject: [PATCH 12/12] docs: add egghead banner to readme (#1798) --- README.md | 10 +++++++++- media/egghead-banner.png | Bin 0 -> 533107 bytes 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 media/egghead-banner.png diff --git a/README.md b/README.md index da26ec1f0..3ba4a5c87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -> [!IMPORTANT]\ > **MSW 2.0 is finally here! 🎉** Read the [Release notes](https://github.com/mswjs/msw/releases/tag/v2.0.0) and please follow the [**Migration guidelines**](https://mswjs.io/docs/migrations/1.x-to-2.x) to upgrade. If you're having any questions while upgrading, please reach out in our [Discord server](https://kettanaito.com/discord). +> +> We've also recorded the most comprehensive introduction to MSW ever. Learn how to mock APIs like a pro in our official video course: + + + Mock REST and GraphQL APIs with Mock Service Worker + +

@@ -24,6 +30,8 @@
+
+ ## 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. diff --git a/media/egghead-banner.png b/media/egghead-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..35584bc4bc04f82e6d3e2958eac5bef09325ec42 GIT binary patch literal 533107 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
yO%ZUs2Ye!x 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

+gO@@2}w> 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

-6)33YH?)}{ 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

$rutGjXw&lw)sL1v_CJ|ZA#0Y*W7MfzHR-{Z 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 z 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

tCsO{k`F^B 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 aCk)tUsflx$cgAT>pT`e$?^mi@hy53U_>1iT literal 0 HcmV?d00001