Skip to content
vxvx
GitHubBlueskydev.toRSS
Releases

Local dependencies keyed, outputs restored as built

Editing a file: or link: dependency, or patching one, now rebuilds what uses it. A hit brings back empty folders and symlinks as the build left them. A remote cache token lives in your keychain or comes from GitHub OIDC, warm runs are faster at scale, and Ctrl-C stops at once.

vx 0.0.642: Local dependencies keyed, outputs restored as built

A dependency that lives next to your code, such as a file: package or a patched one, now moves the key of every project that uses it. A cache hit gives back the output tree exactly as the build left it, empty folders and symlinks included. A remote cache no longer needs a token in every shell, and large workspaces start and finish sooner.

Every cached task misses once after you update, as the cache format changed. Read the breaking changes first.

Enjoy the release!

In this release

Edit a local dependency, rebuild what uses it

Section titled “Edit a local dependency, rebuild what uses it”

A dependency like "dep": "file:../vendor/dep" is copied into node_modules at install, and the lockfile records only its path. Edit the folder, reinstall, and the build reads new code while no key moves: the next run replayed output built from the old copy. Patches had the same gap. A Yarn patch added through resolutions moved no key at all, and a pnpm patch moved every key in the workspace.

Now the lockfile plugins key what each project actually installs:

  • pnpm(), bun(), npm() and yarn() hash the files of a file: or link: directory (Yarn’s portal: too), and the files its symlinks point to, into each project that reaches it.
  • yarn() links every patch: entry to the package it patches.
  • pnpm() re-keys only the projects whose dependencies reach a patched or overridden package.

vx run --affected selects the same projects.

Terminal window
$ echo 'module.exports = "v2"' > vendor/dep/index.js && bun install
$ vx run build --all
⏺︎ 7ms success miss app#build
$ vx run build --affected=HEAD --dry
would run:
▶ app#build cache miss — would exec 12eb39d0 ~10ms
affected: vendor/dep changed (workspace-wide)

See Lockfiles.

A hit could hand back a different tree from the one the build made. An empty output folder was gone. A symlink came back as a copy of its target, and a link to a folder made every save fail, so the task never hit. Files came back 0644 under a umask 077. A restore onto another disk that filled up halfway left some files new and some old.

Now the tree after a hit is the tree after the build:

  • Empty output folders come back.
  • A symlink is stored and restored as a link, with its target as written. It must be relative and stay inside the workspace, or the save fails and names it.
  • Restored files take your umask.
  • A restore onto another disk copies every file first and replaces nothing until all have landed.
build: {
exec: { command: 'mkdir -p dist/empty && cp src/out.js dist/ && ln -sf out.js dist/latest.js' },
inputs: { files: ['src/**'] },
cache: { outputs: { files: ['dist/**'] } },
},
Terminal window
$ rm -rf app/dist && vx run build --all
local 1↓
$ ls -l app/dist
drwxr-xr-x empty
lrwxrwxrwx latest.js -> out.js
-rw-r--r-- out.js

vx also kept .git, .vx and vx.lock.json out of every key and restore at any depth, so a src/.vx/ edit was a stale hit. They are now reserved only at a project’s root.

A remote cache token you type once, or never

Section titled “A remote cache token you type once, or never”

turboCache() and nxCache() needed TURBO_TOKEN in every shell, and CI needed a stored secret.

Now a laptop needs no variable. With no token set, the first run in a terminal asks once and keeps the answer in the OS keychain:

Terminal window
$ vx run build --all
Token for vercel.com (my-team), saved to the OS keychain:

On CI a missing token is one warning and a local-only run, never a failure. On GitHub Actions, turboCache({ oidc: true }) needs no stored token at all: with permissions: id-token: write it trades the job’s OIDC token for a short-lived one and revokes it when the run ends.

.vx/workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
import { turboCache } from '@vzn/vx-migrate'
export default defineWorkspace({
plugins: [turboCache({ teamSlug: 'my-team', oidc: true })],
})

A plugin of your own gets the same keychain through ctx.secret. A Turbo or Nx remote that rate-limits now waits the limit out, so its hits stay hits. See CI.

A warm run at 5,000 projects spent most of its key stage re-reading what had not changed. vx now remembers each task’s last key and reads it back while the project’s files are unchanged. It also reads cache lookups without the stored bytes, gets git’s file list sooner, and restores a missing output folder with one rename. --dry reads every task’s timings in one go.

We measured against Turbo on our bench workspace. In our run, vx was faster than Turbo on every row at 5,000 projects (cold, warm, restore, dry run, affected), and on every row at 1,000 except restore.

The run summary now prints the whole run id, the one vx last and CI logs carry:

Terminal window
info 4 workers · local cache · run 01a14b16-9bf9-71df-88a1-fc60391773bc

Ctrl-C cleared the live view straight away, then waited for every remote download in flight on a blank screen. Afterwards the summary called the downloaded tasks successes.

Now vx drops the downloads in flight, and the next run fetches them again. The view stays: a running task reads aborting with its timer, one that exits reads aborted, and the summary replaces the view. During a 30 MB download, vx used to exit after 12.5 s; now it exits after 0.04 s.

Terminal window
✗ 2.43s aborted b#build
◌ not run e#build
3.46s aborting a#build
3 aborted · 2 not run · 6 total

Free rows in the live view now show what waits on what from the first frame, instead of idle:

Terminal window
128ms running app#types blocks 3
blocked app#docs by app#types
blocked app#build by app#types

vx docs reads the reference your version wrote

Section titled “vx docs reads the reference your version wrote”

vx docs searched seven pages that shipped inside every install. Now it reads the whole reference at your version’s release tag on GitHub, so the answer matches the vx you run.

Terminal window
$ vx docs frozen lock --limit 1
── caching.md § Runtime inputs and the lock (the env parallel) · https://vznjs.github.io/vx/caching/#runtime-inputs-and-the-lock-the-env-parallel

With no network it stops with VX_E_NETWORK and names the site. Set GITHUB_TOKEN or GH_TOKEN to lift GitHub’s limit on anonymous reads.

  • A task that leaves a process writing, like tsc & vite build, keeps its result but saves no cache entry, and says to wait for it.
  • vx lock refuses a lockfile older than the manifests, and a package.json that does not parse (merge conflict markers), naming the file and the install to run.
  • vx run on a project added after the last lock says to run vx lock instead of running nothing.
  • A gitignored input named in a brace ({gen/a.txt,gen/b.txt}) is keyed, and gen/** reads untracked files beside a kept gen/.gitkeep.
  • A cache directory you cannot write still serves its hits; the run saves nothing and says so.
  • --dry=json prints an empty plan when nothing is affected.
  • The footer counts a failed save as not saved, and a scoped upload under remote ↑.
  • On macOS a sandboxed task may list the folders above its grants, as on Linux.
  • The remote token prompt stays on screen until you answer it.
  • Migrated tsc -b builds add their .tsbuildinfo to the outputs, so a hit restores the whole dist/.
  • vx-migrate runs an Nx codegen uncached when the task that writes its input was not migrated, and vx init on an Nx repo suggests a task that exists.
  • With @vzn/vx-reapi, a remote task past its timeout is cancelled on the worker and reported as timed out.
  • Cache format → every cached task misses once after upgrading.
  • A symlink under a task’s outputs → cached as a link. An absolute link, or one leading out of the workspace, fails the save.
  • .git, .vx and vx.lock.json below a project’s root → ordinary inputs and outputs.
  • vx docs → needs the network. The npm package no longer ships docs/*.md.
  • Plugin API: CacheLayer.save drops readable; restorableOutput takes a scope, 'project' or 'workspace'.

See also Upgrading.

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

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.