@msw/cloudflare 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # `@msw/cloudflare`
2
+
3
+ Develop and test Cloudflare applications with Mock Service Worker.
4
+
5
+ ## Getting started
6
+
7
+ ### Install
8
+
9
+ ```sh
10
+ npm i https://pkg.pr.new/mswjs/cloudflare/@msw/cloudflare@beta msw
11
+ ```
12
+
13
+ > This package requires `msw` as a peer dependency.
14
+
15
+ ### Usage
16
+
17
+ Let's say your worker performs a GET request to `https://example.com/user`.
18
+
19
+ ```ts
20
+ // worker.ts
21
+ export default {
22
+ async fetch(req, env, ctx) {
23
+ const response = await fetch('https://api.example.com/user')
24
+ const user = await response.json()
25
+
26
+ return new Response(user.id, {
27
+ headers: { 'content-type': 'text/plain' },
28
+ })
29
+ },
30
+ }
31
+ ```
32
+
33
+ Here's how you can intercept and mock that third-party request in order to reliably test your worker in Vitest.
34
+
35
+ ```ts
36
+ import { env } from 'cloudflare:workers'
37
+ import { createExecutionContext } from 'cloudflare:test'
38
+ import { http, HttpResponse } from 'msw'
39
+ import { setupNetwork } from '@msw/cloudflare'
40
+ import worker from './worker'
41
+
42
+ const network = setupNetwork()
43
+
44
+ beforeAll(() => {
45
+ network.enable()
46
+ })
47
+
48
+ afterEach(() => {
49
+ network.resetHandlers()
50
+ })
51
+
52
+ afterAll(() => {
53
+ network.disable()
54
+ })
55
+
56
+ it('responds with the user id', async () => {
57
+ network.use(
58
+ http.get('https://api.example.com/user', () => {
59
+ return HttpResponse.json({ id: 1, name: 'John Maverick' })
60
+ }),
61
+ )
62
+
63
+ const ctx = createExecutionContext()
64
+ const response = await worker.fetch(
65
+ new Request('http://localhost/'),
66
+ env,
67
+ ctx,
68
+ )
69
+
70
+ expect.soft(response.status).toBe(200)
71
+ await expect.soft(response.text()).resolves.toBe('1')
72
+ })
73
+ ```
74
+
75
+ ## Related materials
76
+
77
+ - [**Write your first test in Cloudflare Docs**](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/)
78
+ - [Mocking HTTP with MSW](https://mswjs.io/docs/http/)
79
+ - [Mocking GraphQL with MSW](https://mswjs.io/docs/graphql/)
80
+ - [Mocking WebSocket with MSW](https://mswjs.io/docs/websocket/)
@@ -0,0 +1,8 @@
1
+ import * as _$msw_experimental0 from "msw/experimental";
2
+ import { InterceptorSource } from "msw/experimental";
3
+
4
+ //#region src/index.d.ts
5
+ declare function setupNetwork(): _$msw_experimental0.NetworkApi<InterceptorSource[]>;
6
+ //#endregion
7
+ export { setupNetwork };
8
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,98 @@
1
+ import { bypass, ws } from "msw";
2
+ import { InMemoryHandlersController, InterceptorSource, defineNetwork } from "msw/experimental";
3
+ import { createRequestId, resolveWebSocketUrl } from "@mswjs/interceptors";
4
+ import { FetchInterceptor } from "@mswjs/interceptors/fetch";
5
+ import { WebSocketInterceptor } from "@mswjs/interceptors/WebSocket";
6
+ //#region src/index.ts
7
+ function setupNetwork() {
8
+ const handlersController = new InMemoryHandlersController([]);
9
+ ws.onUpgrade = async ({ requestId, request }) => {
10
+ const handlers = handlersController.getHandlersByKind("websocket");
11
+ if (handlers.length === 0) return;
12
+ const connectionUrl = resolveWebSocketUrl(new URL(request.url));
13
+ const [client, server] = Object.values(new WebSocketPair());
14
+ const connection = {
15
+ client: new CloudflareWebSocketClientConnection({
16
+ url: connectionUrl,
17
+ socket: server
18
+ }),
19
+ server: new CloudflareWebSocketServerConnection({ url: request.url }),
20
+ info: { protocols: [] }
21
+ };
22
+ for (const handler of handlers) await handler.run({
23
+ requestId,
24
+ request,
25
+ ...connection
26
+ });
27
+ return new Response(null, {
28
+ status: 101,
29
+ webSocket: client
30
+ });
31
+ };
32
+ return defineNetwork({
33
+ sources: [new InterceptorSource({ interceptors: [new FetchInterceptor(), new WebSocketInterceptor()] })],
34
+ handlers: handlersController,
35
+ context: { quiet: true }
36
+ });
37
+ }
38
+ var CloudflareWebSocketClientConnection = class {
39
+ #socket;
40
+ id;
41
+ url;
42
+ constructor(options) {
43
+ this.#socket = options.socket;
44
+ this.id = createRequestId();
45
+ this.url = new URL(options.url);
46
+ }
47
+ send(data) {
48
+ this.#socket.accept();
49
+ this.#socket.send(data);
50
+ }
51
+ close(code, reason) {
52
+ this.#socket.close(code, reason);
53
+ }
54
+ addEventListener(type, listener, options) {
55
+ this.#socket.addEventListener(type, listener, options);
56
+ }
57
+ removeEventListener(event, listener, options) {
58
+ this.#socket.removeEventListener(event, listener, options);
59
+ }
60
+ };
61
+ var CloudflareWebSocketServerConnection = class {
62
+ #url;
63
+ #pendingSocket;
64
+ constructor(options) {
65
+ this.#url = options.url;
66
+ this.#pendingSocket = Promise.withResolvers();
67
+ }
68
+ connect() {
69
+ const upgradeRequest = new Request(this.#url, { headers: { upgrade: "websocket" } });
70
+ fetch(bypass(upgradeRequest)).then((response) => {
71
+ if (!response.webSocket) throw new Error(`Failed to establish an actual WebSocket connection at "${this.#url}": the server did not approve the handshake`);
72
+ response.webSocket.accept();
73
+ this.#pendingSocket.resolve(response.webSocket);
74
+ }).catch((error) => {
75
+ this.#pendingSocket.reject(error);
76
+ });
77
+ }
78
+ send(data) {
79
+ this.#pendingSocket.promise.then((socket) => socket.send(data));
80
+ }
81
+ close() {
82
+ this.#pendingSocket.promise.then((socket) => socket.close());
83
+ }
84
+ addEventListener(event, listener, options) {
85
+ this.#pendingSocket.promise.then((socket) => {
86
+ socket.addEventListener(event, listener, options);
87
+ });
88
+ }
89
+ removeEventListener(event, listener, options) {
90
+ this.#pendingSocket.promise.then((socket) => {
91
+ socket.removeEventListener(event, listener, options);
92
+ });
93
+ }
94
+ };
95
+ //#endregion
96
+ export { setupNetwork };
97
+
98
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["#socket","#url","#pendingSocket"],"sources":["../src/index.ts"],"sourcesContent":["import { bypass, ws } from 'msw'\nimport {\n defineNetwork,\n InterceptorSource,\n InMemoryHandlersController,\n} from 'msw/experimental'\nimport { createRequestId, resolveWebSocketUrl } from '@mswjs/interceptors'\nimport { FetchInterceptor } from '@mswjs/interceptors/fetch'\nimport {\n WebSocketInterceptor,\n type WebSocketData,\n type WebSocketServerEventMap,\n type WebSocketClientEventMap,\n type WebSocketClientConnectionProtocol,\n type WebSocketServerConnectionProtocol,\n} from '@mswjs/interceptors/WebSocket'\n\nexport function setupNetwork() {\n const handlersController = new InMemoryHandlersController([])\n\n ws.onUpgrade = async ({ requestId, request }) => {\n const handlers = handlersController.getHandlersByKind('websocket')\n\n if (handlers.length === 0) {\n return\n }\n\n const url = new URL(request.url)\n const connectionUrl = resolveWebSocketUrl(url)\n const [client, server] = Object.values(new WebSocketPair())\n\n const connection = {\n client: new CloudflareWebSocketClientConnection({\n url: connectionUrl,\n socket: server,\n }),\n server: new CloudflareWebSocketServerConnection({\n url: request.url,\n }),\n info: {\n protocols: [],\n },\n }\n\n for (const handler of handlers) {\n await handler.run({\n requestId,\n request,\n ...connection,\n })\n }\n\n return new Response(null, {\n status: 101,\n webSocket: client,\n })\n }\n\n const network = defineNetwork({\n sources: [\n new InterceptorSource({\n interceptors: [new FetchInterceptor(), new WebSocketInterceptor()],\n }),\n ],\n handlers: handlersController,\n context: {\n quiet: true,\n },\n })\n\n return network\n}\n\nclass CloudflareWebSocketClientConnection implements WebSocketClientConnectionProtocol {\n #socket: WebSocket\n\n public id: string\n public url: URL\n\n constructor(options: { url: string; socket: WebSocket }) {\n this.#socket = options.socket\n\n this.id = createRequestId()\n this.url = new URL(options.url)\n }\n\n send(data: WebSocketData): void {\n this.#socket.accept()\n this.#socket.send(data as any)\n }\n\n close(code?: number, reason?: string): void {\n this.#socket.close(code, reason)\n }\n\n addEventListener<EventType extends keyof WebSocketClientEventMap>(\n type: EventType,\n listener: (\n this: WebSocket,\n event: WebSocketClientEventMap[EventType],\n ) => void,\n options?: AddEventListenerOptions | boolean,\n ): void {\n this.#socket.addEventListener(type, listener, options)\n }\n\n removeEventListener<EventType extends keyof WebSocketClientEventMap>(\n event: EventType,\n listener: (\n this: WebSocket,\n event: WebSocketClientEventMap[EventType],\n ) => void,\n options?: EventListenerOptions | boolean,\n ): void {\n this.#socket.removeEventListener(event, listener, options)\n }\n}\n\nclass CloudflareWebSocketServerConnection implements WebSocketServerConnectionProtocol {\n #url: string\n #pendingSocket: PromiseWithResolvers<WebSocket>\n\n constructor(options: { url: string }) {\n this.#url = options.url\n this.#pendingSocket = Promise.withResolvers<WebSocket>()\n }\n\n connect(): void {\n const upgradeRequest = new Request(this.#url, {\n headers: {\n upgrade: 'websocket',\n },\n })\n\n fetch(bypass(upgradeRequest))\n .then((response) => {\n if (!response.webSocket) {\n throw new Error(\n `Failed to establish an actual WebSocket connection at \"${this.#url}\": the server did not approve the handshake`,\n )\n }\n\n response.webSocket.accept()\n this.#pendingSocket.resolve(response.webSocket)\n })\n .catch((error) => {\n this.#pendingSocket.reject(error)\n })\n }\n\n send(data: WebSocketData): void {\n this.#pendingSocket.promise.then((socket) => socket.send(data as any))\n }\n\n close(): void {\n this.#pendingSocket.promise.then((socket) => socket.close())\n }\n\n addEventListener<EventType extends keyof WebSocketServerEventMap>(\n event: EventType,\n listener: (\n this: WebSocket,\n event: WebSocketServerEventMap[EventType],\n ) => void,\n options?: AddEventListenerOptions | boolean,\n ): void {\n this.#pendingSocket.promise.then((socket) => {\n socket.addEventListener(event, listener, options)\n })\n }\n\n removeEventListener<EventType extends keyof WebSocketServerEventMap>(\n event: EventType,\n listener: (\n this: WebSocket,\n event: WebSocketServerEventMap[EventType],\n ) => void,\n options?: EventListenerOptions | boolean,\n ): void {\n this.#pendingSocket.promise.then((socket) => {\n socket.removeEventListener(event, listener, options)\n })\n }\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,eAAe;CAC7B,MAAM,qBAAqB,IAAI,2BAA2B,EAAE,CAAC;AAE7D,IAAG,YAAY,OAAO,EAAE,WAAW,cAAc;EAC/C,MAAM,WAAW,mBAAmB,kBAAkB,YAAY;AAElE,MAAI,SAAS,WAAW,EACtB;EAIF,MAAM,gBAAgB,oBAAoB,IAD1B,IAAI,QAAQ,IACiB,CAAC;EAC9C,MAAM,CAAC,QAAQ,UAAU,OAAO,OAAO,IAAI,eAAe,CAAC;EAE3D,MAAM,aAAa;GACjB,QAAQ,IAAI,oCAAoC;IAC9C,KAAK;IACL,QAAQ;IACT,CAAC;GACF,QAAQ,IAAI,oCAAoC,EAC9C,KAAK,QAAQ,KACd,CAAC;GACF,MAAM,EACJ,WAAW,EAAE,EACd;GACF;AAED,OAAK,MAAM,WAAW,SACpB,OAAM,QAAQ,IAAI;GAChB;GACA;GACA,GAAG;GACJ,CAAC;AAGJ,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ;GACR,WAAW;GACZ,CAAC;;AAeJ,QAZgB,cAAc;EAC5B,SAAS,CACP,IAAI,kBAAkB,EACpB,cAAc,CAAC,IAAI,kBAAkB,EAAE,IAAI,sBAAsB,CAAC,EACnE,CAAC,CACH;EACD,UAAU;EACV,SAAS,EACP,OAAO,MACR;EACF,CAEa;;AAGhB,IAAM,sCAAN,MAAuF;CACrF;CAEA;CACA;CAEA,YAAY,SAA6C;AACvD,QAAA,SAAe,QAAQ;AAEvB,OAAK,KAAK,iBAAiB;AAC3B,OAAK,MAAM,IAAI,IAAI,QAAQ,IAAI;;CAGjC,KAAK,MAA2B;AAC9B,QAAA,OAAa,QAAQ;AACrB,QAAA,OAAa,KAAK,KAAY;;CAGhC,MAAM,MAAe,QAAuB;AAC1C,QAAA,OAAa,MAAM,MAAM,OAAO;;CAGlC,iBACE,MACA,UAIA,SACM;AACN,QAAA,OAAa,iBAAiB,MAAM,UAAU,QAAQ;;CAGxD,oBACE,OACA,UAIA,SACM;AACN,QAAA,OAAa,oBAAoB,OAAO,UAAU,QAAQ;;;AAI9D,IAAM,sCAAN,MAAuF;CACrF;CACA;CAEA,YAAY,SAA0B;AACpC,QAAA,MAAY,QAAQ;AACpB,QAAA,gBAAsB,QAAQ,eAA0B;;CAG1D,UAAgB;EACd,MAAM,iBAAiB,IAAI,QAAQ,MAAA,KAAW,EAC5C,SAAS,EACP,SAAS,aACV,EACF,CAAC;AAEF,QAAM,OAAO,eAAe,CAAC,CAC1B,MAAM,aAAa;AAClB,OAAI,CAAC,SAAS,UACZ,OAAM,IAAI,MACR,0DAA0D,MAAA,IAAU,6CACrE;AAGH,YAAS,UAAU,QAAQ;AAC3B,SAAA,cAAoB,QAAQ,SAAS,UAAU;IAC/C,CACD,OAAO,UAAU;AAChB,SAAA,cAAoB,OAAO,MAAM;IACjC;;CAGN,KAAK,MAA2B;AAC9B,QAAA,cAAoB,QAAQ,MAAM,WAAW,OAAO,KAAK,KAAY,CAAC;;CAGxE,QAAc;AACZ,QAAA,cAAoB,QAAQ,MAAM,WAAW,OAAO,OAAO,CAAC;;CAG9D,iBACE,OACA,UAIA,SACM;AACN,QAAA,cAAoB,QAAQ,MAAM,WAAW;AAC3C,UAAO,iBAAiB,OAAO,UAAU,QAAQ;IACjD;;CAGJ,oBACE,OACA,UAIA,SACM;AACN,QAAA,cAAoB,QAAQ,MAAM,WAAW;AAC3C,UAAO,oBAAoB,OAAO,UAAU,QAAQ;IACpD"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@msw/cloudflare",
4
+ "version": "0.0.0",
5
+ "description": "Mock API in Cloudflare with Mock Service Worker.",
6
+ "exports": {
7
+ ".": "./build/index.mjs"
8
+ },
9
+ "files": [
10
+ "./build"
11
+ ],
12
+ "scripts": {
13
+ "start": "tsdown -w",
14
+ "test": "vitest",
15
+ "lint": "publint",
16
+ "build": "tsdown",
17
+ "publish": "release publish"
18
+ },
19
+ "keywords": [
20
+ "cloudflare",
21
+ "mock",
22
+ "api",
23
+ "intercept",
24
+ "request",
25
+ "network",
26
+ "test"
27
+ ],
28
+ "author": "Artem Zakharchenko <kettanaito@gmail.com>",
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "msw": ">=2.14.1"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "homepage": "https://github.com/mswjs/cloudflare",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/mswjs/cloudflare.git"
40
+ },
41
+ "dependencies": {
42
+ "@mswjs/interceptors": "^0.41.6"
43
+ },
44
+ "devDependencies": {
45
+ "@cloudflare/vitest-pool-workers": "^0.14.9",
46
+ "@cloudflare/workers-types": "^4.20260424.1",
47
+ "@epic-web/test-server": "^0.1.6",
48
+ "@ossjs/release": "^0.11.0",
49
+ "msw": "^2.14.6",
50
+ "publint": "^0.3.18",
51
+ "tsdown": "^0.21.10",
52
+ "typescript": "^6.0.3",
53
+ "vitest": "^4.1.5"
54
+ }
55
+ }