DEV Community

Cover image for A pipeline with seams
VX
VX

Posted on Originally published at vznjs.github.io

A pipeline with seams

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
Enter fullscreen mode Exit fullscreen mode
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-migrate fills the project stage from a turbo.json and each package's scripts: a two-line workspace file, the temporary start of a migration to native config.
  • @vzn/vx-lockfile uses fingerprint to claim pnpm-lock.yaml (or bun.lock, package-lock.json, yarn.lock) and key each task on its own project's dependency closure. --affected follows the same claim.
  • @vzn/vx-schedule-history fills schedule with the critical path learned from run history, and admit with a memory reservation packed from what each task used before.
  • @vzn/vx-reapi provides both executor and cache against any Bazel Remote Execution API server: remote cache and remote execution from one plugin.
  • turboCache() and nxCache(), from the same package, are cache layers speaking Turbo's /v8/artifacts and Nx's /v1/cache wire formats, so an existing self-hosted cache server keeps working.
  • @vzn/vx-otel and @vzn/vx-ci are telemetry sinks: an OTLP exporter with no OpenTelemetry SDK dependency, and a GitHub Actions job summary plus a check run on the built commit.
  • @vzn/vx-mcp is commands: 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
migrate
became @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.