use std::{
borrow::Cow,
ffi::OsStr,
path::{Path, PathBuf},
};
use sugar_path::{SugarPath as _, SugarPathBuf as _};
pub trait PathExt {
fn expect_to_str(&self) -> &str;
fn expect_to_slash(&self) -> String;
fn is_in_node_modules(&self) -> bool;
fn representative_file_name(&self) -> Cow<'_, str>;
}
impl PathExt for Path {
fn expect_to_str(&self) -> &str {
self.to_str().unwrap_or_else(|| {
panic!("Failed to convert {:?} to valid utf8 str", self.display());
})
}
fn expect_to_slash(&self) -> String {
self.to_slash().into_owned()
}
fn is_in_node_modules(&self) -> bool {
self.components().any(|comp| comp.as_os_str() == "node_modules")
}
fn representative_file_name(&self) -> Cow<'_, str> {
let file_name =
self.file_stem().and_then(OsStr::to_str).or_else(|| self.to_str()).unwrap_or_default();
match file_name {
"index" | "mod" => Cow::Borrowed(
self.parent().and_then(Path::file_name).and_then(OsStr::to_str).unwrap_or(file_name),
),
_ => Cow::Borrowed(file_name),
}
}
}
pub fn representative_file_name_for_preserve_modules(
path: &Path,
) -> (Cow<'_, str>, String, Option<Cow<'_, str>>) {
let file_name = Cow::Borrowed(
path.file_stem().and_then(OsStr::to_str).or_else(|| path.to_str()).unwrap_or_default(),
);
let ab_path = path.with_extension("").to_str().unwrap_or_default().to_owned();
(file_name, ab_path, path.extension().and_then(OsStr::to_str).map(Cow::Borrowed))
}
pub fn strip_path_prefix_to_slash(path: &Path, prefix: &Path) -> Option<String> {
path.strip_prefix(prefix).ok().map(PathExt::expect_to_slash)
}
#[inline]
pub fn relative_path_to_slash(target: impl AsRef<Path>, base: impl AsRef<Path>) -> String {
target.as_ref().relative(base).into_owned().into_slash()
}
#[inline]
pub fn relative_path_as_js_specifier(target: impl AsRef<Path>, base: impl AsRef<Path>) -> String {
let relative = target.as_ref().relative(base);
if relative.as_os_str().is_empty() {
return ".".to_string();
}
let slash = relative.into_owned().into_slash();
if slash == ".." || slash.starts_with("../") { slash } else { format!("./{slash}") }
}
#[inline]
pub fn absolute_path_to_relative_slash(path: impl AsRef<Path>, cwd: impl AsRef<Path>) -> String {
let path = path.as_ref();
if path.is_absolute() { relative_path_to_slash(path, cwd) } else { path.expect_to_slash() }
}
#[inline]
pub fn absolutize_path_buf(path: PathBuf) -> PathBuf {
if path.is_absolute() { path } else { path.absolutize().into_owned() }
}
#[inline]
pub fn path_buf_to_slash(path: PathBuf) -> String {
path.into_slash()
}
#[test]
fn test_relative_path_helpers() {
let workspace = std::env::current_dir().unwrap().join("path-helper-tests");
let base = workspace.join("src");
let nested = base.join("lib").join("mod.js");
assert_eq!(relative_path_to_slash(&nested, &base), "lib/mod.js");
assert_eq!(relative_path_as_js_specifier(&nested, &base), "./lib/mod.js");
assert_eq!(relative_path_as_js_specifier(&base, &base), ".");
assert_eq!(relative_path_as_js_specifier(workspace.join("other"), &base), "../other");
assert_eq!(relative_path_as_js_specifier(base.join("..foo.js"), &base), "./..foo.js");
assert_eq!(relative_path_as_js_specifier(base.join(".hidden.js"), &base), "./.hidden.js");
assert_eq!(absolute_path_to_relative_slash(&nested, &workspace), "src/lib/mod.js");
assert!(absolutize_path_buf(PathBuf::from("path-helper-tests")).is_absolute());
assert_eq!(path_buf_to_slash(PathBuf::from("src").join("lib.js")), "src/lib.js");
}
#[test]
fn test_representative_file_name() {
let cwd = Path::new(".").join("project");
let path = cwd.join("src").join("vue.js");
assert_eq!(path.representative_file_name(), "vue");
let path = cwd.join("vue").join("index.js");
assert_eq!(path.representative_file_name(), "vue");
let path = cwd.join("vue").join("mod.ts");
assert_eq!(path.representative_file_name(), "vue");
let path = cwd.join("foo.bar").join("index.js");
assert_eq!(path.representative_file_name(), "foo.bar");
let path = cwd.join("x.jsx");
let (_, ab_path, _) = representative_file_name_for_preserve_modules(&path);
assert_eq!(Path::new(&ab_path).file_name().unwrap().to_string_lossy(), "x");
#[cfg(not(target_os = "windows"))]
{
let path = cwd.join("src").join("vue.js");
assert_eq!(representative_file_name_for_preserve_modules(&path).1, "./project/src/vue");
}
}
#[test]
fn test_strip_path_prefix_to_slash() {
let path = Path::new("/project/src/bin/index");
let prefix = Path::new("/project/src");
assert_eq!(strip_path_prefix_to_slash(path, prefix).as_deref(), Some("bin/index"));
let path = Path::new("/project/src2/bin/index");
let prefix = Path::new("/project/src");
assert_eq!(strip_path_prefix_to_slash(path, prefix), None);
}
#[cfg(target_os = "windows")]
#[test]
fn test_strip_path_prefix_to_slash_with_mixed_windows_separators() {
let path = Path::new(r"C:/project/src/bin/index");
let prefix = Path::new(r"C:\project\src");
assert_eq!(strip_path_prefix_to_slash(path, prefix).as_deref(), Some("bin/index"));
}