From 6813867d95405c724ce6dff67e38c81c49f19f8c Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 27 Jul 2025 02:21:49 +0400 Subject: [PATCH] feat: support workspace .nvmrc --- src/main.rs | 7 +- src/node_downloader.rs | 2 +- src/node_version_manager.rs | 47 +--- tests/workspace_version_integration_test.rs | 259 ++++++++++++++++++++ 4 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 tests/workspace_version_integration_test.rs diff --git a/src/main.rs b/src/main.rs index 7a68aee..0505a22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod bundler; +mod executable; mod node_downloader; mod node_version_manager; mod platform; @@ -33,6 +34,9 @@ enum Commands { /// Disable compression for faster bundling (useful for testing) #[arg(long)] no_compression: bool, + /// Ignore cached version resolution results + #[arg(long)] + ignore_cached_versions: bool, }, } @@ -46,8 +50,9 @@ async fn main() -> anyhow::Result<()> { output, name, no_compression, + ignore_cached_versions, } => { - bundler::bundle_project(path, output, name, no_compression).await?; + bundler::bundle_project(path, output, name, no_compression, ignore_cached_versions).await?; } } diff --git a/src/node_downloader.rs b/src/node_downloader.rs index 4c2dbf8..cc7a415 100644 --- a/src/node_downloader.rs +++ b/src/node_downloader.rs @@ -27,7 +27,7 @@ impl NodeDownloader { // Resolve the version specification to a concrete version let resolved_version = version_resolver - .resolve_version(version_spec) + .resolve_version(version_spec, false) .await .context(format!( "Failed to resolve Node.js version '{}'", diff --git a/src/node_version_manager.rs b/src/node_version_manager.rs index ac90570..785cf12 100644 --- a/src/node_version_manager.rs +++ b/src/node_version_manager.rs @@ -27,7 +27,7 @@ impl VersionCache { Self { versions: Vec::new(), last_updated: None, - cache_duration: Duration::from_secs(3600), // 1 hour + cache_duration: Duration::from_secs(86400), // 1 day } } @@ -87,15 +87,6 @@ impl ParsedVersion { true } - - pub fn to_string(&self) -> String { - match (self.minor, self.patch) { - (Some(minor), Some(patch)) => format!("{}.{}.{}", self.major, minor, patch), - (Some(minor), None) => format!("{}.{}", self.major, minor), - (None, None) => format!("{}", self.major), - (None, Some(_)) => unreachable!("Cannot have patch without minor"), - } - } } impl PartialOrd for ParsedVersion { @@ -144,8 +135,8 @@ impl NodeVersionManager { } /// Resolve a version specification like "23", "23.5", "v22.1.0" to a complete version - pub async fn resolve_version(&self, version_spec: &str) -> Result { - let versions = self.fetch_versions().await?; + pub async fn resolve_version(&self, version_spec: &str, ignore_cached_versions: bool) -> Result { + let versions = self.fetch_versions(ignore_cached_versions).await?; let parsed_spec = self.parse_version_spec(version_spec)?; let matching_versions = self.find_matching_versions(&versions, &parsed_spec); @@ -154,46 +145,22 @@ impl NodeVersionManager { anyhow::bail!("No Node.js version found matching '{}'", version_spec); } - // Return the latest matching version let latest = matching_versions.last().unwrap(); Ok(latest.version.trim_start_matches('v').to_string()) } - /// Get the latest version for a major version (e.g., latest v23.x.x) - pub async fn get_latest_for_major(&self, major: u32) -> Result { - self.resolve_version(&major.to_string()).await - } - - /// List all available versions matching a pattern - pub async fn list_versions(&self, pattern: Option<&str>) -> Result> { - let versions = self.fetch_versions().await?; - - let filtered_versions = if let Some(pattern) = pattern { - let parsed_spec = self.parse_version_spec(pattern)?; - self.find_matching_versions(&versions, &parsed_spec) - } else { - versions.iter().collect() - }; - - Ok(filtered_versions - .iter() - .map(|v| v.version.trim_start_matches('v').to_string()) - .collect()) - } - - async fn fetch_versions(&self) -> Result> { + async fn fetch_versions(&self, ignore_cached_versions: bool) -> Result> { // Check cache first { let cache = VERSION_CACHE .lock() .map_err(|e| anyhow::anyhow!("Failed to acquire cache lock: {}", e))?; - if !cache.is_expired() && !cache.versions.is_empty() { + if !ignore_cached_versions && !cache.is_expired() && !cache.versions.is_empty() { return Ok(cache.versions.clone()); } } - // Fetch from Node.js API let url = "https://nodejs.org/dist/index.json"; let response = self .client @@ -219,7 +186,6 @@ impl NodeVersionManager { version_a.cmp(&version_b) }); - // Update cache { let mut cache = VERSION_CACHE .lock() @@ -286,7 +252,6 @@ impl NodeVersionManager { } } - // Sort by version number matching.sort_by(|a, b| { let version_a = self.parse_node_version(&a.version).unwrap_or_default(); let version_b = self.parse_node_version(&b.version).unwrap_or_default(); @@ -365,7 +330,7 @@ mod tests { let resolver = NodeVersionManager::new(); // This test requires internet connection - if let Ok(version) = resolver.resolve_version("18").await { + if let Ok(version) = resolver.resolve_version("18", false).await { assert!(version.starts_with("18.")); println!("Resolved '18' to: {}", version); } diff --git a/tests/workspace_version_integration_test.rs b/tests/workspace_version_integration_test.rs new file mode 100644 index 0000000..866e319 --- /dev/null +++ b/tests/workspace_version_integration_test.rs @@ -0,0 +1,259 @@ +mod common; + +use anyhow::Result; +use common::{ + BundlerTestHelper, TestAssertions, TestCacheManager, TestProject, TestProjectManager, +}; +use serial_test::serial; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_workspace_nvmrc_file_handling() -> Result<()> { + println!("Testing workspace .nvmrc file handling..."); + + // Create a workspace project + let project = TestProject::new("nvmrc-workspace-app") + .workspace() + .with_dependency("lodash", "^4.17.21"); + + let manager = TestProjectManager::create(project)?; + + // Create .nvmrc file in workspace root with version 20 + let workspace_root = manager.workspace_root().unwrap(); + fs::write(workspace_root.join(".nvmrc"), "20")?; + + // Install dependencies + manager.install_workspace_dependencies()?; + + // Bundle the project + let executable_path = BundlerTestHelper::bundle_project_with_compression( + manager.project_path(), + manager.temp_dir(), + Some("nvmrc-workspace-test"), + false, + )?; + + // Test the bundled executable + TestAssertions::assert_executable_works( + &executable_path, + &[ + "Hello from workspace project!", + "Dependencies:", + "Workspace project test completed!", + ], + &[], + &[], + )?; + + println!("✅ workspace .nvmrc file handling test passed!"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_workspace_node_version_file_handling() -> Result<()> { + println!("Testing workspace .node-version file handling..."); + + // Create a workspace project + let project = TestProject::new("node-version-workspace-app") + .workspace() + .with_dependency("uuid", "^9.0.1"); + + let manager = TestProjectManager::create(project)?; + + // Create .node-version file in workspace root with version 18.17.0 + let workspace_root = manager.workspace_root().unwrap(); + fs::write(workspace_root.join(".node-version"), "18.17.0")?; + + // Install dependencies + manager.install_workspace_dependencies()?; + + // Bundle the project + let executable_path = BundlerTestHelper::bundle_project_with_compression( + manager.project_path(), + manager.temp_dir(), + Some("node-version-workspace-test"), + false, + )?; + + // Test the bundled executable + TestAssertions::assert_executable_works( + &executable_path, + &[ + "Hello from workspace project!", + "Dependencies:", + "Workspace project test completed!", + ], + &[], + &[], + )?; + + println!("✅ workspace .node-version file handling test passed!"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_project_level_version_overrides_workspace() -> Result<()> { + println!("Testing project-level version file overrides workspace version..."); + + // Create a workspace project + let project = TestProject::new("version-override-app") + .workspace() + .with_dependency("date-fns", "^2.30.0"); + + let manager = TestProjectManager::create(project)?; + + // Create .nvmrc file in workspace root with version 20 + let workspace_root = manager.workspace_root().unwrap(); + fs::write(workspace_root.join(".nvmrc"), "20")?; + + // Create .nvmrc file in project directory with version 18 (should override workspace) + fs::write(manager.project_path().join(".nvmrc"), "18")?; + + // Install dependencies + manager.install_workspace_dependencies()?; + + // Bundle the project + let executable_path = BundlerTestHelper::bundle_project_with_compression( + manager.project_path(), + manager.temp_dir(), + Some("version-override-test"), + false, + )?; + + // Test the bundled executable + TestAssertions::assert_executable_works( + &executable_path, + &[ + "Hello from workspace project!", + "Dependencies:", + "Workspace project test completed!", + ], + &[], + &[], + )?; + + println!("✅ project-level version override test passed!"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_version_format_compatibility() -> Result<()> { + println!("Testing various Node version format compatibility..."); + + // Test different version formats that nvmrc supports + let test_cases = vec![ + ("23", "major-only"), + ("23.5", "major-minor"), + ("v20.10.0", "full-with-v-prefix"), + ("20.10.0", "full-without-prefix"), + ]; + + for (version_spec, test_name) in test_cases { + println!("Testing version format: {} ({})", version_spec, test_name); + + let project = TestProject::new(&format!("version-format-{}", test_name)) + .workspace() + .with_dependency("fs-extra", "^11.1.1"); + + let manager = TestProjectManager::create(project)?; + + // Create .nvmrc file with the test version + let workspace_root = manager.workspace_root().unwrap(); + fs::write(workspace_root.join(".nvmrc"), version_spec)?; + + // Install dependencies + manager.install_workspace_dependencies()?; + + // Bundle the project + let executable_path = BundlerTestHelper::bundle_project_with_compression( + manager.project_path(), + manager.temp_dir(), + Some(&format!("version-format-{}-test", test_name)), + false, + )?; + + // Test the bundled executable + TestAssertions::assert_executable_works( + &executable_path, + &[ + "Hello from workspace project!", + "Dependencies:", + "Workspace project test completed!", + ], + &[], + &[], + )?; + + println!("✅ version format {} test passed!", version_spec); + } + + println!("✅ all version format compatibility tests passed!"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_nested_workspace_package_version_resolution() -> Result<()> { + println!("Testing nested workspace package version resolution..."); + + // Create a deeply nested workspace structure + let project = TestProject::new("nested-version-app") + .workspace() + .with_dependency("commander", "^11.0.0"); + + let manager = TestProjectManager::create(project)?; + + // Create version files at different levels + let workspace_root = manager.workspace_root().unwrap(); + + // Workspace root has Node 20 + fs::write(workspace_root.join(".nvmrc"), "20")?; + + // Create an intermediate directory (simulating packages/ directory) + let packages_dir = workspace_root.join("packages"); + fs::create_dir_all(&packages_dir)?; + + // Packages directory has Node 18 (should be ignored since project is deeper) + fs::write(packages_dir.join(".node-version"), "18")?; + + // Install dependencies + manager.install_workspace_dependencies()?; + + // Bundle the project + let executable_path = BundlerTestHelper::bundle_project_with_compression( + manager.project_path(), + manager.temp_dir(), + Some("nested-version-test"), + false, + )?; + + // Test the bundled executable + TestAssertions::assert_executable_works( + &executable_path, + &[ + "Hello from workspace project!", + "Dependencies:", + "Workspace project test completed!", + ], + &[], + &[], + )?; + + println!("✅ nested workspace package version resolution test passed!"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_zzz_cleanup_workspace_version_cache() -> Result<()> { + println!("Cleaning up application cache after workspace version tests..."); + + TestCacheManager::clear_application_cache()?; + + println!("✅ Workspace version cache cleanup completed!"); + Ok(()) +}