@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/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 }
|
package/src/turbo-cache/index.ts
CHANGED
|
@@ -393,8 +393,6 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
393
393
|
private readonly absent = new Set<string>()
|
|
394
394
|
private readonly key: Uint8Array | undefined
|
|
395
395
|
readonly endpoint: string
|
|
396
|
-
/** What `TURBO_REMOTE_CACHE_READ_ONLY` and friends leave this run (`turboRemoteAccess`). */
|
|
397
|
-
access?: { read: boolean; write: boolean }
|
|
398
396
|
constructor(
|
|
399
397
|
private readonly config: TurboCacheConfig,
|
|
400
398
|
private readonly fetchImpl: typeof fetch = fetch,
|
|
@@ -653,37 +651,6 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
653
651
|
}
|
|
654
652
|
}
|
|
655
653
|
|
|
656
|
-
/**
|
|
657
|
-
* What Turbo's own switches leave of the remote cache: `TURBO_CACHE`
|
|
658
|
-
* (`--cache-policy`'s syntax, where an omitted source is off) and
|
|
659
|
-
* `TURBO_REMOTE_CACHE_READ_ONLY`. A CI that sets either for an untrusted
|
|
660
|
-
* pull request kept Turbo from writing the shared cache and not vx. They
|
|
661
|
-
* only narrow what `--cache-policy` allows, never widen it.
|
|
662
|
-
*/
|
|
663
|
-
export function turboRemoteAccess(env: Record<string, string | undefined>): {
|
|
664
|
-
read: boolean
|
|
665
|
-
write: boolean
|
|
666
|
-
} {
|
|
667
|
-
let read = true
|
|
668
|
-
let write = true
|
|
669
|
-
const spec = env['TURBO_CACHE']
|
|
670
|
-
if (spec !== undefined && spec !== '') {
|
|
671
|
-
read = write = false
|
|
672
|
-
for (const part of spec.split(',')) {
|
|
673
|
-
const m = /^(local|remote):([rw]*)$/.exec(part.trim())
|
|
674
|
-
if (m === null)
|
|
675
|
-
throw new Error(
|
|
676
|
-
`vx/turbo-cache: TURBO_CACHE ${JSON.stringify(spec)} is not --cache-policy's syntax (e.g. local:rw,remote:r)`,
|
|
677
|
-
)
|
|
678
|
-
if (m[1] !== 'remote') continue
|
|
679
|
-
read = m[2]!.includes('r')
|
|
680
|
-
write = m[2]!.includes('w')
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
if (/^(1|true)$/i.test(env['TURBO_REMOTE_CACHE_READ_ONLY'] ?? '')) write = false
|
|
684
|
-
return { read, write }
|
|
685
|
-
}
|
|
686
|
-
|
|
687
654
|
/**
|
|
688
655
|
* The repo's `.turbo/config.json`, by Turbo's field names and their
|
|
689
656
|
* aliases (`apiurl`, `ApiUrl`, `APIURL`, …); `{}` when absent or unreadable.
|
|
@@ -757,14 +724,10 @@ export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
|
|
|
757
724
|
localConfigOf(ctx.workspaceRoot),
|
|
758
725
|
)
|
|
759
726
|
if (config === undefined) return undefined
|
|
760
|
-
const access = turboRemoteAccess(Bun.env)
|
|
761
|
-
if (!access.read && !access.write) return undefined
|
|
762
727
|
// A signed download waits in a temp until its tag checks: in vx's
|
|
763
728
|
// cache directory, which the sandbox walls, not the shared temp dir a
|
|
764
729
|
// sandboxed task may read (L-45).
|
|
765
|
-
|
|
766
|
-
remote.access = access
|
|
767
|
-
return remote
|
|
730
|
+
return new TurboRemoteCache(config, fetch, ctx.cacheDir)
|
|
768
731
|
},
|
|
769
732
|
})
|
|
770
733
|
}
|
|
@@ -382,7 +382,7 @@ export async function mapViteTaskWorkspace(
|
|
|
382
382
|
([e, n]) =>
|
|
383
383
|
`env ${JSON.stringify(e)}: the configs list the names the files of each task's package and its ` +
|
|
384
384
|
`workspace dependencies spell (${n} task${n === 1 ? '' : 's'}) — add any other a task reads to ` +
|
|
385
|
-
'
|
|
385
|
+
'inputs.env and exec.env.passThrough',
|
|
386
386
|
)
|
|
387
387
|
return { projects, notes }
|
|
388
388
|
}
|
|
@@ -392,7 +392,7 @@ function autoTodo(what: string, known?: Record<string, unknown>): string {
|
|
|
392
392
|
const have = known === undefined ? '' : ` (declared so far: \`${JSON.stringify(known)}\`)`
|
|
393
393
|
return (
|
|
394
394
|
`cache: Vite Task traced this task's ${what} and vx infers none — add ` +
|
|
395
|
-
`\`
|
|
395
|
+
`\`inputs: { files: [...] }, cache: { outputs: { files: [...] } }\` with the real ones${have}; ` +
|
|
396
396
|
'without it the task always runs'
|
|
397
397
|
)
|
|
398
398
|
}
|
|
@@ -460,6 +460,10 @@ function mapTask(name: string, def: TaskDef, ctx: TaskCtx): GeneratedTask {
|
|
|
460
460
|
todos.push(autoTodo(what, Object.keys(known).length > 0 ? known : undefined))
|
|
461
461
|
return { name, todos, task }
|
|
462
462
|
}
|
|
463
|
+
// Declared inputs key the task cached or not: a dependant folds them.
|
|
464
|
+
const inputLists: Record<string, unknown> = { ...inputs.lists }
|
|
465
|
+
if (env.length > 0) inputLists['env'] = env
|
|
466
|
+
task['inputs'] = inputLists
|
|
463
467
|
if (outputs.uncached) return { name, todos, task }
|
|
464
468
|
|
|
465
469
|
const outFiles = outputs.lists['files'] as string[]
|
|
@@ -473,9 +477,7 @@ function mapTask(name: string, def: TaskDef, ctx: TaskCtx): GeneratedTask {
|
|
|
473
477
|
todos.push(ownFileTodo(ownFile))
|
|
474
478
|
return { name, todos, task }
|
|
475
479
|
}
|
|
476
|
-
|
|
477
|
-
if (env.length > 0) inputLists['env'] = env
|
|
478
|
-
task['cache'] = { inputs: inputLists, outputs: outputs.lists }
|
|
480
|
+
task['cache'] = { outputs: outputs.lists }
|
|
479
481
|
return { name, todos, task }
|
|
480
482
|
}
|
|
481
483
|
|
|
@@ -510,7 +512,7 @@ function envNames(
|
|
|
510
512
|
}
|
|
511
513
|
if (/[*?[\]{}=\0]/.test(e) || e === '') {
|
|
512
514
|
todos.push(
|
|
513
|
-
`${field} ${JSON.stringify(e)}: vx takes exact env names — list the ones the task reads in exec.env.passThrough${field === 'cache.env' ? ' and
|
|
515
|
+
`${field} ${JSON.stringify(e)}: vx takes exact env names — list the ones the task reads in exec.env.passThrough${field === 'cache.env' ? ' and inputs.env' : ''}`,
|
|
514
516
|
)
|
|
515
517
|
continue
|
|
516
518
|
}
|