Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class ExecProcess implements Result {
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this);
throw new NonZeroExitError(this, undefined, this._command, this._args);
}
}

Expand Down Expand Up @@ -268,7 +268,7 @@ export class ExecProcess implements Result {
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this, result);
throw new NonZeroExitError(this, result, this._command, this._args);
}

return result;
Expand Down Expand Up @@ -431,7 +431,16 @@ export function xSync(
};

if (opts.throwOnError && exitCode !== 0 && exitCode !== undefined) {
throw new NonZeroExitError(result, result);
throw new NonZeroExitError(
result,
{
Comment thread
43081j marked this conversation as resolved.
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
},
command,
args
);
}

return result;
Expand Down
36 changes: 28 additions & 8 deletions src/non-zero-exit-error.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import type {Output, CommonOutputApi} from './main.js';

export class NonZeroExitError extends Error {
public get exitCode(): number | undefined {
if (this.result.exitCode !== null) {
return this.result.exitCode;
}
return undefined;
}
public readonly exitCode: number;

public constructor(
public readonly result: CommonOutputApi,
public readonly output?: Output
public readonly output?: Output,
command?: string,
args?: readonly string[]
) {
super(`Process exited with non-zero status (${result.exitCode})`);
let target = 'The process';
if (command) {
const fullCommand = args?.length
? `${command} ${args.map((a) => (/[ "'`()]/.test(a) ? JSON.stringify(a) : a)).join(' ')}`
: command;
target = `The command \`${fullCommand}\``;
}

// This error is normally only created when the exit code is non-nullable
// and non-zero, so it must exist here. However, due to types compatibility,
// we default to 1 in case.
const exitCode = result.exitCode ?? 1;

super(`${target} exited with a non-zero status (${exitCode})`);
this.exitCode = exitCode;

// `result` is usually passed the entire instance of the exec process
// depending on the exec API so that handlers can interact with it fully.
// As such, its log can be very large so we hide it by making it non-enumerable.
Object.defineProperty(this, 'result', {
enumerable: false,
writable: false,
configurable: false
});
}
}
38 changes: 29 additions & 9 deletions src/test/main_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ describe.each(variants)('exec ($name)', ({x, isAsync}) => {
describe('exec (async)', () => {
test('non-zero exitCode throws when throwOnError=true', async () => {
const proc = x('node', ['-e', 'process.exit(1);'], {throwOnError: true});
await expect(async () => {
try {
await proc;
}).rejects.toThrow(NonZeroExitError);
expect.fail('Expected to throw');
} catch (err) {
expect.assert(err instanceof NonZeroExitError);
expect(err.exitCode).toBe(1);
expect(err.message).toBe(
'The command `node -e "process.exit(1);"` exited with a non-zero status (1)'
);
}
expect(proc.exitCode).toBe(1);
});

Expand All @@ -72,11 +79,18 @@ describe('exec (async)', () => {
throwOnError: true
});
const lines: string[] = [];
await expect(async () => {
try {
for await (const line of proc) {
lines.push(line);
}
}).rejects.toThrow(NonZeroExitError);
expect.fail('Expected to throw');
} catch (err) {
expect.assert(err instanceof NonZeroExitError);
expect(err.exitCode).toBe(1);
expect(err.message).toBe(
'The command `node -e "console.log(\'foo\'); process.exit(1);"` exited with a non-zero status (1)'
);
}
expect(lines).toEqual(['foo']);
expect(proc.exitCode).toBe(1);
});
Expand Down Expand Up @@ -136,9 +150,15 @@ describe('exec (async)', () => {

describe('exec (sync)', () => {
test('non-zero exitCode throws when throwOnError=true', () => {
expect(() => {
try {
xSync('node', ['-e', 'process.exit(1);'], {throwOnError: true});
}).toThrow(NonZeroExitError);
expect.fail('Expected to throw');
} catch (err) {
expect(err instanceof NonZeroExitError).ok;
expect((err as NonZeroExitError).message).toBe(
'The command `node -e "process.exit(1);"` exited with a non-zero status (1)'
);
}
});
});

Expand Down Expand Up @@ -171,12 +191,12 @@ if (isWindows) {
await proc;
expect.fail('Expected to throw');
} catch (err) {
expect(err instanceof NonZeroExitError).ok;
expect((err as NonZeroExitError).output?.stderr).toBe(
expect.assert(err instanceof NonZeroExitError);
expect(err.output?.stderr).toBe(
"'definitelyNonExistent' is not recognized as an internal" +
' or external command,\r\noperable program or batch file.\r\n'
);
expect((err as NonZeroExitError).output?.stdout).toBe('');
expect(err.output?.stdout).toBe('');
}
});

Expand Down