@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 CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">vx</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>A faster runner for your Turborepo or Nx repo.</strong><br>
9
- Keep <code>turbo.json</code> or <code>nx.json</code> and run it unchanged.
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 | Nx |
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
- (Windows via WSL); no Bun needed. The binary alone, from a
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 and no `turbo.json` or
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
- ## Configs of your own
98
+ ## Coming from Turbo or Nx
155
99
 
156
- When you want them, `bunx @vzn/vx-migrate` writes a `vx.config.ts` per
157
- package from `turbo.json` or the Nx graph. They derive the same cache
158
- keys, so what `turbo()` cached still hits. `turboCache()` and `nxCache()`
159
- keep your Turbo or Nx remote cache. [Parity map](packages/vx/docs/parity.md) ·
160
- [migration guide](https://vznjs.github.io/vx/guides/migrate/).
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) | Run a Turbo or Nx repo unchanged; write configs; keep its remote cache |
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.354",
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.354",
25
- "@vzn/vx-linux-arm64": "0.0.354",
26
- "@vzn/vx-darwin-x64": "0.0.354",
27
- "@vzn/vx-darwin-arm64": "0.0.354"
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"
@@ -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`, else
28
- * `HEAD~1`. Never folded into a cache key.
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
- throw new UserError(`${where} must be an object`)
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
- // Reverse adjacency: who declares X as a workspace dep.
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
- for (const [name, deps] of reachDeps) {
140
- for (const d of deps) {
141
- const arr = directDependents.get(d)
142
- if (arr) arr.push(name)
143
- else directDependents.set(d, [name])
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)