Vite Task implements a caching system to avoid re-running tasks when their inputs haven't changed. This document describes the architecture, design decisions, and implementation details of the task cache system.
The task cache system enables:
- Incremental builds: Only run tasks when inputs have changed
- Shared caching: Multiple tasks with identical commands can share cache entries
- Content-based hashing: Cache keys based on actual content, not timestamps
- Output replay: Cached stdout/stderr are replayed exactly as originally produced
- Two-tier caching: Cache entries shared across tasks, with task-run associations
- Configurable input: Control which files are tracked for cache invalidation
For tasks defined as below:
the task cache system is able to hit the same cache for the test task and for the first subcommand in the build task:
- user runs
vp run build-> no cache hit. runecho $fooand create cache - user runs
vp run testecho $foo-> hit cache created in step 1 and replayecho $bar-> no cache hit. runecho $barand create cache
- user changes env
$foo - user runs
vp run testecho $foo- the cache system should be able to locate the cache that was created in step 1 and hit in step 2.1
- compare the spawn fingerprint and report cache miss because
$foois changed. - re-run and replace the cache with a new one.
echo $bar-> hit cache created in step 2.2 and replay
- user runs
vp run build: hit the cache created in step 4.1.3 and replay.
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ Task Execution Flow │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Task Request │
│ ──────────────── │
│ app#build │
│ │ │
│ ▼ │
│ 2. Cache Key Generation │
│ ────────────────────── │
│ • Spawn fingerprint (cwd, program, args, env) │
│ • Input configuration │
│ │ │
│ ▼ │
│ 3. Cache Lookup (SQLite) │
│ ──────────────────────── │
│ ┌─────────────────┬──────────────────────┐──────────────────────────┐ │
│ │ Cache Hit │ Cache Not Found │ Cache Found but Miss │ │
│ └────────┬────────┴─────────┬────────────┘──────────────────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 4a. Validate Fingerprint 4b. Execute Task ◀───── 4c. Report what changed │
│ ──────────────────────── ──────────────── │
│ • Inputs unchanged? • Run command │
│ • Spawn config same? • Monitor files (fspy) │
│ • Capture stdout/stderr │
│ │ │ │
│ ▼ ▼ │
│ 5a. Replay Outputs 5b. Store in Cache │
│ ────────────────── ────────────────── │
│ • Write to stdout • Save fingerprint │
│ • Write to stderr • Save outputs │
│ • Update database │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
The cache entry key uniquely identifies a command execution context:
pub struct CacheEntryKey {
pub spawn_fingerprint: SpawnFingerprint,
pub input_config: ResolvedGlobConfig,
}The spawn fingerprint captures the complete execution context:
pub struct SpawnFingerprint {
pub cwd: RelativePathBuf,
pub program_fingerprint: ProgramFingerprint,
pub args: Arc<[Str]>,
pub env_fingerprints: EnvFingerprints,
}
pub struct EnvFingerprints {
pub fingerprinted_envs: BTreeMap<Str, Arc<str>>,
pub untracked_env_config: Arc<[Str]>,
}
enum ProgramFingerprint {
OutsideWorkspace { program_name: Str },
InsideWorkspace { relative_program_path: RelativePathBuf },
}This ensures cache invalidation when:
- Working directory changes (package location changes)
- Command or arguments change
- Declared environment variables differ (untracked envs don't affect cache)
- Program location changes (inside/outside workspace)
The fingerprinted_envs field in EnvFingerprints is crucial for cache correctness:
- Only includes env vars explicitly declared in the task's
envarray - Does NOT include untracked envs (PATH, CI, etc.)
- These env vars become part of the cache key
When a task runs:
- All env vars (including untracked) are available to the process
- Only declared env vars affect the cache key
- If a declared env var changes value, cache will miss
- If an untracked env changes, cache will still hit
The untracked_env_config field stores env names (not values) — if the set of untracked env names changes, the cache invalidates, but value changes don't.
The execution cache key associates a task identity with its cache entry:
pub enum ExecutionCacheKey {
UserTask {
task_name: Str,
and_item_index: usize,
extra_args: Arc<[Str]>,
package_path: RelativePathBuf,
},
ExecAPI(Arc<[Str]>),
}The cached execution result:
pub struct CacheEntryValue {
pub post_run_fingerprint: PostRunFingerprint,
pub std_outputs: Arc<[StdOutput]>,
pub duration: Duration,
pub globbed_inputs: BTreeMap<RelativePathBuf, u64>,
}Vite Task uses fspy to monitor file system access during task execution:
┌──────────────────────────────────────────────────────────────┐
│ File System Monitoring │
├──────────────────────────────────────────────────────────────┤
│ │
│ Task Execution: │
│ ────────────── │
│ 1. Start fspy monitoring │
│ 2. Execute task command │
│ 3. Capture accessed files │
│ 4. Stop monitoring │
│ │ │
│ ▼ │
│ Fingerprint Generation: │
│ ────────────────────── │
│ For each accessed file: │
│ • Check if file exists │
│ • If file: Hash contents with xxHash3 │
│ • If directory: Record structure │
│ • If missing: Mark as NotFound │
│ │ │
│ ▼ │
│ Path Fingerprint Types: │
│ ────────────────────── │
│ enum PathFingerprint { │
│ NotFound, // File doesn't exist │
│ FileContentHash(u64), // xxHash3 of content │
│ Folder(Option<BTreeMap<Str, DirEntryKind>>), │
│ } ▲ │
│ │ │
│ This value is `None` when fspy reports that the task is │
│ opening a folder but not reading its entries. This can │
│ happen when the opened folder is used as a dirfd for │
│ `openat(2)`. In such case, the folder's entries don't need │
│ to be fingerprinted. │
│ Folders with empty entries fingerprinted are represented as │
│ `Folder(Some(empty BTreeMap))`. │
│ │
└──────────────────────────────────────────────────────────────┘
The input field in vite-task.json controls which files are tracked for cache fingerprinting:
{
"tasks": {
"build": {
"input": ["src/**", "!dist/**", { "auto": true }]
}
}
}- Omitted (default):
[{auto: true}]— automatically tracks which files the task reads viafspy [](empty array): disables file tracking entirely- Glob patterns (e.g.
"src/**"): select specific files {auto: true}: enables automatic file tracking- Negative patterns (e.g.
"!dist/**"): exclude matched files
See inputs.md for full details.
When a cache entry exists, the fingerprint is validated to detect changes:
pub enum FingerprintMismatch {
SpawnFingerprint { old: SpawnFingerprint, new: SpawnFingerprint },
InputConfig,
InputChanged { kind: InputChangeKind, path: RelativePathBuf },
}
pub enum InputChangeKind {
ContentModified,
Added,
Removed,
}Vite Task uses SQLite with WAL (Write-Ahead Logging) mode for cache storage:
// Database initialization
let conn = Connection::open(cache_path)?;
conn.pragma_update(None, "journal_mode", "WAL")?; // Better concurrency
conn.pragma_update(None, "synchronous", "NORMAL")?; // Balance speed/safety-- Cache entries keyed by spawn fingerprint + input config
CREATE TABLE cache_entries (
key BLOB PRIMARY KEY, -- Serialized CacheEntryKey
value BLOB -- Serialized CacheEntryValue
);
-- Maps task identity to its cache entry key
CREATE TABLE task_fingerprints (
key BLOB PRIMARY KEY, -- Serialized ExecutionCacheKey
value BLOB -- Serialized CacheEntryKey
);Cache entries are serialized using bincode for efficient storage.
┌──────────────────────────────────────────────────────────────┐
│ Cache Hit Process │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. Generate Cache Keys │
│ ────────────────────── │
│ CacheEntryKey { │
│ spawn_fingerprint: SpawnFingerprint { ... }, │
│ input_config: ResolvedGlobConfig { ... }, │
│ } │
│ ExecutionCacheKey::UserTask { │
│ task_name: "build", │
│ package_path: "packages/app", │
│ ... │
│ } │
│ │ │
│ ▼ │
│ 2. Query Cache │
│ ────────────── │
│ SELECT value FROM cache_entries WHERE key = ? │
│ │ │
│ ▼ │
│ 3. Validate Post-Run Fingerprint │
│ ───────────────────────────────── │
│ • Check input file hashes │
│ • Detect file content changes, additions, removals │
│ │ │
│ ▼ │
│ 4. Replay Outputs │
│ ───────────────── │
│ • Write to stdout/stderr │
│ • Preserve original order │
│ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Cache Miss Process │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. Execute Task with Monitoring │
│ ─────────────────────────────── │
│ • Start fspy file monitoring │
│ • Capture stdout/stderr │
│ • Execute command │
│ • Stop monitoring │
│ │ │
│ ▼ │
│ 2. Generate Post-Run Fingerprint │
│ ───────────────────────────────── │
│ • Hash all accessed files │
│ • Record file system access patterns │
│ │ │
│ ▼ │
│ 3. Create CacheEntryValue │
│ ──────────────────────────── │
│ CacheEntryValue { │
│ post_run_fingerprint, │
│ std_outputs, │
│ duration, │
│ globbed_inputs, │
│ } │
│ │ │
│ ▼ │
│ 4. Store in Database │
│ ──────────────────── │
│ INSERT/UPDATE cache_entries + task_fingerprints │
│ │
└──────────────────────────────────────────────────────────────┘
Cache entries are automatically invalidated when:
- Command changes: Different command, arguments, or working directory
- Package location changes: Working directory (
cwd) in spawn fingerprint changes - Environment changes: Modified declared environment variables (untracked env values don't affect cache)
- Untracked env config changes: Untracked environment names added/removed from configuration
- Input files change: Content hash differs (detected via xxHash3)
- File structure changes: Files added, removed, or type changed
- Input config changes: The
inputconfiguration itself changes
The cache database is stored at node_modules/.vite/task-cache in the workspace root.
The root vite-task.json can configure caching for the entire workspace:
{
"cache": true,
"tasks": { ... }
}true— enables caching for both scripts and tasksfalse— disables all caching{ "scripts": false, "tasks": true }— default; tasks are cached but package.json scripts are not{ "scripts": true, "tasks": true }— cache everything
Individual tasks can enable or disable caching:
{
"tasks": {
"build": {
"command": "tsc && rollup -c",
"cache": true,
"dependsOn": ["lint"]
},
"deploy": {
"command": "deploy-script.sh",
"cache": false
}
}
}The --cache and --no-cache flags override all cache configuration for a single run:
vp run build --no-cache # force cache off
vp run build --cache # force cache on (even for scripts)Outputs are captured exactly as produced:
- Preserves order of stdout/stderr interleaving
- Handles binary output (e.g., from tools that output progress bars)
- Maintains ANSI color codes and formatting
When a task hits cache, outputs are replayed exactly:
┌──────────────────────────────────────────────────────────────┐
│ Output Replay │
├──────────────────────────────────────────────────────────────┤
│ │
│ Cached Outputs: │
│ ────────────── │
│ [ │
│ StdOutput { kind: StdOut, "Compiling..." }, │
│ StdOutput { kind: StdErr, "Warning: ..." }, │
│ StdOutput { kind: StdOut, "✓ Build complete" } │
│ ] │
│ │ │
│ ▼ │
│ Replay Process: │
│ ────────────── │
│ 1. Write "Compiling..." to stdout │
│ 2. Write "Warning: ..." to stderr │
│ 3. Write "✓ Build complete" to stdout │
│ │ │
│ ▼ │
│ Result: Identical output as original execution │
│ │
└──────────────────────────────────────────────────────────────┘
Vite Task uses xxHash3 for file content hashing, providing excellent performance (~10GB/s on modern CPUs).
Instead of scanning all possible input files, fspy monitors actual file access:
Traditional Approach:
Scan all src/**/*.ts files → Hash everything
Problem: Hashes files never accessed
Vite Task Approach:
Monitor with fspy → Hash only accessed files
Benefit: Minimal work, accurate dependencies
- WAL mode for better concurrency
- Balanced durability for performance
- Prepared/cached statements for efficiency
Using bincode for compact, fast serialization with direct storage without text conversion.
Ensure commands produce identical outputs for identical inputs:
// ✅ Good: Deterministic output
{
"tasks": {
"build": {
"command": "tsc && echo Build complete"
}
}
}{
"tasks": {
"deploy": {
"command": "deploy-to-production.sh",
"cache": false
}
}
}{
"tasks": {
"build": {
"input": ["src/**", "tsconfig.json", "!src/**/*.test.ts"]
}
}
}{
"scripts": {
"build": "tsc && rollup -c && terser dist/bundle.js"
}
}Each && separated command is cached independently. If only terser config changes, TypeScript and rollup will hit cache.
crates/vite_task/src/session/
├── cache/
│ ├── mod.rs # ExecutionCache, CacheEntryKey/Value, FingerprintMismatch
│ └── display.rs # Cache status display formatting
├── execute/
│ ├── mod.rs # execute_spawn, SpawnOutcome
│ ├── fingerprint.rs # PostRunFingerprint, PathFingerprint, DirEntryKind
│ └── spawn.rs # spawn_with_tracking, fspy integration
└── reporter/
└── mod.rs # Reporter traits for cache hit/miss display
crates/vite_task_plan/src/
├── cache_metadata.rs # ExecutionCacheKey, SpawnFingerprint, ProgramFingerprint, CacheMetadata
├── envs.rs # EnvFingerprints
└── plan.rs # Planning logic