Skip to content
GitHubRSS

From Turborepo: run it as it is, then migrate at your pace

vx is shaped like Turborepo on purpose. Same per-package model, same dependsOn micro-syntax ('build', '^build', 'pkg#build'), same --filter DSL, same --affected. The migration is easy because almost nothing has to change in how you think about the graph; what changes is where the config lives and what it can say.

turbo() from @vzn/vx-migrate fills vx’s project stage from your existing turbo.json and each package’s scripts. One file, and the repository runs under vx:

vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { turbo } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [turbo()] })
Terminal window
bun add -d @vzn/vx @vzn/vx-migrate # or npm / pnpm / yarn
vx run build --all

That is how solidjs/solid was benchmarked: five packages, pnpm 9, Turbo 2.10.10 as the repo’s own dependency, and vx on top of the untouched turbo.json. Both tools see the same graph and restore the same 64 output files; vx’s warm restore is 66 ms to Turbo’s 127.

Whatever the mapping cannot express becomes a warning on every run, which is the same list bunx @vzn/vx-migrate --dry prints once. A package that writes its own vx.config.ts keeps it; the plugin fills and never overwrites. So you can migrate one package at a time, or never.

Terminal window
bunx @vzn/vx-migrate --dry # preview the generated files and a report
bunx @vzn/vx-migrate # write them; never overwrites without --force

@vzn/vx-migrate is its own package so it runs before any vx file exists. It reads the root pipeline and any per-package extends, inlines the matching package.json script as the task’s command, and emits one vx.config.ts per package. It emits a task only where the script exists. Anything it cannot infer becomes a TODO(vx-migrate) comment, never a silently wrong value. It renders from the same mapper turbo() runs, so the files say exactly what the plugin was already doing.

turbo.jsonvx.config.ts
tasks / pipelinetasks
dependsOndependsOn, identical syntax
inputs / outputscache.inputs.files / cache.outputs.files
envcache.inputs.env and exec.env.passThrough
passThroughEnvexec.env.passThrough
cache: falseomit the cache block
persistent: trueexec.persistent: { readyWhen }
extendsa package task merges over the root’s; false alone opts out, false + keys runs on those alone
outputLogsno per-task knob: the per-run --output-logs flag
$TURBO_ROOT$/filecache.inputs.workspaceFiles
dotEnv (Turbo 1)cache.inputs.runtime: a probe that hashes the .env files
command (Turbo 2.11)the task’s exec.command; null is no task
descriptionthe task’s description
globalDependencies, globalEnv, globalPassThroughEnv, globalDotEnva generated vx-preset.ts you import and spread

Those are every key the mapper knows. Any other key in a task becomes a TODO naming it, so nothing is dropped silently.

Three things you get that the JSON could not give you:

  • The command is in the config. Turborepo runs the script with the task’s name; vx makes exec.command explicit. A task is one shell command, and you can read it where it is declared.
  • Inputs are required and explicit. The migration writes Turbo’s default, every file in the package, as **/*, where you can see and narrow it; the sandbox can then prove them.
  • Presets are imports. globalDependencies becomes a constant in a file every config imports, and the resolved-config hash sees it. No list to keep in sync.
  • A bare task name never widens an anchored task’s scope. In Turbo, turbo run web#lint build also runs web#build; in vx, build takes the filter scope and web#lint stays anchored.
  • No --parallel. It exists in Turbo as an escape hatch for over-declared edges. dependsOn in vx is explicit, so the hatch is --concurrency 1 to serialise and nothing to drop edges.
  • Failure propagation starts one notch further along. Turbo stops the run at the first failure; a vx run with no flag is deps-ok — a task runs when its own dependencies succeeded, and only its dependents are skipped. --continue=never is Turbo’s default behaviour, and bare --continue is always, which is what bare --continue means in Turbo too.

Every other Turbo behaviour a user would reach for is pinned by a parity case that runs vx’s real CLI against the Turbo contract it stands in for. The full guide, with before/after configs, is Migrate from Turborepo.