diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_conflict/snapshots/command_env_install_conflict.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_conflict/snapshots/command_env_install_conflict.md index 4993aa2237..ef3858f9af 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_conflict/snapshots/command_env_install_conflict.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_conflict/snapshots/command_env_install_conflict.md @@ -25,21 +25,19 @@ Uninstalled conflict-pkg Install with specific Node.js version -**Exit code:** 1 - ``` VITE+ - The Unified Toolchain for the Web info: Installing 1 global package with Node.js -error: Failed to install conflict-pkg: Package was not installed correctly, package.json not found at /.vite-plus/packages/conflict-pkg#/lib/node_modules/conflict-pkg/package.json +warn: Package 'conflict-pkg' provides 'node' binary, but it conflicts with a built-in shim. Skipping. +✓ Installed conflict-pkg 1.0.0 + Bins: conflict-cli ``` ## `vp remove -g conflict-pkg` Cleanup -**Exit code:** 1 - ``` -Failed to uninstall conflict-pkg: Package conflict-pkg is not installed +Uninstalled conflict-pkg ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_node_version/snapshots/command_env_install_node_version.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_node_version/snapshots/command_env_install_node_version.md index 5b20dde9e3..6a10bde4d1 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_node_version/snapshots/command_env_install_node_version.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_node_version/snapshots/command_env_install_node_version.md @@ -32,51 +32,26 @@ Uninstalled command-env-install-node-version-pkg Install with Node.js 20 -**Exit code:** 1 - ``` VITE+ - The Unified Toolchain for the Web info: Installing 1 global package with Node.js -error: Failed to install command-env-install-node-version-pkg: Package was not installed correctly, package.json not found at /.vite-plus/packages/command-env-install-node-version-pkg#/lib/node_modules/command-env-install-node-version-pkg/package.json +✓ Installed command-env-install-node-version-pkg 1.0.0 + Bins: command-env-install-node-version-pkg-cli ``` ## `node -e 'const d=JSON.parse(require('\''fs'\'').readFileSync(process.env.VP_HOME+'\''/bins/command-env-install-node-version-pkg-cli.json'\'','\''utf8'\'')); console.log('\''Node major:'\'', d.nodeVersion.split('\''.'\'')[0])'` Verify Node 20 -**Exit code:** 1 - ``` -node:fs:441 - return binding.readFileUtf8(path, stringToFlags(options.flag)); - ^ - -Error: ENOENT: no such file or directory, open '/.vite-plus/bins/command-env-install-node-version-pkg-cli.json' - at Object.readFileSync (node:fs:441:20) - at [eval]:1:34 - at runScriptInThisContext (node:internal/vm:219:10) - at node:internal/process/execution:451:12 - at [eval]-wrapper:6:24 - at runScriptInContext (node:internal/process/execution:449:60) - at evalFunction (node:internal/process/execution:283:30) - at evalTypeScript (node:internal/process/execution:295:3) - at node:internal/main/eval_string:71:3 { - errno: -2, - code: 'ENOENT', - syscall: 'open', - path: '/.vite-plus/bins/command-env-install-node-version-pkg-cli.json' -} - -Node.js +Node major: 20 ``` ## `vp remove -g command-env-install-node-version-pkg` Cleanup -**Exit code:** 1 - ``` -Failed to uninstall command-env-install-node-version-pkg: Package command-env-install-node-version-pkg is not installed +Uninstalled command-env-install-node-version-pkg ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_which/snapshots/command_env_which.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_which/snapshots/command_env_which.md index 461da946a5..e498576334 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_which/snapshots/command_env_which.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_env_which/snapshots/command_env_which.md @@ -83,7 +83,7 @@ Global package - shows binary path with metadata ``` VITE+ - The Unified Toolchain for the Web -/.vite-plus/packages/cowsay#/lib/node_modules/cowsay/./cli.js +/.vite-plus/packages/cowsay//lib/node_modules/cowsay/./cli.js Package: cowsay@1.6.0 Binaries: cowsay, cowthink Node: diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_outdated_global/snapshots/command_outdated_global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_outdated_global/snapshots/command_outdated_global.md index e083cce7b7..9df649c7ab 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_outdated_global/snapshots/command_outdated_global.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/command_outdated_global/snapshots/command_outdated_global.md @@ -26,7 +26,7 @@ should support global json output "wanted": "1.0.1", "latest": "1.0.1", "dependent": "global", - "location": "/.vite-plus/packages/testnpm2#/lib/node_modules/testnpm2" + "location": "/.vite-plus/packages/testnpm2//lib/node_modules/testnpm2" } } ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/env_install_interrupt/check-stale-packages.js b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/env_install_interrupt/check-stale-packages.js index 05343d9654..fa06c8b448 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/env_install_interrupt/check-stale-packages.js +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/env_install_interrupt/check-stale-packages.js @@ -5,26 +5,24 @@ const packageBase = 'long-time-install-package'; const scopeDir = path.join(process.env.VP_HOME, 'packages', '@scope'); const metadataPath = path.join(scopeDir, `${packageBase}.json`); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); -const activeDir = `${packageBase}${metadata.installId}`; +const packageDir = path.join(scopeDir, packageBase); +const activeDir = metadata.installId; const expectStale = process.argv.includes('--expect-stale'); const packageDirs = fs - .readdirSync(scopeDir, { withFileTypes: true }) + .readdirSync(packageDir, { withFileTypes: true }) .filter((entry) => { - if (!entry.isDirectory()) { - return false; - } - if (entry.name === packageBase) { - return true; - } - return /^long-time-install-package#[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test( - entry.name, + return ( + entry.isDirectory() + && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test( + entry.name, + ) ); }) .map((entry) => entry.name) .sort(); -const hasIdentifiedStale = packageDirs.some((name) => name !== packageBase && name !== activeDir); +const hasIdentifiedStale = packageDirs.some((name) => name !== activeDir); console.log( hasIdentifiedStale ? 'interrupted stale package exists' : 'interrupted stale package removed', diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/bin.js b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/bin.js new file mode 100755 index 0000000000..79c7ab2f12 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/bin.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const loadedModule = join(dirname(fileURLToPath(import.meta.url)), 'loaded.js'); +// Keep `#` unescaped to reproduce the old install layout's URL-fragment failure. +const loadedModuleUrl = pathToFileURL(loadedModule).href.replaceAll('%23', '#'); +await import(loadedModuleUrl); diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/loaded.js b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/loaded.js new file mode 100644 index 0000000000..16018e0981 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/loaded.js @@ -0,0 +1 @@ +console.log('global dynamic import loaded'); diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/package.json new file mode 100644 index 0000000000..c9dc8db6e1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/package.json @@ -0,0 +1,8 @@ +{ + "name": "global-install-dynamic-import", + "version": "0.0.0", + "type": "module", + "bin": { + "global-install-dynamic-import": "./bin.js" + } +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots.toml new file mode 100644 index 0000000000..402dcece03 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots.toml @@ -0,0 +1,11 @@ +[[case]] +name = "global_install_dynamic_import" +vp = "global" +comment = "Regression test for #2220: globally installed packages can dynamically import an absolute path inside their installation." +steps = [ + { argv = ["vp", "install", "-g", "."] }, + { argv = ["global-install-dynamic-import"] }, +] +after = [ + { argv = ["vp", "remove", "-g", "global-install-dynamic-import"], continue-on-failure = true }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots/global_install_dynamic_import.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots/global_install_dynamic_import.md new file mode 100644 index 0000000000..2ba806fd5d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/global_install_dynamic_import/snapshots/global_install_dynamic_import.md @@ -0,0 +1,19 @@ +# global_install_dynamic_import + +Regression test for #2220: globally installed packages can dynamically import an absolute path inside their installation. + +## `vp install -g .` + +``` +VITE+ - The Unified Toolchain for the Web + +info: Installing 1 global package with Node.js +✓ Installed global-install-dynamic-import 0.0.0 + Bins: global-install-dynamic-import +``` + +## `global-install-dynamic-import` + +``` +global dynamic import loaded +``` diff --git a/crates/vite_global_cli/src/commands/env/package_metadata.rs b/crates/vite_global_cli/src/commands/env/package_metadata.rs index af979b06f8..35c8c7da10 100644 --- a/crates/vite_global_cli/src/commands/env/package_metadata.rs +++ b/crates/vite_global_cli/src/commands/env/package_metadata.rs @@ -10,19 +10,21 @@ use vite_path::AbsolutePathBuf; use super::config::get_packages_dir; use crate::error::Error; -// `#` is filesystem-safe but invalid in npm package names, so sibling installs cannot collide. -pub(crate) const INSTALL_ID_PREFIX: char = '#'; -// Keeps npm's 214-byte maximum package name within the common 255-byte filename limit. -pub(crate) const INSTALL_ID_LENGTH: usize = 37; +// This is legacy, for old Vite+ version's compatibility +const LEGACY_INSTALL_ID_PREFIX: char = '#'; +const INSTALL_ID_LENGTH: usize = 36; -pub(crate) fn is_install_id(value: &str) -> bool { +pub(crate) fn is_nested_install_id(value: &str) -> bool { value.len() == INSTALL_ID_LENGTH - && value - .strip_prefix(INSTALL_ID_PREFIX) - .and_then(|uuid| Uuid::parse_str(uuid).ok()) + && Uuid::parse_str(value) + .ok() .is_some_and(|uuid| uuid.get_version() == Some(Version::Random)) } +pub(crate) fn is_legacy_install_id(value: &str) -> bool { + value.strip_prefix(LEGACY_INSTALL_ID_PREFIX).is_some_and(is_nested_install_id) +} + /// Metadata for a globally installed package. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -31,7 +33,7 @@ pub struct PackageMetadata { pub name: String, /// Package version pub version: String, - /// Directory identifier for this installation. Empty for legacy installs. + /// Directory identifier for this installation. Empty or `#`-prefixed for legacy installs. #[serde(default)] pub install_id: String, /// Platform versions used during installation @@ -96,15 +98,18 @@ impl PackageMetadata { Self::installation_dir_for(&self.name, &self.install_id) } - /// Resolve an installation prefix, including the legacy empty-ID layout. + /// Resolve an installation prefix, including both legacy layouts. pub fn installation_dir_for( package_name: &str, install_id: &str, ) -> Result { let packages_dir = get_packages_dir()?; + let package_dir = packages_dir.join(package_name); if install_id.is_empty() { - Ok(packages_dir.join(package_name)) - } else if is_install_id(install_id) { + Ok(package_dir) + } else if is_nested_install_id(install_id) { + Ok(package_dir.join(install_id)) + } else if is_legacy_install_id(install_id) { Ok(packages_dir.join(format!("{package_name}{install_id}"))) } else { Err(Error::ConfigError( @@ -241,7 +246,7 @@ mod tests { } #[test] - fn test_installation_dir_uses_install_id() { + fn test_installation_dir_supports_current_and_legacy_layouts() { use tempfile::TempDir; let temp_dir = TempDir::new().unwrap(); @@ -250,18 +255,28 @@ mod tests { ); let legacy = PackageMetadata::installation_dir_for("@scope/pkg", "").unwrap(); - let identified = PackageMetadata::installation_dir_for( + let legacy_identified = PackageMetadata::installation_dir_for( "@scope/pkg", "#123e4567-e89b-42d3-a456-426614174000", ) .unwrap(); + let identified = PackageMetadata::installation_dir_for( + "@scope/pkg", + "987e6543-e21b-42d3-a456-426614174000", + ) + .unwrap(); assert!(legacy.as_path().ends_with("packages/@scope/pkg")); assert!( - identified + legacy_identified .as_path() .ends_with("packages/@scope/pkg#123e4567-e89b-42d3-a456-426614174000") ); + assert!( + identified + .as_path() + .ends_with("packages/@scope/pkg/987e6543-e21b-42d3-a456-426614174000") + ); assert!(PackageMetadata::installation_dir_for("@scope/pkg", "invalid").is_err()); } diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index d2618c5314..f40217ab51 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -18,14 +18,12 @@ use vite_js_runtime::NodeProvider; use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir}; use vite_shared::{format_path_prepended, output}; -#[cfg(test)] -use crate::commands::env::package_metadata::INSTALL_ID_LENGTH; use crate::{ commands::{ env::{ bin_config::BinConfig, config::{get_bin_dir, get_node_modules_dir, resolve_version, resolve_version_alias}, - package_metadata::{INSTALL_ID_PREFIX, PackageMetadata, is_install_id}, + package_metadata::{PackageMetadata, is_legacy_install_id, is_nested_install_id}, }, global::{CORE_SHIMS, is_local_package_spec, parse_package_spec}, }, @@ -609,7 +607,7 @@ async fn install_one( } fn new_install_id() -> String { - format!("{INSTALL_ID_PREFIX}{}", Uuid::new_v4()) + Uuid::new_v4().to_string() } async fn restore_package_metadata(package_name: &str, previous_metadata: Option<&PackageMetadata>) { @@ -788,26 +786,108 @@ async fn cleanup_stale_installations(package_name: &str, current_install_id: &st ); } } + + if !is_nested_install_id(current_install_id) { + return; + } + + let result: Result<(), Error> = async { + let package_dir = PackageMetadata::installation_dir_for(package_name, "")?; + let package_json = get_node_modules_dir(&package_dir, package_name).join("package.json"); + if !tokio::fs::try_exists(&package_json).await.unwrap_or(false) { + return Ok(()); + } + + let Some((lock_path, lock_file)) = try_lock_install_dir(&package_dir)? else { + return Ok(()); + }; + + remove_legacy_installation_contents(&package_dir).await?; + + drop(lock_file); + match tokio::fs::remove_file(&lock_path).await { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.into()), + } + } + .await; + if let Err(error) = result { + tracing::warn!( + "Failed to clean legacy global package installation for {package_name}: {error}" + ); + } +} + +async fn remove_legacy_installation_contents(package_dir: &AbsolutePathBuf) -> Result<(), Error> { + // Preserve nested installs while removing npm contents from the legacy prefix. + let mut entries = tokio::fs::read_dir(package_dir).await?; + while let Some(entry) = entries.next_entry().await? { + let file_name = entry.file_name(); + let Some(file_name) = file_name.to_str() else { continue }; + if is_nested_install_id(file_name) + || file_name.strip_suffix(".lock").is_some_and(is_nested_install_id) + { + continue; + } + + if entry.file_type().await?.is_dir() { + tokio::fs::remove_dir_all(entry.path()).await?; + } else { + tokio::fs::remove_file(entry.path()).await?; + } + } + + match tokio::fs::remove_dir(package_dir).await { + Ok(()) => Ok(()), + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::NotFound | std::io::ErrorKind::DirectoryNotEmpty + ) => + { + Ok(()) + } + Err(error) => Err(error.into()), + } } async fn stale_installation_dirs( package_name: &str, current_install_id: &str, ) -> Result, Error> { - let legacy_install_dir = PackageMetadata::installation_dir_for(package_name, "")?; - let Some(parent) = legacy_install_dir.as_path().parent() else { - return Ok(Vec::new()); - }; - if !tokio::fs::try_exists(parent).await.unwrap_or(false) { - return Ok(Vec::new()); + let package_dir = PackageMetadata::installation_dir_for(package_name, "")?; + let mut stale_dirs = Vec::new(); + // Current installs are UUID children of the package directory. + if tokio::fs::try_exists(&package_dir).await.unwrap_or(false) { + let mut entries = tokio::fs::read_dir(&package_dir).await?; + while let Some(entry) = entries.next_entry().await? { + if !entry.file_type().await?.is_dir() { + continue; + } + + let file_name = entry.file_name(); + let Some(install_id) = file_name.to_str().filter(|name| is_nested_install_id(name)) + else { + continue; + }; + if install_id != current_install_id + && let Some(path) = AbsolutePathBuf::new(entry.path()) + { + stale_dirs.push(path); + } + } } - let Some(base_name) = legacy_install_dir.as_path().file_name().and_then(|name| name.to_str()) - else { - return Ok(Vec::new()); + let Some(parent) = package_dir.as_path().parent() else { return Ok(stale_dirs) }; + let Some(base_name) = package_dir.as_path().file_name().and_then(|name| name.to_str()) else { + return Ok(stale_dirs); }; + if !tokio::fs::try_exists(parent).await.unwrap_or(false) { + return Ok(stale_dirs); + } - let mut stale_dirs = Vec::new(); + // Vite+ 0.2.2 through 0.2.5 stored immutable installs as `#` siblings. let mut entries = tokio::fs::read_dir(parent).await?; while let Some(entry) = entries.next_entry().await? { if !entry.file_type().await?.is_dir() { @@ -818,7 +898,9 @@ async fn stale_installation_dirs( let Some(file_name) = file_name.to_str() else { continue; }; - let Some(install_id) = stale_install_id(base_name, file_name) else { + let Some(install_id) = + file_name.strip_prefix(base_name).filter(|install_id| is_legacy_install_id(install_id)) + else { continue; }; if install_id == current_install_id { @@ -831,16 +913,6 @@ async fn stale_installation_dirs( Ok(stale_dirs) } - -fn stale_install_id<'a>(base_name: &str, file_name: &'a str) -> Option<&'a str> { - if file_name == base_name { - return Some(""); - } - - let install_id = file_name.strip_prefix(base_name)?; - if is_install_id(install_id) { Some(install_id) } else { None } -} - async fn stale_bin_names_for_package( previous_metadata: Option<&PackageMetadata>, package_name: &str, @@ -929,7 +1001,11 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { None => PackageMetadata::installation_dir_for(&package_name, "")?, }; if tokio::fs::try_exists(&package_dir).await.unwrap_or(false) { - tokio::fs::remove_dir_all(&package_dir).await?; + if metadata.as_ref().is_none_or(|metadata| metadata.install_id.is_empty()) { + remove_legacy_installation_contents(&package_dir).await?; + } else { + tokio::fs::remove_dir_all(&package_dir).await?; + } } // Remove metadata file @@ -1402,6 +1478,53 @@ mod tests { assert!(!package_dir.as_path().exists(), "identified package directory should be removed"); } + #[tokio::test] + async fn test_uninstall_legacy_install_preserves_in_progress_nested_install() { + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let _guard = vite_shared::EnvConfig::test_guard( + vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), + ); + let package_name = "test-package"; + + let metadata = PackageMetadata::new( + package_name.to_string(), + "1.0.0".to_string(), + "20.0.0".to_string(), + None, + Vec::new(), + HashSet::new(), + "npm".to_string(), + ); + metadata.save().await.unwrap(); + + let legacy_dir = metadata.installation_dir().unwrap(); + let legacy_package_json = + get_node_modules_dir(&legacy_dir, package_name).join("package.json"); + tokio::fs::create_dir_all(legacy_package_json.parent().unwrap()).await.unwrap(); + tokio::fs::write(&legacy_package_json, "{}").await.unwrap(); + + // Model uninstall starting after npm populated the replacement but before metadata changed. + let replacement_dir = PackageMetadata::installation_dir_for( + package_name, + "123e4567-e89b-42d3-a456-426614174000", + ) + .unwrap(); + let replacement_package_json = + get_node_modules_dir(&replacement_dir, package_name).join("package.json"); + let replacement_lock = lock_install_dir(&replacement_dir).unwrap(); + tokio::fs::create_dir_all(replacement_package_json.parent().unwrap()).await.unwrap(); + tokio::fs::write(&replacement_package_json, "{}").await.unwrap(); + + uninstall(package_name, false).await.unwrap(); + + assert!(!legacy_package_json.as_path().exists()); + assert!(replacement_package_json.as_path().exists()); + assert!(install_dir_lock_path(&replacement_dir).unwrap().as_path().exists()); + drop(replacement_lock); + } + #[tokio::test] #[cfg_attr(windows, serial_test::serial)] async fn test_restore_previous_install_state_removes_partial_new_bins() { @@ -1478,13 +1601,47 @@ mod tests { } } + #[tokio::test] + async fn test_cleanup_stale_installations_supports_all_layouts() { + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let _guard = vite_shared::EnvConfig::test_guard( + vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), + ); + let package_name = "@scope/test-package"; + let current_id = "123e4567-e89b-42d3-a456-426614174000"; + let stale_id = "987e6543-e21b-42d3-a456-426614174000"; + let legacy_id = "#2753e02c-9319-456f-bc0f-23d6d7b6fba5"; + let package_dir = PackageMetadata::installation_dir_for(package_name, "").unwrap(); + let current_dir = PackageMetadata::installation_dir_for(package_name, current_id).unwrap(); + let stale_dir = PackageMetadata::installation_dir_for(package_name, stale_id).unwrap(); + let legacy_dir = PackageMetadata::installation_dir_for(package_name, legacy_id).unwrap(); + + tokio::fs::create_dir_all(¤t_dir).await.unwrap(); + tokio::fs::create_dir_all(&stale_dir).await.unwrap(); + tokio::fs::create_dir_all(&legacy_dir).await.unwrap(); + tokio::fs::write(install_dir_lock_path(¤t_dir).unwrap(), "").await.unwrap(); + let legacy_package_json = + get_node_modules_dir(&package_dir, package_name).join("package.json"); + tokio::fs::create_dir_all(legacy_package_json.parent().unwrap()).await.unwrap(); + tokio::fs::write(&legacy_package_json, "{}").await.unwrap(); + + cleanup_stale_installations(package_name, current_id).await; + + assert!(current_dir.as_path().is_dir()); + assert!(install_dir_lock_path(¤t_dir).unwrap().as_path().is_file()); + assert!(!stale_dir.as_path().exists()); + assert!(!legacy_dir.as_path().exists()); + assert!(!legacy_package_json.as_path().exists()); + assert!(!install_dir_lock_path(&package_dir).unwrap().as_path().exists()); + } + #[test] - fn test_new_install_id_has_fixed_reserved_shape() { + fn test_new_install_id_is_uuid() { let install_id = new_install_id(); - assert!(is_install_id(&install_id)); - assert_eq!(install_id.len(), INSTALL_ID_LENGTH); - assert!(install_id.starts_with(INSTALL_ID_PREFIX)); + assert!(is_nested_install_id(&install_id)); } #[test]