wrangler 3.12.0 → 3.13.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/package.json +2 -2
- package/templates/startDevWorker/InspectorProxyWorker.ts +526 -0
- package/templates/startDevWorker/ProxyWorker.ts +260 -0
- package/wrangler-dist/InspectorProxyWorker.js +361 -0
- package/wrangler-dist/InspectorProxyWorker.js.map +6 -0
- package/wrangler-dist/ProxyWorker.js +188 -0
- package/wrangler-dist/ProxyWorker.js.map +6 -0
- package/wrangler-dist/cli.d.ts +21785 -98
- package/wrangler-dist/cli.js +11522 -11001
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wrangler",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Command-line interface for all things Cloudflare Workers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wrangler",
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"dev": "pnpm run clean && concurrently -c black,blue --kill-others-on-fail false 'pnpm run bundle --watch' 'pnpm run check:type --watch --preserveWatchOutput'",
|
|
201
201
|
"emit-types": "tsc -p tsconfig.emit.json && node -r esbuild-register scripts/emit-types.ts",
|
|
202
202
|
"start": "pnpm run bundle && cross-env NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js",
|
|
203
|
-
"test": "pnpm run assert-git-version && jest",
|
|
203
|
+
"test": "pnpm run assert-git-version && jest --runInBand",
|
|
204
204
|
"test:ci": "pnpm run test --coverage",
|
|
205
205
|
"test:debug": "pnpm run test --silent=false --verbose=true",
|
|
206
206
|
"test:e2e": "vitest --test-timeout 240000 --single-thread --dir ./e2e run",
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import {
|
|
3
|
+
DevToolsCommandRequest,
|
|
4
|
+
DevToolsCommandRequests,
|
|
5
|
+
DevToolsCommandResponses,
|
|
6
|
+
DevToolsEvent,
|
|
7
|
+
DevToolsEvents,
|
|
8
|
+
type InspectorProxyWorkerIncomingWebSocketMessage,
|
|
9
|
+
type InspectorProxyWorkerOutgoingRequestBody,
|
|
10
|
+
type InspectorProxyWorkerOutgoingWebsocketMessage,
|
|
11
|
+
type ProxyData,
|
|
12
|
+
serialiseError,
|
|
13
|
+
} from "../../src/api/startDevWorker/events";
|
|
14
|
+
import {
|
|
15
|
+
assertNever,
|
|
16
|
+
createDeferred,
|
|
17
|
+
DeferredPromise,
|
|
18
|
+
MaybePromise,
|
|
19
|
+
urlFromParts,
|
|
20
|
+
} from "../../src/api/startDevWorker/utils";
|
|
21
|
+
|
|
22
|
+
interface Env {
|
|
23
|
+
PROXY_CONTROLLER: Fetcher;
|
|
24
|
+
PROXY_CONTROLLER_AUTH_SECRET: string;
|
|
25
|
+
WRANGLER_VERSION: string;
|
|
26
|
+
DURABLE_OBJECT: DurableObjectNamespace;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
fetch(req, env) {
|
|
31
|
+
const singleton = env.DURABLE_OBJECT.idFromName("");
|
|
32
|
+
const inspectorProxy = env.DURABLE_OBJECT.get(singleton);
|
|
33
|
+
|
|
34
|
+
return inspectorProxy.fetch(req);
|
|
35
|
+
},
|
|
36
|
+
} as ExportedHandler<Env>;
|
|
37
|
+
|
|
38
|
+
function isDevToolsEvent<Method extends DevToolsEvents["method"]>(
|
|
39
|
+
event: unknown,
|
|
40
|
+
name: Method
|
|
41
|
+
): event is DevToolsEvent<Method> {
|
|
42
|
+
return (
|
|
43
|
+
typeof event === "object" &&
|
|
44
|
+
event !== null &&
|
|
45
|
+
"method" in event &&
|
|
46
|
+
event.method === name
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class InspectorProxyWorker implements DurableObject {
|
|
51
|
+
constructor(_state: DurableObjectState, readonly env: Env) {}
|
|
52
|
+
|
|
53
|
+
websockets: {
|
|
54
|
+
proxyController?: WebSocket;
|
|
55
|
+
runtime?: WebSocket;
|
|
56
|
+
devtools?: WebSocket;
|
|
57
|
+
|
|
58
|
+
// Browser DevTools cannot read the filesystem,
|
|
59
|
+
// instead they fetch via `Network.loadNetworkResource` messages.
|
|
60
|
+
// IDE DevTools can read the filesystem and expect absolute paths.
|
|
61
|
+
devtoolsHasFileSystemAccess?: boolean;
|
|
62
|
+
|
|
63
|
+
// We want to be able to delay devtools connection response
|
|
64
|
+
// until we've connected to the runtime inspector server
|
|
65
|
+
// so this deferred holds a promise to websockets.runtime
|
|
66
|
+
runtimeDeferred: DeferredPromise<WebSocket>;
|
|
67
|
+
} = {
|
|
68
|
+
runtimeDeferred: createDeferred<WebSocket>(),
|
|
69
|
+
};
|
|
70
|
+
proxyData?: ProxyData;
|
|
71
|
+
runtimeMessageBuffer: (DevToolsCommandResponses | DevToolsEvents)[] = [];
|
|
72
|
+
|
|
73
|
+
async fetch(req: Request) {
|
|
74
|
+
if (
|
|
75
|
+
req.headers.get("Authorization") === this.env.PROXY_CONTROLLER_AUTH_SECRET
|
|
76
|
+
) {
|
|
77
|
+
return this.handleProxyControllerRequest(req);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.headers.get("Upgrade") === "websocket") {
|
|
81
|
+
return this.handleDevToolsWebSocketUpgradeRequest(req);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.handleDevToolsJsonRequest(req);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ************************
|
|
88
|
+
// ** PROXY CONTROLLER **
|
|
89
|
+
// ************************
|
|
90
|
+
|
|
91
|
+
handleProxyControllerRequest(req: Request) {
|
|
92
|
+
assert(
|
|
93
|
+
req.headers.get("Upgrade") === "websocket",
|
|
94
|
+
"Expected proxy controller data request to be WebSocket upgrade"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const { 0: response, 1: proxyController } = new WebSocketPair();
|
|
98
|
+
proxyController.accept();
|
|
99
|
+
proxyController.addEventListener("close", () => {
|
|
100
|
+
// don't reconnect the proxyController websocket
|
|
101
|
+
// ProxyController can detect this event and reconnect itself
|
|
102
|
+
|
|
103
|
+
if (this.websockets.proxyController === proxyController) {
|
|
104
|
+
this.websockets.proxyController = undefined;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
proxyController.addEventListener("error", () => {
|
|
108
|
+
// don't reconnect the proxyController websocket
|
|
109
|
+
// ProxyController can detect this event and reconnect itself
|
|
110
|
+
|
|
111
|
+
if (this.websockets.proxyController === proxyController) {
|
|
112
|
+
this.websockets.proxyController = undefined;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
proxyController.addEventListener(
|
|
116
|
+
"message",
|
|
117
|
+
this.handleProxyControllerIncomingMessage
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
this.websockets.proxyController = proxyController;
|
|
121
|
+
|
|
122
|
+
return new Response(null, {
|
|
123
|
+
status: 101,
|
|
124
|
+
webSocket: response,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sendProxyControllerMessage(
|
|
129
|
+
message: string | InspectorProxyWorkerOutgoingWebsocketMessage
|
|
130
|
+
) {
|
|
131
|
+
message = typeof message === "string" ? message : JSON.stringify(message);
|
|
132
|
+
|
|
133
|
+
// if the proxyController websocket is disconnected, throw away the message
|
|
134
|
+
this.websockets.proxyController?.send(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async sendProxyControllerRequest(
|
|
138
|
+
message: InspectorProxyWorkerOutgoingRequestBody
|
|
139
|
+
) {
|
|
140
|
+
try {
|
|
141
|
+
const res = await this.env.PROXY_CONTROLLER.fetch("http://dummy", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: JSON.stringify(message),
|
|
144
|
+
});
|
|
145
|
+
return res.ok ? await res.text() : undefined;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
this.sendDebugLog(
|
|
148
|
+
"FAILED TO SEND PROXY CONTROLLER REQUEST",
|
|
149
|
+
serialiseError(e)
|
|
150
|
+
);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
sendDebugLog: typeof console.debug = (...args) => {
|
|
156
|
+
this.sendProxyControllerRequest({ type: "debug-log", args });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ***************
|
|
160
|
+
// ** RUNTIME **
|
|
161
|
+
// ***************
|
|
162
|
+
|
|
163
|
+
handleRuntimeIncomingMessage = (event: MessageEvent) => {
|
|
164
|
+
assert(typeof event.data === "string");
|
|
165
|
+
|
|
166
|
+
const msg = JSON.parse(event.data) as
|
|
167
|
+
| DevToolsCommandResponses
|
|
168
|
+
| DevToolsEvents;
|
|
169
|
+
this.sendDebugLog("RUNTIME INCOMING MESSAGE", msg);
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
isDevToolsEvent(msg, "Runtime.exceptionThrown") ||
|
|
173
|
+
isDevToolsEvent(msg, "Runtime.consoleAPICalled")
|
|
174
|
+
) {
|
|
175
|
+
this.sendProxyControllerMessage(event.data);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.runtimeMessageBuffer.push(msg);
|
|
179
|
+
this.tryDrainRuntimeMessageBuffer();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
handleRuntimeScriptParsed(msg: DevToolsEvent<"Debugger.scriptParsed">) {
|
|
183
|
+
// If the devtools does not have filesystem access,
|
|
184
|
+
// rewrite the sourceMapURL to use a special scheme.
|
|
185
|
+
// This special scheme is used to indicate whether
|
|
186
|
+
// to intercept each loadNetworkResource message.
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
!this.websockets.devtoolsHasFileSystemAccess &&
|
|
190
|
+
msg.params.sourceMapURL !== undefined
|
|
191
|
+
) {
|
|
192
|
+
const url = new URL(msg.params.sourceMapURL, msg.params.url);
|
|
193
|
+
if (url.protocol === "file:") {
|
|
194
|
+
msg.params.sourceMapURL = url.href.replace("file:", "wrangler-file:");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void this.sendDevToolsMessage(msg);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
tryDrainRuntimeMessageBuffer = () => {
|
|
202
|
+
// If we don't have a DevTools WebSocket, try again later
|
|
203
|
+
if (this.websockets.devtools === undefined) return;
|
|
204
|
+
|
|
205
|
+
// clear the buffer and replay each message to devtools
|
|
206
|
+
for (const msg of this.runtimeMessageBuffer.splice(0)) {
|
|
207
|
+
if (isDevToolsEvent(msg, "Debugger.scriptParsed")) {
|
|
208
|
+
this.handleRuntimeScriptParsed(msg);
|
|
209
|
+
} else {
|
|
210
|
+
void this.sendDevToolsMessage(msg);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
handleProxyControllerIncomingMessage = (event: MessageEvent) => {
|
|
216
|
+
assert(
|
|
217
|
+
typeof event.data === "string",
|
|
218
|
+
"Expected event.data from proxy controller to be string"
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const message: InspectorProxyWorkerIncomingWebSocketMessage = JSON.parse(
|
|
222
|
+
event.data
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
this.sendDebugLog("handleProxyControllerIncomingMessage", event.data);
|
|
226
|
+
|
|
227
|
+
switch (message.type) {
|
|
228
|
+
case "reloadComplete": {
|
|
229
|
+
this.proxyData = message.proxyData;
|
|
230
|
+
|
|
231
|
+
this.reconnectRuntimeWebSocket();
|
|
232
|
+
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
default: {
|
|
236
|
+
assertNever(message.type);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
runtimeKeepAliveInterval: number | null = null;
|
|
242
|
+
reconnectRuntimeWebSocket() {
|
|
243
|
+
assert(this.proxyData, "Expected this.proxyData to be defined");
|
|
244
|
+
|
|
245
|
+
this.sendDebugLog("reconnectRuntimeWebSocket");
|
|
246
|
+
|
|
247
|
+
this.websockets.runtimeDeferred = createDeferred<WebSocket>(
|
|
248
|
+
this.websockets.runtimeDeferred
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const runtimeWebSocketUrl = urlFromParts(
|
|
252
|
+
this.proxyData.userWorkerInspectorUrl
|
|
253
|
+
).href;
|
|
254
|
+
this.sendDebugLog("NEW RUNTIME WEBSOCKET", runtimeWebSocketUrl);
|
|
255
|
+
const runtime = new WebSocket(runtimeWebSocketUrl);
|
|
256
|
+
|
|
257
|
+
this.websockets.runtime?.close();
|
|
258
|
+
this.websockets.runtime = runtime;
|
|
259
|
+
|
|
260
|
+
runtime.addEventListener("message", this.handleRuntimeIncomingMessage);
|
|
261
|
+
|
|
262
|
+
runtime.addEventListener("close", (event) => {
|
|
263
|
+
this.sendDebugLog("RUNTIME WEBSOCKET CLOSED", event.code, event.reason);
|
|
264
|
+
|
|
265
|
+
clearInterval(this.runtimeKeepAliveInterval);
|
|
266
|
+
|
|
267
|
+
if (this.websockets.runtime === runtime) {
|
|
268
|
+
this.websockets.runtime = undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// don't reconnect the runtime websocket
|
|
272
|
+
// if it closes unexpectedly (very rare or a case where reconnecting won't succeed anyway)
|
|
273
|
+
// wait for a new proxy-data message or manual restart
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
runtime.addEventListener("error", (event) => {
|
|
277
|
+
clearInterval(this.runtimeKeepAliveInterval);
|
|
278
|
+
|
|
279
|
+
if (this.websockets.runtime === runtime) {
|
|
280
|
+
this.websockets.runtime = undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.sendProxyControllerRequest({
|
|
284
|
+
type: "runtime-websocket-error",
|
|
285
|
+
error: {
|
|
286
|
+
message: event.message,
|
|
287
|
+
cause: event.error,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// don't reconnect the runtime websocket
|
|
292
|
+
// if it closes unexpectedly (very rare or a case where reconnecting won't succeed anyway)
|
|
293
|
+
// wait for a new proxy-data message or manual restart
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
runtime.addEventListener("open", () => {
|
|
297
|
+
this.handleRuntimeWebSocketOpen(runtime);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#runtimeMessageCounter = 1e8;
|
|
302
|
+
nextCounter() {
|
|
303
|
+
return ++this.#runtimeMessageCounter;
|
|
304
|
+
}
|
|
305
|
+
handleRuntimeWebSocketOpen(runtime: WebSocket) {
|
|
306
|
+
this.sendDebugLog("RUNTIME WEBSOCKET OPENED");
|
|
307
|
+
|
|
308
|
+
this.sendRuntimeMessage(
|
|
309
|
+
{ method: "Runtime.enable", id: this.nextCounter() },
|
|
310
|
+
runtime
|
|
311
|
+
);
|
|
312
|
+
this.sendRuntimeMessage(
|
|
313
|
+
{ method: "Debugger.enable", id: this.nextCounter() },
|
|
314
|
+
runtime
|
|
315
|
+
);
|
|
316
|
+
this.sendRuntimeMessage(
|
|
317
|
+
{ method: "Network.enable", id: this.nextCounter() },
|
|
318
|
+
runtime
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
clearInterval(this.runtimeKeepAliveInterval);
|
|
322
|
+
this.runtimeKeepAliveInterval = setInterval(() => {
|
|
323
|
+
this.sendRuntimeMessage(
|
|
324
|
+
{ method: "Runtime.getIsolateId", id: this.nextCounter() },
|
|
325
|
+
runtime
|
|
326
|
+
);
|
|
327
|
+
}, 10_000) as any;
|
|
328
|
+
|
|
329
|
+
this.websockets.runtimeDeferred.resolve(runtime);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async sendRuntimeMessage(
|
|
333
|
+
message: string | DevToolsCommandRequests,
|
|
334
|
+
runtime: MaybePromise<WebSocket> = this.websockets.runtimeDeferred.promise
|
|
335
|
+
) {
|
|
336
|
+
runtime = await runtime;
|
|
337
|
+
message = typeof message === "string" ? message : JSON.stringify(message);
|
|
338
|
+
|
|
339
|
+
this.sendDebugLog("SEND TO RUNTIME", message);
|
|
340
|
+
|
|
341
|
+
runtime.send(message);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ****************
|
|
345
|
+
// ** DEVTOOLS **
|
|
346
|
+
// ****************
|
|
347
|
+
|
|
348
|
+
#inspectorId = crypto.randomUUID();
|
|
349
|
+
async handleDevToolsJsonRequest(req: Request) {
|
|
350
|
+
const url = new URL(req.url);
|
|
351
|
+
|
|
352
|
+
if (url.pathname === "/json/version") {
|
|
353
|
+
return Response.json({
|
|
354
|
+
Browser: `wrangler/v${this.env.WRANGLER_VERSION}`,
|
|
355
|
+
// TODO: (someday): The DevTools protocol should match that of workerd.
|
|
356
|
+
// This could be exposed by the preview API.
|
|
357
|
+
"Protocol-Version": "1.3",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (url.pathname === "/json" || url.pathname === "/json/list") {
|
|
362
|
+
// TODO: can we remove the `/ws` here if we only have a single worker?
|
|
363
|
+
const localHost = `${url.host}/ws`;
|
|
364
|
+
const devtoolsFrontendUrl = `https://devtools.devprod.cloudflare.dev/js_app?theme=systemPreferred&debugger=true&ws=${localHost}`;
|
|
365
|
+
|
|
366
|
+
return Response.json([
|
|
367
|
+
{
|
|
368
|
+
id: this.#inspectorId,
|
|
369
|
+
type: "node", // TODO: can we specify different type?
|
|
370
|
+
description: "workers",
|
|
371
|
+
webSocketDebuggerUrl: `ws://${localHost}`,
|
|
372
|
+
devtoolsFrontendUrl,
|
|
373
|
+
devtoolsFrontendUrlCompat: devtoolsFrontendUrl,
|
|
374
|
+
// Below are fields that are visible in the DevTools UI.
|
|
375
|
+
title: "Cloudflare Worker",
|
|
376
|
+
faviconUrl: "https://workers.cloudflare.com/favicon.ico",
|
|
377
|
+
// url: "http://" + localHost, // looks unnecessary
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return new Response(null, { status: 404 });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async handleDevToolsWebSocketUpgradeRequest(req: Request) {
|
|
386
|
+
// DevTools attempting to connect
|
|
387
|
+
this.sendDebugLog("DEVTOOLS WEBSOCKET TRYING TO CONNECT");
|
|
388
|
+
|
|
389
|
+
// Delay devtools connection response until we've connected to the runtime inspector server
|
|
390
|
+
await this.websockets.runtimeDeferred.promise;
|
|
391
|
+
|
|
392
|
+
this.sendDebugLog("DEVTOOLS WEBSOCKET CAN NOW CONNECT");
|
|
393
|
+
|
|
394
|
+
assert(
|
|
395
|
+
req.headers.get("Upgrade") === "websocket",
|
|
396
|
+
"Expected DevTools connection to be WebSocket upgrade"
|
|
397
|
+
);
|
|
398
|
+
const { 0: response, 1: devtools } = new WebSocketPair();
|
|
399
|
+
devtools.accept();
|
|
400
|
+
|
|
401
|
+
if (this.websockets.devtools !== undefined) {
|
|
402
|
+
/** We only want to have one active Devtools instance at a time. */
|
|
403
|
+
// TODO(consider): prioritise new websocket over previous
|
|
404
|
+
devtools.close(
|
|
405
|
+
1013,
|
|
406
|
+
"Too many clients; only one can be connected at a time"
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
devtools.addEventListener("message", this.handleDevToolsIncomingMessage);
|
|
410
|
+
devtools.addEventListener("close", () => {
|
|
411
|
+
if (this.websockets.devtools === devtools) {
|
|
412
|
+
this.websockets.devtools = undefined;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
devtools.addEventListener("error", (event) => {
|
|
416
|
+
if (this.websockets.devtools === devtools) {
|
|
417
|
+
this.websockets.devtools = undefined;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Since Wrangler proxies the inspector, reloading Chrome DevTools won't trigger debugger initialisation events (because it's connecting to an extant session).
|
|
422
|
+
// This sends a `Debugger.disable` message to the remote when a new WebSocket connection is initialised,
|
|
423
|
+
// with the assumption that the new connection will shortly send a `Debugger.enable` event and trigger re-initialisation.
|
|
424
|
+
// The key initialisation messages that are needed are the `Debugger.scriptParsed events`.
|
|
425
|
+
this.sendRuntimeMessage({
|
|
426
|
+
id: this.nextCounter(),
|
|
427
|
+
method: "Debugger.disable",
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
this.sendDebugLog("DEVTOOLS WEBSOCKET CONNECTED");
|
|
431
|
+
|
|
432
|
+
// Our patched DevTools are hosted on a `https://` URL. These cannot
|
|
433
|
+
// access `file://` URLs, meaning local source maps cannot be fetched.
|
|
434
|
+
// To get around this, we can rewrite `Debugger.scriptParsed` events to
|
|
435
|
+
// include a special `worker:` scheme for source maps, and respond to
|
|
436
|
+
// `Network.loadNetworkResource` commands for these. Unfortunately, this
|
|
437
|
+
// breaks IDE's built-in debuggers (e.g. VSCode and WebStorm), so we only
|
|
438
|
+
// want to enable this transformation when we detect hosted DevTools has
|
|
439
|
+
// connected. We do this by looking at the WebSocket handshake headers:
|
|
440
|
+
//
|
|
441
|
+
// DevTools
|
|
442
|
+
//
|
|
443
|
+
// Upgrade: websocket
|
|
444
|
+
// Host: localhost:9229
|
|
445
|
+
// (from Chrome) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
|
|
446
|
+
// (from Firefox) User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0
|
|
447
|
+
// Origin: https://devtools.devprod.cloudflare.dev
|
|
448
|
+
// ...
|
|
449
|
+
//
|
|
450
|
+
// VSCode
|
|
451
|
+
//
|
|
452
|
+
// Upgrade: websocket
|
|
453
|
+
// Host: localhost
|
|
454
|
+
// ...
|
|
455
|
+
//
|
|
456
|
+
// WebStorm
|
|
457
|
+
//
|
|
458
|
+
// Upgrade: websocket
|
|
459
|
+
// Host: localhost:9229
|
|
460
|
+
// Origin: http://localhost:9229
|
|
461
|
+
// ...
|
|
462
|
+
//
|
|
463
|
+
// From this, we could just use the presence of a `User-Agent` header to
|
|
464
|
+
// determine if DevTools connected, but VSCode/WebStorm could very well
|
|
465
|
+
// add this in future versions. We could also look for an `Origin` header
|
|
466
|
+
// matching the hosted DevTools URL, but this would prevent preview/local
|
|
467
|
+
// versions working. Instead, we look for a browser-like `User-Agent`.
|
|
468
|
+
const userAgent = req.headers.get("User-Agent") ?? "";
|
|
469
|
+
const hasFileSystemAccess = !/mozilla/i.test(userAgent);
|
|
470
|
+
|
|
471
|
+
this.websockets.devtools = devtools;
|
|
472
|
+
this.websockets.devtoolsHasFileSystemAccess = hasFileSystemAccess;
|
|
473
|
+
|
|
474
|
+
this.tryDrainRuntimeMessageBuffer();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return new Response(null, { status: 101, webSocket: response });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
handleDevToolsIncomingMessage = (event: MessageEvent) => {
|
|
481
|
+
assert(
|
|
482
|
+
typeof event.data === "string",
|
|
483
|
+
"Expected devtools incoming message to be of type string"
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const message = JSON.parse(event.data) as DevToolsCommandRequests;
|
|
487
|
+
this.sendDebugLog("DEVTOOLS INCOMING MESSAGE", message);
|
|
488
|
+
|
|
489
|
+
if (message.method === "Network.loadNetworkResource") {
|
|
490
|
+
return void this.handleDevToolsLoadNetworkResource(message);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
this.sendRuntimeMessage(JSON.stringify(message));
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
async handleDevToolsLoadNetworkResource(
|
|
497
|
+
message: DevToolsCommandRequest<"Network.loadNetworkResource">
|
|
498
|
+
) {
|
|
499
|
+
const response = await this.sendProxyControllerRequest({
|
|
500
|
+
type: "load-network-resource",
|
|
501
|
+
url: message.params.url,
|
|
502
|
+
});
|
|
503
|
+
if (response === undefined) {
|
|
504
|
+
this.sendRuntimeMessage(JSON.stringify(message));
|
|
505
|
+
} else {
|
|
506
|
+
// this.websockets.devtools can be undefined here
|
|
507
|
+
// the incoming message implies we have a devtools connection, but after
|
|
508
|
+
// the await it could've dropped in which case we can safely not respond
|
|
509
|
+
this.sendDevToolsMessage({
|
|
510
|
+
id: message.id,
|
|
511
|
+
// @ts-expect-error DevTools Protocol type does not match our patched devtools -- result.resource.text was added
|
|
512
|
+
result: { resource: { success: true, text: response } },
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
sendDevToolsMessage(
|
|
518
|
+
message: string | DevToolsCommandResponses | DevToolsEvents
|
|
519
|
+
) {
|
|
520
|
+
message = typeof message === "string" ? message : JSON.stringify(message);
|
|
521
|
+
|
|
522
|
+
this.sendDebugLog("SEND TO DEVTOOLS", message);
|
|
523
|
+
|
|
524
|
+
this.websockets.devtools?.send(message);
|
|
525
|
+
}
|
|
526
|
+
}
|