probe_code/path_resolver/
javascript.rs1use super::PathResolver;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub struct JavaScriptPathResolver;
9
10impl Default for JavaScriptPathResolver {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl JavaScriptPathResolver {
17 pub fn new() -> Self {
19 JavaScriptPathResolver
20 }
21
22 fn find_node_modules(&self) -> Result<PathBuf, String> {
24 let mut current_dir =
26 std::env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
27
28 loop {
30 let node_modules = current_dir.join("node_modules");
31 if node_modules.exists() && node_modules.is_dir() {
32 return Ok(node_modules);
33 }
34
35 if !current_dir.pop() {
37 return Err("Could not find node_modules directory".to_string());
39 }
40 }
41 }
42
43 fn resolve_with_npm(&self, package_name: &str) -> Result<PathBuf, String> {
45 let output = Command::new("npm")
47 .args(["root", "-g"])
48 .output()
49 .map_err(|e| format!("Failed to execute 'npm root -g': {e}"))?;
50
51 if !output.status.success() {
52 return Err(format!(
53 "Error running 'npm root -g': {}",
54 String::from_utf8_lossy(&output.stderr)
55 ));
56 }
57
58 let global_node_modules = String::from_utf8_lossy(&output.stdout).trim().to_string();
60 let global_package_path = Path::new(&global_node_modules).join(package_name);
61
62 if global_package_path.exists() {
63 return Ok(global_package_path);
64 }
65
66 if let Ok(node_modules) = self.find_node_modules() {
68 let local_package_path = node_modules.join(package_name);
69 if local_package_path.exists() {
70 return Ok(local_package_path);
71 }
72 }
73
74 let script = format!(
76 "try {{ console.log(require.resolve('{package_name}')) }} catch(e) {{ process.exit(1) }}"
77 );
78
79 let output = Command::new("node")
80 .args(["-e", &script])
81 .output()
82 .map_err(|e| format!("Failed to execute Node.js: {e}"))?;
83
84 if output.status.success() {
85 let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
86
87 let path = PathBuf::from(&path_str);
89 if path.is_file() {
90 if let Some(parent) = path.parent() {
91 return Ok(parent.to_path_buf());
92 }
93 }
94
95 return Ok(path);
96 }
97
98 Err(format!(
99 "Could not resolve JavaScript package: {package_name}"
100 ))
101 }
102}
103
104impl PathResolver for JavaScriptPathResolver {
105 fn prefix(&self) -> &'static str {
106 "js:"
107 }
108
109 fn split_module_and_subpath(
110 &self,
111 full_path_after_prefix: &str,
112 ) -> Result<(String, Option<String>), String> {
113 if full_path_after_prefix.is_empty() {
114 return Err("JavaScript path cannot be empty".to_string());
115 }
116 if full_path_after_prefix.contains("..") {
117 return Err("JavaScript path cannot contain '..'".to_string());
118 }
119
120 let path = full_path_after_prefix.trim_end_matches('/');
122
123 if path.starts_with('@') {
124 let parts: Vec<&str> = path.splitn(3, '/').collect();
126 match parts.len() {
127 1 => Err(format!(
128 "Invalid scoped package format (missing package name): {path}"
129 )), 2 => {
131 let scope = parts[0];
133 let pkg = parts[1];
134 if scope.len() <= 1 || pkg.is_empty() || pkg.contains('/') {
135 Err(format!("Invalid scoped package format: {path}"))
136 } else {
137 let module_name = format!("{scope}/{pkg}");
138 Ok((module_name, None))
139 }
140 }
141 3 => {
142 let scope = parts[0];
144 let pkg = parts[1];
145 let sub = parts[2];
146 if scope.len() <= 1 || pkg.is_empty() || pkg.contains('/') {
147 Err(format!("Invalid scoped package format: {path}"))
148 } else {
149 let module_name = format!("{scope}/{pkg}");
150 let subpath_opt = if sub.is_empty() {
152 None
153 } else {
154 Some(sub.to_string())
155 };
156 Ok((module_name, subpath_opt))
157 }
158 }
159 _ => unreachable!("splitn(3) limits len to 3"),
160 }
161 } else {
162 let mut parts = path.splitn(2, '/');
164 let module_name = parts.next().unwrap().to_string(); if module_name.is_empty() || module_name.starts_with('/') {
166 Err(format!("Invalid package format: {path}"))
168 } else {
169 let subpath_opt = parts.next().filter(|s| !s.is_empty()).map(String::from); Ok((module_name, subpath_opt))
171 }
172 }
173 }
174
175 fn resolve(&self, module_name: &str) -> Result<PathBuf, String> {
176 let path = PathBuf::from(module_name);
178 if path.exists()
179 && path.is_file()
180 && path.file_name().is_some_and(|name| name == "package.json")
181 {
182 return path.parent().map_or(
184 Err("Could not determine parent directory of package.json".to_string()),
185 |parent| Ok(parent.to_path_buf()),
186 );
187 }
188
189 let package_dir = PathBuf::from(module_name);
191 let package_json = package_dir.join("package.json");
192 if package_dir.exists() && package_dir.is_dir() && package_json.exists() {
193 return Ok(package_dir);
194 }
195
196 self.resolve_with_npm(module_name)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::fs;
205
206 #[test]
207 fn test_js_path_resolver_with_directory() {
208 let temp_dir = tempfile::tempdir().unwrap();
210 let package_json_path = temp_dir.path().join("package.json");
211
212 fs::write(
214 &package_json_path,
215 r#"{"name": "test-package", "version": "1.0.0"}"#,
216 )
217 .expect("Failed to write package.json");
218
219 let resolver = JavaScriptPathResolver::new();
220 let result = resolver.resolve(temp_dir.path().to_str().unwrap());
221
222 assert!(
223 result.is_ok(),
224 "Failed to resolve directory with package.json: {result:?}"
225 );
226 assert_eq!(result.unwrap(), temp_dir.path());
227 }
228
229 #[test]
230 fn test_js_path_resolver_with_package_json() {
231 let temp_dir = tempfile::tempdir().unwrap();
233 let package_json_path = temp_dir.path().join("package.json");
234
235 fs::write(
237 &package_json_path,
238 r#"{"name": "test-package", "version": "1.0.0"}"#,
239 )
240 .expect("Failed to write package.json");
241
242 let resolver = JavaScriptPathResolver::new();
243 let result = resolver.resolve(package_json_path.to_str().unwrap());
244
245 assert!(result.is_ok(), "Failed to resolve package.json: {result:?}");
246 assert_eq!(result.unwrap(), temp_dir.path());
247 }
248
249 #[test]
250 fn test_js_path_resolver_npm_package() {
251 if Command::new("npm").arg("--version").output().is_err() {
253 println!("Skipping test_js_path_resolver_npm_package: npm is not installed");
254 return;
255 }
256
257 let resolver = JavaScriptPathResolver::new();
260
261 if resolver.find_node_modules().is_err() {
263 println!("Skipping test_js_path_resolver_npm_package: node_modules not found");
264 return;
265 }
266
267 let result = resolver.resolve("lodash");
269 if result.is_ok() {
270 let path = result.unwrap();
271 assert!(path.exists(), "Path does not exist: {path:?}");
272
273 let package_json = path.join("package.json");
275 assert!(
276 package_json.exists(),
277 "package.json not found: {package_json:?}"
278 );
279 } else {
280 println!("Skipping assertion for 'lodash': Package not found");
281 }
282 }
283}