Skip to content

Commit dc19288

Browse files
committed
fix(cli): respect ZDOTDIR and XDG_CONFIG_HOME in doctor and implode
1 parent 18ebfb6 commit dc19288

2 files changed

Lines changed: 288 additions & 16 deletions

File tree

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,33 @@ fn check_profile_files(vite_plus_home: &str) -> Option<String> {
476476
}
477477
}
478478

479+
// If ZDOTDIR is set and differs from $HOME, also check $ZDOTDIR/.zshenv and .zshrc
480+
if let Ok(zdotdir) = std::env::var("ZDOTDIR") {
481+
if !zdotdir.is_empty() && zdotdir != home_dir {
482+
for file in [".zshenv", ".zshrc"] {
483+
let path = format!("{zdotdir}/{file}");
484+
if let Ok(content) = std::fs::read_to_string(&path) {
485+
if search_strings.iter().any(|s| content.contains(s)) {
486+
return Some(abbreviate_home(&path));
487+
}
488+
}
489+
}
490+
}
491+
}
492+
493+
// If XDG_CONFIG_HOME is set and differs from default, also check fish conf.d
494+
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
495+
let default_config = format!("{home_dir}/.config");
496+
if !xdg_config.is_empty() && xdg_config != default_config {
497+
let path = format!("{xdg_config}/fish/conf.d/vite-plus.fish");
498+
if let Ok(content) = std::fs::read_to_string(&path) {
499+
if search_strings.iter().any(|s| content.contains(s)) {
500+
return Some(abbreviate_home(&path));
501+
}
502+
}
503+
}
504+
}
505+
479506
None
480507
}
481508

@@ -766,4 +793,96 @@ mod tests {
766793
assert_eq!(abbreviate_home("/usr/local/bin"), "/usr/local/bin");
767794
}
768795
}
796+
797+
/// Guard for env vars used by profile file tests.
798+
struct ProfileEnvGuard {
799+
original_home: Option<std::ffi::OsString>,
800+
original_zdotdir: Option<std::ffi::OsString>,
801+
original_xdg_config: Option<std::ffi::OsString>,
802+
}
803+
804+
impl ProfileEnvGuard {
805+
fn new(
806+
home: &std::path::Path,
807+
zdotdir: Option<&std::path::Path>,
808+
xdg_config: Option<&std::path::Path>,
809+
) -> Self {
810+
let guard = Self {
811+
original_home: std::env::var_os("HOME"),
812+
original_zdotdir: std::env::var_os("ZDOTDIR"),
813+
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
814+
};
815+
unsafe {
816+
std::env::set_var("HOME", home);
817+
match zdotdir {
818+
Some(v) => std::env::set_var("ZDOTDIR", v),
819+
None => std::env::remove_var("ZDOTDIR"),
820+
}
821+
match xdg_config {
822+
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
823+
None => std::env::remove_var("XDG_CONFIG_HOME"),
824+
}
825+
}
826+
guard
827+
}
828+
}
829+
830+
impl Drop for ProfileEnvGuard {
831+
fn drop(&mut self) {
832+
unsafe {
833+
match &self.original_home {
834+
Some(v) => std::env::set_var("HOME", v),
835+
None => std::env::remove_var("HOME"),
836+
}
837+
match &self.original_zdotdir {
838+
Some(v) => std::env::set_var("ZDOTDIR", v),
839+
None => std::env::remove_var("ZDOTDIR"),
840+
}
841+
match &self.original_xdg_config {
842+
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
843+
None => std::env::remove_var("XDG_CONFIG_HOME"),
844+
}
845+
}
846+
}
847+
}
848+
849+
#[test]
850+
#[serial]
851+
#[cfg(not(windows))]
852+
fn test_check_profile_files_finds_zdotdir() {
853+
let temp = TempDir::new().unwrap();
854+
let fake_home = temp.path().join("home");
855+
let zdotdir = temp.path().join("zdotdir");
856+
std::fs::create_dir_all(&fake_home).unwrap();
857+
std::fs::create_dir_all(&zdotdir).unwrap();
858+
859+
std::fs::write(zdotdir.join(".zshenv"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
860+
861+
let _guard = ProfileEnvGuard::new(&fake_home, Some(&zdotdir), None);
862+
863+
let result = check_profile_files("$HOME/.vite-plus");
864+
assert!(result.is_some(), "Should find .zshenv in ZDOTDIR");
865+
assert!(result.unwrap().ends_with(".zshenv"));
866+
}
867+
868+
#[test]
869+
#[serial]
870+
#[cfg(not(windows))]
871+
fn test_check_profile_files_finds_xdg_fish() {
872+
let temp = TempDir::new().unwrap();
873+
let fake_home = temp.path().join("home");
874+
let xdg_config = temp.path().join("xdg_config");
875+
let fish_dir = xdg_config.join("fish/conf.d");
876+
std::fs::create_dir_all(&fake_home).unwrap();
877+
std::fs::create_dir_all(&fish_dir).unwrap();
878+
879+
std::fs::write(fish_dir.join("vite-plus.fish"), "source \"$HOME/.vite-plus/env\"\n")
880+
.unwrap();
881+
882+
let _guard = ProfileEnvGuard::new(&fake_home, None, Some(&xdg_config));
883+
884+
let result = check_profile_files("$HOME/.vite-plus");
885+
assert!(result.is_some(), "Should find vite-plus.fish in XDG_CONFIG_HOME");
886+
assert!(result.unwrap().contains("vite-plus.fish"));
887+
}
769888
}

crates/vite_global_cli/src/commands/implode.rs

Lines changed: 169 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77

88
use directories::BaseDirs;
99
use owo_colors::OwoColorize;
10-
use vite_path::AbsolutePathBuf;
10+
use vite_path::{AbsolutePath, AbsolutePathBuf};
1111
use vite_shared::output;
1212
use vite_str::Str;
1313

@@ -23,6 +23,14 @@ const SHELL_PROFILES: &[(&str, bool)] = &[
2323
(".config/fish/conf.d/vite-plus.fish", true),
2424
];
2525

26+
/// Abbreviate a path for display: replace `$HOME` prefix with `~`.
27+
fn abbreviate_home_path(path: &AbsolutePath, user_home: &AbsolutePath) -> Str {
28+
match path.strip_prefix(user_home) {
29+
Ok(Some(suffix)) => vite_str::format!("~/{suffix}"),
30+
_ => Str::from(path.to_string()),
31+
}
32+
}
33+
2634
/// Comment marker written by the install script above the sourcing line.
2735
const VITE_PLUS_COMMENT: &str = "# Vite+ bin";
2836

@@ -97,26 +105,52 @@ enum AffectedProfileKind {
97105
/// Content is cached so we don't need to re-read during cleaning.
98106
fn collect_affected_profiles(user_home: &AbsolutePathBuf) -> Vec<AffectedProfile> {
99107
let mut affected = Vec::new();
100-
for &(name, is_snippet) in SHELL_PROFILES {
101-
let path = user_home.join(name);
108+
109+
// Build full list of (display_name, path, is_snippet) from the base set
110+
let mut profiles: Vec<(Str, AbsolutePathBuf, bool)> = SHELL_PROFILES
111+
.iter()
112+
.map(|&(name, is_snippet)| {
113+
(vite_str::format!("~/{name}"), user_home.join(name), is_snippet)
114+
})
115+
.collect();
116+
117+
// If ZDOTDIR is set and differs from $HOME, also check there.
118+
if let Ok(zdotdir) = std::env::var("ZDOTDIR")
119+
&& let Some(zdotdir_path) = AbsolutePathBuf::new(zdotdir.into())
120+
&& zdotdir_path != *user_home
121+
{
122+
for name in [".zshenv", ".zshrc"] {
123+
let path = zdotdir_path.join(name);
124+
let display = abbreviate_home_path(&path, user_home);
125+
profiles.push((display, path, false));
126+
}
127+
}
128+
129+
// If XDG_CONFIG_HOME is set and differs from $HOME/.config, also check there.
130+
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME")
131+
&& let Some(xdg_path) = AbsolutePathBuf::new(xdg_config.into())
132+
&& xdg_path != user_home.join(".config")
133+
{
134+
let path = xdg_path.join("fish/conf.d/vite-plus.fish");
135+
let display = abbreviate_home_path(&path, user_home);
136+
profiles.push((display, path, true));
137+
}
138+
139+
for (name, path, is_snippet) in profiles {
102140
// For snippets, check if the file exists only
103141
if is_snippet {
104-
if let Some(true) = std::fs::exists(&path).ok() {
105-
affected.push(AffectedProfile {
106-
name: Str::from(name),
107-
path,
108-
kind: AffectedProfileKind::Snippet,
109-
})
142+
if let Ok(true) = std::fs::exists(&path) {
143+
affected.push(AffectedProfile { name, path, kind: AffectedProfileKind::Snippet })
110144
}
111145
continue;
112146
}
113147
// Read directly — if the file doesn't exist, read_to_string returns Err
114-
// which is_ok_and handles gracefully (no redundant exists() check).
148+
// which .ok().filter() handles gracefully (no redundant exists() check).
115149
if let Some(content) =
116150
std::fs::read_to_string(&path).ok().filter(|c| has_vite_plus_lines(c))
117151
{
118152
affected.push(AffectedProfile {
119-
name: Str::from(name),
153+
name,
120154
path,
121155
kind: AffectedProfileKind::Main { content: Str::from(content) },
122156
});
@@ -144,7 +178,7 @@ fn confirm_implode(
144178
if !affected_profiles.is_empty() {
145179
output::raw(" Shell profiles to clean:");
146180
for profile in affected_profiles {
147-
output::raw(&vite_str::format!(" - ~/{}", profile.name));
181+
output::raw(&vite_str::format!(" - {}", profile.name));
148182
}
149183
}
150184
output::raw("");
@@ -171,16 +205,16 @@ fn clean_affected_profiles(affected_profiles: &[AffectedProfile]) {
171205
AffectedProfileKind::Main { content } => {
172206
let cleaned = remove_vite_plus_lines(content);
173207
match std::fs::write(&profile.path, cleaned.as_bytes()) {
174-
Ok(()) => output::success(&vite_str::format!("Cleaned ~/{}", profile.name)),
208+
Ok(()) => output::success(&vite_str::format!("Cleaned {}", profile.name)),
175209
Err(e) => {
176-
output::warn(&vite_str::format!("Failed to clean ~/{}: {e}", profile.name));
210+
output::warn(&vite_str::format!("Failed to clean {}: {e}", profile.name));
177211
}
178212
}
179213
}
180214
AffectedProfileKind::Snippet => match std::fs::remove_file(&profile.path) {
181-
Ok(()) => output::success(&vite_str::format!("Removed ~/{}", profile.name)),
215+
Ok(()) => output::success(&vite_str::format!("Removed {}", profile.name)),
182216
Err(e) => {
183-
output::warn(&vite_str::format!("Failed to remove ~/{}: {e}", profile.name));
217+
output::warn(&vite_str::format!("Failed to remove {}: {e}", profile.name));
184218
}
185219
},
186220
}
@@ -336,6 +370,8 @@ fn remove_windows_path_entry(bin_path: &vite_path::AbsolutePath) -> std::io::Res
336370

337371
#[cfg(test)]
338372
mod tests {
373+
use serial_test::serial;
374+
339375
use super::*;
340376

341377
#[test]
@@ -420,6 +456,123 @@ mod tests {
420456
assert!(script.contains("timeout /T 1 /NOBREAK"));
421457
}
422458

459+
#[test]
460+
fn test_abbreviate_home_path() {
461+
let home = AbsolutePathBuf::new("/home/user".into()).unwrap();
462+
// Under home → ~/...
463+
let under = AbsolutePathBuf::new("/home/user/.zshrc".into()).unwrap();
464+
assert_eq!(&*abbreviate_home_path(&under, &home), "~/.zshrc");
465+
// Outside home → absolute path as-is
466+
let outside = AbsolutePathBuf::new("/opt/zdotdir/.zshenv".into()).unwrap();
467+
assert_eq!(&*abbreviate_home_path(&outside, &home), "/opt/zdotdir/.zshenv");
468+
}
469+
470+
#[test]
471+
#[serial]
472+
fn test_collect_affected_profiles() {
473+
let temp_dir = tempfile::tempdir().unwrap();
474+
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
475+
476+
// Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
477+
let _guard = ProfileEnvGuard::new(None, None);
478+
479+
// Main profile with vite-plus line
480+
std::fs::write(home.join(".zshrc"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
481+
// Unrelated profile (should be ignored)
482+
std::fs::write(home.join(".bashrc"), "export PATH=/usr/bin\n").unwrap();
483+
// Snippet file (just needs to exist)
484+
let fish_dir = home.join(".config/fish/conf.d");
485+
std::fs::create_dir_all(&fish_dir).unwrap();
486+
std::fs::write(fish_dir.join("vite-plus.fish"), "source ~/.vite-plus/env.fish\n").unwrap();
487+
488+
let profiles = collect_affected_profiles(&home);
489+
assert_eq!(profiles.len(), 2);
490+
assert!(matches!(&profiles[0].kind, AffectedProfileKind::Main { .. }));
491+
assert!(matches!(&profiles[1].kind, AffectedProfileKind::Snippet));
492+
}
493+
494+
/// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
495+
struct ProfileEnvGuard {
496+
original_zdotdir: Option<std::ffi::OsString>,
497+
original_xdg_config: Option<std::ffi::OsString>,
498+
}
499+
500+
impl ProfileEnvGuard {
501+
fn new(zdotdir: Option<&std::path::Path>, xdg_config: Option<&std::path::Path>) -> Self {
502+
let guard = Self {
503+
original_zdotdir: std::env::var_os("ZDOTDIR"),
504+
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
505+
};
506+
unsafe {
507+
match zdotdir {
508+
Some(v) => std::env::set_var("ZDOTDIR", v),
509+
None => std::env::remove_var("ZDOTDIR"),
510+
}
511+
match xdg_config {
512+
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
513+
None => std::env::remove_var("XDG_CONFIG_HOME"),
514+
}
515+
}
516+
guard
517+
}
518+
}
519+
520+
impl Drop for ProfileEnvGuard {
521+
fn drop(&mut self) {
522+
unsafe {
523+
match &self.original_zdotdir {
524+
Some(v) => std::env::set_var("ZDOTDIR", v),
525+
None => std::env::remove_var("ZDOTDIR"),
526+
}
527+
match &self.original_xdg_config {
528+
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
529+
None => std::env::remove_var("XDG_CONFIG_HOME"),
530+
}
531+
}
532+
}
533+
}
534+
535+
#[test]
536+
#[serial]
537+
fn test_collect_affected_profiles_zdotdir() {
538+
let temp_dir = tempfile::tempdir().unwrap();
539+
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
540+
let zdotdir = temp_dir.path().join("zdotdir");
541+
std::fs::create_dir_all(&home).unwrap();
542+
std::fs::create_dir_all(&zdotdir).unwrap();
543+
544+
std::fs::write(zdotdir.join(".zshenv"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
545+
546+
let _guard = ProfileEnvGuard::new(Some(&zdotdir), None);
547+
548+
let profiles = collect_affected_profiles(&home);
549+
let zdotdir_profiles: Vec<_> =
550+
profiles.iter().filter(|p| p.path.as_path().starts_with(&zdotdir)).collect();
551+
assert_eq!(zdotdir_profiles.len(), 1);
552+
assert!(matches!(&zdotdir_profiles[0].kind, AffectedProfileKind::Main { .. }));
553+
}
554+
555+
#[test]
556+
#[serial]
557+
fn test_collect_affected_profiles_xdg_config() {
558+
let temp_dir = tempfile::tempdir().unwrap();
559+
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
560+
let xdg_config = temp_dir.path().join("xdg_config");
561+
let fish_dir = xdg_config.join("fish/conf.d");
562+
std::fs::create_dir_all(&home).unwrap();
563+
std::fs::create_dir_all(&fish_dir).unwrap();
564+
565+
std::fs::write(fish_dir.join("vite-plus.fish"), "").unwrap();
566+
567+
let _guard = ProfileEnvGuard::new(None, Some(&xdg_config));
568+
569+
let profiles = collect_affected_profiles(&home);
570+
let xdg_profiles: Vec<_> =
571+
profiles.iter().filter(|p| p.path.as_path().starts_with(&xdg_config)).collect();
572+
assert_eq!(xdg_profiles.len(), 1);
573+
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
574+
}
575+
423576
#[test]
424577
fn test_execute_not_installed() {
425578
let temp_dir = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)