@vzn/vx 0.0.132 → 0.0.134
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.134",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
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.134",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.134",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.134",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.134"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/plan-format.ts
CHANGED
|
@@ -96,11 +96,25 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
96
96
|
return lines.join('\n') + '\n'
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/** One task of `vx run --dry=json`: the wire, stated by `schemas/plan.json`. */
|
|
100
|
+
export interface PlanTaskJson {
|
|
101
|
+
id: string
|
|
102
|
+
project: string
|
|
103
|
+
task: string
|
|
104
|
+
hash: string
|
|
105
|
+
cacheStatus: CacheStatus
|
|
106
|
+
deps: readonly string[]
|
|
107
|
+
p50Ms?: number
|
|
108
|
+
executor?: string
|
|
109
|
+
download?: 'deferred'
|
|
110
|
+
description?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
export function formatPlanJson(plan: RunPlan): string {
|
|
100
114
|
return (
|
|
101
115
|
JSON.stringify(
|
|
102
116
|
{
|
|
103
|
-
tasks: plan.tasks.map((t) => ({
|
|
117
|
+
tasks: plan.tasks.map((t): PlanTaskJson => ({
|
|
104
118
|
id: t.node.id,
|
|
105
119
|
project: t.node.projectName,
|
|
106
120
|
task: t.node.taskName,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// selection the hash path applies (`selectFoldedDeps`).
|
|
7
7
|
|
|
8
8
|
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
9
|
-
import { selectFoldedDeps, type FoldCandidate } from './upstream.js'
|
|
9
|
+
import { keyedDeps, selectFoldedDeps, type FoldCandidate } from './upstream.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* A run's keyed-set lookup: for a task, the directories of every project
|
|
@@ -31,7 +31,9 @@ import { selectFoldedDeps, type FoldCandidate } from './upstream.js'
|
|
|
31
31
|
*/
|
|
32
32
|
export function keyedProjects(
|
|
33
33
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
34
|
+
keyOnly: ReadonlyMap<string, TaskNode> = new Map(),
|
|
34
35
|
): (node: TaskNode) => ReadonlySet<string> {
|
|
36
|
+
const nodeOf = (id: string): TaskNode => nodes.get(id) ?? keyOnly.get(id)!
|
|
35
37
|
const below = new Map<string, ReadonlySet<string>>()
|
|
36
38
|
// Post-order on an explicit stack: a fold is as deep as the graph, and a
|
|
37
39
|
// recursion per edge threw `RangeError` at the 50,000 the builder takes
|
|
@@ -47,7 +49,7 @@ export function keyedProjects(
|
|
|
47
49
|
continue
|
|
48
50
|
}
|
|
49
51
|
if (deps === undefined) {
|
|
50
|
-
frame[1] = folded(node,
|
|
52
|
+
frame[1] = folded(node, nodeOf)
|
|
51
53
|
for (const { node: dep } of frame[1]) if (!below.has(dep.id)) stack.push([dep, undefined])
|
|
52
54
|
continue
|
|
53
55
|
}
|
|
@@ -63,11 +65,22 @@ export function keyedProjects(
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The dependencies `node`'s key folds, per the hash path's rules above, as
|
|
70
|
+
* `keyUpstream` hands them to the key: the scheduled ones less the edges
|
|
71
|
+
* `--exclude-dependencies` left for order alone, plus the dropped ones it
|
|
72
|
+
* keys all the same (`excludedUpstream`), whose own dependencies the walk
|
|
73
|
+
* reads from the run's `keyOnly` map. `node.deps` held the order-only
|
|
74
|
+
* edges and lacked the dropped ones: the sandbox granted a sibling the key
|
|
75
|
+
* does not answer for.
|
|
76
|
+
*/
|
|
77
|
+
function folded(node: TaskNode, nodeOf: (id: string) => TaskNode): FoldCandidate[] {
|
|
68
78
|
const candidates: FoldCandidate[] = []
|
|
69
|
-
for (const id of node
|
|
70
|
-
const dep =
|
|
79
|
+
for (const id of keyedDeps(node)) {
|
|
80
|
+
const dep = nodeOf(id)
|
|
81
|
+
candidates.push({ node: dep, unit: foldUnit(dep) })
|
|
82
|
+
}
|
|
83
|
+
for (const { node: dep } of node.excludedUpstream ?? []) {
|
|
71
84
|
candidates.push({ node: dep, unit: foldUnit(dep) })
|
|
72
85
|
}
|
|
73
86
|
if (isGroupTask(node)) return candidates
|
|
@@ -89,6 +89,11 @@ export interface PreparedRun {
|
|
|
89
89
|
*/
|
|
90
90
|
priorities: ReadonlyMap<string, number>
|
|
91
91
|
nodes: Map<string, TaskNode>
|
|
92
|
+
/**
|
|
93
|
+
* The tasks `--exclude-dependencies` took out of the schedule: keyed as a
|
|
94
|
+
* full run keys them, never run. Empty without the flag.
|
|
95
|
+
*/
|
|
96
|
+
keyOnly: ReadonlyMap<string, TaskNode>
|
|
92
97
|
/**
|
|
93
98
|
* Requested task specs that matched NO project — a typo, or a stray
|
|
94
99
|
* positional (the value of an `=`-only flag written with a space).
|
|
@@ -391,6 +396,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
391
396
|
hasRemoteLayer,
|
|
392
397
|
priorities: new Map(),
|
|
393
398
|
nodes: new Map(),
|
|
399
|
+
keyOnly: new Map(),
|
|
394
400
|
unresolvedTasks,
|
|
395
401
|
projects,
|
|
396
402
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
@@ -469,8 +475,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
469
475
|
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (m) => log.status(m) })
|
|
470
476
|
}
|
|
471
477
|
const exclude = options.excludeDependencies
|
|
478
|
+
let keyOnly: ReadonlyMap<string, TaskNode> = new Map()
|
|
472
479
|
if (exclude !== undefined && (exclude === 'all' || exclude.length > 0)) {
|
|
473
|
-
const
|
|
480
|
+
const split = excludeDependencies(nodes, exclude)
|
|
481
|
+
keyOnly = split.keyOnly
|
|
482
|
+
const dropped = split.dropped
|
|
474
483
|
if (dropped.size > 0) {
|
|
475
484
|
await keyExcludedDependencies({
|
|
476
485
|
nodes,
|
|
@@ -506,6 +515,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
506
515
|
hasRemoteLayer,
|
|
507
516
|
priorities,
|
|
508
517
|
nodes,
|
|
518
|
+
keyOnly,
|
|
509
519
|
unresolvedTasks,
|
|
510
520
|
projects,
|
|
511
521
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -618,7 +618,7 @@ async function runOnBus(
|
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
const sandboxArmer = prepareSandbox(nodes.values())
|
|
621
|
-
const keyed = keyedProjects(nodes)
|
|
621
|
+
const keyed = keyedProjects(nodes, prepared.keyOnly)
|
|
622
622
|
const outputDirSnapshots: OutputDirSnapshot[] = []
|
|
623
623
|
// Saves run off the execution slot, twice the cap at once (memory:
|
|
624
624
|
// each pack holds an artifact's bytes); a failed save is a miss next
|
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
// the lockfile moves, which the workspace fingerprint already covers.
|
|
21
21
|
// A tsconfig `paths` / `baseUrl` alias is the exception: Bun loads it
|
|
22
22
|
// from disk (D-27).
|
|
23
|
-
// - Descend only through files owned by NO project
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
23
|
+
// - Descend only through files owned by NO project, or by a ROOT project
|
|
24
|
+
// (whose own files are the shared tooling that is otherwise unowned,
|
|
25
|
+
// D-41). A config reaching into another project (say a site's
|
|
26
|
+
// `vx.config.ts` importing `../core/src/index.ts`) records that edge
|
|
27
|
+
// and STOPS there — following it would drag substantially all of that
|
|
28
|
+
// project's `src/` into the closure, and the containment channel
|
|
29
|
+
// already selects the project that owns it.
|
|
29
30
|
|
|
30
31
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
31
32
|
import { realpath } from 'node:fs/promises'
|
|
@@ -452,6 +453,9 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
452
453
|
)
|
|
453
454
|
if (roots.size === 0) return selected
|
|
454
455
|
const dirToName = await realDirIndex(a.projects, a.realDirs)
|
|
456
|
+
// A root project owns every file no member owns: the shared tooling the
|
|
457
|
+
// walk descends through when the root is no project (D-41).
|
|
458
|
+
const rootOwner = dirToName.get(workspaceRoot)
|
|
455
459
|
|
|
456
460
|
// target → the files that import it. Reversed up front so one BFS from the
|
|
457
461
|
// changed set answers every root at once, instead of a walk per root.
|
|
@@ -484,8 +488,10 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
484
488
|
const list = importedBy.get(target)
|
|
485
489
|
if (list) list.push(file)
|
|
486
490
|
else importedBy.set(target, [file])
|
|
487
|
-
// Descend ONLY through unowned files
|
|
488
|
-
|
|
491
|
+
// Descend ONLY through unowned files, or the root project's own:
|
|
492
|
+
// see the header.
|
|
493
|
+
const owner = ownerOf(target, dirToName)
|
|
494
|
+
if (owner === undefined || owner === rootOwner) frontier.push(target)
|
|
489
495
|
}
|
|
490
496
|
}
|
|
491
497
|
}
|