Skip to content

Enable JavaScript/TypeScript AppHost debugging with IGuestProcessLauncher strategy#15091

Merged
adamint merged 23 commits intomicrosoft:release/13.2from
adamint:dev/adamint/js-apphost-run-debug
Mar 10, 2026
Merged

Enable JavaScript/TypeScript AppHost debugging with IGuestProcessLauncher strategy#15091
adamint merged 23 commits intomicrosoft:release/13.2from
adamint:dev/adamint/js-apphost-run-debug

Conversation

@adamint
Copy link
Member

@adamint adamint commented Mar 10, 2026

Summary

This PR enables debugging JavaScript and TypeScript AppHost projects from the VS Code extension. Previously, only .NET (C#) and Python AppHosts could be run/debugged.

Problem

When a user clicks Debug or Run on a TypeScript AppHost (e.g., apphost.ts), the Aspire CLI resolves the runtime spec (from TypeScriptLanguageSupport) and determines the correct command to launch the guest process — for TypeScript this is npx tsx {appHostFile}. However, the VS Code extension needs to launch the process through VS Code's debug adapter (type 'node'), which uses plain node by default. There was no mechanism to pass the custom runtime executable (npx) from the CLI through to the extension's debug configuration.

Solution: IGuestProcessLauncher Strategy Pattern

This PR introduces a strategy pattern (IGuestProcessLauncher) that cleanly separates how guest AppHost processes are launched:

┌─────────────────────────────────────────────────────┐
│                   GuestRuntime                       │
│  (Interprets RuntimeSpec: command, args, env vars)   │
│                                                       │
│  RunAsync(launcher) / PublishAsync(launcher)          │
│        │                                              │
│        ▼                                              │
│  ┌─────────────────────┐                              │
│  │ IGuestProcessLauncher│ ◄── Strategy interface      │
│  └─────┬───────────┬───┘                              │
│        │           │                                  │
│        ▼           ▼                                  │
│  ┌──────────┐  ┌──────────────────┐                   │
│  │ Process  │  │ Extension        │                   │
│  │ Guest    │  │ Guest            │                   │
│  │ Launcher │  │ Launcher         │                   │
│  └──────────┘  └──────────────────┘                   │
│   OS process     Delegates to VS Code                 │
│   via PATH       extension via RPC                    │
└─────────────────────────────────────────────────────┘

Architecture Details

IGuestProcessLauncher — Strategy interface with a single method:

Task<(int ExitCode, OutputCollector? Output)> LaunchAsync(
    string command, string[] args, DirectoryInfo workingDirectory,
    IDictionary<string, string> environmentVariables, CancellationToken ct);

ProcessGuestLauncher — Default implementation that spawns an OS process. Used for:

  • CLI-only mode (no VS Code extension connected)
  • InstallDependenciesAsync (e.g., npm install)
  • PublishAsync (always runs as a direct process)

ExtensionGuestLauncher — Delegates to the VS Code extension via JSON-RPC. The key trick: it prepends the runtime command as args[0] before sending to the extension. This avoids changing the RPC protocol while still passing the custom runtime.

Data-Driven Extension Launcher Selection

The decision of whether to use the extension launcher is driven by the RuntimeSpec data model, not hardcoded per language. Each language's RuntimeSpec (provided by ILanguageSupport.GetRuntimeSpec()) can set ExtensionLaunchCapability to declare the VS Code extension capability required for extension-based launching:

  • TypeScript sets ExtensionLaunchCapability = "node" → CLI checks if the extension has the "node" capability
  • Other languages leave ExtensionLaunchCapability as null → always use the default process launcher

This means new polyglot languages can opt into extension launching simply by setting ExtensionLaunchCapability in their RuntimeSpec — no CLI code changes needed.

Extension-Side Flow (TypeScript)

When the extension receives a launchAppHost RPC call for a JS/TS AppHost:

  1. AspireDebugSession.startAppHost detects the file extension (.ts, .js, etc.)
  2. Extracts args[0] as the runtimeExecutable (e.g., npx)
  3. Uses args.slice(1) as the actual arguments (e.g., ["tsx", "apphost.ts"])
  4. Creates a NodeLaunchConfiguration with runtime_executable set
  5. The node.ts debugger extension configures VS Code's debug adapter:
    • Sets runtimeExecutable to npx (instead of default node)
    • Uses remaining args as runtimeArgs (for non-node executables)
    • This causes VS Code's js-debug to launch npx tsx apphost.ts with full debugging support

Additional Extension Changes

  • New node.ts and browser.ts debugger extensions — Handle Node.js and browser launch configurations respectively
  • NodeLaunchConfiguration and BrowserLaunchConfiguration types added to DCP types
  • AppHost restart handling — When the user clicks "restart" on the AppHost debug toolbar, the entire Aspire debug session restarts (not just the child), implemented via DAP adapter tracker
  • File extension detectionpackage.json activation events and context menu items now support .ts and .js files alongside .cs
  • AppHost file detectionAspireEditorCommandProvider detects JS/TS apphosts via import ... from '...modules/aspire' patterns
  • Removed DebuggerProperties — Simplified launch configuration by removing the generic debugger properties pass-through in favor of typed configurations
  • Removed isDeprecated from ResourceDebuggerExtension — Simplified the interface
  • Capabilities cleanupnode and browser capabilities are now properly gated behind isNodeInstalled() instead of always being present

New Files

File Purpose
src/Aspire.Cli/Projects/IGuestProcessLauncher.cs Strategy interface for launching guest processes
src/Aspire.Cli/Projects/ProcessGuestLauncher.cs OS process implementation (resolves via PATH)
src/Aspire.Cli/Projects/ExtensionGuestLauncher.cs VS Code extension RPC delegation
extension/src/debugger/languages/node.ts Node.js debugger extension configuration
extension/src/debugger/languages/browser.ts Browser debugger extension configuration
tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs 17 tests for GuestRuntime
tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs 6 tests for ExtensionGuestLauncher

Refactoring

  • GuestRuntime.InstallDependenciesAsync — Was duplicating ~30 lines of inline Process code. Now uses CreateDefaultLauncher() (a ProcessGuestLauncher) to eliminate duplication.
  • GuestRuntime.FindCommand removed — No longer needed after refactoring.

Checklist

  • Is this feature complete?
    • No. Follow-up changes expected.
  • Tests
    • Added 23 new tests (17 GuestRuntimeTests + 6 ExtensionGuestLauncherTests)
  • Does this introduce a new public API?
    • Yes: RuntimeSpec.ExtensionLaunchCapability property on the experimental RuntimeSpec class

adamint added 10 commits March 9, 2026 15:35
WithDebugging has no AspireExport attribute on this branch, so the
generated TypeScript SDK doesn't include it, causing tsc to fail.
Add Node.js debugging support with minimal changes:
- NodeLaunchConfiguration extending ExecutableLaunchConfiguration
- Internal WithVSCodeDebugging methods wired into AddNodeApp/CreateDefaultJavaScriptAppBuilder
- Extension: node debugger using built-in js-debug adapter
- No new public APIs or extensibility system
- Tests updated for additional CommandLineArgsCallbackAnnotation from WithDebugSupport
- BrowserLaunchConfiguration: launch config extending ExecutableLaunchConfiguration('browser')
  with url, webRoot, and browser properties
- BrowserDebuggerResource: internal child resource for browser debugging
- WithBrowserDebugger: creates child resource with WithDebugSupport for browser capability
- ValidateBrowserCapability: validates extension supports browser debugging
- browser.ts: extension debugger using VS Code built-in pwa-msedge/chrome adapter
- Added 'browser' capability to extension capabilities
- Fixed duplicate 'run' in node.ts: args from DCP already contain the full command,
  so runtimeArgs should use args directly instead of prepending 'run'
- Fixed getProjectFile to handle empty script_path for package manager mode
…/js-apphost-run-debug

# Conflicts:
#	extension/src/capabilities.ts
#	src/Aspire.Hosting.JavaScript/BrowserLaunchConfiguration.cs
#	src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs
#	src/Aspire.Hosting.JavaScript/NodeLaunchConfiguration.cs
#	src/Aspire.Hosting.Python/Aspire.Hosting.Python.csproj
#	src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
#	src/Aspire.Hosting/Aspire.Hosting.csproj
#	src/Aspire.Hosting/Dcp/DcpExecutor.cs
#	src/Aspire.Hosting/Dcp/Model/ExecutableLaunchConfiguration.cs
#	src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
#	src/Aspire.Hosting/ResourceBuilderExtensions.cs
#	src/Aspire.Hosting/SupportsDebuggingAnnotation.cs
#	tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs
#	tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs
#	tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs
…ching

Introduce a strategy pattern for launching guest language AppHost processes,
enabling the VS Code extension to launch and debug TypeScript/Node.js apphosts
via its debug adapter instead of spawning a plain OS process.

Problem: When launching a TypeScript AppHost from VS Code, the extension used
plain 'node' which cannot resolve .js -> .ts imports. The runtime needs 'npx tsx'
as the executable, which is specified in the RuntimeSpec but was not being
forwarded to the extension's debug session.

Solution: IGuestProcessLauncher strategy interface with two implementations:
- ProcessGuestLauncher: Spawns a local OS process (default, CLI-only path).
  Resolves the command via PATH, starts a Process, captures stdout/stderr.
- ExtensionGuestLauncher: Delegates to the VS Code extension debug session.
  Prepends the runtime command (e.g. 'npx') as args[0] in the RPC call so
  the extension can extract it as runtimeExecutable for the Node debug adapter.

The launcher is non-null throughout the chain: GuestAppHostProject creates the
appropriate launcher and passes it through ExecuteGuestAppHostAsync to
GuestRuntime.RunAsync/PublishAsync to ExecuteCommandAsync to launcher.LaunchAsync.

Also refactored InstallDependenciesAsync to use ProcessGuestLauncher instead of
inline Process code, eliminating duplication.

On the extension side, AspireDebugSession.startAppHost extracts args[0] as the
runtimeExecutable and sets it on the NodeLaunchConfiguration, which flows through
to debugConfiguration.runtimeExecutable in the node debugger extension.

Tests: Added GuestRuntimeTests (15 tests) and ExtensionGuestLauncherTests (6 tests)
covering command selection, placeholder replacement, env var merging, args
prepending, and delegation behavior.
@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15091

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15091"

@adamint adamint changed the title Extract IGuestProcessLauncher strategy for guest AppHost debugging in VS Code Enable JavaScript/TypeScript AppHost debugging with IGuestProcessLauncher strategy Mar 10, 2026
@adamint adamint marked this pull request as ready for review March 10, 2026 05:02
@adamint adamint requested a review from mitchdenny as a code owner March 10, 2026 05:02
Copilot AI review requested due to automatic review settings March 10, 2026 05:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables running/debugging JavaScript/TypeScript AppHost projects from the VS Code extension by introducing a guest-process launcher strategy in the CLI (process vs extension-delegated launch) and adding Node + browser launch-configuration support across Hosting/DCP and the extension.

Changes:

  • Add IGuestProcessLauncher (+ process/extension implementations) and refactor GuestRuntime/GuestAppHostProject to support extension-driven launching for JS/TS AppHosts.
  • Extend DCP/Hosting to produce typed Node and browser launch configurations (and delay launch-config production until endpoints are allocated).
  • Add VS Code debugger extensions for Node and browser resources plus new tests covering the new launcher/runtime paths.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs Adds regression test ensuring IDE launch-config failures fall back to process execution.
tests/Aspire.Hosting.JavaScript.Tests/AddViteAppWithPnpmTests.cs Adjusts how args callback annotation is selected in tests.
tests/Aspire.Hosting.JavaScript.Tests/AddViteAppTests.cs Adjusts how args callback annotation is selected in tests.
tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs Adds tests for VS Code debugging annotations and new browser debugger child resource.
tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs Adds unit tests for GuestRuntime placeholder/env/command selection behavior and launcher usage.
tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs Adds unit tests validating extension-delegated launching argument/env shaping.
src/Aspire.Hosting/Dcp/Model/ExecutableLaunchConfiguration.cs Seals ProjectLaunchConfiguration.
src/Aspire.Hosting/Dcp/DcpExecutor.cs Defers debug launch-config annotation until endpoints are allocated; adds safe fallback on producer errors.
src/Aspire.Hosting/Aspire.Hosting.csproj Adds InternalsVisibleTo entries for JavaScript/Python assemblies and tests.
src/Aspire.Hosting.Python/Aspire.Hosting.Python.csproj Removes linked/shared compile includes now sourced via project reference.
src/Aspire.Hosting.JavaScript/NodeLaunchConfiguration.cs Introduces DCP launch configuration model for Node debugging.
src/Aspire.Hosting.JavaScript/BrowserLaunchConfiguration.cs Introduces DCP launch configuration model for browser debugging.
src/Aspire.Hosting.JavaScript/BrowserDebuggerResource.cs Adds an executable child resource to represent a browser debug session.
src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs Adds VS Code debugging support hooks + WithBrowserDebugger() and emits Node/browser launch configs.
src/Aspire.Hosting.JavaScript/Aspire.Hosting.JavaScript.csproj Removes linked/shared compile includes now sourced via project reference.
src/Aspire.Cli/Utils/ExtensionHelper.cs Adds KnownCapabilities.Node.
src/Aspire.Cli/Projects/IGuestProcessLauncher.cs Adds launcher strategy interface.
src/Aspire.Cli/Projects/ProcessGuestLauncher.cs Adds default OS-process launcher implementation (PATH-resolved).
src/Aspire.Cli/Projects/ExtensionGuestLauncher.cs Adds extension-delegating launcher implementation (prepends runtime executable into args).
src/Aspire.Cli/Projects/GuestRuntime.cs Refactors to use launcher strategy, merges env vars, removes inline process code.
src/Aspire.Cli/Projects/GuestAppHostProject.cs Chooses extension vs process launcher and handles null output for extension-launched runs.
src/Aspire.Cli/Commands/RunCommand.cs Treats ConnectionLostException as success to support extension-driven termination path.
src/Aspire.Cli/Backchannel/ExtensionBackchannel.cs Updates log message wording for LaunchAppHostAsync.
playground/TypeScriptAppHost/.modules/.codegen-hash Updates generated hash for TypeScript playground artifacts.
playground/TypeScriptAppHost/.gitignore Ignores .aspire/dcp/ in TypeScript playground.
playground/TypeScriptAppHost/.aspire/settings.json Updates TypeScript playground settings (channel/sdkVersion).
playground/AspireWithJavaScript/AspireJavaScript.React/webpack.config.js Enables source maps for the React webpack playground.
playground/AspireWithJavaScript/AspireJavaScript.NodeApp/app.js Adjusts 404 handler wiring in Node playground app.
playground/AspireWithJavaScript/AspireJavaScript.MinimalApi/Properties/launchSettings.json Removes launchUrl entries from playground launch settings.
playground/AspireWithJavaScript/AspireJavaScript.MinimalApi/AppHost.cs Removes Swagger/OpenAPI setup from playground API.
playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs Demonstrates WithBrowserDebugger() usage (with diagnostic suppression).
playground/AspireWithJavaScript/.vscode/settings.json Sets dashboard browser preference for playground workspace.
extension/src/server/interactionService.ts Minor formatting tweak in error handling path.
extension/src/loc/strings.ts Adds localized browser display strings.
extension/src/editor/AspireEditorCommandProvider.ts Expands AppHost detection to JS/TS and updates context keys.
extension/src/debugger/languages/node.ts Adds Node debugger extension producing js-debug launch configuration.
extension/src/debugger/languages/browser.ts Adds browser debugger extension producing js-debug launch configuration.
extension/src/debugger/debuggerExtensions.ts Registers node/browser debugger extensions and passes isApphost flag.
extension/src/debugger/adapterTracker.ts Adds restart detection/suppression for AppHost sessions via DAP tracker.
extension/src/debugger/AspireDebugSession.ts Enables node apphost launching (runtimeExecutable extraction) and Aspire-session restart behavior.
extension/src/dcp/types.ts Adds Node/Browser launch-config typings and isApphost flag.
extension/src/capabilities.ts Adds node/browser capabilities and isNodeInstalled() hook.
extension/package.json Adds activation/events & context menu support for apphost.ts/apphost.js and new context key.
extension/loc/xlf/aspire-vscode.xlf Adds XLF entries for new strings and fixes a grammar issue.

You can also share your feedback on Copilot code review. Take the survey.

adamint added 8 commits March 10, 2026 01:26
…dpoint validation

- Add InternalsVisibleTo for Aspire.Hosting.JavaScript.Tests to fix BrowserDebuggerResource accessibility
- Use appHostFile.FullName in GuestRuntimeTests for cross-platform path compatibility
- Update WithBrowserDebugger test to reflect deferred endpoint validation
…t() usage

- Map browser names to pwa-prefixed debug adapter types (msedge -> pwa-msedge)
- Delete debugConfiguration.cwd in browser debugger (matches comment)
- Update comments explaining why First() is used instead of Single()
@adamint
Copy link
Member Author

adamint commented Mar 10, 2026

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@adamint adamint merged commit 88958dc into microsoft:release/13.2 Mar 10, 2026
985 of 992 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Mar 10, 2026
@radical radical mentioned this pull request Mar 11, 2026
16 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants