Skip to content

Commit 56dc5c4

Browse files
authored
Initial support for VSCode Javascript Debug Terminals (#9535)
* Initial support for VSCode Javascript Debug Terminals * turbo env * type tests * Create perfect-plants-compete.md * reset fixtures * Add comments * fix format * fixups * bump timeout * loosen stack trace matching * Support Vite * fix sentry test
1 parent f545a4d commit 56dc5c4

File tree

10 files changed

+327
-218
lines changed

10 files changed

+327
-218
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"miniflare": patch
3+
"wrangler": patch
4+
---
5+
6+
In 2023 we announced [breakpoint debugging support](https://blog.cloudflare.com/debugging-cloudflare-workers/) for Workers, which meant that you could easily debug your Worker code in Wrangler's built-in devtools (accessible via the `[d]` hotkey) as well as multiple other devtools clients, [including VSCode](https://developers.cloudflare.com/workers/observability/dev-tools/breakpoints/). For most developers, breakpoint debugging via VSCode is the most natural flow, but until now it's required [manually configuring a `launch.json` file](https://developers.cloudflare.com/workers/observability/dev-tools/breakpoints/#setup-vs-code-to-use-breakpoints), running `wrangler dev`, and connecting via VSCode's built-in debugger.
7+
8+
Now, using VSCode's built-in [JavaScript Debug Terminals](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_javascript-debug-terminal), there are just two steps: open a JS debug terminal and run `wrangler dev` (or `vite dev`). VSCode will automatically connect to your running Worker (even if you're running multiple Workers at once!) and start a debugging session.

packages/miniflare/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,12 @@ export class Miniflare {
966966

967967
this.#log = this.#sharedOpts.core.log ?? new NoOpLog();
968968

969-
if (enableInspectorProxy) {
969+
// If we're in a JavaScript Debug terminal, Miniflare will send the inspector ports directly to VSCode for registration
970+
// As such, we don't need our inspector proxy and in fact including it causes issue with multiple clients connected to the
971+
// inspector endpoint.
972+
const inVscodeJsDebugTerminal = !!process.env.VSCODE_INSPECTOR_OPTIONS;
973+
974+
if (enableInspectorProxy && !inVscodeJsDebugTerminal) {
970975
if (this.#sharedOpts.core.inspectorPort === undefined) {
971976
throw new MiniflareCoreError(
972977
"ERR_MISSING_INSPECTOR_PROXY_PORT",
@@ -1989,7 +1994,8 @@ export class Miniflare {
19891994
};
19901995
const maybeSocketPorts = await this.#runtime.updateConfig(
19911996
configBuffer,
1992-
runtimeOpts
1997+
runtimeOpts,
1998+
this.#workerOpts.flatMap((w) => w.core.name ?? [])
19931999
);
19942000
if (this.#disposeController.signal.aborted) return;
19952001
if (maybeSocketPorts === undefined) {

packages/miniflare/src/runtime/index.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import assert from "assert";
2-
import childProcess from "child_process";
2+
import childProcess, { spawn } from "child_process";
3+
import { randomBytes } from "crypto";
34
import { Abortable, once } from "events";
5+
import path from "path";
46
import rl from "readline";
57
import { Readable } from "stream";
68
import { $ as $colors, red } from "kleur/colors";
@@ -119,13 +121,40 @@ function getRuntimeArgs(options: RuntimeOptions) {
119121
return args;
120122
}
121123

124+
/**
125+
* Copied from https://github.com/microsoft/vscode-js-debug/blob/0b5e0dade997b3c702a98e1f58989afcb30612d6/src/targets/node/bootloader/environment.ts#L129
126+
*
127+
* This function returns the segment of process.env.VSCODE_INSPECTOR_OPTIONS that corresponds to the current process (rather than a parent process)
128+
*/
129+
function getInspectorOptions() {
130+
const value = process.env.VSCODE_INSPECTOR_OPTIONS;
131+
if (!value) {
132+
return undefined;
133+
}
134+
135+
const ownOptions = value
136+
.split(":::")
137+
.reverse()
138+
.find((v) => !!v);
139+
if (!ownOptions) {
140+
return;
141+
}
142+
143+
try {
144+
return JSON.parse(ownOptions);
145+
} catch {
146+
return undefined;
147+
}
148+
}
149+
122150
export class Runtime {
123151
#process?: childProcess.ChildProcess;
124152
#processExitPromise?: Promise<void>;
125153

126154
async updateConfig(
127155
configBuffer: Buffer,
128-
options: Abortable & RuntimeOptions
156+
options: Abortable & RuntimeOptions,
157+
workerNames: string[]
129158
): Promise<SocketPorts | undefined> {
130159
// 1. Stop existing process (if any) and wait for exit
131160
await this.dispose();
@@ -156,7 +185,46 @@ export class Runtime {
156185
await once(runtimeProcess.stdin, "finish");
157186

158187
// 4. Wait for sockets to start listening
159-
return waitForPorts(controlPipe, options);
188+
const ports = await waitForPorts(controlPipe, options);
189+
if (ports?.has(kInspectorSocket) && process.env.VSCODE_INSPECTOR_OPTIONS) {
190+
// We have an inspector socket and we're in a VSCode Debug Terminal.
191+
// Let's startup a watchdog service to register ourselves as a debuggable target
192+
193+
// First, we need to _find_ the watchdog script. It's located next to bootloader.js, which should be injected as a require hook
194+
const bootloaderPath =
195+
process.env.NODE_OPTIONS?.match(/--require "(.*?)"/)?.[1];
196+
197+
if (!bootloaderPath) {
198+
return ports;
199+
}
200+
const watchdogPath = path.resolve(bootloaderPath, "../watchdog.js");
201+
202+
const info = getInspectorOptions();
203+
204+
for (const name of workerNames) {
205+
// This is copied from https://github.com/microsoft/vscode-js-debug/blob/0b5e0dade997b3c702a98e1f58989afcb30612d6/src/targets/node/bootloader.ts#L284
206+
// It spawns a detached "watchdog" process for each corresponding (user) Worker in workerd which will maintain the VSCode debug connection
207+
const p = spawn(process.execPath, [watchdogPath], {
208+
env: {
209+
NODE_INSPECTOR_INFO: JSON.stringify({
210+
ipcAddress: info.inspectorIpc || "",
211+
pid: String(this.#process.pid),
212+
scriptName: name,
213+
inspectorURL: `ws://127.0.0.1:${ports?.get(kInspectorSocket)}/core:user:${name}`,
214+
waitForDebugger: true,
215+
ownId: randomBytes(12).toString("hex"),
216+
openerId: info.openerId,
217+
}),
218+
NODE_SKIP_PLATFORM_CHECK: process.env.NODE_SKIP_PLATFORM_CHECK,
219+
ELECTRON_RUN_AS_NODE: "1",
220+
},
221+
stdio: "ignore",
222+
detached: true,
223+
});
224+
p.unref();
225+
}
226+
}
227+
return ports;
160228
}
161229

162230
dispose(): Awaitable<void> {

packages/vite-plugin-cloudflare/e2e/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function seed(fixture: string, pm: "pnpm" | "yarn" | "npm") {
6464
maxRetries: 10,
6565
});
6666
}
67-
});
67+
}, 40_000);
6868

6969
return projectPath;
7070
}

packages/vite-plugin-cloudflare/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,13 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
740740
enforce: "pre",
741741
configureServer(viteDevServer) {
742742
assertIsNotPreview(resolvedPluginConfig);
743+
// If we're in a JavaScript Debug terminal, Miniflare will send the inspector ports directly to VSCode for registration
744+
// As such, we don't need our inspector proxy and in fact including it causes issue with multiple clients connected to the
745+
// inspector endpoint.
746+
const inVscodeJsDebugTerminal = !!process.env.VSCODE_INSPECTOR_OPTIONS;
747+
if (inVscodeJsDebugTerminal) {
748+
return;
749+
}
743750

744751
if (
745752
resolvedPluginConfig.type === "workers" &&
@@ -772,6 +779,13 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
772779
},
773780
async configurePreviewServer(vitePreviewServer) {
774781
assertIsPreview(resolvedPluginConfig);
782+
// If we're in a JavaScript Debug terminal, Miniflare will send the inspector ports directly to VSCode for registration
783+
// As such, we don't need our inspector proxy and in fact including it causes issue with multiple clients connected to the
784+
// inspector endpoint.
785+
const inVscodeJsDebugTerminal = !!process.env.VSCODE_INSPECTOR_OPTIONS;
786+
if (inVscodeJsDebugTerminal) {
787+
return;
788+
}
775789

776790
if (
777791
resolvedPluginConfig.workers.length >= 1 &&

packages/wrangler/e2e/startWorker.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ describe.each(OPTIONS)("DevEnv (remote: $remote)", ({ remote }) => {
179179
});
180180

181181
const inspectorUrl = await worker.inspectorUrl;
182+
assert(inspectorUrl);
182183

183184
assert(inspectorUrl, "missing inspectorUrl");
184185
let ws = new WebSocket(inspectorUrl.href, {

0 commit comments

Comments
 (0)