@vzn/vx 0.0.589 → 0.0.591
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.591",
|
|
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.591",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.591",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.591",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.591"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/select.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
claimedAffected,
|
|
32
32
|
fingerprintClaims,
|
|
33
33
|
gitOfDiscovery,
|
|
34
|
+
hasHook,
|
|
34
35
|
isDefaultBuild,
|
|
35
36
|
keepDiscoveryGraph,
|
|
36
37
|
} from '../orchestrator/index.js'
|
|
@@ -300,6 +301,15 @@ export async function resolveFilters(
|
|
|
300
301
|
if (f.raw === affected) {
|
|
301
302
|
changes = await affectedChanges(args)
|
|
302
303
|
names = changes.projects
|
|
304
|
+
// A `graph` plugin may tie any task to a changed path (an edge, an
|
|
305
|
+
// input), and the run alone builds that graph: every project is a
|
|
306
|
+
// candidate, and the run keeps the tasks its final graph reaches.
|
|
307
|
+
if (
|
|
308
|
+
changes.changed.length > 0 &&
|
|
309
|
+
hasHook((await loadCliWorkspace(root)).plugins, 'graph')
|
|
310
|
+
) {
|
|
311
|
+
names = new Set(projects.map((p) => p.name))
|
|
312
|
+
}
|
|
303
313
|
} else {
|
|
304
314
|
names = await affectedProjects(args)
|
|
305
315
|
}
|
|
@@ -88,8 +88,13 @@ export function affectedRoots(
|
|
|
88
88
|
return false
|
|
89
89
|
}
|
|
90
90
|
const seeded = (n: TaskNode): boolean => {
|
|
91
|
-
if (!changes.projects.has(n.projectName)) return false
|
|
92
91
|
const cache = n.config.cache
|
|
92
|
+
// Asked of every node, not only the changed projects' (the
|
|
93
|
+
// `workspaceFiles` owners): a `graph` hook may have given the glob.
|
|
94
|
+
if (cache !== undefined && changes.changed.some((rel) => declaresInput(cache, null, rel))) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (!changes.projects.has(n.projectName)) return false
|
|
93
98
|
// A group runs nothing; only the default `build` (projects.ts) is keyed.
|
|
94
99
|
// An uncached task reads its project: a root file another task of it
|
|
95
100
|
// declares (`workspaceFiles`) is no change there.
|
|
@@ -97,10 +102,7 @@ export function affectedRoots(
|
|
|
97
102
|
return !isGroupTask(n) && (whole.has(n.projectName) || changes.paths.has(n.projectName))
|
|
98
103
|
}
|
|
99
104
|
if (whole.has(n.projectName)) return true
|
|
100
|
-
|
|
101
|
-
return true
|
|
102
|
-
}
|
|
103
|
-
return changes.changed.some((rel) => declaresInput(cache, null, rel))
|
|
105
|
+
return (changes.paths.get(n.projectName) ?? []).some((rel) => declaresInput(cache, rel, null))
|
|
104
106
|
}
|
|
105
107
|
const reached = new Map<string, boolean>()
|
|
106
108
|
const reaches = (id: string): boolean => {
|
|
@@ -294,7 +294,11 @@ export interface ProjectHookContext extends BaseContext {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
export interface GraphHookContext extends BaseContext {
|
|
297
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Task ids the user asked for (the rest were pulled in by `dependsOn`).
|
|
299
|
+
* Under `--affected` this is every candidate: the selection reads the
|
|
300
|
+
* graph the hooks leave and cuts it down afterwards.
|
|
301
|
+
*/
|
|
298
302
|
readonly requested: readonly string[]
|
|
299
303
|
}
|
|
300
304
|
|
|
@@ -521,9 +521,43 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
521
521
|
: {}),
|
|
522
522
|
})
|
|
523
523
|
let nodes = graphOf(requested)
|
|
524
|
+
const hasGraphHook = hasHook(plugins, 'graph')
|
|
525
|
+
const graphStage = async (): Promise<void> => {
|
|
526
|
+
// A hook edits each node's config in place, as `project` does, so the
|
|
527
|
+
// edit is held to what the loader accepts from a user: a misspelled
|
|
528
|
+
// `exec` field ran as an empty command and failed with no reason.
|
|
529
|
+
const configPaths = new Map(projectMetas.map((m) => [m.name, m.configPath]))
|
|
530
|
+
await applyGraphHooks(
|
|
531
|
+
plugins,
|
|
532
|
+
nodes,
|
|
533
|
+
{
|
|
534
|
+
workspaceRoot,
|
|
535
|
+
cacheDir,
|
|
536
|
+
warn: (m) => log.status(m),
|
|
537
|
+
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
538
|
+
},
|
|
539
|
+
(plugin) => {
|
|
540
|
+
for (const n of nodes.values()) {
|
|
541
|
+
if (isDefaultBuild(n.config)) continue
|
|
542
|
+
const where = configPaths.get(n.projectName) ?? `${n.projectName} (no config file)`
|
|
543
|
+
validateProjectConfig(
|
|
544
|
+
{ tasks: { [n.taskName]: n.config } },
|
|
545
|
+
`${where} (after plugin '${plugin.name}')`,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
workspaceConfig?.rules,
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
// `--affected` selects over the FINAL graph: an edge or an input a
|
|
553
|
+
// `graph` hook adds moves a key, so the hooks run first, over every
|
|
554
|
+
// candidate, and the selection prunes what they left.
|
|
555
|
+
const hooksFirst = hasGraphHook && options.affected !== undefined
|
|
524
556
|
// `--affected` keeps a bare request only when the diff reaches its
|
|
525
557
|
// closure; a `pkg#task` the user named runs as it always does.
|
|
526
558
|
if (options.affected !== undefined) {
|
|
559
|
+
const before = hooksFirst ? new Set(nodes.keys()) : null
|
|
560
|
+
if (hooksFirst) await graphStage()
|
|
527
561
|
const named = new Set(tasks.filter((t) => t.includes('#')))
|
|
528
562
|
const outright = new Set(options.selectedOutright)
|
|
529
563
|
const ids = requested.map((r) => `${r.project}#${r.task}`)
|
|
@@ -532,7 +566,20 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
532
566
|
(r, i) => named.has(ids[i]!) || outright.has(r.project) || reached.has(ids[i]!),
|
|
533
567
|
)
|
|
534
568
|
if (kept.length === 0) return emptyRun('none-affected')
|
|
535
|
-
if (
|
|
569
|
+
if (before !== null) {
|
|
570
|
+
const asked = new Set(ids)
|
|
571
|
+
// What a hook added or asked for is the plugin's choice, kept as it
|
|
572
|
+
// would be on a run without the diff.
|
|
573
|
+
const roots = [
|
|
574
|
+
...kept.map((r) => `${r.project}#${r.task}`),
|
|
575
|
+
...[...nodes.values()]
|
|
576
|
+
.filter((n) => !before.has(n.id) || (n.requested && !asked.has(n.id)))
|
|
577
|
+
.map((n) => n.id),
|
|
578
|
+
]
|
|
579
|
+
pruneToClosure(nodes, new Set(roots))
|
|
580
|
+
for (let i = unproven.length - 1; i >= 0; i--)
|
|
581
|
+
if (!nodes.has(unproven[i]![0])) unproven.splice(i, 1)
|
|
582
|
+
} else if (kept.length < requested.length) {
|
|
536
583
|
unproven.length = 0
|
|
537
584
|
nodes = graphOf(kept)
|
|
538
585
|
}
|
|
@@ -605,33 +652,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
605
652
|
localCache,
|
|
606
653
|
)
|
|
607
654
|
mark('git enumeration')
|
|
608
|
-
if (
|
|
609
|
-
// A hook edits each node's config in place, as `project` does, so the
|
|
610
|
-
// edit is held to what the loader accepts from a user: a misspelled
|
|
611
|
-
// `exec` field ran as an empty command and failed with no reason.
|
|
612
|
-
const configPaths = new Map(projectMetas.map((m) => [m.name, m.configPath]))
|
|
613
|
-
await applyGraphHooks(
|
|
614
|
-
plugins,
|
|
615
|
-
nodes,
|
|
616
|
-
{
|
|
617
|
-
workspaceRoot,
|
|
618
|
-
cacheDir,
|
|
619
|
-
warn: (m) => log.status(m),
|
|
620
|
-
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
621
|
-
},
|
|
622
|
-
(plugin) => {
|
|
623
|
-
for (const n of nodes.values()) {
|
|
624
|
-
if (isDefaultBuild(n.config)) continue
|
|
625
|
-
const where = configPaths.get(n.projectName) ?? `${n.projectName} (no config file)`
|
|
626
|
-
validateProjectConfig(
|
|
627
|
-
{ tasks: { [n.taskName]: n.config } },
|
|
628
|
-
`${where} (after plugin '${plugin.name}')`,
|
|
629
|
-
)
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
workspaceConfig?.rules,
|
|
633
|
-
)
|
|
634
|
-
}
|
|
655
|
+
if (hasGraphHook && !hooksFirst) await graphStage()
|
|
635
656
|
// Args after `--` go to requested commands only; with none among them
|
|
636
657
|
// they reached nothing and the run passed, the `--watch` unheard.
|
|
637
658
|
if ((options.forwardArgs?.length ?? 0) > 0) {
|
|
@@ -767,3 +788,23 @@ async function refuseUndeclaredDeps(
|
|
|
767
788
|
if (!declared.has(name)) throw undeclaredDepsError(id, name)
|
|
768
789
|
}
|
|
769
790
|
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Drops every node outside the `dependsOn` closure of `roots`, and demotes a
|
|
794
|
+
* requested one only a kept task pulls in: what a rebuild from the kept
|
|
795
|
+
* requests would hold, with the `graph` hooks' edits kept.
|
|
796
|
+
*/
|
|
797
|
+
function pruneToClosure(nodes: Map<string, TaskNode>, roots: ReadonlySet<string>): void {
|
|
798
|
+
const keep = new Set<string>()
|
|
799
|
+
const stack = [...roots]
|
|
800
|
+
while (stack.length > 0) {
|
|
801
|
+
const id = stack.pop()!
|
|
802
|
+
if (keep.has(id)) continue
|
|
803
|
+
keep.add(id)
|
|
804
|
+
stack.push(...nodes.get(id)!.deps)
|
|
805
|
+
}
|
|
806
|
+
for (const [id, n] of nodes) {
|
|
807
|
+
if (!keep.has(id)) nodes.delete(id)
|
|
808
|
+
else if (!roots.has(id)) n.requested = false
|
|
809
|
+
}
|
|
810
|
+
}
|
|
@@ -663,6 +663,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
663
663
|
`let cleanOutputs delete files outside it)`,
|
|
664
664
|
)
|
|
665
665
|
}
|
|
666
|
+
assertPosixGlob(g, `${where}.cache.outputs.files`, 'project')
|
|
666
667
|
assertNoForeignToken(g, `${where}.cache.outputs.files`)
|
|
667
668
|
if (namesDirItself(g)) {
|
|
668
669
|
throw new UserError(
|
|
@@ -714,6 +715,7 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
714
715
|
`use cache.inputs.workspaceFiles for workspace-root-relative inputs)`,
|
|
715
716
|
)
|
|
716
717
|
}
|
|
718
|
+
assertPosixGlob(g, `${where}.cache.inputs.files`, 'project')
|
|
717
719
|
assertNoForeignToken(g, `${where}.cache.inputs.files`)
|
|
718
720
|
if (namesDirItself(g)) {
|
|
719
721
|
throw new UserError(
|
|
@@ -1196,6 +1198,39 @@ function isAbsoluteGlob(glob: string): boolean {
|
|
|
1196
1198
|
return g.startsWith('/') || (g.includes('{') && /[{,]\//.test(g))
|
|
1197
1199
|
}
|
|
1198
1200
|
|
|
1201
|
+
const DRIVE = /(^|[{,])[A-Za-z]:[\\/]+/g
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* True when a backslash in the glob is a Windows separator. One before a
|
|
1205
|
+
* bracket, a brace, a `!` or a backslash is an escape (`app/\[id\]`,
|
|
1206
|
+
* `\{b\}`); one before anything else is a separator — `a\*b` reads as
|
|
1207
|
+
* `a/*b` on Windows far more often than as a file named with a star, which
|
|
1208
|
+
* `a?b` still selects.
|
|
1209
|
+
*/
|
|
1210
|
+
function hasBackslashSeparator(body: string): boolean {
|
|
1211
|
+
for (let i = body.indexOf('\\'); i !== -1; i = body.indexOf('\\', i + 2)) {
|
|
1212
|
+
if (!'[]{}!\\'.includes(body[i + 1] ?? '/')) return true
|
|
1213
|
+
}
|
|
1214
|
+
return false
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* A Windows spelling (`src\**`, `C:/src/**`) loaded with only a "matched no
|
|
1219
|
+
* files" warning, and a task keyed on it never saw its source (X-67).
|
|
1220
|
+
*/
|
|
1221
|
+
function assertPosixGlob(glob: string, where: string, base: string): void {
|
|
1222
|
+
const neg = glob.startsWith('!') ? '!' : ''
|
|
1223
|
+
const body = glob.slice(neg.length)
|
|
1224
|
+
DRIVE.lastIndex = 0
|
|
1225
|
+
const drive = DRIVE.test(body)
|
|
1226
|
+
if (!drive && !hasBackslashSeparator(body)) return
|
|
1227
|
+
const write = neg + body.replace(DRIVE, '$1').replace(/\\(?![[\]{}!\\])/g, '/')
|
|
1228
|
+
throw new UserError(
|
|
1229
|
+
`${where}: "${glob}" ${drive ? 'starts with a Windows drive' : 'uses a backslash as a path separator'} — ` +
|
|
1230
|
+
`task globs are ${base}-relative with forward slashes: write "${write}"`,
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1199
1234
|
/**
|
|
1200
1235
|
* `!!x` INVERTS the input set instead of double-negating it.
|
|
1201
1236
|
*
|
|
@@ -1405,6 +1440,7 @@ function validateWorkspaceGlobs(v: unknown, where: string, negation: boolean): v
|
|
|
1405
1440
|
`entries are workspace-root-relative and must stay within the workspace root`,
|
|
1406
1441
|
)
|
|
1407
1442
|
}
|
|
1443
|
+
assertPosixGlob(g, where, 'workspace-root')
|
|
1408
1444
|
assertNoForeignToken(g, where)
|
|
1409
1445
|
if (namesDirItself(g)) {
|
|
1410
1446
|
throw new UserError(
|