@vzn/vx-migrate 0.0.650 → 0.0.652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -25
- package/package.json +2 -2
- package/src/adopt.ts +6 -4
- package/src/bin.ts +6 -6
- package/src/migrate-scripts.ts +2 -2
- package/src/migrate-turbo.ts +11 -4
- package/src/migrate.ts +4 -3
- package/src/nx/nx-map.ts +22 -7
- package/src/package-fs.ts +18 -2
- package/src/remote-refusal.ts +6 -1
- package/src/shared-outputs.ts +22 -16
- package/src/tracked-outputs.ts +1 -1
- package/src/turbo/index.ts +47 -0
- package/src/turbo/root-engines.ts +19 -0
- package/src/turbo/turbo-map.ts +210 -29
- package/src/turbo/turbo-reads.ts +95 -4
- package/src/turbo-cache/index.ts +38 -11
- package/src/vite-task/vite-task-map.ts +3 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The package exports `turbo`, `nx`, `turboCache`, `nxCache` and their options typ
|
|
|
30
30
|
|
|
31
31
|
## `turbo()` — a Turbo repo, mid-migration
|
|
32
32
|
|
|
33
|
-
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every package's `build` script the way `turbo run build` would: `dependsOn` edges (`^build`, same-package deps, `pkg#task`), `inputs` as every task's key and `outputs` as
|
|
33
|
+
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every package's `build` script the way `turbo run build` would: `dependsOn` edges (`^build`, same-package deps, `pkg#task`), `inputs` as every task's key and `outputs` as `cache: true`, `env` / `passThroughEnv`, `cache: false`, `persistent`. Turbo's global fields (`globalDependencies`, `globalEnv`, `globalPassThroughEnv`, Turbo 1's `globalDotEnv`) are inlined into every task; a `*` env name (`NEXT_PUBLIC_*`) expands over the run's environment, as Turbo matches it; `*` is Turbo's only wildcard (`\*`, a leading `\!`, `?` and `[` are literal), and a literal vx cannot key (unkey's `NEXT_PUBLIC_\*`) is left out, reported once only when the run's environment sets it; per-package `turbo.json` overlays apply. A `turbo.jsonc` (Turbo 2.5+) is read wherever a `turbo.json` would be, at the root and in a package; a directory holding both is refused, as Turbo refuses it, and a workspace root with neither is refused, naming the fix. An absolute path in `inputs`, `outputs`, `globalDependencies` or `global.inputs` is refused naming the file, as Turbo refuses it. Turbo 2's framework inference (a package on `next` has `NEXT_PUBLIC_*` and `NEXT_DEPLOYMENT_ID` hashed and passed; Turbo's whole table, `expo`, `nuxt`, `remix`, `@sveltejs/kit` and the rest, each package taking the first framework it matches, as Turbo does, and leaving out names under the platform's `TURBO_CI_VENDOR_ENV_KEY`) applies too: the prefix expands over the run's environment into that package's tasks, and a task's `!` entry takes names back (`bunx @vzn/vx-migrate`, which writes files for another environment, names each framework and its packages in a note instead). Turbo's builtin env applies as Turbo's source has it: `VERCEL_ANALYTICS_ID` and `VERCEL_TARGET_ENV` are keyed in every task (a `globalEnv` `!` entry such as `!VERCEL*` takes them back), Next's skew rule keys `VERCEL_DEPLOYMENT_ID` when `VERCEL_SKEW_PROTECTION_ENABLED=1`, and the literal names of Turbo's builtin pass-through list (`TZ`, `HOME`, `CI`, the Vercel, Docker and IDE names) reach every task unkeyed. Its wildcard families (`VERCEL_*`, `GITHUB_*`, `DOCKER_*` and the rest) are not matched against the run's environment: `exec.env.passThrough` is keyed by name, so a live match keyed each machine apart. The written config keys the defaults and `VERCEL_DEPLOYMENT_ID` always (a miss where Turbo would hit, never a stale hit) and passes the same literal names from the preset; a note names the wildcard families to add where a task reads them. What runs is what a migration would have written, minus the file: the key a task derives here equals the key the written config would derive.
|
|
34
34
|
|
|
35
35
|
| Option | Meaning |
|
|
36
36
|
| ------ | ----------------------------------------------------------------------------------------------------------- |
|
|
@@ -62,7 +62,7 @@ Rules:
|
|
|
62
62
|
- A transit node (Turbo's documented pattern, its with-vitest example: `transit: { dependsOn: ["^transit"] }` with no script anywhere, and `test: { dependsOn: ["transit"] }`) is a key-only task in each package that defines it: a keyed group (no command, nothing spawns) keyed on its inputs, with its `^transit` edge, so a dependant's `test` re-keys on its dependencies' sources as Turbo re-hashes it. A `^` task that some package has a script for, or that nothing depends on, is no transit node. A package without the script of a `^` task others run gets the same key-only task, as Turbo hashes a no-op node there: an edit to it re-keys its dependants. A `build` whose only edge is `^build` is not written: core gives every project with no `build` that node (solid's three script-less packages each got a `build` of their own). A dependency without the script whose own edges lack `^x` gets it too, so a `^x` stops there as Turbo's does (opencode's `build: { dependsOn: [] }`: core walked past to the builds below, which Turbo's `^build` never reaches).
|
|
63
63
|
- `with` (tasks Turbo runs alongside, `web#dev` with `api#dev`): an edge to each sidecar that is persistent, so vx starts the task once the sidecar has spawned and runs the sidecar only when the task runs; a sidecar that ends would be waited for, so it is a todo instead, and a pair that names each other keeps one edge (two would be a cycle). A task with no script whose `with` names persistent sidecars (Turbo's with-tailwind example: `ui` has no `dev` script, its `dev` starts `dev:styles` and `dev:components`) is a group task that depends on them.
|
|
64
64
|
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx hashes each dependency's key into it); from a `cache: false` producer (vercel/vercel's `//#generate:cache-keys`, which records the host) the named files, or its outputs, are a `inputs.workspaceRuntime` probe, and `inputs.tasks` leaves the producer out, so core answers the probe after it ran. Then: absent or `[]` → `**/*` (Turbo's default); `$TURBO_DEFAULT$` → `**/*`; exclusions alone narrow `**/*`; `$TURBO_ROOT$/<path>` → `inputs.workspaceFiles` (negation kept); any other `$TURBO_ROOT$` use is a todo. `globalDependencies` and Turbo 1's `globalDotEnv` land in `workspaceFiles` too (Turbo hashes `globalDependencies` apart from a task's own inputs, so neither's `!` reaches the other's files, and vx subtracts each `!` from the whole list: a `!` that may reach the other side's files is dropped, which keys more, never a stale hit; Turbo 2.11's `global.inputs` joins each task's inputs, where its `!` entries apply), and a task's Turbo 1 `dotEnv` in its `files`. Turbo 1's `$NAME` entries, in `globalDependencies` or a task's `dependsOn`, are env vars: they join `inputs.env` and `exec.env.passThrough`. A package nested in the task's package (cal.com's apps inside `@calcom/app-store`) is hashed by Turbo with its parent, and core's globs stop at a project: each glob that reaches one is also listed in `inputs.workspaceFiles`. Turbo's glob grammar is translated as Nx's is (`*.[jt]s` is `*.{[jt],j,t}s`); an input with no safe form widens to `**/*` with a todo. A wildcard `X/**` input, `!` or not, also lists `X`, as outputs do: Turbo's `**` matches zero segments, so `*/**` keys on the package-root files too. A `.env`-shaped input (`.env*`, `.env.local`, a task's Turbo 1 `dotEnv`, create-turbo's `globalDependencies: ["**/.env.*local"]`) is gitignored as a rule, so it is keyed by a probe that hashes the `.env` files it can name (`inputs.runtime`) or, for a root entry, the files its root-relative globs name, hidden ones too, as Turbo's `*` matches them (`workspaceRuntime`; `**/<name>` is a `find -name` that skips `node_modules`, and another `**` walks the workspace), rather than as a file glob git never reports. A package whose `.env` globs all sit at its root gets a one-shell probe of that directory; one below the root walks the package.
|
|
65
|
-
- `outputs`: `$TURBO_ROOT$/<path>` → `
|
|
65
|
+
- `outputs`: `$TURBO_ROOT$/<path>` → `outputs.workspaceFiles`; a negated output rides beside its positives and takes its paths back from the clean, the save and the restore (Next's `.next/**` minus `!.next/cache/**` keeps the cache), and one with no positive beside it takes back nothing and is dropped. Past its first segment an output takes Turbo's grammar (`dist/**/*.[cm]js`); the first stays a literal (a route directory). A wildcard `X/**` also lists `X`: Turbo's `**` matches zero segments, so medusa's `*/**` caches the package-root `tsconfig.tsbuildinfo` too (its `package.json` is taken back, below); a literal `dist/**` needs no `dist`. An output whose first segment is a wildcard (`**/*.d.ts`, medusa's `*/**`) stays cached, as in `nx()` (`turbo()` 2026-10-14): Turbo never cleans an output, vx cleans it before every run and every restore, and such a glob can reach the sources; core never cleans a committed file, and every untracked file it reaches is deleted, which `vx lock`'s warnings name (`output "*/**": … every untracked file this glob reaches is deleted`). Negations beside it (`!src/**`) keep what they name. Without git the files cannot be told apart, and the task runs uncached with a todo. A glob that reaches no tracked kind gets no warning: one segment with a literal extension the package tracks no file of (`*.xml`), `**/<dir>/**` when the package tracks nothing under a directory of that name (vercel/ai's `**/dist/**`), and a first segment no tracked top-level entry matches (tldraw's `dist-*/**`). An output that covers the package's own `package.json` (trpc's client build lists it: the build rewrites `exports`) takes the manifest back with `!package.json` and keeps the cache, in `turbo()` as in `nx()` (2026-10-03): vx would otherwise delete it before every run. The build rewrites a file it may list as an input (`@vzn/vx-lockfile` lists `package.json`), so the Turbo mapping also sets `inputs.stability: { strategy: 'mutable' }` and an identical rewrite counts as unchanged. So does one that covers the package's vx.config; `turbo()` and `nx()` check the config the package has, and `vx-migrate` the one it writes, so sanity's `*.js` shims stay cached beside none. `nx()` keeps a wildcard-first output cached the same way (2026-10-03). A committed file under an output (typescript-eslint's `data/sponsors.json` in a cached `data`) needs nothing: core never cleans a file git tracks, so it survives and the task keeps its cache. A task whose command runs `tsc` that writes a `.tsbuildinfo` (`-b`, `composite` or `incremental`, read through the tsconfig's `extends` and, for `-b`, its `references`) outside its outputs also outputs that file: tsc re-emits only what its buildinfo calls changed, so with `rootDir: "src"` and `outDir: "dist"` its `tsconfig.tsbuildinfo` beside the tsconfig survived the clean before a miss and the miss cached a `dist/` holding only the edited file. Nx infers the file as an output; Turbo never cleans `dist/`.
|
|
66
66
|
- A `**` glued to text in an input, output or `globalDependencies` glob is read as Turbo reads it (turborepo#5431): `src/**.ts` → `src/**/*.ts`, `gen**` → `gen*/**`. Core refuses the glued spelling.
|
|
67
67
|
- `env` / `passThroughEnv`: explicit names go to `inputs.env` (env only) and `exec.env.passThrough` (both, plus both globals); a wildcard expands over the run's environment under `turbo()`, which maps where the tasks run (a new variable name such a wildcard matches maps afresh); the CLI, which writes for another environment, lists the names the task's package and its workspace dependencies spell (a root task: the whole repo; clerk's `E2E_*`), noted once per entry, and one nothing spells is a todo; `*` is the only wildcard, as in Turbo, so `\*`, a leading `\!`, `?` and `[` are literal: unkey's `NEXT_PUBLIC_\*` names one variable, which vx cannot key, so it is dropped and reported once for the workspace with the count of tasks naming it (openstatus's root `build` env `\*` had written one todo into each of 45 configs; `turbo()`: only when the run's environment sets it, as Turbo hashes nothing otherwise); and a `!` entry (openstatus' `!NEXT_PUBLIC_VERCEL_URL`) removes the names it matches from its list, with no todo, since vx matches no name it is not given. A name both a global list and the task's own list carry is listed once. A name that is no shell variable name (`p-q`) is keyed but left out of `exec.env.passThrough`, as `sh` drops it before the task runs.
|
|
68
68
|
- Framework inference (Turbo hashes and passes Next's `NEXT_PUBLIC_*`, Vite's `VITE_*`, … for a package that depends on the framework): `turbo()` expands the prefix over the run's environment; the CLI writes the names the tracked source and `.env.example` of the package and its workspace dependencies spell (the bare prefix, as a `startsWith` test spells it, is none) (Next bundles theirs: cal.com's web, 23 alone, 58 with them), and a note asks for any only an installed dependency reads.
|
|
@@ -70,16 +70,21 @@ Rules:
|
|
|
70
70
|
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — `build` keeps its cache, else the task with a `^` edge, else the first declared (typescript-eslint's `typecheck` has `^typecheck` and writes declarations into `build`'s `dist`: keeping it left `build` uncached); the rest run uncached with a todo naming the keeper and the fix, their own output path. A shared tsc incremental file (create-t3-turbo's `build` and `typecheck` both on `.cache/tsbuildinfo.json`) stays with the keeper alone, and the other task keeps its cache when it emits nothing else (a typecheck); one that emits more runs uncached, as `tsc --incremental` skips files the keeper's tsbuildinfo calls built. When either side is a directory at any depth (solid's build `**/dist`, its types `**/types`), the two meet only where one tree nests in the other: the keeper takes the sibling's glob back (`!coverage/**`, `!**/types`) and both keep their cache, as under Turbo. Same rule for Nx targets.
|
|
71
71
|
- **A task whose inputs read another task's outputs** (a `test` keyed on `**/*` beside a `build` writing `dist/**`): `vx lock` refuses it, since such a key cannot be known before the producer ran. Each overlapping output is taken back from the reader's inputs with `!` (`['**/*', '!dist/**']`), in package and workspace inputs alike, and a workspace output another package writes into this one is taken back from its package inputs too; Turbo and Nx hash what git tracks, and a build's outputs are not, so the key reads what the adopted tool read. A literal input another task writes cannot be taken back: that task runs uncached with a todo. A committed file under such an output is still a source: a reader the `!` would hide it from runs uncached with a todo. A keyed group (an `inputs:<name>` twin, a transit node) runs nothing, so core lets it read what a task writes: it gets no `!` for a literal, the `!` that would hide a committed file goes, and it keeps its key (typescript-eslint's ast-spec commits the `*.shot` snapshots its `test` outputs, and its twin was written as `{}`, which core refuses). An output that names one committed file (eslint's `eslint-suppressions.json` in a `lint` task's outputs) is dropped, and so is the `!` each reader got for it: the file stays in every key, as Turbo and Nx hash it, and the readers stay cached. Same for Nx targets.
|
|
72
72
|
- **Two packages' tasks on one workspace output** (cal.com's shared `post-install` writes `../../node_modules/@prisma/client/**` from every package with the script): core refuses two cached tasks on one path with no edge between them, so `turbo()` (and `nx()`) keeps the first (in package and task order) cached and runs the rest uncached, each with a todo naming the keeper. A package's own outputs count at their workspace path: typescript-eslint's root project caches `dist` and each package's typecheck `dist/packages/<name>`, one path twice. Edges across packages are not read, so an ordered pair loses its cache too.
|
|
73
|
-
- `cache: false` or `persistent: true` → no `cache
|
|
73
|
+
- `cache: false` or `persistent: true` → no `cache: true`, the task still keyed on its `inputs` and `env` as Turbo hashes it; a persistent task gets `exec.persistent: {}` and, when some task depends on it, the consumer's `persistentTodo`.
|
|
74
74
|
- A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
|
|
75
75
|
- `outputLogs: "new-only"` maps to nothing: frames for the tasks that ran and a one-liner per cache hit is vx's default flow already. The other values are per-run in vx, so they are a todo naming the flag (`vx run … --output-logs hash-only`).
|
|
76
76
|
- `interruptible` maps to nothing: `vx watch` keeps a persistent task up across cycles and restarts it only when its config changes or it dies.
|
|
77
|
-
- A
|
|
77
|
+
- A package's own turbo.json `tags` (Turbo 2.11.6) are its vx project `tags`, so `vx run build --filter tag:ci` selects what `turbo run build --filter=tag:ci` does through package tags (the root's label the root project, none inherited, as in Turbo). A task's `tags` map to nothing: vx tags are a project's, so a `tag:` filter never reaches a task by its own tags.
|
|
78
|
+
- A task's `experimentalCI` maps to nothing: Turbo reads it for external CI tooling and acts on nothing.
|
|
78
79
|
- `envMode: "loose"` (top-level or in `global`, or `TURBO_ENV_MODE=loose`, which wins) is a note: Turbo hands every task the whole environment, vx only the declared names.
|
|
79
80
|
- The workspace keys vx has a home for, top level or in `global`, fill what `.vx/workspace.ts` leaves unset: `run.concurrency` (`"10"`, `"50%"` of the cores) → `run.concurrency`. `TURBO_CONCURRENCY` wins over turbo.json, as under Turbo; a set one decides, its `0` included. `cacheMaxSize` / `cacheMaxAge` map to nothing: no vx run evicts, `vx cache clean --max-size / --older-than` does.
|
|
80
|
-
- `
|
|
81
|
+
- `TURBO_FORCE=true` (or `1`), `turbo run --force`, turns every cache read off through the workspace's `cache.policy`: each task runs and refreshes the local cache. The remote is written only where `.vx/workspace.ts`'s own `cache.policy` names it, so a laptop's forced run never writes the cache CI reads; `vx run --force` is the flag. A `--cache-policy` or `VX_CACHE_POLICY` replaces the workspace's policy, this included.
|
|
82
|
+
- `TURBO_SCM_BASE`, the base `turbo run --affected` compares with, is the run's base commit (`setBranch({ base })`): a bare `vx run build --affected` compares with the same ref. With it unset on GitHub Actions (`GITHUB_ACTIONS`), Turbo's own base applies: a pull request's `GITHUB_BASE_REF`, else the push event's `before` from `GITHUB_EVENT_PATH`. `TURBO_SCM_HEAD`, the commit Turbo compares up to, is vx's checkout: `--affected` compares the base with HEAD and its working tree, so a head at HEAD's commit (`$GITHUB_SHA` on a checkout of it) is honoured as is, and any other one is a warning to check that commit out.
|
|
81
83
|
- Turbo hashes each package's `microfrontends.json` (or `.jsonc`) into every task, or the one file `VC_MICROFRONTENDS_CONFIG_FILE_NAME` names; so does `turbo()`.
|
|
84
|
+
- `TURBO_ROOT_TURBO_JSON`, Turbo's root config path (against the cwd), is refused when it names another file than the workspace root's `turbo.json` or `turbo.jsonc`: `turbo()` reads only that one.
|
|
85
|
+
- Turbo's run switches the plugins do not read, on purpose: `TURBO_CACHE`, `TURBO_REMOTE_CACHE_READ_ONLY` and `TURBO_REMOTE_ONLY` (core's `cache.policy` decides); `TURBO_CACHE_DIR`, `TURBO_CACHE_MAX_AGE` and `TURBO_CACHE_MAX_SIZE` (`cache.dir`; no run evicts); `TURBO_LOG_ORDER`, `TURBO_UI`, `TURBO_EXPERIMENTAL_UI`, `TURBO_TUI_SCROLLBACK_LENGTH` and `TURBO_LOG_VERBOSITY` (vx frames each task's output its own way); `TURBO_RUN_SUMMARY` and `TURBO_LOG_FILE` (`vx last`, telemetry plugins); `TURBO_EXPERIMENTAL_OTEL_*` (`@vzn/vx-otel`); `TURBO_DAEMON` (no daemon); `TURBO_DANGEROUSLY_DISABLE_PACKAGE_MANAGER_CHECK` (no such check); `TURBO_ALLOW_NO_TURBO_JSON` (`turbo()` maps a turbo.json and refuses a root with none); `TURBO_PLATFORM_ENV` and `TURBO_PLATFORM_ENV_DISABLED` (Turbo warns of CI variables strict mode drops; a vx task never sees an undeclared variable, and no run warns); `TURBO_LOGIN`, `TURBO_SSO_LOGIN_CALLBACK_PORT` and `TURBO_CONFIG_DIR_PATH` (no login verb and no user config file: the token comes as [Tokens](#tokens-and-the-os-keychain) says); `TURBO_NO_UPDATE_NOTIFIER`, `TURBO_PRINT_VERSION_DISABLED`, `TURBO_GLOBAL_WARNING_DISABLED` and `TURBO_DOWNLOAD_LOCAL_ENABLED` (the `turbo` binary's own; vx runs none).
|
|
82
86
|
- Turbo 2.11's `global` block (`futureFlags.globalConfiguration`) is read as the `globalDependencies`, `globalEnv` and `globalPassThroughEnv` it replaces, except that a task's `!` input takes a `global.inputs` file back, as Turbo prepends them to each task's inputs.
|
|
87
|
+
- The root `package.json`'s `engines` (a non-empty object), which Turbo hashes into every task's key: each task's `inputs.workspaceRuntime` holds one `awk` line that prints that value alone, compacted, at the workspace root, named `rootEngines` in the written `vx-preset.ts`. `turbo()` and the written config carry the same line, so they key alike; raising `engines.node` re-runs every task, and no other root edit re-keys one. A reformat keys alike; reordering its keys misses (Turbo's map would hit). A config written while the root declared no `engines` keys none added later.
|
|
83
88
|
- An unknown Turbo key is a todo naming it; `extends` is accepted and ignored (the overlay order above is what it means).
|
|
84
89
|
|
|
85
90
|
## `nx()` — an Nx repo, mid-migration
|
|
@@ -92,7 +97,7 @@ import { nx } from '@vzn/vx-migrate'
|
|
|
92
97
|
export default defineWorkspace({ plugins: [nx()] })
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line. Every target is keyed on its `inputs` (a target Nx does not cache on its own inputs alone, no `^` named inputs, since Nx hashes no dependency's hash into it), and a target Nx caches (`cache: true`, or the legacy `cacheableOperations` list in `nx.json`) gets its `outputs` as
|
|
100
|
+
Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps every project's `build` target the way `nx run-many -t build` would. `nx()` is also the repo's edge source: each project's dependencies on Nx's graph (package deps, `implicitDependencies`, imports) are its `deps`, so `^build` follows Nx's order with no lockfile plugin. The plugin reads Nx's **resolved** project graph — where Nx has already applied `targetDefaults`, expanded `namedInputs`, inferred targets through its plugins and interpolated `{projectRoot}` and friends — so nothing is re-derived here. Per target: `nx:run-commands` and a plain `command` run as one shell line that does what Nx's run-commands does (below); `nx:run-script` is the package script, with the `$npm_package_name`, `$npm_package_version` and `$npm_lifecycle_event` it reads defined as `<pm> run` sets them (a migration reads the name and version from the manifest it imports, so a bump reaches them; any other `$npm_*` is a todo); `nx:noop` is a group task, and so is a target with neither an executor nor a command that has dependencies, as Nx normalizes it (one with none is dropped, as Nx drops it); **every other executor runs through `nx-exec`** with the executor and its options on the command line. Every target is keyed on its `inputs` (a target Nx does not cache on its own inputs alone, no `^` named inputs, since Nx hashes no dependency's hash into it), and a target Nx caches (`cache: true`, or the legacy `cacheableOperations` list in `nx.json`) gets its `outputs` as `cache: true`, with no `inputs` meaning Nx's `default` and `^default`, named inputs resolved per project (nx.json's under the project's own; one neither defines, such as an `extends` preset not installed, keys the whole project with a todo, never an empty list that keys the task on its config alone), `{workspaceRoot}` / `{projectRoot}` / `{projectName}` interpolated anywhere in a path as Nx does (`{workspaceRoot}/coverage/{projectRoot}`), Nx's glob grammar translated (`*.[jt]s` is `*.{j,t}s`; the default `production` negation `?(*.)+(spec|test).[jt]s?(x)` becomes brace sets, a narrowing only a negation may take; what has no safe form is a todo), a project fileset of negations alone (nx-recipes' `noMarkdown`) starting from every project file, as Nx reads it, and an output outside the project dir (`dist/<project>` at the workspace root, Nx's default layout) a `workspaceFiles` output, an output outside the workspace (an old generator's `reportsDirectory: "../../coverage/<lib>"`) a todo and dropped, as vx caches only inside it, an output whose first segment is a wildcard (`{projectRoot}/**/*.d.ts`) cached, with the committed files it reaches kept (core never cleans one) and the rest cleaned before the run, and an output that covers the project's own `package.json` or vx.config cached with the file taken back by `!`; `dependsOn` becomes the edges (`^build` — dropped when no project has the target, as Nx gives it no edges and core refuses a `^name` nothing declares — `build`, `project:target` → `project#target`, everything past `project:` one target name, as Nx joins it (`ui:build:esm` is ui's `build:esm`; `ui:build:ci` names target `build:ci`, never `build`'s `ci` configuration, and is no edge where ui lacks it), this project's own target winning over a project name, `{ target, projects }` → each matched project's task, `projects` read as Nx's `findMatchingProjects` reads it: a name or a list of names, globs over names and over project directories (`libs/*`), `name:` / `tag:` / `directory:` labels, a bare word as a word in a name, and `!` exclusions; an edge to a target its project lacks is dropped without a word, as Nx drops it — `targetDefaults` that give every `typecheck` a `codegen` one project has; `^name` is every dependency's `name` whatever it holds, `^rsbuild:typecheck` included; a target glob — `test:e2e--*`, `^build-*`, `ui:build-{esm,cjs}`, a `{ target }` object's — expands over every target name in the workspace, as Nx 19.5+ does, before those rules; a same-project glob keeps its own project's matches), `{ env }` inputs pass through (one that is no shell variable name is keyed only), `{ runtime }` inputs run at the workspace root as Nx runs them (`inputs.workspaceRuntime`), with `{workspaceRoot}`, `{projectRoot}` and `{projectName}` written in as the paths and name they stand for (Nx runs the command as written, so `cat {workspaceRoot}/v.txt` keyed on cat's error there), and a command that exits non-zero keys its error and exit code and the task runs on, as Nx hashes its output whatever it exits (`git describe` in a repo with no tags): the written probe is `(<command>) || echo "exit $?"`, a `{ fileset, includeIgnored: true }` literal (Nx 23 hashes it from disk, gitignored or missing) is read by such a probe, since vx's globs see only what git lists, and a glob of one is a todo, a target that says `continuous` — or, in a graph from an Nx older than that field, runs a server executor (`@nx/vite:dev-server`, `@nx/next:server`, …) — is a persistent task and never cached, ready on spawn as Nx has it (Nx starts the dependents once it has started; a run-commands `readyWhen` gates them, below). The target's name never decides: a cached `dev` caches like any other target (nx#32610). `parallelism: false` (Nx runs the target alone) has no vx form and is a todo naming `--concurrency 1`; `syncGenerators` (Nx runs them first) is one workspace note per generator list, counting its tasks and naming `nx sync`; nx.json's `sync.globalGenerators` is one note per run naming `nx sync`. A target with `configurations` is one task per configuration (one whose name another target holds, `vite`'s `build` beside an inferred `vite:build`, is not written, with a todo, as Nx resolves `a:vite:build` to the target): `build` carries the default configuration's options, `build:ci` the `ci` one, and an edge of `build:ci` (own, named or `^`) reaches each target's `ci` task where it declares one, else its default, as Nx's `resolveConfiguration` does; a `^build` there becomes one edge per dependency Nx links. A `^name` input (`^production`, `{ input, dependencies: true }`, the pre-17 `{ input, projects: "dependencies" }`; `projects: "self"` is the own input, as Nx 23 still reads both) is what Nx hashes: each dependency's `name`, over the project graph, whether or not a task edge exists. A `{ input, projects }` list is read as `dependsOn`'s `projects` is; with `dependencies: true` beside it, both the listed projects' dependencies' input (Nx 24's reading) and this project's `^input` (Nx 23's) are keyed. Each project some reader reaches gets an `inputs:<name>` task — a keyed group (no command, nothing spawns, not counted in the run summary) keyed on its own `name` input, depending on its Nx dependencies' twins (a project nothing reaches gets none) — and a task that reads `^name` depends on its direct dependencies' twins, so the whole closure goes into its key with each project hashed once. A dependency FILESET (`^{projectRoot}/tsconfig.lib.json`, `{ fileset, dependencies: true }`, which Nx's own inferred `typecheck` writes) is keyed the same way: its twin, `inputs:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins spawn nothing and stay out of a run's output and task count, so `vx run build` counts the tasks `nx run-many -t build` does. A path input inside an output one of the project's own targets declares (TanStack/table's `public` input lists `{projectRoot}/dist`) is generated, and gitignored where Nx caches it: Nx's file map skips gitignored files, so Nx hashes nothing there (a changed `dist` file is a cache hit under Nx 23.3), and it is dropped without a todo, and the task that writes it keys its dependants through `dependsOn`.
|
|
96
101
|
|
|
97
102
|
| Option | Meaning |
|
|
98
103
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -253,10 +258,10 @@ Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package
|
|
|
253
258
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
254
259
|
| `dependsOn` | `dependsOn`, same micro-syntax (`$TURBO_ROOT$` deps are a TODO) |
|
|
255
260
|
| `inputs` | `inputs.files` (`$TURBO_DEFAULT$` → `**/*`; `$TURBO_ROOT$/x` → `workspaceFiles`) |
|
|
256
|
-
| `outputs` | `
|
|
261
|
+
| `outputs` | `outputs.files`, a negated one included |
|
|
257
262
|
| `env` | `inputs.env` **and** `exec.env.passThrough` (child envs are isolated) |
|
|
258
263
|
| `passThroughEnv` | `exec.env.passThrough` |
|
|
259
|
-
| `cache: false` / `persistent: true` | no `cache
|
|
264
|
+
| `cache: false` / `persistent: true` | no `cache: true`, still keyed on its `inputs`; `exec.persistent: {}`, with a TODO to set `readyWhen` when a task depends on it |
|
|
260
265
|
|
|
261
266
|
### Nx
|
|
262
267
|
|
|
@@ -266,19 +271,19 @@ Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.jso
|
|
|
266
271
|
|
|
267
272
|
Detected when the root `package.json` lists `vite-plus`, after turbo.json and Nx: vite-plus is a whole toolchain, so a repo with either of those runs its tasks there; `--from vite-task` picks Vite Task anyway. There is no live plugin, so `--keep` is refused. Each package's `vite.config.*` (vite-plus's file order) is loaded by Bun as `vp run` sees it, a function config called in build mode, and its `run` block mapped with the package's `package.json` scripts, which `vp run` runs too. The root package's tasks and scripts (`vp run -w`) make it a project when it has a `name`. A command that opens with `vp run` (or `vpr`) is inlined as Vite Task inlines it: `vp run x`, `pkg#x`, `-r x`, `-w x` and `-F <name> x` are `dependsOn` edges (`-r` to each package with `x`, the task's own reference pruned), and the task is a group when nothing follows them. They run in parallel where Vite Task ran a chain in order; any other form (`--parallel`, `-t`, forwarded arguments, a `vp run` after another command) stays in the command with a TODO.
|
|
268
273
|
|
|
269
|
-
| Vite Task | vx
|
|
270
|
-
| --------------------------------------------------------- |
|
|
271
|
-
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task
|
|
272
|
-
| `cwd` | `cd <cwd> && ` before the command
|
|
273
|
-
| `dependsOn: 'x'` / `'pkg#x'` | the same
|
|
274
|
-
| `dependsOn: { task, from }` | `^task`; when a member listed under a field `from` leaves out also has the task, the explicit `pkg#task` edges, since `^` follows every field
|
|
275
|
-
| `cache.input` / `cache.output` globs | `inputs.files` / `
|
|
276
|
-
| omitted or `{ auto: true }` (traced files) | no `cache
|
|
277
|
-
| `cache.env` | `inputs.env` **and** `exec.env.passThrough`; a `*` entry lists the names the package's tracked files and its workspace dependencies' spell (a TODO when none match); `!` entries take names back
|
|
278
|
-
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env`
|
|
279
|
-
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache`
|
|
280
|
-
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache
|
|
281
|
-
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` joined into its command; `false` keeps them tasks of their own
|
|
274
|
+
| Vite Task | vx |
|
|
275
|
+
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
276
|
+
| `command` (a string, or an array run in order) | `exec.command`, the array joined with `&&`; `command: []` is a group task |
|
|
277
|
+
| `cwd` | `cd <cwd> && ` before the command |
|
|
278
|
+
| `dependsOn: 'x'` / `'pkg#x'` | the same |
|
|
279
|
+
| `dependsOn: { task, from }` | `^task`; when a member listed under a field `from` leaves out also has the task, the explicit `pkg#task` edges, since `^` follows every field |
|
|
280
|
+
| `cache.input` / `cache.output` globs | `inputs.files` / `outputs.files`; `base: 'workspace'` → `workspaceFiles`, a root task's too; a `**` glued to text (`src/**.ts`), which wax refuses, widens an input to `**/*` and runs an output's task uncached, with a todo |
|
|
281
|
+
| omitted or `{ auto: true }` (traced files) | no `cache: true`, with a TODO: vx infers no inputs or outputs |
|
|
282
|
+
| `cache.env` | `inputs.env` **and** `exec.env.passThrough`; a `*` entry lists the names the package's tracked files and its workspace dependencies' spell (a TODO when none match); `!` entries take names back |
|
|
283
|
+
| `cache.untrackedEnv` | `exec.env.passThrough`, wildcards as `cache.env` |
|
|
284
|
+
| `cache: false`, root `run.cache` `false` / `tasks: false` | no `cache: true` |
|
|
285
|
+
| a `package.json` script | a task, uncached; with root `run.cache.scripts` a TODO for its cache |
|
|
286
|
+
| `run.enablePrePostScripts` (default `true`) | a script's `pre<name>` / `post<name>` joined into its command; `false` keeps them tasks of their own |
|
|
282
287
|
|
|
283
288
|
### package.json scripts
|
|
284
289
|
|
|
@@ -348,10 +353,10 @@ fails leaves the files as written and the note names its error.
|
|
|
348
353
|
Each script becomes a task with its command verbatim. `build` gets
|
|
349
354
|
`dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
|
|
350
355
|
compile`, is a group, and the edge goes on the script that does the
|
|
351
|
-
work) and **no cache
|
|
356
|
+
work) and **no `cache: true`** — under a
|
|
352
357
|
`TODO(vx-migrate)` showing the block to add with the package's real
|
|
353
358
|
inputs and outputs, and the same block commented as the task's first
|
|
354
|
-
line: set the real globs and delete the `// `. A task without
|
|
359
|
+
line: set the real globs and delete the `// `. A task without `cache: true` always runs; a block
|
|
355
360
|
with EMPTY outputs is not "uncached" but a no-output task that hits on
|
|
356
361
|
unchanged inputs and skips the build with nothing to restore (a deleted
|
|
357
362
|
`dist` stays deleted under a green `up-to-date` run — what `vx init`
|
|
@@ -552,7 +557,7 @@ export default defineWorkspace({
|
|
|
552
557
|
|
|
553
558
|
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()`.
|
|
554
559
|
|
|
555
|
-
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. An empty variable is unset, as Turbo reads it.
|
|
560
|
+
Every option falls back to the tool's own environment variable, so a self-hosted setup carries over unchanged. An empty variable is unset, as Turbo reads it. With no URL the cache is Vercel's hosted Remote Cache (`https://vercel.com/api`), exactly as for `turbo` — so `bunx turbo login && bunx turbo link`, then `turboCache()` with `TURBO_TOKEN` / `TURBO_TEAM` set, is the whole hosted setup. Below the environment, in Turbo's order: a Vercel build's `VERCEL_ARTIFACTS_TOKEN` and `VERCEL_ARTIFACTS_OWNER` (the team id, where `TURBO_TOKEN` with a team is not set), then the repo's `.turbo/config.json` (what `turbo link` writes: `apiUrl`, `teamId`, `teamSlug`, `token`; one that is not a JSON object is named in a warning and read as absent, where Turbo refuses it), then the root `turbo.json`'s `remoteCache` (`apiUrl`, `teamId`, `teamSlug`), whose `enabled: false` declines unless the options name a cache. As for `turbo`, these sources link a cache only with a team: a token without one is named in a warning, and with no team the plugin **declines** and the run stays local. A team with no token takes the token from the OS keychain ([Tokens](#tokens-and-the-os-keychain)). A `url`, `token` or `oidc` option names a cache with or without a team.
|
|
556
561
|
|
|
557
562
|
| Option | Environment variable | Meaning |
|
|
558
563
|
| ----------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -632,7 +637,7 @@ With no token from an option, a variable or a config file, both plugins read the
|
|
|
632
637
|
- 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.
|
|
633
638
|
- 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).
|
|
634
639
|
- 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`.
|
|
635
|
-
- 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`)
|
|
640
|
+
- Core's policy alone decides reads and writes: the plugins read none of Turbo's or Nx's own switches (`TURBO_CACHE`, `TURBO_REMOTE_ONLY`, `TURBO_REMOTE_CACHE_READ_ONLY`, `NX_SKIP_REMOTE_CACHE`, `NX_DISABLE_REMOTE_CACHE`); `turbo()` reads `TURBO_FORCE`, as `--force`. A CI that keeps untrusted pull requests off the shared cache sets `cache.policy` or `--cache-policy=local:rw,remote:r` for them.
|
|
636
641
|
|
|
637
642
|
## Testing
|
|
638
643
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.652",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo, Nx or package.json scripts: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@vzn/vx": "^0.0.
|
|
43
|
+
"@vzn/vx": "^0.0.652"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/adopt.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
|
+
// By namespace: bin.ts loads this before its release check, and a core
|
|
10
|
+
// with no `ui` must still reach that refusal.
|
|
11
|
+
import * as vx from '@vzn/vx'
|
|
9
12
|
import { UserError } from '@vzn/vx'
|
|
10
13
|
|
|
11
14
|
export type AdoptionMode = 'native' | 'keep'
|
|
@@ -270,8 +273,7 @@ async function runManager(
|
|
|
270
273
|
const line = argv.join(' ')
|
|
271
274
|
// The report's `install` row says what ran; a terminal sees it while it
|
|
272
275
|
// runs, on a line the report then takes the place of.
|
|
273
|
-
const
|
|
274
|
-
if (live) process.stdout.write(`\n ${line.slice(0, (process.stdout.columns ?? 80) - 4)} …`)
|
|
276
|
+
const clear = vx.ui.transient(line)
|
|
275
277
|
// The manager's own output (progress, deprecations, audit, postinstalls)
|
|
276
278
|
// is shown only when it fails: mid-init it buried the report (Growth's
|
|
277
279
|
// first-run funnel F3, 2026-10-13).
|
|
@@ -300,9 +302,9 @@ async function runManager(
|
|
|
300
302
|
failed = `: ${(e2 as Error).message}`
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
|
-
|
|
305
|
+
clear()
|
|
304
306
|
if (code !== 0) {
|
|
305
|
-
|
|
307
|
+
vx.ui.data('err', said)
|
|
306
308
|
const missing = notServed(said)
|
|
307
309
|
throw new UserError(
|
|
308
310
|
missing === undefined
|
package/src/bin.ts
CHANGED
|
@@ -27,6 +27,9 @@ try {
|
|
|
27
27
|
const { migrateCmd } = await import('./migrate.js')
|
|
28
28
|
process.exitCode = await migrateCmd(process.argv.slice(2))
|
|
29
29
|
} catch (err) {
|
|
30
|
+
// `ui` by namespace: a core older than it has none, and its refusal
|
|
31
|
+
// still reaches the reader.
|
|
32
|
+
const ui = (core as Partial<typeof core>).ui
|
|
30
33
|
// Asked for JSON (`vx init --dry --format json` hands a Turbo or Nx repo
|
|
31
34
|
// here), a refusal is core's error document on stdout too
|
|
32
35
|
// (schemas/error.json): stdout was empty.
|
|
@@ -38,14 +41,11 @@ try {
|
|
|
38
41
|
const docs = code.startsWith('VX_E_')
|
|
39
42
|
? { docs: `https://vznjs.github.io/vx/cli/#${code.toLowerCase()}` }
|
|
40
43
|
: {}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
+
const doc = `${JSON.stringify({ ok: false, error: { code, message: `vx-migrate: ${message}`, ...docs } })}\n`
|
|
45
|
+
if (ui === undefined) process.stdout.write(doc)
|
|
46
|
+
else ui.data('out', doc)
|
|
44
47
|
}
|
|
45
48
|
if (isUserError(err)) {
|
|
46
|
-
// `ui` by namespace: a core older than it has none, and its refusal
|
|
47
|
-
// still reaches the reader.
|
|
48
|
-
const ui = (core as Partial<typeof core>).ui
|
|
49
49
|
if (ui === undefined) process.stderr.write(`vx-migrate: ${err.message}\n`)
|
|
50
50
|
else ui.say('error', `vx-migrate: ${err.message}`)
|
|
51
51
|
process.exitCode = 1
|
package/src/migrate-scripts.ts
CHANGED
|
@@ -118,7 +118,7 @@ export function cacheTodo(command: string): string {
|
|
|
118
118
|
const words = ` ${command.replace(/[;&|()]/g, ' ')} `
|
|
119
119
|
const hit = BUILD_OUTPUTS.find(([re]) => re.test(words))
|
|
120
120
|
const outputs = (hit?.[1] ?? namedOutputs(command) ?? ['dist/**']).map((o) => `'${o}'`).join(', ')
|
|
121
|
-
return `cache: add \`inputs: { files: ['src/**'] },
|
|
121
|
+
return `cache: add \`inputs: { files: ['src/**'] }, outputs: { files: [${outputs}] }, cache: true\` with this package's real inputs and outputs — without it the task always runs, and so does every task that depends on it; a block with EMPTY outputs would be a cached no-op, not an uncached task`
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// `lint` is not here: a linter reads sources, and an edge to `build`
|
|
@@ -986,7 +986,7 @@ export function migrateScripts(
|
|
|
986
986
|
// Where to add one is the TODO's to say, when there is one: an Nx repo
|
|
987
987
|
// whose only script is `codegen` read "`build` carries a TODO" over
|
|
988
988
|
// "0 TODOs" (the first-five-minutes walk, 2026-09-28).
|
|
989
|
-
'each script became a task with its command verbatim; caching needs declared inputs and outputs, so no task got
|
|
989
|
+
'each script became a task with its command verbatim; caching needs declared inputs and outputs, so no task got `cache: true`',
|
|
990
990
|
],
|
|
991
991
|
projects,
|
|
992
992
|
extraFiles: [],
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import type { PackageMeta } from './packages.js'
|
|
15
15
|
import { mapTurboWorkspace, type TurboGlobal, type TurboMappedProject } from './turbo/turbo-map.js'
|
|
16
16
|
import { rootTaskProject } from './turbo/turbo-reads.js'
|
|
17
|
+
import { ROOT_ENGINES_PROBE } from './turbo/root-engines.js'
|
|
17
18
|
import { relPosix } from './paths.js'
|
|
18
19
|
import { gitIgnored, spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
19
20
|
import { DOTENV_GLOBS_HEAD, DOTENV_PROBE, DOTENV_PROBE_TOP } from './dotenv-probe.js'
|
|
@@ -86,6 +87,7 @@ export async function migrateTurbo(
|
|
|
86
87
|
return {
|
|
87
88
|
name: p.name,
|
|
88
89
|
dir: p.dir,
|
|
90
|
+
...(p.tags !== undefined ? { tags: p.tags } : {}),
|
|
89
91
|
importLines: [
|
|
90
92
|
...(readsManifest ? [MANIFEST_IMPORT] : []),
|
|
91
93
|
...(sealed
|
|
@@ -246,10 +248,11 @@ function identifier(task: string): string {
|
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
/**
|
|
249
|
-
* The
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
251
|
+
* The probes, by name: Turbo hashes a task's `.env` files although git
|
|
252
|
+
* ignores them, and a glob over git's files sees none, so the mapper keys
|
|
253
|
+
* them through a shell line that prints each file; and the root's
|
|
254
|
+
* `engines`, which Turbo folds into every task. Inline in every package's
|
|
255
|
+
* config, each line read as noise nobody could review.
|
|
253
256
|
*/
|
|
254
257
|
type Probe = { name: string; command: string }
|
|
255
258
|
|
|
@@ -262,6 +265,10 @@ const PROBES: readonly Probe[] = [
|
|
|
262
265
|
name: 'dotenvFilesDeep',
|
|
263
266
|
command: DOTENV_PROBE,
|
|
264
267
|
},
|
|
268
|
+
{
|
|
269
|
+
name: 'rootEngines',
|
|
270
|
+
command: ROOT_ENGINES_PROBE,
|
|
271
|
+
},
|
|
265
272
|
]
|
|
266
273
|
|
|
267
274
|
function nameProbes(projects: readonly TurboMappedProject[]): {
|
package/src/migrate.ts
CHANGED
|
@@ -429,7 +429,7 @@ async function lockWorkspace(root: string): Promise<{ code: number; said: string
|
|
|
429
429
|
// A success's warnings print above the report; a refusal is said whole.
|
|
430
430
|
// A success is a report row, matched as text: the colour FORCE_COLOR
|
|
431
431
|
// gave the child would hide its `✓ vx lock` mark from that match.
|
|
432
|
-
if (code === 0)
|
|
432
|
+
if (code === 0) ui.data('err', err)
|
|
433
433
|
return { code, said: code === 0 ? Bun.stripANSI(out) : out + err }
|
|
434
434
|
}
|
|
435
435
|
|
|
@@ -613,7 +613,8 @@ async function keep(
|
|
|
613
613
|
...new Set(['@vzn/vx', '@vzn/vx-migrate', ...(declares ? all.map((p) => p.pkg) : [])]),
|
|
614
614
|
])
|
|
615
615
|
if (args.json) {
|
|
616
|
-
|
|
616
|
+
ui.data(
|
|
617
|
+
'out',
|
|
617
618
|
`${JSON.stringify({
|
|
618
619
|
dry: true,
|
|
619
620
|
source,
|
|
@@ -637,7 +638,7 @@ async function keep(
|
|
|
637
638
|
// A run reads only the lock; with the plugins installed it is written
|
|
638
639
|
// now, else `next` names `vx lock` after the install.
|
|
639
640
|
const lock = !args.dry && !next.includes(' lock && ') ? await lockWorkspace(root) : undefined
|
|
640
|
-
if (lock !== undefined && lock.code !== 0)
|
|
641
|
+
if (lock !== undefined && lock.code !== 0) ui.data('err', lock.said)
|
|
641
642
|
const rows: ui.FieldRow[] = [
|
|
642
643
|
['from', `${source}, run by ${runner}() from @vzn/vx-migrate`],
|
|
643
644
|
...(args.dry ? [['dry run', 'nothing written'] as const] : []),
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
resolveSharedWorkspaceOutputs,
|
|
35
35
|
wildcardOutput,
|
|
36
36
|
wildcardTodo,
|
|
37
|
+
uncache,
|
|
37
38
|
} from '../shared-outputs.js'
|
|
38
39
|
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
39
40
|
import { isRecord, packageScripts, relPosix } from '../paths.js'
|
|
@@ -181,6 +182,7 @@ function checkNxNodes(nodes: Record<string, unknown>, label: string): void {
|
|
|
181
182
|
const data = (n['data'] ?? {}) as Record<string, unknown>
|
|
182
183
|
optional(data['root'], `${at}.data.root`, (v) => typeof v === 'string', 'a string')
|
|
183
184
|
optional(data['targets'], `${at}.data.targets`, isRecord, 'an object of targets')
|
|
185
|
+
checkNamedInputs(data['namedInputs'], `${at}.data.namedInputs`, label)
|
|
184
186
|
for (const [name, target] of Object.entries((data['targets'] ?? {}) as object)) {
|
|
185
187
|
const t = `${at}.data.targets.${JSON.stringify(name)}`
|
|
186
188
|
if (!isRecord(target)) refuse(t, 'an object')
|
|
@@ -195,6 +197,19 @@ function checkNxNodes(nodes: Record<string, unknown>, label: string): void {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
/**
|
|
201
|
+
* `namedInputs` as the mapper expands them: each name a list of inputs.
|
|
202
|
+
* A name holding an object or a number reached the expansion's loop and
|
|
203
|
+
* printed `TypeError: {} is not iterable` with its stack (fuzzed).
|
|
204
|
+
*/
|
|
205
|
+
function checkNamedInputs(v: unknown, at: string, label: string): void {
|
|
206
|
+
if (v === undefined) return
|
|
207
|
+
if (!isRecord(v)) throw new UserError(`${label}: ${at} must be an object of named inputs`)
|
|
208
|
+
for (const [name, members] of Object.entries(v))
|
|
209
|
+
if (!Array.isArray(members) || members.some((x) => typeof x !== 'string' && !isRecord(x)))
|
|
210
|
+
throw new UserError(`${label}: ${at}.${JSON.stringify(name)} must be an array of inputs`)
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
export interface MapNxOptions {
|
|
199
214
|
/** nx.json's legacy `cacheableOperations`, for a graph whose targets carry no `cache` field. */
|
|
200
215
|
readonly cacheable: ReadonlySet<string>
|
|
@@ -582,7 +597,7 @@ export async function mapNxWorkspace(
|
|
|
582
597
|
dropUnheldDeps(t, meta.name, emitted, emittedIds)
|
|
583
598
|
const reader = dependentReaders.get(t)
|
|
584
599
|
if (reader !== undefined && !reachesProducer(t, meta.name, emittedIds, reached)) {
|
|
585
|
-
|
|
600
|
+
uncache(t.task!)
|
|
586
601
|
t.todos.push(orphanReaderTodo(reader.globs, reader.dependsOn))
|
|
587
602
|
}
|
|
588
603
|
}
|
|
@@ -649,10 +664,7 @@ function resolveAtomizedWorkspaceOutputs(
|
|
|
649
664
|
const cached = (ps: readonly { name: string; dir: string; tasks: GeneratedTask[] }[]): number => {
|
|
650
665
|
const copy = structuredClone(ps.map((p) => ({ name: p.name, dir: p.dir, tasks: p.tasks })))
|
|
651
666
|
resolveSharedWorkspaceOutputs(root, copy)
|
|
652
|
-
return copy.reduce(
|
|
653
|
-
(n, p) => n + p.tasks.filter((t) => t.task?.['cache'] !== undefined).length,
|
|
654
|
-
0,
|
|
655
|
-
)
|
|
667
|
+
return copy.reduce((n, p) => n + p.tasks.filter((t) => t.task?.['cache'] === true).length, 0)
|
|
656
668
|
}
|
|
657
669
|
const better = split.size > 0 && cached(last) > cached(projects)
|
|
658
670
|
resolveSharedWorkspaceOutputs(root, better ? last : projects)
|
|
@@ -743,6 +755,7 @@ export async function readNxJsonFacts(root: string): Promise<NxJsonFacts> {
|
|
|
743
755
|
sync?: { globalGenerators?: unknown }
|
|
744
756
|
}
|
|
745
757
|
const named = parsed?.namedInputs
|
|
758
|
+
checkNamedInputs(named ?? undefined, 'namedInputs', 'nx.json')
|
|
746
759
|
const ops = parsed?.tasksRunnerOptions?.default?.options?.cacheableOperations
|
|
747
760
|
return {
|
|
748
761
|
namedInputs:
|
|
@@ -754,7 +767,8 @@ export async function readNxJsonFacts(root: string): Promise<NxJsonFacts> {
|
|
|
754
767
|
? parsed.sync.globalGenerators.filter((g): g is string => typeof g === 'string')
|
|
755
768
|
: [],
|
|
756
769
|
}
|
|
757
|
-
} catch {
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (err instanceof UserError) throw err
|
|
758
772
|
// An unreadable nx.json (or base) just degrades named-input refs to TODOs.
|
|
759
773
|
return none
|
|
760
774
|
}
|
|
@@ -1045,7 +1059,8 @@ function buildTask(
|
|
|
1045
1059
|
if (cacheEnabled) {
|
|
1046
1060
|
const outputs: Record<string, unknown> = { files: takingBack(outFiles) }
|
|
1047
1061
|
if (wsOutFiles.length > 0) outputs.workspaceFiles = takingBack(wsOutFiles)
|
|
1048
|
-
task.
|
|
1062
|
+
task.outputs = outputs
|
|
1063
|
+
task.cache = true
|
|
1049
1064
|
}
|
|
1050
1065
|
|
|
1051
1066
|
const generated: GeneratedTask = { name: variant.name, todos, task }
|
package/src/package-fs.ts
CHANGED
|
@@ -40,19 +40,35 @@ export function normalizeBunGlob(glob: string): string {
|
|
|
40
40
|
return neg ? `!${g}` : g
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Where the `[`…`]` class opening at `i` ends (a `]` right after the `[` is
|
|
45
|
+
* a member); `i` for a `[` with no `]`. A `,` `{` or `}` in a class is a
|
|
46
|
+
* member, never a brace cut, as `Bun.Glob` reads it.
|
|
47
|
+
*/
|
|
48
|
+
function classEnd(pattern: string, i: number): number {
|
|
49
|
+
const end = pattern.indexOf(']', i + 2)
|
|
50
|
+
return end === -1 ? i : end
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
/**
|
|
44
54
|
* `pattern` with its first brace group expanded, recursively, while that
|
|
45
55
|
* group holds a `/`: `Bun.Glob`'s scan finds nothing for one.
|
|
46
56
|
*/
|
|
47
57
|
export function slashBraceExpansions(pattern: string): string[] {
|
|
48
58
|
if (pattern.includes('\\')) return [pattern]
|
|
49
|
-
for (let open =
|
|
59
|
+
for (let open = 0; open < pattern.length; open++) {
|
|
60
|
+
if (pattern[open] === '[') {
|
|
61
|
+
open = classEnd(pattern, open)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (pattern[open] !== '{') continue
|
|
50
65
|
let depth = 0
|
|
51
66
|
const cuts: number[] = []
|
|
52
67
|
let close = -1
|
|
53
68
|
for (let i = open; i < pattern.length && close === -1; i++) {
|
|
54
69
|
const c = pattern[i]
|
|
55
|
-
if (c === '
|
|
70
|
+
if (c === '[') i = classEnd(pattern, i)
|
|
71
|
+
else if (c === '{') depth++
|
|
56
72
|
else if (c === '}' && --depth === 0) close = i
|
|
57
73
|
else if (c === ',' && depth === 1) cuts.push(i)
|
|
58
74
|
}
|
package/src/remote-refusal.ts
CHANGED
|
@@ -27,5 +27,10 @@ export async function refusal(res: Response, what = ''): Promise<Error> {
|
|
|
27
27
|
} catch {
|
|
28
28
|
// quiet: the status alone still names the refusal
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
// Each control as `\xNN`: an `ESC ]` in the server's words retitled the terminal (L-31).
|
|
31
|
+
const line = said
|
|
32
|
+
.slice(0, 200)
|
|
33
|
+
// oxlint-disable-next-line no-control-regex
|
|
34
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
|
35
|
+
return new Error(`${what}HTTP ${res.status}${line === '' ? '' : `: ${line}`}`)
|
|
31
36
|
}
|