@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.
@@ -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 `cache.inputs.env` and `exec.env.passThrough`. */
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 cache.inputs.env)',
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 cache.inputs.env)',
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 cache.inputs.env and exec.env.passThrough',
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 cache.inputs.env and exec.env.passThrough',
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 = (t.task?.['cache'] as { inputs?: Record<string, unknown> } | undefined)?.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
- delete t.task!['cache']
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 cache.inputs.env and exec.env.passThrough',
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 = (t.task?.['cache'] as { inputs?: Record<string, unknown> } | undefined)?.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 (cache === undefined && task['dependsOn'] === undefined) return t
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 cache.inputs.env + exec.env.passThrough',
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
- if (cacheEnabled) {
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
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
- // 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 outFiles: string[] = []
2070
- const zeroed = new Set<string>()
2071
- const wsOutFiles: string[] = []
2072
- for (const raw of def.outputs ?? []) {
2073
- // The first segment stays a literal (a route directory, item 667);
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 mapped = cut < 0 ? raw : `${raw.slice(0, cut + 1)}${rest}`
2086
- // A `!` output takes a path back, as Turbo's does (A-44): Next's
2087
- // `.next/**` minus `!.next/cache/**` saved the cache and cleaned it
2088
- // before every run while vx outputs could not exclude.
2089
- const neg = mapped.startsWith('!') ? '!' : ''
2090
- const o = mapped.slice(neg.length)
2091
- const signed = (g: string) => {
2092
- const head = zeroSegments(g)
2093
- if (head === null) return [neg + g]
2094
- zeroed.add(neg + head)
2095
- return [neg + g, neg + head]
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 (o.startsWith('$TURBO_ROOT$/')) {
2098
- wsOutFiles.push(...signed(o.slice('$TURBO_ROOT$/'.length)))
2099
- } else if (climbed(o) !== null) {
2100
- wsOutFiles.push(...signed(climbed(o)!))
2101
- } else if (o.startsWith('../')) {
2102
- todos.push(`output ${JSON.stringify(o)}: leaves the workspace — map manually`)
2103
- } else if (o.includes('$TURBO_ROOT$')) {
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
- `output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
2106
- 'prefix (→ cache.outputs.workspaceFiles) — map manually',
2046
+ `input ${JSON.stringify(i)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
2047
+ 'prefix (→ inputs.workspaceFiles) — map manually',
2107
2048
  )
2108
- } else outFiles.push(...signed(o))
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
- // With git's file list known, a wildcard-first output stays cached as
2112
- // nx() keeps it (owner, 2026-10-03; turbo() 2026-10-14): core never
2113
- // cleans a committed file. Turbo never cleans, so the untracked files
2114
- // it reaches are named. An X added for `X/**` is judged with it.
2115
- const wild = wildcardOutput(
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
- if (wild !== undefined) todos.push(wildcardCleanTodo(wild))
2125
- // An output over the project's own package.json (trpc's builds list
2126
- // it: tsdown rewrites `exports`) is taken back and keeps the cache, as
2127
- // nx() does (owner, 2026-10-03). The build rewrites an input the key
2128
- // folds, so its identical rewrite counts as unchanged ('mutable').
2129
- const takeBacks = ownFileTakeBacks(outFiles, opts.ownConfig?.(pkgDir))
2130
- outFiles.push(...takeBacks)
2131
- const rewritesManifest = takeBacks.includes('!package.json')
2132
-
2133
- const cacheEnv = uniq([...global('env'), ...envNames], hidden('env'))
2134
-
2135
- // Turbo hashes a root task over the whole repo; core stops a root
2136
- // project's own globs at every member (D-39), so as `files` a root
2137
- // `oxlint` over the repo replayed green after a member file broke.
2138
- // A root task's globs are the workspace's, outputs too.
2139
- if (pkgDir === '' || pkgDir === '.') {
2140
- wsFiles.unshift(...files.splice(0))
2141
- wsOutFiles.unshift(...outFiles.splice(0))
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 inputs: Record<string, unknown> = { files: withoutTakenBack(files) }
2144
- if (wsFiles.length > 0)
2145
- inputs.workspaceFiles = withoutTakenBack(uniq(wsFiles, hidden('inputs')))
2146
- if (cacheEnv.length > 0) inputs.env = cacheEnv
2147
- if (pkgDotenv) inputs.runtime = [pkgDotenvDeep ? DOTENV_PROBE : DOTENV_PROBE_TOP]
2148
- if (wsDotenv.length > 0) inputs.workspaceRuntime = [dotenvGlobsProbe(wsDotenv) ?? DOTENV_PROBE]
2149
- for (const rel of rels.keys()) if (!isLiteral(rel)) rels.delete(rel)
2150
- literals.tasks.push({ inputs, rels })
2151
- const outputs: Record<string, unknown> = { files: takingBack(outFiles) }
2152
- const ws = takingBack(wsOutFiles)
2153
- if (ws.length > 0) outputs.workspaceFiles = ws
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 }
@@ -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
- const remote = new TurboRemoteCache(config, fetch, ctx.cacheDir)
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
- 'cache.inputs.env and exec.env.passThrough',
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
- `\`cache: { inputs: { files: [...] }, outputs: { files: [...] } }\` with the real ones${have}; ` +
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
- const inputLists: Record<string, unknown> = { ...inputs.lists }
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 cache.inputs.env' : ''}`,
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
  }