The word "plugin" usually means one of two things. Either a plugin is a
whole subsystem with its own configuration language (Nx executors), or
it is a callback bolted to one event the tool happened to expose. vx
uses the word the way Vite does: the core is a pipeline, every stage
has a named hook, and a plugin is an object that implements the hooks
it needs.
The stages
config → discover → project → graph → key → fingerprint → schedule
→ admit → executor / cache → telemetry
setup and teardown wrap the run; commands adds a verb
| Stage | What a plugin can do there |
|---|---|
config |
See and adjust the workspace config before anything uses it. |
discover |
Name directories to make projects beyond the package manager's members. |
project |
Add, remove or edit one loaded project's tasks. |
graph |
Add or drop edges, mark tasks requested. |
key |
Contribute extra cache-key material per task. |
fingerprint |
Claim a lockfile out of the workspace fingerprint and key it per project. |
schedule |
Return a priority per ready task. |
admit |
Vet each local dispatch against what is running right now; false holds the task. |
executor |
Decide where one task's command runs. |
cache |
Provide a layer where artifacts live. |
telemetry |
Receive plain-data run records. Cannot change behaviour, by construction. |
setup |
Once per run, after the planning stages and before the first task. |
commands |
Add a CLI verb. A verb named like a core verb is refused, so nothing shadows vx run. |
teardown |
Flush and close at the end of the run. |
A plugin is definePlugin(import.meta, hooks). Its name is its package
name, read from import.meta, never a field you set. Declaration order
in vx.workspace.ts is the order everywhere: executors are consulted in
order, cache layers are chained in order, telemetry sinks receive in
order.
What fits in one hook
The proof that the seams are the right width is what has been built on
them without a special case in core:
-
turbo()from@vzn/vx-migratefills theprojectstage from aturbo.jsonand each package's scripts: a two-line workspace file, the temporary start of a migration to native config. -
@vzn/vx-lockfileusesfingerprintto claimpnpm-lock.yaml(orbun.lock,package-lock.json,yarn.lock) and key each task on its own project's dependency closure.--affectedfollows the same claim. -
@vzn/vx-schedule-historyfillsschedulewith the critical path learned from run history, andadmitwith a memory reservation packed from what each task used before. -
@vzn/vx-reapiprovides bothexecutorandcacheagainst any Bazel Remote Execution API server: remote cache and remote execution from one plugin. -
turboCache()andnxCache(), from the same package, arecachelayers speaking Turbo's/v8/artifactsand Nx's/v1/cachewire formats, so an existing self-hosted cache server keeps working. -
@vzn/vx-oteland@vzn/vx-ciaretelemetrysinks: an OTLP exporter with no OpenTelemetry SDK dependency, and a GitHub Actions job summary plus a check run on the built commit. -
@vzn/vx-mcpiscommands: a Model Context Protocol server for coding agents, a verb core does not know.
Every one of these lives in its own package and imports core only
through @vzn/vx's public façade. A test pins the façade so it cannot
widen by accident.
The rule that keeps the seams honest
Seam over special case. When core grows a branch for one consumer,
the seam is too narrow, and the fix is to widen the seam, not to keep
the branch. Twice in this repository's history a capability shipped
inside core and was moved out once the hook it needed existed: vx became
migrate@vzn/vx-migrate, and the run-history scheduler became
@vzn/vx-schedule-history on schedule. Core got smaller both times.
The second rule is the one that keeps the floor under your feet: core
applies no plugin by default and names none. A capability a plugin
must supply, a remote, a wire format, is declared in vx.workspace.ts
or it does not exist. The one thing that is implicit is the
local floor: running here and caching here.
Writing one is a short guide: Writing a vx plugin.
Originally published on the vx blog. vx is an MIT task runner and build cache for JS monorepos: vznjs.github.io/vx · GitHub.
Written with AI assistance.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.