Skip to content
vxvx
GitHubBlueskydev.toRSS
Blog

An input changes mid-run: what four task tools do

A task reads src/a.ts, and while it runs, something writes src/a.ts. Which cache entry gets saved? We read the source of vx, Turborepo, Nx and Vite Task to find out. Each made a different trade.

A cache key describes a task’s inputs at one moment. If an input changes while the command runs, the output may have been built from bytes the key does not describe. Save it under that key and a later hit restores an output that matches neither version of the file.

It happens more than it sounds: you save a file while a build runs, a code generator in another task rewrites a shared file, or a tool rewrites its own config. We read each tool’s source, at the version named, to see what it does after the command exits.

Turborepo computes the task hash before the task runs. After a successful run, save_outputs globs the declared outputs and stores them under that same hash. It does not look at the inputs again. Source: crates/turborepo-run-cache/src/lib.rs, save_outputs.

Nx hashes a task in processTask before it is scheduled, and after the run postRunSteps calls cache.put(task.hash, …) with that hash. It does not compare inputs before and after. In batch mode, a task whose inputs include its dependencies’ outputs is hashed again after it runs, against the files as they are then. Source: nx@23.3.0, dist/src/tasks-runner/task-orchestrator.js (processTask, postRunSteps) and cache.js (put).

Vite Task traces the files a task opens (its fspy tracer) and checks them after the run. If the task wrote a file it also read, the entry is not saved and the reason is InputModified. The check covers reads the tracer saw from the task’s own processes, not the globbed inputs; its comment notes that a task writing a globbed file it never read gets repeated misses rather than a wrong entry. Source: vite-plus 1.1.0 pins vite-task at 7d69d65, crates/vt/src/session/execute/cache_update.rs.

vx takes the key just before the command and, after it exits, checks every declared input once, whoever wrote it: the task, your editor, or another task. Any file written after its digest was learned counts as moved, even if its bytes are back to the original, because an edit that was undone mid-run cannot be told apart from no edit at all. A moved input keeps the run’s result, saves no entry, and names the file; every task downstream skips its save too.

[vx] app#build: `packages/app/src/routes.gen.ts` changed after its key was taken — the result stands, but is not saved under a key that no longer describes it; if the task writes it, declare it in cache.outputs

How strict this is, is yours to choose with cache.inputStability: 'mutable' saves when the bytes end identical (for a tool that rewrites its own package.json), 'unchecked' turns the check off, and severity: 'error' fails the task instead of warning. Source: packages/vx/src/orchestrator/execute-task.ts; the full story is in When an input moves mid-run.

After the command exitsTurborepo 2.11.7Nx 23.3.0Vite Task 1.1.0vx
Inputs looked at againnono (batch: rehash)files the task openedevery declared input
Writes it noticesnonenonethe task’s ownany process’s
What it does on a changesavessavesskips the saveskips the save, names file
Can be relaxed per taskn/an/an/ainputStability

Checking costs something. vx pays one lstat per input after a run that saves, and a task that touches its own inputs rebuilds next time unless you tell vx how to judge it. Turborepo and Nx skip that work. Vite Task checks what its tracer already recorded. None of these is free, and which trade fits depends on how often your inputs move under a running task.

We chose to make a cache entry mean exactly what its declared inputs say, and to make the strictness a setting. vx is at vznjs.github.io/vx.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.