diff --git a/dotnet-blazor-wasm-debug-control.md b/dotnet-blazor-wasm-debug-control.md new file mode 100644 index 000000000..959b77c29 --- /dev/null +++ b/dotnet-blazor-wasm-debug-control.md @@ -0,0 +1,88 @@ +# Blazor WebAssembly Browser Debug Control + +## Summary + +Expose the project path directly on the existing `blazorwasm` debug configuration so callers can launch browser and WebAssembly debugging for an already-running app without composing low-level debug adapter configurations. Also allow `blazorwasm` launch configurations to start only the app process by disabling browser launch. + +The motivating integration is the Aspire browser launch command described in [Aspire Extension: Browser and WASM debug session support](https://github.com/microsoft/aspire/issues/17797). Aspire can receive a browser launch request for an already-running app and needs VS Code to start browser JavaScript debugging plus Blazor WebAssembly managed debugging. The Aspire extension should be able to invoke the existing high-level `blazorwasm` debug type instead of composing js-debug, `monovsdbg_wasm`, VSDbg bridge ports, and cascade termination behavior itself. + +## Current Behavior + +The existing `BlazorDebugConfigurationProvider` owns the detailed Blazor orchestration: + +- launch the app for `request: "launch"`; +- read `launchSettings.json` for `inspectUri` and `applicationUrl`; +- launch Edge or Chrome through js-debug; +- optionally start the VSDbg WebAssembly bridge for supported .NET 9+ projects; +- track sibling sessions so terminating one Blazor session terminates the rest. + +Today `launchBrowser` calls `useVSDbg` with a value derived from `cwd`. That is incorrect for callers such as Aspire, where the browser launch command can provide the `.csproj` path independently from the web root or working directory. Target framework detection should use a project path, not the launch working directory. + +## Proposed Schema + +Add `projectPath` to `blazorwasm` attach configurations: + +```jsonc +{ + "name": "C#: Launch Blazor Browser Only", + "type": "blazorwasm", + "request": "attach", + "projectPath": "${workspaceFolder}/Client/Client.csproj", + "url": "https://localhost:5001", + "browser": "edge" +} +``` + +The existing request semantics already cover the key modes: + +- `request: "launch"`: start the app and browser/WASM debugging by default. +- `request: "attach"`: start browser/WASM debugging only against an already-running app. + +For app-only scenarios, callers can use `request: "launch"` with browser launch disabled: + +```jsonc +{ + "name": "C#: Launch both app and browser", + "type": "blazorwasm", + "request": "launch", + "projectPath": "${workspaceFolder}/Client/Client.csproj", + "browser": "edge", + "launchBrowser": true +} + +{ + "name": "C#: Launch Blazor App Only", + "type": "blazorwasm", + "request": "launch", + "projectPath": "${workspaceFolder}/Client/Client.csproj", + "url": "https://localhost:5001", + "launchBrowser": false +} + +{ + "name": "C#: Attach debugging browser", + "type": "blazorwasm", + "request": "attach", + "projectPath": "${workspaceFolder}/Client/Client.csproj", + "url": "https://localhost:5001", + "browser": "edge" +} +``` + +## Aspire Mapping + +Aspire browser launch data should map into the existing `blazorwasm` surface: + +- DCP `url` maps to `url`. +- DCP project path maps to `projectPath` when it identifies a `.csproj`. +- DCP `web_root` maps to `webRoot` when it identifies a static web root. +- Browser selection maps to `browser`. +- Browser isolation flags should remain owned by the browser launcher path and should not require Aspire to know C# adapter internals. + +## Implementation Notes + +`BlazorDebugConfigurationProvider.launchBrowser` should resolve `configuration.projectPath` and pass that value to `useVSDbg`. It may fall back to `cwd` for compatibility with existing launch configurations, but `cwd` should no longer be the primary input for target framework detection. + +`useVSDbg` already accepts either a `.csproj` file path or a directory, because `findCsprojFiles` handles both forms. Passing the actual project path fixes .NET 9+ detection for integrations where `cwd` is not the project file or project directory. + +`BlazorDebugConfigurationProvider.resolveDebugConfiguration` should skip `launchBrowser` when `configuration.launchBrowser === false`. This keeps the existing launch path but allows callers to use `blazorwasm` for app-only launch when they intentionally do not want browser or WASM debug sessions. diff --git a/package.json b/package.json index 3752f98b0..f6c4f8494 100644 --- a/package.json +++ b/package.json @@ -4646,6 +4646,10 @@ "description": "The directory of the Blazor WebAssembly app, defaults to the workspace folder.", "default": "${workspaceFolder}" }, + "projectPath": { + "type": "string", + "description": "The path to the Blazor WebAssembly .csproj file. Used to determine target framework and managed WebAssembly debugging support." + }, "url": { "type": "string", "description": "The URL of the application", @@ -4687,6 +4691,11 @@ "default": 30000, "description": "Retry for this number of milliseconds to connect to browser." }, + "launchBrowser": { + "type": "boolean", + "description": "Controls whether the browser and WebAssembly debug sessions are launched. If false, only the Blazor WebAssembly app process is launched.", + "default": true + }, "program": { "type": "string", "default": "${workspaceFolder}/Server/bin/Debug//", @@ -4787,6 +4796,10 @@ }, "attach": { "properties": { + "projectPath": { + "type": "string", + "description": "The path to the Blazor WebAssembly .csproj file. Used to determine target framework and managed WebAssembly debugging support." + }, "url": { "type": "string", "description": "The URL of the application", @@ -4827,6 +4840,18 @@ "type": "number", "default": 30000, "description": "Retry for this number of milliseconds to connect to browser." + }, + "browserConfig": { + "description": "Options passed to the underlying browser debugger.", + "type": "object", + "required": [], + "default": {} + }, + "dotNetConfig": { + "description": "Options passed to the underlying .NET debugger. For more info, see https://github.com/dotnet/vscode-csharp/blob/main/debugger.md.", + "type": "object", + "required": [], + "default": {} } } } diff --git a/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts b/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts index 627a782e7..d9df7d270 100644 --- a/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts +++ b/src/razor/src/blazorDebug/blazorDebugConfigurationProvider.ts @@ -163,7 +163,9 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati this.logger.error('[DEBUGGER] Error while getting information from launchSettings.json: ', error as Error); } - await this.launchBrowser(folder, configuration, inspectUri, url); + if (BlazorDebugConfigurationProvider.shouldLaunchBrowser(configuration)) { + await this.launchBrowser(folder, configuration, inspectUri, url); + } /** * If `resolveDebugConfiguration` returns undefined, then the debugger @@ -222,6 +224,10 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati } } + private static shouldLaunchBrowser(configuration: vscode.DebugConfiguration): boolean { + return configuration.launchBrowser !== false; + } + private async launchBrowser( folder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, @@ -229,12 +235,8 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati url: string ) { const originalInspectUri = inspectUri; - let folderPath = configuration.cwd; - if (folder && folderPath) { - folderPath = folderPath.replace('${workspaceFolder}', fileURLToPath(folder.uri.toString())); - folderPath = folderPath.replaceAll(/[\\/]/g, path.sep); - } - const useVSDbg = await BlazorDebugConfigurationProvider.useVSDbg(folderPath ? folderPath : ''); + const projectPath = BlazorDebugConfigurationProvider.resolveProjectPath(folder, configuration); + const useVSDbg = await BlazorDebugConfigurationProvider.useVSDbg(projectPath); let portBrowserDebug = -1; if (useVSDbg) { [inspectUri, portBrowserDebug] = await this.attachToAppOnBrowser(folder, configuration, url); @@ -317,6 +319,31 @@ export class BlazorDebugConfigurationProvider implements vscode.DebugConfigurati } } + private static resolveProjectPath( + folder: vscode.WorkspaceFolder | undefined, + configuration: vscode.DebugConfiguration + ): string { + const configuredProjectPath = configuration.projectPath as string | undefined; + if (configuredProjectPath) { + return BlazorDebugConfigurationProvider.resolveWorkspaceFolderToken(folder, configuredProjectPath); + } + + const cwd = configuration.cwd as string | undefined; + if (cwd) { + return BlazorDebugConfigurationProvider.resolveWorkspaceFolderToken(folder, cwd); + } + + return ''; + } + + private static resolveWorkspaceFolderToken(folder: vscode.WorkspaceFolder | undefined, value: string): string { + if (folder) { + value = value.replace('${workspaceFolder}', fileURLToPath(folder.uri.toString())); + } + + return value.replaceAll(/[\\/]/g, path.sep); + } + private async attachToAppOnBrowser( folder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration,