@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/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
- const cacheEnabled = !persistent && cacheWanted && wild === undefined
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
- if (cacheEnabled && mapped !== null) {
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(['default', '^default'], upstream.namedOf(nodeName), at, inputs, todos)
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
- for (const edge of upstream.resolve(nodeName, inputs, todos))
911
- if (!deps.includes(edge)) deps.push(edge)
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 = { inputs: cacheInputs, outputs }
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 `cache.inputs.runtime`: the name
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
  */
@@ -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 cacheInputs: Record<string, unknown> = { files }
351
- if (inputs.wsFiles.length > 0) cacheInputs.workspaceFiles = inputs.wsFiles
352
- if (inputs.envNames.length > 0) cacheInputs.env = inputs.envNames
353
- if (inputs.runtimeCmds.length > 0) cacheInputs.workspaceRuntime = inputs.runtimeCmds
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.cache = { inputs: cacheInputs, outputs: { files: [] } }
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)
@@ -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
@@ -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['cache'] !== undefined
357
+ t.task !== null && t.task['exec'] === undefined && t.task['inputs'] !== undefined
358
358
 
359
- /** A task's cached `inputs` block, when it has one with a `files` list. */
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 = (t.task?.['cache'] as { inputs?: { files?: unknown } } | undefined)?.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 (`rules.upfrontKeys`, on by default, core X-54): such a key
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
- delete t.task!['cache']
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 `cache.inputs.workspaceFiles`:
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 = (t.task?.['cache'] as { inputs?: { workspaceFiles?: unknown } } | undefined)
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
- delete t.task!['cache']
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?.['cache'] === undefined) break
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
- delete t.task!['cache']
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?.['cache'] === undefined) break
603
+ if (t.task?.['inputs'] === undefined) break
595
604
  }
596
605
  }
597
606
  }
@@ -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 = (r.task?.['cache'] as { inputs?: Outputs } | undefined)?.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 = (r.task?.['cache'] as { inputs?: Outputs } | undefined)?.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
- delete r.task!['cache']
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 ` +