From 81d3a831652fa598804f6756d365d64056a79be8 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 17:17:55 +0800 Subject: [PATCH 1/6] Revert "fix: match passthough envs with uppercased env names (#25)" This reverts commit 1f1c73b9ce1b0a0e3b1707cf2515edc6766e1292. --- crates/vite_str/src/lib.rs | 8 -------- crates/vite_task/src/execute.rs | 5 ++--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/vite_str/src/lib.rs b/crates/vite_str/src/lib.rs index b18f165d..bf67aaa5 100644 --- a/crates/vite_str/src/lib.rs +++ b/crates/vite_str/src/lib.rs @@ -71,14 +71,6 @@ impl Str { pub fn push_str(&mut self, s: &str) { self.0.push_str(s); } - - pub fn to_uppercase(&self) -> Self { - Self(self.0.to_uppercase()) - } - - pub fn to_lowercase(&self) -> Self { - Self(self.0.to_lowercase()) - } } impl AsRef for Str { diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index d3d1f50c..1de02913 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -265,8 +265,7 @@ impl TaskEnvs { GlobPatternSet::new(task.config.envs.iter().filter(|s| !s.starts_with('!')))?; let sensitive_patterns = GlobPatternSet::new(SENSITIVE_PATTERNS)?; for (name, value) in &all_envs { - let upper_name = name.to_uppercase(); - if !envs_without_pass_through_patterns.is_match(&upper_name) { + if !envs_without_pass_through_patterns.is_match(name) { continue; } let Some(value) = value.to_str() else { @@ -275,7 +274,7 @@ impl TaskEnvs { value: value.to_os_string(), }); }; - let value: Str = if sensitive_patterns.is_match(&upper_name) { + let value: Str = if sensitive_patterns.is_match(name) { let mut hasher = Sha256::new(); hasher.update(value.as_bytes()); format!("sha256:{:x}", hasher.finalize()).into() From e0b9137902b1b8c4cb37d301aba567e372f548e6 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 18:00:30 +0800 Subject: [PATCH 2/6] find PATH case-insensitively on Windows --- crates/vite_task/src/execute.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index 1de02913..21152275 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -310,8 +310,23 @@ impl TaskEnvs { all_envs.insert("VITE_TASK_EXECUTION_ENV".into(), Arc::::from(OsStr::new("1"))); // Add node_modules/.bin to PATH - let env_path = - all_envs.entry("PATH".into()).or_insert_with(|| Arc::::from(OsStr::new(""))); + // On Windows, environment variable names are case-insensitive (e.g., "PATH", "Path", "path" are all the same) + // However, Rust's HashMap keys are case-sensitive, so we need to find the existing PATH variable + // regardless of its casing to avoid creating duplicate PATH entries with different casings. + // For example, if the system has "Path", we should use that instead of creating a new "PATH" entry. + let env_path = { + if cfg!(windows) + && let Some(existing_path) = all_envs.iter_mut().find_map(|(name, value)| { + if name.eq_ignore_ascii_case("path") { Some(value) } else { None } + }) + { + // Found existing PATH variable (with any casing), use it + existing_path + } else { + // On Unix or no existing PATH on Windows, create/get "PATH" entry + all_envs.entry("PATH".into()).or_insert_with(|| Arc::::from(OsStr::new(""))) + } + }; let paths = split_paths(env_path); let node_modules_bin_paths = [ From 406ed1a849e037858253cab68a4e8d4d0bcc8b3f Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 19:48:48 +0800 Subject: [PATCH 3/6] test: add tests for Windows PATH case-insensitivity fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tests for the Windows PATH case-insensitivity handling: - test_windows_path_case_insensitive_mixed_case: Verifies mixed case "Path" is preserved and modified correctly - test_windows_path_case_insensitive_uppercase: Tests uppercase "PATH" baseline case - test_windows_path_created_when_missing: Ensures new "PATH" is created when missing - test_unix_path_case_sensitive: Regression test to ensure Unix maintains case-sensitive PATH behavior All tests verify exact PATH values using OsStr comparisons. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/vite_task/src/execute.rs | 186 ++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index 21152275..5a469cb9 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -901,4 +901,190 @@ mod tests { assert!(all_envs.contains_key("app1_name")); assert!(all_envs.contains_key("app2_name")); } + + #[test] + #[cfg(windows)] + fn test_windows_path_case_insensitive_mixed_case() { + use crate::{ + collections::HashSet, + config::{ResolvedTaskConfig, TaskCommand, TaskConfig}, + }; + + let task_config = TaskConfig { + command: TaskCommand::ShellScript("echo test".into()), + cwd: RelativePathBuf::default(), + cacheable: true, + inputs: HashSet::new(), + envs: HashSet::new(), + pass_through_envs: HashSet::new(), + fingerprint_ignores: None, + }; + let resolved = + ResolvedTaskConfig { config_dir: RelativePathBuf::default(), config: task_config }; + + // Mock environment with mixed case "Path" (common on Windows) + let mock_envs = vec![ + (OsString::from("Path"), OsString::from("C:\\existing\\path")), + (OsString::from("OTHER_VAR"), OsString::from("value")), + ]; + + let base_dir = AbsolutePath::new("C:\\workspace\\packages\\app").unwrap(); + + let result = TaskEnvs::resolve(mock_envs.into_iter(), &base_dir, &resolved).unwrap(); + + let all_envs = result.all_envs; + + // Verify that the original "Path" casing is preserved, not "PATH" + assert!(all_envs.contains_key("Path")); + assert!(!all_envs.contains_key("PATH")); + + // Verify the complete PATH value matches expected + let path_value = all_envs.get("Path").unwrap(); + assert_eq!( + path_value.as_ref(), + OsStr::new( + "C:\\workspace\\packages\\app\\node_modules\\.bin;C:\\workspace\\packages\\app\\node_modules\\.bin;C:\\existing\\path" + ) + ); + + // Verify no duplicate PATH entry was created + let path_like_keys: Vec<_> = + all_envs.keys().filter(|k| k.eq_ignore_ascii_case("path")).collect(); + assert_eq!(path_like_keys.len(), 1); + } + + #[test] + #[cfg(windows)] + fn test_windows_path_case_insensitive_uppercase() { + use crate::{ + collections::HashSet, + config::{ResolvedTaskConfig, TaskCommand, TaskConfig}, + }; + + let task_config = TaskConfig { + command: TaskCommand::ShellScript("echo test".into()), + cwd: RelativePathBuf::default(), + cacheable: true, + inputs: HashSet::new(), + envs: HashSet::new(), + pass_through_envs: HashSet::new(), + fingerprint_ignores: None, + }; + let resolved = + ResolvedTaskConfig { config_dir: RelativePathBuf::default(), config: task_config }; + + // Mock environment with uppercase "PATH" + let mock_envs = vec![ + (OsString::from("PATH"), OsString::from("C:\\existing\\path")), + (OsString::from("OTHER_VAR"), OsString::from("value")), + ]; + + let base_dir = AbsolutePath::new("C:\\workspace\\packages\\app").unwrap(); + + let result = TaskEnvs::resolve(mock_envs.into_iter(), &base_dir, &resolved).unwrap(); + + let all_envs = result.all_envs; + + // Verify that "PATH" is preserved + assert!(all_envs.contains_key("PATH")); + + // Verify the complete PATH value matches expected + let path_value = all_envs.get("PATH").unwrap(); + assert_eq!( + path_value.as_ref(), + OsStr::new( + "C:\\workspace\\packages\\app\\node_modules\\.bin;C:\\workspace\\packages\\app\\node_modules\\.bin;C:\\existing\\path" + ) + ); + } + + #[test] + #[cfg(windows)] + fn test_windows_path_created_when_missing() { + use crate::{ + collections::HashSet, + config::{ResolvedTaskConfig, TaskCommand, TaskConfig}, + }; + + let task_config = TaskConfig { + command: TaskCommand::ShellScript("echo test".into()), + cwd: RelativePathBuf::default(), + cacheable: true, + inputs: HashSet::new(), + envs: HashSet::new(), + pass_through_envs: HashSet::new(), + fingerprint_ignores: None, + }; + let resolved = + ResolvedTaskConfig { config_dir: RelativePathBuf::default(), config: task_config }; + + // Mock environment without any PATH variable + let mock_envs = vec![(OsString::from("OTHER_VAR"), OsString::from("value"))]; + + let base_dir = AbsolutePath::new("C:\\workspace\\packages\\app").unwrap(); + + let result = TaskEnvs::resolve(mock_envs.into_iter(), &base_dir, &resolved).unwrap(); + + let all_envs = result.all_envs; + + // Verify that "PATH" is created when missing + assert!(all_envs.contains_key("PATH")); + + // Verify the complete PATH value matches expected (only node_modules/.bin paths, no existing path) + let path_value = all_envs.get("PATH").unwrap(); + assert_eq!( + path_value.as_ref(), + OsStr::new( + "C:\\workspace\\packages\\app\\node_modules\\.bin;C:\\workspace\\packages\\app\\node_modules\\.bin" + ) + ); + } + + #[test] + #[cfg(unix)] + fn test_unix_path_case_sensitive() { + use crate::{ + collections::HashSet, + config::{ResolvedTaskConfig, TaskCommand, TaskConfig}, + }; + + let task_config = TaskConfig { + command: TaskCommand::ShellScript("echo test".into()), + cwd: RelativePathBuf::default(), + cacheable: true, + inputs: HashSet::new(), + envs: HashSet::new(), + pass_through_envs: HashSet::new(), + fingerprint_ignores: None, + }; + let resolved = + ResolvedTaskConfig { config_dir: RelativePathBuf::default(), config: task_config }; + + // Mock environment with "PATH" in uppercase (standard on Unix) + let mock_envs = vec![ + (OsString::from("PATH"), OsString::from("/existing/path")), + (OsString::from("OTHER_VAR"), OsString::from("value")), + ]; + + let base_dir = AbsolutePath::new("/workspace/packages/app").unwrap(); + + let result = TaskEnvs::resolve(mock_envs.into_iter(), &base_dir, &resolved).unwrap(); + + let all_envs = result.all_envs; + + // Verify "PATH" exists and the complete value matches expected + assert!(all_envs.contains_key("PATH")); + let path_value = all_envs.get("PATH").unwrap(); + assert_eq!( + path_value.as_ref(), + OsStr::new( + "/workspace/packages/app/node_modules/.bin:/workspace/packages/app/node_modules/.bin:/existing/path" + ) + ); + + // Verify that on Unix, the code uses exact "PATH" match (case-sensitive) + // This is a regression test to ensure Windows case-insensitive logic doesn't affect Unix + assert!(!all_envs.contains_key("Path")); + assert!(!all_envs.contains_key("path")); + } } From 942448c0a1fd392f843f82fad111335b945e06b2 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 19:52:11 +0800 Subject: [PATCH 4/6] simplify test --- crates/vite_task/src/execute.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index 5a469cb9..63ba60c6 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -985,9 +985,6 @@ mod tests { let all_envs = result.all_envs; - // Verify that "PATH" is preserved - assert!(all_envs.contains_key("PATH")); - // Verify the complete PATH value matches expected let path_value = all_envs.get("PATH").unwrap(); assert_eq!( @@ -1027,9 +1024,6 @@ mod tests { let all_envs = result.all_envs; - // Verify that "PATH" is created when missing - assert!(all_envs.contains_key("PATH")); - // Verify the complete PATH value matches expected (only node_modules/.bin paths, no existing path) let path_value = all_envs.get("PATH").unwrap(); assert_eq!( @@ -1073,7 +1067,6 @@ mod tests { let all_envs = result.all_envs; // Verify "PATH" exists and the complete value matches expected - assert!(all_envs.contains_key("PATH")); let path_value = all_envs.get("PATH").unwrap(); assert_eq!( path_value.as_ref(), From 6b59599236897f4c80686f25bc2f6c92deed82e5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 20:03:20 +0800 Subject: [PATCH 5/6] use backslash on Windows --- crates/vite_task/src/execute.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index 63ba60c6..7e4f555c 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -329,9 +329,12 @@ impl TaskEnvs { }; let paths = split_paths(env_path); + const NODE_MODULES_DOT_BIN: &str = + if cfg!(windows) { "node_modules\\.bin" } else { "node_modules/.bin" }; + let node_modules_bin_paths = [ - base_dir.join(&task.config.cwd).join("node_modules/.bin").into_path_buf(), - base_dir.join(&task.config_dir).join("node_modules/.bin").into_path_buf(), + base_dir.join(&task.config.cwd).join(NODE_MODULES_DOT_BIN).into_path_buf(), + base_dir.join(&task.config_dir).join(NODE_MODULES_DOT_BIN).into_path_buf(), ]; *env_path = join_paths(node_modules_bin_paths.into_iter().chain(paths))?.into(); From 67a579490a285b481cd5cb990de1bcac693865cf Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 12 Nov 2025 20:31:42 +0800 Subject: [PATCH 6/6] filter out empty paths --- crates/vite_task/src/execute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vite_task/src/execute.rs b/crates/vite_task/src/execute.rs index 7e4f555c..5aa4c3cd 100644 --- a/crates/vite_task/src/execute.rs +++ b/crates/vite_task/src/execute.rs @@ -327,7 +327,7 @@ impl TaskEnvs { all_envs.entry("PATH".into()).or_insert_with(|| Arc::::from(OsStr::new(""))) } }; - let paths = split_paths(env_path); + let paths = split_paths(env_path).filter(|path| !path.as_os_str().is_empty()); const NODE_MODULES_DOT_BIN: &str = if cfg!(windows) { "node_modules\\.bin" } else { "node_modules/.bin" };