@vzn/vx 0.0.23 → 0.0.25
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.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.25",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.25",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.25",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.25"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch.ts
CHANGED
|
@@ -549,11 +549,13 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
549
549
|
// scope is the one resolved at start; a new package joins it only as a
|
|
550
550
|
// dependency of it.
|
|
551
551
|
rediscover: async () => {
|
|
552
|
-
const
|
|
552
|
+
const workspace = await loadWorkspace(workspaceRoot)
|
|
553
|
+
const all = await listProjects(workspace)
|
|
553
554
|
const sweep = await sweepConfigs(all, workspaceRoot, load)
|
|
554
555
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
555
556
|
return {
|
|
556
557
|
projects: now,
|
|
558
|
+
memberBases: memberBaseDirs(workspace),
|
|
557
559
|
workspaceWide: sweep.workspaceWide,
|
|
558
560
|
workspaceInputs: sweep.workspaceInputs,
|
|
559
561
|
outputs: sweep.outputs,
|
|
@@ -762,7 +764,10 @@ export function makeRootEventFilter(
|
|
|
762
764
|
// this line load-bearing.
|
|
763
765
|
if (
|
|
764
766
|
!rel.includes('/') &&
|
|
765
|
-
(isWorkspaceFingerprintFile(rel) ||
|
|
767
|
+
(isWorkspaceFingerprintFile(rel) ||
|
|
768
|
+
isWorkspaceConfigFile(rel) ||
|
|
769
|
+
rel === 'package.json' ||
|
|
770
|
+
claimedRootFiles.has(rel))
|
|
766
771
|
) {
|
|
767
772
|
return true
|
|
768
773
|
}
|
|
@@ -804,7 +809,7 @@ interface WatchLoopArgs {
|
|
|
804
809
|
* loaded once per process, so the set is fixed for the loop.
|
|
805
810
|
*/
|
|
806
811
|
claimedRootFiles: ReadonlySet<string>
|
|
807
|
-
/** The directory each `<dir>/*` package glob names; a member coming or going there is a cycle. */
|
|
812
|
+
/** The directory each `<dir>/*` package glob names at start; a member coming or going there is a cycle. */
|
|
808
813
|
memberBases: readonly string[]
|
|
809
814
|
/** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
|
|
810
815
|
packageDirs: ReadonlySet<string>
|
|
@@ -814,6 +819,7 @@ interface WatchLoopArgs {
|
|
|
814
819
|
|
|
815
820
|
interface Rediscovered {
|
|
816
821
|
projects: readonly ProjectMeta[]
|
|
822
|
+
memberBases: readonly string[]
|
|
817
823
|
workspaceWide: boolean
|
|
818
824
|
workspaceInputs: readonly string[]
|
|
819
825
|
outputs: ReadonlyMap<string, readonly string[]>
|
|
@@ -833,21 +839,29 @@ const PROJECT_CONFIGS: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
|
|
|
833
839
|
* watcher, a new output must stop being an event) or the workspace config
|
|
834
840
|
* (a plugin's `project` stage). Until item 891 only a member directory
|
|
835
841
|
* coming or going re-read the set, so each of these waited for a restart
|
|
836
|
-
* while the loop looked alive.
|
|
842
|
+
* while the loop looked alive. A root fingerprint file too, for
|
|
843
|
+
* `pnpm-workspace.yaml`: a glob added there was a cycle that ran the new
|
|
844
|
+
* packages and watched none of them (item 1018).
|
|
837
845
|
*/
|
|
838
846
|
function shapesWatchedSet(filename: string): boolean {
|
|
839
847
|
const base = path.basename(filename)
|
|
840
|
-
return
|
|
848
|
+
return (
|
|
849
|
+
base === 'package.json' ||
|
|
850
|
+
isWorkspaceFingerprintFile(base) ||
|
|
851
|
+
PROJECT_CONFIGS.has(base) ||
|
|
852
|
+
isWorkspaceConfigFile(base)
|
|
853
|
+
)
|
|
841
854
|
}
|
|
842
855
|
|
|
843
856
|
const CLOSED: WatchHandle = { close() {} }
|
|
844
857
|
|
|
845
858
|
async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
846
|
-
const { opts, stop, workspaceRoot, projects, cacheDir
|
|
859
|
+
const { opts, stop, workspaceRoot, projects, cacheDir } = args
|
|
847
860
|
// The watched set as of the last cycle: `rearm` replaces these after an
|
|
848
861
|
// event that can change it, and every filter below reads the current one.
|
|
849
862
|
let workspaceWide = args.workspaceWide
|
|
850
863
|
let packageDirs = args.packageDirs
|
|
864
|
+
let memberBases = args.memberBases
|
|
851
865
|
let projectDirs = args.projectDirs
|
|
852
866
|
let workspaceInputs = args.workspaceInputs
|
|
853
867
|
let outputs = args.outputs
|
|
@@ -1185,6 +1199,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1185
1199
|
if (
|
|
1186
1200
|
isWorkspaceFingerprintFile(filename) ||
|
|
1187
1201
|
isWorkspaceConfigFile(filename) ||
|
|
1202
|
+
filename === 'package.json' ||
|
|
1188
1203
|
args.claimedRootFiles.has(filename)
|
|
1189
1204
|
) {
|
|
1190
1205
|
if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
|
|
@@ -1334,6 +1349,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1334
1349
|
configImportFiles = next.configImports
|
|
1335
1350
|
wsConfigImportFiles = next.workspaceConfigImports
|
|
1336
1351
|
packageDirs = next.packageDirs
|
|
1352
|
+
memberBases = next.memberBases
|
|
1353
|
+
armBases()
|
|
1337
1354
|
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
1338
1355
|
matters = makeRootEventFilter(
|
|
1339
1356
|
workspaceRoot,
|
|
@@ -1373,28 +1390,45 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1373
1390
|
// triggers re-reads the workspace (`rearm`) so the new package's own
|
|
1374
1391
|
// edits are cycles from then on. Until 2026-09-10 the watched set was
|
|
1375
1392
|
// fixed when the loop armed: the next cycle ran the new package, and
|
|
1376
|
-
// every edit inside it after that was silence.
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1393
|
+
// every edit inside it after that was silence. The bases themselves are
|
|
1394
|
+
// re-read with the set: a glob added to the list watched nothing new
|
|
1395
|
+
// until a restart (item 1018).
|
|
1396
|
+
const baseArms = new Map<string, WatchHandle>()
|
|
1397
|
+
const armBases = (): void => {
|
|
1398
|
+
const want = new Set(memberBases)
|
|
1399
|
+
for (const [base, handle] of baseArms) {
|
|
1400
|
+
if (want.has(base)) continue
|
|
1401
|
+
handle.close()
|
|
1402
|
+
baseArms.delete(base)
|
|
1403
|
+
}
|
|
1404
|
+
for (const base of want) {
|
|
1405
|
+
if (baseArms.has(base)) continue
|
|
1406
|
+
let members = memberEntries(base)
|
|
1407
|
+
try {
|
|
1408
|
+
baseArms.set(
|
|
1409
|
+
base,
|
|
1410
|
+
arm(base, false, (filename) => {
|
|
1411
|
+
if (isIgnoredWatchPath(filename)) return
|
|
1412
|
+
// Only a member coming or going. On macOS a non-recursive watcher
|
|
1413
|
+
// also reports a member whose CONTENTS changed (FSEvents names the
|
|
1414
|
+
// directory a write landed in), so a task writing into its own
|
|
1415
|
+
// project — or the arm's own probe file — read as a member event
|
|
1416
|
+
// and cost an uncached task one execution per cycle (CI,
|
|
1417
|
+
// 2026-09-10).
|
|
1418
|
+
const now = memberEntries(base)
|
|
1419
|
+
if (sameMembers(members, now)) return
|
|
1420
|
+
members = now
|
|
1421
|
+
reread = true
|
|
1422
|
+
trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
|
|
1423
|
+
}),
|
|
1424
|
+
)
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1427
|
+
process.stderr.write(`vx watch: cannot watch ${base}: ${msg}\n`)
|
|
1428
|
+
}
|
|
1396
1429
|
}
|
|
1397
1430
|
}
|
|
1431
|
+
armBases()
|
|
1398
1432
|
armPending(false)
|
|
1399
1433
|
armImports()
|
|
1400
1434
|
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface TaskNode {
|
|
|
30
30
|
config: TaskConfig
|
|
31
31
|
/** Ids of tasks that must complete before this one runs. */
|
|
32
32
|
deps: string[]
|
|
33
|
+
/**
|
|
34
|
+
* The members of `deps` that order the run and fold into no key:
|
|
35
|
+
* `--exclude-dependencies` dropped a task between this one and them
|
|
36
|
+
* (`excludeDependencies`). The dropped task's derived key already folds
|
|
37
|
+
* theirs, so folding them again would give a key no full run derives.
|
|
38
|
+
*/
|
|
39
|
+
orderOnly?: string[]
|
|
33
40
|
/**
|
|
34
41
|
* True for the tasks the user actually asked for (via cwd, `--all`,
|
|
35
42
|
* `--filter`, or `pkg#task`). False for deps pulled in by `dependsOn`
|
|
@@ -588,6 +595,27 @@ export function excludeDependencies(
|
|
|
588
595
|
if (lost.length === 0) continue
|
|
589
596
|
dropped.set(id, lost)
|
|
590
597
|
node.deps = node.deps.filter(stays)
|
|
598
|
+
// A scheduled task reached through a dropped one still runs first: in
|
|
599
|
+
// `test → gen → build` with `gen` excluded and `build` requested, the
|
|
600
|
+
// edge to `gen` went and nothing ordered `test` after `build`, so the
|
|
601
|
+
// two ran at once (item 1019). The walk reads unscheduled nodes only,
|
|
602
|
+
// whose deps this loop leaves as they are.
|
|
603
|
+
const order = new Set<string>()
|
|
604
|
+
const seen = new Set<string>()
|
|
605
|
+
const stack = [...lost]
|
|
606
|
+
while (stack.length > 0) {
|
|
607
|
+
const dep = stack.pop()!
|
|
608
|
+
if (seen.has(dep)) continue
|
|
609
|
+
seen.add(dep)
|
|
610
|
+
if (scheduled.has(dep)) {
|
|
611
|
+
if (!node.deps.includes(dep)) order.add(dep)
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
stack.push(...nodes.get(dep)!.deps)
|
|
615
|
+
}
|
|
616
|
+
if (order.size === 0) continue
|
|
617
|
+
node.orderOnly = [...order].sort()
|
|
618
|
+
node.deps = [...node.deps, ...node.orderOnly].sort()
|
|
591
619
|
}
|
|
592
620
|
for (const id of keyOnly.keys()) nodes.delete(id)
|
|
593
621
|
return { keyOnly, dropped }
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import type { CacheLayer, GitFilesCache } from '../cache/index.js'
|
|
17
17
|
import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
18
18
|
import { computeGroupHash, computeTaskHash, type HashCache } from './task-hash.js'
|
|
19
|
-
import { filterUpstreamHashes } from './upstream.js'
|
|
19
|
+
import { filterUpstreamHashes, keyedDeps } from './upstream.js'
|
|
20
20
|
|
|
21
21
|
export interface KeyExcludedArgs {
|
|
22
22
|
/** The scheduled graph, after `excludeDependencies`. */
|
|
@@ -41,7 +41,7 @@ export interface KeyExcludedArgs {
|
|
|
41
41
|
export async function keyExcludedDependencies(args: KeyExcludedArgs): Promise<void> {
|
|
42
42
|
const nodeOf = (id: string): TaskNode => args.nodes.get(id) ?? args.keyOnly.get(id)!
|
|
43
43
|
const fullDeps = (node: TaskNode): readonly string[] => [
|
|
44
|
-
...node
|
|
44
|
+
...keyedDeps(node),
|
|
45
45
|
...(args.dropped.get(node.id) ?? []),
|
|
46
46
|
]
|
|
47
47
|
// Each key's promise is made after its dependencies' promises, in a
|
|
@@ -17,7 +17,15 @@ import { UserError } from '../util/index.js'
|
|
|
17
17
|
* a key at one of them and not the others.
|
|
18
18
|
*/
|
|
19
19
|
export function keyUpstream(node: TaskNode, upstream: TaskOutcome[]): TaskOutcome[] {
|
|
20
|
-
|
|
20
|
+
const order = node.orderOnly
|
|
21
|
+
const keyed = order === undefined ? upstream : upstream.filter((o) => !order.includes(o.node.id))
|
|
22
|
+
return node.excludedUpstream === undefined ? keyed : [...keyed, ...node.excludedUpstream]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `node.deps` less its ordering-only edges: the dependencies a key may fold. */
|
|
26
|
+
export function keyedDeps(node: TaskNode): readonly string[] {
|
|
27
|
+
const order = node.orderOnly
|
|
28
|
+
return order === undefined ? node.deps : node.deps.filter((d) => !order.includes(d))
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/**
|