@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
|
@@ -12,31 +12,16 @@ import {
|
|
|
12
12
|
thrownValueMessage,
|
|
13
13
|
WATCHED_BUILTIN_NAMES,
|
|
14
14
|
} from './config-eval.js'
|
|
15
|
-
import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
16
|
-
import {
|
|
17
|
-
configEvalKey,
|
|
18
|
-
configEvalKeyFromClosure,
|
|
19
|
-
configEvalKeyFromIdentities,
|
|
20
|
-
configImports,
|
|
21
|
-
type ConfigEvalStore,
|
|
22
|
-
} from './config-cache.js'
|
|
15
|
+
import { configImports, hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
23
16
|
import { readOnce, unreadable } from './load-reads.js'
|
|
24
17
|
|
|
25
18
|
// The validator lives in config-schema.ts; re-exported so a reader that
|
|
26
19
|
// reaches the loader for it (the tests do) keeps working.
|
|
27
20
|
export { validateProjectConfig }
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'vx.workspace.mts',
|
|
33
|
-
'vx.workspace.js',
|
|
34
|
-
'vx.workspace.mjs',
|
|
35
|
-
// A CommonJS config (`module.exports = …`), as Vite and TS name theirs: it
|
|
36
|
-
// was ignored without a word (D-86).
|
|
37
|
-
'vx.workspace.cts',
|
|
38
|
-
'vx.workspace.cjs',
|
|
39
|
-
]
|
|
22
|
+
// The filenames live with the root lookup, which runs before anything loads.
|
|
23
|
+
import { WORKSPACE_CONFIG_FILENAMES, workspaceFile, workspaceRootOf } from '../handoff.js'
|
|
24
|
+
export { WORKSPACE_CONFIG_FILENAMES }
|
|
40
25
|
|
|
41
26
|
/** What a function default export is told; the playground says the same. */
|
|
42
27
|
const EXPORTED_A_FUNCTION =
|
|
@@ -60,9 +45,9 @@ function assertDefaultObject(mod: unknown, kind: string, configPath: string): vo
|
|
|
60
45
|
* The source a load already holds, by the specifier it imports it under.
|
|
61
46
|
* Bun's loader would open and read a project config a second time; the
|
|
62
47
|
* `vx-config-bytes` plugin hands it this instead, so the evaluation runs
|
|
63
|
-
* exactly the bytes the
|
|
64
|
-
*
|
|
65
|
-
*
|
|
48
|
+
* exactly the bytes the import gate saw. It is also the cheaper load:
|
|
49
|
+
* 1,000 configs imported this way took 80–100 ms against 180 ms read by
|
|
50
|
+
* Bun (measured 2026-09-24).
|
|
66
51
|
*
|
|
67
52
|
* Handed over as a string: `onLoad` reads a byte array as Latin-1, and a
|
|
68
53
|
* `ü` in a description came back as `ü`. So only UTF-8 is served, and
|
|
@@ -194,7 +179,7 @@ async function loadDefaultExport(
|
|
|
194
179
|
() =>
|
|
195
180
|
reject(
|
|
196
181
|
new UserError(
|
|
197
|
-
`${kind} config ${configPath} did not finish evaluating within ${budget}ms (
|
|
182
|
+
`${kind} config ${configPath} did not finish evaluating within ${budget}ms (VX_CONFIG_WORKER_TIMEOUT)`,
|
|
198
183
|
),
|
|
199
184
|
),
|
|
200
185
|
budget,
|
|
@@ -296,7 +281,7 @@ const NAMED_FRAME = /\n\s+at .*[\\/]/
|
|
|
296
281
|
*
|
|
297
282
|
* `ResolveMessage` — an import that cannot be resolved. The usual cause has
|
|
298
283
|
* one answer: the workspace runs the vx binary and never installed `@vzn/vx`,
|
|
299
|
-
* which its own
|
|
284
|
+
* which its own `.vx/workspace.ts` imports. Bun's message also carries the
|
|
300
285
|
* module-cache bust query; the user gets the file they wrote.
|
|
301
286
|
*
|
|
302
287
|
* `BuildMessage` — a syntax or transpile error. Its message alone (`Expected
|
|
@@ -366,142 +351,23 @@ function loadFailure(err: unknown, configPath: string, kind: string): unknown {
|
|
|
366
351
|
return new UserError(thrownValueMessage(kind, configPath, Bun.inspect(err, { compact: true })))
|
|
367
352
|
}
|
|
368
353
|
|
|
369
|
-
export interface LoadProjectConfigOptions {
|
|
370
|
-
/** Observe the CURRENT environment: no eval cache, even when `evalCache` is passed. */
|
|
371
|
-
fresh?: boolean
|
|
372
|
-
/**
|
|
373
|
-
* Serve a provably-pure config from its cached evaluation (see
|
|
374
|
-
* config-cache.ts) and store a fresh one for next time. Off for `fresh`.
|
|
375
|
-
*/
|
|
376
|
-
evalCache?: { store: ConfigEvalStore; workspaceRoot?: string; workspaceFingerprint: string }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
354
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* one-
|
|
355
|
+
* Evaluate many configs at once: every file's bytes in parallel, then each
|
|
356
|
+
* one evaluated (in the order given, so a failure names the first broken
|
|
357
|
+
* file the way a one-by-one load did). A single-path load is the
|
|
358
|
+
* one-element case. Only `vx lock` evaluates; every run reads the lock.
|
|
384
359
|
*/
|
|
385
360
|
const LOAD_WIDTH = 128
|
|
386
361
|
|
|
387
|
-
export async function loadProjectConfigs(
|
|
388
|
-
configPaths: readonly string[],
|
|
389
|
-
opts?: LoadProjectConfigOptions,
|
|
390
|
-
): Promise<ProjectConfig[]> {
|
|
391
|
-
const evalCache = opts?.fresh === true ? undefined : opts?.evalCache
|
|
392
|
-
const store = evalCache?.store
|
|
393
|
-
const hashFile = store?.hashFile?.bind(store)
|
|
394
|
-
const hashBytes = store?.hashBytes?.bind(store)
|
|
395
|
-
// The slow path keys from the bytes it already holds; `hashFile` there
|
|
396
|
-
// memoised every closure file one autocommit upsert at a time (item 615).
|
|
397
|
-
const slowKeyHash =
|
|
398
|
-
hashBytes !== undefined ? { hashBytes } : hashFile !== undefined ? { hashFile } : {}
|
|
399
|
-
// The warm fast path: a config whose ordered closure the store remembers
|
|
400
|
-
// is keyed from per-file identities (a stat each, no read, no scan); the
|
|
401
|
-
// slow path below reads, gates and scans, and indexes the closure for
|
|
402
|
-
// next time when every import is explicit.
|
|
403
|
-
const closures =
|
|
404
|
-
hashFile !== undefined && store?.getConfigClosures !== undefined
|
|
405
|
-
? store.getConfigClosures(configPaths)
|
|
406
|
-
: new Map<string, string[]>()
|
|
407
|
-
// Every indexed closure's files identified in ONE batch, then keyed from
|
|
408
|
-
// the map; a file the batch could not stat has no identity, which makes
|
|
409
|
-
// that config's fast key miss and sends it down the slow path exactly as
|
|
410
|
-
// a throwing per-file `hashFile` did.
|
|
411
|
-
let identities: Map<string, string> | undefined
|
|
412
|
-
if (closures.size > 0 && store?.hashFiles !== undefined) {
|
|
413
|
-
const files = new Set<string>()
|
|
414
|
-
for (const closure of closures.values()) for (const f of closure) files.add(f)
|
|
415
|
-
identities = await store.hashFiles([...files])
|
|
416
|
-
}
|
|
417
|
-
// With the batch in hand a fast key is synchronous: an async key per
|
|
418
|
-
// config, awaiting identities already in the map, was ~5 ms of a
|
|
419
|
-
// 1,000-config warm load. Only a config off the fast path is awaited.
|
|
420
|
-
const fastKeyOf = (configPath: string): string | null => {
|
|
421
|
-
const closure = closures.get(configPath)
|
|
422
|
-
if (closure === undefined || evalCache === undefined || identities === undefined) return null
|
|
423
|
-
return configEvalKeyFromIdentities({
|
|
424
|
-
closure,
|
|
425
|
-
identities,
|
|
426
|
-
workspaceFingerprint: evalCache.workspaceFingerprint,
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
const fastHashFile = identities === undefined ? hashFile : undefined
|
|
430
|
-
interface Prepared {
|
|
431
|
-
configPath: string
|
|
432
|
-
bytes: Uint8Array | null
|
|
433
|
-
cacheKey: string | null
|
|
434
|
-
indexed: boolean
|
|
435
|
-
closure?: string[] | undefined
|
|
436
|
-
}
|
|
362
|
+
export async function loadProjectConfigs(configPaths: readonly string[]): Promise<ProjectConfig[]> {
|
|
437
363
|
const prepared = await Promise.all(
|
|
438
|
-
configPaths.map((configPath)
|
|
439
|
-
const fastKey = fastKeyOf(configPath)
|
|
440
|
-
return fastKey !== null
|
|
441
|
-
? { configPath, bytes: null, cacheKey: fastKey, indexed: true }
|
|
442
|
-
: slowPrepared(configPath)
|
|
443
|
-
}),
|
|
444
|
-
)
|
|
445
|
-
async function slowPrepared(configPath: string): Promise<Prepared> {
|
|
446
|
-
const closure = closures.get(configPath)
|
|
447
|
-
if (closure !== undefined && evalCache !== undefined && fastHashFile !== undefined) {
|
|
448
|
-
const fastKey = await configEvalKeyFromClosure({
|
|
449
|
-
closure,
|
|
450
|
-
hashFile: fastHashFile,
|
|
451
|
-
workspaceFingerprint: evalCache.workspaceFingerprint,
|
|
452
|
-
})
|
|
453
|
-
if (fastKey !== null) return { configPath, bytes: null, cacheKey: fastKey, indexed: true }
|
|
454
|
-
}
|
|
455
|
-
const bytes = await Bun.file(configPath)
|
|
456
|
-
.bytes()
|
|
457
|
-
.catch((err: unknown) => unreadable(err, configPath))
|
|
458
|
-
const keyed =
|
|
459
|
-
evalCache === undefined
|
|
460
|
-
? null
|
|
461
|
-
: await configEvalKey({
|
|
462
|
-
configPath,
|
|
463
|
-
bytes,
|
|
464
|
-
workspaceRoot: evalCache.workspaceRoot,
|
|
465
|
-
workspaceFingerprint: evalCache.workspaceFingerprint,
|
|
466
|
-
...slowKeyHash,
|
|
467
|
-
})
|
|
468
|
-
return {
|
|
364
|
+
configPaths.map(async (configPath) => ({
|
|
469
365
|
configPath,
|
|
470
|
-
bytes
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
let hits = new Map<string, string>()
|
|
477
|
-
if (evalCache !== undefined) {
|
|
478
|
-
const keys = prepared.map((p) => p.cacheKey).filter((k): k is string => k !== null)
|
|
479
|
-
if (keys.length > 0) {
|
|
480
|
-
if (evalCache.store.getConfigEvals !== undefined) {
|
|
481
|
-
hits = evalCache.store.getConfigEvals(keys)
|
|
482
|
-
} else {
|
|
483
|
-
for (const k of keys) {
|
|
484
|
-
const hit = evalCache.store.getConfigEval(k)
|
|
485
|
-
if (hit !== null) hits.set(k, hit)
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// What the round learned, written ONCE at the end: one transaction per
|
|
491
|
-
// table where each evaluation was its own (1,000 configs cold: 100 ms of
|
|
492
|
-
// autocommit inserts against 5, item 615). Written in `finally`, so a
|
|
493
|
-
// config that fails validation costs the next attempt only its own
|
|
494
|
-
// evaluation.
|
|
495
|
-
const evals: Array<readonly [string, string]> = []
|
|
496
|
-
const learnedClosures: Array<readonly [string, readonly string[]]> = []
|
|
497
|
-
interface Loaded {
|
|
498
|
-
config: ProjectConfig
|
|
499
|
-
evaluated?: readonly [string, string]
|
|
500
|
-
closure?: readonly [string, readonly string[]]
|
|
501
|
-
}
|
|
502
|
-
// Taken only when a config is evaluated in this process: reading every
|
|
503
|
-
// descriptor of `Bun` makes Bun build its lazy members (`bun:sql`,
|
|
504
|
-
// `node:stream`), ~7 ms of a two-config warm run where every load hit.
|
|
366
|
+
bytes: await Bun.file(configPath)
|
|
367
|
+
.bytes()
|
|
368
|
+
.catch((err: unknown) => unreadable(err, configPath)),
|
|
369
|
+
})),
|
|
370
|
+
)
|
|
505
371
|
let builtins: BuiltinSnapshot | undefined
|
|
506
372
|
let env: Readonly<Record<string, string | undefined>> = {}
|
|
507
373
|
let cwd = ''
|
|
@@ -510,47 +376,19 @@ export async function loadProjectConfigs(
|
|
|
510
376
|
// More than one evaluation in flight: a change seen after one load may
|
|
511
377
|
// be another's.
|
|
512
378
|
let overlapping = false
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const { configPath, cacheKey } = entry
|
|
516
|
-
// A fast key that missed: the closure is stale or the file changed.
|
|
517
|
-
// Take the slow path for this one config, which re-indexes it.
|
|
518
|
-
let bytes = entry.bytes
|
|
519
|
-
let closure = entry.closure
|
|
520
|
-
let key = cacheKey
|
|
521
|
-
if (entry.indexed) {
|
|
522
|
-
bytes = await Bun.file(configPath)
|
|
523
|
-
.bytes()
|
|
524
|
-
.catch((err: unknown) => unreadable(err, configPath))
|
|
525
|
-
const keyed = await configEvalKey({
|
|
526
|
-
configPath,
|
|
527
|
-
bytes,
|
|
528
|
-
workspaceRoot: evalCache!.workspaceRoot,
|
|
529
|
-
workspaceFingerprint: evalCache!.workspaceFingerprint,
|
|
530
|
-
...slowKeyHash,
|
|
531
|
-
})
|
|
532
|
-
key = keyed?.key ?? null
|
|
533
|
-
closure = keyed !== null && keyed.indexable ? keyed.closure : undefined
|
|
534
|
-
const slowRow = key === null ? null : (store!.getConfigEval(key) ?? null)
|
|
535
|
-
const slowHit = slowRow === null ? undefined : storedConfig(slowRow)
|
|
536
|
-
if (slowHit !== undefined) {
|
|
537
|
-
return closure === undefined
|
|
538
|
-
? { config: slowHit }
|
|
539
|
-
: { config: slowHit, closure: [configPath, closure] }
|
|
540
|
-
}
|
|
541
|
-
}
|
|
379
|
+
const loadOne = async (entry: (typeof prepared)[number]): Promise<ProjectConfig> => {
|
|
380
|
+
const { configPath, bytes } = entry
|
|
542
381
|
// A REPEAT load in this process re-evaluates in a worker, because the
|
|
543
382
|
// bust above cannot reach the config's import closure — see
|
|
544
|
-
// config-eval.ts. A FIRST load keeps the in-process import
|
|
545
|
-
// single `vx run` hot path never pays for a worker.
|
|
383
|
+
// config-eval.ts. A FIRST load keeps the in-process import.
|
|
546
384
|
const repeat = loadedConfigs.has(configPath)
|
|
547
385
|
loadedConfigs.add(configPath)
|
|
548
|
-
if (repeat) refuseUnprovidedImports(bytes
|
|
386
|
+
if (repeat) refuseUnprovidedImports(bytes, configPath, 'Project')
|
|
549
387
|
const mod = repeat
|
|
550
388
|
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
551
389
|
throw loadFailure(err, configPath, 'Project')
|
|
552
390
|
})
|
|
553
|
-
: await loadDefaultExport(configPath, 'Project', bytes
|
|
391
|
+
: await loadDefaultExport(configPath, 'Project', bytes)
|
|
554
392
|
// Before anything reads through them: a replaced `Array.prototype.includes`
|
|
555
393
|
// turned the JSON-data walk's own check into "a cyclic reference".
|
|
556
394
|
const changed = repeat
|
|
@@ -578,18 +416,12 @@ export async function loadProjectConfigs(
|
|
|
578
416
|
// malformed config reports the identical UserError whether it was
|
|
579
417
|
// evaluated in-process or in a worker.
|
|
580
418
|
validateProjectConfig(mod as ProjectConfig, configPath)
|
|
581
|
-
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
// config is JSON data (config-schema.ts), so the copy loses nothing.
|
|
588
|
-
const config = JSON.parse(json) as ProjectConfig
|
|
589
|
-
if (key === null) return { config }
|
|
590
|
-
return closure === undefined
|
|
591
|
-
? { config, evaluated: [key, json] }
|
|
592
|
-
: { config, evaluated: [key, json], closure: [configPath, closure] }
|
|
419
|
+
// A tree of its own, as the lock hands out. The module object shares
|
|
420
|
+
// what the config shares: one preset's task in two configs, one `exec`
|
|
421
|
+
// in two tasks. A `project` hook that edits in place then edited them
|
|
422
|
+
// all (item 967). A config is JSON data (config-schema.ts), so the copy
|
|
423
|
+
// loses nothing.
|
|
424
|
+
return JSON.parse(JSON.stringify(mod)) as ProjectConfig
|
|
593
425
|
}
|
|
594
426
|
// One worker for every repeat load in this round, however many there are.
|
|
595
427
|
const endRound = beginEvalRound()
|
|
@@ -598,49 +430,29 @@ export async function loadProjectConfigs(
|
|
|
598
430
|
// configs at ~160 ms of imports where 64 at once take ~60 (D-68), and
|
|
599
431
|
// 128 took `load configs` from 432 to 411 ms more; the width bounds the
|
|
600
432
|
// files a module load may hold open, so it stays under macOS's default
|
|
601
|
-
// 256-descriptor limit. A failure
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const results: Array<Loaded | { failed: unknown }> = []
|
|
606
|
-
// A hit is taken here, synchronously as before: through the lanes each
|
|
607
|
-
// cost the warm path a call and an await.
|
|
608
|
-
const misses: number[] = []
|
|
609
|
-
for (let i = 0; i < prepared.length; i++) {
|
|
610
|
-
const key = prepared[i]!.cacheKey
|
|
611
|
-
const hit = key === null ? undefined : hits.get(key)
|
|
612
|
-
// Stored AFTER validation, so a hit needs none; the key covers every
|
|
613
|
-
// byte the evaluation could have read.
|
|
614
|
-
const cached = hit === undefined ? undefined : storedConfig(hit)
|
|
615
|
-
if (cached !== undefined) results[i] = { config: cached }
|
|
616
|
-
else misses.push(i)
|
|
617
|
-
}
|
|
618
|
-
if (misses.length > 0) {
|
|
433
|
+
// 256-descriptor limit. A failure stops nothing; the error thrown is
|
|
434
|
+
// the first in order, as the serial loop's was.
|
|
435
|
+
const results: Array<ProjectConfig | { failed: unknown }> = []
|
|
436
|
+
if (prepared.length > 0) {
|
|
619
437
|
unredirect = stdoutToStderr()
|
|
620
438
|
builtins = builtinSnapshot()
|
|
621
439
|
env = { ...process.env }
|
|
622
440
|
cwd = process.cwd()
|
|
623
441
|
umask = currentUmask()
|
|
624
442
|
}
|
|
625
|
-
overlapping =
|
|
443
|
+
overlapping = prepared.length > 1
|
|
626
444
|
let next = 0
|
|
627
445
|
const lane = async (): Promise<void> => {
|
|
628
|
-
while (next <
|
|
629
|
-
const i =
|
|
446
|
+
while (next < prepared.length) {
|
|
447
|
+
const i = next++
|
|
630
448
|
results[i] = await loadOne(prepared[i]!).catch((err: unknown) => ({ failed: err }))
|
|
631
449
|
}
|
|
632
450
|
}
|
|
633
|
-
await Promise.all(Array.from({ length: Math.min(LOAD_WIDTH,
|
|
451
|
+
await Promise.all(Array.from({ length: Math.min(LOAD_WIDTH, prepared.length) }, lane))
|
|
634
452
|
let first: { failed: unknown } | undefined
|
|
635
|
-
for (const r of results)
|
|
636
|
-
if ('failed' in r) first ??= r
|
|
637
|
-
else {
|
|
638
|
-
if (r.evaluated !== undefined) evals.push(r.evaluated)
|
|
639
|
-
if (r.closure !== undefined) learnedClosures.push(r.closure)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
453
|
+
for (const r of results) if ('failed' in r) first ??= r
|
|
642
454
|
const changed =
|
|
643
|
-
|
|
455
|
+
prepared.length > 0
|
|
644
456
|
? [
|
|
645
457
|
...restoreBuiltins(builtins),
|
|
646
458
|
...restoreEnv(env),
|
|
@@ -648,9 +460,9 @@ export async function loadProjectConfigs(
|
|
|
648
460
|
...restoreUmask(umask),
|
|
649
461
|
]
|
|
650
462
|
: []
|
|
651
|
-
// Overlapping loads check most built-ins and the env only here, so a
|
|
652
|
-
// config made may have broken another's load: it is refused
|
|
653
|
-
//
|
|
463
|
+
// Overlapping loads check most built-ins and the env only here, so a
|
|
464
|
+
// change one config made may have broken another's load: it is refused
|
|
465
|
+
// first.
|
|
654
466
|
const changedInRound =
|
|
655
467
|
first?.failed instanceof ChangedInRound
|
|
656
468
|
? first.failed.changed
|
|
@@ -658,9 +470,7 @@ export async function loadProjectConfigs(
|
|
|
658
470
|
? changed
|
|
659
471
|
: undefined
|
|
660
472
|
if (changedInRound !== undefined) {
|
|
661
|
-
|
|
662
|
-
for (const i of misses) {
|
|
663
|
-
const configPath = prepared[i]!.configPath
|
|
473
|
+
for (const { configPath } of prepared) {
|
|
664
474
|
const own = await builtinsChangedBy(configPath)
|
|
665
475
|
// The blaming worker is gone, so this thread reads alone; and a
|
|
666
476
|
// worker that outlived its budget was ended before it could put
|
|
@@ -674,24 +484,11 @@ export async function loadProjectConfigs(
|
|
|
674
484
|
// One load in the round, in the worker (a repeat load), is the one
|
|
675
485
|
// that moved it: the worker no longer reads the umask to say so.
|
|
676
486
|
if (changed.length > 0)
|
|
677
|
-
throw builtinsChanged(
|
|
678
|
-
|
|
679
|
-
misses.length === 1 ? prepared[misses[0]!]!.configPath : undefined,
|
|
680
|
-
)
|
|
681
|
-
return results.map((r) => (r as Loaded).config)
|
|
487
|
+
throw builtinsChanged(changed, prepared.length === 1 ? prepared[0]!.configPath : undefined)
|
|
488
|
+
return results as ProjectConfig[]
|
|
682
489
|
} finally {
|
|
683
490
|
unredirect?.()
|
|
684
491
|
endRound()
|
|
685
|
-
if (store !== undefined && !tainted) {
|
|
686
|
-
if (evals.length > 0) {
|
|
687
|
-
if (store.putConfigEvals !== undefined) store.putConfigEvals(evals)
|
|
688
|
-
else for (const [k, json] of evals) store.putConfigEval(k, json)
|
|
689
|
-
}
|
|
690
|
-
if (learnedClosures.length > 0 && store.putConfigClosure !== undefined) {
|
|
691
|
-
if (store.putConfigClosures !== undefined) store.putConfigClosures(learnedClosures)
|
|
692
|
-
else for (const [p, files] of learnedClosures) store.putConfigClosure(p, files)
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
492
|
}
|
|
696
493
|
}
|
|
697
494
|
|
|
@@ -699,8 +496,7 @@ export async function loadProjectConfigs(
|
|
|
699
496
|
* Every route to fd 1 sent to stderr while configs evaluate in this
|
|
700
497
|
* process; returns the release. A verb's stdout is its output (`vx show
|
|
701
498
|
* --format json`), and a config's `console.log` came out ahead of the
|
|
702
|
-
* JSON. The worker does the same for a repeat load (D-64)
|
|
703
|
-
* its whole serve. Taken before a round's built-in snapshot and released
|
|
499
|
+
* JSON. The worker does the same for a repeat load (D-64). Taken before a round's built-in snapshot and released
|
|
704
500
|
* after its check, so the guard sees no change of vx's own.
|
|
705
501
|
*
|
|
706
502
|
* Counted, because rounds overlap (`--affected`'s per-file sweep loads each
|
|
@@ -713,7 +509,7 @@ let redirectHolders = 0
|
|
|
713
509
|
let undoRedirect = (): void => {}
|
|
714
510
|
/**
|
|
715
511
|
* Bun's own `Console`, taken at load: the console a round finds may be a
|
|
716
|
-
* constructed one (
|
|
512
|
+
* constructed one (a plugin's verb may install its own), which has no `Console`.
|
|
717
513
|
* The global's, not `node:console`'s: the playground bundles this module
|
|
718
514
|
* for the browser, where that specifier is a stub.
|
|
719
515
|
*/
|
|
@@ -831,11 +627,11 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
|
831
627
|
'JSONC',
|
|
832
628
|
'Subprocess',
|
|
833
629
|
'Transpiler',
|
|
834
|
-
'YAML',
|
|
835
630
|
'color',
|
|
836
631
|
'deepEquals',
|
|
837
632
|
'env',
|
|
838
633
|
'file',
|
|
634
|
+
'gunzipSync',
|
|
839
635
|
'hash',
|
|
840
636
|
'inspect',
|
|
841
637
|
'main',
|
|
@@ -843,7 +639,6 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
|
843
639
|
'plugin',
|
|
844
640
|
'randomUUIDv7',
|
|
845
641
|
'resolveSync',
|
|
846
|
-
'semver',
|
|
847
642
|
'serve',
|
|
848
643
|
'sleep',
|
|
849
644
|
'sleepSync',
|
|
@@ -1076,11 +871,8 @@ function sameDescriptor(p: Primitives, a: PropertyDescriptor, b: PropertyDescrip
|
|
|
1076
871
|
)
|
|
1077
872
|
}
|
|
1078
873
|
|
|
1079
|
-
export async function loadProjectConfig(
|
|
1080
|
-
configPath
|
|
1081
|
-
opts?: LoadProjectConfigOptions,
|
|
1082
|
-
): Promise<ProjectConfig> {
|
|
1083
|
-
const [config] = await loadProjectConfigs([configPath], opts)
|
|
874
|
+
export async function loadProjectConfig(configPath: string): Promise<ProjectConfig> {
|
|
875
|
+
const [config] = await loadProjectConfigs([configPath])
|
|
1084
876
|
return config!
|
|
1085
877
|
}
|
|
1086
878
|
|
|
@@ -1088,10 +880,10 @@ export async function loadProjectConfig(
|
|
|
1088
880
|
const workspaceLoadedAt = new Map<string, number>()
|
|
1089
881
|
|
|
1090
882
|
/**
|
|
1091
|
-
* A repeat load in one process (`vx
|
|
1092
|
-
*
|
|
883
|
+
* A repeat load in one process (`vx watch`'s every cycle, a plugin verb's
|
|
884
|
+
* every call) busts the workspace config's own URL, but Bun answers what it
|
|
1093
885
|
* imports from the module registry: an edited local plugin kept its first
|
|
1094
|
-
* version and
|
|
886
|
+
* version and its old tasks were served with no word (item 1046).
|
|
1095
887
|
* Bun cannot evaluate an imported module again, so the load is refused,
|
|
1096
888
|
* naming the file, as `vx watch` already says when it sees the edit. Only
|
|
1097
889
|
* a repeat load pays for the walk; a CLI verb loads once.
|
|
@@ -1111,23 +903,43 @@ async function refuseStaleWorkspaceImports(configPath: string, since: number): P
|
|
|
1111
903
|
if (Math.floor(mtime) > since) {
|
|
1112
904
|
// The walk names real paths: a root reached through a symlink (macOS's
|
|
1113
905
|
// temp dir) named `../target/helper.mjs` from the link.
|
|
1114
|
-
const
|
|
906
|
+
const real = realpathSync(configPath)
|
|
907
|
+
const root = workspaceRootOf(real)
|
|
1115
908
|
throw new UserError(
|
|
1116
|
-
`${path.
|
|
909
|
+
`${path.relative(root, real)} imports ${path.relative(root, file)}, which changed after this process loaded it; a running process cannot evaluate an imported module again — restart it to apply the edit`,
|
|
1117
910
|
)
|
|
1118
911
|
}
|
|
1119
912
|
}
|
|
1120
913
|
}
|
|
1121
914
|
|
|
1122
915
|
/**
|
|
1123
|
-
* Find and load
|
|
916
|
+
* Find and load `.vx/workspace.{ts,mts,js,mjs,cts,cjs}` from the workspace
|
|
1124
917
|
* root. Returns `null` if no such file exists (the common case;
|
|
1125
918
|
* the schema is fully optional). Validates the shape and throws
|
|
1126
919
|
* a `UserError` on malformed input.
|
|
1127
920
|
*/
|
|
1128
921
|
export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig | null> {
|
|
1129
|
-
|
|
1130
|
-
|
|
922
|
+
// The root lookup already probed for it (principle 9): read what it
|
|
923
|
+
// found. A file gone since (a `vx watch` cycle after a rename) is probed for.
|
|
924
|
+
const found = await workspaceFile(path.resolve(root))
|
|
925
|
+
const known =
|
|
926
|
+
found !== null && workspaceRootOf(found) === path.resolve(root)
|
|
927
|
+
? await Bun.file(found)
|
|
928
|
+
.bytes()
|
|
929
|
+
.then(
|
|
930
|
+
(bytes) => ({ configPath: found, bytes }),
|
|
931
|
+
() => null,
|
|
932
|
+
)
|
|
933
|
+
: null
|
|
934
|
+
const candidates =
|
|
935
|
+
known !== null
|
|
936
|
+
? [known]
|
|
937
|
+
: found === null
|
|
938
|
+
? []
|
|
939
|
+
: WORKSPACE_CONFIG_FILENAMES.map((f) => ({ configPath: path.join(root, f), bytes: null }))
|
|
940
|
+
for (const candidate of candidates) {
|
|
941
|
+
const { configPath } = candidate
|
|
942
|
+
const bytes = candidate.bytes ?? (await readOnce(undefined, configPath))
|
|
1131
943
|
if (bytes === null) continue
|
|
1132
944
|
const since = workspaceLoadedAt.get(configPath)
|
|
1133
945
|
if (since !== undefined) await refuseStaleWorkspaceImports(configPath, since)
|
|
@@ -1172,20 +984,3 @@ export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig
|
|
|
1172
984
|
}
|
|
1173
985
|
return null
|
|
1174
986
|
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* A stored evaluation, or undefined when the row is not one: a row cut
|
|
1178
|
-
* short (a crash mid-write, a bad disk) failed every later run with a
|
|
1179
|
-
* `SyntaxError` stack from this loader until the cache was wiped
|
|
1180
|
-
* (fuzzed, L-17). As a miss it is evaluated again and the row replaced.
|
|
1181
|
-
*/
|
|
1182
|
-
function storedConfig(json: string): ProjectConfig | undefined {
|
|
1183
|
-
try {
|
|
1184
|
-
const v: unknown = JSON.parse(json)
|
|
1185
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
1186
|
-
? (v as ProjectConfig)
|
|
1187
|
-
: undefined
|
|
1188
|
-
} catch {
|
|
1189
|
-
return undefined
|
|
1190
|
-
}
|
|
1191
|
-
}
|
package/src/workspace/repo-id.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
statSync,
|
|
18
18
|
} from 'node:fs'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
+
import { gitEnv } from '../util/index.js'
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 16 hex of sha256 of the repo key, sha256(`<identity>#<workspace path in
|
|
@@ -235,7 +236,7 @@ async function firstCommit(commonDir: string, root: string): Promise<string | nu
|
|
|
235
236
|
|
|
236
237
|
async function git(cwd: string, args: string[]): Promise<string | null> {
|
|
237
238
|
try {
|
|
238
|
-
const p = Bun.spawn(['git', ...args], { cwd, stdout: 'pipe', stderr: 'ignore' })
|
|
239
|
+
const p = Bun.spawn(['git', ...args], { cwd, env: gitEnv(), stdout: 'pipe', stderr: 'ignore' })
|
|
239
240
|
const [out, code] = await Promise.all([new Response(p.stdout).text(), p.exited])
|
|
240
241
|
return code === 0 ? out : null
|
|
241
242
|
} catch {
|