feat: support workspace .nvmrc

This commit is contained in:
zhom
2025-07-27 02:21:49 +04:00
parent 327bc0839e
commit 6813867d95
4 changed files with 272 additions and 43 deletions
+6 -1
View File
@@ -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?;
}
}
+1 -1
View File
@@ -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 '{}'",
+6 -41
View File
@@ -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<String> {
let versions = self.fetch_versions().await?;
pub async fn resolve_version(&self, version_spec: &str, ignore_cached_versions: bool) -> Result<String> {
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<String> {
self.resolve_version(&major.to_string()).await
}
/// List all available versions matching a pattern
pub async fn list_versions(&self, pattern: Option<&str>) -> Result<Vec<String>> {
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<Vec<NodeVersion>> {
async fn fetch_versions(&self, ignore_cached_versions: bool) -> Result<Vec<NodeVersion>> {
// 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);
}
+259
View File
@@ -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(())
}