├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md ├── fixtures ├── extensions │ ├── js-file.js │ ├── json-file.json │ ├── module.mjs │ ├── native-file.node │ ├── no-ext │ └── other-file.ext ├── node-modules │ ├── package-json │ │ └── node_modules │ │ │ └── dep │ │ │ ├── lib │ │ │ └── index.js │ │ │ └── package.json │ ├── parent-dir │ │ ├── node_modules │ │ │ └── a │ │ │ │ └── index.js │ │ └── src │ │ │ └── .gitkeep │ ├── same-dir │ │ └── node_modules │ │ │ └── a.js │ └── walk │ │ ├── node_modules │ │ └── ok │ │ │ └── index.js │ │ └── src │ │ ├── node_modules │ │ └── not-ok │ │ │ └── index.js │ │ └── sub │ │ └── index.js ├── package-json │ ├── invalid │ │ ├── index.js │ │ └── package.json │ ├── main-dir │ │ ├── package.json │ │ └── subdir │ │ │ └── index.js │ ├── main-file-noext │ │ ├── package.json │ │ └── whatever.js │ ├── main-file │ │ ├── package.json │ │ └── whatever.js │ ├── main-none │ │ ├── index.js │ │ └── package.json │ ├── module-main │ │ ├── main.mjs │ │ └── package.json │ ├── module │ │ └── index.mjs │ └── not-object │ │ ├── index.js │ │ └── package.json └── symlink │ ├── linked │ ├── main.js │ └── package.json │ └── node_modules │ └── dep ├── readme.md └── src └── lib.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [default] 6 | pull_request: 7 | branches: [default] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | clippy: 14 | name: Rust code style 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v3 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | - name: Run tests 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: clippy 29 | 30 | test: 31 | name: Tests 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | rust-toolchain: [stable, beta, nightly] 36 | 37 | steps: 38 | - name: Checkout sources 39 | uses: actions/checkout@v3 40 | - name: Install ${{matrix.rust-toolchain}} toolchain 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: ${{matrix.rust-toolchain}} 45 | override: true 46 | - name: Run tests 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: test 50 | args: --verbose 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # node-resolve change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 2.2.0 8 | * Add `with_main_fields()` to configure the package.json `"main"` field name. 9 | ```rust 10 | Resolver::new() 11 | .with_main_fields(&["module", "main"]) 12 | .with_extensions(&[".js", ".mjs", ".json"]) 13 | ``` 14 | * Implement `Default` for `Resolver`. 15 | 16 | ## 2.1.1 17 | * Exclude test symlink from the package so it can be published. 18 | 19 | ## 2.1.0 20 | * Normalize paths before returning. You will now receive eg. `/a/b/c.js` instead 21 | of `/a/./b/c.js`. 22 | * Implement `preserve_symlinks(bool)`. Symlinks are not resolved by default. 23 | This will change in the next major to match Node's behaviour. 24 | 25 | ## 2.0.0 26 | * Take an `&str` argument instead of a `String` 27 | * Expose `Resolver` 28 | 29 | ## 1.1.0 30 | * Add `is_core_module()` 31 | 32 | ## 1.0.1 33 | * Fix absolute specifiers like `require("/a")` 34 | 35 | ## 1.0.0 36 | * Initial release. 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "node-resolve" 3 | version = "2.2.0" 4 | description = "The Node.js module resolution algorithm" 5 | authors = ["Renée Kooi "] 6 | edition = "2018" 7 | repository = "https://github.com/goto-bus-stop/node-resolve" 8 | documentation = "https://docs.rs/node-resolve" 9 | license = "Apache-2.0" 10 | readme = "readme.md" 11 | 12 | exclude = [ 13 | "fixtures/*", 14 | ] 15 | 16 | [dependencies] 17 | serde_json = "1.0.10" 18 | node-builtins = "0.1.0" 19 | 20 | [lib] 21 | doctest = false 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) 2 | 3 | Copyright 2018 Renée Kooi 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | > http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /fixtures/extensions/js-file.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/extensions/js-file.js -------------------------------------------------------------------------------- /fixtures/extensions/json-file.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/extensions/json-file.json -------------------------------------------------------------------------------- /fixtures/extensions/module.mjs: -------------------------------------------------------------------------------- 1 | export default null 2 | -------------------------------------------------------------------------------- /fixtures/extensions/native-file.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/extensions/native-file.node -------------------------------------------------------------------------------- /fixtures/extensions/no-ext: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/extensions/no-ext -------------------------------------------------------------------------------- /fixtures/extensions/other-file.ext: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/extensions/other-file.ext -------------------------------------------------------------------------------- /fixtures/node-modules/package-json/node_modules/dep/lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/package-json/node_modules/dep/lib/index.js -------------------------------------------------------------------------------- /fixtures/node-modules/package-json/node_modules/dep/package.json: -------------------------------------------------------------------------------- 1 | {"main": "lib"} 2 | -------------------------------------------------------------------------------- /fixtures/node-modules/parent-dir/node_modules/a/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/parent-dir/node_modules/a/index.js -------------------------------------------------------------------------------- /fixtures/node-modules/parent-dir/src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/parent-dir/src/.gitkeep -------------------------------------------------------------------------------- /fixtures/node-modules/same-dir/node_modules/a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/same-dir/node_modules/a.js -------------------------------------------------------------------------------- /fixtures/node-modules/walk/node_modules/ok/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/walk/node_modules/ok/index.js -------------------------------------------------------------------------------- /fixtures/node-modules/walk/src/node_modules/not-ok/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/walk/src/node_modules/not-ok/index.js -------------------------------------------------------------------------------- /fixtures/node-modules/walk/src/sub/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/node-modules/walk/src/sub/index.js -------------------------------------------------------------------------------- /fixtures/package-json/invalid/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/invalid/index.js -------------------------------------------------------------------------------- /fixtures/package-json/invalid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": "error 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/package-json/main-dir/package.json: -------------------------------------------------------------------------------- 1 | {"main": "subdir"} 2 | -------------------------------------------------------------------------------- /fixtures/package-json/main-dir/subdir/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/main-dir/subdir/index.js -------------------------------------------------------------------------------- /fixtures/package-json/main-file-noext/package.json: -------------------------------------------------------------------------------- 1 | {"main": "whatever"} 2 | -------------------------------------------------------------------------------- /fixtures/package-json/main-file-noext/whatever.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/main-file-noext/whatever.js -------------------------------------------------------------------------------- /fixtures/package-json/main-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "whatever.js" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/package-json/main-file/whatever.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/main-file/whatever.js -------------------------------------------------------------------------------- /fixtures/package-json/main-none/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/main-none/index.js -------------------------------------------------------------------------------- /fixtures/package-json/main-none/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "does-not-exist.js" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/package-json/module-main/main.mjs: -------------------------------------------------------------------------------- 1 | export default 'main' 2 | -------------------------------------------------------------------------------- /fixtures/package-json/module-main/package.json: -------------------------------------------------------------------------------- 1 | {"main": "main"} 2 | -------------------------------------------------------------------------------- /fixtures/package-json/module/index.mjs: -------------------------------------------------------------------------------- 1 | export default 'index' 2 | -------------------------------------------------------------------------------- /fixtures/package-json/not-object/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goto-bus-stop/node-resolve/937ea992879346d5f6aeab2aa42a29a6e7e0f5bf/fixtures/package-json/not-object/index.js -------------------------------------------------------------------------------- /fixtures/package-json/not-object/package.json: -------------------------------------------------------------------------------- 1 | "just a string" 2 | -------------------------------------------------------------------------------- /fixtures/symlink/linked/main.js: -------------------------------------------------------------------------------- 1 | module.exports = 'main' 2 | -------------------------------------------------------------------------------- /fixtures/symlink/linked/package.json: -------------------------------------------------------------------------------- 1 | {"main": "main.js"} 2 | -------------------------------------------------------------------------------- /fixtures/symlink/node_modules/dep: -------------------------------------------------------------------------------- 1 | ../linked -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # node-resolve 2 | 3 | [![node-resolve on crates.io](https://img.shields.io/crates/v/node-resolve.svg)](https://crates.io/crates/node-resolve) 4 | 5 | Rust implementation of the [Node.js module resolution algorithm](https://nodejs.org/api/modules.html#modules_all_together). 6 | 7 | Missing features: 8 | 9 | - [ ] async? 10 | - [ ] maybe more 11 | 12 | ## Install 13 | 14 | Add to your Cargo.toml: 15 | 16 | ```toml 17 | [dependencies] 18 | node-resolve = "2.2.0" 19 | ``` 20 | 21 | ## Usage 22 | 23 | See [docs.rs/node-resolve](https://docs.rs/node-resolve). 24 | 25 | ## License 26 | 27 | [Apache-2.0](./LICENSE.md) 28 | -------------------------------------------------------------------------------- /src/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 | 12 | use node_builtins::BUILTINS; 13 | use serde_json::Value; 14 | use std::default::Default; 15 | use std::error::Error as StdError; 16 | use std::fmt; 17 | use std::fs::File; 18 | use std::io::{Error as IOError, ErrorKind as IOErrorKind}; 19 | use std::path::{Component as PathComponent, Path, PathBuf}; 20 | 21 | static ROOT: &str = "/"; 22 | 23 | #[derive(Debug)] 24 | pub enum Error { 25 | /// Failed to parse a package.json file. 26 | JSONError(serde_json::Error), 27 | /// Could not read a file. 28 | IOError(IOError), 29 | /// A Basedir was not configured. 30 | UnconfiguredBasedir, 31 | } 32 | 33 | impl From for Error { 34 | fn from(err: serde_json::Error) -> Error { 35 | Error::JSONError(err) 36 | } 37 | } 38 | impl From for Error { 39 | fn from(err: IOError) -> Error { 40 | Error::IOError(err) 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | enum InternalError { 46 | /// Something went wrong, and we need to tell the user about this. 47 | Public(Error), 48 | /// Something went wrong, but we can fall back to another resolution. 49 | Private(RecoverableError), 50 | } 51 | 52 | impl InternalError { 53 | fn to_public(self) -> Error { 54 | match self { 55 | InternalError::Public(err) => err, 56 | InternalError::Private(err) => panic!("leaking internal error: {}", err), 57 | } 58 | } 59 | } 60 | 61 | impl From for InternalError { 62 | fn from(err: Error) -> InternalError { 63 | InternalError::Public(err) 64 | } 65 | } 66 | 67 | impl From for InternalError { 68 | fn from(err: RecoverableError) -> InternalError { 69 | InternalError::Private(err) 70 | } 71 | } 72 | 73 | /// An error occured but a fallback is available (mostly while parsing package.json) 74 | #[derive(Debug)] 75 | enum RecoverableError { 76 | NonObjectPackageJson, 77 | MissingMain, 78 | } 79 | 80 | impl fmt::Display for RecoverableError { 81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 82 | match self { 83 | RecoverableError::NonObjectPackageJson => write!(f, "package.json is not an object"), 84 | RecoverableError::MissingMain => write!(f, "package.json does not contain a \"main\" string"), 85 | } 86 | } 87 | } 88 | 89 | impl StdError for RecoverableError {} 90 | 91 | /// Resolver instances keep track of options. 92 | #[derive(Clone)] 93 | pub struct Resolver { 94 | basedir: Option, 95 | extensions: Vec, 96 | preserve_symlinks: bool, 97 | main_fields: Vec, 98 | } 99 | 100 | impl Default for Resolver { 101 | /// Create a new resolver with the default Node.js configuration. 102 | /// 103 | /// - It resolves .js, .json, and .node files, in that order; 104 | /// - It expands symlinks; 105 | /// - It uses the package.json "main" field for bare specifier lookups. 106 | fn default() -> Resolver { 107 | Resolver { 108 | basedir: None, 109 | extensions: vec![ 110 | String::from(".js"), 111 | String::from(".json"), 112 | String::from(".node"), 113 | ], 114 | preserve_symlinks: false, 115 | main_fields: vec![String::from("main")], 116 | } 117 | } 118 | } 119 | 120 | impl Resolver { 121 | #[deprecated(since = "2.3.0", note = "use Resolver::default() instead")] 122 | pub fn new() -> Self { 123 | Resolver::default() 124 | } 125 | 126 | fn get_basedir(&self) -> Result<&Path, Error> { 127 | self.basedir 128 | .as_ref() 129 | .ok_or_else(|| Error::UnconfiguredBasedir) 130 | .map(PathBuf::as_path) 131 | } 132 | 133 | /// Create a new resolver with a different basedir. 134 | pub fn with_basedir(&self, basedir: PathBuf) -> Self { 135 | Resolver { 136 | basedir: Some(basedir), 137 | ..self.clone() 138 | } 139 | } 140 | 141 | /// Use a different set of extensions. Consumes the Resolver instance. 142 | /// The default is `&[".js", ".json", ".node"]`. 143 | /// 144 | /// # Examples 145 | /// 146 | /// ```rust 147 | /// use node_resolve::Resolver; 148 | /// 149 | /// assert_eq!(Ok(PathBuf::from("./fixtures/module/index.mjs")), 150 | /// Resolver::default() 151 | /// .extensions(&[".mjs", ".js", ".json"]) 152 | /// .with_basedir("./fixtures") 153 | /// .resolve("./module") 154 | /// ); 155 | /// ``` 156 | pub fn extensions(self, extensions: T) -> Self 157 | where 158 | T: IntoIterator, 159 | T::Item: ToString, 160 | { 161 | Resolver { 162 | extensions: normalize_extensions(extensions), 163 | ..self 164 | } 165 | } 166 | 167 | /// Use a different set of main fields. Consumes the Resolver instance. 168 | /// The default is `&["main"]`. 169 | /// 170 | /// Main fields are used to determine the entry point of a folder with a 171 | /// `package.json` file. Each main field is tried in order, and the value 172 | /// of the first one that exists is used as the path to the entry point of 173 | /// the folder. 174 | /// 175 | /// # Examples 176 | /// 177 | /// ```rust 178 | /// use node_resolve::Resolver; 179 | /// 180 | /// assert_eq!(Ok(PathBuf::from("./fixtures/module-main/main.mjs"), 181 | /// Resolver::default() 182 | /// .extensions(&[".mjs", ".js", ".json"]) 183 | /// .main_fields(&["module", "main"]) 184 | /// .with_basedir("./fixtures") 185 | /// .resolve("./module-main") 186 | /// ); 187 | /// ``` 188 | pub fn main_fields(self, main_fields: T) -> Self 189 | where 190 | T: IntoIterator, 191 | T::Item: ToString, 192 | { 193 | Resolver { 194 | main_fields: main_fields 195 | .into_iter() 196 | .map(|field| field.to_string()) 197 | .collect(), 198 | ..self 199 | } 200 | } 201 | 202 | /// Configure whether symlinks should be preserved. Consumes the Resolver instance. 203 | /// 204 | /// # Examples 205 | /// 206 | /// ```rust 207 | /// use node_resolve::Resolver; 208 | /// 209 | /// assert_eq!(Ok(PathBuf::from("./fixtures/symlink/node_modules/dep/main.js").canonicalize()), 210 | /// Resolver::default() 211 | /// .preserve_symlinks(true) 212 | /// .with_basedir(PathBuf::from("./fixtures/symlink")) 213 | /// .resolve("dep") 214 | /// ); 215 | /// ``` 216 | /// 217 | /// ```rust 218 | /// use node_resolve::Resolver; 219 | /// 220 | /// assert_eq!(Ok(PathBuf::from("./fixtures/symlink/linked/main.js").canonicalize()), 221 | /// Resolver::default() 222 | /// .preserve_symlinks(false) 223 | /// .with_basedir(PathBuf::from("./fixtures/symlink")) 224 | /// .resolve("dep") 225 | /// }; 226 | /// ``` 227 | pub fn preserve_symlinks(self, preserve_symlinks: bool) -> Self { 228 | Resolver { 229 | preserve_symlinks, 230 | ..self 231 | } 232 | } 233 | 234 | /// Resolve a `require('target')` argument. 235 | pub fn resolve(&self, target: &str) -> Result { 236 | // 1. If X is a core module 237 | if is_core_module(target) { 238 | // 1.a. Return the core module 239 | return Ok(PathBuf::from(target)); 240 | } 241 | 242 | // 2. If X begins with '/' 243 | let basedir = if target.starts_with('/') { 244 | // 2.a. Set Y to be the filesystem root 245 | Path::new(ROOT) 246 | } else { 247 | self.get_basedir()? 248 | }; 249 | 250 | // 3. If X begins with './' or '/' or '../' 251 | if target.starts_with("./") || target.starts_with('/') || target.starts_with("../") { 252 | let path = basedir.join(target); 253 | return self 254 | .resolve_as_file(&path) 255 | .or_else(|_| self.resolve_as_directory(&path)) 256 | .and_then(|p| self.normalize(&p)) 257 | .map_err(InternalError::to_public); 258 | } 259 | 260 | self.resolve_node_modules(target) 261 | .and_then(|p| self.normalize(&p)) 262 | .map_err(InternalError::to_public) 263 | } 264 | 265 | /// Normalize a path to a module. If symlinks should be preserved, this only removes 266 | /// unnecessary `./`s and `../`s from the path. Else it does `realpath()`. 267 | fn normalize(&self, path: &Path) -> Result { 268 | if self.preserve_symlinks { 269 | Ok(normalize_path(path)) 270 | } else { 271 | path.canonicalize().map_err(Error::IOError).map_err(Into::into) 272 | } 273 | } 274 | 275 | /// Resolve a path as a file. If `path` refers to a file, it is returned; 276 | /// otherwise the `path` + each extension is tried. 277 | fn resolve_as_file(&self, path: &Path) -> Result { 278 | // 1. If X is a file, load X as JavaScript text. 279 | if path.is_file() { 280 | return Ok(path.to_path_buf()); 281 | } 282 | 283 | // 1. If X.js is a file, load X.js as JavaScript text. 284 | // 2. If X.json is a file, parse X.json to a JavaScript object. 285 | // 3. If X.node is a file, load X.node as binary addon. 286 | let mut ext_path = path.to_path_buf(); 287 | if let Some(file_name) = ext_path.file_name().and_then(|name| name.to_str()).map(String::from) { 288 | for ext in &self.extensions { 289 | ext_path.set_file_name(format!("{}{}", file_name, ext)); 290 | if ext_path.is_file() { 291 | return Ok(ext_path); 292 | } 293 | } 294 | } 295 | 296 | Err(Error::IOError(IOError::new(IOErrorKind::NotFound, "Not Found")).into()) 297 | } 298 | 299 | /// Resolve a path as a directory, using the "main" key from a package.json file if it 300 | /// exists, or resolving to the index.EXT file if it exists. 301 | fn resolve_as_directory(&self, path: &Path) -> Result { 302 | if !path.is_dir() { 303 | return Err(Error::IOError(IOError::new(IOErrorKind::NotFound, "Not Found")).into()); 304 | } 305 | 306 | // 1. If X/package.json is a file, use it. 307 | let pkg_path = path.join("package.json"); 308 | if pkg_path.is_file() { 309 | let main = self.resolve_package_main(&pkg_path); 310 | if main.is_ok() { 311 | return main; 312 | } 313 | } 314 | 315 | // 2. LOAD_INDEX(X) 316 | self.resolve_index(path) 317 | } 318 | 319 | /// Resolve using the package.json "main" key. 320 | fn resolve_package_main(&self, pkg_path: &Path) -> Result { 321 | let pkg_dir = pkg_path.parent().unwrap_or_else(|| Path::new(ROOT)); 322 | let file = File::open(pkg_path).map_err(Error::IOError)?; 323 | let pkg: Value = serde_json::from_reader(file).map_err(Error::JSONError)?; 324 | if !pkg.is_object() { 325 | return Err(RecoverableError::NonObjectPackageJson.into()); 326 | } 327 | 328 | let main_field = self 329 | .main_fields 330 | .iter() 331 | .find(|name| pkg[name].is_string()) 332 | .and_then(|name| pkg[name].as_str()); 333 | match main_field { 334 | Some(target) => { 335 | let path = pkg_dir.join(target); 336 | self.resolve_as_file(&path) 337 | .or_else(|_| self.resolve_as_directory(&path)) 338 | } 339 | None => { 340 | Err(RecoverableError::MissingMain.into()) 341 | } 342 | } 343 | } 344 | 345 | /// Resolve a directory to its index.EXT. 346 | fn resolve_index(&self, path: &Path) -> Result { 347 | // 1. If X/index.js is a file, load X/index.js as JavaScript text. 348 | // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. 349 | // 3. If X/index.node is a file, load X/index.node as binary addon. 350 | for ext in self.extensions.iter() { 351 | let ext_path = path.join(format!("index{}", ext)); 352 | if ext_path.is_file() { 353 | return Ok(ext_path); 354 | } 355 | } 356 | 357 | Err(Error::IOError(IOError::new( 358 | IOErrorKind::NotFound, 359 | "Not Found", 360 | )).into()) 361 | } 362 | 363 | /// Resolve by walking up node_modules folders. 364 | fn resolve_node_modules(&self, target: &str) -> Result { 365 | let basedir = self.get_basedir()?; 366 | let node_modules = basedir.join("node_modules"); 367 | if node_modules.is_dir() { 368 | let path = node_modules.join(target); 369 | let result = self 370 | .resolve_as_file(&path) 371 | .or_else(|_| self.resolve_as_directory(&path)); 372 | if result.is_ok() { 373 | return result; 374 | } 375 | } 376 | 377 | match basedir.parent() { 378 | Some(parent) => self 379 | .with_basedir(parent.to_path_buf()) 380 | .resolve_node_modules(target), 381 | None => Err(Error::IOError(IOError::new( 382 | IOErrorKind::NotFound, 383 | "Not Found", 384 | )).into()), 385 | } 386 | } 387 | } 388 | 389 | /// Remove excess components like `/./` and `/../` from a `Path`. 390 | fn normalize_path(p: &Path) -> PathBuf { 391 | let mut normalized = PathBuf::from("/"); 392 | for part in p.components() { 393 | match part { 394 | PathComponent::Prefix(ref prefix) => { 395 | normalized.push(prefix.as_os_str()); 396 | } 397 | PathComponent::RootDir => { 398 | normalized.push("/"); 399 | } 400 | PathComponent::ParentDir => { 401 | normalized.pop(); 402 | } 403 | PathComponent::CurDir => { 404 | // Nothing 405 | } 406 | PathComponent::Normal(name) => { 407 | normalized.push(name); 408 | } 409 | } 410 | } 411 | normalized 412 | } 413 | 414 | fn normalize_extensions(extensions: T) -> Vec 415 | where 416 | T: IntoIterator, 417 | T::Item: ToString, 418 | { 419 | extensions 420 | .into_iter() 421 | .map(|ext| ext.to_string()) 422 | .map(|ext| { 423 | if ext.starts_with('.') { 424 | ext 425 | } else { 426 | format!(".{}", ext) 427 | } 428 | }) 429 | .collect() 430 | } 431 | 432 | /// Check if a string references a core module, such as "events". 433 | pub fn is_core_module(target: &str) -> bool { 434 | BUILTINS.iter().any(|builtin| builtin == &target) 435 | } 436 | 437 | /// Resolve a node.js module path relative to the current working directory. 438 | /// Returns the absolute path to the module, or an error. 439 | /// 440 | /// ```rust 441 | /// match resolve("./lib") { 442 | /// Ok(path) => println!("Path is: {:?}", path), 443 | /// Err(err) => panic!("Failed: {:?}", err), 444 | /// } 445 | /// ``` 446 | pub fn resolve(target: &str) -> Result { 447 | Resolver::default() 448 | .with_basedir(PathBuf::from(".")) 449 | .resolve(target) 450 | } 451 | 452 | /// Resolve a node.js module path relative to `basedir`. 453 | /// Returns the absolute path to the module, or an error. 454 | /// 455 | /// ```rust 456 | /// match resolve_from("./index.js", env::current_dir().unwrap()) { 457 | /// Ok(path) => println!("Path is: {:?}", path), 458 | /// Err(err) => panic!("Failed: {:?}", err), 459 | /// } 460 | /// ``` 461 | pub fn resolve_from(target: &str, basedir: PathBuf) -> Result { 462 | Resolver::default().with_basedir(basedir).resolve(target) 463 | } 464 | 465 | #[cfg(test)] 466 | mod tests { 467 | use std::env; 468 | use std::path::PathBuf; 469 | use super::*; 470 | 471 | fn fixture(part: &str) -> PathBuf { 472 | env::current_dir().unwrap().join("fixtures").join(part) 473 | } 474 | fn resolve_fixture(target: &str) -> PathBuf { 475 | resolve_from(target, fixture("")).unwrap() 476 | } 477 | 478 | #[test] 479 | fn appends_extensions() { 480 | assert_eq!( 481 | fixture("extensions/js-file.js"), 482 | resolve_fixture("./extensions/js-file") 483 | ); 484 | assert_eq!( 485 | fixture("extensions/json-file.json"), 486 | resolve_fixture("./extensions/json-file") 487 | ); 488 | assert_eq!( 489 | fixture("extensions/native-file.node"), 490 | resolve_fixture("./extensions/native-file") 491 | ); 492 | assert_eq!( 493 | fixture("extensions/other-file.ext"), 494 | resolve_fixture("./extensions/other-file.ext") 495 | ); 496 | assert_eq!( 497 | fixture("extensions/no-ext"), 498 | resolve_fixture("./extensions/no-ext") 499 | ); 500 | assert_eq!( 501 | fixture("extensions/other-file.ext"), 502 | Resolver::default() 503 | .extensions(&[".ext"]) 504 | .with_basedir(fixture("")) 505 | .resolve("./extensions/other-file") 506 | .unwrap() 507 | ); 508 | assert_eq!( 509 | fixture("extensions/module.mjs"), 510 | Resolver::default() 511 | .extensions(&[".mjs"]) 512 | .with_basedir(fixture("")) 513 | .resolve("./extensions/module") 514 | .unwrap() 515 | ); 516 | } 517 | 518 | #[test] 519 | fn resolves_package_json() { 520 | assert_eq!( 521 | fixture("package-json/main-file/whatever.js"), 522 | resolve_fixture("./package-json/main-file") 523 | ); 524 | assert_eq!( 525 | fixture("package-json/main-file-noext/whatever.js"), 526 | resolve_fixture("./package-json/main-file-noext") 527 | ); 528 | assert_eq!( 529 | fixture("package-json/main-dir/subdir/index.js"), 530 | resolve_fixture("./package-json/main-dir") 531 | ); 532 | assert_eq!( 533 | fixture("package-json/not-object/index.js"), 534 | resolve_fixture("./package-json/not-object") 535 | ); 536 | assert_eq!( 537 | fixture("package-json/invalid/index.js"), 538 | resolve_fixture("./package-json/invalid") 539 | ); 540 | assert_eq!( 541 | fixture("package-json/main-none/index.js"), 542 | resolve_fixture("./package-json/main-none") 543 | ); 544 | assert_eq!( 545 | fixture("package-json/main-file/whatever.js"), 546 | Resolver::default() 547 | .main_fields(&["module", "main"]) 548 | .with_basedir(fixture("")) 549 | .resolve("./package-json/main-file") 550 | .unwrap() 551 | ); 552 | assert_eq!( 553 | fixture("package-json/module/index.mjs"), 554 | Resolver::default() 555 | .extensions(&[".mjs", ".js"]) 556 | .main_fields(&["module", "main"]) 557 | .with_basedir(fixture("")) 558 | .resolve("./package-json/module") 559 | .unwrap() 560 | ); 561 | assert_eq!( 562 | fixture("package-json/module-main/main.mjs"), 563 | Resolver::default() 564 | .extensions(&[".mjs", ".js"]) 565 | .main_fields(&["module", "main"]) 566 | .with_basedir(fixture("")) 567 | .resolve("./package-json/module-main") 568 | .unwrap() 569 | ); 570 | } 571 | 572 | #[test] 573 | fn resolves_node_modules() { 574 | assert_eq!( 575 | fixture("node-modules/same-dir/node_modules/a.js"), 576 | resolve_from("a", fixture("node-modules/same-dir")).unwrap() 577 | ); 578 | assert_eq!( 579 | fixture("node-modules/parent-dir/node_modules/a/index.js"), 580 | resolve_from("a", fixture("node-modules/parent-dir/src")).unwrap() 581 | ); 582 | assert_eq!( 583 | fixture("node-modules/package-json/node_modules/dep/lib/index.js"), 584 | resolve_from("dep", fixture("node-modules/package-json")).unwrap() 585 | ); 586 | assert_eq!( 587 | fixture("node-modules/walk/src/node_modules/not-ok/index.js"), 588 | resolve_from("not-ok", fixture("node-modules/walk/src")).unwrap() 589 | ); 590 | assert_eq!( 591 | fixture("node-modules/walk/node_modules/ok/index.js"), 592 | resolve_from("ok", fixture("node-modules/walk/src")).unwrap() 593 | ); 594 | } 595 | 596 | #[test] 597 | fn preserves_symlinks() { 598 | assert_eq!( 599 | fixture("symlink/node_modules/dep/main.js"), 600 | Resolver::default() 601 | .preserve_symlinks(true) 602 | .with_basedir(fixture("symlink")) 603 | .resolve("dep") 604 | .unwrap() 605 | ); 606 | } 607 | 608 | #[test] 609 | fn does_not_preserve_symlinks() { 610 | assert_eq!( 611 | fixture("symlink/linked/main.js"), 612 | Resolver::default() 613 | .preserve_symlinks(false) 614 | .with_basedir(fixture("symlink")) 615 | .resolve("dep") 616 | .unwrap() 617 | ); 618 | } 619 | 620 | #[test] 621 | fn resolves_absolute_specifier() { 622 | let full_path = fixture("extensions/js-file"); 623 | let id = full_path.to_str().unwrap(); 624 | assert_eq!(fixture("extensions/js-file.js"), resolve(id).unwrap()); 625 | } 626 | 627 | #[test] 628 | fn core_modules() { 629 | assert!(is_core_module("events")); 630 | assert!(!is_core_module("events/")); 631 | assert!(!is_core_module("./events")); 632 | assert!(is_core_module("stream")); 633 | assert!(!is_core_module("acorn")); 634 | } 635 | } 636 | --------------------------------------------------------------------------------