diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 3ecc073c2..4652e425f 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -21,7 +21,7 @@ | ✅ | fallback | [] | Same as `alias`, but only used if default resolving fails | | ✅ | fileSystem | | The file system which should be used | | ✅ | 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 | +| ✅ | 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 | | | plugins | [] | A list of additional resolve plugins which should be applied | diff --git a/crates/oxc_resolver/src/cache.rs b/crates/oxc_resolver/src/cache.rs index 519a24cd6..8460c77db 100644 --- a/crates/oxc_resolver/src/cache.rs +++ b/crates/oxc_resolver/src/cache.rs @@ -10,7 +10,7 @@ use std::{ use dashmap::DashMap; use rustc_hash::FxHasher; -use crate::{package_json::PackageJson, FileMetadata, FileSystem, ResolveError}; +use crate::{package_json::PackageJson, FileMetadata, FileSystem, ResolveError, ResolveOptions}; pub struct Cache { pub(crate) fs: Fs, @@ -129,6 +129,7 @@ impl CachedPathImpl { pub fn find_package_json( &self, fs: &Fs, + options: &ResolveOptions, ) -> Result>, ResolveError> { let mut cache_value = self; // Go up a directory when querying a file, this avoids a file read from example.js/package.json @@ -139,7 +140,7 @@ impl CachedPathImpl { } let mut cache_value = Some(cache_value); while let Some(cv) = cache_value { - if let Some(package_json) = cv.package_json(fs)? { + if let Some(package_json) = cv.package_json(fs, options)? { return Ok(Some(Arc::clone(&package_json))); } cache_value = cv.parent.as_deref(); @@ -155,6 +156,7 @@ impl CachedPathImpl { pub fn package_json( &self, fs: &Fs, + options: &ResolveOptions, ) -> Result>, ResolveError> { // Change to `std::sync::OnceLock::get_or_try_init` when it is stable. self.package_json @@ -163,7 +165,7 @@ impl CachedPathImpl { let Ok(package_json_string) = fs.read_to_string(&package_json_path) else { return Ok(None) }; - PackageJson::parse(package_json_path.clone(), &package_json_string) + PackageJson::parse(package_json_path.clone(), &package_json_string, options) .map(Arc::new) .map(Some) .map_err(|error| ResolveError::from_serde_json_error(package_json_path, &error)) diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index 79e979d28..89baa6816 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -303,7 +303,7 @@ impl ResolverGeneric { ) -> ResolveState { // 1. Find the closest package scope SCOPE to DIR. // 2. If no scope was found, return. - let Some(package_json) = cached_path.find_package_json(&self.cache.fs)? else { + let Some(package_json) = cached_path.find_package_json(&self.cache.fs, &self.options)? else { return Ok(None); }; // 3. If the SCOPE/package.json "imports" is null or undefined, return. @@ -418,7 +418,7 @@ impl ResolverGeneric { } fn load_alias_or_file(&self, cached_path: &CachedPath, ctx: &ResolveContext) -> ResolveState { - if let Some(package_json) = cached_path.find_package_json(&self.cache.fs)? { + if let Some(package_json) = cached_path.find_package_json(&self.cache.fs, &self.options)? { let path = cached_path.path(); if let Some(path) = self.load_browser_field(path, None, &package_json, ctx)? { return Ok(Some(path)); @@ -443,9 +443,9 @@ impl ResolverGeneric { // 1. If X/package.json is a file, if !self.options.description_files.is_empty() { // a. Parse X/package.json, and look for "main" field. - if let Some(package_json) = cached_path.package_json(&self.cache.fs)? { + if let Some(package_json) = cached_path.package_json(&self.cache.fs, &self.options)? { // b. If "main" is a falsy value, GOTO 2. - if let Some(main_field) = &package_json.main { + for main_field in &package_json.main_fields { // c. let M = X + (json main field) let main_field_path = cached_path.path().normalize_with(main_field); // d. LOAD_AS_FILE(M) @@ -457,10 +457,9 @@ impl ResolverGeneric { if let Some(path) = self.load_index(&cached_path, ctx)? { return Ok(Some(path)); } - // f. LOAD_INDEX(X) DEPRECATED - // g. THROW "not found" - return Err(ResolveError::NotFound(main_field_path)); } + // f. LOAD_INDEX(X) DEPRECATED + // g. THROW "not found" } } // 2. LOAD_INDEX(X) @@ -520,7 +519,7 @@ impl ResolverGeneric { // return. let (name, subpath) = Self::parse_package_specifier(specifier); let cached_path = self.cache.value(&path.join(name)); - let Some(package_json) = cached_path.package_json(&self.cache.fs)? else { + let Some(package_json) = cached_path.package_json(&self.cache.fs, &self.options)? else { return Ok(None); }; // 3. Parse DIR/NAME/package.json, and look for "exports" field. @@ -552,7 +551,7 @@ impl ResolverGeneric { ) -> ResolveState { // 1. Find the closest package scope SCOPE to DIR. // 2. If no scope was found, return. - let Some(package_json) = cached_path.find_package_json(&self.cache.fs)? else { + let Some(package_json) = cached_path.find_package_json(&self.cache.fs, &self.options)? else { return Ok(None); }; // 3. If the SCOPE/package.json "exports" is null or undefined, return. @@ -714,7 +713,9 @@ impl ResolverGeneric { // 1. Continue the next loop iteration. if cached_path.is_dir(&self.cache.fs) { // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL). - if let Some(package_json) = cached_path.package_json(&self.cache.fs)? { + if let Some(package_json) = + cached_path.package_json(&self.cache.fs, &self.options)? + { // 5. If pjson is not null and pjson.exports is not null or undefined, then if !package_json.exports.is_none() { // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions). @@ -729,11 +730,13 @@ impl ResolverGeneric { // 6. Otherwise, if packageSubpath is equal to ".", then if subpath == "." { // 1. If pjson.main is a string, then - if let Some(main_field) = &package_json.main { + for main_field in &package_json.main_fields { // 1. Return the URL resolution of main in packageURL. let path = cached_path.path().normalize_with(main_field); - let value = self.cache.value(&path); - return Ok(Some(value)); + let cached_path = self.cache.value(&path); + if cached_path.is_file(&self.cache.fs) { + return Ok(Some(cached_path)); + } } } } @@ -867,7 +870,7 @@ impl ResolverGeneric { } // 3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL). // 4. If packageURL is not null, then - if let Some(package_json) = cached_path.find_package_json(&self.cache.fs)? { + if let Some(package_json) = cached_path.find_package_json(&self.cache.fs, &self.options)? { // 1. Let pjson be the result of READ_PACKAGE_JSON(packageURL). // 2. If pjson.imports is a non-null Object, then if !package_json.imports.is_empty() { diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index 4ef5d5a8b..7b38f8f87 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -71,9 +71,8 @@ pub struct ResolveOptions { pub fully_specified: bool, /// A list of main fields in description files - /// This is currently unused, but values are passed in for logging purposes. /// - /// Default `[]`. Should be `["main"]` when enabled. + /// Default `["main"]`. pub main_fields: Vec, /// The filename to be used while resolving directories. @@ -173,7 +172,7 @@ impl Default for ResolveOptions { extensions: vec![".js".into(), ".json".into(), ".node".into()], fallback: vec![], fully_specified: false, - main_fields: vec![], + main_fields: vec!["main".into()], main_files: vec!["index".into()], modules: vec!["node_modules".into()], resolve_to_context: false, diff --git a/crates/oxc_resolver/src/package_json.rs b/crates/oxc_resolver/src/package_json.rs index 3dc969f94..faa1314e0 100644 --- a/crates/oxc_resolver/src/package_json.rs +++ b/crates/oxc_resolver/src/package_json.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use rustc_hash::FxHasher; use serde::Deserialize; -use crate::{path::PathUtil, ResolveError}; +use crate::{path::PathUtil, ResolveError, ResolveOptions}; type FxIndexMap = IndexMap>; @@ -30,8 +30,11 @@ pub struct PackageJson { /// The "main" field defines the entry point of a package when imported by name via a node_modules lookup. Its value is a path. /// When a package has an "exports" field, this will take precedence over the "main" field when importing the package by name. /// + /// Values are dynamically added from [ResolveOptions::main_fields]. + /// /// - pub main: Option, + #[serde(skip)] + pub main_fields: Vec, /// The "exports" field allows defining the entry points of a package when imported by name loaded either via a node_modules lookup or a self-reference to its own name. /// @@ -110,8 +113,28 @@ pub enum BrowserField { } impl PackageJson { - pub fn parse(path: PathBuf, json: &str) -> Result { - let mut package_json: Self = serde_json::from_str(json)?; + pub fn parse( + path: PathBuf, + json: &str, + options: &ResolveOptions, + ) -> Result { + let mut package_json_value: serde_json::Value = serde_json::from_str(json.clone())?; + + // Dynamically create `main_fields`. + let mut main_fields = vec![]; + if let Some(package_json_value) = package_json_value.as_object_mut() { + for main_field_key in &options.main_fields { + if let Some(serde_json::Value::String(value)) = + package_json_value.remove(main_field_key) + { + main_fields.push(value); + } + } + } + + // TODO: can this clone be avoided? + let mut package_json: Self = serde_json::from_str(json.clone())?; + package_json.main_fields = main_fields; // Normalize all relative paths to make browser_field a constant value lookup // TODO: fix BrowserField::String diff --git a/crates/oxc_resolver/src/tests/main_field.rs b/crates/oxc_resolver/src/tests/main_field.rs new file mode 100644 index 000000000..98f790521 --- /dev/null +++ b/crates/oxc_resolver/src/tests/main_field.rs @@ -0,0 +1,24 @@ +//! Not part of enhanced_resolve's test suite + +use crate::{ResolveOptions, Resolver}; + +#[test] +fn test() { + let f = super::fixture().join("restrictions"); + + let resolver = Resolver::new(ResolveOptions { + main_fields: vec!["style".into()], + ..ResolveOptions::default() + }); + + let resolution = resolver.resolve(&f, "pck2").map(|r| r.full_path()); + assert_eq!(resolution, Ok(f.join("node_modules/pck2/index.css"))); + + let resolver = Resolver::new(ResolveOptions { + main_fields: vec!["module".into(), "main".into()], + ..ResolveOptions::default() + }); + + let resolution = resolver.resolve(&f, "pck2").map(|r| r.full_path()); + assert_eq!(resolution, Ok(f.join("node_modules/pck2/module.js"))); +} diff --git a/crates/oxc_resolver/src/tests/mod.rs b/crates/oxc_resolver/src/tests/mod.rs index eb29a58db..b8427e4d5 100644 --- a/crates/oxc_resolver/src/tests/mod.rs +++ b/crates/oxc_resolver/src/tests/mod.rs @@ -7,6 +7,7 @@ mod fallback; mod full_specified; mod imports_field; mod incorrect_description_file; +mod main_field; mod memory_fs; mod resolve; mod restrictions;