typescript-language-server 0.1.0

A high-performance TypeScript and JavaScript language server implemented in Rust
//! Node modules resolution
//! Reserved for cross-file navigation features

#![allow(dead_code)]

use std::path::{Path, PathBuf};

/// Resolve a module from node_modules
pub fn resolve_node_module(specifier: &str, from_dir: &Path) -> Option<PathBuf> {
    // Split the specifier into package name and subpath
    let (package_name, subpath) = parse_package_specifier(specifier);

    // Walk up directory tree looking for node_modules
    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() {
                // Try to resolve within the package
                if let Some(resolved) = resolve_package_entry(&package_dir, subpath.as_deref()) {
                    return Some(resolved);
                }
            }
        }

        // Move up to parent directory
        if let Some(parent) = current_dir.parent() {
            current_dir = parent.to_path_buf();
        } else {
            break;
        }
    }

    None
}

/// Parse a package specifier into package name and subpath
fn parse_package_specifier(specifier: &str) -> (String, Option<String>) {
    if specifier.starts_with('@') {
        // Scoped package: @scope/package or @scope/package/subpath
        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);
        }
    }

    // Regular package: package or package/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)
}

/// Resolve a package entry point
fn resolve_package_entry(package_dir: &Path, subpath: Option<&str>) -> Option<PathBuf> {
    if let Some(subpath) = subpath {
        // Resolve subpath within package
        let target = package_dir.join(subpath);
        return try_resolve_file(&target);
    }

    // Try package.json exports/main
    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) {
                // Try "exports" field first (modern)
                if let Some(exports) = json.get("exports") {
                    if let Some(resolved) = resolve_exports(exports, package_dir, ".") {
                        return Some(resolved);
                    }
                }

                // Try "types" field (for type definitions)
                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);
                        }
                    }
                }

                // Try "main" field
                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);
                        }
                    }
                }
            }
        }
    }

    // Fallback to index file
    try_resolve_file(&package_dir.join("index"))
}

/// Resolve package.json exports field
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) => {
            // Try to find the subpath
            if let Some(entry) = map.get(subpath) {
                return resolve_export_entry(entry, package_dir);
            }

            // Try "." for default export
            if subpath == "." {
                if let Some(entry) = map.get(".") {
                    return resolve_export_entry(entry, package_dir);
                }
            }
        }
        _ => {}
    }
    None
}

/// Resolve a single export entry
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) => {
            // Try conditions in order of preference
            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,
    }
}

/// Try to resolve a file path with extensions
fn try_resolve_file(path: &Path) -> Option<PathBuf> {
    // If path exists as-is
    if path.is_file() {
        return Some(path.to_path_buf());
    }

    // Try with extensions
    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);
        }
    }

    // Try as directory with index
    if path.is_dir() {
        for ext in extensions {
            let index = path.join(format!("index{}", ext));
            if index.is_file() {
                return Some(index);
            }
        }
    }

    None
}