Skip to content

Migrate from Turborepo

vx is shaped like Turborepo on purpose, so this is the easy migration. Same per-package model, same dependsOn micro-syntax, same --filter DSL, same --affected selection, same remote-cache wire. The main change is that config moves from one turbo.json to per-package vx.config.ts files — and vx migrate writes them for you.

Terminal window
bun add -d @vzn/vx
vx migrate --dry # preview the generated files + a report
vx migrate # write them (won't overwrite without --force)

vx migrate detects your turbo.json, reads the root pipeline and any per-package extends, inlines the matching package.json scripts as task commands, and emits a vx.config.ts per package. It only emits a task where the script actually exists, and any value it can’t infer becomes a TODO(vx-migrate) comment — never a silent wrong value. Review the output, fill in the TODOs, and run.

Turborepo (turbo.json)vx (vx.config.ts)
tasks / pipelinetasks
a task’s dependsOndependsOn — identical 'build', '^build', 'pkg#build' syntax
outputscache.outputs.files
inputscache.inputs.files
envcache.inputs.env and exec.env.passThrough
passThroughEnvexec.env.passThrough
cache: falseomit the cache block (the task always runs)
persistent: trueexec.persistent: { … }
$TURBO_ROOT$/filecache.inputs.workspaceFiles / outputs.workspaceFiles
globalDependencies / globalEnv / globalPassThroughEnva generated root vx-preset.ts you import and spread

The command itself comes from your package.json script (Turborepo runs the script of the same name; vx makes the command explicit in exec).

turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**"],
"env": ["NODE_ENV"]
},
"test": { "dependsOn": ["build"], "outputs": [] }
}
}
// packages/app/vx.config.ts (generated, then reviewed)
import { defineProject } from '@vzn/vx'
export default defineProject({
tasks: {
build: {
exec: { command: 'tsc -b', env: { passThrough: ['NODE_ENV'] } },
dependsOn: ['^build'],
cache: {
inputs: { files: ['src/**', 'tsconfig.json'], env: ['NODE_ENV'] },
outputs: { files: ['dist/**'] },
},
},
test: {
dependsOn: ['build'],
exec: { command: 'bun test' },
cache: { inputs: { files: ['src/**', 'tests/**'] }, outputs: { files: [] } },
},
},
})

Note env becomes two entries: inputs.env (so a change busts the cache) and exec.env.passThrough (so the command can see it). vx isolates the child environment — Environment variables explains why.

  • Your config is TypeScript. Share presets with imports instead of copy-pasting JSON across packages — and because vx hashes the resolved config, a preset change correctly invalidates the cache.
  • No stale files after a restore. Turborepo restores additively; vx wipes declared outputs before restore, so dist/ is exactly the cached snapshot.
  • Sparse ^task bridging. ^build reaches through packages that don’t declare the task to the nearest one that does — no more no-op tasks scattered across the repo. Turborepo stops at direct deps.
  • Faster. vx’s warm, fully-cached runs lead Turborepo in the repo’s head-to-head benchmark (bun bench/compare.ts, results in Benchmarks).
Turborepovx
turbo run buildvx run build
turbo run build --filter=@app/*vx run build --filter "@app/*"
turbo run build --affectedvx run build --affected
turbo run build --dryvx run build --dry
turbo run build -- --flagvx run build -- --flag
TURBO_TOKEN / remote cacheVX_REMOTE_CACHE_URL / _TOKEN

Your remote cache server works unchanged — vx speaks the same /v8/artifacts/ wire. See Remote caching.

  • Caching is opt-in and explicit. Where Turborepo caches by default, vx requires a cache block with both inputs and outputs. This is deliberate: a forgotten cache miss costs a re-run; a stale hit ships a broken artifact. vx migrate fills these in from your turbo.json.
  • One command per task. No commands array — chain with && or split into dependsOn-linked tasks (which also lets each step cache).
  • Bun is required (≥ 1.3); there’s no Node runtime for vx itself.