Skip to content

Commit eb04da8

Browse files
committed
feat(utils): integrate logger with executeProcess
1 parent 84caa0d commit eb04da8

File tree

9 files changed

+154
-181
lines changed

9 files changed

+154
-181
lines changed

packages/utils/mocks/logger-demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ try {
4444
async () => {
4545
await sleep(3000);
4646
if (errorStage === 'plugin') {
47-
logger.info('Configuration file not found.');
47+
logger.debug('Configuration file not found.', { force: true });
4848
throw new Error(`Command ${ansis.bold(bin)} exited with code 1`);
4949
}
5050
logger.debug('All files pass linting.');

packages/utils/src/lib/execute-process.unit.test.ts renamed to packages/utils/src/lib/execute-process.int.test.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ChildProcess } from 'node:child_process';
22
import { describe, expect, it, vi } from 'vitest';
33
import { getAsyncProcessRunnerConfig } from '@code-pushup/test-utils';
44
import { type ProcessObserver, executeProcess } from './execute-process.js';
5+
import { logger } from './logger.js';
56

67
describe('executeProcess', () => {
78
const spyObserver: ProcessObserver = {
@@ -12,13 +13,9 @@ describe('executeProcess', () => {
1213
};
1314
const errorSpy = vi.fn();
1415

15-
beforeEach(() => {
16-
vi.clearAllMocks();
17-
});
18-
1916
it('should work with node command `node -v`', async () => {
2017
const processResult = await executeProcess({
21-
command: `node`,
18+
command: 'node',
2219
args: ['-v'],
2320
observer: spyObserver,
2421
});
@@ -32,7 +29,7 @@ describe('executeProcess', () => {
3229

3330
it('should work with npx command `npx --help`', async () => {
3431
const processResult = await executeProcess({
35-
command: `npx`,
32+
command: 'npx',
3633
args: ['--help'],
3734
observer: spyObserver,
3835
});
@@ -101,4 +98,42 @@ describe('executeProcess', () => {
10198
expect(spyObserver.onError).not.toHaveBeenCalled();
10299
expect(spyObserver.onComplete).toHaveBeenCalledOnce();
103100
});
101+
102+
it('should show spinner with serialized command and args', async () => {
103+
await executeProcess({ command: 'echo', args: ['hello'] });
104+
expect(logger.command).toHaveBeenCalledWith(
105+
'echo hello',
106+
expect.any(Function),
107+
);
108+
});
109+
110+
it('should log stdout and stderr if verbose', async () => {
111+
await executeProcess(
112+
getAsyncProcessRunnerConfig({ interval: 10, runs: 2, throwError: false }),
113+
);
114+
expect(logger.debug).toHaveBeenCalledWith(
115+
`
116+
process:start with interval: 10, runs: 2, throwError: false
117+
process:update
118+
process:update
119+
process:complete
120+
`.trimStart(),
121+
);
122+
});
123+
124+
it('should log stdout and stderr if process failed', async () => {
125+
await expect(
126+
executeProcess(
127+
getAsyncProcessRunnerConfig({
128+
interval: 10,
129+
runs: 1,
130+
throwError: true,
131+
}),
132+
),
133+
).rejects.toThrow();
134+
expect(logger.debug).toHaveBeenCalledWith(
135+
expect.stringMatching(/process:start.*Error: dummy-error/s),
136+
{ force: true },
137+
);
138+
});
104139
});
Lines changed: 79 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ansis from 'ansis';
12
import {
23
type ChildProcess,
34
type ChildProcessByStdio,
@@ -6,38 +7,33 @@ import {
67
spawn,
78
} from 'node:child_process';
89
import type { Readable, Writable } from 'node:stream';
9-
import { isVerbose } from './env.js';
10-
import { formatCommandLog } from './format-command-log.js';
11-
import { ui } from './logging.js';
10+
import { logger } from './logger.js';
1211

1312
/**
1413
* Represents the process result.
15-
* @category Types
16-
* @public
17-
* @property {number | null} code - The exit code of the process.
18-
* @property {string} stdout - The stdout of the process.
19-
* @property {string} stderr - The stderr of the process.
2014
*/
2115
export type ProcessResult = {
16+
/** The full command with args that was executed. */
17+
bin: string;
18+
/** The exit code of the process (`null` if terminated by signal). */
2219
code: number | null;
20+
/** The signal which terminated the process, if any. */
21+
signal: NodeJS.Signals | null;
22+
/** The standard output from the process. */
2323
stdout: string;
24+
/** The standard error from the process. */
2425
stderr: string;
2526
};
2627

2728
/**
2829
* Error class for process errors.
2930
* Contains additional information about the process result.
30-
* @category Error
31-
* @public
32-
* @class
33-
* @extends Error
3431
* @example
35-
* const result = await executeProcess({})
36-
* .catch((error) => {
32+
* const result = await executeProcess({}).catch((error) => {
3733
* if (error instanceof ProcessError) {
38-
* console.error(error.code);
39-
* console.error(error.stderr);
40-
* console.error(error.stdout);
34+
* console.error(error.code);
35+
* console.error(error.stderr);
36+
* console.error(error.stdout);
4137
* }
4238
* });
4339
*
@@ -48,7 +44,10 @@ export class ProcessError extends Error {
4844
stdout: string;
4945

5046
constructor(result: ProcessResult) {
51-
super(result.stderr);
47+
const message = result.signal
48+
? `Process ${ansis.bold(result.bin)} terminated by ${result.signal}`
49+
: `Process ${ansis.bold(result.bin)} failed with exit code ${result.code}`;
50+
super(message);
5251
this.code = result.code;
5352
this.stderr = result.stderr;
5453
this.stdout = result.stdout;
@@ -57,9 +56,7 @@ export class ProcessError extends Error {
5756

5857
/**
5958
* Process config object. Contains the command, args and observer.
60-
* @param cfg - process config object with command, args and observer (optional)
61-
* @category Types
62-
* @public
59+
* @param cfg Process config object with command, args and observer (optional)
6360
* @property {string} command - The command to execute.
6461
* @property {string[]} args - The arguments for the command.
6562
* @property {ProcessObserver} observer - The observer for the process.
@@ -74,15 +71,15 @@ export class ProcessError extends Error {
7471
*
7572
* // node command
7673
* const cfg = {
77-
* command: 'node',
78-
* args: ['--version']
74+
* command: 'node',
75+
* args: ['--version']
7976
* };
8077
*
8178
* // npx command
8279
* const cfg = {
83-
* command: 'npx',
84-
* args: ['--version']
85-
*
80+
* command: 'npx',
81+
* args: ['--version']
82+
* };
8683
*/
8784
export type ProcessConfig = Omit<
8885
SpawnOptionsWithStdioTuple<StdioPipe, StdioPipe, StdioPipe>,
@@ -95,22 +92,21 @@ export type ProcessConfig = Omit<
9592
};
9693

9794
/**
98-
* Process observer object. Contains the onStdout, error and complete function.
99-
* @category Types
100-
* @public
101-
* @property {function} onStdout - The onStdout function of the observer (optional).
102-
* @property {function} onError - The error function of the observer (optional).
103-
* @property {function} onComplete - The complete function of the observer (optional).
95+
* Process observer object.
10496
*
10597
* @example
10698
* const observer = {
107-
* onStdout: (stdout) => console.info(stdout)
108-
* }
99+
* onStdout: (stdout) => console.info(stdout)
100+
* }
109101
*/
110102
export type ProcessObserver = {
103+
/** Called when the `stdout` stream receives new data (optional). */
111104
onStdout?: (stdout: string, sourceProcess?: ChildProcess) => void;
105+
/** Called when the `stdout` stream receives new data (optional). */
112106
onStderr?: (stderr: string, sourceProcess?: ChildProcess) => void;
107+
/** Called when the process ends in an error (optional). */
113108
onError?: (error: ProcessError) => void;
109+
/** Called when the process ends successfully (optional). */
114110
onComplete?: () => void;
115111
};
116112

@@ -146,48 +142,59 @@ export function executeProcess(cfg: ProcessConfig): Promise<ProcessResult> {
146142
const { command, args, observer, ignoreExitCode = false, ...options } = cfg;
147143
const { onStdout, onStderr, onError, onComplete } = observer ?? {};
148144

149-
if (isVerbose()) {
150-
ui().logger.log(
151-
formatCommandLog(command, args, `${cfg.cwd ?? process.cwd()}`),
152-
);
153-
}
145+
const bin = [command, ...(args ?? [])].join(' ');
154146

155-
return new Promise((resolve, reject) => {
156-
// shell:true tells Windows to use shell command for spawning a child process
157-
const spawnedProcess = spawn(command, args ?? [], {
158-
shell: true,
159-
windowsHide: true,
160-
...options,
161-
}) as ChildProcessByStdio<Writable, Readable, Readable>;
147+
return logger.command(
148+
bin,
149+
() =>
150+
new Promise((resolve, reject) => {
151+
const spawnedProcess = spawn(command, args ?? [], {
152+
// shell:true tells Windows to use shell command for spawning a child process
153+
// https://stackoverflow.com/questions/60386867/node-spawn-child-process-not-working-in-windows
154+
shell: true,
155+
windowsHide: true,
156+
...options,
157+
}) as ChildProcessByStdio<Writable, Readable, Readable>;
162158

163-
// eslint-disable-next-line functional/no-let
164-
let stdout = '';
165-
// eslint-disable-next-line functional/no-let
166-
let stderr = '';
159+
// eslint-disable-next-line functional/no-let
160+
let stdout = '';
161+
// eslint-disable-next-line functional/no-let
162+
let stderr = '';
163+
// eslint-disable-next-line functional/no-let
164+
let output = ''; // interleaved stdout and stderr
167165

168-
spawnedProcess.stdout.on('data', data => {
169-
stdout += String(data);
170-
onStdout?.(String(data), spawnedProcess);
171-
});
166+
spawnedProcess.stdout.on('data', (data: unknown) => {
167+
const message = String(data);
168+
stdout += message;
169+
output += message;
170+
onStdout?.(message, spawnedProcess);
171+
});
172172

173-
spawnedProcess.stderr.on('data', data => {
174-
stderr += String(data);
175-
onStderr?.(String(data), spawnedProcess);
176-
});
173+
spawnedProcess.stderr.on('data', (data: unknown) => {
174+
const message = String(data);
175+
stderr += message;
176+
output += message;
177+
onStderr?.(message, spawnedProcess);
178+
});
177179

178-
spawnedProcess.on('error', err => {
179-
stderr += err.toString();
180-
});
180+
spawnedProcess.on('error', error => {
181+
reject(error);
182+
});
181183

182-
spawnedProcess.on('close', code => {
183-
if (code === 0 || ignoreExitCode) {
184-
onComplete?.();
185-
resolve({ code, stdout, stderr });
186-
} else {
187-
const errorMsg = new ProcessError({ code, stdout, stderr });
188-
onError?.(errorMsg);
189-
reject(errorMsg);
190-
}
191-
});
192-
});
184+
spawnedProcess.on('close', (code, signal) => {
185+
const result: ProcessResult = { bin, code, signal, stdout, stderr };
186+
if (code === 0 || ignoreExitCode) {
187+
logger.debug(output);
188+
onComplete?.();
189+
resolve(result);
190+
} else {
191+
// ensure stdout and stderr are logged to help debug failure
192+
logger.debug(output, { force: true });
193+
const error = new ProcessError(result);
194+
onError?.(error);
195+
reject(error);
196+
}
197+
});
198+
}),
199+
);
193200
}

packages/utils/src/lib/format-command-log.int.test.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)