@vzn/vx-migrate 0.0.636 → 0.0.638
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 +298 -44
- package/package.json +3 -3
- package/src/adopt.ts +25 -8
- package/src/adoption-plugin.ts +2 -2
- package/src/manifest-graph.ts +65 -0
- package/src/manifests.ts +52 -0
- package/src/mapping-cache.ts +5 -9
- package/src/migrate-nx.ts +63 -22
- 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 +474 -173
- package/src/nx/export-graph.ts +15 -4
- package/src/nx/index.ts +82 -23
- 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/plugin-gaps.ts +20 -5
- 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/src/migrate.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
// `vx-migrate [--from turbo|nx|vite-task] [--native|--keep] [--no-install] [--dry]
|
|
1
|
+
// `vx-migrate [--from turbo|nx|vite-task|scripts] [--native|--keep] [--no-install] [--dry]
|
|
2
2
|
// [--force] [--mjs]` — one vx.config.ts per workspace package from an
|
|
3
3
|
// existing Turbo or Nx setup (`--keep`: the workspace file that reads it
|
|
4
|
-
// live instead,
|
|
5
|
-
// manager.
|
|
4
|
+
// live instead), from Vite Task, or from package.json scripts, and vx
|
|
5
|
+
// installed with the repo's manager. `vx init` hands every repo with a
|
|
6
|
+
// package.json here (M, 2026-10-14: core reads no package.json).
|
|
6
7
|
// Without either mode flag a terminal is asked; anything else is native. Source auto-detect: turbo.json → Turbo;
|
|
7
8
|
// .nx/workspace-data/project-graph.json, nx.json or a Lerna-on-Nx lerna.json → Nx (the resolved
|
|
8
|
-
// graph, exported by nx if absent); `vite-plus` in the root package.json → Vite Task
|
|
9
|
-
// vite-plus is a whole toolchain, so beside turbo.json or Nx it is not the task runner
|
|
9
|
+
// graph, exported by nx if absent); `vite-plus` in the root package.json → Vite Task:
|
|
10
|
+
// vite-plus is a whole toolchain, so beside turbo.json or Nx it is not the task runner;
|
|
11
|
+
// else the package.json scripts.
|
|
10
12
|
// The mappers return a plan; core's migration seam
|
|
11
|
-
// (`applyMigration`) renders, guards, writes and reports
|
|
12
|
-
// package writes reads exactly like what `vx init` writes.
|
|
13
|
+
// (`applyMigration`) renders, guards, writes and reports.
|
|
13
14
|
|
|
14
15
|
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
15
16
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
@@ -19,22 +20,38 @@ import {
|
|
|
19
20
|
applyMigration,
|
|
20
21
|
findWorkspaceRoot,
|
|
21
22
|
isUserError,
|
|
22
|
-
|
|
23
|
-
loadWorkspace,
|
|
23
|
+
VERSION,
|
|
24
24
|
type MigrationFormat,
|
|
25
25
|
type MigrationPlan,
|
|
26
26
|
UserError,
|
|
27
|
+
ui,
|
|
27
28
|
} from '@vzn/vx'
|
|
29
|
+
import { migrateScripts } from './migrate-scripts.js'
|
|
30
|
+
import type { LoadReads } from './package-fs.js'
|
|
31
|
+
import {
|
|
32
|
+
discoverPackages,
|
|
33
|
+
findManifestRoot,
|
|
34
|
+
loadPackageWorkspace,
|
|
35
|
+
type PackageMeta,
|
|
36
|
+
type PackageWorkspace,
|
|
37
|
+
reachesManifest,
|
|
38
|
+
unreachedHint,
|
|
39
|
+
unreachedPackages,
|
|
40
|
+
} from './packages.js'
|
|
41
|
+
import { relPosix } from './paths.js'
|
|
42
|
+
import { REMOTE_CACHE, remoteCacheNote } from './remote-cache.js'
|
|
28
43
|
import { migrateNx, NX_GRAPH_REL, nxWorkspaceFields } from './migrate-nx.js'
|
|
29
|
-
import { exportGraph } from './nx/export-graph.js'
|
|
44
|
+
import { exportGraph, nxBin } from './nx/export-graph.js'
|
|
30
45
|
import { readNxJson } from './nx/nx-map.js'
|
|
31
46
|
import {
|
|
32
47
|
type AdoptionMode,
|
|
33
48
|
install,
|
|
49
|
+
installArgv,
|
|
34
50
|
listedPackages,
|
|
35
51
|
missingPackages,
|
|
36
52
|
MODE_QUESTION,
|
|
37
53
|
ownVersion,
|
|
54
|
+
type PackageManager,
|
|
38
55
|
packageManagerOf,
|
|
39
56
|
parseModeAnswer,
|
|
40
57
|
uninstall,
|
|
@@ -42,6 +59,7 @@ import {
|
|
|
42
59
|
import { migrateTurbo } from './migrate-turbo.js'
|
|
43
60
|
import { migrateViteTask } from './migrate-vite-task.js'
|
|
44
61
|
import { turboConfigFile } from './turbo/turbo-map.js'
|
|
62
|
+
import { lockfilePlugin } from './workspace-notes.js'
|
|
45
63
|
import {
|
|
46
64
|
extendWorkspaceFile,
|
|
47
65
|
generatedPlugins,
|
|
@@ -57,7 +75,7 @@ export interface MigrateArgs {
|
|
|
57
75
|
force: boolean
|
|
58
76
|
/** `vx.config.mjs` (and `vx-preset.mjs`) instead of `.ts`. */
|
|
59
77
|
mjs: boolean
|
|
60
|
-
from?: 'turbo' | 'nx' | 'vite-task'
|
|
78
|
+
from?: 'turbo' | 'nx' | 'vite-task' | 'scripts'
|
|
61
79
|
/** `--native` / `--keep`; unset asks a terminal and is native elsewhere. */
|
|
62
80
|
mode?: AdoptionMode
|
|
63
81
|
/** `--no-install`: leave package.json's dependencies alone. */
|
|
@@ -70,7 +88,7 @@ export interface MigrateArgs {
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
const USAGE =
|
|
73
|
-
'usage: vx-migrate [--from turbo|nx|vite-task] [--native|--keep] [--no-install] [--dry [--format json]] [--force] [--mjs]'
|
|
91
|
+
'usage: vx-migrate [--from turbo|nx|vite-task|scripts] [--native|--keep] [--no-install] [--dry [--format json]] [--force] [--mjs]'
|
|
74
92
|
|
|
75
93
|
export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
76
94
|
const out: MigrateArgs = { dry: false, force: false, mjs: false }
|
|
@@ -94,11 +112,8 @@ export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
|
94
112
|
out.mode = mode
|
|
95
113
|
} else if (a === '--from' || a?.startsWith('--from=')) {
|
|
96
114
|
const v = a === '--from' ? args[++i] : a.slice('--from='.length)
|
|
97
|
-
if (v !== 'turbo' && v !== 'nx' && v !== 'vite-task') {
|
|
98
|
-
return {
|
|
99
|
-
...out,
|
|
100
|
-
error: `--from must be turbo, nx or vite-task (package.json scripts: \`vx init\`)`,
|
|
101
|
-
}
|
|
115
|
+
if (v !== 'turbo' && v !== 'nx' && v !== 'vite-task' && v !== 'scripts') {
|
|
116
|
+
return { ...out, error: '--from must be turbo, nx, vite-task or scripts' }
|
|
102
117
|
}
|
|
103
118
|
out.from = v
|
|
104
119
|
} else if (a === '--help' || a === '-h') return { ...out, help: true }
|
|
@@ -134,10 +149,19 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
134
149
|
process.stderr.write(`vx-migrate: ${parsed.error}\n`)
|
|
135
150
|
return 1
|
|
136
151
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
152
|
+
const reads: LoadReads = new Map()
|
|
153
|
+
// An Nx repo with no package.json (Nx pinned in `.nx/installation`):
|
|
154
|
+
// nothing is installable, so its configs run on core alone.
|
|
155
|
+
let bare = false
|
|
156
|
+
const root = await migrationRoot(process.cwd(), reads).catch(async (err: unknown) => {
|
|
157
|
+
const nx = isUserError(err) ? nxRootWithoutManifest(process.cwd()) : undefined
|
|
158
|
+
if (nx === undefined) throw isUserError(err) ? await noRootReason(process.cwd(), err) : err
|
|
159
|
+
bare = true
|
|
160
|
+
return nx
|
|
139
161
|
})
|
|
140
|
-
const
|
|
162
|
+
const workspace = bare ? { root, packageGlobs: [] } : await loadPackageWorkspace(root, reads)
|
|
163
|
+
const nameless: string[] = []
|
|
164
|
+
const metas = bare ? [] : await discoverPackages(workspace, nameless)
|
|
141
165
|
|
|
142
166
|
const turboFile = await turboConfigFile(root)
|
|
143
167
|
const hasTurbo = turboFile !== null
|
|
@@ -168,7 +192,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
168
192
|
throw new UserError('--from turbo, but no turbo.json at the workspace root')
|
|
169
193
|
}
|
|
170
194
|
|
|
171
|
-
const runner: 'turbo' | 'nx' | 'vite-task' |
|
|
195
|
+
const runner: 'turbo' | 'nx' | 'vite-task' | 'scripts' =
|
|
172
196
|
parsed.from ??
|
|
173
197
|
(hasTurbo
|
|
174
198
|
? 'turbo'
|
|
@@ -176,23 +200,49 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
176
200
|
? 'nx'
|
|
177
201
|
: (await hasVitePlus(root))
|
|
178
202
|
? 'vite-task'
|
|
179
|
-
:
|
|
180
|
-
if (runner ===
|
|
203
|
+
: 'scripts')
|
|
204
|
+
if (runner === 'scripts') return scripts(root, workspace, metas, nameless, parsed)
|
|
205
|
+
if (runner === 'vite-task' && parsed.mode === 'keep') {
|
|
181
206
|
throw new UserError(
|
|
182
|
-
'
|
|
183
|
-
'for package.json scripts, run `vx init`',
|
|
207
|
+
'--keep: no plugin runs a Vite Task config live; drop --keep to write vx.config files',
|
|
184
208
|
)
|
|
185
209
|
}
|
|
186
|
-
|
|
210
|
+
const runnerFile =
|
|
211
|
+
runner === 'turbo' ? path.basename(turboFile ?? 'turbo.json') : lerna ? 'lerna.json' : 'nx.json'
|
|
212
|
+
// A second `vx init` after the native migration refused to overwrite
|
|
213
|
+
// the configs it wrote, exit 1: the first-run walk (2026-10-09). Set up
|
|
214
|
+
// is an answer, with the next step.
|
|
215
|
+
const existing = workspaceFileAt(root)
|
|
216
|
+
if (
|
|
217
|
+
runner !== 'vite-task' &&
|
|
218
|
+
existing !== undefined &&
|
|
219
|
+
!parsed.force &&
|
|
220
|
+
!parsed.dry &&
|
|
221
|
+
parsed.mode === undefined &&
|
|
222
|
+
metas.some((m) => m.configPath !== null) &&
|
|
223
|
+
!readFileSync(path.join(root, existing), 'utf8').includes(`${runner}(`)
|
|
224
|
+
) {
|
|
225
|
+
ui.print(
|
|
226
|
+
'out',
|
|
227
|
+
ui.report(`vx-migrate ${VERSION}`, [
|
|
228
|
+
['kept', `${existing} and the projects' vx.config files`],
|
|
229
|
+
['note', `vx is already set up here; --force regenerates them from ${runnerFile}`],
|
|
230
|
+
null,
|
|
231
|
+
['next', adoptionNext(root, runner, runnerFile, ['@vzn/vx'])],
|
|
232
|
+
]),
|
|
233
|
+
)
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
236
|
+
if (bare && parsed.mode === 'keep') {
|
|
187
237
|
throw new UserError(
|
|
188
|
-
'--keep:
|
|
238
|
+
'--keep: nx() is a plugin to install, and this Nx repo has no package.json to install it in; drop --keep to write vx.config files',
|
|
189
239
|
)
|
|
190
240
|
}
|
|
191
241
|
const mode =
|
|
192
|
-
runner === 'vite-task'
|
|
242
|
+
runner === 'vite-task' || bare
|
|
193
243
|
? 'native'
|
|
194
244
|
: (parsed.mode ?? (await askMode(runner, lerna ? 'lerna.json' : undefined, parsed.dry)))
|
|
195
|
-
if (mode === 'keep' && runner !== 'vite-task') return keep(root, runner,
|
|
245
|
+
if (mode === 'keep' && runner !== 'vite-task') return keep(root, runner, runnerFile, parsed)
|
|
196
246
|
|
|
197
247
|
const format: MigrationFormat = parsed.mjs ? 'mjs' : 'ts'
|
|
198
248
|
// No workspace file yet, or one vx wrote (a `--keep` adoption's): this run
|
|
@@ -217,14 +267,14 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
217
267
|
} else if (runner === 'nx') {
|
|
218
268
|
if (hasGraph) {
|
|
219
269
|
source = NX_GRAPH_REL
|
|
220
|
-
plan = await migrateNx(root, metas, format, undefined, writesWorkspace)
|
|
270
|
+
plan = await migrateNx(root, metas, format, undefined, writesWorkspace, bare)
|
|
221
271
|
} else {
|
|
222
272
|
// Modern Nx stores the graph in SQLite, so the JSON snapshot exists only
|
|
223
273
|
// when exported. The workspace's own nx exports it, as `nx()` does, into
|
|
224
274
|
// a temp file; the user ran that step by hand until 2026-10-01.
|
|
225
275
|
// A fresh clone: the export's own reason names the plugin's `graph`
|
|
226
276
|
// option, which the CLI does not take.
|
|
227
|
-
if (
|
|
277
|
+
if (nxBin(root) === undefined) {
|
|
228
278
|
throw new UserError(
|
|
229
279
|
`nx is not installed here (no node_modules/.bin/nx), and vx-migrate reads the graph it exports — ` +
|
|
230
280
|
`run \`${packageManagerOf(root, process.env['npm_config_user_agent'])} install\`, then vx-migrate again`,
|
|
@@ -241,7 +291,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
241
291
|
)
|
|
242
292
|
}
|
|
243
293
|
source = 'nx graph'
|
|
244
|
-
plan = await migrateNx(root, metas, format, snapshot, writesWorkspace)
|
|
294
|
+
plan = await migrateNx(root, metas, format, snapshot, writesWorkspace, bare)
|
|
245
295
|
} finally {
|
|
246
296
|
await rm(tmp, { recursive: true, force: true })
|
|
247
297
|
}
|
|
@@ -263,9 +313,15 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
263
313
|
) ||
|
|
264
314
|
plan.extraFiles.some((f) => existsSync(path.join(root, f.relPath))))
|
|
265
315
|
const kept = (adopted ?? []).filter((p) => !ADOPTION_PLUGINS.has(`${p.pkg}#${p.factory}`))
|
|
266
|
-
const plugins =
|
|
267
|
-
|
|
268
|
-
|
|
316
|
+
const plugins =
|
|
317
|
+
writesWorkspace && !bare
|
|
318
|
+
? [
|
|
319
|
+
...kept,
|
|
320
|
+
...workspacePlugins(root, runner === 'vite-task' ? undefined : runner).filter(
|
|
321
|
+
(p) => !kept.some((k) => k.factory === p.factory),
|
|
322
|
+
),
|
|
323
|
+
]
|
|
324
|
+
: []
|
|
269
325
|
let workspaceText: string | undefined
|
|
270
326
|
if (writesWorkspace) {
|
|
271
327
|
const fields =
|
|
@@ -284,7 +340,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
284
340
|
),
|
|
285
341
|
extraFiles:
|
|
286
342
|
existingWorkspace === undefined
|
|
287
|
-
? [...plan.extraFiles, { relPath:
|
|
343
|
+
? [...plan.extraFiles, { relPath: `.vx/workspace.${format}`, contents: workspaceText }]
|
|
288
344
|
: plan.extraFiles,
|
|
289
345
|
}
|
|
290
346
|
}
|
|
@@ -298,36 +354,80 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
298
354
|
)
|
|
299
355
|
// Nothing written imports vx-migrate or runs its bins: a `--keep`
|
|
300
356
|
// adoption's install of it is dropped with turbo() / nx().
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
357
|
+
const finalWorkspace = workspaceText ?? readFileSync(path.join(root, existingWorkspace!), 'utf8')
|
|
358
|
+
const needsSelf = runsBins || finalWorkspace.includes("'@vzn/vx-migrate'")
|
|
359
|
+
const cacheNote =
|
|
360
|
+
runner === 'vite-task'
|
|
361
|
+
? undefined
|
|
362
|
+
: remoteCacheNote(root, runner, finalWorkspace.includes(`${REMOTE_CACHE[runner]}(`))
|
|
363
|
+
const steps: Step[] =
|
|
364
|
+
refused || bare
|
|
365
|
+
? []
|
|
366
|
+
: [
|
|
367
|
+
...(existingWorkspace !== undefined && workspaceText !== undefined
|
|
368
|
+
? await rewriteWorkspace(root, existingWorkspace, workspaceText, adopted!, parsed.dry)
|
|
369
|
+
: []),
|
|
370
|
+
...(await prepareRepo(
|
|
371
|
+
root,
|
|
372
|
+
[
|
|
373
|
+
...new Set([
|
|
374
|
+
'@vzn/vx',
|
|
375
|
+
...(needsSelf ? ['@vzn/vx-migrate'] : []),
|
|
376
|
+
...plugins.map((p) => p.pkg),
|
|
377
|
+
]),
|
|
378
|
+
],
|
|
379
|
+
parsed,
|
|
380
|
+
needsSelf ? [] : ['@vzn/vx-migrate'],
|
|
381
|
+
)),
|
|
382
|
+
]
|
|
319
383
|
return applyMigration({
|
|
320
384
|
root,
|
|
321
385
|
metas,
|
|
322
386
|
plan,
|
|
323
387
|
source,
|
|
324
388
|
verb: 'vx-migrate',
|
|
325
|
-
|
|
389
|
+
steps,
|
|
390
|
+
notes: refused || bare || cacheNote === undefined ? [] : [cacheNote],
|
|
326
391
|
dry: parsed.dry,
|
|
327
392
|
json: parsed.json === true,
|
|
328
393
|
force: parsed.force,
|
|
329
394
|
format,
|
|
395
|
+
// The written workspace file imports plugins `--no-install` left out.
|
|
396
|
+
...(parsed.noInstall === true ? {} : { lock: lockWorkspace }),
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Core's `vx lock`, run where the configs were written: the step `vx init`
|
|
402
|
+
* ends with, through the core this repo resolves.
|
|
403
|
+
*/
|
|
404
|
+
async function lockWorkspace(root: string): Promise<{ code: number; said: string }> {
|
|
405
|
+
const p = Bun.spawn([process.execPath, '--no-install', coreBin(root), 'lock'], {
|
|
406
|
+
cwd: root,
|
|
407
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
330
408
|
})
|
|
409
|
+
const [out, err, code] = await Promise.all([
|
|
410
|
+
new Response(p.stdout).text(),
|
|
411
|
+
new Response(p.stderr).text(),
|
|
412
|
+
p.exited,
|
|
413
|
+
])
|
|
414
|
+
// A success's warnings print above the report; a refusal is said whole.
|
|
415
|
+
if (code === 0) process.stderr.write(err)
|
|
416
|
+
return { code, said: code === 0 ? out : out + err }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** A report row: what the tool did, e.g. `['ran', 'bun add -d …']`. */
|
|
420
|
+
type Step = readonly [label: string, value: string]
|
|
421
|
+
|
|
422
|
+
/** The `vx` entry of the core this repo resolves. */
|
|
423
|
+
function coreBin(root: string): string {
|
|
424
|
+
// A resolve that misses from a directory with no node_modules is an
|
|
425
|
+
// auto-install under Bun: ask the root only where it has the package.
|
|
426
|
+
const installed = path.join(root, 'node_modules', '@vzn', 'vx')
|
|
427
|
+
const core = existsSync(path.join(installed, 'package.json'))
|
|
428
|
+
? path.join(realpathSync(installed), 'src', 'index.ts')
|
|
429
|
+
: Bun.resolveSync('@vzn/vx', import.meta.dir)
|
|
430
|
+
return path.join(path.dirname(core), 'bin.ts')
|
|
331
431
|
}
|
|
332
432
|
|
|
333
433
|
/** vite-plus (`vp run`) in the root package.json's dependencies. */
|
|
@@ -359,7 +459,7 @@ const ADOPTION_PLUGINS = new Set(['@vzn/vx-migrate#turbo', '@vzn/vx-migrate#nx']
|
|
|
359
459
|
|
|
360
460
|
/**
|
|
361
461
|
* A `--keep` adoption's workspace file, written over for a native one.
|
|
362
|
-
* Returns the report's
|
|
462
|
+
* Returns the report's row; writes nothing under `--dry`.
|
|
363
463
|
*/
|
|
364
464
|
async function rewriteWorkspace(
|
|
365
465
|
root: string,
|
|
@@ -367,176 +467,377 @@ async function rewriteWorkspace(
|
|
|
367
467
|
text: string,
|
|
368
468
|
before: readonly WorkspacePlugin[],
|
|
369
469
|
dry: boolean,
|
|
370
|
-
): Promise<
|
|
470
|
+
): Promise<Step[]> {
|
|
371
471
|
if (readFileSync(path.join(root, file), 'utf8') === text) return []
|
|
372
472
|
const dropped = before.filter((p) => ADOPTION_PLUGINS.has(`${p.pkg}#${p.factory}`))
|
|
373
473
|
const names = dropped.map((p) => `${p.factory}()`).join(', ')
|
|
374
474
|
if (dry)
|
|
375
|
-
return [`would ${dropped.length > 0 ? `drop ${names} from` : 'rewrite'} ${file}
|
|
475
|
+
return [['config', `would ${dropped.length > 0 ? `drop ${names} from` : 'rewrite'} ${file}`]]
|
|
376
476
|
await Bun.write(path.join(root, file), text)
|
|
377
|
-
return [`${dropped.length > 0 ? `dropped ${names} from` : 'rewrote'} ${file}`]
|
|
477
|
+
return [['config', `${dropped.length > 0 ? `dropped ${names} from` : 'rewrote'} ${file}`]]
|
|
378
478
|
}
|
|
379
479
|
|
|
380
480
|
/**
|
|
381
481
|
* Install what the written files import and remove what they no longer
|
|
382
|
-
* do. Returns the report's
|
|
482
|
+
* do. Returns the report's rows; changes nothing under `--dry`.
|
|
383
483
|
*/
|
|
384
484
|
async function prepareRepo(
|
|
385
485
|
root: string,
|
|
386
486
|
wanted: readonly string[],
|
|
387
487
|
args: MigrateArgs,
|
|
388
488
|
unwanted: readonly string[] = [],
|
|
389
|
-
): Promise<
|
|
489
|
+
): Promise<Step[]> {
|
|
390
490
|
if (args.noInstall === true) return []
|
|
391
|
-
const
|
|
491
|
+
const steps: Step[] = []
|
|
392
492
|
const version = ownVersion()
|
|
393
493
|
const missing = missingPackages(root, wanted, version)
|
|
394
494
|
if (missing.length > 0) {
|
|
395
|
-
|
|
396
|
-
|
|
495
|
+
steps.push(
|
|
496
|
+
args.dry ? ['install', missing.join(' ')] : ['ran', await install(root, missing, version)],
|
|
497
|
+
)
|
|
397
498
|
}
|
|
398
499
|
// Last: the package removed may be the one running this.
|
|
399
500
|
const listed = listedPackages(root, unwanted)
|
|
400
501
|
if (listed.length > 0) {
|
|
401
|
-
|
|
402
|
-
else notes.push(`removed ${listed.join(' ')} (${await uninstall(root, listed)})`)
|
|
502
|
+
steps.push(args.dry ? ['remove', listed.join(' ')] : ['ran', await uninstall(root, listed)])
|
|
403
503
|
}
|
|
404
|
-
return
|
|
504
|
+
return steps
|
|
405
505
|
}
|
|
406
506
|
|
|
407
507
|
/**
|
|
408
|
-
* `--keep`:
|
|
409
|
-
*
|
|
410
|
-
*
|
|
508
|
+
* `--keep`: `.vx/workspace.ts` declaring `turbo()` or `nx()`, so the
|
|
509
|
+
* runner's own config stays the source, beside the plugins the repo calls
|
|
510
|
+
* for (its remote cache among them), and the one command from here to a
|
|
511
|
+
* run. A workspace file already here is kept when it declares the runner.
|
|
411
512
|
*/
|
|
412
513
|
async function keep(
|
|
413
514
|
root: string,
|
|
414
515
|
runner: 'turbo' | 'nx',
|
|
415
|
-
|
|
416
|
-
lerna: boolean,
|
|
516
|
+
source: string,
|
|
417
517
|
args: MigrateArgs,
|
|
418
518
|
): Promise<number> {
|
|
419
|
-
|
|
420
|
-
|
|
519
|
+
const format: MigrationFormat = args.mjs ? 'mjs' : 'ts'
|
|
520
|
+
const name = `.vx/workspace.${format}`
|
|
521
|
+
const all = [{ pkg: '@vzn/vx-migrate', factory: runner }, ...workspacePlugins(root, runner)]
|
|
522
|
+
const existing = workspaceFileAt(root)
|
|
523
|
+
const before =
|
|
524
|
+
existing === undefined || args.force
|
|
525
|
+
? undefined
|
|
526
|
+
: readFileSync(path.join(root, existing), 'utf8')
|
|
527
|
+
if (before !== undefined && !before.includes(`${runner}(`)) {
|
|
421
528
|
throw new UserError(
|
|
422
|
-
|
|
529
|
+
`${existing} exists; add ${runner}() from @vzn/vx-migrate to its plugins, or --force replaces it`,
|
|
423
530
|
)
|
|
424
531
|
}
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
? workspacePlugins(root)
|
|
432
|
-
: []
|
|
433
|
-
for (const note of await prepareRepo(
|
|
532
|
+
// A kept file in the one-line shape gains the plugins it lacks; any
|
|
533
|
+
// other shape is the user's, and left alone.
|
|
534
|
+
const extended = before === undefined ? null : extendWorkspaceFile(before, all)
|
|
535
|
+
const text = before === undefined ? renderWorkspaceFile(all, format) : (extended ?? before)
|
|
536
|
+
const declares = before === undefined || extended !== null
|
|
537
|
+
const steps = await prepareRepo(
|
|
434
538
|
root,
|
|
435
|
-
['@vzn/vx', '@vzn/vx-migrate', ...
|
|
539
|
+
[...new Set(['@vzn/vx', '@vzn/vx-migrate', ...(declares ? all.map((p) => p.pkg) : [])])],
|
|
436
540
|
args,
|
|
437
|
-
)
|
|
438
|
-
|
|
541
|
+
)
|
|
542
|
+
if (args.json)
|
|
543
|
+
for (const [label, value] of steps) process.stderr.write(`vx-migrate: ${label}: ${value}\n`)
|
|
544
|
+
const ignore = existsSync(path.join(root, '.vx', '.gitignore')) ? [] : ['.vx/.gitignore']
|
|
545
|
+
if (!args.dry) {
|
|
546
|
+
if (before === undefined) {
|
|
547
|
+
if (existing !== undefined && existing !== name) await rm(path.join(root, existing))
|
|
548
|
+
await Bun.write(path.join(root, name), text)
|
|
549
|
+
} else if (text !== before) await Bun.write(path.join(root, existing!), text)
|
|
550
|
+
// vx's own housekeeping, as a run's first cache open writes it.
|
|
551
|
+
if (ignore.length > 0) await Bun.write(path.join(root, '.vx', '.gitignore'), DOT_VX_GITIGNORE)
|
|
439
552
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
553
|
+
const cache = REMOTE_CACHE[runner]
|
|
554
|
+
const cacheLine =
|
|
555
|
+
before?.includes(`${cache}(`) === true
|
|
556
|
+
? undefined
|
|
557
|
+
: remoteCacheNote(root, runner, text.includes(`${cache}(`))
|
|
558
|
+
const added = before === undefined ? [] : undeclared(before, all).map((p) => `${p.factory}()`)
|
|
559
|
+
const wrote: Step =
|
|
560
|
+
before === undefined
|
|
561
|
+
? ['files', [name, ...ignore].join('\n')]
|
|
562
|
+
: text === before
|
|
563
|
+
? ['kept', `${existing} (declares ${runner}())`]
|
|
564
|
+
: [
|
|
565
|
+
'config',
|
|
566
|
+
`${args.dry ? 'would declare' : 'declared'} ${added.join(', ')} in ${existing}`,
|
|
567
|
+
]
|
|
568
|
+
const next = adoptionNext(root, runner, source, [
|
|
569
|
+
...new Set(['@vzn/vx', '@vzn/vx-migrate', ...(declares ? all.map((p) => p.pkg) : [])]),
|
|
570
|
+
])
|
|
571
|
+
if (args.json) {
|
|
572
|
+
process.stdout.write(
|
|
573
|
+
`${JSON.stringify({
|
|
574
|
+
dry: true,
|
|
575
|
+
source,
|
|
576
|
+
files: [
|
|
577
|
+
...(text === before
|
|
578
|
+
? []
|
|
579
|
+
: [{ path: before === undefined ? name : existing!, contents: text }]),
|
|
580
|
+
...ignore.map((p) => ({ path: p, contents: DOT_VX_GITIGNORE })),
|
|
581
|
+
],
|
|
582
|
+
kept: text === before ? [existing] : [],
|
|
583
|
+
replaced:
|
|
584
|
+
before === undefined && existing !== undefined && existing !== name ? [existing] : [],
|
|
585
|
+
todos: [],
|
|
586
|
+
notes: cacheLine === undefined ? [] : [cacheLine],
|
|
587
|
+
next,
|
|
588
|
+
})}\n`,
|
|
589
|
+
)
|
|
590
|
+
return 0
|
|
591
|
+
}
|
|
592
|
+
if (args.dry && before === undefined) process.stdout.write(`── ${name} ──\n${text}\n`)
|
|
593
|
+
// A run reads only the lock; with the plugins installed it is written
|
|
594
|
+
// now, else `next` names `vx lock` after the install.
|
|
595
|
+
const lock = !args.dry && !next.includes(' lock && ') ? await lockWorkspace(root) : undefined
|
|
596
|
+
if (lock !== undefined && lock.code !== 0) process.stderr.write(lock.said)
|
|
597
|
+
const rows: ui.FieldRow[] = [
|
|
598
|
+
['from', `${source}, run by ${runner}() from @vzn/vx-migrate`],
|
|
599
|
+
...(args.dry ? [['dry run', 'nothing written'] as const] : []),
|
|
600
|
+
...steps,
|
|
601
|
+
wrote,
|
|
602
|
+
...(lock === undefined ? [] : [lockRow(lock)]),
|
|
456
603
|
[
|
|
457
|
-
|
|
458
|
-
'--
|
|
459
|
-
|
|
460
|
-
'init',
|
|
461
|
-
'--keep',
|
|
462
|
-
...flags,
|
|
604
|
+
'note',
|
|
605
|
+
'a temporary start: vx init --native writes native config',
|
|
606
|
+
...(cacheLine === undefined ? [] : [cacheLine]),
|
|
463
607
|
],
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return
|
|
608
|
+
null,
|
|
609
|
+
[
|
|
610
|
+
'next',
|
|
611
|
+
lock === undefined || lock.code === 0 ? next : `fix the above, then vx lock and ${next}`,
|
|
612
|
+
],
|
|
613
|
+
]
|
|
614
|
+
ui.print('out', ui.report(`vx-migrate ${VERSION}`, rows))
|
|
615
|
+
return lock?.code ?? 0
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** `vx lock`'s outcome as the report's row. */
|
|
619
|
+
function lockRow(lock: { code: number; said: string }): ui.FieldRow {
|
|
620
|
+
return [
|
|
621
|
+
'lock',
|
|
622
|
+
lock.code === 0
|
|
623
|
+
? lock.said.trim().replace(/^✓ vx lock\s+/, '')
|
|
624
|
+
: ui.tone('error', 'not written'),
|
|
625
|
+
]
|
|
477
626
|
}
|
|
478
627
|
|
|
479
|
-
/**
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
628
|
+
/** `.vx/.gitignore`'s text, as core writes it: the machine-local cache stays out of git. */
|
|
629
|
+
const DOT_VX_GITIGNORE = '/cache\n'
|
|
630
|
+
|
|
631
|
+
// npx and bunx name the package: their bare `vx` is a stranger's npm package.
|
|
632
|
+
const EXEC: Record<PackageManager, string> = {
|
|
633
|
+
pnpm: 'pnpm vx',
|
|
634
|
+
yarn: 'yarn vx',
|
|
635
|
+
bun: 'bunx @vzn/vx',
|
|
636
|
+
npm: 'npx @vzn/vx',
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Install what the workspace file imports, if missing, then run the repo's
|
|
641
|
+
* build. Pinned to this release: every @vzn package releases in lockstep,
|
|
642
|
+
* and `latest` beside an older vx mismatched them (agent trial 2).
|
|
643
|
+
*/
|
|
644
|
+
export function adoptionNext(
|
|
645
|
+
root: string,
|
|
646
|
+
runner: 'turbo' | 'nx',
|
|
647
|
+
source: string,
|
|
648
|
+
needs: readonly string[] = ['@vzn/vx', '@vzn/vx-migrate'],
|
|
649
|
+
version: string | null = ownVersion(),
|
|
650
|
+
): string {
|
|
651
|
+
const pm = packageManagerOf(root)
|
|
652
|
+
const missing = needs.filter(
|
|
653
|
+
(p) => !existsSync(path.join(root, 'node_modules', p, 'package.json')),
|
|
487
654
|
)
|
|
488
|
-
|
|
655
|
+
// A global vx (no runner started this one) runs the workspace's plugins as they are.
|
|
656
|
+
const vx = process.env['npm_config_user_agent'] === undefined ? 'vx' : EXEC[pm]
|
|
657
|
+
const run = `${vx} run ${firstTask(path.join(root, source), runner)} --all`
|
|
658
|
+
if (missing.length === 0) return run
|
|
659
|
+
const pinned = version === null ? missing : missing.map((p) => `${p}@${version}`)
|
|
660
|
+
return `${installArgv(root, pm, pinned).join(' ')} && ${vx} lock && ${run}`
|
|
489
661
|
}
|
|
490
662
|
|
|
491
|
-
/**
|
|
492
|
-
|
|
663
|
+
/** `build` when the runner's config names it, else the first task it names. */
|
|
664
|
+
function firstTask(file: string, runner: 'turbo' | 'nx'): string {
|
|
665
|
+
let text: string
|
|
666
|
+
try {
|
|
667
|
+
text = readFileSync(file, 'utf8')
|
|
668
|
+
} catch {
|
|
669
|
+
return 'build'
|
|
670
|
+
}
|
|
671
|
+
if (/"build"\s*:/.test(text)) return 'build'
|
|
672
|
+
const field = runner === 'turbo' ? '(?:tasks|pipeline)' : 'targetDefaults'
|
|
673
|
+
const first = new RegExp(`"${field}"\\s*:\\s*\\{\\s*"([^"]+)"`).exec(text)?.[1]
|
|
674
|
+
return first ?? 'build'
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* package.json scripts → one vx.config.ts per package, and the workspace
|
|
679
|
+
* file declaring the lockfile plugin its lockfile calls for: `vx init`'s
|
|
680
|
+
* mapper until core stopped reading package.json (M, 2026-10-14).
|
|
681
|
+
*/
|
|
682
|
+
async function scripts(
|
|
493
683
|
root: string,
|
|
494
|
-
|
|
684
|
+
workspace: PackageWorkspace,
|
|
685
|
+
metas: readonly PackageMeta[],
|
|
686
|
+
nameless: readonly string[],
|
|
495
687
|
args: MigrateArgs,
|
|
496
688
|
): Promise<number> {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
689
|
+
if (args.mode !== undefined) {
|
|
690
|
+
throw new UserError(
|
|
691
|
+
`--${args.mode} answers a Turbo or Nx repo's question, and no turbo.json or nx.json is at ${relPosix(process.cwd(), root) || '.'}`,
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
const unmapped = namelessNotes(root, nameless)
|
|
695
|
+
// Only a workspace file this run writes declares it; one already here is the user's.
|
|
696
|
+
const lockfile = workspaceFileAt(root) === undefined ? lockfilePlugin(root) : undefined
|
|
697
|
+
const plugins =
|
|
698
|
+
lockfile === undefined ? [] : [{ pkg: '@vzn/vx-lockfile', factory: lockfile.factory }]
|
|
699
|
+
const plan = migrateScripts(
|
|
700
|
+
metas,
|
|
701
|
+
metas.some((m) => m.dir === root) ? undefined : await readRootManifest(root),
|
|
702
|
+
root,
|
|
703
|
+
)
|
|
704
|
+
const empty = plan.projects.length === 0
|
|
705
|
+
// Single-project mode with packages the root's missing `workspaces` never
|
|
706
|
+
// reaches: the scripts exist, the globs do not (item 248).
|
|
707
|
+
const unreached = empty ? await unreachedPackages(workspace) : []
|
|
708
|
+
// Globs that reach no package: "no scripts" was true and named nothing
|
|
709
|
+
// to fix (M-42). Asked of the globs, not of `metas`: a nameless or
|
|
710
|
+
// shared-name member is matched yet absent from `metas` (M-46).
|
|
711
|
+
const globs = workspace.packageGlobs.filter((g) => g !== '.')
|
|
712
|
+
const noMembers =
|
|
713
|
+
empty && globs.length > 0 && !(await reachesManifest(workspace))
|
|
714
|
+
? [
|
|
715
|
+
`the workspace globs (${globs.map((g) => `"${g}"`).join(', ')}) match no package.json: add a package under one, or fix the glob`,
|
|
716
|
+
]
|
|
717
|
+
: []
|
|
718
|
+
const installed = await prepareRepo(
|
|
719
|
+
root,
|
|
720
|
+
plugins.map((p) => p.pkg),
|
|
721
|
+
args,
|
|
722
|
+
)
|
|
723
|
+
return applyMigration({
|
|
724
|
+
root,
|
|
725
|
+
metas,
|
|
726
|
+
plan,
|
|
727
|
+
source: 'package.json scripts',
|
|
728
|
+
verb: 'vx-migrate',
|
|
729
|
+
notes: [...noMembers, ...(await unreadWorkspaces(root, workspace.packageGlobs)), ...unmapped],
|
|
730
|
+
steps: installed,
|
|
731
|
+
unmapped: unmapped.length > 0,
|
|
732
|
+
...(unreached.length > 0 ? { emptyReason: unreachedHint(unreached, root) } : {}),
|
|
733
|
+
dry: args.dry,
|
|
734
|
+
json: args.json === true,
|
|
735
|
+
force: args.force,
|
|
736
|
+
init: true,
|
|
737
|
+
format: args.mjs ? 'mjs' : 'ts',
|
|
738
|
+
plugins,
|
|
739
|
+
...(args.noInstall === true ? {} : { lock: lockWorkspace }),
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* A member whose package.json has no `name` is no project, so its scripts
|
|
745
|
+
* map to nothing, and init said nothing of them (D-106).
|
|
746
|
+
*/
|
|
747
|
+
function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
748
|
+
const withScripts = dirs
|
|
749
|
+
.filter((dir) => {
|
|
750
|
+
const scripts = (
|
|
751
|
+
JSON.parse(readText(path.join(dir, 'package.json')) || '{}') as { scripts?: unknown }
|
|
752
|
+
).scripts
|
|
753
|
+
return (
|
|
754
|
+
typeof scripts === 'object' &&
|
|
755
|
+
scripts !== null &&
|
|
756
|
+
Object.values(scripts).some((v) => typeof v === 'string' && v !== '')
|
|
521
757
|
)
|
|
522
|
-
|
|
758
|
+
})
|
|
759
|
+
.map((dir) => relPosix(root, dir))
|
|
760
|
+
.sort()
|
|
761
|
+
// The root's own manifest is the empty relative path: a nameless
|
|
762
|
+
// single-package repo read `not mapped: — its package.json…` (M-54).
|
|
763
|
+
.map((rel) => (rel === '' ? 'the root' : rel))
|
|
764
|
+
if (withScripts.length === 0) return []
|
|
765
|
+
const shown =
|
|
766
|
+
withScripts.slice(0, 3).join(', ') +
|
|
767
|
+
(withScripts.length > 3 ? `, and ${withScripts.length - 3} more` : '')
|
|
768
|
+
return [
|
|
769
|
+
`not mapped: ${shown} — ${withScripts.length === 1 ? 'its package.json has' : 'their package.json files have'} no "name", and vx names a project by it; give ${withScripts.length === 1 ? 'it one' : 'each one'} and run \`vx init\` again`,
|
|
770
|
+
]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* `pnpm-workspace.yaml`'s `packages` decides the members, as pnpm reads it,
|
|
775
|
+
* and a root `workspaces` that lists other globs was dropped without a word:
|
|
776
|
+
* a bun or npm repo with a stale yaml lost those members (M-51).
|
|
777
|
+
*/
|
|
778
|
+
async function unreadWorkspaces(root: string, globs: readonly string[]): Promise<string[]> {
|
|
779
|
+
if (!existsSync(path.join(root, 'pnpm-workspace.yaml'))) return []
|
|
780
|
+
const ws = (await readRootManifest(root))['workspaces']
|
|
781
|
+
const list = Array.isArray(ws)
|
|
782
|
+
? ws
|
|
783
|
+
: typeof ws === 'object' &&
|
|
784
|
+
ws !== null &&
|
|
785
|
+
Array.isArray((ws as { packages?: unknown }).packages)
|
|
786
|
+
? (ws as { packages: unknown[] }).packages
|
|
787
|
+
: undefined
|
|
788
|
+
if (list === undefined) return []
|
|
789
|
+
const pkg = list.filter((g): g is string => typeof g === 'string')
|
|
790
|
+
if ([...pkg].sort().join('\0') === [...globs].sort().join('\0')) return []
|
|
791
|
+
return [
|
|
792
|
+
`pnpm-workspace.yaml's \`packages\` lists the members, as pnpm reads them; package.json's \`workspaces\` (${pkg.map((g) => `"${g}"`).join(', ')}) is not read: if bun, npm or yarn installs this repo, copy those globs into pnpm-workspace.yaml's \`packages\`, or delete that key so package.json decides`,
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** The root package.json as an object, `{}` when unreadable or not one. */
|
|
797
|
+
async function readRootManifest(root: string): Promise<Record<string, unknown>> {
|
|
798
|
+
const pj: unknown = await Bun.file(path.join(root, 'package.json'))
|
|
799
|
+
.json()
|
|
800
|
+
.catch(() => ({}))
|
|
801
|
+
return typeof pj === 'object' && pj !== null && !Array.isArray(pj)
|
|
802
|
+
? (pj as Record<string, unknown>)
|
|
803
|
+
: {}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function readText(file: string): string {
|
|
807
|
+
try {
|
|
808
|
+
return readFileSync(file, 'utf8').replace(/^/, '')
|
|
809
|
+
} catch {
|
|
810
|
+
return ''
|
|
523
811
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The workspace root, or where this run makes one: the package manager's
|
|
816
|
+
* root. A nested workspace the outer one does not list is its own: the
|
|
817
|
+
* outer .vx/workspace.ts made init report the outer configs and write
|
|
818
|
+
* nothing.
|
|
819
|
+
*/
|
|
820
|
+
async function migrationRoot(cwd: string, reads: LoadReads): Promise<string> {
|
|
821
|
+
const marked = await findWorkspaceRoot(cwd).catch(() => undefined)
|
|
822
|
+
if (marked === undefined) return findManifestRoot(cwd, reads)
|
|
823
|
+
const manifest = await findManifestRoot(cwd, reads).catch(() => marked)
|
|
824
|
+
if (manifest.startsWith(`${marked}${path.sep}`)) {
|
|
825
|
+
const nested = await loadPackageWorkspace(manifest, reads).catch(() => undefined)
|
|
826
|
+
if (nested !== undefined && nested.packageGlobs.length > 0) return manifest
|
|
531
827
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
828
|
+
return marked
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The nearest directory up to the git top-level holding nx.json, for a
|
|
833
|
+
* repo with no package.json: Nx's `nx init` outside JS pins Nx in
|
|
834
|
+
* `.nx/installation` and finds projects by `project.json`.
|
|
835
|
+
*/
|
|
836
|
+
function nxRootWithoutManifest(cwd: string): string | undefined {
|
|
837
|
+
for (let d = cwd; ; d = path.dirname(d)) {
|
|
838
|
+
if (existsSync(path.join(d, 'nx.json'))) return d
|
|
839
|
+
if (existsSync(path.join(d, '.git')) || path.dirname(d) === d) return undefined
|
|
535
840
|
}
|
|
536
|
-
if (existing !== undefined && existing !== name) await rm(path.join(root, existing))
|
|
537
|
-
await Bun.write(path.join(root, name), text)
|
|
538
|
-
process.stdout.write(`vx-migrate: lerna.json found — wrote ${name} declaring nx()\n`)
|
|
539
|
-
return 0
|
|
540
841
|
}
|
|
541
842
|
|
|
542
843
|
/**
|