mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): implement main_fields
This commit is contained in:
parent
f717cb02e3
commit
11954326b3
7 changed files with 77 additions and 25 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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<Fs> {
|
||||
pub(crate) fs: Fs,
|
||||
|
|
@ -129,6 +129,7 @@ impl CachedPathImpl {
|
|||
pub fn find_package_json<Fs: FileSystem>(
|
||||
&self,
|
||||
fs: &Fs,
|
||||
options: &ResolveOptions,
|
||||
) -> Result<Option<Arc<PackageJson>>, 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<Fs: FileSystem>(
|
||||
&self,
|
||||
fs: &Fs,
|
||||
options: &ResolveOptions,
|
||||
) -> Result<Option<Arc<PackageJson>>, 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))
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
) -> 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
}
|
||||
|
||||
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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
) -> 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
}
|
||||
// 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() {
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
|
||||
/// 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,
|
||||
|
|
|
|||
|
|
@ -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<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
|
|
@ -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].
|
||||
///
|
||||
/// <https://nodejs.org/api/packages.html#main>
|
||||
pub main: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub main_fields: Vec<String>,
|
||||
|
||||
/// 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<Self, serde_json::Error> {
|
||||
let mut package_json: Self = serde_json::from_str(json)?;
|
||||
pub fn parse(
|
||||
path: PathBuf,
|
||||
json: &str,
|
||||
options: &ResolveOptions,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
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
|
||||
|
|
|
|||
24
crates/oxc_resolver/src/tests/main_field.rs
Normal file
24
crates/oxc_resolver/src/tests/main_field.rs
Normal file
|
|
@ -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")));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue