use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum PreferLocal {
#[default]
Off,
Cwd,
Dirs(Vec<PathBuf>),
}
pub const PATH_DELIMITER: &str = if cfg!(windows) { ";" } else { ":" };
pub fn path_key<'a, I>(keys: I) -> String
where
I: IntoIterator<Item = &'a String>,
{
if cfg!(windows) {
keys.into_iter()
.find(|k| k.eq_ignore_ascii_case("path"))
.cloned()
.unwrap_or_else(|| "Path".to_string())
} else {
"PATH".to_string()
}
}
fn absolutize(dir: &Path) -> PathBuf {
if dir.is_absolute() {
dir.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(dir))
.unwrap_or_else(|_| dir.to_path_buf())
}
}
pub fn prefer_local_bin<P: AsRef<Path>>(path: Option<&str>, dirs: &[P]) -> String {
let mut parts: Vec<String> = Vec::new();
for dir in dirs {
let dir = absolutize(dir.as_ref());
parts.push(
dir.join("node_modules")
.join(".bin")
.to_string_lossy()
.into_owned(),
);
parts.push(dir.to_string_lossy().into_owned());
}
if let Some(path) = path {
parts.push(path.to_string());
}
parts.join(PATH_DELIMITER)
}
pub(crate) fn preferred_path(
env: Option<&HashMap<String, String>>,
cwd: &Path,
preference: &PreferLocal,
) -> Option<(String, String)> {
let dirs = match preference {
PreferLocal::Off => return None,
PreferLocal::Cwd => vec![cwd.to_path_buf()],
PreferLocal::Dirs(dirs) => dirs.clone(),
};
if dirs.is_empty() {
return None;
}
let (key, current) = if let Some(env) = env {
let key = path_key(env.keys());
let value = env.get(&key).cloned();
(key, value)
} else {
let vars: Vec<String> = std::env::vars().map(|(key, _)| key).collect();
let key = path_key(vars.iter());
let value = std::env::var(&key).ok();
(key, value)
};
Some((key, prefer_local_bin(current.as_deref(), &dirs)))
}
pub(crate) fn apply_prefer_local(
command: &mut tokio::process::Command,
env: Option<&HashMap<String, String>>,
cwd: &Path,
preference: &PreferLocal,
) {
if let Some((key, path)) = preferred_path(env, cwd, preference) {
command.env(key, path);
}
}