Skip to content

Commit 9c0680d

Browse files
wan9chiclaude
andcommitted
feat(cache): runner-aware auto output tracking + ignore consumption
Completes runner-aware caching on top of the IPC infra: - ignoreInput/ignoreOutput land across the whole IPC surface: protocol verbs, sync client (with cwd-resolution and Windows path canonicalization), napi addon methods, and server-side recording with absolute-path validation. - `output: None` resolves to auto inference again, so fspy-written files are archived and restored on a cache hit (auto output restoration). - The reported ignores are 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. - Adds the real-vite e2e coverage, which needs this full client surface: ignoreInput keeps the cache valid, ignoreOutput allows a read-write overlap, vite build caches/restores its outputs and re-runs on tracked env changes, and vite dev disables caching end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c64754d commit 9c0680d

111 files changed

Lines changed: 1058 additions & 261 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.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Changed** Cached tasks without an `output` config now automatically archive the files the task writes and restore them on cache hits (`output: []` disables restoration). Runner-aware tools can exclude internal paths via `ignoreInput`/`ignoreOutput`, so `vite build` caches and restores `dist/` with zero manual cache config ([#431](https://github.com/voidzero-dev/vite-task/pull/431))
34
- **Added** Runner-aware env tracking: tools can ask the runner for env values via `getEnv`/`getEnvs`, and the served values (and glob match-sets) become part of the cache fingerprint, so changing them invalidates the cache with the env var named in the miss message ([#430](https://github.com/voidzero-dev/vite-task/pull/430))
45
- **Added** Runner-aware tools can now opt the current task run out of caching through the new IPC channel; Vite dev server integration uses this automatically ([#441](https://github.com/voidzero-dev/vite-task/pull/441))
56
- **Fixed** Prefix environment assignments like `PATH=... command` now affect executable lookup during task planning, so tools provided only by the prefixed `PATH` can be resolved correctly ([#440](https://github.com/voidzero-dev/vite-task/pull/440))

Cargo.lock

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

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

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,14 @@ pub(super) async fn update_cache(
6767
return (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::ToolRequested), None);
6868
}
6969

70-
// `ignoreInput`/`ignoreOutput` are accepted over IPC but not yet
71-
// applied — runner-aware output tracking (which consumes them) lands
72-
// in a follow-up. Treat the reported ignore sets as empty so they
73-
// have no effect on input/output inference or the read-write overlap
74-
// check.
75-
let ignored_input_rels: FxHashSet<RelativePathBuf> = FxHashSet::default();
76-
let ignored_output_rels: FxHashSet<RelativePathBuf> = FxHashSet::default();
70+
// Tool-reported paths to exclude from auto-tracking. Absolute paths
71+
// are normalized to workspace-relative; anything outside is dropped.
72+
let ignored_input_rels: FxHashSet<RelativePathBuf> = reports
73+
.map(|r| normalize_ignored_paths(&r.ignored_inputs, workspace_root))
74+
.unwrap_or_default();
75+
let ignored_output_rels: FxHashSet<RelativePathBuf> = reports
76+
.map(|r| normalize_ignored_paths(&r.ignored_outputs, workspace_root))
77+
.unwrap_or_default();
7778

7879
if cancelled {
7980
// Cancelled (Ctrl-C or sibling failure) — result is untrustworthy.
@@ -240,6 +241,67 @@ fn observe_fspy(
240241
}
241242
}
242243

244+
/// Normalize tool-reported absolute paths to workspace-relative. Paths outside
245+
/// the workspace are dropped — they can't contribute to inputs or outputs.
246+
fn normalize_ignored_paths(
247+
paths: &FxHashSet<Arc<AbsolutePath>>,
248+
workspace_root: &AbsolutePath,
249+
) -> FxHashSet<RelativePathBuf> {
250+
// On Windows, `workspace_root` may carry a `\\?\` extended-path prefix
251+
// (it does when the runner derived it from `std::fs::canonicalize`)
252+
// while a tool's `current_dir()`-based ignoreInput/ignoreOutput path
253+
// doesn't. `Path::strip_prefix` is a byte-exact comparison so the
254+
// prefix mismatch silently drops every tool-reported path. Pre-build
255+
// an alternate workspace root with the `\\?\` / `\\.\` / `\??\`
256+
// prefix dropped and try it as a fallback. `fspy_shared::NativePath::
257+
// strip_path_prefix` does the inverse (strips `\\?\` from incoming
258+
// fspy paths) so each side stays agnostic to how the other side
259+
// canonicalised.
260+
#[cfg(windows)]
261+
let workspace_root_stripped: Option<vite_path::AbsolutePathBuf> =
262+
windows_strip_verbatim_prefix(workspace_root.as_path().as_os_str());
263+
264+
paths
265+
.iter()
266+
.filter_map(|p| {
267+
if let Some(rel) = p.strip_prefix(workspace_root).ok().flatten() {
268+
return Some(rel);
269+
}
270+
#[cfg(windows)]
271+
if let Some(alt_root) = workspace_root_stripped.as_ref() {
272+
if let Some(rel) = p.strip_prefix(alt_root).ok().flatten() {
273+
return Some(rel);
274+
}
275+
}
276+
None
277+
})
278+
.collect()
279+
}
280+
281+
/// Build an alternate workspace-root path by dropping a `\\?\`, `\\.\`,
282+
/// or `\??\` prefix if present. Returns `None` when the input is already
283+
/// in plain `C:\...` form (no fallback needed). Mirrors
284+
/// `fspy_shared::NativePath::strip_path_prefix`'s helper so the inputs of
285+
/// `strip_prefix` can match across `current_dir`-derived and
286+
/// `canonicalize`-derived paths.
287+
#[cfg(windows)]
288+
#[expect(
289+
clippy::disallowed_types,
290+
reason = "OsStr-level prefix matching for Windows extended-path normalization"
291+
)]
292+
fn windows_strip_verbatim_prefix(p: &std::ffi::OsStr) -> Option<vite_path::AbsolutePathBuf> {
293+
use std::os::windows::ffi::{OsStrExt, OsStringExt};
294+
let wide: Vec<u16> = p.encode_wide().collect();
295+
for prefix in [r"\\?\", r"\\.\", r"\??\"] {
296+
let prefix_wide: Vec<u16> = prefix.encode_utf16().collect();
297+
if wide.starts_with(prefix_wide.as_slice()) {
298+
let stripped = std::ffi::OsString::from_wide(&wide[prefix_wide.len()..]);
299+
return vite_path::AbsolutePathBuf::new(std::path::PathBuf::from(stripped));
300+
}
301+
}
302+
None
303+
}
304+
243305
/// Whether `path` is covered by any `ignored` entry. An ignored entry matches
244306
/// itself (exact file) and everything under it (directory subtree).
245307
fn is_ignored(path: &RelativePathBuf, ignored: &FxHashSet<RelativePathBuf>) -> bool {

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ struct CacheState<'a> {
8989
/// Captured stdout/stderr for cache replay. Written in place during drain;
9090
/// always present (possibly empty) once we reach the cache-update phase.
9191
std_outputs: Vec<StdOutput>,
92-
/// `Some` iff auto-input tracking is on (`includes_auto` + successful
93-
/// IPC bind). Bundles fspy's input negative globs with the per-task IPC
94-
/// server that runner-aware tools talk to. Parts are borrowed in place
95-
/// during the wait/join; the struct is never moved out.
92+
/// `Some` iff auto-input or auto-output tracking is on (`includes_auto` +
93+
/// successful IPC bind). Bundles fspy's input negative globs with the
94+
/// per-task IPC server that runner-aware tools talk to. Parts are borrowed
95+
/// in place during the wait/join; the struct is never moved out.
9696
tracking: Option<Tracking>,
9797
}
9898

@@ -160,7 +160,9 @@ impl<'a> ExecutionMode<'a> {
160160
});
161161
};
162162

163-
let tracking = if metadata.input_config.includes_auto {
163+
let tracking = if metadata.input_config.includes_auto
164+
|| metadata.output_config.includes_auto
165+
{
164166
// Resolve input negative globs for fspy path filtering (already
165167
// workspace-root-relative).
166168
let negatives = metadata

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+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>vp-run-vite-cache</title>
5+
</head>
6+
<body>
7+
<script type="module" src="/src/main.js"></script>
8+
</body>
9+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "vite-build-cache-fixture",
3+
"private": true,
4+
"type": "module"
5+
}

0 commit comments

Comments
 (0)