Parity map for Turbo and Nx users
What a Turborepo or Nx user relies on, the vx spelling of it, and the
test that proves it. Two end-to-end suites run the real CLI on one
four-package fixture (tests/helpers/parity.ts: app → ui → lib,
app → lib, docs alone) and name each case for the upstream contract:
tests/parity-turbo.test.ts (turborepo.dev, 2.10) and
tests/parity-nx.test.ts (nx.dev, 23). They are the scan surface; the
right column names the suite that pins the behaviour in depth. A
deliberate divergence is marked ≠ and explained.
Two more suites port the upstream projects’ own cache-invalidation
tests, each case named for the Turbo or Nx test it stands in for:
tests/parity-turbo-hashing.test.ts (crates turborepo-scm,
turborepo-env, turborepo-task-hash) and tests/parity-nx-hashing.test.ts
(the native hashers and hasher/*.spec.ts).
| Turbo | vx | Deep pin |
|---|---|---|
"dependsOn": ["^build"] |
dependsOn: ['^build'] |
tests/task-graph.test.ts |
"dependsOn": ["build"] (same package) |
dependsOn: ['build'] |
tests/task-graph.test.ts |
"dependsOn": ["pkg#task"] |
dependsOn: ['pkg#task'] |
tests/dependency-spec.test.ts |
--only |
--exclude-dependencies[=<names>] |
tests/cli.test.ts |
--dry, --dry=json |
--dry, --dry=json |
tests/plan-format.test.ts, tests/plan-predict.test.ts |
--graph=file.dot |
--dry=json (each task with its deps; no DOT) |
tests/cli.test.ts |
--filter=pkg, pkg..., ...pkg, ./dir, !pkg |
the same DSL (docs/cli.md § Filter DSL) |
tests/filter.test.ts |
--filter=[ref], --affected |
--filter '[<ref>]', --affected[=<ref>]; ...[<ref>] adds dependents |
tests/affected.test.ts |
--filter + --affected intersect |
≠ they union (docs/comparison.md § Filter DSL) |
tests/filter.test.ts |
| a filter matching nothing exits 1 | the same | tests/task-selection.test.ts |
second run hits; missing outputs restored; logs replayed |
the same, with outputs cleaned before exec and restore (≠ additive) | tests/orchestrator.test.ts, tests/output-dirs.test.ts |
"inputs": [...] narrows the hash |
inputs.files (required; nothing is inferred) |
tests/inputs.test.ts, tests/task-hash.test.ts |
"inputs": ["src/"], "outputs": ["dist"] (a directory literal) |
the same: a literal is the file or its whole tree | tests/inputs-resolution.test.ts |
--filter=./apps/* (a path glob) |
--filter './apps/*', {apps/**} — over root-relative project dirs |
tests/filter.test.ts |
--affected diffs from the merge base |
the same | tests/affected.test.ts |
TURBO_CACHE=remote:rw with no remote → “Remote caching disabled” |
--cache-policy=remote:rw with no remote layer → one status line |
tests/parity-turbo.test.ts |
| a dependency’s change re-hashes dependents | upstream input keys are folded (never outputs) | tests/task-hash-derive.test.ts, tests/upstream.test.ts |
"env": ["MODE"] |
inputs.env: ['MODE'] |
tests/env.test.ts |
"passThroughEnv" |
exec.env.passThrough (reaches the command, never the hash) |
tests/env.test.ts |
"cache": false |
no cache: true (no-cache on its row) |
tests/no-cache-word.test.ts, tests/parity-turbo.test.ts |
--force |
--force (reads off, writes on) |
tests/cli.test.ts |
--cache=local:r,remote: / --no-cache (reads, no writes) |
--cache-policy=<spec> / --cache-policy=local:r,remote:r (≠ vx’s --no-cache: no reads either) |
tests/cli.test.ts, tests/download-policy.test.ts |
turbo run test -- --watch |
vx run test -- --watch (forwarded args are in the hash) |
tests/cli.test.ts |
| a lockfile change re-keys the packages whose dependencies moved | ≠ nothing without a plugin; @vzn/vx-lockfile keys the same |
tests/lockfile-claim.test.ts, packages/vx-lockfile/tests/discover.test.ts |
a package’s package.json is in its hash |
≠ under @vzn/vx-lockfile, an input of every cached task |
packages/vx-lockfile/tests/discover.test.ts |
--continue=dependencies-successful (default) |
--continue=deps-ok (default) |
tests/continue-taint.test.ts |
--continue=always |
--continue (a task built on a failure runs but is never saved) |
tests/continue-taint.test.ts |
--continue=never |
--continue=never |
tests/scheduler.test.ts |
graceful shutdown (graceful_shutdown_test.rs) |
the signal received, a 2 s grace, SIGKILL; a second one skips the grace | tests/signal-handling.test.ts |
recursive turbo run refused (recursive_turbo_test.rs) |
VX_RUN_WORKSPACE marker; vx run in its own workspace is refused |
tests/recursive-run.test.ts |
| failures are never cached | the same | tests/execute-task.test.ts |
--output-logs=errors-only, hash-only, none |
the same (full too; the default follows the run’s flow) |
tests/output-flow.test.ts |
--summarize |
vx last --format json (the run history) |
tests/parity-turbo.test.ts |
--profile |
--profile[=<path>] |
tests/run-artifacts.test.ts |
--concurrency |
--concurrency <n | n%> |
tests/scheduler.test.ts |
"persistent": true |
exec.persistent: { readyWhen } (readiness gates dependents) |
tests/persistent.test.ts, tests/dev.test.ts |
"interactive": true |
exec.interactive: true (on a TTY: the terminal, alone; never cached) |
tests/interactive.test.ts, tests/terminal.unsafe.test.ts |
turbo watch |
vx watch <task> |
tests/watch-rules.test.ts, tests/watch-loop.test.ts, tests/watch-signals.test.ts |
turbo prune |
no equivalent | — |
--summarize + Vercel Remote Cache |
turboCache() in @vzn/vx-migrate (same wire, self-hosted or Vercel) |
packages/vx-migrate/tests/turbo-cache.test.ts |
$TURBO_ROOT$/tsconfig.json input |
inputs.workspaceFiles: ['tsconfig.json'] (outputs too) |
tests/workspace-files.test.ts |
globalDependencies, globalEnv, extends |
≠ rejected: configs are TypeScript and compose by import | docs/comparison.md § Likely-worth-adding item 4 (global inputs and env), § Config schema comparison “Target defaults / inheritance” (extends) |
| Nx | vx | Deep pin |
|---|---|---|
nx run app:build |
vx run app#build |
tests/task-selection.test.ts |
nx run app:build:production |
vx run app#build:production — under nx() or the migration a configuration is a task of its own; the default configuration is the base task |
packages/vx-migrate/tests/nx.test.ts |
nx run-many -t build test |
vx run build test --all |
tests/task-selection.test.ts |
nx run-many -t lint -p app ui |
vx run lint --filter app --filter ui |
tests/filter.test.ts |
nx run-many -t lint -p tag:scope:web, project.json tags |
vx run lint --filter tag:scope:web, vx.config tags |
tests/project-tags.test.ts, tests/filter.test.ts |
--exclude docs |
--filter '!docs' |
tests/filter.test.ts |
nx affected (changed and dependents) |
--affected follows task edges (Nx 23.3’s NX_LEGACY_AFFECTED=false); --filter '...[<ref>]' is changed and dependents, [<ref>] the changed set |
tests/affected-dependents.test.ts |
nx affected --base=main |
--affected=main |
tests/affected.test.ts |
packages: ['packages/*', '!packages/fixtures'] (a negated workspace glob) |
the same: negations subtract in discovery | tests/workspace.test.ts |
nx affected with nothing changed exits 0 |
the same (nothing affected since <ref>) |
tests/affected.test.ts |
--parallel=3 |
--concurrency 3 |
tests/scheduler.test.ts |
--skipNxCache |
--force (reads off, writes on) |
|
--nxBail |
--continue=never |
tests/scheduler.test.ts |
nx show projects, nx show project app --json |
vx show, vx show app --format json |
tests/show-info.test.ts |
nx graph |
vx run <task> --dry=json (tasks and deps) |
tests/cli.test.ts |
"dependsOn": ["^build"] ({ projects: "dependencies" } deprecated) |
dependsOn: ['^build'] |
tests/task-graph.test.ts |
{ projects: ["ui"], target: "build" } |
dependsOn: ['ui#build'] |
tests/dependency-spec.test.ts |
"dependsOn": ["build-*"] (wildcards) |
dependsOn: ['check.*'], ['^build.*'] |
tests/wildcard-depends.test.ts |
| a target that only aggregates | a task with no exec — a group; its members run, it is not a task |
tests/task-graph.test.ts, tests/tally.test.ts |
{ "runtime": "node -v" } input |
inputs.runtime: ['node -v'] (workspaceRuntime at the root) |
tests/runtime-inputs.test.ts |
{ "env": "NODE_ENV" } input |
inputs.env: ['NODE_ENV'] |
tests/env.test.ts |
{workspaceRoot}/tsconfig.base.json input |
inputs.workspaceFiles: ['tsconfig.base.json'] |
tests/workspace-files.test.ts |
"outputs" restored on a hit |
outputs.files (cleaned before exec and restore, ≠ additive) |
tests/orchestrator.test.ts |
project.json / package.json changes re-hash |
the resolved config and the project package.json are in every key |
tests/config-staleness.test.ts, tests/task-hash.test.ts |
| failures are never cached | the same | tests/execute-task.test.ts |
"continuous": true |
exec.persistent: { readyWhen } |
tests/persistent.test.ts |
nx watch |
vx watch <task> |
tests/watch-rules.test.ts, tests/watch-loop.test.ts |
| Nx Cloud’s flaky-task flag | local: same key, failed after it passed → the footer, vx info |
tests/flaky.test.ts, tests/failure-mode.test.ts |
| lockfile-aware affected / hashing (the daemon’s pruned lockfile) | @vzn/vx-lockfile: per-project dependency closures, --affected follows |
packages/vx-lockfile/tests, tests/lockfile-claim.test.ts |
nx reset |
vx cache clean |
tests/cache.test.ts, tests/cli.test.ts |
namedInputs, targetDefaults, {projectRoot} tokens, .env auto-loading |
≠ rejected: TypeScript configs compose by import | docs/comparison.md § Explicitly rejected (namedInputs, targetDefaults), § Deliberate divergences › Config schema (tokens), › Env handling (.env) |
| Nx Cloud (agents, dashboard, DTE) | ≠ not in this repo: the executor / cache / telemetry seams, @vzn/vx-reapi |
packages/vx-reapi/tests |
Reading the map
Section titled “Reading the map”- Every row’s vx spelling is documented in
docs/cli.mdordocs/schema.md; the parity suite asserts the row against the real CLI, the deep pin asserts the edge cases. - The two ≠ rows that change what a command selects or leaves on
disk are decisions recorded in
docs/comparison.mdwith the reasoning: union not intersection under § Deliberate divergences, cleaned-not-additive outputs under § Where vx is ahead. - What neither runner does and vx does — the
resolved-config hash,
vx lock(runs read only the lock),vx why, restore-ahead scheduling — isdocs/comparison.md§ Where vx is ahead; the parity map lists only what a user of the other runner would look for.