Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 218 additions & 1 deletion packages/cli/binding/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async fn execute_direct_subcommand(
.map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str())))
.collect(),
);
let envs = envs_with_explicit_package_manager_path(cwd, envs).await?;

let status = match subcommand {
SynthesizableSubcommand::Check {
Expand Down Expand Up @@ -113,6 +114,96 @@ async fn execute_direct_subcommand(
Ok(status)
}

fn is_path_env_key(key: &OsStr) -> bool {
if cfg!(windows) { key.eq_ignore_ascii_case("PATH") } else { key == "PATH" }
}

fn try_prepend_to_env_path(
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
bin_prefix: &AbsolutePath,
) -> Result<Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>, Error> {
let path_key = envs
.keys()
.find(|key| is_path_env_key(key.as_ref()))
.cloned()
.unwrap_or_else(|| Arc::from(OsStr::new("PATH")));
let current_path =
envs.get(&path_key).map_or_else(Default::default, |path| path.to_os_string());
let paths = if current_path.is_empty() {
Vec::new()
} else {
env::split_paths(&current_path).collect::<Vec<_>>()
};

if paths.first().is_some_and(|path| path == bin_prefix.as_path()) {
return Ok(Arc::clone(envs));
}

let new_path = env::join_paths(
std::iter::once(bin_prefix.as_path().to_path_buf()).chain(paths.into_iter()),
)
.map_err(|error| Error::Anyhow(anyhow::Error::new(error)))?;

let mut envs = FxHashMap::clone(envs);
envs.insert(path_key, Arc::from(new_path.as_os_str()));
Ok(Arc::new(envs))
}

fn prepend_to_env_path(
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
bin_prefix: &AbsolutePath,
) -> Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
match try_prepend_to_env_path(envs, bin_prefix) {
Ok(updated_envs) => updated_envs,
Err(error) => {
tracing::debug!(
?error,
"failed to prepend managed package manager bin to direct command PATH"
);
Arc::clone(envs)
}
}
}

async fn envs_with_explicit_package_manager_path(
cwd: &AbsolutePath,
envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
) -> Result<Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>, Error> {
let Some(resolution) =
(match vite_install::package_manager::resolve_package_manager_from_package_json(cwd) {
Ok(resolution) => resolution,
Err(error) => {
tracing::debug!(
?error,
"failed to resolve explicit packageManager for direct command PATH setup"
);
return Ok(envs);
}
})
else {
return Ok(envs);
};

let (install_dir, _, _) = match vite_install::download_package_manager(
resolution.package_manager_type,
&resolution.version,
resolution.hash.as_deref(),
)
.await
{
Ok(result) => result,
Err(error) => {
tracing::debug!(
?error,
"failed to ensure managed package manager for direct command PATH setup"
);
return Ok(envs);
}
};

Ok(prepend_to_env_path(&envs, &install_dir.join("bin")))
}

/// Execute a vite-task command (run, cache) through Session.
async fn execute_vite_task_command(
command: vite_task::Command,
Expand Down Expand Up @@ -224,10 +315,136 @@ async fn execute_pm_command(

#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::{
ffi::OsStr,
fs,
path::PathBuf,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};

use rustc_hash::FxHashMap;
use vite_path::AbsolutePathBuf;
use vite_task::config::UserRunConfig;

use super::{envs_with_explicit_package_manager_path, prepend_to_env_path};

fn envs_with_path(path: &std::ffi::OsStr) -> Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
Arc::new(FxHashMap::from_iter([(Arc::from(OsStr::new("PATH")), Arc::from(path))]))
}

#[test]
fn prepends_package_manager_bin_to_env_path() {
let cwd = std::env::current_dir().expect("current_dir should exist");
let old_bin = cwd.join("old-bin");
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH");
let envs = envs_with_path(original_path.as_os_str());

let updated = prepend_to_env_path(&envs, &pm_bin);
let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist");
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();

assert_eq!(paths.first().map(std::path::PathBuf::as_path), Some(pm_bin.as_path()));
assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path()));
}

#[test]
fn does_not_duplicate_package_manager_bin_when_already_first() {
let cwd = std::env::current_dir().expect("current_dir should exist");
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
let original_path = std::env::join_paths([pm_bin.as_path()]).expect("valid PATH");
let envs = envs_with_path(original_path.as_os_str());

let updated = prepend_to_env_path(&envs, &pm_bin);
let path_value = updated.get(OsStr::new("PATH")).expect("PATH should exist");
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();

assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]);
}

#[test]
fn creates_path_when_env_map_has_no_path() {
let cwd = std::env::current_dir().expect("current_dir should exist");
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
let envs = Arc::new(FxHashMap::default());

let updated = prepend_to_env_path(&envs, &pm_bin);
let path_value = updated.get(OsStr::new("PATH")).expect("PATH should be created");
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();

assert_eq!(paths, vec![pm_bin.as_path().to_path_buf()]);
}

#[test]
fn preserves_path_key_casing_on_windows() {
let cwd = std::env::current_dir().expect("current_dir should exist");
let old_bin = cwd.join("old-bin");
let pm_bin = AbsolutePathBuf::new(cwd.join("pm-bin")).expect("pm bin should be absolute");
let original_path = std::env::join_paths([old_bin.as_path()]).expect("valid PATH");
let key = if cfg!(windows) { "Path" } else { "PATH" };
let envs = Arc::new(FxHashMap::from_iter([(
Arc::from(OsStr::new(key)),
Arc::from(original_path.as_os_str()),
)]));

let updated = prepend_to_env_path(&envs, &pm_bin);
let path_value = updated.get(OsStr::new(key)).expect("existing PATH key should be updated");
let paths = std::env::split_paths(path_value).collect::<Vec<_>>();

assert_eq!(paths.first().map(std::path::PathBuf::as_path), Some(pm_bin.as_path()));
assert_eq!(paths.get(1).map(std::path::PathBuf::as_path), Some(old_bin.as_path()));
}

#[tokio::test]
async fn ignores_invalid_explicit_package_manager() {
let suffix =
SystemTime::now().duration_since(UNIX_EPOCH).expect("time should be valid").as_nanos();
let temp_dir = std::env::temp_dir().join(format!("vite-plus-invalid-pm-{suffix}"));
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
fs::write(
temp_dir.join("package.json"),
r#"{"name":"fixture","packageManager":"unknown@1.0.0"}"#,
)
.expect("package.json should be written");
let cwd = AbsolutePathBuf::new(temp_dir.clone()).expect("temp dir should be absolute");
let original_path = std::env::join_paths([temp_dir.join("old-bin")]).expect("valid PATH");
let envs = envs_with_path(original_path.as_os_str());

let updated = envs_with_explicit_package_manager_path(&cwd, Arc::clone(&envs))
.await
.expect("package manager preflight errors should not fail direct commands");

assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH")));
fs::remove_dir_all(temp_dir).expect("temp dir should be removed");
}

#[tokio::test]
async fn ignores_lockfile_without_explicit_package_manager() {
let suffix =
SystemTime::now().duration_since(UNIX_EPOCH).expect("time should be valid").as_nanos();
let temp_dir = std::env::temp_dir().join(format!("vite-plus-no-pm-{suffix}"));
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
fs::write(temp_dir.join("package.json"), r#"{"name":"fixture"}"#)
.expect("package.json should be written");
fs::write(temp_dir.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'\n")
.expect("lockfile should be written");
let cwd = AbsolutePathBuf::new(temp_dir.clone()).expect("temp dir should be absolute");
let original_path = std::env::join_paths([temp_dir.join("old-bin")]).expect("valid PATH");
let envs = envs_with_path(original_path.as_os_str());

let updated = envs_with_explicit_package_manager_path(&cwd, Arc::clone(&envs))
.await
.expect("missing packageManager should not error");

assert_eq!(updated.get(OsStr::new("PATH")), envs.get(OsStr::new("PATH")));
assert_eq!(
fs::read_to_string(temp_dir.join("package.json")).expect("package.json should exist"),
r#"{"name":"fixture"}"#
);
fs::remove_dir_all(temp_dir).expect("temp dir should be removed");
}

#[test]
fn run_config_types_in_sync() {
// Remove \r for cross-platform consistency
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "test-managed-package-manager-path",
"private": true,
"type": "module",
"packageManager": "pnpm@11.2.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
> sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s "$node_path" "$node_bin/node" && sanitized_path="$node_bin:/bin:/usr/bin" && PATH="$sanitized_path" node --version >/dev/null && if PATH="$sanitized_path" command -v pnpm >/dev/null; then echo "pnpm unexpectedly available on sanitized PATH"; exit 1; fi && PATH="$sanitized_path" "$vp_bin" test --slowTestThreshold 10000'
RUN <cwd>

✓ src/managed-pm-path.test.ts (1 test) <variable>ms

Test Files 1 passed (1)
Tests 1 passed (1)
Start at <date>
Duration <variable>ms (transform <variable>ms, setup <variable>ms, import <variable>ms, tests <variable>ms, environment <variable>ms)

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { execFileSync } from 'node:child_process';

import { expect, test } from '@voidzero-dev/vite-plus-test';

test('direct test command exposes the configured package manager on PATH', () => {
const version = execFileSync('pnpm', ['--version'], { encoding: 'utf8' }).trim();
expect(version).toBe('11.2.2');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ignoredPlatforms": ["win32"],
"commands": [
"sh -c 'vp_bin=$(command -v vp) && node_path=$(command -v node) && node_bin=$(mktemp -d) && ln -s \"$node_path\" \"$node_bin/node\" && sanitized_path=\"$node_bin:/bin:/usr/bin\" && PATH=\"$sanitized_path\" node --version >/dev/null && if PATH=\"$sanitized_path\" command -v pnpm >/dev/null; then echo \"pnpm unexpectedly available on sanitized PATH\"; exit 1; fi && PATH=\"$sanitized_path\" \"$vp_bin\" test --slowTestThreshold 10000'"
]
}
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default defineConfig({
'./rolldown/**',
'**/node_modules/**',
'**/snap-tests/**',
'**/snap-tests-global/**',
// FIXME: Error: failed to prepare the command for injection: Invalid argument (os error 22)
'packages/*/binding/__tests__/',
],
Expand Down
Loading