use std::{
fmt::{self, Debug, Display},
io,
path::PathBuf,
sync::Arc,
};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum ResolveError {
#[error("Path is ignored {0}")]
Ignored(PathBuf),
#[error("Cannot find module '{0}'")]
NotFound( String),
#[error("Cannot find module '{0}' for matched aliased key '{1}'")]
MatchedAliasNotFound( String, String),
#[error("Tsconfig not found {0}")]
TsconfigNotFound(PathBuf),
#[error("Tsconfig's project reference path points to this tsconfig {0}")]
TsconfigSelfReference(PathBuf),
#[error("Tsconfig extends configs circularly: {0}")]
TsconfigCircularExtend(CircularPathBufs),
#[error("{0}")]
IOError(IOError),
#[error("Path {0:?} contains unsupported construct.")]
PathNotSupported(PathBuf),
#[error("Builtin module {resolved}")]
Builtin { resolved: String, is_runtime_module: bool },
#[error("Cannot resolve '{0}' for extension aliases '{1}' in '{2}'")]
ExtensionAlias(
String,
String,
PathBuf,
),
#[error("{0}")]
Specifier(SpecifierError),
#[error("{0:?}")]
Json(JSONError),
#[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)]
InvalidModuleSpecifier(String, PathBuf),
#[error(r#"Invalid "exports" target "{0}" defined for '{1}' in the package config {2}"#)]
InvalidPackageTarget(String, String, PathBuf),
#[error(r#""{subpath}" is not exported under {conditions} from package {package_path} (see exports field in {package_json_path})"#)]
PackagePathNotExported {
subpath: String,
package_path: PathBuf,
package_json_path: PathBuf,
conditions: ConditionNames,
},
#[error(r#"Invalid package config "{0}", "exports" cannot contain some keys starting with '.' and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only."#)]
InvalidPackageConfig(PathBuf),
#[error(r#"Default condition should be last one in "{0}""#)]
InvalidPackageConfigDefault(PathBuf),
#[error(r#"Expecting folder to folder mapping. "{0}" should end with "/"#)]
InvalidPackageConfigDirectory(PathBuf),
#[error(r#"Package import specifier "{0}" is not defined in package {1}"#)]
PackageImportNotDefined(String, PathBuf),
#[error("{0} is unimplemented")]
Unimplemented(&'static str),
#[error("Recursion in resolving")]
Recursion,
#[cfg(feature = "yarn_pnp")]
#[error("Failed to find yarn pnp manifest in {0}.")]
FailedToFindYarnPnpManifest(PathBuf),
#[cfg(feature = "yarn_pnp")]
#[error("{0}")]
YarnPnpError(pnp::Error),
}
impl ResolveError {
#[must_use]
pub const fn is_ignore(&self) -> bool {
matches!(self, Self::Ignored(_))
}
#[cold]
#[must_use]
pub fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self {
Self::Json(JSONError {
path,
message: error.to_string(),
line: error.line(),
column: error.column(),
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Error)]
pub enum SpecifierError {
#[error("The specifiers must be a non-empty string. Received \"{0}\"")]
Empty(String),
}
#[derive(Debug, Clone, Eq, PartialEq, Error)]
#[error("{message}")]
pub struct JSONError {
pub path: PathBuf,
pub message: String,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Error)]
#[error("{0}")]
pub struct IOError(Arc<io::Error>);
impl PartialEq for IOError {
fn eq(&self, other: &Self) -> bool {
self.0.kind() == other.0.kind()
}
}
impl From<IOError> for io::Error {
#[cold]
fn from(error: IOError) -> Self {
let io_error = error.0.as_ref();
Self::new(io_error.kind(), io_error.to_string())
}
}
impl From<io::Error> for ResolveError {
#[cold]
fn from(err: io::Error) -> Self {
Self::IOError(IOError(Arc::new(err)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CircularPathBufs(Vec<PathBuf>);
impl CircularPathBufs {
#[must_use]
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
}
impl Display for CircularPathBufs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, path) in self.0.iter().enumerate() {
if i != 0 {
write!(f, " -> ")?;
}
path.fmt(f)?;
}
Ok(())
}
}
impl From<Vec<PathBuf>> for CircularPathBufs {
#[cold]
fn from(value: Vec<PathBuf>) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConditionNames(Vec<String>);
impl ConditionNames {
#[must_use]
pub fn names(&self) -> &[String] {
&self.0
}
}
impl From<Vec<String>> for ConditionNames {
fn from(conditions: Vec<String>) -> Self {
Self(conditions)
}
}
impl Display for ConditionNames {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0.len() {
0 => write!(f, "no conditions"),
1 => write!(f, "the condition \"{}\"", self.0[0]),
_ => {
write!(f, "the conditions ")?;
let conditions_str =
self.0.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ");
write!(f, "[{conditions_str}]")
}
}
}
}
#[test]
fn test_into_io_error() {
use std::io::{self, ErrorKind};
let error_string = "IOError occurred";
let string_error = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
let string_error2 = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
let resolve_io_error: ResolveError = ResolveError::from(string_error2);
assert_eq!(resolve_io_error, ResolveError::from(string_error));
assert_eq!(resolve_io_error.clone(), resolve_io_error);
let ResolveError::IOError(io_error) = resolve_io_error else { unreachable!() };
assert_eq!(
format!("{io_error:?}"),
r#"IOError(Custom { kind: Interrupted, error: "IOError occurred" })"#
);
let std_io_error: io::Error = io_error.into();
assert_eq!(std_io_error.kind(), ErrorKind::Interrupted);
assert_eq!(std_io_error.to_string(), error_string);
assert_eq!(
format!("{std_io_error:?}"),
r#"Custom { kind: Interrupted, error: "IOError occurred" }"#
);
}
#[test]
fn test_coverage() {
let error = ResolveError::NotFound("x".into());
assert_eq!(format!("{error:?}"), r#"NotFound("x")"#);
assert_eq!(error.clone(), error);
let error = ResolveError::Specifier(SpecifierError::Empty("x".into()));
assert_eq!(format!("{error:?}"), r#"Specifier(Empty("x"))"#);
assert_eq!(error.clone(), error);
}
#[test]
fn test_circular_path_bufs_display() {
use std::path::PathBuf;
let paths = vec![
PathBuf::from("/foo/tsconfig.json"),
PathBuf::from("/bar/tsconfig.json"),
PathBuf::from("/baz/tsconfig.json"),
];
let circular = CircularPathBufs::from(paths);
let display_str = format!("{circular}");
assert!(display_str.contains("/foo/tsconfig.json"));
assert!(display_str.contains(" -> "));
assert!(display_str.contains("/bar/tsconfig.json"));
assert!(display_str.contains("/baz/tsconfig.json"));
}