mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): initialize implementation of package.json exports field (#630)
This commit is contained in:
parent
1d504ac94a
commit
32b8ad2b57
6 changed files with 107 additions and 16 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -852,6 +852,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1588,6 +1589,7 @@ dependencies = [
|
|||
"dashmap",
|
||||
"dunce",
|
||||
"identity-hash",
|
||||
"indexmap 2.0.0",
|
||||
"jemallocator",
|
||||
"mimalloc",
|
||||
"nodejs-resolver",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ dashmap = { workspace = true }
|
|||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
indexmap = { workspace = true, features = ["serde"] }
|
||||
dunce = "1.0.4"
|
||||
identity-hash = "0.1.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -141,9 +141,6 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
cache_value: &CacheValue,
|
||||
request: &str,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
if let Some(path) = self.load_package_self(cache_value, request)? {
|
||||
return Ok(path);
|
||||
}
|
||||
let path = cache_value.path().normalize_with(request);
|
||||
let cache_value = self.cache.value(&path);
|
||||
// a. LOAD_AS_FILE(Y + X)
|
||||
|
|
@ -321,32 +318,52 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
|
||||
fn load_package_exports(&self, _path: &Path, _request: &str) -> ResolveState {
|
||||
fn load_package_exports(&self, path: &Path, request: &str) -> ResolveState {
|
||||
let cache_value = self.cache.value(&path.join(request));
|
||||
// 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 (`/`).
|
||||
// 2. If X does not match this pattern or DIR/NAME/package.json is not a file,
|
||||
// return.
|
||||
let Some(package_json) = cache_value.package_json(&self.cache.fs).transpose()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
// 3. Parse DIR/NAME/package.json, and look for "exports" field.
|
||||
// 4. If "exports" is null or undefined, return.
|
||||
// (checked in package_json.package_exports_resolve)
|
||||
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
|
||||
// `package.json` "exports", ["node", "require"]) defined in the ESM resolver.
|
||||
if let Some(path) = package_json.package_exports_resolve(".") {
|
||||
let cache_value = self.cache.value(&path);
|
||||
return Ok(Some(cache_value));
|
||||
}
|
||||
// 6. RESOLVE_ESM_MATCH(MATCH)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// * Parent of package.json is None
|
||||
fn load_package_self(&self, cache_value: &CacheValue, request: &str) -> ResolveState {
|
||||
if let Some(package_json) = cache_value.find_package_json(&self.cache.fs)? {
|
||||
if let Some(path) =
|
||||
self.load_browser_field(cache_value.path(), Some(request), &package_json)?
|
||||
{
|
||||
return Ok(Some(path));
|
||||
// 1. Find the closest package scope SCOPE to DIR.
|
||||
// 2. If no scope was found, return.
|
||||
let Some(package_json) = cache_value.find_package_json(&self.cache.fs)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
// 3. If the SCOPE/package.json "exports" is null or undefined, return.
|
||||
// (checked in package_json.package_exports_resolve)
|
||||
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
|
||||
// TODO: get first segment of X
|
||||
if package_json.name.as_ref().is_some_and(|name| name.starts_with(request)) {
|
||||
// return Ok(None);
|
||||
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
|
||||
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
|
||||
// defined in the ESM resolver.
|
||||
if let Some(path) = package_json.package_exports_resolve(request) {
|
||||
let cache_value = self.cache.value(&path);
|
||||
return Ok(Some(cache_value));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
// 6. RESOLVE_ESM_MATCH(MATCH)
|
||||
|
||||
// Try non-spec-compliant "browser" field since its another form of export
|
||||
self.load_browser_field(cache_value.path(), Some(request), &package_json)
|
||||
}
|
||||
|
||||
fn load_browser_field(
|
||||
|
|
|
|||
|
|
@ -1,18 +1,42 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
hash::BuildHasherDefault,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use rustc_hash::FxHasher;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{path::PathUtil, ResolveError};
|
||||
|
||||
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
// TODO: allocate everything into an arena or SoA
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PackageJson {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The "name" field defines your package's name.
|
||||
/// The "name" field can be used in addition to the "exports" field to self-reference a package using its name.
|
||||
///
|
||||
/// <https://nodejs.org/api/packages.html#name>
|
||||
pub name: Option<String>,
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// <https://nodejs.org/api/packages.html#main>
|
||||
pub main: Option<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.
|
||||
///
|
||||
/// <https://nodejs.org/api/packages.html#exports>
|
||||
pub exports: Option<ExportsField>,
|
||||
|
||||
/// The browser field is provided by a module author as a hint to javascript bundlers or component tools when packaging modules for client side use.
|
||||
///
|
||||
/// <https://github.com/defunctzombie/package-browser-field-spec>
|
||||
pub browser: Option<BrowserField>,
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +44,15 @@ pub struct PackageJson {
|
|||
#[serde(untagged)]
|
||||
pub enum BrowserField {
|
||||
String(String),
|
||||
Map(HashMap<PathBuf, serde_json::Value>),
|
||||
Map(FxIndexMap<PathBuf, serde_json::Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ExportsField {
|
||||
String(String),
|
||||
Array(Vec<ExportsField>),
|
||||
Map(FxIndexMap<String, ExportsField>),
|
||||
}
|
||||
|
||||
impl PackageJson {
|
||||
|
|
@ -83,4 +115,22 @@ impl PackageJson {
|
|||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// [PACKAGE_EXPORTS_RESOLVE](https://nodejs.org/api/esm.html#resolution-algorithm-specification)
|
||||
#[allow(clippy::single_match)]
|
||||
pub fn package_exports_resolve(&self, request: &str) -> Option<PathBuf> {
|
||||
let Some(exports) = &self.exports else {
|
||||
return None;
|
||||
};
|
||||
match exports {
|
||||
ExportsField::Map(map) => match map.get(request) {
|
||||
Some(ExportsField::String(value)) => {
|
||||
return Some(self.path.parent().unwrap().join(value));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
//! https://github.com/webpack/enhanced-resolve/blob/main/test/exportsField.test.js
|
||||
//!
|
||||
//! The resolution tests are at the bottom of the file.
|
||||
|
||||
use oxc_resolver::{Resolution, ResolveOptions, Resolver};
|
||||
|
||||
#[test]
|
||||
fn exports_field() {
|
||||
let fixture = super::fixture().join("exports-field");
|
||||
|
||||
let resolver = Resolver::new(ResolveOptions {
|
||||
extensions: vec![".js".into()],
|
||||
// fullySpecified: true,
|
||||
// conditionNames: ["webpack"]
|
||||
..ResolveOptions::default()
|
||||
});
|
||||
|
||||
let resolved_path = resolver.resolve(&fixture, "exports-field").map(Resolution::full_path);
|
||||
assert_eq!(resolved_path, Ok(fixture.join("node_modules/exports-field/x.js")));
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod alias;
|
||||
mod browser_field;
|
||||
mod exports_field;
|
||||
mod extension_alias;
|
||||
mod extensions;
|
||||
mod fallback;
|
||||
|
|
|
|||
Loading…
Reference in a new issue