@vzn/vx 0.0.82 → 0.0.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -297
- package/package.json +5 -5
- package/src/exec/executor.ts +7 -0
- package/src/orchestrator/download-policy.ts +32 -7
- package/src/orchestrator/execute-task.ts +1 -0
package/README.md
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
vx
|
|
6
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://vznjs.github.io/vx/logo-mark.svg" width="72" height="72" alt="vx logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">vx</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A fast, correct task runner and build cache for JavaScript monorepos.</strong><br>
|
|
9
|
+
One binary. No daemon, no cloud, no account.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@vzn/vx"><img src="https://img.shields.io/npm/v/@vzn/vx?color=7cb518" alt="npm"></a>
|
|
14
|
+
<a href="https://github.com/vznjs/vx/actions/workflows/ci.yml"><img src="https://github.com/vznjs/vx/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
15
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
16
|
+
<a href="https://vznjs.github.io/vx/"><img src="https://img.shields.io/badge/docs-vznjs.github.io%2Fvx-c6f84e" alt="Documentation"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<picture>
|
|
21
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://vznjs.github.io/vx/bench-dark.svg">
|
|
22
|
+
<img src="https://vznjs.github.io/vx/bench-light.svg" width="760" alt="Bar chart: vx against Turborepo and Nx on a 3,270-task graph, cold and fully cached.">
|
|
23
|
+
</picture>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<em>Same graph, same commands, same concurrency. <a href="https://vznjs.github.io/vx/benchmarks/">Methodology and how to reproduce it</a>.</em>
|
|
28
|
+
</p>
|
|
7
29
|
|
|
8
30
|
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
9
31
|
|
|
@@ -16,339 +38,169 @@ burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx; a
|
|
|
16
38
|
fully cached run replays the graph in 510 ms.
|
|
17
39
|
<!-- bench:end -->
|
|
18
40
|
|
|
19
|
-
On a real Turbo
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
On a real Turbo repo (`solidjs/solid`, its own `turbo.json`, nothing
|
|
42
|
+
rewritten) vx replays a cached build in 51 ms where Turbo takes 95,
|
|
43
|
+
restores every output in 66 ms against 127, and builds cold in 40.6 s
|
|
44
|
+
against 45.5 s ([benchmarks](packages/vx/docs/benchmarks.md)).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install -D @vzn/vx # or: pnpm add -D · yarn add -D · bun add -d
|
|
50
|
+
```
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
52
|
+
The package ships a prebuilt binary for Linux and macOS, x64 and arm64
|
|
53
|
+
(Windows via WSL); no Bun needed. The binary alone, from a
|
|
54
|
+
[release](https://github.com/vznjs/vx/releases), needs no Node either.
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
there. Remote caches, remote execution, telemetry, AI agents, learned
|
|
31
|
-
scheduling and your own verbs are plugins on documented seams, not
|
|
32
|
-
features inside: what Nx would be if it weren't a product.
|
|
56
|
+
## Quick start
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
architecture, caching, and the full CLI / config reference.
|
|
58
|
+
In any git repo with a `package.json` workspace:
|
|
36
59
|
|
|
37
60
|
```sh
|
|
38
|
-
|
|
39
|
-
npm install -g @vzn/vx # or: pnpm add -g @vzn/vx · bun add -g @vzn/vx
|
|
61
|
+
npx vx init # a vx.config.ts per package, from its scripts
|
|
40
62
|
```
|
|
41
63
|
|
|
64
|
+
Give a task a cache block (what it reads, what it writes):
|
|
65
|
+
|
|
42
66
|
```ts
|
|
43
|
-
// vx.config.ts
|
|
67
|
+
// packages/app/vx.config.ts
|
|
44
68
|
import { defineProject } from '@vzn/vx'
|
|
45
69
|
|
|
46
70
|
export default defineProject({
|
|
47
71
|
tasks: {
|
|
48
72
|
build: {
|
|
49
73
|
exec: { command: 'tsc -b' },
|
|
50
|
-
dependsOn: ['^build'],
|
|
74
|
+
dependsOn: ['^build'], // dependencies build first
|
|
51
75
|
cache: {
|
|
52
|
-
inputs: { files: ['src/**'] },
|
|
76
|
+
inputs: { files: ['src/**', 'tsconfig.json'] },
|
|
53
77
|
outputs: { files: ['dist/**'] },
|
|
54
78
|
},
|
|
55
79
|
},
|
|
56
|
-
test: {
|
|
57
|
-
exec: { command: 'bun test' },
|
|
58
|
-
dependsOn: ['build'],
|
|
59
|
-
cache: { inputs: { files: ['src/**', 'tests/**'] }, outputs: { files: [] } },
|
|
60
|
-
},
|
|
61
|
-
ci: { dependsOn: ['lint', 'test'] }, // umbrella; runs both
|
|
62
80
|
},
|
|
63
81
|
})
|
|
64
82
|
```
|
|
65
83
|
|
|
66
84
|
```sh
|
|
67
|
-
vx
|
|
68
|
-
vx run build
|
|
69
|
-
|
|
70
|
-
vx run
|
|
71
|
-
vx watch lint # re-run on file changes
|
|
72
|
-
vx run build --dry # show the plan, don't execute
|
|
85
|
+
npx vx run build --all # builds, in dependency order; stores the results
|
|
86
|
+
npx vx run build --all # nothing changed: up-to-date, in milliseconds
|
|
87
|
+
rm -rf packages/*/dist
|
|
88
|
+
npx vx run build --all # restored from the cache, not rebuilt
|
|
73
89
|
```
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
others would redo:
|
|
79
|
-
|
|
80
|
-
- **Config is code, and the cache knows it.** `vx.config.ts` is
|
|
81
|
-
evaluated before hashing, so imports, presets, and computed values
|
|
82
|
-
all participate in cache identity. Change a shared preset, and
|
|
83
|
-
exactly the right tasks re-run.
|
|
84
|
-
- **Outputs are owned.** Declared outputs are wiped before every
|
|
85
|
-
execution and every restore. Your tree ends each run bit-identical
|
|
86
|
-
to the cached snapshot — stale files cannot exist.
|
|
87
|
-
- **Hashes come from git.** On a clean tree, deriving every cache key
|
|
88
|
-
costs zero file reads, zero stats, zero database lookups. At
|
|
89
|
-
15,000 files that's a 3.2× faster warm path.
|
|
90
|
-
|
|
91
|
-
## Speed is a design discipline
|
|
92
|
-
|
|
93
|
-
Exact bitset graph algorithms for scheduling. One bulk git
|
|
94
|
-
enumeration per run, partitioned by binary search. Restores that
|
|
95
|
-
skip extraction entirely when the tree already matches. In-process
|
|
96
|
-
tar (no subprocess on the hot path). Atomic artifact publishes.
|
|
97
|
-
Single-transaction metadata writes. Every optimization is recorded
|
|
98
|
-
with the invariant that keeps it valid —
|
|
99
|
-
[`packages/vx/docs/optimizations.md`](packages/vx/docs/optimizations.md) is the ledger, and
|
|
100
|
-
[`packages/vx-bench/`](packages/vx-bench/) reproduces the numbers.
|
|
101
|
-
|
|
102
|
-
## Built for trust
|
|
103
|
-
|
|
104
|
-
- **Corruption can't go live.** A remote artifact is validated before
|
|
105
|
-
it enters the store: a zstd bomb, bytes that are not a vx archive,
|
|
106
|
-
and an archive recording another key than the one it was fetched
|
|
107
|
-
under are refused, and `@vzn/vx-reapi` re-hashes every blob against
|
|
108
|
-
its content digest; bad bytes degrade to a cache miss, never a wrong
|
|
109
|
-
hit and never a crash.
|
|
110
|
-
- **Clean exits.** SIGINT/SIGTERM reap every child process — no
|
|
111
|
-
orphaned dev servers in CI.
|
|
112
|
-
- **Readiness you can bound.** Persistent tasks gate downstream work
|
|
113
|
-
on a `readyWhen` signal; `exec.timeout` bounds any task (with
|
|
114
|
-
`--timeout` / workspace defaults), `exec.retries` + `--retry` absorb
|
|
115
|
-
flakes.
|
|
116
|
-
- **Flaky tasks are found, not guessed.** The same inputs both passing
|
|
117
|
-
and failing is the definition, and vx holds every hash and outcome
|
|
118
|
-
locally: a run names them under its footer, `--summarize` types them
|
|
119
|
-
per task, `vx info` lists them. No service.
|
|
120
|
-
- **Kernel-level sandboxing**, opt-in per task, that fails the build
|
|
121
|
-
on violation instead of hiding it.
|
|
122
|
-
|
|
123
|
-
## Reproducible graphs, when you want them
|
|
124
|
-
|
|
125
|
-
Configs are TypeScript — powerful, but a program's output can vary
|
|
126
|
-
with its environment. `vx lock` freezes the fully-resolved task graph
|
|
127
|
-
into a committed `vx-lock.json`, pnpm-style:
|
|
91
|
+
More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
|
|
92
|
+
[Configure](https://vznjs.github.io/vx/guides/configure/) ·
|
|
93
|
+
[CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md).
|
|
128
94
|
|
|
129
|
-
|
|
130
|
-
vx lock # evaluate everything once, write vx-lock.json
|
|
131
|
-
vx lock --check && vx run ci --frozen # CI: audit, then run EXACTLY that graph
|
|
132
|
-
```
|
|
95
|
+
## Already on Turborepo or Nx?
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
| `vx run` | always, live | never — local truth has no asterisks |
|
|
137
|
-
| `vx run --frozen` | never | yes, as written; refuses only a missing lock or project entry |
|
|
138
|
-
| `vx lock --check` | full graph | compares — a changed file, and env or import drift it cannot see |
|
|
139
|
-
|
|
140
|
-
`--frozen` checks nothing about staleness itself: an edited config runs
|
|
141
|
-
its locked form until `vx lock` re-writes it, and `vx lock --check` is
|
|
142
|
-
the guard that fails first. Env values read at lock time are frozen by design — cache keys become
|
|
143
|
-
reproducible across machines. Take it for that, not for speed: measured
|
|
144
|
-
on the 1,000-project bench, a plain warm run and a frozen one are a tie
|
|
145
|
-
(177 ms against 165, 2026-09-12), because the config-evaluation cache
|
|
146
|
-
already serves a pure config without evaluating it. No other runner has
|
|
147
|
-
an equivalent.
|
|
148
|
-
|
|
149
|
-
## Everything you need, nothing to configure twice
|
|
150
|
-
|
|
151
|
-
TypeScript config with real imports · task graph with `^task`
|
|
152
|
-
resolution that bridges packages without the task · multi-task runs
|
|
153
|
-
with one shared graph · pnpm-style filters and `--affected` ·
|
|
154
|
-
watch mode · `--dry` / `--graph` plans · persistent dev servers ·
|
|
155
|
-
retries, timeouts, `--continue` modes · per-layer cache control
|
|
156
|
-
(`--cache=local:r,remote:`) · `vx why` explains a re-run from the
|
|
157
|
-
persisted input fingerprints · `vx last` replays a recorded run ·
|
|
158
|
-
`vx info`,
|
|
159
|
-
`--summarize`, `--profile` Chrome traces, `--report` · `vx cache prune`
|
|
160
|
-
with TTL and size caps · `bunx @vzn/vx-migrate` from turbo.json or an Nx graph.
|
|
161
|
-
|
|
162
|
-
## A pipeline with seams — plugins decide what happens
|
|
163
|
-
|
|
164
|
-
Core is the pipeline: discover projects, evaluate configs, build the
|
|
165
|
-
task graph, derive keys, schedule, execute, cache, observe. Plugins
|
|
166
|
-
declared in `vx.workspace.ts` hook each stage, Vite-style, on one
|
|
167
|
-
`VxPlugin` object:
|
|
168
|
-
|
|
169
|
-
| Stage | Hook | A plugin can… |
|
|
170
|
-
| --------- | -------------------------- | ------------------------------------------------------------ |
|
|
171
|
-
| workspace | `config(ws, ctx)` | edit the workspace config before it is used |
|
|
172
|
-
| project | `project(config, ctx)` | add, remove or rewrite a project's tasks (keyed like yours) |
|
|
173
|
-
| graph | `graph(nodes, ctx)` | add or drop edges, mark tasks requested |
|
|
174
|
-
| key | `key(task, ctx)` | fold extra material into the cache key (named in `vx why`) |
|
|
175
|
-
| key | `fingerprint` (a claim) | claim a root file (a lockfile) and key each task on its part |
|
|
176
|
-
| schedule | `schedule(nodes, ctx)` | decide which ready task runs first |
|
|
177
|
-
| schedule | `admit(task, ctx)` | hold a ready task until something running here finishes |
|
|
178
|
-
| execute | `executor(ctx)` | decide WHERE a task's command runs (local, a REAPI worker) |
|
|
179
|
-
| store | `cache(ctx)` | decide where artifacts live (local, a shared remote) |
|
|
180
|
-
| observe | `telemetry(ctx)` / `setup` | receive every run record, or the raw event bus |
|
|
181
|
-
| run | `setup` / `teardown` | acquire before the run, release after it |
|
|
182
|
-
| cli | `commands` | add verbs to `vx` |
|
|
183
|
-
|
|
184
|
-
Core applies **no plugin by default** and names none. Running here and
|
|
185
|
-
caching here are its floor — the tail of every executor list and cache
|
|
186
|
-
chain — so a workspace with no `vx.workspace.ts` runs and caches, and a
|
|
187
|
-
plugin that declines a task hands it back to this machine. First-party
|
|
188
|
-
plugins:
|
|
189
|
-
[`@vzn/vx-reapi`](packages/vx-reapi) (Bazel Remote Execution API —
|
|
190
|
-
remote cache and remote execution against NativeLink, BuildBuddy,
|
|
191
|
-
Buildbarn or bazel-remote), [`@vzn/vx-otel`](packages/vx-otel)
|
|
192
|
-
(OpenTelemetry traces + metrics + logs, zero SDK deps),
|
|
193
|
-
[`@vzn/vx-github`](packages/vx-github) (Actions job summary + Checks
|
|
194
|
-
API), [`@vzn/vx-mcp`](packages/vx-mcp) (`vx mcp` — a read-only Model
|
|
195
|
-
Context Protocol server for AI coding agents, no SDK),
|
|
196
|
-
[`@vzn/vx-migrate`](packages/vx-migrate) (adoption in one package:
|
|
197
|
-
`turbo()` runs a Turbo repo under vx with nothing written, `bunx
|
|
198
|
-
@vzn/vx-migrate` writes configs from `turbo.json` or an Nx graph, and
|
|
199
|
-
`turboCache()` / `nxCache()` keep a remote cache speaking Turbo's
|
|
200
|
-
`/v8/artifacts` API or Nx's self-hosted spec — the wire is theirs, the
|
|
201
|
-
artifacts are vx's),
|
|
202
|
-
[`@vzn/vx-lockfile`](packages/vx-lockfile) (`pnpm()`, `bun()`, `npm()`,
|
|
203
|
-
`yarn()`: the lockfile keyed per project, so one install re-keys only the
|
|
204
|
-
projects it reaches, and `--affected` follows) and [`@vzn/vx-schedule-history`](packages/vx-schedule-history)
|
|
205
|
-
(order by learned critical path, and pack admission by what past runs
|
|
206
|
-
used). Core ships no plugin and reads no other runner's
|
|
207
|
-
format; nothing
|
|
208
|
-
distributed ships in this repo; the seams are how you build it.
|
|
97
|
+
Keep your `turbo.json` or Nx project graph. Two lines run it under vx,
|
|
98
|
+
with nothing rewritten:
|
|
209
99
|
|
|
210
|
-
|
|
100
|
+
```ts
|
|
101
|
+
// vx.workspace.ts
|
|
102
|
+
import { defineWorkspace } from '@vzn/vx'
|
|
103
|
+
import { turbo } from '@vzn/vx-migrate' // or: nx
|
|
211
104
|
|
|
212
|
-
|
|
213
|
-
| ------------------------- | ---------------------------------------------------------------- | ------------------------------ | ---------------- |
|
|
214
|
-
| Fully cached, 1,090 pkgs¹ | **510 ms** | 760 ms | 3.59 s |
|
|
215
|
-
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
|
|
216
|
-
| Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
|
|
217
|
-
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
|
|
218
|
-
| Daemon required for speed | **No** | Optional | Yes |
|
|
219
|
-
| Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
|
|
220
|
-
| Plugin API | **Yes** — executor / cache / telemetry seams | No | Yes (TS-tied) |
|
|
221
|
-
| OTel CI/CD spans | **Yes** — `otel()` plugin, zero OTel-SDK deps | No | Paid |
|
|
222
|
-
| Install | **Single binary** — npm, or one release file; no Node/Bun needed | npm + Node | npm + Node |
|
|
223
|
-
|
|
224
|
-
¹ Warm, nothing to rebuild, on the 3,270-task graph quoted at the top:
|
|
225
|
-
wall-clock, direct binaries, same machine and workspace — methodology
|
|
226
|
-
and more scenarios in
|
|
227
|
-
[`packages/vx/docs/benchmarks.md`](packages/vx/docs/benchmarks.md) § A real monorepo.
|
|
228
|
-
|
|
229
|
-
## Switching from another runner
|
|
230
|
-
|
|
231
|
-
Most projects can move in an afternoon. The mapping is mechanical:
|
|
232
|
-
|
|
233
|
-
```jsonc
|
|
234
|
-
// turbo.json (before)
|
|
235
|
-
{
|
|
236
|
-
"tasks": {
|
|
237
|
-
"build": {
|
|
238
|
-
"dependsOn": ["^build"],
|
|
239
|
-
"inputs": ["src/**"],
|
|
240
|
-
"outputs": ["dist/**"],
|
|
241
|
-
"env": ["NODE_ENV"],
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
}
|
|
105
|
+
export default defineWorkspace({ plugins: [turbo()] })
|
|
245
106
|
```
|
|
246
107
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
tasks: {
|
|
252
|
-
build: {
|
|
253
|
-
// Name the command (Turbo reads package.json scripts). The child
|
|
254
|
-
// env is ISOLATED: a cache-input env var must also be passed
|
|
255
|
-
// through, or the key would vary while the task can't see it.
|
|
256
|
-
exec: { command: 'tsc -b', env: { passThrough: ['NODE_ENV'] } },
|
|
257
|
-
dependsOn: ['^build'],
|
|
258
|
-
cache: {
|
|
259
|
-
inputs: { files: ['src/**'], env: ['NODE_ENV'] },
|
|
260
|
-
outputs: { files: ['dist/**'] },
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
})
|
|
108
|
+
When you want configs of your own, one command writes them:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
bunx @vzn/vx-migrate # vx.config.ts per package, from turbo.json or an Nx graph
|
|
265
112
|
```
|
|
266
113
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
114
|
+
`turboCache()` and `nxCache()` keep the remote cache you have (Turbo's
|
|
115
|
+
`/v8/artifacts` API, Vercel's included, or Nx's `/v1/cache`). What each
|
|
116
|
+
Turbo and Nx feature is in vx, pinned by a test:
|
|
117
|
+
[parity map](packages/vx/docs/parity.md) ·
|
|
118
|
+
[migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
119
|
+
|
|
120
|
+
> The plugin packages (`@vzn/vx-migrate` and the rest below) are not on
|
|
121
|
+
> npm yet; only `@vzn/vx` is.
|
|
122
|
+
|
|
123
|
+
## Why vx
|
|
124
|
+
|
|
125
|
+
- **A cache you can trust.** Configs are TypeScript, evaluated before
|
|
126
|
+
hashing, so imports and computed values are part of the key. Declared
|
|
127
|
+
outputs are wiped before every run and every restore: no stale file
|
|
128
|
+
survives. On a clean tree keys come from git's index, with no file
|
|
129
|
+
read.
|
|
130
|
+
- **Fast where it counts.** Milliseconds of overhead per package, no
|
|
131
|
+
daemon, one git walk per run. Every optimization is recorded with
|
|
132
|
+
the invariant that keeps it valid ([ledger](packages/vx/docs/optimizations.md)).
|
|
133
|
+
- **Sandboxed tasks.** Opt in per task: a read the task did not
|
|
134
|
+
declare fails the build instead of poisoning the cache.
|
|
135
|
+
- **Clean exits.** Ctrl-C reaps every child. No orphaned dev servers.
|
|
136
|
+
- **Plugins at every stage.** Remote cache, remote execution,
|
|
137
|
+
telemetry and new CLI verbs are plugins on documented seams, the way
|
|
138
|
+
Vite does it. Core ships none and needs none.
|
|
139
|
+
- **Free.** MIT. No paywall, no cloud, no account.
|
|
140
|
+
|
|
141
|
+
## How it compares
|
|
142
|
+
|
|
143
|
+
| | vx | Turborepo | Nx |
|
|
144
|
+
| ------------------------- | ----------------------------------------- | ------------------------------ | ---------------- |
|
|
145
|
+
| Fully cached, 1,090 pkgs¹ | **510 ms** | 760 ms | 3.59 s |
|
|
146
|
+
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
|
|
147
|
+
| Output ownership | **Strict**: wiped before exec and restore | Additive (stale files survive) | Additive |
|
|
148
|
+
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
|
|
149
|
+
| Daemon required for speed | **No** | Optional | Yes |
|
|
150
|
+
| Per-task sandbox | **Yes**: kernel-level, opt-in | No | No |
|
|
151
|
+
| Plugin API | **Yes**: every pipeline stage | No | Yes (TS-tied) |
|
|
152
|
+
| OTel CI/CD spans | **Yes**: `otel()` plugin, no OTel SDK | No | Paid |
|
|
153
|
+
| Install | **Single binary**; no Bun, Node optional | npm + Node | npm + Node |
|
|
154
|
+
|
|
155
|
+
¹ Warm, nothing to rebuild, the graph charted above. Every gap, and
|
|
156
|
+
when another tool is the better pick:
|
|
157
|
+
[comparison](packages/vx/docs/comparison.md) ·
|
|
158
|
+
[vx, Turborepo, Nx, Bazel](https://vznjs.github.io/vx/compare/).
|
|
159
|
+
|
|
160
|
+
## Plugins
|
|
161
|
+
|
|
162
|
+
Declared in `vx.workspace.ts`. Without one, vx runs and caches on this
|
|
163
|
+
machine.
|
|
164
|
+
|
|
165
|
+
| Package | What it adds |
|
|
166
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
167
|
+
| [`@vzn/vx-migrate`](packages/vx-migrate) | Run a Turbo or Nx repo unchanged; write configs; keep a Turbo or Nx cache |
|
|
168
|
+
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
|
|
169
|
+
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
|
|
170
|
+
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
|
|
171
|
+
| [`@vzn/vx-github`](packages/vx-github) | GitHub Actions job summary and Checks API |
|
|
172
|
+
| [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: a read-only MCP server for AI coding agents |
|
|
173
|
+
| [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
|
|
174
|
+
|
|
175
|
+
Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
|
|
176
|
+
[architecture](packages/vx/docs/architecture.md).
|
|
314
177
|
|
|
315
178
|
## Status
|
|
316
179
|
|
|
317
|
-
**Pre-alpha
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
| -------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
331
|
-
| Core task runner + caching | **production-ready** | dogfooded continuously; over 4,000 core tests + the package suites, green |
|
|
332
|
-
| Plugin pipeline (13 hooks, `commands` included) | **shippable** | crash-isolated, re-validated; the local executor + cache are the floor under every plugin |
|
|
333
|
-
| `vx init` / `@vzn/vx-migrate` (scripts; Turbo, Nx) | **shippable** | one config per package, TODOs where a source cannot say |
|
|
334
|
-
| REAPI remote cache + execution (`@vzn/vx-reapi`) | **shippable** | Bazel AC + CAS + Execute; NativeLink / BuildBuddy / Buildbarn / bazel-remote |
|
|
335
|
-
| OTel export (`@vzn/vx-otel`) | **shippable** | OTLP traces + metrics + logs, zero SDK deps |
|
|
336
|
-
| GitHub Actions (`@vzn/vx-github`) | **shippable** | job summary + Checks API run |
|
|
337
|
-
| MCP server (`@vzn/vx-mcp`) | **shippable** | `vx mcp` — read-only tools for AI agents, no SDK |
|
|
338
|
-
| Adoption (`@vzn/vx-migrate`) | **shippable** | `turbo()`: a `turbo.json` workspace runs with no `vx.config` written; `turboCache()` / `nxCache()`: any `/v8/artifacts` or Nx `/v1/cache` server; the migrate CLI |
|
|
339
|
-
| Lockfile keys (`@vzn/vx-lockfile`) | **shippable** | `pnpm()` `bun()` `npm()` `yarn()`: per-project dependency-closure keys; `--affected` follows |
|
|
340
|
-
|
|
341
|
-
## Development
|
|
180
|
+
**Pre-alpha**, dogfooded: this repo lints, tests and builds itself
|
|
181
|
+
with vx. Over 4,000 core tests plus the package suites, CI green on
|
|
182
|
+
every commit. The schema may still change; what 1.0 will promise is in
|
|
183
|
+
[versioning-1.0.md](packages/vx/docs/design/versioning-1.0.md).
|
|
184
|
+
|
|
185
|
+
| Surface | Maturity |
|
|
186
|
+
| -------------------------------------------------- | -------------------- |
|
|
187
|
+
| Core task runner + caching | **production-ready** |
|
|
188
|
+
| Plugin pipeline (13 hooks, `commands` included) | **shippable** |
|
|
189
|
+
| `vx init` / `@vzn/vx-migrate` (scripts; Turbo, Nx) | **shippable** |
|
|
190
|
+
| First-party plugins (table above) | **shippable** |
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
342
193
|
|
|
343
194
|
```sh
|
|
344
195
|
git clone https://github.com/vznjs/vx && cd vx
|
|
345
196
|
bun install
|
|
346
|
-
bun packages/vx/src/bin.ts run ci --all # lint
|
|
347
|
-
bun packages/vx/src/bin.ts run build --filter @vzn/vx # cross-target binaries → packages/vx/dist/
|
|
197
|
+
bun packages/vx/src/bin.ts run ci --all # lint, test, docs build: every package
|
|
348
198
|
```
|
|
349
199
|
|
|
350
|
-
|
|
200
|
+
Needs Bun ≥ 1.4. Every dev task is a vx task; there are no
|
|
201
|
+
`package.json` scripts. Start with [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
202
|
+
and [STATUS.md](packages/vx/docs/STATUS.md), the living handoff.
|
|
351
203
|
|
|
352
204
|
## License
|
|
353
205
|
|
|
354
|
-
MIT
|
|
206
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.84",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.84",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.84",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.84",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.84"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/executor.ts
CHANGED
|
@@ -139,6 +139,13 @@ export interface ExecuteRequest {
|
|
|
139
139
|
readonly timeoutMs?: number
|
|
140
140
|
readonly onStdout: (chunk: string) => void
|
|
141
141
|
readonly onStderr: (chunk: string) => void
|
|
142
|
+
/**
|
|
143
|
+
* Aborted when the run stops: Ctrl-C, SIGTERM, SIGHUP or an embedder's
|
|
144
|
+
* `RunOptions.signal`. An executor ends its work and returns; core waits
|
|
145
|
+
* for `execute` before its teardown, and a child an executor spawned is
|
|
146
|
+
* not one core can reach (item H-10).
|
|
147
|
+
*/
|
|
148
|
+
readonly signal?: AbortSignal
|
|
142
149
|
/** See `RunOptions.liveChildren`: the run's SIGINT/SIGTERM registry. */
|
|
143
150
|
readonly liveChildren?: Set<ReturnType<typeof Bun.spawn>>
|
|
144
151
|
readonly sandbox?: ExecuteSandbox
|
|
@@ -37,12 +37,36 @@ export type DownloadMode = 'eager' | 'deferred' | 'never'
|
|
|
37
37
|
export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, string> {
|
|
38
38
|
const ineligible = new Map<string, string>()
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
type Reader = { taskId: string; prefixes: string[]; via: string }
|
|
41
|
+
const readersByProject = new Map<string, Reader[]>()
|
|
42
|
+
const addReader = (n: TaskNode, prefixes: string[], via: string): void => {
|
|
43
|
+
const list = readersByProject.get(n.projectName)
|
|
44
|
+
if (list) list.push({ taskId: n.id, prefixes, via })
|
|
45
|
+
else readersByProject.set(n.projectName, [{ taskId: n.id, prefixes, via }])
|
|
46
|
+
}
|
|
47
|
+
// A task with no `cache` block keys on every file in its project, and a
|
|
48
|
+
// cached task that depends on it folds that key: deferring a producer
|
|
49
|
+
// there moved both keys with the transfer flag. Only such a task is a
|
|
50
|
+
// reader — one nothing cached folds keys nothing that lasts.
|
|
51
|
+
const folded = new Set<string>()
|
|
52
|
+
const stack = [...nodes.values()]
|
|
53
|
+
.filter((n) => n.config.cache !== undefined)
|
|
54
|
+
.flatMap((n) => n.deps)
|
|
55
|
+
for (let id = stack.pop(); id !== undefined; id = stack.pop()) {
|
|
56
|
+
if (folded.has(id)) continue
|
|
57
|
+
folded.add(id)
|
|
58
|
+
stack.push(...(nodes.get(id)?.deps ?? []))
|
|
59
|
+
}
|
|
41
60
|
let workspaceReader: string | undefined
|
|
42
61
|
let runtimeReader: string | undefined
|
|
43
62
|
for (const n of nodes.values()) {
|
|
44
63
|
const cache = n.config.cache
|
|
45
|
-
if (cache === undefined)
|
|
64
|
+
if (cache === undefined) {
|
|
65
|
+
if (folded.has(n.id) && !isGroupTask(n) && n.config.exec?.persistent === undefined) {
|
|
66
|
+
addReader(n, ['.'], 'as a task with no cache block that a cached task folds')
|
|
67
|
+
}
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
46
70
|
if ((cache.inputs?.workspaceFiles?.length ?? 0) > 0) workspaceReader ??= n.id
|
|
47
71
|
// A `runtime` input is a SHELL COMMAND whose reads are unknowable —
|
|
48
72
|
// the same reason vx refuses to infer inputs by tracing. It can `cat` a
|
|
@@ -60,10 +84,11 @@ export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, s
|
|
|
60
84
|
runtimeReader ??= n.id
|
|
61
85
|
}
|
|
62
86
|
const files = cache.inputs?.files
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
addReader(
|
|
88
|
+
n,
|
|
89
|
+
files === undefined || files.length === 0 ? ['.'] : files.map(staticPrefix),
|
|
90
|
+
'with cache.inputs.files',
|
|
91
|
+
)
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
for (const n of nodes.values()) {
|
|
@@ -101,7 +126,7 @@ export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, s
|
|
|
101
126
|
if (clash !== undefined) {
|
|
102
127
|
ineligible.set(
|
|
103
128
|
n.id,
|
|
104
|
-
`${reader.taskId} reads ${clash === '.' ? 'the whole project' : clash}
|
|
129
|
+
`${reader.taskId} reads ${clash === '.' ? 'the whole project' : clash} ${reader.via}`,
|
|
105
130
|
)
|
|
106
131
|
break
|
|
107
132
|
}
|
|
@@ -875,6 +875,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
875
875
|
onStdout: (chunk) => log.taskStdout(node, chunk),
|
|
876
876
|
onStderr: (chunk) => log.taskStderr(node, chunk),
|
|
877
877
|
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
878
|
+
...(args.stopSignal !== undefined ? { signal: args.stopSignal } : {}),
|
|
878
879
|
...(effectiveTimeout !== undefined ? { timeoutMs: effectiveTimeout } : {}),
|
|
879
880
|
...(inputs !== undefined ? { inputs } : {}),
|
|
880
881
|
...(cfgCacheable ? { cacheKey: hash } : {}),
|