node_resolve/
lib.rs

1//! Resolve module identifiers in a Node-style `require()` to a full file path.
2//!
3//! ```rust
4//! use node_resolve::{resolve, resolve_from};
5//!
6//! resolve("abc");
7//! // → Ok("/path/to/cwd/node_modules/abc/index.js")
8//! resolve_from("abc", PathBuf::from("/other/path"));
9//! // → Ok("/other/path/node_modules/abc/index.js")
10//! ```
11
12extern crate serde_json;
13extern crate node_builtins;
14
15use std::fmt;
16use std::fs::File;
17use std::error::Error;
18use std::default::Default;
19use std::path::{Path, PathBuf, Component as PathComponent};
20use serde_json::Value;
21use node_builtins::BUILTINS;
22
23/// An Error, returned when the module could not be resolved.
24#[derive(Debug)]
25pub struct ResolutionError {
26    description: String
27}
28impl ResolutionError {
29    fn new(description: &str) -> Self {
30        ResolutionError { description: String::from(description) }
31    }
32}
33
34impl From<serde_json::Error> for ResolutionError {
35    fn from(_error: serde_json::Error) -> Self {
36        ResolutionError::new("Json parse error")
37    }
38}
39
40impl From<std::io::Error> for ResolutionError {
41    fn from(_error: std::io::Error) -> Self {
42        ResolutionError::new("Io error")
43    }
44}
45
46impl fmt::Display for ResolutionError {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        write!(f, "{}", self.description)
49    }
50}
51
52impl Error for ResolutionError {
53    fn description(&self) -> &str {
54        self.description.as_str()
55    }
56    fn cause(&self) -> Option<&Error> {
57        None
58    }
59}
60
61/// Resolver instances keep track of options.
62#[derive(Clone)]
63pub struct Resolver {
64    basedir: Option<PathBuf>,
65    extensions: Vec<String>,
66    preserve_symlinks: bool,
67    main_fields: Vec<String>,
68}
69
70impl Default for Resolver {
71    fn default() -> Resolver {
72        Resolver {
73            basedir: None,
74            extensions: vec![
75                String::from(".js"),
76                String::from(".json"),
77                String::from(".node"),
78            ],
79            preserve_symlinks: false,
80            main_fields: vec![
81                String::from("main"),
82            ],
83        }
84    }
85}
86
87impl Resolver {
88    /// Create a new resolver with the given options.
89    pub fn new() -> Self {
90        Resolver::default()
91    }
92
93    fn get_basedir(&self) -> Result<&PathBuf, ResolutionError> {
94        self.basedir.as_ref().ok_or_else(|| ResolutionError::new("Must set a basedir before resolving"))
95    }
96
97    /// Create a new resolver with a different basedir.
98    pub fn with_basedir(&self, basedir: PathBuf) -> Self {
99        Resolver { basedir: Some(basedir), ..self.clone() }
100    }
101
102    /// Create a new resolver with a different set of extensions.
103    pub fn with_extensions<T>(&self, extensions: T) -> Self
104        where T: IntoIterator,
105              T::Item: ToString
106    {
107        Resolver {
108            extensions: extensions.into_iter()
109                .map(|ext| ext.to_string())
110                .map(|ext| if ext.starts_with('.') {
111                    ext
112                } else {
113                    format!(".{}", ext)
114                })
115                .collect(),
116            ..self.clone()
117        }
118    }
119
120    /// Create a new resolver with a different set of main fields.
121    pub fn with_main_fields<T>(&self, main_fields: T) -> Self
122        where T: IntoIterator,
123              T::Item: ToString
124    {
125        Resolver {
126            main_fields: main_fields.into_iter()
127                .map(|field| field.to_string())
128                .collect(),
129            ..self.clone()
130        }
131    }
132
133    /// Create a new resolver with a different symlink option.
134    pub fn preserve_symlinks(&self, preserve_symlinks: bool) -> Self {
135        Resolver { preserve_symlinks, ..self.clone() }
136    }
137
138    /// Resolve a `require()` argument.
139    pub fn resolve(&self, target: &str) -> Result<PathBuf, ResolutionError> {
140        // 1. If X is a core module
141        if is_core_module(target) {
142            // 1.a. Return the core module
143            return Ok(PathBuf::from(target));
144        }
145
146        // TODO how to not always initialise this here?
147        let root = PathBuf::from("/");
148        // 2. If X begins with '/'
149        let basedir = if target.starts_with('/') {
150            // 2.a. Set Y to be the filesystem root
151            &root
152        } else {
153            self.get_basedir()?
154        };
155
156        // 3. If X begins with './' or '/' or '../'
157        if target.starts_with("./") || target.starts_with('/') || target.starts_with("../") {
158            let path = basedir.as_path().join(target);
159            return self.resolve_as_file(&path)
160                .or_else(|_| self.resolve_as_directory(&path))
161                .and_then(|p| self.normalize(&p));
162        }
163
164        self.resolve_node_modules(target)
165            .and_then(|p| self.normalize(&p))
166    }
167
168    fn normalize(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
169        if self.preserve_symlinks {
170            Ok(normalize_path(path))
171        } else {
172            path.canonicalize().map_err(|e| e.into())
173        }
174    }
175
176    /// Resolve a path as a file. If `path` refers to a file, it is returned;
177    /// otherwise the `path` + each extension is tried.
178    fn resolve_as_file(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
179        // 1. If X is a file, load X as JavaScript text.
180        if path.is_file() {
181            return Ok(path.clone());
182        }
183
184        // 1. If X.js is a file, load X.js as JavaScript text.
185        // 2. If X.json is a file, parse X.json to a JavaScript object.
186        // 3. If X.node is a file, load X.node as binary addon.
187        let str_path = path.to_str().ok_or_else(|| ResolutionError::new("Invalid path"))?;
188        for ext in &self.extensions {
189            let ext_path = PathBuf::from(format!("{}{}", str_path, ext));
190            if ext_path.is_file() {
191                return Ok(ext_path);
192            }
193        }
194
195        Err(ResolutionError::new("Not found"))
196    }
197
198    /// Resolve a path as a directory, using the "main" key from a package.json file if it
199    /// exists, or resolving to the index.EXT file if it exists.
200    fn resolve_as_directory(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
201        // 1. If X/package.json is a file, use it.
202        let pkg_path = path.join("package.json");
203        if pkg_path.is_file() {
204            let main = self.resolve_package_main(&pkg_path);
205            if main.is_ok() {
206                return main
207            }
208        }
209
210        // 2. LOAD_INDEX(X)
211        self.resolve_index(path)
212    }
213
214    /// Resolve using the package.json "main" key.
215    fn resolve_package_main(&self, pkg_path: &PathBuf) -> Result<PathBuf, ResolutionError> {
216        // TODO how to not always initialise this here?
217        let root = PathBuf::from("/");
218        let pkg_dir = pkg_path.parent().unwrap_or(&root);
219        let file = File::open(pkg_path)?;
220        let pkg: Value = serde_json::from_reader(file)?;
221        if !pkg.is_object() {
222            return Err(ResolutionError::new("package.json is not an object"));
223        }
224
225        let main_field = self.main_fields.iter()
226            .find(|name| pkg[name].is_string())
227            .and_then(|name| pkg[name].as_str());
228        match main_field {
229            Some(target) => {
230                let path = pkg_dir.join(target);
231                self.resolve_as_file(&path)
232                    .or_else(|_| self.resolve_as_directory(&path))
233            },
234            None => Err(ResolutionError::new("package.json does not contain a \"main\" string"))
235        }
236    }
237
238    /// Resolve a directory to its index.EXT.
239    fn resolve_index(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
240        // 1. If X/index.js is a file, load X/index.js as JavaScript text.
241        // 2. If X/index.json is a file, parse X/index.json to a JavaScript object.
242        // 3. If X/index.node is a file, load X/index.node as binary addon.
243        for ext in &self.extensions {
244            let ext_path = path.join(format!("index{}", ext));
245            if ext_path.is_file() {
246                return Ok(ext_path);
247            }
248        }
249
250        Err(ResolutionError::new("Not found"))
251    }
252
253    /// Resolve by walking up node_modules folders.
254    fn resolve_node_modules(&self, target: &str) -> Result<PathBuf, ResolutionError> {
255        let basedir = self.get_basedir()?;
256        let node_modules = basedir.join("node_modules");
257        if node_modules.is_dir() {
258            let path = node_modules.join(target);
259            let result = self.resolve_as_file(&path)
260                .or_else(|_| self.resolve_as_directory(&path));
261            if result.is_ok() {
262                return result
263            }
264        }
265
266        match basedir.parent() {
267            Some(parent) => self.with_basedir(parent.to_path_buf()).resolve_node_modules(target),
268            None => Err(ResolutionError::new("Not found")),
269        }
270    }
271}
272
273fn normalize_path(p: &Path) -> PathBuf {
274    let mut normalized = PathBuf::from("/");
275    for part in p.components() {
276        match part {
277            PathComponent::Prefix(ref prefix) => {
278                normalized.push(prefix.as_os_str());
279            },
280            PathComponent::RootDir => {
281                normalized.push("/");
282            },
283            PathComponent::ParentDir => {
284                normalized.pop();
285            },
286            PathComponent::CurDir => {
287                // Nothing
288            },
289            PathComponent::Normal(name) => {
290                normalized.push(name);
291            },
292        }
293    }
294    normalized
295}
296
297/// Check if a string references a core module, such as "events".
298pub fn is_core_module(target: &str) -> bool {
299    BUILTINS.iter().any(|builtin| builtin == &target)
300}
301
302/// Resolve a node.js module path relative to the current working directory.
303/// Returns the absolute path to the module, or an error.
304///
305/// ```rust
306/// match resolve("./lib") {
307///     Ok(path) => println!("Path is: {:?}", path),
308///     Err(err) => panic!("Failed: {:?}", err),
309/// }
310/// ```
311pub fn resolve(target: &str) -> Result<PathBuf, ResolutionError> {
312    Resolver::new().with_basedir(PathBuf::from(".")).resolve(target)
313}
314
315/// Resolve a node.js module path relative to `basedir`.
316/// Returns the absolute path to the module, or an error.
317///
318/// ```rust
319/// match resolve_from("./index.js", env::current_dir().unwrap()) {
320///     Ok(path) => println!("Path is: {:?}", path),
321///     Err(err) => panic!("Failed: {:?}", err),
322/// }
323/// ```
324pub fn resolve_from(target: &str, basedir: PathBuf) -> Result<PathBuf, ResolutionError> {
325    Resolver::new().with_basedir(basedir).resolve(target)
326}
327
328#[cfg(test)]
329mod tests {
330    use std::env;
331    use std::path::PathBuf;
332
333    fn fixture(part: &str) -> PathBuf {
334        env::current_dir().unwrap().join("fixtures").join(part)
335    }
336    fn resolve_fixture(target: &str) -> PathBuf {
337        ::resolve_from(target, fixture("")).unwrap()
338    }
339
340    #[test]
341    fn appends_extensions() {
342        assert_eq!(fixture("extensions/js-file.js"), resolve_fixture("./extensions/js-file"));
343        assert_eq!(fixture("extensions/json-file.json"), resolve_fixture("./extensions/json-file"));
344        assert_eq!(fixture("extensions/native-file.node"), resolve_fixture("./extensions/native-file"));
345        assert_eq!(fixture("extensions/other-file.ext"), resolve_fixture("./extensions/other-file.ext"));
346        assert_eq!(fixture("extensions/no-ext"), resolve_fixture("./extensions/no-ext"));
347        assert_eq!(fixture("extensions/other-file.ext"), ::Resolver::new()
348                   .with_extensions(&[".ext"])
349                   .with_basedir(fixture(""))
350                   .resolve("./extensions/other-file").unwrap());
351        assert_eq!(fixture("extensions/module.mjs"), ::Resolver::new()
352                   .with_extensions(&[".mjs"])
353                   .with_basedir(fixture(""))
354                   .resolve("./extensions/module").unwrap());
355    }
356
357    #[test]
358    fn resolves_package_json() {
359        assert_eq!(fixture("package-json/main-file/whatever.js"), resolve_fixture("./package-json/main-file"));
360        assert_eq!(fixture("package-json/main-file-noext/whatever.js"), resolve_fixture("./package-json/main-file-noext"));
361        assert_eq!(fixture("package-json/main-dir/subdir/index.js"), resolve_fixture("./package-json/main-dir"));
362        assert_eq!(fixture("package-json/not-object/index.js"), resolve_fixture("./package-json/not-object"));
363        assert_eq!(fixture("package-json/invalid/index.js"), resolve_fixture("./package-json/invalid"));
364        assert_eq!(fixture("package-json/main-none/index.js"), resolve_fixture("./package-json/main-none"));
365        assert_eq!(fixture("package-json/main-file/whatever.js"), ::Resolver::new()
366                   .with_main_fields(&["module", "main"])
367                   .with_basedir(fixture(""))
368                   .resolve("./package-json/main-file").unwrap());
369        assert_eq!(fixture("package-json/module/index.mjs"), ::Resolver::new()
370                   .with_extensions(&[".mjs", ".js"])
371                   .with_main_fields(&["module", "main"])
372                   .with_basedir(fixture(""))
373                   .resolve("./package-json/module").unwrap());
374        assert_eq!(fixture("package-json/module-main/main.mjs"), ::Resolver::new()
375                   .with_extensions(&[".mjs", ".js"])
376                   .with_main_fields(&["module", "main"])
377                   .with_basedir(fixture(""))
378                   .resolve("./package-json/module-main").unwrap());
379
380    }
381
382    #[test]
383    fn resolves_node_modules() {
384        assert_eq!(fixture("node-modules/same-dir/node_modules/a.js"), ::resolve_from("a", fixture("node-modules/same-dir")).unwrap());
385        assert_eq!(fixture("node-modules/parent-dir/node_modules/a/index.js"), ::resolve_from("a", fixture("node-modules/parent-dir/src")).unwrap());
386        assert_eq!(fixture("node-modules/package-json/node_modules/dep/lib/index.js"), ::resolve_from("dep", fixture("node-modules/package-json")).unwrap());
387        assert_eq!(fixture("node-modules/walk/src/node_modules/not-ok/index.js"), ::resolve_from("not-ok", fixture("node-modules/walk/src")).unwrap());
388        assert_eq!(fixture("node-modules/walk/node_modules/ok/index.js"), ::resolve_from("ok", fixture("node-modules/walk/src")).unwrap());
389    }
390
391    #[test]
392    fn preserves_symlinks() {
393        assert_eq!(fixture("symlink/node_modules/dep/main.js"),
394            ::Resolver::new()
395                   .preserve_symlinks(true)
396                   .with_basedir(fixture("symlink"))
397                   .resolve("dep").unwrap()
398       );
399    }
400
401    #[test]
402    fn does_not_preserve_symlinks() {
403        assert_eq!(fixture("symlink/linked/main.js"),
404            ::Resolver::new()
405                   .preserve_symlinks(false)
406                   .with_basedir(fixture("symlink"))
407                   .resolve("dep").unwrap()
408       );
409    }
410
411    #[test]
412    fn resolves_absolute_specifier() {
413        let full_path = fixture("extensions/js-file");
414        let id = full_path.to_str().unwrap();
415        assert_eq!(fixture("extensions/js-file.js"), ::resolve(id).unwrap());
416    }
417
418    #[test]
419    fn core_modules() {
420        assert!(::is_core_module("events"));
421        assert!(!::is_core_module("events/"));
422        assert!(!::is_core_module("./events"));
423        assert!(::is_core_module("stream"));
424        assert!(!::is_core_module("acorn"));
425    }
426}