@vzn/vx 0.0.421 → 0.0.423
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.423",
|
|
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.423",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.423",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.423",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.423"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/init.ts
CHANGED
|
@@ -75,7 +75,8 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
75
75
|
throw err
|
|
76
76
|
})
|
|
77
77
|
const nameless: string[] = []
|
|
78
|
-
const
|
|
78
|
+
const workspace = await loadWorkspace(root, reads)
|
|
79
|
+
const metas = await discoverProjects(workspace, nameless)
|
|
79
80
|
// `init` reads scripts only; a runner's own config beside them is the
|
|
80
81
|
// richer source (dependsOn, inputs, outputs) and was ignored without a
|
|
81
82
|
// word — the walkthrough on a Turbo repo (2026-09-09) got the scripts'
|
|
@@ -107,7 +108,10 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
107
108
|
),
|
|
108
109
|
source: 'package.json scripts',
|
|
109
110
|
verb: 'vx init',
|
|
110
|
-
notes:
|
|
111
|
+
notes: [
|
|
112
|
+
...(await unreadWorkspaces(root, workspace.packageGlobs)),
|
|
113
|
+
...namelessNotes(root, nameless),
|
|
114
|
+
],
|
|
111
115
|
dry: parsed.dry,
|
|
112
116
|
force: parsed.force,
|
|
113
117
|
init: true,
|
|
@@ -379,6 +383,29 @@ function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
|
379
383
|
]
|
|
380
384
|
}
|
|
381
385
|
|
|
386
|
+
/**
|
|
387
|
+
* `pnpm-workspace.yaml`'s `packages` decides the members, as pnpm reads it,
|
|
388
|
+
* and a root `workspaces` that lists other globs was dropped without a word:
|
|
389
|
+
* a bun or npm repo with a stale yaml lost those members (M-51).
|
|
390
|
+
*/
|
|
391
|
+
async function unreadWorkspaces(root: string, globs: readonly string[]): Promise<string[]> {
|
|
392
|
+
if (!existsSync(path.join(root, 'pnpm-workspace.yaml'))) return []
|
|
393
|
+
const ws = (await readRootManifest(root))['workspaces']
|
|
394
|
+
const list = Array.isArray(ws)
|
|
395
|
+
? ws
|
|
396
|
+
: typeof ws === 'object' &&
|
|
397
|
+
ws !== null &&
|
|
398
|
+
Array.isArray((ws as { packages?: unknown }).packages)
|
|
399
|
+
? (ws as { packages: unknown[] }).packages
|
|
400
|
+
: undefined
|
|
401
|
+
if (list === undefined) return []
|
|
402
|
+
const pkg = list.filter((g): g is string => typeof g === 'string')
|
|
403
|
+
if ([...pkg].sort().join('\0') === [...globs].sort().join('\0')) return []
|
|
404
|
+
return [
|
|
405
|
+
`pnpm-workspace.yaml's \`packages\` lists the members, as pnpm reads them; package.json's \`workspaces\` (${pkg.map((g) => `"${g}"`).join(', ')}) is not read: if bun, npm or yarn installs this repo, copy those globs into pnpm-workspace.yaml's \`packages\`, or delete that key so package.json decides`,
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
|
|
382
409
|
/** The root package.json as an object, `{}` when unreadable or not one. */
|
|
383
410
|
async function readRootManifest(root: string): Promise<Record<string, unknown>> {
|
|
384
411
|
const pj: unknown = await Bun.file(path.join(root, 'package.json'))
|
package/src/cli/watch-filter.ts
CHANGED
|
@@ -160,26 +160,51 @@ function outputContainer(raw: string): string {
|
|
|
160
160
|
*/
|
|
161
161
|
export function gitIgnored(workspaceRoot: string, paths: readonly string[]): Set<string> {
|
|
162
162
|
const ignored = new Set<string>()
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
163
|
+
// `-v -n` answers every path with a record, so on a refusal the count of
|
|
164
|
+
// records says which path git refused: a path inside a submodule ends
|
|
165
|
+
// the batch (exit 128, "is in submodule"), and reading that as nothing
|
|
166
|
+
// ignored let a pid file in the same window start cycles again. The
|
|
167
|
+
// refused path is skipped and the rest asked again; a refusal before
|
|
168
|
+
// any record outside a work tree is git refusing them all.
|
|
169
|
+
let rest = paths
|
|
170
|
+
let inWorkTree: boolean | undefined
|
|
171
|
+
while (rest.length > 0) {
|
|
172
|
+
let proc: ReturnType<typeof Bun.spawnSync>
|
|
173
|
+
try {
|
|
174
|
+
proc = Bun.spawnSync({
|
|
175
|
+
cmd: [executablePath('git'), 'check-ignore', '-v', '-n', '-z', '--stdin'],
|
|
176
|
+
cwd: workspaceRoot,
|
|
177
|
+
stdin: Buffer.from(rest.map((p) => `${p}\0`).join('')),
|
|
178
|
+
stdout: 'pipe',
|
|
179
|
+
stderr: 'ignore',
|
|
180
|
+
})
|
|
181
|
+
} catch {
|
|
182
|
+
// No git on PATH: nothing is ignored, and the loop keeps judging —
|
|
183
|
+
// the initial run already said what vx requires.
|
|
184
|
+
return ignored
|
|
185
|
+
}
|
|
186
|
+
// Each record: source, line, pattern, path. No source: no pattern
|
|
187
|
+
// matched; a `!` pattern: re-included. Either way not ignored.
|
|
188
|
+
const fields = new TextDecoder().decode(proc.stdout).split('\0')
|
|
189
|
+
const records = Math.floor(fields.length / 4)
|
|
190
|
+
for (let i = 0; i < records; i++) {
|
|
191
|
+
const [source, , pattern, p] = fields.slice(i * 4, i * 4 + 4)
|
|
192
|
+
if (source !== '' && !pattern!.startsWith('!')) ignored.add(p!)
|
|
193
|
+
}
|
|
194
|
+
// 0: some ignored; 1: none. Anything else is git refusing.
|
|
195
|
+
if (proc.exitCode === 0 || proc.exitCode === 1) return ignored
|
|
196
|
+
if (records === 0) {
|
|
197
|
+
inWorkTree ??=
|
|
198
|
+
Bun.spawnSync({
|
|
199
|
+
cmd: [executablePath('git'), 'rev-parse', '--is-inside-work-tree'],
|
|
200
|
+
cwd: workspaceRoot,
|
|
201
|
+
stdout: 'ignore',
|
|
202
|
+
stderr: 'ignore',
|
|
203
|
+
}).exitCode === 0
|
|
204
|
+
if (!inWorkTree) return ignored
|
|
205
|
+
}
|
|
206
|
+
rest = rest.slice(records + 1)
|
|
177
207
|
}
|
|
178
|
-
// 0: some ignored; 1: none. Anything else is git refusing (not a
|
|
179
|
-
// repository, a path inside a nested one): no path is ignored.
|
|
180
|
-
if (proc.exitCode !== 0) return ignored
|
|
181
|
-
for (const p of new TextDecoder().decode(proc.stdout).split('\0'))
|
|
182
|
-
if (p.length > 0) ignored.add(p)
|
|
183
208
|
return ignored
|
|
184
209
|
}
|
|
185
210
|
|
|
@@ -217,7 +217,7 @@ interface Pending {
|
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
219
|
* How long a single config evaluation may take, in the worker or in process
|
|
220
|
-
* (D-66). Real evaluations are ~10 ms; this exists only so a hung config or a
|
|
220
|
+
* (D-66); in process a synchronous loop holds the thread the deadline needs. Real evaluations are ~10 ms; this exists only so a hung config or a
|
|
221
221
|
* killed worker cannot stall a run or a long-lived process indefinitely. Read
|
|
222
222
|
* per call so a test can drive the deadline instead of waiting it out.
|
|
223
223
|
*/
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// it. The map lives exactly as long as the load: a `vx watch` cycle is a
|
|
6
6
|
// new load and reads fresh bytes.
|
|
7
7
|
|
|
8
|
+
import { isPermissionError, UserError } from '../util/index.js'
|
|
9
|
+
|
|
8
10
|
/** The files one load has read, by absolute path: the bytes, or null when no file is there. */
|
|
9
11
|
export type LoadReads = Map<string, Promise<Uint8Array | null>>
|
|
10
12
|
|
|
@@ -28,5 +30,17 @@ export function readOnce(reads: LoadReads | undefined, file: string): Promise<Ui
|
|
|
28
30
|
|
|
29
31
|
async function readIfFile(file: string): Promise<Uint8Array | null> {
|
|
30
32
|
const f = Bun.file(file)
|
|
31
|
-
return (await f.exists()) ? await f.bytes() : null
|
|
33
|
+
return (await f.exists()) ? await f.bytes().catch((err: unknown) => unreadable(err, file)) : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A read the file system refused is the user's to fix, named as a read: a
|
|
38
|
+
* config at mode 000 reached the user as "a path vx must write is not
|
|
39
|
+
* writable" (D-132). Anything else is rethrown as it came.
|
|
40
|
+
*/
|
|
41
|
+
export function unreadable(err: unknown, file: string): never {
|
|
42
|
+
if (isPermissionError(err)) {
|
|
43
|
+
throw new UserError(`${file}: not readable by this user (${err.code})`)
|
|
44
|
+
}
|
|
45
|
+
throw err
|
|
32
46
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
configImports,
|
|
21
21
|
type ConfigEvalStore,
|
|
22
22
|
} from './config-cache.js'
|
|
23
|
-
import { readOnce } from './load-reads.js'
|
|
23
|
+
import { readOnce, unreadable } from './load-reads.js'
|
|
24
24
|
|
|
25
25
|
// The validator lives in config-schema.ts; re-exported so a reader that
|
|
26
26
|
// reaches the loader for it (the tests do) keeps working.
|
|
@@ -401,7 +401,9 @@ export async function loadProjectConfigs(
|
|
|
401
401
|
})
|
|
402
402
|
if (fastKey !== null) return { configPath, bytes: null, cacheKey: fastKey, indexed: true }
|
|
403
403
|
}
|
|
404
|
-
const bytes = await Bun.file(configPath)
|
|
404
|
+
const bytes = await Bun.file(configPath)
|
|
405
|
+
.bytes()
|
|
406
|
+
.catch((err: unknown) => unreadable(err, configPath))
|
|
405
407
|
const keyed =
|
|
406
408
|
evalCache === undefined
|
|
407
409
|
? null
|
|
@@ -465,7 +467,9 @@ export async function loadProjectConfigs(
|
|
|
465
467
|
let closure = entry.closure
|
|
466
468
|
let key = cacheKey
|
|
467
469
|
if (entry.indexed) {
|
|
468
|
-
bytes = await Bun.file(configPath)
|
|
470
|
+
bytes = await Bun.file(configPath)
|
|
471
|
+
.bytes()
|
|
472
|
+
.catch((err: unknown) => unreadable(err, configPath))
|
|
469
473
|
const keyed = await configEvalKey({
|
|
470
474
|
configPath,
|
|
471
475
|
bytes,
|
|
@@ -670,7 +674,52 @@ interface BuiltinSnapshot {
|
|
|
670
674
|
readonly props: readonly (OwnProperties | undefined)[]
|
|
671
675
|
}
|
|
672
676
|
|
|
673
|
-
|
|
677
|
+
/**
|
|
678
|
+
* The members of `Bun` vx reads, which the workspace config's guard checks;
|
|
679
|
+
* of the rest it keeps the keys and their order, not their descriptors.
|
|
680
|
+
* Some members are built on their first read (`postgres` loaded `bun:sql`,
|
|
681
|
+
* 3.4 ms), and the guard runs on every warm run, where its full read of
|
|
682
|
+
* `Bun` was 4.4–5 ms of a 45 ms no-op (compiled, 2026-10-03). A project
|
|
683
|
+
* config's guard reads them all. Held to every `Bun.<name>` in `src/` by
|
|
684
|
+
* tests/workspace-guard-bun.test.ts.
|
|
685
|
+
*/
|
|
686
|
+
const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
687
|
+
'Archive',
|
|
688
|
+
'BunFile',
|
|
689
|
+
'CryptoHasher',
|
|
690
|
+
'Glob',
|
|
691
|
+
'JSONC',
|
|
692
|
+
'Subprocess',
|
|
693
|
+
'Transpiler',
|
|
694
|
+
'YAML',
|
|
695
|
+
'color',
|
|
696
|
+
'deepEquals',
|
|
697
|
+
'env',
|
|
698
|
+
'file',
|
|
699
|
+
'hash',
|
|
700
|
+
'main',
|
|
701
|
+
'nanoseconds',
|
|
702
|
+
'plugin',
|
|
703
|
+
'randomUUIDv7',
|
|
704
|
+
'resolveSync',
|
|
705
|
+
'semver',
|
|
706
|
+
'serve',
|
|
707
|
+
'sleep',
|
|
708
|
+
'spawn',
|
|
709
|
+
'spawnSync',
|
|
710
|
+
'stdout',
|
|
711
|
+
'stringWidth',
|
|
712
|
+
'version',
|
|
713
|
+
'which',
|
|
714
|
+
'write',
|
|
715
|
+
'zstdCompress',
|
|
716
|
+
'zstdDecompress',
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
/** A descriptor the snapshot did not read. */
|
|
720
|
+
const UNREAD: PropertyDescriptor = Object.freeze({})
|
|
721
|
+
|
|
722
|
+
function builtinSnapshot(leaveOut?: string, onlyBunMembersVxReads = false): BuiltinSnapshot {
|
|
674
723
|
const prims: Primitives = {
|
|
675
724
|
ownKeys: Reflect.ownKeys,
|
|
676
725
|
descriptorOf: Object.getOwnPropertyDescriptor,
|
|
@@ -689,7 +738,12 @@ function builtinSnapshot(leaveOut?: string): BuiltinSnapshot {
|
|
|
689
738
|
const proto = WATCHED_BUILTINS[i]![1]
|
|
690
739
|
const keys = prims.ownKeys(proto)
|
|
691
740
|
const descriptors: PropertyDescriptor[] = []
|
|
692
|
-
|
|
741
|
+
const narrow = onlyBunMembersVxReads && proto === Bun
|
|
742
|
+
for (let j = 0; j < keys.length; j++) {
|
|
743
|
+
const key = keys[j]!
|
|
744
|
+
descriptors[j] =
|
|
745
|
+
narrow && !BUN_MEMBERS_VX_READS.includes(key) ? UNREAD : prims.descriptorOf(proto, key)!
|
|
746
|
+
}
|
|
693
747
|
props[i] = { keys, descriptors }
|
|
694
748
|
}
|
|
695
749
|
return { prims, props }
|
|
@@ -712,7 +766,9 @@ function unchanged(
|
|
|
712
766
|
for (let j = 0; j < keys.length; j++) {
|
|
713
767
|
const key = keys[j]!
|
|
714
768
|
if (key !== was.keys[j]) return false
|
|
715
|
-
|
|
769
|
+
const prior = was.descriptors[j]!
|
|
770
|
+
if (prior === UNREAD) continue
|
|
771
|
+
if (!sameDescriptor(p, prior, p.descriptorOf(proto, key)!)) return false
|
|
716
772
|
}
|
|
717
773
|
return true
|
|
718
774
|
}
|
|
@@ -785,6 +841,7 @@ function restoreBuiltins(before: BuiltinSnapshot | undefined, everyLoadOnly = fa
|
|
|
785
841
|
break
|
|
786
842
|
}
|
|
787
843
|
}
|
|
844
|
+
if (prior === UNREAD) continue
|
|
788
845
|
if (prior !== undefined && sameDescriptor(p, prior, p.descriptorOf(proto, key)!)) continue
|
|
789
846
|
changed[changed.length] = name + '.' + p.keyName(key)
|
|
790
847
|
if (prior === undefined) p.deleteOwn(proto, key)
|
|
@@ -794,7 +851,8 @@ function restoreBuiltins(before: BuiltinSnapshot | undefined, everyLoadOnly = fa
|
|
|
794
851
|
const key = was.keys[k]!
|
|
795
852
|
if (p.hasOwn(proto, key)) continue
|
|
796
853
|
changed[changed.length] = name + '.' + p.keyName(key)
|
|
797
|
-
|
|
854
|
+
// An unread member cannot be put back as it was; it is refused all the same.
|
|
855
|
+
if (was.descriptors[k] !== UNREAD) p.defineOwn(proto, key, was.descriptors[k]!)
|
|
798
856
|
}
|
|
799
857
|
}
|
|
800
858
|
return changed
|
|
@@ -933,7 +991,7 @@ export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig
|
|
|
933
991
|
// left out: it loads first in every run, filtered or not, so none
|
|
934
992
|
// depends on what else loaded (D-122's case), and plugins' tests and
|
|
935
993
|
// tools hand state through them.
|
|
936
|
-
const builtins = builtinSnapshot('globalThis')
|
|
994
|
+
const builtins = builtinSnapshot('globalThis', true)
|
|
937
995
|
const env = { ...process.env }
|
|
938
996
|
const cwd = process.cwd()
|
|
939
997
|
const umask = currentUmask()
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { readdir, stat } from 'node:fs/promises'
|
|
1
|
+
import { constants, type Dirent } from 'node:fs'
|
|
2
|
+
import { access, readdir, stat } from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import type { ProjectConfig, WorkspaceConfig } from '../config.js'
|
|
5
5
|
import {
|
|
6
6
|
BUN_GLOB_WILDCARDS,
|
|
7
7
|
isOutOfFds,
|
|
8
|
+
isPermissionError,
|
|
8
9
|
relPosix,
|
|
9
10
|
slashBraceExpansions,
|
|
10
11
|
UserError,
|
|
11
12
|
normalizeBunGlob,
|
|
12
13
|
} from '../util/index.js'
|
|
13
|
-
import { type LoadReads, readOnce } from './load-reads.js'
|
|
14
|
+
import { type LoadReads, readOnce, unreadable } from './load-reads.js'
|
|
14
15
|
|
|
15
16
|
export interface PackageJson {
|
|
16
17
|
name: string
|
|
@@ -548,6 +549,30 @@ function absent<T>(err: unknown, value: T): T {
|
|
|
548
549
|
return value
|
|
549
550
|
}
|
|
550
551
|
|
|
552
|
+
/**
|
|
553
|
+
* A member's manifest text, or null when there is none. A manifest this
|
|
554
|
+
* user may not read is refused, not skipped: its project dropped out of
|
|
555
|
+
* `--all` and the run went green without it (D-132). A directory it may
|
|
556
|
+
* not search hides whether a manifest is there at all, so that one is
|
|
557
|
+
* named and skipped (a service's data directory under `packages/*`).
|
|
558
|
+
*/
|
|
559
|
+
async function readManifest(root: string, dir: string, file: string): Promise<string | null> {
|
|
560
|
+
try {
|
|
561
|
+
return await Bun.file(file).text()
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (!isPermissionError(err)) return absent(err, null)
|
|
564
|
+
const searchable = await access(dir, constants.X_OK).then(
|
|
565
|
+
() => true,
|
|
566
|
+
() => false,
|
|
567
|
+
)
|
|
568
|
+
if (searchable) unreadable(err, file)
|
|
569
|
+
process.stderr.write(
|
|
570
|
+
`vx: ${relPosix(root, dir)} is not readable by this user — skipped, with any project in it\n`,
|
|
571
|
+
)
|
|
572
|
+
return null
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
551
576
|
export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
552
577
|
return discoverProjects(workspace)
|
|
553
578
|
}
|
|
@@ -591,9 +616,7 @@ export async function discoverProjects(
|
|
|
591
616
|
const pkgJsonPath = dir + path.sep + 'package.json'
|
|
592
617
|
const [configPath, text] = await Promise.all([
|
|
593
618
|
findConfigFile(dir),
|
|
594
|
-
|
|
595
|
-
.text()
|
|
596
|
-
.catch((err: unknown) => absent(err, null)),
|
|
619
|
+
readManifest(workspace.root, dir, pkgJsonPath),
|
|
597
620
|
])
|
|
598
621
|
if (text === null) {
|
|
599
622
|
// A member dir with a vx config and no manifest was skipped without
|
|
@@ -712,9 +735,7 @@ export async function namedProject(
|
|
|
712
735
|
const pkgJsonPath = dir + path.sep + 'package.json'
|
|
713
736
|
const [configPath, text] = await Promise.all([
|
|
714
737
|
findConfigFile(dir),
|
|
715
|
-
|
|
716
|
-
.text()
|
|
717
|
-
.catch((err: unknown) => absent(err, null)),
|
|
738
|
+
readManifest(workspace.root, dir, pkgJsonPath),
|
|
718
739
|
])
|
|
719
740
|
if (
|
|
720
741
|
text === null &&
|