js_resolve/
lib.rs

1use std::fs::read_to_string;
2use std::io::{Error, ErrorKind};
3use std::path::{Component, Path, PathBuf};
4
5pub fn resolve(name: String, context: &Path) -> Result<PathBuf, Error> {
6    let parent = context.parent().unwrap();
7    let path = Path::new(&name);
8    if path.starts_with("./") || path.starts_with("../") {
9        let new_path = normalize(&parent.join(path));
10
11        load(&new_path)
12    } else if path.is_absolute() {
13        load(path)
14    } else if name.is_empty() {
15        load(parent)
16    } else {
17        let new_path = parent.join("node_modules").join(&path);
18
19        load(&new_path).or_else(|_| resolve(name, parent))
20    }
21}
22
23fn load(path: &Path) -> Result<PathBuf, Error> {
24    if path.is_file() {
25        return Ok(path.to_path_buf());
26    }
27
28    let extensions = vec!["js", "mjs", "json"];
29    for extension in extensions {
30        let new_path = path.with_extension(extension);
31        if new_path.is_file() {
32            return Ok(new_path);
33        }
34    }
35
36    let pkg_path = path.join("package.json");
37    if let Ok(data) = read_to_string(&pkg_path) {
38        if let Ok(pkg_info) = json::parse(&data) {
39            if let Some(main) = pkg_info["main"].as_str() {
40                if main != "." && main != ".." {
41                    return load(&path.join(main));
42                }
43            }
44        }
45    }
46    if path.is_dir() {
47        return load(&path.join("index"));
48    }
49    Err(Error::new(
50        ErrorKind::NotFound,
51        format!("Can't find {}", path.display()),
52    ))
53}
54
55pub fn normalize(p: &Path) -> PathBuf {
56    p.components().fold(PathBuf::from(""), |path, c| match c {
57        Component::Prefix(ref prefix) => PathBuf::from(prefix.as_os_str().to_owned()),
58        Component::RootDir | Component::CurDir => path.join("/"),
59        Component::Normal(part) => path.join(part),
60        Component::ParentDir => match path.parent() {
61            Some(path) => path.to_owned(),
62            None => path,
63        },
64    })
65}
66
67#[test]
68fn test_resolve() {
69    fn assert_resolves(name: &str, path: &str, expected: &str) {
70        let fixtures = std::env::current_dir().unwrap().join("fixtures");
71        assert_eq!(
72            resolve(name.to_string(), &fixtures.join(path).join("index.js")).unwrap(),
73            normalize(&fixtures.join(path).join(expected.to_string())),
74        );
75    }
76
77    assert_resolves("", "no-entry", "index.js");
78    assert_resolves("./counter", "relative-file", "counter");
79    assert_resolves("./counter", "relative-file-js", "counter.js");
80    assert_resolves("./counter", "relative-file-mjs", "counter.mjs");
81    assert_resolves("./counter/counter", "relative-file-nested", "counter/counter.js");
82    assert_resolves("./😅", "relative-file-unicode", "😅.js");
83    assert_resolves("./😅", "relative-dir-unicode", "😅/index.js");
84    assert_resolves("./😅/🤔", "relative-nested-unicode", "😅/🤔.js");
85    assert_resolves("../counter", "parent-dir/entry", "../counter/index.js");
86    assert_resolves("../counter", "parent-js/entry", "../counter.js");
87    assert_resolves("../counter/counter", "parent-nested/entry", "../counter/counter.js");
88    assert_resolves("./counter", "subdir", "counter/index.js");
89    assert_resolves("./counter", "subdir-noext", "counter/index");
90    assert_resolves("./", "pkginfo-basic", "counter.js");
91    assert_resolves(".", "pkginfo-basic", "counter.js");
92    assert_resolves("./counter", "pkginfo-nested", "counter/counter.js");
93    assert_resolves("../", "pkginfo-parent/entry", "../counter.js");
94    assert_resolves("..", "pkginfo-parent/entry", "../counter.js");
95    assert_resolves(".", "pkginfo-dot", "index.js");
96    assert_resolves("..", "pkginfo-dot/entry", "../index.js");
97    assert_resolves("package", "modules-basic", "node_modules/package/index.js");
98    assert_resolves("package", "modules-file", "node_modules/package.js");
99    assert_resolves("package", "modules-pkginfo", "node_modules/package/entry.js");
100    assert_resolves("package", "modules-pkginfo-relative", "node_modules/package/lib/index.js");
101    assert_resolves("package/lib/counter", "modules-nested", "node_modules/package/lib/counter.js");
102    assert_resolves(".package", "modules-dotted", "node_modules/.package/index.js");
103    assert_resolves("counter", "modules-parent/subdir", "../node_modules/counter/index.js");
104    assert_resolves("counter", "modules-multilevels/subdir/subdir/subdir/subdir", "../../../../node_modules/counter/index.js");
105    assert_resolves("😅", "unicode-pkg", "node_modules/😅/index.js");
106    assert_resolves("package", "unicode-pkg-entry", "node_modules/package/🤔.js");
107    assert_resolves("🤔", "unicode-both", "node_modules/🤔/😅");
108}
109
110#[test]
111fn test_normalize() {
112    assert_eq!(
113        normalize(&Path::new("/Users/shf/Projects").join(Path::new("/Users/shf/Projects/paq"))),
114        PathBuf::from("/Users/shf/Projects/paq")
115    );
116    assert_eq!(
117        normalize(&Path::new("/Users/shf/Projects").join(Path::new("paq"))),
118        PathBuf::from("/Users/shf/Projects/paq")
119    );
120}