@vzn/vx 0.0.417 → 0.0.419

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.417",
3
+ "version": "0.0.419",
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.417",
25
- "@vzn/vx-linux-arm64": "0.0.417",
26
- "@vzn/vx-darwin-x64": "0.0.417",
27
- "@vzn/vx-darwin-arm64": "0.0.417"
24
+ "@vzn/vx-linux-x64": "0.0.419",
25
+ "@vzn/vx-linux-arm64": "0.0.419",
26
+ "@vzn/vx-darwin-x64": "0.0.419",
27
+ "@vzn/vx-darwin-arm64": "0.0.419"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -290,3 +290,35 @@ export function isWorkspaceConfigFile(name: string): boolean {
290
290
  * workspace is the one it ignores.
291
291
  */
292
292
  const FINGERPRINT_FILES: ReadonlySet<string> = new Set(WORKSPACE_FINGERPRINT_FILES)
293
+
294
+ /**
295
+ * The files under the workspace git lists, tracked and untracked, ignored
296
+ * ones aside, and every directory above one (git lists no directory, and
297
+ * a project moved away whole is one event on its directory): what existed
298
+ * when watch armed, so a path born and gone since is told from a deletion
299
+ * (watch-judge.ts). Undefined when git cannot answer: no inventory, every
300
+ * gone path is a deletion as before.
301
+ */
302
+ export function gitFiles(workspaceRoot: string): Set<string> | undefined {
303
+ let proc: ReturnType<typeof Bun.spawnSync>
304
+ try {
305
+ proc = Bun.spawnSync({
306
+ cmd: [executablePath('git'), 'ls-files', '-z', '--cached', '--others', '--exclude-standard'],
307
+ cwd: workspaceRoot,
308
+ stdout: 'pipe',
309
+ stderr: 'ignore',
310
+ })
311
+ } catch {
312
+ return undefined
313
+ }
314
+ if (proc.exitCode !== 0) return undefined
315
+ const files = new Set<string>()
316
+ for (const p of new TextDecoder().decode(proc.stdout).split('\0')) {
317
+ if (p.length === 0) continue
318
+ let abs = path.join(workspaceRoot, p)
319
+ files.add(abs)
320
+ for (abs = path.dirname(abs); abs !== workspaceRoot && !files.has(abs); abs = path.dirname(abs))
321
+ files.add(abs)
322
+ }
323
+ return files
324
+ }
@@ -53,6 +53,8 @@ export interface JudgeContext {
53
53
  held(): boolean
54
54
  /** Projects whose uncached task may read a git-ignored file. */
55
55
  uncached(): ReadonlySet<string>
56
+ /** The files git listed at the arm; absent when git could not answer. */
57
+ existedAtArm?: ReadonlySet<string>
56
58
  }
57
59
 
58
60
  export class ChangeJudge {
@@ -108,12 +110,22 @@ export class ChangeJudge {
108
110
  // and a first sighting used to pass unconditionally; the path's mtime
109
111
  // says which side of the arm it belongs to. A path already gone is a
110
112
  // change: a deletion has no date to read.
111
- private sameState(abs: string): boolean {
113
+ // One exception: a path git did not list at the arm, and gone now, was
114
+ // born and removed since (vim's `4913` write probe, a tool's lock file);
115
+ // it started a cycle with nothing changed. The blind spot: one born in
116
+ // the moment between a judgement and that cycle's keys, and gone by the
117
+ // next judgement, was read and its deletion re-runs nothing. Git lists
118
+ // no ignored path, so one of those is a deletion as before.
119
+ private sameState(abs: string, ignored = false): boolean {
112
120
  const state = settledState(abs)
113
121
  const prev = this.lastState.get(abs)
114
122
  this.lastState.set(abs, state)
115
123
  if (prev !== undefined) return prev === state
116
- return state !== ABSENT && modifiedBefore(abs, this.ctx.armedAt)
124
+ if (state === ABSENT) {
125
+ const listed = this.ctx.existedAtArm
126
+ return !ignored && listed !== undefined && !listed.has(abs)
127
+ }
128
+ return modifiedBefore(abs, this.ctx.armedAt)
117
129
  }
118
130
 
119
131
  private writtenDuringLastCycle(abs: string, openWhileHeld = false): boolean {
@@ -158,7 +170,7 @@ export class ChangeJudge {
158
170
  let gone: [label: string, abs: string] | undefined
159
171
  for (const [p, l] of this.pending) {
160
172
  if (ignored.has(p) && !editForUncached(p)) continue
161
- if (this.sameState(p) || first !== undefined) continue
173
+ if (this.sameState(p, ignored.has(p)) || first !== undefined) continue
162
174
  if (!fs.existsSync(p)) {
163
175
  gone ??= [l, p]
164
176
  continue
package/src/cli/watch.ts CHANGED
@@ -35,6 +35,7 @@ import { type CliLoadOptions, discoverCliProjects, loadCliWorkspace } from './wo
35
35
  import {
36
36
  isIgnoredWatchPath,
37
37
  isWorkspaceConfigFile,
38
+ gitFiles,
38
39
  isWorkspaceFingerprintFile,
39
40
  makeRootEventFilter,
40
41
  makeWatchIgnore,
@@ -416,12 +417,15 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
416
417
 
417
418
  /** The instant the watchers go live, on the mtime clock (see `fsClockNow`): a path last modified before it is the initial run's, not an edit. */
418
419
  const armedAt = fsClockNow(cacheDir)
420
+ /** What existed at the arm, so a file born and gone since is no deletion (watch-judge.ts). */
421
+ const existedAtArm = gitFiles(workspaceRoot)
419
422
  /** Which settled paths are changes (watch-judge.ts); `pending` holds what fired since. */
420
423
  const changes = new ChangeJudge({
421
424
  workspaceRoot,
422
425
  armedAt,
423
426
  held: () => held !== undefined,
424
427
  uncached: () => uncached,
428
+ ...(existedAtArm !== undefined ? { existedAtArm } : {}),
425
429
  })
426
430
  /** Per-project arms by directory, so `rearm` can add and drop them. */
427
431
  const perProject = new Map<string, WatchHandle>()