use std::path::{Component, Path, PathBuf};
pub(crate) fn with_ext(p: &Path, ext: &str) -> PathBuf {
let mut s = p.as_os_str().to_owned();
s.push(".");
s.push(ext);
PathBuf::from(s)
}
pub(crate) fn strip_query(url: &str) -> &str {
split_query(url).0
}
pub(crate) fn split_query(url: &str) -> (&str, &str) {
url.split_at(url.find(['?', '#']).unwrap_or(url.len()))
}
pub(crate) fn normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other),
}
}
out
}
pub(crate) fn relative_path(from_dir: &Path, target: &Path) -> String {
let segments = |p: &Path| -> Vec<String> {
normalize(p)
.components()
.filter_map(|c| match c {
Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
_ => None,
})
.collect()
};
let from = segments(from_dir);
let to = segments(target);
let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let mut parts = vec![".."; from.len() - common];
parts.extend(to[common..].iter().map(String::as_str));
let joined = parts.join("/");
if joined.starts_with("..") {
joined
} else {
format!("./{joined}")
}
}
pub(crate) fn join_relative(base: &str, rel: &str) -> String {
let mut segments: Vec<&str> = base
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.collect();
for part in rel.split('/') {
match part {
"" | "." => {}
".." if segments.last().is_some_and(|s| *s != "..") => {
segments.pop();
}
other => segments.push(other),
}
}
let joined = segments.join("/");
if joined.starts_with("..") {
joined
} else {
format!("./{joined}")
}
}
pub(crate) fn posix_join(base_dir: &str, rel: &str) -> String {
let mut segments: Vec<&str> = base_dir.split('/').filter(|s| !s.is_empty()).collect();
for part in rel.split('/') {
match part {
"" | "." => {}
".." => {
segments.pop();
}
other => segments.push(other),
}
}
format!("/{}", segments.join("/"))
}
pub fn node_modules_load_paths(dir: &Path) -> Vec<PathBuf> {
dir.ancestors()
.filter(|d| d.file_name().is_none_or(|n| n != "node_modules"))
.map(|d| d.join("node_modules"))
.filter(|nm| nm.is_dir())
.collect()
}
pub(crate) fn read_package_json(dir: &Path) -> Option<serde_json::Value> {
let text = std::fs::read_to_string(dir.join("package.json")).ok()?;
serde_json::from_str(&text).ok()
}
pub(crate) fn is_external(spec: &str) -> bool {
spec.starts_with("data:") || spec.starts_with("//") || spec.contains("://")
}