use super::PathResolver;
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct JavaScriptPathResolver;
impl Default for JavaScriptPathResolver {
fn default() -> Self {
Self::new()
}
}
impl JavaScriptPathResolver {
pub fn new() -> Self {
JavaScriptPathResolver
}
fn find_node_modules(&self) -> Result<PathBuf, String> {
let mut current_dir =
std::env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
loop {
let node_modules = current_dir.join("node_modules");
if node_modules.exists() && node_modules.is_dir() {
return Ok(node_modules);
}
if !current_dir.pop() {
return Err("Could not find node_modules directory".to_string());
}
}
}
fn resolve_with_npm(&self, package_name: &str) -> Result<PathBuf, String> {
let output = Command::new("npm")
.args(["root", "-g"])
.output()
.map_err(|e| format!("Failed to execute 'npm root -g': {e}"))?;
if !output.status.success() {
return Err(format!(
"Error running 'npm root -g': {}",
String::from_utf8_lossy(&output.stderr)
));
}
let global_node_modules = String::from_utf8_lossy(&output.stdout).trim().to_string();
let global_package_path = Path::new(&global_node_modules).join(package_name);
if global_package_path.exists() {
return Ok(global_package_path);
}
if let Ok(node_modules) = self.find_node_modules() {
let local_package_path = node_modules.join(package_name);
if local_package_path.exists() {
return Ok(local_package_path);
}
}
let script = format!(
"try {{ console.log(require.resolve('{package_name}')) }} catch(e) {{ process.exit(1) }}"
);
let output = Command::new("node")
.args(["-e", &script])
.output()
.map_err(|e| format!("Failed to execute Node.js: {e}"))?;
if output.status.success() {
let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
let path = PathBuf::from(&path_str);
if path.is_file() {
if let Some(parent) = path.parent() {
return Ok(parent.to_path_buf());
}
}
return Ok(path);
}
Err(format!(
"Could not resolve JavaScript package: {package_name}"
))
}
}
impl PathResolver for JavaScriptPathResolver {
fn prefix(&self) -> &'static str {
"js:"
}
fn split_module_and_subpath(
&self,
full_path_after_prefix: &str,
) -> Result<(String, Option<String>), String> {
if full_path_after_prefix.is_empty() {
return Err("JavaScript path cannot be empty".to_string());
}
if full_path_after_prefix.contains("..") {
return Err("JavaScript path cannot contain '..'".to_string());
}
let path = full_path_after_prefix.trim_end_matches('/');
if path.starts_with('@') {
let parts: Vec<&str> = path.splitn(3, '/').collect();
match parts.len() {
1 => Err(format!(
"Invalid scoped package format (missing package name): {path}"
)), 2 => {
let scope = parts[0];
let pkg = parts[1];
if scope.len() <= 1 || pkg.is_empty() || pkg.contains('/') {
Err(format!("Invalid scoped package format: {path}"))
} else {
let module_name = format!("{scope}/{pkg}");
Ok((module_name, None))
}
}
3 => {
let scope = parts[0];
let pkg = parts[1];
let sub = parts[2];
if scope.len() <= 1 || pkg.is_empty() || pkg.contains('/') {
Err(format!("Invalid scoped package format: {path}"))
} else {
let module_name = format!("{scope}/{pkg}");
let subpath_opt = if sub.is_empty() {
None
} else {
Some(sub.to_string())
};
Ok((module_name, subpath_opt))
}
}
_ => unreachable!("splitn(3) limits len to 3"),
}
} else {
let mut parts = path.splitn(2, '/');
let module_name = parts.next().unwrap().to_string(); if module_name.is_empty() || module_name.starts_with('/') {
Err(format!("Invalid package format: {path}"))
} else {
let subpath_opt = parts.next().filter(|s| !s.is_empty()).map(String::from); Ok((module_name, subpath_opt))
}
}
}
fn resolve(&self, module_name: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(module_name);
if path.exists()
&& path.is_file()
&& path.file_name().is_some_and(|name| name == "package.json")
{
return path.parent().map_or(
Err("Could not determine parent directory of package.json".to_string()),
|parent| Ok(parent.to_path_buf()),
);
}
let package_dir = PathBuf::from(module_name);
let package_json = package_dir.join("package.json");
if package_dir.exists() && package_dir.is_dir() && package_json.exists() {
return Ok(package_dir);
}
self.resolve_with_npm(module_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_js_path_resolver_with_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let package_json_path = temp_dir.path().join("package.json");
fs::write(
&package_json_path,
r#"{"name": "test-package", "version": "1.0.0"}"#,
)
.expect("Failed to write package.json");
let resolver = JavaScriptPathResolver::new();
let result = resolver.resolve(temp_dir.path().to_str().unwrap());
assert!(
result.is_ok(),
"Failed to resolve directory with package.json: {result:?}"
);
assert_eq!(result.unwrap(), temp_dir.path());
}
#[test]
fn test_js_path_resolver_with_package_json() {
let temp_dir = tempfile::tempdir().unwrap();
let package_json_path = temp_dir.path().join("package.json");
fs::write(
&package_json_path,
r#"{"name": "test-package", "version": "1.0.0"}"#,
)
.expect("Failed to write package.json");
let resolver = JavaScriptPathResolver::new();
let result = resolver.resolve(package_json_path.to_str().unwrap());
assert!(result.is_ok(), "Failed to resolve package.json: {result:?}");
assert_eq!(result.unwrap(), temp_dir.path());
}
#[test]
fn test_js_path_resolver_npm_package() {
if Command::new("npm").arg("--version").output().is_err() {
println!("Skipping test_js_path_resolver_npm_package: npm is not installed");
return;
}
let resolver = JavaScriptPathResolver::new();
if resolver.find_node_modules().is_err() {
println!("Skipping test_js_path_resolver_npm_package: node_modules not found");
return;
}
let result = resolver.resolve("lodash");
if result.is_ok() {
let path = result.unwrap();
assert!(path.exists(), "Path does not exist: {path:?}");
let package_json = path.join("package.json");
assert!(
package_json.exists(),
"package.json not found: {package_json:?}"
);
} else {
println!("Skipping assertion for 'lodash': Package not found");
}
}
}