Skip to content

Commit d12a63f

Browse files
committed
fix(cli): use captured module refs in exit handlers instead of dead import()
1 parent 92ab3bf commit d12a63f

1 file changed

Lines changed: 42 additions & 48 deletions

File tree

packages/cli/src/cli.ts

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,26 @@ const hasJsonFlag = process.argv.includes("--json");
148148
// exit handler is synchronous-only).
149149
let _flush: (() => Promise<void>) | undefined;
150150
let _flushSync: (() => void) | undefined;
151+
let _trackCliError:
152+
| ((props: {
153+
error_name: string;
154+
error_message: string;
155+
stack_trace?: string;
156+
command?: string;
157+
kind: "uncaught_exception" | "unhandled_rejection" | "command_error";
158+
}) => void)
159+
| undefined;
160+
let _trackCommandResult:
161+
| ((props: { command: string; success: boolean; exitCode: number; durationMs: number }) => void)
162+
| undefined;
151163
let _printUpdateNotice: (() => void) | undefined;
152164

153165
if (!isHelp && command !== "telemetry" && command !== "unknown") {
154166
import("./telemetry/index.js").then((mod) => {
155167
_flush = mod.flush;
156168
_flushSync = mod.flushSync;
169+
_trackCliError = mod.trackCliError;
170+
_trackCommandResult = mod.trackCommandResult;
157171
mod.showTelemetryNotice();
158172
mod.trackCommand(command);
159173
if (mod.shouldTrack()) mod.incrementCommandCount();
@@ -176,6 +190,7 @@ if (!isHelp && !hasJsonFlag && command !== "upgrade") {
176190
});
177191
}
178192

193+
const commandStart = Date.now();
179194
let commandFailed = false;
180195

181196
// Async flush for normal exit (beforeExit fires when the event loop drains)
@@ -184,65 +199,45 @@ process.on("beforeExit", () => {
184199
if (!hasJsonFlag) _printUpdateNotice?.();
185200
});
186201

187-
// Sync flush for process.exit() calls (exit event only allows synchronous code).
188-
// Also emits cli_command_result with the real exit code.
202+
// Sync-only: exit handlers cannot await promises or drain microtasks.
203+
// _trackCommandResult / _trackCliError are captured references resolved
204+
// at init time, so they're callable synchronously here.
189205
process.on("exit", (code) => {
190-
try {
191-
import("./telemetry/index.js")
192-
.then((mod) => {
193-
mod.trackCommandResult({
194-
command,
195-
success: code === 0 && !commandFailed,
196-
exitCode: code,
197-
durationMs: Date.now() - commandStart,
198-
});
199-
})
200-
.catch(() => {});
201-
} catch {
202-
// telemetry must never block exit
203-
}
206+
_trackCommandResult?.({
207+
command,
208+
success: code === 0 && !commandFailed,
209+
exitCode: code,
210+
durationMs: Date.now() - commandStart,
211+
});
204212
_flushSync?.();
205213
});
206214

207215
process.on("uncaughtException", (error) => {
208216
commandFailed = true;
209-
try {
210-
import("./telemetry/index.js")
211-
.then((mod) => {
212-
mod.trackCliError({
213-
error_name: error.name,
214-
error_message: error.message,
215-
stack_trace: error.stack,
216-
command,
217-
kind: "uncaught_exception",
218-
});
219-
})
220-
.catch(() => {});
221-
} catch {
222-
// telemetry must never suppress the crash
223-
}
217+
_trackCliError?.({
218+
error_name: error.name,
219+
error_message: error.message,
220+
stack_trace: error.stack,
221+
command,
222+
kind: "uncaught_exception",
223+
});
224224
_flushSync?.();
225225
process.exit(1);
226226
});
227227

228+
// unhandledRejection does not call process.exit() — Node may continue
229+
// running if the rejection is non-fatal (e.g. a fire-and-forget promise).
230+
// The exit handler above will still fire with the real exit code.
228231
process.on("unhandledRejection", (reason) => {
229232
commandFailed = true;
230-
try {
231-
const error = reason instanceof Error ? reason : new Error(String(reason));
232-
import("./telemetry/index.js")
233-
.then((mod) => {
234-
mod.trackCliError({
235-
error_name: error.name,
236-
error_message: error.message,
237-
stack_trace: error.stack,
238-
command,
239-
kind: "unhandled_rejection",
240-
});
241-
})
242-
.catch(() => {});
243-
} catch {
244-
// telemetry must never suppress the crash
245-
}
233+
const error = reason instanceof Error ? reason : new Error(String(reason));
234+
_trackCliError?.({
235+
error_name: error.name,
236+
error_message: error.message,
237+
stack_trace: error.stack,
238+
command,
239+
kind: "unhandled_rejection",
240+
});
246241
});
247242

248243
// Lazy-load help renderer — avoids allocating help data on non-help invocations
@@ -254,5 +249,4 @@ async function showUsage<T extends ArgsDef>(
254249
return impl(cmd as CommandDef, parent as CommandDef | undefined);
255250
}
256251

257-
const commandStart = Date.now();
258252
runMain(main, { showUsage });

0 commit comments

Comments
 (0)