debugger: Special-case `npm` et al. as `program` field for JS debug definitions (#32549)

Cole Miller created

Send `runtimeExecutable` and `runtimeArgs` instead of `program` and
`args` to avoid the DAP implicitly wrapping the command in `node`.

This means that putting `pnpm vitest <file>` as the command in the
launch modal will work, as will this in debug.json:

```
[
  {
    "adapter": "JavaScript",
    "type": "pwa-node",
    "label": "Label",
    "request": "launch",
    "program": "pnpm",
    "args": ["vitest", "<file>"],
    "cwd": "/Users/name/project"
  }
]
```


Release Notes:

- Debugger Beta: made it possible to use commands like `pnpm
<subcommand> <args>` in the launch modal and debug.json

Change summary

crates/dap_adapters/src/javascript.rs | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)

Detailed changes

crates/dap_adapters/src/javascript.rs 🔗

@@ -71,6 +71,26 @@ impl JsDebugAdapter {
 
         let mut configuration = task_definition.config.clone();
         if let Some(configuration) = configuration.as_object_mut() {
+            if let Some(program) = configuration
+                .get("program")
+                .cloned()
+                .and_then(|value| value.as_str().map(str::to_owned))
+            {
+                match program.as_str() {
+                    "npm" | "pnpm" | "yarn" | "bun"
+                        if !configuration.contains_key("runtimeExecutable")
+                            && !configuration.contains_key("runtimeArgs") =>
+                    {
+                        configuration.remove("program");
+                        configuration.insert("runtimeExecutable".to_owned(), program.into());
+                        if let Some(args) = configuration.remove("args") {
+                            configuration.insert("runtimeArgs".to_owned(), args);
+                        }
+                    }
+                    _ => {}
+                }
+            }
+
             configuration
                 .entry("cwd")
                 .or_insert(delegate.worktree_root_path().to_string_lossy().into());