@vzn/vx-migrate 0.0.632 → 0.0.633
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 +2 -2
- package/package.json +2 -2
- package/src/adopt.ts +48 -10
- package/src/migrate.ts +46 -6
- package/src/nx/nx-upstream.ts +4 -3
- package/src/turbo/turbo-map.ts +45 -20
package/README.md
CHANGED
|
@@ -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 is a group too in a package nothing but the root depends on, since no dependant's `^` edge runs its edges there (vercel/ai's `turbo run type-check` builds each example through its script-less `type-check: [build]`, and kitchen-sink's `turbo run test` builds `@repo/ui` for `admin`, which has no `test` script); elsewhere it is none, its edges run by its dependants'; a name Turbo does not define has no edge; `$TURBO_ROOT$` deps are a todo. A task turbo.json defines and no package has a script for (documenso's `lint`, shadcn-ui's `check`) is a group task in each package that defines it, its edges kept (a `pkg#task` edge only in `pkg`), so `vx run lint` runs Turbo's no-op nodes and exits 0 as `turbo run lint` does. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed; 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:
|
|
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: 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).
|
|
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 —
|
|
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 — a keyed group (no command, nothing spawns, not counted in the run summary) keyed on its own `name` input, depending on its Nx dependencies' twins (a project nothing reaches gets none) — and a task that reads `^name` depends on its direct dependencies' twins, so the whole closure folds into its key with each project hashed once. A dependency FILESET (`^{projectRoot}/tsconfig.lib.json`, `{ fileset, dependencies: true }`, which Nx's own inferred `typecheck` writes) folds the same way: its twin, `nx-input:fileset-<hash>` (a glob's `*` is no task name; the task's description names the fileset), keys on that fileset in each project. A twin lists its project's `package.json` and `project.json` first (whichever exists; Nx keys every task on its project's manifest and configuration): a fileset that matches nothing (an absent `tsconfig.spec.json`, a dependency's unbuilt `*.d.ts`) is nothing to Nx and no "matched no files" warning, and the file appearing re-keys its readers. The twins spawn nothing and stay out of a run's output and task count, so `vx run build` counts the tasks `nx run-many -t build` does. A path input inside an output one of the project's own targets declares (TanStack/table's `public` input lists `{projectRoot}/dist`) is generated, and gitignored where Nx caches it: Nx's file map skips gitignored files, so Nx hashes nothing there (a changed `dist` file is a cache hit under Nx 23.3), and it is dropped without a todo, and the task that writes it keys its dependants through `dependsOn`.
|
|
92
92
|
|
|
93
93
|
| Option | Meaning |
|
|
94
94
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.633",
|
|
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.633"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/adopt.ts
CHANGED
|
@@ -158,14 +158,16 @@ export function install(
|
|
|
158
158
|
packages: readonly string[],
|
|
159
159
|
version: string | null = null,
|
|
160
160
|
): Promise<string> {
|
|
161
|
+
const pm = packageManagerOf(root, process.env['npm_config_user_agent'])
|
|
161
162
|
return runManager(
|
|
162
163
|
root,
|
|
163
|
-
installArgv(
|
|
164
|
-
root,
|
|
165
|
-
packageManagerOf(root, process.env['npm_config_user_agent']),
|
|
166
|
-
version === null ? packages : packages.map((p) => `${p}@${version}`),
|
|
167
|
-
),
|
|
164
|
+
installArgv(root, pm, version === null ? packages : packages.map((p) => `${p}@${version}`)),
|
|
168
165
|
'installing vx',
|
|
166
|
+
// pnpm 12 refuses a version younger than minimumReleaseAge, and vx
|
|
167
|
+
// releases daily; its words scroll by above, so the way out is named.
|
|
168
|
+
pm === 'pnpm'
|
|
169
|
+
? `; if pnpm refused a version published within its minimumReleaseAge, list ${packages.join(', ')} under minimumReleaseAgeExclude in pnpm-workspace.yaml`
|
|
170
|
+
: '',
|
|
169
171
|
)
|
|
170
172
|
}
|
|
171
173
|
|
|
@@ -178,17 +180,53 @@ export function uninstall(root: string, packages: readonly string[]): Promise<st
|
|
|
178
180
|
)
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
/**
|
|
184
|
+
* The command that runs `argv`'s manager when its name on PATH cannot be
|
|
185
|
+
* spawned: the one that launched us (`npm_execpath`), if it is that
|
|
186
|
+
* manager. Under `pnpm exec`, PATH can hold a pnpm placeholder with no
|
|
187
|
+
* shebang that exec refuses (ENOEXEC), while `npm_execpath` is the pnpm
|
|
188
|
+
* running now. PATH stays first: it is what the user's shell would run.
|
|
189
|
+
*/
|
|
190
|
+
export function managerArgv(
|
|
191
|
+
argv: readonly string[],
|
|
192
|
+
env: Record<string, string | undefined>,
|
|
193
|
+
): string[] {
|
|
194
|
+
const exec = env['npm_execpath']
|
|
195
|
+
const manager = argv[0]!
|
|
196
|
+
if (exec === undefined || exec === '' || !path.basename(exec).startsWith(manager))
|
|
197
|
+
return [...argv]
|
|
198
|
+
// A JS entry (pnpm.cjs, npm-cli.js) runs under the Node that runs it.
|
|
199
|
+
return /\.[cm]?js$/.test(exec) ? ['node', exec, ...argv.slice(1)] : [exec, ...argv.slice(1)]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runManager(
|
|
203
|
+
root: string,
|
|
204
|
+
argv: string[],
|
|
205
|
+
what: string,
|
|
206
|
+
ranHint = '',
|
|
207
|
+
): Promise<string> {
|
|
182
208
|
const line = argv.join(' ')
|
|
183
209
|
process.stdout.write(`vx-migrate: ${line}\n`)
|
|
210
|
+
const spawn = (cmd: string[]) =>
|
|
211
|
+
Bun.spawn(cmd, { cwd: root, stdio: ['inherit', 'inherit', 'inherit'] }).exited
|
|
184
212
|
let code: number
|
|
213
|
+
let why = ''
|
|
185
214
|
try {
|
|
186
|
-
code = await
|
|
187
|
-
} catch {
|
|
188
|
-
|
|
215
|
+
code = await spawn(argv)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
const launcher = managerArgv(argv, process.env)
|
|
218
|
+
try {
|
|
219
|
+
if (launcher[0] === argv[0]) throw e
|
|
220
|
+
code = await spawn(launcher)
|
|
221
|
+
} catch (e2) {
|
|
222
|
+
code = -1
|
|
223
|
+
why = `: ${(e2 as Error).message}`
|
|
224
|
+
}
|
|
189
225
|
}
|
|
190
226
|
if (code !== 0) {
|
|
191
|
-
throw new UserError(
|
|
227
|
+
throw new UserError(
|
|
228
|
+
`${what} failed (${line} exited ${code}${why}); run it, then vx-migrate again${code > 0 ? ranHint : ''}`,
|
|
229
|
+
)
|
|
192
230
|
}
|
|
193
231
|
return line
|
|
194
232
|
}
|
package/src/migrate.ts
CHANGED
|
@@ -64,11 +64,13 @@ export interface MigrateArgs {
|
|
|
64
64
|
noInstall?: boolean
|
|
65
65
|
/** `--help` / `-h`: the usage on stdout, exit 0 (as `nx-env --help`). */
|
|
66
66
|
help?: boolean
|
|
67
|
+
/** `--format json` (with `--dry`): the plan as one JSON document (core's `schemas/init.json`). */
|
|
68
|
+
json?: boolean
|
|
67
69
|
error?: string
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const USAGE =
|
|
71
|
-
'usage: vx-migrate [--from turbo|nx|vite-task] [--native|--keep] [--no-install] [--dry] [--force] [--mjs]'
|
|
73
|
+
'usage: vx-migrate [--from turbo|nx|vite-task] [--native|--keep] [--no-install] [--dry [--format json]] [--force] [--mjs]'
|
|
72
74
|
|
|
73
75
|
export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
74
76
|
const out: MigrateArgs = { dry: false, force: false, mjs: false }
|
|
@@ -78,7 +80,13 @@ export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
|
78
80
|
else if (a === '--force') out.force = true
|
|
79
81
|
else if (a === '--mjs') out.mjs = true
|
|
80
82
|
else if (a === '--no-install') out.noInstall = true
|
|
81
|
-
else if (a === '--
|
|
83
|
+
else if (a === '--format' || a?.startsWith('--format=')) {
|
|
84
|
+
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
85
|
+
if (v !== 'json' && v !== 'pretty') {
|
|
86
|
+
return { ...out, error: `--format must be pretty or json (got ${v ?? 'nothing'})` }
|
|
87
|
+
}
|
|
88
|
+
out.json = v === 'json'
|
|
89
|
+
} else if (a === '--native' || a === '--keep') {
|
|
82
90
|
const mode = a === '--native' ? 'native' : 'keep'
|
|
83
91
|
if (out.mode !== undefined && out.mode !== mode) {
|
|
84
92
|
return { ...out, error: '--native and --keep are two answers to one question; pass one' }
|
|
@@ -97,6 +105,8 @@ export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
|
97
105
|
else if (a?.startsWith('-')) return { ...out, error: `unknown flag: ${a}\n${USAGE}` }
|
|
98
106
|
else return { ...out, error: `unexpected argument: ${a}\n${USAGE}` }
|
|
99
107
|
}
|
|
108
|
+
if (out.json && !out.dry)
|
|
109
|
+
return { ...out, error: '--format json prints the plan, so it needs --dry' }
|
|
100
110
|
return out
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -314,6 +324,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
314
324
|
verb: 'vx-migrate',
|
|
315
325
|
notes: headerNotes,
|
|
316
326
|
dry: parsed.dry,
|
|
327
|
+
json: parsed.json === true,
|
|
317
328
|
force: parsed.force,
|
|
318
329
|
format,
|
|
319
330
|
})
|
|
@@ -424,7 +435,7 @@ async function keep(
|
|
|
424
435
|
['@vzn/vx', '@vzn/vx-migrate', ...plugins.map((p) => p.pkg)],
|
|
425
436
|
args,
|
|
426
437
|
)) {
|
|
427
|
-
process.stdout.write(`vx-migrate: ${note}\n`)
|
|
438
|
+
;(args.json ? process.stderr : process.stdout).write(`vx-migrate: ${note}\n`)
|
|
428
439
|
}
|
|
429
440
|
// `vx init` adopts nx() by nx.json, and a Lerna repo may have none: it
|
|
430
441
|
// would map the scripts instead, so the file is written here.
|
|
@@ -435,9 +446,12 @@ async function keep(
|
|
|
435
446
|
const core = existsSync(path.join(installed, 'package.json'))
|
|
436
447
|
? path.join(realpathSync(installed), 'src', 'index.ts')
|
|
437
448
|
: Bun.resolveSync('@vzn/vx', import.meta.dir)
|
|
438
|
-
const flags = [
|
|
439
|
-
|
|
440
|
-
|
|
449
|
+
const flags = [
|
|
450
|
+
args.dry && '--dry',
|
|
451
|
+
args.json && '--format=json',
|
|
452
|
+
args.force && '--force',
|
|
453
|
+
args.mjs && '--mjs',
|
|
454
|
+
].filter((f): f is string => typeof f === 'string')
|
|
441
455
|
const code = await Bun.spawn(
|
|
442
456
|
[
|
|
443
457
|
process.execPath,
|
|
@@ -462,6 +476,18 @@ async function keep(
|
|
|
462
476
|
return code
|
|
463
477
|
}
|
|
464
478
|
|
|
479
|
+
/** `--keep --dry --format json` in a Lerna repo: core's `schemas/init.json` document. */
|
|
480
|
+
function lernaPlan(
|
|
481
|
+
files: { path: string; contents: string }[],
|
|
482
|
+
kept: string[],
|
|
483
|
+
replaced: string[],
|
|
484
|
+
): number {
|
|
485
|
+
process.stdout.write(
|
|
486
|
+
`${JSON.stringify({ dry: true, source: 'lerna.json', files, kept, replaced, todos: [], notes: [], next: null })}\n`,
|
|
487
|
+
)
|
|
488
|
+
return 0
|
|
489
|
+
}
|
|
490
|
+
|
|
465
491
|
/** `--keep` in a Lerna repo with no nx.json: the workspace file declaring `nx()`. */
|
|
466
492
|
async function keepLerna(
|
|
467
493
|
root: string,
|
|
@@ -482,6 +508,13 @@ async function keepLerna(
|
|
|
482
508
|
}
|
|
483
509
|
if (!args.dry && extended !== text) await Bun.write(path.join(root, existing), extended)
|
|
484
510
|
const names = undeclared(text, all).map((p) => `${p.factory}()`)
|
|
511
|
+
if (args.json) {
|
|
512
|
+
return lernaPlan(
|
|
513
|
+
extended === text ? [] : [{ path: existing, contents: extended }],
|
|
514
|
+
extended === text ? [existing] : [],
|
|
515
|
+
[],
|
|
516
|
+
)
|
|
517
|
+
}
|
|
485
518
|
if (names.length > 0)
|
|
486
519
|
process.stdout.write(
|
|
487
520
|
`vx-migrate: ${args.dry ? 'would declare' : 'declared'} ${names.join(', ')} in ${existing}\n`,
|
|
@@ -489,6 +522,13 @@ async function keepLerna(
|
|
|
489
522
|
return 0
|
|
490
523
|
}
|
|
491
524
|
const text = renderWorkspaceFile(all, format)
|
|
525
|
+
if (args.json) {
|
|
526
|
+
return lernaPlan(
|
|
527
|
+
[{ path: name, contents: text }],
|
|
528
|
+
[],
|
|
529
|
+
existing !== undefined && existing !== name ? [existing] : [],
|
|
530
|
+
)
|
|
531
|
+
}
|
|
492
532
|
if (args.dry) {
|
|
493
533
|
process.stdout.write(`── ${name} ──\n${text}\nvx-migrate: would write ${name} (dry run)\n`)
|
|
494
534
|
return 0
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// dependency's `name` input for every project the graph reaches, whether or
|
|
3
3
|
// not a task edge exists, so a `test` with `^production` and no `dependsOn`
|
|
4
4
|
// re-keys on a dependency's source. vx folds upstream KEYS along
|
|
5
|
-
// `dependsOn`, so each project gets one `nx-input:<name>` task —
|
|
6
|
-
//
|
|
5
|
+
// `dependsOn`, so each project gets one `nx-input:<name>` task — a keyed
|
|
6
|
+
// group, keyed on the project's own `name` input — with an edge to its
|
|
7
7
|
// dependencies' twin. A task that reads `^name` depends on its direct
|
|
8
8
|
// dependencies' twins and folds the whole closure through them: each
|
|
9
9
|
// project's files are hashed once, not once per dependant (listing the
|
|
@@ -350,7 +350,8 @@ export function planNxUpstream(
|
|
|
350
350
|
if (inputs.wsFiles.length > 0) cacheInputs.workspaceFiles = inputs.wsFiles
|
|
351
351
|
if (inputs.envNames.length > 0) cacheInputs.env = inputs.envNames
|
|
352
352
|
if (inputs.runtimeCmds.length > 0) cacheInputs.workspaceRuntime = inputs.runtimeCmds
|
|
353
|
-
|
|
353
|
+
// A keyed group: dependants fold its key; nothing spawns.
|
|
354
|
+
const task: Record<string, unknown> = {}
|
|
354
355
|
if (isFileset(name))
|
|
355
356
|
task.description = `Nx fileset ${name} of this project and what it reaches`
|
|
356
357
|
if (edges.size > 0) task.dependsOn = [...edges].sort()
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -1140,8 +1140,8 @@ export async function mapTurboWorkspace(
|
|
|
1140
1140
|
// `test: { dependsOn: ["transit"] }` (or `["^transit"]`). Turbo hashes the no-op per package,
|
|
1141
1141
|
// over its files, so `test` keys on its dependencies' sources. Dropped,
|
|
1142
1142
|
// vx keyed `test` on its own files alone: a dependency's edit was a hit.
|
|
1143
|
-
// Each package
|
|
1144
|
-
// `nx-input:*` twins
|
|
1143
|
+
// Each package has it as a key-only task, a keyed group, as nx()'s
|
|
1144
|
+
// `nx-input:*` twins are.
|
|
1145
1145
|
const withScript = new Set<string>()
|
|
1146
1146
|
for (const set of runnable.values()) for (const name of set) withScript.add(name)
|
|
1147
1147
|
const sameRefs = new Set<string>()
|
|
@@ -1307,7 +1307,7 @@ export async function mapTurboWorkspace(
|
|
|
1307
1307
|
const noop = keyOnly.get(meta.name)?.has(name) === true
|
|
1308
1308
|
// Core gives a project with no `build` this very node (a group behind
|
|
1309
1309
|
// `^build`, keyed on the project's files), so writing it is noise:
|
|
1310
|
-
// solid's three script-less packages each got a `build
|
|
1310
|
+
// solid's three script-less packages each got a key-only `build`.
|
|
1311
1311
|
const noopEdges = defFor(name)!.dependsOn ?? []
|
|
1312
1312
|
if (
|
|
1313
1313
|
noop &&
|
|
@@ -1322,23 +1322,25 @@ export async function mapTurboWorkspace(
|
|
|
1322
1322
|
((transit.has(name) && own.has(name)) || noop)
|
|
1323
1323
|
) {
|
|
1324
1324
|
tasks.push(
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1325
|
+
keyOnlyGroup(
|
|
1326
|
+
buildTask(
|
|
1327
|
+
literals,
|
|
1328
|
+
name,
|
|
1329
|
+
noop ? { ...defFor(name)!, outputs: [] } : defFor(name)!,
|
|
1330
|
+
'true',
|
|
1331
|
+
own,
|
|
1332
|
+
defFor,
|
|
1333
|
+
emitted,
|
|
1334
|
+
emittedAnywhere,
|
|
1335
|
+
globals,
|
|
1336
|
+
opts,
|
|
1337
|
+
relPosix(root, meta.dir),
|
|
1338
|
+
rootDotenv,
|
|
1339
|
+
rootMeta?.name,
|
|
1340
|
+
{ name: meta.name, persistentAt, withOf },
|
|
1341
|
+
inferredOf.get(meta.name) ?? [],
|
|
1342
|
+
spelledOf.get(meta.name),
|
|
1343
|
+
),
|
|
1342
1344
|
),
|
|
1343
1345
|
)
|
|
1344
1346
|
continue
|
|
@@ -1723,6 +1725,29 @@ function entryEdges(deps: readonly string[], pkg: string, rootName: string | und
|
|
|
1723
1725
|
})
|
|
1724
1726
|
}
|
|
1725
1727
|
|
|
1728
|
+
/**
|
|
1729
|
+
* A no-op `true` task as a keyed group: dependants fold the same key and
|
|
1730
|
+
* nothing spawns. Kept a task when it writes outputs, stays up or takes a
|
|
1731
|
+
* terminal, and when it has neither a key nor an edge (a group needs one).
|
|
1732
|
+
*/
|
|
1733
|
+
function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
|
|
1734
|
+
const task = t.task
|
|
1735
|
+
if (task === null) return t
|
|
1736
|
+
const exec = task['exec'] as Record<string, unknown>
|
|
1737
|
+
if (exec['persistent'] !== undefined || exec['interactive'] !== undefined) return t
|
|
1738
|
+
const cache = task['cache'] as
|
|
1739
|
+
| { outputs: { files: unknown[]; workspaceFiles?: unknown[] } }
|
|
1740
|
+
| undefined
|
|
1741
|
+
if (cache === undefined && task['dependsOn'] === undefined) return t
|
|
1742
|
+
if (
|
|
1743
|
+
cache !== undefined &&
|
|
1744
|
+
(cache.outputs.files.length > 0 || cache.outputs.workspaceFiles !== undefined)
|
|
1745
|
+
)
|
|
1746
|
+
return t
|
|
1747
|
+
const { exec: _exec, ...group } = task
|
|
1748
|
+
return { ...t, task: group }
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1726
1751
|
function buildTask(
|
|
1727
1752
|
literals: { readonly tasks: Literals[]; readonly globalIgnored: readonly string[] },
|
|
1728
1753
|
name: string,
|