@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.
- package/README.md +33 -31
- package/package.json +2 -2
- package/src/adopt.ts +23 -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 +10 -2
- 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/shared-outputs.ts +66 -18
- package/src/tracked-outputs.ts +4 -4
- package/src/turbo/turbo-map.ts +178 -178
- package/src/turbo-cache/index.ts +28 -3
- package/src/vite-task/vite-task-map.ts +8 -6
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/shared-outputs.ts
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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['
|
|
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
|
|
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 =
|
|
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 (`
|
|
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
|
-
|
|
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 `
|
|
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 =
|
|
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
|
-
|
|
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?.['
|
|
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
|
-
|
|
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?.['
|
|
603
|
+
if (t.task?.['inputs'] === undefined) break
|
|
556
604
|
}
|
|
557
605
|
}
|
|
558
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 ` +
|