Skip to main content

command_stream/
local_bin.rs

1//! Project-local executable resolution shared by the default, zx, and Bun runners.
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6/// Directories whose `node_modules/.bin` and own path precede the usual PATH.
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub enum PreferLocal {
9    /// Leave PATH unchanged.
10    #[default]
11    Off,
12    /// Use the command's working directory.
13    Cwd,
14    /// Use these directories, in order.
15    Dirs(Vec<PathBuf>),
16}
17
18/// Platform PATH list separator.
19pub const PATH_DELIMITER: &str = if cfg!(windows) { ";" } else { ":" };
20
21/// Name of the PATH variable in an environment map.
22pub fn path_key<'a, I>(keys: I) -> String
23where
24    I: IntoIterator<Item = &'a String>,
25{
26    if cfg!(windows) {
27        keys.into_iter()
28            .find(|k| k.eq_ignore_ascii_case("path"))
29            .cloned()
30            .unwrap_or_else(|| "Path".to_string())
31    } else {
32        "PATH".to_string()
33    }
34}
35
36fn absolutize(dir: &Path) -> PathBuf {
37    if dir.is_absolute() {
38        dir.to_path_buf()
39    } else {
40        std::env::current_dir()
41            .map(|cwd| cwd.join(dir))
42            .unwrap_or_else(|_| dir.to_path_buf())
43    }
44}
45
46/// Build a PATH that prefers `<dir>/node_modules/.bin` and `<dir>`.
47pub fn prefer_local_bin<P: AsRef<Path>>(path: Option<&str>, dirs: &[P]) -> String {
48    let mut parts: Vec<String> = Vec::new();
49    for dir in dirs {
50        let dir = absolutize(dir.as_ref());
51        parts.push(
52            dir.join("node_modules")
53                .join(".bin")
54                .to_string_lossy()
55                .into_owned(),
56        );
57        parts.push(dir.to_string_lossy().into_owned());
58    }
59    if let Some(path) = path {
60        parts.push(path.to_string());
61    }
62    parts.join(PATH_DELIMITER)
63}
64
65/// Compute the child's preferred PATH without changing the supplied environment.
66pub(crate) fn preferred_path(
67    env: Option<&HashMap<String, String>>,
68    cwd: &Path,
69    preference: &PreferLocal,
70) -> Option<(String, String)> {
71    let dirs = match preference {
72        PreferLocal::Off => return None,
73        PreferLocal::Cwd => vec![cwd.to_path_buf()],
74        PreferLocal::Dirs(dirs) => dirs.clone(),
75    };
76    if dirs.is_empty() {
77        return None;
78    }
79    let (key, current) = if let Some(env) = env {
80        let key = path_key(env.keys());
81        let value = env.get(&key).cloned();
82        (key, value)
83    } else {
84        let vars: Vec<String> = std::env::vars().map(|(key, _)| key).collect();
85        let key = path_key(vars.iter());
86        let value = std::env::var(&key).ok();
87        (key, value)
88    };
89    Some((key, prefer_local_bin(current.as_deref(), &dirs)))
90}
91
92/// Override only the child's PATH, leaving the supplied environment untouched.
93pub(crate) fn apply_prefer_local(
94    command: &mut tokio::process::Command,
95    env: Option<&HashMap<String, String>>,
96    cwd: &Path,
97    preference: &PreferLocal,
98) {
99    if let Some((key, path)) = preferred_path(env, cwd, preference) {
100        command.env(key, path);
101    }
102}