Skip to content

Commit 09f1343

Browse files
authored
feat(cache): add explicit inputs config for cache fingerprinting (#104)
## Summary - Add `inputs` field to task configuration for explicit cache fingerprinting - Support glob patterns, auto-inference from fspy, negative patterns, and mixed mode - Expand trailing `/` in globs to `/**` (e.g., `"src/"` → `"src/**"`) - Bare directory names (e.g., `"src"` without `/`) match nothing — only files are fingerprinted - Remove `fstat` interception in fspy (fd was already tracked via `open`) - Skip duplicate files across overlapping glob patterns via entry API ### Input modes - Explicit globs: `inputs: ["src/**/*.ts"]` - Auto-inference: `inputs: [{ "auto": true }]` - Negative patterns: `inputs: ["src/**", "!**/*.test.ts"]` - Directory shorthand: `inputs: ["src/"]` (expands to `"src/**"`) - Mixed: `inputs: ["package.json", { "auto": true }, "!dist/**"]` - Empty (no file tracking): `inputs: []` ## Test plan - [x] Plan snapshot: `inputs-trailing-slash` verifies `src/` → `src/**` and `!dist/` → `dist/**` - [x] E2E: all input combinations (positive, negative, auto, mixed, empty) - [x] E2E: `folder-slash-input` — cache miss on direct and nested file changes, hit on outside - [x] E2E: `folder-input` — bare directory name fingerprints nothing - [x] E2E: glob meta chars in package paths (`packages/[lib]`) - [x] E2E: cross-package `..` globs in subpackages - [x] Unit tests: overlapping globs deduplicate, negative exclusions, sibling packages 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 389ca7a commit 09f1343

171 files changed

Lines changed: 3686 additions & 320 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy/src/unix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ impl SpyImpl {
100100
)
101101
.map_err(|err| SpawnError::Injection(err.into()))?;
102102
command.set_exec(exec);
103+
command.env("FSPY", "1");
103104

104105
let mut tokio_command = command.into_tokio_command();
105106

crates/fspy/src/windows/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ impl SpyImpl {
7373
}
7474

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

8081
command.creation_flags(CREATE_SUSPENDED);

crates/fspy_preload_unix/src/interceptions/stat.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ use fspy_shared::ipc::AccessMode;
22
use libc::{c_char, c_int, stat as stat_struct};
33

44
use crate::{
5-
client::{
6-
convert::{Fd, PathAt},
7-
handle_open,
8-
},
5+
client::{convert::PathAt, handle_open},
96
macros::intercept,
107
};
118

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

33-
intercept!(fstat(64): unsafe extern "C" fn(fd: c_int, buf: *mut stat_struct) -> c_int);
34-
unsafe extern "C" fn fstat(fd: c_int, buf: *mut stat_struct) -> c_int {
35-
// SAFETY: fd is a valid file descriptor provided by the caller of the interposed function
36-
unsafe {
37-
handle_open(Fd(fd), AccessMode::READ);
38-
}
39-
// SAFETY: calling the original libc fstat() with the same arguments forwarded from the interposed function
40-
unsafe { fstat::original()(fd, buf) }
41-
}
42-
4330
intercept!(fstatat(64): unsafe extern "C" fn(dirfd: c_int, pathname: *const c_char, buf: *mut stat_struct, flags: c_int) -> c_int);
4431
unsafe extern "C" fn fstatat(
4532
dirfd: c_int,

crates/vite_glob/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rust-version.workspace = true
99

1010
[dependencies]
1111
thiserror = { workspace = true }
12+
vite_path = { workspace = true }
1213
wax = { workspace = true }
1314

1415
[dev-dependencies]

crates/vite_glob/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
pub enum Error {
33
#[error(transparent)]
44
WaxBuild(#[from] wax::BuildError),
5+
#[error(transparent)]
6+
Walk(#[from] wax::walk::WalkError),
7+
#[error(transparent)]
8+
InvalidPathData(#[from] vite_path::relative::InvalidPathDataError),
59
}

crates/vite_path/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rust-version.workspace = true
99
[dependencies]
1010
bincode = { workspace = true }
1111
diff-struct = { workspace = true }
12+
path-clean = { workspace = true }
1213
ref-cast = { workspace = true }
1314
serde = { workspace = true, features = ["derive", "rc"] }
1415
thiserror = { workspace = true }

crates/vite_path/src/absolute/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,24 @@ impl AbsolutePath {
200200
pub fn ends_with<P: AsRef<Path>>(&self, path: P) -> bool {
201201
self.0.ends_with(path.as_ref())
202202
}
203+
204+
/// Lexically normalizes the path by resolving `.` and `..` components
205+
/// without accessing the filesystem.
206+
///
207+
/// **Symlink limitation**: Because this is purely lexical, it can produce
208+
/// incorrect results when symlinks are involved. For example, if
209+
/// `/a/link` is a symlink to `/x/y`, then cleaning `/a/link/../c`
210+
/// yields `/a/c` instead of the correct `/x/c`. Use
211+
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
212+
#[must_use]
213+
pub fn clean(&self) -> AbsolutePathBuf {
214+
use path_clean::PathClean as _;
215+
216+
let cleaned = self.0.clean();
217+
// SAFETY: Lexical cleaning of an absolute path preserves absoluteness —
218+
// it only removes `.`/`..` components and redundant separators.
219+
unsafe { AbsolutePathBuf::assume_absolute(cleaned) }
220+
}
203221
}
204222

205223
/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`

crates/vite_path/src/relative.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ impl RelativePath {
6262
relative_path_buf
6363
}
6464

65+
/// Lexically normalizes the path by resolving `..` components without
66+
/// accessing the filesystem. (`.` components are already stripped by
67+
/// [`RelativePathBuf::new`].)
68+
///
69+
/// **Symlink limitation**: Because this is purely lexical, it can produce
70+
/// incorrect results when symlinks are involved. For example, if
71+
/// `a/link` is a symlink to `x/y`, then cleaning `a/link/../c`
72+
/// yields `a/c` instead of the correct `x/c`. Use
73+
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
74+
///
75+
/// # Panics
76+
///
77+
/// Panics if the cleaned path is no longer a valid relative path, which
78+
/// should never happen in practice.
79+
#[must_use]
80+
pub fn clean(&self) -> RelativePathBuf {
81+
use path_clean::PathClean as _;
82+
83+
let cleaned = self.as_path().clean();
84+
RelativePathBuf::new(cleaned).expect("cleaning a relative path preserves relativity")
85+
}
86+
6587
/// Returns a path that, when joined onto `base`, yields `self`.
6688
///
6789
/// If `base` is not a prefix of `self`, returns [`None`].

crates/vite_task/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ thiserror = { workspace = true }
3333
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "io-util", "macros", "sync"] }
3434
tracing = { workspace = true }
3535
twox-hash = { workspace = true }
36-
vite_glob = { workspace = true }
3736
vite_path = { workspace = true }
3837
vite_select = { workspace = true }
3938
vite_str = { workspace = true }
4039
vite_task_graph = { workspace = true }
4140
vite_task_plan = { workspace = true }
4241
vite_workspace = { workspace = true }
42+
wax = { workspace = true }
43+
44+
[dev-dependencies]
45+
tempfile = { workspace = true }
4346

4447
[target.'cfg(unix)'.dependencies]
4548
nix = { workspace = true }

0 commit comments

Comments
 (0)