@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.196",
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.196",
21
- "@vzn/vx-linux-arm64": "0.0.196",
22
- "@vzn/vx-darwin-x64": "0.0.196",
23
- "@vzn/vx-darwin-arm64": "0.0.196"
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"
@@ -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 { accessSync, constants, existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs'
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
- this.db = new Database(absent ? ':memory:' : dbFile, { create: true })
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)) throw 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 &&