@vzn/vx-migrate 0.0.651 → 0.0.652

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 CHANGED
@@ -30,7 +30,7 @@ The package exports `turbo`, `nx`, `turboCache`, `nxCache` and their options typ
30
30
 
31
31
  ## `turbo()` — a Turbo repo, mid-migration
32
32
 
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 directory holding both is refused, as Turbo refuses it, and a workspace root with neither is refused, naming the fix. An absolute path in `inputs`, `outputs`, `globalDependencies` or `global.inputs` is refused naming the file, as Turbo refuses it. 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). Turbo's builtin env applies as Turbo's source has it: `VERCEL_ANALYTICS_ID` and `VERCEL_TARGET_ENV` are keyed in every task (a `globalEnv` `!` entry such as `!VERCEL*` takes them back), Next's skew rule keys `VERCEL_DEPLOYMENT_ID` when `VERCEL_SKEW_PROTECTION_ENABLED=1`, and the literal names of Turbo's builtin pass-through list (`TZ`, `HOME`, `CI`, the Vercel, Docker and IDE names) reach every task unkeyed. Its wildcard families (`VERCEL_*`, `GITHUB_*`, `DOCKER_*` and the rest) are not matched against the run's environment: `exec.env.passThrough` is keyed by name, so a live match keyed each machine apart. The written config keys the defaults and `VERCEL_DEPLOYMENT_ID` always (a miss where Turbo would hit, never a stale hit) and passes the same literal names from the preset; a note names the wildcard families to add where a task reads them. 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.
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 `cache: true`, `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 directory holding both is refused, as Turbo refuses it, and a workspace root with neither is refused, naming the fix. An absolute path in `inputs`, `outputs`, `globalDependencies` or `global.inputs` is refused naming the file, as Turbo refuses it. 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). Turbo's builtin env applies as Turbo's source has it: `VERCEL_ANALYTICS_ID` and `VERCEL_TARGET_ENV` are keyed in every task (a `globalEnv` `!` entry such as `!VERCEL*` takes them back), Next's skew rule keys `VERCEL_DEPLOYMENT_ID` when `VERCEL_SKEW_PROTECTION_ENABLED=1`, and the literal names of Turbo's builtin pass-through list (`TZ`, `HOME`, `CI`, the Vercel, Docker and IDE names) reach every task unkeyed. Its wildcard families (`VERCEL_*`, `GITHUB_*`, `DOCKER_*` and the rest) are not matched against the run's environment: `exec.env.passThrough` is keyed by name, so a live match keyed each machine apart. The written config keys the defaults and `VERCEL_DEPLOYMENT_ID` always (a miss where Turbo would hit, never a stale hit) and passes the same literal names from the preset; a note names the wildcard families to add where a task reads them. 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
35
  | Option | Meaning |
36
36
  | ------ | ----------------------------------------------------------------------------------------------------------- |
@@ -62,7 +62,7 @@ Rules:
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
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/`.
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/`.
66
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.
67
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.
68
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.
@@ -70,7 +70,7 @@ Rules:
70
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
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.
72
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.
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
+ - `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
74
  - A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
75
75
  - `outputLogs: "new-only"` maps to nothing: frames for the tasks that ran and a one-liner per cache hit is vx's default flow already. The other values are per-run in vx, so they are a todo naming the flag (`vx run … --output-logs hash-only`).
76
76
  - `interruptible` maps to nothing: `vx watch` keeps a persistent task up across cycles and restarts it only when its config changes or it dies.
@@ -97,7 +97,7 @@ import { nx } from '@vzn/vx-migrate'
97
97
  export default defineWorkspace({ plugins: [nx()] })
98
98
  ```
99
99
 
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 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`.
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
101
 
102
102
  | Option | Meaning |
103
103
  | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -258,10 +258,10 @@ Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package
258
258
  | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
259
259
  | `dependsOn` | `dependsOn`, same micro-syntax (`$TURBO_ROOT$` deps are a TODO) |
260
260
  | `inputs` | `inputs.files` (`$TURBO_DEFAULT$` → `**/*`; `$TURBO_ROOT$/x` → `workspaceFiles`) |
261
- | `outputs` | `cache.outputs.files`, a negated one included |
261
+ | `outputs` | `outputs.files`, a negated one included |
262
262
  | `env` | `inputs.env` **and** `exec.env.passThrough` (child envs are isolated) |
263
263
  | `passThroughEnv` | `exec.env.passThrough` |
264
- | `cache: false` / `persistent: true` | no `cache` block, still keyed on its `inputs`; `exec.persistent: {}`, with a TODO to set `readyWhen` when a task depends on it |
264
+ | `cache: false` / `persistent: true` | no `cache: true`, still keyed on its `inputs`; `exec.persistent: {}`, with a TODO to set `readyWhen` when a task depends on it |
265
265
 
266
266
  ### Nx
267
267
 
@@ -271,19 +271,19 @@ Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.jso
271
271
 
272
272
  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.
273
273
 
274
- | Vite Task | vx |
275
- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
276
- | `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task |
277
- | `cwd` | `cd <cwd> && ` before the command |
278
- | `dependsOn: 'x'` / `'pkg#x'` | the same |
279
- | `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 |
280
- | `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 |
281
- | omitted or `{ auto: true }` (traced files) | no `cache` block, with a TODO: vx infers no inputs or outputs |
282
- | `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 |
283
- | `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env` |
284
- | `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache` block |
285
- | a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache |
286
- | `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` joined into its command; `false` keeps them tasks of their own |
274
+ | Vite Task | vx |
275
+ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
276
+ | `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task |
277
+ | `cwd` | `cd <cwd> && ` before the command |
278
+ | `dependsOn: 'x'` / `'pkg#x'` | the same |
279
+ | `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 |
280
+ | `cache.input` / `cache.output` globs | `inputs.files` / `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 |
281
+ | omitted or `{ auto: true }` (traced files) | no `cache: true`, with a TODO: vx infers no inputs or outputs |
282
+ | `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 |
283
+ | `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env` |
284
+ | `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache: true` |
285
+ | a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache |
286
+ | `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` joined into its command; `false` keeps them tasks of their own |
287
287
 
288
288
  ### package.json scripts
289
289
 
@@ -353,10 +353,10 @@ fails leaves the files as written and the note names its error.
353
353
  Each script becomes a task with its command verbatim. `build` gets
354
354
  `dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
355
355
  compile`, is a group, and the edge goes on the script that does the
356
- work) and **no cache block** — under a
356
+ work) and **no `cache: true`** — under a
357
357
  `TODO(vx-migrate)` showing the block to add with the package's real
358
358
  inputs and outputs, and the same block commented as the task's first
359
- line: set the real globs and delete the `// `. A task without a cache block always runs; a block
359
+ line: set the real globs and delete the `// `. A task without `cache: true` always runs; a block
360
360
  with EMPTY outputs is not "uncached" but a no-output task that hits on
361
361
  unchanged inputs and skips the build with nothing to restore (a deleted
362
362
  `dist` stays deleted under a green `up-to-date` run — what `vx init`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.651",
3
+ "version": "0.0.652",
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.651"
43
+ "@vzn/vx": "^0.0.652"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
@@ -118,7 +118,7 @@ export function cacheTodo(command: string): string {
118
118
  const words = ` ${command.replace(/[;&|()]/g, ' ')} `
119
119
  const hit = BUILD_OUTPUTS.find(([re]) => re.test(words))
120
120
  const outputs = (hit?.[1] ?? namedOutputs(command) ?? ['dist/**']).map((o) => `'${o}'`).join(', ')
121
- return `cache: add \`inputs: { files: ['src/**'] }, cache: { outputs: { files: [${outputs}] } }\` with this package's real inputs and outputs — without it the task always runs, and so does every task that depends on it; a block with EMPTY outputs would be a cached no-op, not an uncached task`
121
+ return `cache: add \`inputs: { files: ['src/**'] }, outputs: { files: [${outputs}] }, cache: true\` with this package's real inputs and outputs — without it the task always runs, and so does every task that depends on it; a block with EMPTY outputs would be a cached no-op, not an uncached task`
122
122
  }
123
123
 
124
124
  // `lint` is not here: a linter reads sources, and an edge to `build`
@@ -986,7 +986,7 @@ export function migrateScripts(
986
986
  // Where to add one is the TODO's to say, when there is one: an Nx repo
987
987
  // whose only script is `codegen` read "`build` carries a TODO" over
988
988
  // "0 TODOs" (the first-five-minutes walk, 2026-09-28).
989
- 'each script became a task with its command verbatim; caching needs declared inputs and outputs, so no task got a cache block',
989
+ 'each script became a task with its command verbatim; caching needs declared inputs and outputs, so no task got `cache: true`',
990
990
  ],
991
991
  projects,
992
992
  extraFiles: [],
package/src/nx/nx-map.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  resolveSharedWorkspaceOutputs,
35
35
  wildcardOutput,
36
36
  wildcardTodo,
37
+ uncache,
37
38
  } from '../shared-outputs.js'
38
39
  import type { TrackedKinds } from '../tracked-outputs.js'
39
40
  import { isRecord, packageScripts, relPosix } from '../paths.js'
@@ -596,7 +597,7 @@ export async function mapNxWorkspace(
596
597
  dropUnheldDeps(t, meta.name, emitted, emittedIds)
597
598
  const reader = dependentReaders.get(t)
598
599
  if (reader !== undefined && !reachesProducer(t, meta.name, emittedIds, reached)) {
599
- delete t.task!['cache']
600
+ uncache(t.task!)
600
601
  t.todos.push(orphanReaderTodo(reader.globs, reader.dependsOn))
601
602
  }
602
603
  }
@@ -663,10 +664,7 @@ function resolveAtomizedWorkspaceOutputs(
663
664
  const cached = (ps: readonly { name: string; dir: string; tasks: GeneratedTask[] }[]): number => {
664
665
  const copy = structuredClone(ps.map((p) => ({ name: p.name, dir: p.dir, tasks: p.tasks })))
665
666
  resolveSharedWorkspaceOutputs(root, copy)
666
- return copy.reduce(
667
- (n, p) => n + p.tasks.filter((t) => t.task?.['cache'] !== undefined).length,
668
- 0,
669
- )
667
+ return copy.reduce((n, p) => n + p.tasks.filter((t) => t.task?.['cache'] === true).length, 0)
670
668
  }
671
669
  const better = split.size > 0 && cached(last) > cached(projects)
672
670
  resolveSharedWorkspaceOutputs(root, better ? last : projects)
@@ -1061,7 +1059,8 @@ function buildTask(
1061
1059
  if (cacheEnabled) {
1062
1060
  const outputs: Record<string, unknown> = { files: takingBack(outFiles) }
1063
1061
  if (wsOutFiles.length > 0) outputs.workspaceFiles = takingBack(wsOutFiles)
1064
- task.cache = { outputs }
1062
+ task.outputs = outputs
1063
+ task.cache = true
1065
1064
  }
1066
1065
 
1067
1066
  const generated: GeneratedTask = { name: variant.name, todos, task }
@@ -179,8 +179,8 @@ interface Cached {
179
179
  function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
180
180
  const task = t.task
181
181
  if (task === null) return null
182
- const cache = task['cache'] as { outputs?: { files?: unknown } } | undefined
183
- const files = cache?.outputs?.files
182
+ const outputs = task['outputs'] as { files?: unknown } | undefined
183
+ const files = outputs?.files
184
184
  if (!Array.isArray(files)) return null
185
185
  // Where a task's outputs might land is its positive globs, as core reads
186
186
  // them: a `!` compiled as a glob is true of every other path.
@@ -267,9 +267,7 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
267
267
  for (const { k, gb } of pairs) {
268
268
  if (k.back.has(gb)) continue
269
269
  k.back.add(gb)
270
- ;(tasks[k.index]!.task!['cache'] as { outputs: { files: string[] } }).outputs.files.push(
271
- `!${gb}`,
272
- )
270
+ ;(tasks[k.index]!.task!['outputs'] as { files: string[] }).files.push(`!${gb}`)
273
271
  }
274
272
  kept.push(c)
275
273
  continue
@@ -289,14 +287,14 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
289
287
  sharedFiles.every((g) => isLiteralPattern(g) && TSBUILDINFO.test(g))
290
288
  ) {
291
289
  c.files = c.files.filter((g) => !sharedFiles.includes(g))
292
- const outputs = (t.task!['cache'] as { outputs: { files: string[] } }).outputs
290
+ const outputs = t.task!['outputs'] as { files: string[] }
293
291
  outputs.files = outputs.files.filter((g) => !sharedFiles.includes(g))
294
292
  kept.push(c)
295
293
  continue
296
294
  }
297
295
  dropped.add(c.index)
298
296
  const shared = c.files.find((gb) => clash.files.some((ga) => hits(clash, ga, gb)))!
299
- delete t.task!['cache']
297
+ uncache(t.task!)
300
298
  t.todos.push(
301
299
  `declares the output ${JSON.stringify(shared)} that ${JSON.stringify(clash.name)} also ` +
302
300
  "declares — vx cleans a task's outputs before it runs and before a restore, so two " +
@@ -369,10 +367,18 @@ export const isKeyedGroup = (t: { readonly task: Record<string, unknown> | null
369
367
  * alone, its incomplete key still let a dependant hit stale.
370
368
  */
371
369
  export function unkey(task: Record<string, unknown>): void {
372
- delete task['cache']
370
+ uncache(task)
373
371
  delete task['inputs']
374
372
  }
375
373
 
374
+ /** No cache: `outputs` and `inputs.stability` go with it, as they mean nothing without. */
375
+ export function uncache(task: Record<string, unknown>): void {
376
+ delete task['cache']
377
+ delete task['outputs']
378
+ const inputs = task['inputs'] as Record<string, unknown> | undefined
379
+ if (inputs !== undefined) delete inputs['stability']
380
+ }
381
+
376
382
  /** A task's `inputs` block, when it has one with a `files` list. */
377
383
  function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown } | undefined {
378
384
  const inputs = t.task?.['inputs'] as { files?: unknown } | undefined
@@ -457,11 +463,11 @@ export function resolveSharedWorkspaceOutputs(
457
463
  for (const p of projects) {
458
464
  const rel = relPosix(root, p.dir)
459
465
  for (const t of p.tasks) {
460
- const cache = t.task?.['cache'] as
461
- | { outputs?: { files?: unknown; workspaceFiles?: unknown } }
466
+ const outputs = t.task?.['outputs'] as
467
+ | { files?: unknown; workspaceFiles?: unknown }
462
468
  | undefined
463
- const ws = positive(cache?.outputs?.workspaceFiles)
464
- const own = positive(cache?.outputs?.files).map((g) =>
469
+ const ws = positive(outputs?.workspaceFiles)
470
+ const own = positive(outputs?.files).map((g) =>
465
471
  rel === '' ? g.replace(/^(\.\/)+/, '') : `${rel}/${g.replace(/^(\.\/)+/, '')}`,
466
472
  )
467
473
  if (ws.length === 0 && own.length === 0) continue
@@ -485,7 +491,7 @@ export function resolveSharedWorkspaceOutputs(
485
491
  kept.push({ id: `${p.name}#${t.name}`, project: p.name, ws, own })
486
492
  continue
487
493
  }
488
- delete t.task!['cache']
494
+ uncache(t.task!)
489
495
  t.todos.push(
490
496
  `declares the workspace output ${JSON.stringify(shared)} that ${clash.id} also declares — ` +
491
497
  "vx cleans a task's outputs before it runs and before a restore, so two cached tasks " +
@@ -525,9 +531,9 @@ export function excludeWorkspaceOutputs(
525
531
  for (const p of projects) {
526
532
  const rel = relPosix(root, p.dir)
527
533
  for (const t of p.tasks) {
528
- const outputs = (
529
- t.task?.['cache'] as { outputs?: { files?: unknown; workspaceFiles?: unknown } } | undefined
530
- )?.outputs
534
+ const outputs = t.task?.['outputs'] as
535
+ | { files?: unknown; workspaceFiles?: unknown }
536
+ | undefined
531
537
  const globs = [
532
538
  ...positive(outputs?.workspaceFiles),
533
539
  ...positive(outputs?.files).map((g) =>
@@ -266,7 +266,7 @@ export function spareTrackedOutputs(
266
266
  const rel = relPosix(root, p.dir)
267
267
  const own = rel === '' ? tracked : filesUnder(tracked, rel).map((f) => f.slice(rel.length + 1))
268
268
  for (const t of p.tasks) {
269
- const outputs = (t.task?.['cache'] as { outputs?: Outputs } | undefined)?.outputs
269
+ const outputs = t.task?.['outputs'] as Outputs | undefined
270
270
  if (outputs === undefined) continue
271
271
  const hits: [key: 'files' | 'workspaceFiles', hit: string[]][] = []
272
272
  for (const [key, base] of [
@@ -24,6 +24,7 @@ import {
24
24
  resolveSharedOutputs,
25
25
  resolveSharedWorkspaceOutputs,
26
26
  takingBack,
27
+ uncache,
27
28
  unkey,
28
29
  wildcardCleanTodo,
29
30
  wildcardOutput,
@@ -1606,7 +1607,7 @@ function sortOrderFree(projects: readonly TurboMappedProject[]): void {
1606
1607
  const task = t.task
1607
1608
  if (task === null) continue
1608
1609
  if (Array.isArray(task['dependsOn'])) task['dependsOn'] = [...task['dependsOn']].sort(byText)
1609
- const outputs = (task['cache'] as { outputs?: Record<string, unknown> } | undefined)?.outputs
1610
+ const outputs = task['outputs'] as Record<string, unknown> | undefined
1610
1611
  if (outputs === undefined) continue
1611
1612
  for (const k of ['files', 'workspaceFiles'])
1612
1613
  if (Array.isArray(outputs[k])) outputs[k] = [...outputs[k]].sort(negationsLast)
@@ -1822,16 +1823,12 @@ function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
1822
1823
  if (task === null) return t
1823
1824
  const exec = task['exec'] as Record<string, unknown>
1824
1825
  if (exec['persistent'] !== undefined || exec['interactive'] !== undefined) return t
1825
- const cache = task['cache'] as
1826
- | { outputs: { files: unknown[]; workspaceFiles?: unknown[] } }
1827
- | undefined
1826
+ const outputs = task['outputs'] as { files: unknown[]; workspaceFiles?: unknown[] } | undefined
1828
1827
  if (task['inputs'] === undefined && task['dependsOn'] === undefined) return t
1829
- if (
1830
- cache !== undefined &&
1831
- (cache.outputs.files.length > 0 || cache.outputs.workspaceFiles !== undefined)
1832
- )
1828
+ if (outputs !== undefined && (outputs.files.length > 0 || outputs.workspaceFiles !== undefined))
1833
1829
  return t
1834
- const { exec: _exec, cache: _cache, ...group } = task
1830
+ const { exec: _exec, ...group } = task
1831
+ uncache(group)
1835
1832
  return { ...t, task: group }
1836
1833
  }
1837
1834
 
@@ -2216,7 +2213,7 @@ function buildTask(
2216
2213
  } else if (o.includes('$TURBO_ROOT$')) {
2217
2214
  todos.push(
2218
2215
  `output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
2219
- 'prefix (→ cache.outputs.workspaceFiles) — map manually',
2216
+ 'prefix (→ outputs.workspaceFiles) — map manually',
2220
2217
  )
2221
2218
  } else outFiles.push(...signed(o))
2222
2219
  }
@@ -2255,9 +2252,13 @@ function buildTask(
2255
2252
  const outputs: Record<string, unknown> = { files: takingBack(outFiles).sort(negationsLast) }
2256
2253
  const ws = takingBack(wsOutFiles).sort(negationsLast)
2257
2254
  if (ws.length > 0) outputs.workspaceFiles = ws
2258
- task.cache = {
2259
- outputs,
2260
- ...(rewritesManifest ? { inputStability: { strategy: 'mutable' } } : {}),
2255
+ task.outputs = outputs
2256
+ task.cache = true
2257
+ if (rewritesManifest) {
2258
+ task.inputs = {
2259
+ ...(task.inputs as Record<string, unknown>),
2260
+ stability: { strategy: 'mutable' },
2261
+ }
2261
2262
  }
2262
2263
 
2263
2264
  return { name, todos, task, uses }
@@ -396,7 +396,7 @@ function autoTodo(what: string, known?: Record<string, unknown>): string {
396
396
  const have = known === undefined ? '' : ` (declared so far: \`${JSON.stringify(known)}\`)`
397
397
  return (
398
398
  `cache: Vite Task traced this task's ${what} and vx infers none — add ` +
399
- `\`inputs: { files: [...] }, cache: { outputs: { files: [...] } }\` with the real ones${have}; ` +
399
+ `\`inputs: { files: [...] }, outputs: { files: [...] }, cache: true\` with the real ones${have}; ` +
400
400
  'without it the task always runs'
401
401
  )
402
402
  }
@@ -481,7 +481,8 @@ function mapTask(name: string, def: TaskDef, ctx: TaskCtx): GeneratedTask {
481
481
  todos.push(ownFileTodo(ownFile))
482
482
  return { name, todos, task }
483
483
  }
484
- task['cache'] = { outputs: outputs.lists }
484
+ task['outputs'] = outputs.lists
485
+ task['cache'] = true
485
486
  return { name, todos, task }
486
487
  }
487
488