feat(resolver): implement extension_alias (#556)

This commit is contained in:
Boshen 2023-07-16 16:39:51 +08:00 committed by GitHub
parent 2375bc8ccc
commit 05c9b5dfd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 25 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
mod extension_alias;
mod extensions;
mod resolve;
mod simple;

View file

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

View file

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

View file

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