@vzn/vx 0.0.565 → 0.0.566

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.565",
3
+ "version": "0.0.566",
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.565",
25
- "@vzn/vx-linux-arm64": "0.0.565",
26
- "@vzn/vx-darwin-x64": "0.0.565",
27
- "@vzn/vx-darwin-arm64": "0.0.565"
24
+ "@vzn/vx-linux-x64": "0.0.566",
25
+ "@vzn/vx-linux-arm64": "0.0.566",
26
+ "@vzn/vx-darwin-x64": "0.0.566",
27
+ "@vzn/vx-darwin-arm64": "0.0.566"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -229,6 +229,7 @@ export function makeRootEventFilter(
229
229
  projectDirs: readonly string[],
230
230
  workspaceInputs: readonly string[],
231
231
  claimedRootFiles: ReadonlySet<string> = new Set(),
232
+ fenced: (ownDir: string, abs: string) => boolean = () => false,
232
233
  ): (filename: string) => boolean {
233
234
  const dirs = projectDirs.map((d) => path.resolve(d))
234
235
  const globs = workspaceInputs
@@ -257,12 +258,33 @@ export function makeRootEventFilter(
257
258
  return true
258
259
  }
259
260
  const abs = path.resolve(workspaceRoot, filename)
260
- for (const d of dirs) if (abs === d || abs.startsWith(d + path.sep)) return true
261
+ for (const d of dirs)
262
+ if (abs === d || (abs.startsWith(d + path.sep) && !fenced(d, abs))) return true
261
263
  for (const g of globs) if (g.match(rel)) return true
262
264
  return false
263
265
  }
264
266
  }
265
267
 
268
+ /**
269
+ * Whether `abs`, under the project at `ownDir`, lies inside another
270
+ * project nested there: the key's boundary (`computeNestedProjectDirs`)
271
+ * leaves such a file out, so it is no edit of `ownDir`'s. A root project
272
+ * watched its whole tree and ran a cycle for every edit in a nested one
273
+ * (X-42). The fences are the projects with a config, which fence a key
274
+ * whatever the plugins; a config-less one may not, and an edit there
275
+ * still counts. The fence's own config is let through: it coming or going
276
+ * moves the boundary, and the cycle it starts re-reads the set.
277
+ */
278
+ export function makeFence(fenceDirs: readonly string[]): (ownDir: string, abs: string) => boolean {
279
+ const fences = fenceDirs.map((d) => path.resolve(d))
280
+ return (ownDir, abs) => {
281
+ const config = PROJECT_CONFIGS.has(path.basename(abs)) ? path.dirname(abs) : undefined
282
+ return fences.some(
283
+ (f) => f !== config && f.startsWith(ownDir + path.sep) && abs.startsWith(f + path.sep),
284
+ )
285
+ }
286
+ }
287
+
266
288
  /**
267
289
  * A file whose edit can change what the loop watches: a `package.json`
268
290
  * (a dependency added under `--filter` widens the closure), a project
package/src/cli/watch.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  isWorkspaceConfigFile,
38
38
  gitFiles,
39
39
  isWorkspaceFingerprintFile,
40
+ makeFence,
40
41
  makeRootEventFilter,
41
42
  makeWatchIgnore,
42
43
  shapesWatchedSet,
@@ -242,6 +243,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
242
243
  workspaceConfigImports: swept.workspaceConfigImports,
243
244
  memberBases: memberBaseDirs(workspace),
244
245
  packageDirs: new Set(allProjects.map((p) => p.dir)),
246
+ fenceDirs: fenceDirs(allProjects),
245
247
  // The workspace as the cycle that just ran saw it: a package added or
246
248
  // removed since the loop armed joins or leaves the watched set. The
247
249
  // scope is the one resolved at start; a new package joins it only as a
@@ -266,6 +268,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
266
268
  configImports: sweep.configImports,
267
269
  workspaceConfigImports: sweep.workspaceConfigImports,
268
270
  packageDirs: new Set(all.map((p) => p.dir)),
271
+ fenceDirs: fenceDirs(all),
269
272
  }
270
273
  },
271
274
  // Under --frozen every cycle's configs are the lock's, so a re-lock is
@@ -312,10 +315,15 @@ interface WatchLoopArgs {
312
315
  memberBases: readonly string[]
313
316
  /** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
314
317
  packageDirs: ReadonlySet<string>
318
+ /** Every project with a config, in scope or not: a project's key leaves out what lies in one nested under it. */
319
+ fenceDirs: readonly string[]
315
320
  /** The watched set again, after a cycle that followed an event which can change it. */
316
321
  rediscover: () => Promise<Rediscovered>
317
322
  }
318
323
 
324
+ const fenceDirs = (all: readonly ProjectMeta[]): string[] =>
325
+ all.filter((p) => p.configPath !== null).map((p) => p.dir)
326
+
319
327
  interface Rediscovered {
320
328
  projects: readonly ProjectMeta[]
321
329
  claimedRootFiles: ReadonlySet<string>
@@ -328,6 +336,7 @@ interface Rediscovered {
328
336
  configImports: readonly string[]
329
337
  workspaceConfigImports: readonly string[]
330
338
  packageDirs: ReadonlySet<string>
339
+ fenceDirs: readonly string[]
331
340
  }
332
341
 
333
342
  async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
@@ -424,7 +433,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
424
433
  // project would otherwise trigger every save during `bun install` —
425
434
  // and vx's own cache writes would trigger a cycle that writes again.
426
435
  let isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
427
- let matters = makeRootEventFilter(workspaceRoot, projectDirs, workspaceInputs, claimedRootFiles)
436
+ let fenced = makeFence(args.fenceDirs)
437
+ let matters = makeRootEventFilter(
438
+ workspaceRoot,
439
+ projectDirs,
440
+ workspaceInputs,
441
+ claimedRootFiles,
442
+ fenced,
443
+ )
428
444
  /** Since the last cycle, a member came or went, or a file that shapes the watched set changed (`shapesWatchedSet`). */
429
445
  let reread = false
430
446
 
@@ -472,9 +488,10 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
472
488
  const armProject = (proj: ProjectMeta): void => {
473
489
  try {
474
490
  const handle = arm(proj.dir, true, (filename) => {
475
- if (isIgnoredPath(proj.dir, filename)) return
491
+ const abs = path.join(proj.dir, filename)
492
+ if (isIgnoredPath(proj.dir, filename) || fenced(proj.dir, abs)) return
476
493
  if (shapesWatchedSet(filename)) reread = true
477
- trigger(`${proj.name} ${filename}`, path.join(proj.dir, filename))
494
+ trigger(`${proj.name} ${filename}`, abs)
478
495
  })
479
496
  perProject.set(proj.dir, handle)
480
497
  armedAs.set(proj.dir, inodeOf(proj.dir) ?? '')
@@ -659,7 +676,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
659
676
  memberBases = next.memberBases
660
677
  armBases()
661
678
  isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
662
- matters = makeRootEventFilter(workspaceRoot, projectDirs, workspaceInputs, claimedRootFiles)
679
+ fenced = makeFence(next.fenceDirs)
680
+ matters = makeRootEventFilter(
681
+ workspaceRoot,
682
+ projectDirs,
683
+ workspaceInputs,
684
+ claimedRootFiles,
685
+ fenced,
686
+ )
663
687
  if (next.workspaceWide !== workspaceWide) {
664
688
  // A task started or stopped declaring `workspaceFiles`: the other
665
689
  // arm's shape. Until item 891 the choice was made once, at start, and