@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.
- package/esm/esbuild-bundler.d.ts +14 -0
- package/esm/esbuild-bundler.d.ts.map +1 -1
- package/esm/esbuild-bundler.js +384 -51
- package/package.json +2 -2
package/esm/esbuild-bundler.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/esm/esbuild-bundler.js
CHANGED
|
@@ -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 (!
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
mapped
|
|
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
|
|
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
|
|
706
|
+
await (await currentContext()).cancel();
|
|
404
707
|
}, contextScope),
|
|
405
708
|
dispose: () => runBundlerOperation(async () => {
|
|
406
709
|
try {
|
|
407
|
-
|
|
710
|
+
if (contextGeneration === esbuildServiceGeneration)
|
|
711
|
+
await ctx.dispose();
|
|
408
712
|
}
|
|
409
713
|
finally {
|
|
410
|
-
|
|
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.
|
|
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.
|
|
53
|
+
"veryfront": "^0.1.1239"
|
|
54
54
|
},
|
|
55
55
|
"type": "module",
|
|
56
56
|
"types": "./esm/index.d.ts",
|