@vzn/vx 0.0.595 → 0.0.597
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/README.md +1 -1
- package/launcher.cjs +7 -3
- package/package.json +5 -5
- package/src/cache/archive.ts +35 -6
- package/src/cache/cache.ts +170 -99
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/git-inputs.ts +23 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +99 -44
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +9 -6
- package/src/cache/policy.ts +5 -0
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +12 -6
- package/src/cli/foreign-flags.ts +57 -3
- package/src/cli/info.ts +1 -1
- package/src/cli/init.ts +2 -2
- package/src/cli/last.ts +10 -4
- package/src/cli/lock.ts +5 -3
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/run.ts +73 -30
- package/src/cli/select.ts +6 -3
- package/src/cli/show.ts +8 -2
- package/src/cli/upgrade.ts +1 -1
- package/src/cli/watch-filter.ts +63 -12
- package/src/cli/watch-fs.ts +11 -3
- package/src/cli/watch-judge.ts +15 -8
- package/src/cli/watch.ts +18 -11
- package/src/cli/workspace-config.ts +3 -1
- package/src/config.ts +42 -21
- package/src/exec/executor.ts +1 -1
- package/src/exec/kill-tree.ts +89 -18
- package/src/exec/proc-sample.ts +1 -1
- package/src/exec/runner.ts +74 -36
- package/src/exec/sandbox-paths.ts +12 -1
- package/src/exec/sandbox-runtime.ts +283 -88
- package/src/exec/sandbox-violations.ts +115 -15
- package/src/graph/scheduler.ts +30 -14
- package/src/graph/task-graph.ts +265 -44
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/affected-tasks.ts +6 -2
- package/src/orchestrator/deferred-outputs.ts +3 -2
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/excluded-keys.ts +2 -13
- package/src/orchestrator/execute-task.ts +85 -39
- package/src/orchestrator/failure-mode.ts +43 -28
- package/src/orchestrator/framed-output.ts +8 -8
- package/src/orchestrator/index.ts +0 -1
- package/src/orchestrator/keyed-projects.ts +6 -3
- package/src/orchestrator/logger.ts +21 -9
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +9 -13
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +7 -23
- package/src/orchestrator/plugin.ts +37 -16
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +4 -3
- package/src/orchestrator/run-lock.ts +25 -24
- package/src/orchestrator/run.ts +22 -5
- package/src/orchestrator/sandbox-request.ts +96 -18
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/signals.ts +22 -4
- package/src/orchestrator/stable-keys.ts +3 -11
- package/src/orchestrator/summary.ts +7 -13
- package/src/orchestrator/task-hash.ts +13 -3
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/orchestrator/upstream.ts +11 -0
- package/src/util/hangup.ts +42 -0
- package/src/util/index.ts +2 -0
- package/src/util/paths.ts +24 -4
- package/src/workspace/affected.ts +79 -42
- package/src/workspace/config-eval.ts +27 -6
- package/src/workspace/config-imports.ts +8 -53
- package/src/workspace/config-schema.ts +56 -9
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/lockfile.ts +2 -1
- package/src/workspace/migrate-scripts.ts +5 -3
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/project-loader.ts +19 -4
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +53 -24
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CONFIG_EXIT,
|
|
10
10
|
evalBudgetMs,
|
|
11
11
|
evaluateConfigFresh,
|
|
12
|
+
thrownValueMessage,
|
|
12
13
|
WATCHED_BUILTIN_NAMES,
|
|
13
14
|
} from './config-eval.js'
|
|
14
15
|
import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
@@ -127,10 +128,12 @@ function servableSource(bytes: Uint8Array, loader: 'ts' | 'js'): string | null {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
// What makes Bun run a file as CommonJS is one of these names at the top
|
|
130
|
-
// level (an escaped `\u006dodule` too, so any backslash counts)
|
|
131
|
+
// level (an escaped `\u006dodule` too, so any backslash counts), or
|
|
132
|
+
// TypeScript's `export =`, which spells none of them. Source
|
|
131
133
|
// with none of them runs as a module whichever path loads it, so it skips
|
|
132
134
|
// the parse: 16–20 µs a config, 1,000 cold configs (2026-10-03).
|
|
133
|
-
const COMMONJS_HINT =
|
|
135
|
+
const COMMONJS_HINT =
|
|
136
|
+
/\b(?:module|exports|require|this|__dirname|__filename)\b|\\|\bexport\s*=(?!=)/
|
|
134
137
|
|
|
135
138
|
/** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
|
|
136
139
|
const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
|
|
@@ -189,7 +192,7 @@ async function loadDefaultExport(
|
|
|
189
192
|
if (err instanceof Error && err.stack !== undefined) {
|
|
190
193
|
err.stack = err.stack.replaceAll(specifier, configPath).replace(BUST_QUERY, '')
|
|
191
194
|
}
|
|
192
|
-
throw
|
|
195
|
+
throw loadFailure(err, configPath, kind)
|
|
193
196
|
} finally {
|
|
194
197
|
clearTimeout(deadline)
|
|
195
198
|
unguard()
|
|
@@ -339,6 +342,16 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
|
|
|
339
342
|
return null
|
|
340
343
|
}
|
|
341
344
|
|
|
345
|
+
/** What a failed load throws: Bun's own errors and non-Error throws as user errors, the rest as thrown. */
|
|
346
|
+
function loadFailure(err: unknown, configPath: string, kind: string): unknown {
|
|
347
|
+
const user = configLoadError(err, configPath, kind)
|
|
348
|
+
if (user !== null) return user
|
|
349
|
+
if (err !== null && typeof err === 'object' && typeof (err as Error).message === 'string') {
|
|
350
|
+
return err
|
|
351
|
+
}
|
|
352
|
+
return new UserError(thrownValueMessage(kind, configPath, Bun.inspect(err, { compact: true })))
|
|
353
|
+
}
|
|
354
|
+
|
|
342
355
|
export interface LoadProjectConfigOptions {
|
|
343
356
|
/** Observe the CURRENT environment: no eval cache, even when `evalCache` is passed. */
|
|
344
357
|
fresh?: boolean
|
|
@@ -521,7 +534,7 @@ export async function loadProjectConfigs(
|
|
|
521
534
|
if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
|
|
522
535
|
const mod = repeat
|
|
523
536
|
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
524
|
-
throw
|
|
537
|
+
throw loadFailure(err, configPath, 'Project')
|
|
525
538
|
})
|
|
526
539
|
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
527
540
|
// Before anything reads through them: a replaced `Array.prototype.includes`
|
|
@@ -810,6 +823,7 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
|
810
823
|
'env',
|
|
811
824
|
'file',
|
|
812
825
|
'hash',
|
|
826
|
+
'inspect',
|
|
813
827
|
'main',
|
|
814
828
|
'nanoseconds',
|
|
815
829
|
'plugin',
|
|
@@ -818,6 +832,7 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
|
818
832
|
'semver',
|
|
819
833
|
'serve',
|
|
820
834
|
'sleep',
|
|
835
|
+
'sleepSync',
|
|
821
836
|
'spawn',
|
|
822
837
|
'spawnSync',
|
|
823
838
|
'stderr',
|
package/src/workspace/repo-id.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
lstatSync,
|
|
14
14
|
openSync,
|
|
15
15
|
readFileSync,
|
|
16
|
+
realpathSync,
|
|
16
17
|
statSync,
|
|
17
18
|
} from 'node:fs'
|
|
18
19
|
import path from 'node:path'
|
|
@@ -25,12 +26,20 @@ import path from 'node:path'
|
|
|
25
26
|
* git: those share nothing.
|
|
26
27
|
*/
|
|
27
28
|
export async function repoIdOf(root: string): Promise<string | null> {
|
|
28
|
-
|
|
29
|
+
// Git walks the physical path; the logical one found no `.git` through a
|
|
30
|
+
// symlink from outside the repository, and an alias inside it another id.
|
|
31
|
+
let real: string
|
|
32
|
+
try {
|
|
33
|
+
real = realpathSync(root)
|
|
34
|
+
} catch {
|
|
35
|
+
real = path.resolve(root)
|
|
36
|
+
}
|
|
37
|
+
const located = locateGitDir(real)
|
|
29
38
|
if (located === null) return null
|
|
30
39
|
const identity =
|
|
31
40
|
(await remoteIdentity(root, located.commonDir)) ?? (await firstCommit(located.commonDir, root))
|
|
32
41
|
if (identity === null) return null
|
|
33
|
-
const rel = path.relative(located.gitRoot,
|
|
42
|
+
const rel = path.relative(located.gitRoot, real).split(path.sep).join('/')
|
|
34
43
|
const key = sha256(`${identity}#${rel}`)
|
|
35
44
|
return sha256(key).slice(0, 16)
|
|
36
45
|
}
|
|
@@ -108,11 +117,40 @@ function readOwnedFile(p: string): string | null {
|
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* The remote's identity from the config file, else from `git remote -v`
|
|
122
|
+
* when the file cannot settle it: a remote none of whose urls parse (a
|
|
123
|
+
* rewrite may live in a file vx does not read, as Nx asks git), or
|
|
124
|
+
* remotes git could read from `config.worktree`.
|
|
125
|
+
*/
|
|
112
126
|
async function remoteIdentity(root: string, commonDir: string): Promise<string | null> {
|
|
113
127
|
const config = readOwnedFile(path.join(commonDir, 'config'))
|
|
114
128
|
const remotes = config === null ? null : parseGitConfigRemotes(config)
|
|
115
|
-
|
|
129
|
+
if (remotes !== null) {
|
|
130
|
+
const picked = pickRemote(remotes)
|
|
131
|
+
// A remote-less repo with nothing to pull one from elsewhere spawns
|
|
132
|
+
// nothing: git would answer the same, and that spawn was a cost per run.
|
|
133
|
+
if (picked !== null || (remotes.length === 0 && !worktreeConfigOn(config!))) return picked
|
|
134
|
+
}
|
|
135
|
+
return pickRemote(await remotesFromGit(root))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `extensions.worktreeConfig` set: git reads `config.worktree` too. */
|
|
139
|
+
function worktreeConfigOn(contents: string): boolean {
|
|
140
|
+
let inExtensions = false
|
|
141
|
+
for (const raw of contents.split('\n')) {
|
|
142
|
+
const line = raw.trim()
|
|
143
|
+
if (line.startsWith('[')) {
|
|
144
|
+
inExtensions = /^\[\s*extensions\s*\]/i.test(line)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const m = /^worktreeconfig\s*(?:=\s*(\S*))?/i.exec(line)
|
|
148
|
+
if (inExtensions && m !== null && !/^(false|no|off|0)$/i.test(m[1] ?? 'true')) return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pickRemote(urls: [string, string][]): string | null {
|
|
116
154
|
const found = new Map<string, string>()
|
|
117
155
|
let first: string | null = null
|
|
118
156
|
for (const [name, url] of urls) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { constants, type Dirent } from 'node:fs'
|
|
1
|
+
import { constants, existsSync, type Dirent } from 'node:fs'
|
|
2
2
|
import { access, readdir, realpath, stat } from 'node:fs/promises'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -267,11 +267,13 @@ async function readRootManifests(
|
|
|
267
267
|
}
|
|
268
268
|
catalogs = catalogsOf([parsed])
|
|
269
269
|
const packages = (parsed as { packages?: unknown }).packages
|
|
270
|
-
|
|
270
|
+
// A list commented out leaves `packages:` null, which pnpm reads as
|
|
271
|
+
// absent; vx refused it as "must be an array" (D-149).
|
|
272
|
+
if (packages !== undefined && packages !== null) {
|
|
271
273
|
return { globs: assertGlobList(packages, yamlPath, 'packages'), catalogs }
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
|
-
// No `packages
|
|
276
|
+
// No `packages:` (or an empty one): pnpm 10 keeps its settings and catalogs in this file
|
|
275
277
|
// for a single-package repo too. The root's package.json decides, as it
|
|
276
278
|
// would without the file; read as an empty list, the root found zero
|
|
277
279
|
// projects and every verb ran nothing and exited 0 (item 984).
|
|
@@ -288,13 +290,17 @@ async function readRootManifests(
|
|
|
288
290
|
)
|
|
289
291
|
}
|
|
290
292
|
if (ws === undefined || ws === null) return { globs: ['.'], catalogs }
|
|
293
|
+
// bun's `{ catalog }` and yarn's `{ nohoist }` name no members, and both
|
|
294
|
+
// managers run the root alone; vx refused them as no array (D-151).
|
|
295
|
+
if (typeof ws === 'object' && !Array.isArray(ws) && !('packages' in ws)) {
|
|
296
|
+
return { globs: ['.'], catalogs }
|
|
297
|
+
}
|
|
291
298
|
if (ws && typeof ws === 'object' && !Array.isArray(ws) && 'packages' in ws) {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
return { globs, catalogs }
|
|
299
|
+
const packages = (ws as { packages?: unknown }).packages
|
|
300
|
+
// Yarn 1 and 4 run the root alone under `packages: null`; read as an
|
|
301
|
+
// empty list, the root was no project and every verb ran nothing (D-150).
|
|
302
|
+
if (packages === null) return { globs: ['.'], catalogs }
|
|
303
|
+
return { globs: assertGlobList(packages, pkgPath, 'workspaces.packages'), catalogs }
|
|
298
304
|
}
|
|
299
305
|
return { globs: assertGlobList(ws, pkgPath, 'workspaces'), catalogs }
|
|
300
306
|
}
|
|
@@ -348,8 +354,13 @@ export async function unreachedPackages(workspace: Workspace): Promise<string[]>
|
|
|
348
354
|
return found.sort()
|
|
349
355
|
}
|
|
350
356
|
|
|
351
|
-
/**
|
|
352
|
-
|
|
357
|
+
/**
|
|
358
|
+
* The line for `unreachedPackages`' finding: the cause, the packages, the
|
|
359
|
+
* glob to add, and where. A `pnpm-workspace.yaml` with no `packages:` (pnpm
|
|
360
|
+
* keeps its settings there) is where pnpm reads the globs: the hint said
|
|
361
|
+
* there was no such file and named package.json, beside ngrx's (2026-10-07).
|
|
362
|
+
*/
|
|
363
|
+
export function unreachedHint(unreached: readonly string[], root: string): string {
|
|
353
364
|
const n = unreached.length
|
|
354
365
|
const shown = unreached.slice(0, 3).join(', ') + (n > 3 ? ` and ${n - 3} more` : '')
|
|
355
366
|
const globs = [
|
|
@@ -357,11 +368,11 @@ export function unreachedHint(unreached: readonly string[]): string {
|
|
|
357
368
|
]
|
|
358
369
|
.map((g) => `"${g}"`)
|
|
359
370
|
.join(', ')
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
`
|
|
363
|
-
`
|
|
364
|
-
|
|
371
|
+
const rest = `so the root is the only project and ${n} package.json below it ${n === 1 ? 'is' : 'are'} not: ${shown}.`
|
|
372
|
+
return existsSync(path.join(root, 'pnpm-workspace.yaml'))
|
|
373
|
+
? `pnpm-workspace.yaml lists no \`packages\`, ${rest} Add \`packages: [${globs}]\` to pnpm-workspace.yaml and re-run.`
|
|
374
|
+
: `package.json declares no \`workspaces\` (and there is no pnpm-workspace.yaml), ${rest} ` +
|
|
375
|
+
`Add \`"workspaces": [${globs}]\` to package.json and re-run.`
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
/**
|
|
@@ -447,13 +458,27 @@ function extglobRefusal(pattern: string, file: string, field: string): UserError
|
|
|
447
458
|
* to the workspace root.
|
|
448
459
|
*/
|
|
449
460
|
export function resolveCacheDir(root: string, config: WorkspaceConfig | null): string {
|
|
450
|
-
const
|
|
461
|
+
const env = process.env['VX_CACHE_DIR']
|
|
462
|
+
// `cacheDir: ' '` is refused at load (X-22); the variable made the same
|
|
463
|
+
// directory named in spaces at the root (D-160).
|
|
464
|
+
if (config?.cacheDir === undefined && env !== undefined && env !== '' && env.trim() === '') {
|
|
465
|
+
throw new UserError('VX_CACHE_DIR is only whitespace — name a directory, or unset it')
|
|
466
|
+
}
|
|
467
|
+
const rel = config?.cacheDir ?? (env || path.join('.vx', 'cache'))
|
|
468
|
+
const home = process.env['HOME'] || homedir()
|
|
451
469
|
// No shell expands `~` in a config string or a quoted variable, and
|
|
452
470
|
// `'~/.cache/vx'` made a directory named `~` in the workspace.
|
|
453
|
-
|
|
454
|
-
|
|
471
|
+
const dir =
|
|
472
|
+
rel === '~' || rel.startsWith('~/') ? path.join(home, rel.slice(1)) : path.resolve(root, rel)
|
|
473
|
+
// The cache writes a `*` .gitignore beside its index: a bare `~` made a
|
|
474
|
+
// directory named `~`, and expanded it would land in home itself (D-153).
|
|
475
|
+
if (path.resolve(dir) === path.resolve(home)) {
|
|
476
|
+
const source = config?.cacheDir !== undefined ? 'cacheDir' : 'VX_CACHE_DIR'
|
|
477
|
+
throw new UserError(
|
|
478
|
+
`${source} ${JSON.stringify(rel)} is the home directory itself, where the cache's \`*\` .gitignore and index would land — name a directory under it, like '~/.cache/vx'`,
|
|
479
|
+
)
|
|
455
480
|
}
|
|
456
|
-
return
|
|
481
|
+
return dir
|
|
457
482
|
}
|
|
458
483
|
|
|
459
484
|
/**
|
|
@@ -704,12 +729,17 @@ async function readManifest(root: string, dir: string, file: string): Promise<st
|
|
|
704
729
|
)
|
|
705
730
|
if (searchable) unreadable(err, file)
|
|
706
731
|
process.stderr.write(
|
|
707
|
-
`vx: ${
|
|
732
|
+
`vx: ${shownDir(root, dir)} is not readable by this user — skipped, with any project in it\n`,
|
|
708
733
|
)
|
|
709
734
|
return null
|
|
710
735
|
}
|
|
711
736
|
}
|
|
712
737
|
|
|
738
|
+
/** `dir` as a discovery line names it: the root's own path is '' (D-127, D-154). */
|
|
739
|
+
function shownDir(root: string, dir: string): string {
|
|
740
|
+
return relPosix(root, dir) || 'the workspace root'
|
|
741
|
+
}
|
|
742
|
+
|
|
713
743
|
export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
714
744
|
return discoverProjects(workspace)
|
|
715
745
|
}
|
|
@@ -762,7 +792,7 @@ export async function discoverProjects(
|
|
|
762
792
|
// same flight as the failed read, so naming it costs nothing.
|
|
763
793
|
if (configPath !== null) {
|
|
764
794
|
process.stderr.write(
|
|
765
|
-
`vx: ${
|
|
795
|
+
`vx: ${shownDir(workspace.root, dir)} has a vx config but no package.json — skipped: vx names a project by its package.json "name"\n`,
|
|
766
796
|
)
|
|
767
797
|
}
|
|
768
798
|
return null
|
|
@@ -793,9 +823,8 @@ export async function discoverProjects(
|
|
|
793
823
|
// fine for a dir that declares no tasks; a dir with a vx config was
|
|
794
824
|
// meant to run.
|
|
795
825
|
if (configPath !== null) {
|
|
796
|
-
const rel = relPosix(workspace.root, dir)
|
|
797
826
|
process.stderr.write(
|
|
798
|
-
`vx: ${
|
|
827
|
+
`vx: ${shownDir(workspace.root, dir)} has a vx config but its package.json has no "name" — skipped\n`,
|
|
799
828
|
)
|
|
800
829
|
}
|
|
801
830
|
continue
|