@vzn/vx-migrate 0.0.639 → 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 +32 -30
- 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 +21 -12
- package/src/tracked-outputs.ts +4 -4
- package/src/turbo/turbo-map.ts +178 -178
- 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
|
@@ -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 ` +
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveSharedOutputs,
|
|
25
25
|
resolveSharedWorkspaceOutputs,
|
|
26
26
|
takingBack,
|
|
27
|
+
unkey,
|
|
27
28
|
wildcardCleanTodo,
|
|
28
29
|
wildcardOutput,
|
|
29
30
|
wildcardTodo,
|
|
@@ -68,7 +69,7 @@ interface TurboJson {
|
|
|
68
69
|
|
|
69
70
|
const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
70
71
|
|
|
71
|
-
/** A name core takes in `
|
|
72
|
+
/** A name core takes in `inputs.env` and `exec.env.passThrough`. */
|
|
72
73
|
const keyable = (name: string): boolean =>
|
|
73
74
|
name.length > 0 && !name.startsWith('!') && !/[*?[\]{}=\0]/.test(name)
|
|
74
75
|
|
|
@@ -936,7 +937,7 @@ export async function mapTurboWorkspace(
|
|
|
936
937
|
if (envMode === 'loose') {
|
|
937
938
|
notes.push(
|
|
938
939
|
'envMode "loose": Turbo passes every environment variable to every task; vx passes only ' +
|
|
939
|
-
'the declared ones — list what each task reads in exec.env.passThrough (or
|
|
940
|
+
'the declared ones — list what each task reads in exec.env.passThrough (or inputs.env)',
|
|
940
941
|
)
|
|
941
942
|
} else if (
|
|
942
943
|
!envMode &&
|
|
@@ -954,7 +955,7 @@ export async function mapTurboWorkspace(
|
|
|
954
955
|
notes.push(
|
|
955
956
|
`Turbo 1 runs ${loose.join(', ')} in loose env mode (no passThroughEnv, so its "infer" ` +
|
|
956
957
|
'mode passes every environment variable); vx passes only the declared ones — list what ' +
|
|
957
|
-
'each reads in exec.env.passThrough (or
|
|
958
|
+
'each reads in exec.env.passThrough (or inputs.env)',
|
|
958
959
|
)
|
|
959
960
|
}
|
|
960
961
|
}
|
|
@@ -1288,12 +1289,12 @@ export async function mapTurboWorkspace(
|
|
|
1288
1289
|
notes.push(
|
|
1289
1290
|
`${fw.slug} in ${few(users)}: Turbo passes ${few(fw.env)} implicitly; the configs list ` +
|
|
1290
1291
|
'the names the package and its workspace dependencies spell, so add any an installed ' +
|
|
1291
|
-
'dependency reads to
|
|
1292
|
+
'dependency reads to inputs.env and exec.env.passThrough',
|
|
1292
1293
|
)
|
|
1293
1294
|
for (const [fw, users] of usersOf)
|
|
1294
1295
|
notes.push(
|
|
1295
1296
|
`${fw.slug} in ${few(users)}: Turbo passes ${few(fw.env)} implicitly; list the ones ` +
|
|
1296
|
-
'they read in
|
|
1297
|
+
'they read in inputs.env and exec.env.passThrough',
|
|
1297
1298
|
)
|
|
1298
1299
|
|
|
1299
1300
|
// A task's own `*` env name, in a written config: expanded over the
|
|
@@ -1470,7 +1471,7 @@ export async function mapTurboWorkspace(
|
|
|
1470
1471
|
for (const t of projects.find((p) => p.name === meta.name)?.tasks ?? []) {
|
|
1471
1472
|
const def = defFor(t.name)
|
|
1472
1473
|
const dep = def === undefined ? undefined : dependencyOutputs.get(def)
|
|
1473
|
-
const inputs =
|
|
1474
|
+
const inputs = t.task?.['inputs'] as Record<string, unknown> | undefined
|
|
1474
1475
|
if (dep === undefined || inputs === undefined) continue
|
|
1475
1476
|
const rels: string[] = []
|
|
1476
1477
|
const unfold = new Set<string>()
|
|
@@ -1500,7 +1501,7 @@ export async function mapTurboWorkspace(
|
|
|
1500
1501
|
if (rels.length === 0) continue
|
|
1501
1502
|
const probe = rels.every(isLiteral) ? ignoredFilesProbe(rels) : dotenvGlobsProbe(rels)
|
|
1502
1503
|
if (probe === null) {
|
|
1503
|
-
|
|
1504
|
+
unkey(t.task!)
|
|
1504
1505
|
t.todos.push(
|
|
1505
1506
|
`inputs read ${rels.map((r) => JSON.stringify(r)).join(', ')} from an uncached ` +
|
|
1506
1507
|
'dependency, a glob vx cannot key by a probe — task runs uncached; key it in a vx.config to cache it',
|
|
@@ -1534,7 +1535,7 @@ export async function mapTurboWorkspace(
|
|
|
1534
1535
|
notes.push(
|
|
1535
1536
|
`env ${JSON.stringify(entry)}: the configs list the names the files of each task's ` +
|
|
1536
1537
|
`package and its workspace dependencies spell (${n} task${n === 1 ? '' : 's'}) — add ` +
|
|
1537
|
-
'any other a task reads to
|
|
1538
|
+
'any other a task reads to inputs.env and exec.env.passThrough',
|
|
1538
1539
|
)
|
|
1539
1540
|
literalEnvGapsOnce(projects, notes)
|
|
1540
1541
|
resolveSharedWorkspaceOutputs(root, projects)
|
|
@@ -1571,7 +1572,7 @@ function nestedInputs(root: string, projects: readonly TurboMappedProject[]): vo
|
|
|
1571
1572
|
const nested = inside.filter((n) => !inside.some((m) => n.startsWith(`${m}/`)))
|
|
1572
1573
|
if (nested.length === 0) return
|
|
1573
1574
|
for (const t of p.tasks) {
|
|
1574
|
-
const inputs =
|
|
1575
|
+
const inputs = t.task?.['inputs'] as Record<string, unknown> | undefined
|
|
1575
1576
|
const files = inputs?.['files']
|
|
1576
1577
|
if (inputs === undefined || !Array.isArray(files)) continue
|
|
1577
1578
|
const extra: string[] = []
|
|
@@ -1763,13 +1764,13 @@ function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
|
|
|
1763
1764
|
const cache = task['cache'] as
|
|
1764
1765
|
| { outputs: { files: unknown[]; workspaceFiles?: unknown[] } }
|
|
1765
1766
|
| undefined
|
|
1766
|
-
if (
|
|
1767
|
+
if (task['inputs'] === undefined && task['dependsOn'] === undefined) return t
|
|
1767
1768
|
if (
|
|
1768
1769
|
cache !== undefined &&
|
|
1769
1770
|
(cache.outputs.files.length > 0 || cache.outputs.workspaceFiles !== undefined)
|
|
1770
1771
|
)
|
|
1771
1772
|
return t
|
|
1772
|
-
const { exec: _exec, ...group } = task
|
|
1773
|
+
const { exec: _exec, cache: _cache, ...group } = task
|
|
1773
1774
|
return { ...t, task: group }
|
|
1774
1775
|
}
|
|
1775
1776
|
|
|
@@ -1949,7 +1950,7 @@ function buildTask(
|
|
|
1949
1950
|
'env',
|
|
1950
1951
|
e,
|
|
1951
1952
|
literal,
|
|
1952
|
-
' in vx env names — list explicit names in
|
|
1953
|
+
' in vx env names — list explicit names in inputs.env + exec.env.passThrough',
|
|
1953
1954
|
),
|
|
1954
1955
|
),
|
|
1955
1956
|
opts.envNames,
|
|
@@ -1981,181 +1982,180 @@ function buildTask(
|
|
|
1981
1982
|
if (typeof def['description'] === 'string') task.description = def['description']
|
|
1982
1983
|
if (deps.length > 0) task.dependsOn = deps
|
|
1983
1984
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
continue
|
|
2010
|
-
}
|
|
2011
|
-
const neg = i.startsWith('!')
|
|
2012
|
-
// Turbo's globs have character classes and extglobs; a vx bracket is
|
|
2013
|
-
// a literal, so `src/**/*.[jt]s` keyed on nothing and an edit
|
|
2014
|
-
// replayed the old build (item 1031). A positive glob with no safe
|
|
2015
|
-
// form widens to every package file; a negation with none is dropped
|
|
2016
|
-
// (it would exclude fewer files, never more).
|
|
2017
|
-
const wax = minimatchToVx(neg ? i.slice(1) : i, neg)
|
|
2018
|
-
if (wax === null) {
|
|
2019
|
-
todos.push(`input ${JSON.stringify(i)}: glob syntax vx cannot take — map manually`)
|
|
2020
|
-
if (!neg) files.push('**/*')
|
|
2021
|
-
continue
|
|
2022
|
-
}
|
|
2023
|
-
const translated = (neg ? '!' : '') + wax
|
|
2024
|
-
const body = wax
|
|
2025
|
-
const up = climbed(translated)
|
|
2026
|
-
if (!neg && isDotenvGlob(body)) {
|
|
2027
|
-
if (body.startsWith('$TURBO_ROOT$/')) wsDotenv.push(body.slice('$TURBO_ROOT$/'.length))
|
|
2028
|
-
else if (up !== null) wsDotenv.push(up)
|
|
2029
|
-
else {
|
|
2030
|
-
pkgDotenv = true
|
|
2031
|
-
if (body.includes('/')) pkgDotenvDeep = true
|
|
2032
|
-
}
|
|
2033
|
-
continue
|
|
2034
|
-
}
|
|
2035
|
-
if (body.startsWith('$TURBO_ROOT$/')) {
|
|
2036
|
-
wsFiles.push(...withZeroSegments((neg ? '!' : '') + body.slice('$TURBO_ROOT$/'.length)))
|
|
2037
|
-
const rel = body.slice('$TURBO_ROOT$/'.length)
|
|
2038
|
-
if (!neg) rels.set(rel, ['workspaceFiles', rel])
|
|
2039
|
-
} else if (up !== null) {
|
|
2040
|
-
wsFiles.push(...withZeroSegments(up))
|
|
2041
|
-
if (!neg) rels.set(up, ['workspaceFiles', up])
|
|
2042
|
-
} else if (body.startsWith('../')) {
|
|
2043
|
-
todos.push(`input ${JSON.stringify(i)}: leaves the workspace — map manually`)
|
|
2044
|
-
} else if (i.includes('$TURBO_ROOT$')) {
|
|
2045
|
-
todos.push(
|
|
2046
|
-
`input ${JSON.stringify(i)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
2047
|
-
'prefix (→ cache.inputs.workspaceFiles) — map manually',
|
|
2048
|
-
)
|
|
2049
|
-
} else {
|
|
2050
|
-
// Unkeyed, a root file edit `*\/**` reaches in Turbo replayed a stale hit.
|
|
2051
|
-
files.push(...withZeroSegments(translated))
|
|
2052
|
-
const rel = path.posix.join(atRoot ? '.' : pkgDir, body)
|
|
2053
|
-
if (!neg) rels.set(rel, [atRoot ? 'workspaceFiles' : 'files', translated])
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
// Exclusions alone narrow every package file: core refuses a list
|
|
2057
|
-
// with nothing to narrow, and that refusal failed the whole run
|
|
2058
|
-
// (item 936). Every file is the widest reading, so it can cost a
|
|
2059
|
-
// hit and never serve a stale one.
|
|
2060
|
-
if (files.length > 0 && files.every((f) => typeof f === 'string' && f.startsWith('!'))) {
|
|
2061
|
-
files.unshift('**/*')
|
|
1985
|
+
const files: unknown[] = []
|
|
1986
|
+
// `.env` files Turbo hashes although git ignores them; a glob over git's
|
|
1987
|
+
// files keyed none of them (item 1032). Probed instead, per package and
|
|
1988
|
+
// at the root.
|
|
1989
|
+
let pkgDotenv = false
|
|
1990
|
+
// Any of them below the package root needs the walk.
|
|
1991
|
+
let pkgDotenvDeep = false
|
|
1992
|
+
// The root-relative `.env` globs, probed at the workspace root.
|
|
1993
|
+
const wsDotenv = [...rootDotenv]
|
|
1994
|
+
// globalDependencies are workspace-root-relative by definition —
|
|
1995
|
+
// they map to inputs.workspaceFiles, not project-relative files.
|
|
1996
|
+
const wsFiles: unknown[] = [...global('inputs')]
|
|
1997
|
+
// Root-relative path → the entry naming it.
|
|
1998
|
+
const rels = new Map<string, Named>(literals.globalIgnored.map((f) => [f, [null, f]]))
|
|
1999
|
+
const atRoot = pkgDir === '' || pkgDir === '.'
|
|
2000
|
+
if (def.inputs === undefined || def.inputs.length === 0) {
|
|
2001
|
+
// Turbo's default input set is every package file, and an empty
|
|
2002
|
+
// `inputs` is that default: mapped as no files, it keyed on nothing
|
|
2003
|
+
// in the package and an edit replayed a stale build (item 936).
|
|
2004
|
+
files.push('**/*')
|
|
2005
|
+
} else {
|
|
2006
|
+
for (const i of def.inputs) {
|
|
2007
|
+
if (i === '$TURBO_DEFAULT$') {
|
|
2008
|
+
files.push('**/*')
|
|
2009
|
+
continue
|
|
2062
2010
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
// the rest is Turbo's grammar: `dist/**/*.[cm]js` matched nothing, so
|
|
2075
|
-
// a hit restored nothing (item 1031).
|
|
2076
|
-
const cut = raw.indexOf('/')
|
|
2077
|
-
const rest = cut < 0 ? null : minimatchToVx(raw.slice(cut + 1), raw.startsWith('!'))
|
|
2078
|
-
if (cut >= 0 && rest === null) {
|
|
2079
|
-
todos.push(
|
|
2080
|
-
`output ${JSON.stringify(raw)}: glob syntax vx cannot take — task runs uncached; ` +
|
|
2081
|
-
'declare the exact outputs in a vx.config to cache it',
|
|
2082
|
-
)
|
|
2083
|
-
return { name, todos, task, uses }
|
|
2011
|
+
const neg = i.startsWith('!')
|
|
2012
|
+
// Turbo's globs have character classes and extglobs; a vx bracket is
|
|
2013
|
+
// a literal, so `src/**/*.[jt]s` keyed on nothing and an edit
|
|
2014
|
+
// replayed the old build (item 1031). A positive glob with no safe
|
|
2015
|
+
// form widens to every package file; a negation with none is dropped
|
|
2016
|
+
// (it would exclude fewer files, never more).
|
|
2017
|
+
const wax = minimatchToVx(neg ? i.slice(1) : i, neg)
|
|
2018
|
+
if (wax === null) {
|
|
2019
|
+
todos.push(`input ${JSON.stringify(i)}: glob syntax vx cannot take — map manually`)
|
|
2020
|
+
if (!neg) files.push('**/*')
|
|
2021
|
+
continue
|
|
2084
2022
|
}
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2023
|
+
const translated = (neg ? '!' : '') + wax
|
|
2024
|
+
const body = wax
|
|
2025
|
+
const up = climbed(translated)
|
|
2026
|
+
if (!neg && isDotenvGlob(body)) {
|
|
2027
|
+
if (body.startsWith('$TURBO_ROOT$/')) wsDotenv.push(body.slice('$TURBO_ROOT$/'.length))
|
|
2028
|
+
else if (up !== null) wsDotenv.push(up)
|
|
2029
|
+
else {
|
|
2030
|
+
pkgDotenv = true
|
|
2031
|
+
if (body.includes('/')) pkgDotenvDeep = true
|
|
2032
|
+
}
|
|
2033
|
+
continue
|
|
2096
2034
|
}
|
|
2097
|
-
if (
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
} else if (
|
|
2102
|
-
|
|
2103
|
-
|
|
2035
|
+
if (body.startsWith('$TURBO_ROOT$/')) {
|
|
2036
|
+
wsFiles.push(...withZeroSegments((neg ? '!' : '') + body.slice('$TURBO_ROOT$/'.length)))
|
|
2037
|
+
const rel = body.slice('$TURBO_ROOT$/'.length)
|
|
2038
|
+
if (!neg) rels.set(rel, ['workspaceFiles', rel])
|
|
2039
|
+
} else if (up !== null) {
|
|
2040
|
+
wsFiles.push(...withZeroSegments(up))
|
|
2041
|
+
if (!neg) rels.set(up, ['workspaceFiles', up])
|
|
2042
|
+
} else if (body.startsWith('../')) {
|
|
2043
|
+
todos.push(`input ${JSON.stringify(i)}: leaves the workspace — map manually`)
|
|
2044
|
+
} else if (i.includes('$TURBO_ROOT$')) {
|
|
2104
2045
|
todos.push(
|
|
2105
|
-
`
|
|
2106
|
-
'prefix (→
|
|
2046
|
+
`input ${JSON.stringify(i)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
2047
|
+
'prefix (→ inputs.workspaceFiles) — map manually',
|
|
2107
2048
|
)
|
|
2108
|
-
} else
|
|
2049
|
+
} else {
|
|
2050
|
+
// Unkeyed, a root file edit `*\/**` reaches in Turbo replayed a stale hit.
|
|
2051
|
+
files.push(...withZeroSegments(translated))
|
|
2052
|
+
const rel = path.posix.join(atRoot ? '.' : pkgDir, body)
|
|
2053
|
+
if (!neg) rels.set(rel, [atRoot ? 'workspaceFiles' : 'files', translated])
|
|
2054
|
+
}
|
|
2109
2055
|
}
|
|
2110
|
-
|
|
2111
|
-
//
|
|
2112
|
-
//
|
|
2113
|
-
//
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
outFiles.filter((g) => !zeroed.has(g)),
|
|
2117
|
-
opts.tracked?.(pkgDir),
|
|
2118
|
-
true,
|
|
2119
|
-
)
|
|
2120
|
-
if (wild !== undefined && opts.tracked === undefined) {
|
|
2121
|
-
todos.push(wildcardTodo(wild))
|
|
2122
|
-
return { name, todos, task, uses }
|
|
2056
|
+
// Exclusions alone narrow every package file: core refuses a list
|
|
2057
|
+
// with nothing to narrow, and that refusal failed the whole run
|
|
2058
|
+
// (item 936). Every file is the widest reading, so it can cost a
|
|
2059
|
+
// hit and never serve a stale one.
|
|
2060
|
+
if (files.length > 0 && files.every((f) => typeof f === 'string' && f.startsWith('!'))) {
|
|
2061
|
+
files.unshift('**/*')
|
|
2123
2062
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2063
|
+
}
|
|
2064
|
+
// Turbo 1's task `dotEnv`: package-relative `.env` files it hashes,
|
|
2065
|
+
// gitignored as a rule, so read as files they keyed nothing (item 1032).
|
|
2066
|
+
const dotEnv = (def as { dotEnv?: unknown }).dotEnv
|
|
2067
|
+
if (Array.isArray(dotEnv) && dotEnv.some((f) => typeof f === 'string')) pkgDotenv = true
|
|
2068
|
+
|
|
2069
|
+
const keyEnv = uniq([...global('env'), ...envNames], hidden('env'))
|
|
2070
|
+
|
|
2071
|
+
// Turbo hashes a root task over the whole repo; core stops a root
|
|
2072
|
+
// project's own globs at every member (D-39), so as `files` a root
|
|
2073
|
+
// `oxlint` over the repo replayed green after a member file broke.
|
|
2074
|
+
// A root task's globs are the workspace's, outputs too.
|
|
2075
|
+
if (atRoot) wsFiles.unshift(...files.splice(0))
|
|
2076
|
+
const inputs: Record<string, unknown> = { files: withoutTakenBack(files) }
|
|
2077
|
+
if (wsFiles.length > 0) inputs.workspaceFiles = withoutTakenBack(uniq(wsFiles, hidden('inputs')))
|
|
2078
|
+
if (keyEnv.length > 0) inputs.env = keyEnv
|
|
2079
|
+
if (pkgDotenv) inputs.runtime = [pkgDotenvDeep ? DOTENV_PROBE : DOTENV_PROBE_TOP]
|
|
2080
|
+
if (wsDotenv.length > 0) inputs.workspaceRuntime = [dotenvGlobsProbe(wsDotenv) ?? DOTENV_PROBE]
|
|
2081
|
+
for (const rel of rels.keys()) if (!isLiteral(rel)) rels.delete(rel)
|
|
2082
|
+
literals.tasks.push({ inputs, rels })
|
|
2083
|
+
// Turbo keys every task on its inputs, cached or not, and a dependant
|
|
2084
|
+
// folds that key: an uncached task's inputs still move its dependants.
|
|
2085
|
+
task.inputs = inputs
|
|
2086
|
+
if (!cacheEnabled) return { name, todos, task, uses }
|
|
2087
|
+
|
|
2088
|
+
const outFiles: string[] = []
|
|
2089
|
+
const zeroed = new Set<string>()
|
|
2090
|
+
const wsOutFiles: string[] = []
|
|
2091
|
+
for (const raw of def.outputs ?? []) {
|
|
2092
|
+
// The first segment stays a literal (a route directory, item 667);
|
|
2093
|
+
// the rest is Turbo's grammar: `dist/**/*.[cm]js` matched nothing, so
|
|
2094
|
+
// a hit restored nothing (item 1031).
|
|
2095
|
+
const cut = raw.indexOf('/')
|
|
2096
|
+
const rest = cut < 0 ? null : minimatchToVx(raw.slice(cut + 1), raw.startsWith('!'))
|
|
2097
|
+
if (cut >= 0 && rest === null) {
|
|
2098
|
+
todos.push(
|
|
2099
|
+
`output ${JSON.stringify(raw)}: glob syntax vx cannot take — task runs uncached; ` +
|
|
2100
|
+
'declare the exact outputs in a vx.config to cache it',
|
|
2101
|
+
)
|
|
2102
|
+
return { name, todos, task, uses }
|
|
2142
2103
|
}
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
task.cache = {
|
|
2155
|
-
inputs,
|
|
2156
|
-
outputs,
|
|
2157
|
-
...(rewritesManifest ? { inputStability: { strategy: 'mutable' } } : {}),
|
|
2104
|
+
const mapped = cut < 0 ? raw : `${raw.slice(0, cut + 1)}${rest}`
|
|
2105
|
+
// A `!` output takes a path back, as Turbo's does (A-44): Next's
|
|
2106
|
+
// `.next/**` minus `!.next/cache/**` saved the cache and cleaned it
|
|
2107
|
+
// before every run while vx outputs could not exclude.
|
|
2108
|
+
const neg = mapped.startsWith('!') ? '!' : ''
|
|
2109
|
+
const o = mapped.slice(neg.length)
|
|
2110
|
+
const signed = (g: string) => {
|
|
2111
|
+
const head = zeroSegments(g)
|
|
2112
|
+
if (head === null) return [neg + g]
|
|
2113
|
+
zeroed.add(neg + head)
|
|
2114
|
+
return [neg + g, neg + head]
|
|
2158
2115
|
}
|
|
2116
|
+
if (o.startsWith('$TURBO_ROOT$/')) {
|
|
2117
|
+
wsOutFiles.push(...signed(o.slice('$TURBO_ROOT$/'.length)))
|
|
2118
|
+
} else if (climbed(o) !== null) {
|
|
2119
|
+
wsOutFiles.push(...signed(climbed(o)!))
|
|
2120
|
+
} else if (o.startsWith('../')) {
|
|
2121
|
+
todos.push(`output ${JSON.stringify(o)}: leaves the workspace — map manually`)
|
|
2122
|
+
} else if (o.includes('$TURBO_ROOT$')) {
|
|
2123
|
+
todos.push(
|
|
2124
|
+
`output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
2125
|
+
'prefix (→ cache.outputs.workspaceFiles) — map manually',
|
|
2126
|
+
)
|
|
2127
|
+
} else outFiles.push(...signed(o))
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// With git's file list known, a wildcard-first output stays cached as
|
|
2131
|
+
// nx() keeps it (owner, 2026-10-03; turbo() 2026-10-14): core never
|
|
2132
|
+
// cleans a committed file. Turbo never cleans, so the untracked files
|
|
2133
|
+
// it reaches are named. An X added for `X/**` is judged with it.
|
|
2134
|
+
const wild = wildcardOutput(
|
|
2135
|
+
outFiles.filter((g) => !zeroed.has(g)),
|
|
2136
|
+
opts.tracked?.(pkgDir),
|
|
2137
|
+
true,
|
|
2138
|
+
)
|
|
2139
|
+
if (wild !== undefined && opts.tracked === undefined) {
|
|
2140
|
+
todos.push(wildcardTodo(wild))
|
|
2141
|
+
return { name, todos, task, uses }
|
|
2142
|
+
}
|
|
2143
|
+
if (wild !== undefined) todos.push(wildcardCleanTodo(wild))
|
|
2144
|
+
// An output over the project's own package.json (trpc's builds list
|
|
2145
|
+
// it: tsdown rewrites `exports`) is taken back and keeps the cache, as
|
|
2146
|
+
// nx() does (owner, 2026-10-03). The build rewrites an input the key
|
|
2147
|
+
// folds, so its identical rewrite counts as unchanged ('mutable').
|
|
2148
|
+
const takeBacks = ownFileTakeBacks(outFiles, opts.ownConfig?.(pkgDir))
|
|
2149
|
+
outFiles.push(...takeBacks)
|
|
2150
|
+
const rewritesManifest = takeBacks.includes('!package.json')
|
|
2151
|
+
|
|
2152
|
+
if (atRoot) wsOutFiles.unshift(...outFiles.splice(0))
|
|
2153
|
+
const outputs: Record<string, unknown> = { files: takingBack(outFiles) }
|
|
2154
|
+
const ws = takingBack(wsOutFiles)
|
|
2155
|
+
if (ws.length > 0) outputs.workspaceFiles = ws
|
|
2156
|
+
task.cache = {
|
|
2157
|
+
outputs,
|
|
2158
|
+
...(rewritesManifest ? { inputStability: { strategy: 'mutable' } } : {}),
|
|
2159
2159
|
}
|
|
2160
2160
|
|
|
2161
2161
|
return { name, todos, task, uses }
|