Repository: oxc-project/oxc-resolver
Pull request #228 (2024-07-15 14:10:20)

fix: resolve module `ipaddr.js` correctly when `extensionAlias` is provided
closes #227

Managed to figure this out after stepping through https://github.com/webpack/enhanced-resolve/blob/main/lib/ExtensionAliasPlugin.js

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/oxc-project/oxc-resolver/branches/fix-extension-alias)

### Merging #228 will **not alter performance**

<sub>Comparing <code>fix-extension-alias</code> (f8ff397) with <code>main</code> (312d612)</sub>



### Summary

`✅ 3` untouched benchmarks





codecov[bot]: ## [Codecov](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/228?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.97%. Comparing base [(`312d612`)](https://app.codecov.io/gh/oxc-project/oxc-resolver/commit/312d612c82f53ee02b653e77f6953da6bee71d7e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project) to head [(`f8ff397`)](https://app.codecov.io/gh/oxc-project/oxc-resolver/commit/f8ff39771e93c218f819fcb2ccaffc98469841f4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #228      +/-   ##
==========================================
+ Coverage   96.95%   96.97%   +0.01%     
==========================================
  Files          11       11              
  Lines        2297     2312      +15     
==========================================
+ Hits         2227     2242      +15     
  Misses         70       70              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/228?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project).


Patch:
diff --git a/examples/resolver.rs b/examples/resolver.rs
index 9209e7e..813a2d1 100644
--- a/examples/resolver.rs
+++ b/examples/resolver.rs
@@ -18,7 +18,7 @@ fn main() {
         alias_fields: vec![vec!["browser".into()]],
         alias: vec![("asdf".into(), vec![AliasValue::from("./test.js")])],
         extensions: vec![".js".into(), ".ts".into()],
-        extension_alias: vec![(".js".into(), vec![".ts".into()])],
+        extension_alias: vec![(".js".into(), vec![".ts".into(), ".js".into()])],
         ..ResolveOptions::default()
     };
 
diff --git a/fixtures/pnpm/package.json b/fixtures/pnpm/package.json
index 78712e2..b763d74 100644
--- a/fixtures/pnpm/package.json
+++ b/fixtures/pnpm/package.json
@@ -4,7 +4,8 @@
   "private": true,
   "devDependencies": {
     "axios": "1.6.2",
-    "styled-components": "6.1.1",
-    "postcss": "8.4.33"
+    "ipaddr.js": "2.2.0",
+    "postcss": "8.4.33",
+    "styled-components": "6.1.1"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5327948..a56efb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,9 @@ importers:
       axios:
         specifier: 1.6.2
         version: 1.6.2
+      ipaddr.js:
+        specifier: 2.2.0
+        version: 2.2.0
       postcss:
         specifier: 8.4.33
         version: 8.4.33
@@ -895,6 +898,10 @@ packages:
     resolution: {integrity: sha512-XgthhRIn0Ci9JdGJpUo2EtpPfaczbooZbGTN+FTzSCyUb7YHJcPPnuSXfeG5903bJMy3OyEoVTQMnvO4Ly5tFg==}
     engines: {node: '>=18'}
 
+  ipaddr.js@2.2.0:
+    resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
+    engines: {node: '>= 10'}
+
   irregular-plurals@3.5.0:
     resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==}
     engines: {node: '>=8'}
@@ -2195,6 +2202,8 @@ snapshots:
       run-async: 3.0.0
       rxjs: 7.8.1
 
+  ipaddr.js@2.2.0: {}
+
   irregular-plurals@3.5.0: {}
 
   is-extglob@2.1.1: {}
diff --git a/src/error.rs b/src/error.rs
index 901e67f..f9676c9 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -43,8 +43,8 @@ pub enum ResolveError {
     Builtin(String),
 
     /// All of the aliased extension are not found
-    #[error("All of the aliased extension are not found")]
-    ExtensionAlias,
+    #[error("All of the aliased extensions are not found for {0}")]
+    ExtensionAlias(PathBuf),
 
     /// The provided path specifier cannot be parsed
     #[error("{0}")]
diff --git a/src/lib.rs b/src/lib.rs
index cfe6c25..1895aed 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -510,9 +510,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
         // 2. If X.js is a file, load X.js as JavaScript text. STOP
         // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP
         // 4. If X.node is a file, load X.node as binary addon. STOP
-        if let Some(path) =
-            self.load_extensions(cached_path.path(), &self.options.extensions, ctx)?
-        {
+        if let Some(path) = self.load_extensions(cached_path, &self.options.extensions, ctx)? {
             return Ok(Some(path));
         }
         Ok(None)
@@ -571,11 +569,16 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
         Ok(None)
     }
 
-    fn load_extensions(&self, path: &Path, extensions: &[String], ctx: &mut Ctx) -> ResolveResult {
+    fn load_extensions(
+        &self,
+        path: &CachedPath,
+        extensions: &[String],
+        ctx: &mut Ctx,
+    ) -> ResolveResult {
         if ctx.fully_specified {
             return Ok(None);
         }
-        let path = path.as_os_str();
+        let path = path.path().as_os_str();
         for extension in extensions {
             let mut path_with_extension = path.to_os_string();
             path_with_extension.reserve_exact(extension.len());
@@ -637,9 +640,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
             // 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
             // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
             // 3. If X/index.node is a file, load X/index.node as binary addon. STOP
-            if let Some(path) =
-                self.load_extensions(cached_path.path(), &self.options.extensions, ctx)?
-            {
+            if let Some(path) = self.load_extensions(&cached_path, &self.options.extensions, ctx)? {
                 return Ok(Some(path));
             }
         }
@@ -972,7 +973,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
         Ok(None)
     }
 
-    /// Given an extension alias map `{".js": [".ts", "js"]}`,
+    /// Given an extension alias map `{".js": [".ts", ".js"]}`,
     /// load the mapping instead of the provided extension
     ///
     /// This is an enhanced-resolve feature
@@ -996,11 +997,24 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
             return Ok(None);
         };
         let path = cached_path.path().with_extension("");
-        ctx.with_fully_specified(false);
-        if let Some(path) = self.load_extensions(&path, extensions, ctx)? {
-            return Ok(Some(path));
+        let path = path.as_os_str();
+        ctx.with_fully_specified(true);
+        for extension in extensions {
+            let mut path_with_extension = path.to_os_string();
+            path_with_extension.reserve_exact(extension.len());
+            path_with_extension.push(extension);
+            let cached_path = self.cache.value(Path::new(&path_with_extension));
+            // Bail if path is module directory such as `ipaddr.js`
+            if cached_path.is_dir(&self.cache.fs, ctx) {
+                ctx.with_fully_specified(false);
+                return Ok(None);
+            }
+            if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
+                ctx.with_fully_specified(false);
+                return Ok(Some(path));
+            }
         }
-        Err(ResolveError::ExtensionAlias)
+        Err(ResolveError::ExtensionAlias(cached_path.to_path_buf()))
     }
 
     /// enhanced-resolve: RootsPlugin


Test patch:
diff --git a/src/tests/exports_field.rs b/src/tests/exports_field.rs
index fd68c1f..7ca73fb 100644
--- a/src/tests/exports_field.rs
+++ b/src/tests/exports_field.rs
@@ -269,7 +269,7 @@ fn extension_alias_throw_error() {
     let fail = [
         // enhanced-resolve has two test cases that are exactly the same here
         // https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/test/exportsField.test.js#L2976-L3024
-        ("should throw error with the `extensionAlias` option", f, "pkg/string.js", ResolveError::ExtensionAlias),
+        ("should throw error with the `extensionAlias` option", f.clone(), "pkg/string.js", ResolveError::ExtensionAlias(f.join("node_modules/pkg/dist/string.js"))),
         // TODO: The error is PackagePathNotExported in enhanced-resolve
         // ("should throw error with the `extensionAlias` option", f.clone(), "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.ts".to_string())),
     ];
diff --git a/src/tests/extension_alias.rs b/src/tests/extension_alias.rs
index 86afab7..cc6a906 100644
--- a/src/tests/extension_alias.rs
+++ b/src/tests/extension_alias.rs
@@ -31,12 +31,16 @@ fn extension_alias() {
 
     #[rustfmt::skip]
     let fail = [
-        ("should not allow to fallback to the original extension or add extensions", f, "./index.mjs"),
+        ("should not allow to fallback to the original extension or add extensions", f.clone(), "./index.mjs"),
     ];
 
     for (comment, path, request) in fail {
         let resolution = resolver.resolve(&path, request);
-        assert_eq!(resolution, Err(ResolveError::ExtensionAlias), "{comment} {path:?} {request}");
+        assert_eq!(
+            resolution,
+            Err(ResolveError::ExtensionAlias(f.join(request))),
+            "{comment} {path:?} {request}"
+        );
     }
 }
 
diff --git a/tests/resolve_test.rs b/tests/resolve_test.rs
index 30d1cef..45934cf 100644
--- a/tests/resolve_test.rs
+++ b/tests/resolve_test.rs
@@ -102,3 +102,31 @@ fn postcss() {
     let resolution = resolver.resolve(&module_path, "./lib/terminal-highlight");
     assert_eq!(resolution, Err(ResolveError::Ignored(module_path.join("lib/terminal-highlight"))));
 }
+
+#[test]
+fn ipaddr_js() {
+    let dir = dir();
+    let path = dir.join("fixtures/pnpm");
+    let module_path =
+        dir.join("node_modules/.pnpm/ipaddr.js@2.2.0/node_modules/ipaddr.js/lib/ipaddr.js");
+
+    let resolvers = [
+        // with `extension_alias`
+        Resolver::new(ResolveOptions {
+            extension_alias: vec![(".js".into(), vec![".js".into(), ".ts".into(), ".tsx".into()])],
+            ..ResolveOptions::default()
+        }),
+        // with `extensions` should still resolve to module main
+        Resolver::new(ResolveOptions {
+            extensions: vec![(".ts".into())],
+            ..ResolveOptions::default()
+        }),
+        // default
+        Resolver::default(),
+    ];
+
+    for resolver in resolvers {
+        let resolution = resolver.resolve(&path, "ipaddr.js").map(|r| r.full_path());
+        assert_eq!(resolution, Ok(module_path.clone()));
+    }
+}