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
3 changes: 3 additions & 0 deletions crates/vite_global_cli/src/commands/env/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ async fn set_default(version: &str) -> Result<ExitStatus, Error> {
config.default_node_version = Some(store_version);
save_config(&config).await?;

// Invalidate resolve cache so the new default takes effect immediately
crate::shim::invalidate_cache();

println!("\u{2713} Default Node.js version set to {display_version}");

Ok(ExitStatus::default())
Expand Down
92 changes: 92 additions & 0 deletions crates/vite_global_cli/src/commands/env/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ async fn do_pin(
// Write the version to .node-version
tokio::fs::write(&node_version_path, format!("{resolved_version}\n")).await?;

// Invalidate resolve cache so the pinned version takes effect immediately
crate::shim::invalidate_cache();

// Print success message
if was_alias {
output::success(&format!(
Expand Down Expand Up @@ -206,13 +209,18 @@ pub async fn do_unpin(cwd: &AbsolutePathBuf) -> Result<ExitStatus, Error> {
}

tokio::fs::remove_file(&node_version_path).await?;

// Invalidate resolve cache so the unpinned version falls back correctly
crate::shim::invalidate_cache();

output::success(&format!("Removed {} from {}", NODE_VERSION_FILE, cwd.as_path().display()));

Ok(ExitStatus::default())
}

#[cfg(test)]
mod tests {
use serial_test::serial;
use tempfile::TempDir;
use vite_path::AbsolutePathBuf;

Expand Down Expand Up @@ -275,6 +283,90 @@ mod tests {
assert!(!tokio::fs::try_exists(&node_version_path).await.unwrap());
}

#[tokio::test]
// Run serially: mutates VITE_PLUS_HOME env var which affects invalidate_cache()
#[serial]
async fn test_do_unpin_invalidates_cache() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Point VITE_PLUS_HOME to temp dir
unsafe {
std::env::set_var(vite_shared::env_vars::VITE_PLUS_HOME, temp_path.as_path());
}

// Create cache file manually
let cache_dir = temp_path.join("cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let cache_file = cache_dir.join("resolve_cache.json");
std::fs::write(&cache_file, r#"{"version":2,"entries":{}}"#).unwrap();
assert!(
std::fs::metadata(cache_file.as_path()).is_ok(),
"Cache file should exist before unpin"
);

// Create .node-version and unpin
let node_version_path = temp_path.join(".node-version");
tokio::fs::write(&node_version_path, "20.18.0\n").await.unwrap();
let result = do_unpin(&temp_path).await;
assert!(result.is_ok());

// Cache file should be removed by invalidate_cache()
assert!(
std::fs::metadata(cache_file.as_path()).is_err(),
"Cache file should be removed after unpin"
);

// Cleanup
unsafe {
std::env::remove_var(vite_shared::env_vars::VITE_PLUS_HOME);
}
}

// Run serially: mutates VITE_PLUS_HOME env var which affects invalidate_cache()
#[tokio::test]
#[serial]
async fn test_do_pin_invalidates_cache() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Point VITE_PLUS_HOME to temp dir
unsafe {
std::env::set_var(vite_shared::env_vars::VITE_PLUS_HOME, temp_path.as_path());
}

// Create cache file manually
let cache_dir = temp_path.join("cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let cache_file = cache_dir.join("resolve_cache.json");
std::fs::write(&cache_file, r#"{"version":2,"entries":{}}"#).unwrap();
assert!(
std::fs::metadata(cache_file.as_path()).is_ok(),
"Cache file should exist before pin"
);

// Pin an exact version (no_install=true to skip download, force=true to skip prompt)
let result = do_pin(&temp_path, "20.18.0", true, true).await;
assert!(result.is_ok());

// .node-version should be created
let node_version_path = temp_path.join(".node-version");
assert!(tokio::fs::try_exists(&node_version_path).await.unwrap());
let content = tokio::fs::read_to_string(&node_version_path).await.unwrap();
assert_eq!(content.trim(), "20.18.0");

// Cache file should be removed by invalidate_cache()
assert!(
std::fs::metadata(cache_file.as_path()).is_err(),
"Cache file should be removed after pin"
);

// Cleanup
unsafe {
std::env::remove_var(vite_shared::env_vars::VITE_PLUS_HOME);
}
}

#[tokio::test]
async fn test_do_unpin_no_file() {
let temp_dir = TempDir::new().unwrap();
Expand Down
57 changes: 57 additions & 0 deletions crates/vite_global_cli/src/shim/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ pub fn get_cache_path() -> Option<AbsolutePathBuf> {
Some(home.join("cache").join("resolve_cache.json"))
}

/// Invalidate the entire resolve cache by deleting the cache file.
/// Called after version configuration changes (e.g., `vp env default`, `vp env pin`, `vp env unpin`).
pub fn invalidate_cache() {
if let Some(cache_path) = get_cache_path() {
std::fs::remove_file(cache_path.as_path()).ok();
}
}

/// Get the mtime of a file as Unix timestamp.
pub fn get_file_mtime(path: &AbsolutePath) -> Option<u64> {
let metadata = std::fs::metadata(path).ok()?;
Expand Down Expand Up @@ -335,4 +343,53 @@ mod tests {
assert!(cached_entry.is_some(), "Range version cache should be valid within TTL");
assert_eq!(cached_entry.unwrap().version, "20.20.0");
}

// Run serially: mutates VITE_PLUS_HOME env var which affects get_cache_path()
#[test]
#[serial_test::serial]
fn test_invalidate_cache_removes_file() {
let temp_dir = TempDir::new().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Set VITE_PLUS_HOME to temp dir so invalidate_cache() targets our test file
let cache_dir = temp_path.join("cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let cache_file = cache_dir.join("resolve_cache.json");

// Create a cache with an entry and save it
let mut cache = ResolveCache::default();
cache.insert(
&temp_path,
ResolveCacheEntry {
version: "20.18.0".to_string(),
source: ".node-version".to_string(),
project_root: None,
resolved_at: now_timestamp(),
version_file_mtime: 0,
source_path: None,
is_range: false,
},
);
cache.save(&cache_file);
assert!(std::fs::metadata(cache_file.as_path()).is_ok(), "Cache file should exist");

// Point VITE_PLUS_HOME to our temp dir and call invalidate_cache
unsafe {
std::env::set_var(vite_shared::env_vars::VITE_PLUS_HOME, temp_path.as_path());
}
invalidate_cache();
unsafe {
std::env::remove_var(vite_shared::env_vars::VITE_PLUS_HOME);
}

// Cache file should be removed
assert!(
std::fs::metadata(cache_file.as_path()).is_err(),
"Cache file should be removed after invalidation"
);

// Loading from removed file should return empty default cache
let loaded_cache = ResolveCache::load(&cache_file);
assert!(loaded_cache.get(&temp_path).is_none(), "Cache should be empty after invalidation");
}
}
1 change: 1 addition & 0 deletions crates/vite_global_cli/src/shim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod cache;
pub(crate) mod dispatch;
pub(crate) mod exec;

pub(crate) use cache::invalidate_cache;
pub use dispatch::dispatch;
use vite_shared::env_vars;

Expand Down
Loading