Skip to main content

debugger/setup/adapters/
js_debug.rs

1//! js-debug installer
2//!
3//! Installs Microsoft's JavaScript/TypeScript debugger via npm.
4
5use crate::common::{Error, Result};
6use crate::setup::installer::{
7    adapters_dir, ensure_adapters_dir, run_command_args, write_version_file,
8    InstallMethod, InstallOptions, InstallResult, InstallStatus, Installer,
9};
10use crate::setup::registry::{DebuggerInfo, Platform};
11use crate::setup::verifier::{verify_dap_adapter_tcp, VerifyResult};
12use async_trait::async_trait;
13use std::path::PathBuf;
14
15static INFO: DebuggerInfo = DebuggerInfo {
16    id: "js-debug",
17    name: "js-debug",
18    languages: &["javascript", "typescript"],
19    platforms: &[Platform::Linux, Platform::MacOS, Platform::Windows],
20    description: "Microsoft's JavaScript/TypeScript debugger",
21    primary: true,
22};
23
24pub struct JsDebugInstaller;
25
26#[async_trait]
27impl Installer for JsDebugInstaller {
28    fn info(&self) -> &DebuggerInfo {
29        &INFO
30    }
31
32    async fn status(&self) -> Result<InstallStatus> {
33        let adapter_dir = adapters_dir().join("js-debug");
34        let dap_path = get_dap_executable(&adapter_dir);
35
36        if dap_path.exists() {
37            let version = read_package_version(&adapter_dir);
38            return Ok(InstallStatus::Installed {
39                path: dap_path,
40                version,
41            });
42        }
43
44        Ok(InstallStatus::NotInstalled)
45    }
46
47    async fn best_method(&self) -> Result<InstallMethod> {
48        if which::which("npm").is_err() {
49            return Err(Error::Internal(
50                "npm not found. Please install Node.js and npm first.".to_string(),
51            ));
52        }
53
54        Ok(InstallMethod::LanguagePackage {
55            tool: "npm".to_string(),
56            package: "@vscode/js-debug".to_string(),
57        })
58    }
59
60    async fn install(&self, opts: InstallOptions) -> Result<InstallResult> {
61        install_js_debug(&opts).await
62    }
63
64    async fn uninstall(&self) -> Result<()> {
65        let adapter_dir = adapters_dir().join("js-debug");
66        if adapter_dir.exists() {
67            std::fs::remove_dir_all(&adapter_dir)?;
68            println!("Removed {}", adapter_dir.display());
69        } else {
70            println!("js-debug managed installation not found");
71        }
72        Ok(())
73    }
74
75    async fn verify(&self) -> Result<VerifyResult> {
76        let status = self.status().await?;
77
78        match status {
79            InstallStatus::Installed { path, .. } => {
80                // js-debug's dapDebugServer.js must be run via node
81                let node_path = which::which("node").map_err(|_| {
82                    Error::Internal("node not found in PATH".to_string())
83                })?;
84                // TcpPortArg appends port as positional argument, no extra args needed
85                verify_dap_adapter_tcp(&node_path, &[path.to_string_lossy().to_string()], crate::common::config::TcpSpawnStyle::TcpPortArg).await
86            }
87            InstallStatus::Broken { reason, .. } => Ok(VerifyResult {
88                success: false,
89                capabilities: None,
90                error: Some(reason),
91            }),
92            InstallStatus::NotInstalled => Ok(VerifyResult {
93                success: false,
94                capabilities: None,
95                error: Some("Not installed".to_string()),
96            }),
97        }
98    }
99}
100
101fn get_dap_executable(adapter_dir: &PathBuf) -> PathBuf {
102    // @vscode/js-debug installs to node_modules/@vscode/js-debug
103    let js_path = adapter_dir.join("node_modules/@vscode/js-debug/src/dapDebugServer.js");
104    if js_path.exists() {
105        return js_path;
106    }
107    adapter_dir.join("node_modules/@vscode/js-debug/dist/src/dapDebugServer.js")
108}
109
110fn read_package_version(adapter_dir: &PathBuf) -> Option<String> {
111    let package_json = adapter_dir.join("node_modules/@vscode/js-debug/package.json");
112    if !package_json.exists() {
113        return None;
114    }
115
116    let content = std::fs::read_to_string(&package_json).ok()?;
117    let parsed: serde_json::Value = serde_json::from_str(&content).ok()?;
118    parsed.get("version")
119        .and_then(|v| v.as_str())
120        .map(|s| s.to_string())
121}
122
123async fn install_js_debug(opts: &InstallOptions) -> Result<InstallResult> {
124    println!("Checking for existing installation... not found");
125
126    let npm_path = which::which("npm").map_err(|_| {
127        Error::Internal("npm not found in PATH".to_string())
128    })?;
129    let node_path = which::which("node").map_err(|_| {
130        Error::Internal("node not found in PATH".to_string())
131    })?;
132    println!("Using npm: {}", npm_path.display());
133
134    let adapter_dir = ensure_adapters_dir()?.join("js-debug");
135
136    if opts.force && adapter_dir.exists() {
137        std::fs::remove_dir_all(&adapter_dir)?;
138    }
139
140    std::fs::create_dir_all(&adapter_dir)?;
141
142    let package = if let Some(version) = &opts.version {
143        format!("@vscode/js-debug@{}", version)
144    } else {
145        "@vscode/js-debug".to_string()
146    };
147
148    println!("Installing {}...", package);
149    run_command_args(
150        &npm_path,
151        &["install", "--prefix", adapter_dir.to_str().unwrap_or("."), &package]
152    ).await?;
153
154    let dap_path = get_dap_executable(&adapter_dir);
155    if !dap_path.exists() {
156        return Err(Error::Internal(
157            "@vscode/js-debug installation succeeded but dapDebugServer.js not found".to_string(),
158        ));
159    }
160
161    let version = read_package_version(&adapter_dir);
162
163    if let Some(v) = &version {
164        write_version_file(&adapter_dir, v)?;
165    }
166
167    println!("js-debug installation completed.");
168
169    // Return node as the executable with the JS file as an argument
170    // TcpPortArg will append the port as a positional argument
171    Ok(InstallResult {
172        path: node_path,
173        version,
174        args: vec![dap_path.to_string_lossy().to_string()],
175    })
176}