@vzn/vx-migrate 0.0.652 → 0.0.653
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 +42 -33
- package/package.json +2 -2
- package/src/migrate-scripts.ts +11 -5
- package/src/migrate-turbo.ts +8 -1
- package/src/migrate.ts +102 -18
- package/src/nx/index.ts +61 -11
- package/src/nx/nx-inputs.ts +48 -24
- package/src/nx/nx-map.ts +199 -14
- package/src/nx/nx-outputs.ts +6 -1
- package/src/nx/nx-upstream.ts +11 -11
- package/src/nx-cache/index.ts +42 -28
- package/src/packages.ts +51 -1
- package/src/remote-refusal.ts +49 -4
- package/src/shared-outputs.ts +64 -9
- package/src/tracked-outputs.ts +58 -19
- package/src/turbo/index.ts +2 -1
- package/src/turbo/turbo-map.ts +157 -22
- package/src/turbo-cache/index.ts +71 -40
- package/src/turbo-cache/oidc.ts +15 -7
- package/src/workspace-notes.ts +19 -8
- package/src/workspace-plugins.ts +11 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Everything for adopting [`@vzn/vx`](https://vznjs.github.io/vx/) from Turborepo,
|
|
|
6
6
|
|
|
7
7
|
- **`turbo()`** — a temporary start for a Turbo repository: the plugin makes each package-manager member a project (`discover`; core finds only directories holding a `vx.config`) and fills vx's `project` stage from `turbo.json` and each package's `package.json` scripts until the migrator writes native config.
|
|
8
8
|
- **`nx()`** — the same temporary start for an Nx repository, filled from Nx's resolved project graph. Executor targets (`@nx/js:tsc`, `@nx/vite:build`, your own) run as themselves through **`nx-exec`**, one executor per process.
|
|
9
|
-
- **`bunx @vzn/vx-migrate`** — write one `vx.config.ts` per workspace package from your `turbo.json`, an exported Nx project graph, vite-plus's `run.tasks` (Vite Task, `vp run`), or else each package's `package.json` scripts, plus the workspace file every run needs. Runs without a workspace file, so it is the first command, not the second; `vx init` runs it in
|
|
9
|
+
- **`bunx @vzn/vx-migrate`** — write one `vx.config.ts` per workspace package from your `turbo.json`, an exported Nx project graph, vite-plus's `run.tasks` (Vite Task, `vp run`), or else each package's `package.json` scripts, plus the workspace file every run needs. Runs without a workspace file, so it is the first command, not the second; `vx init` runs it in a repo with a `turbo.json`, `turbo.jsonc` or `nx.json`.
|
|
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
|
|
@@ -43,7 +43,7 @@ Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps eve
|
|
|
43
43
|
### What it does not do
|
|
44
44
|
|
|
45
45
|
- A task the package's own `vx.config` already declares is left alone — the plugin fills, it never overwrites. Migrate a package by writing its config; the rest of the repo keeps running from `turbo.json`.
|
|
46
|
-
- The mapping's gaps are the migration's gaps, reported as warnings by `vx lock` (and `vx lock --check`), never on a run, instead of `TODO(vx-migrate)` comments: `$TURBO_ROOT$` tasks, an env name vx cannot key that the run's environment sets (`FOO_?`, which Turbo reads literally), unknown turbo keys. A persistent task's readiness note (`readyWhen`) is reported only when something depends on it — with no dependent there is nothing to gate (refine printed it for 375 `dev` targets a run). `bunx @vzn/vx-migrate --dry` lists the same set once. Each gap is one line for every task that carries it, not one per task (n8n marks `dev` and `watch` persistent in most of its 84 packages; astro negates `vendor/**` in the outputs of 57 tasks). A per-package `{ "extends": false }` with nothing else is Turbo's opt-out and maps to no task; with keys, the task runs on those keys alone. A glob that climbs out of the package (`../../packages/app-store/*.generated.ts`) is re-anchored on the workspace root as a `workspaceFiles` glob. A negated output under a literal-rooted one (`dist/**` minus `!dist/**/*.map`) takes its paths back (see `outputs` below); one against a wildcard-rooted output (medusa's `*/**` minus `!src/**`)
|
|
46
|
+
- The mapping's gaps are the migration's gaps, reported as warnings by `vx lock` (and `vx lock --check`), never on a run, instead of `TODO(vx-migrate)` comments: `$TURBO_ROOT$` tasks, an env name vx cannot key that the run's environment sets (`FOO_?`, which Turbo reads literally), unknown turbo keys. A persistent task's readiness note (`readyWhen`) is reported only when something depends on it — with no dependent there is nothing to gate (refine printed it for 375 `dev` targets a run). `bunx @vzn/vx-migrate --dry` lists the same set once. Each gap is one line for every task that carries it, not one per task (n8n marks `dev` and `watch` persistent in most of its 84 packages; astro negates `vendor/**` in the outputs of 57 tasks). A per-package `{ "extends": false }` with nothing else is Turbo's opt-out and maps to no task; with keys, the task runs on those keys alone. A glob that climbs out of the package (`../../packages/app-store/*.generated.ts`) is re-anchored on the workspace root as a `workspaceFiles` glob. A glob's `.` and `..` segments are cleaned first, as Turbo cleans them: `./src/**` is `src/**`, and `./../shared/**` and `src/../../shared/**` are `../shared/**`. A negated output under a literal-rooted one (`dist/**` minus `!dist/**/*.map`) takes its paths back (see `outputs` below); one against a wildcard-rooted output (medusa's `*/**` minus `!src/**`) keeps what it names (see `outputs` below).
|
|
47
47
|
- The cost is the stage's: on a 1,000-package workspace with no `vx.config` files the mapping loads in about 42 ms where 1,000 evaluated configs load in 22.
|
|
48
48
|
- The mapping is kept in `<cache dir>/vx-migrate-turbo-mapping.json`, keyed on every `turbo.json` / `turbo.jsonc`, every package manifest, the mapper's own code and the environment variable names a wildcard can match (one in a Turbo config or Turbo's framework table) or core cannot key, so an unrelated variable keeps it; a run with none changed reads it instead of mapping (astro, 562 packages: ~40 ms of mapping against ~12 ms of key reads).
|
|
49
49
|
- The mapping is read once per run, never once per process: under `vx watch`, a `package.json` script edit or a per-package `turbo.json` overlay edit is the next cycle's tasks. The root `turbo.json` is no task's input and lives in no project dir, so an edit to it is not a cycle — restart the watch.
|
|
@@ -59,16 +59,17 @@ Rules:
|
|
|
59
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
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
|
-
- 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).
|
|
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, also when the task is `cache: false` (the node stores nothing; Turbo hashes it cached or not). 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). `turbo()`, which writes nothing, keeps it: core's node is skipped by a bare name, so `vx run build` scoped to such a package (its directory, `--filter`) was refused where Turbo builds its dependencies. 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 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>` → `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 `inputs.stability: { 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/`.
|
|
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 (one a `^` edge reaches too), 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>` → `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. A path a negated output takes back is also left out of the task's own inputs when git tracks no file under it: the task still writes it, and where it is not gitignored (`!dist/skip/**`) it changed after the key was taken, so no run was saved. One over a committed file (medusa's `!src/**`, `lib/**` minus `!lib/vendor/**`) is source and stays an input, as Turbo keys it; without git every one stays. Another task of the package whose inputs take back the positive output, and so hide that file, drops the take-back if it is key-only, or else runs uncached with a todo (core refuses an input that reaches another task's output). 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 `inputs.stability: { 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 gitignored file an input names is keyed, as Turbo hashes it: Turbo walks an explicit glob on disk (`$TURBO_DEFAULT$` alone skips ignored files). A literal path is keyed by a probe; each ignored file an explicit task or `globalDependencies` glob matches (`src/**` over an ignored `src/gen.ts`) is named beside the glob, and a directory ignored whole as the glob's rest under it (`src/out/**`); core reads both from disk. A path a `!` entry takes back stays out. Unlike Turbo, nothing under `node_modules` (the lockfile plugin keys an install) or `.vx/` is named. The paths are listed when the mapping is made (one `git ls-files`), so a new ignored file outside every directory listed then is keyed from the next mapping (a `turbo.json`, manifest or git index change).
|
|
66
67
|
- 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.
|
|
67
68
|
- `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.
|
|
68
69
|
- 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.
|
|
69
70
|
|
|
70
71
|
- **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)
|
|
72
|
+
- **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. Under `turbo()` so does a glob that names another task's output (`build` reading `gen/**` beside a `gen` writing `gen/**`) when no `dependsOn` path reaches that task: Turbo walks an explicit glob on disk, so it re-runs `build` after `gen` rewrites it, and taken back, `build` replayed the old output; a glob led by `**` (`**/*.ts`) keeps the `!`, as the default does. 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, a codegen's `gen.ts`) stays an output, so a task that rewrites it caches, and the `!` each reader got for it is dropped: the file stays in every key, as Turbo and Nx hash it, and the readers stay cached. Same for Nx targets.
|
|
72
73
|
- **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.
|
|
73
74
|
- `cache: false` or `persistent: true` → no `cache: true`, 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`.
|
|
74
75
|
- A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
|
|
@@ -97,7 +98,7 @@ import { nx } from '@vzn/vx-migrate'
|
|
|
97
98
|
export default defineWorkspace({ plugins: [nx()] })
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
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 `cache: true`, 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`.
|
|
101
|
+
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 `cache: true`, 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, a `!` acting only inside its own set, as Nx hashes the `{projectRoot}` and `{workspaceRoot}` filesets apart and vx subtracts each `!` from its whole list (production's `!{projectRoot}/jest.config.[jt]s` beside a `{workspaceRoot}/libs/a/jest.config.ts` input keeps the file: a `!` that may reach another set's files is dropped, which keys more, never a stale hit), and an output outside the project dir (`dist/<project>` at the workspace root, Nx's default layout) a `workspaceFiles` output, as is every `{workspaceRoot}` output of the root project (one may lie in a nested project, which a project glob never reaches), 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`. Only an output git ignores drops it (one `git check-ignore` per mapping): one git tracks a file under, or leaves untracked and unignored, is in Nx's file map, so the input stays.
|
|
101
102
|
|
|
102
103
|
| Option | Meaning |
|
|
103
104
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -159,7 +160,9 @@ A task whose command still runs Nx or Lerna (`nx-exec`, `nx-env`, `nx exec`, `le
|
|
|
159
160
|
|
|
160
161
|
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.
|
|
161
162
|
|
|
162
|
-
`{ dependentTasksOutputFiles }` hashes the outputs of the tasks a target depends on; vx hashes those tasks' keys into it through `dependsOn`, and
|
|
163
|
+
`{ dependentTasksOutputFiles }` hashes the outputs of the tasks a target depends on; vx hashes those tasks' keys into it through `dependsOn`, and a cached task's output follows from its inputs. An uncached producer's does not (a `cache: false` `fetch` that writes a gitignored schema from outside): the files the glob names under its outputs are a `inputs.workspaceRuntime` probe, and `inputs.tasks` leaves the producer out, so core answers the probe after it ran, as for `turbo()`'s `dependencyOutputs`; an output the probe cannot list (a wildcard directory) leaves the reader uncached with a todo. With `transitive: true` the reader reads every task below it, so an uncached producer behind an `nx:noop` group or another task counts too: the direct edge it sits under is left out instead, and the probe lists every output below that edge. 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. A `^build` edge on a project Nx links to no dependency (a leaf library's inferred `build`) reads nothing, and stays cached. `{ 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.
|
|
164
|
+
|
|
165
|
+
`{ externalDependencies }` hashes the named packages' resolved versions, and core keys no lockfile. A `@vzn/vx-lockfile` plugin keys each task on its project's and the root's dependencies, so a name the root `package.json` declares needs nothing there, and any other is a todo. With no plugin claiming the root lockfile (`nx()` reads the workspace's plugins' `fingerprint` claims), the task lists the lockfile in `inputs.workspaceFiles`, a superset: any edit to it re-runs the task, as a `left-pad` bump re-runs it under Nx.
|
|
163
166
|
|
|
164
167
|
### The environment a task sees
|
|
165
168
|
|
|
@@ -191,7 +194,7 @@ An Nx project's `tags` are its vx `tags`, so `vx run build --filter tag:scope:we
|
|
|
191
194
|
|
|
192
195
|
### The graph snapshot
|
|
193
196
|
|
|
194
|
-
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.
|
|
197
|
+
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`, and the tool configs an inferring `@nx/*` plugin reads: `jest.config.*`, `jest.preset.js`, `.eslintrc*`, `eslint.config.*`, and the `vite`, `vitest`, `playwright`, `cypress`, `webpack`, `rollup`, `rspack` and `next` configs), and anything in the directory of a local plugin `nx.json` names by path (`./tools/plugin.js`; at the root, the file itself, any extension). Another root file a plugin reads, or a plugin named by package or path alias that sits outside every project root, moves the snapshot once committed (`HEAD` moves). 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.
|
|
195
198
|
|
|
196
199
|
### What it does not do
|
|
197
200
|
|
|
@@ -236,9 +239,9 @@ It is the one command a repo needs (`pbunx @vzn/vx-migrate` in a pnpm repo works
|
|
|
236
239
|
|
|
237
240
|
- 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`.
|
|
238
241
|
- 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.
|
|
239
|
-
- 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()
|
|
242
|
+
- 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()`; with none yet, the one its install writes), `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.
|
|
240
243
|
- 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.
|
|
241
|
-
- 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
|
|
244
|
+
- 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, the configs stay written, and vx-migrate exits 1 (a run reads only the lock), as `vx init` does. `--no-install` skips it, since the plugins the workspace file declares are not installed yet, and `next` then starts with `vx lock`.
|
|
242
245
|
|
|
243
246
|
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.
|
|
244
247
|
|
|
@@ -246,13 +249,13 @@ Your `package.json` scripts are never edited. `--dry` installs nothing and says
|
|
|
246
249
|
|
|
247
250
|
`--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).
|
|
248
251
|
|
|
249
|
-
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.
|
|
252
|
+
Every source hands a plan to core's `applyMigration` from `@vzn/vx`), which renders, refuses to overwrite without `--force`, writes and reports. No file is written through a symlink, `--keep`'s workspace file and `.vx/.gitignore` included: a target that is a link, a dangling one too, is refused before anything is installed or written. Anything a source cannot say becomes a `TODO(vx-migrate)` comment, never a silent wrong value.
|
|
250
253
|
|
|
251
254
|
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.
|
|
252
255
|
|
|
253
256
|
### Turbo
|
|
254
257
|
|
|
255
|
-
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
|
|
258
|
+
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 no lockfile, so until a plugin owns it a dependency bump re-runs nothing.
|
|
256
259
|
|
|
257
260
|
| Turborepo | vx |
|
|
258
261
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -290,8 +293,8 @@ Detected when the root `package.json` lists `vite-plus`, after turbo.json and Nx
|
|
|
290
293
|
Where no runner's config is the source (or under `--from scripts`), each
|
|
291
294
|
package's `package.json` scripts become its `vx.config.ts`, plus a
|
|
292
295
|
`.vx/workspace.ts` that declares the `@vzn/vx-lockfile` plugin for the lockfile at
|
|
293
|
-
the root (`bun()`, `pnpm()`, `npm()` or `yarn()`;
|
|
294
|
-
|
|
296
|
+
the root (`bun()`, `pnpm()`, `npm()` or `yarn()`; with none yet, the
|
|
297
|
+
one its install writes; `{ plugins: [] }` under `--no-install`): package edges (`^build`, `--affected`) come from it.
|
|
295
298
|
vx runs and caches on this machine with no plugin, so it declares no executor or
|
|
296
299
|
cache, and the file carries no comment (it is the user's). When
|
|
297
300
|
`@vzn/vx-lockfile` is not installed, it installs it with that manager,
|
|
@@ -306,7 +309,10 @@ declares no workspaces … Add "workspaces": ["packages/*"] to
|
|
|
306
309
|
package.json and re-run") rather than "no scripts" or "run vx init". A
|
|
307
310
|
workspace whose globs match no `package.json` gets a note naming them
|
|
308
311
|
(M-42); a nameless member, or two sharing a name, is matched and
|
|
309
|
-
named on its own line instead (M-46)
|
|
312
|
+
named on its own line instead (M-46), and so is a member directory whose
|
|
313
|
+
name is not valid UTF-8, its bytes escaped (`packages/x\xff: the
|
|
314
|
+
directory's name is not valid UTF-8, so its package.json cannot be read —
|
|
315
|
+
rename it`): no path names it. When such skipped scripts are all there
|
|
310
316
|
is, the report's `tasks` row reads `none: no package.json script became a task` rather than
|
|
311
317
|
saying there are none (M-59). When `pnpm-workspace.yaml` lists the
|
|
312
318
|
members and the root `package.json`'s `workspaces` lists other globs,
|
|
@@ -382,7 +388,7 @@ root's `vite: workspace:*`) also waits on that package's build
|
|
|
382
388
|
edge); `dev` / `start` / `serve` / `watch` /
|
|
383
389
|
`preview` become persistent tasks, with a TODO to add `readyWhen` when
|
|
384
390
|
another task depends on one, and so does a watcher: a `watch` segment in the script's name (`build:watch`),
|
|
385
|
-
a `--watch` flag, `tsc -w` / `rollup -w`, or nodemon (D-40), and a server:
|
|
391
|
+
a `--watch` flag (not `--watch=false` / `--watchAll false`), `tsc -w` / `rollup -w`, or nodemon (D-40), and a server:
|
|
386
392
|
`serve <dir>`, `http-server`, bare `vite`, a tool's `dev` / `serve` /
|
|
387
393
|
`start` / `preview` verb (`next start`, `netlify dev`), or a script that
|
|
388
394
|
runs such a script of its package by name (`cross-env X=1 pnpm start`) or
|
|
@@ -439,7 +445,8 @@ the report says so and names `nodeLinker: node-modules` or `yarn exec
|
|
|
439
445
|
A script reading `$npm_package_version`, `$npm_package_name` or
|
|
440
446
|
`$npm_lifecycle_event`, which every manager sets and vx does not, gets
|
|
441
447
|
them under `exec.env.define`, the first two read from an imported
|
|
442
|
-
`package.json` so a version bump reaches them
|
|
448
|
+
`package.json` so a version bump reaches them (a field the manifest
|
|
449
|
+
lacks stays unset, as npm leaves it); any other `$npm_*` it
|
|
443
450
|
reads gets a TODO (D-34). Among several packages, a workspace root
|
|
444
451
|
script that runs the members (`pnpm -r build`, `--filter`, `-C`, pnpm's `pn` alias
|
|
445
452
|
included (D-97), Yarn's
|
|
@@ -532,6 +539,8 @@ The root a migration starts from and its members are the package manager's (`src
|
|
|
532
539
|
| `<file>: workspaces.packages must be an array of glob strings` | The yarn-legacy `workspaces: { packages: [...] }` form holds a non-string entry. |
|
|
533
540
|
| `<file>: <field> entry "<glob>" is an extglob, which vx's glob engine does not read` | A member glob holds `!(…)`, `@(…)`, `+(…)`, `*(…)` or `?(…)`. `Bun.Glob` has no extglob (its scan widened `packages/!(x)` to include x, turborepo#3766); a whole-segment `!(a\|b)` gets its exact rewrite, `["packages/*", "!packages/a", "!packages/b"]`. |
|
|
534
541
|
|
|
542
|
+
A member whose directory is a link out of the root (`packages/a -> ../../out/a`) is refused when the configs are written, before any is: `packages/a/vx.config.ts: outside the workspace root (packages/a resolves to <real>); vx loads no project there`. Core's discovery never loads a config there. A link to a directory inside the root is written at its real place.
|
|
543
|
+
|
|
535
544
|
## `turboCache()` — a Turbo remote cache
|
|
536
545
|
|
|
537
546
|
Store vx artifacts in any server speaking Turbo's `/v8/artifacts` API — Vercel's hosted cache or a self-hosted implementation of the published OpenAPI spec (Bearer auth, `x-artifact-duration`, HMAC-SHA256 `x-artifact-tag` signatures). The wire is theirs; the bytes are vx's own artifacts under vx's own keys. The server is storage — the other tool cannot read what vx stores there, and vx does not read its entries.
|
|
@@ -559,19 +568,19 @@ Off CI a run reads the remote and does not write it unless its policy names the
|
|
|
559
568
|
|
|
560
569
|
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. With no URL the cache is Vercel's hosted Remote Cache (`https://vercel.com/api`), exactly as 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`; one that is not a JSON object is named in a warning and read as absent, where Turbo refuses it), then the root `turbo.json`'s `remoteCache` (`apiUrl`, `teamId`, `teamSlug`), whose `enabled: false` declines unless the options name a cache. As for `turbo`, these sources link a cache only with a team: a token without one is named in a warning, and with no team the plugin **declines** and the run stays local. A team with no token takes the token from the OS keychain ([Tokens](#tokens-and-the-os-keychain)). A `url`, `token` or `oidc` option names a cache with or without a team.
|
|
561
570
|
|
|
562
|
-
| Option | Environment variable | Meaning
|
|
563
|
-
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
564
|
-
| `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
|
|
565
|
-
| `token` | `TURBO_TOKEN` | Bearer token on every request; then OIDC, `.turbo/config.json`, the OS keychain
|
|
566
|
-
| `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))
|
|
567
|
-
| `teamId` | `TURBO_TEAMID` | `teamId` query parameter (Vercel's `team_` form only, as Turbo sends it); part of the signature
|
|
568
|
-
| `teamSlug` | `TURBO_TEAM` | `slug` query parameter
|
|
569
|
-
| `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
|
|
570
|
-
| `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
|
|
571
|
-
| `uploadTimeoutMs` | `TURBO_REMOTE_CACHE_UPLOAD_TIMEOUT`, then `remoteCache.uploadTimeout` (seconds) | PUT deadline (default 60 s; 0 none), bounded as `timeoutMs`
|
|
572
|
-
| `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
|
|
573
|
-
| `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)
|
|
574
|
-
| `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)
|
|
571
|
+
| Option | Environment variable | Meaning |
|
|
572
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
573
|
+
| `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 |
|
|
574
|
+
| `token` | `TURBO_TOKEN` | Bearer token on every request; then OIDC, `.turbo/config.json`, the OS keychain |
|
|
575
|
+
| `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)) |
|
|
576
|
+
| `teamId` | `TURBO_TEAMID` | `teamId` query parameter (Vercel's `team_` form only, as Turbo sends it); part of the signature |
|
|
577
|
+
| `teamSlug` | `TURBO_TEAM` | `slug` query parameter |
|
|
578
|
+
| `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 |
|
|
579
|
+
| `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 |
|
|
580
|
+
| `uploadTimeoutMs` | `TURBO_REMOTE_CACHE_UPLOAD_TIMEOUT`, then `remoteCache.uploadTimeout` (seconds) | PUT deadline (default 60 s; 0 none), bounded as `timeoutMs` |
|
|
581
|
+
| `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 |
|
|
582
|
+
| `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; a `Location` that is not http(s), or plain http the token would go to from an https URL, fails the request (default off) |
|
|
583
|
+
| `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) |
|
|
575
584
|
|
|
576
585
|
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.
|
|
577
586
|
|
|
@@ -630,8 +639,8 @@ With no token from an option, a variable or a config file, both plugins read the
|
|
|
630
639
|
|
|
631
640
|
- 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.
|
|
632
641
|
- 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/).
|
|
633
|
-
- 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
|
|
634
|
-
- 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.
|
|
642
|
+
- 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: one a server's refusal quotes (the option's, the keychain's, an OIDC exchange's, GitHub's, a lent one) is written `***`. The run never fails because of the cache.
|
|
643
|
+
- 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, also when another machine of the run (a `@vzn/vx-dte` worker) read the bad copy. Nx's records are immutable (`409`), so `nxCache()` cannot repair one: that `put` returns `false`, so the run does not count the copy fixed.
|
|
635
644
|
- 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).
|
|
636
645
|
- 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.
|
|
637
646
|
- 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.653",
|
|
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": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@vzn/vx": "^0.0.
|
|
43
|
+
"@vzn/vx": "^0.0.653"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/migrate-scripts.ts
CHANGED
|
@@ -202,12 +202,15 @@ const MANIFEST_IMPORT = "import pkg from './package.json' with { type: 'json' }"
|
|
|
202
202
|
* and the script's name) and vx sets none, so a migrated `echo
|
|
203
203
|
* $npm_package_version` printed nothing (D-34). The name and version are
|
|
204
204
|
* read from the manifest at evaluation, so a bump reaches them; the event
|
|
205
|
-
* is the script's own name, which a folded hook does not share.
|
|
205
|
+
* is the script's own name, which a folded hook does not share. A field
|
|
206
|
+
* the manifest lacks is left unset, as npm leaves it: `pkg.version` would
|
|
207
|
+
* be undefined, which `vx lock` refuses.
|
|
206
208
|
*/
|
|
207
209
|
function npmEnv(
|
|
208
210
|
command: string,
|
|
209
211
|
script: string,
|
|
210
212
|
folded: boolean,
|
|
213
|
+
manifest: { readonly name?: unknown; readonly version?: unknown },
|
|
211
214
|
): { define: Record<string, unknown>; unset: string[]; readsManifest: boolean } {
|
|
212
215
|
const define: Record<string, unknown> = {}
|
|
213
216
|
const unset: string[] = []
|
|
@@ -215,7 +218,9 @@ function npmEnv(
|
|
|
215
218
|
for (const [, v] of command.matchAll(/\$\{?(npm_[A-Za-z0-9_]+)/g)) {
|
|
216
219
|
if (v! in define || unset.includes(v!)) continue
|
|
217
220
|
if (v === 'npm_package_name' || v === 'npm_package_version') {
|
|
218
|
-
|
|
221
|
+
const key = v === 'npm_package_name' ? 'name' : 'version'
|
|
222
|
+
if (typeof manifest[key] !== 'string') continue
|
|
223
|
+
define[v] = { raw: `pkg.${key}` }
|
|
219
224
|
readsManifest = true
|
|
220
225
|
} else if (v === 'npm_lifecycle_event' && !folded) define[v] = script
|
|
221
226
|
else unset.push(v!)
|
|
@@ -228,12 +233,13 @@ function npmEnv(
|
|
|
228
233
|
* name (`build:watch`, `watch-css`), a `--watch` flag, `tsc -w` /
|
|
229
234
|
* `rollup -w`, or nodemon. Run as a plain task it held its dependents
|
|
230
235
|
* forever. A bare `-w` elsewhere is not read: `npm run x -w pkg` is a
|
|
231
|
-
* workspace.
|
|
236
|
+
* workspace. `--watch=false` / `--watchAll false` turn watching off (CRA's
|
|
237
|
+
* `react-scripts test --watchAll=false`): a run that exits.
|
|
232
238
|
*/
|
|
233
239
|
function isWatcher(name: string, command: string): boolean {
|
|
234
240
|
if (name.split(/[:\-_.]/).includes('watch')) return true
|
|
235
241
|
return (
|
|
236
|
-
/(?:^|\s)--watch(?:All)?(?:[=\s]|$)/.test(command) ||
|
|
242
|
+
/(?:^|\s)--watch(?:All)?(?![=\s]false(?:[\s&|;]|$))(?:[=\s]|$)/.test(command) ||
|
|
237
243
|
/\b(?:tsc|rollup)\b[^&|;]*\s-w(?:\s|$)/.test(command) ||
|
|
238
244
|
/(?:^|[\s&|;])nodemon\b/.test(command)
|
|
239
245
|
)
|
|
@@ -930,7 +936,7 @@ export function migrateScripts(
|
|
|
930
936
|
)
|
|
931
937
|
}
|
|
932
938
|
const exec: Record<string, unknown> = { command }
|
|
933
|
-
const npm = npmEnv(command, name, hooks.length > 0)
|
|
939
|
+
const npm = npmEnv(command, name, hooks.length > 0, meta.packageJson)
|
|
934
940
|
if (Object.keys(npm.define).length > 0) exec['env'] = { define: npm.define }
|
|
935
941
|
if (npm.readsManifest) readsManifest = true
|
|
936
942
|
for (const v of npm.unset) {
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { mapTurboWorkspace, type TurboGlobal, type TurboMappedProject } from './
|
|
|
16
16
|
import { rootTaskProject } from './turbo/turbo-reads.js'
|
|
17
17
|
import { ROOT_ENGINES_PROBE } from './turbo/root-engines.js'
|
|
18
18
|
import { relPosix } from './paths.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
gitIgnored,
|
|
21
|
+
gitIgnoredUnder,
|
|
22
|
+
spareTrackedOutputs,
|
|
23
|
+
trackedFiles,
|
|
24
|
+
trackedKinds,
|
|
25
|
+
} from './tracked-outputs.js'
|
|
20
26
|
import { DOTENV_GLOBS_HEAD, DOTENV_PROBE, DOTENV_PROBE_TOP } from './dotenv-probe.js'
|
|
21
27
|
import { adoptedToolNotes } from './workspace-notes.js'
|
|
22
28
|
import { spelledNames } from './spelled-env.js'
|
|
@@ -54,6 +60,7 @@ export async function migrateTurbo(
|
|
|
54
60
|
ownConfig: () => `vx.config.${format}`,
|
|
55
61
|
sourceNames: (dirs) => spelledNames(root, dirs, tracked),
|
|
56
62
|
ignored: (rels) => gitIgnored(root, rels),
|
|
63
|
+
ignoredUnder: (globs) => gitIgnoredUnder(root, globs),
|
|
57
64
|
buildInfo: (rel, command) => tscBuildInfo(tsc.read, path.join(root, rel), command),
|
|
58
65
|
})
|
|
59
66
|
// A reader whose `!` hides a committed file under another task's output
|