@vzn/vx-migrate 0.0.645 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -73
- package/package.json +3 -3
- package/src/adopt.ts +53 -12
- package/src/adoption-plugin.ts +9 -11
- package/src/bin.ts +21 -1
- package/src/glob-grammar.ts +82 -0
- package/src/index.ts +36 -10
- package/src/mapping-cache.ts +14 -6
- package/src/migrate-nx.ts +7 -4
- package/src/migrate-scripts.ts +10 -102
- package/src/migrate-turbo.ts +2 -6
- package/src/migrate.ts +57 -38
- package/src/nx/index.ts +19 -5
- package/src/nx/nx-inputs.ts +44 -9
- package/src/nx/nx-map.ts +65 -25
- package/src/nx/nx-outputs.ts +16 -6
- package/src/nx/nx-upstream.ts +15 -7
- package/src/nx-cache/index.ts +60 -50
- package/src/nx-exec.cjs +2 -2
- package/src/package-fs.ts +6 -1
- package/src/packages.ts +9 -7
- package/src/plugin-gaps.ts +32 -6
- package/src/remote-cache.ts +5 -1
- package/src/remote-deadline.ts +7 -1
- package/src/remote-refusal.ts +31 -0
- package/src/remote-retry.ts +9 -4
- package/src/remote-url.ts +28 -0
- package/src/script-command.ts +110 -4
- package/src/shared-outputs.ts +24 -12
- package/src/tracked-outputs.ts +11 -7
- package/src/turbo/index.ts +33 -17
- package/src/turbo/turbo-map.ts +97 -192
- package/src/turbo/turbo-reads.ts +183 -0
- package/src/turbo-cache/index.ts +261 -102
- package/src/turbo-cache/oidc.ts +52 -26
- package/src/turbo-cache/scope.ts +5 -3
- package/src/vite-task/vite-task-map.ts +11 -6
- package/src/workspace-plugins.ts +5 -2
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ Everything for adopting [`@vzn/vx`](https://vznjs.github.io/vx/) from Turborepo,
|
|
|
10
10
|
- **`turboCache()`** and **`nxCache()`** — keep the remote cache you have: any server speaking Turbo's `/v8/artifacts` API (Vercel's hosted cache included) or Nx's self-hosted `/v1/cache` spec.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
13
|
+
bun add -d @vzn/vx @vzn/vx-migrate
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
In a Turbo or Nx repo, `
|
|
16
|
+
In a Turbo or Nx repo, `bunx @vzn/vx init --keep` runs this package, which
|
|
17
17
|
writes the `.vx/workspace.ts` that declares `turbo()` or `nx()`, installs
|
|
18
18
|
what it imports and writes the lock. Where the repo shows a remote cache it
|
|
19
19
|
declares `turboCache()` or `nxCache()` too; by hand it reads:
|
|
@@ -32,9 +32,9 @@ The package exports `turbo`, `nx`, `turboCache`, `nxCache` and their options typ
|
|
|
32
32
|
|
|
33
33
|
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every package's `build` script the way `turbo run build` would: `dependsOn` edges (`^build`, same-package deps, `pkg#task`), `inputs` as every task's key and `outputs` as the cache block, `env` / `passThroughEnv`, `cache: false`, `persistent`. Turbo's global fields (`globalDependencies`, `globalEnv`, `globalPassThroughEnv`, Turbo 1's `globalDotEnv`) are inlined into every task; a `*` env name (`NEXT_PUBLIC_*`) expands over the run's environment, as Turbo matches it; `*` is Turbo's only wildcard (`\*`, a leading `\!`, `?` and `[` are literal), and a literal vx cannot key (unkey's `NEXT_PUBLIC_\*`) is left out, reported once only when the run's environment sets it; per-package `turbo.json` overlays apply. A `turbo.jsonc` (Turbo 2.5+) is read wherever a `turbo.json` would be, at the root and in a package; a workspace root with neither is refused, naming the fix. Turbo 2's framework inference (a package on `next` has `NEXT_PUBLIC_*` and `NEXT_DEPLOYMENT_ID` hashed and passed; Turbo's whole table, `expo`, `nuxt`, `remix`, `@sveltejs/kit` and the rest, each package taking the first framework it matches, as Turbo does, and leaving out names under the platform's `TURBO_CI_VENDOR_ENV_KEY`) applies too: the prefix expands over the run's environment into that package's tasks, and a task's `!` entry takes names back (`bunx @vzn/vx-migrate`, which writes files for another environment, names each framework and its packages in a note instead). What runs is what a migration would have written, minus the file: the key a task derives here equals the key the written config would derive.
|
|
34
34
|
|
|
35
|
-
| Option | Meaning
|
|
36
|
-
| ------ |
|
|
37
|
-
| `root` | Directory holding `turbo.json
|
|
35
|
+
| Option | Meaning |
|
|
36
|
+
| ------ | ----------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `root` | Directory holding `turbo.json`, relative to the workspace root or absolute. Defaults to the workspace root. |
|
|
38
38
|
|
|
39
39
|
### Locking
|
|
40
40
|
|
|
@@ -50,24 +50,25 @@ Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps eve
|
|
|
50
50
|
|
|
51
51
|
### The mapper (`mapTurboWorkspace`)
|
|
52
52
|
|
|
53
|
-
The plugin and the CLI read a repo the same way: the mapper the plugin runs live is what `bunx @vzn/vx-migrate` writes `vx.config.ts` per package from, splicing Turbo's global fields in as imports of a generated `vx-preset.ts` (its exports alone, no comments), where the plugin inlines the values; a global input another task writes to is taken back after the spread (`[...globalInputs, '!packages/plugin/dist/**']`), as the plugin takes it back from the inlined values. `splice` is
|
|
53
|
+
The plugin and the CLI read a repo the same way: the mapper the plugin runs live is what `bunx @vzn/vx-migrate` writes `vx.config.ts` per package from, splicing Turbo's global fields in as imports of a generated `vx-preset.ts` (its exports alone, no comments), where the plugin inlines the values; a global input another task writes to is taken back after the spread (`[...globalInputs, '!packages/plugin/dist/**']`), as the plugin takes it back from the inlined values. `splice` is where the two consumers meet; `uses` names which globals a task drew on. Before 2026-09-10 the mapper lived in `@vzn/vx` itself; until 2026-09-11 the plugin was its own package, `@vzn/vx-turbo`.
|
|
54
54
|
|
|
55
55
|
Rules:
|
|
56
56
|
|
|
57
57
|
- **A task exists for a package only when the package declares the script** — Turbo's own rule. An absent script is silent; a script key whose value cannot be a command (a number, `null`, an empty string, an array) produces a `task: null` entry whose todo says why, rather than a config that fails to load.
|
|
58
58
|
- **Definition order**: root `pkg#name` if there is one, else root `name` (the first replaces the second whole, as Turbo looks it up), then each package config its own `turbo.json` extends (`"extends": ["//", "shared"]`, read by Turbo 2.11, refused by 2.5; nearest last, a parent missing or a cycle refused as Turbo refuses it) and its own, each winning field by field, except an overlay array holding `$TURBO_EXTENDS$` (Turbo 2.5+), which is the inherited list plus the overlay's other entries (`inputs: ["$TURBO_EXTENDS$", "config.json"]`).
|
|
59
|
-
- **The command is the script body** with its `pre<name>` / `post<name>` hooks
|
|
59
|
+
- **The command is the script body** with its `pre<name>` / `post<name>` hooks joined in, in that order, through core's `foldScriptHooks`: each part in its own subshell, the chain stopping at the first that fails, forwarded `--` args reaching the body alone (npm and pnpm run them around `<name>` without being asked; novu's `prebuild` copies the CSS its `build` inlines), one process less per task than `pnpm run <name>` — except a body that calls yarn's `run` builtin (`run -T rollup -c`, `run clean && run build`; yarn ≥ 2 runs scripts in its own shell), which is `run: command not found` in sh: it runs as `yarn run <name>`, as Turbo and Nx run it, and yarn ≥ 2 runs no hooks, so none are joined there. Nor are they where the package's manager runs none, as for `bunx @vzn/vx-migrate`'s scripts (Yarn 2+, npm under `ignore-scripts=true`, pnpm under `enable-pre-post-scripts=false` or `enablePrePostScripts: false`; an `.npmrc` or `pnpm-workspace.yaml` change maps afresh): Turbo and Nx run `<pm> run <name>`, so the hook never ran there. A Yarn Plug'n'Play workspace (`.yarnrc.yml` with no `nodeLinker`, or `pnp`) has no `node_modules`: a dependency resolves only through `.pnp.cjs` and a bin only through yarn's shims, so there every script runs as `yarn run <name>`, and a `nodeLinker` change maps afresh. Same rules for `nx:run-script` below (strapi and novu, 2026-09-11).
|
|
60
60
|
- **Turbo 2.11's task `command`** (`futureFlags.experimentalTaskCommand`) wins over the script, as Turbo holds it: an argv is the command — each word quoted, run from the package dir, no `pre`/`post` hooks — and gives the package the task even with no script (turborepo's `@turbo/types#build`, `docs#schema`); `null` or `[]` is Turbo's no-op node, no task even where the script exists, its edges passed through; a per-toolchain map applies its `javascript` entry (or `typescript`, Turbo's alias), and without one the script runs. A shape Turbo would refuse keeps the script, with a todo.
|
|
61
|
-
- `dependsOn`: `^x` passes through when some package runs `x` or turbo.json defines it, and is dropped otherwise (core refuses a `^x` no project declares as a typo); `pkg#task` is kept only when `pkg` emits `task` (else a todo: edge dropped); a same-package task Turbo defines but the package has no script for is Turbo's no-op node, so its own edges pass through in its place (`test → codegen → ^build` with no `codegen` script is `test → ^build`); another package's `^x` or `pkg#x` reaches such a node through a group task when its edges name a task of its own package or another (with-tailwind's `ui` has no `build` script; its `build` builds `build:styles` and `build:components`, and `web#build` waits on them), and so does one whose `^` edge names another task (rallly's script-less `build` holds `^db:generate`), while one whose only edge is `^` to its own name needs none, since core's `^x` walks past a package without `x`; a no-op node nothing else reaches is a group too in a package nothing but the root depends on, since no dependant's `^` edge runs its edges there (vercel/ai's `turbo run type-check` builds each example through its script-less `type-check: [build]`, and kitchen-sink's `turbo run test` builds `@repo/ui` for `admin`, which has no `test` script); elsewhere it is none, its edges run by its dependants'; a name Turbo does not define has no edge; `$TURBO_ROOT$` deps are a todo. A task turbo.json defines and no package has a script for (documenso's `lint`, shadcn-ui's `check`) is a group task in each package that defines it, its edges kept (a `pkg#task` edge only in `pkg`), so `vx run lint` runs Turbo's no-op nodes and exits 0 as `turbo run lint` does. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed), as it names every package the package manager lists, and `//#x` in `dependsOn` is an edge to it, named by the root `package.json` `name`; a root task's globs are the workspace's (`workspaceFiles`), since Turbo hashes a root task over the whole repo; with no root project (a root `package.json` with no `name`, or one a package holds) they are a note and such an edge a todo. Turbo runs no plain task in the root package of a monorepo, and neither does `turbo()`; in a single-package repo (no workspaces, Turbo's `non-monorepo` example) turbo.json's plain tasks run on the root package, as under Turbo.
|
|
61
|
+
- `dependsOn`: `^x` passes through when some package runs `x` or turbo.json defines it, and is dropped otherwise (core refuses a `^x` no project declares as a typo); `pkg#task` is kept only when `pkg` emits `task` (else a todo: edge dropped); a same-package task Turbo defines but the package has no script for is Turbo's no-op node, so its own edges pass through in its place (`test → codegen → ^build` with no `codegen` script is `test → ^build`); another package's `^x` or `pkg#x` reaches such a node through a group task when its edges name a task of its own package or another (with-tailwind's `ui` has no `build` script; its `build` builds `build:styles` and `build:components`, and `web#build` waits on them), and so does one whose `^` edge names another task (rallly's script-less `build` holds `^db:generate`), while one whose only edge is `^` to its own name needs none, since core's `^x` walks past a package without `x`; a no-op node nothing else reaches is a group too in a package nothing but the root depends on, since no dependant's `^` edge runs its edges there (vercel/ai's `turbo run type-check` builds each example through its script-less `type-check: [build]`, and kitchen-sink's `turbo run test` builds `@repo/ui` for `admin`, which has no `test` script); elsewhere it is none, its edges run by its dependants'; a name Turbo does not define has no edge; `$TURBO_ROOT$` deps are a todo. A task turbo.json defines and no package has a script for (documenso's `lint`, shadcn-ui's `check`) is a group task in each package that defines it, its edges kept (a `pkg#task` edge only in `pkg`, a `//#task` edge in a group of the same name on the root project), so `vx run lint` runs Turbo's no-op nodes and exits 0 as `turbo run lint` does. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed), as it names every package the package manager lists, and `//#x` in `dependsOn` is an edge to it, named by the root `package.json` `name`; a root task's globs are the workspace's (`workspaceFiles`), since Turbo hashes a root task over the whole repo; with no root project (a root `package.json` with no `name`, or one a package holds) they are a note and such an edge a todo. Turbo runs no plain task in the root package of a monorepo, and neither does `turbo()`; in a single-package repo (no workspaces, Turbo's `non-monorepo` example) turbo.json's plain tasks run on the root package, as under Turbo.
|
|
62
62
|
- A transit node (Turbo's documented pattern, its with-vitest example: `transit: { dependsOn: ["^transit"] }` with no script anywhere, and `test: { dependsOn: ["transit"] }`) is a key-only task in each package that defines it: a keyed group (no command, nothing spawns) keyed on its inputs, with its `^transit` edge, so a dependant's `test` re-keys on its dependencies' sources as Turbo re-hashes it. A `^` task that some package has a script for, or that nothing depends on, is no transit node. A package without the script of a `^` task others run gets the same key-only task, as Turbo hashes a no-op node there: an edit to it re-keys its dependants. A `build` whose only edge is `^build` is not written: core gives every project with no `build` that node (solid's three script-less packages each got a `build` of their own). A dependency without the script whose own edges lack `^x` gets it too, so a `^x` stops there as Turbo's does (opencode's `build: { dependsOn: [] }`: core walked past to the builds below, which Turbo's `^build` never reaches).
|
|
63
63
|
- `with` (tasks Turbo runs alongside, `web#dev` with `api#dev`): an edge to each sidecar that is persistent, so vx starts the task once the sidecar has spawned and runs the sidecar only when the task runs; a sidecar that ends would be waited for, so it is a todo instead, and a pair that names each other keeps one edge (two would be a cycle). A task with no script whose `with` names persistent sidecars (Turbo's with-tailwind example: `ui` has no `dev` script, its `dev` starts `dev:styles` and `dev:components`) is a group task that depends on them.
|
|
64
|
-
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx
|
|
65
|
-
- `outputs`: `$TURBO_ROOT$/<path>` → `cache.outputs.workspaceFiles`; a negated output rides beside its positives and takes its paths back from the clean, the save and the restore (Next's `.next/**` minus `!.next/cache/**` keeps the cache), and one with no positive beside it takes back nothing and is dropped. Past its first segment an output takes Turbo's grammar (`dist/**/*.[cm]js`); the first stays a literal (a route directory). A wildcard `X/**` also lists `X`: Turbo's `**` matches zero segments, so medusa's `*/**` caches the package-root `tsconfig.tsbuildinfo` too (its `package.json` is taken back, below); a literal `dist/**` needs no `dist`. An output whose first segment is a wildcard (`**/*.d.ts`, medusa's `*/**`) stays cached, as in `nx()` (`turbo()` 2026-10-14): Turbo never cleans an output, vx cleans it before every run and every restore, and such a glob can reach the sources; core never cleans a committed file, and every untracked file it reaches is deleted, which `vx lock`'s warnings name (`output "*/**": … every untracked file this glob reaches is deleted`). Negations beside it (`!src/**`) keep what they name. Without git the files cannot be told apart, and the task runs uncached with a todo. A glob that reaches no tracked kind gets no warning: one segment with a literal extension the package tracks no file of (`*.xml`), `**/<dir>/**` when the package tracks nothing under a directory of that name (vercel/ai's `**/dist/**`), and a first segment no tracked top-level entry matches (tldraw's `dist-*/**`). An output that covers the package's own `package.json` (trpc's client build lists it: the build rewrites `exports`) takes the manifest back with `!package.json` and keeps the cache, in `turbo()` as in `nx()` (
|
|
64
|
+
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx hashes each dependency's key into it); from a `cache: false` producer (vercel/vercel's `//#generate:cache-keys`, which records the host) the named files, or its outputs, are a `inputs.workspaceRuntime` probe, and `inputs.tasks` leaves the producer out, so core answers the probe after it ran. Then: absent or `[]` → `**/*` (Turbo's default); `$TURBO_DEFAULT$` → `**/*`; exclusions alone narrow `**/*`; `$TURBO_ROOT$/<path>` → `inputs.workspaceFiles` (negation kept); any other `$TURBO_ROOT$` use is a todo. `globalDependencies` and Turbo 1's `globalDotEnv` land in `workspaceFiles` too (Turbo hashes `globalDependencies` apart from a task's own inputs, so neither's `!` reaches the other's files, and vx subtracts each `!` from the whole list: a `!` that may reach the other side's files is dropped, which keys more, never a stale hit; Turbo 2.11's `global.inputs` joins each task's inputs, where its `!` entries apply), and a task's Turbo 1 `dotEnv` in its `files`. Turbo 1's `$NAME` entries, in `globalDependencies` or a task's `dependsOn`, are env vars: they join `inputs.env` and `exec.env.passThrough`. A package nested in the task's package (cal.com's apps inside `@calcom/app-store`) is hashed by Turbo with its parent, and core's globs stop at a project: each glob that reaches one is also listed in `inputs.workspaceFiles`. Turbo's glob grammar is translated as Nx's is (`*.[jt]s` is `*.{[jt],j,t}s`); an input with no safe form widens to `**/*` with a todo. A wildcard `X/**` input, `!` or not, also lists `X`, as outputs do: Turbo's `**` matches zero segments, so `*/**` keys on the package-root files too. A `.env`-shaped input (`.env*`, `.env.local`, a task's Turbo 1 `dotEnv`, create-turbo's `globalDependencies: ["**/.env.*local"]`) is gitignored as a rule, so it is keyed by a probe that hashes the `.env` files it can name (`inputs.runtime`) or, for a root entry, the files its root-relative globs name, hidden ones too, as Turbo's `*` matches them (`workspaceRuntime`; `**/<name>` is a `find -name` that skips `node_modules`, and another `**` walks the workspace), rather than as a file glob git never reports. A package whose `.env` globs all sit at its root gets a one-shell probe of that directory; one below the root walks the package.
|
|
65
|
+
- `outputs`: `$TURBO_ROOT$/<path>` → `cache.outputs.workspaceFiles`; a negated output rides beside its positives and takes its paths back from the clean, the save and the restore (Next's `.next/**` minus `!.next/cache/**` keeps the cache), and one with no positive beside it takes back nothing and is dropped. Past its first segment an output takes Turbo's grammar (`dist/**/*.[cm]js`); the first stays a literal (a route directory). A wildcard `X/**` also lists `X`: Turbo's `**` matches zero segments, so medusa's `*/**` caches the package-root `tsconfig.tsbuildinfo` too (its `package.json` is taken back, below); a literal `dist/**` needs no `dist`. An output whose first segment is a wildcard (`**/*.d.ts`, medusa's `*/**`) stays cached, as in `nx()` (`turbo()` 2026-10-14): Turbo never cleans an output, vx cleans it before every run and every restore, and such a glob can reach the sources; core never cleans a committed file, and every untracked file it reaches is deleted, which `vx lock`'s warnings name (`output "*/**": … every untracked file this glob reaches is deleted`). Negations beside it (`!src/**`) keep what they name. Without git the files cannot be told apart, and the task runs uncached with a todo. A glob that reaches no tracked kind gets no warning: one segment with a literal extension the package tracks no file of (`*.xml`), `**/<dir>/**` when the package tracks nothing under a directory of that name (vercel/ai's `**/dist/**`), and a first segment no tracked top-level entry matches (tldraw's `dist-*/**`). An output that covers the package's own `package.json` (trpc's client build lists it: the build rewrites `exports`) takes the manifest back with `!package.json` and keeps the cache, in `turbo()` as in `nx()` (2026-10-03): vx would otherwise delete it before every run. The build rewrites a file it may list as an input (`@vzn/vx-lockfile` lists `package.json`), so the Turbo mapping also sets `inputStability: { strategy: 'mutable' }` and an identical rewrite counts as unchanged. So does one that covers the package's vx.config; `turbo()` and `nx()` check the config the package has, and `vx-migrate` the one it writes, so sanity's `*.js` shims stay cached beside none. `nx()` keeps a wildcard-first output cached the same way (2026-10-03). A committed file under an output (typescript-eslint's `data/sponsors.json` in a cached `data`) needs nothing: core never cleans a file git tracks, so it survives and the task keeps its cache. A task whose command runs `tsc` that writes a `.tsbuildinfo` (`-b`, `composite` or `incremental`, read through the tsconfig's `extends` and, for `-b`, its `references`) outside its outputs also outputs that file: tsc re-emits only what its buildinfo calls changed, so with `rootDir: "src"` and `outDir: "dist"` its `tsconfig.tsbuildinfo` beside the tsconfig survived the clean before a miss and the miss cached a `dist/` holding only the edited file. Nx infers the file as an output; Turbo never cleans `dist/`.
|
|
66
|
+
- A `**` glued to text in an input, output or `globalDependencies` glob is read as Turbo reads it (turborepo#5431): `src/**.ts` → `src/**/*.ts`, `gen**` → `gen*/**`. Core refuses the glued spelling.
|
|
66
67
|
- `env` / `passThroughEnv`: explicit names go to `inputs.env` (env only) and `exec.env.passThrough` (both, plus both globals); a wildcard expands over the run's environment under `turbo()`, which maps where the tasks run (a new variable name such a wildcard matches maps afresh); the CLI, which writes for another environment, lists the names the task's package and its workspace dependencies spell (a root task: the whole repo; clerk's `E2E_*`), noted once per entry, and one nothing spells is a todo; `*` is the only wildcard, as in Turbo, so `\*`, a leading `\!`, `?` and `[` are literal: unkey's `NEXT_PUBLIC_\*` names one variable, which vx cannot key, so it is dropped and reported once for the workspace with the count of tasks naming it (openstatus's root `build` env `\*` had written one todo into each of 45 configs; `turbo()`: only when the run's environment sets it, as Turbo hashes nothing otherwise); and a `!` entry (openstatus' `!NEXT_PUBLIC_VERCEL_URL`) removes the names it matches from its list, with no todo, since vx matches no name it is not given. A name both a global list and the task's own list carry is listed once. A name that is no shell variable name (`p-q`) is keyed but left out of `exec.env.passThrough`, as `sh` drops it before the task runs.
|
|
67
68
|
- Framework inference (Turbo hashes and passes Next's `NEXT_PUBLIC_*`, Vite's `VITE_*`, … for a package that depends on the framework): `turbo()` expands the prefix over the run's environment; the CLI writes the names the tracked source and `.env.example` of the package and its workspace dependencies spell (the bare prefix, as a `startsWith` test spells it, is none) (Next bundles theirs: cal.com's web, 23 alone, 58 with them), and a note asks for any only an installed dependency reads.
|
|
68
69
|
|
|
69
|
-
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — `build` keeps its cache, else the task with a `^` edge, else the first declared (typescript-eslint's `typecheck` has `^typecheck` and writes declarations into `build`'s `dist`: keeping it left `build` uncached); the rest run uncached with a todo naming the keeper and the fix, their own output path. A shared tsc incremental file (create-t3-turbo's `build` and `typecheck` both on `.cache/tsbuildinfo.json`) stays with the keeper alone, and the other task keeps its cache when it emits nothing else (a typecheck); one that emits more runs uncached, as `tsc --incremental` skips files the keeper's tsbuildinfo calls built.
|
|
70
|
-
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): `vx lock` refuses it, since such a key cannot be known before the producer ran. Each overlapping output is taken back from the reader's inputs with `!` (`['**/*', '!dist/**']`), in package and workspace inputs alike, and a workspace output another package writes into this one is taken back from its package inputs too; Turbo and Nx hash what git tracks, and a build's outputs are not, so the key reads what the adopted tool read. A literal input another task writes cannot be taken back: that task runs uncached with a todo. A committed file under such an output is still a source: a reader the `!` would hide it from runs uncached with a todo. A keyed group (an `
|
|
70
|
+
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — `build` keeps its cache, else the task with a `^` edge, else the first declared (typescript-eslint's `typecheck` has `^typecheck` and writes declarations into `build`'s `dist`: keeping it left `build` uncached); the rest run uncached with a todo naming the keeper and the fix, their own output path. A shared tsc incremental file (create-t3-turbo's `build` and `typecheck` both on `.cache/tsbuildinfo.json`) stays with the keeper alone, and the other task keeps its cache when it emits nothing else (a typecheck); one that emits more runs uncached, as `tsc --incremental` skips files the keeper's tsbuildinfo calls built. When either side is a directory at any depth (solid's build `**/dist`, its types `**/types`), the two meet only where one tree nests in the other: the keeper takes the sibling's glob back (`!coverage/**`, `!**/types`) and both keep their cache, as under Turbo. Same rule for Nx targets.
|
|
71
|
+
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): `vx lock` refuses it, since such a key cannot be known before the producer ran. Each overlapping output is taken back from the reader's inputs with `!` (`['**/*', '!dist/**']`), in package and workspace inputs alike, and a workspace output another package writes into this one is taken back from its package inputs too; Turbo and Nx hash what git tracks, and a build's outputs are not, so the key reads what the adopted tool read. A literal input another task writes cannot be taken back: that task runs uncached with a todo. A committed file under such an output is still a source: a reader the `!` would hide it from runs uncached with a todo. A keyed group (an `inputs:<name>` twin, a transit node) runs nothing, so core lets it read what a task writes: it gets no `!` for a literal, the `!` that would hide a committed file goes, and it keeps its key (typescript-eslint's ast-spec commits the `*.shot` snapshots its `test` outputs, and its twin was written as `{}`, which core refuses). An output that names one committed file (eslint's `eslint-suppressions.json` in a `lint` task's outputs) is dropped, and so is the `!` each reader got for it: the file stays in every key, as Turbo and Nx hash it, and the readers stay cached. Same for Nx targets.
|
|
71
72
|
- **Two packages' tasks on one workspace output** (cal.com's shared `post-install` writes `../../node_modules/@prisma/client/**` from every package with the script): core refuses two cached tasks on one path with no edge between them, so `turbo()` (and `nx()`) keeps the first (in package and task order) cached and runs the rest uncached, each with a todo naming the keeper. A package's own outputs count at their workspace path: typescript-eslint's root project caches `dist` and each package's typecheck `dist/packages/<name>`, one path twice. Edges across packages are not read, so an ordered pair loses its cache too.
|
|
72
73
|
- `cache: false` or `persistent: true` → no `cache` block, the task still keyed on its `inputs` and `env` as Turbo hashes it; a persistent task gets `exec.persistent: {}` and, when some task depends on it, the consumer's `persistentTodo`.
|
|
73
74
|
- A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
|
|
@@ -78,7 +79,7 @@ Rules:
|
|
|
78
79
|
- The workspace keys vx has a home for, top level or in `global`, fill what `.vx/workspace.ts` leaves unset: `run.concurrency` (`"10"`, `"50%"` of the cores) → `run.concurrency`. `TURBO_CONCURRENCY` wins over turbo.json, as under Turbo; a set one decides, its `0` included. `cacheMaxSize` / `cacheMaxAge` map to nothing: no vx run evicts, `vx cache clean --max-size / --older-than` does.
|
|
79
80
|
- `TURBO_SCM_BASE`, the base `turbo run --affected` compares with, is the run's base commit (`setBranch({ base })`): a bare `vx run build --affected` compares with the same ref. With it unset on GitHub Actions (`GITHUB_ACTIONS`), Turbo's own base applies: a pull request's `GITHUB_BASE_REF`, else the push event's `before` from `GITHUB_EVENT_PATH`.
|
|
80
81
|
- Turbo hashes each package's `microfrontends.json` (or `.jsonc`) into every task, or the one file `VC_MICROFRONTENDS_CONFIG_FILE_NAME` names; so does `turbo()`.
|
|
81
|
-
- Turbo 2.11's `global` block (`futureFlags.globalConfiguration`) is read as the `globalDependencies`, `globalEnv` and `globalPassThroughEnv` it replaces.
|
|
82
|
+
- Turbo 2.11's `global` block (`futureFlags.globalConfiguration`) is read as the `globalDependencies`, `globalEnv` and `globalPassThroughEnv` it replaces, except that a task's `!` input takes a `global.inputs` file back, as Turbo prepends them to each task's inputs.
|
|
82
83
|
- An unknown Turbo key is a todo naming it; `extends` is accepted and ignored (the overlay order above is what it means).
|
|
83
84
|
|
|
84
85
|
## `nx()` — an Nx repo, mid-migration
|
|
@@ -91,11 +92,11 @@ import { nx } from '@vzn/vx-migrate'
|
|
|
91
92
|
export default defineWorkspace({ plugins: [nx()] })
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line. Every target is keyed on its `inputs` (a target Nx does not cache on its own inputs alone, no `^` named inputs, since Nx
|
|
95
|
+
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line. Every target is keyed on its `inputs` (a target Nx does not cache on its own inputs alone, no `^` named inputs, since Nx hashes no dependency's hash into it), and a target Nx caches (`cache: true`, or the legacy `cacheableOperations` list in `nx.json`) gets its `outputs` as the cache block, with no `inputs` meaning Nx's `default` and `^default`, named inputs resolved per project (nx.json's under the project's own; one neither defines, such as an `extends` preset not installed, keys the whole project with a todo, never an empty list that keys the task on its config alone), `{workspaceRoot}` / `{projectRoot}` / `{projectName}` interpolated anywhere in a path as Nx does (`{workspaceRoot}/coverage/{projectRoot}`), Nx's glob grammar translated (`*.[jt]s` is `*.{j,t}s`; the default `production` negation `?(*.)+(spec|test).[jt]s?(x)` becomes brace sets, a narrowing only a negation may take; what has no safe form is a todo), a project fileset of negations alone (nx-recipes' `noMarkdown`) starting from every project file, as Nx reads it, and an output outside the project dir (`dist/<project>` at the workspace root, Nx's default layout) a `workspaceFiles` output, an output outside the workspace (an old generator's `reportsDirectory: "../../coverage/<lib>"`) a todo and dropped, as vx caches only inside it, an output whose first segment is a wildcard (`{projectRoot}/**/*.d.ts`) cached, with the committed files it reaches kept (core never cleans one) and the rest cleaned before the run, and an output that covers the project's own `package.json` or vx.config cached with the file taken back by `!`; `dependsOn` becomes the edges (`^build` — dropped when no project has the target, as Nx gives it no edges and core refuses a `^name` nothing declares — `build`, `project:target` → `project#target`, everything past `project:` one target name, as Nx joins it (`ui:build:esm` is ui's `build:esm`; `ui:build:ci` names target `build:ci`, never `build`'s `ci` configuration, and is no edge where ui lacks it), this project's own target winning over a project name, `{ target, projects }` → each matched project's task, `projects` read as Nx's `findMatchingProjects` reads it: a name or a list of names, globs over names and over project directories (`libs/*`), `name:` / `tag:` / `directory:` labels, a bare word as a word in a name, and `!` exclusions; an edge to a target its project lacks is dropped without a word, as Nx drops it — `targetDefaults` that give every `typecheck` a `codegen` one project has; `^name` is every dependency's `name` whatever it holds, `^rsbuild:typecheck` included; a target glob — `test:e2e--*`, `^build-*`, `ui:build-{esm,cjs}`, a `{ target }` object's — expands over every target name in the workspace, as Nx 19.5+ does, before those rules; a same-project glob keeps its own project's matches), `{ env }` inputs pass through (one that is no shell variable name is keyed only), `{ runtime }` inputs run at the workspace root as Nx runs them (`inputs.workspaceRuntime`), with `{workspaceRoot}`, `{projectRoot}` and `{projectName}` written in as the paths and name they stand for (Nx runs the command as written, so `cat {workspaceRoot}/v.txt` keyed on cat's error there), and a command that exits non-zero keys its error and exit code and the task runs on, as Nx hashes its output whatever it exits (`git describe` in a repo with no tags): the written probe is `(<command>) || echo "exit $?"`, a `{ fileset, includeIgnored: true }` literal (Nx 23 hashes it from disk, gitignored or missing) is read by such a probe, since vx's globs see only what git lists, and a glob of one is a todo, a target that says `continuous` — or, in a graph from an Nx older than that field, runs a server executor (`@nx/vite:dev-server`, `@nx/next:server`, …) — is a persistent task and never cached, ready on spawn as Nx has it (Nx starts the dependents once it has started; a run-commands `readyWhen` gates them, below). The target's name never decides: a cached `dev` caches like any other target (nx#32610). `parallelism: false` (Nx runs the target alone) has no vx form and is a todo naming `--concurrency 1`; `syncGenerators` (Nx runs them first) is one workspace note per generator list, counting its tasks and naming `nx sync`; nx.json's `sync.globalGenerators` is one note per run naming `nx sync`. A target with `configurations` is one task per configuration (one whose name another target holds, `vite`'s `build` beside an inferred `vite:build`, is not written, with a todo, as Nx resolves `a:vite:build` to the target): `build` carries the default configuration's options, `build:ci` the `ci` one, and an edge of `build:ci` (own, named or `^`) reaches each target's `ci` task where it declares one, else its default, as Nx's `resolveConfiguration` does; a `^build` there becomes one edge per dependency Nx links. A `^name` input (`^production`, `{ input, dependencies: true }`, the pre-17 `{ input, projects: "dependencies" }`; `projects: "self"` is the own input, as Nx 23 still reads both) is what Nx hashes: each dependency's `name`, over the project graph, whether or not a task edge exists. A `{ input, projects }` list is read as `dependsOn`'s `projects` is; with `dependencies: true` beside it, both the listed projects' dependencies' input (Nx 24's reading) and this project's `^input` (Nx 23's) are keyed. Each project some reader reaches gets an `inputs:<name>` task — a keyed group (no command, nothing spawns, not counted in the run summary) keyed on its own `name` input, depending on its Nx dependencies' twins (a project nothing reaches gets none) — and a task that reads `^name` depends on its direct dependencies' twins, so the whole closure goes into its key with each project hashed once. A dependency FILESET (`^{projectRoot}/tsconfig.lib.json`, `{ fileset, dependencies: true }`, which Nx's own inferred `typecheck` writes) is keyed the same way: its twin, `inputs:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins spawn nothing and stay out of a run's output and task count, so `vx run build` counts the tasks `nx run-many -t build` does. A path input inside an output one of the project's own targets declares (TanStack/table's `public` input lists `{projectRoot}/dist`) is generated, and gitignored where Nx caches it: Nx's file map skips gitignored files, so Nx hashes nothing there (a changed `dist` file is a cache hit under Nx 23.3), and it is dropped without a todo, and the task that writes it keys its dependants through `dependsOn`.
|
|
95
96
|
|
|
96
97
|
| Option | Meaning |
|
|
97
98
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
98
|
-
| `root` | Directory holding `nx.json
|
|
99
|
+
| `root` | Directory holding `nx.json`, relative to the workspace root or absolute. Defaults to the workspace root. |
|
|
99
100
|
| `graph` | An exported graph (`nx graph --file=<path>`) to read instead of keeping a snapshot; with it set the plugin never runs `nx`. Relative to `root`, or absolute. |
|
|
100
101
|
|
|
101
102
|
### Module boundaries
|
|
@@ -147,13 +148,13 @@ Nx loads a task's `.env` files into its environment — the project's before the
|
|
|
147
148
|
|
|
148
149
|
### The target a task sees
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
A task whose command still runs Nx or Lerna (`nx-exec`, `nx-env`, `nx exec`, `lerna`) or names one of them gets `NX_TASK_TARGET_PROJECT`, `NX_TASK_TARGET_TARGET`, `LERNA_PACKAGE_NAME` (the project, which Lerna on Nx's runner documents to scripts) and, where it runs a configuration, `NX_TASK_TARGET_CONFIGURATION` in `exec.env.define`, as Nx hands them to every task; a run-commands `env` wins. Any other task gets none. Nx's own `npm:public` / `npm:private` tags are not written. A package script's `nx exec -- <cmd>` reads them: without them it starts Nx's own task runner, which runs the target and its dependencies again.
|
|
151
152
|
|
|
152
153
|
### Inputs that key nothing here
|
|
153
154
|
|
|
154
155
|
Nx hashes the root TypeScript config (`tsconfig.base.json`, else `tsconfig.json`) into every task, so every mapped task lists it in `inputs.workspaceFiles`: a `compilerOptions` edit re-runs it, as under Nx. vx keys the whole file, where Nx leaves other projects' `paths` entries out, so a `paths` edit re-runs every task.
|
|
155
156
|
|
|
156
|
-
`{ dependentTasksOutputFiles }` hashes the outputs of the tasks a target depends on; vx
|
|
157
|
+
`{ dependentTasksOutputFiles }` hashes the outputs of the tasks a target depends on; vx hashes those tasks' keys into it through `dependsOn`, and an output follows from its inputs. A target that reads them with no `dependsOn` edge left to a migrated task (its producer is a project vx did not migrate, such as a .NET project) is written uncached, with a todo naming the producer: its key would see nothing it reads. `{ workingDirectory }` hashes the directory Nx was started from; a vx task runs in its project dir from wherever vx is started. Neither is a todo.
|
|
157
158
|
|
|
158
159
|
### The environment a task sees
|
|
159
160
|
|
|
@@ -185,7 +186,7 @@ An Nx project's `tags` are its vx `tags`, so `vx run build --filter tag:scope:we
|
|
|
185
186
|
|
|
186
187
|
### The graph snapshot
|
|
187
188
|
|
|
188
|
-
Once per run the plugin keys its snapshot (`<cache dir>/nx-project-graph.json`) on what Nx computes the graph from: `nx.json` and the files its `extends` chain names by content, and the worktree as git sees it — `HEAD`, `git status -uall` (core's own for the run, `DiscoverContext.worktreeChanges`: the tree is walked once, which took ~96 ms per run on refine; a `root` elsewhere asks git itself), and the content of every listed path under a project root (the last graph's too, so a project `discover` names counts before it is one), any `project.json
|
|
189
|
+
Once per run the plugin keys its snapshot (`<cache dir>/nx-project-graph.json`) on what Nx computes the graph from: `nx.json` and the files its `extends` chain names by content, and the worktree as git sees it — `HEAD`, `git status -uall` (core's own for the run, `DiscoverContext.worktreeChanges`: the tree is walked once, which took ~96 ms per run on refine; a `root` elsewhere asks git itself), and the content of every listed path under a project root (the last graph's too, so a project `discover` names counts before it is one), any `project.json` or `.gitignore` (Nx's project walk honours each one), or at the root (manifests, lockfiles, `tsconfig*.json`). A different key, or no snapshot, runs `node_modules/.bin/nx graph --file=<snapshot>` (or, where the root links no `nx`, a Lerna repo on pnpm, the `nx` Lerna depends on, which `lerna run` runs) — the one time Nx itself runs, served from the daemon when one is up. So a source edit that adds a cross-package `import` (an edge Nx derives) or a config an Nx plugin infers targets from (`vite.config.ts`) re-exports before the run, tracked or untracked, and a touch without an edit, or a stray file a task writes at the root, does not. Outside a git worktree the plugin falls back to the manifests' mtimes. The snapshot and its key live in the cache dir, which ignores itself (a `*` `.gitignore` inside it), so writing them moves nothing. Nx's own caches (`.nx/cache/`, `.nx/workspace-data/`, which the export writes) never enter the key: under a root project (a standalone repo) that does not ignore them, each export re-exported on the next run. An export that fails with a snapshot in hand warns and runs on the previous graph. The snapshot is machine-local, like the cache: nothing about it enters a key. Measured at 1,000 projects: the export runs once (1.5 s with the daemon off after an edit, 0.9 s with it on; Nx's own computation), and a warm run with nothing changed pays the key, 43 ms at min where the manifest stats it replaced took 9 (item 1075), plus the mapping. Under `vx watch` the same rule runs per cycle: a `project.json` edit is the next cycle's tasks, the export included — 1.3 s from the edit to the new command's effect on that workspace. The mapping itself is kept beside it (`<cache dir>/vx-migrate-nx-mapping.json`), keyed on everything it reads — the graph, `nx.json` and its `extends` chain, every package manifest, each project dir's `.env` names, `NX_LOAD_DOT_ENV_FILES`, which bins are installed and the mapper's own code — so a warm run with nothing changed maps nothing.
|
|
189
190
|
|
|
190
191
|
### What it does not do
|
|
191
192
|
|
|
@@ -226,12 +227,12 @@ bunx @vzn/vx-migrate --keep # keep turbo.json / nx.json as the source: turbo(
|
|
|
226
227
|
bunx @vzn/vx-migrate --help # the usage, exit 0
|
|
227
228
|
```
|
|
228
229
|
|
|
229
|
-
It is the one command a repo needs (`
|
|
230
|
+
It is the one command a repo needs (`pbunx @vzn/vx-migrate` in a pnpm repo works the same):
|
|
230
231
|
|
|
231
232
|
- In a Turbo or Nx repo, in a terminal, it asks which adoption you want: **native** (the default, a `vx.config.ts` per package) or **keep** (a workspace file declaring `turbo()` or `nx()`). `--native` / `--keep` answer it; with no terminal (CI, a pipe) it is native. Elsewhere both are refused. A repo already set up (a workspace file declaring neither, and project configs) is said to be, exit 0, unless `--force`.
|
|
232
233
|
- Native or keep, a remote cache the repo shows is declared: `turboCache()` for turbo.json's enabled `remoteCache`, a `turbo link`ed `.turbo/config.json` or a CI file (`.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/config.yml`) setting `TURBO_TOKEN`; `nxCache()` for one setting `NX_SELF_HOSTED_REMOTE_CACHE_SERVER`. The report names the file that showed it. Nx Cloud (`nxCloudId`, `nxCloudAccessToken`, the `nx-cloud` runner) is named instead: vx does not speak its wire.
|
|
233
|
-
- With no `.vx/workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the remote cache above, the `@vzn/vx-lockfile` factory for the lockfile (`
|
|
234
|
-
- It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, every `@vzn/vx-*` package the workspace file imports (one an earlier init wrote included), and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w
|
|
234
|
+
- With no `.vx/workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the remote cache above, the `@vzn/vx-lockfile` factory for the lockfile (`bun()`, `pnpm()`, `npm()`, `yarn()`), `vxHistory()`, and `github()` when `.github/workflows` exists and `gitlab()` when `.gitlab-ci.yml` does, from `@vzn/vx-ci` (a remote cache on CI reads the refs they name), each installed beside `@vzn/vx` at vx-migrate's own version. Keep adds them to the file it writes, or to one already in that shape; any other workspace file is the user's and left alone. Native over a file vx wrote (a keep adoption's) rewrites it: `turbo()` and `nx()` go, since the configs written replace them, and the other plugins stay.
|
|
235
|
+
- It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, every `@vzn/vx-*` package the workspace file imports (one an earlier init wrote included), and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w` in a pnpm workspace and without `-w` in a lone package, `yarn add -D -W` on Yarn 1 and without `-W` on Yarn 2+, `bun add -d`, `npm install -D`). A package the root both lists and has installed at vx-migrate's own version is left alone; any other is installed at that version, and so is every other `@vzn/vx-*` plugin the root lists from the registry (one install moves them together, as `vx upgrade` does). Native removes a listed `@vzn/vx-migrate` when nothing written needs it (no `nx-exec` / `nx-env` task, no `turboCache()` or `nxCache()` left in the workspace file): `pnpm remove` (`-w` in a pnpm workspace), `yarn remove` (`-W` on Yarn 1), `bun remove`, `npm uninstall`. `--no-install` skips both. An install or removal that fails does not stop the migration: the manager's output prints, the report's `failed` row names the command, the files are still written, and vx-migrate exits 1. vx-migrate runs only with the `@vzn/vx` of its own release, which it installs: one of another release (a `bun x` install directory keeps the one it had) is refused, naming where it was loaded.
|
|
235
236
|
- Native, once the files are written, it ends with `vx lock`: `.vx/projects.lock.json` is written and the graph rules are checked, its line the report's `lock` row. A refusal prints above the report and the configs stay written (exit 0). `--no-install` skips it, since the plugins the workspace file declares are not installed yet, and `next` then starts with `vx lock`.
|
|
236
237
|
|
|
237
238
|
A Lerna repo is an Nx one: Lerna 6+ runs `lerna run` on Nx's task runner over the graph `nx graph` exports, nx.json or not, so a `lerna.json` with no nx.json and no turbo.json is mapped from that graph, and keep writes the workspace file declaring `nx()`. Lerna is the installed one, else the root manifest's range; with neither, the lerna.json is another tool's (lerna-lite reads it too, and runs no Nx). A root whose scripts never `lerna run` runs its tasks another way and publishes with Lerna (webdriverio: `run-s`, `pnpm -r`), so its scripts stay the source. Not where Lerna runs its own runner: `useNx: false`, or Lerna 5 without `useNx: true`; those map their scripts. Beside turbo.json, Turbo runs the tasks and Lerna only publishes: turbo.json is the source.
|
|
@@ -240,13 +241,13 @@ Your `package.json` scripts are never edited. `--dry` installs nothing and says
|
|
|
240
241
|
|
|
241
242
|
`--dry` prints the files instead of writing them; `--force` overwrites existing ones; `--mjs` writes `vx.config.mjs` (and `vx-preset.mjs`) instead of `.ts` — the same objects with no type import and no `satisfies`, for a package whose own `tsconfig` includes every `.ts` under it and would compile the config into its dist (TanStack/query, 2026-09-11).
|
|
242
243
|
|
|
243
|
-
Every source hands a plan to core's
|
|
244
|
+
Every source hands a plan to core's `applyMigration` from `@vzn/vx`), which renders, refuses to overwrite without `--force`, writes and reports. Anything a source cannot say becomes a `TODO(vx-migrate)` comment, never a silent wrong value.
|
|
244
245
|
|
|
245
246
|
The report reads like a run's footer: a blank line, the `─ vx-migrate <version> ───` rule, then one dim label per row and each fact once: `from` (the source), `ran` (each install or removal command), `tasks` (clean in green, todo in yellow), `todo`, `files`, `kept`, `replaced`, `lock`, `note`, and last, after a blank line, `next` in bold. A terminal sees the install command while it runs, on a line the report replaces; the manager's own output prints only when it fails.
|
|
246
247
|
|
|
247
248
|
### Turbo
|
|
248
249
|
|
|
249
|
-
Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package `turbo.json` `extends` overlays and each package's scripts, through the same mapper `turbo()` runs live — so the written configs say what the plugin was doing. Turbo's global fields become a generated root `vx-preset.ts` each config imports and spreads: TypeScript composition replaces global config. A config that a `tsconfig*.json` in its directory or above (below the root) takes in, where that project is composite or sets `rootDir`, declares the values it uses instead: `tsc` refuses a file outside the package (TS6059, TS6307; withastro/astro's `scripts/` and the `performance/` project its `packages/astro/tsconfig.test.json` takes in). So does a task's `env` that several configs share (`buildEnv`), where inline it would repeat in every package (vercel/ai: 63 names, twice in each of ~100 configs, half the output). Turbo's `//#` root tasks are written to a `vx.config.ts` at the workspace root, which makes the root a project (core's D-39), and a package task's `//#x` edge reaches it. When a `.vx/workspace.ts` already exists without one, the report names the `@vzn/vx-lockfile` plugin for the repo's lockfile: Turbo keys each package on its own lockfile entries, and core keys every task on the whole file until a plugin
|
|
250
|
+
Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package `turbo.json` `extends` overlays and each package's scripts, through the same mapper `turbo()` runs live — so the written configs say what the plugin was doing. Turbo's global fields become a generated root `vx-preset.ts` each config imports and spreads: TypeScript composition replaces global config. A config that a `tsconfig*.json` in its directory or above (below the root) takes in, where that project is composite or sets `rootDir`, declares the values it uses instead: `tsc` refuses a file outside the package (TS6059, TS6307; withastro/astro's `scripts/` and the `performance/` project its `packages/astro/tsconfig.test.json` takes in). So does a task's `env` that several configs share (`buildEnv`), where inline it would repeat in every package (vercel/ai: 63 names, twice in each of ~100 configs, half the output). Turbo's `//#` root tasks are written to a `vx.config.ts` at the workspace root, which makes the root a project (core's D-39), and a package task's `//#x` edge reaches it. When a `.vx/workspace.ts` already exists without one, the report names the `@vzn/vx-lockfile` plugin for the repo's lockfile: Turbo keys each package on its own lockfile entries, and core keys every task on the whole file until a plugin owns it.
|
|
250
251
|
|
|
251
252
|
| Turborepo | vx |
|
|
252
253
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -259,34 +260,34 @@ Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package
|
|
|
259
260
|
|
|
260
261
|
### Nx
|
|
261
262
|
|
|
262
|
-
Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.json` when exported, else the one the workspace's own `nx graph` exports into a temp file, as `nx()` does; with no `node_modules/.bin/nx`, as in a fresh clone, it stops and names the repo manager's install), through the same mapper `nx()` runs live, executors aside (below). Targets Nx plugins infer at runtime are frozen as the snapshot saw them. `nx:run-commands` is the one shell line `nx()` runs (see [`nx:run-commands`](#nxrun-commands) above: where, parallel or in order, forwarded arguments, `env`, `readyWhen`) — storybook's `compile` is `cd ../../.. && node ./scripts/build/build-package.ts --cwd code/lib/cli`; a plain `command` is that shorthand; `nx:run-script` is the package's script body with its `pre<name>` / `post<name>` hooks
|
|
263
|
+
Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.json` when exported, else the one the workspace's own `nx graph` exports into a temp file, as `nx()` does; with no `node_modules/.bin/nx`, as in a fresh clone, it stops and names the repo manager's install), through the same mapper `nx()` runs live, executors aside (below). Targets Nx plugins infer at runtime are frozen as the snapshot saw them. `nx:run-commands` is the one shell line `nx()` runs (see [`nx:run-commands`](#nxrun-commands) above: where, parallel or in order, forwarded arguments, `env`, `readyWhen`) — storybook's `compile` is `cd ../../.. && node ./scripts/build/build-package.ts --cwd code/lib/cli`; a plain `command` is that shorthand; `nx:run-script` is the package's script body with its `pre<name>` / `post<name>` hooks joined in (or `yarn run <name>` when the body calls yarn's `run` builtin; an empty script is the placeholder with a todo), `nx:noop` is a group task; Nx 15–16's `@nrwl/workspace:run-commands` and `run-script` (and their `@nx/workspace:` names) are the `nx:` executors they re-exported; **an executor target is its `nx-exec` line**, as `nx()` runs it: the migrator translates no executor, so Nx and `@vzn/vx-migrate` stay installed until each such line is rewritten as the command it runs (a server executor Nx knows, `@nx/js:node` or a dev server, is still a persistent task). The report also says what `.vx/workspace.ts` still holds, as the Turbo migration does: the `nx()` a keep adoption declared, which keeps reading nx.json, and a lockfile with no `@vzn/vx-lockfile` plugin, where Nx keyed each project on the npm packages it depends on. A written command that still runs Nx itself (a run-commands `nx run b:build`, `npx nx test b`, a script's `nx exec -- tsc`) carries a TODO: it works only while Nx is installed. An Nx project no workspace glob lists (an integrated repo's `project.json` library) gets a `package.json` (`name`, `private`) where it has none, and a note names the directories to add to `workspaces` (or `pnpm-workspace.yaml`), so the package manager installs it and a lockfile plugin names it; the migrator never edits the root manifest. A repo with no `package.json` at all (Nx's `nx init` outside JS: Nx pinned in `.nx/installation`, projects as `project.json`) is migrated too, natively only: the root is the directory holding `nx.json`, the graph is exported by `.nx/installation`'s `nx`, each config writes the project's Nx `name`, every edge is the explicit `project#target` Nx's graph draws (a `^` edge needs a lockfile plugin, which has no lockfile to read), and `.vx/workspace.ts` declares no plugin, so nothing is installed and core runs the configs alone; `--keep` refuses, as `nx()` would have no `package.json` to install into, and an executor target's `nx-exec` line is a note to rewrite as a command. A target with `configurations` writes one task per configuration (`build`, `build:ci`). A project's Nx `tags` are written as its `tags`. Named inputs expand from `nx.json` when readable. An output path is kept as written, a `!` one too (`{projectRoot}/dist` → `dist`, `{projectRoot}/bin/tool` → `bin/tool`; one naming an unset `{options.x}` is dropped, as Nx drops it; a dotted `{options.outputPath.base}` walks the options, as Nx does; an extglob is put in vx's grammar: Next's inferred `.next/!(cache)/**/*` is `.next/*/**/*` with `!.next/cache/**/*`, `@(js|map)` is `{js,map}`, Nx's bare group `*.(js|map)` is `*.{js,map}` in inputs and outputs alike, and a form with no vx spelling is dropped with a TODO; a `**` glued to text, in an output or an input, is one `*` as Nx reads it, `src/**.ts` → `src/*.ts`): vx reads a bare path as the file or the whole tree under it, so a directory and an extensionless binary both save and restore. nx.json's `parallel` and `defaultBase`, which `nx()` applies live, are each a field of the `.vx/workspace.ts` it writes (`run.concurrency`, `baseBranch`), read from nx.json alone, never the environment; beside a workspace file already there, each is a note naming the field to add. vx derives package edges from `package.json`; an Nx graph edge with no manifest path (`implicitDependencies`, a tsconfig path) becomes, for each `^target` of the dependant, an explicit `pkg#target` edge to what Nx's own walk reaches — each dependency that has the target, and through one that lacks it, its dependencies — so the order and the key are Nx's. The reverse too: where a project's manifest names a workspace package its Nx graph does not reach (an `implicitDependencies: ["!a"]` that breaks a manifest cycle), its `^target` becomes the explicit edges Nx draws, since vx's `^` would follow the manifest and bring the cycle back.
|
|
263
264
|
|
|
264
265
|
### Vite Task
|
|
265
266
|
|
|
266
267
|
Detected when the root `package.json` lists `vite-plus`, after turbo.json and Nx: vite-plus is a whole toolchain, so a repo with either of those runs its tasks there; `--from vite-task` picks Vite Task anyway. There is no live plugin, so `--keep` is refused. Each package's `vite.config.*` (vite-plus's file order) is loaded by Bun as `vp run` sees it, a function config called in build mode, and its `run` block mapped with the package's `package.json` scripts, which `vp run` runs too. The root package's tasks and scripts (`vp run -w`) make it a project when it has a `name`. A command that opens with `vp run` (or `vpr`) is inlined as Vite Task inlines it: `vp run x`, `pkg#x`, `-r x`, `-w x` and `-F <name> x` are `dependsOn` edges (`-r` to each package with `x`, the task's own reference pruned), and the task is a group when nothing follows them. They run in parallel where Vite Task ran a chain in order; any other form (`--parallel`, `-t`, forwarded arguments, a `vp run` after another command) stays in the command with a TODO.
|
|
267
268
|
|
|
268
|
-
| Vite Task | vx
|
|
269
|
-
| --------------------------------------------------------- |
|
|
270
|
-
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task
|
|
271
|
-
| `cwd` | `cd <cwd> && ` before the command
|
|
272
|
-
| `dependsOn: 'x'` / `'pkg#x'` | the same
|
|
273
|
-
| `dependsOn: { task, from }` | `^task`; when a member listed under a field `from` leaves out also has the task, the explicit `pkg#task` edges, since `^` follows every field
|
|
274
|
-
| `cache.input` / `cache.output` globs | `inputs.files` / `cache.outputs.files`; `base: 'workspace'` → `workspaceFiles`, a root task's too
|
|
275
|
-
| omitted or `{ auto: true }` (traced files) | no `cache` block, with a TODO: vx infers no inputs or outputs
|
|
276
|
-
| `cache.env` | `inputs.env` **and** `exec.env.passThrough`; a `*` entry lists the names the package's tracked files and its workspace dependencies' spell (a TODO when none match); `!` entries take names back
|
|
277
|
-
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env`
|
|
278
|
-
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache` block
|
|
279
|
-
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache
|
|
280
|
-
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>`
|
|
269
|
+
| Vite Task | vx |
|
|
270
|
+
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
271
|
+
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task |
|
|
272
|
+
| `cwd` | `cd <cwd> && ` before the command |
|
|
273
|
+
| `dependsOn: 'x'` / `'pkg#x'` | the same |
|
|
274
|
+
| `dependsOn: { task, from }` | `^task`; when a member listed under a field `from` leaves out also has the task, the explicit `pkg#task` edges, since `^` follows every field |
|
|
275
|
+
| `cache.input` / `cache.output` globs | `inputs.files` / `cache.outputs.files`; `base: 'workspace'` → `workspaceFiles`, a root task's too; a `**` glued to text (`src/**.ts`), which wax refuses, widens an input to `**/*` and runs an output's task uncached, with a todo |
|
|
276
|
+
| omitted or `{ auto: true }` (traced files) | no `cache` block, with a TODO: vx infers no inputs or outputs |
|
|
277
|
+
| `cache.env` | `inputs.env` **and** `exec.env.passThrough`; a `*` entry lists the names the package's tracked files and its workspace dependencies' spell (a TODO when none match); `!` entries take names back |
|
|
278
|
+
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env` |
|
|
279
|
+
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache` block |
|
|
280
|
+
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache |
|
|
281
|
+
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` joined into its command; `false` keeps them tasks of their own |
|
|
281
282
|
|
|
282
283
|
### package.json scripts
|
|
283
284
|
|
|
284
285
|
Where no runner's config is the source (or under `--from scripts`), each
|
|
285
286
|
package's `package.json` scripts become its `vx.config.ts`, plus a
|
|
286
287
|
`.vx/workspace.ts` that declares the `@vzn/vx-lockfile` plugin for the lockfile at
|
|
287
|
-
the root (`
|
|
288
|
+
the root (`bun()`, `pnpm()`, `npm()` or `yarn()`; `{ plugins: [] }`
|
|
288
289
|
without one): package edges (`^build`, `--affected`) come from it.
|
|
289
|
-
|
|
290
|
+
vx runs and caches on this machine with no plugin, so it declares no executor or
|
|
290
291
|
cache, and the file carries no comment (it is the user's). When
|
|
291
292
|
`@vzn/vx-lockfile` is not installed, it installs it with that manager,
|
|
292
293
|
pinned to its own version, before `vx lock` runs (`--dry` names the
|
|
@@ -386,7 +387,9 @@ a launcher's `--package <name>` / `-p <name>` (`pnpm dlx --package
|
|
|
386
387
|
netlify-cli netlify dev`, D-112). A
|
|
387
388
|
script whose name no task may carry (`lint#fix`, `^up`; the schema's
|
|
388
389
|
rule, item 1000) is left out with a TODO rather than written into a
|
|
389
|
-
config every later command refuses,
|
|
390
|
+
config every later command refuses; a Turbo task, Nx target or Vite Task
|
|
391
|
+
task of such a name is too, under `turbo()` and `nx()` as well as in the
|
|
392
|
+
written configs, and a `__proto__` script is written
|
|
390
393
|
as a computed key, since a literal `__proto__:` sets the prototype. An
|
|
391
394
|
existing `.vx/workspace.*` in any extension the loader reads (`.mts`
|
|
392
395
|
included) is kept, and `--force` REPLACES a package's config of another
|
|
@@ -399,7 +402,7 @@ the kept ones under `kept`, so a half-adopted workspace adopts the rest
|
|
|
399
402
|
|
|
400
403
|
Two npm conventions are mapped rather than copied, because copying them
|
|
401
404
|
loses behaviour. `pre<x>` / `post<x>` hooks, which npm runs around `x`
|
|
402
|
-
without being named, are
|
|
405
|
+
without being named, are joined into `x`'s command in that order, each
|
|
403
406
|
in its own subshell, so the chain stops at the first that fails whatever
|
|
404
407
|
a part holds (a `;`, an `exit`), and forwarded `--` args reach `x` alone,
|
|
405
408
|
as npm hands them to the script and not its hooks (item 905), appended
|
|
@@ -407,7 +410,7 @@ as text, so no part sees them as `$1` (X-60). The
|
|
|
407
410
|
command is a small shell function, `vx_script`, around the three parts;
|
|
408
411
|
it carries a TODO saying so; a `pre<x>` with no `x` stays a task of its own, and
|
|
409
412
|
npm's lifecycle hooks (`prepack`, `prepublishOnly`, …) are never tasks.
|
|
410
|
-
Every project gets a `vx.config.ts` (
|
|
413
|
+
Every project gets a `vx.config.ts` (2026-10-14): a member with
|
|
411
414
|
no script to run (none, or only lifecycle hooks: a types-only or config
|
|
412
415
|
package), or with none a Turbo or Nx repo runs, gets one declaring
|
|
413
416
|
`tasks: {}`. A project that already has a config keeps it. A package with no `build` script whose `prepack`, `prepublishOnly`,
|
|
@@ -471,7 +474,9 @@ TODO naming that part: the graph runs that member's task once, so name it
|
|
|
471
474
|
under `dependsOn` and drop it from the command (pinia). A directory inside
|
|
472
475
|
the member or the root is no other member.
|
|
473
476
|
A script that is nothing but `npm run <other>` (`pnpm <other>`, `yarn
|
|
474
|
-
<other>`, `bun run <other>`, `npm test`, `npm start`
|
|
477
|
+
<other>`, `bun run <other>`, `npm test`, `npm start`, `npm stop`, and the
|
|
478
|
+
aliases `npm run-script` / `rum` / `urn`, `npm t` / `tst`, `pnpm
|
|
479
|
+
run-script`, `pnpm t` / `tst`) becomes a **group**
|
|
475
480
|
over `<other>` — `dependsOn` and no command — so the graph runs and
|
|
476
481
|
caches the target instead of a package-manager subprocess it cannot
|
|
477
482
|
see. When `build` is such a group, the task it reaches that runs a
|
|
@@ -485,7 +490,7 @@ version` (npm's), `bun deploy`, `yarn check` (D-32); `pnpm test`, `yarn
|
|
|
485
490
|
test` and `bun lint` do run the script. Arguments, flags or a `&&`
|
|
486
491
|
chain make it a real command again and
|
|
487
492
|
it is left verbatim, and so is one whose target becomes no task (a
|
|
488
|
-
lifecycle script, or a hook
|
|
493
|
+
lifecycle script, or a hook joined into another script): a group over
|
|
489
494
|
it would name a task nothing defines (D-12). A verbatim chain whose
|
|
490
495
|
parts run this package's own scripts (`check: pnpm run build && pnpm run
|
|
491
496
|
lint`) gets a TODO naming those parts: each ran again inside the command,
|
|
@@ -497,11 +502,8 @@ order may matter, so it is not made a group.
|
|
|
497
502
|
The report lists each TODO once per reason: tasks that share one are
|
|
498
503
|
named together (the first five, then a count; the files carry each),
|
|
499
504
|
and when no task caches it says uncommenting a TODO task's cache line
|
|
500
|
-
makes the second run a hit. Its `next:` line is a command the user can type
|
|
501
|
-
|
|
502
|
-
`npm_config_user_agent`) with the installed `vx` bin, else the
|
|
503
|
-
`@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
|
|
504
|
-
runner, a bare `vx`. Outside a git work tree it starts with
|
|
505
|
+
makes the second run a hit. Its `next:` line is a command the user can type,
|
|
506
|
+
with a bare `vx` (`vx init` installs it). Outside a git work tree it starts with
|
|
505
507
|
`git init`, and with no script mapped with declaring a task, since the
|
|
506
508
|
run would refuse without either. It runs `build`, else the first task that
|
|
507
509
|
neither serves (`persistent`) nor changes the repo (`clean`, `release`,
|
|
@@ -538,7 +540,7 @@ import { turboCache } from '@vzn/vx-migrate'
|
|
|
538
540
|
export default defineWorkspace({
|
|
539
541
|
plugins: [
|
|
540
542
|
turboCache({
|
|
541
|
-
|
|
543
|
+
url: 'https://cache.example.com',
|
|
542
544
|
token: process.env.CACHE_TOKEN,
|
|
543
545
|
teamSlug: 'acme',
|
|
544
546
|
// Optional: sign uploads and verify downloads (Turbo's artifact signature).
|
|
@@ -550,21 +552,21 @@ export default defineWorkspace({
|
|
|
550
552
|
|
|
551
553
|
Off CI a run reads the remote and does not write it unless its policy names the remote. Give the write token only to the default branch's CI and a read-only one to laptops and pull request jobs: the server's token is the boundary (`docs/security.md` § Cache poisoning). The same holds for `nxCache()`.
|
|
552
554
|
|
|
553
|
-
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. A token with no
|
|
554
|
-
|
|
555
|
-
| Option | Environment variable
|
|
556
|
-
| ----------------- |
|
|
557
|
-
| `
|
|
558
|
-
| `token` | `TURBO_TOKEN`
|
|
559
|
-
| `oidc` | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` (GitHub sets them)
|
|
560
|
-
| `teamId` | `TURBO_TEAMID`
|
|
561
|
-
| `teamSlug` | `TURBO_TEAM`
|
|
562
|
-
| `signatureKey` | `TURBO_REMOTE_CACHE_SIGNATURE_KEY`, read only under turbo.json's `remoteCache.signature: true
|
|
563
|
-
| `timeoutMs` | `TURBO_REMOTE_CACHE_TIMEOUT`, then `remoteCache.timeout` (seconds)
|
|
564
|
-
| `uploadTimeoutMs` | `TURBO_REMOTE_CACHE_UPLOAD_TIMEOUT`, then `remoteCache.uploadTimeout` (seconds)
|
|
565
|
-
| `retries` | —
|
|
566
|
-
| `preflight` | `TURBO_PREFLIGHT` (1 or 0), then `remoteCache.preflight`
|
|
567
|
-
| `scope` | —
|
|
555
|
+
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. An empty variable is unset, as Turbo reads it. A token with no URL means Vercel's hosted Remote Cache (`https://vercel.com/api`), exactly as it does for `turbo` — so `bunx turbo login && bunx turbo link`, then `turboCache()` with `TURBO_TOKEN` / `TURBO_TEAM` set, is the whole hosted setup. Below the environment, in Turbo's order: a Vercel build's `VERCEL_ARTIFACTS_TOKEN` and `VERCEL_ARTIFACTS_OWNER` (the team id, where `TURBO_TOKEN` with a team is not set), then the repo's `.turbo/config.json` (what `turbo link` writes: `apiUrl`, `teamId`, `teamSlug`, `token`), then the root `turbo.json`'s `remoteCache` (`apiUrl`, `teamId`, `teamSlug`), whose `enabled: false` declines unless the options name a cache. A team with no URL also means Vercel's. With a URL or a team but no token, the token comes from the OS keychain ([Tokens](#tokens-and-the-os-keychain)); with none of the three the plugin **declines** and the run stays local.
|
|
556
|
+
|
|
557
|
+
| Option | Environment variable | Meaning |
|
|
558
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
559
|
+
| `url` | `TURBO_API` | base URL of the cache server; default with a token: `https://vercel.com/api`; http(s) only (http to any host, as Turbo and Nx take it); a `user:pass@` in it is refused, the option's when the config loads |
|
|
560
|
+
| `token` | `TURBO_TOKEN` | Bearer token on every request; then OIDC, `.turbo/config.json`, the OS keychain |
|
|
561
|
+
| `oidc` | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` (GitHub sets them) | `true` or `{ tokenUrl, revokeUrl, audience, params }` (`TurboOidcOptions`): exchange the GitHub Actions job's OIDC token for a short-lived one ([OIDC](#oidc-on-github-actions)) |
|
|
562
|
+
| `teamId` | `TURBO_TEAMID` | `teamId` query parameter (Vercel's `team_` form only, as Turbo sends it); part of the signature |
|
|
563
|
+
| `teamSlug` | `TURBO_TEAM` | `slug` query parameter |
|
|
564
|
+
| `signatureKey` | `TURBO_REMOTE_CACHE_SIGNATURE_KEY`, read only under turbo.json's `remoteCache.signature: true`, as Turbo reads it | HMAC-SHA256 key, used raw, over the team id (`''` without one, as in Turbo); the option needs ≥ 32 bytes, the env key warns below that unless `futureFlags.longerSignatureKey` refuses it; signing on with no key turns the remote off with a warning; a download whose tag does not verify is a miss |
|
|
565
|
+
| `timeoutMs` | `TURBO_REMOTE_CACHE_TIMEOUT`, then `remoteCache.timeout` (seconds) | HEAD/GET/POST deadline (default 30 s; 0 none); the option is ms up to 2³¹−1 (the longest a timer holds), the variable and file are held to it |
|
|
566
|
+
| `uploadTimeoutMs` | `TURBO_REMOTE_CACHE_UPLOAD_TIMEOUT`, then `remoteCache.uploadTimeout` (seconds) | PUT deadline (default 60 s; 0 none), bounded as `timeoutMs` |
|
|
567
|
+
| `retries` | — | resends of a request answered 5xx (not 501) or never connected (default 1, Turbo's); a 429 waits out the limit; 0 turns both off |
|
|
568
|
+
| `preflight` | `TURBO_PREFLIGHT` (1 or 0), then `remoteCache.preflight` | send Turbo's `OPTIONS` preflight before each artifact request and go where its `Location` points (relative to the URL); the token goes along only when the API's `Access-Control-Allow-Headers` admits `Authorization`; a redirected preflight is not followed (default off) |
|
|
569
|
+
| `scope` | — | `RemoteScope`: `'header'` (default): every read names the run's refs, own branch first, in `x-artifact-read-scopes`; `'key'`: each entry is keyed on its ref, a read asks every ref's key in one batch (a server that ignores the header) |
|
|
568
570
|
|
|
569
571
|
The signature is Turbo's current scheme (`artifact-signature:v2`: prefix, hash, team id and body, each length-prefixed, under HMAC-SHA256, base64 in `x-artifact-tag`). A signed body is written to a temp file before its tag can be checked, so one past core's artifact ceiling (2 GiB, at zstd's bound) is refused as it passes it, and is a miss.
|
|
570
572
|
|
|
@@ -579,18 +581,18 @@ import { defineWorkspace } from '@vzn/vx/config'
|
|
|
579
581
|
import { nxCache } from '@vzn/vx-migrate'
|
|
580
582
|
|
|
581
583
|
export default defineWorkspace({
|
|
582
|
-
plugins: [nxCache({
|
|
584
|
+
plugins: [nxCache({ url: 'https://cache.example.com', token: process.env.CACHE_TOKEN })],
|
|
583
585
|
})
|
|
584
586
|
```
|
|
585
587
|
|
|
586
588
|
Every option falls back to the tool's own environment variable, and a missing token to the OS keychain ([Tokens](#tokens-and-the-os-keychain)); with no server the plugin **declines** and the run stays local.
|
|
587
589
|
|
|
588
|
-
| Option
|
|
589
|
-
|
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `timeoutMs`
|
|
593
|
-
| `retries`
|
|
590
|
+
| Option | Environment variable | Meaning |
|
|
591
|
+
| ----------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
592
|
+
| `url` | `NX_SELF_HOSTED_REMOTE_CACHE_SERVER` | base URL of the cache server; http(s) only (http to any host, as Turbo and Nx take it); a `user:pass@` in it is refused, the option's when the config loads |
|
|
593
|
+
| `token` | `NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN` | Bearer token, else the OS keychain; `''` (option or variable): an open server |
|
|
594
|
+
| `timeoutMs` | — | per-request deadline (default 30 s; ms above 0, up to 2³¹−1) |
|
|
595
|
+
| `retries` | — | resends, as `turboCache()`'s (default 1) |
|
|
594
596
|
|
|
595
597
|
The Nx spec has no existence probe, so `has` (the `--dry` prediction; the prefetch pass calls `get`) is a `GET` whose body is cancelled before it answers. The wire carries no producing-task duration, so a remote hit reports none.
|
|
596
598
|
|
|
@@ -621,8 +623,10 @@ With no token from an option, a variable or a config file, both plugins read the
|
|
|
621
623
|
|
|
622
624
|
## Remote-cache behaviour (both)
|
|
623
625
|
|
|
624
|
-
- Task keys carry no branch. `turboCache()` names the run's refs on every read (`HEAD`, `GET`, the batch `POST`) in `x-artifact-read-scopes`, own branch first (`refs/heads/feat-x, refs/heads/main`): from CI's env, else git's current branch, then the default branch. A `PUT` names none: where a write lands is the server's call, from the CI OIDC token checked against its trust policy. `scope: 'key'`
|
|
626
|
+
- Task keys carry no branch. `turboCache()` names the run's refs on every read (`HEAD`, `GET`, the batch `POST`) in `x-artifact-read-scopes`, own branch first (`refs/heads/feat-x, refs/heads/main`): from CI's env, else git's current branch, then the default branch. A `PUT` names none: where a write lands is the server's call, from the CI OIDC token checked against its trust policy. `scope: 'key'` hashes the ref into each key instead, for a server that ignores the header. `nxCache()` sends plain requests.
|
|
627
|
+
- The batch `POST` carries at most 100 hashes, Vercel's limit; more go in parallel batches. A batch refused with any status but 404, 405 or 501 (no batch query) is reported with its body, and so is a 200 answer that is no JSON object. How a server implements the header: [Branch caches a server enforces](https://vznjs.github.io/vx/blog/branch-caches-a-server-enforces/).
|
|
625
628
|
- A remote error degrades to a **miss** and a warning that names the request, the artifact and the server — `[vx] plugin '@vzn/vx-migrate': upload 32248a2a7c89e241 to https://cache.example.com/v8/artifacts failed: no answer within 60000 ms` — never the token. The run never fails because of the cache.
|
|
629
|
+
- A hit that does not restore (bad bytes, a signature that does not verify) re-runs the task, and `turboCache()` uploads its save over the stored entry, skipping the `HEAD` it asks before other uploads. Nx's records are immutable (`409`), so `nxCache()` cannot repair one.
|
|
626
630
|
- The same failure is said once per run: an unreachable server fails the probe, the download and the upload alike, and the run prints the first and, at its end, `[vx] plugin '@vzn/vx-migrate': 2 more requests failed the same way: Unable to connect. Is the computer able to access the url?` (core does the counting, for every cache plugin).
|
|
627
631
|
- A request answered `5xx` (not `501`), or one that never connected (a refused port, an unresolved host), is sent again after 2 s, `retries` times. A `429` pauses every request to that remote for its `Retry-After` (capped at 10 s a wait, 2 s without one), and the request is sent again when the pause ends, for up to 20 s from its first `429`, as Turbo's client does. A spent deadline is not resent: it has already cost its wait.
|
|
628
632
|
- Three outages in a row (no connection, no answer within the deadline, or a `5xx` once the resends are spent) open Turbo's outage breaker: no request is sent for 30 s, each lookup is a miss at once, then one request probes and its answer closes or reopens it. A hit is judged when its body ends, so a server that sends headers and then stalls the body counts the same way. A hung server costs three deadlines, not one per task.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.648",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo, Nx or package.json scripts: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"LICENSE"
|
|
35
35
|
],
|
|
36
36
|
"engines": {
|
|
37
|
-
"bun": ">=1.4"
|
|
37
|
+
"bun": ">=1.4.4"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@vzn/vx": "^0.0.
|
|
43
|
+
"@vzn/vx": "^0.0.648"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|