@vzn/vx 0.0.179 → 0.0.181

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.179",
3
+ "version": "0.0.181",
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.179",
21
- "@vzn/vx-linux-arm64": "0.0.179",
22
- "@vzn/vx-darwin-x64": "0.0.179",
23
- "@vzn/vx-darwin-arm64": "0.0.179"
20
+ "@vzn/vx-linux-x64": "0.0.181",
21
+ "@vzn/vx-linux-arm64": "0.0.181",
22
+ "@vzn/vx-darwin-x64": "0.0.181",
23
+ "@vzn/vx-darwin-arm64": "0.0.181"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -39,6 +39,9 @@ import path from 'node:path'
39
39
  import { MAX_TIMEOUT_MS, UserError } from '../util/index.js'
40
40
  import { nonJsonMessage, nonJsonPaths, type NonJsonValue } from './json-data.js'
41
41
 
42
+ /** Why a config's `process.exit` throws, on both load paths (D-65). */
43
+ export const CONFIG_EXIT = 'a config exports its object; it cannot end the run'
44
+
42
45
  const WORKER_SRC = `
43
46
  const nonJsonPaths = ${nonJsonPaths.toString()}
44
47
  // The parent's stdout is a verb's JSON or vx mcp's JSON-RPC stream, and a
@@ -60,6 +63,11 @@ const quiet = (async () => {
60
63
  const ownBunWrite = B.write
61
64
  B.write = (dest, ...rest) => ownBunWrite(dest === B.stdout ? B.stderr : dest, ...rest)
62
65
  B.stdout.writer = (...args) => B.stderr.writer(...args)
66
+ // An exit here ended the worker unheard, and the load waited out its
67
+ // whole deadline to say the worker did not answer (D-65).
68
+ P.exit = (code) => {
69
+ throw new Error('process.exit(' + (code ?? '') + ') in a config: ' + ${JSON.stringify(CONFIG_EXIT)})
70
+ }
63
71
  })()
64
72
  self.onmessage = async (e) => {
65
73
  await quiet
@@ -124,12 +132,12 @@ interface Pending {
124
132
  }
125
133
 
126
134
  /**
127
- * How long a single config evaluation may take before we treat the worker as
128
- * wedged. Real evaluations are ~10 ms; this exists only so a killed or hung
129
- * worker cannot stall a long-lived process indefinitely. Read per call so a
130
- * test can drive the deadline instead of waiting it out.
135
+ * How long a single config evaluation may take, in the worker or in process
136
+ * (D-66). Real evaluations are ~10 ms; this exists only so a hung config or a
137
+ * killed worker cannot stall a run or a long-lived process indefinitely. Read
138
+ * per call so a test can drive the deadline instead of waiting it out.
131
139
  */
132
- function workerTimeoutMs(): number {
140
+ export function evalBudgetMs(): number {
133
141
  const raw = process.env['VX_CONFIG_WORKER_TIMEOUT_MS']
134
142
  // Same kind of value as the teardown deadline, so the same treatment: a
135
143
  // BOUND on a worker that may be wedged, with no "no limit" reading. Falls
@@ -258,7 +266,7 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
258
266
  // otherwise take milliseconds. Nothing else bounds it: there is no
259
267
  // run-level timeout. The budget is enormous next to the ~10 ms a real
260
268
  // evaluation costs, so it can only fire on a genuine wedge.
261
- const budget = workerTimeoutMs()
269
+ const budget = evalBudgetMs()
262
270
  const reply = await new Promise<WorkerReply>((resolve, reject) => {
263
271
  pending.set(id, { resolve, reject })
264
272
  timer = setTimeout(() => {
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import type { ProjectConfig, WorkspaceConfig } from '../config.js'
4
4
  import { UserError, xxh3hex } from '../util/index.js'
5
5
  import { validateProjectConfig, validateWorkspace } from './config-schema.js'
6
- import { beginEvalRound, evaluateConfigFresh } from './config-eval.js'
6
+ import { beginEvalRound, CONFIG_EXIT, evalBudgetMs, evaluateConfigFresh } from './config-eval.js'
7
7
  import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
8
8
  import {
9
9
  configEvalKey,
@@ -136,8 +136,27 @@ async function loadDefaultExport(
136
136
  holdSource(hash, source)
137
137
  }
138
138
  let ns: { default?: unknown }
139
+ const unguard = guardExit()
140
+ let deadline: ReturnType<typeof setTimeout> | undefined
139
141
  try {
140
- ns = (await import(specifier)) as { default?: unknown }
142
+ // The worker's budget, here too: a top-level await that never settled
143
+ // while a timer kept the loop alive hung `vx run` for good, silently
144
+ // (D-66).
145
+ const budget = evalBudgetMs()
146
+ ns = (await Promise.race([
147
+ import(specifier),
148
+ new Promise<never>((_, reject) => {
149
+ deadline = setTimeout(
150
+ () =>
151
+ reject(
152
+ new UserError(
153
+ `${kind} config ${configPath} did not finish evaluating within ${budget}ms (VX_CONFIG_WORKER_TIMEOUT_MS)`,
154
+ ),
155
+ ),
156
+ budget,
157
+ )
158
+ }),
159
+ ])) as { default?: unknown }
141
160
  } catch (err) {
142
161
  // A served module's frames name its specifier; the user wrote the path.
143
162
  if (err instanceof Error && err.stack !== undefined) {
@@ -145,6 +164,8 @@ async function loadDefaultExport(
145
164
  }
146
165
  throw configLoadError(err, configPath, kind) ?? err
147
166
  } finally {
167
+ clearTimeout(deadline)
168
+ unguard()
148
169
  if (source !== null) releaseSource(hash)
149
170
  }
150
171
  const mod = ns?.default
@@ -152,6 +173,28 @@ async function loadDefaultExport(
152
173
  return mod
153
174
  }
154
175
 
176
+ /**
177
+ * A config's `process.exit` ended vx mid-load: `exit(0)` was a green run
178
+ * that ran nothing and printed nothing (D-65). While any config evaluates
179
+ * in process it throws instead, at the config's line. vx's own exit is
180
+ * signal forwarding's, which a run installs after its load and removes
181
+ * when it ends.
182
+ */
183
+ let evaluating = 0
184
+ let ownExit: typeof process.exit = process.exit
185
+ function guardExit(): () => void {
186
+ if (evaluating++ === 0) {
187
+ ownExit = process.exit
188
+ process.exit = configExit
189
+ }
190
+ return () => {
191
+ if (--evaluating === 0) process.exit = ownExit
192
+ }
193
+ }
194
+ const configExit = ((code?: number | string | null) => {
195
+ throw new Error(`process.exit(${code ?? ''}) in a config: ${CONFIG_EXIT}`)
196
+ }) as typeof process.exit
197
+
155
198
  /**
156
199
  * A bare import nothing above the config provides is refused BEFORE the
157
200
  * evaluation: left to Bun, a workspace with no `node_modules` would have