@vzn/vx-migrate 0.0.639 → 0.0.641
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 +33 -31
- package/package.json +2 -2
- package/src/adopt.ts +38 -0
- package/src/bin.ts +19 -2
- package/src/migrate-scripts.ts +1 -1
- package/src/migrate-turbo.ts +6 -9
- package/src/migrate.ts +19 -5
- package/src/nx/nx-inputs.ts +39 -5
- package/src/nx/nx-map.ts +51 -15
- package/src/nx/nx-upstream.ts +5 -5
- package/src/nx-cache/index.ts +0 -4
- package/src/shared-outputs.ts +21 -12
- package/src/tracked-outputs.ts +4 -4
- package/src/turbo/turbo-map.ts +178 -178
- package/src/turbo-cache/index.ts +1 -38
- package/src/vite-task/vite-task-map.ts +8 -6
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`
|
|
33
|
+
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every package's `build` script the way `turbo run build` would: `dependsOn` edges (`^build`, same-package deps, `pkg#task`), `inputs` as every task's key and `outputs` as the cache block, `env` / `passThroughEnv`, `cache: false`, `persistent`. Turbo's global fields (`globalDependencies`, `globalEnv`, `globalPassThroughEnv`, Turbo 1's `globalDotEnv`) are inlined into every task; a `*` env name (`NEXT_PUBLIC_*`) expands over the run's environment, as Turbo matches it; `*` is Turbo's only wildcard (`\*`, a leading `\!`, `?` and `[` are literal), and a literal vx cannot key (unkey's `NEXT_PUBLIC_\*`) is left out, reported once only when the run's environment sets it; per-package `turbo.json` overlays apply. A `turbo.jsonc` (Turbo 2.5+) is read wherever a `turbo.json` would be, at the root and in a package; a workspace root with neither is refused, naming the fix. Turbo 2's framework inference (a package on `next` has `NEXT_PUBLIC_*` and `NEXT_DEPLOYMENT_ID` hashed and passed; Turbo's whole table, `expo`, `nuxt`, `remix`, `@sveltejs/kit` and the rest, each package taking the first framework it matches, as Turbo does, and leaving out names under the platform's `TURBO_CI_VENDOR_ENV_KEY`) applies too: the prefix expands over the run's environment into that package's tasks, and a task's `!` entry takes names back (`bunx @vzn/vx-migrate`, which writes files for another environment, names each framework and its packages in a note instead). What runs is what a migration would have written, minus the file: the key a task derives here equals the key the written config would derive.
|
|
34
34
|
|
|
35
35
|
| Option | Meaning |
|
|
36
36
|
| ------ | --------------------------------------------------------------- |
|
|
@@ -61,15 +61,15 @@ Rules:
|
|
|
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`), so `vx run lint` runs Turbo's no-op nodes and exits 0 as `turbo run lint` does. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed), as it names every package the package manager lists, and `//#x` in `dependsOn` is an edge to it, named by the root `package.json` `name`; a root task's globs are the workspace's (`workspaceFiles`), since Turbo hashes a root task over the whole repo; with no root project (a root `package.json` with no `name`, or one a package holds) they are a note and such an edge a todo. Turbo runs no plain task in the root package of a monorepo, and neither does `turbo()`; in a single-package repo (no workspaces, Turbo's `non-monorepo` example) turbo.json's plain tasks run on the root package, as under Turbo.
|
|
62
62
|
- A transit node (Turbo's documented pattern, its with-vitest example: `transit: { dependsOn: ["^transit"] }` with no script anywhere, and `test: { dependsOn: ["transit"] }`) is a key-only task in each package that defines it: a keyed group (no command, nothing spawns) keyed on its inputs, with its `^transit` edge, so a dependant's `test` re-keys on its dependencies' sources as Turbo re-hashes it. A `^` task that some package has a script for, or that nothing depends on, is no transit node. A package without the script of a `^` task others run gets the same key-only task, as Turbo hashes a no-op node there: an edit to it re-keys its dependants. A `build` whose only edge is `^build` is not written: core gives every project with no `build` that node (solid's three script-less packages each got a `build` of their own). A dependency without the script whose own edges lack `^x` gets it too, so a `^x` stops there as Turbo's does (opencode's `build: { dependsOn: [] }`: core walked past to the builds below, which Turbo's `^build` never reaches).
|
|
63
63
|
- `with` (tasks Turbo runs alongside, `web#dev` with `api#dev`): an edge to each sidecar that is persistent, so vx starts the task once the sidecar has spawned and runs the sidecar only when the task runs; a sidecar that ends would be waited for, so it is a todo instead, and a pair that names each other keeps one edge (two would be a cycle). A task with no script whose `with` names persistent sidecars (Turbo's with-tailwind example: `ui` has no `dev` script, its `dev` starts `dev:styles` and `dev:components`) is a group task that depends on them.
|
|
64
|
-
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx folds each dependency's key); from a `cache: false` producer (vercel/vercel's `//#generate:cache-keys`, which records the host) the named files, or its outputs, are a `
|
|
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 folds each dependency's key); 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, 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
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()` (owner, 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 (owner, 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.
|
|
66
|
-
- `env` / `passThroughEnv`: explicit names go to `
|
|
66
|
+
- `env` / `passThroughEnv`: explicit names go to `inputs.env` (env only) and `exec.env.passThrough` (both, plus both globals); a wildcard expands over the run's environment under `turbo()`, which maps where the tasks run (a new variable name such a wildcard matches maps afresh); the CLI, which writes for another environment, lists the names the task's package and its workspace dependencies spell (a root task: the whole repo; clerk's `E2E_*`), noted once per entry, and one nothing spells is a todo; `*` is the only wildcard, as in Turbo, so `\*`, a leading `\!`, `?` and `[` are literal: unkey's `NEXT_PUBLIC_\*` names one variable, which vx cannot key, so it is dropped and reported once for the workspace with the count of tasks naming it (openstatus's root `build` env `\*` had written one todo into each of 45 configs; `turbo()`: only when the run's environment sets it, as Turbo hashes nothing otherwise); and a `!` entry (openstatus' `!NEXT_PUBLIC_VERCEL_URL`) removes the names it matches from its list, with no todo, since vx matches no name it is not given. A name both a global list and the task's own list carry is listed once. A name that is no shell variable name (`p-q`) is keyed but left out of `exec.env.passThrough`, as `sh` drops it before the task runs.
|
|
67
67
|
- Framework inference (Turbo hashes and passes Next's `NEXT_PUBLIC_*`, Vite's `VITE_*`, … for a package that depends on the framework): `turbo()` expands the prefix over the run's environment; the CLI writes the names the tracked source and `.env.example` of the package and its workspace dependencies spell (the bare prefix, as a `startsWith` test spells it, is none) (Next bundles theirs: cal.com's web, 23 alone, 58 with them), and a note asks for any only an installed dependency reads.
|
|
68
68
|
|
|
69
69
|
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — `build` keeps its cache, else the task with a `^` edge, else the first declared (typescript-eslint's `typecheck` has `^typecheck` and writes declarations into `build`'s `dist`: keeping it left `build` uncached); the rest run uncached with a todo naming the keeper and the fix, their own output path. A shared tsc incremental file (create-t3-turbo's `build` and `typecheck` both on `.cache/tsbuildinfo.json`) stays with the keeper alone, and the other task keeps its cache when it emits nothing else (a typecheck); one that emits more runs uncached, as `tsc --incremental` skips files the keeper's tsbuildinfo calls built. A keeper whose output is a directory at any depth (solid's build: `**/dist`) meets a sibling's tree (`coverage/**`) only at a `dist` inside it: the keeper takes that tree back with `!coverage/**` and both keep their cache, as under Turbo. Same rule for Nx targets.
|
|
70
|
-
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): vx
|
|
70
|
+
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): `vx lock` refuses it, since such a key cannot be known before the producer ran. Each overlapping output is taken back from the reader's inputs with `!` (`['**/*', '!dist/**']`), in package and workspace inputs alike, and a workspace output another package writes into this one is taken back from its package inputs too; Turbo and Nx hash what git tracks, and a build's outputs are not, so the key reads what the adopted tool read. A literal input another task writes cannot be taken back: that task runs uncached with a todo. A committed file under such an output is still a source: a reader the `!` would hide it from runs uncached with a todo. A keyed group (an `nx-input:<name>` twin, a transit node) runs nothing, so core lets it read what a task writes: it gets no `!` for a literal, the `!` that would hide a committed file goes, and it keeps its key (typescript-eslint's ast-spec commits the `*.shot` snapshots its `test` outputs, and its twin was written as `{}`, which core refuses). An output that names one committed file (eslint's `eslint-suppressions.json` in a `lint` task's outputs) is dropped, and so is the `!` each reader got for it: the file stays in every key, as Turbo and Nx hash it, and the readers stay cached. Same for Nx targets.
|
|
71
71
|
- **Two packages' tasks on one workspace output** (cal.com's shared `post-install` writes `../../node_modules/@prisma/client/**` from every package with the script): core refuses two cached tasks on one path with no edge between them, so `turbo()` (and `nx()`) keeps the first (in package and task order) cached and runs the rest uncached, each with a todo naming the keeper. A package's own outputs count at their workspace path: typescript-eslint's root project caches `dist` and each package's typecheck `dist/packages/<name>`, one path twice. Edges across packages are not read, so an ordered pair loses its cache too.
|
|
72
|
-
- `cache: false` or `persistent: true` → no `cache` block; a persistent task gets `exec.persistent: {}` and, when some task depends on it, the consumer's `persistentTodo`.
|
|
72
|
+
- `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
73
|
- A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
|
|
74
74
|
- `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`).
|
|
75
75
|
- `interruptible` maps to nothing: `vx watch` keeps a persistent task up across cycles and restarts it only when its config changes or it dies.
|
|
@@ -91,7 +91,7 @@ import { nx } from '@vzn/vx-migrate'
|
|
|
91
91
|
export default defineWorkspace({ plugins: [nx()] })
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line.
|
|
94
|
+
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line. Every target is keyed on its `inputs` (a target Nx does not cache on its own inputs alone, no `^` named inputs, since Nx folds 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. Each project some reader reaches gets an `nx-input:<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 folds 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) folds the same way: its twin, `nx-input:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins spawn nothing and stay out of a run's output and task count, so `vx run build` counts the tasks `nx run-many -t build` does. A path input inside an output one of the project's own targets declares (TanStack/table's `public` input lists `{projectRoot}/dist`) is generated, and gitignored where Nx caches it: Nx's file map skips gitignored files, so Nx hashes nothing there (a changed `dist` file is a cache hit under Nx 23.3), and it is dropped without a todo, and the task that writes it keys its dependants through `dependsOn`.
|
|
95
95
|
|
|
96
96
|
| Option | Meaning |
|
|
97
97
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -143,7 +143,7 @@ Each function (`NxExecutorTranslator`) gets one target, an `NxExecutorTarget`: `
|
|
|
143
143
|
|
|
144
144
|
### `.env` files
|
|
145
145
|
|
|
146
|
-
Nx loads a task's `.env` files into its environment — the project's before the workspace root's, the most specific name first (`.env.build.production.local`, `.env.build.production`, …, `.env.local`, `.local.env`, `.env`), a grouped target's named by its group's owner and parent (cypress's atomized `e2e-ci--a.cy.ts` loads `.env.e2e-ci` and `.env.e2e`), the first to define a name winning and the environment winning over every file — unless `NX_LOAD_DOT_ENV_FILES=false`. `nx()` finds the ones that exist from one listing of each project dir per run (about 4 ms at 1,000 projects) and the task loads them when it runs, with Nx's own parser: a shell line runs under `nx-env --dotenv <file>… --`, an executor line passes `--dotenv <file>` to `nx-exec`. Their values never enter a config, so a `.env.local` secret is not in `vx show`, `.vx/projects.lock.json` or a migrated `vx.config.ts`. A cached task keys on their bytes through a `
|
|
146
|
+
Nx loads a task's `.env` files into its environment — the project's before the workspace root's, the most specific name first (`.env.build.production.local`, `.env.build.production`, …, `.env.local`, `.local.env`, `.env`), a grouped target's named by its group's owner and parent (cypress's atomized `e2e-ci--a.cy.ts` loads `.env.e2e-ci` and `.env.e2e`), the first to define a name winning and the environment winning over every file — unless `NX_LOAD_DOT_ENV_FILES=false`. `nx()` finds the ones that exist from one listing of each project dir per run (about 4 ms at 1,000 projects) and the task loads them when it runs, with Nx's own parser: a shell line runs under `nx-env --dotenv <file>… --`, an executor line passes `--dotenv <file>` to `nx-exec`. Their values never enter a config, so a `.env.local` secret is not in `vx show`, `.vx/projects.lock.json` or a migrated `vx.config.ts`. A cached task keys on their bytes through a `inputs.runtime` probe (`for f in …; do echo "$f"; cat -- "$f"; …`), which sees a gitignored file a glob would not; a file added or removed changes the command, and so the key, on the next run. `tests/nx-exec-live.test.ts` compares what the line sees with what `nx run` gives the same target.
|
|
147
147
|
|
|
148
148
|
### The target a task sees
|
|
149
149
|
|
|
@@ -151,7 +151,9 @@ Every task gets `NX_TASK_TARGET_PROJECT`, `NX_TASK_TARGET_TARGET`, `LERNA_PACKAG
|
|
|
151
151
|
|
|
152
152
|
### Inputs that key nothing here
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
Nx hashes the root TypeScript config (`tsconfig.base.json`, else `tsconfig.json`) into every task, so every mapped task lists it in `inputs.workspaceFiles`: a `compilerOptions` edit re-runs it, as under Nx. vx keys the whole file, where Nx leaves other projects' `paths` entries out, so a `paths` edit re-runs every task.
|
|
155
|
+
|
|
156
|
+
`{ dependentTasksOutputFiles }` hashes the outputs of the tasks a target depends on; vx folds those tasks' keys through `dependsOn`, and an output follows from its inputs. A target that reads them with no `dependsOn` edge left to a migrated task (its producer is a project vx did not migrate, such as a .NET project) is written uncached, with a todo naming the producer: its key would see nothing it reads. `{ workingDirectory }` hashes the directory Nx was started from; a vx task runs in its project dir from wherever vx is started. Neither is a todo.
|
|
155
157
|
|
|
156
158
|
### The environment a task sees
|
|
157
159
|
|
|
@@ -229,7 +231,7 @@ It is the one command a repo needs (`pnpx @vzn/vx-migrate` in a pnpm repo works
|
|
|
229
231
|
- 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`.
|
|
230
232
|
- 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.
|
|
231
233
|
- With no `.vx/workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the remote cache above, the `@vzn/vx-lockfile` factory for the lockfile (`pnpm()`, `bun()`, `npm()`, `yarn()`), `scheduleHistoryPlugin()`, and `github()` when `.github/workflows` exists, from `@vzn/vx-ci`, 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.
|
|
232
|
-
- It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, the
|
|
234
|
+
- It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, every `@vzn/vx-*` package the workspace file imports (one an earlier init wrote included), and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w`, `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`, `yarn remove` (`-W` on Yarn 1), `bun remove`, `npm uninstall`. `--no-install` skips both. 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.
|
|
233
235
|
- Native, once the files are written, it ends with `vx lock`: `.vx/projects.lock.json` is written and the graph rules are checked, its line the report's `lock` row. A refusal prints above the report and the configs stay written (exit 0). `--no-install` skips it, since the plugins the workspace file declares are not installed yet, and `next` then starts with `vx lock`.
|
|
234
236
|
|
|
235
237
|
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.
|
|
@@ -246,14 +248,14 @@ The report reads like a run's footer: a blank line, the `─ vx-migrate <version
|
|
|
246
248
|
|
|
247
249
|
Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package `turbo.json` `extends` overlays and each package's scripts, through the same mapper `turbo()` runs live — so the written configs say what the plugin was doing. Turbo's global fields become a generated root `vx-preset.ts` each config imports and spreads: TypeScript composition replaces global config. A config that a `tsconfig*.json` in its directory or above (below the root) takes in, where that project is composite or sets `rootDir`, declares the values it uses instead: `tsc` refuses a file outside the package (TS6059, TS6307; withastro/astro's `scripts/` and the `performance/` project its `packages/astro/tsconfig.test.json` takes in). So does a task's `env` that several configs share (`buildEnv`), where inline it would repeat in every package (vercel/ai: 63 names, twice in each of ~100 configs, half the output). Turbo's `//#` root tasks are written to a `vx.config.ts` at the workspace root, which makes the root a project (core's D-39), and a package task's `//#x` edge reaches it. When a `.vx/workspace.ts` already exists without one, the report names the `@vzn/vx-lockfile` plugin for the repo's lockfile: Turbo keys each package on its own lockfile entries, and core keys every task on the whole file until a plugin claims it.
|
|
248
250
|
|
|
249
|
-
| Turborepo | vx
|
|
250
|
-
| ----------------------------------- |
|
|
251
|
-
| `dependsOn` | `dependsOn`, same micro-syntax (`$TURBO_ROOT$` deps are a TODO)
|
|
252
|
-
| `inputs` | `
|
|
253
|
-
| `outputs` | `cache.outputs.files`, a negated one included
|
|
254
|
-
| `env` | `
|
|
255
|
-
| `passThroughEnv` | `exec.env.passThrough`
|
|
256
|
-
| `cache: false` / `persistent: true` | no `cache` block
|
|
251
|
+
| Turborepo | vx |
|
|
252
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
253
|
+
| `dependsOn` | `dependsOn`, same micro-syntax (`$TURBO_ROOT$` deps are a TODO) |
|
|
254
|
+
| `inputs` | `inputs.files` (`$TURBO_DEFAULT$` → `**/*`; `$TURBO_ROOT$/x` → `workspaceFiles`) |
|
|
255
|
+
| `outputs` | `cache.outputs.files`, a negated one included |
|
|
256
|
+
| `env` | `inputs.env` **and** `exec.env.passThrough` (child envs are isolated) |
|
|
257
|
+
| `passThroughEnv` | `exec.env.passThrough` |
|
|
258
|
+
| `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 |
|
|
257
259
|
|
|
258
260
|
### Nx
|
|
259
261
|
|
|
@@ -263,19 +265,19 @@ Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.jso
|
|
|
263
265
|
|
|
264
266
|
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.
|
|
265
267
|
|
|
266
|
-
| Vite Task | vx
|
|
267
|
-
| --------------------------------------------------------- |
|
|
268
|
-
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task
|
|
269
|
-
| `cwd` | `cd <cwd> && ` before the command
|
|
270
|
-
| `dependsOn: 'x'` / `'pkg#x'` | the same
|
|
271
|
-
| `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
|
|
272
|
-
| `cache.input` / `cache.output` globs | `
|
|
273
|
-
| omitted or `{ auto: true }` (traced files) | no `cache` block, with a TODO: vx infers no inputs or outputs
|
|
274
|
-
| `cache.env` | `
|
|
275
|
-
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env`
|
|
276
|
-
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache` block
|
|
277
|
-
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache
|
|
278
|
-
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` folded into its command; `false` keeps them tasks of their own
|
|
268
|
+
| Vite Task | vx |
|
|
269
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
270
|
+
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task |
|
|
271
|
+
| `cwd` | `cd <cwd> && ` before the command |
|
|
272
|
+
| `dependsOn: 'x'` / `'pkg#x'` | the same |
|
|
273
|
+
| `dependsOn: { task, from }` | `^task`; when a member listed under a field `from` leaves out also has the task, the explicit `pkg#task` edges, since `^` follows every field |
|
|
274
|
+
| `cache.input` / `cache.output` globs | `inputs.files` / `cache.outputs.files`; `base: 'workspace'` → `workspaceFiles`, a root task's too |
|
|
275
|
+
| omitted or `{ auto: true }` (traced files) | no `cache` block, with a TODO: vx infers no inputs or outputs |
|
|
276
|
+
| `cache.env` | `inputs.env` **and** `exec.env.passThrough`; a `*` entry lists the names the package's tracked files and its workspace dependencies' spell (a TODO when none match); `!` entries take names back |
|
|
277
|
+
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env` |
|
|
278
|
+
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache` block |
|
|
279
|
+
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache |
|
|
280
|
+
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` folded into its command; `false` keeps them tasks of their own |
|
|
279
281
|
|
|
280
282
|
### package.json scripts
|
|
281
283
|
|
|
@@ -598,7 +600,7 @@ The Nx spec has no existence probe, so `has` (the `--dry` prediction; the prefet
|
|
|
598
600
|
- 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.
|
|
599
601
|
- A refused token (`401`/`403`) warns **once** and turns the layer off for the rest of the process (a `403` on an upload is a read-only token — Nx's spec, or turborepo-remote-cache's `READ_ONLY` and write-less JWTs: it turns off uploads alone, and reads go on) — including the requests already in flight when the refusal lands, which degrade in silence rather than repeating it (a six-project run printed five identical lines before 2026-09-20).
|
|
600
602
|
- Policy (`--cache-policy=remote:r`, `VX_CACHE_POLICY`, `cache.policy`) is enforced by core, which layers each plugin's remote over the local cache — a read-only token pairs naturally with `remote:r`.
|
|
601
|
-
-
|
|
603
|
+
- Core's policy alone decides reads and writes: the plugins read none of Turbo's or Nx's own switches (`TURBO_CACHE`, `TURBO_REMOTE_CACHE_READ_ONLY`, `NX_SKIP_REMOTE_CACHE`, `NX_DISABLE_REMOTE_CACHE`). A CI that keeps untrusted pull requests off the shared cache sets `cache.policy` or `--cache-policy=local:rw,remote:r` for them.
|
|
602
604
|
|
|
603
605
|
## Testing
|
|
604
606
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.641",
|
|
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.641"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/adopt.ts
CHANGED
|
@@ -152,6 +152,44 @@ export function listedPackages(root: string, packages: readonly string[]): strin
|
|
|
152
152
|
)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The `@vzn/vx-*` plugins the root lists from the registry (a `workspace:`
|
|
157
|
+
* or `file:` link is the repo's own; a platform binary comes with core).
|
|
158
|
+
* Every @vzn package releases together: an install moves them with core,
|
|
159
|
+
* as `vx upgrade` does (M, 2026-10-16: "always install latest and upgrade all").
|
|
160
|
+
*/
|
|
161
|
+
export function listedPlugins(root: string): string[] {
|
|
162
|
+
const pj = JSON.parse(readText(path.join(root, 'package.json')) || '{}') as Record<
|
|
163
|
+
string,
|
|
164
|
+
Record<string, unknown> | undefined
|
|
165
|
+
>
|
|
166
|
+
const plugins = new Set<string>()
|
|
167
|
+
for (const f of ['dependencies', 'devDependencies']) {
|
|
168
|
+
for (const [name, spec] of Object.entries(pj[f] ?? {})) {
|
|
169
|
+
if (!name.startsWith('@vzn/vx-') || /^@vzn\/vx-(?:darwin|linux)-(?:x64|arm64)$/.test(name))
|
|
170
|
+
continue
|
|
171
|
+
if (typeof spec === 'string' && !/^(?:workspace|file|link|portal):/.test(spec))
|
|
172
|
+
plugins.add(name)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [...plugins].sort()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The `@vzn/vx*` packages a workspace file imports, parsed, not matched (a
|
|
180
|
+
* formatter's quotes hid a match). Each is installed with core: a file an
|
|
181
|
+
* earlier init wrote imports @vzn/vx-lockfile that package.json need not
|
|
182
|
+
* list, and the lock could not find it (M, 2026-10-16).
|
|
183
|
+
*/
|
|
184
|
+
export function importedPackages(text: string): string[] {
|
|
185
|
+
const out = new Set<string>()
|
|
186
|
+
for (const i of new Bun.Transpiler({ loader: 'ts' }).scanImports(text)) {
|
|
187
|
+
const m = /^(@vzn\/vx(?:-[^/]+)?)(?:\/|$)/.exec(i.path)
|
|
188
|
+
if (m !== null) out.add(m[1]!)
|
|
189
|
+
}
|
|
190
|
+
return [...out].sort()
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
/** Install `packages` (at `version`, given one) with the repo's manager; its output only on failure. */
|
|
156
194
|
export function install(
|
|
157
195
|
root: string,
|
package/src/bin.ts
CHANGED
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
// `bunx @vzn/vx-migrate` — the adoption tool's own entry. Core's error
|
|
3
3
|
// classes are honoured the way `vx` honours them: a UserError is one line
|
|
4
4
|
// on stderr, anything else is a crash worth a stack.
|
|
5
|
+
//
|
|
6
|
+
// Only what every core release exports is imported before the release
|
|
7
|
+
// check: `bun x` keeps the @vzn/vx already in its install directory beside
|
|
8
|
+
// a newer vx-migrate (a peer range it does not enforce), and a static
|
|
9
|
+
// import of the mapper crashed on a missing export there (Growth,
|
|
10
|
+
// 2026-10-16).
|
|
5
11
|
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { isUserError, UserError, VERSION } from '@vzn/vx'
|
|
14
|
+
import { ownVersion } from './adopt.js'
|
|
8
15
|
|
|
9
16
|
try {
|
|
17
|
+
const own = ownVersion()
|
|
18
|
+
if (own !== null && VERSION !== '0.0.0' && VERSION !== own) {
|
|
19
|
+
const core = Bun.resolveSync('@vzn/vx', import.meta.dir)
|
|
20
|
+
const at = core.lastIndexOf(`${path.sep}node_modules${path.sep}`)
|
|
21
|
+
const modules = at === -1 ? path.dirname(core) : core.slice(0, at + '/node_modules'.length)
|
|
22
|
+
throw new UserError(
|
|
23
|
+
`vx-migrate ${own} loaded @vzn/vx ${VERSION} from ${modules}; every @vzn package releases together — install @vzn/vx@${own} there, or delete that node_modules and run it again`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
const { migrateCmd } = await import('./migrate.js')
|
|
10
27
|
process.exitCode = await migrateCmd(process.argv.slice(2))
|
|
11
28
|
} catch (err) {
|
|
12
29
|
if (isUserError(err)) {
|
package/src/migrate-scripts.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function cacheTodo(command: string): string {
|
|
|
117
117
|
const words = ` ${command.replace(/[;&|()]/g, ' ')} `
|
|
118
118
|
const hit = BUILD_OUTPUTS.find(([re]) => re.test(words))
|
|
119
119
|
const outputs = (hit?.[1] ?? namedOutputs(command) ?? ['dist/**']).map((o) => `'${o}'`).join(', ')
|
|
120
|
-
return `cache: add \`
|
|
120
|
+
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
121
|
}
|
|
122
122
|
|
|
123
123
|
// `lint` is not here: a linter reads sources, and an edge to `build`
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -166,7 +166,7 @@ const HOIST_MIN = 3
|
|
|
166
166
|
/**
|
|
167
167
|
* Turbo states a task's `env` once, in the root turbo.json; written inline,
|
|
168
168
|
* vercel/ai's 63 `build` names repeated twice in each of ~100 configs. A
|
|
169
|
-
* task's own `
|
|
169
|
+
* task's own `inputs.env` names (after the spliced globals) that two
|
|
170
170
|
* or more configs share become one preset export, spread where they stood
|
|
171
171
|
* and where they lead `exec.env.passThrough` (its `passThroughEnv` names
|
|
172
172
|
* follow): the evaluated arrays, and so every key, are unchanged. A
|
|
@@ -177,10 +177,7 @@ function hoistTaskEnv(projects: readonly TurboMappedProject[]): {
|
|
|
177
177
|
lists: SharedList[]
|
|
178
178
|
usedBy: Map<string, Set<string>>
|
|
179
179
|
} {
|
|
180
|
-
const lists = (t: Task): (unknown[] | undefined)[] => [
|
|
181
|
-
t.cache?.inputs?.env,
|
|
182
|
-
t.exec?.env?.passThrough,
|
|
183
|
-
]
|
|
180
|
+
const lists = (t: Task): (unknown[] | undefined)[] => [t.inputs?.env, t.exec?.env?.passThrough]
|
|
184
181
|
// The trailing run of names after the spliced globals, or null when the
|
|
185
182
|
// list is all names or mixes them in.
|
|
186
183
|
const own = (list: unknown[] | undefined): string[] | null => {
|
|
@@ -194,7 +191,7 @@ function hoistTaskEnv(projects: readonly TurboMappedProject[]): {
|
|
|
194
191
|
for (const p of projects)
|
|
195
192
|
for (const t of p.tasks) {
|
|
196
193
|
if (t.task === null) continue
|
|
197
|
-
const names = own((t.task as Task).
|
|
194
|
+
const names = own((t.task as Task).inputs?.env)
|
|
198
195
|
if (names === null || names.length < HOIST_MIN) continue
|
|
199
196
|
const k = keyOf(t.name, names)
|
|
200
197
|
counts.set(k, (counts.get(k) ?? 0) + 1)
|
|
@@ -219,7 +216,7 @@ function hoistTaskEnv(projects: readonly TurboMappedProject[]): {
|
|
|
219
216
|
for (const p of projects)
|
|
220
217
|
for (const t of p.tasks) {
|
|
221
218
|
if (t.task === null) continue
|
|
222
|
-
const env = own((t.task as Task).
|
|
219
|
+
const env = own((t.task as Task).inputs?.env)
|
|
223
220
|
const name = env === null ? undefined : nameOf.get(keyOf(t.name, env))
|
|
224
221
|
if (name === undefined) continue
|
|
225
222
|
for (const list of lists(t.task)) {
|
|
@@ -236,7 +233,7 @@ function hoistTaskEnv(projects: readonly TurboMappedProject[]): {
|
|
|
236
233
|
}
|
|
237
234
|
|
|
238
235
|
type Task = {
|
|
239
|
-
|
|
236
|
+
inputs?: { env?: unknown[] }
|
|
240
237
|
exec?: { env?: { passThrough?: unknown[] } }
|
|
241
238
|
}
|
|
242
239
|
|
|
@@ -289,7 +286,7 @@ function nameProbes(projects: readonly TurboMappedProject[]): {
|
|
|
289
286
|
}
|
|
290
287
|
for (const p of projects)
|
|
291
288
|
for (const t of p.tasks) {
|
|
292
|
-
const inputs =
|
|
289
|
+
const inputs = t.task?.['inputs'] as Record<string, unknown> | undefined
|
|
293
290
|
for (const field of ['runtime', 'workspaceRuntime']) {
|
|
294
291
|
const list = inputs?.[field]
|
|
295
292
|
if (!Array.isArray(list)) continue
|
package/src/migrate.ts
CHANGED
|
@@ -45,9 +45,11 @@ import { exportGraph, nxBin } from './nx/export-graph.js'
|
|
|
45
45
|
import { readNxJson } from './nx/nx-map.js'
|
|
46
46
|
import {
|
|
47
47
|
type AdoptionMode,
|
|
48
|
+
importedPackages,
|
|
48
49
|
install,
|
|
49
50
|
installArgv,
|
|
50
51
|
listedPackages,
|
|
52
|
+
listedPlugins,
|
|
51
53
|
missingPackages,
|
|
52
54
|
MODE_QUESTION,
|
|
53
55
|
ownVersion,
|
|
@@ -355,7 +357,10 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
355
357
|
// Nothing written imports vx-migrate or runs its bins: a `--keep`
|
|
356
358
|
// adoption's install of it is dropped with turbo() / nx().
|
|
357
359
|
const finalWorkspace = workspaceText ?? readFileSync(path.join(root, existingWorkspace!), 'utf8')
|
|
358
|
-
|
|
360
|
+
// Imports parsed, not matched: a formatter's double quotes hid it, and
|
|
361
|
+
// the file still imported the package removed (owner, 2026-10-16).
|
|
362
|
+
const imported = importedPackages(finalWorkspace)
|
|
363
|
+
const needsSelf = runsBins || imported.includes('@vzn/vx-migrate')
|
|
359
364
|
const cacheNote =
|
|
360
365
|
runner === 'vite-task'
|
|
361
366
|
? undefined
|
|
@@ -374,6 +379,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
374
379
|
'@vzn/vx',
|
|
375
380
|
...(needsSelf ? ['@vzn/vx-migrate'] : []),
|
|
376
381
|
...plugins.map((p) => p.pkg),
|
|
382
|
+
...imported,
|
|
377
383
|
]),
|
|
378
384
|
],
|
|
379
385
|
parsed,
|
|
@@ -490,7 +496,8 @@ async function prepareRepo(
|
|
|
490
496
|
if (args.noInstall === true) return []
|
|
491
497
|
const steps: Step[] = []
|
|
492
498
|
const version = ownVersion()
|
|
493
|
-
const
|
|
499
|
+
const all = [...new Set([...wanted, ...listedPlugins(root).filter((p) => !unwanted.includes(p))])]
|
|
500
|
+
const missing = missingPackages(root, all, version)
|
|
494
501
|
if (missing.length > 0) {
|
|
495
502
|
steps.push(
|
|
496
503
|
args.dry ? ['install', missing.join(' ')] : ['ran', await install(root, missing, version)],
|
|
@@ -536,7 +543,14 @@ async function keep(
|
|
|
536
543
|
const declares = before === undefined || extended !== null
|
|
537
544
|
const steps = await prepareRepo(
|
|
538
545
|
root,
|
|
539
|
-
[
|
|
546
|
+
[
|
|
547
|
+
...new Set([
|
|
548
|
+
'@vzn/vx',
|
|
549
|
+
'@vzn/vx-migrate',
|
|
550
|
+
...(declares ? all.map((p) => p.pkg) : []),
|
|
551
|
+
...importedPackages(text),
|
|
552
|
+
]),
|
|
553
|
+
],
|
|
540
554
|
args,
|
|
541
555
|
)
|
|
542
556
|
if (args.json)
|
|
@@ -625,8 +639,8 @@ function lockRow(lock: { code: number; said: string }): ui.FieldRow {
|
|
|
625
639
|
]
|
|
626
640
|
}
|
|
627
641
|
|
|
628
|
-
/** `.vx/.gitignore`'s text, as core writes it: the machine-local cache
|
|
629
|
-
const DOT_VX_GITIGNORE = '/cache\n'
|
|
642
|
+
/** `.vx/.gitignore`'s text, as core writes it: the machine-local cache and restore staging stay out of git. */
|
|
643
|
+
const DOT_VX_GITIGNORE = '/cache\n/tmp\n'
|
|
630
644
|
|
|
631
645
|
// npx and bunx name the package: their bare `vx` is a stranger's npm package.
|
|
632
646
|
const EXEC: Record<PackageManager, string> = {
|
package/src/nx/nx-inputs.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface NxInputs {
|
|
|
17
17
|
readonly envNames: string[]
|
|
18
18
|
/**
|
|
19
19
|
* Nx's `{ runtime: "<cmd>" }` hashes the command's output, run at the
|
|
20
|
-
* WORKSPACE ROOT (`hash_runtime.rs`): `
|
|
20
|
+
* WORKSPACE ROOT (`hash_runtime.rs`): `inputs.workspaceRuntime`.
|
|
21
21
|
* It was mapped to `runtime`, which runs in the project dir, so
|
|
22
22
|
* `cat tools/version.txt` failed the run and a command that runs in both
|
|
23
23
|
* hashed a different fact (item 913). It was once reported "not
|
|
@@ -26,10 +26,39 @@ export interface NxInputs {
|
|
|
26
26
|
readonly runtimeCmds: string[]
|
|
27
27
|
/** `^name` / `{ input, dependencies | projects }`: whose `name` to fold. */
|
|
28
28
|
readonly upstream: Array<{ readonly name: string; readonly of: 'deps' | readonly string[] }>
|
|
29
|
+
/** `{ dependentTasksOutputFiles }` globs: keyed only through a `dependsOn` edge. */
|
|
30
|
+
readonly dependentOutputs: string[]
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function emptyNxInputs(): NxInputs {
|
|
32
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
files: [],
|
|
36
|
+
wsFiles: [],
|
|
37
|
+
envNames: [],
|
|
38
|
+
runtimeCmds: [],
|
|
39
|
+
upstream: [],
|
|
40
|
+
dependentOutputs: [],
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A runtime command with its path tokens written in, as the paths from the
|
|
46
|
+
* workspace root it runs at. Nx runs the command as written and hashes its
|
|
47
|
+
* output whatever it exits, so `cat {workspaceRoot}/v.txt` keyed on cat's
|
|
48
|
+
* error there and failed the task here (Growth, 2026-10-16).
|
|
49
|
+
*
|
|
50
|
+
* vx's probe fails the task on a non-zero exit, so a failing command runs
|
|
51
|
+
* in a subshell and prints its exit code instead: the key holds the error
|
|
52
|
+
* and the code (more than Nx keys, never less), and a success prints
|
|
53
|
+
* exactly what the command does. A `#` comment would swallow the `)`, so
|
|
54
|
+
* then the subshell closes on its own line.
|
|
55
|
+
*/
|
|
56
|
+
function runtimeCommand(cmd: string, rel: string, name: string): string {
|
|
57
|
+
const c = cmd
|
|
58
|
+
.replaceAll('{workspaceRoot}', '.')
|
|
59
|
+
.replaceAll('{projectRoot}', shellQuote(rel === '' ? '.' : rel))
|
|
60
|
+
.replaceAll('{projectName}', shellQuote(name))
|
|
61
|
+
return `(${c}${c.includes('#') ? '\n' : ''}) || echo "exit $?"`
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
/** Expands `entries` into `into`; a gap is a line in `todos`. */
|
|
@@ -113,7 +142,7 @@ export function expandNxInputs(
|
|
|
113
142
|
return
|
|
114
143
|
}
|
|
115
144
|
if (typeof o.runtime === 'string') {
|
|
116
|
-
into.runtimeCmds.push(o.runtime)
|
|
145
|
+
into.runtimeCmds.push(runtimeCommand(o.runtime, at.rel, at.name))
|
|
117
146
|
return
|
|
118
147
|
}
|
|
119
148
|
if (typeof o.fileset === 'string' && o.includeIgnored === true) {
|
|
@@ -156,7 +185,12 @@ export function expandNxInputs(
|
|
|
156
185
|
// those tasks' keys (their inputs, transitively) through `dependsOn`,
|
|
157
186
|
// and an output follows from its inputs. Nothing to map, and the todo
|
|
158
187
|
// it was sat on 881 tasks of three real Nx repos (G-49).
|
|
159
|
-
|
|
188
|
+
// The mapper checks an edge is left to carry them.
|
|
189
|
+
if (o.dependentTasksOutputFiles !== undefined) {
|
|
190
|
+
const g = o.dependentTasksOutputFiles
|
|
191
|
+
into.dependentOutputs.push(typeof g === 'string' ? g : JSON.stringify(g))
|
|
192
|
+
return
|
|
193
|
+
}
|
|
160
194
|
// `{ workingDirectory }` hashes where Nx was started (`cwd:relative`
|
|
161
195
|
// in its plan). A vx task runs in its project dir from wherever vx
|
|
162
196
|
// is started, so the fact is the same for every run: nothing to key.
|
|
@@ -195,7 +229,7 @@ export function expandNxInputs(
|
|
|
195
229
|
if (deps || p === null || /[*?[{]/.test(p)) {
|
|
196
230
|
todos.push(
|
|
197
231
|
`input ${JSON.stringify(entry)}: vx keys only the files git lists, so a gitignored ` +
|
|
198
|
-
'match is not in the key — read it with a
|
|
232
|
+
'match is not in the key — read it with a inputs.workspaceRuntime probe',
|
|
199
233
|
)
|
|
200
234
|
return
|
|
201
235
|
}
|