diff --git a/crates/vite_global_cli/src/commands/env/default.rs b/crates/vite_global_cli/src/commands/env/default.rs index 4692951d5d..b6ccb87911 100644 --- a/crates/vite_global_cli/src/commands/env/default.rs +++ b/crates/vite_global_cli/src/commands/env/default.rs @@ -87,6 +87,9 @@ async fn set_default(version: &str) -> Result { 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()) diff --git a/crates/vite_global_cli/src/commands/env/pin.rs b/crates/vite_global_cli/src/commands/env/pin.rs index 4e43c29d62..180569d3b1 100644 --- a/crates/vite_global_cli/src/commands/env/pin.rs +++ b/crates/vite_global_cli/src/commands/env/pin.rs @@ -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!( @@ -206,6 +209,10 @@ pub async fn do_unpin(cwd: &AbsolutePathBuf) -> Result { } 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()) @@ -213,6 +220,7 @@ pub async fn do_unpin(cwd: &AbsolutePathBuf) -> Result { #[cfg(test)] mod tests { + use serial_test::serial; use tempfile::TempDir; use vite_path::AbsolutePathBuf; @@ -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(); diff --git a/crates/vite_global_cli/src/shim/cache.rs b/crates/vite_global_cli/src/shim/cache.rs index f9ad302c1f..8a8d4a359f 100644 --- a/crates/vite_global_cli/src/shim/cache.rs +++ b/crates/vite_global_cli/src/shim/cache.rs @@ -188,6 +188,14 @@ pub fn get_cache_path() -> Option { 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 { let metadata = std::fs::metadata(path).ok()?; @@ -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"); + } } diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index 1b11304f04..de10b654d5 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -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;