@vzn/vx 0.0.182 → 0.0.184
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/config.ts +23 -6
- package/src/workspace/project-loader.ts +95 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.184",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.184",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.184",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.184",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.184"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/config.ts
CHANGED
|
@@ -530,6 +530,17 @@ type DependsOnEntry<K extends string> =
|
|
|
530
530
|
| `${string}#${string}`
|
|
531
531
|
| `${string}*${string}`
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* The keys of `T` that `Shape` does not declare, typed `never`. A generic
|
|
535
|
+
* argument skips TypeScript's excess-property check, so a Turbo-shaped
|
|
536
|
+
* `outputs` on a task, or `cwd` in `exec`, type-checked and failed only
|
|
537
|
+
* when vx loaded the config (D-69); `never` puts the error on that line.
|
|
538
|
+
*/
|
|
539
|
+
type Known<T, Shape> = { [P in keyof T]: P extends keyof Shape ? unknown : never }
|
|
540
|
+
|
|
541
|
+
/** `T[P]` when `T` has it; `unknown` (no keys to check) when it does not. */
|
|
542
|
+
type At<T, P extends PropertyKey> = T extends { readonly [Q in P]?: infer V } ? V : unknown
|
|
543
|
+
|
|
533
544
|
/**
|
|
534
545
|
* Identity function — exists only so TypeScript narrows literal types
|
|
535
546
|
* and, crucially, **validates `dependsOn` against this project's own
|
|
@@ -538,13 +549,19 @@ type DependsOnEntry<K extends string> =
|
|
|
538
549
|
* free strings. Runtime behavior is unchanged (it returns its input).
|
|
539
550
|
*/
|
|
540
551
|
export function defineProject<const T extends ProjectConfig>(
|
|
541
|
-
config: T &
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
552
|
+
config: T &
|
|
553
|
+
Known<T, ProjectConfig> & {
|
|
554
|
+
tasks?: {
|
|
555
|
+
[K in keyof NonNullable<T['tasks']>]?: Known<NonNullable<T['tasks']>[K], TaskConfig> & {
|
|
556
|
+
dependsOn?: readonly DependsOnEntry<Extract<keyof NonNullable<T['tasks']>, string>>[]
|
|
557
|
+
exec?: Known<At<NonNullable<T['tasks']>[K], 'exec'>, ExecConfig>
|
|
558
|
+
cache?: Known<At<NonNullable<T['tasks']>[K], 'cache'>, CacheConfig> & {
|
|
559
|
+
inputs?: Known<At<At<NonNullable<T['tasks']>[K], 'cache'>, 'inputs'>, CacheInputs>
|
|
560
|
+
outputs?: Known<At<At<NonNullable<T['tasks']>[K], 'cache'>, 'outputs'>, CacheOutputs>
|
|
561
|
+
}
|
|
562
|
+
}
|
|
545
563
|
}
|
|
546
|
-
}
|
|
547
|
-
},
|
|
564
|
+
},
|
|
548
565
|
): T {
|
|
549
566
|
return config as T
|
|
550
567
|
}
|
|
@@ -291,6 +291,8 @@ export interface LoadProjectConfigOptions {
|
|
|
291
291
|
* order given, so a failure names the first broken file the way a
|
|
292
292
|
* one-by-one load did). A single-path load is the one-element case.
|
|
293
293
|
*/
|
|
294
|
+
const LOAD_WIDTH = 64
|
|
295
|
+
|
|
294
296
|
export async function loadProjectConfigs(
|
|
295
297
|
configPaths: readonly string[],
|
|
296
298
|
opts?: LoadProjectConfigOptions,
|
|
@@ -372,7 +374,6 @@ export async function loadProjectConfigs(
|
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
}
|
|
375
|
-
const out: ProjectConfig[] = []
|
|
376
377
|
// What the round learned, written ONCE at the end: one transaction per
|
|
377
378
|
// table where each evaluation was its own (1,000 configs cold: 100 ms of
|
|
378
379
|
// autocommit inserts against 5, item 615). Written in `finally`, so a
|
|
@@ -380,73 +381,107 @@ export async function loadProjectConfigs(
|
|
|
380
381
|
// evaluation.
|
|
381
382
|
const evals: Array<readonly [string, string]> = []
|
|
382
383
|
const learnedClosures: Array<readonly [string, readonly string[]]> = []
|
|
384
|
+
interface Loaded {
|
|
385
|
+
config: ProjectConfig
|
|
386
|
+
evaluated?: readonly [string, string]
|
|
387
|
+
closure?: readonly [string, readonly string[]]
|
|
388
|
+
}
|
|
389
|
+
const loadOne = async (entry: (typeof prepared)[number]): Promise<Loaded> => {
|
|
390
|
+
const { configPath, cacheKey } = entry
|
|
391
|
+
// A fast key that missed: the closure is stale or the file changed.
|
|
392
|
+
// Take the slow path for this one config, which re-indexes it.
|
|
393
|
+
let bytes = entry.bytes
|
|
394
|
+
let closure = entry.closure
|
|
395
|
+
let key = cacheKey
|
|
396
|
+
if (entry.indexed) {
|
|
397
|
+
bytes = await Bun.file(configPath).bytes()
|
|
398
|
+
const keyed = await configEvalKey({
|
|
399
|
+
configPath,
|
|
400
|
+
bytes,
|
|
401
|
+
workspaceRoot: evalCache!.workspaceRoot,
|
|
402
|
+
workspaceFingerprint: evalCache!.workspaceFingerprint,
|
|
403
|
+
...slowKeyHash,
|
|
404
|
+
})
|
|
405
|
+
key = keyed?.key ?? null
|
|
406
|
+
closure = keyed !== null && keyed.indexable ? keyed.closure : undefined
|
|
407
|
+
const slowRow = key === null ? null : (store!.getConfigEval(key) ?? null)
|
|
408
|
+
const slowHit = slowRow === null ? undefined : storedConfig(slowRow)
|
|
409
|
+
if (slowHit !== undefined) {
|
|
410
|
+
return closure === undefined
|
|
411
|
+
? { config: slowHit }
|
|
412
|
+
: { config: slowHit, closure: [configPath, closure] }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// A REPEAT load in this process re-evaluates in a worker, because the
|
|
416
|
+
// bust above cannot reach the config's import closure — see
|
|
417
|
+
// config-eval.ts. A FIRST load keeps the in-process import, so the
|
|
418
|
+
// single `vx run` hot path never pays for a worker.
|
|
419
|
+
const repeat = loadedConfigs.has(configPath)
|
|
420
|
+
loadedConfigs.add(configPath)
|
|
421
|
+
if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
|
|
422
|
+
const mod = repeat
|
|
423
|
+
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
424
|
+
throw configLoadError(err, configPath, 'Project') ?? err
|
|
425
|
+
})
|
|
426
|
+
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
427
|
+
assertDefaultObject(mod, 'Project', configPath)
|
|
428
|
+
// Validation runs HERE, on whichever object we ended up with, so a
|
|
429
|
+
// malformed config reports the identical UserError whether it was
|
|
430
|
+
// evaluated in-process or in a worker.
|
|
431
|
+
validateProjectConfig(mod as ProjectConfig, configPath)
|
|
432
|
+
const json = JSON.stringify(mod)
|
|
433
|
+
// A tree of its own, as a hit and the lock hand out. The module object
|
|
434
|
+
// shares what the config shares: one preset's task in two configs, one
|
|
435
|
+
// `exec` in two tasks. A `project` hook that edits in place then edited
|
|
436
|
+
// them all, so a cold run ran `echo P +plug +plug` where the warm run
|
|
437
|
+
// and `--frozen` ran `echo P +plug`, under another key (item 967). A
|
|
438
|
+
// config is JSON data (config-schema.ts), so the copy loses nothing.
|
|
439
|
+
const config = JSON.parse(json) as ProjectConfig
|
|
440
|
+
if (key === null) return { config }
|
|
441
|
+
return closure === undefined
|
|
442
|
+
? { config, evaluated: [key, json] }
|
|
443
|
+
: { config, evaluated: [key, json], closure: [configPath, closure] }
|
|
444
|
+
}
|
|
383
445
|
// One worker for every repeat load in this round, however many there are.
|
|
384
446
|
const endRound = beginEvalRound()
|
|
385
447
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
448
|
+
// Loaded LOAD_WIDTH at a time: one import after another put 1,000 cold
|
|
449
|
+
// configs at ~160 ms of imports where 64 at once take ~60 (D-68), and
|
|
450
|
+
// the width bounds the files a module load may hold open. A failure
|
|
451
|
+
// stops nothing (the rest are evaluated and stored for the next
|
|
452
|
+
// attempt); the error thrown is the first in order, as the serial
|
|
453
|
+
// loop's was.
|
|
454
|
+
const results: Array<Loaded | { failed: unknown }> = []
|
|
455
|
+
// A hit is taken here, synchronously as before: through the lanes each
|
|
456
|
+
// cost the warm path a call and an await.
|
|
457
|
+
const misses: number[] = []
|
|
458
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
459
|
+
const key = prepared[i]!.cacheKey
|
|
460
|
+
const hit = key === null ? undefined : hits.get(key)
|
|
389
461
|
// Stored AFTER validation, so a hit needs none; the key covers every
|
|
390
462
|
// byte the evaluation could have read.
|
|
391
463
|
const cached = hit === undefined ? undefined : storedConfig(hit)
|
|
392
|
-
if (cached !== undefined) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
let key = cacheKey
|
|
401
|
-
if (entry.indexed) {
|
|
402
|
-
bytes = await Bun.file(configPath).bytes()
|
|
403
|
-
const keyed = await configEvalKey({
|
|
404
|
-
configPath,
|
|
405
|
-
bytes,
|
|
406
|
-
workspaceRoot: evalCache!.workspaceRoot,
|
|
407
|
-
workspaceFingerprint: evalCache!.workspaceFingerprint,
|
|
408
|
-
...slowKeyHash,
|
|
409
|
-
})
|
|
410
|
-
key = keyed?.key ?? null
|
|
411
|
-
closure = keyed !== null && keyed.indexable ? keyed.closure : undefined
|
|
412
|
-
const slowRow = key === null ? null : (store!.getConfigEval(key) ?? null)
|
|
413
|
-
const slowHit = slowRow === null ? undefined : storedConfig(slowRow)
|
|
414
|
-
if (slowHit !== undefined) {
|
|
415
|
-
out.push(slowHit)
|
|
416
|
-
if (closure !== undefined) learnedClosures.push([configPath, closure])
|
|
417
|
-
continue
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// A REPEAT load in this process re-evaluates in a worker, because the
|
|
421
|
-
// bust above cannot reach the config's import closure — see
|
|
422
|
-
// config-eval.ts. A FIRST load keeps the in-process import, so the
|
|
423
|
-
// single `vx run` hot path never pays for a worker.
|
|
424
|
-
const repeat = loadedConfigs.has(configPath)
|
|
425
|
-
loadedConfigs.add(configPath)
|
|
426
|
-
if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
|
|
427
|
-
const mod = repeat
|
|
428
|
-
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
429
|
-
throw configLoadError(err, configPath, 'Project') ?? err
|
|
430
|
-
})
|
|
431
|
-
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
432
|
-
assertDefaultObject(mod, 'Project', configPath)
|
|
433
|
-
// Validation runs HERE, on whichever object we ended up with, so a
|
|
434
|
-
// malformed config reports the identical UserError whether it was
|
|
435
|
-
// evaluated in-process or in a worker.
|
|
436
|
-
validateProjectConfig(mod as ProjectConfig, configPath)
|
|
437
|
-
const json = JSON.stringify(mod)
|
|
438
|
-
if (key !== null) {
|
|
439
|
-
evals.push([key, json])
|
|
440
|
-
if (closure !== undefined) learnedClosures.push([configPath, closure])
|
|
464
|
+
if (cached !== undefined) results[i] = { config: cached }
|
|
465
|
+
else misses.push(i)
|
|
466
|
+
}
|
|
467
|
+
let next = 0
|
|
468
|
+
const lane = async (): Promise<void> => {
|
|
469
|
+
while (next < misses.length) {
|
|
470
|
+
const i = misses[next++]!
|
|
471
|
+
results[i] = await loadOne(prepared[i]!).catch((err: unknown) => ({ failed: err }))
|
|
441
472
|
}
|
|
442
|
-
// A tree of its own, as a hit and the lock hand out. The module object
|
|
443
|
-
// shares what the config shares: one preset's task in two configs, one
|
|
444
|
-
// `exec` in two tasks. A `project` hook that edits in place then edited
|
|
445
|
-
// them all, so a cold run ran `echo P +plug +plug` where the warm run
|
|
446
|
-
// and `--frozen` ran `echo P +plug`, under another key (item 967). A
|
|
447
|
-
// config is JSON data (config-schema.ts), so the copy loses nothing.
|
|
448
|
-
out.push(JSON.parse(json) as ProjectConfig)
|
|
449
473
|
}
|
|
474
|
+
await Promise.all(Array.from({ length: Math.min(LOAD_WIDTH, misses.length) }, lane))
|
|
475
|
+
const out: ProjectConfig[] = []
|
|
476
|
+
for (const r of results) {
|
|
477
|
+
if ('failed' in r) continue
|
|
478
|
+
if (r.evaluated !== undefined) evals.push(r.evaluated)
|
|
479
|
+
if (r.closure !== undefined) learnedClosures.push(r.closure)
|
|
480
|
+
out.push(r.config)
|
|
481
|
+
}
|
|
482
|
+
const first = results.find((r) => 'failed' in r)
|
|
483
|
+
if (first !== undefined) throw (first as { failed: unknown }).failed
|
|
484
|
+
return out
|
|
450
485
|
} finally {
|
|
451
486
|
endRound()
|
|
452
487
|
if (store !== undefined) {
|
|
@@ -460,7 +495,6 @@ export async function loadProjectConfigs(
|
|
|
460
495
|
}
|
|
461
496
|
}
|
|
462
497
|
}
|
|
463
|
-
return out
|
|
464
498
|
}
|
|
465
499
|
|
|
466
500
|
export async function loadProjectConfig(
|