From 05c9b5dfd84b378582b0988da923a7e0c85ba5f7 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 16 Jul 2023 16:39:51 +0800 Subject: [PATCH] feat(resolver): implement extension_alias (#556) --- crates/oxc_resolver/README.md | 4 +- crates/oxc_resolver/src/error.rs | 7 +- crates/oxc_resolver/src/lib.rs | 32 +++++++- crates/oxc_resolver/src/options.rs | 43 ++++++++--- .../enhanced_resolve/test/extension_alias.rs | 77 +++++++++++++++++++ .../tests/enhanced_resolve/test/extensions.rs | 3 +- .../tests/enhanced_resolve/test/mod.rs | 1 + .../tests/enhanced_resolve/test/resolve.rs | 2 +- .../tests/enhanced_resolve/test/simple.rs | 14 ++-- .../oxc_resolver/tests/error_handling/mod.rs | 2 +- 10 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 crates/oxc_resolver/tests/enhanced_resolve/test/extension_alias.rs diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 3fa8032a6..dac90e7fb 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -6,7 +6,7 @@ |------|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- | | | alias | [] | A list of module alias configurations or an object which maps key to value | | | aliasFields | [] | A list of alias fields in description files | -| | extensionAlias | {} | An object which maps extension to extension aliases | +| ✅ | extensionAlias | {} | An object which maps extension to extension aliases | | | cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties. | | | cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key | | | conditionNames | [] | A list of exports field condition names | @@ -40,7 +40,7 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve - [ ] browserField.test.js - [ ] dependencies.test.js - [ ] exportsField.test.js -- [ ] extension-alias.test.js +- [x] extension-alias.test.js - [x] extensions.test.js - [ ] fallback.test.js - ~[ ] forEachBail.test.js~ diff --git a/crates/oxc_resolver/src/error.rs b/crates/oxc_resolver/src/error.rs index 916e99c5f..e1d2bf694 100644 --- a/crates/oxc_resolver/src/error.rs +++ b/crates/oxc_resolver/src/error.rs @@ -5,8 +5,9 @@ use crate::request::RequestError; #[derive(Debug, Eq, PartialEq)] pub enum ResolveError { NotFound, - RequestError(RequestError), - JSONError(JSONError), + Request(RequestError), + ExtensionAlias, + JSON(JSONError), } #[derive(Debug, Eq, PartialEq)] @@ -19,7 +20,7 @@ pub struct JSONError { impl ResolveError { pub(crate) fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self { - Self::JSONError(JSONError { + Self::JSON(JSONError { path, message: error.to_string(), line: error.line(), diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index 034955423..23c783d6d 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -12,6 +12,7 @@ mod path; mod request; use std::{ + ffi::OsStr, fs, path::{Path, PathBuf}, }; @@ -85,7 +86,7 @@ impl Resolver { path: P, request: &str, ) -> Result { - let request = Request::parse(request).map_err(ResolveError::RequestError)?; + let request = Request::parse(request).map_err(ResolveError::Request)?; let path = self.require(path.as_ref(), &request)?; Ok(Resolution { path, @@ -147,6 +148,11 @@ impl Resolver { #[allow(clippy::unnecessary_wraps)] fn load_as_file(&self, path: &Path) -> ResolveState { + // enhanced-resolve feature: extension_alias + if let Some(path) = self.load_extension_alias(path)? { + return Ok(Some(path)); + } + // 1. If X is a file, load X as its file extension format. STOP if self.fs.is_file(path) { return Ok(Some(path.to_path_buf())); @@ -229,4 +235,28 @@ impl Resolver { } Ok(None) } + + /// Given an extension alias map `{".js": [".ts", "js"]}`, + /// load the mapping instead of the provided extension + /// + /// This is an enhanced-resolve feature + /// + /// # Errors + /// + /// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found + fn load_extension_alias(&self, path: &Path) -> ResolveState { + let Some(path_extension) = path.extension() else { return Ok(None) }; + let Some((_, extensions)) = + self.options.extension_alias.iter().find(|(ext, _)| OsStr::new(ext) == path_extension) + else { + return Ok(None); + }; + for extension in extensions { + let path_with_extension = path.with_extension(extension); + if self.fs.is_file(&path_with_extension) { + return Ok(Some(path_with_extension)); + } + } + Err(ResolveError::ExtensionAlias) + } } diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index 6bee61a8f..04ecd8c69 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -1,31 +1,56 @@ #[derive(Debug, Clone)] pub struct ResolveOptions { - /// A list of extensions which should be tried for - /// Default: `[".js", ".json", ".node"]` - pub extensions: Vec, + /// An object which maps extension to extension aliases + /// + /// Default `{}` + pub extension_alias: Vec<(String, Vec)>, /// Enforce that a extension from extensions must be used - /// Default: false + /// + /// Default `false` pub enforce_extension: bool, + + /// A list of extensions which should be tried for + /// + /// Default `[".js", ".json", ".node"]` + pub extensions: Vec, + + /// A list of main files in directories + /// + /// Default `["index"]` + pub main_files: Vec, } impl Default for ResolveOptions { fn default() -> Self { Self { - extensions: vec![".js".into(), ".json".into(), ".node".into()], + extension_alias: vec![], enforce_extension: false, + extensions: vec![".js".into(), ".json".into(), ".node".into()], + main_files: vec!["index".into()], } } } impl ResolveOptions { pub(crate) fn sanitize(mut self) -> Self { - // Remove the leading `.` because `Path::with_extension` does not accept the leading dot. - self.extensions = self - .extensions + self.extensions = Self::remove_leading_dots(self.extensions); + self.extension_alias = self + .extension_alias .into_iter() - .map(|ext| ext.trim_start_matches('.').to_string()) + .map(|(extension, extensions)| { + (Self::remove_leading_dot(&extension), Self::remove_leading_dots(extensions)) + }) .collect(); self } + + // Remove the leading `.` because `Path::with_extension` does not accept the dot. + fn remove_leading_dot(s: &str) -> String { + s.trim_start_matches('.').to_string() + } + + fn remove_leading_dots(v: Vec) -> Vec { + v.into_iter().map(|s| Self::remove_leading_dot(&s)).collect() + } } diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/extension_alias.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/extension_alias.rs new file mode 100644 index 000000000..0de786e2b --- /dev/null +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/extension_alias.rs @@ -0,0 +1,77 @@ +//! + +use std::path::PathBuf; + +use oxc_resolver::{ResolveError, ResolveOptions, Resolver}; + +fn fixture() -> PathBuf { + super::fixture().join("extension-alias") +} + +#[test] +fn extension_alias() -> Result<(), ResolveError> { + let options = ResolveOptions { + extensions: vec![".js".into()], + main_files: vec!["index.js".into()], + extension_alias: vec![ + (".js".into(), vec![".ts".into(), ".js".into()]), + (".mjs".into(), vec![".mts".into()]), + ], + ..ResolveOptions::default() + }; + let resolver = Resolver::new(options); + let f = fixture(); + + #[rustfmt::skip] + let pass = [ + ("should alias fully specified file", f.clone(), "./index.js", f.join("index.ts")), + ("should alias fully specified file when there are two alternatives", f.clone(), "./dir/index.js", f.join("dir/index.ts")), + ("should also allow the second alternative", f.clone(), "./dir2/index.js", f.join("dir2/index.js")), + ("should support alias option without an array", f.clone(), "./dir2/index.mjs", f.join("dir2/index.mts")), + ]; + + for (comment, path, request, expected) in pass { + let resolution = resolver.resolve(&path, request)?; + let resolved_path = resolution.path(); + assert_eq!(resolved_path, expected, "{comment} {path:?} {request}"); + } + + #[rustfmt::skip] + let fail = [ + ("should not allow to fallback to the original extension or add extensions", f, "./index.mjs"), + ]; + + for (comment, path, request) in fail { + let resolution = resolver.resolve(&path, request); + assert_eq!(resolution, Err(ResolveError::ExtensionAlias), "{comment} {path:?} {request}"); + } + + Ok(()) +} + +#[test] +// should not apply extension alias to extensions or mainFiles field +fn not_apply_to_extension_nor_main_files() -> Result<(), ResolveError> { + let options = ResolveOptions { + extensions: vec![".js".into()], + main_files: vec!["index.js".into()], + extension_alias: vec![(".js".into(), vec![])], + ..ResolveOptions::default() + }; + let resolver = Resolver::new(options); + let f = fixture(); + + #[rustfmt::skip] + let pass = [ + ("directory", f.clone(), "./dir2", f.join("dir2/index.js")), + ("file", f.clone(), "./dir2/index", f.join("dir2/index.js")), + ]; + + for (comment, path, request, expected) in pass { + let resolution = resolver.resolve(&path, request)?; + let resolved_path = resolution.path(); + assert_eq!(resolved_path, expected, "{comment} {path:?} {request}"); + } + + Ok(()) +} diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/extensions.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/extensions.rs index db3f741be..f12f3e0ef 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/extensions.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/extensions.rs @@ -74,8 +74,9 @@ fn respect_enforce_extension() { let fixture = fixture(); let options = ResolveOptions { - extensions: vec![".ts".into(), String::new(), ".js".into()], enforce_extension: false, + extensions: vec![".ts".into(), String::new(), ".js".into()], + ..ResolveOptions::default() }; let resolver = Resolver::new(options); diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs index 2c808dcef..c96ac3db3 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs @@ -1,3 +1,4 @@ +mod extension_alias; mod extensions; mod resolve; mod simple; diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/resolve.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/resolve.rs index a276ed5f7..ea8d116eb 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/resolve.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/resolve.rs @@ -3,7 +3,7 @@ use oxc_resolver::{ResolveError, Resolver}; #[test] -fn test() -> Result<(), ResolveError> { +fn resolve() -> Result<(), ResolveError> { let f = super::fixture(); let resolver = Resolver::default(); diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/simple.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/simple.rs index 09a5f402d..983ecbc6e 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/simple.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/simple.rs @@ -5,19 +5,19 @@ use std::env; use oxc_resolver::{ResolveError, Resolver}; #[test] -fn test() -> Result<(), ResolveError> { +fn simple() -> Result<(), ResolveError> { + let resolver = Resolver::default(); + // mimic `enhanced-resolve/test/simple.test.js` let dirname = env::current_dir().unwrap().join("tests/enhanced_resolve/test/"); let data = [ - (dirname.clone(), "../lib/index", "direct"), - (dirname.clone(), "..", "as directory"), - (dirname.join("../../").canonicalize().unwrap(), "./enhanced_resolve", "as module"), + ("direct", dirname.clone(), "../lib/index"), + ("as directory", dirname.clone(), ".."), + ("as module", dirname.join("../../").canonicalize().unwrap(), "./enhanced_resolve"), ]; - let resolver = Resolver::default(); - - for (path, request, comment) in data { + for (comment, path, request) in data { let resolution = resolver.resolve(&path, request)?; let resolved_path = resolution.path().canonicalize().unwrap(); let expected = dirname.join("../lib/index.js").canonicalize().unwrap(); diff --git a/crates/oxc_resolver/tests/error_handling/mod.rs b/crates/oxc_resolver/tests/error_handling/mod.rs index a8ade2645..f3aba5c4a 100644 --- a/crates/oxc_resolver/tests/error_handling/mod.rs +++ b/crates/oxc_resolver/tests/error_handling/mod.rs @@ -7,7 +7,7 @@ fn broken_json() { let dir = env::current_dir().unwrap().join("tests/error_handling/"); let resolver = Resolver::default(); let resolution = resolver.resolve(&dir, "./broken_package_json"); - let error = ResolveError::JSONError(JSONError { + let error = ResolveError::JSON(JSONError { path: dir.join("broken_package_json").join("package.json"), message: String::from("expected value at line 1 column 1"), line: 1,