@vzn/vx-migrate 0.0.636 → 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/README.md +294 -42
- package/package.json +3 -3
- package/src/adopt.ts +23 -8
- package/src/manifest-graph.ts +65 -0
- package/src/manifests.ts +52 -0
- package/src/mapping-cache.ts +2 -8
- package/src/migrate-nx.ts +17 -16
- package/src/migrate-scripts.ts +1261 -0
- package/src/migrate-turbo.ts +6 -7
- package/src/migrate-vite-task.ts +5 -4
- package/src/migrate.ts +376 -139
- package/src/nx/index.ts +77 -20
- package/src/nx/nx-boundaries.ts +191 -0
- package/src/nx/nx-deps.ts +3 -3
- package/src/nx/nx-map.ts +37 -41
- package/src/nx/nx-upstream.ts +3 -2
- package/src/nx-cache/index.ts +5 -11
- package/src/package-fs.ts +152 -0
- package/src/packages.ts +806 -0
- package/src/paths.ts +2 -2
- package/src/remote-cache.ts +118 -0
- package/src/shared-outputs.ts +63 -19
- package/src/tracked-outputs.ts +54 -35
- package/src/turbo/index.ts +32 -20
- package/src/turbo/turbo-map.ts +96 -56
- package/src/turbo-cache/index.ts +14 -20
- package/src/vite-task/vite-task-map.ts +13 -12
- package/src/workspace-notes.ts +12 -6
- package/src/workspace-plugins.ts +41 -11
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.637",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "Adopt @vzn/vx from Turborepo or
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
211
|
-
|
|
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
|
|
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
|
-
|
|
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}${
|
|
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
|
+
}
|
package/src/manifests.ts
ADDED
|
@@ -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
|
+
}
|
package/src/mapping-cache.ts
CHANGED
|
@@ -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
|
-
...[
|
|
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
|
|
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
|
|
21
|
+
metas: readonly PackageMeta[],
|
|
21
22
|
format: MigrationFormat = 'ts',
|
|
22
23
|
snapshot = path.join(root, NX_GRAPH_REL),
|
|
23
|
-
/** This run writes
|
|
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
|
-
//
|
|
37
|
-
//
|
|
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
|
|
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
|
|
125
|
+
/** A `.vx/workspace.ts` field nx.json's run settings call for. */
|
|
126
126
|
export interface NxWorkspaceField {
|
|
127
|
-
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
171
|
+
field: 'cache.retention',
|
|
171
172
|
source: /^\d+[KMG]?B$/.test(size) ? `{ maxSize: ${quoted(size)} }` : undefined,
|
|
172
|
-
note: `nx.json \`maxCacheSize\`: add \`
|
|
173
|
+
note: `nx.json \`maxCacheSize\`: add \`cache: { retention: { maxSize: ${JSON.stringify(size)} } }\` to .vx/workspace.ts`,
|
|
173
174
|
})
|
|
174
175
|
return out
|
|
175
176
|
}
|