Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c4a3709
feat(cache): add explicit inputs config for cache fingerprinting
branchseer Jan 13, 2026
0ec94fa
update message
branchseer Mar 5, 2026
6daa8fa
refactor(cache): rename FingerprintMismatch variants and fix lint war…
branchseer Mar 6, 2026
d5ba6e8
refactor(cache): update comments to remove pre-run/post-run key-value…
branchseer Mar 6, 2026
06775f8
refactor(cache): remove redundant includes_auto guard on post-run val…
branchseer Mar 6, 2026
2c8bd52
docs(cache): document key vs value design principle
branchseer Mar 6, 2026
8ce4762
refactor(cache): destructure CacheEntryKey in mismatch detection
branchseer Mar 6, 2026
a6369f5
refactor: remove ResolvedInputConfig::inference_disabled method
branchseer Mar 6, 2026
758c519
feat: add directory pruning for negative glob patterns in input selec…
branchseer Mar 7, 2026
a285687
docs: replace fspy references with user-friendly language in inputs c…
branchseer Mar 7, 2026
453e44e
refactor: extract AnchoredGlob into vite_glob crate
branchseer Mar 7, 2026
9263ec2
feat(vite_glob): add walk module with common-ancestor rerooting
branchseer Mar 8, 2026
3748ecf
refactor(vite_glob): move rerooting logic into AnchoredGlob
branchseer Mar 8, 2026
f65ef90
refactor(vite_glob): remove AnchoredGlob and walk module
branchseer Mar 8, 2026
9291a76
add new globbing plan
branchseer Mar 8, 2026
713bf28
refactor: resolve glob patterns to workspace-root-relative at task gr…
claude Mar 8, 2026
a738020
Clean fspy paths at strip_path_prefix site, simplify downstream code
claude Mar 8, 2026
078f722
Fold all fspy path filtering into strip_path_prefix callback
claude Mar 8, 2026
f3440e4
Keep original fspy path for fingerprinting, only clean for glob matching
claude Mar 8, 2026
1b833d5
refactor: add clean() methods to AbsolutePath/RelativePath, replace d…
branchseer Mar 8, 2026
eeb389e
refactor: use typed AbsolutePath::strip_prefix in resolve_glob_to_wor…
branchseer Mar 8, 2026
849336e
docs: note that ResolvedInputConfig globs are relative to workspace root
branchseer Mar 8, 2026
707d3cd
add test completion message
branchseer Mar 8, 2026
3a1f2ae
refactor: simplify glob_inputs to walk from workspace root and use wa…
branchseer Mar 8, 2026
7a277fd
fix: remove fstat interception that caused duplicate path tracking
branchseer Mar 8, 2026
88d719e
test: add e2e test for bare directory name as explicit input
branchseer Mar 8, 2026
5a4a707
feat: expand trailing `/` in input globs to `/**`
branchseer Mar 8, 2026
a6d4d1b
refactor: skip duplicate glob entries via entry API, error on non-UTF…
branchseer Mar 8, 2026
608e062
fix ci issues
branchseer Mar 8, 2026
4311180
fix clippy warnings in test harnesses
branchseer Mar 8, 2026
33e01f8
fix: normalize `..` in fspy paths for cross-platform consistency
branchseer Mar 8, 2026
6efbfc2
fix: move clean logic
branchseer Mar 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl SpyImpl {
)
.map_err(|err| SpawnError::Injection(err.into()))?;
command.set_exec(exec);
command.env("FSPY", "1");

let mut tokio_command = command.into_tokio_command();

Expand Down
3 changes: 2 additions & 1 deletion crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ impl SpyImpl {
}

#[expect(clippy::unused_async, reason = "async signature required by SpyImpl trait")]
pub(crate) async fn spawn(&self, command: Command) -> Result<TrackedChild, SpawnError> {
pub(crate) async fn spawn(&self, mut command: Command) -> Result<TrackedChild, SpawnError> {
let ansi_dll_path_with_nul = Arc::clone(&self.ansi_dll_path_with_nul);
command.env("FSPY", "1");
let mut command = command.into_tokio_command();

command.creation_flags(CREATE_SUSPENDED);
Expand Down
15 changes: 1 addition & 14 deletions crates/fspy_preload_unix/src/interceptions/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use fspy_shared::ipc::AccessMode;
use libc::{c_char, c_int, stat as stat_struct};

use crate::{
client::{
convert::{Fd, PathAt},
handle_open,
},
client::{convert::PathAt, handle_open},
macros::intercept,
};

Expand All @@ -30,16 +27,6 @@ unsafe extern "C" fn lstat(path: *const c_char, buf: *mut stat_struct) -> c_int
unsafe { lstat::original()(path, buf) }
}

intercept!(fstat(64): unsafe extern "C" fn(fd: c_int, buf: *mut stat_struct) -> c_int);
unsafe extern "C" fn fstat(fd: c_int, buf: *mut stat_struct) -> c_int {
// SAFETY: fd is a valid file descriptor provided by the caller of the interposed function
unsafe {
handle_open(Fd(fd), AccessMode::READ);
}
// SAFETY: calling the original libc fstat() with the same arguments forwarded from the interposed function
unsafe { fstat::original()(fd, buf) }
}

intercept!(fstatat(64): unsafe extern "C" fn(dirfd: c_int, pathname: *const c_char, buf: *mut stat_struct, flags: c_int) -> c_int);
unsafe extern "C" fn fstatat(
dirfd: c_int,
Expand Down
1 change: 1 addition & 0 deletions crates/vite_glob/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust-version.workspace = true

[dependencies]
thiserror = { workspace = true }
vite_path = { workspace = true }
wax = { workspace = true }

[dev-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions crates/vite_glob/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
pub enum Error {
#[error(transparent)]
WaxBuild(#[from] wax::BuildError),
#[error(transparent)]
Walk(#[from] wax::walk::WalkError),
#[error(transparent)]
InvalidPathData(#[from] vite_path::relative::InvalidPathDataError),
}
1 change: 1 addition & 0 deletions crates/vite_path/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust-version.workspace = true
[dependencies]
bincode = { workspace = true }
diff-struct = { workspace = true }
path-clean = { workspace = true }
ref-cast = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
thiserror = { workspace = true }
Expand Down
18 changes: 18 additions & 0 deletions crates/vite_path/src/absolute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,24 @@ impl AbsolutePath {
pub fn ends_with<P: AsRef<Path>>(&self, path: P) -> bool {
self.0.ends_with(path.as_ref())
}

/// Lexically normalizes the path by resolving `.` and `..` components
/// without accessing the filesystem.
///
/// **Symlink limitation**: Because this is purely lexical, it can produce
/// incorrect results when symlinks are involved. For example, if
/// `/a/link` is a symlink to `/x/y`, then cleaning `/a/link/../c`
/// yields `/a/c` instead of the correct `/x/c`. Use
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
#[must_use]
pub fn clean(&self) -> AbsolutePathBuf {
use path_clean::PathClean as _;

let cleaned = self.0.clean();
// SAFETY: Lexical cleaning of an absolute path preserves absoluteness —
// it only removes `.`/`..` components and redundant separators.
unsafe { AbsolutePathBuf::assume_absolute(cleaned) }
}
}

/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`
Expand Down
22 changes: 22 additions & 0 deletions crates/vite_path/src/relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ impl RelativePath {
relative_path_buf
}

/// Lexically normalizes the path by resolving `..` components without
/// accessing the filesystem. (`.` components are already stripped by
/// [`RelativePathBuf::new`].)
///
/// **Symlink limitation**: Because this is purely lexical, it can produce
/// incorrect results when symlinks are involved. For example, if
/// `a/link` is a symlink to `x/y`, then cleaning `a/link/../c`
/// yields `a/c` instead of the correct `x/c`. Use
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
///
/// # Panics
///
/// Panics if the cleaned path is no longer a valid relative path, which
/// should never happen in practice.
#[must_use]
pub fn clean(&self) -> RelativePathBuf {
use path_clean::PathClean as _;

let cleaned = self.as_path().clean();
RelativePathBuf::new(cleaned).expect("cleaning a relative path preserves relativity")
}

/// Returns a path that, when joined onto `base`, yields `self`.
///
/// If `base` is not a prefix of `self`, returns [`None`].
Expand Down
5 changes: 4 additions & 1 deletion crates/vite_task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "io-util", "macros", "sync"] }
tracing = { workspace = true }
twox-hash = { workspace = true }
vite_glob = { workspace = true }
vite_path = { workspace = true }
vite_select = { workspace = true }
vite_str = { workspace = true }
vite_task_graph = { workspace = true }
vite_task_plan = { workspace = true }
vite_workspace = { workspace = true }
wax = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
40 changes: 8 additions & 32 deletions crates/vite_task/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ pub enum SpawnFingerprintChange {
// Working directory change
/// Working directory changed
CwdChanged,

// Fingerprint ignores changes
/// Fingerprint ignore pattern added
FingerprintIgnoreAdded { pattern: Str },
/// Fingerprint ignore pattern removed
FingerprintIgnoreRemoved { pattern: Str },
}

/// Format a single spawn fingerprint change as human-readable text.
Expand All @@ -70,12 +64,6 @@ pub fn format_spawn_change(change: &SpawnFingerprintChange) -> Str {
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
SpawnFingerprintChange::CwdChanged => Str::from("working directory changed"),
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' added")
}
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' removed")
}
}
}

Expand Down Expand Up @@ -141,20 +129,6 @@ pub fn detect_spawn_fingerprint_changes(
changes.push(SpawnFingerprintChange::CwdChanged);
}

// Check fingerprint ignores changes
let old_ignores: FxHashSet<_> =
old.fingerprint_ignores().map(|v| v.iter().collect()).unwrap_or_default();
let new_ignores: FxHashSet<_> =
new.fingerprint_ignores().map(|v| v.iter().collect()).unwrap_or_default();
for pattern in old_ignores.difference(&new_ignores) {
changes
.push(SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern: (*pattern).clone() });
}
for pattern in new_ignores.difference(&old_ignores) {
changes
.push(SpawnFingerprintChange::FingerprintIgnoreAdded { pattern: (*pattern).clone() });
}

changes
}

Expand All @@ -181,7 +155,7 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
CacheStatus::Miss(CacheMiss::FingerprintMismatch(mismatch)) => {
// Show "cache miss" with reason why cache couldn't be used
let reason = match mismatch {
FingerprintMismatch::SpawnFingerprintMismatch { old, new } => {
FingerprintMismatch::SpawnFingerprint { old, new } => {
let changes = detect_spawn_fingerprint_changes(old, new);
match changes.first() {
Some(
Expand All @@ -196,14 +170,16 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
Some(SpawnFingerprintChange::ProgramChanged) => "program changed",
Some(SpawnFingerprintChange::ArgsChanged) => "args changed",
Some(SpawnFingerprintChange::CwdChanged) => "working directory changed",
Some(
SpawnFingerprintChange::FingerprintIgnoreAdded { .. }
| SpawnFingerprintChange::FingerprintIgnoreRemoved { .. },
) => "fingerprint ignores changed",
None => "configuration changed",
}
}
FingerprintMismatch::PostRunFingerprintMismatch(diff) => {
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 } => {
Expand Down
Loading