Skip to content

Shared patterns with Turborepo and Nx

The companion to comparison.md. That doc lists what Turbo / Nx do that vx doesn’t. This one lists what we deliberately inherited or independently converged on — the parts a Turbo or Nx user should find familiar.

Every row cites the vx source file that implements the pattern; where a comment on the line names the upstream tool, that’s the explicit parity claim.

vx’s overall shape is Turborepo with three swaps and a few subtractions:

  • per-package config (Turbo’s turbo.json → vx’s vx.config.ts),
  • opt-in cache keyed by inputs + env + upstream + lockfile,
  • shell-only task contract,
  • tar artifact + a plugin-driven remote-cache layer,
  • topological scheduler with bounded parallelism.

The swaps: TypeScript config instead of JSON, resolved-config hash instead of file hash, strict output ownership instead of additive restore. Everything else lines up with Turbo or Nx by design.

The set of bytes fed into the hash mirrors Turbo’s key recipe with one Nx-borrowed extension.

ComponentTurboNxvxvx source
Task identity (project#task)yesyesyessrc/cache/cache.ts:25 (CacheKeyInput.taskId)
Resolved command + env declarationsyesyesyessrc/cache/cache.ts:33 (taskConfigHash)
Declared input file contentsyesyesyessrc/cache/cache.ts:40 (inputFiles)
Declared env-var values at runtimeyesyesyessrc/cache/cache.ts:38 (envValues)
Upstream task hashes (cascade)yesyesyessrc/cache/cache.ts:43 (upstreamHashes)
Workspace lockfile fingerprintyesyesyessrc/cache/cache.ts:49 (workspaceFingerprint)
Forwarded CLI args (after --)yesyesyessrc/cache/cache.ts:55 (forwardArgs)
Project package.json bytes (direct)via lockfileexternalDependenciesdirectsrc/cache/cache.ts:58 (“Turbo / Nx parity”)

The package.json fold is the Nx-borrowed move (Turbo gets it transitively through the lockfile; Nx’s externalDependencies does it explicitly). We fold the bytes directly so narrow cache.inputs.files like ['src/**'] doesn’t miss a package.json dep bump.

The two divergences from Turbo’s recipe — TypeScript-resolved config hash and per-task forwarded-args fold — are documented in caching.md and listed under “Where vx is ahead” in comparison.md.

PatternSourcevx source
Defer to git ls-files for tracked + untracked-but-not-ignoredTurbo + Nxsrc/cache/inputs.ts (“Turbo / Nx model”)
Git blob OIDs as per-file content hashes (index-harvested)Turbosrc/cache/inputs.ts + src/cache/cache.ts
Project-boundary enforcement (no cross-project globs)Turbo + Nxsrc/workspace/nested-dirs.ts

(vx hard-requires git — there is no fallback walker; a non-repo workspace gets a clean UserError telling the user to git init.)

The string DSL is verbatim Turbo (Nx supports the same forms plus a richer object form).

FormMeaningSourcevx source
'lint'Same project, other taskTurbo + Nxsrc/graph/dependency-spec.ts:1 (“Turbo/Nx-style”)
'^lint'Same task in workspace dependenciesTurbo + Nxsrc/graph/dependency-spec.ts
'pkg#lint'Arbitrary other package’s taskTurbo + Nxsrc/graph/dependency-spec.ts
'*' / '^*'Wildcard upstream (filter-only)Turbo’s $TURBO_DEFAULT$-adjacentsrc/orchestrator/upstream.ts:13
'!<form>'Negation (filter-only)Turbo inputs exclusionsrc/orchestrator/upstream.ts:13

Loader-side validation rejects wildcards / negation in dependsOn itself — they’re filter-only — and the error message echoes the Turbo/Nx vocabulary: src/workspace/project-loader.ts:142.

CapabilitySourcevx source
pnpm-style --filter (pkg, pkg..., ...pkg, path globs)Turbo + pnpmsrc/workspace/filter.ts
Transitive-dep expansion (pkg...)Turbo + pnpmsrc/workspace/filter.ts:11 (“Turbo-style”)
[<since>] git-relative selectionTurbosrc/workspace/affected.ts:10 (“Matches Turbo’s [<since>]”)
--affected[=<base>] subcommandTurbo + Nxsrc/workspace/affected.ts
pkg#task direct addressingTurbo + Nxsrc/cli/run.ts
CapabilitySourcevx source
Read pnpm-workspace.yamlpnpm + Turbosrc/workspace/workspace.ts
Read package.json workspaces (npm/yarn/bun)Turbosrc/workspace/workspace.ts
Build package-dep graph from workspace dependenciesTurbo + Nxsrc/workspace/package-graph.ts
Lockfile fingerprint at workspace levelTurbo + Nxsrc/workspace/fingerprint.ts
LayerTurboNxvxvx source
Local: content-addressed artifactstarball-per-hash in .turbo/cache.nx/cache SQLiteSQLite index + <hash>.tar.zst in .vx/cache/src/cache/cache.ts
Local: skip-restore when tree is currentyes (fingerprint check)yesyes — isOutputsCurrent stat check → up-to-datesrc/cache/cache.ts
Read-through then write-through layeringyesyesyessrc/cache/layered-cache.ts
Run-history table for analytics(no — --summarize JSON)(Nx Cloud)runs + invocations tables in cache.dbsrc/cache/cache.ts

The remote cache is plugin-driven (owner directive 2026-07-10; design/native-cache-wire-2026-07.md): core keeps the seams (LayeredCache + the RemoteCacheLayer interface + the cache plugin capability), and the first-party cloud plugin ships the vx-native /v1/cache/:hash wire (streaming PUT, x-vx-digest structural integrity, trust scopes). Turbo /v8/artifacts compatibility was dropped from core — a Turbo-wire cache is a third-party plugin story; the recipe lives in the extensibility guide.

Inside the tar we depart from Turbo (we don’t mimic Turbo’s .turbo/turbo-<task>-<project>.log file naming — our metadata lives in SQLite). But the on-the-wire framing stays POSIX-tar so any Turbo-aware cache server can transit our blobs unchanged.

PatternSourcevx source
POSIX ustar headers (no PAX), in-process packTurbo (Rust tar crate, Header::new_gnu())src/cache/tar.ts (hand-rolled writer)
No AppleDouble ._* companionsTurbo (in-process writer, no recurse)src/cache/tar.ts
Extract-side filter strips legacy PAX + ._*(vx-only defense-in-depth)src/cache/tar.ts
zstd compression on the wireTurboBun.zstdCompress (tar.zst artifacts)
PatternSourcevx source
Topological order, bounded parallelismTurbo + Nxsrc/graph/scheduler.ts
Cascade abort: failed task’s transitive dependents abortTurbo (mid-mode) + Nxsrc/graph/scheduler.ts
Independent siblings continue past failureTurbo --continue=continue-tasks-with-no-depssrc/graph/scheduler.ts
Persistent / long-running tasks (dev servers)Turbo persistent, Nx continuoussrc/exec/runner.ts (runPersistent) + src/orchestrator/execute-task.ts
Project-local node_modules/.bin on PATHTurbo + pnpmsrc/orchestrator/execute-task.ts
Implicit project-package.json invalidationTurbo (via lockfile) + Nx (externalDependencies)src/orchestrator/execute-task.ts:428 (“Matches Turbo and Nx’s implicit dependencies”)
PatternSourcevx source
Glob-based outputs declarationTurbo + Nxsrc/config.ts, src/cache/inputs.ts
Restore by file (tar extract, not symlink/hardlink)Turbo + Nxsrc/cache/tar.ts (extractOutputs)
Log replay on cache hitTurbo + Nxsrc/orchestrator/execute-task.ts
Wipe outputs before exec AND before restore (strict ownership)(vx-only)src/cache/inputs.ts (cleanOutputs)
Flag / behaviorSourcevx source
-- separator forwards args to taskTurbosrc/cli/run.ts
--filter DSL (pnpm shape)Turbo + pnpmsrc/workspace/filter.ts
--concurrency <n>Turbosrc/cli/run.ts
--no-cache (all off) / --force (reads off, writes on) — distinct, not synonymsTurbo-adjacentsrc/cli/run.ts
--dry / --dry=json (plan output)Turbosrc/orchestrator/plan.ts
--graph[=<path>] (DOT)Turbo + Nxsrc/cli/plan-format.ts
--summarize[=<path>] (per-run JSON)Turbosrc/orchestrator/run-artifacts.ts
--profile[=<path>] (Chrome-trace)Turbosrc/orchestrator/run-artifacts.ts
--affected[=<base>]Turbo + Nxsrc/workspace/affected.ts
watch <task> subcommandTurbo + Nxsrc/cli/watch.ts
PatternSourcevx source
Framed per-task blocks (┌─ … └─)Turbosrc/orchestrator/framed-output.ts
End-of-run summary footer (task/cache meters + time)Turbo-adjacent (vx renders meters, not a banner)src/orchestrator/summary.ts
Per-task output buffered until task finishesTurbosrc/orchestrator/logger.ts (“same as Turbo”)

(There is no >>> FULL TURBO-style all-cached banner — a fully-green cache meter carries the message.)

Sharing the patterns doesn’t mean sharing the overhead. On a 100-project × 3-task workspace (2026-05 numbers):

RunnerOverhead vs. raw shell, no cacheCache (full restore)
vx+2.95 s159 ms
Turbo+11.38 s589 ms
Nx+20.62 s858 ms

Full breakdown + methodology in benchmarks.md.

For features Turbo or Nx have that vx lacks, see comparison.md § Gaps.

For places vx made a deliberately different call (TypeScript config, resolved-config hash, strict output ownership, no executor plugins, no daemon, no TUI), see comparison.md § Where vx is ahead and README.md.

Every “Turbo” / “Nx parity” claim above is anchored in a comment in the vx source. To audit:

Terminal window
grep -rn "Turbo\|Nx parity\|turborepo" src/

That returns ~20 lines, each one a deliberate decision to mirror an upstream pattern. Adding to that list is preferable to inventing new vocabulary — vx’s value is in the swaps, not in renaming the parts we kept.