Skip to content

Commit 0a3e6ec

Browse files
committed
fix: streamline node manager behavior
1 parent e50d71b commit 0a3e6ec

7 files changed

Lines changed: 172 additions & 150 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tauri-app",
33
"private": true,
4-
"version": "1.0.4",
4+
"version": "1.0.5",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tauri-app"
3-
version = "1.0.4"
3+
version = "1.0.5"
44
description = "A Tauri App"
55
authors = ["you"]
66
edition = "2021"
@@ -48,3 +48,4 @@ windows-sys = { version = "0.59", features = [
4848
"Win32_Networking_WinSock",
4949
"Win32_UI_Shell",
5050
] }
51+
winreg = "0.55"

src-tauri/src/lib.rs

Lines changed: 148 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,30 +1147,135 @@ fn node_current_dir(base_dir: &str) -> std::path::PathBuf {
11471147
node_root_dir(base_dir).join("current")
11481148
}
11491149

1150-
fn detect_active_node_version_internal(base_dir: &str) -> Result<Option<String>, String> {
1151-
let current_exe = node_current_dir(base_dir).join("node.exe");
1152-
if !current_exe.exists() {
1153-
return Ok(None);
1150+
#[cfg(target_os = "windows")]
1151+
fn node_search_paths_from_path(path_value: &str) -> Vec<String> {
1152+
path_value
1153+
.split(';')
1154+
.map(|part| part.trim())
1155+
.filter(|part| !part.is_empty())
1156+
.map(|part| part.trim_end_matches('\\').replace("/", "\\"))
1157+
.filter_map(|dir| {
1158+
let candidate = Path::new(&dir).join("node.exe");
1159+
if candidate.exists() {
1160+
Some(candidate.to_string_lossy().replace("/", "\\"))
1161+
} else {
1162+
None
1163+
}
1164+
})
1165+
.collect()
1166+
}
1167+
1168+
#[cfg(target_os = "windows")]
1169+
fn read_user_path_value() -> Result<String, String> {
1170+
use winreg::RegKey;
1171+
use winreg::enums::HKEY_CURRENT_USER;
1172+
1173+
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
1174+
let env = hkcu
1175+
.open_subkey("Environment")
1176+
.map_err(|e| format!("Failed to open HKCU\\Environment: {}", e))?;
1177+
1178+
match env.get_value::<String, _>("Path") {
1179+
Ok(path) => Ok(path),
1180+
Err(_) => Ok(String::new()),
11541181
}
1182+
}
1183+
1184+
#[cfg(target_os = "windows")]
1185+
fn write_user_path_value(path_value: &str) -> Result<(), String> {
1186+
use winreg::RegKey;
1187+
use winreg::enums::HKEY_CURRENT_USER;
1188+
1189+
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
1190+
let (env, _) = hkcu
1191+
.create_subkey("Environment")
1192+
.map_err(|e| format!("Failed to open HKCU\\Environment for writing: {}", e))?;
1193+
env.set_value("Path", &path_value)
1194+
.map_err(|e| format!("Failed to write User PATH: {}", e))
1195+
}
11551196

1156-
let output = std::process::Command::new(&current_exe)
1157-
.arg("-v")
1197+
#[cfg(target_os = "windows")]
1198+
fn create_junction(link: &Path, target: &Path) -> Result<(), String> {
1199+
use std::os::windows::process::CommandExt;
1200+
const CREATE_NO_WINDOW: u32 = 0x08000000;
1201+
1202+
let link_str = link.to_string_lossy().replace("/", "\\");
1203+
let target_str = target.to_string_lossy().replace("/", "\\");
1204+
let output = Command::new("cmd")
1205+
.args(["/C", "mklink", "/J", &link_str, &target_str])
1206+
.creation_flags(CREATE_NO_WINDOW)
11581207
.output()
1159-
.map_err(|e| format!("Failed to query active Node version: {}", e))?;
1208+
.map_err(|e| format!("Failed to create Node junction: {}", e))?;
1209+
1210+
if output.status.success() {
1211+
Ok(())
1212+
} else {
1213+
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1214+
Err(if stderr.is_empty() {
1215+
"Failed to create Node junction".into()
1216+
} else {
1217+
stderr
1218+
})
1219+
}
1220+
}
11601221

1161-
if !output.status.success() {
1222+
#[cfg(target_os = "windows")]
1223+
fn remove_junction(link: &Path) -> Result<(), String> {
1224+
use std::os::windows::process::CommandExt;
1225+
const CREATE_NO_WINDOW: u32 = 0x08000000;
1226+
1227+
if !link.exists() {
1228+
return Ok(());
1229+
}
1230+
1231+
let link_str = link.to_string_lossy().replace("/", "\\");
1232+
let output = Command::new("cmd")
1233+
.args(["/C", "rmdir", &link_str])
1234+
.creation_flags(CREATE_NO_WINDOW)
1235+
.output()
1236+
.map_err(|e| format!("Failed to remove Node junction: {}", e))?;
1237+
1238+
if output.status.success() {
1239+
Ok(())
1240+
} else {
1241+
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1242+
Err(if stderr.is_empty() {
1243+
"Failed to remove Node junction".into()
1244+
} else {
1245+
stderr
1246+
})
1247+
}
1248+
}
1249+
1250+
fn detect_active_node_version_internal(base_dir: &str) -> Result<Option<String>, String> {
1251+
let current_dir = node_current_dir(base_dir);
1252+
if !current_dir.exists() {
11621253
return Ok(None);
11631254
}
11641255

1165-
let version = String::from_utf8_lossy(&output.stdout)
1166-
.trim()
1167-
.trim_start_matches('v')
1168-
.to_string();
1256+
#[cfg(target_os = "windows")]
1257+
{
1258+
let target = fs::canonicalize(&current_dir)
1259+
.map_err(|e| format!("Failed to resolve active Node version: {}", e))?
1260+
.to_string_lossy()
1261+
.replace("/", "\\");
1262+
1263+
let folder_name = Path::new(&target)
1264+
.file_name()
1265+
.map(|value| value.to_string_lossy().to_string())
1266+
.unwrap_or_default();
1267+
1268+
if let Some(version) = folder_name.strip_prefix("node-v") {
1269+
if !version.is_empty() {
1270+
return Ok(Some(version.to_string()));
1271+
}
1272+
}
11691273

1170-
if version.is_empty() {
11711274
Ok(None)
1172-
} else {
1173-
Ok(Some(version))
1275+
}
1276+
#[cfg(not(target_os = "windows"))]
1277+
{
1278+
Ok(None)
11741279
}
11751280
}
11761281

@@ -1218,48 +1323,20 @@ fn list_node_versions(base_dir: String) -> Result<Vec<NodeVersionState>, String>
12181323
fn activate_node_version(base_dir: String, version: String) -> Result<String, String> {
12191324
#[cfg(target_os = "windows")]
12201325
{
1221-
let normalized = normalize_node_version_tag(&version)?;
1222-
let root = node_root_dir(&base_dir);
1223-
fs::create_dir_all(&root).map_err(|e| e.to_string())?;
1224-
1225-
let target_dir = node_version_dir(&base_dir, &normalized);
1226-
if !target_dir.join("node.exe").exists() {
1227-
return Err(format!("Node {} is not installed", normalized));
1228-
}
1229-
1230-
let current_dir = node_current_dir(&base_dir);
1231-
let script = format!(
1232-
r#"
1233-
$current = '{current}'
1234-
$target = '{target}'
1235-
1236-
if (Test-Path -LiteralPath $current) {{
1237-
$item = Get-Item -LiteralPath $current -Force
1238-
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {{
1239-
cmd /c rmdir "$current" | Out-Null
1240-
}} else {{
1241-
Remove-Item -LiteralPath $current -Force -Recurse
1242-
}}
1243-
}}
1326+
let normalized = normalize_node_version_tag(&version)?;
1327+
let root = node_root_dir(&base_dir);
1328+
fs::create_dir_all(&root).map_err(|e| e.to_string())?;
12441329

1245-
New-Item -ItemType Junction -Path $current -Target $target | Out-Null
1246-
Write-Output $current
1247-
"#,
1248-
current = current_dir.to_string_lossy().replace('\'', "''"),
1249-
target = target_dir.to_string_lossy().replace('\'', "''"),
1250-
);
1330+
let target_dir = node_version_dir(&base_dir, &normalized);
1331+
if !target_dir.join("node.exe").exists() {
1332+
return Err(format!("Node {} is not installed", normalized));
1333+
}
12511334

1252-
let output = run_hidden_powershell(&script)?;
1253-
if !output.status.success() {
1254-
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1255-
return Err(if stderr.is_empty() {
1256-
format!("Failed to activate Node {}", normalized)
1257-
} else {
1258-
stderr
1259-
});
1260-
}
1335+
let current_dir = node_current_dir(&base_dir);
1336+
remove_junction(&current_dir)?;
1337+
create_junction(&current_dir, &target_dir)?;
12611338

1262-
Ok(current_dir.to_string_lossy().replace("\\", "/"))
1339+
Ok(current_dir.to_string_lossy().replace("\\", "/"))
12631340
}
12641341
#[cfg(not(target_os = "windows"))]
12651342
{
@@ -1274,37 +1351,7 @@ fn deactivate_node_version(base_dir: String) -> Result<(), String> {
12741351
#[cfg(target_os = "windows")]
12751352
{
12761353
let current_dir = node_current_dir(&base_dir);
1277-
if !current_dir.exists() {
1278-
return Ok(());
1279-
}
1280-
1281-
let script = format!(
1282-
r#"
1283-
$current = '{current}'
1284-
1285-
if (Test-Path -LiteralPath $current) {{
1286-
$item = Get-Item -LiteralPath $current -Force
1287-
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {{
1288-
cmd /c rmdir "$current" | Out-Null
1289-
}} else {{
1290-
Remove-Item -LiteralPath $current -Force -Recurse
1291-
}}
1292-
}}
1293-
"#,
1294-
current = current_dir.to_string_lossy().replace('\'', "''"),
1295-
);
1296-
1297-
let output = run_hidden_powershell(&script)?;
1298-
if output.status.success() {
1299-
Ok(())
1300-
} else {
1301-
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1302-
Err(if stderr.is_empty() {
1303-
"Failed to deactivate current Node version".into()
1304-
} else {
1305-
stderr
1306-
})
1307-
}
1354+
remove_junction(&current_dir)
13081355
}
13091356
#[cfg(not(target_os = "windows"))]
13101357
{
@@ -1330,25 +1377,8 @@ fn get_node_path_status(base_dir: String) -> Result<NodePathStatus, String> {
13301377
.replace("/", "\\")
13311378
.trim_end_matches('\\')
13321379
.to_string();
1333-
1334-
let output = run_hidden_powershell(
1335-
r#"
1336-
$cmd = Get-Command node -All -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -Unique
1337-
if ($cmd) {
1338-
$cmd | ForEach-Object { Write-Output $_ }
1339-
}
1340-
"#,
1341-
)?;
1342-
1343-
if !output.status.success() {
1344-
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
1345-
}
1346-
1347-
let paths = String::from_utf8_lossy(&output.stdout)
1348-
.lines()
1349-
.map(|line| line.trim().replace("/", "\\"))
1350-
.filter(|line| !line.is_empty())
1351-
.collect::<Vec<_>>();
1380+
let process_path = std::env::var("PATH").unwrap_or_default();
1381+
let paths = node_search_paths_from_path(&process_path);
13521382

13531383
let current_node_path = paths.first().cloned();
13541384
let devstack_prefix = format!("{}\\node.exe", devstack_current);
@@ -1357,7 +1387,7 @@ if ($cmd) {
13571387
.map(|path| path.eq_ignore_ascii_case(&devstack_prefix))
13581388
.unwrap_or(false);
13591389

1360-
let user_path = std::env::var("PATH").unwrap_or_default();
1390+
let user_path = read_user_path_value()?;
13611391
let user_path_contains_devstack = user_path
13621392
.split(';')
13631393
.map(|part| part.trim().replace("/", "\\").trim_end_matches('\\').to_string())
@@ -1391,43 +1421,20 @@ fn set_node_global_path(base_dir: String) -> Result<(), String> {
13911421
.replace("/", "\\")
13921422
.trim_end_matches('\\')
13931423
.to_string();
1394-
let escaped = target.replace('\'', "''");
1395-
1396-
let script = format!(
1397-
r#"
1398-
$target = '{target}'
1399-
$normalizedTarget = $target.ToLowerInvariant().TrimEnd('\')
1400-
$existing = [Environment]::GetEnvironmentVariable('Path', 'User')
1401-
$parts = @()
1402-
1403-
if ($existing) {{
1404-
$parts = $existing -split ';' | Where-Object {{
1405-
$_ -and ($_.Trim() -ne '')
1406-
}} | ForEach-Object {{
1407-
$_.Trim()
1408-
}} | Where-Object {{
1409-
($_.ToLowerInvariant().Replace('/','\').TrimEnd('\')) -ne $normalizedTarget
1410-
}}
1411-
}}
1412-
1413-
$newParts = @($target) + $parts
1414-
$newPath = ($newParts -join ';').Trim(';')
1415-
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
1416-
"#,
1417-
target = escaped
1418-
);
1424+
let normalized_target = target.to_lowercase();
1425+
let existing = read_user_path_value()?;
1426+
let parts = existing
1427+
.split(';')
1428+
.map(|part| part.trim())
1429+
.filter(|part| !part.is_empty())
1430+
.map(|part| part.replace("/", "\\").trim_end_matches('\\').to_string())
1431+
.filter(|part| part.to_lowercase() != normalized_target)
1432+
.collect::<Vec<_>>();
14191433

1420-
let output = run_hidden_powershell(&script)?;
1421-
if output.status.success() {
1422-
Ok(())
1423-
} else {
1424-
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1425-
Err(if stderr.is_empty() {
1426-
"Failed to update User PATH for Node.js".into()
1427-
} else {
1428-
stderr
1429-
})
1430-
}
1434+
let mut new_parts = vec![target];
1435+
new_parts.extend(parts);
1436+
let new_path = new_parts.join(";");
1437+
write_user_path_value(&new_path)
14311438
}
14321439
#[cfg(not(target_os = "windows"))]
14331440
{

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "DevStack",
4-
"version": "1.0.4",
4+
"version": "1.0.5",
55
"identifier": "com.devstack.desktop",
66
"build": {
77
"beforeDevCommand": "npm run dev",

0 commit comments

Comments
 (0)