From 8552ecfa272724f99a5bc64a90aa1e8515c7598f Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 9 Mar 2026 05:12:12 +0800 Subject: [PATCH 1/3] feat: improve cache miss messages with specific input change kinds Unify GlobbedInput and PostRunFingerprint mismatch variants into a single InputChanged { kind, path } with Added/Removed/ContentModified discrimination. Messages now show '{path} modified', '{file} added in {dir}', or '{file} removed from {dir}' instead of generic 'content of input changed'. Co-Authored-By: Claude Opus 4.6 # Conflicts: # crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap --- crates/vite_task/src/session/cache/display.rs | 34 ++++---- crates/vite_task/src/session/cache/mod.rs | 77 ++++++++++++----- .../src/session/execute/fingerprint.rs | 86 ++++++++++++++----- .../vite_task/src/session/reporter/summary.rs | 25 ++---- .../snapshots/builtin different cwd.snap | 2 +- .../fixtures/cache-miss-reasons/extra.txt | 1 + .../cache-miss-reasons/snapshots.toml | 20 ++++- .../snapshots/glob input changes.snap | 22 +++++ .../snapshots/inferred input changes.snap | 22 +++++ .../snapshots/input content changed.snap | 12 --- .../cache-miss-reasons/vite-task.json | 5 ++ .../e2e-lint-cache/snapshots/direct lint.snap | 2 +- .../root glob - matches src files.snap | 2 +- ...wd - glob relative to package not cwd.snap | 2 +- ...bpackage glob - matches own src files.snap | 2 +- ...wd - glob relative to package not cwd.snap | 2 +- ...o only - miss on inferred file change.snap | 2 +- ... - miss on non-excluded inferred file.snap | 2 +- ...iss on direct and nested file changes.snap | 6 +- ...negative - miss on explicit glob file.snap | 2 +- ...auto negative - miss on inferred file.snap | 2 +- ...bs only - miss on matched file change.snap | 2 +- ...ive globs - miss on non-excluded file.snap | 2 +- .../cache hit then miss on file change.snap | 2 +- ...on non-excluded sibling inferred file.snap | 2 +- ...ve glob - miss on sibling file change.snap | 2 +- ...e - miss on non-excluded sibling file.snap | 2 +- ... - miss on non-excluded inferred file.snap | 2 +- .../snapshots/shared caching inputs.snap | 2 +- .../cache hit after file modification.snap | 2 +- packages/tools/src/print-file.ts | 8 +- 31 files changed, 244 insertions(+), 112 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/extra.txt create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/glob input changes.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/inferred input changes.snap delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/input content changed.snap diff --git a/crates/vite_task/src/session/cache/display.rs b/crates/vite_task/src/session/cache/display.rs index 9392d41f..c8b4e659 100644 --- a/crates/vite_task/src/session/cache/display.rs +++ b/crates/vite_task/src/session/cache/display.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use vite_str::Str; use vite_task_plan::cache_metadata::SpawnFingerprint; -use super::{CacheMiss, FingerprintMismatch}; +use super::{CacheMiss, FingerprintMismatch, InputChangeKind, split_path}; use crate::session::event::CacheStatus; /// Describes a single atomic change between two spawn fingerprints. @@ -174,20 +174,9 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option { } } FingerprintMismatch::InputConfig => "inputs configuration changed", - FingerprintMismatch::GlobbedInput { path } => { - return Some(vite_str::format!( - "✗ cache miss: content of input '{path}' changed, executing" - )); - } - FingerprintMismatch::PostRunFingerprint(diff) => { - use crate::session::execute::fingerprint::PostRunFingerprintMismatch; - match diff { - PostRunFingerprintMismatch::InputContentChanged { path } => { - return Some(vite_str::format!( - "✗ cache miss: content of input '{path}' changed, executing" - )); - } - } + FingerprintMismatch::InputChanged { kind, path } => { + let desc = format_input_change_str(*kind, path.as_str()); + return Some(vite_str::format!("✗ cache miss: {desc}, executing")); } }; Some(vite_str::format!("✗ cache miss: {reason}, executing")) @@ -195,3 +184,18 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option { CacheStatus::Disabled(_) => Some(Str::from("⊘ cache disabled")), } } + +/// Format an input change as a [`Str`] for inline display. +pub fn format_input_change_str(kind: InputChangeKind, path: &str) -> Str { + match kind { + InputChangeKind::ContentModified => vite_str::format!("'{path}' modified"), + InputChangeKind::Added => { + let (dir, filename) = split_path(path); + vite_str::format!("'{filename}' added in {dir}") + } + InputChangeKind::Removed => { + let (dir, filename) = split_path(path); + vite_str::format!("'{filename}' removed from {dir}") + } + } +} diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index a4b3f588..bc0b6fdc 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -7,7 +7,10 @@ use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, t use bincode::{Decode, Encode, decode_from_slice, encode_to_vec}; // Re-export display functions for convenience pub use display::format_cache_status_inline; -pub use display::{SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_spawn_change}; +pub use display::{ + SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_input_change_str, + format_spawn_change, +}; use rusqlite::{Connection, OptionalExtension as _, config::DbConfig}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; @@ -15,10 +18,7 @@ use vite_path::{AbsolutePath, RelativePathBuf}; use vite_task_graph::config::ResolvedInputConfig; use vite_task_plan::cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint}; -use super::execute::{ - fingerprint::{PostRunFingerprint, PostRunFingerprintMismatch}, - spawn::StdOutput, -}; +use super::execute::{fingerprint::PostRunFingerprint, spawn::StdOutput}; /// Cache lookup key identifying a task's execution configuration. /// @@ -83,6 +83,16 @@ pub enum CacheMiss { FingerprintMismatch(FingerprintMismatch), } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum InputChangeKind { + /// File content changed but path is the same + ContentModified, + /// New file or folder added + Added, + /// Existing file or folder removed + Removed, +} + #[derive(Debug, Serialize, Deserialize)] pub enum FingerprintMismatch { /// Found a previous cache entry key for the same task, but the spawn fingerprint differs. @@ -95,10 +105,11 @@ pub enum FingerprintMismatch { }, /// Found a previous cache entry key for the same task, but `input_config` differs. InputConfig, - /// Found the cache entry with the same spawn fingerprint, but an explicit globbed input changed - GlobbedInput { path: RelativePathBuf }, - /// Found the cache entry with the same spawn fingerprint, but the post-run fingerprint mismatches - PostRunFingerprint(PostRunFingerprintMismatch), + + InputChanged { + kind: InputChangeKind, + path: RelativePathBuf, + }, } impl Display for FingerprintMismatch { @@ -110,14 +121,22 @@ impl Display for FingerprintMismatch { Self::InputConfig => { write!(f, "inputs configuration changed") } - Self::GlobbedInput { path } => { - write!(f, "content of input '{path}' changed") + Self::InputChanged { kind, path } => { + write!(f, "{}", display::format_input_change_str(*kind, path.as_str())) } - Self::PostRunFingerprint(diff) => Display::fmt(diff, f), } } } +/// Split a relative path into `(parent_dir, filename)`. +/// Returns `("workspace root", path)` if there is no parent directory. +pub fn split_path(path: &str) -> (&str, &str) { + match path.rsplit_once('/') { + Some((parent, filename)) => (parent, filename), + None => ("workspace root", path), + } +} + impl ExecutionCache { #[tracing::instrument(level = "debug", skip_all)] pub fn load_from_path(path: &AbsolutePath) -> anyhow::Result { @@ -197,11 +216,9 @@ impl ExecutionCache { } // Validate post-run fingerprint (inferred inputs from fspy) - if let Some(post_run_fingerprint_mismatch) = - cache_value.post_run_fingerprint.validate(workspace_root)? - { + if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? { return Ok(Err(CacheMiss::FingerprintMismatch( - FingerprintMismatch::PostRunFingerprint(post_run_fingerprint_mismatch), + FingerprintMismatch::InputChanged { kind, path }, ))); } // Associate the execution key to the cache entry key if not already, @@ -264,22 +281,40 @@ fn detect_globbed_input_change( loop { match (s, c) { (None, None) => return None, - (Some((path, _)), None) | (None, Some((path, _))) => { - return Some(FingerprintMismatch::GlobbedInput { path: path.clone() }); + (Some((sp, _)), None) => { + return Some(FingerprintMismatch::InputChanged { + kind: InputChangeKind::Removed, + path: sp.clone(), + }); + } + (None, Some((cp, _))) => { + return Some(FingerprintMismatch::InputChanged { + kind: InputChangeKind::Added, + path: cp.clone(), + }); } (Some((sp, sh)), Some((cp, ch))) => match sp.cmp(cp) { std::cmp::Ordering::Equal => { if sh != ch { - return Some(FingerprintMismatch::GlobbedInput { path: sp.clone() }); + return Some(FingerprintMismatch::InputChanged { + kind: InputChangeKind::ContentModified, + path: sp.clone(), + }); } s = stored_iter.next(); c = current_iter.next(); } std::cmp::Ordering::Less => { - return Some(FingerprintMismatch::GlobbedInput { path: sp.clone() }); + return Some(FingerprintMismatch::InputChanged { + kind: InputChangeKind::Removed, + path: sp.clone(), + }); } std::cmp::Ordering::Greater => { - return Some(FingerprintMismatch::GlobbedInput { path: cp.clone() }); + return Some(FingerprintMismatch::InputChanged { + kind: InputChangeKind::Added, + path: cp.clone(), + }); } }, } diff --git a/crates/vite_task/src/session/execute/fingerprint.rs b/crates/vite_task/src/session/execute/fingerprint.rs index bbb28b9e..12b0b51a 100644 --- a/crates/vite_task/src/session/execute/fingerprint.rs +++ b/crates/vite_task/src/session/execute/fingerprint.rs @@ -17,7 +17,7 @@ use vite_path::{AbsolutePath, RelativePathBuf}; use vite_str::Str; use super::spawn::PathRead; -use crate::collections::HashMap; +use crate::{collections::HashMap, session::cache::InputChangeKind}; /// Post-run fingerprint capturing file state after execution. /// Used to validate whether cached outputs are still valid. @@ -50,22 +50,6 @@ pub enum DirEntryKind { Symlink, } -/// Describes why the post-run fingerprint validation failed -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum PostRunFingerprintMismatch { - InputContentChanged { path: RelativePathBuf }, -} - -impl std::fmt::Display for PostRunFingerprintMismatch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InputContentChanged { path } => { - write!(f, "{path} content changed") - } - } - } -} - impl PostRunFingerprint { /// Creates a new fingerprint from path accesses after task execution. /// @@ -94,12 +78,12 @@ impl PostRunFingerprint { } /// Validates the fingerprint against current filesystem state. - /// Returns `Some(mismatch)` if validation fails, `None` if valid. + /// Returns `Some((kind, path))` if an input changed, `None` if all valid. #[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")] pub fn validate( &self, base_dir: &AbsolutePath, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let input_mismatch = self.inferred_inputs.par_iter().find_map_any( |(input_relative_path, path_fingerprint)| { let input_full_path = Arc::::from(base_dir.join(input_relative_path)); @@ -113,9 +97,19 @@ impl PostRunFingerprint { if path_fingerprint == ¤t_path_fingerprint { None } else { - Some(Ok(PostRunFingerprintMismatch::InputContentChanged { - path: input_relative_path.clone(), - })) + let (kind, entry_name) = + determine_change_kind(path_fingerprint, ¤t_path_fingerprint); + let path = if let Some(name) = entry_name { + // For folder changes, build `dir/entry` path + let entry = match RelativePathBuf::new(name.as_str()) { + Ok(p) => p, + Err(e) => return Some(Err(e.into())), + }; + input_relative_path.as_relative_path().join(entry) + } else { + input_relative_path.clone() + }; + Some(Ok((kind, path))) } }, ); @@ -123,6 +117,54 @@ impl PostRunFingerprint { } } +/// Determine the kind of change between two differing path fingerprints. +/// Caller guarantees `stored != current`. +/// +/// Returns `(kind, entry_name)` where `entry_name` is `Some` for folder changes +/// when a specific added/removed entry can be identified. +fn determine_change_kind<'a>( + stored: &'a PathFingerprint, + current: &'a PathFingerprint, +) -> (InputChangeKind, Option<&'a Str>) { + match (stored, current) { + (PathFingerprint::NotFound, _) => (InputChangeKind::Added, None), + (_, PathFingerprint::NotFound) => (InputChangeKind::Removed, None), + (PathFingerprint::FileContentHash(_), PathFingerprint::FileContentHash(_)) => { + (InputChangeKind::ContentModified, None) + } + (PathFingerprint::Folder(old), PathFingerprint::Folder(new)) => { + determine_folder_change_kind(old.as_ref(), new.as_ref()) + } + // Type changed (file ↔ folder) + _ => (InputChangeKind::Added, None), + } +} + +/// Determine whether a folder change is an addition or removal by comparing entries. +/// Returns the specific entry name that was added or removed, if identifiable. +fn determine_folder_change_kind<'a>( + old: Option<&'a HashMap>, + new: Option<&'a HashMap>, +) -> (InputChangeKind, Option<&'a Str>) { + match (old, new) { + (Some(old_entries), Some(new_entries)) => { + for key in old_entries.keys() { + if !new_entries.contains_key(key) { + return (InputChangeKind::Removed, Some(key)); + } + } + for key in new_entries.keys() { + if !old_entries.contains_key(key) { + return (InputChangeKind::Added, Some(key)); + } + } + // Same keys but different entry kinds — default to Added + (InputChangeKind::Added, None) + } + _ => (InputChangeKind::Added, None), + } +} + /// Hash file content using `xxHash3_64` fn hash_content(mut stream: impl Read) -> io::Result { let mut hasher = twox_hash::XxHash3_64::default(); diff --git a/crates/vite_task/src/session/reporter/summary.rs b/crates/vite_task/src/session/reporter/summary.rs index 39f5f9e5..78499991 100644 --- a/crates/vite_task/src/session/reporter/summary.rs +++ b/crates/vite_task/src/session/reporter/summary.rs @@ -17,8 +17,8 @@ use vite_str::Str; use super::{CACHE_MISS_STYLE, COMMAND_STYLE, ColorizeExt}; use crate::session::{ cache::{ - CacheMiss, FingerprintMismatch, SpawnFingerprintChange, detect_spawn_fingerprint_changes, - format_spawn_change, + CacheMiss, FingerprintMismatch, InputChangeKind, SpawnFingerprintChange, + detect_spawn_fingerprint_changes, format_input_change_str, format_spawn_change, }, event::{CacheDisabledReason, CacheErrorKind, CacheStatus, ExecutionError}, }; @@ -114,8 +114,8 @@ pub enum SavedCacheMissReason { SpawnFingerprintChanged(Vec), /// Task configuration changed (`input_config` or `glob_base`). ConfigChanged, - /// Content of an input file changed. - InputContentChanged { path: Str }, + /// An input file or folder changed. + InputChanged { kind: InputChangeKind, path: Str }, } /// An execution error, serializable for persistence. @@ -241,16 +241,8 @@ impl SavedCacheMissReason { Self::SpawnFingerprintChanged(detect_spawn_fingerprint_changes(old, new)) } FingerprintMismatch::InputConfig => Self::ConfigChanged, - FingerprintMismatch::GlobbedInput { path } => { - Self::InputContentChanged { path: Str::from(path.as_str()) } - } - FingerprintMismatch::PostRunFingerprint(diff) => { - use crate::session::execute::fingerprint::PostRunFingerprintMismatch; - match diff { - PostRunFingerprintMismatch::InputContentChanged { path } => { - Self::InputContentChanged { path: Str::from(path.as_str()) } - } - } + FingerprintMismatch::InputChanged { kind, path } => { + Self::InputChanged { kind: *kind, path: Str::from(path.as_str()) } } }, } @@ -434,8 +426,9 @@ impl TaskResult { SavedCacheMissReason::ConfigChanged => { Str::from("→ Cache miss: inputs configuration changed") } - SavedCacheMissReason::InputContentChanged { path } => { - vite_str::format!("→ Cache miss: content of input '{path}' changed") + SavedCacheMissReason::InputChanged { kind, path } => { + let desc = format_input_change_str(*kind, path.as_str()); + vite_str::format!("→ Cache miss: {desc}") } }, }, diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap index 85835032..6ccf9e29 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap @@ -46,7 +46,7 @@ Finished in on 2 files with 90 rules using threads. > echo 'console.log(1);' > folder2/a.js # modify folder2 > cd folder1 && vp run lint # cache hit -$ vp lint ✗ cache miss: content of input 'folder2/a.js' changed, executing +$ vp lint ✗ cache miss: 'folder2/a.js' modified, executing ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. ,-[folder1/a.js:1:1] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/extra.txt b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/extra.txt new file mode 100644 index 00000000..c6ae2b1b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/extra.txt @@ -0,0 +1 @@ +extra content diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots.toml index ead59c25..0e56468e 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots.toml @@ -49,11 +49,15 @@ steps = [ ] [[e2e]] -name = "input content changed" +name = "inferred input changes" steps = [ "vp run test # cache miss", "replace-file-content test.txt initial modified # modify input", - "vp run test # cache miss: input changed", + "vp run test # cache miss: input modified", + "rm test.txt # remove tracked input", + "vp run test # cache miss: input removed", + "echo new content > test.txt # recreate previously removed file", + "vp run test # cache miss: input added", ] [[e2e]] @@ -63,3 +67,15 @@ steps = [ "json-edit vite-task.json \"_.tasks.test.inputs = ['test.txt']\" # change inputs config", "vp run test # cache miss: configuration changed", ] + +[[e2e]] +name = "glob input changes" +steps = [ + "vp run glob-test # cache miss", + "replace-file-content test.txt initial modified # modify glob input", + "vp run glob-test # cache miss: input modified", + "echo new content > new.txt # add a new .txt file", + "vp run glob-test # cache miss: input added", + "rm extra.txt # remove a .txt file", + "vp run glob-test # cache miss: input removed", +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/glob input changes.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/glob input changes.snap new file mode 100644 index 00000000..1bd64b3f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/glob input changes.snap @@ -0,0 +1,22 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +> vp run glob-test # cache miss +$ print glob-test +glob-test +> replace-file-content test.txt initial modified # modify glob input + +> vp run glob-test # cache miss: input modified +$ print glob-test ✗ cache miss: 'test.txt' modified, executing +glob-test +> echo new content > new.txt # add a new .txt file + +> vp run glob-test # cache miss: input added +$ print glob-test ✗ cache miss: 'new.txt' added in workspace root, executing +glob-test +> rm extra.txt # remove a .txt file + +> vp run glob-test # cache miss: input removed +$ print glob-test ✗ cache miss: 'extra.txt' removed from workspace root, executing +glob-test diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/inferred input changes.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/inferred input changes.snap new file mode 100644 index 00000000..0278d04d --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/inferred input changes.snap @@ -0,0 +1,22 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +> vp run test # cache miss +$ print-file test.txt +initial content +> replace-file-content test.txt initial modified # modify input + +> vp run test # cache miss: input modified +$ print-file test.txt ✗ cache miss: 'test.txt' modified, executing +modified content +> rm test.txt # remove tracked input + +> vp run test # cache miss: input removed +$ print-file test.txt ✗ cache miss: 'test.txt' removed from workspace root, executing +test.txt: not found +> echo new content > test.txt # recreate previously removed file + +> vp run test # cache miss: input added +$ print-file test.txt ✗ cache miss: 'test.txt' added in workspace root, executing +new content diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/input content changed.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/input content changed.snap deleted file mode 100644 index b1fd05df..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/snapshots/input content changed.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -expression: e2e_outputs ---- -> vp run test # cache miss -$ print-file test.txt -initial content -> replace-file-content test.txt initial modified # modify input - -> vp run test # cache miss: input changed -$ print-file test.txt ✗ cache miss: content of input 'test.txt' changed, executing -modified content diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/vite-task.json index a364da13..e9cb1a3a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/vite-task.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-reasons/vite-task.json @@ -5,6 +5,11 @@ "command": "print-file test.txt", "envs": ["MY_ENV"], "cache": true + }, + "glob-test": { + "command": "print glob-test", + "inputs": ["*.txt"], + "cache": true } } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap index d1662e35..aab38150 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap @@ -9,7 +9,7 @@ Finished in on 0 files with 90 rules using threads. > echo debugger > main.js # add lint error > vp run lint # cache miss, lint fails -$ vp lint ✗ cache miss: content of input '' changed, executing +$ vp lint ✗ cache miss: 'main.js' added in workspace root, executing ! eslint(no-debugger): `debugger` statement is not allowed ,-[main.js:1:1] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob - matches src files.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob - matches src files.snap index e33a40c0..71a720b6 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob - matches src files.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob - matches src files.snap @@ -8,5 +8,5 @@ export const root = 'initial'; > replace-file-content src/root.ts initial modified > vp run root-glob-test -$ print-file src/root.ts ✗ cache miss: content of input 'src/root.ts' changed, executing +$ print-file src/root.ts ✗ cache miss: 'src/root.ts' modified, executing export const root = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob with cwd - glob relative to package not cwd.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob with cwd - glob relative to package not cwd.snap index 818540ad..c9784664 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob with cwd - glob relative to package not cwd.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/root glob with cwd - glob relative to package not cwd.snap @@ -8,5 +8,5 @@ export const root = 'initial'; > replace-file-content src/root.ts initial modified > vp run root-glob-with-cwd -~/src$ print-file root.ts ✗ cache miss: content of input 'src/root.ts' changed, executing +~/src$ print-file root.ts ✗ cache miss: 'src/root.ts' modified, executing export const root = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob - matches own src files.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob - matches own src files.snap index ac5a5a62..5d37b2bf 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob - matches own src files.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob - matches own src files.snap @@ -8,5 +8,5 @@ export const sub = 'initial'; > replace-file-content packages/sub-pkg/src/sub.ts initial modified > vp run sub-pkg#sub-glob-test -~/packages/sub-pkg$ print-file src/sub.ts ✗ cache miss: content of input 'packages/sub-pkg/src/sub.ts' changed, executing +~/packages/sub-pkg$ print-file src/sub.ts ✗ cache miss: 'packages/sub-pkg/src/sub.ts' modified, executing export const sub = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob with cwd - glob relative to package not cwd.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob with cwd - glob relative to package not cwd.snap index 30a09767..b4fa2f66 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob with cwd - glob relative to package not cwd.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/glob-base-test/snapshots/subpackage glob with cwd - glob relative to package not cwd.snap @@ -8,5 +8,5 @@ export const sub = 'initial'; > replace-file-content packages/sub-pkg/src/sub.ts initial modified > vp run sub-pkg#sub-glob-with-cwd -~/packages/sub-pkg/src$ print-file sub.ts ✗ cache miss: content of input 'packages/sub-pkg/src/sub.ts' changed, executing +~/packages/sub-pkg/src$ print-file sub.ts ✗ cache miss: 'packages/sub-pkg/src/sub.ts' modified, executing export const sub = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto only - miss on inferred file change.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto only - miss on inferred file change.snap index 6f17df22..5ecaa950 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto only - miss on inferred file change.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto only - miss on inferred file change.snap @@ -8,5 +8,5 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run auto-only -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto with negative - miss on non-excluded inferred file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto with negative - miss on non-excluded inferred file.snap index 9a98e9e1..e72332ad 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto with negative - miss on non-excluded inferred file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/auto with negative - miss on non-excluded inferred file.snap @@ -9,6 +9,6 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run auto-with-negative -$ print-file src/main.ts dist/output.js ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts dist/output.js ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; // initial output diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/folder slash input - miss on direct and nested file changes.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/folder slash input - miss on direct and nested file changes.snap index 82f9346b..174f96e6 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/folder slash input - miss on direct and nested file changes.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/folder slash input - miss on direct and nested file changes.snap @@ -8,15 +8,15 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run folder-slash-input -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; > replace-file-content src/main.ts modified initial > vp run folder-slash-input -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'initial'; > replace-file-content src/sub/nested.ts initial modified > vp run folder-slash-input -$ print-file src/main.ts ✗ cache miss: content of input 'src/sub/nested.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/sub/nested.ts' modified, executing export const main = 'initial'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on explicit glob file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on explicit glob file.snap index 3dcdee47..e8ea5940 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on explicit glob file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on explicit glob file.snap @@ -8,5 +8,5 @@ export const main = 'initial'; > replace-file-content package.json inputs-cache-test modified-pkg > vp run positive-auto-negative -$ print-file src/main.ts ✗ cache miss: content of input 'package.json' changed, executing +$ print-file src/main.ts ✗ cache miss: 'package.json' modified, executing export const main = 'initial'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on inferred file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on inferred file.snap index 156d17f1..1253d141 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on inferred file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive auto negative - miss on inferred file.snap @@ -8,5 +8,5 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run positive-auto-negative -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive globs only - miss on matched file change.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive globs only - miss on matched file change.snap index d2ccf7bf..03d5e6c9 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive globs only - miss on matched file change.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive globs only - miss on matched file change.snap @@ -8,5 +8,5 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run positive-globs-only -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive negative globs - miss on non-excluded file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive negative globs - miss on non-excluded file.snap index b67d6810..fd3dbf3b 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive negative globs - miss on non-excluded file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-cache-test/snapshots/positive negative globs - miss on non-excluded file.snap @@ -8,5 +8,5 @@ export const main = 'initial'; > replace-file-content src/main.ts initial modified > vp run positive-negative-globs -$ print-file src/main.ts ✗ cache miss: content of input 'src/main.ts' changed, executing +$ print-file src/main.ts ✗ cache miss: 'src/main.ts' modified, executing export const main = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-glob-meta-in-path/snapshots/cache hit then miss on file change.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-glob-meta-in-path/snapshots/cache hit then miss on file change.snap index 207f484e..6c31422e 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-glob-meta-in-path/snapshots/cache hit then miss on file change.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-glob-meta-in-path/snapshots/cache hit then miss on file change.snap @@ -14,5 +14,5 @@ export const lib = 'initial'; > replace-file-content packages/[lib]/src/main.ts initial modified > vp run [lib]#build -~/packages/[lib]$ print-file src/main.ts ✗ cache miss: content of input 'packages/[lib]/src/main.ts' changed, executing +~/packages/[lib]$ print-file src/main.ts ✗ cache miss: 'packages/[lib]/src/main.ts' modified, executing export const lib = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap index 3a6b1ca1..83dcfc68 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap @@ -9,6 +9,6 @@ export const shared = 'initial'; > replace-file-content packages/shared/src/utils.ts initial modified > vp run sub-pkg#dotdot-auto-negative -~/packages/sub-pkg$ print-file ../shared/src/utils.ts ../shared/dist/output.js ✗ cache miss: content of input 'packages/shared/src/utils.ts' changed, executing +~/packages/sub-pkg$ print-file ../shared/src/utils.ts ../shared/dist/output.js ✗ cache miss: 'packages/shared/src/utils.ts' modified, executing export const shared = 'modified'; // initial output diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive glob - miss on sibling file change.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive glob - miss on sibling file change.snap index 385f2fce..f10b8be5 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive glob - miss on sibling file change.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive glob - miss on sibling file change.snap @@ -8,5 +8,5 @@ export const shared = 'initial'; > replace-file-content packages/shared/src/utils.ts initial modified > vp run sub-pkg#dotdot-positive -~/packages/sub-pkg$ print-file ../shared/src/utils.ts ✗ cache miss: content of input 'packages/shared/src/utils.ts' changed, executing +~/packages/sub-pkg$ print-file ../shared/src/utils.ts ✗ cache miss: 'packages/shared/src/utils.ts' modified, executing export const shared = 'modified'; diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive negative - miss on non-excluded sibling file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive negative - miss on non-excluded sibling file.snap index 735a8bdb..2838b48a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive negative - miss on non-excluded sibling file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot positive negative - miss on non-excluded sibling file.snap @@ -9,6 +9,6 @@ export const shared = 'initial'; > replace-file-content packages/shared/src/utils.ts initial modified > vp run sub-pkg#dotdot-positive-negative -~/packages/sub-pkg$ print-file ../shared/src/utils.ts ../shared/dist/output.js ✗ cache miss: content of input 'packages/shared/src/utils.ts' changed, executing +~/packages/sub-pkg$ print-file ../shared/src/utils.ts ../shared/dist/output.js ✗ cache miss: 'packages/shared/src/utils.ts' modified, executing export const shared = 'modified'; // initial output diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/subpackage auto with negative - miss on non-excluded inferred file.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/subpackage auto with negative - miss on non-excluded inferred file.snap index 8d615517..341b6a61 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/subpackage auto with negative - miss on non-excluded inferred file.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/subpackage auto with negative - miss on non-excluded inferred file.snap @@ -9,6 +9,6 @@ export const main = 'initial'; > replace-file-content packages/sub-pkg/src/main.ts initial modified > vp run sub-pkg#auto-with-negative -~/packages/sub-pkg$ print-file src/main.ts dist/output.js ✗ cache miss: content of input 'packages/sub-pkg/src/main.ts' changed, executing +~/packages/sub-pkg$ print-file src/main.ts dist/output.js ✗ cache miss: 'packages/sub-pkg/src/main.ts' modified, executing export const main = 'modified'; // initial output diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/shared-caching-inputs/snapshots/shared caching inputs.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/shared-caching-inputs/snapshots/shared caching inputs.snap index bc83c8bb..9f3be6de 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/shared-caching-inputs/snapshots/shared caching inputs.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/shared-caching-inputs/snapshots/shared caching inputs.snap @@ -14,7 +14,7 @@ initial content > replace-file-content foo.txt initial modified # modify shared input > vp run script2 # cache miss, input changed -$ print-file foo.txt ✗ cache miss: content of input 'foo.txt' changed, executing +$ print-file foo.txt ✗ cache miss: 'foo.txt' modified, executing modified content > vp run script1 # cache hit, script2 already warmed cache $ print-file foo.txt ✓ cache hit, replaying diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite-task-smoke/snapshots/cache hit after file modification.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite-task-smoke/snapshots/cache hit after file modification.snap index bcb1e6a3..063e173d 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite-task-smoke/snapshots/cache hit after file modification.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite-task-smoke/snapshots/cache hit after file modification.snap @@ -17,7 +17,7 @@ console.log('foo'); $ echo hello ⊘ cache disabled hello -$ print-file main.js ✗ cache miss: content of input 'main.js' changed, executing +$ print-file main.js ✗ cache miss: 'main.js' modified, executing console.log('bar'); --- diff --git a/packages/tools/src/print-file.ts b/packages/tools/src/print-file.ts index 792f2d67..28551306 100755 --- a/packages/tools/src/print-file.ts +++ b/packages/tools/src/print-file.ts @@ -3,6 +3,10 @@ import { readFileSync } from 'node:fs'; for (const file of process.argv.slice(2)) { - const content = readFileSync(file); - process.stdout.write(content); + try { + const content = readFileSync(file); + process.stdout.write(content); + } catch { + console.error(`${file}: not found`); + } } From eccc2849c59c7d38c8156b2d4550ab4afc1f6e17 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 9 Mar 2026 05:19:35 +0800 Subject: [PATCH 2/3] refactor: use BTreeMap for folder fingerprint entries Enables sorted lockstep diffing consistent with detect_globbed_input_change. Co-Authored-By: Claude Opus 4.6 --- .../src/session/execute/fingerprint.rs | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/vite_task/src/session/execute/fingerprint.rs b/crates/vite_task/src/session/execute/fingerprint.rs index 12b0b51a..672cba59 100644 --- a/crates/vite_task/src/session/execute/fingerprint.rs +++ b/crates/vite_task/src/session/execute/fingerprint.rs @@ -4,6 +4,7 @@ //! fingerprints of file system state after task execution. use std::{ + collections::BTreeMap, fs::File, hash::Hasher as _, io::{self, BufRead, Read}, @@ -38,8 +39,8 @@ pub enum PathFingerprint { /// Directory with optional entry listing. /// `Folder(None)` means the directory was opened but entries were not read /// (e.g., for `openat` calls). - /// `Folder(Some(_))` contains the directory entries. - Folder(Option>), + /// `Folder(Some(_))` contains the directory entries sorted by name. + Folder(Option>), } /// Kind of directory entry @@ -141,27 +142,38 @@ fn determine_change_kind<'a>( } /// Determine whether a folder change is an addition or removal by comparing entries. +/// Both maps are `BTreeMap` so we iterate them in sorted lockstep. /// Returns the specific entry name that was added or removed, if identifiable. fn determine_folder_change_kind<'a>( - old: Option<&'a HashMap>, - new: Option<&'a HashMap>, + old: Option<&'a BTreeMap>, + new: Option<&'a BTreeMap>, ) -> (InputChangeKind, Option<&'a Str>) { - match (old, new) { - (Some(old_entries), Some(new_entries)) => { - for key in old_entries.keys() { - if !new_entries.contains_key(key) { - return (InputChangeKind::Removed, Some(key)); - } - } - for key in new_entries.keys() { - if !old_entries.contains_key(key) { - return (InputChangeKind::Added, Some(key)); + let (Some(old_entries), Some(new_entries)) = (old, new) else { + return (InputChangeKind::Added, None); + }; + + let mut old_iter = old_entries.iter(); + let mut new_iter = new_entries.iter(); + let mut o = old_iter.next(); + let mut n = new_iter.next(); + + loop { + match (o, n) { + (None, None) => return (InputChangeKind::Added, None), + (Some((name, _)), None) => return (InputChangeKind::Removed, Some(name)), + (None, Some((name, _))) => return (InputChangeKind::Added, Some(name)), + (Some((ok, ov)), Some((nk, nv))) => match ok.cmp(nk) { + std::cmp::Ordering::Equal => { + if ov != nv { + return (InputChangeKind::Added, Some(ok)); + } + o = old_iter.next(); + n = new_iter.next(); } - } - // Same keys but different entry kinds — default to Added - (InputChangeKind::Added, None) + std::cmp::Ordering::Less => return (InputChangeKind::Removed, Some(ok)), + std::cmp::Ordering::Greater => return (InputChangeKind::Added, Some(nk)), + }, } - _ => (InputChangeKind::Added, None), } } @@ -248,7 +260,7 @@ fn process_directory( return Ok(PathFingerprint::Folder(None)); } - let mut entries = HashMap::new(); + let mut entries = BTreeMap::new(); for entry in std::fs::read_dir(path)? { let entry = entry?; let name = entry.file_name(); @@ -286,7 +298,7 @@ fn process_directory_unix(file: &File, path_read: PathRead) -> anyhow::Result Date: Mon, 9 Mar 2026 05:53:03 +0800 Subject: [PATCH 3/3] style: quote folder paths in cache miss messages 'file' added in 'src' instead of 'file' added in src. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/cache/display.rs | 10 ++++++++-- crates/vite_task/src/session/cache/mod.rs | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/vite_task/src/session/cache/display.rs b/crates/vite_task/src/session/cache/display.rs index c8b4e659..07ba184b 100644 --- a/crates/vite_task/src/session/cache/display.rs +++ b/crates/vite_task/src/session/cache/display.rs @@ -191,11 +191,17 @@ pub fn format_input_change_str(kind: InputChangeKind, path: &str) -> Str { InputChangeKind::ContentModified => vite_str::format!("'{path}' modified"), InputChangeKind::Added => { let (dir, filename) = split_path(path); - vite_str::format!("'{filename}' added in {dir}") + dir.map_or_else( + || vite_str::format!("'{filename}' added in workspace root"), + |dir| vite_str::format!("'{filename}' added in '{dir}'"), + ) } InputChangeKind::Removed => { let (dir, filename) = split_path(path); - vite_str::format!("'{filename}' removed from {dir}") + dir.map_or_else( + || vite_str::format!("'{filename}' removed from workspace root"), + |dir| vite_str::format!("'{filename}' removed from '{dir}'"), + ) } } } diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index bc0b6fdc..1bb5f802 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -129,11 +129,11 @@ impl Display for FingerprintMismatch { } /// Split a relative path into `(parent_dir, filename)`. -/// Returns `("workspace root", path)` if there is no parent directory. -pub fn split_path(path: &str) -> (&str, &str) { +/// Returns `None` for the parent if the path has no `/` separator. +pub fn split_path(path: &str) -> (Option<&str>, &str) { match path.rsplit_once('/') { - Some((parent, filename)) => (parent, filename), - None => ("workspace root", path), + Some((parent, filename)) => (Some(parent), filename), + None => (None, path), } }