@vzn/vx 0.0.646 → 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.
Files changed (184) hide show
  1. package/README.md +29 -22
  2. package/launcher.cjs +9 -2
  3. package/launcher.sh +2 -2
  4. package/package.json +5 -5
  5. package/schemas/cache.json +5 -5
  6. package/schemas/error.json +1 -1
  7. package/schemas/info.json +16 -2
  8. package/schemas/last.json +112 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +18 -2
  12. package/src/bin.ts +53 -12
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +268 -53
  15. package/src/cache/git-inputs.ts +219 -87
  16. package/src/cache/index.ts +3 -2
  17. package/src/cache/inputs.ts +460 -78
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +49 -9
  20. package/src/cache/layered-cache.ts +123 -58
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +19 -3
  23. package/src/cache/plugin-cache.ts +49 -23
  24. package/src/cache/remote-hits.ts +72 -0
  25. package/src/cache/remote-lane.ts +90 -14
  26. package/src/cache/run-history.ts +70 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +25 -6
  29. package/src/cache/tar-stream.ts +5 -0
  30. package/src/cache/umask.ts +1 -2
  31. package/src/cli/cache.ts +58 -27
  32. package/src/cli/completions.ts +12 -5
  33. package/src/cli/docs.ts +42 -16
  34. package/src/cli/help.ts +58 -19
  35. package/src/cli/index.ts +79 -33
  36. package/src/cli/info.ts +12 -6
  37. package/src/cli/init.ts +186 -31
  38. package/src/cli/last.ts +439 -45
  39. package/src/cli/lock.ts +197 -52
  40. package/src/cli/plan-format.ts +91 -49
  41. package/src/cli/plugin-commands.ts +70 -14
  42. package/src/cli/plugin-templates.ts +13 -13
  43. package/src/cli/registry.ts +209 -0
  44. package/src/cli/run.ts +109 -34
  45. package/src/cli/select.ts +143 -46
  46. package/src/cli/show.ts +59 -16
  47. package/src/cli/task-verb.ts +16 -6
  48. package/src/cli/upgrade.ts +151 -49
  49. package/src/cli/watch-cycle.ts +10 -5
  50. package/src/cli/watch-filter.ts +71 -3
  51. package/src/cli/watch-fs.ts +22 -6
  52. package/src/cli/watch-judge.ts +48 -6
  53. package/src/cli/watch.ts +163 -42
  54. package/src/cli/why.ts +211 -73
  55. package/src/cli/workspace-config.ts +54 -15
  56. package/src/config.ts +13 -1
  57. package/src/exec/env.ts +7 -2
  58. package/src/exec/executor.ts +154 -10
  59. package/src/exec/index.ts +6 -2
  60. package/src/exec/kill-tree.ts +15 -0
  61. package/src/exec/local-executor.ts +1 -1
  62. package/src/exec/proc-sample.ts +3 -1
  63. package/src/exec/runner.ts +253 -54
  64. package/src/exec/sandbox-binds.ts +7 -3
  65. package/src/exec/sandbox-paths.ts +1 -1
  66. package/src/exec/sandbox-runtime.ts +211 -24
  67. package/src/exec/sandbox-violations.ts +52 -20
  68. package/src/graph/critical-path.ts +79 -0
  69. package/src/graph/default-build.ts +19 -0
  70. package/src/graph/definition.ts +38 -0
  71. package/src/graph/folds.ts +38 -5
  72. package/src/graph/index.ts +4 -1
  73. package/src/graph/priorities.ts +74 -8
  74. package/src/graph/scheduler.ts +270 -60
  75. package/src/graph/task-graph.ts +228 -35
  76. package/src/handoff.ts +97 -9
  77. package/src/history/failure-mode.ts +6 -3
  78. package/src/history/history.ts +167 -61
  79. package/src/history/index.ts +2 -0
  80. package/src/history/metrics.ts +90 -30
  81. package/src/index.ts +30 -13
  82. package/src/orchestrator/admission.ts +10 -6
  83. package/src/orchestrator/affected-tasks.ts +11 -6
  84. package/src/orchestrator/deferred-outputs.ts +2 -0
  85. package/src/orchestrator/doctor.ts +134 -42
  86. package/src/orchestrator/execute-args.ts +4 -1
  87. package/src/orchestrator/execute-task.ts +130 -38
  88. package/src/orchestrator/fingerprint-watch.ts +1 -1
  89. package/src/orchestrator/graph-memo.ts +140 -0
  90. package/src/orchestrator/graph-stage.ts +18 -1
  91. package/src/orchestrator/held-servers.ts +1 -1
  92. package/src/orchestrator/hit-restore.ts +217 -119
  93. package/src/orchestrator/index.ts +4 -3
  94. package/src/orchestrator/local-floor.ts +119 -25
  95. package/src/orchestrator/local-shortcircuit.ts +28 -4
  96. package/src/orchestrator/lockfile-claim.ts +96 -16
  97. package/src/orchestrator/miss-save.ts +42 -8
  98. package/src/orchestrator/options.ts +8 -6
  99. package/src/orchestrator/output-host.ts +43 -14
  100. package/src/orchestrator/output-log.ts +90 -10
  101. package/src/orchestrator/persistent-task.ts +84 -29
  102. package/src/orchestrator/persistent.ts +16 -4
  103. package/src/orchestrator/placement.ts +185 -17
  104. package/src/orchestrator/plan.ts +64 -7
  105. package/src/orchestrator/plugin-host.ts +214 -42
  106. package/src/orchestrator/plugin.ts +113 -37
  107. package/src/orchestrator/prepare.ts +168 -30
  108. package/src/orchestrator/projects.ts +65 -40
  109. package/src/orchestrator/remote-prefetch.ts +119 -25
  110. package/src/orchestrator/run-artifacts.ts +95 -32
  111. package/src/orchestrator/run-context.ts +39 -41
  112. package/src/orchestrator/run-failures.ts +65 -23
  113. package/src/orchestrator/run-id.ts +6 -3
  114. package/src/orchestrator/run-keys.ts +149 -13
  115. package/src/orchestrator/run-live.ts +294 -0
  116. package/src/orchestrator/run-lock.ts +13 -7
  117. package/src/orchestrator/run-records.ts +35 -29
  118. package/src/orchestrator/run-report.ts +3 -3
  119. package/src/orchestrator/run-steps.ts +58 -22
  120. package/src/orchestrator/run-stop.ts +22 -5
  121. package/src/orchestrator/run.ts +331 -88
  122. package/src/orchestrator/sandbox-fix.ts +7 -5
  123. package/src/orchestrator/sandbox-request.ts +70 -20
  124. package/src/orchestrator/shell-verdict.ts +5 -5
  125. package/src/orchestrator/signals.ts +26 -1
  126. package/src/orchestrator/stable-keys.ts +92 -8
  127. package/src/orchestrator/steps.ts +2 -2
  128. package/src/orchestrator/task-artifacts.ts +90 -26
  129. package/src/orchestrator/task-hash.ts +78 -13
  130. package/src/orchestrator/task-log-buffer.ts +72 -36
  131. package/src/orchestrator/task-process.ts +7 -1
  132. package/src/orchestrator/telemetry-host.ts +22 -2
  133. package/src/orchestrator/telemetry.ts +53 -21
  134. package/src/orchestrator/upstream.ts +15 -0
  135. package/src/report/events.ts +32 -13
  136. package/src/report/forecast.ts +31 -9
  137. package/src/report/framed-output.ts +141 -50
  138. package/src/report/index.ts +1 -0
  139. package/src/report/logger.ts +170 -179
  140. package/src/report/path-links.ts +12 -4
  141. package/src/report/plain-output.ts +13 -5
  142. package/src/report/status-line.ts +81 -53
  143. package/src/report/summary.ts +114 -32
  144. package/src/report/terminal-signals.ts +13 -2
  145. package/src/settings.ts +7 -6
  146. package/src/util/bun-version.ts +4 -1
  147. package/src/util/colors.ts +4 -2
  148. package/src/util/config-frame.ts +41 -4
  149. package/src/{graph → util}/dependency-spec.ts +22 -13
  150. package/src/util/edit-distance.ts +13 -1
  151. package/src/util/env-flag.ts +9 -0
  152. package/src/util/errors.ts +59 -3
  153. package/src/util/essential-env.ts +57 -0
  154. package/src/util/git-ignored.ts +1 -1
  155. package/src/util/hash.ts +35 -0
  156. package/src/util/hide-env.ts +45 -5
  157. package/src/util/index.ts +21 -5
  158. package/src/util/num.ts +8 -0
  159. package/src/util/paths.ts +91 -21
  160. package/src/util/report.ts +6 -1
  161. package/src/util/secret-mask.ts +62 -1
  162. package/src/util/secrets.ts +7 -2
  163. package/src/util/size.ts +11 -3
  164. package/src/util/tail.ts +6 -1
  165. package/src/util/timing.ts +13 -1
  166. package/src/util/ui.ts +61 -3
  167. package/src/util/which.ts +1 -1
  168. package/src/util/write-file.ts +67 -0
  169. package/src/workspace/affected.ts +710 -107
  170. package/src/workspace/bracket-literal.ts +84 -0
  171. package/src/workspace/canonical-dir.ts +37 -0
  172. package/src/workspace/config-bytecode.ts +17 -11
  173. package/src/workspace/config-eval.ts +29 -8
  174. package/src/workspace/config-imports.ts +199 -58
  175. package/src/workspace/config-schema.ts +242 -39
  176. package/src/workspace/filter.ts +37 -8
  177. package/src/workspace/index.ts +11 -1
  178. package/src/workspace/json-data.ts +3 -4
  179. package/src/workspace/lockfile.ts +57 -8
  180. package/src/workspace/migration.ts +69 -18
  181. package/src/workspace/package-graph.ts +5 -4
  182. package/src/workspace/project-loader.ts +226 -37
  183. package/src/workspace/repo-id.ts +1 -1
  184. package/src/workspace/workspace.ts +176 -33
@@ -0,0 +1,84 @@
1
+ import { existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { UserError } from '../util/index.js'
4
+ import type { ProjectEntry } from './workspace.js'
5
+
6
+ /**
7
+ * Each unescaped `[`…`]` class in a segment, or null when the segment holds
8
+ * none or another wildcard (`*`, `?`, `{`) a literal reading cannot settle.
9
+ */
10
+ function classes(segment: string): [number, number][] | null {
11
+ const found: [number, number][] = []
12
+ for (let i = 0; i < segment.length; i++) {
13
+ const c = segment[i]!
14
+ if (c === '\\') i++
15
+ else if (c === '*' || c === '?' || c === '{') return null
16
+ else if (c === '[') {
17
+ const end = segment.indexOf(']', i + 2)
18
+ if (end === -1) return null
19
+ found.push([i, end])
20
+ i = end
21
+ }
22
+ }
23
+ return found.length > 0 ? found : null
24
+ }
25
+
26
+ const unescape = (s: string): string => s.replace(/\\(.)/g, '$1')
27
+ const literalSegment = (s: string): boolean => !/(?<!\\)[*?{[]/.test(s)
28
+
29
+ /**
30
+ * The glob with its first class segment escaped, when that segment taken
31
+ * literally names a path under `base`; else null. Only a literal prefix is
32
+ * followed: `**\/[id]` would need a walk, and this runs at `vx lock`.
33
+ */
34
+ function literalBracketFix(glob: string, base: string): string | null {
35
+ const neg = glob.startsWith('!') ? '!' : ''
36
+ const segments = glob.slice(neg.length).split('/')
37
+ const prefix: string[] = []
38
+ for (const [i, s] of segments.entries()) {
39
+ if (literalSegment(s)) {
40
+ prefix.push(unescape(s))
41
+ continue
42
+ }
43
+ const spans = classes(s)
44
+ if (spans === null) return null
45
+ if (!existsSync(path.join(base, ...prefix, unescape(s)))) return null
46
+ let fixed = s
47
+ for (const [open, close] of spans.toReversed())
48
+ fixed = `${fixed.slice(0, open)}\\[${fixed.slice(open + 1, close)}\\]${fixed.slice(close + 1)}`
49
+ return neg + [...segments.slice(0, i), fixed, ...segments.slice(i + 1)].join('/')
50
+ }
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * A bare `app/[id]/**` is the class matching `app/i` and `app/d`, never the
56
+ * route directory `app/[id]` (schema.md). Beside another glob it matched
57
+ * nothing with no warning, so an edit under the route replayed a stale hit
58
+ * (vercel/turborepo#14850 reads an existing bracketed path literally). vx
59
+ * keeps one reading and refuses the glob when that path exists. Checked by
60
+ * `vx lock` only: a run trusts the lock.
61
+ */
62
+ export function checkBracketLiterals(root: string, projects: Iterable<ProjectEntry>): void {
63
+ for (const p of projects) {
64
+ for (const [task, t] of Object.entries(p.config.tasks ?? {})) {
65
+ const lists: [string, readonly string[] | undefined, string][] = [
66
+ ['inputs.files', t.inputs?.files, p.dir],
67
+ ['inputs.workspaceFiles', t.inputs?.workspaceFiles, root],
68
+ ['cache.outputs.files', t.cache?.outputs?.files, p.dir],
69
+ ['cache.outputs.workspaceFiles', t.cache?.outputs?.workspaceFiles, root],
70
+ ]
71
+ for (const [field, globs, base] of lists) {
72
+ for (const glob of globs ?? []) {
73
+ if (!glob.includes('[')) continue
74
+ const fixed = literalBracketFix(glob, base)
75
+ if (fixed === null) continue
76
+ throw new UserError(
77
+ `${p.name}#${task} ${field}: "${glob}" reads its brackets as a character class, ` +
78
+ `so it never matches the path of that name, which exists: write "${fixed}"`,
79
+ )
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,37 @@
1
+ // A directory at where it leads, spelled under the workspace root: its own
2
+ // module, as discovery and the filters both ask it and filter.ts is one
3
+ // discovery imports.
4
+
5
+ import path from 'node:path'
6
+ import { isOutOfFds, realPath } from '../util/index.js'
7
+
8
+ const realRoots = new Map<string, string>()
9
+
10
+ /**
11
+ * `dir` at the directory it leads to, spelled under `root` as given (a
12
+ * root reached through a link, as macOS's temp dir is, keeps its
13
+ * spelling); `dir` itself when that directory lies outside the root; null
14
+ * when `dir` is not there. A workspace glob over linked packages (pnpm's
15
+ * `widgets/**`, `widgets/a -> ../submodules/a`) names the link, and git
16
+ * lists the config at the real directory: matched by spelling, one
17
+ * directory was two projects, the config-less twin at the link taking
18
+ * every edge.
19
+ */
20
+ export function canonicalDir(root: string, dir: string): string | null {
21
+ const real = realOrNull(dir)
22
+ if (real === null) return null
23
+ let realRoot = realRoots.get(root)
24
+ if (realRoot === undefined) realRoots.set(root, (realRoot = realOrNull(root) ?? root))
25
+ const rel = path.relative(realRoot, real)
26
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return dir
27
+ return path.join(root, rel)
28
+ }
29
+
30
+ function realOrNull(p: string): string | null {
31
+ try {
32
+ return realPath(p)
33
+ } catch (err) {
34
+ if (isOutOfFds(err)) throw err
35
+ return null
36
+ }
37
+ }
@@ -8,9 +8,10 @@
8
8
  // every input still has the bytes it was built from requires that instead.
9
9
  //
10
10
  // The bundle is built from the bytes it records, file by file, and is used
11
- // only while each recorded file still hashes the same and each bare import
12
- // still resolves to the file it did; anything else loads the source as
13
- // before and builds again. A graph the bundle could run differently is
11
+ // only while each recorded file still hashes the same and each import still
12
+ // resolves to the file it did (a relative one too: `./preset` finds a new
13
+ // `preset.ts` before the bundled `preset.js`); anything else loads the
14
+ // source as before and builds again. A graph the bundle could run differently is
14
15
  // never built: an `import.meta` field beyond its location, a `require` or
15
16
  // `import()` of a computed specifier (relative to the bundle, not the
16
17
  // file), top-level await, a CommonJS config, a file this process already
@@ -28,6 +29,7 @@ import {
28
29
  realpathSync,
29
30
  renameSync,
30
31
  rmSync,
32
+ writeFileSync,
31
33
  } from 'node:fs'
32
34
  import { writeFile } from 'node:fs/promises'
33
35
  import { createRequire } from 'node:module'
@@ -35,7 +37,7 @@ import path from 'node:path'
35
37
  import { xxh3hex } from '../util/index.js'
36
38
 
37
39
  /** Bumped when what a bundle holds, or how it is checked, changes. */
38
- const COMPILED_VERSION = 1
40
+ const COMPILED_VERSION = 2
39
41
 
40
42
  /** What `import.meta` becomes in a bundled file: an identifier no source spells. */
41
43
  const META = '__vxImportMeta'
@@ -49,8 +51,8 @@ interface Manifest {
49
51
  entry: string
50
52
  /** Every file the bundle was built from, by real path, and its bytes' hash. */
51
53
  inputs: Record<string, string>
52
- /** Each bare import: the importing file's directory, the specifier, the real path it resolved to. */
53
- bare: [string, string, string][]
54
+ /** Each import: the importing file's directory, the specifier, the real path it resolved to. */
55
+ imports: [string, string, string][]
54
56
  /** The graph cannot be bundled faithfully: load the source while the inputs stay as they are. */
55
57
  skip?: true
56
58
  }
@@ -74,7 +76,7 @@ function fresh(manifest: Manifest): boolean {
74
76
  }
75
77
  if (xxh3hex(bytes) !== hash) return false
76
78
  }
77
- for (const [from, spec, to] of manifest.bare) {
79
+ for (const [from, spec, to] of manifest.imports) {
78
80
  try {
79
81
  if (realpathSync(Bun.resolveSync(spec, from)) !== to) return false
80
82
  } catch {
@@ -127,7 +129,6 @@ const answered = new Map<string, { bytes: Uint8Array; ns: { default?: unknown }
127
129
  * process, and a bundle would run a second copy of its state.
128
130
  */
129
131
  export function loadedModules(root: string): Set<string> {
130
- // Not `import.meta`: core's planner also runs as a browser script (the playground).
131
132
  return new Set(Object.keys(createRequire(path.join(root, 'index.js')).cache))
132
133
  }
133
134
 
@@ -165,6 +166,12 @@ export async function buildCompiled(
165
166
  const tmp = path.join(dir, `.tmp-${process.pid}-${Date.now()}`)
166
167
  try {
167
168
  mkdirSync(tmp, { recursive: true })
169
+ // Ignored by git under a cache dir elsewhere, where no `.vx/.gitignore` is written.
170
+ try {
171
+ writeFileSync(path.join(dir, '.gitignore'), '*\n', { flag: 'wx' })
172
+ } catch {
173
+ // there already: the user's or an earlier run's
174
+ }
168
175
  const built = await Bun.build({
169
176
  entrypoints: [configPath],
170
177
  target: 'bun',
@@ -232,7 +239,7 @@ export async function buildCompiled(
232
239
  version: COMPILED_VERSION,
233
240
  entry: entryFile,
234
241
  inputs,
235
- bare: bareImports(built.metafile),
242
+ imports: importsOf(built.metafile),
236
243
  ...(faithful && bytecode ? {} : { skip: true as const }),
237
244
  }
238
245
  // The metafile names every input; one no onLoad saw (a JSON import) is
@@ -261,14 +268,13 @@ export async function buildCompiled(
261
268
  }
262
269
  }
263
270
 
264
- function bareImports(metafile: Bun.BuildMetafile | undefined): [string, string, string][] {
271
+ function importsOf(metafile: Bun.BuildMetafile | undefined): [string, string, string][] {
265
272
  const out: [string, string, string][] = []
266
273
  for (const [input, entry] of Object.entries(metafile?.inputs ?? {})) {
267
274
  const from = path.dirname(realpathSync(path.resolve(input)))
268
275
  for (const imp of entry.imports) {
269
276
  const spec = imp.original
270
277
  if (imp.external === true || spec === undefined) continue
271
- if (spec.startsWith('.') || path.isAbsolute(spec)) continue
272
278
  out.push([from, spec, realpathSync(path.resolve(imp.path))])
273
279
  }
274
280
  }
@@ -104,17 +104,19 @@ const nonJsonPaths = ${nonJsonPaths.toString()}
104
104
  }
105
105
  }
106
106
  // Bun's resolver reads \\ as a separator and ? as a query, so a config under a\\b or a?b is served
107
- // from the path as written (project-loader.ts's LITERAL_QUERY), read now.
107
+ // from the path as written (project-loader.ts's LITERAL_QUERY), read now. By the request's id:
108
+ // Bun 1.4.4 hands onLoad the path with \\ made /, and a\\b's config was read from a/b.
108
109
  let literal = false
110
+ const literals = new Map()
109
111
  const serveLiteral = () => {
110
112
  if (literal) return
111
113
  literal = true
112
114
  globalThis.Bun.plugin({
113
115
  name: 'vx-config-literal',
114
116
  setup(build) {
115
- build.onResolve({ filter: /\\?vx-literal$/ }, (args) => ({ path: args.path, namespace: 'file' }))
116
- build.onLoad({ filter: /\\?vx-literal$/ }, async (args) => {
117
- const file = args.path.slice(0, -'?vx-literal'.length)
117
+ build.onResolve({ filter: /\\?vx-literal=\\d+$/ }, (args) => ({ path: args.path, namespace: 'file' }))
118
+ build.onLoad({ filter: /\\?vx-literal=\\d+$/ }, async (args) => {
119
+ const file = literals.get(/=(\\d+)$/.exec(args.path)[1])
118
120
  return { contents: await globalThis.Bun.file(file).text(), loader: /\\.[cm]?ts$/.test(file) ? 'ts' : 'js' }
119
121
  })
120
122
  },
@@ -127,8 +129,6 @@ self.onmessage = async (e) => {
127
129
  // env var set in-process evaluated with it on the first load and without
128
130
  // it on every later one, and an embedder's second run() derived a
129
131
  // different key (D-61). Each request carries the parent's env now.
130
- // Through globalThis: the playground bundles this source as a string, and
131
- // its free-global scan reads a bare process after a paren as code.
132
132
  const live = globalThis.process.env
133
133
  for (const k of Object.keys(live)) if (!(k in env)) delete live[k]
134
134
  Object.assign(live, env)
@@ -173,8 +173,11 @@ self.onmessage = async (e) => {
173
173
  }
174
174
  try {
175
175
  const literalPath = /[\\\\?]/.test(path)
176
- if (literalPath) serveLiteral()
177
- const ns = await import(literalPath ? path + '?vx-literal' : path)
176
+ if (literalPath) {
177
+ serveLiteral()
178
+ literals.set(String(id), path)
179
+ }
180
+ const ns = await import(literalPath ? path + '?vx-literal=' + id : path)
178
181
  // Awaited as the in-process load's async return flattens it: a Promise
179
182
  // default loaded on a run and was refused as "an instance of Promise"
180
183
  // on every later evaluation in the process (D-5).
@@ -207,7 +210,16 @@ self.onmessage = async (e) => {
207
210
  // A thrown string or plain object has no message, and String() of a
208
211
  // null-prototype object throws here, inside the catch.
209
212
  const isError = err !== null && typeof err === 'object' && typeof err.message === 'string'
213
+ // A cause holds the reason a wrapper names only by its own words.
214
+ const causes = []
215
+ const seen = new Set([err])
216
+ for (let c = isError ? err.cause : undefined; c !== undefined && c !== null && !seen.has(c) && causes.length < 8; c = c.cause) {
217
+ seen.add(c)
218
+ const isE = typeof c === 'object' && typeof c.message === 'string'
219
+ causes.push(isE ? (c.stack ?? (c.name + ': ' + c.message)) : globalThis.Bun.inspect(c, { compact: true }))
220
+ }
210
221
  postMessage({
222
+ causes,
211
223
  id,
212
224
  ok: false,
213
225
  thrown: isError ? null : globalThis.Bun.inspect(err, { compact: true }),
@@ -240,6 +252,8 @@ interface WorkerReply {
240
252
  /** What a config threw that is not an Error, as `thrownValueMessage` shows it; null for an Error. */
241
253
  thrown?: string | null
242
254
  stack: string | null
255
+ /** Each `cause` under a thrown Error, outermost first, as its stack (or inspected value). */
256
+ causes?: string[]
243
257
  /** A transpile error's location — `BuildMessage.position`, trimmed to what the loader reads. */
244
258
  position: { file?: string; line?: number; column?: number } | null
245
259
  }
@@ -333,6 +347,13 @@ function acquireWorker(): Worker {
333
347
  if (msg.stack !== null) err.stack = msg.stack
334
348
  else delete err.stack
335
349
  if (msg.position !== null) err.position = msg.position
350
+ let cause: Error | undefined
351
+ for (const stack of (msg.causes ?? []).toReversed()) {
352
+ const c = new Error('', cause === undefined ? undefined : { cause })
353
+ c.stack = stack
354
+ cause = c
355
+ }
356
+ if (cause !== undefined) err.cause = cause
336
357
  p.reject(err)
337
358
  }
338
359
  w.onerror = (event: ErrorEvent): void => {