@vzn/vx 0.0.529 → 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.529",
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.529",
25
- "@vzn/vx-linux-arm64": "0.0.529",
26
- "@vzn/vx-darwin-x64": "0.0.529",
27
- "@vzn/vx-darwin-arm64": "0.0.529"
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"
@@ -666,10 +666,10 @@ export async function cleanOutputs(args: {
666
666
  // `force: true` makes rm tolerate ENOENT (e.g. when two output
667
667
  // globs overlap and a sibling already deleted a path mid-iteration).
668
668
  // A symlink is unlinked, never followed.
669
- await removeAll(files, args.projectDir)
669
+ const removed = await removeAll(files, args.projectDir)
670
670
  await pruneEmptiedDirs(
671
671
  args.projectDir,
672
- files,
672
+ removed,
673
673
  args.keepGlobRoots === true ? globRoots(args.projectDir, args.outputs) : undefined,
674
674
  )
675
675
  // Project-relative posix paths of what was removed — the caller
@@ -689,8 +689,7 @@ export async function cleanOutputPaths(args: {
689
689
  rels: readonly string[]
690
690
  }): Promise<void> {
691
691
  const files = args.rels.map((r) => path.resolve(args.projectDir, r))
692
- await removeAll(files, args.projectDir)
693
- await pruneEmptiedDirs(args.projectDir, files)
692
+ await pruneEmptiedDirs(args.projectDir, await removeAll(files, args.projectDir))
694
693
  }
695
694
 
696
695
  /** A file's identity for the additive diff: what the hit path's fingerprint trusts too. */
@@ -791,7 +790,8 @@ const SYNC_CLEAN_MAX = 128
791
790
  * the scheduler prints any other error as an "internal error", which
792
791
  * sends the reader to file a bug against a permission bit.
793
792
  */
794
- 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)
795
795
  const refused = (f: string, err: NodeJS.ErrnoException): UserError => {
796
796
  const rel = path.relative(root, f).split(path.sep).join('/')
797
797
  return new UserError(
@@ -808,7 +808,7 @@ async function removeAll(files: readonly string[], root: string): Promise<void>
808
808
  throw refused(f, err as NodeJS.ErrnoException)
809
809
  }
810
810
  }
811
- return
811
+ return files
812
812
  }
813
813
  await Promise.all(
814
814
  files.map((f) =>
@@ -817,6 +817,39 @@ async function removeAll(files: readonly string[], root: string): Promise<void>
817
817
  }),
818
818
  ),
819
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
+ })
820
853
  }
821
854
 
822
855
  /**
@@ -922,8 +955,7 @@ export async function cleanWorkspaceOutputs(args: {
922
955
  outputs: string[]
923
956
  }): Promise<string[]> {
924
957
  const files = await resolveWorkspaceOutputs(args)
925
- await removeAll(files, args.workspaceRoot)
926
- await pruneEmptiedDirs(args.workspaceRoot, files)
958
+ await pruneEmptiedDirs(args.workspaceRoot, await removeAll(files, args.workspaceRoot))
927
959
  return files.map((f) => path.relative(args.workspaceRoot, f).split(path.sep).join('/'))
928
960
  }
929
961