Performance, modularity, extensibility. In that order.
Performance, modularity, extensibility. In that order.
Three priorities in a fixed order settle every design choice in vx: performance, then modularity, then extensibility. Nine rules follow.
vx never guesses what a task reads. A trace says what a task read once, after it ran. A cache key has to be right before the task runs.
vx never guesses what a task reads. You list its inputs. Here is why:
the worst thing a build cache can do is restore the wrong file. This
task copies notes.md, but its inputs name only src/**:
notes: { exec: { command: 'mkdir -p out && cp notes.md out/notes.md' }, inputs: { files: ['src/**'] }, outputs: { files: ['out/**'] }, cache: true,}$ vx run packages/a#notes local 1↑$ cat packages/a/out/notes.mdversion 1
$ echo "version 2" > packages/a/notes.md$ vx run packages/a#notes local 1✓$ cat packages/a/out/notes.mdversion 1A green run, a cache hit, and the wrong file. We call that a stale hit. It came from one missing input.
flowchart LR
K[key needed] --> D{run or restore?}
D --> T[task runs]
T --> TR[trace of files read]
TR -.->|too late| K
Some tools record the files a task opens. That list can’t be the key:
config/prod.json in
production and config/dev.json otherwise has two traces.** as the default, a
README edit rebuilds every task in the package.Only the task’s author knows what it depends on.
| Mistake | What happens | Cost |
|---|---|---|
| Extra file listed | the task runs again | an extra run: visible, cheap |
| File missing from the list | the old output is restored | a stale hit: green, silent, wrong |
Tracing makes the second kind, because one run never sees every file a task can read. Listing inputs lets you err toward the first.
For the second, the sandbox helps: give it reads no
wider than the inputs, and the notes.md read above fails the run and
names the path.
vx why explains any re-run.
cache is opt-in. No block, no cache, no default globs.dependsOn is explicit. ^task skips a package without that task.PATH, HOME, CI,
NODE_OPTIONS and a few more), env reaches a task only through
exec.env.passThrough. One
that changes the output also goes under inputs.env, which puts it in
the key.cache. What a task may touch and
what changes its key are separate lists.Demo · vx 0be0367 · 2026-10-23
Write one glob, and skip an afternoon of debugging a stale hit. More at vznjs.github.io/vx.