@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 +5 -5
- package/src/cli/watch-filter.ts +1 -0
- package/src/cli/watch.ts +19 -1
- package/src/graph/task-graph.ts +35 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
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"
|
package/src/cli/watch-filter.ts
CHANGED
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
|
-
|
|
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
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
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(`!${
|
|
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
|
)
|