DEV Community

Cover image for What vx is, and what it refuses to be
VX
VX

Posted on Originally published at vznjs.github.io

What vx is, and what it refuses to be

Every monorepo tool eventually describes itself with a paragraph of
nouns: caching, task graph, remote execution, affected detection,
generators, a dashboard, a cloud. vx's description is one sentence.

vx runs and caches a task graph, correctly, and stops there.

That sentence is a design, not a slogan, and this post walks through
what it commits us to.

The pipeline

A run is a pipeline with five stages, and each one has a documented
seam a plugin can fill:

  1. Discover the projects in the workspace (the package manager's workspace globs, one package.json each).
  2. Evaluate each vx.config.ts. Configs are TypeScript programs, not JSON; the pipeline sees the object they evaluate to.
  3. Build one task graph across the whole workspace from dependsOn and the package dependency graph.
  4. Derive a content-addressed key per task from its declared inputs, its resolved config, and the keys of everything upstream.
  5. Schedule the graph: look each key up, restore hits, execute misses with bounded parallelism, save results.

In plugin terms the stages are named config → discover → project → graph →
key → fingerprint → schedule → admit, followed by the two
behaviour capabilities executor (where a command runs) and cache
(where artifacts live), the observe-only telemetry capability,
setup and teardown around the run, and commands (CLI verbs).
Core applies no plugin by default and names
none. A workspace with no vx.workspace.ts still runs and caches,
because the local executor and the local cache are the floor under
every list, not plugins you have to remember to add.

What a task is

A task is one shell command with declared inputs and outputs:

build: {
  exec: { command: 'tsc -b' },
  cache: {
    inputs: { files: ['src/**', 'tsconfig.json'] },
    outputs: { files: ['dist/**'] },
  },
}
Enter fullscreen mode Exit fullscreen mode

Three rules hold across the whole tool:

  • Caching is opt-in. No cache block, no cache. When there is one, both inputs and outputs are required. vx never infers what a task reads.
  • One command per task. A plugin may change where the command runs, never what it is. Chain with && or split into tasks wired by dependsOn so each step caches on its own.
  • Project boundaries are hard. A glob never crosses into another project's directory. What one project needs from another arrives through the graph, as an upstream task's outputs.

What is not inside

The list below is not a roadmap gap. It is a boundary, and it is in the
repository's memory file so nobody re-proposes it by accident:

  • No cloud, no account, no dashboard. vx does not know your organisation exists.
  • No daemon. Every run pays its own discovery and still answers a fully cached 3,270-task graph in about half a second.
  • No auto-inferred inputs. A traced read set describes what a task read once, on one machine, after the fact. A key is needed before the task runs. You declare inputs, and the sandbox lets you enforce the declaration.
  • No JavaScript-function tasks. The shell is the API. Your tools stay yours.
  • No named inputs, no global inputs, no global env. Configs are TypeScript. A shared preset is an import and a spread.
  • Nothing distributed in the core repository. Remote caches, remote execution, telemetry sinks, agent protocols, GitHub summaries are all plugins in their own packages. @vzn/vx-reapi (Bazel Remote Execution API) is the proof the seams are wide enough to build those on.

Why the boundary matters

A tool that owns the cloud has an incentive to make the local path
merely adequate. A tool that owns nothing but the graph has one job:
be correct and be fast on the machine in front of you. Everything that
follows in this series is a consequence of that job. The next post is
about the fast part.

Where to look next: the Quickstart, the
Architecture page, and the
comparison with Turborepo, Nx and vite-task.


Originally published on the vx blog. vx is an MIT task runner and build cache for JS monorepos: github.com/vznjs/vx. Written with AI assistance.

Top comments (2)

Collapse
 
sinarezaei profile image
Sina Rezaei •

The explicit-input approach makes sense, but it puts a lot of weight on getting the dependency contract right. Take an OpenAPI codegen task in a monorepo: if its cache key includes the schema but misses the generator version or configuration, vx can restore a perfectly valid cache hit that produces an outdated client.

That's the trade-off with declared inputs: you get predictable caching, but only if the declaration captures everything that can affect the output. I'd be interested in how vx's sandbox helps developers catch those undeclared reads before they turn into subtle CI-only failures.

Collapse
 
vzn-vx profile image
VX • • Edited

Good example, and that's the risk we built around. The generator version is covered already: a task's key includes its package's package.json, and with the lockfile plugin, the exact resolved versions of its dependencies. So bumping the generator is a miss, not a stale hit.

(Lockfiles: vznjs.github.io/vx/guides/configur...)

For config and anything else the task reads, turn on exec.sandbox for the task. A read of a file you didn't declare is then denied and the task fails, and a failed task is never cached. Run it sandboxed locally or in CI once, and an undeclared generator config shows up as a red task with the path in the error, instead of a quiet stale client later.

(Sandboxing: vznjs.github.io/vx/guides/sandboxing/)

Once a violation is found, you decide: allow the read in the sandbox's allow.read, or leave it out and it stays blocked. Allowing a read doesn't make it a cache input; if the file should change the key, list it in cache.inputs.files too. Keeping the two explicit is the point.

(Caching: vznjs.github.io/vx/guides/configur...)