@veryfront/ext-bundler-esbuild 0.1.1237 → 0.1.1239

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.
@@ -1,5 +1,15 @@
1
1
  import type { BuildContext, BundleOptions, Bundler, BundleResult, TransformOptions, TransformResult } from "veryfront/extensions/bundler";
2
2
  import type { ChildProcess } from "node:child_process";
3
+ /**
4
+ * Unexpected service-child deaths tolerated before the adapter gives up.
5
+ *
6
+ * The child can be killed by something outside the process (OOM-kill, a
7
+ * container runtime signal). That is recoverable: esbuild respawns the
8
+ * service once its module state is reset, so it must not poison the process
9
+ * the way foreign ownership does. The budget keeps a crash-looping binary
10
+ * from respawning forever.
11
+ */
12
+ export declare const MAX_SERVICE_RESTARTS = 3;
3
13
  /**
4
14
  * Keep lifecycle tracking tolerant of Node-compatible child-process shims.
5
15
  *
@@ -8,8 +18,12 @@ import type { ChildProcess } from "node:child_process";
8
18
  */
9
19
  /** The ownership latch is module-wide; tests must clear it between cases. */
10
20
  export declare function __resetOwnershipErrorForTests(): void;
21
+ /** Crash-recovery state is module-wide; tests must reset it between cases. */
22
+ export declare function __resetServiceRecoveryForTests(): void;
11
23
  /** Exercise the latch without starting a real esbuild service. */
12
24
  export declare function __recordOwnershipErrorForTests(cause?: unknown): Error;
25
+ /** Exercise trusted esbuild diagnostic normalization without starting a service. */
26
+ export declare function __markEsbuildSourceDiagnosticForTests(error: unknown): void;
13
27
  export declare function isLiveEsbuildServiceProcess(child: Pick<ChildProcess, "killed" | "exitCode" | "signalCode">): boolean;
14
28
  /**
15
29
  * esbuild-backed {@link Bundler} implementation.
@@ -1 +1 @@
1
- {"version":3,"file":"esbuild-bundler.d.ts","sourceRoot":"","sources":["../src/esbuild-bundler.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,OAAO,EACP,YAAY,EAGZ,gBAAgB,EAChB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAgPvD;;;;;GAKG;AACH,6EAA6E;AAC7E,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAED,kEAAkE;AAClE,wBAAgB,8BAA8B,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAErE;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC,GAC9D,OAAO,CAIT;AA6HD;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,OAAO;IACtC,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAsCrD,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAa9D,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAqCtD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAiF5B"}
1
+ {"version":3,"file":"esbuild-bundler.d.ts","sourceRoot":"","sources":["../src/esbuild-bundler.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,OAAO,EACP,YAAY,EAGZ,gBAAgB,EAChB,eAAe,EAChB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAkBvD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAqftC;;;;;GAKG;AACH,6EAA6E;AAC7E,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAED,8EAA8E;AAC9E,wBAAgB,8BAA8B,IAAI,IAAI,CAQrD;AAED,kEAAkE;AAClE,wBAAgB,8BAA8B,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAErE;AAED,oFAAoF;AACpF,wBAAgB,qCAAqC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE1E;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC,GAC9D,OAAO,CAIT;AA8ID;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,OAAO;IACtC,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAsCrD,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAkB9D,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAiEtD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAsH5B"}
@@ -16,6 +16,22 @@ import { createRequire } from "node:module";
16
16
  import { ensureEsbuildBinary } from "./binary.js";
17
17
  import { toEsbuildPlugin } from "./plugin-adapter.js";
18
18
  const ESBUILD_STOP_TIMEOUT_MS = 5_000;
19
+ const ESBUILD_SOURCE_DIAGNOSTIC = Symbol.for("veryfront.bundler.esbuild-source-diagnostic");
20
+ const ArrayIsArray = Array.isArray;
21
+ const ObjectDefineProperty = Object.defineProperty;
22
+ const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty;
23
+ const ReflectApply = Reflect.apply;
24
+ const ReflectGetOwnPropertyDescriptor = Reflect.getOwnPropertyDescriptor;
25
+ /**
26
+ * Unexpected service-child deaths tolerated before the adapter gives up.
27
+ *
28
+ * The child can be killed by something outside the process (OOM-kill, a
29
+ * container runtime signal). That is recoverable: esbuild respawns the
30
+ * service once its module state is reset, so it must not poison the process
31
+ * the way foreign ownership does. The budget keeps a crash-looping binary
32
+ * from respawning forever.
33
+ */
34
+ export const MAX_SERVICE_RESTARTS = 3;
19
35
  const childProcess = createRequire(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)("node:child_process");
20
36
  let esbuildModule = null;
21
37
  let esbuildService = null;
@@ -23,14 +39,86 @@ let esbuildOwnershipError = null;
23
39
  let esbuildShutdownError = null;
24
40
  let pluginDisposalError = null;
25
41
  let esbuildStopPromise = null;
42
+ let esbuildServiceLost = false;
43
+ let esbuildServiceLostDetail = "";
44
+ let remainingServiceRestarts = MAX_SERVICE_RESTARTS;
45
+ let esbuildServiceRecovery = null;
46
+ let esbuildServiceGeneration = 0;
47
+ let serviceLossSpawnGuard = null;
48
+ let esbuildServiceForeignReplacement = null;
26
49
  let activeOperationCount = 0;
27
50
  let activeOperationsIdle = Promise.resolve();
28
51
  let resolveActiveOperationsIdle = null;
52
+ let stopBarrierCount = 0;
53
+ let stopBarrierIdle = Promise.resolve();
54
+ let resolveStopBarrierIdle = null;
29
55
  const operationScopes = new AsyncLocalStorage();
30
56
  const OWNERSHIP_ERROR_MESSAGE = "[ext-bundler-esbuild] Cannot own an esbuild service started outside the module-wide adapter; restart the process and use only the Bundler contract";
31
57
  const MAX_CAUSE_DETAIL_LENGTH = 200;
32
58
  /** Absolute POSIX and Windows paths, reduced to a basename below. */
33
59
  const ABSOLUTE_PATH_PATTERN = /(?:[A-Za-z]:)?(?:\/|\\\\)[^\s"']*/g;
60
+ function readOwnDataProperty(value, key) {
61
+ if (value === null ||
62
+ (typeof value !== "object" && typeof value !== "function")) {
63
+ return undefined;
64
+ }
65
+ try {
66
+ const descriptor = ReflectGetOwnPropertyDescriptor(value, key);
67
+ if (descriptor !== undefined &&
68
+ ReflectApply(ObjectPrototypeHasOwnProperty, descriptor, ["value"]) === true) {
69
+ return descriptor.value;
70
+ }
71
+ }
72
+ catch {
73
+ // A hostile proxy cannot provide trusted diagnostic evidence.
74
+ }
75
+ return undefined;
76
+ }
77
+ /**
78
+ * Read esbuild's own diagnostic collection at the direct package boundary.
79
+ *
80
+ * esbuild exposes `errors` through an own accessor. Invoking that accessor is
81
+ * safe only here, before the failure crosses into framework classification.
82
+ */
83
+ function readTrustedEsbuildErrors(error) {
84
+ if (error === null || (typeof error !== "object" && typeof error !== "function")) {
85
+ return undefined;
86
+ }
87
+ try {
88
+ const descriptor = ReflectGetOwnPropertyDescriptor(error, "errors");
89
+ if (descriptor === undefined)
90
+ return undefined;
91
+ if (ReflectApply(ObjectPrototypeHasOwnProperty, descriptor, ["value"]) === true) {
92
+ return descriptor.value;
93
+ }
94
+ const getter = readOwnDataProperty(descriptor, "get");
95
+ return typeof getter === "function" ? ReflectApply(getter, error, []) : undefined;
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ function markEsbuildSourceDiagnostic(error) {
102
+ const diagnostics = readTrustedEsbuildErrors(error);
103
+ if (!ArrayIsArray(diagnostics))
104
+ return;
105
+ const length = readOwnDataProperty(diagnostics, "length");
106
+ if (typeof length !== "number")
107
+ return;
108
+ for (let index = 0; index < length; index++) {
109
+ const diagnostic = readOwnDataProperty(diagnostics, index);
110
+ const location = readOwnDataProperty(diagnostic, "location");
111
+ if (typeof location !== "object" || location === null)
112
+ continue;
113
+ try {
114
+ ObjectDefineProperty(error, ESBUILD_SOURCE_DIAGNOSTIC, { value: true });
115
+ }
116
+ catch {
117
+ // Non-extensible failures remain unmarked and classify as infrastructure.
118
+ }
119
+ return;
120
+ }
121
+ }
34
122
  /**
35
123
  * Reduce a cause to a single redacted line.
36
124
  *
@@ -99,10 +187,28 @@ function endOperation() {
99
187
  activeOperationsIdle = Promise.resolve();
100
188
  resolve?.();
101
189
  }
190
+ function enterStopBarrier() {
191
+ if (stopBarrierCount === 0) {
192
+ stopBarrierIdle = new Promise((resolve) => {
193
+ resolveStopBarrierIdle = resolve;
194
+ });
195
+ }
196
+ stopBarrierCount += 1;
197
+ }
198
+ function leaveStopBarrier() {
199
+ stopBarrierCount -= 1;
200
+ if (stopBarrierCount !== 0)
201
+ return;
202
+ const resolve = resolveStopBarrierIdle;
203
+ resolveStopBarrierIdle = null;
204
+ stopBarrierIdle = Promise.resolve();
205
+ resolve?.();
206
+ }
102
207
  function createPluginDisposalBarrier(scope) {
103
208
  const callbacks = [];
104
209
  let activated = false;
105
210
  let holdingOperation = false;
211
+ let disposalError = null;
106
212
  const releaseIfSettled = () => {
107
213
  if (!holdingOperation || callbacks.some((callback) => !callback.settled))
108
214
  return;
@@ -114,36 +220,52 @@ function createPluginDisposalBarrier(scope) {
114
220
  if (callback.settled)
115
221
  return;
116
222
  callback.settled = true;
223
+ callback.resolveSettled();
117
224
  releaseIfSettled();
118
225
  };
119
226
  const fail = (callback, error) => {
120
- if (!pluginDisposalError) {
121
- pluginDisposalError = new Error("[ext-bundler-esbuild] Plugin disposal failed", { cause: error });
227
+ if (!disposalError) {
228
+ disposalError = new Error("[ext-bundler-esbuild] Plugin disposal failed", { cause: error });
122
229
  }
230
+ if (!pluginDisposalError)
231
+ pluginDisposalError = disposalError;
123
232
  settle(callback);
124
233
  };
234
+ const start = (state) => {
235
+ if (state.started || state.settled)
236
+ return;
237
+ state.started = true;
238
+ try {
239
+ const result = state.callback();
240
+ if (result !== null &&
241
+ (typeof result === "object" || typeof result === "function") &&
242
+ typeof result.then === "function") {
243
+ void Promise.resolve(result).then(() => settle(state), (error) => fail(state, error));
244
+ }
245
+ else {
246
+ settle(state);
247
+ }
248
+ }
249
+ catch (error) {
250
+ fail(state, error);
251
+ }
252
+ };
125
253
  return {
126
254
  wrap(callback) {
127
- const state = { started: false, settled: false };
255
+ let resolveSettled = () => { };
256
+ const settledPromise = new Promise((resolve) => {
257
+ resolveSettled = resolve;
258
+ });
259
+ const state = {
260
+ callback,
261
+ started: false,
262
+ settled: false,
263
+ settledPromise,
264
+ resolveSettled,
265
+ };
128
266
  callbacks.push(state);
129
267
  return () => {
130
- if (state.settled)
131
- return;
132
- state.started = true;
133
- try {
134
- const result = callback();
135
- if (result !== null &&
136
- (typeof result === "object" || typeof result === "function") &&
137
- typeof result.then === "function") {
138
- void Promise.resolve(result).then(() => settle(state), (error) => fail(state, error));
139
- }
140
- else {
141
- settle(state);
142
- }
143
- }
144
- catch (error) {
145
- fail(state, error);
146
- }
268
+ start(state);
147
269
  };
148
270
  },
149
271
  activate() {
@@ -167,8 +289,62 @@ function createPluginDisposalBarrier(scope) {
167
289
  releaseIfSettled();
168
290
  }, 0);
169
291
  },
292
+ async dispose() {
293
+ const pending = callbacks.filter((callback) => !callback.settled);
294
+ for (const callback of pending)
295
+ start(callback);
296
+ await Promise.all(pending.map((callback) => callback.settledPromise));
297
+ if (disposalError)
298
+ throw disposalError;
299
+ },
170
300
  };
171
301
  }
302
+ /**
303
+ * Recover after the managed service child died unexpectedly (crash, OOM-kill).
304
+ *
305
+ * esbuild 0.28 keeps a dead service cached in its module state and rejects
306
+ * every later call, so the reset must go through its `stop()`, which clears
307
+ * that state and lets the next API call spawn a fresh child. This path only
308
+ * runs for a child the adapter itself captured; a service started outside the
309
+ * adapter is still latched permanently by {@link invokeEsbuild}. Recovery is
310
+ * single-flight and waits for in-flight operations to drain: they fail with
311
+ * esbuild's own error for the dead child and must not race the reset.
312
+ */
313
+ function recoverLostService() {
314
+ esbuildServiceRecovery ??= (async () => {
315
+ await activeOperationsIdle;
316
+ if (!esbuildServiceLost)
317
+ return;
318
+ const foreignService = esbuildServiceForeignReplacement ??
319
+ serviceLossSpawnGuard?.foreignService;
320
+ if (foreignService) {
321
+ const error = recordOwnershipError(new Error("esbuild service was replaced outside the module-wide adapter"));
322
+ uninstallServiceLossSpawnGuard();
323
+ throw error;
324
+ }
325
+ uninstallServiceLossSpawnGuard();
326
+ if (remainingServiceRestarts <= 0) {
327
+ throw recordOwnershipError(new Error(`esbuild service exited unexpectedly ${MAX_SERVICE_RESTARTS + 1} times (last: ${esbuildServiceLostDetail})`));
328
+ }
329
+ remainingServiceRestarts -= 1;
330
+ const detail = esbuildServiceLostDetail;
331
+ const m = esbuildModule;
332
+ esbuildModule = null;
333
+ esbuildService = null;
334
+ esbuildServiceLost = false;
335
+ try {
336
+ await m?.stop();
337
+ }
338
+ catch {
339
+ // Best effort: the child is already gone; stop() only resets state.
340
+ }
341
+ esbuildServiceGeneration += 1;
342
+ console.warn(`[ext-bundler-esbuild] esbuild service exited unexpectedly (${detail}); restarting it (${remainingServiceRestarts} restart(s) left)`);
343
+ })().finally(() => {
344
+ esbuildServiceRecovery = null;
345
+ });
346
+ return esbuildServiceRecovery;
347
+ }
172
348
  async function runBundlerOperation(operation, preferredScope) {
173
349
  if (esbuildOwnershipError)
174
350
  throw esbuildOwnershipError;
@@ -176,22 +352,39 @@ async function runBundlerOperation(operation, preferredScope) {
176
352
  throw esbuildShutdownError;
177
353
  const inheritedScope = operationScopes.getStore();
178
354
  const isReentrant = inheritedScope !== undefined && inheritedScope.activeCount > 0;
355
+ let stopBarrierEntered = false;
179
356
  if (!isReentrant) {
180
357
  while (esbuildStopPromise)
181
358
  await esbuildStopPromise;
359
+ enterStopBarrier();
360
+ stopBarrierEntered = true;
182
361
  }
183
- // Admission is synchronous after the stop barrier check. This makes a stop
184
- // exclusive without serializing independent operations. Work re-entered by
185
- // an active plugin shares its live scope so shutdown cannot deadlock on it.
186
- const scope = preferredScope ?? (isReentrant ? inheritedScope : { activeCount: 0 });
187
- beginOperation();
188
- scope.activeCount += 1;
189
362
  try {
190
- return await operationScopes.run(scope, () => operation(scope));
363
+ if (!isReentrant) {
364
+ // A lost service is recovered before admission, so one child death costs
365
+ // one restart instead of poisoning every later operation. The operation
366
+ // already holds the stop barrier, so shutdown cannot complete in this
367
+ // recovery gap and then leave the operation to spawn a new service.
368
+ while (esbuildServiceLost)
369
+ await recoverLostService();
370
+ }
371
+ // Admission is synchronous after the stop barrier check. This makes a stop
372
+ // exclusive without serializing independent operations. Work re-entered by
373
+ // an active plugin shares its live scope so shutdown cannot deadlock on it.
374
+ const scope = preferredScope ?? (isReentrant ? inheritedScope : { activeCount: 0 });
375
+ beginOperation();
376
+ scope.activeCount += 1;
377
+ try {
378
+ return await operationScopes.run(scope, () => operation(scope));
379
+ }
380
+ finally {
381
+ scope.activeCount -= 1;
382
+ endOperation();
383
+ }
191
384
  }
192
385
  finally {
193
- scope.activeCount -= 1;
194
- endOperation();
386
+ if (stopBarrierEntered)
387
+ leaveStopBarrier();
195
388
  }
196
389
  }
197
390
  function isEsbuildServiceSpawn(spawnArgs) {
@@ -200,6 +393,66 @@ function isEsbuildServiceSpawn(spawnArgs) {
200
393
  args.some((arg) => typeof arg === "string" && arg.startsWith("--service=")) &&
201
394
  args.includes("--ping");
202
395
  }
396
+ function trackServiceChild(child) {
397
+ let resolveClosed = () => { };
398
+ const closed = new Promise((resolve) => {
399
+ resolveClosed = resolve;
400
+ });
401
+ const service = { child, closed, expectedClose: false };
402
+ const recordUnexpectedLoss = (exitCode, signalCode) => recordUnexpectedManagedServiceLoss(service, exitCode, signalCode);
403
+ child.once("exit", recordUnexpectedLoss);
404
+ child.once("close", (exitCode, signalCode) => {
405
+ recordUnexpectedLoss(exitCode, signalCode);
406
+ resolveClosed();
407
+ if (esbuildService === service)
408
+ esbuildService = null;
409
+ });
410
+ return service;
411
+ }
412
+ function recordUnexpectedManagedServiceLoss(service, exitCode, signalCode) {
413
+ // An unexpected close of a child the adapter owns is a crash, not a
414
+ // lifecycle violation; mark it recoverable instead of latching the
415
+ // permanent ownership error.
416
+ if (service.expectedClose || service.lossRecorded)
417
+ return;
418
+ service.lossRecorded = true;
419
+ esbuildServiceLost = true;
420
+ esbuildServiceLostDetail = `exit code ${exitCode}, signal ${signalCode}`;
421
+ installServiceLossSpawnGuard();
422
+ }
423
+ function observeForeignServiceChild(child) {
424
+ let resolveClosed = () => { };
425
+ const closed = new Promise((resolve) => {
426
+ resolveClosed = resolve;
427
+ });
428
+ const service = { child, closed, expectedClose: false };
429
+ child.once("close", () => {
430
+ resolveClosed();
431
+ });
432
+ return service;
433
+ }
434
+ function installServiceLossSpawnGuard() {
435
+ if (serviceLossSpawnGuard)
436
+ return;
437
+ const previous = childProcess.spawn;
438
+ const guard = ((...spawnArgs) => {
439
+ const child = Reflect.apply(previous, childProcess, spawnArgs);
440
+ if (isEsbuildServiceSpawn(spawnArgs)) {
441
+ const foreignService = observeForeignServiceChild(child);
442
+ esbuildServiceForeignReplacement = foreignService;
443
+ serviceLossSpawnGuard.foreignService = foreignService;
444
+ }
445
+ return child;
446
+ });
447
+ serviceLossSpawnGuard = { guard, previous, foreignService: null };
448
+ childProcess.spawn = guard;
449
+ }
450
+ function uninstallServiceLossSpawnGuard() {
451
+ const guard = serviceLossSpawnGuard;
452
+ serviceLossSpawnGuard = null;
453
+ if (guard && childProcess.spawn === guard.guard)
454
+ childProcess.spawn = guard.previous;
455
+ }
203
456
  /**
204
457
  * Keep lifecycle tracking tolerant of Node-compatible child-process shims.
205
458
  *
@@ -210,10 +463,24 @@ function isEsbuildServiceSpawn(spawnArgs) {
210
463
  export function __resetOwnershipErrorForTests() {
211
464
  esbuildOwnershipError = null;
212
465
  }
466
+ /** Crash-recovery state is module-wide; tests must reset it between cases. */
467
+ export function __resetServiceRecoveryForTests() {
468
+ esbuildOwnershipError = null;
469
+ esbuildServiceLost = false;
470
+ esbuildServiceLostDetail = "";
471
+ esbuildServiceForeignReplacement = null;
472
+ esbuildServiceGeneration = 0;
473
+ remainingServiceRestarts = MAX_SERVICE_RESTARTS;
474
+ uninstallServiceLossSpawnGuard();
475
+ }
213
476
  /** Exercise the latch without starting a real esbuild service. */
214
477
  export function __recordOwnershipErrorForTests(cause) {
215
478
  return recordOwnershipError(cause);
216
479
  }
480
+ /** Exercise trusted esbuild diagnostic normalization without starting a service. */
481
+ export function __markEsbuildSourceDiagnosticForTests(error) {
482
+ markEsbuildSourceDiagnostic(error);
483
+ }
217
484
  export function isLiveEsbuildServiceProcess(child) {
218
485
  return !child.killed &&
219
486
  (child.exitCode === null || child.exitCode === undefined) &&
@@ -233,18 +500,7 @@ function invokeEsbuild(operation) {
233
500
  const trackedSpawn = ((...spawnArgs) => {
234
501
  const child = Reflect.apply(originalSpawn, childProcess, spawnArgs);
235
502
  if (isEsbuildServiceSpawn(spawnArgs)) {
236
- let resolveClosed = () => { };
237
- const closed = new Promise((resolve) => {
238
- resolveClosed = resolve;
239
- });
240
- const service = { child, closed, expectedClose: false };
241
- child.once("close", () => {
242
- if (!service.expectedClose)
243
- recordOwnershipError();
244
- resolveClosed();
245
- if (esbuildService === service)
246
- esbuildService = null;
247
- });
503
+ const service = trackServiceChild(child);
248
504
  capturedService = service;
249
505
  esbuildService = service;
250
506
  if (childProcess.spawn === trackedSpawn)
@@ -262,6 +518,16 @@ function invokeEsbuild(operation) {
262
518
  }
263
519
  const ownedService = capturedService ?? esbuildService;
264
520
  if (!ownedService || !isLiveService(ownedService)) {
521
+ // A managed child that died out from under this operation is the
522
+ // recoverable crash case handled by runBundlerOperation, not foreign
523
+ // ownership; the operation surfaces esbuild's own error for the dead
524
+ // child instead of latching the permanent one.
525
+ if (esbuildServiceLost)
526
+ return result;
527
+ if (ownedService && ownedService === esbuildService) {
528
+ recordUnexpectedManagedServiceLoss(ownedService, ownedService.child.exitCode, ownedService.child.signalCode);
529
+ return result;
530
+ }
265
531
  // Latch synchronously so a concurrent operation cannot pass the admission
266
532
  // check in runBundlerOperation and drive esbuild while ownership is already
267
533
  // known to be invalid. The rejection handler still supplies the cause: the
@@ -326,8 +592,18 @@ function mapOptions(options, scope) {
326
592
  return {
327
593
  options: mapped,
328
594
  activatePluginDisposals: pluginDisposals.activate,
595
+ disposePluginGeneration: pluginDisposals.dispose,
329
596
  };
330
597
  }
598
+ async function finalizePluginDisposals(mapped) {
599
+ const service = esbuildService;
600
+ if (esbuildServiceLost ||
601
+ (service && !service.expectedClose && !isLiveService(service))) {
602
+ await mapped.disposePluginGeneration();
603
+ return;
604
+ }
605
+ mapped.activatePluginDisposals();
606
+ }
331
607
  /**
332
608
  * esbuild-backed {@link Bundler} implementation.
333
609
  *
@@ -365,7 +641,7 @@ export class EsbuildBundler {
365
641
  };
366
642
  }
367
643
  finally {
368
- mapped.activatePluginDisposals();
644
+ await finalizePluginDisposals(mapped);
369
645
  }
370
646
  });
371
647
  }
@@ -373,7 +649,10 @@ export class EsbuildBundler {
373
649
  return runBundlerOperation(async () => {
374
650
  const esbuild = await getEsbuild();
375
651
  const { code, ...rest } = options;
376
- const result = await invokeEsbuild(() => esbuild.transform(code, rest));
652
+ const result = await invokeEsbuild(() => esbuild.transform(code, rest)).catch((error) => {
653
+ markEsbuildSourceDiagnostic(error);
654
+ throw error;
655
+ });
377
656
  return {
378
657
  code: result.code,
379
658
  map: result.map,
@@ -384,14 +663,38 @@ export class EsbuildBundler {
384
663
  async context(options) {
385
664
  return runBundlerOperation(async (contextScope) => {
386
665
  const esbuild = await getEsbuild();
387
- const mapped = mapOptions(options, contextScope);
388
- const ctx = await invokeEsbuild(() => esbuild.context(mapped.options)).catch((error) => {
389
- mapped.activatePluginDisposals();
666
+ let mapped = mapOptions(options, contextScope);
667
+ let ctx = await invokeEsbuild(() => esbuild.context(mapped.options)).catch(async (error) => {
668
+ await finalizePluginDisposals(mapped);
390
669
  throw error;
391
670
  });
671
+ let contextGeneration = esbuildServiceGeneration;
672
+ let contextRefresh = null;
673
+ const currentContext = async () => {
674
+ if (contextGeneration === esbuildServiceGeneration)
675
+ return ctx;
676
+ contextRefresh ??= (async () => {
677
+ if (contextGeneration === esbuildServiceGeneration)
678
+ return;
679
+ await mapped.disposePluginGeneration();
680
+ const nextMapped = mapOptions(options, contextScope);
681
+ const currentEsbuild = await getEsbuild();
682
+ ctx = await invokeEsbuild(() => currentEsbuild.context(nextMapped.options)).catch(async (error) => {
683
+ await finalizePluginDisposals(nextMapped);
684
+ mapped = mapOptions(options, contextScope);
685
+ throw error;
686
+ });
687
+ mapped = nextMapped;
688
+ contextGeneration = esbuildServiceGeneration;
689
+ })().finally(() => {
690
+ contextRefresh = null;
691
+ });
692
+ await contextRefresh;
693
+ return ctx;
694
+ };
392
695
  return {
393
696
  rebuild: () => runBundlerOperation(async () => {
394
- const result = await ctx.rebuild();
697
+ const result = await (await currentContext()).rebuild();
395
698
  return {
396
699
  outputFiles: (result.outputFiles ?? []).map(toOutput),
397
700
  warnings: toMessages(result.warnings),
@@ -400,14 +703,20 @@ export class EsbuildBundler {
400
703
  };
401
704
  }, contextScope),
402
705
  cancel: () => runBundlerOperation(async () => {
403
- await ctx.cancel();
706
+ await (await currentContext()).cancel();
404
707
  }, contextScope),
405
708
  dispose: () => runBundlerOperation(async () => {
406
709
  try {
407
- await ctx.dispose();
710
+ if (contextGeneration === esbuildServiceGeneration)
711
+ await ctx.dispose();
408
712
  }
409
713
  finally {
410
- mapped.activatePluginDisposals();
714
+ if (contextGeneration === esbuildServiceGeneration) {
715
+ mapped.activatePluginDisposals();
716
+ }
717
+ else {
718
+ await mapped.disposePluginGeneration();
719
+ }
411
720
  }
412
721
  }, contextScope),
413
722
  };
@@ -422,10 +731,31 @@ export class EsbuildBundler {
422
731
  return;
423
732
  }
424
733
  const stopping = (async () => {
734
+ await stopBarrierIdle;
425
735
  await activeOperationsIdle;
736
+ if (esbuildServiceLost &&
737
+ (esbuildServiceForeignReplacement ?? serviceLossSpawnGuard?.foreignService)) {
738
+ const error = recordOwnershipError(new Error("esbuild service was replaced outside the module-wide adapter"));
739
+ uninstallServiceLossSpawnGuard();
740
+ throw error;
741
+ }
742
+ if (esbuildServiceLost && remainingServiceRestarts <= 0) {
743
+ const error = recordOwnershipError(new Error(`esbuild service exited unexpectedly ${MAX_SERVICE_RESTARTS + 1} times (last: ${esbuildServiceLostDetail})`));
744
+ uninstallServiceLossSpawnGuard();
745
+ throw error;
746
+ }
747
+ let chargedLostService = false;
748
+ if (esbuildServiceLost) {
749
+ remainingServiceRestarts -= 1;
750
+ chargedLostService = true;
751
+ uninstallServiceLossSpawnGuard();
752
+ }
426
753
  const m = esbuildModule;
427
754
  const trackedService = esbuildService;
428
- if (trackedService && !trackedService.expectedClose && !isLiveService(trackedService)) {
755
+ if (trackedService && !trackedService.expectedClose && !isLiveService(trackedService) &&
756
+ remainingServiceRestarts <= 0 && !chargedLostService) {
757
+ // A dead managed child within the restart budget is a crash, which a
758
+ // stop resets anyway; only an exhausted budget still means giving up.
429
759
  recordOwnershipError();
430
760
  }
431
761
  const ownershipError = esbuildOwnershipError;
@@ -477,6 +807,9 @@ export class EsbuildBundler {
477
807
  if (esbuildService === service)
478
808
  esbuildService = null;
479
809
  esbuildShutdownError = null;
810
+ // A clean stop resets esbuild's module state, so a pending crash needs
811
+ // no recovery pass anymore.
812
+ esbuildServiceLost = false;
480
813
  if (disposalError) {
481
814
  if (pluginDisposalError === disposalError)
482
815
  pluginDisposalError = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veryfront/ext-bundler-esbuild",
3
- "version": "0.1.1237",
3
+ "version": "0.1.1239",
4
4
  "description": "Veryfront first-party extension package for ext-bundler-esbuild",
5
5
  "keywords": [
6
6
  "veryfront",
@@ -50,7 +50,7 @@
50
50
  "esbuild": "0.28.1"
51
51
  },
52
52
  "peerDependencies": {
53
- "veryfront": "^0.1.1237"
53
+ "veryfront": "^0.1.1239"
54
54
  },
55
55
  "type": "module",
56
56
  "types": "./esm/index.d.ts",