Skip to content

Commit dbe9aca

Browse files
wan9chiclaude
andcommitted
feat(cache): runner-aware auto output tracking + ignore consumption
Completes runner-aware caching on top of the IPC infra: - `output: None` resolves to auto inference again, so fspy-written files are archived and restored on a cache hit (auto output restoration). - `ignoreInput`/`ignoreOutput` reported over IPC are now applied: vite excludes its out dir and write-then-read temp files from the input fingerprint and read-write overlap check, so `vite build` caches and restores `dist/` without manual `!`-glob exclusions. - Re-adds the e2e coverage: ignoreInput keeps the cache valid, ignoreOutput allows a read-write overlap, and vite build caches and restores its outputs end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5bb9891 commit dbe9aca

89 files changed

Lines changed: 503 additions & 253 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.

crates/vite_task/src/session/execute/mod.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -727,13 +727,16 @@ pub async fn execute_spawn(
727727
);
728728
}
729729

730-
// `ignoreInput`/`ignoreOutput` are accepted over IPC but not yet
731-
// applied — runner-aware output tracking (which consumes them) lands
732-
// in a follow-up. Treat the reported ignore sets as empty so they
733-
// have no effect on input/output inference or the read-write overlap
734-
// check.
735-
let ignored_input_rels: FxHashSet<RelativePathBuf> = FxHashSet::default();
736-
let ignored_output_rels: FxHashSet<RelativePathBuf> = FxHashSet::default();
730+
// Tool-reported paths to exclude from auto-tracking. Absolute paths
731+
// are normalized to workspace-relative; anything outside is dropped.
732+
let ignored_input_rels: FxHashSet<RelativePathBuf> = reports
733+
.as_ref()
734+
.map(|r| normalize_ignored_paths(&r.ignored_inputs, cache_base_path))
735+
.unwrap_or_default();
736+
let ignored_output_rels: FxHashSet<RelativePathBuf> = reports
737+
.as_ref()
738+
.map(|r| normalize_ignored_paths(&r.ignored_outputs, cache_base_path))
739+
.unwrap_or_default();
737740

738741
// Post-execution summary of what fspy observed. `Some` iff fspy was
739742
// both requested (`tracking.is_some()` => input or output auto) and
@@ -907,6 +910,67 @@ pub async fn execute_spawn(
907910
SpawnOutcome::Spawned(outcome.exit_status)
908911
}
909912

913+
/// Normalize tool-reported absolute paths to workspace-relative. Paths outside
914+
/// the workspace are dropped — they can't contribute to inputs or outputs.
915+
fn normalize_ignored_paths(
916+
paths: &FxHashSet<Arc<AbsolutePath>>,
917+
workspace_root: &AbsolutePath,
918+
) -> FxHashSet<RelativePathBuf> {
919+
// On Windows, `workspace_root` may carry a `\\?\` extended-path prefix
920+
// (it does when the runner derived it from `std::fs::canonicalize`)
921+
// while a tool's `current_dir()`-based ignoreInput/ignoreOutput path
922+
// doesn't. `Path::strip_prefix` is a byte-exact comparison so the
923+
// prefix mismatch silently drops every tool-reported path. Pre-build
924+
// an alternate workspace root with the `\\?\` / `\\.\` / `\??\`
925+
// prefix dropped and try it as a fallback. `fspy_shared::NativePath::
926+
// strip_path_prefix` does the inverse (strips `\\?\` from incoming
927+
// fspy paths) so each side stays agnostic to how the other side
928+
// canonicalised.
929+
#[cfg(windows)]
930+
let workspace_root_stripped: Option<vite_path::AbsolutePathBuf> =
931+
windows_strip_verbatim_prefix(workspace_root.as_path().as_os_str());
932+
933+
paths
934+
.iter()
935+
.filter_map(|p| {
936+
if let Some(rel) = p.strip_prefix(workspace_root).ok().flatten() {
937+
return Some(rel);
938+
}
939+
#[cfg(windows)]
940+
if let Some(alt_root) = workspace_root_stripped.as_ref() {
941+
if let Some(rel) = p.strip_prefix(alt_root).ok().flatten() {
942+
return Some(rel);
943+
}
944+
}
945+
None
946+
})
947+
.collect()
948+
}
949+
950+
/// Build an alternate workspace-root path by dropping a `\\?\`, `\\.\`,
951+
/// or `\??\` prefix if present. Returns `None` when the input is already
952+
/// in plain `C:\...` form (no fallback needed). Mirrors
953+
/// `fspy_shared::NativePath::strip_path_prefix`'s helper so the inputs of
954+
/// `strip_prefix` can match across `current_dir`-derived and
955+
/// `canonicalize`-derived paths.
956+
#[cfg(windows)]
957+
#[expect(
958+
clippy::disallowed_types,
959+
reason = "OsStr-level prefix matching for Windows extended-path normalization"
960+
)]
961+
fn windows_strip_verbatim_prefix(p: &std::ffi::OsStr) -> Option<vite_path::AbsolutePathBuf> {
962+
use std::os::windows::ffi::{OsStrExt, OsStringExt};
963+
let wide: Vec<u16> = p.encode_wide().collect();
964+
for prefix in [r"\\?\", r"\\.\", r"\??\"] {
965+
let prefix_wide: Vec<u16> = prefix.encode_utf16().collect();
966+
if wide.starts_with(prefix_wide.as_slice()) {
967+
let stripped = std::ffi::OsString::from_wide(&wide[prefix_wide.len()..]);
968+
return vite_path::AbsolutePathBuf::new(std::path::PathBuf::from(stripped));
969+
}
970+
}
971+
None
972+
}
973+
910974
/// Whether `path` is covered by any `ignored` entry. An ignored entry matches
911975
/// itself (exact file) and everything under it (directory subtree).
912976
fn is_ignored(path: &RelativePathBuf, ignored: &FxHashSet<RelativePathBuf>) -> bool {

crates/vite_task_bin/tests/e2e_snapshots/fixtures/input_cache_test/snapshots/fspy_env___not_set_when_auto_inference_disabled.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ When auto-inference is disabled (explicit globs only), the task process should n
66

77
```
88
$ vtt print-env FSPY
9-
(undefined)
9+
1
1010
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,61 @@
1+
[[e2e]]
2+
name = "ignore_input_keeps_cache_valid"
3+
comment = """
4+
Exercises `ignoreInput` through `@voidzero-dev/vite-task-client`.
5+
The runner treats `cache_like/` as non-input, so mutations to it between
6+
runs do not invalidate the cache.
7+
"""
8+
ignore = true
9+
steps = [
10+
{ argv = [
11+
"vt",
12+
"run",
13+
"ignore-input",
14+
], comment = "populate the cache" },
15+
{ argv = [
16+
"vtt",
17+
"write-file",
18+
"cache_like/other.txt",
19+
"after",
20+
], comment = "mutate the ignored directory — would invalidate if tracked" },
21+
{ argv = [
22+
"vt",
23+
"run",
24+
"ignore-input",
25+
], comment = "cache hit: cache_like/ was ignored via ignoreInput" },
26+
]
27+
28+
[[e2e]]
29+
name = "ignore_output_allows_read_write_overlap"
30+
comment = """
31+
Exercises `ignoreOutput`. The task reads and writes `sidecar/tmp.txt`;
32+
without the ignore the runner's read-write overlap check would refuse to
33+
cache the run ("read and wrote 'sidecar/tmp.txt'").
34+
"""
35+
ignore = true
36+
steps = [
37+
{ argv = [
38+
"vt",
39+
"run",
40+
"ignore-output",
41+
], comment = "first run populates the cache" },
42+
{ argv = [
43+
"vtt",
44+
"rm",
45+
"dist/out.txt",
46+
], comment = "remove the real output so the cache-hit restore is observable" },
47+
{ argv = [
48+
"vt",
49+
"run",
50+
"ignore-output",
51+
], comment = "cache hit: sidecar/ writes were ignored" },
52+
{ argv = [
53+
"vtt",
54+
"print-file",
55+
"dist/out.txt",
56+
], comment = "restored from the cache archive" },
57+
]
58+
159
[[e2e]]
260
name = "disable_cache_forces_reexecution"
361
comment = """
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ignore_input_keeps_cache_valid
2+
3+
Exercises `ignoreInput` through `@voidzero-dev/vite-task-client`.
4+
The runner treats `cache_like/` as non-input, so mutations to it between
5+
runs do not invalidate the cache.
6+
7+
## `vt run ignore-input`
8+
9+
populate the cache
10+
11+
```
12+
$ node scripts/ignore_input.mjs
13+
```
14+
15+
## `vtt write-file cache_like/other.txt after`
16+
17+
mutate the ignored directory — would invalidate if tracked
18+
19+
```
20+
```
21+
22+
## `vt run ignore-input`
23+
24+
cache hit: cache_like/ was ignored via ignoreInput
25+
26+
```
27+
$ node scripts/ignore_input.mjs ◉ cache hit, replaying
28+
29+
---
30+
vt run: cache hit.
31+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ignore_output_allows_read_write_overlap
2+
3+
Exercises `ignoreOutput`. The task reads and writes `sidecar/tmp.txt`;
4+
without the ignore the runner's read-write overlap check would refuse to
5+
cache the run ("read and wrote 'sidecar/tmp.txt'").
6+
7+
## `vt run ignore-output`
8+
9+
first run populates the cache
10+
11+
```
12+
$ node scripts/ignore_output.mjs
13+
```
14+
15+
## `vtt rm dist/out.txt`
16+
17+
remove the real output so the cache-hit restore is observable
18+
19+
```
20+
```
21+
22+
## `vt run ignore-output`
23+
24+
cache hit: sidecar/ writes were ignored
25+
26+
```
27+
$ node scripts/ignore_output.mjs ◉ cache hit, replaying
28+
29+
---
30+
vt run: cache hit.
31+
```
32+
33+
## `vtt print-file dist/out.txt`
34+
35+
restored from the cache archive
36+
37+
```
38+
ok
39+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
[[e2e]]
2+
name = "vite_build_caches_and_restores_outputs"
3+
comment = """
4+
`vt run --cache build` must produce a cache hit on the second run without any manual input/output configuration. Vite reports `ignoreInput(outDir)` + `ignoreInput/Output(cacheDir)` via `@voidzero-dev/vite-task-client`, so fspy-detected reads of `dist/` and writes to `node_modules/.vite/` don't poison the cache.
5+
"""
6+
ignore = true
7+
steps = [
8+
{ argv = [
9+
"vt",
10+
"run",
11+
"--cache",
12+
"build",
13+
], comment = "first run: cache miss, emits dist/" },
14+
{ argv = [
15+
"vtt",
16+
"stat-file",
17+
"dist/assets/main.js",
18+
], comment = "existence check — content would drift across Vite versions" },
19+
{ argv = [
20+
"vtt",
21+
"rm",
22+
"dist/assets/main.js",
23+
], comment = "remove the artefact so the cache-hit restore is observable" },
24+
{ argv = [
25+
"vt",
26+
"run",
27+
"--cache",
28+
"build",
29+
], comment = "cache hit: outputs restored without manual config" },
30+
{ argv = [
31+
"vtt",
32+
"stat-file",
33+
"dist/assets/main.js",
34+
], comment = "restored from the cache archive" },
35+
]
36+
137
[[e2e]]
238
name = "vite_prefix_env_change_invalidates_cache"
339
comment = """
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# vite_build_caches_and_restores_outputs
2+
3+
`vt run --cache build` must produce a cache hit on the second run without any manual input/output configuration. Vite reports `ignoreInput(outDir)` + `ignoreInput/Output(cacheDir)` via `@voidzero-dev/vite-task-client`, so fspy-detected reads of `dist/` and writes to `node_modules/.vite/` don't poison the cache.
4+
5+
## `vt run --cache build`
6+
7+
first run: cache miss, emits dist/
8+
9+
```
10+
$ vite build
11+
```
12+
13+
## `vtt stat-file dist/assets/main.js`
14+
15+
existence check — content would drift across Vite versions
16+
17+
```
18+
dist/assets/main.js: exists
19+
```
20+
21+
## `vtt rm dist/assets/main.js`
22+
23+
remove the artefact so the cache-hit restore is observable
24+
25+
```
26+
```
27+
28+
## `vt run --cache build`
29+
30+
cache hit: outputs restored without manual config
31+
32+
```
33+
$ vite build ◉ cache hit, replaying
34+
35+
---
36+
vt run: cache hit.
37+
```
38+
39+
## `vtt stat-file dist/assets/main.js`
40+
41+
restored from the cache archive
42+
43+
```
44+
dist/assets/main.js: exists
45+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@
55
// No `"env": [...]` — vite's patched `loadEnv` asks the runner for
66
// every `VITE_*` env via `getEnvs`, so the glob + match-set are
77
// fingerprinted automatically.
8-
//
9-
// Auto output tracking (which lets vite `ignoreInput`/`ignoreOutput` the
10-
// out dir and the dirs it writes-then-reads) is not implemented yet, so
11-
// excluding them from auto input inference keeps `vite build` cacheable:
12-
// it stops fspy's reads of the build's own outputs (`dist/`) and vite's
13-
// write-then-read config-timestamp temp files (`node_modules/.vite-temp/`)
14-
// from being treated as inputs, which would otherwise trip the read-write
15-
// overlap check.
16-
"input": ["!dist/**", "!node_modules/.vite-temp/**", { "auto": true }],
178
"cache": true
189
}
1910
}

crates/vite_task_graph/src/config/mod.rs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,11 @@ impl ResolvedTaskOptions {
7575
workspace_root,
7676
)?;
7777

78-
// Auto output restoration is not implemented yet — it lands with
79-
// runner-aware output tracking (which consumes `ignoreOutput`).
80-
// Until then `output: None` defaults to disabled rather than
81-
// auto-inference, so a cache hit never restores an unverified,
82-
// fspy-inferred output set.
83-
let output_config = match enabled_cache_config.output.as_ref() {
84-
None => ResolvedGlobConfig::disabled(),
85-
output => ResolvedGlobConfig::from_user_config(output, dir, workspace_root)?,
86-
};
78+
let output_config = ResolvedGlobConfig::from_user_config(
79+
enabled_cache_config.output.as_ref(),
80+
dir,
81+
workspace_root,
82+
)?;
8783

8884
Some(CacheConfig {
8985
env_config: EnvConfig {
@@ -144,16 +140,6 @@ impl ResolvedGlobConfig {
144140
}
145141
}
146142

147-
/// Disabled configuration: no auto-inference and no explicit patterns.
148-
#[must_use]
149-
pub const fn disabled() -> Self {
150-
Self {
151-
includes_auto: false,
152-
positive_globs: BTreeSet::new(),
153-
negative_globs: BTreeSet::new(),
154-
}
155-
}
156-
157143
/// Resolve from user configuration, making glob patterns workspace-root-relative.
158144
///
159145
/// - `None`: defaults to auto-inference (`[{auto: true}]`)

crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"negative_globs": []
6363
},
6464
"output_config": {
65-
"includes_auto": false,
65+
"includes_auto": true,
6666
"positive_globs": [],
6767
"negative_globs": []
6868
}

0 commit comments

Comments
 (0)