feat(resolver): implement main_fields

This commit is contained in:
Boshen 2023-08-10 18:30:43 +08:00
parent f717cb02e3
commit 11954326b3
7 changed files with 77 additions and 25 deletions

View file

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

View file

@ -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))

View file

@ -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() {

View file

@ -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,

View file

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

View 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")));
}

View file

@ -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;