@vzn/vx 0.0.597 → 0.0.599
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/bin.ts +9 -5
- package/src/cache/archive.ts +7 -4
- package/src/cache/cache.ts +40 -26
- package/src/cache/layered-cache.ts +14 -9
- package/src/cache/output-index.ts +12 -7
- package/src/cache/run-history.ts +16 -8
- package/src/cli/plan-format.ts +23 -17
- package/src/cli/select.ts +13 -2
- package/src/cli/show.ts +13 -25
- package/src/cli/watch-cycle.ts +43 -0
- package/src/cli/watch.ts +37 -5
- package/src/exec/kill-tree.ts +34 -4
- package/src/exec/runner.ts +150 -12
- package/src/exec/sandbox-runtime.ts +23 -9
- package/src/graph/priorities.ts +1 -2
- package/src/index.ts +3 -1
- package/src/orchestrator/doctor.ts +3 -1
- package/src/orchestrator/execute-task.ts +27 -3
- package/src/orchestrator/hit-restore.ts +1 -1
- package/src/orchestrator/lockfile-claim.ts +14 -7
- package/src/orchestrator/logger.ts +7 -4
- package/src/orchestrator/plugin-host.ts +7 -2
- package/src/orchestrator/plugin.ts +5 -2
- package/src/orchestrator/prepare.ts +4 -2
- package/src/orchestrator/projects.ts +1 -1
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-lock.ts +10 -1
- package/src/workspace/config-cache.ts +15 -1
- package/src/workspace/config-eval.ts +20 -1
- package/src/workspace/project-loader.ts +18 -4
|
@@ -15,7 +15,7 @@ import type { Cache, CacheLayer, CachePolicy } from '../cache/index.js'
|
|
|
15
15
|
import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
|
|
16
16
|
import type { TaskExecutor } from '../exec/index.js'
|
|
17
17
|
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
18
|
-
import { nearest, UserError } from '../util/index.js'
|
|
18
|
+
import { maskedLine, nearest, UserError } from '../util/index.js'
|
|
19
19
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
20
20
|
import type { EventBus, RunStartInfo } from './events.js'
|
|
21
21
|
import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
@@ -716,7 +716,10 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
716
716
|
for (const d of disposers) d()
|
|
717
717
|
throw new PluginSetupError(
|
|
718
718
|
plugin,
|
|
719
|
-
|
|
719
|
+
// Masked at the source, as plugin-host.ts's `safe()` does (L-11).
|
|
720
|
+
maskedLine(
|
|
721
|
+
`plugin '${plugin.name}' failed in setup: ${err instanceof Error ? err.message : String(err)}`,
|
|
722
|
+
),
|
|
720
723
|
)
|
|
721
724
|
}
|
|
722
725
|
}
|
|
@@ -379,9 +379,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
379
379
|
// it, and the run said no project did (C-3). Only what is still
|
|
380
380
|
// unjudged pays for the rest of the workspace.
|
|
381
381
|
unresolvedTasks = undeclaredIn(unresolvedTasks, projects)
|
|
382
|
+
// By name, not by count: a config-less project in the scope counts as
|
|
383
|
+
// loaded, and a changed one hid an unloaded `test` declarer (X-129).
|
|
382
384
|
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
+
unresolvedTasks.some((t) => !t.includes('#')) &&
|
|
386
|
+
projectsWithConfigs.some((m) => !projects.has(m.name))
|
|
385
387
|
) {
|
|
386
388
|
unresolvedTasks = await declaredNowhere(unresolvedTasks, () =>
|
|
387
389
|
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
@@ -321,7 +321,7 @@ const DEFAULT_BUILD: TaskConfig = Object.freeze({
|
|
|
321
321
|
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
322
322
|
outputs: Object.freeze({ files: Object.freeze([]) }),
|
|
323
323
|
}),
|
|
324
|
-
})
|
|
324
|
+
})
|
|
325
325
|
|
|
326
326
|
/** The default `build`, which a menu of the tasks a user declared leaves out. */
|
|
327
327
|
export function isDefaultBuild(task: TaskConfig | undefined): boolean {
|
|
@@ -379,7 +379,7 @@ function ciLinks(provider: string, env: Record<string, string | undefined>): CiL
|
|
|
379
379
|
default:
|
|
380
380
|
return {}
|
|
381
381
|
}
|
|
382
|
-
return Object.fromEntries(Object.entries(links).filter(([, x]) => x !== undefined))
|
|
382
|
+
return Object.fromEntries(Object.entries(links).filter(([, x]) => x !== undefined))
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
/** Host name (null on failure) + platform + arch. */
|
|
@@ -66,7 +66,8 @@ import { rmdirSync, unlinkSync } from 'node:fs'
|
|
|
66
66
|
import { mkdir, readdir, readFile, rename, rm, rmdir, unlink, writeFile } from 'node:fs/promises'
|
|
67
67
|
import os from 'node:os'
|
|
68
68
|
import path from 'node:path'
|
|
69
|
-
import {
|
|
69
|
+
import { VX_RUN_TASK_ENV, VX_RUN_WORKSPACE_ENV } from '../exec/index.js'
|
|
70
|
+
import { isTmpdirRefusal, procfsIsOwn, TMPDIR_HINT, UserError, xxh3hex } from '../util/index.js'
|
|
70
71
|
|
|
71
72
|
/** Runs in this process currently holding the lock, per lock directory. */
|
|
72
73
|
const heldHere = new Map<string, number>()
|
|
@@ -329,6 +330,14 @@ export async function acquireRunLock(
|
|
|
329
330
|
opts: RunLockOptions,
|
|
330
331
|
): Promise<() => Promise<void>> {
|
|
331
332
|
const lockDir = runLockPath(workspaceRoot, opts.dir)
|
|
333
|
+
// A task's own process (`vx cache prune` in a task) would wait for the
|
|
334
|
+
// run that started it, which holds the lock until that task ends.
|
|
335
|
+
const outer = process.env[VX_RUN_WORKSPACE_ENV]
|
|
336
|
+
if (outer !== undefined && runLockPath(outer, opts.dir) === lockDir) {
|
|
337
|
+
throw new UserError(
|
|
338
|
+
`this vx runs inside task ${process.env[VX_RUN_TASK_ENV] ?? '<unknown>'} of a vx run on this workspace, which holds its run lock until it ends — run it before or after that run, not from one of its tasks`,
|
|
339
|
+
)
|
|
340
|
+
}
|
|
332
341
|
const started = Date.now()
|
|
333
342
|
let said = false
|
|
334
343
|
const release = async (): Promise<void> => {
|
|
@@ -606,8 +606,12 @@ export function configEvalKeyFromIdentities(a: {
|
|
|
606
606
|
* `node_modules`, the config itself excluded. `vx watch` watches these: a
|
|
607
607
|
* shared preset outside the project (`../../shared/preset.mjs`, the way
|
|
608
608
|
* configs compose) changed what a run evaluates and no watcher saw it
|
|
609
|
-
* (item 949). A file that cannot be read or resolved ends its branch
|
|
609
|
+
* (item 949). A file that cannot be read or resolved ends its branch; one
|
|
610
|
+
* that cannot be resolved is still listed, by the name it would have.
|
|
610
611
|
*/
|
|
612
|
+
// The names an extensionless relative import may resolve to, as Bun tries them.
|
|
613
|
+
const IMPORT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json']
|
|
614
|
+
|
|
611
615
|
export async function configImports(configPath: string): Promise<string[]> {
|
|
612
616
|
const seen = new Set<string>([configPath])
|
|
613
617
|
const out: string[] = []
|
|
@@ -635,6 +639,16 @@ export async function configImports(configPath: string): Promise<string[]> {
|
|
|
635
639
|
dir ??= realDirOf(file)
|
|
636
640
|
resolved = realPath(Bun.resolveSync(spec, dir))
|
|
637
641
|
} catch {
|
|
642
|
+
// Missing for now, still imported: the config that names it fails
|
|
643
|
+
// to load until it is back, and a list without it left `vx watch`
|
|
644
|
+
// deaf to its return. Named as written, and not walked.
|
|
645
|
+
if (dir === undefined) continue
|
|
646
|
+
for (const ext of path.extname(spec) === '' ? IMPORT_EXTENSIONS : ['']) {
|
|
647
|
+
const missing = path.resolve(dir, spec + ext)
|
|
648
|
+
if (seen.has(missing)) continue
|
|
649
|
+
seen.add(missing)
|
|
650
|
+
out.push(missing)
|
|
651
|
+
}
|
|
638
652
|
continue
|
|
639
653
|
}
|
|
640
654
|
if (seen.has(resolved) || resolved.split(path.sep).includes('node_modules')) continue
|
|
@@ -103,6 +103,23 @@ const nonJsonPaths = ${nonJsonPaths.toString()}
|
|
|
103
103
|
throw new Error('process.exit(' + (code ?? '') + ') in a config: ' + ${JSON.stringify(CONFIG_EXIT)})
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
// Bun's resolver reads \\ as a separator, so a config under a\\b is served
|
|
107
|
+
// from the path as written (project-loader.ts's LITERAL_QUERY), read now.
|
|
108
|
+
let literal = false
|
|
109
|
+
const serveLiteral = () => {
|
|
110
|
+
if (literal) return
|
|
111
|
+
literal = true
|
|
112
|
+
globalThis.Bun.plugin({
|
|
113
|
+
name: 'vx-config-literal',
|
|
114
|
+
setup(build) {
|
|
115
|
+
build.onResolve({ filter: /\\?vx-literal$/ }, (args) => ({ path: args.path, namespace: 'file' }))
|
|
116
|
+
build.onLoad({ filter: /\\?vx-literal$/ }, async (args) => {
|
|
117
|
+
const file = args.path.slice(0, -'?vx-literal'.length)
|
|
118
|
+
return { contents: await globalThis.Bun.file(file).text(), loader: /\\.[cm]?ts$/.test(file) ? 'ts' : 'js' }
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
106
123
|
self.onmessage = async (e) => {
|
|
107
124
|
const { id, path, env } = e.data
|
|
108
125
|
// A Worker starts with the process's STARTUP environment, not the
|
|
@@ -155,7 +172,9 @@ self.onmessage = async (e) => {
|
|
|
155
172
|
if (umaskIn !== null && globalThis.process.umask() !== umaskIn) globalThis.process.umask(umaskIn)
|
|
156
173
|
}
|
|
157
174
|
try {
|
|
158
|
-
const
|
|
175
|
+
const literalPath = path.includes('\\\\')
|
|
176
|
+
if (literalPath) serveLiteral()
|
|
177
|
+
const ns = await import(literalPath ? path + '?vx-literal' : path)
|
|
159
178
|
// Awaited as the in-process load's async return flattens it: a Promise
|
|
160
179
|
// default loaded on a run and was refused as "an instance of Promise"
|
|
161
180
|
// on every later evaluation in the process (D-5).
|
|
@@ -87,7 +87,12 @@ function assertDefaultObject(mod: unknown, kind: string, configPath: string): vo
|
|
|
87
87
|
const heldSources = new Map<string, { source: string; uses: number }>()
|
|
88
88
|
let serving = false
|
|
89
89
|
|
|
90
|
-
const HELD_QUERY = /\?vx-held=([0-9a-f]+)$/
|
|
90
|
+
const HELD_QUERY = /\?vx-(?:held|literal)=([0-9a-f]+)$/
|
|
91
|
+
|
|
92
|
+
// Bun's resolver reads `\` as a separator, even in a file: URL, so a
|
|
93
|
+
// config under `a\b` was looked for under `a/b`. Resolving it here keeps
|
|
94
|
+
// the path as written; only the served load can take it.
|
|
95
|
+
const LITERAL_QUERY = /\?vx-literal=[0-9a-f]+$/
|
|
91
96
|
|
|
92
97
|
function serveHeldSources(): void {
|
|
93
98
|
if (serving) return
|
|
@@ -95,6 +100,7 @@ function serveHeldSources(): void {
|
|
|
95
100
|
Bun.plugin({
|
|
96
101
|
name: 'vx-config-bytes',
|
|
97
102
|
setup(build) {
|
|
103
|
+
build.onResolve({ filter: LITERAL_QUERY }, (args) => ({ path: args.path, namespace: 'file' }))
|
|
98
104
|
build.onLoad({ filter: HELD_QUERY }, (args) => ({
|
|
99
105
|
contents: heldSources.get(HELD_QUERY.exec(args.path)![1]!)!.source,
|
|
100
106
|
loader: /\.[cm]?ts\?/.test(args.path) ? 'ts' : 'js',
|
|
@@ -136,7 +142,7 @@ const COMMONJS_HINT =
|
|
|
136
142
|
/\b(?:module|exports|require|this|__dirname|__filename)\b|\\|\bexport\s*=(?!=)/
|
|
137
143
|
|
|
138
144
|
/** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
|
|
139
|
-
const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
|
|
145
|
+
const BUST_QUERY = /\?vx-(?:bust|held|literal)=[^'"\s]*/g
|
|
140
146
|
|
|
141
147
|
// Bun has native TS / ESM execution — no transpiler dep needed. We fold
|
|
142
148
|
// a short content hash into the import URL as a cache-bust key so that:
|
|
@@ -157,10 +163,18 @@ async function loadDefaultExport(
|
|
|
157
163
|
// URL yet, and a repeat load (the one that could replay an evaluation
|
|
158
164
|
// made under earlier env values) re-evaluates in a worker instead
|
|
159
165
|
// (`loadedConfigs`, item 678).
|
|
166
|
+
const literal = configPath.includes('\\')
|
|
160
167
|
const source =
|
|
161
|
-
kind === 'Project'
|
|
168
|
+
kind === 'Project' || literal
|
|
169
|
+
? servableSource(bytes, /\.[cm]?ts$/.test(configPath) ? 'ts' : 'js')
|
|
170
|
+
: null
|
|
171
|
+
if (literal && source === null) {
|
|
172
|
+
throw new UserError(
|
|
173
|
+
`${kind} config ${configPath} sits under a path that holds a backslash, which Bun loads only as an ES module in UTF-8`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
162
176
|
const hash = xxh3hex(bytes)
|
|
163
|
-
const specifier = `${configPath}?vx-${source !== null ? 'held' : 'bust'}=${hash}`
|
|
177
|
+
const specifier = `${configPath}?vx-${literal ? 'literal' : source !== null ? 'held' : 'bust'}=${hash}`
|
|
164
178
|
if (source !== null) {
|
|
165
179
|
serveHeldSources()
|
|
166
180
|
holdSource(hash, source)
|