1extern 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#[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#[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 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 pub fn with_basedir(&self, basedir: PathBuf) -> Self {
99 Resolver { basedir: Some(basedir), ..self.clone() }
100 }
101
102 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 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 pub fn preserve_symlinks(&self, preserve_symlinks: bool) -> Self {
135 Resolver { preserve_symlinks, ..self.clone() }
136 }
137
138 pub fn resolve(&self, target: &str) -> Result<PathBuf, ResolutionError> {
140 if is_core_module(target) {
142 return Ok(PathBuf::from(target));
144 }
145
146 let root = PathBuf::from("/");
148 let basedir = if target.starts_with('/') {
150 &root
152 } else {
153 self.get_basedir()?
154 };
155
156 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 fn resolve_as_file(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
179 if path.is_file() {
181 return Ok(path.clone());
182 }
183
184 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 fn resolve_as_directory(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
201 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 self.resolve_index(path)
212 }
213
214 fn resolve_package_main(&self, pkg_path: &PathBuf) -> Result<PathBuf, ResolutionError> {
216 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 fn resolve_index(&self, path: &PathBuf) -> Result<PathBuf, ResolutionError> {
240 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 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 },
289 PathComponent::Normal(name) => {
290 normalized.push(name);
291 },
292 }
293 }
294 normalized
295}
296
297pub fn is_core_module(target: &str) -> bool {
299 BUILTINS.iter().any(|builtin| builtin == &target)
300}
301
302pub fn resolve(target: &str) -> Result<PathBuf, ResolutionError> {
312 Resolver::new().with_basedir(PathBuf::from(".")).resolve(target)
313}
314
315pub 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}