@vzn/vx 0.0.244 → 0.0.246

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.244",
3
+ "version": "0.0.246",
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.244",
21
- "@vzn/vx-linux-arm64": "0.0.244",
22
- "@vzn/vx-darwin-x64": "0.0.244",
23
- "@vzn/vx-darwin-arm64": "0.0.244"
20
+ "@vzn/vx-linux-x64": "0.0.246",
21
+ "@vzn/vx-linux-arm64": "0.0.246",
22
+ "@vzn/vx-darwin-x64": "0.0.246",
23
+ "@vzn/vx-darwin-arm64": "0.0.246"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -29,6 +29,7 @@ import {
29
29
  shellArgv,
30
30
  splitNegations,
31
31
  slashBraceExpansions,
32
+ staticPrefix,
32
33
  taskGlob,
33
34
  UserError,
34
35
  } from '../util/index.js'
@@ -615,13 +616,25 @@ export async function cleanOutputs(args: {
615
616
  projectDir: string
616
617
  outputs: string[]
617
618
  nestedProjectDirs: string[]
619
+ /**
620
+ * Before a miss: keep the directory each wildcard glob is rooted at
621
+ * (`dist` for `dist/**`). The task writes its matches under it, so a
622
+ * remove there bought only an rmdir and the task's mkdir: 0.4 ms of a
623
+ * 4.9 ms one-file miss (B-49). A restore prunes it, since the entry's
624
+ * shape decides there.
625
+ */
626
+ keepGlobRoots?: boolean
618
627
  }): Promise<string[]> {
619
628
  const files = await resolveOutputs(args)
620
629
  // `force: true` makes rm tolerate ENOENT (e.g. when two output
621
630
  // globs overlap and a sibling already deleted a path mid-iteration).
622
631
  // A symlink is unlinked, never followed.
623
632
  await removeAll(files, args.projectDir)
624
- await pruneEmptiedDirs(args.projectDir, files)
633
+ await pruneEmptiedDirs(
634
+ args.projectDir,
635
+ files,
636
+ args.keepGlobRoots === true ? globRoots(args.projectDir, args.outputs) : undefined,
637
+ )
625
638
  // Project-relative posix paths of what was removed — the caller
626
639
  // feeds these to GitFilesCache.markOutputsChanged after a restore.
627
640
  return files.map((f) => path.relative(args.projectDir, f).split(path.sep).join('/'))
@@ -778,7 +791,11 @@ async function removeAll(files: readonly string[], root: string): Promise<void>
778
791
  * holds something — a stray the globs do not cover — stays, and the restore
779
792
  * says so if it is in the way.
780
793
  */
781
- async function pruneEmptiedDirs(root: string, removed: readonly string[]): Promise<void> {
794
+ async function pruneEmptiedDirs(
795
+ root: string,
796
+ removed: readonly string[],
797
+ keep?: ReadonlySet<string>,
798
+ ): Promise<void> {
782
799
  const rootResolved = path.resolve(root)
783
800
  // LEVEL ORDER, not a walk-up per directory. A parent is attempted only
784
801
  // once every one of its children has had its turn, which is what makes
@@ -794,7 +811,8 @@ async function pruneEmptiedDirs(root: string, removed: readonly string[]): Promi
794
811
  while (level.size > 0) {
795
812
  const parents = new Set<string>()
796
813
  const dirs = [...level].filter(
797
- (dir) => dir !== rootResolved && dir.startsWith(rootResolved + path.sep),
814
+ (dir) =>
815
+ dir !== rootResolved && dir.startsWith(rootResolved + path.sep) && keep?.has(dir) !== true,
798
816
  )
799
817
  const gone = (err: NodeJS.ErrnoException): boolean => err.code === 'ENOENT'
800
818
  if (dirs.length <= SYNC_CLEAN_MAX) {
@@ -817,6 +835,21 @@ async function pruneEmptiedDirs(root: string, removed: readonly string[]): Promi
817
835
  }
818
836
  }
819
837
 
838
+ /**
839
+ * The directories the wildcard output globs are rooted at, absolute. A
840
+ * literal names a file or a tree whose shape the task decides, so it has
841
+ * none; nor has a glob rooted at the project itself.
842
+ */
843
+ function globRoots(projectDir: string, outputs: readonly string[]): Set<string> {
844
+ const roots = new Set<string>()
845
+ for (const g of outputs) {
846
+ if (g.startsWith('!') || isLiteralPattern(g)) continue
847
+ const prefix = staticPrefix(g)
848
+ if (prefix !== '.') roots.add(path.resolve(projectDir, prefix))
849
+ }
850
+ return roots
851
+ }
852
+
820
853
  /**
821
854
  * Resolve declared `outputs.workspaceFiles` globs (workspace-root-
822
855
  * relative) to actual produced files. Live-FS glob like
@@ -758,7 +758,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
758
758
  // re-create would stay in a same-project consumer's input set, keeping
759
759
  // that consumer's key unchanged while the file is gone from disk.
760
760
  const endClean = span('miss: clean outputs')
761
- const cleanedRels = await cleanOutputs(cleanArgs)
761
+ const cleanedRels = await cleanOutputs({ ...cleanArgs, keepGlobRoots: true })
762
762
  endClean()
763
763
  args.gitFilesCache?.noteClean(node.id, node.projectDir, cleanedRels)
764
764
  args.gitFilesCache?.markOutputsChanged(node.projectDir, cleanedRels)
@@ -96,8 +96,8 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
96
96
  }
97
97
 
98
98
  /**
99
- * Whether the package manager that owns `dir` runs `pre` / `post` hooks
100
- * around a script. The manager is the nearest `packageManager` field or
99
+ * The package manager that owns `dir`, when it runs `pre` / `post` hooks
100
+ * around a script; null when it runs none. The manager is the nearest `packageManager` field or
101
101
  * lockfile, and its settings are read beside it, as each reads them for a
102
102
  * member too. Probed with a `prebuild` / `build` / `postbuild` trio: Yarn
103
103
  * 2+ runs none (D-31); npm runs none under `ignore-scripts=true` in
@@ -105,7 +105,7 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
105
105
  * `enablePrePostScripts: false` in `pnpm-workspace.yaml` (D-33); Bun and
106
106
  * Yarn 1 run them whatever those say. No manager found is npm's default.
107
107
  */
108
- function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
108
+ function runsScriptHooks(dir: string, memo: Map<string, string | null>): string | null {
109
109
  const known = memo.get(dir)
110
110
  if (known !== undefined) return known
111
111
  const read = (f: string): string | undefined => {
@@ -129,15 +129,15 @@ function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
129
129
  else if (existsSync(path.join(dir, 'package-lock.json'))) manager = 'npm'
130
130
  else if (['bun.lock', 'bun.lockb'].some((f) => existsSync(path.join(dir, f)))) manager = 'bun'
131
131
  }
132
- let answer: boolean
132
+ let answer: string | null
133
133
  if (manager === undefined) {
134
134
  const up = path.dirname(dir)
135
- answer = up === dir ? true : runsScriptHooks(up, memo)
135
+ answer = up === dir ? 'npm' : runsScriptHooks(up, memo)
136
136
  } else {
137
137
  const npmrc = read('.npmrc') ?? ''
138
138
  const set = (key: string, value: string): boolean =>
139
139
  new RegExp(`^\\s*${key}\\s*=\\s*${value}\\s*$`, 'm').test(npmrc)
140
- answer =
140
+ const runs =
141
141
  manager === 'berry'
142
142
  ? false
143
143
  : manager === 'npm'
@@ -146,6 +146,7 @@ function runsScriptHooks(dir: string, memo: Map<string, boolean>): boolean {
146
146
  ? !set('enable-pre-post-scripts', 'false') &&
147
147
  !/^enablePrePostScripts:\s*false\s*$/m.test(read('pnpm-workspace.yaml') ?? '')
148
148
  : true
149
+ answer = runs ? manager : null
149
150
  }
150
151
  memo.set(dir, answer)
151
152
  return answer
@@ -242,7 +243,7 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
242
243
 
243
244
  export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
244
245
  const projects: GeneratedProject[] = []
245
- const hookMemo = new Map<string, boolean>()
246
+ const hookMemo = new Map<string, string | null>()
246
247
  // The workspace root among members: its scripts run the workspace
247
248
  // (`npm run build --workspaces`, `pnpm -r build`), and a root task made
248
249
  // of one ran every member's build again under `--all`. Since D-39 a
@@ -258,7 +259,8 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
258
259
  for (const meta of metas) {
259
260
  if (meta === root) continue
260
261
  const scripts = scriptsOf(meta)
261
- const runsHooks = runsScriptHooks(meta.dir, hookMemo)
262
+ const hooksBy = runsScriptHooks(meta.dir, hookMemo)
263
+ const runsHooks = hooksBy !== null
262
264
  const runnable = Object.keys(scripts).filter(
263
265
  (n) => typeof scripts[n] === 'string' && scripts[n] !== '',
264
266
  )
@@ -314,7 +316,7 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
314
316
  )
315
317
  if (hooks.length > 0) {
316
318
  todos.push(
317
- `npm ran ${hooks.map((h) => `\`${h}\``).join(' and ')} around this script without being asked; folded into the command in that order`,
319
+ `${hooksBy} ran ${hooks.map((h) => `\`${h}\``).join(' and ')} around this script without being asked; folded into the command in that order`,
318
320
  )
319
321
  }
320
322
  const exec: Record<string, unknown> = { command }