@vzn/vx-migrate 0.0.596 → 0.0.598
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 +4 -4
- package/package.json +2 -2
- package/src/adoption-plugin.ts +2 -3
- package/src/migrate-nx.ts +3 -2
- package/src/migrate-turbo.ts +8 -51
- package/src/nx/export-graph.ts +21 -3
- package/src/nx/index.ts +2 -2
- package/src/nx/nx-inputs.ts +8 -1
- package/src/nx/nx-map.ts +1 -4
- package/src/nx/nx-upstream.ts +8 -1
- package/src/nx-cache/index.ts +14 -18
- package/src/paths.ts +4 -1
- package/src/remote-deadline.ts +13 -5
- package/src/tracked-outputs.ts +3 -2
- package/src/turbo/turbo-map.ts +15 -7
- package/src/turbo-cache/index.ts +21 -25
- package/src/vite-task/vite-task-map.ts +34 -4
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Until `bunx @vzn/vx-migrate` writes native config, `vx run build --all` maps eve
|
|
|
47
47
|
|
|
48
48
|
### The mapper (`mapTurboWorkspace`)
|
|
49
49
|
|
|
50
|
-
The plugin and the CLI read a repo the same way: the mapper the plugin runs live is what `bunx @vzn/vx-migrate` writes `vx.config.ts` per package from, splicing Turbo's global fields in as imports of a generated `vx-preset.ts
|
|
50
|
+
The plugin and the CLI read a repo the same way: the mapper the plugin runs live is what `bunx @vzn/vx-migrate` writes `vx.config.ts` per package from, splicing Turbo's global fields in as imports of a generated `vx-preset.ts` (its exports alone, no comments), where the plugin inlines the values; a global input another task writes to is taken back after the spread (`[...globalInputs, '!packages/plugin/dist/**']`), as the plugin takes it back from the inlined values. `splice` is the seam between the two consumers; `uses` names which globals a task drew on. Before 2026-09-10 the mapper lived in `@vzn/vx` itself; until 2026-09-11 the plugin was its own package, `@vzn/vx-turbo`.
|
|
51
51
|
|
|
52
52
|
Rules:
|
|
53
53
|
|
|
@@ -56,7 +56,7 @@ Rules:
|
|
|
56
56
|
- **The command is the script body** with its `pre<name>` / `post<name>` hooks folded in, in that order, through core's `foldScriptHooks`: each part in its own subshell, the chain stopping at the first that fails, forwarded `--` args reaching the body alone (npm and pnpm run them around `<name>` without being asked; novu's `prebuild` copies the CSS its `build` inlines), one process less per task than `pnpm run <name>` — except a body that calls yarn's `run` builtin (`run -T rollup -c`, `run clean && run build`; yarn ≥ 2 runs scripts in its own shell), which is `run: command not found` in sh: it runs as `yarn run <name>`, as Turbo and Nx run it, and yarn ≥ 2 runs no hooks, so none are folded there. A Yarn Plug'n'Play workspace (`.yarnrc.yml` with no `nodeLinker`, or `pnp`) has no `node_modules`: a dependency resolves only through `.pnp.cjs` and a bin only through yarn's shims, so there every script runs as `yarn run <name>`, and a `nodeLinker` change maps afresh. Same rules for `nx:run-script` below (strapi and novu, 2026-09-11).
|
|
57
57
|
- **Turbo 2.11's task `command`** (`futureFlags.experimentalTaskCommand`) wins over the script, as Turbo holds it: an argv is the command — each word quoted, run from the package dir, no `pre`/`post` hooks — and gives the package the task even with no script (turborepo's `@turbo/types#build`, `docs#schema`); `null` or `[]` is Turbo's no-op node, no task even where the script exists, its edges passed through; a per-toolchain map applies its `javascript` entry (or `typescript`, Turbo's alias), and without one the script runs. A shape Turbo would refuse keeps the script, with a todo.
|
|
58
58
|
- `dependsOn`: `^x` passes through when some package runs `x` or turbo.json defines it, and is dropped otherwise (core refuses a `^x` no project declares as a typo); `pkg#task` is kept only when `pkg` emits `task` (else a todo: edge dropped); a same-package task Turbo defines but the package has no script for is Turbo's no-op node, so its own edges pass through in its place (`test → codegen → ^build` with no `codegen` script is `test → ^build`); another package's `^x` or `pkg#x` reaches such a node through a group task when its edges name a task of its own package or another (with-tailwind's `ui` has no `build` script; its `build` builds `build:styles` and `build:components`, and `web#build` waits on them), and so does one whose `^` edge names another task (rallly's script-less `build` holds `^db:generate`), while one whose only edge is `^` to its own name needs none, since core's `^x` walks past a package without `x`; a no-op node nothing else reaches runs nothing, its own edges included, where Turbo still runs them (kitchen-sink's `turbo run test` builds `@repo/ui` for `admin`, which has no `test` script; `vx run test --all` does not, and `vx run build test --all` does both); a name Turbo does not define has no edge; `$TURBO_ROOT$` deps are a todo. A task turbo.json defines and no package has a script for (documenso's `lint`, shadcn-ui's `check`) is a group task in each package that defines it, its edges kept (a `pkg#task` edge only in `pkg`), so `vx run lint` runs Turbo's no-op nodes and exits 0 as `turbo run lint` does. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed; a root no member glob lists), and `//#x` in `dependsOn` is an edge to it, named by the root `package.json` `name`; a root task's globs are the workspace's (`workspaceFiles`), since Turbo hashes a root task over the whole repo; with no root project (a root `package.json` with no `name`, or one a package holds) they are a note and such an edge a todo. Turbo runs no plain task in the root package of a monorepo, and neither does `turbo()`; in a single-package repo (no workspaces, Turbo's `non-monorepo` example) turbo.json's plain tasks run on the root package, as under Turbo.
|
|
59
|
-
- 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: it runs `true`, cached and 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` running `true`).
|
|
59
|
+
- 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: it runs `true`, cached and 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` running `true`). 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).
|
|
60
60
|
- `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.
|
|
61
61
|
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx folds each dependency's key); from a `cache: false` producer (vercel/vercel's `//#generate:cache-keys`, which records the host) the named files, or its outputs, are a `cache.inputs.workspaceRuntime` probe, and `cache.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>` → `cache.inputs.workspaceFiles` (negation kept); any other `$TURBO_ROOT$` use is a todo. `globalDependencies` and Turbo 1's `globalDotEnv` land in `workspaceFiles` too, and a task's Turbo 1 `dotEnv` in its `files`. Turbo 1's `$NAME` entries, in `globalDependencies` or a task's `dependsOn`, are env vars: they join `cache.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 `cache.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 `.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 (`cache.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.
|
|
62
62
|
- `outputs`: `$TURBO_ROOT$/<path>` → `cache.outputs.workspaceFiles`; a negated output rides beside its positives and takes its paths back from the clean, the save and the restore (Next's `.next/**` minus `!.next/cache/**` keeps the cache), and one with no positive beside it takes back nothing and is dropped. Past its first segment an output takes Turbo's grammar (`dist/**/*.[cm]js`); the first stays a literal (a route directory). An output whose first segment is a wildcard (`**/*.d.ts`, `*/**`) runs the task uncached with a todo: Turbo never cleans an output, vx cleans it before every run, and such a glob reaches the sources. One segment with a literal extension the package tracks no file of (`*.xml`, `*.tsbuildinfo`) reaches only top-level artifacts and stays cached, and so does `**/<dir>/**` when the package tracks nothing under a directory of that name (vercel/ai's `**/dist/**`; `node_modules` is never an output), and so does a first segment no tracked top-level entry matches (tldraw's `dist-*/**`). A wildcard first segment with a literal rest (clerk's `*/package.json`, the subpath stubs it commits) reaches only files of that name: each committed one, up to 16, is taken back with `!` and the task stays cached; more, and it runs uncached. An output that covers the package's own `package.json` (trpc's client build lists it: the build rewrites `exports`) runs the task uncached with a todo: vx would delete the manifest before every run, and core refuses that config. `nx()` takes the manifest back with `!package.json` instead and keeps the cache (owner, 2026-10-03). 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 instead: each committed file it reaches is taken back with `!` (past sixteen, uncached with a todo), and the clean removes the rest, what the task writes (owner, 2026-10-03). A committed file under an output (typescript-eslint's `data/sponsors.json` in a cached `data`) is taken back with `!` — Turbo and Nx never clean an output, vx does — so it survives the clean and the task keeps its cache; past sixteen such files the task runs uncached with a todo. `turbo()` and `nx()` both do this, and so do the configs `vx-migrate` writes from turbo.json or an Nx graph (each take-back is a todo there, as it names files a later commit may move).
|
|
@@ -88,7 +88,7 @@ import { nx } from '@vzn/vx-migrate'
|
|
|
88
88
|
export default defineWorkspace({ plugins: [nx()] })
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
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. 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. A target Nx caches (`cache: true`, or the legacy `cacheableOperations` list in `nx.json`) gets its `inputs` / `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 each committed file it reaches taken back by `!` and the rest cleaned before the run (typescript-eslint's `**/*.shot`, 3,656 committed snapshots, is past sixteen and runs uncached with a todo), 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 (`cache.inputs.workspaceRuntime`), a `{ fileset, includeIgnored: true }` literal (Nx 23 hashes it from disk, gitignored or missing) is read by such a probe, since vx's globs see only what git lists, and a glob of one is a todo, a target that says `continuous` — or, in a graph from an Nx older than that field, runs a server executor (`@nx/vite:dev-server`, `@nx/next:server`, …) — is a persistent task and never cached, ready on spawn as Nx has it (Nx starts the dependents once it has started; a run-commands `readyWhen` gates them, below). The target's name never decides: a cached `dev` caches like any other target (nx#32610). `parallelism: false` (Nx runs the target alone) has no vx form and is a todo naming `--concurrency 1`; `syncGenerators` (Nx runs them first) is one workspace note per generator list, counting its tasks and naming `nx sync`; nx.json's `sync.globalGenerators` is one note per run naming `nx sync`. A target with `configurations` is one task per configuration (one whose name another target holds, `vite`'s `build` beside an inferred `vite:build`, is not written, with a todo, as Nx resolves `a:vite:build` to the target): `build` carries the default configuration's options, `build:ci` the `ci` one, and an edge of `build:ci` (own, named or `^`) reaches each target's `ci` task where it declares one, else its default, as Nx's `resolveConfiguration` does; a `^build` there becomes one edge per dependency Nx links. A `^name` input (`^production`, `{ input, dependencies: true }`, the pre-17 `{ input, projects: "dependencies" }`; `projects: "self"` is the own input, as Nx 23 still reads both) is what Nx hashes: each dependency's `name`, over the project graph, whether or not a task edge exists. A `{ input, projects }` list is read as `dependsOn`'s `projects` is. Each project some reader reaches gets an `nx-input:<name>` task — `true`, cached, keyed on its own `name` input, depending on its Nx dependencies' twins (a project nothing reaches gets none) — and a task that reads `^name` depends on its direct dependencies' twins, so the whole closure folds into its key with each project hashed once. A dependency FILESET (`^{projectRoot}/tsconfig.lib.json`, `{ fileset, dependencies: true }`, which Nx's own inferred `typecheck` writes) folds the same way: its twin, `nx-input:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. The twins show in a run's output; each costs about 0.16 ms of a warm run. 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`.
|
|
91
|
+
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. 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. A target Nx caches (`cache: true`, or the legacy `cacheableOperations` list in `nx.json`) gets its `inputs` / `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 each committed file it reaches taken back by `!` and the rest cleaned before the run (typescript-eslint's `**/*.shot`, 3,656 committed snapshots, is past sixteen and runs uncached with a todo), 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 (`cache.inputs.workspaceRuntime`), a `{ fileset, includeIgnored: true }` literal (Nx 23 hashes it from disk, gitignored or missing) is read by such a probe, since vx's globs see only what git lists, and a glob of one is a todo, a target that says `continuous` — or, in a graph from an Nx older than that field, runs a server executor (`@nx/vite:dev-server`, `@nx/next:server`, …) — is a persistent task and never cached, ready on spawn as Nx has it (Nx starts the dependents once it has started; a run-commands `readyWhen` gates them, below). The target's name never decides: a cached `dev` caches like any other target (nx#32610). `parallelism: false` (Nx runs the target alone) has no vx form and is a todo naming `--concurrency 1`; `syncGenerators` (Nx runs them first) is one workspace note per generator list, counting its tasks and naming `nx sync`; nx.json's `sync.globalGenerators` is one note per run naming `nx sync`. A target with `configurations` is one task per configuration (one whose name another target holds, `vite`'s `build` beside an inferred `vite:build`, is not written, with a todo, as Nx resolves `a:vite:build` to the target): `build` carries the default configuration's options, `build:ci` the `ci` one, and an edge of `build:ci` (own, named or `^`) reaches each target's `ci` task where it declares one, else its default, as Nx's `resolveConfiguration` does; a `^build` there becomes one edge per dependency Nx links. A `^name` input (`^production`, `{ input, dependencies: true }`, the pre-17 `{ input, projects: "dependencies" }`; `projects: "self"` is the own input, as Nx 23 still reads both) is what Nx hashes: each dependency's `name`, over the project graph, whether or not a task edge exists. A `{ input, projects }` list is read as `dependsOn`'s `projects` is. Each project some reader reaches gets an `nx-input:<name>` task — `true`, cached, keyed on its own `name` input, depending on its Nx dependencies' twins (a project nothing reaches gets none) — and a task that reads `^name` depends on its direct dependencies' twins, so the whole closure folds into its key with each project hashed once. A dependency FILESET (`^{projectRoot}/tsconfig.lib.json`, `{ fileset, dependencies: true }`, which Nx's own inferred `typecheck` writes) folds the same way: its twin, `nx-input:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins show in a run's output; each costs about 0.16 ms of a warm run. 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`.
|
|
92
92
|
|
|
93
93
|
| Option | Meaning |
|
|
94
94
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -176,7 +176,7 @@ An Nx project's `tags` are its vx `tags`, so `vx run build --filter tag:scope:we
|
|
|
176
176
|
|
|
177
177
|
### The graph snapshot
|
|
178
178
|
|
|
179
|
-
Once per run the plugin keys its snapshot (`<cache dir>/nx-project-graph.json`) on what Nx computes the graph from: `nx.json` and the files its `extends` chain names by content, and the worktree as git sees it — `HEAD`, `git status -uall` (core's own for the run, `DiscoverContext.worktreeChanges`: the tree is walked once, which took ~96 ms per run on refine; a `root` elsewhere asks git itself), and the content of every listed path under a project root (the last graph's too, so a project `discover` names counts before it is one), any `project.json`, or at the root (manifests, lockfiles, `tsconfig*.json`). A different key, or no snapshot, runs `node_modules/.bin/nx graph --file=<snapshot>` — the one time Nx itself runs, served from the daemon when one is up. So a source edit that adds a cross-package `import` (an edge Nx derives) or a config an Nx plugin infers targets from (`vite.config.ts`) re-exports before the run, tracked or untracked, and a touch without an edit, or a stray file a task writes at the root, does not. Outside a git worktree the plugin falls back to the manifests' mtimes. The snapshot and its key live in the cache dir, which ignores itself (a `*` `.gitignore` inside it), so writing them moves nothing. Nx's own caches (`.nx/cache/`, `.nx/workspace-data/`, which the export writes) never enter the key: under a root project (a standalone repo) that does not ignore them, each export re-exported on the next run. An export that fails with a snapshot in hand warns and runs on the previous graph. The snapshot is machine-local, like the cache: nothing about it enters a key. Measured at 1,000 projects: the export runs once (1.5 s with the daemon off after an edit, 0.9 s with it on; Nx's own computation), and a warm run with nothing changed pays the key, 43 ms at min where the manifest stats it replaced took 9 (item 1075), plus the mapping. Under `vx watch` the same rule runs per cycle: a `project.json` edit is the next cycle's tasks, the export included — 1.3 s from the edit to the new command's effect on that workspace. The mapping itself is kept beside it (`<cache dir>/vx-migrate-nx-mapping.json`), keyed on everything it reads — the graph, `nx.json` and its `extends` chain, every package manifest, each project dir's `.env` names, `NX_LOAD_DOT_ENV_FILES`, which bins are installed and the mapper's own code — so a warm run with nothing changed maps nothing.
|
|
179
|
+
Once per run the plugin keys its snapshot (`<cache dir>/nx-project-graph.json`) on what Nx computes the graph from: `nx.json` and the files its `extends` chain names by content, and the worktree as git sees it — `HEAD`, `git status -uall` (core's own for the run, `DiscoverContext.worktreeChanges`: the tree is walked once, which took ~96 ms per run on refine; a `root` elsewhere asks git itself), and the content of every listed path under a project root (the last graph's too, so a project `discover` names counts before it is one), any `project.json`, or at the root (manifests, lockfiles, `tsconfig*.json`). A different key, or no snapshot, runs `node_modules/.bin/nx graph --file=<snapshot>` (or, where the root links no `nx`, a Lerna repo on pnpm, the `nx` Lerna depends on, which `lerna run` runs) — the one time Nx itself runs, served from the daemon when one is up. So a source edit that adds a cross-package `import` (an edge Nx derives) or a config an Nx plugin infers targets from (`vite.config.ts`) re-exports before the run, tracked or untracked, and a touch without an edit, or a stray file a task writes at the root, does not. Outside a git worktree the plugin falls back to the manifests' mtimes. The snapshot and its key live in the cache dir, which ignores itself (a `*` `.gitignore` inside it), so writing them moves nothing. Nx's own caches (`.nx/cache/`, `.nx/workspace-data/`, which the export writes) never enter the key: under a root project (a standalone repo) that does not ignore them, each export re-exported on the next run. An export that fails with a snapshot in hand warns and runs on the previous graph. The snapshot is machine-local, like the cache: nothing about it enters a key. Measured at 1,000 projects: the export runs once (1.5 s with the daemon off after an edit, 0.9 s with it on; Nx's own computation), and a warm run with nothing changed pays the key, 43 ms at min where the manifest stats it replaced took 9 (item 1075), plus the mapping. Under `vx watch` the same rule runs per cycle: a `project.json` edit is the next cycle's tasks, the export included — 1.3 s from the edit to the new command's effect on that workspace. The mapping itself is kept beside it (`<cache dir>/vx-migrate-nx-mapping.json`), keyed on everything it reads — the graph, `nx.json` and its `extends` chain, every package manifest, each project dir's `.env` names, `NX_LOAD_DOT_ENV_FILES`, which bins are installed and the mapper's own code — so a warm run with nothing changed maps nothing.
|
|
180
180
|
|
|
181
181
|
### What it does not do
|
|
182
182
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.598",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo or Nx: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@vzn/vx": "^0.0.
|
|
42
|
+
"@vzn/vx": "^0.0.598"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/adoption-plugin.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// against all of them at once). `turbo()` and `nx()` each carried this
|
|
6
6
|
// skeleton until item 592; what differs is only how the mapping is made.
|
|
7
7
|
|
|
8
|
-
import { definePlugin, type ProjectHookContext, type
|
|
8
|
+
import { definePlugin, type ProjectHookContext, type VxPlugin } from '@vzn/vx'
|
|
9
9
|
import { type AdoptionMapping, cachedMapping } from './mapping-cache.js'
|
|
10
10
|
import { warnGaps } from './plugin-gaps.js'
|
|
11
11
|
import { headStamp, spareTrackedOutputs, trackedFiles } from './tracked-outputs.js'
|
|
@@ -111,8 +111,7 @@ export function adoptionPlugin(
|
|
|
111
111
|
// only reads a stage-given task after validating it. The copy was
|
|
112
112
|
// the per-process memo's guard and cost 10 ms per run at 1,000
|
|
113
113
|
// projects, 3,000 clones (item 609).
|
|
114
|
-
if (!Object.hasOwn(config.tasks, t.name))
|
|
115
|
-
config.tasks[t.name] = t.task as unknown as TaskConfig
|
|
114
|
+
if (!Object.hasOwn(config.tasks, t.name)) config.tasks[t.name] = t.task
|
|
116
115
|
}
|
|
117
116
|
},
|
|
118
117
|
// The root files the mapping reads (`turbo.json`, `nx.json`) shape every
|
package/src/migrate-nx.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type MigrationFormat, type MigrationPlan, type ProjectMeta } from '@vzn
|
|
|
7
7
|
import { mapNxWorkspace, nxSizeText, parseNxGraph, readNxJson } from './nx/nx-map.js'
|
|
8
8
|
import { spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
9
9
|
import { adoptedToolNotes } from './workspace-notes.js'
|
|
10
|
+
import { relPosix } from './paths.js'
|
|
10
11
|
|
|
11
12
|
/** What a task's `npm_package_*` read: the manifest, so a bump reaches them. */
|
|
12
13
|
const MANIFEST_IMPORT = "import pkg from './package.json' with { type: 'json' }"
|
|
@@ -90,7 +91,7 @@ async function unlistedProjects(
|
|
|
90
91
|
(p) => path.resolve(p.dir) !== path.resolve(root) && !listed.has(path.resolve(p.dir)),
|
|
91
92
|
)
|
|
92
93
|
if (unlisted.length === 0) return { manifests: [], notes: [] }
|
|
93
|
-
const rels = unlisted.map((p) =>
|
|
94
|
+
const rels = unlisted.map((p) => relPosix(root, p.dir)).sort()
|
|
94
95
|
const manifests: { relPath: string; contents: string }[] = []
|
|
95
96
|
for (const p of unlisted) {
|
|
96
97
|
const file = path.join(p.dir, 'package.json')
|
|
@@ -102,7 +103,7 @@ async function unlistedProjects(
|
|
|
102
103
|
)
|
|
103
104
|
continue
|
|
104
105
|
manifests.push({
|
|
105
|
-
relPath:
|
|
106
|
+
relPath: relPosix(root, file),
|
|
106
107
|
contents: `${JSON.stringify({ name: p.name, private: true }, null, 2)}\n`,
|
|
107
108
|
})
|
|
108
109
|
}
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -217,7 +217,7 @@ function hoistTaskEnv(projects: readonly TurboMappedProject[]): {
|
|
|
217
217
|
const env = own((t.task as Task).cache?.inputs?.env)
|
|
218
218
|
const name = env === null ? undefined : nameOf.get(keyOf(t.name, env))
|
|
219
219
|
if (name === undefined) continue
|
|
220
|
-
for (const list of lists(t.task
|
|
220
|
+
for (const list of lists(t.task)) {
|
|
221
221
|
const names = own(list)
|
|
222
222
|
if (names === null || list === undefined) continue
|
|
223
223
|
if (!env!.every((n, i) => names[i] === n)) continue
|
|
@@ -250,20 +250,16 @@ function identifier(task: string): string {
|
|
|
250
250
|
* keys them through a shell line that prints each file. Inline in every
|
|
251
251
|
* package's config, that line read as noise nobody could review.
|
|
252
252
|
*/
|
|
253
|
-
type Probe = { name: string; command: string
|
|
253
|
+
type Probe = { name: string; command: string }
|
|
254
254
|
|
|
255
255
|
const PROBES: readonly Probe[] = [
|
|
256
256
|
{
|
|
257
257
|
name: 'dotenvFiles',
|
|
258
258
|
command: DOTENV_PROBE_TOP,
|
|
259
|
-
doc: "Each `.env` file in the task's directory, name and bytes",
|
|
260
|
-
field: 'runtime',
|
|
261
259
|
},
|
|
262
260
|
{
|
|
263
261
|
name: 'dotenvFilesDeep',
|
|
264
262
|
command: DOTENV_PROBE,
|
|
265
|
-
doc: "Each `.env` file under the task's directory, name and bytes",
|
|
266
|
-
field: 'runtime',
|
|
267
263
|
},
|
|
268
264
|
]
|
|
269
265
|
|
|
@@ -282,8 +278,6 @@ function nameProbes(projects: readonly TurboMappedProject[]): {
|
|
|
282
278
|
const probe: Probe = {
|
|
283
279
|
name: `dotenvRootFiles${rootGlobs.length === 0 ? '' : rootGlobs.length + 1}`,
|
|
284
280
|
command: v,
|
|
285
|
-
doc: "The `.env` files the workspace's root globs name, name and bytes",
|
|
286
|
-
field: 'workspaceRuntime',
|
|
287
281
|
}
|
|
288
282
|
rootGlobs.push(probe)
|
|
289
283
|
return probe
|
|
@@ -320,49 +314,12 @@ function renderPreset(
|
|
|
320
314
|
// glob, or an env name a user hand-wrote) may contain a `'`/`\`/newline that
|
|
321
315
|
// would otherwise splice into a malformed, unloadable `vx-preset.ts`.
|
|
322
316
|
const arr = (xs: string[]): string => `[${xs.map(quote).join(', ')}]`
|
|
323
|
-
const lines = [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
'',
|
|
330
|
-
'// From globalDependencies and what Turbo adds to them (the packages the',
|
|
331
|
-
'// root depends on, microfrontends configs) — workspace-root-relative,',
|
|
332
|
-
'// spread into each task’s cache.inputs.workspaceFiles.',
|
|
333
|
-
`export const globalInputs = ${arr(inputs)}`,
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
if (env.length > 0) {
|
|
337
|
-
lines.push(
|
|
338
|
-
'',
|
|
339
|
-
'// From globalEnv: cache inputs AND passed through to every task',
|
|
340
|
-
'// (vx child environments are isolated; see docs/schema.md).',
|
|
341
|
-
`export const globalEnvInputs = ${arr(env)}`,
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
if (pass.length > 0) {
|
|
345
|
-
lines.push(
|
|
346
|
-
'',
|
|
347
|
-
'// From globalPassThroughEnv: forwarded to every task, never hashed.',
|
|
348
|
-
`export const globalPassThroughEnv = ${arr(pass)}`,
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
for (const l of shared)
|
|
352
|
-
lines.push(
|
|
353
|
-
'',
|
|
354
|
-
`// turbo.json's \`${l.task}\` env: hashed and passed where a config spreads it.`,
|
|
355
|
-
`export const ${l.name} = ${arr([...l.values])}`,
|
|
356
|
-
)
|
|
357
|
-
for (const probe of probes) {
|
|
358
|
-
lines.push(
|
|
359
|
-
'',
|
|
360
|
-
`// ${probe.doc}. Turbo hashes`,
|
|
361
|
-
"// `.env` files although git ignores them, and a glob over git's files",
|
|
362
|
-
`// sees none: cache.inputs.${probe.field} keys them.`,
|
|
363
|
-
`export const ${probe.name} = ${quote(probe.command)}`,
|
|
364
|
-
)
|
|
365
|
-
}
|
|
317
|
+
const lines: string[] = []
|
|
318
|
+
if (inputs.length > 0) lines.push(`export const globalInputs = ${arr(inputs)}`)
|
|
319
|
+
if (env.length > 0) lines.push(`export const globalEnvInputs = ${arr(env)}`)
|
|
320
|
+
if (pass.length > 0) lines.push(`export const globalPassThroughEnv = ${arr(pass)}`)
|
|
321
|
+
for (const l of shared) lines.push(`export const ${l.name} = ${arr([...l.values])}`)
|
|
322
|
+
for (const probe of probes) lines.push(`export const ${probe.name} = ${quote(probe.command)}`)
|
|
366
323
|
lines.push('')
|
|
367
324
|
return lines.join('\n')
|
|
368
325
|
}
|
package/src/nx/export-graph.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The resolved Nx graph, exported by the workspace's own nx: `nx()` keys and
|
|
2
2
|
// keeps it, `vx-migrate --from nx` maps it once.
|
|
3
3
|
|
|
4
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
4
5
|
import { mkdir } from 'node:fs/promises'
|
|
5
6
|
import path from 'node:path'
|
|
6
7
|
|
|
@@ -10,9 +11,10 @@ import path from 'node:path'
|
|
|
10
11
|
* daemon up the export is served from memory, without it Nx computes.
|
|
11
12
|
*/
|
|
12
13
|
export async function exportGraph(root: string, snapshot: string): Promise<string | null> {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const linked = path.join(root, 'node_modules', '.bin', 'nx')
|
|
15
|
+
const bin = (await Bun.file(linked).exists()) ? linked : lernasNx(root)
|
|
16
|
+
if (bin === undefined) {
|
|
17
|
+
return `no ${path.relative(root, linked)} — install nx, or export a graph with \`nx graph --file=<path>\` and pass it as graph: '<path>'`
|
|
16
18
|
}
|
|
17
19
|
// `discover` runs before the run opens (and makes) the cache dir.
|
|
18
20
|
await mkdir(path.dirname(snapshot), { recursive: true })
|
|
@@ -27,3 +29,19 @@ export async function exportGraph(root: string, snapshot: string): Promise<strin
|
|
|
27
29
|
const tail = err.trim().split('\n').slice(-3).join(' ')
|
|
28
30
|
return `nx graph --file exited ${code}${tail ? `: ${tail}` : ''}`
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The nx Lerna 6+ runs on, from Lerna's own dependencies: pnpm links only
|
|
35
|
+
* a root's direct dependencies into `node_modules/.bin`, so a Lerna repo
|
|
36
|
+
* on pnpm has no `nx` there, and `lerna run` uses this one.
|
|
37
|
+
*/
|
|
38
|
+
function lernasNx(root: string): string | undefined {
|
|
39
|
+
try {
|
|
40
|
+
const lerna = realpathSync(path.join(root, 'node_modules', 'lerna'))
|
|
41
|
+
const manifest = Bun.resolveSync('nx/package.json', lerna)
|
|
42
|
+
const bin = (JSON.parse(readFileSync(manifest, 'utf8')) as { bin?: { nx?: unknown } }).bin?.nx
|
|
43
|
+
return typeof bin === 'string' ? path.join(path.dirname(manifest), bin) : undefined
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/nx/index.ts
CHANGED
|
@@ -524,7 +524,7 @@ async function graphInputKey(
|
|
|
524
524
|
if (changed === null) return null
|
|
525
525
|
// The cache dir holds the snapshot itself: a workspace that does not
|
|
526
526
|
// ignore it would see the export move the key it was keyed on.
|
|
527
|
-
const cacheRel =
|
|
527
|
+
const cacheRel = relPosix(root, cacheDir)
|
|
528
528
|
const inCache = (p: string): boolean =>
|
|
529
529
|
cacheRel !== '' &&
|
|
530
530
|
!cacheRel.startsWith('..') &&
|
|
@@ -540,7 +540,7 @@ async function graphInputKey(
|
|
|
540
540
|
// stray write at the root (a report, a log) is not, and counting it
|
|
541
541
|
// re-exported the graph on every run after it.
|
|
542
542
|
// A `project.json` anywhere: a new one is a project no root lists yet.
|
|
543
|
-
const roots = dirs.map((d) =>
|
|
543
|
+
const roots = dirs.map((d) => relPosix(root, d))
|
|
544
544
|
const graphFile = (p: string): boolean =>
|
|
545
545
|
p === 'project.json' ||
|
|
546
546
|
p.endsWith('/project.json') ||
|
package/src/nx/nx-inputs.ts
CHANGED
|
@@ -77,7 +77,14 @@ export function expandNxInputs(
|
|
|
77
77
|
todos.push(`input ${JSON.stringify(entry)}: glob syntax vx cannot take — map manually`)
|
|
78
78
|
return
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// Nx globs a `{workspaceRoot}` path over every file of the
|
|
81
|
+
// workspace, a vx project glob only over its project's own: on the
|
|
82
|
+
// root project TanStack Query's `{workspaceRoot}/**/package.json`
|
|
83
|
+
// (sherif's input) left every package's manifest out of the key.
|
|
84
|
+
const own =
|
|
85
|
+
(at.rel === '' || at.rel === '.') && s.startsWith('{workspaceRoot}')
|
|
86
|
+
? null
|
|
87
|
+
: underProject(g, at.rel)
|
|
81
88
|
if (own === null) into.wsFiles.push(neg + withProjectDir(g, at.rel, true))
|
|
82
89
|
else into.files.push(neg + withProjectDir(own, at.rel, true))
|
|
83
90
|
return
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
wildcardTodo,
|
|
37
37
|
} from '../shared-outputs.js'
|
|
38
38
|
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
39
|
-
import { packageScripts, relPosix } from '../paths.js'
|
|
39
|
+
import { isRecord, packageScripts, relPosix } from '../paths.js'
|
|
40
40
|
import { withLernaOrder } from './lerna.js'
|
|
41
41
|
import { mapNxDeps, matchNxProjects, type TaskNameFor } from './nx-deps.js'
|
|
42
42
|
import {
|
|
@@ -143,9 +143,6 @@ const LEGACY: Readonly<Record<string, string>> = {
|
|
|
143
143
|
'@nx/workspace:run-script': 'nx:run-script',
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
147
|
-
typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
148
|
-
|
|
149
146
|
/** Every dependency name the root package.json declares; none without one. */
|
|
150
147
|
async function rootDependencies(root: string): Promise<ReadonlySet<string>> {
|
|
151
148
|
const pkg = (await Bun.file(path.join(root, 'package.json'))
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -339,7 +339,14 @@ export function planNxUpstream(
|
|
|
339
339
|
if (u.of !== 'deps') foldProjects(u.name, u.of, inputs, todos, edges)
|
|
340
340
|
else if (u.name !== name) fold(node, u.name, peers, inputs, todos, edges)
|
|
341
341
|
}
|
|
342
|
-
|
|
342
|
+
// Nx hashes a fileset that matches nothing as nothing; core warns of
|
|
343
|
+
// an input set that resolves to no file. A project has a package.json
|
|
344
|
+
// or a project.json, and Nx keys each task on both (its manifest, its
|
|
345
|
+
// configuration), so listing them keeps the twin of an absent
|
|
346
|
+
// `tsconfig.spec.json` (or a dependency's unbuilt `*.d.ts`) quiet; the
|
|
347
|
+
// file appearing still re-keys it, and a missing literal is silent.
|
|
348
|
+
const files = [...new Set(['package.json', 'project.json', ...inputs.files])]
|
|
349
|
+
const cacheInputs: Record<string, unknown> = { files }
|
|
343
350
|
if (inputs.wsFiles.length > 0) cacheInputs.workspaceFiles = inputs.wsFiles
|
|
344
351
|
if (inputs.envNames.length > 0) cacheInputs.env = inputs.envNames
|
|
345
352
|
if (inputs.runtimeCmds.length > 0) cacheInputs.workspaceRuntime = inputs.runtimeCmds
|
package/src/nx-cache/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
refuseUnknownOptions,
|
|
21
21
|
type PluginOptionKinds,
|
|
22
22
|
} from '@vzn/vx'
|
|
23
|
-
import {
|
|
23
|
+
import { deadline } from '../remote-deadline.js'
|
|
24
24
|
import { OutageBreaker } from '../remote-breaker.js'
|
|
25
25
|
import { withRetry } from '../remote-retry.js'
|
|
26
26
|
import { headerValueFault } from '../remote-token.js'
|
|
@@ -142,23 +142,19 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
142
142
|
// every hit read as a corrupt artifact (nx#33092).
|
|
143
143
|
headers['Accept'] = 'application/octet-stream'
|
|
144
144
|
}
|
|
145
|
-
const res = await this.breaker
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
.catch((err: unknown) => {
|
|
160
|
-
throw deadlineNamed(err, this.config.timeoutMs)
|
|
161
|
-
})
|
|
145
|
+
const res = await this.breaker.send(() =>
|
|
146
|
+
withRetry(
|
|
147
|
+
() =>
|
|
148
|
+
this.fetchImpl(`${this.config.server}/v1/cache/${hash}`, {
|
|
149
|
+
method,
|
|
150
|
+
headers,
|
|
151
|
+
...(body === undefined ? {} : { body }),
|
|
152
|
+
signal: deadline(this.config.timeoutMs),
|
|
153
|
+
}),
|
|
154
|
+
this.config.retries,
|
|
155
|
+
this.wait,
|
|
156
|
+
),
|
|
157
|
+
)
|
|
162
158
|
if (method === 'PUT' && res.status === 403) {
|
|
163
159
|
const first = !this.writesRefused
|
|
164
160
|
this.writesRefused = true
|
package/src/paths.ts
CHANGED
|
@@ -33,6 +33,9 @@ export function relPosix(from: string, to: string): string {
|
|
|
33
33
|
export function packageScripts(meta: ProjectMeta): Record<string, unknown> {
|
|
34
34
|
const raw = (meta.packageJson as unknown as { scripts?: unknown }).scripts
|
|
35
35
|
const scripts: Record<string, unknown> = Object.create(null)
|
|
36
|
-
if (
|
|
36
|
+
if (!isRecord(raw)) return scripts
|
|
37
37
|
return Object.assign(scripts, raw)
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
export const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
41
|
+
typeof v === 'object' && v !== null && !Array.isArray(v)
|
package/src/remote-deadline.ts
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
* A request's deadline, named. The runtime's abort says "The operation
|
|
3
3
|
* timed out." and nothing else; core's degrade line already names the
|
|
4
4
|
* request, the artifact and the server, so the cause names the deadline
|
|
5
|
-
* that was spent.
|
|
5
|
+
* that was spent. The name rides the signal's reason, not a catch around
|
|
6
|
+
* `fetch`: the deadline covers the body too, and a body read past it
|
|
7
|
+
* (core's ingest, the signed download's temp) rejects with the reason
|
|
8
|
+
* (measured on Bun 1.4.2), where a catch around `fetch` never saw it.
|
|
9
|
+
* Unref'd, as `AbortSignal.timeout`'s timer is. Shared by `turboCache()`
|
|
10
|
+
* and `nxCache()`.
|
|
6
11
|
*/
|
|
7
|
-
export function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
export function deadline(timeoutMs: number): AbortSignal {
|
|
13
|
+
const controller = new AbortController()
|
|
14
|
+
setTimeout(
|
|
15
|
+
() => controller.abort(new Error(`no answer within ${timeoutMs} ms`)),
|
|
16
|
+
timeoutMs,
|
|
17
|
+
).unref()
|
|
18
|
+
return controller.signal
|
|
11
19
|
}
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { lstat, readFile } from 'node:fs/promises'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { isLiteralPattern, outputsOverlap } from '@vzn/vx'
|
|
13
|
+
import { relPosix } from './paths.js'
|
|
13
14
|
|
|
14
15
|
/** The files git tracks under `root`, root-relative; null outside a repo or without git. */
|
|
15
16
|
export async function trackedFiles(root: string): Promise<string[] | null> {
|
|
@@ -220,7 +221,7 @@ export function spareTrackedOutputs(
|
|
|
220
221
|
/** Drop the `!` entries readers carry for a committed file no output names any more. */
|
|
221
222
|
const unhide = (owner: (typeof projects)[number] | null, rooted: string) => {
|
|
222
223
|
for (const q of projects) {
|
|
223
|
-
const qrel =
|
|
224
|
+
const qrel = relPosix(root, q.dir)
|
|
224
225
|
for (const r of q.tasks) {
|
|
225
226
|
const inputs = (r.task?.['cache'] as { inputs?: Outputs } | undefined)?.inputs
|
|
226
227
|
if (inputs === undefined) continue
|
|
@@ -238,7 +239,7 @@ export function spareTrackedOutputs(
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
for (const p of projects) {
|
|
241
|
-
const rel =
|
|
242
|
+
const rel = relPosix(root, p.dir)
|
|
242
243
|
const own = rel === '' ? tracked : filesUnder(tracked, rel).map((f) => f.slice(rel.length + 1))
|
|
243
244
|
for (const t of p.tasks) {
|
|
244
245
|
const outputs = (t.task?.['cache'] as { outputs?: Outputs } | undefined)?.outputs
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
wildcardOutput,
|
|
29
29
|
wildcardTodo,
|
|
30
30
|
} from '../shared-outputs.js'
|
|
31
|
-
import { packageScripts, relPosix } from '../paths.js'
|
|
31
|
+
import { isRecord, packageScripts, relPosix } from '../paths.js'
|
|
32
32
|
import { MAX_SPARED, type TrackedKinds } from '../tracked-outputs.js'
|
|
33
33
|
import {
|
|
34
34
|
DOTENV_PROBE,
|
|
@@ -439,9 +439,6 @@ async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
|
439
439
|
return parsed as TurboJson
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
443
|
-
typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
444
|
-
|
|
445
442
|
const TOP_LISTS = [
|
|
446
443
|
'globalDependencies',
|
|
447
444
|
'globalEnv',
|
|
@@ -1166,14 +1163,19 @@ export async function mapTurboWorkspace(
|
|
|
1166
1163
|
// One with edges of its own (with-shell-commands' `tooling-config#build`
|
|
1167
1164
|
// → `prebuild`) was a group, which keys nothing: Turbo's node still
|
|
1168
1165
|
// hashes the package's files, so it is key-only with its edges. A node
|
|
1169
|
-
// that starts persistent sidecars stays a group.
|
|
1166
|
+
// that starts persistent sidecars stays a group. One a `^name` reaches
|
|
1167
|
+
// whose own edges lack `^name` (opencode's `build: { dependsOn: [] }`)
|
|
1168
|
+
// is key-only too: Turbo's `^build` stops at it, and core's walked past
|
|
1169
|
+
// it to the builds below, running and keying ones Turbo never waits on.
|
|
1170
1170
|
const keyOnly = new Map<string, Set<string>>()
|
|
1171
1171
|
for (const meta of metas) {
|
|
1172
1172
|
const scripts = packageScripts(meta)
|
|
1173
1173
|
const { defined, defFor } = definitions(meta)
|
|
1174
|
+
const depended = metas.some((m) => m !== meta && declares(m, meta.name))
|
|
1174
1175
|
for (const name of defined) {
|
|
1175
1176
|
const def = defFor(name)
|
|
1176
|
-
|
|
1177
|
+
const stops = depended && caretNames.has(name) && !(def?.dependsOn ?? []).includes(`^${name}`)
|
|
1178
|
+
if (!(caretSelf.has(name) || stops) || !withScript.has(name) || transit.has(name)) continue
|
|
1177
1179
|
if (scripts[name] !== undefined || commandOverride(def) !== undefined) continue
|
|
1178
1180
|
if (sidecarGroups.has(`${meta.name}#${name}`)) continue
|
|
1179
1181
|
if (def?.cache === false || def?.persistent === true) continue
|
|
@@ -1293,7 +1295,13 @@ export async function mapTurboWorkspace(
|
|
|
1293
1295
|
// Core gives a project with no `build` this very node (a group behind
|
|
1294
1296
|
// `^build`, keyed on the project's files), so writing it is noise:
|
|
1295
1297
|
// solid's three script-less packages each got a `build` running `true`.
|
|
1296
|
-
|
|
1298
|
+
const noopEdges = defFor(name)!.dependsOn ?? []
|
|
1299
|
+
if (
|
|
1300
|
+
noop &&
|
|
1301
|
+
name === 'build' &&
|
|
1302
|
+
noopEdges.includes('^build') &&
|
|
1303
|
+
noopEdges.every((d) => d === '^build')
|
|
1304
|
+
)
|
|
1297
1305
|
continue
|
|
1298
1306
|
if (
|
|
1299
1307
|
override === undefined &&
|
package/src/turbo-cache/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
refuseUnknownOptions,
|
|
25
25
|
type PluginOptionKinds,
|
|
26
26
|
} from '@vzn/vx'
|
|
27
|
-
import {
|
|
27
|
+
import { deadline } from '../remote-deadline.js'
|
|
28
28
|
import { OutageBreaker } from '../remote-breaker.js'
|
|
29
29
|
import { withRetry } from '../remote-retry.js'
|
|
30
30
|
import { headerValueFault } from '../remote-token.js'
|
|
@@ -468,32 +468,28 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
468
468
|
): Promise<Response | undefined> {
|
|
469
469
|
const timeoutMs = init.timeoutMs ?? this.config.timeoutMs
|
|
470
470
|
// 0 is Turbo's "no deadline".
|
|
471
|
-
const signal = () => (timeoutMs === 0 ? undefined :
|
|
471
|
+
const signal = () => (timeoutMs === 0 ? undefined : deadline(timeoutMs))
|
|
472
472
|
// The batch query is never preflighted, as in Turbo.
|
|
473
473
|
const preflight = this.config.preflight && method !== 'POST'
|
|
474
|
-
const res = await this.breaker
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
)
|
|
494
|
-
.catch((err: unknown) => {
|
|
495
|
-
throw deadlineNamed(err, timeoutMs)
|
|
496
|
-
})
|
|
474
|
+
const res = await this.breaker.send(() =>
|
|
475
|
+
withRetry(
|
|
476
|
+
async () => {
|
|
477
|
+
const extra = init.headers ?? {}
|
|
478
|
+
const to = preflight
|
|
479
|
+
? await this.preflight(this.url(pathname), method, extra, signal)
|
|
480
|
+
: { url: this.url(pathname), auth: true }
|
|
481
|
+
const s = signal()
|
|
482
|
+
return this.fetchImpl(to.url, {
|
|
483
|
+
method,
|
|
484
|
+
headers: this.headers(extra, to.auth),
|
|
485
|
+
...(init.body === undefined ? {} : { body: init.body }),
|
|
486
|
+
...(s === undefined ? {} : { signal: s }),
|
|
487
|
+
})
|
|
488
|
+
},
|
|
489
|
+
this.config.retries,
|
|
490
|
+
this.wait,
|
|
491
|
+
),
|
|
492
|
+
)
|
|
497
493
|
if (method === 'PUT' && res.status === 403) {
|
|
498
494
|
const first = !this.writesRefused
|
|
499
495
|
this.writesRefused = true
|
|
@@ -67,6 +67,36 @@ function viteConfigFile(dir: string): string | null {
|
|
|
67
67
|
return name === undefined ? null : path.join(dir, name)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Bun's resolver reads `\` as a separator, even in a file: URL, so a config
|
|
71
|
+
// under `a\b` was looked for under `a/b`. Resolved here, the path stays as
|
|
72
|
+
// written; the file is read from it.
|
|
73
|
+
const LITERAL_QUERY = /\?vx-literal$/
|
|
74
|
+
let servingLiteral = false
|
|
75
|
+
|
|
76
|
+
function importAsWritten(file: string): Promise<{ default?: unknown }> {
|
|
77
|
+
if (!file.includes('\\')) return import(file)
|
|
78
|
+
if (!servingLiteral) {
|
|
79
|
+
servingLiteral = true
|
|
80
|
+
Bun.plugin({
|
|
81
|
+
name: 'vx-migrate-literal',
|
|
82
|
+
setup(build) {
|
|
83
|
+
build.onResolve({ filter: LITERAL_QUERY }, (args) => ({
|
|
84
|
+
path: args.path,
|
|
85
|
+
namespace: 'file',
|
|
86
|
+
}))
|
|
87
|
+
build.onLoad({ filter: LITERAL_QUERY }, async (args) => {
|
|
88
|
+
const at = args.path.replace(LITERAL_QUERY, '')
|
|
89
|
+
return {
|
|
90
|
+
contents: await Bun.file(at).text(),
|
|
91
|
+
loader: /\.[cm]?ts$/.test(at) ? 'ts' : 'js',
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
return import(`${file}?vx-literal`)
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
/**
|
|
71
101
|
* A package's `run` block, as `vp run` reads it: the config evaluated in
|
|
72
102
|
* build mode (vite-plus resolves it with `command: 'build'`), a function
|
|
@@ -75,7 +105,7 @@ function viteConfigFile(dir: string): string | null {
|
|
|
75
105
|
async function readRunConfig(file: string, root: string): Promise<RunConfig | null> {
|
|
76
106
|
let config: unknown
|
|
77
107
|
try {
|
|
78
|
-
config = (
|
|
108
|
+
config = (await importAsWritten(file)).default
|
|
79
109
|
if (typeof config === 'function')
|
|
80
110
|
config = await (config as (env: object) => unknown)({
|
|
81
111
|
command: 'build',
|
|
@@ -89,7 +119,7 @@ async function readRunConfig(file: string, root: string): Promise<RunConfig | nu
|
|
|
89
119
|
)
|
|
90
120
|
}
|
|
91
121
|
const run = (config as { run?: unknown } | null | undefined)?.run
|
|
92
|
-
return typeof run === 'object' && run !== null ?
|
|
122
|
+
return typeof run === 'object' && run !== null ? run : null
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
/** `run.cache` at the root: what `vp run` caches with no flag. */
|
|
@@ -292,7 +322,7 @@ export async function mapViteTaskWorkspace(
|
|
|
292
322
|
}
|
|
293
323
|
for (const [name, raw] of Object.entries(own)) {
|
|
294
324
|
const def: TaskDef =
|
|
295
|
-
typeof raw === 'string' || Array.isArray(raw) ? { command: raw } : (
|
|
325
|
+
typeof raw === 'string' || Array.isArray(raw) ? { command: raw } : (raw ?? {})
|
|
296
326
|
tasks.push(
|
|
297
327
|
mapTask(name, def, {
|
|
298
328
|
pkgDir,
|
|
@@ -408,7 +438,7 @@ function mapTask(name: string, def: TaskDef, ctx: TaskCtx): GeneratedTask {
|
|
|
408
438
|
def.cache === false || !ctx.cacheOn
|
|
409
439
|
? null
|
|
410
440
|
: typeof def.cache === 'object' && def.cache !== null
|
|
411
|
-
?
|
|
441
|
+
? def.cache
|
|
412
442
|
: def
|
|
413
443
|
|
|
414
444
|
const env = envNames('cache.env', cache?.env, todos, ctx)
|