@vzn/vx 0.0.196 → 0.0.198
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/cache/cache.ts +45 -3
- package/src/workspace/project-loader.ts +32 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.198",
|
|
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.198",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.198",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.198",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.198"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/cache.ts
CHANGED
|
@@ -26,7 +26,16 @@
|
|
|
26
26
|
// layer speaks is `CacheLayer` in layer.ts; `plugin-host.ts` enforces it.
|
|
27
27
|
|
|
28
28
|
import { Database, type SQLQueryBindings } from 'bun:sqlite'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
accessSync,
|
|
31
|
+
closeSync,
|
|
32
|
+
constants,
|
|
33
|
+
existsSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
openSync,
|
|
36
|
+
renameSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
} from 'node:fs'
|
|
30
39
|
import { readdir, rm, stat, unlink, writeFile } from 'node:fs/promises'
|
|
31
40
|
import path from 'node:path'
|
|
32
41
|
import { createTables } from './schema.js'
|
|
@@ -129,6 +138,26 @@ function unreadableIndexError(dbFile: string, err: unknown): UserError {
|
|
|
129
138
|
)
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
/**
|
|
142
|
+
* SQLite reports a descriptor it could not get as `SQLITE_CANTOPEN`, never
|
|
143
|
+
* `EMFILE`, so an open under a low `ulimit -n` reached the user as a bare
|
|
144
|
+
* `vx: SQLiteError: unable to open database file` (D's lead, A-56). One
|
|
145
|
+
* open of a file that exists says which it was.
|
|
146
|
+
*/
|
|
147
|
+
function outOfFdsAtOpen(dbFile: string, err: unknown): UserError | undefined {
|
|
148
|
+
const code = (err as { code?: unknown } | null)?.code
|
|
149
|
+
if (typeof code !== 'string' || !code.startsWith('SQLITE_CANTOPEN')) return undefined
|
|
150
|
+
try {
|
|
151
|
+
closeSync(openSync(process.execPath, 'r'))
|
|
152
|
+
return undefined
|
|
153
|
+
} catch (probe) {
|
|
154
|
+
if (!isOutOfFds(probe)) return undefined
|
|
155
|
+
return new UserError(
|
|
156
|
+
`the cache index ${dbFile} could not be opened (${(err as Error).message}) — ${OUT_OF_FDS_HINT}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
/** Say once, on the channel the opener has, that an upgrade emptied the index. */
|
|
133
162
|
export function noteSchemaReset(cache: Cache, warn: (message: string) => void): void {
|
|
134
163
|
if (cache.formatChange !== null) {
|
|
@@ -527,7 +556,11 @@ export class Cache implements CacheLayer {
|
|
|
527
556
|
const absent = mode === 'inspect' && !existsSync(dbFile)
|
|
528
557
|
this.writeBlocked = absent ? 'no index there yet' : openCacheDir(cacheDir)
|
|
529
558
|
this.write = localPolicy.write && this.writeBlocked === null
|
|
530
|
-
|
|
559
|
+
try {
|
|
560
|
+
this.db = new Database(absent ? ':memory:' : dbFile, { create: true })
|
|
561
|
+
} catch (err) {
|
|
562
|
+
throw outOfFdsAtOpen(dbFile, err) ?? err
|
|
563
|
+
}
|
|
531
564
|
// busy_timeout makes concurrent writers wait for the lock instead of
|
|
532
565
|
// failing immediately with SQLITE_BUSY. Two parallel `vx run`
|
|
533
566
|
// invocations in CI is a normal pattern; without this the second one
|
|
@@ -544,7 +577,16 @@ export class Cache implements CacheLayer {
|
|
|
544
577
|
try {
|
|
545
578
|
return read()
|
|
546
579
|
} catch (err) {
|
|
547
|
-
if (!unreadableIndex(err))
|
|
580
|
+
if (!unreadableIndex(err)) {
|
|
581
|
+
const fds = outOfFdsAtOpen(dbFile, err)
|
|
582
|
+
if (fds === undefined) throw err
|
|
583
|
+
try {
|
|
584
|
+
closeDb(this.db)
|
|
585
|
+
} catch {
|
|
586
|
+
// The refusal is the error worth reporting.
|
|
587
|
+
}
|
|
588
|
+
throw fds
|
|
589
|
+
}
|
|
548
590
|
try {
|
|
549
591
|
closeDb(this.db)
|
|
550
592
|
} catch {
|
|
@@ -387,6 +387,7 @@ export async function loadProjectConfigs(
|
|
|
387
387
|
closure?: readonly [string, readonly string[]]
|
|
388
388
|
}
|
|
389
389
|
const builtins = builtinSnapshot()
|
|
390
|
+
const env = { ...process.env }
|
|
390
391
|
const loadOne = async (entry: (typeof prepared)[number]): Promise<Loaded> => {
|
|
391
392
|
const { configPath, cacheKey } = entry
|
|
392
393
|
// A fast key that missed: the closure is stale or the file changed.
|
|
@@ -427,7 +428,7 @@ export async function loadProjectConfigs(
|
|
|
427
428
|
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
428
429
|
// Before anything reads through them: a replaced `Array.prototype.includes`
|
|
429
430
|
// turned the JSON-data walk's own check into "a cyclic reference".
|
|
430
|
-
const changed = repeat ? [] : restoreBuiltins(builtins)
|
|
431
|
+
const changed = repeat ? [] : [...restoreBuiltins(builtins), ...restoreEnv(env)]
|
|
431
432
|
if (changed.length > 0) throw builtinsChanged(changed, configPath)
|
|
432
433
|
assertDefaultObject(mod, 'Project', configPath)
|
|
433
434
|
// Validation runs HERE, on whichever object we ended up with, so a
|
|
@@ -485,7 +486,7 @@ export async function loadProjectConfigs(
|
|
|
485
486
|
if (r.closure !== undefined) learnedClosures.push(r.closure)
|
|
486
487
|
}
|
|
487
488
|
}
|
|
488
|
-
const changed = restoreBuiltins(builtins)
|
|
489
|
+
const changed = [...restoreBuiltins(builtins), ...restoreEnv(env)]
|
|
489
490
|
if (first !== undefined) throw first.failed
|
|
490
491
|
if (changed.length > 0) throw builtinsChanged(changed)
|
|
491
492
|
return results.map((r) => (r as Loaded).config)
|
|
@@ -539,8 +540,11 @@ function builtinSnapshot(): BuiltinSnapshot {
|
|
|
539
540
|
/** Loads run together, so the config named is the one whose load saw the change. */
|
|
540
541
|
function builtinsChanged(changed: readonly string[], configPath?: string): UserError {
|
|
541
542
|
const who = configPath === undefined ? 'a project config' : configPath
|
|
543
|
+
const env = changed.some((c) => c.startsWith('process.env.'))
|
|
544
|
+
? '; a task gets an env var through `exec.env.define` or `passThrough`'
|
|
545
|
+
: ''
|
|
542
546
|
return new UserError(
|
|
543
|
-
`${who} changed ${changed.join(', ')} while it was evaluated — a config must not change the built-ins vx runs on: other configs are read through them and cache keys are made with them`,
|
|
547
|
+
`${who} changed ${changed.join(', ')} while it was evaluated — a config must not change the built-ins vx runs on: other configs are read through them and cache keys are made with them${env}`,
|
|
544
548
|
)
|
|
545
549
|
}
|
|
546
550
|
|
|
@@ -567,6 +571,31 @@ function restoreBuiltins(before: BuiltinSnapshot): string[] {
|
|
|
567
571
|
return changed
|
|
568
572
|
}
|
|
569
573
|
|
|
574
|
+
/**
|
|
575
|
+
* A first load's `process.env.X = …` gave every other project's task that
|
|
576
|
+
* value through `passThrough` and reached vx's own `VX_*` reads, while a
|
|
577
|
+
* repeat load, in a worker, left both unchanged: one config, two
|
|
578
|
+
* environments, by load order (D-76). Assigned back, not defined:
|
|
579
|
+
* `process.env` refuses `defineProperty`.
|
|
580
|
+
*/
|
|
581
|
+
function restoreEnv(before: Readonly<Record<string, string | undefined>>): string[] {
|
|
582
|
+
const changed: string[] = []
|
|
583
|
+
const live = process.env
|
|
584
|
+
for (const key of Object.keys(live)) {
|
|
585
|
+
if (live[key] === before[key]) continue
|
|
586
|
+
changed.push(`process.env.${key}`)
|
|
587
|
+
if (key in before) live[key] = before[key]
|
|
588
|
+
else delete live[key]
|
|
589
|
+
}
|
|
590
|
+
for (const key of Object.keys(before)) {
|
|
591
|
+
if (!(key in live)) {
|
|
592
|
+
changed.push(`process.env.${key}`)
|
|
593
|
+
live[key] = before[key]
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return changed
|
|
597
|
+
}
|
|
598
|
+
|
|
570
599
|
function sameDescriptor(a: PropertyDescriptor, b: PropertyDescriptor): boolean {
|
|
571
600
|
return (
|
|
572
601
|
a.value === b.value &&
|