Skip to content
vxvx
GitHubBlueskydev.toRSS
Releases

Affected runs that include dependents

nx() maps an Nx project graph to vx tasks with nothing written, --affected selects dependents too, and remote caches stream artifacts instead of holding them in memory.

vx 0.0.75: Affected runs that include dependents

vx 0.0.22 through 0.0.75 shipped between 2026-09-15 and 2026-09-27. The theme is adoption and correctness: nx() maps an Nx graph as it is, CI selection does what its name says, and several stale cache hits are gone. A few changes are breaking; they are listed at the end.

In this release

nx() from @vzn/vx-migrate turns every target in Nx’s resolved project graph into a vx task. You write no config. Executor targets run through nx-exec, one process per executor on Nx’s own runExecutor, and run-commands targets run as the shell commands they are. Configurations become <target>:<configuration> tasks. nx-exec starts in 214 ms against 346 ms for nx run with the daemon warm.

vx.workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
import { nx } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [nx()] })
Start one executor
nx run, daemon warm346 ms
nx-exec214 ms

PRs #685, #688, #691. Docs: Migrate from Nx. Deep dive: From Nx.

--affected used to select only the changed projects. On a branch that edited lib, a CI gate ran lib’s tests and skipped app, which depends on it. It now selects the changed projects and everything that depends on them. --filter '[<base>]' keeps the changed-only form, and the new --filter '...^<pattern>' selects a package’s dependents without the package itself.

Terminal window
vx run test --affected=origin/main

PR #446, commit c148bd6. Docs: Run and filter.

cacheRetention in vx.workspace.ts evicts local cache entries at the end of a run, by the same policy vx cache prune takes as flags. olderThan drops entries unused for that long, then maxSize drops the least recently used past the size. It does work only when something is due.

vx.workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({
cacheRetention: { olderThan: '30d', maxSize: '10G' },
})

PRs #755, #772. Docs: Workspace config.

A remote cache layer now streams an artifact between disk and the wire instead of holding it in memory. On a 150 MiB round trip, peak memory grew by 45 MiB instead of 495 MiB. Every first-party layer moved with it. If you wrote your own layer, see Breaking changes below.

// a remote cache layer's get and put, as they read today
get(hash: string): Promise<{ body: Blob | Response; durationMs: number | undefined } | null>
put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void>

PRs #756, #761.

The docs site has a “Try it” page that runs vx’s real planner in your browser, bundled from core’s own source. Edit a file, an environment variable or a vx.config.mjs, press Run, and read each task’s key, hit or miss, and why a key moved. Core’s tests hold the page’s plan to what vx run --dry=json prints.

Terminal window
vx run build --all --dry=json

PRs #789, #791, #793, #797. Page: Try it.

A bracket in cache.inputs.files, cache.outputs.files or workspaceFiles now means a bracket. Read as a character class, an input over a Next.js route like app/[id]/ keyed nothing, so an edit replayed an old output. An output over one deleted app/i/page.js and saved nothing. \[id\] still works. CACHE_VERSION moved to v28.

cache: { inputs: { files: ['app/[id]/**'] } }

PR #765.

A project config must be JSON data on every path, because the cache key folds its JSON. A function, symbol, bigint, NaN, Map, Date, cycle or undefined inside an array is refused before the schema, with the path that holds it. Before, vx watch accepted a function that vx run refused, and a Map or Date loaded on both and was keyed as {} or a string. An undefined property is still fine, and vx.workspace.ts is not held to this rule.

Terminal window
vx run build --all

PR #792.

Two bugs could replay wrong output under a green run. Bun’s xxHash3 reads only 32 bits of a seed, so the key fold carried 32 bits of state and two input sets could share a key. The fold now feeds the seed forward (CACHE_VERSION v29). And npm and Yarn link every workspace package at the root, so a sandboxed task could read its own project through node_modules without the read being reported. That read now fails the task (CACHE_VERSION v30). Each bump makes every entry miss once.

Terminal window
vx run build --all

PRs #775, #806, #812. Deep dive: The sandbox.

A watch cycle that re-reads configs now starts one config worker for the round, not one per config. With 50 configs a round went from 148 ms to 10.5 ms.

Terminal window
vx watch test --all
Watch round, 50 configs
before148 ms
after10.5 ms

PR #785.

  • A bracket in a task glob is a literal character, not a class (#765).
  • A project config value JSON cannot carry fails to load (#792).
  • A negated absolute path (!/x) in cache.inputs.files fails to load (#774).
  • A remote cache layer’s get resolves { body: Blob | Response } and put takes a Blob; a layer that returns bytes is refused (#761).

See Upgrading.

Terminal window
npm install -D @vzn/vx@latest

A standalone binary updates itself with vx upgrade.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.