@vzn/vx-migrate 0.0.644 → 0.0.646
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/LICENSE +1 -1
- package/README.md +14 -16
- package/package.json +2 -2
- package/src/adoption-plugin.ts +9 -11
- package/src/index.ts +36 -10
- package/src/mapping-cache.ts +14 -6
- package/src/migrate-turbo.ts +2 -6
- package/src/migrate.ts +5 -14
- package/src/nx/nx-map.ts +30 -15
- package/src/nx/nx-upstream.ts +5 -5
- package/src/nx-cache/index.ts +13 -18
- package/src/shared-outputs.ts +16 -10
- package/src/tracked-outputs.ts +10 -6
- package/src/turbo/index.ts +16 -10
- package/src/turbo/turbo-map.ts +8 -177
- package/src/turbo/turbo-reads.ts +183 -0
- package/src/turbo-cache/index.ts +107 -57
- package/src/turbo-cache/oidc.ts +31 -16
- package/src/turbo-cache/scope.ts +89 -0
- package/src/remote-scope.ts +0 -12
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ Everything for adopting [`@vzn/vx`](https://vznjs.github.io/vx/) from Turborepo,
|
|
|
10
10
|
- **`turboCache()`** and **`nxCache()`** — keep the remote cache you have: any server speaking Turbo's `/v8/artifacts` API (Vercel's hosted cache included) or Nx's self-hosted `/v1/cache` spec.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
13
|
+
bun add -d @vzn/vx @vzn/vx-migrate
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
In a Turbo or Nx repo, `
|
|
16
|
+
In a Turbo or Nx repo, `bunx @vzn/vx init --keep` runs this package, which
|
|
17
17
|
writes the `.vx/workspace.ts` that declares `turbo()` or `nx()`, installs
|
|
18
18
|
what it imports and writes the lock. Where the repo shows a remote cache it
|
|
19
19
|
declares `turboCache()` or `nxCache()` too; by hand it reads:
|
|
@@ -66,8 +66,8 @@ Rules:
|
|
|
66
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
|
-
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — `build` keeps its cache, else the task with a `^` edge, else the first declared (typescript-eslint's `typecheck` has `^typecheck` and writes declarations into `build`'s `dist`: keeping it left `build` uncached); the rest run uncached with a todo naming the keeper and the fix, their own output path. A shared tsc incremental file (create-t3-turbo's `build` and `typecheck` both on `.cache/tsbuildinfo.json`) stays with the keeper alone, and the other task keeps its cache when it emits nothing else (a typecheck); one that emits more runs uncached, as `tsc --incremental` skips files the keeper's tsbuildinfo calls built.
|
|
70
|
-
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): `vx lock` refuses it, since such a key cannot be known before the producer ran. Each overlapping output is taken back from the reader's inputs with `!` (`['**/*', '!dist/**']`), in package and workspace inputs alike, and a workspace output another package writes into this one is taken back from its package inputs too; Turbo and Nx hash what git tracks, and a build's outputs are not, so the key reads what the adopted tool read. A literal input another task writes cannot be taken back: that task runs uncached with a todo. A committed file under such an output is still a source: a reader the `!` would hide it from runs uncached with a todo. A keyed group (an `
|
|
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. 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.
|
|
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 `inputs:<name>` twin, a transit node) runs nothing, so core lets it read what a task writes: it gets no `!` for a literal, the `!` that would hide a committed file goes, and it keeps its key (typescript-eslint's ast-spec commits the `*.shot` snapshots its `test` outputs, and its twin was written as `{}`, which core refuses). An output that names one committed file (eslint's `eslint-suppressions.json` in a `lint` task's outputs) is dropped, and so is the `!` each reader got for it: the file stays in every key, as Turbo and Nx hash it, and the readers stay cached. Same for Nx targets.
|
|
71
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
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`.
|
|
@@ -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. 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 `
|
|
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 `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 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, `inputs:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins spawn nothing and stay out of a run's output and task count, so `vx run build` counts the tasks `nx run-many -t build` does. A path input inside an output one of the project's own targets declares (TanStack/table's `public` input lists `{projectRoot}/dist`) is generated, and gitignored where Nx caches it: Nx's file map skips gitignored files, so Nx hashes nothing there (a changed `dist` file is a cache hit under Nx 23.3), and it is dropped without a todo, and the task that writes it keys its dependants through `dependsOn`.
|
|
95
95
|
|
|
96
96
|
| Option | Meaning |
|
|
97
97
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -147,7 +147,7 @@ Nx loads a task's `.env` files into its environment — the project's before the
|
|
|
147
147
|
|
|
148
148
|
### The target a task sees
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
A task whose command still runs Nx or Lerna (`nx-exec`, `nx-env`, `nx exec`, `lerna`) or names one of them gets `NX_TASK_TARGET_PROJECT`, `NX_TASK_TARGET_TARGET`, `LERNA_PACKAGE_NAME` (the project, which Lerna on Nx's runner documents to scripts) and, where it runs a configuration, `NX_TASK_TARGET_CONFIGURATION` in `exec.env.define`, as Nx hands them to every task; a run-commands `env` wins. Any other task gets none. Nx's own `npm:public` / `npm:private` tags are not written. A package script's `nx exec -- <cmd>` reads them: without them it starts Nx's own task runner, which runs the target and its dependencies again.
|
|
151
151
|
|
|
152
152
|
### Inputs that key nothing here
|
|
153
153
|
|
|
@@ -226,11 +226,11 @@ bunx @vzn/vx-migrate --keep # keep turbo.json / nx.json as the source: turbo(
|
|
|
226
226
|
bunx @vzn/vx-migrate --help # the usage, exit 0
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
It is the one command a repo needs (`
|
|
229
|
+
It is the one command a repo needs (`pbunx @vzn/vx-migrate` in a pnpm repo works the same):
|
|
230
230
|
|
|
231
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`.
|
|
232
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.
|
|
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 (`
|
|
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 (`bun()`, `pnpm()`, `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.
|
|
234
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.
|
|
235
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`.
|
|
236
236
|
|
|
@@ -284,7 +284,7 @@ Detected when the root `package.json` lists `vite-plus`, after turbo.json and Nx
|
|
|
284
284
|
Where no runner's config is the source (or under `--from scripts`), each
|
|
285
285
|
package's `package.json` scripts become its `vx.config.ts`, plus a
|
|
286
286
|
`.vx/workspace.ts` that declares the `@vzn/vx-lockfile` plugin for the lockfile at
|
|
287
|
-
the root (`
|
|
287
|
+
the root (`bun()`, `pnpm()`, `npm()` or `yarn()`; `{ plugins: [] }`
|
|
288
288
|
without one): package edges (`^build`, `--affected`) come from it.
|
|
289
289
|
Running and caching here are the floor, so it declares no executor or
|
|
290
290
|
cache, and the file carries no comment (it is the user's). When
|
|
@@ -497,11 +497,8 @@ order may matter, so it is not made a group.
|
|
|
497
497
|
The report lists each TODO once per reason: tasks that share one are
|
|
498
498
|
named together (the first five, then a count; the files carry each),
|
|
499
499
|
and when no task caches it says uncommenting a TODO task's cache line
|
|
500
|
-
makes the second run a hit. Its `next:` line is a command the user can type
|
|
501
|
-
|
|
502
|
-
`npm_config_user_agent`) with the installed `vx` bin, else the
|
|
503
|
-
`@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
|
|
504
|
-
runner, a bare `vx`. Outside a git work tree it starts with
|
|
500
|
+
makes the second run a hit. Its `next:` line is a command the user can type,
|
|
501
|
+
with a bare `vx` (`vx init` installs it). Outside a git work tree it starts with
|
|
505
502
|
`git init`, and with no script mapped with declaring a task, since the
|
|
506
503
|
run would refuse without either. It runs `build`, else the first task that
|
|
507
504
|
neither serves (`persistent`) nor changes the repo (`clean`, `release`,
|
|
@@ -550,7 +547,7 @@ export default defineWorkspace({
|
|
|
550
547
|
|
|
551
548
|
Off CI a run reads the remote and does not write it unless its policy names the remote. Give the write token only to the default branch's CI and a read-only one to laptops and pull request jobs: the server's token is the boundary (`docs/security.md` § Cache poisoning). The same holds for `nxCache()`.
|
|
552
549
|
|
|
553
|
-
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. A token with no `apiUrl` means Vercel's hosted Remote Cache (`https://vercel.com/api`), exactly as it does for `turbo` — so `
|
|
550
|
+
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. A token with no `apiUrl` means Vercel's hosted Remote Cache (`https://vercel.com/api`), exactly as it does for `turbo` — so `bunx turbo login && bunx turbo link`, then `turboCache()` with `TURBO_TOKEN` / `TURBO_TEAM` set, is the whole hosted setup. Below the environment, in Turbo's order: a Vercel build's `VERCEL_ARTIFACTS_TOKEN` and `VERCEL_ARTIFACTS_OWNER` (the team id, where `TURBO_TOKEN` with a team is not set), then the repo's `.turbo/config.json` (what `turbo link` writes: `apiUrl`, `teamId`, `teamSlug`, `token`), then the root `turbo.json`'s `remoteCache` (`apiUrl`, `teamId`, `teamSlug`), whose `enabled: false` declines unless the options name a cache. A team with no `apiUrl` also means Vercel's. With a URL or a team but no token, the token comes from the OS keychain ([Tokens](#tokens-and-the-os-keychain)); with none of the three the plugin **declines** and the run stays local.
|
|
554
551
|
|
|
555
552
|
| Option | Environment variable | Meaning |
|
|
556
553
|
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -564,6 +561,7 @@ Every option falls back to the tool's own environment variable, so a self-hosted
|
|
|
564
561
|
| `uploadTimeoutMs` | `TURBO_REMOTE_CACHE_UPLOAD_TIMEOUT`, then `remoteCache.uploadTimeout` (seconds) | PUT deadline (default 60 s; 0 none) |
|
|
565
562
|
| `retries` | — | resends of a request answered 5xx (not 501) or never connected (default 1, Turbo's); a 429 waits out the limit; 0 turns both off |
|
|
566
563
|
| `preflight` | `TURBO_PREFLIGHT` (1 or 0), then `remoteCache.preflight` | send Turbo's `OPTIONS` preflight before each artifact request and go where its `Location` points (relative to `apiUrl`); the token goes along only when the API's `Access-Control-Allow-Headers` admits `Authorization`; a redirected preflight is not followed (default off) |
|
|
564
|
+
| `scope` | — | `RemoteScope`: `'header'` (default): every read names the run's refs, own branch first, in `x-artifact-read-scopes`; `'key'`: each entry is keyed on its ref, a read asks every ref's key in one batch (a server that ignores the header) |
|
|
567
565
|
|
|
568
566
|
The signature is Turbo's current scheme (`artifact-signature:v2`: prefix, hash, team id and body, each length-prefixed, under HMAC-SHA256, base64 in `x-artifact-tag`). A signed body is written to a temp file before its tag can be checked, so one past core's artifact ceiling (2 GiB, at zstd's bound) is refused as it passes it, and is a miss.
|
|
569
567
|
|
|
@@ -620,7 +618,7 @@ With no token from an option, a variable or a config file, both plugins read the
|
|
|
620
618
|
|
|
621
619
|
## Remote-cache behaviour (both)
|
|
622
620
|
|
|
623
|
-
-
|
|
621
|
+
- Task keys carry no branch. `turboCache()` names the run's refs on every read (`HEAD`, `GET`, the batch `POST`) in `x-artifact-read-scopes`, own branch first (`refs/heads/feat-x, refs/heads/main`): from CI's env, else git's current branch, then the default branch. A `PUT` names none: where a write lands is the server's call, from the CI OIDC token checked against its trust policy. `scope: 'key'` folds the ref into each key instead, for a server that ignores the header. `nxCache()` sends plain requests. How a server implements the header: [Branch caches a server enforces](https://vznjs.github.io/vx/blog/branch-caches-a-server-enforces/).
|
|
624
622
|
- A remote error degrades to a **miss** and a warning that names the request, the artifact and the server — `[vx] plugin '@vzn/vx-migrate': upload 32248a2a7c89e241 to https://cache.example.com/v8/artifacts failed: no answer within 60000 ms` — never the token. The run never fails because of the cache.
|
|
625
623
|
- The same failure is said once per run: an unreachable server fails the probe, the download and the upload alike, and the run prints the first and, at its end, `[vx] plugin '@vzn/vx-migrate': 2 more requests failed the same way: Unable to connect. Is the computer able to access the url?` (core does the counting, for every cache plugin).
|
|
626
624
|
- A request answered `5xx` (not `501`), or one that never connected (a refused port, an unresolved host), is sent again after 2 s, `retries` times. A `429` pauses every request to that remote for its `Retry-After` (capped at 10 s a wait, 2 s without one), and the request is sent again when the pause ends, for up to 20 s from its first `429`, as Turbo's client does. A spent deadline is not resent: it has already cost its wait.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.646",
|
|
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.646"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/adoption-plugin.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// skeleton until item 592; what differs is only how the mapping is made.
|
|
7
7
|
|
|
8
8
|
import { definePlugin, type ProjectHookContext, type VxPlugin } from '@vzn/vx'
|
|
9
|
-
import { type AdoptionMapping, cachedMapping } from './mapping-cache.js'
|
|
9
|
+
import { type AdoptionMapping, cachedMapping, codeId } from './mapping-cache.js'
|
|
10
10
|
import { warnGaps } from './plugin-gaps.js'
|
|
11
11
|
import { headStamp, spareTrackedOutputs, trackedFiles } from './tracked-outputs.js'
|
|
12
12
|
|
|
@@ -76,18 +76,16 @@ export function adoptionPlugin(
|
|
|
76
76
|
async project(config, ctx) {
|
|
77
77
|
if (mappedFor !== ctx.projects) {
|
|
78
78
|
mappedFor = ctx.projects
|
|
79
|
+
// The key's other reads overlap the plugin's own.
|
|
80
|
+
void codeId()
|
|
81
|
+
const stamp = headStamp(ctx.workspaceRoot)
|
|
79
82
|
mapping = mapRun(ctx).then(async (r) =>
|
|
80
83
|
r.spareTracked === true
|
|
81
|
-
? cachedMapping(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let read: Promise<readonly string[] | null> | undefined
|
|
87
|
-
const tracked = () => (read ??= trackedFiles(ctx.workspaceRoot))
|
|
88
|
-
return spare(ctx, await r.map(tracked), await tracked())
|
|
89
|
-
},
|
|
90
|
-
)
|
|
84
|
+
? cachedMapping(ctx.cacheDir, r.name, [...r.reads, await stamp], async () => {
|
|
85
|
+
let read: Promise<readonly string[] | null> | undefined
|
|
86
|
+
const tracked = () => (read ??= trackedFiles(ctx.workspaceRoot))
|
|
87
|
+
return spare(ctx, await r.map(tracked), await tracked())
|
|
88
|
+
})
|
|
91
89
|
: cachedMapping(ctx.cacheDir, r.name, r.reads, () =>
|
|
92
90
|
r.map(() => trackedFiles(ctx.workspaceRoot)),
|
|
93
91
|
),
|
package/src/index.ts
CHANGED
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
// runs unchanged) and the two remote caches speaking Turbo's and Nx's
|
|
3
3
|
// wire. The CLI (`bunx @vzn/vx-migrate`) is `migrate.ts`; mappers and
|
|
4
4
|
// cache clients are internal (1.0 freezes what this file exports).
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
//
|
|
6
|
+
// Each factory loads its module on first call: a workspace evaluates this
|
|
7
|
+
// file on every run, and a Turbo repo paid ~4 ms per run transpiling nx()
|
|
8
|
+
// and both cache clients it never calls (solid, 2026-10-22).
|
|
9
|
+
import type { VxPlugin } from '@vzn/vx'
|
|
10
|
+
import type * as Turbo from './turbo/index.js'
|
|
11
|
+
import type { TurboPluginOptions } from './turbo/index.js'
|
|
12
|
+
import type { NxPluginOptions } from './nx/index.js'
|
|
13
|
+
import type { TurboCacheOptions } from './turbo-cache/index.js'
|
|
14
|
+
import type { NxCacheOptions } from './nx-cache/index.js'
|
|
15
|
+
import type * as Nx from './nx/index.js'
|
|
16
|
+
import type * as TurboCache from './turbo-cache/index.js'
|
|
17
|
+
import type * as NxCache from './nx-cache/index.js'
|
|
18
|
+
|
|
19
|
+
export type { TurboPluginOptions } from './turbo/index.js'
|
|
20
|
+
export type {
|
|
21
|
+
NxExecutors,
|
|
22
|
+
NxExecutorTarget,
|
|
23
|
+
NxExecutorTranslation,
|
|
24
|
+
NxExecutorTranslator,
|
|
25
|
+
NxPluginOptions,
|
|
13
26
|
} from './nx/index.js'
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
27
|
+
export type { TurboCacheOptions } from './turbo-cache/index.js'
|
|
28
|
+
export type { NxCacheOptions } from './nx-cache/index.js'
|
|
29
|
+
|
|
30
|
+
export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
31
|
+
return (require('./turbo/index.js') as typeof Turbo).turbo(options)
|
|
32
|
+
}
|
|
33
|
+
export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
34
|
+
return (require('./nx/index.js') as typeof Nx).nx(options)
|
|
35
|
+
}
|
|
36
|
+
export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
|
|
37
|
+
return (require('./turbo-cache/index.js') as typeof TurboCache).turboCache(options)
|
|
38
|
+
}
|
|
39
|
+
export function nxCache(options: NxCacheOptions = {}): VxPlugin {
|
|
40
|
+
return (require('./nx-cache/index.js') as typeof NxCache).nxCache(options)
|
|
41
|
+
}
|
package/src/mapping-cache.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// package's sources and the core helpers the mappers call: a new
|
|
9
9
|
// `@vzn/vx-migrate` or `@vzn/vx` maps afresh.
|
|
10
10
|
|
|
11
|
+
import { statSync } from 'node:fs'
|
|
11
12
|
import { mkdir, rename } from 'node:fs/promises'
|
|
12
13
|
import path from 'node:path'
|
|
13
14
|
import { foldScriptHooks, isLiteralPattern, outputsOverlap, VERSION } from '@vzn/vx'
|
|
@@ -30,8 +31,8 @@ export interface AdoptionMapping {
|
|
|
30
31
|
|
|
31
32
|
let code: Promise<string> | undefined
|
|
32
33
|
|
|
33
|
-
/** This package's sources and the core helpers the mappers call,
|
|
34
|
-
function codeId(): Promise<string> {
|
|
34
|
+
/** This package's sources (by size and mtime) and the core helpers the mappers call, once per process. */
|
|
35
|
+
export function codeId(): Promise<string> {
|
|
35
36
|
code ??= (async () => {
|
|
36
37
|
const parts: string[] = [
|
|
37
38
|
VERSION,
|
|
@@ -40,7 +41,13 @@ function codeId(): Promise<string> {
|
|
|
40
41
|
const files: string[] = []
|
|
41
42
|
for await (const f of new Bun.Glob('**/*.{ts,cjs}').scan(import.meta.dir)) files.push(f)
|
|
42
43
|
files.sort()
|
|
43
|
-
|
|
44
|
+
// Size and mtime, not bytes: reading the 650 kB of sources cost ~10 ms
|
|
45
|
+
// on every run, more than the mapping hit it guards saves on a small
|
|
46
|
+
// repo (solid, 2026-10-22). An edit moves both.
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const s = statSync(path.join(import.meta.dir, f))
|
|
49
|
+
parts.push(f, `${s.size}:${s.mtimeMs}`)
|
|
50
|
+
}
|
|
44
51
|
return hex(parts.join('\0'))
|
|
45
52
|
})()
|
|
46
53
|
return code
|
|
@@ -71,11 +78,12 @@ export async function cachedMapping(
|
|
|
71
78
|
parts: readonly string[],
|
|
72
79
|
map: () => Promise<AdoptionMapping>,
|
|
73
80
|
): Promise<AdoptionMapping> {
|
|
74
|
-
const key = hex([await codeId(), ...parts].join('\0'))
|
|
75
81
|
const file = path.join(cacheDir, `vx-migrate-${name}-mapping.json`)
|
|
76
|
-
const
|
|
82
|
+
const reading = Bun.file(file)
|
|
77
83
|
.json()
|
|
78
|
-
.catch(() => null)
|
|
84
|
+
.catch(() => null) as Promise<Held | null>
|
|
85
|
+
const key = hex([await codeId(), ...parts].join('\0'))
|
|
86
|
+
const held = await reading
|
|
79
87
|
if (held?.key === key) {
|
|
80
88
|
return {
|
|
81
89
|
byName: new Map(held.byName),
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -12,12 +12,8 @@ import {
|
|
|
12
12
|
quoteTsLiteral as quote,
|
|
13
13
|
} from '@vzn/vx'
|
|
14
14
|
import type { PackageMeta } from './packages.js'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
rootTaskProject,
|
|
18
|
-
type TurboGlobal,
|
|
19
|
-
type TurboMappedProject,
|
|
20
|
-
} from './turbo/turbo-map.js'
|
|
15
|
+
import { mapTurboWorkspace, type TurboGlobal, type TurboMappedProject } from './turbo/turbo-map.js'
|
|
16
|
+
import { rootTaskProject } from './turbo/turbo-reads.js'
|
|
21
17
|
import { relPosix } from './paths.js'
|
|
22
18
|
import { gitIgnored, spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
23
19
|
import { DOTENV_GLOBS_HEAD, DOTENV_PROBE, DOTENV_PROBE_TOP } from './dotenv-probe.js'
|
package/src/migrate.ts
CHANGED
|
@@ -53,14 +53,13 @@ import {
|
|
|
53
53
|
missingPackages,
|
|
54
54
|
MODE_QUESTION,
|
|
55
55
|
ownVersion,
|
|
56
|
-
type PackageManager,
|
|
57
56
|
packageManagerOf,
|
|
58
57
|
parseModeAnswer,
|
|
59
58
|
uninstall,
|
|
60
59
|
} from './adopt.js'
|
|
61
60
|
import { migrateTurbo } from './migrate-turbo.js'
|
|
62
61
|
import { migrateViteTask } from './migrate-vite-task.js'
|
|
63
|
-
import { turboConfigFile } from './turbo/turbo-
|
|
62
|
+
import { turboConfigFile } from './turbo/turbo-reads.js'
|
|
64
63
|
import { lockfilePlugin } from './workspace-notes.js'
|
|
65
64
|
import {
|
|
66
65
|
extendWorkspaceFile,
|
|
@@ -642,14 +641,6 @@ function lockRow(lock: { code: number; said: string }): ui.FieldRow {
|
|
|
642
641
|
/** `.vx/.gitignore`'s text, as core writes it: the machine-local cache and restore staging stay out of git. */
|
|
643
642
|
const DOT_VX_GITIGNORE = '/cache\n/tmp\n'
|
|
644
643
|
|
|
645
|
-
// npx and bunx name the package: their bare `vx` is a stranger's npm package.
|
|
646
|
-
const EXEC: Record<PackageManager, string> = {
|
|
647
|
-
pnpm: 'pnpm vx',
|
|
648
|
-
yarn: 'yarn vx',
|
|
649
|
-
bun: 'bunx @vzn/vx',
|
|
650
|
-
npm: 'npx @vzn/vx',
|
|
651
|
-
}
|
|
652
|
-
|
|
653
644
|
/**
|
|
654
645
|
* Install what the workspace file imports, if missing, then run the repo's
|
|
655
646
|
* build. Pinned to this release: every @vzn package releases in lockstep,
|
|
@@ -666,12 +657,12 @@ export function adoptionNext(
|
|
|
666
657
|
const missing = needs.filter(
|
|
667
658
|
(p) => !existsSync(path.join(root, 'node_modules', p, 'package.json')),
|
|
668
659
|
)
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
const run =
|
|
660
|
+
// `vx init` installs vx globally first: vx alone, never a package
|
|
661
|
+
// manager's runner (owner, 2026-10-22: vx owns the command).
|
|
662
|
+
const run = `vx run ${firstTask(path.join(root, source), runner)} --all`
|
|
672
663
|
if (missing.length === 0) return run
|
|
673
664
|
const pinned = version === null ? missing : missing.map((p) => `${p}@${version}`)
|
|
674
|
-
return `${installArgv(root, pm, pinned).join(' ')} &&
|
|
665
|
+
return `${installArgv(root, pm, pinned).join(' ')} && vx lock && ${run}`
|
|
675
666
|
}
|
|
676
667
|
|
|
677
668
|
/**
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -284,11 +284,20 @@ export interface NxMapping {
|
|
|
284
284
|
readonly notes: string[]
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
/** A
|
|
287
|
+
/** A command that still runs Nx or Lerna (`nx-exec`, `nx-env`, `nx exec`, `lerna run`) or reads what they set. */
|
|
288
|
+
const RUNS_NX = /(?:^|[\s;&|('"/])(?:nx(?:-exec|-env)?|lerna)\s|NX_TASK_TARGET_|LERNA_PACKAGE_NAME/
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* A node's tags as vx takes them: a blank one would be refused at load, and
|
|
292
|
+
* `npm:public` / `npm:private` are Nx's own, read off package.json.
|
|
293
|
+
*/
|
|
288
294
|
function nxTags(node: NxNode | undefined): string[] {
|
|
289
295
|
const tags = node?.data?.tags
|
|
290
296
|
return Array.isArray(tags)
|
|
291
|
-
? tags.filter(
|
|
297
|
+
? tags.filter(
|
|
298
|
+
(t): t is string =>
|
|
299
|
+
typeof t === 'string' && t.trim() !== '' && t !== 'npm:public' && t !== 'npm:private',
|
|
300
|
+
)
|
|
292
301
|
: []
|
|
293
302
|
}
|
|
294
303
|
|
|
@@ -481,7 +490,7 @@ export async function mapNxWorkspace(
|
|
|
481
490
|
}
|
|
482
491
|
mapped.push({ meta, tasks })
|
|
483
492
|
}
|
|
484
|
-
// The `
|
|
493
|
+
// The `inputs:<name>` twins the `^` inputs above asked for, in every
|
|
485
494
|
// project a graph node owns — one with no targets too.
|
|
486
495
|
const byMeta = new Map(mapped.map((m) => [m.meta, m]))
|
|
487
496
|
for (const [nodeName, list] of upstream.inputTasks()) {
|
|
@@ -898,7 +907,7 @@ function buildTask(
|
|
|
898
907
|
// saw nothing the task reads, and a changed document replayed the old
|
|
899
908
|
// generated client (Growth, 2026-10-16).
|
|
900
909
|
const orphanReader =
|
|
901
|
-
inputs.dependentOutputs.length > 0 && !deps.some((d) => !d.includes('#
|
|
910
|
+
inputs.dependentOutputs.length > 0 && !deps.some((d) => !d.includes('#inputs:'))
|
|
902
911
|
if (orphanReader && cacheWanted && !persistent) {
|
|
903
912
|
todos.push(orphanReaderTodo(inputs.dependentOutputs, target.dependsOn))
|
|
904
913
|
}
|
|
@@ -967,18 +976,24 @@ function buildTask(
|
|
|
967
976
|
// `nx exec -- <cmd>`, a package script's way to run under Nx, reads it:
|
|
968
977
|
// unset, it booted Nx's own task runner, which ran the target and its
|
|
969
978
|
// dependencies again. `LERNA_PACKAGE_NAME` is the project too: Lerna
|
|
970
|
-
// runs on Nx's runner and documents it to scripts.
|
|
971
|
-
// still
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
+
// runs on Nx's runner and documents it to scripts. Only a command that
|
|
980
|
+
// still runs Nx or Lerna gets them: a native task names no other runner
|
|
981
|
+
// (owner, 2026-10-22). A run-commands `env` still wins, as in Nx.
|
|
982
|
+
const define = {
|
|
983
|
+
...(RUNS_NX.test(mapped.command)
|
|
984
|
+
? {
|
|
985
|
+
NX_TASK_TARGET_PROJECT: projectName,
|
|
986
|
+
NX_TASK_TARGET_TARGET: targetName,
|
|
987
|
+
LERNA_PACKAGE_NAME: projectName,
|
|
988
|
+
...(variant.configuration === undefined
|
|
989
|
+
? {}
|
|
990
|
+
: { NX_TASK_TARGET_CONFIGURATION: variant.configuration }),
|
|
991
|
+
}
|
|
992
|
+
: {}),
|
|
979
993
|
...mapped.env,
|
|
980
994
|
}
|
|
981
|
-
|
|
995
|
+
if (Object.keys(define).length > 0) env.define = define
|
|
996
|
+
if (Object.keys(env).length > 0) exec.env = env
|
|
982
997
|
if (mapped.timeout !== undefined) exec.timeout = mapped.timeout
|
|
983
998
|
if (readyWhen !== undefined) {
|
|
984
999
|
exec.persistent = { readyWhen }
|
|
@@ -1040,7 +1055,7 @@ function reachesProducer(
|
|
|
1040
1055
|
if (!Array.isArray(deps)) return false
|
|
1041
1056
|
return deps.some((d) => {
|
|
1042
1057
|
if (typeof d !== 'string' || d.includes('*')) return true
|
|
1043
|
-
if (d.includes('#
|
|
1058
|
+
if (d.includes('#inputs:')) return false
|
|
1044
1059
|
if (d.startsWith('^'))
|
|
1045
1060
|
return [...reached(project)].some((p) => emittedIds.has(`${p}#${d.slice(1)}`))
|
|
1046
1061
|
return d.includes('#') || emittedIds.has(`${project}#${d}`)
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// dependency's `name` input for every project the graph reaches, whether or
|
|
3
3
|
// not a task edge exists, so a `test` with `^production` and no `dependsOn`
|
|
4
4
|
// re-keys on a dependency's source. vx folds upstream KEYS along
|
|
5
|
-
// `dependsOn`, so each project gets one `
|
|
5
|
+
// `dependsOn`, so each project gets one `inputs:<name>` task — a keyed
|
|
6
6
|
// group, keyed on the project's own `name` input — with an edge to its
|
|
7
7
|
// dependencies' twin. A task that reads `^name` depends on its direct
|
|
8
8
|
// dependencies' twins and folds the whole closure through them: each
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The edges are the NX graph's, not vx's package graph (`^` in vx follows
|
|
13
13
|
// package.json, and an Nx edge from a tsconfig path has no manifest entry),
|
|
14
|
-
// so they are explicit `pkg#
|
|
14
|
+
// so they are explicit `pkg#inputs:<name>`. A graph node with no vx
|
|
15
15
|
// project is transparent: its files join as workspace globs and the walk
|
|
16
16
|
// goes on through it. Nx allows a project cycle and vx's task graph does
|
|
17
17
|
// not: inside a cycle a twin carries its peers' files as workspace globs
|
|
@@ -47,8 +47,8 @@ const isFileset = (name: string): boolean =>
|
|
|
47
47
|
/** A fileset twin is named by its hash: a glob's `*` is no task name. */
|
|
48
48
|
const inputTask = (name: string): string =>
|
|
49
49
|
isFileset(name)
|
|
50
|
-
? `
|
|
51
|
-
: `
|
|
50
|
+
? `inputs:fileset-${Bun.hash.xxHash3(name).toString(16).padStart(16, '0')}`
|
|
51
|
+
: `inputs:${name}`
|
|
52
52
|
|
|
53
53
|
function normRel(p: string): string {
|
|
54
54
|
const s = p.replace(/\/+$/, '')
|
|
@@ -63,7 +63,7 @@ export interface NxUpstream {
|
|
|
63
63
|
* needs, with a transparent node's files added to `into`.
|
|
64
64
|
*/
|
|
65
65
|
resolve(node: string, into: NxInputs, todos: string[]): string[]
|
|
66
|
-
/** Each project's `
|
|
66
|
+
/** Each project's `inputs:<name>` tasks, for every twin an edge reaches. */
|
|
67
67
|
inputTasks(): Map<string, GeneratedTask[]>
|
|
68
68
|
}
|
|
69
69
|
|
package/src/nx-cache/index.ts
CHANGED
|
@@ -14,14 +14,12 @@
|
|
|
14
14
|
import {
|
|
15
15
|
definePlugin,
|
|
16
16
|
type RemoteCacheLayer,
|
|
17
|
-
type RemoteKey,
|
|
18
17
|
type RemoteOpenContext,
|
|
19
18
|
type VxPlugin,
|
|
20
19
|
refuseUnknownOptions,
|
|
21
20
|
type PluginOptionKinds,
|
|
22
21
|
} from '@vzn/vx'
|
|
23
22
|
import { deadline } from '../remote-deadline.js'
|
|
24
|
-
import { scopeHeader } from '../remote-scope.js'
|
|
25
23
|
import { OutageBreaker } from '../remote-breaker.js'
|
|
26
24
|
import { Cooldown, withRetry } from '../remote-retry.js'
|
|
27
25
|
import {
|
|
@@ -155,11 +153,10 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
155
153
|
private async request(
|
|
156
154
|
method: 'GET' | 'PUT',
|
|
157
155
|
hash: string,
|
|
158
|
-
at: RemoteKey | undefined,
|
|
159
156
|
body?: Blob,
|
|
160
157
|
): Promise<Response | undefined> {
|
|
161
158
|
if (this.token === undefined) return undefined
|
|
162
|
-
const headers: Record<string, string> =
|
|
159
|
+
const headers: Record<string, string> = {}
|
|
163
160
|
if (this.token !== '') headers['Authorization'] = `Bearer ${this.token}`
|
|
164
161
|
if (body !== undefined) {
|
|
165
162
|
headers['Content-Type'] = 'application/octet-stream'
|
|
@@ -212,18 +209,15 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
212
209
|
return res
|
|
213
210
|
}
|
|
214
211
|
|
|
215
|
-
async get(
|
|
216
|
-
hash
|
|
217
|
-
at?: RemoteKey,
|
|
218
|
-
): Promise<{ body: Response; durationMs: number | undefined } | null> {
|
|
219
|
-
const res = await this.fetch(hash, at)
|
|
212
|
+
async get(hash: string): Promise<{ body: Response; durationMs: number | undefined } | null> {
|
|
213
|
+
const res = await this.fetch(hash)
|
|
220
214
|
// The Nx wire carries no producing-task duration.
|
|
221
215
|
return res === null ? null : { body: res, durationMs: undefined }
|
|
222
216
|
}
|
|
223
217
|
|
|
224
|
-
private async fetch(hash: string
|
|
218
|
+
private async fetch(hash: string): Promise<Response | null> {
|
|
225
219
|
if (this.disabled) return null
|
|
226
|
-
const res = await this.request('GET', hash
|
|
220
|
+
const res = await this.request('GET', hash)
|
|
227
221
|
if (res === undefined) return null
|
|
228
222
|
if (res.status !== 200) {
|
|
229
223
|
await res.body?.cancel()
|
|
@@ -233,17 +227,18 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
233
227
|
return res
|
|
234
228
|
}
|
|
235
229
|
|
|
236
|
-
async has(hash: string
|
|
237
|
-
const res = await this.fetch(hash
|
|
238
|
-
if (res === null) return
|
|
230
|
+
async has(hash: string): Promise<{ size?: number } | null> {
|
|
231
|
+
const res = await this.fetch(hash)
|
|
232
|
+
if (res === null) return null
|
|
239
233
|
await res.body?.cancel()
|
|
240
|
-
|
|
234
|
+
const size = Number(res.headers.get('content-length') ?? NaN)
|
|
235
|
+
return Number.isFinite(size) ? { size } : {}
|
|
241
236
|
}
|
|
242
237
|
|
|
243
|
-
// Nx's record carries no duration
|
|
244
|
-
async put(hash: string, body: Blob
|
|
238
|
+
// Nx's record carries no duration.
|
|
239
|
+
async put(hash: string, body: Blob): Promise<void> {
|
|
245
240
|
if (this.disabled || this.writesRefused) return
|
|
246
|
-
const res = await this.request('PUT', hash,
|
|
241
|
+
const res = await this.request('PUT', hash, body)
|
|
247
242
|
if (res === undefined) return
|
|
248
243
|
if (res.status === 200 || res.status === 202 || res.status === 409) return
|
|
249
244
|
throw new Error(`HTTP ${res.status}`)
|