@vzn/vx-migrate 0.0.640 → 0.0.641

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -231,7 +231,7 @@ It is the one command a repo needs (`pnpx @vzn/vx-migrate` in a pnpm repo works
231
231
  - In a Turbo or Nx repo, in a terminal, it asks which adoption you want: **native** (the default, a `vx.config.ts` per package) or **keep** (a workspace file declaring `turbo()` or `nx()`). `--native` / `--keep` answer it; with no terminal (CI, a pipe) it is native. Elsewhere both are refused. A repo already set up (a workspace file declaring neither, and project configs) is said to be, exit 0, unless `--force`.
232
232
  - Native or keep, a remote cache the repo shows is declared: `turboCache()` for turbo.json's enabled `remoteCache`, a `turbo link`ed `.turbo/config.json` or a CI file (`.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/config.yml`) setting `TURBO_TOKEN`; `nxCache()` for one setting `NX_SELF_HOSTED_REMOTE_CACHE_SERVER`. The report names the file that showed it. Nx Cloud (`nxCloudId`, `nxCloudAccessToken`, the `nx-cloud` runner) is named instead: vx does not speak its wire.
233
233
  - With no `.vx/workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the remote cache above, the `@vzn/vx-lockfile` factory for the lockfile (`pnpm()`, `bun()`, `npm()`, `yarn()`), `scheduleHistoryPlugin()`, and `github()` when `.github/workflows` exists, from `@vzn/vx-ci`, each installed beside `@vzn/vx` at vx-migrate's own version. Keep adds them to the file it writes, or to one already in that shape; any other workspace file is the user's and left alone. Native over a file vx wrote (a keep adoption's) rewrites it: `turbo()` and `nx()` go, since the configs written replace them, and the other plugins stay.
234
- - It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, the declared plugins' packages, and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w`, `yarn add -D -W` on Yarn 1 and without `-W` on Yarn 2+, `bun add -d`, `npm install -D`). A package the root both lists and has installed at vx-migrate's own version is left alone; any other is installed at that version, and so is every other `@vzn/vx-*` plugin the root lists from the registry (one install moves them together, as `vx upgrade` does). Native removes a listed `@vzn/vx-migrate` when nothing written needs it (no `nx-exec` / `nx-env` task, no `turboCache()` or `nxCache()` left in the workspace file): `pnpm remove -w`, `yarn remove` (`-W` on Yarn 1), `bun remove`, `npm uninstall`. `--no-install` skips both. vx-migrate runs only with the `@vzn/vx` of its own release, which it installs: one of another release (a `bun x` install directory keeps the one it had) is refused, naming where it was loaded.
234
+ - It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, every `@vzn/vx-*` package the workspace file imports (one an earlier init wrote included), and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w`, `yarn add -D -W` on Yarn 1 and without `-W` on Yarn 2+, `bun add -d`, `npm install -D`). A package the root both lists and has installed at vx-migrate's own version is left alone; any other is installed at that version, and so is every other `@vzn/vx-*` plugin the root lists from the registry (one install moves them together, as `vx upgrade` does). Native removes a listed `@vzn/vx-migrate` when nothing written needs it (no `nx-exec` / `nx-env` task, no `turboCache()` or `nxCache()` left in the workspace file): `pnpm remove -w`, `yarn remove` (`-W` on Yarn 1), `bun remove`, `npm uninstall`. `--no-install` skips both. vx-migrate runs only with the `@vzn/vx` of its own release, which it installs: one of another release (a `bun x` install directory keeps the one it had) is refused, naming where it was loaded.
235
235
  - Native, once the files are written, it ends with `vx lock`: `.vx/projects.lock.json` is written and the graph rules are checked, its line the report's `lock` row. A refusal prints above the report and the configs stay written (exit 0). `--no-install` skips it, since the plugins the workspace file declares are not installed yet, and `next` then starts with `vx lock`.
236
236
 
237
237
  A Lerna repo is an Nx one: Lerna 6+ runs `lerna run` on Nx's task runner over the graph `nx graph` exports, nx.json or not, so a `lerna.json` with no nx.json and no turbo.json is mapped from that graph, and keep writes the workspace file declaring `nx()`. Lerna is the installed one, else the root manifest's range; with neither, the lerna.json is another tool's (lerna-lite reads it too, and runs no Nx). A root whose scripts never `lerna run` runs its tasks another way and publishes with Lerna (webdriverio: `run-s`, `pnpm -r`), so its scripts stay the source. Not where Lerna runs its own runner: `useNx: false`, or Lerna 5 without `useNx: true`; those map their scripts. Beside turbo.json, Turbo runs the tasks and Lerna only publishes: turbo.json is the source.
@@ -600,7 +600,7 @@ The Nx spec has no existence probe, so `has` (the `--dry` prediction; the prefet
600
600
  - Three outages in a row (no connection, no answer within the deadline, or a `5xx` once the resends are spent) open Turbo's outage breaker: no request is sent for 30 s, each lookup is a miss at once, then one request probes and its answer closes or reopens it. A hit is judged when its body ends, so a server that sends headers and then stalls the body counts the same way. A hung server costs three deadlines, not one per task.
601
601
  - A refused token (`401`/`403`) warns **once** and turns the layer off for the rest of the process (a `403` on an upload is a read-only token — Nx's spec, or turborepo-remote-cache's `READ_ONLY` and write-less JWTs: it turns off uploads alone, and reads go on) — including the requests already in flight when the refusal lands, which degrade in silence rather than repeating it (a six-project run printed five identical lines before 2026-09-20).
602
602
  - Policy (`--cache-policy=remote:r`, `VX_CACHE_POLICY`, `cache.policy`) is enforced by core, which layers each plugin's remote over the local cache — a read-only token pairs naturally with `remote:r`.
603
- - Each tool's own switches narrow that policy, never widen it, as they do for the tool: `TURBO_CACHE` (`--cache-policy`'s syntax, an omitted source off) and `TURBO_REMOTE_CACHE_READ_ONLY` for `turboCache()`, `NX_SKIP_REMOTE_CACHE` / `NX_DISABLE_REMOTE_CACHE` (`true`) for `nxCache()`. A CI that keeps untrusted pull requests off the shared cache that way keeps vx off it too.
603
+ - Core's policy alone decides reads and writes: the plugins read none of Turbo's or Nx's own switches (`TURBO_CACHE`, `TURBO_REMOTE_CACHE_READ_ONLY`, `NX_SKIP_REMOTE_CACHE`, `NX_DISABLE_REMOTE_CACHE`). A CI that keeps untrusted pull requests off the shared cache sets `cache.policy` or `--cache-policy=local:rw,remote:r` for them.
604
604
 
605
605
  ## Testing
606
606
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.640",
3
+ "version": "0.0.641",
4
4
  "license": "MIT",
5
5
  "description": "Adopt @vzn/vx from Turborepo, Nx or package.json scripts: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "peerDependencies": {
43
- "@vzn/vx": "^0.0.640"
43
+ "@vzn/vx": "^0.0.641"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
package/src/adopt.ts CHANGED
@@ -175,6 +175,21 @@ export function listedPlugins(root: string): string[] {
175
175
  return [...plugins].sort()
176
176
  }
177
177
 
178
+ /**
179
+ * The `@vzn/vx*` packages a workspace file imports, parsed, not matched (a
180
+ * formatter's quotes hid a match). Each is installed with core: a file an
181
+ * earlier init wrote imports @vzn/vx-lockfile that package.json need not
182
+ * list, and the lock could not find it (M, 2026-10-16).
183
+ */
184
+ export function importedPackages(text: string): string[] {
185
+ const out = new Set<string>()
186
+ for (const i of new Bun.Transpiler({ loader: 'ts' }).scanImports(text)) {
187
+ const m = /^(@vzn\/vx(?:-[^/]+)?)(?:\/|$)/.exec(i.path)
188
+ if (m !== null) out.add(m[1]!)
189
+ }
190
+ return [...out].sort()
191
+ }
192
+
178
193
  /** Install `packages` (at `version`, given one) with the repo's manager; its output only on failure. */
179
194
  export function install(
180
195
  root: string,
package/src/migrate.ts CHANGED
@@ -45,6 +45,7 @@ import { exportGraph, nxBin } from './nx/export-graph.js'
45
45
  import { readNxJson } from './nx/nx-map.js'
46
46
  import {
47
47
  type AdoptionMode,
48
+ importedPackages,
48
49
  install,
49
50
  installArgv,
50
51
  listedPackages,
@@ -358,11 +359,8 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
358
359
  const finalWorkspace = workspaceText ?? readFileSync(path.join(root, existingWorkspace!), 'utf8')
359
360
  // Imports parsed, not matched: a formatter's double quotes hid it, and
360
361
  // the file still imported the package removed (owner, 2026-10-16).
361
- const needsSelf =
362
- runsBins ||
363
- new Bun.Transpiler({ loader: 'ts' })
364
- .scanImports(finalWorkspace)
365
- .some((i) => i.path === '@vzn/vx-migrate')
362
+ const imported = importedPackages(finalWorkspace)
363
+ const needsSelf = runsBins || imported.includes('@vzn/vx-migrate')
366
364
  const cacheNote =
367
365
  runner === 'vite-task'
368
366
  ? undefined
@@ -381,6 +379,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
381
379
  '@vzn/vx',
382
380
  ...(needsSelf ? ['@vzn/vx-migrate'] : []),
383
381
  ...plugins.map((p) => p.pkg),
382
+ ...imported,
384
383
  ]),
385
384
  ],
386
385
  parsed,
@@ -544,7 +543,14 @@ async function keep(
544
543
  const declares = before === undefined || extended !== null
545
544
  const steps = await prepareRepo(
546
545
  root,
547
- [...new Set(['@vzn/vx', '@vzn/vx-migrate', ...(declares ? all.map((p) => p.pkg) : [])])],
546
+ [
547
+ ...new Set([
548
+ '@vzn/vx',
549
+ '@vzn/vx-migrate',
550
+ ...(declares ? all.map((p) => p.pkg) : []),
551
+ ...importedPackages(text),
552
+ ]),
553
+ ],
548
554
  args,
549
555
  )
550
556
  if (args.json)
@@ -633,8 +639,8 @@ function lockRow(lock: { code: number; said: string }): ui.FieldRow {
633
639
  ]
634
640
  }
635
641
 
636
- /** `.vx/.gitignore`'s text, as core writes it: the machine-local cache stays out of git. */
637
- const DOT_VX_GITIGNORE = '/cache\n'
642
+ /** `.vx/.gitignore`'s text, as core writes it: the machine-local cache and restore staging stay out of git. */
643
+ const DOT_VX_GITIGNORE = '/cache\n/tmp\n'
638
644
 
639
645
  // npx and bunx name the package: their bare `vx` is a stranger's npm package.
640
646
  const EXEC: Record<PackageManager, string> = {
@@ -46,10 +46,6 @@ export function resolveNxCacheConfig(
46
46
  options: NxCacheOptions,
47
47
  env: Record<string, string | undefined> = Bun.env,
48
48
  ): NxCacheConfig | undefined {
49
- // Nx's own off switches (`skipRemoteCache`): a CI that sets one for an
50
- // untrusted pull request kept Nx off the shared cache and not vx.
51
- if (env['NX_SKIP_REMOTE_CACHE'] === 'true' || env['NX_DISABLE_REMOTE_CACHE'] === 'true')
52
- return undefined
53
49
  const server = (options.server ?? env['NX_SELF_HOSTED_REMOTE_CACHE_SERVER'])?.replace(/\/+$/, '')
54
50
  if (!server) return undefined
55
51
  // The URL is printed in every refusal line, so a `user:pass@` in it would
@@ -393,8 +393,6 @@ export class TurboRemoteCache implements RemoteCacheLayer {
393
393
  private readonly absent = new Set<string>()
394
394
  private readonly key: Uint8Array | undefined
395
395
  readonly endpoint: string
396
- /** What `TURBO_REMOTE_CACHE_READ_ONLY` and friends leave this run (`turboRemoteAccess`). */
397
- access?: { read: boolean; write: boolean }
398
396
  constructor(
399
397
  private readonly config: TurboCacheConfig,
400
398
  private readonly fetchImpl: typeof fetch = fetch,
@@ -653,37 +651,6 @@ export class TurboRemoteCache implements RemoteCacheLayer {
653
651
  }
654
652
  }
655
653
 
656
- /**
657
- * What Turbo's own switches leave of the remote cache: `TURBO_CACHE`
658
- * (`--cache-policy`'s syntax, where an omitted source is off) and
659
- * `TURBO_REMOTE_CACHE_READ_ONLY`. A CI that sets either for an untrusted
660
- * pull request kept Turbo from writing the shared cache and not vx. They
661
- * only narrow what `--cache-policy` allows, never widen it.
662
- */
663
- export function turboRemoteAccess(env: Record<string, string | undefined>): {
664
- read: boolean
665
- write: boolean
666
- } {
667
- let read = true
668
- let write = true
669
- const spec = env['TURBO_CACHE']
670
- if (spec !== undefined && spec !== '') {
671
- read = write = false
672
- for (const part of spec.split(',')) {
673
- const m = /^(local|remote):([rw]*)$/.exec(part.trim())
674
- if (m === null)
675
- throw new Error(
676
- `vx/turbo-cache: TURBO_CACHE ${JSON.stringify(spec)} is not --cache-policy's syntax (e.g. local:rw,remote:r)`,
677
- )
678
- if (m[1] !== 'remote') continue
679
- read = m[2]!.includes('r')
680
- write = m[2]!.includes('w')
681
- }
682
- }
683
- if (/^(1|true)$/i.test(env['TURBO_REMOTE_CACHE_READ_ONLY'] ?? '')) write = false
684
- return { read, write }
685
- }
686
-
687
654
  /**
688
655
  * The repo's `.turbo/config.json`, by Turbo's field names and their
689
656
  * aliases (`apiurl`, `ApiUrl`, `APIURL`, …); `{}` when absent or unreadable.
@@ -757,14 +724,10 @@ export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
757
724
  localConfigOf(ctx.workspaceRoot),
758
725
  )
759
726
  if (config === undefined) return undefined
760
- const access = turboRemoteAccess(Bun.env)
761
- if (!access.read && !access.write) return undefined
762
727
  // A signed download waits in a temp until its tag checks: in vx's
763
728
  // cache directory, which the sandbox walls, not the shared temp dir a
764
729
  // sandboxed task may read (L-45).
765
- const remote = new TurboRemoteCache(config, fetch, ctx.cacheDir)
766
- remote.access = access
767
- return remote
730
+ return new TurboRemoteCache(config, fetch, ctx.cacheDir)
768
731
  },
769
732
  })
770
733
  }