@vzn/vx 0.0.354 → 0.0.355
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 +21 -77
- package/package.json +5 -5
- package/src/cache/git-inputs.ts +2 -1
- package/src/config.ts +3 -2
- package/src/workspace/config-schema.ts +8 -1
- package/src/workspace/package-graph.ts +21 -13
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">vx</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
9
|
-
|
|
8
|
+
<strong>The fastest task runner for JS monorepos.</strong><br>
|
|
9
|
+
Measured against Turborepo and Nx, each in its own native config.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -18,50 +18,20 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
20
20
|
|
|
21
|
-
| 1,090 packages, 3,270 tasks | vx | Turborepo
|
|
22
|
-
| -------------------------------- | ---------- |
|
|
23
|
-
| Cold build: time the runner adds | **9 s** | 1 min 35 s | 31 min |
|
|
24
|
-
| Cold build: CPU burned | **34 s** | 1 min 13 s | 114 min |
|
|
25
|
-
| Fully cached run | **476 ms** | 760 ms | 3.59 s |
|
|
26
|
-
| Overhead per package | **8 ms** | 88 ms | 1,712 ms |
|
|
21
|
+
| 1,090 packages, 3,270 tasks | vx | Turborepo | Nx |
|
|
22
|
+
| -------------------------------- | ---------- | -------------------------- | ------------------------ |
|
|
23
|
+
| Cold build: time the runner adds | **9 s** | 1 min 35 s (vx 91% faster) | 31 min (vx 99% faster) |
|
|
24
|
+
| Cold build: CPU burned | **34 s** | 1 min 13 s (vx 53% faster) | 114 min (vx 99% faster) |
|
|
25
|
+
| Fully cached run | **476 ms** | 760 ms (vx 37% faster) | 3.59 s (vx 86% faster) |
|
|
26
|
+
| Overhead per package | **8 ms** | 88 ms (vx 91% faster) | 1,712 ms (vx 99% faster) |
|
|
27
|
+
|
|
28
|
+
vx N% faster: vx takes N% less time than that tool (1 − vx ÷ theirs); N% slower: N% more (vx ÷ theirs − 1).
|
|
27
29
|
|
|
28
30
|
Time added is the wall time over the tasks' own ideal schedule (3 min 38 s).
|
|
29
31
|
Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
|
|
30
32
|
|
|
31
33
|
<!-- bench:end -->
|
|
32
34
|
|
|
33
|
-
## Tried on real repos
|
|
34
|
-
|
|
35
|
-
Public monorepos, each run with its own `turbo.json` or Nx graph against
|
|
36
|
-
the tool it ships with, same machine and tasks per row, medians.
|
|
37
|
-
|
|
38
|
-
<!-- repos:start — generated by packages/vx-bench/update-site.ts from real-repos.json; do not hand-edit -->
|
|
39
|
-
|
|
40
|
-
| Repo | Against | Cold build | Restore outputs | Nothing to do |
|
|
41
|
-
| ------------------------------------------------------------------- | ------- | --------------------- | ------------------- | ---------------------- |
|
|
42
|
-
| [solidjs/solid](https://github.com/solidjs/solid) | Turbo | 40.6 s / 45.5 s | 66 ms / 127 ms | 51 ms / 95 ms |
|
|
43
|
-
| [withastro/astro](https://github.com/withastro/astro) | Turbo | 48.0 s / 57.9 s | 478 ms / 1.43 s | 249 ms / 1.38 s |
|
|
44
|
-
| [payloadcms/payload](https://github.com/payloadcms/payload) | Turbo | 126.9 s / 127.5 s | 3.44 s / 3.46 s | 256 ms / **237 ms** |
|
|
45
|
-
| [medusajs/medusa](https://github.com/medusajs/medusa) | Turbo | 308 s / 315 s | 3.86 s / 7.16 s | 947 ms / 3.29 s |
|
|
46
|
-
| [n8n-io/n8n](https://github.com/n8n-io/n8n) | Turbo | 137.2 s / **133.7 s** | 8.27 s / 12.5 s | 842 ms / 1.36 s |
|
|
47
|
-
| [calcom/cal.com](https://github.com/calcom/cal.com) | Turbo | 245.7 s / 250.7 s | 17.4 s / 19.9 s | 14.7 s / 18.5 s |
|
|
48
|
-
| [unocss/unocss](https://github.com/unocss/unocss) | Turbo | 45.1 s / 47.3 s | 859 ms / **185 ms** | 770 ms / **126 ms** |
|
|
49
|
-
| [vueuse/vueuse](https://github.com/vueuse/vueuse) | Turbo | 21.4 s / 29.2 s | 263 ms / **128 ms** | 246 ms / **51–161 ms** |
|
|
50
|
-
| [t3-oss/create-t3-turbo](https://github.com/t3-oss/create-t3-turbo) | Turbo | 27.2 s / 29.6 s | 199 ms / 340 ms | 174 ms / 342 ms |
|
|
51
|
-
| [TanStack/query](https://github.com/TanStack/query) | Nx | 34.6 s / 37.1 s | 910 ms / 1.62 s | 291 ms / 1.60 s |
|
|
52
|
-
| [strapi/strapi](https://github.com/strapi/strapi) | Nx | 238.2 s / 245.7 s | 1.61 s / 3.94 s | 341 ms / 4.02 s |
|
|
53
|
-
| [novuhq/novu](https://github.com/novuhq/novu) | Nx | 291.2 s / 299.4 s | 3.10 s / 9.05 s | 655 ms / 8.55 s |
|
|
54
|
-
| [TanStack/router](https://github.com/TanStack/router) | Nx | 123.0 s / 138.0 s | 1.27 s / 3.82 s | 626 ms / 3.91 s |
|
|
55
|
-
| [refinedev/refine](https://github.com/refinedev/refine) | Nx | 96.2 s / 105.9 s | 1.06 s / 1.19 s | 303 ms / 1.16 s |
|
|
56
|
-
|
|
57
|
-
vx first, theirs second; **bold** marks the other tool winning. These
|
|
58
|
-
rows ran vx without a lock (every config evaluated per run); the harness
|
|
59
|
-
now runs it from a `vx lock` snapshot (`--frozen`). How each
|
|
60
|
-
repo was run, versions, dates and why a tool wins where it does:
|
|
61
|
-
[vx on real Turbo and Nx repos](https://vznjs.github.io/vx/benchmarks/real-repos/).
|
|
62
|
-
|
|
63
|
-
<!-- repos:end -->
|
|
64
|
-
|
|
65
35
|
## Why it is faster
|
|
66
36
|
|
|
67
37
|
- **Nothing boots per task.** No daemon, no Node process per task: vx
|
|
@@ -71,46 +41,20 @@ repo was run, versions, dates and why a tool wins where it does:
|
|
|
71
41
|
- **One pass over the repo.** One git walk per run; each stat and
|
|
72
42
|
lookup happens once.
|
|
73
43
|
|
|
74
|
-
## Try it on your repo
|
|
75
|
-
|
|
76
|
-
In a Turborepo or Nx workspace:
|
|
77
|
-
|
|
78
|
-
```sh
|
|
79
|
-
npm install -D @vzn/vx @vzn/vx-migrate
|
|
80
|
-
npx vx init # writes vx.workspace.ts, below; nothing else
|
|
81
|
-
npx vx run build --all # your turbo.json or Nx targets, run by vx
|
|
82
|
-
npx vx run build --all # again: up-to-date in milliseconds
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The one file `vx init` writes (in an Nx repo, `nx` for `turbo`):
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import type { WorkspaceConfig } from '@vzn/vx/config'
|
|
89
|
-
import { turbo } from '@vzn/vx-migrate'
|
|
90
|
-
|
|
91
|
-
export default { plugins: [turbo()] } satisfies WorkspaceConfig
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Nothing is rewritten. `@vzn/vx-migrate` is not on npm yet (first publish
|
|
95
|
-
pending); [`examples/turbo`](examples/turbo) runs this setup, and
|
|
96
|
-
`packages/vx/tests/try-it.unsafe.test.ts` runs these exact lines on a Turbo
|
|
97
|
-
and an Nx repo, on every commit. Which `turbo.json`, `nx.json` and `project.json` keys vx honours,
|
|
98
|
-
key by key: [Turbo and Nx config support](https://vznjs.github.io/vx/compare/turbo-nx-support/).
|
|
99
|
-
|
|
100
44
|
## Install
|
|
101
45
|
|
|
102
46
|
```sh
|
|
103
47
|
npm install -D @vzn/vx # or: pnpm add -D -w · yarn add -D (-W on Yarn 1) · bun add -d
|
|
104
48
|
```
|
|
105
49
|
|
|
106
|
-
The package ships a prebuilt binary for Linux and macOS, x64 and arm64
|
|
107
|
-
|
|
50
|
+
The package ships a prebuilt binary for Linux and macOS, x64 and arm64;
|
|
51
|
+
no Bun needed. The binary alone, from a
|
|
108
52
|
[release](https://github.com/vznjs/vx/releases), needs no Node either.
|
|
53
|
+
Windows: use WSL.
|
|
109
54
|
|
|
110
55
|
## Quick start
|
|
111
56
|
|
|
112
|
-
In a git repo with a `package.json` workspace
|
|
113
|
-
`nx.json` (those get the one file above):
|
|
57
|
+
In a git repo with a `package.json` workspace:
|
|
114
58
|
|
|
115
59
|
```sh
|
|
116
60
|
npx vx init # a vx.config.ts per package, from its scripts
|
|
@@ -151,13 +95,13 @@ runs on every commit. More: [Quickstart](https://vznjs.github.io/vx/quickstart/)
|
|
|
151
95
|
[CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
|
|
152
96
|
[playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
|
|
153
97
|
|
|
154
|
-
##
|
|
98
|
+
## Coming from Turbo or Nx
|
|
155
99
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
[
|
|
100
|
+
`bunx @vzn/vx-migrate` writes a `vx.config.ts` per package from
|
|
101
|
+
`turbo.json` or the Nx graph; `vx init` in such a repo writes a
|
|
102
|
+
`vx.workspace.ts` that reads them in place. Either is a temporary start:
|
|
103
|
+
move to native vx config, which is what the numbers above measure.
|
|
104
|
+
[Migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
161
105
|
|
|
162
106
|
## More than speed
|
|
163
107
|
|
|
@@ -197,7 +141,7 @@ machine.
|
|
|
197
141
|
|
|
198
142
|
| Package | What it adds |
|
|
199
143
|
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
200
|
-
| [`@vzn/vx-migrate`](packages/vx-migrate) |
|
|
144
|
+
| [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
|
|
201
145
|
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
|
|
202
146
|
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
|
|
203
147
|
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.355",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.355",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.355",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.355",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.355"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/git-inputs.ts
CHANGED
|
@@ -556,7 +556,8 @@ export interface BlobSizeMemo {
|
|
|
556
556
|
* the ones it lacks from one `cat-file` (65 ms over 3,000 loose objects,
|
|
557
557
|
* the whole of the cost, measured 2026-10-02). A smudged (racy) entry
|
|
558
558
|
* records 0 and is hashed from disk. An answer that cannot be read trusts
|
|
559
|
-
* nothing.
|
|
559
|
+
* nothing. A filter that keeps the size (`tr a-z A-Z`) is not caught: only
|
|
560
|
+
* a read of every trusted file would be (caching.md, step 12).
|
|
560
561
|
*/
|
|
561
562
|
async function dropResizedOids(enumeration: GitEnumeration, memo?: BlobSizeMemo): Promise<void> {
|
|
562
563
|
const { trusted, indexed } = enumeration
|
package/src/config.ts
CHANGED
|
@@ -24,8 +24,9 @@ export interface WorkspaceConfig {
|
|
|
24
24
|
cacheRetention?: { olderThan?: string; maxSize?: string }
|
|
25
25
|
/**
|
|
26
26
|
* The git ref a bare `--affected` compares with (`origin/develop`).
|
|
27
|
-
* `--affected=<base>` overrides it. Omitted → `origin/HEAD`,
|
|
28
|
-
* `
|
|
27
|
+
* `--affected=<base>` overrides it. Omitted → `origin/HEAD`, then the
|
|
28
|
+
* first of `origin/main`, `origin/master`, `main`, `master` that is not
|
|
29
|
+
* HEAD, else `HEAD~1`. Never folded into a cache key.
|
|
29
30
|
*/
|
|
30
31
|
affectedBase?: string
|
|
31
32
|
/**
|
|
@@ -282,7 +282,14 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
282
282
|
const where = `${configPath}: tasks.${name}`
|
|
283
283
|
assertTaskName(name, configPath)
|
|
284
284
|
if (!task || typeof task !== 'object') {
|
|
285
|
-
|
|
285
|
+
// package.json's shape, `name: 'command'`, was refused without the
|
|
286
|
+
// object that holds the command.
|
|
287
|
+
const raw: unknown = task
|
|
288
|
+
throw new UserError(
|
|
289
|
+
typeof raw === 'string' && raw.trim() !== ''
|
|
290
|
+
? `${where} must be an object — a command is { exec: { command: ${JSON.stringify(raw)} } }`
|
|
291
|
+
: `${where} must be an object`,
|
|
292
|
+
)
|
|
286
293
|
}
|
|
287
294
|
assertKnownFields(task, TASK_FIELDS, where)
|
|
288
295
|
const exec = (task as { exec?: unknown }).exec
|
|
@@ -60,7 +60,6 @@ export function buildPackageGraph(
|
|
|
60
60
|
// insurance against a future rule that does depend on it; it is not
|
|
61
61
|
// what the stability rests on.
|
|
62
62
|
const directDeps = new Map<string, string[]>()
|
|
63
|
-
const reachDeps = new Map<string, string[]>()
|
|
64
63
|
const order = new Map<string, Set<string>>()
|
|
65
64
|
const peers = new Map<string, Set<string>>()
|
|
66
65
|
const sorted = [...projects].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
@@ -128,22 +127,30 @@ export function buildPackageGraph(
|
|
|
128
127
|
if (!own.has(peer) && !reaches(peer, p.name)) own.add(peer)
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
|
-
for (const p of sorted)
|
|
132
|
-
const own = order.get(p.name)!
|
|
133
|
-
directDeps.set(p.name, [...own].sort())
|
|
134
|
-
reachDeps.set(p.name, [...new Set([...own, ...peers.get(p.name)!])].sort())
|
|
135
|
-
}
|
|
130
|
+
for (const p of sorted) directDeps.set(p.name, [...order.get(p.name)!].sort())
|
|
136
131
|
|
|
137
|
-
//
|
|
132
|
+
// REACH and its reverse (who declares X as a workspace dep) are read only
|
|
133
|
+
// by the transitive questions below, which an unscoped run never asks:
|
|
134
|
+
// built on the first one, as the closures are.
|
|
135
|
+
const reachDeps = new Map<string, string[]>()
|
|
138
136
|
const directDependents = new Map<string, string[]>()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
let reachBuilt = false
|
|
138
|
+
const buildReach = (): void => {
|
|
139
|
+
if (reachBuilt) return
|
|
140
|
+
reachBuilt = true
|
|
141
|
+
for (const p of sorted) {
|
|
142
|
+
const own = order.get(p.name)!
|
|
143
|
+
reachDeps.set(p.name, [...new Set([...own, ...peers.get(p.name)!])].sort())
|
|
144
|
+
}
|
|
145
|
+
for (const [name, deps] of reachDeps) {
|
|
146
|
+
for (const d of deps) {
|
|
147
|
+
const arr = directDependents.get(d)
|
|
148
|
+
if (arr) arr.push(name)
|
|
149
|
+
else directDependents.set(d, [name])
|
|
150
|
+
}
|
|
144
151
|
}
|
|
152
|
+
for (const arr of directDependents.values()) arr.sort()
|
|
145
153
|
}
|
|
146
|
-
for (const arr of directDependents.values()) arr.sort()
|
|
147
154
|
|
|
148
155
|
// Set-union DFS closures are O(P²) entries on dense layered graphs
|
|
149
156
|
// (same disease the scheduler's reachOf had — 68 ms at 1090
|
|
@@ -224,6 +231,7 @@ export function buildPackageGraph(
|
|
|
224
231
|
return (name) => {
|
|
225
232
|
const cached = memo.get(name)
|
|
226
233
|
if (cached) return cached
|
|
234
|
+
buildReach()
|
|
227
235
|
if (closures === undefined) closures = bitsetClosures(edges)
|
|
228
236
|
if (closures === null) {
|
|
229
237
|
const result = reachableFrom(name, edges)
|