Skip to content

Commit 0b3ad59

Browse files
committed
fix(cli): expose configured package manager to direct commands
Prepend the managed package manager bin from an explicit packageManager field to direct built-in command environments so tools spawned by vp test can find the configured package manager without relying on the user's shell PATH. Keep the behavior limited to explicit packageManager metadata and avoid lockfile inference or package.json mutation.
1 parent 917281c commit 0b3ad59

1 file changed

Lines changed: 123 additions & 1 deletion

File tree

  • packages/cli/binding/src/cli

packages/cli/binding/src/cli/mod.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ async fn execute_direct_subcommand(
6060
.map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str())))
6161
.collect(),
6262
);
63+
let envs = envs_with_explicit_package_manager_path(cwd, envs).await?;
6364

6465
let status = match subcommand {
6566
SynthesizableSubcommand::Check {
@@ -113,6 +114,57 @@ async fn execute_direct_subcommand(
113114
Ok(status)
114115
}
115116

117+
fn is_path_env_key(key: &OsStr) -> bool {
118+
if cfg!(windows) { key.eq_ignore_ascii_case("PATH") } else { key == "PATH" }
119+
}
120+
121+
fn prepend_to_env_path(
122+
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
123+
bin_prefix: &AbsolutePath,
124+
) -> Result<Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>, Error> {
125+
let path_key = envs
126+
.keys()
127+
.find(|key| is_path_env_key(key.as_ref()))
128+
.cloned()
129+
.unwrap_or_else(|| Arc::from(OsStr::new("PATH")));
130+
let current_path =
131+
envs.get(&path_key).map_or_else(Default::default, |path| path.to_os_string());
132+
let paths = env::split_paths(&current_path).collect::<Vec<_>>();
133+
134+
if paths.first().is_some_and(|path| path == bin_prefix.as_path()) {
135+
return Ok(Arc::clone(envs));
136+
}
137+
138+
let new_path = env::join_paths(
139+
std::iter::once(bin_prefix.as_path().to_path_buf()).chain(paths.into_iter()),
140+
)
141+
.map_err(|error| Error::Anyhow(anyhow::Error::new(error)))?;
142+
143+
let mut envs = FxHashMap::clone(envs);
144+
envs.insert(path_key, Arc::from(new_path.as_os_str()));
145+
Ok(Arc::new(envs))
146+
}
147+
148+
async fn envs_with_explicit_package_manager_path(
149+
cwd: &AbsolutePath,
150+
envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
151+
) -> Result<Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>, Error> {
152+
let Some(resolution) =
153+
vite_install::package_manager::resolve_package_manager_from_package_json(cwd)?
154+
else {
155+
return Ok(envs);
156+
};
157+
158+
let (install_dir, _, _) = vite_install::download_package_manager(
159+
resolution.package_manager_type,
160+
&resolution.version,
161+
resolution.hash.as_deref(),
162+
)
163+
.await?;
164+
165+
prepend_to_env_path(&envs, &install_dir.join("bin"))
166+
}
167+
116168
/// Execute a vite-task command (run, cache) through Session.
117169
async fn execute_vite_task_command(
118170
command: vite_task::Command,
@@ -224,10 +276,80 @@ async fn execute_pm_command(
224276

225277
#[cfg(test)]
226278
mod tests {
227-
use std::path::PathBuf;
279+
use std::{
280+
ffi::OsStr,
281+
fs,
282+
path::PathBuf,
283+
sync::Arc,
284+
time::{SystemTime, UNIX_EPOCH},
285+
};
228286

287+
use rustc_hash::FxHashMap;
288+
use vite_path::AbsolutePathBuf;
229289
use vite_task::config::UserRunConfig;
230290

291+
use super::{envs_with_explicit_package_manager_path, prepend_to_env_path};
292+
293+
fn envs_with_path(path: &std::ffi::OsStr) -> Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
294+
Arc::new(FxHashMap::from_iter([(Arc::from(OsStr::new("PATH")), Arc::from(path))]))
295+
}
296+
297+
#[test]
298+
fn prepends_package_manager_bin_to_env_path() {
299+
let cwd = std::env::current_dir().expect("current_dir should exist");
300+
let old_bin = cwd.join("old-bin");
301+
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
302+
let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH");
303+
let envs = envs_with_path(original_path.as_os_str());
304+
305+
let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update");
306+
let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist");
307+
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();
308+
309+
assert_eq!(paths.first().map(std::path::PathBuf::as_path), Some(pm_bin.as_path()));
310+
assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path()));
311+
}
312+
313+
#[test]
314+
fn does_not_duplicate_package_manager_bin_when_already_first() {
315+
let cwd = std::env::current_dir().expect("current_dir should exist");
316+
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
317+
let original_path = std::env::join_paths([pm_bin.as_path()]).expect("valid PATH");
318+
let envs = envs_with_path(original_path.as_os_str());
319+
320+
let updated = prepend_to_env_path(&envs, &pm_bin).expect("PATH should update");
321+
let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist");
322+
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();
323+
324+
assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]);
325+
}
326+
327+
#[tokio::test]
328+
async fn ignores_lockfile_without_explicit_package_manager() {
329+
let suffix =
330+
SystemTime::now().duration_since(UNIX_EPOCH).expect("time should be valid").as_nanos();
331+
let temp_dir = std::env::temp_dir().join(format!("vite-plus-no-pm-{suffix}"));
332+
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
333+
fs::write(temp_dir.join("package.json"), r#"{"name":"fixture"}"#)
334+
.expect("package.json should be written");
335+
fs::write(temp_dir.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'\n")
336+
.expect("lockfile should be written");
337+
let cwd = AbsolutePathBuf::new(temp_dir.clone()).expect("temp dir should be absolute");
338+
let original_path = std::env::join_paths([temp_dir.join("old-bin")]).expect("valid PATH");
339+
let envs = envs_with_path(original_path.as_os_str());
340+
341+
let updated = envs_with_explicit_package_manager_path(&cwd, Arc::clone(&envs))
342+
.await
343+
.expect("missing packageManager should not error");
344+
345+
assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH")));
346+
assert_eq!(
347+
fs::read_to_string(temp_dir.join("package.json")).expect("package.json should exist"),
348+
r#"{"name":"fixture"}"#
349+
);
350+
fs::remove_dir_all(temp_dir).expect("temp dir should be removed");
351+
}
352+
231353
#[test]
232354
fn run_config_types_in_sync() {
233355
// Remove \r for cross-platform consistency

0 commit comments

Comments
 (0)