@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.614",
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.614",
25
- "@vzn/vx-linux-arm64": "0.0.614",
26
- "@vzn/vx-darwin-x64": "0.0.614",
27
- "@vzn/vx-darwin-arm64": "0.0.614"
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"
@@ -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 = splitNegations(cache.outputs.files).positive
1435
- const outWs = splitNegations(cache.outputs.workspaceFiles ?? []).positive
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 = splitNegations(cache.inputs.files).positive
1447
- const inWs = splitNegations(cache.inputs.workspaceFiles ?? []).positive
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 = splitNegations(node.config.cache!.inputs.files).positive.map((g) =>
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 = splitNegations(node.config.cache!.outputs.files).positive.map((g) =>
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 })