Skip to content

Commit 15e8947

Browse files
committed
refactor: simplify VP_HOME matcher, PATH inference, and installer helpers
- implode: normalize source-line args to forward slashes once instead of enumerating separator variants per root; drop path_ref_with_backslashes, push_path_ref and the env.nu special case; document the matcher's multi-install rationale and writer cross-references - home: share VP_BINARY_NAME from vite_shared (re-exported by vite_setup), avoid the per-PATH-entry clone, and drop redundant test guards - env/setup: reuse render_home_relative_path for the Nu VP_HOME and explain why it must stay absolute - install.sh: reuse abbreviate_path for the shims prompt, simplify normalize_existing_dir, use command -v directly - install.ps1: reuse the USERPROFILE-to-~ display idiom, drop the dead .Source fallback and redundant TrimEnd calls
1 parent e1bba3d commit 15e8947

7 files changed

Lines changed: 86 additions & 168 deletions

File tree

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -783,8 +783,10 @@ fn render_env_content(shell: EnvShell, vite_plus_home: &vite_path::AbsolutePath)
783783
// Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not
784784
// expanded at parse time, so PATH entries would contain a literal "$HOME/...".
785785
let bin_path_ref_nu = bin_path_ref.replace("$HOME/", "~/");
786-
let home_path_ref_nu =
787-
vite_plus_home.as_path().display().to_string().replace('\\', "/");
786+
// `~` is only expanded in Nushell path-literal positions, not general
787+
// strings, so a `~/...` VP_HOME would leak literally into the env var;
788+
// bake the absolute path instead (`None` skips $HOME-relativization).
789+
let home_path_ref_nu = render_home_relative_path(vite_plus_home.as_path(), None);
788790
ENV_TEMPLATE_NU
789791
.replace("__VP_HOME__", &home_path_ref_nu)
790792
.replace("__VP_BIN__", &bin_path_ref_nu)
@@ -1206,13 +1208,9 @@ mod tests {
12061208
create_env_files(&home).await.unwrap();
12071209

12081210
let cmd_content = tokio::fs::read_to_string(bin_dir.join("vp-use.cmd")).await.unwrap();
1209-
let vp_home_index =
1210-
cmd_content.find("set VP_HOME=%~dp0..").expect("vp-use.cmd should set VP_HOME");
1211-
let env_use_index = cmd_content.find("for /f").expect("vp-use.cmd should run env use");
1212-
12131211
assert!(
1214-
vp_home_index < env_use_index,
1215-
"vp-use.cmd should set VP_HOME before invoking vp env use"
1212+
cmd_content.contains("set VP_HOME=%~dp0..\r\nfor /f"),
1213+
"vp-use.cmd should set VP_HOME before invoking vp env use, got: {cmd_content}"
12161214
);
12171215
assert!(
12181216
cmd_content.contains("%~dp0..\\current\\bin\\vp.exe env use %*"),

crates/vite_global_cli/src/commands/implode.rs

Lines changed: 43 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,9 @@ fn collect_affected_profiles(
106106

107107
// Read directly — if the file doesn't exist, read_to_string returns Err
108108
// which .ok().filter() handles gracefully (no redundant exists() check).
109-
if let Some(content) = std::fs::read_to_string(&path)
110-
.ok()
111-
.filter(|c| contains_vite_plus_source_line(c, source_matcher, profile.env_file))
112-
{
109+
if let Some(content) = std::fs::read_to_string(&path).ok().filter(|c| {
110+
c.lines().any(|line| source_matcher.is_vite_plus_source_line(line, profile.env_file))
111+
}) {
113112
if matches!(profile.kind, ShellProfileKind::Snippet) {
114113
affected.push(AffectedProfile { name, path, kind: AffectedProfileKind::Snippet });
115114
continue;
@@ -273,81 +272,57 @@ fn spawn_deferred_delete(trash_path: &std::path::Path) -> std::io::Result<std::p
273272
.spawn()
274273
}
275274

275+
/// Matches shell-profile `source` lines that reference *this* install's env
276+
/// files, so a second Vite+ install's lines are left untouched.
277+
///
278+
/// The recognized home spellings must mirror what the writers emit:
279+
/// `install.sh`/`install.ps1` (shell PATH setup) and `render_env_content` in
280+
/// `env/setup.rs`. `env/doctor.rs::check_profile_files` derives the same
281+
/// variants for its profile scan; keep them in sync.
276282
struct VitePlusSourceMatcher {
283+
/// Home-dir spellings with forward-slash separators: the absolute path,
284+
/// plus `$HOME`- and `~`-relative forms when the home is under `$HOME`.
277285
roots: Vec<Str>,
278286
}
279287

280288
impl VitePlusSourceMatcher {
281289
fn new(home_dir: &AbsolutePathBuf, user_home: &AbsolutePathBuf) -> Self {
282-
Self { roots: vite_plus_home_refs(home_dir, user_home) }
290+
let mut roots = vec![normalize_path_separators(&home_dir.as_path().display().to_string())];
291+
292+
if let Ok(Some(suffix)) = home_dir.strip_prefix(user_home) {
293+
// `RelativePathBuf` guarantees forward-slash separators.
294+
let suffix = vite_str::format!("{suffix}");
295+
if suffix.is_empty() {
296+
roots.push(Str::from("$HOME"));
297+
roots.push(Str::from("~"));
298+
} else {
299+
roots.push(vite_str::format!("$HOME/{suffix}"));
300+
roots.push(vite_str::format!("~/{suffix}"));
301+
}
302+
}
303+
304+
Self { roots }
283305
}
284306

285307
fn is_vite_plus_source_line(&self, line: &str, env_file: &str) -> bool {
286308
let Some(arg) = source_line_arg(line) else {
287309
return false;
288310
};
289311

290-
self.roots.iter().any(|root| {
291-
let path = join_path_ref(root, env_file);
292-
arg == &*path || (env_file == "env.nu" && arg == &*path_ref_with_backslashes(&path))
293-
})
294-
}
295-
}
296-
297-
fn vite_plus_home_refs(home_dir: &AbsolutePathBuf, user_home: &AbsolutePathBuf) -> Vec<Str> {
298-
let mut refs = Vec::new();
299-
push_path_ref(&mut refs, vite_str::format!("{}", home_dir.as_path().display()));
300-
push_path_ref(&mut refs, normalize_path_separators(&home_dir.as_path().display().to_string()));
301-
302-
if let Ok(Some(suffix)) = home_dir.strip_prefix(user_home) {
303-
let suffix = normalize_path_separators(&vite_str::format!("{suffix}"));
304-
if suffix.is_empty() {
305-
push_path_ref(&mut refs, Str::from("$HOME"));
306-
push_path_ref(&mut refs, Str::from("~"));
307-
} else {
308-
push_path_ref(&mut refs, vite_str::format!("$HOME/{suffix}"));
309-
push_path_ref(&mut refs, vite_str::format!("~/{suffix}"));
310-
}
311-
}
312-
313-
refs
314-
}
315-
316-
fn push_path_ref(paths: &mut Vec<Str>, path: Str) {
317-
if !paths.iter().any(|existing| existing == &path) {
318-
paths.push(path);
312+
// Windows profiles may spell the path with backslashes (e.g. Nushell's
313+
// `source '~\.vite-plus\env.nu'`); compare in forward-slash form.
314+
let arg = normalize_path_separators(arg);
315+
self.roots.iter().any(|root| arg == join_path_ref(root, env_file))
319316
}
320317
}
321318

322319
fn join_path_ref(root: &str, env_file: &str) -> Str {
323-
let separator = if root.ends_with('/') || root.ends_with('\\') { "" } else { "/" };
320+
let separator = if root.ends_with('/') { "" } else { "/" };
324321
vite_str::format!("{root}{separator}{env_file}")
325322
}
326323

327-
#[expect(clippy::disallowed_types)]
328324
fn normalize_path_separators(path: &str) -> Str {
329-
let mut normalized = String::with_capacity(path.len());
330-
for ch in path.chars() {
331-
normalized.push(if ch == '\\' { '/' } else { ch });
332-
}
333-
Str::from(normalized)
334-
}
335-
336-
#[expect(clippy::disallowed_types)]
337-
fn path_ref_with_backslashes(path: &str) -> Str {
338-
let mut normalized = String::with_capacity(path.len());
339-
for ch in path.chars() {
340-
normalized.push(if ch == '/' { '\\' } else { ch });
341-
}
342-
Str::from(normalized)
343-
}
344-
345-
fn contains_vite_plus_source_line(
346-
content: &str,
347-
source_matcher: &VitePlusSourceMatcher,
348-
env_file: &str,
349-
) -> bool {
350-
content.lines().any(|line| source_matcher.is_vite_plus_source_line(line, env_file))
325+
Str::from(path.replace('\\', "/"))
351326
}
352327

353328
fn source_line_arg(line: &str) -> Option<&str> {
@@ -459,13 +434,6 @@ mod tests {
459434
VitePlusSourceMatcher::new(&home_dir, &user_home)
460435
}
461436

462-
fn source_matcher_for(
463-
home_dir: &AbsolutePathBuf,
464-
user_home: &AbsolutePathBuf,
465-
) -> VitePlusSourceMatcher {
466-
VitePlusSourceMatcher::new(home_dir, user_home)
467-
}
468-
469437
#[test]
470438
fn test_remove_vite_plus_lines_posix() {
471439
let matcher = default_source_matcher();
@@ -486,7 +454,7 @@ mod tests {
486454
fn test_remove_vite_plus_lines_absolute_path() {
487455
let user_home = default_user_home();
488456
let home_dir = user_home.join(".vite-plus");
489-
let matcher = source_matcher_for(&home_dir, &user_home);
457+
let matcher = VitePlusSourceMatcher::new(&home_dir, &user_home);
490458
let env_path = shell_path(&home_dir.join("env"));
491459
let content = vite_str::format!("# existing\n. \"{env_path}\"\n");
492460
let result = remove_vite_plus_lines(&content, &matcher, "env");
@@ -497,7 +465,7 @@ mod tests {
497465
fn test_remove_vite_plus_lines_custom_absolute_path() {
498466
let user_home = custom_user_home();
499467
let home_dir = user_home.join("tools").join("vp");
500-
let matcher = source_matcher_for(&home_dir, &user_home);
468+
let matcher = VitePlusSourceMatcher::new(&home_dir, &user_home);
501469
let env_path = shell_path(&home_dir.join("env"));
502470
let content = vite_str::format!("# existing\n. \"{env_path}\"\n");
503471
let result = remove_vite_plus_lines(&content, &matcher, "env");
@@ -508,7 +476,7 @@ mod tests {
508476
fn test_remove_vite_plus_lines_custom_home_relative_path() {
509477
let user_home = custom_user_home();
510478
let home_dir = user_home.join("tools").join("vp");
511-
let matcher = source_matcher_for(&home_dir, &user_home);
479+
let matcher = VitePlusSourceMatcher::new(&home_dir, &user_home);
512480
let content = "# existing\n. \"$HOME/tools/vp/env\"\n";
513481
let result = remove_vite_plus_lines(content, &matcher, "env");
514482
assert_eq!(&*result, "# existing\n");
@@ -518,7 +486,7 @@ mod tests {
518486
fn test_remove_vite_plus_lines_custom_tilde_path() {
519487
let user_home = custom_user_home();
520488
let home_dir = user_home.join("tools").join("vp");
521-
let matcher = source_matcher_for(&home_dir, &user_home);
489+
let matcher = VitePlusSourceMatcher::new(&home_dir, &user_home);
522490
let content = "# existing\nsource '~/tools/vp/env.nu'\n";
523491
let result = remove_vite_plus_lines(content, &matcher, "env.nu");
524492
assert_eq!(&*result, "# existing\n");
@@ -577,7 +545,7 @@ mod tests {
577545
let temp_dir = tempfile::tempdir().unwrap();
578546
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
579547
let home_dir = temp_path.join(".vite-plus");
580-
let matcher = source_matcher_for(&home_dir, &temp_path);
548+
let matcher = VitePlusSourceMatcher::new(&home_dir, &temp_path);
581549
let profile_path = temp_path.join(".zshrc");
582550
let original = "# my config\nexport FOO=bar\n\n# Vite+ bin (https://viteplus.dev)\n. \"$HOME/.vite-plus/env\"\n";
583551
std::fs::write(&profile_path, original).unwrap();
@@ -647,7 +615,7 @@ mod tests {
647615
let temp_dir = tempfile::tempdir().unwrap();
648616
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
649617
let home_dir = home.join(".vite-plus");
650-
let matcher = source_matcher_for(&home_dir, &home);
618+
let matcher = VitePlusSourceMatcher::new(&home_dir, &home);
651619

652620
// Clear env overrides so the test environment doesn't affect results
653621
let _guard = ProfileEnvGuard::new(None, None, None);
@@ -674,7 +642,7 @@ mod tests {
674642
let temp_dir = tempfile::tempdir().unwrap();
675643
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
676644
let home_dir = home.join("tools/vp");
677-
let matcher = source_matcher_for(&home_dir, &home);
645+
let matcher = VitePlusSourceMatcher::new(&home_dir, &home);
678646

679647
let _guard = ProfileEnvGuard::new(None, None, None);
680648

@@ -760,7 +728,7 @@ mod tests {
760728
std::fs::write(zdotdir.join(".zshenv"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
761729

762730
let _guard = ProfileEnvGuard::new(Some(&zdotdir), None, None);
763-
let matcher = source_matcher_for(&home.join(".vite-plus"), &home);
731+
let matcher = VitePlusSourceMatcher::new(&home.join(".vite-plus"), &home);
764732

765733
let profiles = collect_affected_profiles(&home, &matcher);
766734
let zdotdir_profiles: Vec<_> =
@@ -784,7 +752,7 @@ mod tests {
784752
.unwrap();
785753

786754
let _guard = ProfileEnvGuard::new(None, Some(&xdg_config), None);
787-
let matcher = source_matcher_for(&home.join(".vite-plus"), &home);
755+
let matcher = VitePlusSourceMatcher::new(&home.join(".vite-plus"), &home);
788756

789757
let profiles = collect_affected_profiles(&home, &matcher);
790758
let xdg_profiles: Vec<_> =
@@ -807,7 +775,7 @@ mod tests {
807775
std::fs::write(nushell_dir.join("vite-plus.nu"), "source '~/.vite-plus/env.nu'\n").unwrap();
808776

809777
let _guard = ProfileEnvGuard::new(None, None, Some(&xdg_data));
810-
let matcher = source_matcher_for(&home.join(".vite-plus"), &home);
778+
let matcher = VitePlusSourceMatcher::new(&home.join(".vite-plus"), &home);
811779

812780
let profiles = collect_affected_profiles(&home, &matcher);
813781
let xdg_profiles: Vec<_> =

crates/vite_setup/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,4 @@ pub mod registry;
2424
/// Maximum number of old versions to keep.
2525
pub const MAX_VERSIONS_KEEP: usize = 3;
2626

27-
/// Platform-specific binary name for the `vp` CLI.
28-
pub const VP_BINARY_NAME: &str = if cfg!(windows) { "vp.exe" } else { "vp" };
27+
pub use vite_shared::VP_BINARY_NAME;

crates/vite_shared/src/home.rs

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{env, path::PathBuf};
1+
use std::env;
22

33
use directories::BaseDirs;
44
use vite_path::{AbsolutePathBuf, current_dir};
@@ -8,6 +8,9 @@ use crate::EnvConfig;
88
/// Default `VP_HOME` directory name
99
const VITE_PLUS_HOME_DIR: &str = ".vite-plus";
1010

11+
/// Platform-specific binary name for the `vp` CLI.
12+
pub const VP_BINARY_NAME: &str = if cfg!(windows) { "vp.exe" } else { "vp" };
13+
1114
/// Get the vite-plus home directory.
1215
///
1316
/// Uses `EnvConfig::get().vite_plus_home` if set,
@@ -50,7 +53,11 @@ fn infer_vp_home_from_path() -> std::io::Result<Option<AbsolutePathBuf>> {
5053
continue;
5154
}
5255

53-
let bin_dir = absolute_path_entry(path_entry)?;
56+
let bin_dir = if path_entry.is_absolute() {
57+
AbsolutePathBuf::new(path_entry).unwrap()
58+
} else {
59+
current_dir()?.join(path_entry)
60+
};
5461
if bin_dir.as_path().file_name().is_none_or(|name| name != "bin") {
5562
continue;
5663
}
@@ -65,23 +72,9 @@ fn infer_vp_home_from_path() -> std::io::Result<Option<AbsolutePathBuf>> {
6572
Ok(None)
6673
}
6774

68-
fn absolute_path_entry(path: PathBuf) -> std::io::Result<AbsolutePathBuf> {
69-
if let Some(path) = AbsolutePathBuf::new(path.clone()) {
70-
return Ok(path);
71-
}
72-
73-
Ok(current_dir()?.join(path))
74-
}
75-
7675
fn is_vp_home_layout(bin_dir: &vite_path::AbsolutePath, home: &vite_path::AbsolutePath) -> bool {
77-
let vp_bin = if cfg!(windows) { bin_dir.join("vp.exe") } else { bin_dir.join("vp") };
78-
let current_vp = if cfg!(windows) {
79-
home.join("current").join("bin").join("vp.exe")
80-
} else {
81-
home.join("current").join("bin").join("vp")
82-
};
83-
84-
vp_bin.as_path().is_file() && current_vp.as_path().is_file()
76+
bin_dir.join(VP_BINARY_NAME).as_path().is_file()
77+
&& home.join("current").join("bin").join(VP_BINARY_NAME).as_path().is_file()
8578
}
8679

8780
#[cfg(test)]
@@ -102,13 +95,6 @@ mod tests {
10295
unsafe { std::env::set_var(name, value) };
10396
guard
10497
}
105-
106-
fn remove(name: &'static str) -> Self {
107-
let guard = Self { name, original: std::env::var_os(name) };
108-
// SAFETY: these serial tests own process environment mutations and restore them on drop.
109-
unsafe { std::env::remove_var(name) };
110-
guard
111-
}
11298
}
11399

114100
impl Drop for EnvVarGuard {
@@ -172,31 +158,21 @@ mod tests {
172158
#[test]
173159
#[serial_test::serial]
174160
fn test_get_vp_home_without_vp_home_infers_from_vp_on_path() {
175-
let temp_dir = PathBuf::from(
176-
std::env::temp_dir().join(format!("vp-test-vp-path-{}", std::process::id())),
177-
);
161+
let temp_dir = std::env::temp_dir().join(format!("vp-test-vp-path-{}", std::process::id()));
178162
let vite_plus_home = temp_dir.join(".vite-plus");
179163
let bin_dir = vite_plus_home.join("bin");
180164
let current_bin_dir = vite_plus_home.join("current").join("bin");
181165
std::fs::create_dir_all(&bin_dir).unwrap();
182166
std::fs::create_dir_all(&current_bin_dir).unwrap();
183167

184-
#[cfg(windows)]
185-
let vp_path = bin_dir.join("vp.exe");
186-
#[cfg(not(windows))]
187-
let vp_path = bin_dir.join("vp");
188-
write_executable(&vp_path);
189-
190-
#[cfg(windows)]
191-
let current_vp_path = current_bin_dir.join("vp.exe");
192-
#[cfg(not(windows))]
193-
let current_vp_path = current_bin_dir.join("vp");
194-
write_executable(&current_vp_path);
168+
write_executable(&bin_dir.join(VP_BINARY_NAME));
169+
write_executable(&current_bin_dir.join(VP_BINARY_NAME));
195170

196171
let path = std::env::join_paths([bin_dir.as_os_str()]).unwrap();
197172
let _path_guard = EnvVarGuard::set("PATH", path);
198-
let _vp_home_guard = EnvVarGuard::remove(crate::env_vars::VP_HOME);
199173

174+
// `EnvConfig::for_test()` leaves `vite_plus_home` unset, so `get_vp_home`
175+
// ignores any real `VP_HOME` env var and exercises the PATH inference.
200176
EnvConfig::test_scope(EnvConfig::for_test(), || {
201177
let home = get_vp_home().unwrap();
202178
assert_eq!(home.as_path(), vite_plus_home.as_path());
@@ -208,23 +184,17 @@ mod tests {
208184
#[test]
209185
#[serial_test::serial]
210186
fn test_get_vp_home_without_vp_home_ignores_relative_bin_without_current_vp() {
211-
let temp_dir = PathBuf::from(
212-
std::env::temp_dir().join(format!("vp-test-relative-bin-{}", std::process::id())),
213-
);
187+
let temp_dir =
188+
std::env::temp_dir().join(format!("vp-test-relative-bin-{}", std::process::id()));
214189
let project_dir = temp_dir.join("project");
215190
let bin_dir = project_dir.join("tools").join("bin");
216191
std::fs::create_dir_all(&bin_dir).unwrap();
217192

218-
#[cfg(windows)]
219-
let vp_path = bin_dir.join("vp.exe");
220-
#[cfg(not(windows))]
221-
let vp_path = bin_dir.join("vp");
222-
write_executable(&vp_path);
193+
write_executable(&bin_dir.join(VP_BINARY_NAME));
223194

224195
let _cwd_guard = CurrentDirGuard::set(&project_dir);
225196
let path = std::env::join_paths([std::path::Path::new("tools/bin")]).unwrap();
226197
let _path_guard = EnvVarGuard::set("PATH", path);
227-
let _vp_home_guard = EnvVarGuard::remove(crate::env_vars::VP_HOME);
228198

229199
EnvConfig::test_scope(EnvConfig::for_test(), || {
230200
let home = get_vp_home().unwrap();

0 commit comments

Comments
 (0)