@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 +80 -0
- package/build/index.d.mts +8 -0
- package/build/index.mjs +98 -0
- package/build/index.mjs.map +1 -0
- package/package.json +55 -0
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
|
package/build/index.mjs
ADDED
|
@@ -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
|
+
}
|