feat(resolver): implement the basics of path alias (#564)

This commit is contained in:
Boshen 2023-07-18 13:30:14 +08:00 committed by GitHub
parent 3cbec3459a
commit 0c17f4f783
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 279 additions and 67 deletions

View file

@ -9,7 +9,7 @@
| Done | Field | Default | Description |
|------|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| | alias | [] | A list of module alias configurations or an object which maps key to value |
| | 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 |
| | 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. |
@ -24,7 +24,7 @@
| | 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 |
| ✅ | 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 |
| | 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 |
| | resolver | undefined | A prepared Resolver to which the plugins are attached |
| | resolveToContext | false | Resolve to a context instead of a file |
@ -41,7 +41,7 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve
- [ ] CachedInputFileSystem.test.js
- [ ] SyncAsyncFileSystemDecorator.test.js
- [ ] alias.test.js
- [x] alias.test.js (partially done)
- [x] browserField.test.js (reading the browser field is currently static - not read from the `browserField` option)
- [ ] dependencies.test.js
- [ ] exportsField.test.js

View file

@ -26,6 +26,9 @@ pub enum ResolveError {
/// All of the aliased extension are not found
ExtensionAlias,
/// All of the aliases are not found
Alias(String),
/// JSON parse error
JSON(JSONError),
}

View file

@ -16,6 +16,7 @@ mod request;
mod resolution;
use std::{
borrow::Cow,
ffi::OsStr,
path::{Path, PathBuf},
};
@ -24,7 +25,7 @@ pub use crate::{
cache::Cache,
error::{JSONError, ResolveError},
file_system::{FileMetadata, FileSystem, FileSystemOs},
options::ResolveOptions,
options::{AliasValue, ResolveOptions},
resolution::Resolution,
};
use crate::{
@ -66,10 +67,15 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
pub fn resolve<P: AsRef<Path>>(
&self,
path: P,
request: &str,
request_str: &str,
) -> Result<Resolution, ResolveError> {
let request = Request::parse(request).map_err(ResolveError::Request)?;
let path = self.require(path.as_ref(), &request)?;
let path = path.as_ref();
let request = Request::parse(request_str).map_err(ResolveError::Request)?;
let path = if let Some(path) = self.load_alias(path, request.path.as_str())? {
path
} else {
self.require(path, &request)?
};
Ok(Resolution {
path,
query: request.query.map(ToString::to_string),
@ -114,6 +120,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
return Err(ResolveError::NotFound(path.into_boxed_path()));
}
// 4. If X begins with '#'
RequestPath::Hash(hash_path) => {
request_str = hash_path;
}
// a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
RequestPath::Module(module_path) => {
request_str = module_path;
@ -159,13 +168,19 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
#[allow(clippy::unnecessary_wraps)]
fn load_index(&self, path: &Path) -> ResolveState {
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
// 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
// 3. If X/index.node is a file, load X/index.node as binary addon. STOP
for extension in &self.options.extensions {
let index_path = path.join("index").with_extension(extension);
if self.cache.is_file(&index_path) {
return Ok(Some(index_path));
for main_field in &self.options.main_files {
let main_path = path.join(main_field);
if !self.options.enforce_extension && self.cache.is_file(&main_path) {
return Ok(Some(main_path));
}
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
// 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
// 3. If X/index.node is a file, load X/index.node as binary addon. STOP
for extension in &self.options.extensions {
let main_path_with_extension = main_path.with_extension(extension);
if self.cache.is_file(&main_path_with_extension) {
return Ok(Some(main_path_with_extension));
}
}
}
Ok(None)
@ -200,7 +215,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
fn load_node_modules(&self, start: &Path, request_str: &str) -> ResolveState {
// 1. let DIRS = NODE_MODULES_PATHS(START)
let dirs = Self::node_module_paths(start);
let dirs = self.node_module_paths(start);
// 2. for each DIR in DIRS:
for node_module_path in dirs {
let node_module_path = node_module_path.join(request_str);
@ -224,10 +239,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Ok(None)
}
fn node_module_paths(path: &Path) -> impl Iterator<Item = PathBuf> + '_ {
fn node_module_paths<'a>(&'a self, path: &'a Path) -> impl Iterator<Item = PathBuf> + 'a {
path.ancestors()
.filter(|path| path.file_name().is_some_and(|name| name != "node_modules"))
.map(|path| path.join("node_modules"))
.flat_map(|path| self.options.modules.iter().map(|module| path.join(module)))
}
fn load_package_self(&self, path: &Path, request_str: &str) -> ResolveState {
@ -247,6 +261,37 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Ok(None)
}
fn load_alias(&self, path: &Path, request_str: &str) -> ResolveState {
for (alias, requests) in &self.options.alias {
let exact_match = alias.strip_prefix(request_str).is_some_and(|c| c == "$");
if request_str.starts_with(alias) || exact_match {
for request in requests {
match request {
AliasValue::Path(new_request) => {
let new_request = if exact_match {
Cow::Borrowed(new_request)
} else {
Cow::Owned(request_str.replacen(alias, new_request, 1))
};
let new_request =
Request::parse(&new_request).map_err(ResolveError::Request)?;
match self.require(path, &new_request) {
Err(ResolveError::NotFound(_)) => { /* noop */ }
Ok(path) => return Ok(Some(path)),
Err(err) => return Err(err),
}
}
AliasValue::Ignore => {
return Err(ResolveError::Ignored(path.join(alias).into_boxed_path()));
}
}
}
return Err(ResolveError::Alias(alias.clone()));
}
}
Ok(None)
}
/// Given an extension alias map `{".js": [".ts", "js"]}`,
/// load the mapping instead of the provided extension
///

View file

@ -1,5 +1,17 @@
#[derive(Debug, Clone)]
pub enum AliasValue {
/// The path value
Path(String),
/// The `false` value
Ignore,
}
#[derive(Debug, Clone)]
pub struct ResolveOptions {
/// A list of module alias configurations or an object which maps key to value
pub alias: Vec<(String, Vec<AliasValue>)>,
/// A list of alias fields in description files.
/// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec).
///
@ -25,16 +37,23 @@ pub struct ResolveOptions {
///
/// Default `["index"]`
pub main_files: Vec<String>,
/// A list of directories to resolve modules from, can be absolute path or folder name
///
/// Default `["node_modules"]`
pub modules: Vec<String>,
}
impl Default for ResolveOptions {
fn default() -> Self {
Self {
alias: vec![],
alias_fields: vec![],
extension_alias: vec![],
enforce_extension: false,
extensions: vec![".js".into(), ".json".into(), ".node".into()],
main_files: vec!["index".into()],
modules: vec!["node_modules".into()],
}
}
}

View file

@ -16,21 +16,53 @@ pub enum RequestPath<'a> {
Absolute(&'a str),
/// `./path`, `../path`
Relative(&'a str),
/// `#path`
Hash(&'a str),
/// `path`, `@scope/path`
Module(&'a str),
}
impl<'a> RequestPath<'a> {
pub fn as_str(&self) -> &str {
match self {
Self::Absolute(s) | Self::Relative(s) | Self::Hash(s) | Self::Module(s) => s,
}
}
}
impl<'a> Request<'a> {
pub fn parse(request: &'a str) -> Result<Request<'a>, RequestError> {
let (request, query, fragment) = Self::parse_query_framgment(request);
RequestPath::parse(request).map(|path| Self { path, query, fragment })
if request.is_empty() {
return Err(RequestError::Empty);
}
let (path, query, fragment) = match request.as_bytes()[0] {
b'/' => {
let (path, query, fragment) = Self::parse_query_framgment(request, 1);
(RequestPath::Absolute(path), query, fragment)
}
b'.' => {
let (path, query, fragment) = Self::parse_query_framgment(request, 1);
(RequestPath::Relative(path), query, fragment)
}
b'#' => {
let (path, query, fragment) = Self::parse_query_framgment(request, 1);
(RequestPath::Hash(path), query, fragment)
}
_ => {
let (path, query, fragment) = Self::parse_query_framgment(request, 0);
(RequestPath::Module(path), query, fragment)
}
};
Ok(Self { path, query, fragment })
}
fn parse_query_framgment(request: &str) -> (&str, Option<&str>, Option<&str>) {
fn parse_query_framgment(request: &str, skip: usize) -> (&str, Option<&str>, Option<&str>) {
let mut query_start: Option<usize> = None;
let mut fragment_start: Option<usize> = None;
for (i, c) in request.as_bytes().iter().enumerate() {
for (i, c) in request.as_bytes().iter().enumerate().skip(skip) {
match *c {
b'?' => query_start = Some(i),
b'#' => fragment_start = Some(i),
@ -52,17 +84,6 @@ impl<'a> Request<'a> {
}
}
impl<'a> RequestPath<'a> {
fn parse(request: &'a str) -> Result<Self, RequestError> {
match request.chars().next() {
Some('/') => Ok(Self::Absolute(request)),
Some('.') => Ok(Self::Relative(request)),
Some(_) => Ok(Self::Module(request)),
_ => Err(RequestError::Empty),
}
}
}
#[cfg(test)]
mod tests {
use super::{Request, RequestError, RequestPath};
@ -81,18 +102,38 @@ mod tests {
#[test]
fn absolute() -> Result<(), RequestError> {
let request = "/test";
let request = "/test?#";
let parsed = Request::parse(request)?;
assert_eq!(parsed.path, RequestPath::Absolute(request));
assert_eq!(parsed.path, RequestPath::Absolute("/test"));
assert_eq!(parsed.query, Some("?"));
assert_eq!(parsed.fragment, Some("#"));
Ok(())
}
#[test]
fn parse_relative() -> Result<(), RequestError> {
let requests = ["./test", "../test", "../../test"];
fn relative() -> Result<(), RequestError> {
let requests = ["./test", "../test?#", "../../test?#"];
for request in requests {
let parsed = Request::parse(request)?;
let mut r = request.to_string();
r.push_str("?#");
let parsed = Request::parse(&r)?;
assert_eq!(parsed.path, RequestPath::Relative(request));
assert_eq!(parsed.query, Some("?"));
assert_eq!(parsed.fragment, Some("#"));
}
Ok(())
}
#[test]
fn hash() -> Result<(), RequestError> {
let requests = ["#", "#path"];
for request in requests {
let mut r = request.to_string();
r.push_str("?#");
let parsed = Request::parse(&r)?;
assert_eq!(parsed.path, RequestPath::Hash(request));
assert_eq!(parsed.query, Some("?"));
assert_eq!(parsed.fragment, Some("#"));
}
Ok(())
}
@ -101,31 +142,38 @@ mod tests {
fn module() -> Result<(), RequestError> {
let requests = ["module"];
for request in requests {
let parsed = Request::parse(request)?;
let mut r = request.to_string();
r.push_str("?#");
let parsed = Request::parse(&r)?;
assert_eq!(parsed.path, RequestPath::Module(request));
assert_eq!(parsed.query, Some("?"));
assert_eq!(parsed.fragment, Some("#"));
}
Ok(())
}
#[test]
fn query_fragment() -> Result<(), RequestError> {
assert_eq!(Request::parse("a?")?.query, Some("?"));
assert_eq!(Request::parse("a?b")?.query, Some("?b"));
let data = [
("a?", Some("?"), None),
("a?query", Some("?query"), None),
("a#", None, Some("#")),
("a#fragment", None, Some("#fragment")),
("a?#", Some("?"), Some("#")),
("a?#fragment", Some("?"), Some("#fragment")),
("a?query#", Some("?query"), Some("#")),
("a?query#fragment", Some("?query"), Some("#fragment")),
("a#fragment?", Some("?"), Some("#fragment")),
("a#fragment?query", Some("?query"), Some("#fragment")),
];
assert_eq!(Request::parse("a#")?.fragment, Some("#"));
assert_eq!(Request::parse("a#b")?.fragment, Some("#b"));
for (request_str, query, fragment) in data {
let request = Request::parse(request_str)?;
assert_eq!(request.path.as_str(), "a", "{request_str}");
assert_eq!(request.query, query, "{request_str}");
assert_eq!(request.fragment, fragment, "{request_str}");
}
let request = Request::parse("a?#")?;
assert_eq!(request.query, Some("?"));
assert_eq!(request.fragment, Some("#"));
let request = Request::parse("a?b#c")?;
assert_eq!(request.query, Some("?b"));
assert_eq!(request.fragment, Some("#c"));
let request = Request::parse("a#b?c")?;
assert_eq!(request.query, Some("?c"));
assert_eq!(request.fragment, Some("#b"));
Ok(())
}
}

View file

@ -12,19 +12,35 @@ pub struct Resolution {
}
impl Resolution {
/// Returns the path without query and fragment
pub fn path(&self) -> &Path {
&self.path
}
/// Returns the path without query and fragment
pub fn into_path_buf(self) -> PathBuf {
self.path
}
/// Returns the path query `?query`, contains the leading `?`
pub fn query(&self) -> Option<&str> {
self.query.as_deref()
}
/// Returns the path fragment `#fragment`, contains the leading `#`
pub fn fragment(&self) -> Option<&str> {
self.fragment.as_deref()
}
/// Returns the full path with query and fragment
pub fn full_path(self) -> PathBuf {
let mut path = self.path.into_os_string();
if let Some(query) = &self.query {
path.push(query);
}
if let Some(fragment) = &self.fragment {
path.push(fragment);
}
PathBuf::from(path)
}
}

View file

@ -1,11 +1,16 @@
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/alias.test.js>
use oxc_resolver::{ResolveOptions, ResolverGeneric};
use std::path::Path;
use oxc_resolver::{AliasValue, ResolveError, ResolveOptions, Resolver, ResolverGeneric};
use crate::MemoryFS;
#[test]
fn alias() {
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
fn alias() -> Result<(), ResolveError> {
let f = Path::new("/");
let file_system = MemoryFS::new(&[
("/a/index", ""),
("/a/dir/index", ""),
@ -21,7 +26,91 @@ fn alias() {
("/e/anotherDir/index", ""),
("/e/dir/file", ""),
]);
let options = ResolveOptions::default();
#[rustfmt::skip]
let options = ResolveOptions {
alias: vec![
("aliasA".into(), vec![AliasValue::Path("a".into())]),
("b$".into(), vec![AliasValue::Path("a/index".into())]),
("c$".into(), vec![AliasValue::Path("/a/index".into())]),
("multiAlias".into(), vec![AliasValue::Path("b".into()), AliasValue::Path("c".into()), AliasValue::Path("d".into()), AliasValue::Path("e".into()), AliasValue::Path("a".into())]),
("recursive".into(), vec![AliasValue::Path("recursive/dir".into())]),
("/d/dir".into(), vec![AliasValue::Path("/c/dir".into())]),
("/d/index.js".into(), vec![AliasValue::Path("/c/index".into())]),
// alias configuration should work
("#".into(), vec![AliasValue::Path("/c/dir".into())]),
("@".into(), vec![AliasValue::Path("/c/dir".into())]),
("ignored".into(), vec![AliasValue::Ignore]),
],
modules: vec!["/".into()],
..ResolveOptions::default()
};
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(options, file_system);
assert!(resolver.resolve("/a/index", ".").is_ok());
#[rustfmt::skip]
let pass = [
("should resolve a not aliased module 1", "a", "/a/index"),
("should resolve a not aliased module 2", "a/index", "/a/index"),
("should resolve a not aliased module 3", "a/dir", "/a/dir/index"),
("should resolve a not aliased module 4", "a/dir/index", "/a/dir/index"),
("should resolve an aliased module 1", "aliasA", "/a/index"),
("should resolve an aliased module 2", "aliasA/index", "/a/index"),
("should resolve an aliased module 3", "aliasA/dir", "/a/dir/index"),
("should resolve an aliased module 4", "aliasA/dir/index", "/a/dir/index"),
("should resolve '#' alias 1", "#", "/c/dir/index"),
("should resolve '#' alias 2", "#/index", "/c/dir/index"),
("should resolve '@' alias 1", "@", "/c/dir/index"),
("should resolve '@' alias 2", "@/index", "/c/dir/index"),
("should resolve a recursive aliased module 1", "recursive", "/recursive/dir/index"),
("should resolve a recursive aliased module 2", "recursive/index", "/recursive/dir/index"),
// TODO recursive
// ("should resolve a recursive aliased module 3", "recursive/dir", "/recursive/dir/index"),
// ("should resolve a recursive aliased module 4", "recursive/dir/index", "/recursive/dir/index"),
("should resolve a file aliased module 1", "b", "/a/index"),
("should resolve a file aliased module 2", "c", "/a/index"),
("should resolve a file aliased module with a query 1", "b?query", "/a/index?query"),
("should resolve a file aliased module with a query 1", "c?query", "/a/index?query"),
("should resolve a path in a file aliased module 1", "b/index", "/b/index"),
("should resolve a path in a file aliased module 2", "b/dir", "/b/dir/index"),
("should resolve a path in a file aliased module 3", "b/dir/index", "/b/dir/index"),
("should resolve a path in a file aliased module 4", "c/index", "/c/index"),
("should resolve a path in a file aliased module 5", "c/dir", "/c/dir/index"),
("should resolve a path in a file aliased module 6", "c/dir/index", "/c/dir/index"),
// TODO aliased file
// ("should resolve a file aliased file 1", "d", "/c/index"),
// ("should resolve a file aliased file 2", "d/dir/index", "/c/dir/index"),
("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "/e/dir/file"),
("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "/e/anotherDir/index"),
];
for (comment, request, expected) in pass {
let resolution = resolver.resolve(f, request)?;
assert_eq!(resolution.full_path(), Path::new(expected), "{comment} {request}");
}
#[rustfmt::skip]
let ignore = [
("should resolve an ignore module", "ignored", ResolveError::Ignored(f.join("ignored").into_boxed_path()))
];
for (comment, request, expected) in ignore {
let resolution = resolver.resolve(f, request);
assert_eq!(resolution, Err(expected), "{comment} {request}");
}
Ok(())
}
#[test]
#[ignore = "TODO: absolute path"]
fn absolute_path() {
let f = super::fixture();
let resolver = Resolver::new(ResolveOptions {
alias: vec![(f.join("foo").to_str().unwrap().to_string(), vec![AliasValue::Ignore])],
modules: vec![f.clone().to_str().unwrap().to_string()],
..ResolveOptions::default()
});
let resolution = resolver.resolve(&f, "foo/index");
assert_eq!(resolution, Err(ResolveError::Ignored(f.into_boxed_path())));
}

View file

@ -44,15 +44,7 @@ fn resolve() -> Result<(), ResolveError> {
for (comment, path, request, expected) in data {
let resolution = resolver.resolve(&path, request)?;
let mut file_name = resolution.path().file_name().unwrap().to_string_lossy().to_string();
if let Some(query) = resolution.query() {
file_name.push_str(query);
}
if let Some(fragment) = resolution.fragment() {
file_name.push_str(fragment);
}
let resolved_path = resolution.path().with_file_name(file_name);
assert_eq!(resolved_path, expected, "{comment} {path:?} {request}");
assert_eq!(resolution.full_path(), expected, "{comment} {path:?} {request}");
}
Ok(())