@zimic/interceptor
Version:
Next-gen TypeScript-first HTTP intercepting and mocking
94 lines (74 loc) • 2.96 kB
text/typescript
import createCachedDynamicImport from '@zimic/utils/import/createCachedDynamicImport';
export const PROCESS_EXIT_EVENTS = Object.freeze([
'beforeExit',
'uncaughtExceptionMonitor',
'SIGINT',
'SIGTERM',
'SIGHUP',
'SIGBREAK',
] as const);
export type ProcessExitEvent = (typeof PROCESS_EXIT_EVENTS)[number];
// Having an undefined exit code means that the process will already exit with the default exit code.
export const PROCESS_EXIT_CODE_BY_EXIT_EVENT: Record<string, number | undefined> = {
beforeExit: undefined,
uncaughtExceptionMonitor: undefined,
SIGINT: 130,
SIGTERM: 143,
SIGHUP: 129,
SIGBREAK: 131,
} satisfies Record<ProcessExitEvent, number | undefined>;
export const importExeca = createCachedDynamicImport(() => import('execa'));
interface CommandErrorOptions {
command?: string[];
exitCode?: number;
signal?: NodeJS.Signals;
originalMessage?: string;
}
export class CommandError extends Error {
static readonly DEFAULT_EXIT_CODE = 1;
readonly command: string[];
readonly exitCode: number;
readonly signal?: NodeJS.Signals;
constructor(executable: string, options: CommandErrorOptions) {
const message = CommandError.createMessage(executable, options);
super(message);
this.name = 'CommandError';
this.command = options.command ?? [executable];
this.exitCode = this.getExitCode(options);
this.signal = options.signal;
}
private getExitCode(options: CommandErrorOptions): number {
const existingExitCode = options.exitCode;
const exitCodeInferredFromSignal =
options.signal === undefined ? undefined : PROCESS_EXIT_CODE_BY_EXIT_EVENT[options.signal];
return existingExitCode ?? exitCodeInferredFromSignal ?? CommandError.DEFAULT_EXIT_CODE;
}
private static createMessage(command: string, options: CommandErrorOptions) {
const suffix = options.originalMessage ? `: ${options.originalMessage}` : '';
if (options.exitCode === undefined && options.signal === undefined) {
return `Command '${command}' failed${suffix}`;
}
const prefix = `Command '${command}' exited `;
const infix = options.exitCode === undefined ? `after signal ${options.signal}` : `with code ${options.exitCode}`;
return `${prefix}${infix}${suffix}`;
}
}
export async function runCommand(commandEntry: string, commandArguments: string[]) {
const { execa: $, ExecaError } = await importExeca();
try {
await $(commandEntry, commandArguments, { stdio: 'inherit' });
} catch (error) {
/* istanbul ignore if -- @preserve
* This is a safeguard if the error is not an ExecaError. It is not expected to run. */
if (!(error instanceof ExecaError)) {
throw error;
}
const commandError = new CommandError(commandEntry, {
command: [commandEntry, ...commandArguments],
exitCode: error.exitCode,
signal: error.signal,
originalMessage: error.originalMessage,
});
throw commandError;
}
}