@vzn/vx 0.0.580 → 0.0.582

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.580",
3
+ "version": "0.0.582",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.580",
25
- "@vzn/vx-linux-arm64": "0.0.580",
26
- "@vzn/vx-darwin-x64": "0.0.580",
27
- "@vzn/vx-darwin-arm64": "0.0.580"
24
+ "@vzn/vx-linux-x64": "0.0.582",
25
+ "@vzn/vx-linux-arm64": "0.0.582",
26
+ "@vzn/vx-darwin-x64": "0.0.582",
27
+ "@vzn/vx-darwin-arm64": "0.0.582"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/lock.ts CHANGED
@@ -90,8 +90,8 @@ async function writeLock(root: string, metas: ConfiguredMeta[], bare: number): P
90
90
  if (leaks.length > 0) {
91
91
  process.stderr.write(
92
92
  `vx lock: ${LOCKFILE_NAME} is committed, and these configs evaluated to a secret value:\n` +
93
- leaks.map((l) => ` ${l}\n`).join('') +
94
- `let the shell expand it ($API_TOKEN in the command, the name in exec.env.passThrough) instead of reading process.env in the config\n`,
93
+ leaks.map((l) => ` ${l.at}\n`).join('') +
94
+ `let the shell expand it ($${leaks[0]!.name} in the command, the name in exec.env.passThrough) instead of reading process.env in the config\n`,
95
95
  )
96
96
  return 1
97
97
  }
@@ -111,7 +111,7 @@ async function writeLock(root: string, metas: ConfiguredMeta[], bare: number): P
111
111
  * such a value would be published, and masking it would freeze a `***`
112
112
  * that `--frozen` runs (L-42).
113
113
  */
114
- function secretsIn(projects: Record<string, LockfileEntry>): string[] {
114
+ function secretsIn(projects: Record<string, LockfileEntry>): Array<{ at: string; name: string }> {
115
115
  const named = new Set<string>()
116
116
  for (const entry of Object.values(projects)) {
117
117
  for (const task of Object.values(entry.config.tasks ?? {}))
@@ -123,11 +123,11 @@ function secretsIn(projects: Record<string, LockfileEntry>): string[] {
123
123
  secrets.push([value, name])
124
124
  }
125
125
  if (secrets.length === 0) return []
126
- const found: string[] = []
126
+ const found: Array<{ at: string; name: string }> = []
127
127
  const walk = (project: string, at: string, v: unknown): void => {
128
128
  if (typeof v === 'string') {
129
129
  for (const [value, name] of secrets)
130
- if (v.includes(value)) found.push(`${project}: ${at} holds $${name}`)
130
+ if (v.includes(value)) found.push({ at: `${project}: ${at} holds $${name}`, name })
131
131
  } else if (Array.isArray(v)) {
132
132
  v.forEach((x, i) => walk(project, `${at}[${i}]`, x))
133
133
  } else if (v !== null && typeof v === 'object') {
@@ -553,6 +553,7 @@ export function checkGraph(
553
553
  delete node.addsToOutputsOf
554
554
  delete node.outputsAddedToBy
555
555
  }
556
+ for (const node of nodes.values()) refuseSelfClean(node)
556
557
  detectCycle(nodes)
557
558
  detectOutputCollisions(nodes, workspaceRoot, rules?.exclusiveOutputs !== false)
558
559
  if (rules?.upfrontKeys !== false) detectInputOverlaps(nodes, workspaceRoot)
@@ -1123,6 +1124,44 @@ export function outputTakenBack(output: string, negatives: readonly string[]): b
1123
1124
  )
1124
1125
  }
1125
1126
 
1127
+ /**
1128
+ * A task whose input entry its own outputs take back whole cleans its own
1129
+ * sources: vx removes a task's outputs before it runs, so a formatter
1130
+ * declaring `src/**` as both deleted every committed file under `src`,
1131
+ * and its key read nothing (hunt 8). Always refused, whatever the rules.
1132
+ */
1133
+ function refuseSelfClean(node: TaskNode): void {
1134
+ const cache = node.config.cache
1135
+ if (cache === undefined) return
1136
+ for (const field of ['files', 'workspaceFiles'] as const) {
1137
+ const inputs = splitNegations(cache.inputs[field] ?? [])
1138
+ const out = splitNegations(cache.outputs[field] ?? [])
1139
+ const outputs = out.positive
1140
+ if (outputs.length === 0) continue
1141
+ const roots = outputs.map((g) => rootless(staticPrefix(g)))
1142
+ for (const gi of inputs.positive) {
1143
+ // Only an output rooted at or above the input's prefix can cover it;
1144
+ // the prefix test keeps the glob matchers off the common disjoint pair.
1145
+ const pi = rootless(staticPrefix(gi))
1146
+ const near = outputs.filter((_, k) => {
1147
+ const r = roots[k]!
1148
+ return r === '' || pi === r || pi.startsWith(`${r}/`)
1149
+ })
1150
+ // Taken back by the task's own `!` inputs, or by an output `!` (no
1151
+ // output, so never cleaned: A-44), the entry reads what it says.
1152
+ if (near.length === 0 || outputTakenBack(gi, inputs.negative)) continue
1153
+ if (outputTakenBack(gi, out.negative)) continue
1154
+ if (!outputTakenBack(gi, near)) continue
1155
+ throw new UserError(
1156
+ `${node.id}: every file ${JSON.stringify(gi)} in cache.inputs.${field} selects is also ` +
1157
+ `its own output — vx removes a task's outputs before it runs, so the task would ` +
1158
+ `delete its own sources. A task that rewrites files in place (a formatter) declares ` +
1159
+ `no outputs.`,
1160
+ )
1161
+ }
1162
+ }
1163
+ }
1164
+
1126
1165
  /** One task's inputs (`reads`) or outputs in one namespace, for `detectInputOverlaps`. */
1127
1166
  interface Side {
1128
1167
  node: TaskNode