#![allow(dead_code)]
use std::path::{Path, PathBuf};
pub fn resolve_node_module(specifier: &str, from_dir: &Path) -> Option<PathBuf> {
let (package_name, subpath) = parse_package_specifier(specifier);
let mut current_dir = from_dir.to_path_buf();
loop {
let node_modules = current_dir.join("node_modules");
if node_modules.is_dir() {
let package_dir = node_modules.join(&package_name);
if package_dir.is_dir() {
if let Some(resolved) = resolve_package_entry(&package_dir, subpath.as_deref()) {
return Some(resolved);
}
}
}
if let Some(parent) = current_dir.parent() {
current_dir = parent.to_path_buf();
} else {
break;
}
}
None
}
fn parse_package_specifier(specifier: &str) -> (String, Option<String>) {
if specifier.starts_with('@') {
let parts: Vec<&str> = specifier.splitn(3, '/').collect();
if parts.len() >= 2 {
let package_name = format!("{}/{}", parts[0], parts[1]);
let subpath = if parts.len() == 3 {
Some(parts[2].to_string())
} else {
None
};
return (package_name, subpath);
}
}
let parts: Vec<&str> = specifier.splitn(2, '/').collect();
let package_name = parts[0].to_string();
let subpath = parts.get(1).map(|s| s.to_string());
(package_name, subpath)
}
fn resolve_package_entry(package_dir: &Path, subpath: Option<&str>) -> Option<PathBuf> {
if let Some(subpath) = subpath {
let target = package_dir.join(subpath);
return try_resolve_file(&target);
}
let package_json = package_dir.join("package.json");
if package_json.is_file() {
if let Ok(content) = std::fs::read_to_string(&package_json) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(exports) = json.get("exports") {
if let Some(resolved) = resolve_exports(exports, package_dir, ".") {
return Some(resolved);
}
}
if let Some(types) = json.get("types").or_else(|| json.get("typings")) {
if let Some(types_str) = types.as_str() {
let types_path = package_dir.join(types_str);
if types_path.is_file() {
return Some(types_path);
}
}
}
if let Some(main) = json.get("main") {
if let Some(main_str) = main.as_str() {
let main_path = package_dir.join(main_str);
if let Some(resolved) = try_resolve_file(&main_path) {
return Some(resolved);
}
}
}
}
}
}
try_resolve_file(&package_dir.join("index"))
}
fn resolve_exports(
exports: &serde_json::Value,
package_dir: &Path,
subpath: &str,
) -> Option<PathBuf> {
match exports {
serde_json::Value::String(s) => {
if subpath == "." {
let path = package_dir.join(s.trim_start_matches("./"));
return try_resolve_file(&path);
}
}
serde_json::Value::Object(map) => {
if let Some(entry) = map.get(subpath) {
return resolve_export_entry(entry, package_dir);
}
if subpath == "." {
if let Some(entry) = map.get(".") {
return resolve_export_entry(entry, package_dir);
}
}
}
_ => {}
}
None
}
fn resolve_export_entry(entry: &serde_json::Value, package_dir: &Path) -> Option<PathBuf> {
match entry {
serde_json::Value::String(s) => {
let path = package_dir.join(s.trim_start_matches("./"));
try_resolve_file(&path)
}
serde_json::Value::Object(map) => {
let conditions = ["types", "import", "require", "default"];
for condition in conditions {
if let Some(value) = map.get(condition) {
if let Some(resolved) = resolve_export_entry(value, package_dir) {
return Some(resolved);
}
}
}
None
}
_ => None,
}
}
fn try_resolve_file(path: &Path) -> Option<PathBuf> {
if path.is_file() {
return Some(path.to_path_buf());
}
let extensions = [".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mts", ".mjs"];
for ext in extensions {
let with_ext = path.with_extension(ext.trim_start_matches('.'));
if with_ext.is_file() {
return Some(with_ext);
}
}
if path.is_dir() {
for ext in extensions {
let index = path.join(format!("index{}", ext));
if index.is_file() {
return Some(index);
}
}
}
None
}