mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 20:32:10 +00:00
feat(resolver): implement extension_alias (#556)
This commit is contained in:
parent
2375bc8ccc
commit
05c9b5dfd8
10 changed files with 160 additions and 25 deletions
|
|
@ -6,7 +6,7 @@
|
|||
|------|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| | alias | [] | A list of module alias configurations or an object which maps key to value |
|
||||
| | aliasFields | [] | A list of alias fields in description files |
|
||||
| | extensionAlias | {} | An object which maps extension to extension aliases |
|
||||
| ✅ | extensionAlias | {} | An object which maps extension to extension aliases |
|
||||
| | cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties. |
|
||||
| | cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key |
|
||||
| | conditionNames | [] | A list of exports field condition names |
|
||||
|
|
@ -40,7 +40,7 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve
|
|||
- [ ] browserField.test.js
|
||||
- [ ] dependencies.test.js
|
||||
- [ ] exportsField.test.js
|
||||
- [ ] extension-alias.test.js
|
||||
- [x] extension-alias.test.js
|
||||
- [x] extensions.test.js
|
||||
- [ ] fallback.test.js
|
||||
- ~[ ] forEachBail.test.js~
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ use crate::request::RequestError;
|
|||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ResolveError {
|
||||
NotFound,
|
||||
RequestError(RequestError),
|
||||
JSONError(JSONError),
|
||||
Request(RequestError),
|
||||
ExtensionAlias,
|
||||
JSON(JSONError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
|
@ -19,7 +20,7 @@ pub struct JSONError {
|
|||
|
||||
impl ResolveError {
|
||||
pub(crate) fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self {
|
||||
Self::JSONError(JSONError {
|
||||
Self::JSON(JSONError {
|
||||
path,
|
||||
message: error.to_string(),
|
||||
line: error.line(),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod path;
|
|||
mod request;
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
|
@ -85,7 +86,7 @@ impl Resolver {
|
|||
path: P,
|
||||
request: &str,
|
||||
) -> Result<Resolution, ResolveError> {
|
||||
let request = Request::parse(request).map_err(ResolveError::RequestError)?;
|
||||
let request = Request::parse(request).map_err(ResolveError::Request)?;
|
||||
let path = self.require(path.as_ref(), &request)?;
|
||||
Ok(Resolution {
|
||||
path,
|
||||
|
|
@ -147,6 +148,11 @@ impl Resolver {
|
|||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn load_as_file(&self, path: &Path) -> ResolveState {
|
||||
// enhanced-resolve feature: extension_alias
|
||||
if let Some(path) = self.load_extension_alias(path)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
||||
// 1. If X is a file, load X as its file extension format. STOP
|
||||
if self.fs.is_file(path) {
|
||||
return Ok(Some(path.to_path_buf()));
|
||||
|
|
@ -229,4 +235,28 @@ impl Resolver {
|
|||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Given an extension alias map `{".js": [".ts", "js"]}`,
|
||||
/// load the mapping instead of the provided extension
|
||||
///
|
||||
/// This is an enhanced-resolve feature
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found
|
||||
fn load_extension_alias(&self, path: &Path) -> ResolveState {
|
||||
let Some(path_extension) = path.extension() else { return Ok(None) };
|
||||
let Some((_, extensions)) =
|
||||
self.options.extension_alias.iter().find(|(ext, _)| OsStr::new(ext) == path_extension)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
for extension in extensions {
|
||||
let path_with_extension = path.with_extension(extension);
|
||||
if self.fs.is_file(&path_with_extension) {
|
||||
return Ok(Some(path_with_extension));
|
||||
}
|
||||
}
|
||||
Err(ResolveError::ExtensionAlias)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,56 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ResolveOptions {
|
||||
/// A list of extensions which should be tried for
|
||||
/// Default: `[".js", ".json", ".node"]`
|
||||
pub extensions: Vec<String>,
|
||||
/// An object which maps extension to extension aliases
|
||||
///
|
||||
/// Default `{}`
|
||||
pub extension_alias: Vec<(String, Vec<String>)>,
|
||||
|
||||
/// Enforce that a extension from extensions must be used
|
||||
/// Default: false
|
||||
///
|
||||
/// Default `false`
|
||||
pub enforce_extension: bool,
|
||||
|
||||
/// A list of extensions which should be tried for
|
||||
///
|
||||
/// Default `[".js", ".json", ".node"]`
|
||||
pub extensions: Vec<String>,
|
||||
|
||||
/// A list of main files in directories
|
||||
///
|
||||
/// Default `["index"]`
|
||||
pub main_files: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ResolveOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
extensions: vec![".js".into(), ".json".into(), ".node".into()],
|
||||
extension_alias: vec![],
|
||||
enforce_extension: false,
|
||||
extensions: vec![".js".into(), ".json".into(), ".node".into()],
|
||||
main_files: vec!["index".into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveOptions {
|
||||
pub(crate) fn sanitize(mut self) -> Self {
|
||||
// Remove the leading `.` because `Path::with_extension` does not accept the leading dot.
|
||||
self.extensions = self
|
||||
.extensions
|
||||
self.extensions = Self::remove_leading_dots(self.extensions);
|
||||
self.extension_alias = self
|
||||
.extension_alias
|
||||
.into_iter()
|
||||
.map(|ext| ext.trim_start_matches('.').to_string())
|
||||
.map(|(extension, extensions)| {
|
||||
(Self::remove_leading_dot(&extension), Self::remove_leading_dots(extensions))
|
||||
})
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
// Remove the leading `.` because `Path::with_extension` does not accept the dot.
|
||||
fn remove_leading_dot(s: &str) -> String {
|
||||
s.trim_start_matches('.').to_string()
|
||||
}
|
||||
|
||||
fn remove_leading_dots(v: Vec<String>) -> Vec<String> {
|
||||
v.into_iter().map(|s| Self::remove_leading_dot(&s)).collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/extension-alias.test.js>
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use oxc_resolver::{ResolveError, ResolveOptions, Resolver};
|
||||
|
||||
fn fixture() -> PathBuf {
|
||||
super::fixture().join("extension-alias")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_alias() -> Result<(), ResolveError> {
|
||||
let options = ResolveOptions {
|
||||
extensions: vec![".js".into()],
|
||||
main_files: vec!["index.js".into()],
|
||||
extension_alias: vec![
|
||||
(".js".into(), vec![".ts".into(), ".js".into()]),
|
||||
(".mjs".into(), vec![".mts".into()]),
|
||||
],
|
||||
..ResolveOptions::default()
|
||||
};
|
||||
let resolver = Resolver::new(options);
|
||||
let f = fixture();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let pass = [
|
||||
("should alias fully specified file", f.clone(), "./index.js", f.join("index.ts")),
|
||||
("should alias fully specified file when there are two alternatives", f.clone(), "./dir/index.js", f.join("dir/index.ts")),
|
||||
("should also allow the second alternative", f.clone(), "./dir2/index.js", f.join("dir2/index.js")),
|
||||
("should support alias option without an array", f.clone(), "./dir2/index.mjs", f.join("dir2/index.mts")),
|
||||
];
|
||||
|
||||
for (comment, path, request, expected) in pass {
|
||||
let resolution = resolver.resolve(&path, request)?;
|
||||
let resolved_path = resolution.path();
|
||||
assert_eq!(resolved_path, expected, "{comment} {path:?} {request}");
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
let fail = [
|
||||
("should not allow to fallback to the original extension or add extensions", f, "./index.mjs"),
|
||||
];
|
||||
|
||||
for (comment, path, request) in fail {
|
||||
let resolution = resolver.resolve(&path, request);
|
||||
assert_eq!(resolution, Err(ResolveError::ExtensionAlias), "{comment} {path:?} {request}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
// should not apply extension alias to extensions or mainFiles field
|
||||
fn not_apply_to_extension_nor_main_files() -> Result<(), ResolveError> {
|
||||
let options = ResolveOptions {
|
||||
extensions: vec![".js".into()],
|
||||
main_files: vec!["index.js".into()],
|
||||
extension_alias: vec![(".js".into(), vec![])],
|
||||
..ResolveOptions::default()
|
||||
};
|
||||
let resolver = Resolver::new(options);
|
||||
let f = fixture();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let pass = [
|
||||
("directory", f.clone(), "./dir2", f.join("dir2/index.js")),
|
||||
("file", f.clone(), "./dir2/index", f.join("dir2/index.js")),
|
||||
];
|
||||
|
||||
for (comment, path, request, expected) in pass {
|
||||
let resolution = resolver.resolve(&path, request)?;
|
||||
let resolved_path = resolution.path();
|
||||
assert_eq!(resolved_path, expected, "{comment} {path:?} {request}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -74,8 +74,9 @@ fn respect_enforce_extension() {
|
|||
let fixture = fixture();
|
||||
|
||||
let options = ResolveOptions {
|
||||
extensions: vec![".ts".into(), String::new(), ".js".into()],
|
||||
enforce_extension: false,
|
||||
extensions: vec![".ts".into(), String::new(), ".js".into()],
|
||||
..ResolveOptions::default()
|
||||
};
|
||||
|
||||
let resolver = Resolver::new(options);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod extension_alias;
|
||||
mod extensions;
|
||||
mod resolve;
|
||||
mod simple;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use oxc_resolver::{ResolveError, Resolver};
|
||||
|
||||
#[test]
|
||||
fn test() -> Result<(), ResolveError> {
|
||||
fn resolve() -> Result<(), ResolveError> {
|
||||
let f = super::fixture();
|
||||
let resolver = Resolver::default();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,19 @@ use std::env;
|
|||
use oxc_resolver::{ResolveError, Resolver};
|
||||
|
||||
#[test]
|
||||
fn test() -> Result<(), ResolveError> {
|
||||
fn simple() -> Result<(), ResolveError> {
|
||||
let resolver = Resolver::default();
|
||||
|
||||
// mimic `enhanced-resolve/test/simple.test.js`
|
||||
let dirname = env::current_dir().unwrap().join("tests/enhanced_resolve/test/");
|
||||
|
||||
let data = [
|
||||
(dirname.clone(), "../lib/index", "direct"),
|
||||
(dirname.clone(), "..", "as directory"),
|
||||
(dirname.join("../../").canonicalize().unwrap(), "./enhanced_resolve", "as module"),
|
||||
("direct", dirname.clone(), "../lib/index"),
|
||||
("as directory", dirname.clone(), ".."),
|
||||
("as module", dirname.join("../../").canonicalize().unwrap(), "./enhanced_resolve"),
|
||||
];
|
||||
|
||||
let resolver = Resolver::default();
|
||||
|
||||
for (path, request, comment) in data {
|
||||
for (comment, path, request) in data {
|
||||
let resolution = resolver.resolve(&path, request)?;
|
||||
let resolved_path = resolution.path().canonicalize().unwrap();
|
||||
let expected = dirname.join("../lib/index.js").canonicalize().unwrap();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ fn broken_json() {
|
|||
let dir = env::current_dir().unwrap().join("tests/error_handling/");
|
||||
let resolver = Resolver::default();
|
||||
let resolution = resolver.resolve(&dir, "./broken_package_json");
|
||||
let error = ResolveError::JSONError(JSONError {
|
||||
let error = ResolveError::JSON(JSONError {
|
||||
path: dir.join("broken_package_json").join("package.json"),
|
||||
message: String::from("expected value at line 1 column 1"),
|
||||
line: 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue