@vzn/vx 0.0.530 → 0.0.531

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.530",
3
+ "version": "0.0.531",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.530",
25
- "@vzn/vx-linux-arm64": "0.0.530",
26
- "@vzn/vx-darwin-x64": "0.0.530",
27
- "@vzn/vx-darwin-arm64": "0.0.530"
24
+ "@vzn/vx-linux-x64": "0.0.531",
25
+ "@vzn/vx-linux-arm64": "0.0.531",
26
+ "@vzn/vx-darwin-x64": "0.0.531",
27
+ "@vzn/vx-darwin-arm64": "0.0.531"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -339,6 +339,7 @@ function entryOf(row: EntryRow, fileRows: OutputFileRow[]): CacheEntry {
339
339
  command: row.command,
340
340
  exitCode: row.exit_code,
341
341
  durationMs: row.duration_ms,
342
+ sizeBytes: row.size_bytes,
342
343
  ...(row.cpu_ms !== null ? { cpuMs: row.cpu_ms } : {}),
343
344
  ...(row.peak_rss_bytes !== null ? { peakRssBytes: row.peak_rss_bytes } : {}),
344
345
  outputFiles: fileRows.map((r) => r.path),
@@ -487,9 +488,24 @@ function errorText(err: unknown): string {
487
488
  return err instanceof Error ? err.message : String(err)
488
489
  }
489
490
 
491
+ /** What a run's background uploads moved: totals over the ones that landed. */
492
+ export interface UploadTally {
493
+ count: number
494
+ bytes: number
495
+ /** Summed per-upload time; uploads overlap, so this exceeds the wall. */
496
+ ms: number
497
+ failed: number
498
+ }
499
+
490
500
  export class Cache implements CacheLayer {
491
501
  /** This IS the local layer — there is nothing slower behind it. */
492
502
  readonly hasRemote = false
503
+ /**
504
+ * The run's uploads to a remote behind this cache, tallied by the
505
+ * `LayeredCache` over it (a plugin's, constructed over `ctx.localCache`)
506
+ * so the run can report them without a method on the layer seam.
507
+ */
508
+ readonly uploads: UploadTally = { count: 0, bytes: 0, ms: 0, failed: 0 }
493
509
 
494
510
  private readonly db: Database
495
511
  private readonly insertEntry: ReturnType<Database['prepare']>
@@ -159,6 +159,10 @@ export interface CacheEntry {
159
159
  * them work vs. a stale-local replay.
160
160
  */
161
161
  source?: 'local' | 'remote'
162
+ /** The artifact's compressed size, when the index knows it. */
163
+ sizeBytes?: number
164
+ /** On a remote hit this run pulled: how long the download and its ingest took. */
165
+ fetchMs?: number
162
166
  }
163
167
 
164
168
  export interface RunRecord {
@@ -260,8 +260,9 @@ export class LayeredCache implements CacheLayer {
260
260
  * remote cache — so we flip `source` to `'remote'` and the
261
261
  * orchestrator reports `cache-hit-remote`. Without this, a prefetch
262
262
  * followed by a `get` would mislabel a genuine remote hit as local.
263
+ * Each maps to how long its download and ingest took.
263
264
  */
264
- private readonly remoteSourced = new Set<string>()
265
+ private readonly remoteSourced = new Map<string, number>()
265
266
 
266
267
  /**
267
268
  * Background write-through uploads. `save()` returns after the local
@@ -364,7 +365,7 @@ export class LayeredCache implements CacheLayer {
364
365
  // A prefetch this run may have materialized this entry FROM
365
366
  // remote; the local row exists now, but the work was saved by
366
367
  // the remote cache, so the provenance stays 'remote'.
367
- return this.remoteSourced.has(hash) ? { ...localHit, source: 'remote' } : localHit
368
+ return this.asRemote(hash, localHit)
368
369
  }
369
370
 
370
371
  // Remote reads disabled (e.g. `--cache=remote:`): a local miss is a
@@ -395,7 +396,12 @@ export class LayeredCache implements CacheLayer {
395
396
  // above and that skip's `local.has`, stamping 'remote' here reported a
396
397
  // purely-local hit as `cache-hit-remote` and inflated the remote
397
398
  // hit-rate. Whatever provenance the entry has is the truth.
398
- return this.remoteSourced.has(hash) ? { ...materialized, source: 'remote' } : materialized
399
+ return this.asRemote(hash, materialized)
400
+ }
401
+
402
+ private asRemote(hash: string, entry: CacheEntry): CacheEntry {
403
+ const fetchMs = this.remoteSourced.get(hash)
404
+ return fetchMs === undefined ? entry : { ...entry, source: 'remote', fetchMs }
399
405
  }
400
406
 
401
407
  // Existence probe: local first, then a remote HEAD — no body
@@ -439,6 +445,7 @@ export class LayeredCache implements CacheLayer {
439
445
  // or remote if a concurrent prefetch set it).
440
446
  if ((await this.local.has(hash)) === 'local') return true
441
447
 
448
+ const fetchStart = performance.now()
442
449
  let remoteResult: unknown
443
450
  try {
444
451
  remoteResult = await this.wire.get(hash)
@@ -490,7 +497,7 @@ export class LayeredCache implements CacheLayer {
490
497
  this.reportRemoteError('download', hash, err)
491
498
  return false
492
499
  }
493
- this.remoteSourced.add(hash)
500
+ this.remoteSourced.set(hash, Math.round(performance.now() - fetchStart))
494
501
  return true
495
502
  }
496
503
 
@@ -543,11 +550,17 @@ export class LayeredCache implements CacheLayer {
543
550
  }
544
551
  }
545
552
  this.enqueueUpload(async () => {
553
+ const tally = this.local.uploads
554
+ const start = performance.now()
546
555
  try {
547
556
  const body =
548
557
  packed !== undefined ? new Blob([packed]) : Bun.file(this.local.outputsPath(hash))
549
558
  await this.wire.put(hash, body, { durationMs })
559
+ tally.count++
560
+ tally.bytes += body.size
561
+ tally.ms += Math.round(performance.now() - start)
550
562
  } catch (err) {
563
+ tally.failed++
551
564
  this.reportRemoteError('upload', hash, err)
552
565
  }
553
566
  })
@@ -86,6 +86,15 @@ export interface TaskOutcome {
86
86
  queuedMs?: number
87
87
  /** On a cacheable task that ran: how many files its key read. */
88
88
  inputFiles?: number
89
+ /**
90
+ * The artifact's compressed size: on a hit, the entry's; on a miss, the one
91
+ * its save wrote (measured only when a telemetry sink listens).
92
+ */
93
+ artifactBytes?: number
94
+ /** On a remote hit this run pulled: the download and its ingest. */
95
+ fetchMs?: number
96
+ /** On a miss that saved, with a sink listening: the save's own time. */
97
+ saveMs?: number
89
98
  /**
90
99
  * On a cacheable task that ran: what its key changed since the last entry
91
100
  * saved for it. Only when a telemetry sink asked, and the cache holds one.
@@ -234,7 +243,7 @@ export interface ScheduleOptions {
234
243
  * execute request), so it waits for the save to land, while the freed
235
244
  * slot admits other work at once. Undefined: nothing owed.
236
245
  */
237
- settledOf?: (outcome: TaskOutcome) => Promise<void> | undefined
246
+ settledOf?: (outcome: TaskOutcome) => Promise<Partial<TaskOutcome> | void> | undefined
238
247
  /**
239
248
  * Optional priority override: callers pass their own per-node weight
240
249
  * (e.g. `computePredictedPriorities` from the orchestrator's history
@@ -826,11 +835,12 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
826
835
  return
827
836
  }
828
837
  tick()
829
- const done = (): void => {
830
- finishOne(id, outcome)
838
+ // What the save cost rides the outcome it settles.
839
+ const done = (owed?: Partial<TaskOutcome> | void): void => {
840
+ finishOne(id, owed ? { ...outcome, ...owed } : outcome)
831
841
  tick()
832
842
  }
833
- void settled.then(done, done)
843
+ void settled.then(done, () => done())
834
844
  },
835
845
  (err: unknown) => {
836
846
  if (err instanceof RestoreDemoted) {
@@ -256,6 +256,12 @@ export interface OutcomeView {
256
256
  inputFiles?: number
257
257
  /** What a missed task's key changed since its last saved entry (see `TaskOutcome`). */
258
258
  inputChanges?: InputChanges
259
+ /** The artifact's compressed size, a hit's or a measured save's (see `TaskOutcome`). */
260
+ artifactBytes?: number
261
+ /** A remote hit's download and ingest (see `TaskOutcome`). */
262
+ fetchMs?: number
263
+ /** A measured save's own time (see `TaskOutcome`). */
264
+ saveMs?: number
259
265
  restored?: boolean
260
266
  sandboxViolations?: number
261
267
  sandboxViolationLines?: string[]
@@ -378,6 +384,9 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
378
384
  if (outcome.queuedMs !== undefined) view.queuedMs = outcome.queuedMs
379
385
  if (outcome.inputFiles !== undefined) view.inputFiles = outcome.inputFiles
380
386
  if (outcome.inputChanges !== undefined) view.inputChanges = outcome.inputChanges
387
+ if (outcome.artifactBytes !== undefined) view.artifactBytes = outcome.artifactBytes
388
+ if (outcome.fetchMs !== undefined) view.fetchMs = outcome.fetchMs
389
+ if (outcome.saveMs !== undefined) view.saveMs = outcome.saveMs
381
390
  if (outcome.timedOut === true) view.timedOut = true
382
391
  if (outcome.notReady !== undefined) view.notReady = outcome.notReady
383
392
  if (outcome.blockedBy !== undefined) view.blockedBy = outcome.blockedBy
@@ -65,7 +65,7 @@ import {
65
65
  type WithheldLink,
66
66
  withheldLinkLine,
67
67
  } from './sandbox-request.js'
68
- import { markUnsaved, saveMiss, type OutputDirSnapshot } from './miss-save.js'
68
+ import { markUnsaved, saveMiss, type OutputDirSnapshot, type SaveFacts } from './miss-save.js'
69
69
  import type { FingerprintWatch } from './fingerprint-watch.js'
70
70
  import { restoreHit } from './hit-restore.js'
71
71
  import type { MissExplainer } from './miss-reason.js'
@@ -211,7 +211,7 @@ export interface ExecuteArgs {
211
211
  * in-flight join (admission.ts): a duplicate of this task in another run
212
212
  * must not probe the cache before the entry is there.
213
213
  */
214
- deferredSaves?: Map<string, Promise<void>>
214
+ deferredSaves?: Map<string, Promise<SaveFacts>>
215
215
  /**
216
216
  * `continueMode: 'always'` let this task run although an upstream —
217
217
  * directly or through a chain of successes — failed or aborted. It still
@@ -1261,6 +1261,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1261
1261
  ...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
1262
1262
  outputDirSnapshots: args.outputDirSnapshots,
1263
1263
  deferSave: args.deferSave,
1264
+ // A sink listens: say what the save cost.
1265
+ measure: args.explainMiss !== undefined,
1264
1266
  })
1265
1267
  args.deferredSaves?.set(node.id, landed)
1266
1268
  } else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants !== true) {
@@ -303,6 +303,8 @@ export async function restoreHit(restore: RestoreHitArgs): Promise<TaskOutcome>
303
303
  storedDurationMs: hit.durationMs,
304
304
  ...(hit.cpuMs !== undefined ? { storedCpuMs: hit.cpuMs } : {}),
305
305
  ...(hit.peakRssBytes !== undefined ? { storedPeakRssBytes: hit.peakRssBytes } : {}),
306
+ ...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
307
+ ...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
306
308
  hash,
307
309
  restored,
308
310
  wallclockStartNs: taskStartNs,
@@ -7,7 +7,7 @@
7
7
  // a later run replays. Moved out of execute-task.ts 2026-09-09 as pure
8
8
  // code motion.
9
9
 
10
- import { lstatSync, realpathSync } from 'node:fs'
10
+ import { lstatSync, realpathSync, statSync } from 'node:fs'
11
11
  import path from 'node:path'
12
12
  import {
13
13
  type CacheLayer,
@@ -91,10 +91,21 @@ export interface SaveMissArgs {
91
91
  * index go to the run's save lane (save-lane.ts).
92
92
  */
93
93
  deferSave?: ((save: () => Promise<void>) => Promise<void>) | undefined
94
+ /** Time the save and size its artifact, for a telemetry sink: one stat. */
95
+ measure?: boolean | undefined
94
96
  }
95
97
 
96
- /** `landed` settles when the entry is in the cache — at once without a lane. */
97
- export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<void> }> {
98
+ /** What a measured save cost, on the outcome once it lands. */
99
+ export interface SaveFacts {
100
+ saveMs?: number
101
+ artifactBytes?: number
102
+ }
103
+
104
+ /**
105
+ * `landed` settles when the entry is in the cache — at once without a lane —
106
+ * with what it cost when `measure` asked.
107
+ */
108
+ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveFacts> }> {
98
109
  const { node, cache, log } = a
99
110
  const endResolve = span('miss: resolve outputs')
100
111
  const outputFiles =
@@ -145,7 +156,9 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<void>
145
156
  : ''),
146
157
  )
147
158
  }
159
+ const facts: SaveFacts = {}
148
160
  const save = async (): Promise<void> => {
161
+ const saveStart = a.measure === true ? performance.now() : 0
149
162
  const endSave = span('miss: save')
150
163
  // Tier-3 input fingerprint: the digest rows captured by the pre-exec
151
164
  // describe above, persisted with the entry inside `cache.save`'s
@@ -171,6 +184,14 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<void>
171
184
  },
172
185
  })
173
186
  endSave()
187
+ if (a.measure === true) {
188
+ facts.saveMs = Math.round(performance.now() - saveStart)
189
+ try {
190
+ facts.artifactBytes = statSync(cache.outputsPath(a.hash)).size
191
+ } catch {
192
+ // No local artifact (local writes off): no size to say.
193
+ }
194
+ }
174
195
  // The files the task just wrote are the entry's bytes: stamp them,
175
196
  // so the next hit can tell them from another entry's with the same
176
197
  // size and mtime (item 886).
@@ -198,9 +219,9 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<void>
198
219
  // Off the slot when the run keeps a lane (the save lane bounds and
199
220
  // drains it); in the slot otherwise — an embedder without a lane gets
200
221
  // the entry before the outcome.
201
- if (a.deferSave !== undefined) return { landed: a.deferSave(save) }
222
+ if (a.deferSave !== undefined) return { landed: a.deferSave(save).then(() => facts) }
202
223
  await save()
203
- return { landed: Promise.resolve() }
224
+ return { landed: Promise.resolve(facts) }
204
225
  }
205
226
 
206
227
  /** What `markUnsaved` reads of a miss: where its declared outputs are. */
@@ -42,7 +42,7 @@ import {
42
42
  } from '../util/index.js'
43
43
  import { keyedProjects } from './keyed-projects.js'
44
44
  import { prepareSandbox } from './sandbox-request.js'
45
- import type { OutputDirSnapshot } from './miss-save.js'
45
+ import type { OutputDirSnapshot, SaveFacts } from './miss-save.js'
46
46
  import { admitTasks, taintTracker } from './admission.js'
47
47
  import { excludedTaint } from './excluded-keys.js'
48
48
  import { keyUpstream } from './upstream.js'
@@ -800,7 +800,7 @@ async function runOnBus(
800
800
  const saveLane = createSaveLane(2 * concurrency, (err) =>
801
801
  log.status(`[vx] cache save failed: ${err instanceof Error ? err.message : String(err)}`),
802
802
  )
803
- const deferredSaves = new Map<string, Promise<void>>()
803
+ const deferredSaves = new Map<string, Promise<SaveFacts>>()
804
804
 
805
805
  // Focused flow: a requested GROUP has no output of its own, so
806
806
  // surface the same-project, non-group tasks it chains (one level)
@@ -1209,27 +1209,6 @@ async function runOnBus(
1209
1209
  log.status(`[vx] run history not recorded: ${message} — the verdict above stands`)
1210
1210
  }
1211
1211
  mark('record history')
1212
- // Hand the per-run summary to the telemetry sinks + drain them. Only
1213
- // when a sink is active (telemetry !== undefined) — otherwise this
1214
- // whole block is skipped and the run is byte-identical to before.
1215
- // emitSummary/flush are crash-isolated, so a faulty sink can't fail
1216
- // the run; flush is the sink's last chance to ship buffered records.
1217
- if (telemetry !== undefined && runContextRecord !== undefined) {
1218
- const summary = assembleRunSummary(runContextRecord, records.telemetryTasks, {
1219
- startedAt: endedAtMsAtStart,
1220
- endedAt: endedAtMs,
1221
- totalDurationMs: Math.round(totalMs),
1222
- exitOk: ok,
1223
- abortedCount: list.filter((o) => o.status === 'aborted' && !isGroupTask(o.node)).length,
1224
- stages: stageTimes(),
1225
- })
1226
- telemetry.emitSummary(summary)
1227
- await telemetry.flush()
1228
- }
1229
- // End-of-run plugin lifecycle: each plugin's teardown(). Crash-isolated
1230
- // + time-bounded inside teardownPlugins, so a faulty plugin can neither
1231
- // fail nor hang the run. Here on the normal path, after the drains
1232
- // below; every other exit takes it through `teardown` too.
1233
1212
  // Drain any still-in-flight background prefetches before closing the
1234
1213
  // cache handle — a prefetch ingesting into a closed SQLite DB would
1235
1214
  // throw. Tasks that resolved as local hits never awaited their
@@ -1252,6 +1231,30 @@ async function runOnBus(
1252
1231
  // drain of the lane stood here until item 634; deleting it changed
1253
1232
  // nothing the suite could see, because this gate is the rule.
1254
1233
  await cache.drainUploads?.()
1234
+ // Hand the per-run summary to the telemetry sinks + drain them. Only
1235
+ // when a sink is active (telemetry !== undefined) — otherwise this
1236
+ // whole block is skipped and the run is byte-identical to before.
1237
+ // emitSummary/flush are crash-isolated, so a faulty sink can't fail
1238
+ // the run; flush is the sink's last chance to ship buffered records.
1239
+ // After the drain, so the summary says what the uploads moved; a live
1240
+ // sink's task records went out as each task ended.
1241
+ if (telemetry !== undefined && runContextRecord !== undefined) {
1242
+ const summary = assembleRunSummary(runContextRecord, records.telemetryTasks, {
1243
+ startedAt: endedAtMsAtStart,
1244
+ endedAt: endedAtMs,
1245
+ totalDurationMs: Math.round(totalMs),
1246
+ exitOk: ok,
1247
+ abortedCount: list.filter((o) => o.status === 'aborted' && !isGroupTask(o.node)).length,
1248
+ stages: stageTimes(),
1249
+ uploads: prepared.localCache.uploads,
1250
+ })
1251
+ telemetry.emitSummary(summary)
1252
+ await telemetry.flush()
1253
+ }
1254
+ // End-of-run plugin lifecycle: each plugin's teardown(). Crash-isolated
1255
+ // + time-bounded inside teardownPlugins, so a faulty plugin can neither
1256
+ // fail nor hang the run. Here on the normal path, after the drains
1257
+ // above; every other exit takes it through `teardown` too.
1255
1258
  // The miss path's output-directory snapshots, taken now that the
1256
1259
  // directories are old enough for the snapshot's racy window (see
1257
1260
  // miss-save.ts). A few at a time: each is an lstat + readdir per
@@ -232,6 +232,12 @@ export interface TaskTelemetry {
232
232
  * it: what its key changed since, the first ten named. Additive.
233
233
  */
234
234
  inputChanges?: InputChanges
235
+ /** The artifact's compressed size: on a hit the entry's, on a miss the save's. Additive. */
236
+ artifactBytes?: number
237
+ /** On a remote hit this run pulled: the download and its ingest. Additive. */
238
+ fetchMs?: number
239
+ /** On a miss that saved: the save's own time (pack, write, index). Additive. */
240
+ saveMs?: number
235
241
  /**
236
242
  * On a cache hit: whether outputs were written this run (`true`) or the
237
243
  * disk already matched the entry and nothing was restored (`false`, an
@@ -339,6 +345,12 @@ export interface RunSummaryRecord {
339
345
  * stages ahead of the run lock end before `startedAt`. Additive.
340
346
  */
341
347
  stages?: readonly RunStage[]
348
+ /**
349
+ * The run's uploads to a remote cache, once they settled: how many landed,
350
+ * their bytes, their summed time (they overlap), and how many failed.
351
+ * Absent when nothing was uploaded or tried. Additive.
352
+ */
353
+ uploads?: { count: number; bytes: number; ms: number; failed: number }
342
354
  }
343
355
 
344
356
  /** One stage of a run, a wall window in epoch ms. */
@@ -369,6 +381,7 @@ export function assembleRunSummary(
369
381
  exitOk: boolean
370
382
  abortedCount: number
371
383
  stages?: readonly RunStage[]
384
+ uploads?: { count: number; bytes: number; ms: number; failed: number }
372
385
  },
373
386
  ): RunSummaryRecord {
374
387
  let failedCount = 0
@@ -404,6 +417,9 @@ export function assembleRunSummary(
404
417
  exitOk: timing.exitOk,
405
418
  tasks,
406
419
  ...(timing.stages !== undefined && timing.stages.length > 0 ? { stages: timing.stages } : {}),
420
+ ...(timing.uploads !== undefined && timing.uploads.count + timing.uploads.failed > 0
421
+ ? { uploads: { ...timing.uploads } }
422
+ : {}),
407
423
  }
408
424
  }
409
425
 
@@ -769,6 +785,9 @@ export function taskTelemetryOf(o: TaskOutcome): TaskTelemetry {
769
785
  if (o.queuedMs !== undefined) t.queuedMs = o.queuedMs
770
786
  if (o.inputFiles !== undefined) t.inputFiles = o.inputFiles
771
787
  if (o.inputChanges !== undefined) t.inputChanges = o.inputChanges
788
+ if (o.artifactBytes !== undefined) t.artifactBytes = o.artifactBytes
789
+ if (o.fetchMs !== undefined) t.fetchMs = o.fetchMs
790
+ if (o.saveMs !== undefined) t.saveMs = o.saveMs
772
791
  if (isCacheHit(o.status)) t.restored = o.restored === true
773
792
  if (o.wallclockStartNs !== undefined) t.wallclockStartNs = o.wallclockStartNs.toString()
774
793
  if (o.wallclockEndNs !== undefined) t.wallclockEndNs = o.wallclockEndNs.toString()
@@ -294,7 +294,12 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
294
294
  // below are Bun's own and nothing else answers to them, so this is
295
295
  // narrower than it looks: every other throw still passes through (2026-09-20).
296
296
  if (err === null || typeof err !== 'object') return null
297
- const { name, message } = err as { name?: unknown; message?: unknown }
297
+ const { name, message, errors } = err as { name?: unknown; message?: unknown; errors?: unknown }
298
+ // Two syntax errors arrive as one `AggregateError` of `BuildMessage`s,
299
+ // which printed a stack and no position (X-6). The first is where to look.
300
+ if (name === 'AggregateError' && Array.isArray(errors) && errors.length > 0) {
301
+ return configLoadError(errors[0], configPath, kind)
302
+ }
298
303
  if (typeof message !== 'string') return null
299
304
  if (name === 'ResolveMessage') {
300
305
  const spec = /Cannot find (?:package|module) ['"]([^'"]+)['"]/.exec(message)?.[1]