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}