@vzn/vx 0.0.153 → 0.0.155

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.153",
3
+ "version": "0.0.155",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.153",
21
- "@vzn/vx-linux-arm64": "0.0.153",
22
- "@vzn/vx-darwin-x64": "0.0.153",
23
- "@vzn/vx-darwin-arm64": "0.0.153"
20
+ "@vzn/vx-linux-x64": "0.0.155",
21
+ "@vzn/vx-linux-arm64": "0.0.155",
22
+ "@vzn/vx-darwin-x64": "0.0.155",
23
+ "@vzn/vx-darwin-arm64": "0.0.155"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -52,7 +52,7 @@ export function isIgnoredWatchPath(rel: string): boolean {
52
52
  export function makeWatchIgnore(
53
53
  cacheDir: string,
54
54
  outputs: ReadonlyMap<string, readonly string[]> = new Map(),
55
- inputs: ReadonlyMap<string, readonly string[]> = new Map(),
55
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>> = new Map(),
56
56
  ): (base: string, filename: string) => boolean {
57
57
  const cacheAbs = path.resolve(cacheDir)
58
58
  // A task's own outputs are not edits: without this every cycle that
@@ -82,23 +82,37 @@ export function makeWatchIgnore(
82
82
  // A path some task takes as an INPUT is never an output to ignore, even
83
83
  // when another task declares it one: an in-place formatter declaring
84
84
  // `src/**` hid every `src` edit from a `build` watched beside it, and no
85
- // cycle ran (item 946). Negations are not consulted — they only narrow,
86
- // and a path they would exclude costs one cache-hit cycle.
87
- const read = [...inputs].map(
88
- ([dir, globs]) =>
89
- [
90
- path.resolve(dir),
91
- asTrees(globs.filter((g) => !g.startsWith('!'))).map((g) => taskGlob(g)),
92
- ] as const,
85
+ // cycle ran (item 946). Each task's inputs are judged on their own, less
86
+ // their `!` entries (its own outputs among them): a turbo() task reading
87
+ // `**/*` took its own `dist/` write for an edit, and every save ran one
88
+ // more "up-to-date" cycle (2026-09-28).
89
+ const read = [...inputs].flatMap(([dir, tasks]) =>
90
+ tasks.map(
91
+ (globs) =>
92
+ [
93
+ path.resolve(dir),
94
+ asTrees(globs.filter((g) => !g.startsWith('!'))).map((g) => taskGlob(g)),
95
+ asTrees(globs.filter((g) => g.startsWith('!')).map((g) => g.slice(1))).map((g) =>
96
+ taskGlob(g),
97
+ ),
98
+ // The directory an own output lives in is the task's too (see below).
99
+ globs
100
+ .filter((g) => g.startsWith('!'))
101
+ .map((g) => outputContainer(g.slice(1)))
102
+ .filter((c) => c !== ''),
103
+ ] as const,
104
+ ),
93
105
  )
94
106
  const isInput = (abs: string): boolean =>
95
- read.some(([dir, globs]) => {
107
+ read.some(([dir, globs, not, containers]) => {
96
108
  if (!abs.startsWith(dir + path.sep)) return false
97
109
  const rel = abs
98
110
  .slice(dir.length + 1)
99
111
  .split(path.sep)
100
112
  .join('/')
101
- return globs.some((g) => g.match(rel))
113
+ if (!globs.some((g) => g.match(rel))) return false
114
+ if (not.some((g) => g.match(rel))) return false
115
+ return !containers.some((c) => c === rel || c.startsWith(`${rel}/`))
102
116
  })
103
117
  return (base, filename) => {
104
118
  if (isIgnoredWatchPath(filename)) return true
@@ -56,8 +56,12 @@ export interface ConfigSweep {
56
56
  workspaceWide: boolean
57
57
  workspaceInputs: string[]
58
58
  outputs: Map<string, string[]>
59
- /** Declared input globs per directory they are relative to, the counterweight to `outputs`. */
60
- inputs: Map<string, string[]>
59
+ /**
60
+ * Each task's input globs per directory they are relative to, the
61
+ * counterweight to `outputs`; the task's own outputs ride along as `!`
62
+ * entries, as its key leaves them out.
63
+ */
64
+ inputs: Map<string, string[][]>
61
65
  /** Projects with a task that has a command and no cache: it reads what it likes, git-ignored files included. */
62
66
  uncached: Set<string>
63
67
  /** Files the project configs import by relative path (item 949). */
@@ -86,7 +90,19 @@ export async function sweepConfigs(
86
90
  load: CliLoadOptions = {},
87
91
  ): Promise<ConfigSweep> {
88
92
  const outputs = new Map<string, string[]>()
89
- const inputs = new Map<string, string[]>()
93
+ const inputs = new Map<string, string[][]>()
94
+ // One task's inputs, less what it writes itself: the key leaves a task's
95
+ // own outputs out of its inputs, so a turbo() task reading `**/*` does not
96
+ // read the `dist/` it builds, and its write is no edit to watch.
97
+ const addInputs = (
98
+ dir: string,
99
+ files: readonly string[] | undefined,
100
+ own: readonly string[] | undefined,
101
+ ): void => {
102
+ if (files === undefined || files.length === 0) return
103
+ const minus = (own ?? []).filter((o) => !o.startsWith('!')).map((o) => `!${o}`)
104
+ inputs.set(dir, [...(inputs.get(dir) ?? []), [...files, ...minus]])
105
+ }
90
106
  const addTo = (
91
107
  into: Map<string, string[]>,
92
108
  dir: string,
@@ -104,8 +120,12 @@ export async function sweepConfigs(
104
120
  if (task.cache === undefined && task.exec !== undefined && task.exec.persistent === undefined)
105
121
  uncached.add(dir)
106
122
  for (const g of task.cache?.inputs?.workspaceFiles ?? []) workspaceInputs.add(g)
107
- addTo(inputs, dir, task.cache?.inputs?.files)
108
- addTo(inputs, workspaceRoot, task.cache?.inputs?.workspaceFiles)
123
+ addInputs(dir, task.cache?.inputs?.files, task.cache?.outputs?.files)
124
+ addInputs(
125
+ workspaceRoot,
126
+ task.cache?.inputs?.workspaceFiles,
127
+ task.cache?.outputs?.workspaceFiles,
128
+ )
109
129
  add(dir, task.cache?.outputs?.files)
110
130
  add(workspaceRoot, task.cache?.outputs?.workspaceFiles)
111
131
  }
package/src/cli/watch.ts CHANGED
@@ -272,7 +272,7 @@ interface WatchLoopArgs {
272
272
  /** Declared output globs per directory they are relative to (project dir, or the root for `workspaceFiles`). */
273
273
  outputs: ReadonlyMap<string, readonly string[]>
274
274
  /** Declared input globs per directory: never ignored as another task's output. */
275
- inputs: ReadonlyMap<string, readonly string[]>
275
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>>
276
276
  /** Projects whose uncached task may read a git-ignored file: such a path there is still an edit. */
277
277
  uncached: ReadonlySet<string>
278
278
  /** Files the configs import: an edit to one outside the watched projects is a cycle that re-reads them. */
@@ -299,7 +299,7 @@ interface Rediscovered {
299
299
  workspaceWide: boolean
300
300
  workspaceInputs: readonly string[]
301
301
  outputs: ReadonlyMap<string, readonly string[]>
302
- inputs: ReadonlyMap<string, readonly string[]>
302
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>>
303
303
  uncached: ReadonlySet<string>
304
304
  configImports: readonly string[]
305
305
  workspaceConfigImports: readonly string[]
@@ -3,7 +3,7 @@
3
3
  // ownership moves to the run's registry, and this is the one place that
4
4
  // decides which of them outlive the graph and how the rest go down.
5
5
 
6
- import type { TaskNode } from '../graph/index.js'
6
+ import { isGroupTask, type TaskNode } from '../graph/index.js'
7
7
  import { killGraceMs } from '../util/index.js'
8
8
  import { holdGroups, killTree, untilGroupsGone } from '../exec/index.js'
9
9
 
@@ -25,9 +25,12 @@ export interface KeepAlive {
25
25
  /**
26
26
  * A persistent task the user REQUESTED (or one surfaced for display) is the
27
27
  * run's whole purpose — it is left running and blocked on at the very end,
28
- * after the summary. Only in the real CLI foreground: a custom logger or
29
- * `handleSignals: false` (watch mode, embedders) expects `run()` to return,
30
- * not to sit on a server.
28
+ * after the summary. So is every persistent task a kept one depends on,
29
+ * directly or through groups: `vx run dev --filter app` stopped the api#dev
30
+ * its app#dev was started against (C-46). A one-shot's persistent deps are
31
+ * not kept on its account; it has finished with them. Only in the real CLI
32
+ * foreground: a custom logger or `handleSignals: false` (watch mode,
33
+ * embedders) expects `run()` to return, not to sit on a server.
31
34
  */
32
35
  export function selectKeepAlive(
33
36
  registry: ReadonlyMap<string, Child>,
@@ -36,13 +39,25 @@ export function selectKeepAlive(
36
39
  ): KeepAlive {
37
40
  const out: KeepAlive = { nodes: [], children: [] }
38
41
  if (!foreground) return out
39
- for (const [id, child] of registry) {
42
+ const stack = [...registry.keys()].filter((id) => {
40
43
  const n = nodes.get(id)
41
- if (n !== undefined && (n.requested || n.surfaced === true)) {
42
- out.nodes.push(n)
43
- out.children.push(child)
44
+ return n !== undefined && (n.requested || n.surfaced === true)
45
+ })
46
+ const reached = new Set<string>()
47
+ while (stack.length > 0) {
48
+ const id = stack.pop()!
49
+ if (reached.has(id)) continue
50
+ reached.add(id)
51
+ for (const dep of nodes.get(id)?.deps ?? []) {
52
+ const d = nodes.get(dep)
53
+ if (d !== undefined && (registry.has(dep) || isGroupTask(d))) stack.push(dep)
44
54
  }
45
55
  }
56
+ for (const [id, child] of registry) {
57
+ if (!reached.has(id)) continue
58
+ out.nodes.push(nodes.get(id)!)
59
+ out.children.push(child)
60
+ }
46
61
  return out
47
62
  }
48
63
 
@@ -20,6 +20,7 @@
20
20
  // that implements neither still prefetches, per-hash.
21
21
 
22
22
  import type { TaskNode } from '../graph/index.js'
23
+ import { maskedCommand } from '../util/index.js'
23
24
  import type { CacheLayer, GitFilesCache } from '../cache/index.js'
24
25
  import type { HashCache } from './task-hash.js'
25
26
  import { deriveStableKeys } from './stable-keys.js'
@@ -114,7 +115,9 @@ export function getContext(node: TaskNode, command: string) {
114
115
  const outputs = node.config.cache?.outputs
115
116
  return {
116
117
  taskId: node.id,
117
- command,
118
+ // Stored in the entry row a remote hit inserts: masked as a save's is,
119
+ // or the token a config inlined sat in cache.db (L-27).
120
+ command: maskedCommand(command, node.config.exec?.env),
118
121
  outputs: { files: outputs?.files ?? [], workspaceFiles: outputs?.workspaceFiles ?? [] },
119
122
  }
120
123
  }