@vzn/vx-migrate 0.0.635 → 0.0.637

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.635",
3
+ "version": "0.0.637",
4
4
  "license": "MIT",
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",
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": [
7
7
  "vx",
8
8
  "vx-plugin",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "peerDependencies": {
43
- "@vzn/vx": "^0.0.635"
43
+ "@vzn/vx": "^0.0.637"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
package/src/adopt.ts CHANGED
@@ -152,7 +152,7 @@ export function listedPackages(root: string, packages: readonly string[]): strin
152
152
  )
153
153
  }
154
154
 
155
- /** Install `packages` (at `version`, given one) with the repo's manager, its output on the terminal. */
155
+ /** Install `packages` (at `version`, given one) with the repo's manager; its output only on failure. */
156
156
  export function install(
157
157
  root: string,
158
158
  packages: readonly string[],
@@ -164,10 +164,11 @@ export function install(
164
164
  installArgv(root, pm, version === null ? packages : packages.map((p) => `${p}@${version}`)),
165
165
  'installing vx',
166
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.
167
+ // releases daily; its words print above, so the way out is named.
168
168
  pm === 'pnpm'
169
169
  ? `; if pnpm refused a version published within its minimumReleaseAge, list ${packages.join(', ')} under minimumReleaseAgeExclude in pnpm-workspace.yaml`
170
170
  : '',
171
+ ' (what the written files import)',
171
172
  )
172
173
  }
173
174
 
@@ -204,13 +205,26 @@ async function runManager(
204
205
  argv: string[],
205
206
  what: string,
206
207
  ranHint = '',
208
+ why = '',
207
209
  ): Promise<string> {
208
210
  const line = argv.join(' ')
209
- process.stdout.write(`vx-migrate: ${line}\n`)
210
- const spawn = (cmd: string[]) =>
211
- Bun.spawn(cmd, { cwd: root, stdio: ['inherit', 'inherit', 'inherit'] }).exited
211
+ process.stdout.write(`vx-migrate: ${line}${why}\n`)
212
+ // The manager's own output (progress, deprecations, audit, postinstalls)
213
+ // is shown only when it fails: mid-init it buried the report (Growth's
214
+ // first-run funnel F3, 2026-10-13).
215
+ let said = ''
216
+ const spawn = async (cmd: string[]) => {
217
+ const p = Bun.spawn(cmd, { cwd: root, stdin: 'inherit', stdout: 'pipe', stderr: 'pipe' })
218
+ const [out, err, code] = await Promise.all([
219
+ new Response(p.stdout).text(),
220
+ new Response(p.stderr).text(),
221
+ p.exited,
222
+ ])
223
+ said = out + err
224
+ return code
225
+ }
212
226
  let code: number
213
- let why = ''
227
+ let failed = ''
214
228
  try {
215
229
  code = await spawn(argv)
216
230
  } catch (e) {
@@ -220,12 +234,13 @@ async function runManager(
220
234
  code = await spawn(launcher)
221
235
  } catch (e2) {
222
236
  code = -1
223
- why = `: ${(e2 as Error).message}`
237
+ failed = `: ${(e2 as Error).message}`
224
238
  }
225
239
  }
226
240
  if (code !== 0) {
241
+ process.stderr.write(said)
227
242
  throw new UserError(
228
- `${what} failed (${line} exited ${code}${why}); run it, then vx-migrate again${code > 0 ? ranHint : ''}`,
243
+ `${what} failed (${line} exited ${code}${failed}); run it, then vx-migrate again${code > 0 ? ranHint : ''}`,
229
244
  )
230
245
  }
231
246
  return line
@@ -0,0 +1,65 @@
1
+ // The workspace edges the mappers reason about. Core reads no manifest
2
+ // edge: a run's `^task` follows a plugin's (`@vzn/vx-lockfile`'s, from the
3
+ // lockfile the manager wrote from these manifests). Turbo and Nx draw
4
+ // package edges from the same manifests, so the mappers read them here: a
5
+ // dependency, dev or optional entry whose key names a workspace package
6
+ // orders it; a peer on one only reaches it.
7
+
8
+ import type { PackageMeta } from './packages.js'
9
+
10
+ export interface ManifestGraph {
11
+ /** Packages this one builds after, sorted. */
12
+ directDeps(name: string): readonly string[]
13
+ /** Every package a change reaches this one through, sorted. */
14
+ transitiveDeps(name: string): readonly string[]
15
+ /** Every package a change here reaches, sorted. */
16
+ transitiveDependents(name: string): readonly string[]
17
+ }
18
+
19
+ const ORDER_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'] as const
20
+
21
+ export function manifestGraph(metas: readonly PackageMeta[]): ManifestGraph {
22
+ const names = new Set(metas.map((m) => m.name))
23
+ const order = new Map<string, string[]>()
24
+ const reach = new Map<string, string[]>()
25
+ const dependents = new Map<string, string[]>()
26
+ const keys = (field: unknown): string[] =>
27
+ field !== null && typeof field === 'object' ? Object.keys(field) : []
28
+ for (const m of metas) {
29
+ const pkg = m.packageJson as unknown as Record<string, unknown>
30
+ const own = new Set<string>()
31
+ for (const f of ORDER_FIELDS) for (const k of keys(pkg[f])) if (names.has(k)) own.add(k)
32
+ own.delete(m.name)
33
+ const all = new Set(own)
34
+ for (const k of keys(pkg['peerDependencies'])) if (names.has(k) && k !== m.name) all.add(k)
35
+ order.set(m.name, [...own].sort())
36
+ reach.set(m.name, [...all].sort())
37
+ for (const d of all) {
38
+ const list = dependents.get(d)
39
+ if (list === undefined) dependents.set(d, [m.name])
40
+ else list.push(m.name)
41
+ }
42
+ }
43
+ const closure = (edges: ReadonlyMap<string, readonly string[]>) => {
44
+ const memo = new Map<string, readonly string[]>()
45
+ return (name: string): readonly string[] => {
46
+ let out = memo.get(name)
47
+ if (out !== undefined) return out
48
+ const seen = new Set<string>()
49
+ const stack = [...(edges.get(name) ?? [])]
50
+ while (stack.length > 0) {
51
+ const n = stack.pop()!
52
+ if (seen.has(n)) continue
53
+ seen.add(n)
54
+ stack.push(...(edges.get(n) ?? []))
55
+ }
56
+ memo.set(name, (out = [...seen].sort()))
57
+ return out
58
+ }
59
+ }
60
+ return {
61
+ directDeps: (name) => order.get(name) ?? [],
62
+ transitiveDeps: closure(reach),
63
+ transitiveDependents: closure(dependents),
64
+ }
65
+ }
@@ -0,0 +1,52 @@
1
+ // Core reads no `package.json`: a project is a directory with a vx.config,
2
+ // named by its config, a `discover` plugin or its path. A Turbo or Nx repo
3
+ // writes no vx.config, so its projects are the package manager's members,
4
+ // which `discover` names here (`memberProjects`). The mappers read scripts,
5
+ // names and dependencies, so each project's manifest is read here once
6
+ // per run: the members' as `discover` lists them, the rest once per
7
+ // projects array (core hands every `project` call of a run the same one).
8
+ import path from 'node:path'
9
+ import { type DiscoveredProject, type ProjectMeta } from '@vzn/vx'
10
+ import { listPackages, type PackageMeta } from './packages.js'
11
+
12
+ /** The manifests the last `discover` read, by directory. */
13
+ let listed = new Map<string, PackageMeta>()
14
+ const read = new WeakMap<readonly ProjectMeta[], Promise<PackageMeta[]>>()
15
+
16
+ /**
17
+ * The package manager's members at `root` as `discover` entries, each
18
+ * named by its `package.json`; none where the root holds no
19
+ * `package.json` or `pnpm-workspace.yaml`.
20
+ */
21
+ export async function memberProjects(root: string): Promise<DiscoveredProject[]> {
22
+ const exists = await Promise.all(
23
+ ['package.json', 'pnpm-workspace.yaml'].map((f) => Bun.file(path.join(root, f)).exists()),
24
+ )
25
+ const members = exists.some(Boolean) ? await listPackages(root) : []
26
+ listed = new Map(members.map((m) => [path.resolve(m.dir), m]))
27
+ return members.map((m) => ({ dir: m.dir, name: m.name }))
28
+ }
29
+
30
+ /** `metas` with each one's `package.json` (`{ name }` where it has none or it does not parse). */
31
+ export function withManifests(metas: readonly ProjectMeta[]): Promise<PackageMeta[]> {
32
+ let out = read.get(metas)
33
+ if (out === undefined) {
34
+ out = Promise.all(metas.map(withManifest))
35
+ read.set(metas, out)
36
+ }
37
+ return out
38
+ }
39
+
40
+ async function withManifest(meta: ProjectMeta): Promise<PackageMeta> {
41
+ const known = listed.get(path.resolve(meta.dir))
42
+ const pkg: unknown =
43
+ known?.packageJson ??
44
+ (await Bun.file(path.join(meta.dir, 'package.json'))
45
+ .json()
46
+ .catch(() => null))
47
+ const packageJson =
48
+ pkg !== null && typeof pkg === 'object' && !Array.isArray(pkg)
49
+ ? (pkg as PackageMeta['packageJson'])
50
+ : { name: meta.name }
51
+ return { name: meta.name, dir: meta.dir, configPath: meta.configPath, packageJson }
52
+ }
@@ -10,13 +10,7 @@
10
10
 
11
11
  import { mkdir, rename } from 'node:fs/promises'
12
12
  import path from 'node:path'
13
- import {
14
- buildPackageGraph,
15
- foldScriptHooks,
16
- isLiteralPattern,
17
- outputsOverlap,
18
- VERSION,
19
- } from '@vzn/vx'
13
+ import { foldScriptHooks, isLiteralPattern, outputsOverlap, VERSION } from '@vzn/vx'
20
14
  import type { Gaps } from './plugin-gaps.js'
21
15
 
22
16
  /** What the stage uses of a mapping: tags and tasks per package name, and the gaps to report once. */
@@ -41,7 +35,7 @@ function codeId(): Promise<string> {
41
35
  code ??= (async () => {
42
36
  const parts: string[] = [
43
37
  VERSION,
44
- ...[buildPackageGraph, foldScriptHooks, isLiteralPattern, outputsOverlap].map(String),
38
+ ...[foldScriptHooks, isLiteralPattern, outputsOverlap].map(String),
45
39
  ]
46
40
  const files: string[] = []
47
41
  for await (const f of new Bun.Glob('**/*.{ts,cjs}').scan(import.meta.dir)) files.push(f)
package/src/migrate-nx.ts CHANGED
@@ -3,7 +3,8 @@
3
3
 
4
4
  import { stat } from 'node:fs/promises'
5
5
  import path from 'node:path'
6
- import { type MigrationFormat, type MigrationPlan, type ProjectMeta } from '@vzn/vx'
6
+ import { type MigrationFormat, type MigrationPlan } from '@vzn/vx'
7
+ import type { PackageMeta } from './packages.js'
7
8
  import { mapNxWorkspace, nxSizeText, parseNxGraph, readNxJson } from './nx/nx-map.js'
8
9
  import { spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
9
10
  import { adoptedToolNotes } from './workspace-notes.js'
@@ -17,10 +18,10 @@ export const NX_GRAPH_REL = '.nx/workspace-data/project-graph.json'
17
18
  /** `snapshot`: the graph file to read, absolute; the checked-in one by default. */
18
19
  export async function migrateNx(
19
20
  root: string,
20
- metas: readonly ProjectMeta[],
21
+ metas: readonly PackageMeta[],
21
22
  format: MigrationFormat = 'ts',
22
23
  snapshot = path.join(root, NX_GRAPH_REL),
23
- /** This run writes `vx.workspace.ts`, with nx.json's run settings as fields. */
24
+ /** This run writes `.vx/workspace.ts`, with nx.json's run settings as fields. */
24
25
  writesWorkspace = false,
25
26
  ): Promise<MigrationPlan> {
26
27
  const graph = parseNxGraph(await Bun.file(snapshot).text(), path.relative(root, snapshot))
@@ -33,9 +34,8 @@ export async function migrateNx(
33
34
  manifestField: (key) => ({ raw: `pkg.${key}` }),
34
35
  migration: true,
35
36
  })
36
- // nx() takes each committed file under an output back every run; a
37
- // written config must carry the same `!` entries, or its first run's
38
- // clean deletes them.
37
+ // A reader whose `!` hides a committed file under another task's output
38
+ // runs uncached, as under nx().
39
39
  if (tracked !== null)
40
40
  for (const [id, todo] of spareTrackedOutputs(root, mapped.projects, tracked)) {
41
41
  const at = id.lastIndexOf('#')
@@ -83,7 +83,7 @@ export async function migrateNx(
83
83
  */
84
84
  async function unlistedProjects(
85
85
  root: string,
86
- metas: readonly ProjectMeta[],
86
+ metas: readonly PackageMeta[],
87
87
  projects: readonly { name: string; dir: string }[],
88
88
  ): Promise<{ manifests: { relPath: string; contents: string }[]; notes: string[] }> {
89
89
  const listed = new Set(metas.map((m) => path.resolve(m.dir)))
@@ -122,9 +122,10 @@ async function unlistedProjects(
122
122
  }
123
123
  }
124
124
 
125
- /** A `vx.workspace.ts` field nx.json's run settings call for. */
125
+ /** A `.vx/workspace.ts` field nx.json's run settings call for. */
126
126
  export interface NxWorkspaceField {
127
- readonly field: 'concurrency' | 'affectedBase' | 'cacheRetention'
127
+ /** The setting's path: its group, then its field. */
128
+ readonly field: 'run.concurrency' | 'run.affectedBase' | 'cache.retention'
128
129
  /** The value as TypeScript source; undefined when vx cannot read it. */
129
130
  readonly source: string | undefined
130
131
  /** What to tell a user whose workspace file this run does not write. */
@@ -135,7 +136,7 @@ const quoted = (s: string) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}
135
136
 
136
137
  /**
137
138
  * nx.json's run-wide settings `nx()` applies live: each a field of the
138
- * written `vx.workspace.ts`, or a migrated repo that ran one task at a time
139
+ * written `.vx/workspace.ts`, or a migrated repo that ran one task at a time
139
140
  * for a shared database ran on every core. nx.json only: the files are
140
141
  * written for every machine, so `NX_PARALLEL` and friends are not read.
141
142
  */
@@ -153,23 +154,23 @@ export function nxWorkspaceFields(json: Record<string, unknown> | undefined): Nx
153
154
  const parallel = nx?.parallel ?? nx?.tasksRunnerOptions?.default?.options?.parallel
154
155
  if (typeof parallel === 'number' && Number.isInteger(parallel) && parallel > 0)
155
156
  out.push({
156
- field: 'concurrency',
157
+ field: 'run.concurrency',
157
158
  source: String(parallel),
158
- note: `nx.json \`parallel: ${parallel}\`: add \`concurrency: ${parallel}\` to vx.workspace.ts`,
159
+ note: `nx.json \`parallel: ${parallel}\`: add \`run: { concurrency: ${parallel} }\` to .vx/workspace.ts`,
159
160
  })
160
161
  const base = nx?.defaultBase ?? nx?.affected?.defaultBase
161
162
  if (typeof base === 'string' && base.trim() !== '')
162
163
  out.push({
163
- field: 'affectedBase',
164
+ field: 'run.affectedBase',
164
165
  source: quoted(base.trim()),
165
- note: `nx.json \`defaultBase\` ${JSON.stringify(base.trim())}: add \`affectedBase: ${JSON.stringify(base.trim())}\` to vx.workspace.ts`,
166
+ note: `nx.json \`defaultBase\` ${JSON.stringify(base.trim())}: add \`run: { affectedBase: ${JSON.stringify(base.trim())} }\` to .vx/workspace.ts`,
166
167
  })
167
168
  const size = nxSizeText(nx?.maxCacheSize)
168
169
  if (size !== undefined)
169
170
  out.push({
170
- field: 'cacheRetention',
171
+ field: 'cache.retention',
171
172
  source: /^\d+[KMG]?B$/.test(size) ? `{ maxSize: ${quoted(size)} }` : undefined,
172
- note: `nx.json \`maxCacheSize\`: add \`cacheRetention: { maxSize: ${JSON.stringify(size)} }\` to vx.workspace.ts`,
173
+ note: `nx.json \`maxCacheSize\`: add \`cache: { retention: { maxSize: ${JSON.stringify(size)} } }\` to .vx/workspace.ts`,
173
174
  })
174
175
  return out
175
176
  }