Repository: oxc-project/oxc-resolver
Pull request #484: refactor!: remove `modules` options
Author: login: Boshen
Created: 2025-05-01T01:25:40Z | Merged: 2025-05-01T02:41:52Z

The only value for this option in the ecosystem is `node_modules`.

This option also slightly impacts performance.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Documentation**
  - Updated documentation to reflect the removal of the `modules` option from supported options.
- **Refactor**
  - Simplified module resolution by removing support for custom module directories, relying solely on standard `node_modules` resolution.
  - Removed configuration options, methods, and internal logic related to custom module directories.
- **Tests**
  - Updated test setups to reflect module paths rooted under `/node_modules`.
  - Removed a specific module resolution test related to custom module paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/README.md b/README.md
index 9da8ba5c..5bc13d23 100644
--- a/README.md
+++ b/README.md
@@ -179,7 +179,6 @@ See [index.d.ts](https://github.com/oxc-project/oxc-resolver/blob/main/napi/inde
 | fullySpecified   | false                     | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) |
 | mainFields       | ["main"]                  | A list of main fields in description files                                                                                                                |
 | mainFiles        | ["index"]                 | A list of main files in directories                                                                                                                       |
-| modules          | ["node_modules"]          | A list of directories to resolve modules from, can be absolute path or folder name                                                                        |
 | resolveToContext | false                     | Resolve to a context instead of a file                                                                                                                    |
 | preferRelative   | false                     | Prefer to resolve module requests as relative request and fallback to resolving as module                                                                 |
 | preferAbsolute   | false                     | Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots                                                          |
@@ -191,6 +190,7 @@ See [index.d.ts](https://github.com/oxc-project/oxc-resolver/blob/main/napi/inde
 
 | Field            | Default                     | Description                                                                                                                                   |
 | ---------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| modules          | ["node_modules"]            | A list of directories to resolve modules from, can be absolute path or folder name                                                            |
 | 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                                                                       |
 | plugins          | []                          | A list of additional resolve plugins which should be applied                                                                                  |
diff --git a/napi/index.d.ts b/napi/index.d.ts
index b240737e..1c682705 100644
--- a/napi/index.d.ts
+++ b/napi/index.d.ts
@@ -131,12 +131,6 @@ export interface NapiResolveOptions {
    * Default `["index"]`
    */
   mainFiles?: Array<string>
-  /**
-   * A list of directories to resolve modules from, can be absolute path or folder name.
-   *
-   * Default `["node_modules"]`
-   */
-  modules?: string | string[]
   /**
    * Resolve to a context instead of a file.
    *
diff --git a/napi/src/lib.rs b/napi/src/lib.rs
index ee7f87c1..39ab1a0f 100644
--- a/napi/src/lib.rs
+++ b/napi/src/lib.rs
@@ -198,7 +198,6 @@ impl ResolverFactory {
                 .map(|o| StrOrStrList(o).into())
                 .unwrap_or(default.main_fields),
             main_files: op.main_files.unwrap_or(default.main_files),
-            modules: op.modules.map(|o| StrOrStrList(o).into()).unwrap_or(default.modules),
             resolve_to_context: op.resolve_to_context.unwrap_or(default.resolve_to_context),
             prefer_relative: op.prefer_relative.unwrap_or(default.prefer_relative),
             prefer_absolute: op.prefer_absolute.unwrap_or(default.prefer_absolute),
diff --git a/napi/src/options.rs b/napi/src/options.rs
index a817461d..48e3e5f2 100644
--- a/napi/src/options.rs
+++ b/napi/src/options.rs
@@ -105,12 +105,6 @@ pub struct NapiResolveOptions {
     /// Default `["index"]`
     pub main_files: Option<Vec<String>>,
 
-    /// A list of directories to resolve modules from, can be absolute path or folder name.
-    ///
-    /// Default `["node_modules"]`
-    #[napi(ts_type = "string | string[]")]
-    pub modules: Option<StrOrStrListType>,
-
     /// Resolve to a context instead of a file.
     ///
     /// Default `false`
diff --git a/src/cache.rs b/src/cache.rs
index 6c579f6b..d1cae944 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -62,17 +62,6 @@ pub trait CachedPath: Sized {
 
     fn parent(&self) -> Option<&Self>;
 
-    fn node_modules(&self) -> Option<&Self>;
-
-    fn module_directory<C: Cache<Cp = Self>>(
-        &self,
-        module_name: &str,
-        cache: &C,
-        ctx: &mut Ctx,
-    ) -> Option<Self>;
-
-    fn cached_node_modules<C: Cache<Cp = Self>>(&self, cache: &C, ctx: &mut Ctx) -> Option<Self>;
-
     /// Find package.json of a path by traversing parent directories.
     #[allow(clippy::type_complexity)]
     fn find_package_json<C: Cache<Cp = Self>>(
diff --git a/src/fs_cache.rs b/src/fs_cache.rs
index 99db01ee..92f7ca72 100644
--- a/src/fs_cache.rs
+++ b/src/fs_cache.rs
@@ -265,7 +265,6 @@ pub struct CachedPathImpl {
     meta: OnceLock<Option<FileMetadata>>,
     canonicalized: OnceLock<Result<FsCachedPath, ResolveError>>,
     canonicalizing: AtomicU64,
-    node_modules: OnceLock<Option<FsCachedPath>>,
     package_json: OnceLock<Option<(FsCachedPath, Arc<PackageJsonSerde>)>>,
 }
 
@@ -278,7 +277,6 @@ impl CachedPathImpl {
             meta: OnceLock::new(),
             canonicalized: OnceLock::new(),
             canonicalizing: AtomicU64::new(0),
-            node_modules: OnceLock::new(),
             package_json: OnceLock::new(),
         }
     }
@@ -305,24 +303,6 @@ impl CachedPath for FsCachedPath {
         self.0.parent.as_ref()
     }
 
-    fn module_directory<C: Cache<Cp = Self>>(
-        &self,
-        module_name: &str,
-        cache: &C,
-        ctx: &mut Ctx,
-    ) -> Option<Self> {
-        let cached_path = cache.value(&self.path.join(module_name));
-        cache.is_dir(&cached_path, ctx).then_some(cached_path)
-    }
-
-    fn node_modules(&self) -> Option<&Self> {
-        self.node_modules.get().and_then(|o| o.as_ref())
-    }
-
-    fn cached_node_modules<C: Cache<Cp = Self>>(&self, cache: &C, ctx: &mut Ctx) -> Option<Self> {
-        self.node_modules.get_or_init(|| self.module_directory("node_modules", cache, ctx)).clone()
-    }
-
     /// Find package.json of a path by traversing parent directories.
     ///
     /// # Errors
diff --git a/src/lib.rs b/src/lib.rs
index 7ecf2c9c..ab67d06d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -770,73 +770,67 @@ impl<C: Cache> ResolverGeneric<C> {
 
         // 1. let DIRS = NODE_MODULES_PATHS(START)
         // 2. for each DIR in DIRS:
-        for module_name in &self.options.modules {
-            for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
-                // Skip if /path/to/node_modules does not exist
-                if !self.cache.is_dir(cached_path, ctx) {
-                    continue;
-                }
-
-                let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx)
-                else {
-                    continue;
-                };
-                // Optimize node_modules lookup by inspecting whether the package exists
-                // From LOAD_PACKAGE_EXPORTS(X, DIR)
-                // 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 (`/`).
-                if !package_name.is_empty() {
-                    let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
-                    // Try foo/node_modules/package_name
-                    if self.cache.is_dir(&cached_path, ctx) {
-                        // a. LOAD_PACKAGE_EXPORTS(X, DIR)
-                        if let Some(path) =
-                            self.load_package_exports(specifier, subpath, &cached_path, ctx)?
-                        {
-                            return Ok(Some(path));
-                        }
-                    } else {
-                        // foo/node_modules/package_name is not a directory, so useless to check inside it
-                        if !subpath.is_empty() {
-                            continue;
-                        }
-                        // Skip if the directory lead to the scope package does not exist
-                        // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
-                        if package_name.starts_with('@') {
-                            if let Some(path) = cached_path.parent() {
-                                if !self.cache.is_dir(path, ctx) {
-                                    continue;
-                                }
+        for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
+            // Skip if /path/to/node_modules does not exist
+            let cached_path = cached_path.normalize_with("node_modules", self.cache.as_ref());
+            if !self.cache.is_dir(&cached_path, ctx) {
+                continue;
+            }
+            // Optimize node_modules lookup by inspecting whether the package exists
+            // From LOAD_PACKAGE_EXPORTS(X, DIR)
+            // 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 (`/`).
+            if !package_name.is_empty() {
+                let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
+                // Try foo/node_modules/package_name
+                if self.cache.is_dir(&cached_path, ctx) {
+                    // a. LOAD_PACKAGE_EXPORTS(X, DIR)
+                    if let Some(path) =
+                        self.load_package_exports(specifier, subpath, &cached_path, ctx)?
+                    {
+                        return Ok(Some(path));
+                    }
+                } else {
+                    // foo/node_modules/package_name is not a directory, so useless to check inside it
+                    if !subpath.is_empty() {
+                        continue;
+                    }
+                    // Skip if the directory lead to the scope package does not exist
+                    // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
+                    if package_name.starts_with('@') {
+                        if let Some(path) = cached_path.parent() {
+                            if !self.cache.is_dir(path, ctx) {
+                                continue;
                             }
                         }
                     }
                 }
+            }
 
-                // Try as file or directory for all other cases
-                // b. LOAD_AS_FILE(DIR/X)
-                // c. LOAD_AS_DIRECTORY(DIR/X)
+            // Try as file or directory for all other cases
+            // b. LOAD_AS_FILE(DIR/X)
+            // c. LOAD_AS_DIRECTORY(DIR/X)
 
-                let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
+            let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
 
-                // Perf: try the directory first for package specifiers.
-                if self.options.resolve_to_context {
-                    return Ok(self.cache.is_dir(&cached_path, ctx).then(|| cached_path.clone()));
-                }
-                if self.cache.is_dir(&cached_path, ctx) {
-                    if let Some(path) = self.load_browser_field_or_alias(&cached_path, ctx)? {
-                        return Ok(Some(path));
-                    }
-                    if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
-                        return Ok(Some(path));
-                    }
-                }
-                if let Some(path) = self.load_as_file(&cached_path, ctx)? {
+            // Perf: try the directory first for package specifiers.
+            if self.options.resolve_to_context {
+                return Ok(self.cache.is_dir(&cached_path, ctx).then(|| cached_path.clone()));
+            }
+            if self.cache.is_dir(&cached_path, ctx) {
+                if let Some(path) = self.load_browser_field_or_alias(&cached_path, ctx)? {
                     return Ok(Some(path));
                 }
                 if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
                     return Ok(Some(path));
                 }
             }
+            if let Some(path) = self.load_as_file(&cached_path, ctx)? {
+                return Ok(Some(path));
+            }
+            if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
+                return Ok(Some(path));
+            }
         }
         Ok(None)
     }
@@ -880,23 +874,6 @@ impl<C: Cache> ResolverGeneric<C> {
         }
     }
 
-    fn get_module_directory(
-        &self,
-        cached_path: &C::Cp,
-        module_name: &str,
-        ctx: &mut Ctx,
-    ) -> Option<C::Cp> {
-        if module_name == "node_modules" {
-            cached_path.cached_node_modules(self.cache.as_ref(), ctx)
-        } else if cached_path.path().components().next_back()
-            == Some(Component::Normal(OsStr::new(module_name)))
-        {
-            Some(cached_path.clone())
-        } else {
-            cached_path.module_directory(module_name, self.cache.as_ref(), ctx)
-        }
-    }
-
     fn load_package_exports(
         &self,
         specifier: &str,
@@ -1339,51 +1316,49 @@ impl<C: Cache> ResolverGeneric<C> {
         self.require_core(package_name)?;
 
         // 11. While parentURL is not the file system root,
-        for module_name in &self.options.modules {
-            for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
-                // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
-                let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx)
-                else {
-                    continue;
-                };
-                // 2. Set parentURL to the parent folder URL of parentURL.
-                let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
-                // 3. If the folder at packageURL does not exist, then
-                //   1. Continue the next loop iteration.
-                if self.cache.is_dir(&cached_path, ctx) {
-                    // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
-                    if let Some((_, package_json)) =
-                        self.cache.get_package_json(&cached_path, &self.options, ctx)?
-                    {
-                        // 5. If pjson is not null and pjson.exports is not null or undefined, then
-                        // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
-                        for exports in package_json.exports_fields(&self.options.exports_fields) {
-                            if let Some(path) = self.package_exports_resolve(
-                                &cached_path,
-                                &format!(".{subpath}"),
-                                &exports,
-                                ctx,
-                            )? {
-                                return Ok(Some(path));
-                            }
+        for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
+            // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
+            let cached_path = cached_path.normalize_with("node_modules", self.cache.as_ref());
+            if !self.cache.is_dir(&cached_path, ctx) {
+                continue;
+            }
+            // 2. Set parentURL to the parent folder URL of parentURL.
+            let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
+            // 3. If the folder at packageURL does not exist, then
+            //   1. Continue the next loop iteration.
+            if self.cache.is_dir(&cached_path, ctx) {
+                // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
+                if let Some((_, package_json)) =
+                    self.cache.get_package_json(&cached_path, &self.options, ctx)?
+                {
+                    // 5. If pjson is not null and pjson.exports is not null or undefined, then
+                    // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
+                    for exports in package_json.exports_fields(&self.options.exports_fields) {
+                        if let Some(path) = self.package_exports_resolve(
+                            &cached_path,
+                            &format!(".{subpath}"),
+                            &exports,
+                            ctx,
+                        )? {
+                            return Ok(Some(path));
                         }
-                        // 6. Otherwise, if packageSubpath is equal to ".", then
-                        if subpath == "." {
-                            // 1. If pjson.main is a string, then
-                            for main_field in package_json.main_fields(&self.options.main_fields) {
-                                // 1. Return the URL resolution of main in packageURL.
-                                let cached_path =
-                                    cached_path.normalize_with(main_field, self.cache.as_ref());
-                                if self.cache.is_file(&cached_path, ctx) {
-                                    return Ok(Some(cached_path));
-                                }
+                    }
+                    // 6. Otherwise, if packageSubpath is equal to ".", then
+                    if subpath == "." {
+                        // 1. If pjson.main is a string, then
+                        for main_field in package_json.main_fields(&self.options.main_fields) {
+                            // 1. Return the URL resolution of main in packageURL.
+                            let cached_path =
+                                cached_path.normalize_with(main_field, self.cache.as_ref());
+                            if self.cache.is_file(&cached_path, ctx) {
+                                return Ok(Some(cached_path));
                             }
                         }
                     }
-                    let subpath = format!(".{subpath}");
-                    ctx.with_fully_specified(false);
-                    return self.require(&cached_path, &subpath, ctx).map(Some);
                 }
+                let subpath = format!(".{subpath}");
+                ctx.with_fully_specified(false);
+                return self.require(&cached_path, &subpath, ctx).map(Some);
             }
         }
 
diff --git a/src/options.rs b/src/options.rs
index a9c8a0b0..5dd7d8a8 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -113,11 +113,6 @@ pub struct ResolveOptions {
     /// Default `["index"]`
     pub main_files: Vec<String>,
 
-    /// A list of directories to resolve modules from, can be absolute path or folder name.
-    ///
-    /// Default `["node_modules"]`
-    pub modules: Vec<String>,
-
     /// A manifest loaded from pnp::load_pnp_manifest.
     ///
     /// Default `None`
@@ -328,22 +323,6 @@ impl ResolveOptions {
         self
     }
 
-    /// Adds a module to [ResolveOptions::modules]
-    ///
-    /// ## Examples
-    ///
-    /// ```
-    /// use oxc_resolver::{ResolveOptions};
-    ///
-    /// let options = ResolveOptions::default().with_module("module");
-    /// assert!(options.modules.contains(&"module".to_string()));
-    /// ```
-    #[must_use]
-    pub fn with_module<M: Into<String>>(mut self, module: M) -> Self {
-        self.modules.push(module.into());
-        self
-    }
-
     /// Adds a main file to [ResolveOptions::main_files]
     ///
     /// ## Examples
@@ -474,7 +453,6 @@ impl Default for ResolveOptions {
             fully_specified: false,
             main_fields: vec!["main".into()],
             main_files: vec!["index".into()],
-            modules: vec!["node_modules".into()],
             #[cfg(feature = "yarn_pnp")]
             pnp_manifest: None,
             resolve_to_context: false,
@@ -530,9 +508,6 @@ impl fmt::Display for ResolveOptions {
         if !self.main_files.is_empty() {
             write!(f, "main_files:{:?},", self.main_files)?;
         }
-        if !self.modules.is_empty() {
-            write!(f, "modules:{:?},", self.modules)?;
-        }
         if self.resolve_to_context {
             write!(f, "resolve_to_context:{:?},", self.resolve_to_context)?;
         }
@@ -607,7 +582,7 @@ mod test {
             ..ResolveOptions::default()
         };
 
-        let expected = r#"tsconfig:TsconfigOptions { config_file: "tsconfig.json", references: Auto },alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],imports_fields:[["imports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],modules:["node_modules"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#;
+        let expected = r#"tsconfig:TsconfigOptions { config_file: "tsconfig.json", references: Auto },alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],imports_fields:[["imports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#;
         assert_eq!(format!("{options}"), expected);
 
         let options = ResolveOptions {
@@ -625,7 +600,6 @@ mod test {
             imports_fields: vec![],
             main_fields: vec![],
             main_files: vec![],
-            modules: vec![],
             #[cfg(feature = "yarn_pnp")]
             pnp_manifest: None,
             prefer_absolute: false,
diff --git a/src/tests/alias.rs b/src/tests/alias.rs
index 4824f7bd..894fa9be 100644
--- a/src/tests/alias.rs
+++ b/src/tests/alias.rs
@@ -21,20 +21,20 @@ fn alias() {
     let f = Path::new("/");
 
     let file_system = MemoryFS::new(&[
-        ("/a/index", ""),
-        ("/a/dir/index", ""),
-        ("/recursive/index", ""),
-        ("/recursive/dir/index", ""),
-        ("/b/index", ""),
-        ("/b/dir/index", ""),
-        ("/c/index", ""),
-        ("/c/dir/index", ""),
-        ("/d/index.js", ""),
-        ("/d/dir/.empty", ""),
-        ("/e/index", ""),
-        ("/e/anotherDir/index", ""),
-        ("/e/dir/file", ""),
-        ("/dashed-name", ""),
+        ("/node_modules/a/index", ""),
+        ("/node_modules/a/dir/index", ""),
+        ("/node_modules/recursive/index", ""),
+        ("/node_modules/recursive/dir/index", ""),
+        ("/node_modules/b/index", ""),
+        ("/node_modules/b/dir/index", ""),
+        ("/node_modules/c/index", ""),
+        ("/node_modules/c/dir/index", ""),
+        ("/node_modules/d/index.js", ""),
+        ("/node_modules/d/dir/.empty", ""),
+        ("/node_modules/e/index", ""),
+        ("/node_modules/e/anotherDir/index", ""),
+        ("/node_modules/e/dir/file", ""),
+        ("/node_modules/dashed-name", ""),
     ]);
 
     let resolver = ResolverGeneric::new_with_cache(
@@ -43,7 +43,7 @@ fn alias() {
             alias: vec![
                 ("aliasA".into(), vec![AliasValue::from("a")]),
                 ("b$".into(), vec![AliasValue::from("a/index")]),
-                ("c$".into(), vec![AliasValue::from("/a/index")]),
+                ("c$".into(), vec![AliasValue::from("/node_modules/a/index")]),
                 (
                     "multiAlias".into(),
                     vec![
@@ -55,79 +55,82 @@ fn alias() {
                     ],
                 ),
                 ("recursive".into(), vec![AliasValue::from("recursive/dir")]),
-                ("/d/dir".into(), vec![AliasValue::from("/c/dir")]),
-                ("/d/index.js".into(), vec![AliasValue::from("/c/index")]),
-                ("#".into(), vec![AliasValue::from("/c/dir")]),
-                ("@".into(), vec![AliasValue::from("/c/dir")]),
+                ("/node_modules/d/dir".into(), vec![AliasValue::from("/node_modules/c/dir")]),
+                (
+                    "/node_modules/d/index.js".into(),
+                    vec![AliasValue::from("/node_modules/c/index")],
+                ),
+                ("#".into(), vec![AliasValue::from("/node_modules/c/dir")]),
+                ("@".into(), vec![AliasValue::from("/node_modules/c/dir")]),
                 ("ignored".into(), vec![AliasValue::Ignore]),
                 // not part of enhanced-resolve, added to make sure query in alias value would work
                 ("alias_query".into(), vec![AliasValue::from("a?query_after")]),
                 ("alias_fragment".into(), vec![AliasValue::from("a#fragment_after")]),
                 ("dash".into(), vec![AliasValue::Ignore]),
-                ("@scope/package-name/file$".into(), vec![AliasValue::from("/c/dir")]),
+                ("@scope/package-name/file$".into(), vec![AliasValue::from("/node_modules/c/dir")]),
                 // wildcard https://github.com/webpack/enhanced-resolve/pull/439
-                ("@adir/*".into(), vec![AliasValue::from("./a/")]), // added to test value without wildcard
-                ("@*".into(), vec![AliasValue::from("/*")]),
-                ("@e*".into(), vec![AliasValue::from("/e/*")]),
-                ("@e*file".into(), vec![AliasValue::from("/e*file")]),
+                ("@adir/*".into(), vec![AliasValue::from("./node_modules/a/")]), // added to test value without wildcard
+                ("@*".into(), vec![AliasValue::from("/node_modules/*")]),
+                ("@e*".into(), vec![AliasValue::from("/node_modules/e/*")]),
+                ("@e*file".into(), vec![AliasValue::from("/node_modules/e*file")]),
             ],
-            modules: vec!["/".into()],
             ..ResolveOptions::default()
         },
     );
 
     #[rustfmt::skip]
     let pass = [
-        ("should resolve a not aliased module 1", "a", "/a/index"),
-        ("should resolve a not aliased module 2", "a/index", "/a/index"),
-        ("should resolve a not aliased module 3", "a/dir", "/a/dir/index"),
-        ("should resolve a not aliased module 4", "a/dir/index", "/a/dir/index"),
-        ("should resolve an aliased module 1", "aliasA", "/a/index"),
-        ("should resolve an aliased module 2", "aliasA/index", "/a/index"),
-        ("should resolve an aliased module 3", "aliasA/dir", "/a/dir/index"),
-        ("should resolve an aliased module 4", "aliasA/dir/index", "/a/dir/index"),
-        ("should resolve '#' alias 1", "#", "/c/dir/index"),
-        ("should resolve '#' alias 2", "#/index", "/c/dir/index"),
-        ("should resolve '@' alias 1", "@", "/c/dir/index"),
-        ("should resolve '@' alias 2", "@/index", "/c/dir/index"),
-        ("should resolve '@' alias 3", "@/", "/c/dir/index"),
-        ("should resolve a recursive aliased module 1", "recursive", "/recursive/dir/index"),
-        ("should resolve a recursive aliased module 2", "recursive/index", "/recursive/dir/index"),
-        ("should resolve a recursive aliased module 3", "recursive/dir", "/recursive/dir/index"),
-        ("should resolve a recursive aliased module 4", "recursive/dir/index", "/recursive/dir/index"),
-        ("should resolve a file aliased module 1", "b", "/a/index"),
-        ("should resolve a file aliased module 2", "c", "/a/index"),
-        ("should resolve a file aliased module with a query 1", "b?query", "/a/index?query"),
-        ("should resolve a file aliased module with a query 2", "c?query", "/a/index?query"),
-        ("should resolve a path in a file aliased module 1", "b/index", "/b/index"),
-        ("should resolve a path in a file aliased module 2", "b/dir", "/b/dir/index"),
-        ("should resolve a path in a file aliased module 3", "b/dir/index", "/b/dir/index"),
-        ("should resolve a path in a file aliased module 4", "c/index", "/c/index"),
-        ("should resolve a path in a file aliased module 5", "c/dir", "/c/dir/index"),
-        ("should resolve a path in a file aliased module 6", "c/dir/index", "/c/dir/index"),
-        ("should resolve a file aliased file 1", "d", "/c/index"),
-        ("should resolve a file aliased file 2", "d/dir/index", "/c/dir/index"),
-        ("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "/e/dir/file"),
-        ("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "/e/anotherDir/index"),
+        ("should resolve a not aliased module 1", "a", "a/index"),
+        ("should resolve a not aliased module 2", "a/index", "a/index"),
+        ("should resolve a not aliased module 3", "a/dir", "a/dir/index"),
+        ("should resolve a not aliased module 4", "a/dir/index", "a/dir/index"),
+        ("should resolve an aliased module 1", "aliasA", "a/index"),
+        ("should resolve an aliased module 2", "aliasA/index", "a/index"),
+        ("should resolve an aliased module 3", "aliasA/dir", "a/dir/index"),
+        ("should resolve an aliased module 4", "aliasA/dir/index", "a/dir/index"),
+        ("should resolve '#' alias 1", "#", "c/dir/index"),
+        ("should resolve '#' alias 2", "#/index", "c/dir/index"),
+        ("should resolve '@' alias 1", "@", "c/dir/index"),
+        ("should resolve '@' alias 2", "@/index", "c/dir/index"),
+        ("should resolve '@' alias 3", "@/", "c/dir/index"),
+        ("should resolve a recursive aliased module 1", "recursive", "recursive/dir/index"),
+        ("should resolve a recursive aliased module 2", "recursive/index", "recursive/dir/index"),
+        ("should resolve a recursive aliased module 3", "recursive/dir", "recursive/dir/index"),
+        ("should resolve a recursive aliased module 4", "recursive/dir/index", "recursive/dir/index"),
+        ("should resolve a file aliased module 1", "b", "a/index"),
+        ("should resolve a file aliased module 2", "c", "a/index"),
+        ("should resolve a file aliased module with a query 1", "b?query", "a/index?query"),
+        ("should resolve a file aliased module with a query 2", "c?query", "a/index?query"),
+        ("should resolve a path in a file aliased module 1", "b/index", "b/index"),
+        ("should resolve a path in a file aliased module 2", "b/dir", "b/dir/index"),
+        ("should resolve a path in a file aliased module 3", "b/dir/index", "b/dir/index"),
+        ("should resolve a path in a file aliased module 4", "c/index", "c/index"),
+        ("should resolve a path in a file aliased module 5", "c/dir", "c/dir/index"),
+        ("should resolve a path in a file aliased module 6", "c/dir/index", "c/dir/index"),
+        ("should resolve a file aliased file 1", "d", "c/index"),
+        ("should resolve a file aliased file 2", "d/dir/index", "c/dir/index"),
+        ("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "e/dir/file"),
+        ("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "e/anotherDir/index"),
         // wildcard
-        ("should resolve wildcard alias 1", "@a", "/a/index"),
-        ("should resolve wildcard alias 2", "@a/dir", "/a/dir/index"),
-        ("should resolve wildcard alias 3", "@e/dir/file", "/e/dir/file"),
-        ("should resolve wildcard alias 4", "@e/anotherDir", "/e/anotherDir/index"),
-        ("should resolve wildcard alias 5", "@e/dir/file", "/e/dir/file"),
+        ("should resolve wildcard alias 1", "@a", "a/index"),
+        ("should resolve wildcard alias 2", "@a/dir", "a/dir/index"),
+        ("should resolve wildcard alias 3", "@e/dir/file", "e/dir/file"),
+        ("should resolve wildcard alias 4", "@e/anotherDir", "e/anotherDir/index"),
+        ("should resolve wildcard alias 5", "@e/dir/file", "e/dir/file"),
         // added to test value without wildcard
-        ("should resolve scoped package name with sub dir 1", "@adir/index", "/a/index"),
-        ("should resolve scoped package name with sub dir 2", "@adir/dir", "/a/index"),
+        ("should resolve scoped package name with sub dir 1", "@adir/index", "a/index"),
+        ("should resolve scoped package name with sub dir 2", "@adir/dir", "a/index"),
         // not part of enhanced-resolve, added to make sure query in alias value works
-        ("should resolve query in alias value", "alias_query?query_before", "/a/index?query_after"),
-        ("should resolve query in alias value", "alias_fragment#fragment_before", "/a/index#fragment_after"),
-        ("should resolve dashed name", "dashed-name", "/dashed-name"),
-        ("should resolve scoped package name with sub dir", "@scope/package-name/file", "/c/dir/index"),
+        ("should resolve query in alias value", "alias_query?query_before", "a/index?query_after"),
+        ("should resolve query in alias value", "alias_fragment#fragment_before", "a/index#fragment_after"),
+        ("should resolve dashed name", "dashed-name", "dashed-name"),
+        ("should resolve scoped package name with sub dir", "@scope/package-name/file", "c/dir/index"),
     ];
 
     for (comment, request, expected) in pass {
         let resolved_path = resolver.resolve(f, request).map(|r| r.full_path());
-        assert_eq!(resolved_path, Ok(PathBuf::from(expected)), "{comment} {request}");
+        let expected = PathBuf::from("/node_modules").join(expected);
+        assert_eq!(resolved_path, Ok(expected), "{comment} {request}");
     }
 
     #[rustfmt::skip]
@@ -174,12 +177,14 @@ fn check_slash(path: &Path) {
 fn absolute_path() {
     let f = super::fixture();
     let resolver = Resolver::new(ResolveOptions {
-        alias: vec![(f.join("foo").to_str().unwrap().to_string(), vec![AliasValue::Ignore])],
-        modules: vec![f.clone().to_str().unwrap().to_string()],
+        alias: vec![(
+            f.join("node_modules").join("m1").to_str().unwrap().to_string(),
+            vec![AliasValue::Ignore],
+        )],
         ..ResolveOptions::default()
     });
-    let resolution = resolver.resolve(&f, "foo/index");
-    assert_eq!(resolution, Err(ResolveError::Ignored(f.join("foo"))));
+    let resolution = resolver.resolve(&f, "m1/a.js");
+    assert_eq!(resolution, Err(ResolveError::Ignored(f.join("node_modules").join("m1"))));
 }
 
 #[test]
diff --git a/src/tests/dependencies.rs b/src/tests/dependencies.rs
index 9f1b6da2..d6846c45 100644
--- a/src/tests/dependencies.rs
+++ b/src/tests/dependencies.rs
@@ -12,7 +12,7 @@ mod windows {
             ("/a/b/node_modules/some-module/index.js", ""),
             ("/a/node_modules/module/package.json", r#"{"main":"entry.js"}"#),
             ("/a/node_modules/module/file.js", r#"{"main":"entry.js"}"#),
-            ("/modules/other-module/file.js", ""),
+            ("/node_modules/other-module/file.js", ""),
         ])
     }
 
@@ -24,7 +24,6 @@ mod windows {
             Arc::new(FsCache::new(file_system)),
             ResolveOptions {
                 extensions: vec![".json".into(), ".js".into()],
-                modules: vec!["/modules".into(), "node_modules".into()],
                 ..ResolveOptions::default()
             },
         );
@@ -49,44 +48,40 @@ mod windows {
                 ],
                 vec![
                     // missing package.jsons
-                    // "/a/b/c/package.json",
-                    "/a/b/package.json",
-                    "/a/package.json",
                     "/package.json",
-                    // missing modules directories
-                    "/a/b/c",
-                    // "/a/b/c/node_modules",
-                    // missing single file modules
-                    "/modules/module",
-                    "/a/b/node_modules/module",
-                    // missing files with alternative extensions
+                    "/a/package.json",
                     "/a/node_modules/module/file",
+                    "/a/b/package.json",
+                    "/a/b/node_modules/module",
                     "/a/node_modules/module/file.json",
+                    "/a/b/c/node_modules",
+                    "/a/b/c",
                 ],
             ),
             (
                 "fast found module",
                 "/a/b/c",
                 "other-module/file.js",
-                "/modules/other-module/file.js",
+                "/node_modules/other-module/file.js",
                 // These dependencies are different from enhanced-resolve due to different code path to
                 // querying the file system
                 vec![
                     // symlink checks
-                    "/modules/other-module/file.js",
+                    "/node_modules/other-module/file.js",
                     // "/modules/other-module",
                     // "/modules",
                     // "/",
                 ],
                 vec![
                     // missing package.jsons
-                    // "/a/b/c/package.json",
-                    "/a/b/c",
-                    "/a/b/package.json",
-                    "/a/package.json",
                     "/package.json",
-                    "/modules/other-module/package.json",
-                    "/modules/package.json",
+                    "/a/package.json",
+                    "/a/node_modules/other-module",
+                    "/a/b/package.json",
+                    "/a/b/node_modules/other-module",
+                    "/node_modules/other-module/package.json",
+                    "/a/b/c/node_modules",
+                    "/a/b/c",
                 ],
             ),
         ];
diff --git a/src/tests/fallback.rs b/src/tests/fallback.rs
index b5c5fb33..1be44d34 100644
--- a/src/tests/fallback.rs
+++ b/src/tests/fallback.rs
@@ -14,21 +14,21 @@ fn fallback() {
     let f = Path::new("/");
 
     let file_system = MemoryFS::new(&[
-        ("/a/index", ""),
-        ("/a/dir/index", ""),
-        ("/recursive/index", ""),
-        ("/recursive/dir/index", ""),
-        ("/recursive/dir/file", ""),
-        ("/recursive/dir/dir/index", ""),
-        ("/b/index", ""),
-        ("/b/dir/index", ""),
-        ("/c/index", ""),
-        ("/c/dir/index", ""),
-        ("/d/index.js", ""),
-        ("/d/dir/.empty", ""),
-        ("/e/index", ""),
-        ("/e/anotherDir/index", ""),
-        ("/e/dir/file", ""),
+        ("/node_modules/a/index", ""),
+        ("/node_modules/a/dir/index", ""),
+        ("/node_modules/recursive/index", ""),
+        ("/node_modules/recursive/dir/index", ""),
+        ("/node_modules/recursive/dir/file", ""),
+        ("/node_modules/recursive/dir/dir/index", ""),
+        ("/node_modules/b/index", ""),
+        ("/node_modules/b/dir/index", ""),
+        ("/node_modules/c/index", ""),
+        ("/node_modules/c/dir/index", ""),
+        ("/node_modules/d/index.js", ""),
+        ("/node_modules/d/dir/.empty", ""),
+        ("/node_modules/e/index", ""),
+        ("/node_modules/e/anotherDir/index", ""),
+        ("/node_modules/e/dir/file", ""),
     ]);
 
     let resolver = ResolverGeneric::new_with_cache(
@@ -36,8 +36,8 @@ fn fallback() {
         ResolveOptions {
             fallback: vec![
                 ("aliasA".into(), vec![AliasValue::Path("a".into())]),
-                ("b$".into(), vec![AliasValue::Path("a/index".into())]),
-                ("c$".into(), vec![AliasValue::Path("/a/index".into())]),
+                ("b$".into(), vec![AliasValue::Path("node_modules/a/index".into())]),
+                ("c$".into(), vec![AliasValue::Path("node_modules/a/index".into())]),
                 (
                     "multiAlias".into(),
                     vec![
@@ -49,46 +49,46 @@ fn fallback() {
                     ],
                 ),
                 ("recursive".into(), vec![AliasValue::Path("recursive/dir".into())]),
-                ("/d/dir".into(), vec![AliasValue::Path("/c/dir".into())]),
-                ("/d/index.js".into(), vec![AliasValue::Path("/c/index".into())]),
+                ("/d/dir".into(), vec![AliasValue::Path("/node_modules/c/dir".into())]),
+                ("/d/index.js".into(), vec![AliasValue::Path("/node_modules/c/index".into())]),
                 ("ignored".into(), vec![AliasValue::Ignore]),
                 ("node:path".into(), vec![AliasValue::Ignore]),
             ],
-            modules: vec!["/".into()],
             ..ResolveOptions::default()
         },
     );
 
     #[rustfmt::skip]
     let pass = [
-        ("should resolve a not aliased module 1", "a", "/a/index"),
-        ("should resolve a not aliased module 2", "a/index", "/a/index"),
-        ("should resolve a not aliased module 3", "a/dir", "/a/dir/index"),
-        ("should resolve a not aliased module 4", "a/dir/index", "/a/dir/index"),
-        ("should resolve an fallback module 1", "aliasA", "/a/index"),
-        ("should resolve an fallback module 2", "aliasA/index", "/a/index"),
-        ("should resolve an fallback module 3", "aliasA/dir", "/a/dir/index"),
-        ("should resolve an fallback module 4", "aliasA/dir/index", "/a/dir/index"),
-        ("should resolve a recursive aliased module 1", "recursive", "/recursive/index"),
-        ("should resolve a recursive aliased module 2", "recursive/index", "/recursive/index"),
-        ("should resolve a recursive aliased module 3", "recursive/dir", "/recursive/dir/index"),
-        ("should resolve a recursive aliased module 4", "recursive/dir/index", "/recursive/dir/index"),
-        ("should resolve a recursive aliased module 5", "recursive/file", "/recursive/dir/file"),
-        ("should resolve a file aliased module with a query 1", "b?query", "/b/index?query"),
-        ("should resolve a file aliased module with a query 2", "c?query", "/c/index?query"),
-        ("should resolve a path in a file aliased module 1", "b/index", "/b/index"),
-        ("should resolve a path in a file aliased module 2", "b/dir", "/b/dir/index"),
-        ("should resolve a path in a file aliased module 3", "b/dir/index", "/b/dir/index"),
-        ("should resolve a path in a file aliased module 4", "c/index", "/c/index"),
-        ("should resolve a path in a file aliased module 5", "c/dir", "/c/dir/index"),
-        ("should resolve a path in a file aliased module 6", "c/dir/index", "/c/dir/index"),
-        ("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "/e/dir/file"),
-        ("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "/e/anotherDir/index"),
+        ("should resolve a not aliased module 1", "a", "a/index"),
+        ("should resolve a not aliased module 2", "a/index", "a/index"),
+        ("should resolve a not aliased module 3", "a/dir", "a/dir/index"),
+        ("should resolve a not aliased module 4", "a/dir/index", "a/dir/index"),
+        ("should resolve an fallback module 1", "aliasA", "a/index"),
+        ("should resolve an fallback module 2", "aliasA/index", "a/index"),
+        ("should resolve an fallback module 3", "aliasA/dir", "a/dir/index"),
+        ("should resolve an fallback module 4", "aliasA/dir/index", "a/dir/index"),
+        ("should resolve a recursive aliased module 1", "recursive", "recursive/index"),
+        ("should resolve a recursive aliased module 2", "recursive/index", "recursive/index"),
+        ("should resolve a recursive aliased module 3", "recursive/dir", "recursive/dir/index"),
+        ("should resolve a recursive aliased module 4", "recursive/dir/index", "recursive/dir/index"),
+        ("should resolve a recursive aliased module 5", "recursive/file", "recursive/dir/file"),
+        ("should resolve a file aliased module with a query 1", "b?query", "b/index?query"),
+        ("should resolve a file aliased module with a query 2", "c?query", "c/index?query"),
+        ("should resolve a path in a file aliased module 1", "b/index", "b/index"),
+        ("should resolve a path in a file aliased module 2", "b/dir", "b/dir/index"),
+        ("should resolve a path in a file aliased module 3", "b/dir/index", "b/dir/index"),
+        ("should resolve a path in a file aliased module 4", "c/index", "c/index"),
+        ("should resolve a path in a file aliased module 5", "c/dir", "c/dir/index"),
+        ("should resolve a path in a file aliased module 6", "c/dir/index", "c/dir/index"),
+        ("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "e/dir/file"),
+        ("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "e/anotherDir/index"),
     ];
 
     for (comment, request, expected) in pass {
         let resolved_path = resolver.resolve(f, request).map(|r| r.full_path());
-        assert_eq!(resolved_path, Ok(PathBuf::from(expected)), "{comment} {request}");
+        let expected = PathBuf::from("/node_modules").join(expected);
+        assert_eq!(resolved_path, Ok(expected), "{comment} {request}");
     }
 
     #[rustfmt::skip]
diff --git a/src/tests/resolve.rs b/src/tests/resolve.rs
index 3e3ebc6a..434f15f9 100644
--- a/src/tests/resolve.rs
+++ b/src/tests/resolve.rs
@@ -64,19 +64,6 @@ fn resolve() {
     }
 }
 
-#[test]
-fn issue238_resolve() {
-    let f = super::fixture().join("issue-238");
-    let resolver = Resolver::new(ResolveOptions {
-        extensions: vec![".js".into(), ".jsx".into(), ".ts".into(), ".tsx".into()],
-        modules: vec!["src/a".into(), "src/b".into(), "src/common".into(), "node_modules".into()],
-        ..ResolveOptions::default()
-    });
-    let resolved_path =
-        resolver.resolve(f.join("src/common"), "config/myObjectFile").map(|r| r.full_path());
-    assert_eq!(resolved_path, Ok(f.join("src/common/config/myObjectFile.js")),);
-}
-
 #[test]
 fn prefer_relative() {
     let f = super::fixture();
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index bd2e7739..de7c57d3 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -108,7 +108,6 @@ fn options_api() {
         .with_fully_specified(true)
         .with_main_field("asdf")
         .with_main_file("main")
-        .with_module("module")
         .with_prefer_absolute(true)
         .with_prefer_relative(true)
         .with_root(PathBuf::new())


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/fs_cache.rs (1)</summary><blockquote>

`313-318`: **Minor inefficiency & double `is_dir` call**

Inside `cached_node_modules` we already call `cache.is_dir(&cached_path, ctx)` before returning the directory.  
The extra `self.cache.is_dir(cached_path, ctx)` guard executed in the caller (`load_node_modules`) therefore checks the parent directory twice on the hot-path.

Consider eliminating one of the checks to shave a hash-lookup per iteration:

```diff
-            if !self.cache.is_dir(cached_path, ctx) {
-                continue;
-            }
-
-            let Some(cached_path) = cached_path.cached_node_modules(self.cache.as_ref(), ctx) else {
+            let Some(cached_path) = cached_path.cached_node_modules(self.cache.as_ref(), ctx) else {
                 continue;
             };
```
Only a micro-optimisation, but safe and measurable when resolving thousands of paths.

</blockquote></details>
<details>
<summary>src/lib.rs (1)</summary><blockquote>

`773-838`: **`load_node_modules` – small readability / perf tweaks**

1. The outer `is_dir` check (see previous comment) duplicates the inner one performed by `cached_node_modules`.  
2. Variable reuse (`cached_path` is reassigned several times) makes the loop harder to follow.

A lightweight refactor keeps the behaviour but avoids the double check and clarifies intent:

```diff
for ancestor in std::iter::successors(Some(cached_path), |p| p.parent()) {
-    if !self.cache.is_dir(ancestor, ctx) {
-        continue;
-    }
-
-    let Some(node_modules) = ancestor.cached_node_modules(self.cache.as_ref(), ctx) else {
+    let Some(node_modules) = ancestor.cached_node_modules(self.cache.as_ref(), ctx) else {
         continue;
     };
+    // `node_modules` definitely exists here.

     if !package_name.is_empty() {
         let package_dir = node_modules.normalize_with(package_name, self.cache.as_ref());
         ...
```

Behaviour is identical, but we save one metadata lookup and reduce shadowing.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9931467eced87a02c83691bb2acbc14bcf9532ea and 9cd2ba40272df8012a710663a0dfa891c8df6f23.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `README.md` (1 hunks)
* `src/cache.rs` (0 hunks)
* `src/fs_cache.rs` (1 hunks)
* `src/lib.rs` (2 hunks)
* `src/options.rs` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/cache.rs
* src/options.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Actions: CI</summary>

<details>
<summary>src/lib.rs</summary>

[error] 201-201: Rust compiler error E0560: struct `ResolveOptions` has no field named `modules`.

---

[error] 201-201: Rust compiler error E0609: no field `modules` on type `ResolveOptions`. Available fields are: `tsconfig`, `alias`, `alias_fields`, `condition_names`, `description_files`, and others.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>README.md (1)</summary>

`193-194`: **Documentation aligns with code – looks good**

The `modules` option is now correctly moved to “Unimplemented Options”, reflecting the removal in the codebase. No further action required.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-01T01:29:52Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-01T02:28:10Z

Review comments:
diff_hunk: @@ -1339,51 +1320,49 @@ impl<C: Cache> ResolverGeneric<C> {
         self.require_core(package_name)?;
 
         // 11. While parentURL is not the file system root,
-        for module_name in &self.options.modules {
-            for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
-                // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
-                let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx)
-                else {
-                    continue;
-                };
-                // 2. Set parentURL to the parent folder URL of parentURL.
-                let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
-                // 3. If the folder at packageURL does not exist, then
-                //   1. Continue the next loop iteration.
-                if self.cache.is_dir(&cached_path, ctx) {
-                    // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
-                    if let Some((_, package_json)) =
-                        self.cache.get_package_json(&cached_path, &self.options, ctx)?
-                    {
-                        // 5. If pjson is not null and pjson.exports is not null or undefined, then
-                        // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
-                        for exports in package_json.exports_fields(&self.options.exports_fields) {
-                            if let Some(path) = self.package_exports_resolve(
-                                &cached_path,
-                                &format!(".{subpath}"),
-                                &exports,
-                                ctx,
-                            )? {
-                                return Ok(Some(path));
-                            }
+        for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
+            // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
+            let Some(cached_path) = cached_path.cached_node_modules(self.cache.as_ref(), ctx)
+            else {
+                continue;
+            };
+            // 2. Set parentURL to the parent folder URL of parentURL.
+            let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
+            // 3. If the folder at packageURL does not exist, then
+            //   1. Continue the next loop iteration.
+            if self.cache.is_dir(&cached_path, ctx) {
+                // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
+                if let Some((_, package_json)) =
+                    self.cache.get_package_json(&cached_path, &self.options, ctx)?
+                {
+                    // 5. If pjson is not null and pjson.exports is not null or undefined, then
+                    // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
+                    for exports in package_json.exports_fields(&self.options.exports_fields) {
+                        if let Some(path) = self.package_exports_resolve(
+                            &cached_path,
+                            &format!(".{subpath}"),
+                            &exports,
+                            ctx,
+                        )? {
+                            return Ok(Some(path));
                         }
-                        // 6. Otherwise, if packageSubpath is equal to ".", then
-                        if subpath == "." {
-                            // 1. If pjson.main is a string, then
-                            for main_field in package_json.main_fields(&self.options.main_fields) {
-                                // 1. Return the URL resolution of main in packageURL.
-                                let cached_path =
-                                    cached_path.normalize_with(main_field, self.cache.as_ref());
-                                if self.cache.is_file(&cached_path, ctx) {
-                                    return Ok(Some(cached_path));
-                                }
+                    }
+                    // 6. Otherwise, if packageSubpath is equal to ".", then
+                    if subpath == "." {
+                        // 1. If pjson.main is a string, then
+                        for main_field in package_json.main_fields(&self.options.main_fields) {
+                            // 1. Return the URL resolution of main in packageURL.
+                            let cached_path =
+                                cached_path.normalize_with(main_field, self.cache.as_ref());
+                            if self.cache.is_file(&cached_path, ctx) {
+                                return Ok(Some(cached_path));
                             }
                         }
                     }
-                    let subpath = format!(".{subpath}");
-                    ctx.with_fully_specified(false);
-                    return self.require(&cached_path, &subpath, ctx).map(Some);
                 }
+                let subpath = format!(".{subpath}");
+                ctx.with_fully_specified(false);
+                return self.require(&cached_path, &subpath, ctx).map(Some);
             }
path: src/lib.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Left-over `modules` field still referenced elsewhere – CI is red**

CI logs report:

```
error[E0560]: struct `ResolveOptions` has no field named `modules`
```

so at least one file (not in this diff) still constructs `ResolveOptions { modules: ... }` or accesses `options.modules`.

Please drop those remnants or gate them behind the new “unimplemented” status.


To locate the offenders quickly, run:



Remove the struct initialisers / field accesses, or refactor logic that still relies on the vector.

---

🏁 Script executed:

```shell
#!/bin/bash
# Find any remaining references to the removed `modules` option
rg -n "\bmodules\b" --type rust --ignore-case
```

Length of output: 3830

---



</details>

**Remove all remaining `modules` references in code and tests**

CI is failing with  
```
error[E0560]: struct `ResolveOptions` has no field named `modules`
```  
because there are still initializers and field accesses for the removed `modules` option. Please drop or refactor these references:

- **src/options.rs**  
  - Doc comments at lines 19 & 127 referencing `modules`  
  - `Display`/`Debug` expected output at line 585 containing `modules:["node_modules"]`
- **src/tests/fallback.rs** (line 57): `modules: vec![…]` in test setup  
- **src/tests/resolve.rs** (line 72): `modules: vec![…]`  
- **src/tests/dependencies.rs** (lines 15 & 27): `modules: vec![…]`  
- **src/tests/alias.rs** (lines 74 & 178): `modules: vec![…]`  
- **napi/src/lib.rs** (line 201): `op.modules.map(|o| …)`  
- **napi/src/options.rs** (lines 108–112): the `pub modules: Option<…>` field and its docs

Either remove these initializers and accesses entirely or guard them (e.g., behind `unimplemented!()` if you plan a future implementation). After cleaning up, rerun CI to confirm the errors are resolved.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T01:29:51Z
author_association: NONE
diff_hunk: @@ -191,6 +190,7 @@ See [index.d.ts](https://github.com/oxc-project/oxc-resolver/blob/main/napi/inde
 
 | Field            | Default                     | Description                                                                                                                                   |
 | ---------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| modules          | ["node_modules"]            | A list of directories to resolve modules from, can be absolute path or folder name                                                            |
path: README.md
user: login: graphite-app[bot]
body: The `modules` option appears to have been incorrectly added to the "Unsupported options" table in the README. Since this PR is specifically removing the `modules` option entirely from the codebase, this entry should be removed from this table as well. It was correctly removed from the supported options table above, but shouldn't be listed as an unsupported option either.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-01T02:28:08Z
author_association: NONE
diff_hunk: @@ -770,73 +770,67 @@ impl<C: Cache> ResolverGeneric<C> {
 
         // 1. let DIRS = NODE_MODULES_PATHS(START)
         // 2. for each DIR in DIRS:
-        for module_name in &self.options.modules {
-            for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
-                // Skip if /path/to/node_modules does not exist
-                if !self.cache.is_dir(cached_path, ctx) {
-                    continue;
-                }
-
-                let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx)
-                else {
-                    continue;
-                };
-                // Optimize node_modules lookup by inspecting whether the package exists
-                // From LOAD_PACKAGE_EXPORTS(X, DIR)
-                // 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 (`/`).
-                if !package_name.is_empty() {
-                    let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
-                    // Try foo/node_modules/package_name
-                    if self.cache.is_dir(&cached_path, ctx) {
-                        // a. LOAD_PACKAGE_EXPORTS(X, DIR)
-                        if let Some(path) =
-                            self.load_package_exports(specifier, subpath, &cached_path, ctx)?
-                        {
-                            return Ok(Some(path));
-                        }
-                    } else {
-                        // foo/node_modules/package_name is not a directory, so useless to check inside it
-                        if !subpath.is_empty() {
-                            continue;
-                        }
-                        // Skip if the directory lead to the scope package does not exist
-                        // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
-                        if package_name.starts_with('@') {
-                            if let Some(path) = cached_path.parent() {
-                                if !self.cache.is_dir(path, ctx) {
-                                    continue;
-                                }
+        for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
+            // Skip if /path/to/node_modules does not exist
+            let cached_path = cached_path.normalize_with("node_modules", self.cache.as_ref());
+            if !self.cache.is_dir(&cached_path, ctx) {
+                continue;
+            }
+            // Optimize node_modules lookup by inspecting whether the package exists
+            // From LOAD_PACKAGE_EXPORTS(X, DIR)
+            // 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 (`/`).
+            if !package_name.is_empty() {
+                let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
+                // Try foo/node_modules/package_name
+                if self.cache.is_dir(&cached_path, ctx) {
+                    // a. LOAD_PACKAGE_EXPORTS(X, DIR)
+                    if let Some(path) =
+                        self.load_package_exports(specifier, subpath, &cached_path, ctx)?
+                    {
+                        return Ok(Some(path));
+                    }
+                } else {
+                    // foo/node_modules/package_name is not a directory, so useless to check inside it
+                    if !subpath.is_empty() {
+                        continue;
+                    }
+                    // Skip if the directory lead to the scope package does not exist
+                    // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
+                    if package_name.starts_with('@') {
+                        if let Some(path) = cached_path.parent() {
+                            if !self.cache.is_dir(path, ctx) {
+                                continue;
                             }
                         }
                     }
                 }
+            }
 
-                // Try as file or directory for all other cases
-                // b. LOAD_AS_FILE(DIR/X)
-                // c. LOAD_AS_DIRECTORY(DIR/X)
+            // Try as file or directory for all other cases
+            // b. LOAD_AS_FILE(DIR/X)
+            // c. LOAD_AS_DIRECTORY(DIR/X)
 
-                let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
+            let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
 
-                // Perf: try the directory first for package specifiers.
-                if self.options.resolve_to_context {
-                    return Ok(self.cache.is_dir(&cached_path, ctx).then(|| cached_path.clone()));
-                }
-                if self.cache.is_dir(&cached_path, ctx) {
-                    if let Some(path) = self.load_browser_field_or_alias(&cached_path, ctx)? {
-                        return Ok(Some(path));
-                    }
-                    if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
-                        return Ok(Some(path));
-                    }
-                }
-                if let Some(path) = self.load_as_file(&cached_path, ctx)? {
+            // Perf: try the directory first for package specifiers.
+            if self.options.resolve_to_context {
+                return Ok(self.cache.is_dir(&cached_path, ctx).then(|| cached_path.clone()));
+            }
+            if self.cache.is_dir(&cached_path, ctx) {
+                if let Some(path) = self.load_browser_field_or_alias(&cached_path, ctx)? {
                     return Ok(Some(path));
                 }
                 if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
                     return Ok(Some(path));
                 }
             }
+            if let Some(path) = self.load_as_file(&cached_path, ctx)? {
+                return Ok(Some(path));
+            }
+            if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
+                return Ok(Some(path));
+            }
path: src/lib.rs
user: login: graphite-app[bot]
body: There appears to be a duplicate call to `load_as_directory` at lines 831-833. This method is already called at lines 824-826 when `cached_path` is confirmed to be a directory. The second invocation is redundant and could impact performance by:

1. Re-attempting directory resolution that was already performed
2. Trying to load a directory from a path that was already determined not to be a directory

Removing this duplicate call would maintain the same functionality while improving efficiency.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-01T02:28:09Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-01T01:25:46Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This set of changes removes support for custom module directories from the module resolution logic. The `modules` option is eliminated from both the codebase and documentation, leaving only the default `node_modules` directory for resolution. This involves removing the `modules` field and related methods from the `ResolveOptions` struct, deleting the `module_directory` helper method from the `CachedPath` trait and its implementation, and simplifying the control flow in the resolver to only search for `node_modules` directories while walking up the directory tree. Documentation is updated to reflect that the `modules` option is now unimplemented. Corresponding test files were updated to reflect the removal of the `modules` option and the shift to using `/node_modules` as the root path for module resolution in tests. One test function was removed due to reliance on the old modules option.

## Changes

| File(s)                       | Change Summary                                                                                       |
|-------------------------------|----------------------------------------------------------------------------------------------------|
| README.md                     | Moved `modules` option from main options table to unimplemented options in documentation.          |
| src/options.rs                | Removed `modules` field and `with_module` method from `ResolveOptions`; updated default and display; removed related test code. |
| src/cache.rs, src/fs_cache.rs | Removed `module_directory` method and related methods from `CachedPath` trait and `FsCachedPath` implementation; removed `node_modules` cache field. |
| src/lib.rs                   | Removed iteration over `modules` in resolver logic; deleted `get_module_directory` helper; simplified control flow to only walk directory tree for `node_modules`. |
| src/tests/alias.rs           | Updated test paths and alias mappings to use `/node_modules` prefix; adjusted expected results accordingly. |
| src/tests/dependencies.rs    | Replaced all `/modules` paths with `/node_modules` in test setup and expected results.             |
| src/tests/fallback.rs        | Updated in-memory filesystem and alias paths to use `/node_modules`; removed `modules` option from resolver options. |
| src/tests/resolve.rs         | Removed `issue238_resolve` test function that relied on the `modules` option.                      |
| tests/integration_test.rs    | Removed `.with_module("module")` call from `ResolveOptions` builder chain in test setup.           |
| napi/index.d.ts              | Removed `modules` property from `NapiResolveOptions` interface.                                    |
| napi/src/options.rs          | Removed `modules` field from `NapiResolveOptions` struct.                                          |
| napi/src/lib.rs              | Removed assignment of `modules` field in `normalize_options` method of `ResolverFactory`.          |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Caller
    participant Resolver
    participant Cache

    Caller->>Resolver: load_node_modules(cached_path, ...)
    loop Walk up directory tree
        Resolver->>Cache: cached_node_modules(current_path, ...)
        alt node_modules found
            Resolver->>Cache: Check for package directory
            alt package found
                Resolver->>Cache: Load package exports
            else file/directory fallback
                Resolver->>Cache: Try as file or directory
            end
        end
    end
    Resolver-->>Caller: Return resolved module or error
```

## Poem

> In burrows deep, the modules slept,  
> But now just "node_modules" is kept.  
> No more custom paths to roam,  
> The code is simpler, close to home.  
> The rabbit hops with tidy cheer,  
> For cluttered options disappear!  
> 🐇✨

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6c9c4e07a87ab0424044690dea2ae29700d48a27 and c8daf9651725295b482e9ce0baad45cc17486781.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/cache.rs` (0 hunks)
* `src/fs_cache.rs` (0 hunks)
* `src/lib.rs` (2 hunks)
* `src/tests/alias.rs` (4 hunks)
* `src/tests/dependencies.rs` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/cache.rs
* src/fs_cache.rs

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/tests/alias.rs

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/tests/dependencies.rs
* src/lib.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Benchmark

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=oxc-project/oxc-resolver&utm_content=484):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-01T02:04:58Z
body: ## [Codecov](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/484?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project) Report
Attention: Patch coverage is `88.57143%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 93.80%. Comparing base [(`9931467`)](https://app.codecov.io/gh/oxc-project/oxc-resolver/commit/9931467eced87a02c83691bb2acbc14bcf9532ea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project) to head [(`c8daf96`)](https://app.codecov.io/gh/oxc-project/oxc-resolver/commit/c8daf9651725295b482e9ce0baad45cc17486781?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/484?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project) | Patch % | Lines |
|---|---|---|
| [src/lib.rs](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/484?src=pr&el=tree&filepath=src%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oxc-project#diff-c3JjL2xpYi5ycw==) | 88.40% | [8 Missing :warning: ](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/484?src=pr&el=tree&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     #484      +/-   ##
==========================================
+ Coverage   93.73%   93.80%   +0.06%     
==========================================
  Files          13       13              
  Lines        2825     2791      -34     
==========================================
- Hits         2648     2618      -30     
+ Misses        177      173       -4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/oxc-project/oxc-resolver/pull/484?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).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: codspeed-hq[bot]
created_at: 2025-05-01T02:09:00Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/oxc-project/oxc-resolver/branches/05-01-refactor_remove_modules_options)

### Merging #484 will **not alter performance**

<sub>Comparing <code>05-01-refactor_remove_modules_options</code> (c8daf96) with <code>main</code> (74de9cf)</sub>



### Summary

`✅ 3` untouched benchmarks  




author_association: NONE
user: login: sxzz
created_at: 2025-05-03T21:36:26Z
body: Some libraries use this option to resolve `@types/dep`

author_association: MEMBER
user: login: JounQin
created_at: 2025-05-04T04:44:58Z
body: > Some libraries use this option to resolve `@types/dep`
> 
> 

That won't work for scoped packages?
author_association: CONTRIBUTOR
user: login: sxzz
created_at: 2025-05-04T10:19:35Z
body: See https://github.com/oxc-project/oxc-resolver/issues/493
author_association: MEMBER