@vzn/vx 0.0.337 → 0.0.339

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.337",
3
+ "version": "0.0.339",
4
4
  "description": "A fast, correct 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.337",
25
- "@vzn/vx-linux-arm64": "0.0.337",
26
- "@vzn/vx-darwin-x64": "0.0.337",
27
- "@vzn/vx-darwin-arm64": "0.0.337"
24
+ "@vzn/vx-linux-x64": "0.0.339",
25
+ "@vzn/vx-linux-arm64": "0.0.339",
26
+ "@vzn/vx-darwin-x64": "0.0.339",
27
+ "@vzn/vx-darwin-arm64": "0.0.339"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -753,8 +753,7 @@ function parseStatusOutput(
753
753
  const untracked: string[] = []
754
754
  const undecodable = new Set<string>()
755
755
  let ignoredAttributes = false
756
- for (let i = 0; i < tokens.length; i++) {
757
- const token = tokens[i]!
756
+ for (const token of tokens) {
758
757
  if (token.length < 4) continue
759
758
  if (token[0] === '!') {
760
759
  if (token === '!! .gitattributes' || token.endsWith('/.gitattributes'))
@@ -767,19 +766,9 @@ function parseStatusOutput(
767
766
  untracked.push(token.slice(3))
768
767
  continue
769
768
  }
769
+ // No rename record follows: the spawn passes `--no-renames`, so a
770
+ // rename's source is a record of its own (items 976, A-59).
770
771
  dirty.add(token.slice(3))
771
- // An R or C in either column → the next token is the rename/copy
772
- // source path. Y carries it for a rename git sees in the worktree (the
773
- // copy added with `git add -N`, the original removed): read on X alone,
774
- // the source was parsed as a record of its own, stayed trusted, and a
775
- // deleted file was keyed from the index (item 976).
776
- const x = token[0]
777
- const y = token[1]
778
- const renamed = x === 'R' || x === 'C' || y === 'R' || y === 'C'
779
- if (renamed && i + 1 < tokens.length && tokens[i + 1]!.length > 0) {
780
- i++
781
- dirty.add(tokens[i]!)
782
- }
783
772
  }
784
773
  return { dirty, untracked, undecodable, ignoredAttributes }
785
774
  }
@@ -931,7 +920,18 @@ export async function startGitEnumeration(
931
920
  // `--ignored=matching` names an ignored path without descending into an
932
921
  // ignored directory: git applies an ignored `.gitattributes` as it does
933
922
  // any other, and the filter gate below must see it (A-19).
934
- spawnGit(['status', '--porcelain', '-z', '-uall', '--ignored=matching', '--', ...pathspecs]),
923
+ // `--no-renames`: a deletion paired as an unmerged path's rename source
924
+ // prints only as `UU <path>`, and the deleted file stayed trusted (A-59).
925
+ spawnGit([
926
+ 'status',
927
+ '--porcelain',
928
+ '-z',
929
+ '-uall',
930
+ '--ignored=matching',
931
+ '--no-renames',
932
+ '--',
933
+ ...pathspecs,
934
+ ]),
935
935
  // The gate for whether a clean filter can rewrite bytes between the
936
936
  // index and the worktree: git's merged config (`core.autocrlf`) and,
937
937
  // from git 2.42, the attributes files it reads outside the tree
@@ -96,7 +96,10 @@ import type { CacheKeyInput } from './layer.js'
96
96
  // artifact records none (item 943).
97
97
  // v36: each artifact ends in a `.vx-sum` CRC-32 of its entries, checked on
98
98
  // scan and restore; a v35 artifact has none (L-19).
99
- export const CACHE_VERSION = 'vx-cache-v36'
99
+ // v37: an entry saved while `git status` hid a deletion (paired as an
100
+ // unmerged path's rename source) holds output built without a file its key
101
+ // folds (A-59).
102
+ export const CACHE_VERSION = 'vx-cache-v37'
100
103
 
101
104
  /**
102
105
  * The digest a key folds for an input gone between its enumeration and its
@@ -34,20 +34,55 @@ import type {
34
34
  * clean. We pair them by pid instead (a process has at most
35
35
  * one syscall in flight, so the pid is a sufficient key).
36
36
  *
37
- * We capture the first quoted-string argument as the path. paths that
38
- * are relative resolve against the task's cwd (set by Bun.spawn).
37
+ * We capture the first quoted-string argument as the path, a C string
38
+ * (`cStringPath` decodes it). Paths that are relative resolve against the
39
+ * task's cwd (set by Bun.spawn).
39
40
  */
40
41
  const SYSCALLS = 'openat|access|statx|newfstatat'
42
+ const QUOTED = '"((?:[^"\\\\]|\\\\.)+)"'
41
43
  const STRACE_DONE_RE = new RegExp(
42
- `^(\\d+)\\s+(${SYSCALLS})\\([^"]*"([^"]+)"[^)]*\\)\\s*=\\s*-1\\s+(ENOENT|EACCES|EPERM)`,
44
+ `^(\\d+)\\s+(${SYSCALLS})\\([^"]*${QUOTED}[^)]*\\)\\s*=\\s*-1\\s+(ENOENT|EACCES|EPERM)`,
43
45
  )
44
- const STRACE_UNFINISHED_RE = new RegExp(`^(\\d+)\\s+(${SYSCALLS})\\([^"]*"([^"]+)"[^)]*<unfinished`)
46
+ const STRACE_UNFINISHED_RE = new RegExp(`^(\\d+)\\s+(${SYSCALLS})\\([^"]*${QUOTED}[^)]*<unfinished`)
45
47
  const STRACE_RESUMED_RE = new RegExp(
46
48
  `^(\\d+)\\s+<\\.\\.\\. (${SYSCALLS}) resumed>.*?=\\s*-1\\s+(ENOENT|EACCES|EPERM)`,
47
49
  )
48
50
  /** A resumed call that SUCCEEDED — clears the pending entry, emits nothing. */
49
51
  const STRACE_RESUMED_OK_RE = new RegExp(`^(\\d+)\\s+<\\.\\.\\. (${SYSCALLS}) resumed>`)
50
52
 
53
+ const C_ESCAPES: Record<string, number> = { n: 10, t: 9, r: 13, v: 11, f: 12, a: 7, b: 8 }
54
+
55
+ /**
56
+ * The path a strace C string spells. strace escapes a quote, a backslash
57
+ * and a control byte, and writes a non-ASCII byte as octal (`é` is
58
+ * `\303\251`): read raw, `q"t.txt` was cut at `q\` and `é.txt` named
59
+ * `\303\251.txt`, so the report and every `ignore` pattern missed the file.
60
+ */
61
+ function cStringPath(raw: string): string {
62
+ if (!raw.includes('\\')) return raw
63
+ const bytes: number[] = []
64
+ for (let i = 0; i < raw.length; i++) {
65
+ const c = raw[i]!
66
+ if (c !== '\\') {
67
+ bytes.push(...Buffer.from(c, 'utf8'))
68
+ continue
69
+ }
70
+ const next = raw[++i] ?? ''
71
+ const octal = /^[0-7]{1,3}/.exec(raw.slice(i, i + 3))?.[0]
72
+ const hex = next === 'x' ? /^[0-9a-fA-F]{1,2}/.exec(raw.slice(i + 1, i + 3))?.[0] : undefined
73
+ if (octal !== undefined) {
74
+ bytes.push(parseInt(octal, 8))
75
+ i += octal.length - 1
76
+ } else if (hex !== undefined) {
77
+ bytes.push(parseInt(hex, 16))
78
+ i += hex.length
79
+ } else {
80
+ bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0))
81
+ }
82
+ }
83
+ return Buffer.from(bytes).toString('utf8')
84
+ }
85
+
51
86
  /** One denied syscall, however strace chose to lay it out. */
52
87
  export interface DeniedCall {
53
88
  syscall: string
@@ -68,7 +103,7 @@ export function deniedCalls(text: string): DeniedCall[] {
68
103
  for (const line of text.split('\n')) {
69
104
  const done = STRACE_DONE_RE.exec(line)
70
105
  if (done?.[2] !== undefined && done[3] !== undefined && done[4] !== undefined) {
71
- out.push({ syscall: done[2], rawPath: done[3], errno: done[4] })
106
+ out.push({ syscall: done[2], rawPath: cStringPath(done[3]), errno: done[4] })
72
107
  continue
73
108
  }
74
109
  const unfinished = STRACE_UNFINISHED_RE.exec(line)
@@ -77,7 +112,7 @@ export function deniedCalls(text: string): DeniedCall[] {
77
112
  unfinished[2] !== undefined &&
78
113
  unfinished[3] !== undefined
79
114
  ) {
80
- pending.set(unfinished[1], { syscall: unfinished[2], rawPath: unfinished[3] })
115
+ pending.set(unfinished[1], { syscall: unfinished[2], rawPath: cStringPath(unfinished[3]) })
81
116
  continue
82
117
  }
83
118
  const resumedOk = STRACE_RESUMED_OK_RE.exec(line)
@@ -615,7 +615,7 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
615
615
  } catch (err) {
616
616
  throw new PluginSetupError(
617
617
  plugin,
618
- `plugin '${plugin.name}' failed to load: ${err instanceof Error ? err.message : String(err)}`,
618
+ `plugin '${plugin.name}' failed in setup: ${err instanceof Error ? err.message : String(err)}`,
619
619
  )
620
620
  }
621
621
  }
@@ -247,12 +247,13 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
247
247
  /**
248
248
  * A root script that runs the members rather than checking the repo:
249
249
  * pnpm's `-r` / `--filter` / `-C`, Yarn's `--cwd` (excalidraw's
250
- * `build:common`), npm's `--prefix`, the workspace flags, Bun's `--filter`,
250
+ * `build:common`) and `yarn workspace <name>` (cal.com's `prisma`), npm's
251
+ * `--prefix`, the workspace flags, Bun's `--filter`,
251
252
  * Vite+'s `vp run` (tiptap), and the other runners, vx itself included. Mapped, it ran every member's
252
253
  * task again beside the member's own (D-45).
253
254
  */
254
255
  const RUNS_MEMBERS =
255
- /(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\bworkspaces?\s+(foreach|run)\b|\bvp\s+run\s|\bcd\s/
256
+ /(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\bworkspaces?\s+(foreach|run)\b|\byarn\s+workspace\s|\bvp\s+run\s|\bcd\s/
256
257
 
257
258
  /** A workspace flag, read only where a package manager takes it (`pmRunsMembers`). */
258
259
  const MEMBER_FLAG =