@vzn/vx 0.0.528 → 0.0.530

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.528",
3
+ "version": "0.0.530",
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.528",
25
- "@vzn/vx-linux-arm64": "0.0.528",
26
- "@vzn/vx-darwin-x64": "0.0.528",
27
- "@vzn/vx-darwin-arm64": "0.0.528"
24
+ "@vzn/vx-linux-x64": "0.0.530",
25
+ "@vzn/vx-linux-arm64": "0.0.530",
26
+ "@vzn/vx-darwin-x64": "0.0.530",
27
+ "@vzn/vx-darwin-arm64": "0.0.530"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -7,7 +7,13 @@
7
7
 
8
8
  import path from 'node:path'
9
9
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
10
- import { UserError, executablePath, gitSpawnRefusal, xxh3hex } from '../util/index.js'
10
+ import {
11
+ UserError,
12
+ executablePath,
13
+ gitSpawnRefusal,
14
+ isInstalledPath,
15
+ xxh3hex,
16
+ } from '../util/index.js'
11
17
  import { FILE_HASH_RACY_MS, racyWindowMs } from './layer.js'
12
18
 
13
19
  /** Three facts of the repository a directory is in: read off the disk, or one `git rev-parse`. */
@@ -517,7 +523,8 @@ function walkFiles(dir: string, rel = ''): string[] {
517
523
  }
518
524
  const out: string[] = []
519
525
  for (const e of entries) {
520
- if (e.name === '.git') continue
526
+ // Nothing tracks a vendored tree's files; an install there is not one.
527
+ if (e.name === '.git' || e.name === 'node_modules') continue
521
528
  const r = rel === '' ? e.name : `${rel}/${e.name}`
522
529
  if (e.isDirectory()) out.push(...walkFiles(dir, r))
523
530
  else out.push(r)
@@ -562,6 +569,7 @@ function parseLsFilesOutput(
562
569
  if (record.length === 0) continue
563
570
  const m = LS_FILES_STAGE_RE.exec(record)
564
571
  const filePath = m === null ? record : record.slice(m[0].length) // --others: bare path
572
+ if (m === null && isInstalledPath(filePath)) continue
565
573
  files.push(filePath)
566
574
  if (undecodableRecords.size > 0 && undecodableRecords.has(record)) undecodable.push(filePath)
567
575
  if (m === null) continue
@@ -1376,7 +1384,11 @@ export async function startGitEnumeration(
1376
1384
  // is). They come from the status walk, repo-root-relative like the dirty
1377
1385
  // set. Without a status answer the enumeration is the index alone.
1378
1386
  const untracked =
1379
- parsedStatus === null ? [] : [...stripPrefixFromSet(new Set(parsedStatus.untracked), gitPrefix)]
1387
+ parsedStatus === null
1388
+ ? []
1389
+ : [...stripPrefixFromSet(new Set(parsedStatus.untracked), gitPrefix)].filter(
1390
+ (rel) => !isInstalledPath(rel),
1391
+ )
1380
1392
  const listed = untracked.length === 0 ? tracked : tracked.concat(untracked)
1381
1393
  const nested = expandNestedRepos(workspaceRoot, listed, gitlinks)
1382
1394
  const all = nested.files
@@ -58,8 +58,10 @@ export {
58
58
  // re-lock can't bust every cache key the way a tracked source file
59
59
  // would. (Literal pattern, not the workspace `LOCKFILE_NAME` constant:
60
60
  // cache is a leaf module and must not import from workspace.)
61
+ //
62
+ // `node_modules` is not here: the enumeration drops an untracked file
63
+ // under one (`isInstalledPath`), and a tracked one is a source.
61
64
  const ALWAYS_IGNORE = [
62
- '**/node_modules/**',
63
65
  // Defense in depth, and measured as exactly that (item 497): git never
64
66
  // reports a path under `.git`, so dropping this line changes NOTHING on
65
67
  // any route into the input set. Probed three ways with the pattern in
@@ -664,10 +666,10 @@ export async function cleanOutputs(args: {
664
666
  // `force: true` makes rm tolerate ENOENT (e.g. when two output
665
667
  // globs overlap and a sibling already deleted a path mid-iteration).
666
668
  // A symlink is unlinked, never followed.
667
- await removeAll(files, args.projectDir)
669
+ const removed = await removeAll(files, args.projectDir)
668
670
  await pruneEmptiedDirs(
669
671
  args.projectDir,
670
- files,
672
+ removed,
671
673
  args.keepGlobRoots === true ? globRoots(args.projectDir, args.outputs) : undefined,
672
674
  )
673
675
  // Project-relative posix paths of what was removed — the caller
@@ -687,8 +689,7 @@ export async function cleanOutputPaths(args: {
687
689
  rels: readonly string[]
688
690
  }): Promise<void> {
689
691
  const files = args.rels.map((r) => path.resolve(args.projectDir, r))
690
- await removeAll(files, args.projectDir)
691
- await pruneEmptiedDirs(args.projectDir, files)
692
+ await pruneEmptiedDirs(args.projectDir, await removeAll(files, args.projectDir))
692
693
  }
693
694
 
694
695
  /** A file's identity for the additive diff: what the hit path's fingerprint trusts too. */
@@ -789,7 +790,8 @@ const SYNC_CLEAN_MAX = 128
789
790
  * the scheduler prints any other error as an "internal error", which
790
791
  * sends the reader to file a bug against a permission bit.
791
792
  */
792
- async function removeAll(files: readonly string[], root: string): Promise<void> {
793
+ async function removeAll(all: readonly string[], root: string): Promise<string[]> {
794
+ const files = notThroughLink(all, root)
793
795
  const refused = (f: string, err: NodeJS.ErrnoException): UserError => {
794
796
  const rel = path.relative(root, f).split(path.sep).join('/')
795
797
  return new UserError(
@@ -806,7 +808,7 @@ async function removeAll(files: readonly string[], root: string): Promise<void>
806
808
  throw refused(f, err as NodeJS.ErrnoException)
807
809
  }
808
810
  }
809
- return
811
+ return files
810
812
  }
811
813
  await Promise.all(
812
814
  files.map((f) =>
@@ -815,6 +817,39 @@ async function removeAll(files: readonly string[], root: string): Promise<void>
815
817
  }),
816
818
  ),
817
819
  )
820
+ return files
821
+ }
822
+
823
+ /**
824
+ * The paths whose directory is really where it sits, not reached through a
825
+ * symlinked directory. A save follows `dist -> real-out` on purpose
826
+ * (turborepo#13042), but a clean through a link deletes the target's
827
+ * files: `public -> static` in the same project took the tracked
828
+ * `static/logo.svg` before every run (X-5). The link is a declared
829
+ * output's own entry; what it leads to is not.
830
+ */
831
+ function notThroughLink(files: readonly string[], root: string): string[] {
832
+ const real = (p: string): string | null => {
833
+ try {
834
+ return realpathSync(p)
835
+ } catch {
836
+ return null
837
+ }
838
+ }
839
+ const realRoot = real(root) ?? root
840
+ const own = new Map<string, boolean>()
841
+ return files.filter((f) => {
842
+ const dir = path.dirname(f)
843
+ let ok = own.get(dir)
844
+ if (ok === undefined) {
845
+ // A directory already gone has nothing to delete through; its path
846
+ // stays so the prune still reaches the parents it emptied.
847
+ const r = real(dir)
848
+ ok = r === null || r === path.join(realRoot, path.relative(root, dir))
849
+ own.set(dir, ok)
850
+ }
851
+ return ok
852
+ })
818
853
  }
819
854
 
820
855
  /**
@@ -920,8 +955,7 @@ export async function cleanWorkspaceOutputs(args: {
920
955
  outputs: string[]
921
956
  }): Promise<string[]> {
922
957
  const files = await resolveWorkspaceOutputs(args)
923
- await removeAll(files, args.workspaceRoot)
924
- await pruneEmptiedDirs(args.workspaceRoot, files)
958
+ await pruneEmptiedDirs(args.workspaceRoot, await removeAll(files, args.workspaceRoot))
925
959
  return files.map((f) => path.relative(args.workspaceRoot, f).split(path.sep).join('/'))
926
960
  }
927
961
 
package/src/util/index.ts CHANGED
@@ -43,6 +43,7 @@ export {
43
43
  splitNegations,
44
44
  staticPrefix,
45
45
  anyTaskGlob,
46
+ isInstalledPath,
46
47
  taskGlob,
47
48
  wholeSubtreePrefixes,
48
49
  slashBraceExpansions,
package/src/util/paths.ts CHANGED
@@ -366,6 +366,16 @@ export function slashBraceExpansions(pattern: string): string[] {
366
366
  return [pattern]
367
367
  }
368
368
 
369
+ /**
370
+ * Whether a path git lists as UNTRACKED is an installed dependency: it sits
371
+ * under a `node_modules` directory. Such a file is never an input, and the
372
+ * enumeration drops it. A TRACKED file there (a committed test fixture) is
373
+ * a source like any other: dropped by path, an edit to it was a stale hit.
374
+ */
375
+ export function isInstalledPath(rel: string): boolean {
376
+ return rel.startsWith('node_modules/') || rel.includes('/node_modules/')
377
+ }
378
+
369
379
  // oxlint-disable-next-line no-control-regex
370
380
  const CONTROL = /[\x00-\x1f\x7f-\x9f]/g
371
381
 
@@ -15,6 +15,7 @@ import {
15
15
  UserError,
16
16
  gitSpawnRefusal,
17
17
  isExecutableMissing,
18
+ isInstalledPath,
18
19
  relPosix,
19
20
  taskGlob,
20
21
  } from '../util/index.js'
@@ -234,7 +235,11 @@ export async function affectedChanges(
234
235
 
235
236
  // vx-lock.json (workspace-root metadata) is excluded like a gitignored
236
237
  // file: re-running `vx lock` must not mark every project affected.
237
- const changed = [...diffed, ...untracked].filter((s) => s !== LOCKFILE_NAME)
238
+ // An untracked file under `node_modules` is an install, which the input
239
+ // enumeration drops too (`isInstalledPath`); a tracked one is a source.
240
+ const changed = [...diffed, ...untracked.filter((s) => !isInstalledPath(s))].filter(
241
+ (s) => s !== LOCKFILE_NAME,
242
+ )
238
243
 
239
244
  // A lockfile or workspace-definition change re-keys EVERY task, because the
240
245
  // workspace fingerprint folds those files into every cache key. Mapping