From 53242c0e512dfdde6c8a1ab6b559971989c846a2 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 23 Jul 2023 17:57:03 +0800 Subject: [PATCH] refactor(resolver): improve how browser field is resolved (#589) --- crates/oxc_resolver/src/cache.rs | 4 ++ crates/oxc_resolver/src/lib.rs | 79 ++++++++++++++++++------- crates/oxc_resolver/src/package_json.rs | 2 +- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/crates/oxc_resolver/src/cache.rs b/crates/oxc_resolver/src/cache.rs index e3cc6941b..c92252a6a 100644 --- a/crates/oxc_resolver/src/cache.rs +++ b/crates/oxc_resolver/src/cache.rs @@ -29,6 +29,10 @@ impl Cache { self.cache_value(path).meta(&self.fs).is_some_and(|m| m.is_file) } + pub fn dirname(&self, path: &Path) -> PathBuf { + (if self.is_file(path) { path.parent().unwrap() } else { path }).to_path_buf() + } + pub fn canonicalize(&self, path: &Path) -> Option { self.cache_value(path).symlink(&self.fs) } diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index af11134c3..a039c13aa 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -29,6 +29,7 @@ pub use crate::{ resolution::Resolution, }; use crate::{ + package_json::PackageJson, path::PathUtil, request::{Request, RequestPath}, }; @@ -145,12 +146,13 @@ impl ResolverGeneric { } fn require_path(&self, path: &Path, request_str: &str) -> Result { + let dirname = self.cache.dirname(path); // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) - if let Some(path) = self.load_package_self(path, request_str)? { + if let Some(path) = self.load_package_self(&dirname, request_str)? { return Ok(path); } // 6. LOAD_NODE_MODULES(X, dirname(Y)) - if let Some(path) = self.load_node_modules(path, request_str)? { + if let Some(path) = self.load_node_modules(&dirname, request_str)? { return Ok(path); } if let Some(path) = self.load_as_file(&path.join(request_str))? { @@ -188,8 +190,14 @@ impl ResolverGeneric { } #[allow(clippy::unnecessary_wraps)] - fn load_index(&self, path: &Path) -> ResolveState { + fn load_index(&self, path: &Path, package_json: Option<&PackageJson>) -> ResolveState { for main_field in &self.options.main_files { + if let Some(package_json) = package_json { + if let Some(path) = self.load_browser_field(path, main_field, package_json)? { + return Ok(Some(path)); + } + } + let main_path = path.join(main_field); if self.options.enforce_extension == Some(false) && self.cache.is_file(&main_path) { return Ok(Some(main_path)); @@ -223,17 +231,23 @@ impl ResolverGeneric { return Ok(Some(path)); } // e. LOAD_INDEX(M) - if let Some(path) = self.load_index(&main_field_path)? { + if let Some(path) = + self.load_index(&main_field_path, Some(package_json.as_ref()))? + { return Ok(Some(path)); } // f. LOAD_INDEX(X) DEPRECATED // g. THROW "not found" return Err(ResolveError::NotFound(main_field_path.into_boxed_path())); } + + if let Some(path) = self.load_index(path, Some(package_json.as_ref()))? { + return Ok(Some(path)); + } } } // 2. LOAD_INDEX(X) - self.load_index(path) + self.load_index(path, None) } fn load_node_modules(&self, start: &Path, request_str: &str) -> ResolveState { @@ -241,21 +255,20 @@ impl ResolverGeneric { let dirs = self.node_module_paths(start); // 2. for each DIR in DIRS: for node_module_path in dirs { - let node_module_path = node_module_path.join(request_str); - for main_file in &self.options.main_files { - if let Some(path) = self.load_package_self(&node_module_path, main_file)? { - return Ok(Some(path)); - } - } // a. LOAD_PACKAGE_EXPORTS(X, DIR) + if let Some(path) = self.load_package_exports(&node_module_path, request_str)? { + return Ok(Some(path)); + } + + let node_module_file = node_module_path.join(request_str); // b. LOAD_AS_FILE(DIR/X) if !request_str.ends_with('/') { - if let Some(path) = self.load_as_file(&node_module_path)? { + if let Some(path) = self.load_as_file(&node_module_file)? { return Ok(Some(path)); } } // c. LOAD_AS_DIRECTORY(DIR/X) - if let Some(path) = self.load_as_directory(&node_module_path)? { + if let Some(path) = self.load_as_directory(&node_module_file)? { return Ok(Some(path)); } } @@ -267,23 +280,49 @@ impl ResolverGeneric { .flat_map(|path| self.options.modules.iter().map(|module| path.join(module))) } + #[allow(clippy::unnecessary_wraps, clippy::unused_self)] + fn load_package_exports(&self, _path: &Path, _request_str: &str) -> ResolveState { + // 1. Try to interpret X as a combination of NAME and SUBPATH where the name + // may have a @scope/ prefix and the subpath begins with a slash (`/`). + // 2. If X does not match this pattern or DIR/NAME/package.json is not a file, + // return. + // 3. Parse DIR/NAME/package.json, and look for "exports" field. + // 4. If "exports" is null or undefined, return. + // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, + // `package.json` "exports", ["node", "require"]) defined in the ESM resolver. + // 6. RESOLVE_ESM_MATCH(MATCH) + Ok(None) + } + /// # Panics /// /// * Parent of package.json is None fn load_package_self(&self, path: &Path, request_str: &str) -> ResolveState { if let Some(package_json) = self.cache.find_package_json(path)? { - if let Some(request_str) = - package_json.resolve_request(path, request_str, &self.options.extensions)? - { - let request = Request::parse(request_str).map_err(ResolveError::Request)?; - debug_assert!(package_json.path.file_name().is_some_and(|x| x == "package.json")); - // TODO: Do we need to pass query and fragment? - return self.require(package_json.path.parent().unwrap(), &request).map(Some); + if let Some(path) = self.load_browser_field(path, request_str, &package_json)? { + return Ok(Some(path)); } } Ok(None) } + fn load_browser_field( + &self, + path: &Path, + request_str: &str, + package_json: &PackageJson, + ) -> ResolveState { + if let Some(request_str) = + package_json.resolve(path, request_str, &self.options.extensions)? + { + let request = Request::parse(request_str).map_err(ResolveError::Request)?; + debug_assert!(package_json.path.file_name().is_some_and(|x| x == "package.json")); + // TODO: Do we need to pass query and fragment? + return self.require(package_json.path.parent().unwrap(), &request).map(Some); + } + Ok(None) + } + fn load_alias(&self, path: &Path, request_str: &str, alias: &Alias) -> ResolveState { for (alias, requests) in alias { let exact_match = alias.strip_prefix(request_str).is_some_and(|c| c == "$"); diff --git a/crates/oxc_resolver/src/package_json.rs b/crates/oxc_resolver/src/package_json.rs index 5c3bf743d..5caa05d90 100644 --- a/crates/oxc_resolver/src/package_json.rs +++ b/crates/oxc_resolver/src/package_json.rs @@ -35,7 +35,7 @@ impl PackageJson { /// # Errors /// /// * Returns [ResolveError::Ignored] for `"path": false` in `browser` field. - pub fn resolve_request( + pub fn resolve( &self, path: &Path, request_str: &str,