A key with nothing to run
A key with nothing to run
A task can have inputs and no command. Tests that depend on it run again when those files change, and nothing extra spawns.
One command turns a Turborepo workspace into native vx config. Same dependsOn syntax, same --filter, and nothing dropped silently.
One command turns a Turborepo workspace into native vx config. Five tasks mapped here, nothing to fix by hand.
$ cat turbo.json{ "tasks": { "build": { "dependsOn": ["^build"], "inputs": ["src/**"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["build"], "inputs": ["src/**", "test/**"] }, "dev": { "cache": false, "persistent": true } }}
$ bunx @vzn/vx-migrate from turbo.json ran bun add -d @vzn/vx @vzn/vx-lockfile @vzn/vx-history tasks 5 clean files packages/types/vx.config.ts packages/ui/vx.config.ts packages/web/vx.config.ts .vx/workspace.ts .vx/.gitignore lock 3 project configs → .vx/projects.lock.json
next vx run build --all
$ vx run build test --all ⏺︎ 10ms success miss ui#build ⏺︎ 7ms success miss ui#test ⏺︎ 6ms success miss web#build$ vx run build test --all cache local 3✓ time 28msThree-package Turborepo · vx-migrate 69617eb · 2026-10-23
flowchart LR T[turbo.json] --> M[bunx @vzn/vx-migrate] P[package.json scripts] --> M M --> C[vx.config.ts per package] M --> W[.vx/workspace.ts] M --> L[.vx/projects.lock.json]
import type { ProjectConfig } from '@vzn/vx/config'
export default { tasks: { build: { exec: { command: 'bun build src/index.ts --outdir dist', }, dependsOn: ['^build'], inputs: { files: ['src/**'], }, outputs: { files: ['dist/**'], }, cache: true, }, test: { dependsOn: ['build'], }, dev: { exec: { command: 'bun --watch src/index.ts', persistent: {}, }, inputs: { files: ['**/*', '!dist/**'], }, }, },} satisfies ProjectConfigEach script becomes exec.command. A task goes where the script exists,
and to a package without it that another package’s ^ task reaches: a
key-only group with no outputs.
So an edit there still reruns its dependants. Anything it cannot map
becomes a TODO(vx-migrate) comment. It never overwrites a file without
--force.
bunx @vzn/vx init --keep writes only a .vx/workspace.ts that reads
turbo.json through turbo():
import type { WorkspaceConfig } from '@vzn/vx/config'import { turbo } from '@vzn/vx-migrate'import { npm } from '@vzn/vx-lockfile'import { vxHistory } from '@vzn/vx-history'
export default { plugins: [ turbo(), npm(), vxHistory(), ],} satisfies WorkspaceConfigWhen the build matches, bunx @vzn/vx-migrate writes the native files.
Both use the same mapper.
turbo.json |
vx.config.ts |
|---|---|
tasks / pipeline |
tasks |
dependsOn |
dependsOn, identical syntax |
inputs / outputs |
inputs.files / outputs.files |
env |
inputs.env and exec.env.passThrough |
passThroughEnv |
exec.env.passThrough |
cache: false |
omit cache: true |
persistent: true |
exec.persistent: {}, with a TODO to set readyWhen when a task depends on it |
with |
dependsOn a persistent sidecar, started beside the task |
interactive: true |
exec.interactive: true: the task gets the terminal, alone |
extends |
a package task merges over the root’s; false alone opts out, false + keys runs on those alone |
interruptible |
nothing: vx watch keeps a persistent task up and restarts it only when its config changes or it dies |
tags |
nothing: labels Turbo keeps out of the hash and the behaviour |
experimentalCI |
nothing: Turbo reads it for external CI tooling and acts on nothing |
outputLogs (Turbo 1: outputMode) |
no per-task knob: the per-run --output-logs flag |
$TURBO_ROOT$/file |
inputs.workspaceFiles |
dotEnv (Turbo 1) |
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:
**/*, which you can
narrow and the sandbox can check.turbo run web#lint build also runs web#build in Turbo. In vx,
build follows the filter and web#lint stays put.--parallel. Use --concurrency 1 or
--exclude-dependencies[=<names>].deps-ok: only the failed task’s
dependents skip. --continue=never is Turbo’s default, and bare
--continue is always, as in Turbo.Guide: Migrate from Turborepo. More at vznjs.github.io/vx.