diff --git a/apps/vscode-wing/.projenrc.ts b/apps/vscode-wing/.projenrc.ts index 03d9b167440..2907435a702 100644 --- a/apps/vscode-wing/.projenrc.ts +++ b/apps/vscode-wing/.projenrc.ts @@ -97,6 +97,7 @@ vscodeIgnore.addPatterns( ); const contributes: VSCodeExtensionContributions = { + breakpoints: [{ language: "wing" }], languages: [ { id: "wing", diff --git a/apps/vscode-wing/package.json b/apps/vscode-wing/package.json index b16e8855c76..db78d4e1169 100644 --- a/apps/vscode-wing/package.json +++ b/apps/vscode-wing/package.json @@ -89,6 +89,11 @@ "onLanguage:wing" ], "contributes": { + "breakpoints": [ + { + "language": "wing" + } + ], "languages": [ { "id": "wing", diff --git a/docs/docs/06-tools/03-debugging.md b/docs/docs/06-tools/03-debugging.md new file mode 100644 index 00000000000..7b4433efcf7 --- /dev/null +++ b/docs/docs/06-tools/03-debugging.md @@ -0,0 +1,31 @@ +--- +title: Debugging +id: Debugging +description: Learn how to debug your Wing application +keywords: [debugging, debug, test, vscode] +--- + +## Overview + +Internally Wing uses JavaScript to execute preflight and inflight code, so standard JavaScript debugging tools can be used to debug your Wing application. The best-supported debugger is the built-in VS Code one so this guide will focus on that. + +### Local/Simulator Debugging + +To start, open your .w file in VS Code and set a breakpoint by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files. There are several ways to start the debugger, but let's use the "JavaScript Debug Terminal". +Open the command palette and type "Debug: Open JavaScript Debug Terminal". This works for any wing commands like `wing test` and `wing it`, although keep in mind that `wing compile` will only debug preflight code. + +### Limitations + +- ([Issue](https://github.com/winglang/wing/issues/5988)) When using the Wing Console (`wing it`) and attempting to debug inflight code in a `test` or Function, the first execution of the test will not hit a breakpoint and will need to be run again +- ([Issue](https://github.com/winglang/wing/issues/5986)) inflight code by default has a timeout that continues during debugging, so if execution is paused for too long the program is terminate +- Caught/Unhandled will often not stop at expected places + +#### Non-VSCode Support + +The Wing CLI itself is a Node.js application, so you can use the `--inspect` flag to debug it and expose a debug server. + +```bash +node --inspect $(which wing) +``` + +Note that inflight code will be executed among multiple child processes, so it's recommended to use a debugger that supports automatically attaching to child processes. diff --git a/libs/wingsdk/src/shared/legacy-sandbox.ts b/libs/wingsdk/src/shared/legacy-sandbox.ts index f7f3dcadaf8..b27c7d0573f 100644 --- a/libs/wingsdk/src/shared/legacy-sandbox.ts +++ b/libs/wingsdk/src/shared/legacy-sandbox.ts @@ -1,6 +1,4 @@ -import { mkdtemp, readFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { readFile } from "node:fs/promises"; import * as util from "node:util"; import * as vm from "node:vm"; import { createBundle } from "./bundling"; @@ -90,8 +88,7 @@ export class LegacySandbox { private async createBundle() { // load bundle into context on first run - const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-")); - const bundle = createBundle(this.entrypoint, [], workdir); + const bundle = createBundle(this.entrypoint); this.entrypoint = bundle.entrypointPath; this.code = await readFile(this.entrypoint, "utf-8"); diff --git a/libs/wingsdk/src/shared/sandbox.ts b/libs/wingsdk/src/shared/sandbox.ts index 4d73c7ce1be..820b48a37df 100644 --- a/libs/wingsdk/src/shared/sandbox.ts +++ b/libs/wingsdk/src/shared/sandbox.ts @@ -1,8 +1,7 @@ import * as cp from "child_process"; import { writeFileSync } from "fs"; -import { mkdtemp, readFile, stat } from "fs/promises"; -import { tmpdir } from "os"; -import path from "path"; +import { readFile, stat } from "fs/promises"; +import { url as inspectorUrl } from "inspector"; import { Bundle, createBundle } from "./bundling"; import { processStream } from "./stream-processor"; @@ -39,9 +38,7 @@ export class Sandbox { entrypoint: string, log?: (message: string) => void ): Promise { - const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-")); - - let contents = (await readFile(entrypoint)).toString(); + let contents = await readFile(entrypoint, "utf-8"); // log a warning if contents includes __dirname or __filename if (contents.includes("__dirname") || contents.includes("__filename")) { @@ -52,9 +49,7 @@ export class Sandbox { // wrap contents with a shim that handles the communication with the parent process // we insert this shim before bundling to ensure source maps are generated correctly - contents = ` -"use strict"; -${contents} + contents += ` process.on("message", async (message) => { const { fn, args } = message; try { @@ -67,7 +62,7 @@ process.on("message", async (message) => { `; const wrappedPath = entrypoint.replace(/\.js$/, ".sandbox.js"); writeFileSync(wrappedPath, contents); // async fsPromises.writeFile "flush" option is not available in Node 20 - const bundle = createBundle(wrappedPath, [], workdir); + const bundle = createBundle(wrappedPath); if (process.env.DEBUG) { const fileStats = await stat(entrypoint); @@ -118,14 +113,19 @@ process.on("message", async (message) => { public async initialize() { this.debugLog("Initializing sandbox."); const childEnv = this.options.env ?? {}; - if ( - process.env.NODE_OPTIONS?.includes("--inspect") || - process.execArgv.some((a) => a.startsWith("--inspect")) - ) { + if (inspectorUrl?.()) { // We're exposing a debugger, let's attempt to ensure the child process automatically attaches childEnv.NODE_OPTIONS = (childEnv.NODE_OPTIONS ?? "") + (process.env.NODE_OPTIONS ?? ""); + // If the child process is not already configured to attach a debugger, add a flag to do so + if ( + !childEnv.NODE_OPTIONS.includes("--inspect") && + !process.execArgv.includes("--inspect") + ) { + childEnv.NODE_OPTIONS += " --inspect=0"; + } + // VSCode's debugger adds some environment variables that we want to pass to the child process for (const key in process.env) { if (key.startsWith("VSCODE_")) { diff --git a/libs/wingsdk/test/simulator/simulator.test.ts b/libs/wingsdk/test/simulator/simulator.test.ts index dd494bf6627..89c8236c84e 100644 --- a/libs/wingsdk/test/simulator/simulator.test.ts +++ b/libs/wingsdk/test/simulator/simulator.test.ts @@ -1,14 +1,13 @@ import * as fs from "fs"; +import * as inspector from "inspector"; import { Construct } from "constructs"; import { test, expect, describe } from "vitest"; import { Api, Bucket, Function, - IApiClient, IBucketClient, IFunctionClient, - IServiceClient, OnDeploy, Service, } from "../../src/cloud"; @@ -593,6 +592,24 @@ describe("in-place updates", () => { "root/OnDeploy started", ]); }); + + test("debugging inspector inherited by sandbox", async () => { + const app = new SimApp(); + const handler = Testing.makeHandler( + `async handle() { if(require('inspector').url() === undefined) { throw new Error('inspector not available'); } }` + ); + new OnDeploy(app, "OnDeploy", handler); + + inspector.open(0); + const sim = await app.startSimulator(); + await sim.stop(); + + expect( + sim + .listTraces() + .some((t) => t.data.message.startsWith("Debugger listening on ")) + ); + }); }); test("tryGetResource returns undefined if the resource not found", async () => { diff --git a/libs/wingsdk/test/util.ts b/libs/wingsdk/test/util.ts index eb211185981..1de711dab9e 100644 --- a/libs/wingsdk/test/util.ts +++ b/libs/wingsdk/test/util.ts @@ -130,6 +130,15 @@ export function directorySnapshot(initialRoot: string) { if (f === "node_modules") { continue; } + // skip sandbox entrypoints since they are mostly a duplicate of the original + if (f.endsWith(".sandbox.js")) { + continue; + } + // skip esbuild output + if (f.endsWith(".js.bundle")) { + continue; + } + const relpath = join(subdir, f); const abspath = join(root, relpath); const key = prefix + relpath; @@ -149,9 +158,6 @@ export function directorySnapshot(initialRoot: string) { break; case ".js": - if (f.endsWith(".sandbox.js")) { - continue; - } const code = readFileSync(abspath, "utf-8"); snapshot[key] = sanitizeCode(code); break;