Skip to content
GitHubRSS

Migrate

Run your Turborepo, Nx, moon, wireit or lage repo under vx today, and move its config to TypeScript at your own pace.

  1. Install: bun add -d @vzn/vx @vzn/vx-migrate.
  2. Add this vx.workspace.ts. It is the only new file.
  3. Run vx run build --all. It runs what turbo run build ran, under vx’s cache.
  4. Preview the configs with bunx @vzn/vx-migrate --dry, then write them with bunx @vzn/vx-migrate. It never overwrites a file without --force.
  5. Review each TODO(vx-migrate) comment. A package with its own vx.config.ts keeps it; turbo() fills only the rest.
vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { turbo } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [turbo()] })

examples/turbo is a Turbo repo with that vx.workspace.ts added. Every line below is what a test runs on each commit (packages/vx/tests/examples.unsafe.test.ts).

Terminal window
npm install && git init && git add -A && git commit -m init
npx vx run test --all # 3 miss: lib#build, app#build, app#test
npx vx run test --all # 3 up-to-date
bunx @vzn/vx-migrate # 3 tasks migrated clean, 0 TODOs
git add -A && git commit -m migrate
npx vx run test --all # 3 up-to-date: the written configs derive the same keys
rm vx.workspace.ts # drop turbo(); the configs stand alone
git add -A && git commit -m done
npx vx run test --all # 3 up-to-date
Turborepo (turbo.json)vx (vx.config.ts)
tasks / pipelinetasks
dependsOndependsOn, the same 'build', '^build', 'pkg#build' syntax
inputscache.inputs.files
outputscache.outputs.files
envcache.inputs.env and exec.env.passThrough
passThroughEnvexec.env.passThrough
cache: falseno cache block: the task always runs
persistent: trueexec.persistent: { … }
outputLogs"new-only" is the default; other values are the run’s --output-logs
dotEnv (Turbo 1), a .env inputcache.inputs.runtime: a probe that prints every .env file’s name and bytes, because a gitignored .env is invisible to a git glob; a root one ($TURBO_ROOT$/.env, globalDotEnv) is cache.inputs.workspaceRuntime
command (Turbo 2.11)exec.command (the argv, quoted); null or [] is no task
descriptiondescription
extendsnothing: a package task merges over the root’s, field by field
$TURBO_ROOT$/filecache.inputs.workspaceFiles / outputs.workspaceFiles
globalDependencies / globalEnv / globalPassThroughEnv (and Turbo 1’s globalDotEnv)a generated vx-preset.ts you import; a wildcard env name is reported, not mapped

The command itself comes from your package.json script, with its pre<name> / post<name> hooks folded in.

Turborepovx
turbo run buildvx run build --all
turbo run build --filter=@app/*vx run build --filter "@app/*"
turbo run build --affectedvx run build --affected
turbo run build --continuevx run build --continue (the default is deps-ok)
TURBO_TOKEN remote cacheturboCache() reads the same variables
  1. Install: bun add -d @vzn/vx @vzn/vx-migrate.
  2. Add this vx.workspace.ts. It is the only new file.
  3. Run vx run build --all. It runs what nx run-many -t build ran, under vx’s cache.
  4. Write the resolved graph: nx graph --file=.nx/workspace-data/project-graph.json. vx-migrate reads it and never guesses from nx.json.
  5. Preview the configs with bunx @vzn/vx-migrate --dry, then write them with bunx @vzn/vx-migrate.
vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { nx } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [nx()] })

Executor targets keep running as executors. Each becomes one nx-exec line, which runs the executor through Nx’s public runExecutor, with its options on the command line so the cache key sees them:

Terminal window
nx-exec @nx/js:tsc --project lib --target build --options '{"main":"src/index.ts","tsConfig":"tsconfig.lib.json"}'

Replace each with the command the executor wraps when you want to drop Nx; until the last one is gone, keep nx and @vzn/vx-migrate installed.

Nxvx
a project’s targetstasks
dependsOn (^build, app:build)dependsOn (^build, app#build)
configurationsone task per configuration: build, build:ci
inputs / namedInputscache.inputs.files
{workspaceRoot}/filecache.inputs.workspaceFiles
{ "env": "VAR" }cache.inputs.env and exec.env.passThrough
{ "runtime": "<cmd>" }cache.inputs.workspaceRuntime: it runs at the workspace root, as Nx’s does
outputscache.outputs.files (or workspaceFiles for dist/<project>)
nx build appvx run app#build
nx run app:build:productionvx run app#build:production
nx affected -t testvx run test --affected
nx graphvx run build --all --graph
nx resetnothing: there is no daemon
Nx Cloud cachenxCache() for a self-hosted Nx cache

Generators, Nx Console and module-boundary rules have no vx equivalent; keep Nx for those.

  1. Install: bun add -d @vzn/vx @vzn/vx-migrate.
  2. Add this vx.workspace.ts. It is the only new file.
  3. Run vx run build --all. It runs what moon run :build ran, under vx’s cache.
  4. Preview the configs with bunx @vzn/vx-migrate --dry, then write them with bunx @vzn/vx-migrate.
vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { moon } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [moon()] })

vx runs the projects your package manager’s workspaces list; a moon project with no package.json there is reported, not run.

moonvx
.moon/tasks.yml, .moon/tasks/*.ymlinherited as moon inherits them (by name, or inheritedBy)
command + argsexec.command
deps: ^:build, app:builddependsOn: ^build, app#build
inputs (none: every project file)cache.inputs.files
@group(sources)the file group’s entries
/tsconfig.jsoncache.inputs.workspaceFiles
$VAR inputcache.inputs.env and exec.env.passThrough
outputscache.outputs.files
options.cache: falseno cache block
local: true, preset: serverexec.persistent: {}
moon run app:buildvx run app#build
moon run :build --affectedvx run build --affected

The full table and what is not mapped: the @vzn/vx-migrate README.

  1. Install: bun add -d @vzn/vx @vzn/vx-migrate.
  2. Add this vx.workspace.ts. It is the only new file.
  3. Run vx run build --all. It runs each package’s wireit.build, under vx’s cache.
  4. Preview the configs with bunx @vzn/vx-migrate --dry, then write them with bunx @vzn/vx-migrate.
vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { wireit } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [wireit()] })
wireitvx
commandexec.command
dependencies: ../pkg:builddependsOn: pkg#build
files + outputcache.inputs.files + cache.outputs.files
env: { "external": true }cache.inputs.env and exec.env.passThrough
serviceexec.persistent (with readyWhen)
npm run buildvx run build

The full table: the @vzn/vx-migrate README.

  1. Install: bun add -d @vzn/vx @vzn/vx-migrate.
  2. Add this vx.workspace.ts. It is the only new file.
  3. Run vx run build --all. It runs what lage build ran, under vx’s cache.
  4. Preview the configs with bunx @vzn/vx-migrate --dry, then write them with bunx @vzn/vx-migrate.
vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { lage } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [lage()] })
lagevx
pipeline.build: ['^build']dependsOn: ['^build']
^^transpilea pkg#transpile edge per transitive dependency
inputs / outputscache.inputs.files / cache.outputs.files
cacheOptions.environmentGlobcache.inputs.workspaceFiles
type: 'noop'a group task
type: 'worker'a lage-worker line: the module, one process
lage build --to appvx run app#build

A target with no outputs and no cacheOptions.outputGlob runs uncached: lage would cache every package file, and vx cleans outputs before a run. The full table: the @vzn/vx-migrate README.

A root package.json that runs pnpm -r build, npm run test --workspaces or yarn workspaces foreach -t run build runs under vx with workspaceScripts():

vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { workspaceScripts } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [workspaceScripts()] })
Root scriptvx
pnpm -r --filter './packages/*' buildbuild in those packages, after ^build
pnpm -r --parallel devdev in each package, persistent, no edges
pnpm -r build && pnpm -r testtest after its package’s build
pnpm buildvx run build

Nothing is cached until a package’s vx.config.ts declares its inputs and outputs. With no fan-out scripts at all, vx init writes the configs.

  • A task always runs. vx caches only a task with a cache block. vx-migrate fills it from turbo.json, the Nx graph, .moon/, wireit scripts or lage.config.js.
  • An env var is missing in the command. vx isolates the environment: list it in exec.env.passThrough (Environment variables).
  • vx run build ran one package. Without --all, vx runs the package you are in.

Every Turborepo and Nx behaviour, spelled in vx and pinned by a test: the parity map.