1+ import ansis from 'ansis' ;
12import {
23 type ChildProcess ,
34 type ChildProcessByStdio ,
@@ -6,38 +7,33 @@ import {
67 spawn ,
78} from 'node:child_process' ;
89import 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 */
2115export 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 */
8784export 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 */
110102export 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}
0 commit comments