@vzn/vx-migrate 0.0.638 → 0.0.640

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.
@@ -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)
@@ -171,6 +171,8 @@ interface Cached {
171
171
  name: string
172
172
  files: string[]
173
173
  hasUpstreamEdge: boolean
174
+ /** Globs of a sibling's this task's `!` entries take back (`separable`). */
175
+ back: Set<string>
174
176
  }
175
177
 
176
178
  function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
@@ -186,9 +188,32 @@ function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
186
188
  const deps = task['dependsOn']
187
189
  const hasUpstreamEdge =
188
190
  Array.isArray(deps) && deps.some((d) => typeof d === 'string' && d.startsWith('^'))
189
- return { index, name: t.name, files: strings, hasUpstreamEdge }
191
+ return { index, name: t.name, files: strings, hasUpstreamEdge, back: new Set() }
190
192
  }
191
193
 
194
+ /**
195
+ * Can the keeper's `ga` give `gb`'s tree back? Only when `ga` is a
196
+ * directory at any depth (`**\/dist`, `**\/dist/**`) and `gb`'s literal
197
+ * head names no such directory: the two meet only at a `dist` nested in
198
+ * `gb`'s tree, which is the sibling's. solid's build outputs `**\/dist`
199
+ * beside types' `types/**`; Turbo caches both, and uncaching types made
200
+ * vx the slower of the two.
201
+ */
202
+ function separable(ga: string, gb: string): boolean {
203
+ const name = /^\*\*\/([^*?[\]{}()!/\\]+)(?:\/\*\*)?$/.exec(ga)?.[1]
204
+ if (name === undefined) return false
205
+ const head: string[] = []
206
+ for (const seg of gb.replace(/^(\.\/)+/, '').split('/')) {
207
+ if (!isLiteralPattern(seg) || seg === '') break
208
+ head.push(seg)
209
+ }
210
+ return head.length > 0 && !head.includes(name)
211
+ }
212
+
213
+ /** Does keeper `k`'s `ga` still reach `gb` once its `!` entries are read? */
214
+ const hits = (k: Cached, ga: string, gb: string): boolean =>
215
+ !k.back.has(gb) && outputsOverlap(ga, gb)
216
+
192
217
  /**
193
218
  * Uncache every task whose declared outputs overlap a keeper's, in place,
194
219
  * and return the tasks. Deterministic: keepers are chosen in declaration
@@ -220,14 +245,28 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
220
245
  const kept: Cached[] = [keeper]
221
246
  for (const c of all) {
222
247
  if (c === keeper) continue
223
- const clash = kept.find((k) =>
224
- c.files.some((gb) => k.files.some((ga) => outputsOverlap(ga, gb))),
225
- )
248
+ const clash = kept.find((k) => c.files.some((gb) => k.files.some((ga) => hits(k, ga, gb))))
226
249
  if (clash === undefined) {
227
250
  kept.push(c)
228
251
  continue
229
252
  }
230
253
  const t = tasks[c.index]!
254
+ const pairs = kept.flatMap((k) =>
255
+ c.files.flatMap((gb) =>
256
+ k.files.filter((ga) => hits(k, ga, gb)).map((ga) => ({ k, ga, gb })),
257
+ ),
258
+ )
259
+ if (pairs.every((p) => separable(p.ga, p.gb))) {
260
+ for (const { k, gb } of pairs) {
261
+ if (k.back.has(gb)) continue
262
+ k.back.add(gb)
263
+ ;(tasks[k.index]!.task!['cache'] as { outputs: { files: string[] } }).outputs.files.push(
264
+ `!${gb}`,
265
+ )
266
+ }
267
+ kept.push(c)
268
+ continue
269
+ }
231
270
  // tsc's incremental state is no product: create-t3-turbo's build and
232
271
  // typecheck both output `.cache/tsbuildinfo.json`, and typecheck ran
233
272
  // uncached on every first run (Growth funnel F8). Left to the keeper,
@@ -236,7 +275,7 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
236
275
  // the keeper's tsbuildinfo calls built, missing or not, so its save
237
276
  // could be a partial dist.
238
277
  const sharedFiles = c.files.filter((gb) =>
239
- kept.some((k) => k.files.some((ga) => outputsOverlap(ga, gb))),
278
+ kept.some((k) => k.files.some((ga) => hits(k, ga, gb))),
240
279
  )
241
280
  if (
242
281
  sharedFiles.length === c.files.length &&
@@ -249,7 +288,7 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
249
288
  continue
250
289
  }
251
290
  dropped.add(c.index)
252
- const shared = c.files.find((gb) => clash.files.some((ga) => outputsOverlap(ga, gb)))!
291
+ const shared = c.files.find((gb) => clash.files.some((ga) => hits(clash, ga, gb)))!
253
292
  delete t.task!['cache']
254
293
  t.todos.push(
255
294
  `declares the output ${JSON.stringify(shared)} that ${JSON.stringify(clash.name)} also ` +
@@ -315,11 +354,21 @@ function prefixIndex(): {
315
354
  * task core refuses, and every verb failed.
316
355
  */
317
356
  export const isKeyedGroup = (t: { readonly task: Record<string, unknown> | null }): boolean =>
318
- t.task !== null && t.task['exec'] === undefined && t.task['cache'] !== undefined
357
+ t.task !== null && t.task['exec'] === undefined && t.task['inputs'] !== undefined
358
+
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
+ }
319
368
 
320
- /** A task's cached `inputs` block, when it has one with a `files` list. */
369
+ /** A task's `inputs` block, when it has one with a `files` list. */
321
370
  function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown } | undefined {
322
- const inputs = (t.task?.['cache'] as { inputs?: { files?: unknown } } | undefined)?.inputs
371
+ const inputs = t.task?.['inputs'] as { files?: unknown } | undefined
323
372
  return Array.isArray(inputs?.files)
324
373
  ? (inputs as { files: string[]; workspaceFiles?: unknown })
325
374
  : undefined
@@ -327,7 +376,7 @@ function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown
327
376
 
328
377
  /**
329
378
  * Core refuses a task whose inputs can match another task's declared
330
- * outputs (`rules.upfrontKeys`, on by default, core X-54): such a key
379
+ * outputs (`vx lock`, always, core X-54): such a key
331
380
  * reads what a producer writes this run and cannot be known before it ran.
332
381
  * Turbo and Nx default a task's inputs to every file of its package
333
382
  * (`**\/*`), which matches a sibling build's `dist`. Each overlapping
@@ -357,7 +406,7 @@ export function excludeSiblingOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
357
406
  const literal = read.find((g) => isLiteralPattern(g))
358
407
  if (literal !== undefined && isKeyedGroup(t)) continue
359
408
  if (literal !== undefined) {
360
- delete t.task!['cache']
409
+ unkey(t.task!)
361
410
  t.todos.push(
362
411
  `reads ${JSON.stringify(literal)}, which ${JSON.stringify(w.name)} writes — vx keys a ` +
363
412
  'task only on files no other task writes, so it runs uncached; read the source ' +
@@ -441,7 +490,7 @@ export function resolveSharedWorkspaceOutputs(
441
490
  }
442
491
 
443
492
  /**
444
- * `excludeSiblingOutputs` for root-anchored `cache.inputs.workspaceFiles`:
493
+ * `excludeSiblingOutputs` for root-anchored `inputs.workspaceFiles`:
445
494
  * every other task's `outputs.workspaceFiles`, and its `outputs.files` at
446
495
  * their workspace path, is taken back from a reader's workspace inputs
447
496
  * (Turbo's `globalDependencies` and `$TURBO_ROOT$` entries, Nx's
@@ -491,8 +540,7 @@ export function excludeWorkspaceOutputs(
491
540
  if (writers.length === 0) return
492
541
  for (const p of projects) {
493
542
  for (const t of p.tasks) {
494
- const inputs = (t.task?.['cache'] as { inputs?: { workspaceFiles?: unknown } } | undefined)
495
- ?.inputs
543
+ const inputs = t.task?.['inputs'] as { workspaceFiles?: unknown } | undefined
496
544
  if (inputs === undefined || !Array.isArray(inputs.workspaceFiles)) continue
497
545
  const files = inputs.workspaceFiles as string[]
498
546
  const reads = positive(files.flatMap((f) => (typeof f === 'string' ? [f] : expand(f))))
@@ -507,7 +555,7 @@ export function excludeWorkspaceOutputs(
507
555
  const literal = read.find((g) => isLiteralPattern(g))
508
556
  if (literal !== undefined && isKeyedGroup(t)) continue
509
557
  if (literal !== undefined) {
510
- delete t.task!['cache']
558
+ unkey(t.task!)
511
559
  t.todos.push(
512
560
  `reads ${JSON.stringify(literal)}, which ${w.id} writes — vx keys a task only on ` +
513
561
  'files no other task writes, so it runs uncached; read the source instead in a ' +
@@ -517,7 +565,7 @@ export function excludeWorkspaceOutputs(
517
565
  }
518
566
  files.push(`!${out}`)
519
567
  }
520
- if (t.task?.['cache'] === undefined) break
568
+ if (t.task?.['inputs'] === undefined) break
521
569
  }
522
570
  }
523
571
  }
@@ -542,7 +590,7 @@ export function excludeWorkspaceOutputs(
542
590
  const literal = out.startsWith(at) ? read.find((g) => isLiteralPattern(g)) : read[0]
543
591
  if (literal !== undefined && isKeyedGroup(t)) continue
544
592
  if (literal !== undefined) {
545
- delete t.task!['cache']
593
+ unkey(t.task!)
546
594
  t.todos.push(
547
595
  `reads ${JSON.stringify(literal.slice(at.length))}, which ${w.id} writes — vx keys ` +
548
596
  'a task only on files no other task writes, so it runs uncached; read the ' +
@@ -552,7 +600,7 @@ export function excludeWorkspaceOutputs(
552
600
  }
553
601
  files.push(back)
554
602
  }
555
- if (t.task?.['cache'] === undefined) break
603
+ if (t.task?.['inputs'] === undefined) break
556
604
  }
557
605
  }
558
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 ` +