@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import type { ProjectConfig, WorkspaceConfig } from '../config.js'
|
|
4
|
-
import { UserError, xxh3hex } from '../util/index.js'
|
|
4
|
+
import { isUserError, UserError, xxh3hex } from '../util/index.js'
|
|
5
5
|
import { isSkippedPlugin, validateProjectConfig, validateWorkspace } from './config-schema.js'
|
|
6
6
|
import {
|
|
7
7
|
beginEvalRound,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './config-eval.js'
|
|
15
15
|
import { configImports, hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
16
16
|
import { readOnce, unreadable } from './load-reads.js'
|
|
17
|
+
import { buildCompiled, loadCompiled, loadedModules } from './config-bytecode.js'
|
|
17
18
|
|
|
18
19
|
// The validator lives in config-schema.ts; re-exported so a reader that
|
|
19
20
|
// reaches the loader for it (the tests do) keeps working.
|
|
@@ -23,7 +24,7 @@ export { validateProjectConfig }
|
|
|
23
24
|
import { WORKSPACE_CONFIG_FILENAMES, workspaceFile, workspaceRootOf } from '../handoff.js'
|
|
24
25
|
export { WORKSPACE_CONFIG_FILENAMES }
|
|
25
26
|
|
|
26
|
-
/** What a function default export is told
|
|
27
|
+
/** What a function default export is told. */
|
|
27
28
|
const EXPORTED_A_FUNCTION =
|
|
28
29
|
'it exports a function, and vx reads the object itself — export what the function returns'
|
|
29
30
|
|
|
@@ -91,19 +92,72 @@ function serveHeldSources(): void {
|
|
|
91
92
|
contents: heldSources.get(HELD_QUERY.exec(args.path)![1]!)!.source,
|
|
92
93
|
loader: /\.[cm]?ts\?/.test(args.path) ? 'ts' : 'js',
|
|
93
94
|
}))
|
|
95
|
+
build.onLoad({ filter: PLAIN_CONFIG }, (args) => {
|
|
96
|
+
const loader = /\.[cm]?ts$/.test(args.path) ? 'ts' : 'js'
|
|
97
|
+
const held = heldPaths.get(args.path) ?? heldByRealPath(args.path)
|
|
98
|
+
if (held !== undefined) {
|
|
99
|
+
held.served = true
|
|
100
|
+
return { contents: heldSources.get(held.hash)!.source, loader }
|
|
101
|
+
}
|
|
102
|
+
// A config no load holds now, imported by another module. `onLoad`
|
|
103
|
+
// cannot decline it, so it is served as Bun reads it, and what
|
|
104
|
+
// `onLoad` cannot carry (CommonJS, not UTF-8) is Bun's own load.
|
|
105
|
+
const bytes = readFileSync(args.path)
|
|
106
|
+
const source = servableSource(bytes, loader)
|
|
107
|
+
if (source !== null) return { contents: source, loader }
|
|
108
|
+
const native = JSON.stringify(`${args.path}?vx-bust=${xxh3hex(bytes)}`)
|
|
109
|
+
return {
|
|
110
|
+
contents: `import * as m from ${native}\nexport * from ${native}\nexport default m.default\n`,
|
|
111
|
+
loader: 'js',
|
|
112
|
+
}
|
|
113
|
+
})
|
|
94
114
|
},
|
|
95
115
|
})
|
|
96
116
|
}
|
|
97
117
|
|
|
98
|
-
function holdSource(hash: string, source: string): void {
|
|
118
|
+
function holdSource(hash: string, source: string, plain: Held | null, configPath: string): void {
|
|
99
119
|
const held = heldSources.get(hash)
|
|
100
120
|
if (held === undefined) heldSources.set(hash, { source, uses: 1 })
|
|
101
121
|
else held.uses++
|
|
122
|
+
if (plain !== null) heldPaths.set(configPath, plain)
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
function releaseSource(hash: string): void {
|
|
125
|
+
function releaseSource(hash: string, plain: Held | null, configPath: string): void {
|
|
105
126
|
const held = heldSources.get(hash)!
|
|
106
127
|
if (--held.uses === 0) heldSources.delete(hash)
|
|
128
|
+
if (plain !== null) heldPaths.delete(configPath)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Served under `?vx-held=`, a config was a second module beside the plain
|
|
132
|
+
// path an import cycle reaches back to, and its `import.meta.url` carried
|
|
133
|
+
// the query, unlike the worker's. So it is imported under its own path,
|
|
134
|
+
// and `served` says whether this load's bytes were what Bun evaluated:
|
|
135
|
+
// unset, the registry held the path already (a config another imported,
|
|
136
|
+
// maybe older bytes), and only a query evaluates afresh.
|
|
137
|
+
interface Held {
|
|
138
|
+
hash: string
|
|
139
|
+
served: boolean
|
|
140
|
+
/** Learned only when `onLoad` misses: Bun hands it the real path. */
|
|
141
|
+
real?: string | null
|
|
142
|
+
}
|
|
143
|
+
const heldPaths = new Map<string, Held>()
|
|
144
|
+
|
|
145
|
+
const PLAIN_CONFIG = /[\\/]vx\.config\.[cm]?[jt]s$/
|
|
146
|
+
|
|
147
|
+
// A realpath for every config was 7–10 ms on 1,000 (2026-10-28); only a
|
|
148
|
+
// path reached through a link misses, so only a miss pays it.
|
|
149
|
+
function heldByRealPath(real: string): Held | undefined {
|
|
150
|
+
for (const [configPath, held] of heldPaths) {
|
|
151
|
+
if (held.real === undefined) {
|
|
152
|
+
try {
|
|
153
|
+
held.real = realpathSync(configPath)
|
|
154
|
+
} catch {
|
|
155
|
+
held.real = null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (held.real === real) return held
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
107
161
|
}
|
|
108
162
|
|
|
109
163
|
const utf8 = new TextDecoder('utf-8', { fatal: true })
|
|
@@ -142,8 +196,13 @@ async function loadDefaultExport(
|
|
|
142
196
|
configPath: string,
|
|
143
197
|
kind: string,
|
|
144
198
|
bytes: Uint8Array,
|
|
199
|
+
/** The workspace root, where the load may run the config's bytecode bundle (config-bytecode.ts). */
|
|
200
|
+
compiledRoot?: string,
|
|
201
|
+
/** File texts read for the refusal, kept across one load's configs. */
|
|
202
|
+
reads = new Map<string, Promise<string>>(),
|
|
145
203
|
): Promise<unknown> {
|
|
146
|
-
|
|
204
|
+
const text = new TextDecoder().decode(bytes)
|
|
205
|
+
refuseUnprovided(text, configPath, configPath, kind)
|
|
147
206
|
// No random bust for `vx lock`: a project config reaches this import
|
|
148
207
|
// only on its FIRST load in the process, so nothing is cached under the
|
|
149
208
|
// URL yet, and a repeat load (the one that could replay an evaluation
|
|
@@ -160,21 +219,43 @@ async function loadDefaultExport(
|
|
|
160
219
|
)
|
|
161
220
|
}
|
|
162
221
|
const hash = xxh3hex(bytes)
|
|
222
|
+
const plain: Held | null =
|
|
223
|
+
source !== null && !literal && PLAIN_CONFIG.test(configPath) ? { hash, served: false } : null
|
|
163
224
|
const specifier = `${configPath}?vx-${literal ? 'literal' : source !== null ? 'held' : 'bust'}=${hash}`
|
|
164
225
|
if (source !== null) {
|
|
165
226
|
serveHeldSources()
|
|
166
|
-
holdSource(hash, source)
|
|
227
|
+
holdSource(hash, source, plain, configPath)
|
|
167
228
|
}
|
|
168
229
|
let ns: { default?: unknown }
|
|
230
|
+
let compiled: { default?: unknown } | null | undefined
|
|
231
|
+
let loaded: Set<string> | undefined
|
|
169
232
|
const unguard = guardExit()
|
|
170
233
|
let deadline: ReturnType<typeof setTimeout> | undefined
|
|
171
234
|
try {
|
|
235
|
+
// A bundle that throws is evaluated from source below, so the error
|
|
236
|
+
// the user sees is the source's.
|
|
237
|
+
compiled =
|
|
238
|
+
literal || compiledRoot === undefined ? null : tryCompiled(compiledRoot, configPath, bytes)
|
|
239
|
+
// Awaited here: a default that is a rejected Promise is the config's
|
|
240
|
+
// throw too, and passed out unawaited it reached bin.ts as a vx
|
|
241
|
+
// defect (2026-10-28).
|
|
242
|
+
if (compiled) return await defaultOf(compiled, kind, configPath)
|
|
243
|
+
if (compiled === undefined) loaded = loadedModules(compiledRoot!)
|
|
172
244
|
// The worker's budget, here too: a top-level await that never settled
|
|
173
245
|
// while a timer kept the loop alive hung `vx run` for good, silently
|
|
174
246
|
// (D-66).
|
|
247
|
+
await refuseUnprovidedClosure(text, configPath, kind, reads)
|
|
175
248
|
const budget = evalBudgetMs()
|
|
176
249
|
ns = (await Promise.race([
|
|
177
|
-
|
|
250
|
+
plain === null
|
|
251
|
+
? import(specifier)
|
|
252
|
+
: import(configPath).then(
|
|
253
|
+
(ns: unknown) => (plain.served ? ns : import(specifier)),
|
|
254
|
+
(err: unknown) => {
|
|
255
|
+
if (plain.served) throw err
|
|
256
|
+
return import(specifier)
|
|
257
|
+
},
|
|
258
|
+
),
|
|
178
259
|
new Promise<never>((_, reject) => {
|
|
179
260
|
deadline = setTimeout(
|
|
180
261
|
() =>
|
|
@@ -189,20 +270,48 @@ async function loadDefaultExport(
|
|
|
189
270
|
])) as { default?: unknown }
|
|
190
271
|
} catch (err) {
|
|
191
272
|
// A served module's frames name its specifier; the user wrote the path.
|
|
192
|
-
|
|
193
|
-
|
|
273
|
+
const seen = new Set<unknown>()
|
|
274
|
+
for (let e: unknown = err; e instanceof Error && !seen.has(e); e = e.cause) {
|
|
275
|
+
seen.add(e)
|
|
276
|
+
if (e.stack !== undefined)
|
|
277
|
+
e.stack = e.stack.replaceAll(specifier, configPath).replace(BUST_QUERY, '')
|
|
194
278
|
}
|
|
195
279
|
throw loadFailure(err, configPath, kind)
|
|
196
280
|
} finally {
|
|
197
281
|
clearTimeout(deadline)
|
|
198
282
|
unguard()
|
|
199
|
-
if (source !== null) releaseSource(hash)
|
|
283
|
+
if (source !== null) releaseSource(hash, plain, configPath)
|
|
284
|
+
}
|
|
285
|
+
const mod = defaultOf(ns, kind, configPath)
|
|
286
|
+
if (compiledRoot !== undefined && loaded !== undefined) {
|
|
287
|
+
await buildCompiled(compiledRoot, configPath, bytes, loaded, ns)
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
return await mod
|
|
291
|
+
} catch (err) {
|
|
292
|
+
throw loadFailure(err, configPath, kind)
|
|
200
293
|
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function defaultOf(ns: { default?: unknown }, kind: string, configPath: string): unknown {
|
|
201
297
|
const mod = ns?.default
|
|
202
298
|
assertDefaultObject(mod, kind, configPath)
|
|
203
299
|
return mod
|
|
204
300
|
}
|
|
205
301
|
|
|
302
|
+
/** The bundle's namespace; `undefined` to build one after this load, `null` when none can serve. */
|
|
303
|
+
function tryCompiled(
|
|
304
|
+
root: string,
|
|
305
|
+
configPath: string,
|
|
306
|
+
bytes: Uint8Array,
|
|
307
|
+
): { default?: unknown } | null | undefined {
|
|
308
|
+
try {
|
|
309
|
+
return loadCompiled(root, configPath, bytes)
|
|
310
|
+
} catch {
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
206
315
|
/**
|
|
207
316
|
* A config's `process.exit` ended vx mid-load: `exit(0)` was a green run
|
|
208
317
|
* that ran nothing and printed nothing (D-65). While any config evaluates
|
|
@@ -238,29 +347,60 @@ function pnpAbove(dir: string): boolean {
|
|
|
238
347
|
* evaluation: left to Bun, a workspace with no `node_modules` would have
|
|
239
348
|
* the package auto-installed from the registry first (config-imports.ts),
|
|
240
349
|
* and a config must never download. The message keeps the shape Bun's own
|
|
241
|
-
* refusal has, with the remedy.
|
|
350
|
+
* refusal has, with the remedy. `source` is `file`'s: the config's own,
|
|
351
|
+
* or one it reaches.
|
|
242
352
|
*/
|
|
243
|
-
function
|
|
244
|
-
const loader = /\.[cm]?
|
|
245
|
-
const missing = unprovidedBareImports(
|
|
246
|
-
new TextDecoder().decode(bytes),
|
|
247
|
-
path.dirname(configPath),
|
|
248
|
-
loader,
|
|
249
|
-
)
|
|
353
|
+
function refuseUnprovided(source: string, file: string, configPath: string, kind: string): void {
|
|
354
|
+
const loader = /\.[cm]?tsx?$/.test(file) ? 'ts' : 'js'
|
|
355
|
+
const missing = unprovidedBareImports(source, path.dirname(file), loader)
|
|
250
356
|
if (missing.length === 0) return
|
|
357
|
+
const own = file === configPath
|
|
358
|
+
const what = own
|
|
359
|
+
? `cannot find '${missing[0]}'`
|
|
360
|
+
: `${file} imports '${missing[0]}', which no node_modules above it provides`
|
|
251
361
|
// Under Yarn Plug'n'Play the dependencies ARE installed, into a
|
|
252
362
|
// `.pnp.cjs` Bun does not read: "install them first" sent the user to a
|
|
253
363
|
// `yarn install` that changed nothing (D-109).
|
|
254
|
-
if (pnpAbove(path.dirname(
|
|
364
|
+
if (pnpAbove(path.dirname(file))) {
|
|
255
365
|
throw new UserError(
|
|
256
|
-
`${kind} config ${configPath}:
|
|
366
|
+
`${kind} config ${configPath}: ${what} — Yarn Plug'n'Play installed the workspace's dependencies into .pnp.cjs, which Bun does not read; set \`nodeLinker: node-modules\` in .yarnrc.yml and run \`yarn install\``,
|
|
257
367
|
)
|
|
258
368
|
}
|
|
259
369
|
throw new UserError(
|
|
260
|
-
`${kind} config ${configPath}:
|
|
370
|
+
`${kind} config ${configPath}: ${what}${own ? ' — no node_modules above the config provides it' : ''}; install the workspace's dependencies first`,
|
|
261
371
|
)
|
|
262
372
|
}
|
|
263
373
|
|
|
374
|
+
const RELATIVE_IMPORT = /["']\.\.?\//
|
|
375
|
+
const SCRIPT = /\.[cm]?[jt]sx?$/
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The same refusal for each script the config reaches by relative import:
|
|
379
|
+
* a preset's bare import was installed and run where the config's own was
|
|
380
|
+
* refused (L-22; `--no-install` holds only the shebang, not `bun src/bin.ts`
|
|
381
|
+
* or an embedder). Only a source evaluation pays: a fresh bundle resolved
|
|
382
|
+
* every import it holds. `reads` keeps each file's text for the load's other
|
|
383
|
+
* configs: a preset a thousand configs share is read once.
|
|
384
|
+
*/
|
|
385
|
+
async function refuseUnprovidedClosure(
|
|
386
|
+
source: string,
|
|
387
|
+
configPath: string,
|
|
388
|
+
kind: string,
|
|
389
|
+
reads: Map<string, Promise<string>>,
|
|
390
|
+
): Promise<void> {
|
|
391
|
+
if (!RELATIVE_IMPORT.test(source)) return
|
|
392
|
+
const read = (file: string): Promise<string> => {
|
|
393
|
+
let text = reads.get(file)
|
|
394
|
+
if (text === undefined) reads.set(file, (text = Bun.file(file).text()))
|
|
395
|
+
return text
|
|
396
|
+
}
|
|
397
|
+
for (const file of await configImports(configPath, source, read)) {
|
|
398
|
+
if (!SCRIPT.test(file)) continue
|
|
399
|
+
const text = await read(file).catch(() => null)
|
|
400
|
+
if (text !== null) refuseUnprovided(text, file, configPath, kind)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
264
404
|
/**
|
|
265
405
|
* Where a transpile error was found. Bun's `BuildMessage` carries it as
|
|
266
406
|
* `position`; the config worker forwards the same three fields (see
|
|
@@ -342,16 +482,87 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
|
|
|
342
482
|
return null
|
|
343
483
|
}
|
|
344
484
|
|
|
345
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* What a failed load throws: always a user error. A config's own throw, or
|
|
487
|
+
* a plugin factory's it called, is the config's to fix, not a vx defect:
|
|
488
|
+
* passed through, it printed a stack under VX_E_INTERNAL, whose fix is to
|
|
489
|
+
* report a vx bug (2026-10-28). Its line stands in for the stack.
|
|
490
|
+
*/
|
|
346
491
|
function loadFailure(err: unknown, configPath: string, kind: string): unknown {
|
|
347
492
|
const user = configLoadError(err, configPath, kind)
|
|
348
493
|
if (user !== null) return user
|
|
494
|
+
if (isUserError(err)) return err
|
|
349
495
|
if (err !== null && typeof err === 'object' && typeof (err as Error).message === 'string') {
|
|
350
|
-
|
|
496
|
+
const user = new UserError(
|
|
497
|
+
`${kind} config ${configPath} threw: ${thrownLine(err as Error, configPath)}${causeLines(err as Error, configPath)}`,
|
|
498
|
+
'VX_E_CONFIG',
|
|
499
|
+
)
|
|
500
|
+
// The thrown error, stack and all, for an embedder of the loader.
|
|
501
|
+
user.cause = err
|
|
502
|
+
return user
|
|
351
503
|
}
|
|
352
504
|
return new UserError(thrownValueMessage(kind, configPath, Bun.inspect(err, { compact: true })))
|
|
353
505
|
}
|
|
354
506
|
|
|
507
|
+
const VX_SRC = `${path.resolve(import.meta.dir, '..')}${path.sep}`
|
|
508
|
+
|
|
509
|
+
/** A thrown error's message and where it was thrown. */
|
|
510
|
+
function thrownLine(err: Error, configPath: string): string {
|
|
511
|
+
const stack = String(err.stack ?? '')
|
|
512
|
+
// A cause rebuilt from the config worker carries its stack alone.
|
|
513
|
+
const message =
|
|
514
|
+
err.message !== ''
|
|
515
|
+
? err.message
|
|
516
|
+
: (/^[\w$.]+: (.*)$/.exec(stack.split('\n')[0] ?? '')?.[1] ?? stack.split('\n')[0] ?? '')
|
|
517
|
+
const at = throwSite(stack, configPath)
|
|
518
|
+
return `${message}${at === undefined ? '' : ` (at ${at})`}`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Each `cause` under a wrapped throw (`new Error(msg, { cause })`), which
|
|
523
|
+
* holds the reason the wrapper names only in its own words. Bounded, and a
|
|
524
|
+
* cause seen before ends the chain.
|
|
525
|
+
*/
|
|
526
|
+
function causeLines(err: Error, configPath: string): string {
|
|
527
|
+
let out = ''
|
|
528
|
+
const seen = new Set<unknown>([err])
|
|
529
|
+
for (
|
|
530
|
+
let c = err.cause, depth = 0;
|
|
531
|
+
c !== undefined && c !== null && !seen.has(c) && depth < 8;
|
|
532
|
+
c = c instanceof Error ? c.cause : undefined, depth++
|
|
533
|
+
) {
|
|
534
|
+
seen.add(c)
|
|
535
|
+
out += `; caused by: ${c instanceof Error ? thrownLine(c, configPath) : Bun.inspect(c, { compact: true })}`
|
|
536
|
+
}
|
|
537
|
+
return out
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Where a throw came from: its first frame that names a file, and the
|
|
542
|
+
* config's own line that led there when that is another.
|
|
543
|
+
*/
|
|
544
|
+
function throwSite(stack: string, configPath: string): string | undefined {
|
|
545
|
+
// vx's own frames (the process.exit guard) are not where the config went wrong.
|
|
546
|
+
const sites = [...stack.matchAll(/^\s+at (?:.*\()?(\/[^()\n]+:\d+:\d+)\)?$/gm)]
|
|
547
|
+
.map((m) => m[1]!.replace(BUST_QUERY, ''))
|
|
548
|
+
.filter((s) => !s.startsWith(VX_SRC))
|
|
549
|
+
const own = sites.find((s) => s.startsWith(`${configPath}:`))
|
|
550
|
+
if (sites[0] === undefined || own === undefined || own === sites[0]) return sites[0] ?? own
|
|
551
|
+
return `${sites[0]}, from ${own}`
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* More than one config refused in one load: the first is the error, and
|
|
556
|
+
* `all` names each. A rename after an upgrade refuses every config at once,
|
|
557
|
+
* and naming one a run made a 200-config repo take 200 runs.
|
|
558
|
+
*/
|
|
559
|
+
export class ConfigsRefused extends UserError {
|
|
560
|
+
constructor(readonly all: readonly UserError[]) {
|
|
561
|
+
super(all[0]!.message, all[0]!.code)
|
|
562
|
+
if (all[0]!.parts !== undefined) this.parts = all[0]!.parts
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
355
566
|
/**
|
|
356
567
|
* Evaluate many configs at once: every file's bytes in parallel, then each
|
|
357
568
|
* one evaluated (in the order given, so a failure names the first broken
|
|
@@ -377,6 +588,7 @@ export async function loadProjectConfigs(configPaths: readonly string[]): Promis
|
|
|
377
588
|
// More than one evaluation in flight: a change seen after one load may
|
|
378
589
|
// be another's.
|
|
379
590
|
let overlapping = false
|
|
591
|
+
const reads = new Map<string, Promise<string>>()
|
|
380
592
|
const loadOne = async (entry: (typeof prepared)[number]): Promise<ProjectConfig> => {
|
|
381
593
|
const { configPath, bytes } = entry
|
|
382
594
|
// A REPEAT load in this process re-evaluates in a worker, because the
|
|
@@ -384,12 +596,16 @@ export async function loadProjectConfigs(configPaths: readonly string[]): Promis
|
|
|
384
596
|
// config-eval.ts. A FIRST load keeps the in-process import.
|
|
385
597
|
const repeat = loadedConfigs.has(configPath)
|
|
386
598
|
loadedConfigs.add(configPath)
|
|
387
|
-
if (repeat)
|
|
599
|
+
if (repeat) {
|
|
600
|
+
const text = new TextDecoder().decode(bytes)
|
|
601
|
+
refuseUnprovided(text, configPath, configPath, 'Project')
|
|
602
|
+
await refuseUnprovidedClosure(text, configPath, 'Project', reads)
|
|
603
|
+
}
|
|
388
604
|
const mod = repeat
|
|
389
605
|
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
390
606
|
throw loadFailure(err, configPath, 'Project')
|
|
391
607
|
})
|
|
392
|
-
: await loadDefaultExport(configPath, 'Project', bytes)
|
|
608
|
+
: await loadDefaultExport(configPath, 'Project', bytes, undefined, reads)
|
|
393
609
|
// Before anything reads through them: a replaced `Array.prototype.includes`
|
|
394
610
|
// turned the JSON-data walk's own check into "a cyclic reference".
|
|
395
611
|
const changed = repeat
|
|
@@ -481,7 +697,14 @@ export async function loadProjectConfigs(configPaths: readonly string[]): Promis
|
|
|
481
697
|
}
|
|
482
698
|
throw builtinsChanged(changedInRound)
|
|
483
699
|
}
|
|
484
|
-
if (first !== undefined)
|
|
700
|
+
if (first !== undefined) {
|
|
701
|
+
const refused = results.flatMap((r) =>
|
|
702
|
+
'failed' in r && isUserError(r.failed) ? [r.failed] : [],
|
|
703
|
+
)
|
|
704
|
+
throw refused.length > 1 && refused[0] === first.failed
|
|
705
|
+
? new ConfigsRefused(refused)
|
|
706
|
+
: first.failed
|
|
707
|
+
}
|
|
485
708
|
// One load in the round, in the worker (a repeat load), is the one
|
|
486
709
|
// that moved it: the worker no longer reads the umask to say so.
|
|
487
710
|
if (changed.length > 0)
|
|
@@ -509,12 +732,52 @@ export async function loadProjectConfigs(configPaths: readonly string[]): Promis
|
|
|
509
732
|
let redirectHolders = 0
|
|
510
733
|
let undoRedirect = (): void => {}
|
|
511
734
|
/**
|
|
512
|
-
* Bun's own
|
|
513
|
-
* constructed one (a plugin's verb may install its own), which has no
|
|
514
|
-
*
|
|
515
|
-
* for the browser, where that specifier is a stub.
|
|
735
|
+
* Bun's own console, taken at load: the console a round finds may be a
|
|
736
|
+
* constructed one (a plugin's verb may install its own), which has no
|
|
737
|
+
* `Console`.
|
|
516
738
|
*/
|
|
517
|
-
const
|
|
739
|
+
const bunConsole = globalThis.console as typeof console & { Console?: typeof console.Console }
|
|
740
|
+
let stderrConsole: typeof console | undefined
|
|
741
|
+
const BUILT_ON_READ: ReadonlySet<PropertyKey> = new Set(['Console', '_stdout', '_stderr'])
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* A console that writes to stderr, built on the first call: reading
|
|
745
|
+
* `Console` loads `node:console`, ~4 ms of a warm run, and the workspace
|
|
746
|
+
* config each run loads in a redirect seldom prints.
|
|
747
|
+
*/
|
|
748
|
+
function redirectedConsole(): typeof console {
|
|
749
|
+
const real = (): Record<PropertyKey, (...a: unknown[]) => unknown> =>
|
|
750
|
+
(stderrConsole ??= Object.assign(new bunConsole.Console!(process.stderr, process.stderr), {
|
|
751
|
+
// Bun's console adds `write`, which a constructed Console lacks.
|
|
752
|
+
write: (...data: string[]) => {
|
|
753
|
+
const text = data.join('')
|
|
754
|
+
process.stderr.write(text)
|
|
755
|
+
return text.length
|
|
756
|
+
},
|
|
757
|
+
})) as unknown as Record<PropertyKey, (...a: unknown[]) => unknown>
|
|
758
|
+
const facade = {}
|
|
759
|
+
for (const key of [...Reflect.ownKeys(bunConsole), 'write']) {
|
|
760
|
+
// Read through the real one when read: even their descriptors build a
|
|
761
|
+
// module or a stream (~17 ms together, Bun 1.4.2).
|
|
762
|
+
if (BUILT_ON_READ.has(key)) {
|
|
763
|
+
Object.defineProperty(facade, key, { get: () => real()[key], configurable: true })
|
|
764
|
+
continue
|
|
765
|
+
}
|
|
766
|
+
const own = Object.getOwnPropertyDescriptor(bunConsole, key)
|
|
767
|
+
Object.defineProperty(
|
|
768
|
+
facade,
|
|
769
|
+
key,
|
|
770
|
+
own === undefined || typeof own.value === 'function'
|
|
771
|
+
? {
|
|
772
|
+
value: (...args: unknown[]) => real()[key]!(...args),
|
|
773
|
+
writable: true,
|
|
774
|
+
configurable: true,
|
|
775
|
+
}
|
|
776
|
+
: own,
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
return facade as typeof console
|
|
780
|
+
}
|
|
518
781
|
|
|
519
782
|
function stdoutToStderr(): () => void {
|
|
520
783
|
if (redirectHolders++ === 0) undoRedirect = installRedirect()
|
|
@@ -540,14 +803,7 @@ function installRedirect(): () => void {
|
|
|
540
803
|
;(Bun.stdout as { writer: typeof Bun.stdout.writer }).writer = ((
|
|
541
804
|
...args: Parameters<typeof Bun.stderr.writer>
|
|
542
805
|
) => Bun.stderr.writer(...args)) as typeof Bun.stdout.writer
|
|
543
|
-
|
|
544
|
-
globalThis.console = Object.assign(new BunConsole!(process.stderr, process.stderr), {
|
|
545
|
-
write: (...data: string[]) => {
|
|
546
|
-
const text = data.join('')
|
|
547
|
-
process.stderr.write(text)
|
|
548
|
-
return text.length
|
|
549
|
-
},
|
|
550
|
-
})
|
|
806
|
+
globalThis.console = redirectedConsole()
|
|
551
807
|
return () => {
|
|
552
808
|
if (ownWrite === undefined) delete (out as { write?: unknown }).write
|
|
553
809
|
else Object.defineProperty(out, 'write', ownWrite)
|
|
@@ -568,9 +824,7 @@ function installRedirect(): () => void {
|
|
|
568
824
|
*/
|
|
569
825
|
// `Bun` and `Bun.hash` are what vx itself runs on: `Bun.hash.xxHash3 = ()
|
|
570
826
|
// => 7n` gave every task the key 00000000, and a changed command replayed
|
|
571
|
-
// the old output (D-75).
|
|
572
|
-
// globalThis: the docs playground bundles this file with the identifier
|
|
573
|
-
// rewritten to its shim, and a browser has no global `Bun`.
|
|
827
|
+
// the old output (D-75).
|
|
574
828
|
const WATCHED_BUILTINS: ReadonlyArray<readonly [string, object]> = WATCHED_BUILTIN_NAMES.map(
|
|
575
829
|
(name) =>
|
|
576
830
|
[
|
|
@@ -622,25 +876,33 @@ interface BuiltinSnapshot {
|
|
|
622
876
|
*/
|
|
623
877
|
const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
|
|
624
878
|
'Archive',
|
|
879
|
+
'BuildMetafile',
|
|
625
880
|
'BunFile',
|
|
626
881
|
'CryptoHasher',
|
|
627
882
|
'Glob',
|
|
628
883
|
'JSONC',
|
|
629
884
|
'Subprocess',
|
|
885
|
+
'TOML',
|
|
630
886
|
'Transpiler',
|
|
887
|
+
'build',
|
|
631
888
|
'color',
|
|
889
|
+
'connect',
|
|
632
890
|
'deepEquals',
|
|
633
891
|
'env',
|
|
634
892
|
'file',
|
|
635
893
|
'gunzipSync',
|
|
636
894
|
'hash',
|
|
637
895
|
'inspect',
|
|
896
|
+
'listen',
|
|
638
897
|
'main',
|
|
639
898
|
'nanoseconds',
|
|
899
|
+
'pathToFileURL',
|
|
640
900
|
'plugin',
|
|
641
901
|
'randomUUIDv7',
|
|
642
902
|
'resolveSync',
|
|
903
|
+
'revision',
|
|
643
904
|
'secrets',
|
|
905
|
+
'semver',
|
|
644
906
|
'serve',
|
|
645
907
|
'sleep',
|
|
646
908
|
'sleepSync',
|
|
@@ -832,8 +1094,6 @@ function restoreEnv(before: Readonly<Record<string, string | undefined>>): strin
|
|
|
832
1094
|
* choice (D-120). Put back, and named as the change.
|
|
833
1095
|
*/
|
|
834
1096
|
function restoreCwd(before: string): string[] {
|
|
835
|
-
// Through globalThis: the playground bundles this module, and its
|
|
836
|
-
// browser shim carries no free process global.
|
|
837
1097
|
const proc = globalThis.process
|
|
838
1098
|
if (before === '' || proc.cwd() === before) return []
|
|
839
1099
|
proc.chdir(before)
|
|
@@ -846,7 +1106,6 @@ function restoreCwd(before: string): string[] {
|
|
|
846
1106
|
* a user other than root could not read back (D-125). Put back, and named.
|
|
847
1107
|
*/
|
|
848
1108
|
function currentUmask(): number {
|
|
849
|
-
// Through globalThis, as restoreCwd: the playground's shim has no umask.
|
|
850
1109
|
const proc = globalThis.process as { umask?: (mask?: number) => number }
|
|
851
1110
|
if (typeof proc.umask !== 'function') return -1
|
|
852
1111
|
// Bun reads it by setting 0 and putting it back (probed on 1.4.2: four
|
|
@@ -959,7 +1218,15 @@ export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig
|
|
|
959
1218
|
const umask = currentUmask()
|
|
960
1219
|
let mod: WorkspaceConfig
|
|
961
1220
|
try {
|
|
962
|
-
mod = (await loadDefaultExport(
|
|
1221
|
+
mod = (await loadDefaultExport(
|
|
1222
|
+
configPath,
|
|
1223
|
+
'Workspace',
|
|
1224
|
+
bytes,
|
|
1225
|
+
// Every load, not only the first: a repeat load (the run after the
|
|
1226
|
+
// CLI's selection pass) must get the same plugin instances, and a
|
|
1227
|
+
// required bundle is the cached module.
|
|
1228
|
+
path.resolve(root),
|
|
1229
|
+
)) as WorkspaceConfig
|
|
963
1230
|
} finally {
|
|
964
1231
|
const changed = [
|
|
965
1232
|
...restoreBuiltins(builtins),
|
package/src/workspace/repo-id.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Which repository a workspace is, so its checkouts share one store under
|
|
2
2
|
// `~/.vx/<id>` and other repositories keep theirs: Nx 23.2's
|
|
3
3
|
// `~/.nx/<id>` (`utils/workspace-id.js`, `utils/git-utils.js`), ported
|
|
4
|
-
// rule for rule (
|
|
4
|
+
// rule for rule (2026-10-06: "do exactly like nx"), minus the Nx
|
|
5
5
|
// Cloud id. Read from the `.git` files; git is asked only when they cannot
|
|
6
6
|
// settle it (no parseable remote, an `include`, an `insteadOf`).
|
|
7
7
|
|