1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use std::fs::read_to_string;
use std::path::{Component, Path, PathBuf};

pub fn resolve(name: String, context: &Path) -> Option<PathBuf> {
    let path = Path::new(&name);
    if path.starts_with("./") || path.starts_with("../") {
        let new_path = normalize(&context.join(path));

        load(&new_path)
    } else if path.is_absolute() {
        load(path)
    } else if name.is_empty() {
        load(context)
    } else {
        let parent = context.parent()?;
        let new_path = context.join("node_modules").join(&path);

        load(&new_path).or(resolve(name, parent))
    }
}

fn load(path: &Path) -> Option<PathBuf> {
    if path.is_file() {
        return Some(path.to_path_buf());
    }

    let extensions = vec!["js", "mjs", "json"];
    for extension in extensions {
        let new_path = path.with_extension(extension);
        if new_path.is_file() {
            return Some(new_path);
        }
    }

    let pkg_path = path.join("package.json");
    if let Ok(data) = read_to_string(&pkg_path) {
        if let Ok(pkg_info) = json::parse(&data) {
            if let Some(main) = pkg_info["main"].as_str() {
                if main != "." && main != ".." {
                    return load(&path.join(main));
                }
            }
        }
    }
    if path.is_dir() {
        return load(&path.join("index"));
    }
    None
}

fn normalize(p: &Path) -> PathBuf {
    p.components().fold(PathBuf::from("/"), |path, c| match c {
        Component::Prefix(ref prefix) => PathBuf::from(prefix.as_os_str().to_owned()),
        Component::RootDir => path.join("/"),
        Component::CurDir => path,
        Component::ParentDir => path.parent().unwrap().to_owned(),
        Component::Normal(part) => path.join(part),
    })
}

#[test]
fn test_resolve() {
    fn assert_resolves(name: &str, path: &str, expected: &str) {
        let fixtures = std::env::current_dir().unwrap().join("fixtures");
        assert_eq!(resolve(name.to_string(), &fixtures.join(path)), Some(
            normalize(&fixtures.join(path).join(expected.to_string()))
        ));
    }

    assert_resolves("", "no-entry", "index.js");
    assert_resolves("./counter", "relative-file", "counter");
    assert_resolves("./counter", "relative-file-js", "counter.js");
    assert_resolves("./counter", "relative-file-mjs", "counter.mjs");
    assert_resolves("./counter/counter", "relative-file-nested", "counter/counter.js");
    assert_resolves("./😅", "relative-file-unicode", "😅.js");
    assert_resolves("./😅", "relative-dir-unicode", "😅/index.js");
    assert_resolves("./😅/🤔", "relative-nested-unicode", "😅/🤔.js");
    assert_resolves("../counter", "parent-dir/entry", "../counter/index.js");
    assert_resolves("../counter", "parent-js/entry", "../counter.js");
    assert_resolves("../counter/counter", "parent-nested/entry", "../counter/counter.js");
    assert_resolves("./counter", "subdir", "counter/index.js");
    assert_resolves("./counter", "subdir-noext", "counter/index");
    assert_resolves("./", "pkginfo-basic", "counter.js");
    assert_resolves(".", "pkginfo-basic", "counter.js");
    assert_resolves("./counter", "pkginfo-nested", "counter/counter.js");
    assert_resolves("../", "pkginfo-parent/entry", "../counter.js");
    assert_resolves("..", "pkginfo-parent/entry", "../counter.js");
    assert_resolves(".", "pkginfo-dot", "index.js");
    assert_resolves("..", "pkginfo-dot/entry", "../index.js");
    assert_resolves("package", "modules-basic", "node_modules/package/index.js");
    assert_resolves("package", "modules-file", "node_modules/package.js");
    assert_resolves("package", "modules-pkginfo", "node_modules/package/entry.js");
    assert_resolves("package", "modules-pkginfo-relative", "node_modules/package/lib/index.js");
    assert_resolves("package/lib/counter", "modules-nested", "node_modules/package/lib/counter.js");
    assert_resolves(".package", "modules-dotted", "node_modules/.package/index.js");
    assert_resolves("counter", "modules-parent/subdir", "../node_modules/counter/index.js");
    assert_resolves("counter", "modules-multilevels/subdir/subdir/subdir/subdir", "../../../../node_modules/counter/index.js");
    assert_resolves("😅", "unicode-pkg", "node_modules/😅/index.js");
    assert_resolves("package", "unicode-pkg-entry", "node_modules/package/🤔.js");
    assert_resolves("🤔", "unicode-both", "node_modules/🤔/😅");
}

#[test]
fn test_normalize() {
    assert_eq!(
        normalize(&Path::new("/Users/shf/Projects").join(Path::new("/Users/shf/Projects/paq"))),
        PathBuf::from("/Users/shf/Projects/paq")
    );
    assert_eq!(
        normalize(&Path::new("/Users/shf/Projects").join(Path::new("paq"))),
        PathBuf::from("/Users/shf/Projects/paq")
    );
}