mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): imports field (#681)
This commit is contained in:
parent
de110828cc
commit
2e3934db49
7 changed files with 1421 additions and 61 deletions
|
|
@ -52,7 +52,7 @@ Crossed out test files are irrelevant.
|
|||
- [ ] fullSpecified.test.js
|
||||
- [ ] getPaths.test.js
|
||||
- [x] identifier.test.js (see unit test in `crates/oxc_resolver/src/request.rs`)
|
||||
- [ ] importsField.test.js
|
||||
- [x] importsField.test.js
|
||||
- [x] incorrect-description-file.test.js (need to add ctx.fileDependencies)
|
||||
- [ ] missing.test.js
|
||||
- [ ] path.test.js
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ pub enum ResolveError {
|
|||
|
||||
// TODO: Expecting folder to folder mapping. "./data/timezones" should end with "/"
|
||||
InvalidPackageConfigDirectory(PathBuf),
|
||||
|
||||
PackageImportNotDefined(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ use std::{
|
|||
use crate::{
|
||||
cache::{Cache, CacheValue},
|
||||
file_system::FileSystemOs,
|
||||
package_json::{ExportsKey, MatchObject, PackageJson},
|
||||
package_json::{ExportsKey, PackageJson},
|
||||
request::{Request, RequestPath},
|
||||
};
|
||||
pub use crate::{
|
||||
error::{JSONError, ResolveError},
|
||||
file_system::{FileMetadata, FileSystem},
|
||||
options::{Alias, AliasValue, ResolveOptions},
|
||||
package_json::ExportsField,
|
||||
package_json::{ExportsField, MatchObject},
|
||||
path::PathUtil,
|
||||
resolution::Resolution,
|
||||
};
|
||||
|
|
@ -131,8 +131,10 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
self.require_relative(cache_value, relative_path)
|
||||
}
|
||||
// 4. If X begins with '#'
|
||||
// a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
|
||||
RequestPath::Hash(hash_path) => self.package_resolve(cache_value, hash_path),
|
||||
RequestPath::Hash(specifier) => {
|
||||
// a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
|
||||
self.package_imports_resolve(cache_value, specifier)
|
||||
}
|
||||
// (ESM) 5. Otherwise,
|
||||
// Note: specifier is now a bare specifier.
|
||||
// Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
|
||||
|
|
@ -421,11 +423,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
|
||||
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
|
||||
// defined in the ESM resolver.
|
||||
let path = package_json.path.parent().unwrap();
|
||||
let package_url = package_json.path.parent().unwrap();
|
||||
// Note: The subpath is not prepended with a dot on purpose
|
||||
// because `package_exports_resolve` matches subpath without the leading dot.
|
||||
if let Some(path) = self.package_exports_resolve(
|
||||
path,
|
||||
package_url,
|
||||
subpath,
|
||||
&package_json.exports,
|
||||
&self.options.condition_names,
|
||||
|
|
@ -606,9 +608,13 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// Note: `package_imports_exports_resolve` does not require the leading dot.
|
||||
let match_key = subpath;
|
||||
// 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
|
||||
if let Some(path) =
|
||||
self.package_imports_exports_resolve(match_key, exports, package_url, conditions)?
|
||||
{
|
||||
if let Some(path) = self.package_imports_exports_resolve(
|
||||
match_key,
|
||||
exports,
|
||||
package_url,
|
||||
/* is_imports */ false,
|
||||
conditions,
|
||||
)? {
|
||||
// 3. If resolved is not null or undefined, return resolved.
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
|
@ -617,12 +623,52 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
Err(ResolveError::PackagePathNotExported(format!(".{subpath}")))
|
||||
}
|
||||
|
||||
/// PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
|
||||
fn package_imports_resolve(
|
||||
&self,
|
||||
cache_value: &CacheValue,
|
||||
specifier: &str,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
// 1. Assert: specifier begins with "#".
|
||||
debug_assert!(specifier.starts_with('#'), "{specifier}");
|
||||
// 2. If specifier is exactly equal to "#" or starts with "#/", then
|
||||
if specifier == "#" || specifier.starts_with("#/") {
|
||||
// 1. Throw an Invalid Module Specifier error.
|
||||
return Err(ResolveError::InvalidModuleSpecifier(specifier.to_string()));
|
||||
}
|
||||
// 3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
|
||||
// 4. If packageURL is not null, then
|
||||
if let Some(package_json) = cache_value.find_package_json(&self.cache.fs)? {
|
||||
// 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() {
|
||||
// 1. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
|
||||
let package_url = package_json.path.parent().unwrap();
|
||||
if let Some(path) = self.package_imports_exports_resolve(
|
||||
specifier,
|
||||
&package_json.imports,
|
||||
package_url,
|
||||
/* is_imports */ true,
|
||||
&self.options.condition_names,
|
||||
)? {
|
||||
// 2. If resolved is not null or undefined, return resolved.
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 5. Throw a Package Import Not Defined error.
|
||||
Err(ResolveError::PackageImportNotDefined(specifier.to_string()))
|
||||
}
|
||||
|
||||
/// PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)
|
||||
fn package_imports_exports_resolve(
|
||||
///
|
||||
/// # Errors
|
||||
pub fn package_imports_exports_resolve(
|
||||
&self,
|
||||
match_key: &str,
|
||||
match_obj: &MatchObject,
|
||||
package_url: &Path,
|
||||
is_imports: bool,
|
||||
conditions: &[String],
|
||||
) -> ResolveState {
|
||||
// enhanced_resolve behaves differently, it throws
|
||||
|
|
@ -640,7 +686,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
match_key,
|
||||
target,
|
||||
None,
|
||||
/* is_imports */ false,
|
||||
is_imports,
|
||||
conditions,
|
||||
);
|
||||
}
|
||||
|
|
@ -690,7 +736,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
best_key,
|
||||
best_target,
|
||||
Some(best_match),
|
||||
/* is_imports */ false,
|
||||
is_imports,
|
||||
conditions,
|
||||
);
|
||||
}
|
||||
|
|
@ -708,6 +754,32 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
is_imports: bool,
|
||||
conditions: &[String],
|
||||
) -> ResolveState {
|
||||
fn normalize_string_target<'a>(
|
||||
target_key: &'a str,
|
||||
target: &'a str,
|
||||
pattern_match: Option<&'a str>,
|
||||
package_url: &Path,
|
||||
) -> Result<Cow<'a, str>, ResolveError> {
|
||||
let target = if let Some(pattern_match) = pattern_match {
|
||||
if !target_key.contains('*') && !target.contains('*') {
|
||||
// enhanced_resolve behaviour
|
||||
// TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json.
|
||||
if target_key.ends_with('/') && target.ends_with('/') {
|
||||
Cow::Owned(format!("{target}{pattern_match}"))
|
||||
} else {
|
||||
return Err(ResolveError::InvalidPackageConfigDirectory(
|
||||
package_url.join("package.json"),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
Cow::Owned(target.replace('*', pattern_match))
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(target)
|
||||
};
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
match target {
|
||||
ExportsField::None => {}
|
||||
// 1. If target is a String, then
|
||||
|
|
@ -723,38 +795,23 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
}
|
||||
// 2. If patternMatch is a String, then
|
||||
// 1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/").
|
||||
// 3. Return PACKAGE_RESOLVE(target, packageURL + "/").
|
||||
let target =
|
||||
normalize_string_target(target_key, target, pattern_match, package_url)?;
|
||||
let package_url = self.cache.value(package_url);
|
||||
// // 3. Return PACKAGE_RESOLVE(target, packageURL + "/").
|
||||
return self.package_resolve(&package_url, &target).map(Some);
|
||||
}
|
||||
// 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
|
||||
let target = if let Some(pattern_match) = pattern_match {
|
||||
if !target_key.contains('*') && !target.contains('*') {
|
||||
// enhanced_resolve behaviour
|
||||
// TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json.
|
||||
if target_key.ends_with('/') && target.ends_with('/') {
|
||||
Cow::Owned(format!("{target}{pattern_match}"))
|
||||
} else {
|
||||
return Err(ResolveError::InvalidPackageConfigDirectory(
|
||||
package_url.join("package.json"),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// if !target.contains('*') && target.ends_with('/') {
|
||||
// Cow::Owned(format!("{target}{pattern_match}"))
|
||||
// } else {
|
||||
Cow::Owned(target.replace('*', pattern_match))
|
||||
}
|
||||
// }
|
||||
} else {
|
||||
Cow::Borrowed(target)
|
||||
};
|
||||
|
||||
if Path::new(target.as_str()).is_invalid_exports_target() {
|
||||
return Err(ResolveError::InvalidPackageTarget(target.to_string()));
|
||||
}
|
||||
// 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
|
||||
// 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
|
||||
let resolved_target = package_url.join(target.as_str()).normalize();
|
||||
// 4. Assert: resolvedTarget is contained in packageURL.
|
||||
// 5. If patternMatch is null, then
|
||||
let target =
|
||||
normalize_string_target(target_key, target, pattern_match, package_url)?;
|
||||
if Path::new(target.as_ref()).is_invalid_exports_target() {
|
||||
return Err(ResolveError::InvalidPackageTarget(target.to_string()));
|
||||
}
|
||||
let resolved_target = package_url.join(target.as_ref()).normalize();
|
||||
// 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error.
|
||||
// 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch.
|
||||
return Ok(Some(self.cache.value(&resolved_target)));
|
||||
|
|
@ -785,7 +842,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
target_key,
|
||||
target_value,
|
||||
pattern_match,
|
||||
/* is_imports */ false,
|
||||
is_imports,
|
||||
conditions,
|
||||
);
|
||||
// 3. If resolved is equal to undefined, continue the loop.
|
||||
|
|
@ -816,7 +873,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
target_key,
|
||||
target_value,
|
||||
pattern_match,
|
||||
/* is_imports */ false,
|
||||
is_imports,
|
||||
conditions,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,19 +38,18 @@ pub struct PackageJson {
|
|||
#[serde(default)]
|
||||
pub exports: 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.
|
||||
/// In addition to the "exports" field, there is a package "imports" field to create private mappings that only apply to import specifiers from within the package itself.
|
||||
///
|
||||
/// <https://nodejs.org/api/packages.html#subpath-imports>
|
||||
#[serde(default)]
|
||||
pub imports: MatchObject,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum BrowserField {
|
||||
String(String),
|
||||
Map(FxIndexMap<PathBuf, serde_json::Value>),
|
||||
}
|
||||
|
||||
/// `matchObj` defined in `PACKAGE_IMPORTS_EXPORTS_RESOLVE`
|
||||
pub type MatchObject = FxIndexMap<ExportsKey, ExportsField>;
|
||||
|
||||
|
|
@ -86,8 +85,8 @@ impl From<&str> for ExportsKey {
|
|||
Self::Main
|
||||
} else if key.starts_with("./") {
|
||||
Self::Pattern(key.trim_start_matches('.').to_string())
|
||||
} else if let Some(key) = key.strip_prefix('#') {
|
||||
Self::Hash(key.to_string())
|
||||
} else if key.starts_with('#') {
|
||||
Self::Pattern(key.to_string())
|
||||
} else {
|
||||
Self::CustomCondition(key.to_string())
|
||||
}
|
||||
|
|
@ -104,6 +103,13 @@ impl<'a, 'de: 'a> Deserialize<'de> for ExportsKey {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum BrowserField {
|
||||
String(String),
|
||||
Map(FxIndexMap<PathBuf, serde_json::Value>),
|
||||
}
|
||||
|
||||
impl PackageJson {
|
||||
pub fn parse(path: PathBuf, json: &str) -> Result<Self, serde_json::Error> {
|
||||
let mut package_json: Self = serde_json::from_str(json)?;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
//! https://github.com/webpack/enhanced-resolve/blob/main/test/exportsField.test.js
|
||||
//!
|
||||
//! The resolution tests are at the bottom of the file.
|
||||
//! The huge exports field test cases are at the bottom of this file.
|
||||
|
||||
use oxc_resolver::{ExportsField, PathUtil, Resolution, ResolveError, ResolveOptions, Resolver};
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
|
|
@ -235,7 +235,7 @@ fn exports_field(value: serde_json::Value) -> ExportsField {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn entry_points() {
|
||||
fn test_cases() {
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "sample #1",
|
||||
|
|
@ -2456,12 +2456,7 @@ fn entry_points() {
|
|||
);
|
||||
} else {
|
||||
for expect in expect {
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Ok(Some(PathBuf::from(expect).normalize())),
|
||||
"{}",
|
||||
&case.name
|
||||
);
|
||||
assert_eq!(resolved, Ok(Some(Path::new(expect).normalize())), "{}", &case.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
1299
crates/oxc_resolver/tests/enhanced_resolve/test/imports_field.rs
Normal file
1299
crates/oxc_resolver/tests/enhanced_resolve/test/imports_field.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@ mod exports_field;
|
|||
mod extension_alias;
|
||||
mod extensions;
|
||||
mod fallback;
|
||||
mod imports_field;
|
||||
mod incorrect_description_file;
|
||||
mod resolve;
|
||||
mod roots;
|
||||
|
|
|
|||
Loading…
Reference in a new issue