@vzn/vx-migrate 0.0.639 → 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 +33 -31
- package/package.json +2 -2
- package/src/adopt.ts +38 -0
- package/src/bin.ts +19 -2
- package/src/migrate-scripts.ts +1 -1
- package/src/migrate-turbo.ts +6 -9
- package/src/migrate.ts +19 -5
- package/src/nx/nx-inputs.ts +39 -5
- package/src/nx/nx-map.ts +51 -15
- package/src/nx/nx-upstream.ts +5 -5
- package/src/nx-cache/index.ts +0 -4
- package/src/shared-outputs.ts +21 -12
- package/src/tracked-outputs.ts +4 -4
- package/src/turbo/turbo-map.ts +178 -178
- package/src/turbo-cache/index.ts +1 -38
- package/src/vite-task/vite-task-map.ts +8 -6
package/src/nx/nx-map.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// configuration is a task of its own, `<target>:<configuration>`; the
|
|
17
17
|
// default configuration is folded into the base task.
|
|
18
18
|
|
|
19
|
+
import { existsSync } from 'node:fs'
|
|
19
20
|
import path from 'node:path'
|
|
20
21
|
import { type Duration, type GeneratedProject, type GeneratedTask, UserError } from '@vzn/vx'
|
|
21
22
|
import type { PackageMeta } from '../packages.js'
|
|
@@ -270,6 +271,8 @@ export const BUILTIN_EXECUTORS: ReadonlySet<string> = new Set([
|
|
|
270
271
|
/** The options with what the mapper reads itself: the root's dependency names. */
|
|
271
272
|
type MapOpts = MapNxOptions & {
|
|
272
273
|
readonly rootDeps: ReadonlySet<string>
|
|
274
|
+
/** The root TypeScript config Nx folds into every task's hash, if any. */
|
|
275
|
+
readonly rootTsConfig: string | undefined
|
|
273
276
|
readonly pnp: boolean
|
|
274
277
|
/** Tasks per `syncGenerators` list, said once in the notes. */
|
|
275
278
|
readonly syncTasks: Map<string, number>
|
|
@@ -317,6 +320,9 @@ export async function mapNxWorkspace(
|
|
|
317
320
|
...opts,
|
|
318
321
|
cacheable: new Set([...opts.cacheable, ...cacheable]),
|
|
319
322
|
rootDeps: await rootDependencies(root),
|
|
323
|
+
rootTsConfig: ['tsconfig.base.json', 'tsconfig.json'].find((f) =>
|
|
324
|
+
existsSync(path.join(root, f)),
|
|
325
|
+
),
|
|
320
326
|
pnp: yarnPnp(root),
|
|
321
327
|
syncTasks: new Map(),
|
|
322
328
|
}
|
|
@@ -881,7 +887,19 @@ function buildTask(
|
|
|
881
887
|
if (wild !== undefined && cacheWanted && !persistent) todos.push(wildcardTodo(wild))
|
|
882
888
|
// The manifest or config an output covers is taken back, so core loads it.
|
|
883
889
|
if (wild === undefined) outFiles.push(...ownFileTakeBacks(outFiles, opts.ownConfig?.(projectRel)))
|
|
884
|
-
|
|
890
|
+
// Nx keys the outputs of the tasks this one depends on, and vx their
|
|
891
|
+
// keys through `dependsOn`. With no such edge left (the producer is a
|
|
892
|
+
// project vx did not migrate: nx-examples' .NET ProductsApi), the key
|
|
893
|
+
// saw nothing the task reads, and a changed document replayed the old
|
|
894
|
+
// generated client (Growth, 2026-10-16).
|
|
895
|
+
const orphanReader =
|
|
896
|
+
inputs.dependentOutputs.length > 0 && !deps.some((d) => !d.includes('#nx-input:'))
|
|
897
|
+
if (orphanReader && cacheWanted && !persistent) {
|
|
898
|
+
todos.push(
|
|
899
|
+
`reads ${inputs.dependentOutputs.map((g) => `\`${g}\``).join(', ')} from the outputs of the tasks it depends on, and no task it depends on is migrated (dependsOn ${JSON.stringify(target.dependsOn ?? [])}) — uncached here, so it never replays output generated from an older input; migrate the producer and depend on it to cache this task`,
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
const cacheEnabled = !persistent && cacheWanted && wild === undefined && !orphanReader
|
|
885
903
|
if (persistent && cacheWanted) {
|
|
886
904
|
todos.push('Nx caches this target, and vx never caches a persistent task — uncached here')
|
|
887
905
|
}
|
|
@@ -898,17 +916,28 @@ function buildTask(
|
|
|
898
916
|
const gens = target.syncGenerators.map((g) => JSON.stringify(g)).join(', ')
|
|
899
917
|
opts.syncTasks.set(gens, (opts.syncTasks.get(gens) ?? 0) + 1)
|
|
900
918
|
}
|
|
901
|
-
|
|
919
|
+
// vx keys an uncached task too, and a dependant folds that key. Its own
|
|
920
|
+
// inputs are enough there: Nx never folds a dependency's hash, so what a
|
|
921
|
+
// dependant reads upstream its own `^` inputs already name, and twins
|
|
922
|
+
// for the uncached one's `^` inputs would key nothing new.
|
|
923
|
+
if (mapped !== null) {
|
|
902
924
|
// No `inputs` is Nx's `default` and `^default`: the project's
|
|
903
925
|
// `default` named input (its whole tree unless declared) and each
|
|
904
926
|
// dependency's. No gap to report (487 lines per run on refine,
|
|
905
927
|
// 2026-09-22).
|
|
906
928
|
if (target.inputs === undefined)
|
|
907
|
-
expandNxInputs(
|
|
929
|
+
expandNxInputs(
|
|
930
|
+
cacheEnabled ? ['default', '^default'] : ['default'],
|
|
931
|
+
upstream.namedOf(nodeName),
|
|
932
|
+
at,
|
|
933
|
+
inputs,
|
|
934
|
+
todos,
|
|
935
|
+
)
|
|
908
936
|
if (inputs.files.length === 0 && target.inputs === undefined) inputs.files.push('**/*')
|
|
909
937
|
// Before the env block: a dependency's `{ env }` passes through too.
|
|
910
|
-
|
|
911
|
-
|
|
938
|
+
if (cacheEnabled)
|
|
939
|
+
for (const edge of upstream.resolve(nodeName, inputs, todos))
|
|
940
|
+
if (!deps.includes(edge)) deps.push(edge)
|
|
912
941
|
}
|
|
913
942
|
|
|
914
943
|
if (mapped === null) {
|
|
@@ -957,18 +986,25 @@ function buildTask(
|
|
|
957
986
|
}
|
|
958
987
|
const task: Record<string, unknown> = { exec }
|
|
959
988
|
if (deps.length > 0) task.dependsOn = deps
|
|
989
|
+
const keyed: Record<string, unknown> = { files: withoutTakenBack(inputs.files) }
|
|
990
|
+
// Nx hashes the root TypeScript config into every task (its
|
|
991
|
+
// TsConfiguration input, other projects' `paths` left out): a
|
|
992
|
+
// compilerOptions edit re-ran Nx's tests and replayed vx's (Growth,
|
|
993
|
+
// 2026-10-16). The whole file here: a `paths` edit re-keys every task.
|
|
994
|
+
if (opts.rootTsConfig !== undefined && !inputs.wsFiles.includes(opts.rootTsConfig))
|
|
995
|
+
inputs.wsFiles.push(opts.rootTsConfig)
|
|
996
|
+
if (inputs.wsFiles.length > 0) keyed.workspaceFiles = withoutTakenBack(inputs.wsFiles)
|
|
997
|
+
if (inputs.envNames.length > 0) keyed.env = inputs.envNames
|
|
998
|
+
// The `.env` files the task loads are inputs, and gitignored ones
|
|
999
|
+
// (`.env.local`) are invisible to a glob: their bytes, read per run.
|
|
1000
|
+
// Their paths are project-relative: `runtime`, in the project dir.
|
|
1001
|
+
if (mapped.envInputs.length > 0) keyed.runtime = [envProbe(mapped.envInputs)]
|
|
1002
|
+
if (inputs.runtimeCmds.length > 0) keyed.workspaceRuntime = inputs.runtimeCmds
|
|
1003
|
+
task.inputs = keyed
|
|
960
1004
|
if (cacheEnabled) {
|
|
961
|
-
const cacheInputs: Record<string, unknown> = { files: withoutTakenBack(inputs.files) }
|
|
962
|
-
if (inputs.wsFiles.length > 0) cacheInputs.workspaceFiles = withoutTakenBack(inputs.wsFiles)
|
|
963
|
-
if (inputs.envNames.length > 0) cacheInputs.env = inputs.envNames
|
|
964
|
-
// The `.env` files the task loads are inputs, and gitignored ones
|
|
965
|
-
// (`.env.local`) are invisible to a glob: their bytes, read per run.
|
|
966
|
-
// Their paths are project-relative: `runtime`, in the project dir.
|
|
967
|
-
if (mapped.envInputs.length > 0) cacheInputs.runtime = [envProbe(mapped.envInputs)]
|
|
968
|
-
if (inputs.runtimeCmds.length > 0) cacheInputs.workspaceRuntime = inputs.runtimeCmds
|
|
969
1005
|
const outputs: Record<string, unknown> = { files: outFiles }
|
|
970
1006
|
if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
|
|
971
|
-
task.cache = {
|
|
1007
|
+
task.cache = { outputs }
|
|
972
1008
|
}
|
|
973
1009
|
|
|
974
1010
|
return { name: variant.name, todos, task }
|
|
@@ -1211,7 +1247,7 @@ function argsTodo(options: Record<string, unknown>, todos: string[]): void {
|
|
|
1211
1247
|
}
|
|
1212
1248
|
|
|
1213
1249
|
/**
|
|
1214
|
-
* Prints each file's name and bytes, for `
|
|
1250
|
+
* Prints each file's name and bytes, for `inputs.runtime`: the name
|
|
1215
1251
|
* keeps a line moved from one file to the next (a different precedence) a
|
|
1216
1252
|
* different key.
|
|
1217
1253
|
*/
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -347,16 +347,16 @@ export function planNxUpstream(
|
|
|
347
347
|
// `tsconfig.spec.json` (or a dependency's unbuilt `*.d.ts`) quiet; the
|
|
348
348
|
// file appearing still re-keys it, and a missing literal is silent.
|
|
349
349
|
const files = [...new Set(['package.json', 'project.json', ...inputs.files])]
|
|
350
|
-
const
|
|
351
|
-
if (inputs.wsFiles.length > 0)
|
|
352
|
-
if (inputs.envNames.length > 0)
|
|
353
|
-
if (inputs.runtimeCmds.length > 0)
|
|
350
|
+
const keyed: Record<string, unknown> = { files }
|
|
351
|
+
if (inputs.wsFiles.length > 0) keyed.workspaceFiles = inputs.wsFiles
|
|
352
|
+
if (inputs.envNames.length > 0) keyed.env = inputs.envNames
|
|
353
|
+
if (inputs.runtimeCmds.length > 0) keyed.workspaceRuntime = inputs.runtimeCmds
|
|
354
354
|
// A keyed group: dependants fold its key; nothing spawns.
|
|
355
355
|
const task: Record<string, unknown> = {}
|
|
356
356
|
if (isFileset(name))
|
|
357
357
|
task.description = `Nx fileset ${name} of this project and what it reaches`
|
|
358
358
|
if (edges.size > 0) task.dependsOn = [...edges].sort()
|
|
359
|
-
task.
|
|
359
|
+
task.inputs = keyed
|
|
360
360
|
const list = out.get(node) ?? []
|
|
361
361
|
list.push({ name: inputTask(name), task, todos })
|
|
362
362
|
out.set(node, list)
|
package/src/nx-cache/index.ts
CHANGED
|
@@ -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
|
package/src/shared-outputs.ts
CHANGED
|
@@ -354,11 +354,21 @@ function prefixIndex(): {
|
|
|
354
354
|
* task core refuses, and every verb failed.
|
|
355
355
|
*/
|
|
356
356
|
export const isKeyedGroup = (t: { readonly task: Record<string, unknown> | null }): boolean =>
|
|
357
|
-
t.task !== null && t.task['exec'] === undefined && t.task['
|
|
357
|
+
t.task !== null && t.task['exec'] === undefined && t.task['inputs'] !== undefined
|
|
358
358
|
|
|
359
|
-
/**
|
|
359
|
+
/**
|
|
360
|
+
* A task whose key cannot be complete: no cache and no key, so it runs
|
|
361
|
+
* every time and every dependant misses (core's keyless task). Uncached
|
|
362
|
+
* alone, its incomplete key still let a dependant hit stale.
|
|
363
|
+
*/
|
|
364
|
+
export function unkey(task: Record<string, unknown>): void {
|
|
365
|
+
delete task['cache']
|
|
366
|
+
delete task['inputs']
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** A task's `inputs` block, when it has one with a `files` list. */
|
|
360
370
|
function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown } | undefined {
|
|
361
|
-
const inputs =
|
|
371
|
+
const inputs = t.task?.['inputs'] as { files?: unknown } | undefined
|
|
362
372
|
return Array.isArray(inputs?.files)
|
|
363
373
|
? (inputs as { files: string[]; workspaceFiles?: unknown })
|
|
364
374
|
: undefined
|
|
@@ -366,7 +376,7 @@ function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown
|
|
|
366
376
|
|
|
367
377
|
/**
|
|
368
378
|
* Core refuses a task whose inputs can match another task's declared
|
|
369
|
-
* outputs (`
|
|
379
|
+
* outputs (`vx lock`, always, core X-54): such a key
|
|
370
380
|
* reads what a producer writes this run and cannot be known before it ran.
|
|
371
381
|
* Turbo and Nx default a task's inputs to every file of its package
|
|
372
382
|
* (`**\/*`), which matches a sibling build's `dist`. Each overlapping
|
|
@@ -396,7 +406,7 @@ export function excludeSiblingOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
396
406
|
const literal = read.find((g) => isLiteralPattern(g))
|
|
397
407
|
if (literal !== undefined && isKeyedGroup(t)) continue
|
|
398
408
|
if (literal !== undefined) {
|
|
399
|
-
|
|
409
|
+
unkey(t.task!)
|
|
400
410
|
t.todos.push(
|
|
401
411
|
`reads ${JSON.stringify(literal)}, which ${JSON.stringify(w.name)} writes — vx keys a ` +
|
|
402
412
|
'task only on files no other task writes, so it runs uncached; read the source ' +
|
|
@@ -480,7 +490,7 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
480
490
|
}
|
|
481
491
|
|
|
482
492
|
/**
|
|
483
|
-
* `excludeSiblingOutputs` for root-anchored `
|
|
493
|
+
* `excludeSiblingOutputs` for root-anchored `inputs.workspaceFiles`:
|
|
484
494
|
* every other task's `outputs.workspaceFiles`, and its `outputs.files` at
|
|
485
495
|
* their workspace path, is taken back from a reader's workspace inputs
|
|
486
496
|
* (Turbo's `globalDependencies` and `$TURBO_ROOT$` entries, Nx's
|
|
@@ -530,8 +540,7 @@ export function excludeWorkspaceOutputs(
|
|
|
530
540
|
if (writers.length === 0) return
|
|
531
541
|
for (const p of projects) {
|
|
532
542
|
for (const t of p.tasks) {
|
|
533
|
-
const inputs =
|
|
534
|
-
?.inputs
|
|
543
|
+
const inputs = t.task?.['inputs'] as { workspaceFiles?: unknown } | undefined
|
|
535
544
|
if (inputs === undefined || !Array.isArray(inputs.workspaceFiles)) continue
|
|
536
545
|
const files = inputs.workspaceFiles as string[]
|
|
537
546
|
const reads = positive(files.flatMap((f) => (typeof f === 'string' ? [f] : expand(f))))
|
|
@@ -546,7 +555,7 @@ export function excludeWorkspaceOutputs(
|
|
|
546
555
|
const literal = read.find((g) => isLiteralPattern(g))
|
|
547
556
|
if (literal !== undefined && isKeyedGroup(t)) continue
|
|
548
557
|
if (literal !== undefined) {
|
|
549
|
-
|
|
558
|
+
unkey(t.task!)
|
|
550
559
|
t.todos.push(
|
|
551
560
|
`reads ${JSON.stringify(literal)}, which ${w.id} writes — vx keys a task only on ` +
|
|
552
561
|
'files no other task writes, so it runs uncached; read the source instead in a ' +
|
|
@@ -556,7 +565,7 @@ export function excludeWorkspaceOutputs(
|
|
|
556
565
|
}
|
|
557
566
|
files.push(`!${out}`)
|
|
558
567
|
}
|
|
559
|
-
if (t.task?.['
|
|
568
|
+
if (t.task?.['inputs'] === undefined) break
|
|
560
569
|
}
|
|
561
570
|
}
|
|
562
571
|
}
|
|
@@ -581,7 +590,7 @@ export function excludeWorkspaceOutputs(
|
|
|
581
590
|
const literal = out.startsWith(at) ? read.find((g) => isLiteralPattern(g)) : read[0]
|
|
582
591
|
if (literal !== undefined && isKeyedGroup(t)) continue
|
|
583
592
|
if (literal !== undefined) {
|
|
584
|
-
|
|
593
|
+
unkey(t.task!)
|
|
585
594
|
t.todos.push(
|
|
586
595
|
`reads ${JSON.stringify(literal.slice(at.length))}, which ${w.id} writes — vx keys ` +
|
|
587
596
|
'a task only on files no other task writes, so it runs uncached; read the ' +
|
|
@@ -591,7 +600,7 @@ export function excludeWorkspaceOutputs(
|
|
|
591
600
|
}
|
|
592
601
|
files.push(back)
|
|
593
602
|
}
|
|
594
|
-
if (t.task?.['
|
|
603
|
+
if (t.task?.['inputs'] === undefined) break
|
|
595
604
|
}
|
|
596
605
|
}
|
|
597
606
|
}
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { lstat, readFile } from 'node:fs/promises'
|
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { isLiteralPattern, outputsOverlap } from '@vzn/vx'
|
|
13
13
|
import { relPosix } from './paths.js'
|
|
14
|
-
import { isKeyedGroup } from './shared-outputs.js'
|
|
14
|
+
import { isKeyedGroup, unkey } from './shared-outputs.js'
|
|
15
15
|
|
|
16
16
|
/** The files git tracks under `root`, root-relative; null outside a repo or without git. */
|
|
17
17
|
export async function trackedFiles(root: string): Promise<string[] | null> {
|
|
@@ -243,7 +243,7 @@ export function spareTrackedOutputs(
|
|
|
243
243
|
for (const q of projects) {
|
|
244
244
|
const qrel = relPosix(root, q.dir)
|
|
245
245
|
for (const r of q.tasks) {
|
|
246
|
-
const inputs =
|
|
246
|
+
const inputs = r.task?.['inputs'] as Outputs | undefined
|
|
247
247
|
if (inputs === undefined) continue
|
|
248
248
|
const own =
|
|
249
249
|
q === owner &&
|
|
@@ -309,7 +309,7 @@ export function spareTrackedOutputs(
|
|
|
309
309
|
for (const q of projects) {
|
|
310
310
|
for (const r of q.tasks) {
|
|
311
311
|
if (r === t) continue
|
|
312
|
-
const inputs =
|
|
312
|
+
const inputs = r.task?.['inputs'] as Outputs | undefined
|
|
313
313
|
if (inputs === undefined) continue
|
|
314
314
|
const f =
|
|
315
315
|
(q === p ? hidden(inputs.files, local) : undefined) ??
|
|
@@ -324,7 +324,7 @@ export function spareTrackedOutputs(
|
|
|
324
324
|
inputs.workspaceFiles = unhidden(inputs.workspaceFiles, wsBacks, rooted)
|
|
325
325
|
continue
|
|
326
326
|
}
|
|
327
|
-
|
|
327
|
+
unkey(r.task!)
|
|
328
328
|
todos.push([
|
|
329
329
|
`${q.name}#${r.name}`,
|
|
330
330
|
`reads the committed ${f}, which its inputs take back with ${p.name}#${t.name}'s ` +
|