@vzn/vx 0.0.601 → 0.0.603

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.601",
3
+ "version": "0.0.603",
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.601",
25
- "@vzn/vx-linux-arm64": "0.0.601",
26
- "@vzn/vx-darwin-x64": "0.0.601",
27
- "@vzn/vx-darwin-arm64": "0.0.601"
24
+ "@vzn/vx-linux-x64": "0.0.603",
25
+ "@vzn/vx-linux-arm64": "0.0.603",
26
+ "@vzn/vx-darwin-x64": "0.0.603",
27
+ "@vzn/vx-darwin-arm64": "0.0.603"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -272,6 +272,7 @@ export function makeRootEventFilter(
272
272
  (isWorkspaceFingerprintFile(rel) ||
273
273
  isWorkspaceConfigFile(rel) ||
274
274
  rel === 'package.json' ||
275
+ rel === '.gitignore' ||
275
276
  claimedRootFiles.has(rel))
276
277
  ) {
277
278
  return true
package/src/cli/watch.ts CHANGED
@@ -164,7 +164,15 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
164
164
  const workspaceRoot = await findWorkspaceRoot(cwd, reads)
165
165
  const workspace = await loadWorkspace(workspaceRoot, reads)
166
166
  const allProjects = await discoverCliProjects(workspace)
167
- const anchored = opts.tasks.filter((t) => t.includes('#')).map((t) => t.slice(0, t.indexOf('#')))
167
+ // `//#check` is the root project's task, named by its package.json name
168
+ // as the run names it (prepare.ts, D-39): taken literally, no project
169
+ // was in scope and the watch exited 1 after a green run.
170
+ const rootDir = path.resolve(workspaceRoot)
171
+ const rootName = allProjects.find((p) => path.resolve(p.dir) === rootDir)?.name
172
+ const anchored = opts.tasks
173
+ .filter((t) => t.includes('#'))
174
+ .map((t) => t.slice(0, t.indexOf('#')))
175
+ .map((p) => (p === '//' && rootName !== undefined ? rootName : p))
168
176
  const named =
169
177
  anchored.length === opts.tasks.length
170
178
  ? new Set(anchored)
@@ -586,6 +594,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
586
594
  isWorkspaceFingerprintFile(filename) ||
587
595
  isWorkspaceConfigFile(filename) ||
588
596
  filename === 'package.json' ||
597
+ filename === '.gitignore' ||
589
598
  claimedRootFiles.has(filename)
590
599
  ) {
591
600
  if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
@@ -845,6 +854,15 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
845
854
  base,
846
855
  arm(base, false, (filename) => {
847
856
  if (isIgnoredWatchPath(filename)) return
857
+ // Inputs are gitignore-aware: an edit here changes what the
858
+ // members' keys read, and `vx run` would miss on it.
859
+ if (filename === '.gitignore') {
860
+ trigger(
861
+ path.relative(workspaceRoot, path.join(base, filename)),
862
+ path.join(base, filename),
863
+ )
864
+ return
865
+ }
848
866
  // Only a member coming or going. On macOS a non-recursive watcher
849
867
  // also reports a member whose CONTENTS changed (FSEvents names the
850
868
  // directory a write landed in), so a task writing into its own
@@ -1389,7 +1389,7 @@ interface Side {
1389
1389
  node: TaskNode
1390
1390
  globs: readonly string[]
1391
1391
  reads: boolean
1392
- /** The project's root-relative dir for a rebased `files` output. */
1392
+ /** The project's root-relative dir for rebased `files` globs. */
1393
1393
  rel?: string
1394
1394
  }
1395
1395
 
@@ -1407,7 +1407,9 @@ interface Side {
1407
1407
  * scope mirrors `detectOutputCollisions`: `inputs.files` against another
1408
1408
  * same-project task's `outputs.files`; root-anchored `inputs.workspaceFiles`
1409
1409
  * against every other task's `outputs.workspaceFiles` and, with the root
1410
- * known, its `outputs.files` rebased to the root. A task's own outputs are
1410
+ * known, its `outputs.files` rebased to the root; and, with the root known,
1411
+ * `inputs.files` rebased to the root against another task's
1412
+ * `outputs.workspaceFiles`, which may land in any project (X-135). A task's own outputs are
1411
1413
  * already subtracted from its inputs, and an output the reader's `!` entries
1412
1414
  * take back whole is no overlap. A keyed group is exempt: the default
1413
1415
  * `build` reads `**` of a config-less dependency and runs nothing, and its
@@ -1416,6 +1418,8 @@ interface Side {
1416
1418
  function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: string): void {
1417
1419
  const byProject = new Map<string, Side[]>()
1418
1420
  const rooted: Side[] = []
1421
+ const wsWriters: Side[] = []
1422
+ const filesReaders: TaskNode[] = []
1419
1423
  const filesWriters: TaskNode[] = []
1420
1424
  let projectReaders = false
1421
1425
  let rootReaders = false
@@ -1433,12 +1437,17 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
1433
1437
  bucket(n.projectName).push({ node: n, globs: outFiles, reads: false })
1434
1438
  filesWriters.push(n)
1435
1439
  }
1436
- if (outWs.length > 0) rooted.push({ node: n, globs: outWs, reads: false })
1440
+ if (outWs.length > 0) {
1441
+ const side = { node: n, globs: outWs, reads: false }
1442
+ rooted.push(side)
1443
+ wsWriters.push(side)
1444
+ }
1437
1445
  if (isGroupTask(n)) continue
1438
1446
  const inFiles = splitNegations(cache.inputs.files).positive
1439
1447
  const inWs = splitNegations(cache.inputs.workspaceFiles ?? []).positive
1440
1448
  if (inFiles.length > 0) {
1441
1449
  bucket(n.projectName).push({ node: n, globs: inFiles, reads: true })
1450
+ filesReaders.push(n)
1442
1451
  projectReaders = true
1443
1452
  }
1444
1453
  if (inWs.length > 0) {
@@ -1458,6 +1467,23 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
1458
1467
  }
1459
1468
  }
1460
1469
  }
1470
+ if (projectReaders && wsWriters.length > 0 && workspaceRoot !== undefined) {
1471
+ const sides = [...wsWriters]
1472
+ for (const node of filesReaders) {
1473
+ const rel = relPosix(workspaceRoot, node.projectDir)
1474
+ const globs = splitNegations(node.config.cache!.inputs.files).positive.map((g) =>
1475
+ rel === '' ? g : `${rel}/${g}`,
1476
+ )
1477
+ sides.push({ node, globs, reads: true, rel })
1478
+ }
1479
+ for (const [i, j] of overlapCandidates(
1480
+ sides,
1481
+ (s) => s.globs,
1482
+ (s) => s.reads,
1483
+ )) {
1484
+ readsOutputs(sides[i]!, sides[j]!, 'files')
1485
+ }
1486
+ }
1461
1487
  if (!rootReaders) return
1462
1488
  if (workspaceRoot !== undefined) {
1463
1489
  for (const node of filesWriters) {
@@ -1481,6 +1507,8 @@ function readsOutputs(x: Side, y: Side, field: 'files' | 'workspaceFiles'): void
1481
1507
  if (x.reads === y.reads || x.node === y.node) return
1482
1508
  const [reader, writer] = x.reads ? [x, y] : [y, x]
1483
1509
  const cache = reader.node.config.cache!
1510
+ // A reader rebased to the root (X-135) speaks in its project's terms.
1511
+ const at = reader.rel === undefined || reader.rel === '' ? '' : `${reader.rel}/`
1484
1512
  // What the reader's key never reads: its `!` entries and its own outputs.
1485
1513
  const takeBack = [
1486
1514
  ...splitNegations(field === 'files' ? cache.inputs.files : (cache.inputs.workspaceFiles ?? []))
@@ -1488,18 +1516,19 @@ function readsOutputs(x: Side, y: Side, field: 'files' | 'workspaceFiles'): void
1488
1516
  ...splitNegations(
1489
1517
  field === 'files' ? cache.outputs.files : (cache.outputs.workspaceFiles ?? []),
1490
1518
  ).positive,
1491
- ]
1519
+ ].map((g) => at + g)
1492
1520
  for (const go of writer.globs) {
1493
1521
  if (outputTakenBack(go, takeBack)) continue
1494
1522
  for (const gi of reader.globs) {
1495
1523
  if (!outputsOverlap(gi, go)) continue
1496
1524
  const shown =
1497
1525
  writer.rel === undefined || writer.rel === '' ? go : go.slice(writer.rel.length + 1)
1526
+ const out = at !== '' && go.startsWith(at) ? go.slice(at.length) : go
1498
1527
  throw new UserError(
1499
- `${reader.node.id} reads ${JSON.stringify(gi)} in cache.inputs.${field}, which matches ` +
1528
+ `${reader.node.id} reads ${JSON.stringify(gi.slice(at.length))} in cache.inputs.${field}, which matches ` +
1500
1529
  `${writer.node.id}'s output ${JSON.stringify(shown)} — a task's key must not read ` +
1501
1530
  `another task's outputs (the dependency's key already cascades through dependsOn). ` +
1502
- `Exclude it: add ${JSON.stringify(`!${go}`)} to ${reader.node.id}'s ` +
1531
+ `Exclude it: add ${JSON.stringify(`!${out}`)} to ${reader.node.id}'s ` +
1503
1532
  `cache.inputs.${field}, or set rules: { upfrontKeys: false } in vx.workspace.ts to ` +
1504
1533
  `let it wait for its producer.`,
1505
1534
  )