Skip to content

Commit 622930a

Browse files
authored
feat(task): add cache fingerprint ignore patterns (#214)
Allow tasks to exclude specific files/directories from cache fingerprint calculation using glob patterns with gitignore-style negation support. This enables selective caching for tasks like package installation where only dependency manifests (package.json) matter for cache validation, not implementation files. Cache hits occur when ignored files change. Key features: - Optional fingerprintIgnores field accepts glob patterns - Negation patterns (!) to include files within ignored directories - Leverages existing vite_glob crate for pattern matching - Fully backward compatible (defaults to None) Example: ``` {"fingerprintIgnores": ["node_modules/**","!node_modules/**/package.json"]} ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude [noreply@anthropic.com](mailto:noreply@anthropic.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds gitignore-style glob patterns to filter post-run fingerprint inputs, wiring config through execution and caching to control cache invalidation. > > - **Core fingerprinting**: > - `PostRunFingerprint::create()` filters `path_reads` via `vite_glob::GlobPatternSet`, honoring negation and order of `fingerprint_ignores`. > - **Config/Fingerprint**: > - Add `TaskConfig.fingerprint_ignores?: Option<Vec<Str>>`. > - Include `fingerprint_ignores` in `CommandFingerprint` to affect cache keys. > - Propagate in `ResolvedTaskConfig::resolve_command()` and builtin resolver. > - **Caching/Execution flow**: > - `CommandCacheValue::create()` and `schedule.rs` pass `fingerprint_ignores` to post-run fingerprint creation. > - Bump task cache DB `user_version` to `3` and adjust migration handling. > - **Tests/Fixtures**: > - Extensive unit tests for pattern filtering and cache-key sensitivity; add fixture and CLI snap-test demonstrating ignore/negation behavior. > - **Docs**: > - Add RFC documenting schema, behavior, examples, and implementation details. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 73eb4ec. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 096aca2 commit 622930a

15 files changed

Lines changed: 1210 additions & 7 deletions

File tree

crates/vite_task/docs/rfc-cache-fingerprint-ignore-patterns.md

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Fingerprint Ignore Test Fixture
2+
3+
This fixture demonstrates the `fingerprintIgnores` feature for cache fingerprint calculation.
4+
5+
## Task Configuration
6+
7+
The `create-files` task in `vite-task.json` uses the following ignore patterns:
8+
9+
```json
10+
{
11+
"fingerprintIgnores": [
12+
"node_modules/**/*",
13+
"!node_modules/**/package.json",
14+
"dist/**/*"
15+
]
16+
}
17+
```
18+
19+
## Behavior
20+
21+
With these ignore patterns:
22+
23+
1. **`node_modules/**/*`** - Ignores all files under `node_modules/`
24+
2. **`!node_modules/**/package.json`** - BUT keeps `package.json` files (negation pattern)
25+
3. **`dist/**/*`** - Ignores all files under `dist/`
26+
27+
### Cache Behavior
28+
29+
- ✅ Cache **WILL BE INVALIDATED** when `node_modules/pkg-a/package.json` changes
30+
- ❌ Cache **WILL NOT BE INVALIDATED** when `node_modules/pkg-a/index.js` changes
31+
- ❌ Cache **WILL NOT BE INVALIDATED** when `dist/bundle.js` changes
32+
33+
This allows caching package installation tasks where only dependency manifests (package.json) matter for cache validation, not the actual implementation files.
34+
35+
## Example Usage
36+
37+
```bash
38+
# First run - task executes
39+
vite run create-files
40+
41+
# Second run - cache hit (all files tracked in fingerprint remain the same)
42+
vite run create-files
43+
44+
# Modify node_modules/pkg-a/index.js
45+
echo 'modified' > node_modules/pkg-a/index.js
46+
47+
# Third run - still cache hit (index.js is ignored)
48+
vite run create-files
49+
50+
# Modify node_modules/pkg-a/package.json
51+
echo '{"name":"pkg-a","version":"2.0.0"}' > node_modules/pkg-a/package.json
52+
53+
# Fourth run - cache miss (package.json is NOT ignored due to negation pattern)
54+
vite run create-files
55+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@test/fingerprint-ignore",
3+
"version": "1.0.0"
4+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"tasks": {
3+
"create-files": {
4+
"command": "mkdir -p node_modules/pkg-a && echo '{\"name\":\"pkg-a\"}' > node_modules/pkg-a/package.json && echo 'content' > node_modules/pkg-a/index.js && mkdir -p dist && echo 'output' > dist/bundle.js",
5+
"cacheable": true,
6+
"fingerprintIgnores": [
7+
"node_modules/**/*",
8+
"!node_modules/**/package.json",
9+
"dist/**/*"
10+
]
11+
}
12+
}
13+
}

crates/vite_task/src/cache.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ impl CommandCacheValue {
3030
executed_task: ExecutedTask,
3131
fs: &impl FileSystem,
3232
base_dir: &AbsolutePath,
33+
fingerprint_ignores: Option<&[Str]>,
3334
) -> Result<Self, Error> {
34-
let post_run_fingerprint = PostRunFingerprint::create(&executed_task, fs, base_dir)?;
35+
let post_run_fingerprint =
36+
PostRunFingerprint::create(&executed_task, fs, base_dir, fingerprint_ignores)?;
3537
Ok(Self {
3638
post_run_fingerprint,
3739
std_outputs: executed_task.std_outputs,
@@ -105,16 +107,18 @@ impl TaskCache {
105107
"CREATE TABLE taskrun_to_command (key BLOB PRIMARY KEY, value BLOB);",
106108
(),
107109
)?;
108-
conn.execute("PRAGMA user_version = 2", ())?;
110+
// Bump to version 3 to invalidate cache entries due to a change in the serialized cache key content
111+
// (addition of the `fingerprint_ignores` field). No schema change was made.
112+
conn.execute("PRAGMA user_version = 3", ())?;
109113
}
110-
1 => {
114+
1..=2 => {
111115
// old internal db version. reset
112116
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?;
113117
conn.execute("VACUUM", ())?;
114118
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?;
115119
}
116-
2 => break, // current version
117-
3.. => return Err(Error::UnrecognizedDbVersion(user_version)),
120+
3 => break, // current version
121+
4.. => return Err(Error::UnrecognizedDbVersion(user_version)),
118122
}
119123
}
120124
Ok(Self { conn: Mutex::new(conn), path: cache_path })

crates/vite_task/src/config/mod.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ pub struct TaskConfig {
4646

4747
#[serde(default)]
4848
pub(crate) pass_through_envs: HashSet<Str>,
49+
50+
#[serde(default)]
51+
pub(crate) fingerprint_ignores: Option<Vec<Str>>,
52+
}
53+
54+
impl TaskConfig {
55+
pub fn set_fingerprint_ignores(&mut self, fingerprint_ignores: Option<Vec<Str>>) {
56+
self.fingerprint_ignores = fingerprint_ignores;
57+
}
4958
}
5059

5160
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -148,6 +157,7 @@ impl ResolvedTask {
148157
args,
149158
ResolveCommandResult { bin_path, envs },
150159
false,
160+
None,
151161
)
152162
}
153163

@@ -157,14 +167,16 @@ impl ResolvedTask {
157167
args: impl Iterator<Item = impl AsRef<str>> + Clone,
158168
command_result: ResolveCommandResult,
159169
ignore_replay: bool,
170+
fingerprint_ignores: Option<Vec<Str>>,
160171
) -> Result<Self, Error> {
161172
let ResolveCommandResult { bin_path, envs } = command_result;
162173
let builtin_task = TaskCommand::Parsed(TaskParsedCommand {
163174
args: args.clone().map(|arg| arg.as_ref().into()).collect(),
164175
envs: envs.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
165176
program: bin_path.into(),
166177
});
167-
let task_config: TaskConfig = builtin_task.clone().into();
178+
let mut task_config: TaskConfig = builtin_task.clone().into();
179+
task_config.set_fingerprint_ignores(fingerprint_ignores.clone());
168180
let pass_through_envs = task_config.pass_through_envs.iter().cloned().collect();
169181
let cwd = &workspace.cwd;
170182
let resolved_task_config =
@@ -179,6 +191,7 @@ impl ResolvedTask {
179191
.into_iter()
180192
.collect(),
181193
pass_through_envs,
194+
fingerprint_ignores,
182195
},
183196
all_envs: resolved_envs.all_envs,
184197
};
@@ -245,6 +258,13 @@ impl std::fmt::Debug for ResolvedTaskCommand {
245258
/// - The resolver provides envs which become part of the fingerprint
246259
/// - If resolver provides different envs between runs, cache breaks
247260
/// - Each built-in task type must have unique task name to avoid cache collision
261+
///
262+
/// # Fingerprint Ignores Impact on Cache
263+
///
264+
/// The `fingerprint_ignores` field controls which files are tracked in PostRunFingerprint:
265+
/// - Changes to this config must invalidate the cache
266+
/// - Vec maintains insertion order (pattern order matters for last-match-wins semantics)
267+
/// - Even though ignore patterns only affect PostRunFingerprint, the config itself is part of the cache key
248268
#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)]
249269
#[diff(attr(#[derive(Debug)]))]
250270
pub struct CommandFingerprint {
@@ -256,6 +276,10 @@ pub struct CommandFingerprint {
256276
/// even though value changes to `pass_through_envs` shouldn't invalidate the cache,
257277
/// The names should still be fingerprinted so that the cache can be invalidated if the `pass_through_envs` config changes
258278
pub pass_through_envs: BTreeSet<Str>, // using BTreeSet to have a stable order in cache db
279+
280+
/// Glob patterns for fingerprint filtering. Order matters (last match wins).
281+
/// Changes to this config invalidate the cache to ensure correct fingerprint tracking.
282+
pub fingerprint_ignores: Option<Vec<Str>>,
259283
}
260284

261285
#[cfg(test)]

crates/vite_task/src/config/task_command.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl From<TaskCommand> for TaskConfig {
3535
inputs: Default::default(),
3636
envs: Default::default(),
3737
pass_through_envs: Default::default(),
38+
fingerprint_ignores: Default::default(),
3839
}
3940
}
4041
}
@@ -105,6 +106,7 @@ impl ResolvedTaskConfig {
105106
.into_iter()
106107
.collect(),
107108
pass_through_envs: self.config.pass_through_envs.iter().cloned().collect(),
109+
fingerprint_ignores: self.config.fingerprint_ignores.clone(),
108110
},
109111
all_envs: task_envs.all_envs,
110112
})

crates/vite_task/src/execute.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ mod tests {
524524
inputs: HashSet::new(),
525525
envs: HashSet::new(),
526526
pass_through_envs: HashSet::new(),
527+
fingerprint_ignores: None,
527528
};
528529

529530
let resolved_task_config =
@@ -615,6 +616,7 @@ mod tests {
615616
inputs: HashSet::new(),
616617
envs,
617618
pass_through_envs: HashSet::new(),
619+
fingerprint_ignores: None,
618620
};
619621

620622
let resolved_task_config =
@@ -737,6 +739,7 @@ mod tests {
737739
inputs: HashSet::new(),
738740
envs,
739741
pass_through_envs: HashSet::new(),
742+
fingerprint_ignores: None,
740743
};
741744

742745
let resolved_task_config =
@@ -803,6 +806,7 @@ mod tests {
803806
inputs: HashSet::new(),
804807
envs,
805808
pass_through_envs: HashSet::new(),
809+
fingerprint_ignores: None,
806810
};
807811

808812
let resolved_task_config =

0 commit comments

Comments
 (0)