@vzn/vx 0.0.614 → 0.0.615
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/graph/task-graph.ts +100 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.615",
|
|
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.615",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.615",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.615",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.615"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ProjectConfig, TaskConfig, WorkspaceRules } from '../config.js'
|
|
|
2
2
|
import {
|
|
3
3
|
asTrees,
|
|
4
4
|
isLiteralPattern,
|
|
5
|
+
normalizeGlob,
|
|
5
6
|
relPosix,
|
|
6
7
|
splitNegations,
|
|
7
8
|
staticPrefix,
|
|
@@ -1152,6 +1153,64 @@ function collideAcross(
|
|
|
1152
1153
|
*/
|
|
1153
1154
|
const GLOB_HEAD_END = /[*?{}\\!]/
|
|
1154
1155
|
|
|
1156
|
+
/** `firstSegment`'s answer for a glob that can start anywhere. */
|
|
1157
|
+
const ANYWHERE = ''
|
|
1158
|
+
|
|
1159
|
+
/** Where a glob's first segment ends: a separator, or anything `GLOB_HEAD_END` stops at. */
|
|
1160
|
+
const FIRST_SEGMENT_END = /[*?{}\\!/]/
|
|
1161
|
+
|
|
1162
|
+
const firstSegments = new Map<string, string>()
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* The first path segment every path a glob (or its `asTrees` twin) can
|
|
1166
|
+
* match starts with, or `ANYWHERE`. A task glob matches whole paths, so
|
|
1167
|
+
* two globs with different first segments select disjoint sets, and none
|
|
1168
|
+
* of `outputsOverlap`'s three cases can hold for them. Memoised by the
|
|
1169
|
+
* raw string: a pure function, and one glob (`src/**`) recurs in every
|
|
1170
|
+
* project.
|
|
1171
|
+
*/
|
|
1172
|
+
function firstSegment(raw: string): string {
|
|
1173
|
+
let seg = firstSegments.get(raw)
|
|
1174
|
+
if (seg !== undefined) return seg
|
|
1175
|
+
const g = normalizeGlob(raw)
|
|
1176
|
+
const cut = g.search(FIRST_SEGMENT_END)
|
|
1177
|
+
seg = cut === -1 ? g : g[cut] === '/' ? g.slice(0, cut) : ANYWHERE
|
|
1178
|
+
if (seg === '.' || seg === '..') seg = ANYWHERE
|
|
1179
|
+
firstSegments.set(raw, seg)
|
|
1180
|
+
return seg
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* False only when no pair `overlapCandidates` could return can overlap:
|
|
1185
|
+
* every glob starts in a named segment and no two members (on different
|
|
1186
|
+
* sides, given `sideOf`) share one. The per-project buckets are nearly
|
|
1187
|
+
* all of this shape (`src/**` read, `dist/**` written), and building the
|
|
1188
|
+
* indexes for each cost the warm run's graph stage tens of milliseconds
|
|
1189
|
+
* at 1,090 projects (X-148).
|
|
1190
|
+
*/
|
|
1191
|
+
function mayOverlap<T>(
|
|
1192
|
+
tasks: readonly T[],
|
|
1193
|
+
globsOf: (n: T) => readonly string[] | undefined,
|
|
1194
|
+
sideOf?: (n: T) => boolean,
|
|
1195
|
+
): boolean {
|
|
1196
|
+
// Per segment: the first member seen (one side) or the sides seen as a mask.
|
|
1197
|
+
const seen = new Map<string, number>()
|
|
1198
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1199
|
+
const mark = sideOf === undefined ? i : sideOf(tasks[i]!) ? 2 : 1
|
|
1200
|
+
for (const g of globsOf(tasks[i]!) ?? []) {
|
|
1201
|
+
const seg = firstSegment(g)
|
|
1202
|
+
if (seg === ANYWHERE) return true
|
|
1203
|
+
const was = seen.get(seg)
|
|
1204
|
+
if (was === undefined) seen.set(seg, mark)
|
|
1205
|
+
else if (sideOf === undefined) {
|
|
1206
|
+
if (was !== i) return true
|
|
1207
|
+
} else if ((was | mark) === 3) return true
|
|
1208
|
+
else seen.set(seg, was | mark)
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return false
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1155
1214
|
/**
|
|
1156
1215
|
* The pairs `[i, j]`, `i < j`, of `tasks` whose declared outputs CAN
|
|
1157
1216
|
* overlap, ascending by `i` then `j`: a superset of the pairs
|
|
@@ -1181,6 +1240,7 @@ function overlapCandidates<T>(
|
|
|
1181
1240
|
globsOf: (n: T) => readonly string[] | undefined,
|
|
1182
1241
|
sideOf?: (n: T) => boolean,
|
|
1183
1242
|
): Array<[number, number]> {
|
|
1243
|
+
if (!mayOverlap(tasks, globsOf, sideOf)) return []
|
|
1184
1244
|
const sides = sideOf === undefined ? 1 : 2
|
|
1185
1245
|
// The index a member of side `s` looks up: its own side's, or the other's.
|
|
1186
1246
|
const across = (s: number): number => (sides === 1 ? s : 1 - s)
|
|
@@ -1356,6 +1416,7 @@ function refuseSelfClean(node: TaskNode): void {
|
|
|
1356
1416
|
const cache = node.config.cache
|
|
1357
1417
|
if (cache === undefined) return
|
|
1358
1418
|
for (const field of ['files', 'workspaceFiles'] as const) {
|
|
1419
|
+
if (!coversAny(cache.inputs[field], cache.outputs[field])) continue
|
|
1359
1420
|
const inputs = splitNegations(cache.inputs[field] ?? [])
|
|
1360
1421
|
const out = splitNegations(cache.outputs[field] ?? [])
|
|
1361
1422
|
const outputs = out.positive
|
|
@@ -1384,6 +1445,39 @@ function refuseSelfClean(node: TaskNode): void {
|
|
|
1384
1445
|
}
|
|
1385
1446
|
}
|
|
1386
1447
|
|
|
1448
|
+
/**
|
|
1449
|
+
* False only when no positive output can cover a positive input: each
|
|
1450
|
+
* starts in a named first segment and no input shares one with an output
|
|
1451
|
+
* (`firstSegment`). The common task reads `src/**` and writes `dist/**`.
|
|
1452
|
+
*/
|
|
1453
|
+
function coversAny(
|
|
1454
|
+
inputs: readonly string[] | undefined,
|
|
1455
|
+
outputs: readonly string[] | undefined,
|
|
1456
|
+
): boolean {
|
|
1457
|
+
if (inputs === undefined || outputs === undefined) return false
|
|
1458
|
+
let anyOutput = false
|
|
1459
|
+
for (const go of outputs) {
|
|
1460
|
+
if (go.startsWith('!')) continue
|
|
1461
|
+
anyOutput = true
|
|
1462
|
+
if (firstSegment(go) === ANYWHERE) return true
|
|
1463
|
+
}
|
|
1464
|
+
if (!anyOutput) return false
|
|
1465
|
+
for (const gi of inputs) {
|
|
1466
|
+
if (gi.startsWith('!')) continue
|
|
1467
|
+
const seg = firstSegment(gi)
|
|
1468
|
+
if (seg === ANYWHERE) return true
|
|
1469
|
+
for (const go of outputs) if (!go.startsWith('!') && firstSegment(go) === seg) return true
|
|
1470
|
+
}
|
|
1471
|
+
return false
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** A glob list's positive entries: the list itself when it takes nothing back. */
|
|
1475
|
+
function positives(globs: readonly string[] | undefined): readonly string[] {
|
|
1476
|
+
if (globs === undefined) return []
|
|
1477
|
+
for (const g of globs) if (g.startsWith('!')) return splitNegations(globs).positive
|
|
1478
|
+
return globs
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1387
1481
|
/** One task's inputs (`reads`) or outputs in one namespace, for `detectInputOverlaps`. */
|
|
1388
1482
|
interface Side {
|
|
1389
1483
|
node: TaskNode
|
|
@@ -1431,8 +1525,8 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
|
|
|
1431
1525
|
for (const n of nodes.values()) {
|
|
1432
1526
|
const cache = n.config.cache
|
|
1433
1527
|
if (cache === undefined) continue
|
|
1434
|
-
const outFiles =
|
|
1435
|
-
const outWs =
|
|
1528
|
+
const outFiles = positives(cache.outputs.files)
|
|
1529
|
+
const outWs = positives(cache.outputs.workspaceFiles ?? [])
|
|
1436
1530
|
if (outFiles.length > 0) {
|
|
1437
1531
|
bucket(n.projectName).push({ node: n, globs: outFiles, reads: false })
|
|
1438
1532
|
filesWriters.push(n)
|
|
@@ -1443,8 +1537,8 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
|
|
|
1443
1537
|
wsWriters.push(side)
|
|
1444
1538
|
}
|
|
1445
1539
|
if (isGroupTask(n)) continue
|
|
1446
|
-
const inFiles =
|
|
1447
|
-
const inWs =
|
|
1540
|
+
const inFiles = positives(cache.inputs.files)
|
|
1541
|
+
const inWs = positives(cache.inputs.workspaceFiles ?? [])
|
|
1448
1542
|
if (inFiles.length > 0) {
|
|
1449
1543
|
bucket(n.projectName).push({ node: n, globs: inFiles, reads: true })
|
|
1450
1544
|
filesReaders.push(n)
|
|
@@ -1471,7 +1565,7 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
|
|
|
1471
1565
|
const sides = [...wsWriters]
|
|
1472
1566
|
for (const node of filesReaders) {
|
|
1473
1567
|
const rel = relPosix(workspaceRoot, node.projectDir)
|
|
1474
|
-
const globs =
|
|
1568
|
+
const globs = positives(node.config.cache!.inputs.files).map((g) =>
|
|
1475
1569
|
rel === '' ? g : `${rel}/${g}`,
|
|
1476
1570
|
)
|
|
1477
1571
|
sides.push({ node, globs, reads: true, rel })
|
|
@@ -1488,7 +1582,7 @@ function detectInputOverlaps(nodes: Map<string, TaskNode>, workspaceRoot?: strin
|
|
|
1488
1582
|
if (workspaceRoot !== undefined) {
|
|
1489
1583
|
for (const node of filesWriters) {
|
|
1490
1584
|
const rel = relPosix(workspaceRoot, node.projectDir)
|
|
1491
|
-
const globs =
|
|
1585
|
+
const globs = positives(node.config.cache!.outputs.files).map((g) =>
|
|
1492
1586
|
rel === '' ? g : `${rel}/${g}`,
|
|
1493
1587
|
)
|
|
1494
1588
|
rooted.push({ node, globs, reads: false, rel })
|