use crate::{
description::DescriptionData,
info::NormalizedPath,
kind::PathKind,
log::color,
plugin::{
BrowserFieldPlugin, ExportsFieldPlugin, ExtensionAliasPlugin, ImportsFieldPlugin,
MainFieldPlugin, MainFilePlugin, Plugin,
},
Context, EnforceExtension, Info, ResolveResult, Resolver, State,
};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
impl Resolver {
fn resolve_file_with_ext(&self, mut path: PathBuf, info: Info) -> State {
let v = unsafe { &mut *(&mut path as *mut PathBuf as *mut Vec<u8>) };
for ext in &self.options.extensions {
v.extend_from_slice(ext.as_bytes());
if self.load_entry(path.as_ref()).is_file() {
return State::Success(ResolveResult::Resource(
info.with_path(path).with_target(""),
));
}
unsafe {
v.set_len(v.len() - ext.len());
}
}
tracing::debug!(
"'{}[{}]' is not a file",
color::red(&path.display()),
color::red(&self.options.extensions.join("|"))
);
State::Resolving(info)
}
pub(crate) fn resolve_as_context(&self, info: Info, context: &Context) -> State {
if !context.resolve_to_context.get() {
return State::Resolving(info);
}
let path = info.to_resolved_path();
tracing::debug!(
"Attempting to load '{}' as a context",
color::blue(&path.display())
);
if self.load_entry(&path).is_dir() {
State::Success(ResolveResult::Resource(Info::new(path, Default::default())))
} else {
State::Failed(info)
}
}
pub(crate) fn resolve_as_fully_specified(&self, info: Info, context: &mut Context) -> State {
let fully_specified = context.fully_specified.get();
if !fully_specified {
return State::Resolving(info);
}
let path = info.to_resolved_path();
let request = info.request();
let target = request.target();
if self.load_entry(&path).is_file() {
let path = path.to_path_buf();
State::Success(ResolveResult::Resource(
info.with_path(path).with_target(""),
))
} else if matches!(
request.kind(),
PathKind::AbsolutePosix | PathKind::AbsoluteWin | PathKind::Relative
) || split_slash_from_request(target).is_some()
{
State::Failed(info)
} else {
let dir = path.to_path_buf();
let info = info.with_path(dir).with_target(".");
context.fully_specified.set(false);
let state = self._resolve(info.clone(), context);
context.fully_specified.set(true);
if state.is_finished() {
state
} else {
State::Failed(info)
}
}
}
pub(crate) fn resolve_as_file(&self, info: Info, context: &mut Context) -> State {
if info.request().is_directory() {
return State::Resolving(info);
}
self.options
.extension_alias
.iter()
.fold(State::Resolving(info), |state, (extension, alias_list)| {
state.then(|info| {
ExtensionAliasPlugin::new(extension, alias_list).apply(self, info, context)
})
})
.then(|info| {
let path = info.to_resolved_path().to_path_buf();
tracing::debug!(
"Attempting to load '{}' as a file",
color::blue(&path.display())
);
if matches!(self.options.enforce_extension, EnforceExtension::Enabled) {
self.resolve_file_with_ext(path, info)
} else if self.load_entry(&path).is_file() {
State::Success(ResolveResult::Resource(
info.with_path(path).with_target(""),
))
} else {
self.resolve_file_with_ext(path, info)
}
})
}
pub(crate) fn resolve_as_dir(&self, info: Info, context: &mut Context) -> State {
let dir = info.to_resolved_path();
let entry = self.load_entry(&dir);
if !entry.is_dir() {
return State::Failed(info);
}
let pkg_info = match entry.pkg_info(self) {
Ok(pkg_info) => pkg_info,
Err(err) => return State::Error(err),
};
if let Some(pkg_info) = pkg_info {
MainFieldPlugin::new(pkg_info).apply(self, info, context)
} else {
State::Resolving(info)
}
.then(|info| MainFilePlugin.apply(self, info, context))
}
pub(crate) fn resolve_as_modules(&self, info: Info, context: &mut Context) -> State {
let original_dir = info.normalized_path();
for module in &self.options.modules {
let node_modules_path = Path::new(module);
let (node_modules_path, need_find_up) = if node_modules_path.is_absolute() {
(Cow::Borrowed(node_modules_path), false)
} else {
(Cow::Owned(original_dir.as_ref().join(module)), true)
};
let state = self
._resolve_as_modules(info.clone(), original_dir, &node_modules_path, context)
.then(|info| {
if !need_find_up {
State::Resolving(info)
} else if let Some(parent_dir) = original_dir.as_ref().parent() {
self._resolve(info.with_path(parent_dir), context)
} else {
State::Resolving(info)
}
});
if state.is_finished() {
return state;
}
}
State::Failed(info)
}
fn _resolve_as_modules(
&self,
info: Info,
original_dir: &NormalizedPath,
node_modules_path: &Path,
context: &mut Context,
) -> State {
let entry = self.load_entry(node_modules_path);
let pkg_info = match entry.pkg_info(self) {
Ok(pkg_info) => pkg_info.as_ref(),
Err(err) => return State::Error(err),
};
let state = if entry.is_dir() {
self.resolve_node_modules(info, node_modules_path, context)
.then(|info| {
let is_resolve_self = pkg_info.map_or(false, |pkg_info| {
let request_module_name =
get_module_name_from_request(info.request().target());
is_resolve_self(pkg_info, request_module_name)
});
if is_resolve_self {
let pkg_info = pkg_info.unwrap();
ExportsFieldPlugin::new(pkg_info).apply(self, info, context)
} else {
State::Resolving(info)
}
})
} else if pkg_info.map_or(false, |pkg_info| pkg_info.dir().eq(original_dir)) {
let request_module_name = get_module_name_from_request(info.request().target());
if is_resolve_self(pkg_info.unwrap(), request_module_name) {
ExportsFieldPlugin::new(pkg_info.unwrap()).apply(self, info, context)
} else {
State::Resolving(info)
}
} else {
State::Resolving(info)
};
state
}
fn resolve_node_modules(
&self,
info: Info,
node_modules_path: &Path,
context: &mut Context,
) -> State {
let original_dir = info.normalized_path();
let request_module_name = get_module_name_from_request(info.request().target());
let module_path = node_modules_path.join(request_module_name);
let entry = self.load_entry(&module_path);
let module_info = Info::new(node_modules_path, info.request().clone());
if !entry.is_dir() {
let state = self.resolve_as_file(module_info, context);
if state.is_finished() {
state
} else {
State::Resolving(info)
}
} else {
let pkg_info = match entry.pkg_info(self) {
Ok(pkg_info) => pkg_info,
Err(err) => return State::Error(err),
};
let state = if let Some(pkg_info) = pkg_info {
let out_node_modules = pkg_info.dir().eq(original_dir);
if !out_node_modules || is_resolve_self(pkg_info, request_module_name) {
ExportsFieldPlugin::new(pkg_info).apply(self, module_info, context)
} else {
State::Resolving(module_info)
}
.then(|info| ImportsFieldPlugin::new(pkg_info).apply(self, info, context))
.then(|info| MainFieldPlugin::new(pkg_info).apply(self, info, context))
.then(|info| BrowserFieldPlugin::new(pkg_info, true).apply(self, info, context))
} else {
State::Resolving(module_info)
}
.then(|info| self.resolve_as_context(info, context))
.then(|info| self.resolve_as_fully_specified(info, context))
.then(|info| self.resolve_as_file(info, context))
.then(|info| self.resolve_as_dir(info, context));
match state {
State::Failed(info) => State::Resolving(info),
_ => state,
}
}
}
}
fn is_resolve_self(pkg_info: &DescriptionData, request_module_name: &str) -> bool {
pkg_info
.data()
.name()
.map(|pkg_name| request_module_name == pkg_name)
.map_or(false, |ans| ans)
}
pub(crate) fn split_slash_from_request(target: &str) -> Option<usize> {
let has_namespace_scope = target.starts_with('@');
let chars = target.chars().enumerate();
let slash_index_list: Vec<usize> = chars
.filter_map(|(index, char)| if '/' == char { Some(index) } else { None })
.collect();
if has_namespace_scope {
slash_index_list.get(1)
} else {
slash_index_list.first()
}
.copied()
}
fn get_module_name_from_request(target: &str) -> &str {
split_slash_from_request(target).map_or(target, |index| &target[0..index])
}
pub(crate) fn get_path_from_request(target: &str) -> Option<Cow<str>> {
split_slash_from_request(target).map(|index| Cow::Borrowed(&target[index..]))
}
#[cfg(test)]
mod test {
use super::{get_module_name_from_request, get_path_from_request, split_slash_from_request};
#[test]
fn test_split_slash_from_request() {
assert_eq!(split_slash_from_request("a"), None);
assert_eq!(split_slash_from_request("a/b"), Some(1));
assert_eq!(split_slash_from_request("@a"), None);
assert_eq!(split_slash_from_request("@a/b"), None);
assert_eq!(split_slash_from_request("@a/b/c"), Some(4));
}
#[test]
fn test_get_module_name_from_request() {
assert_eq!(get_module_name_from_request("a"), "a");
assert_eq!(get_module_name_from_request("a/b"), "a");
assert_eq!(get_module_name_from_request("@a"), "@a");
assert_eq!(get_module_name_from_request("@a/b"), "@a/b");
assert_eq!(get_module_name_from_request("@a/b/c"), "@a/b");
}
#[test]
fn test_get_path_from_request() {
assert_eq!(get_path_from_request("a"), None);
assert_eq!(get_path_from_request("a/b"), Some("/b".into()));
assert_eq!(get_path_from_request("@a"), None);
assert_eq!(get_path_from_request("@a/b"), None);
assert_eq!(get_path_from_request("@a/b/c"), Some("/c".into()));
}
}