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.
Step zero: do not migrate
Section titled “Step zero: do not migrate”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:
import { defineWorkspace } from '@vzn/vx'import { turbo } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [turbo()] })bun add -d @vzn/vx @vzn/vx-migrate # or npm / pnpm / yarnvx run build --allThat 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.
Step one: let the tool write the files
Section titled “Step one: let the tool write the files”bunx @vzn/vx-migrate --dry # preview the generated files and a reportbunx @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.
What maps, and what is better
Section titled “What maps, and what is better”turbo.json | vx.config.ts |
|---|---|
tasks / pipeline | tasks |
dependsOn | dependsOn, identical syntax |
inputs / outputs | cache.inputs.files / cache.outputs.files |
env | cache.inputs.env and exec.env.passThrough |
passThroughEnv | exec.env.passThrough |
cache: false | omit the cache block |
persistent: true | exec.persistent: { readyWhen } |
extends | a package task merges over the root’s; false alone opts out, false + keys runs on those alone |
outputLogs | no per-task knob: the per-run --output-logs flag |
$TURBO_ROOT$/file | cache.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 |
description | the task’s description |
globalDependencies, globalEnv, globalPassThroughEnv, globalDotEnv | a 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.commandexplicit. 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.
globalDependenciesbecomes a constant in a file every config imports, and the resolved-config hash sees it. No list to keep in sync.
The deliberate divergences
Section titled “The deliberate divergences”- A bare task name never widens an anchored task’s scope. In Turbo,
turbo run web#lint buildalso runsweb#build; in vx,buildtakes the filter scope andweb#lintstays anchored. - No
--parallel. It exists in Turbo as an escape hatch for over-declared edges.dependsOnin vx is explicit, so the hatch is--concurrency 1to 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=neveris Turbo’s default behaviour, and bare--continueisalways, which is what bare--continuemeans 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.