@@ -12,6 +12,8 @@ import {
1212 SpawnOptions as _SpawnOptions ,
1313 spawnSync as _spawnSync ,
1414 SpawnSyncOptions as _SpawnSyncOptions ,
15+ ExecOptions as _ExecOptions ,
16+ exec as _exec ,
1517} from 'child_process' ;
1618import { Log } from './logging.js' ;
1719
@@ -32,6 +34,14 @@ export interface SpawnOptions extends Omit<_SpawnOptions, 'shell' | 'stdio'> {
3234 input ?: string ;
3335}
3436
37+ /** Interface describing the options for exec-ing a process. */
38+ export interface ExecOptions extends Omit < _ExecOptions , 'shell' | 'stdio' > {
39+ /** Console output mode. Defaults to "enabled". */
40+ mode ?: 'enabled' | 'silent' | 'on-error' ;
41+ /** Whether to prevent exit codes being treated as failures. */
42+ suppressErrorOnFailingExitCode ?: boolean ;
43+ }
44+
3545/** Interface describing the options for spawning an interactive process. */
3646export type SpawnInteractiveCommandOptions = Omit < _SpawnOptions , 'shell' | 'stdio' > ;
3747
@@ -81,7 +91,7 @@ export abstract class ChildProcess {
8191 return new Promise ( ( resolve , reject ) => {
8292 const commandText = `${ command } ${ args . join ( ' ' ) } ` ;
8393 const outputMode = options . mode ;
84- const env = getEnvironmentForNonInteractiveSpawn ( options . env ) ;
94+ const env = getEnvironmentForNonInteractiveCommand ( options . env ) ;
8595
8696 Log . debug ( `Executing command: ${ commandText } ` ) ;
8797
@@ -148,7 +158,7 @@ export abstract class ChildProcess {
148158 */
149159 static spawnSync ( command : string , args : string [ ] , options : SpawnSyncOptions = { } ) : SpawnResult {
150160 const commandText = `${ command } ${ args . join ( ' ' ) } ` ;
151- const env = getEnvironmentForNonInteractiveSpawn ( options . env ) ;
161+ const env = getEnvironmentForNonInteractiveCommand ( options . env ) ;
152162
153163 Log . debug ( `Executing command: ${ commandText } ` ) ;
154164
@@ -168,6 +178,63 @@ export abstract class ChildProcess {
168178
169179 throw new Error ( stderr ) ;
170180 }
181+
182+ static exec ( command : string , options : ExecOptions = { } ) {
183+ return new Promise ( ( resolve , reject ) => {
184+ const outputMode = options . mode ;
185+ const env = getEnvironmentForNonInteractiveCommand ( options . env ) ;
186+
187+ Log . debug ( `Executing command: ${ command } ` ) ;
188+
189+ const childProcess = _exec ( command , { ...options , env} ) ;
190+ let logOutput = '' ;
191+ let stdout = '' ;
192+ let stderr = '' ;
193+
194+ // Capture the stdout separately so that it can be passed as resolve value.
195+ // This is useful if commands return parsable stdout.
196+ childProcess . stderr ?. on ( 'data' , ( message ) => {
197+ stderr += message ;
198+ logOutput += message ;
199+ // If console output is enabled, print the message directly to the stderr. Note that
200+ // we intentionally print all output to stderr as stdout should not be polluted.
201+ if ( outputMode === undefined || outputMode === 'enabled' ) {
202+ process . stderr . write ( message ) ;
203+ }
204+ } ) ;
205+
206+ childProcess . stdout ?. on ( 'data' , ( message ) => {
207+ stdout += message ;
208+ logOutput += message ;
209+ // If console output is enabled, print the message directly to the stderr. Note that
210+ // we intentionally print all output to stderr as stdout should not be polluted.
211+ if ( outputMode === undefined || outputMode === 'enabled' ) {
212+ process . stderr . write ( message ) ;
213+ }
214+ } ) ;
215+
216+ // The `close` event is used because the process is guaranteed to have completed writing to
217+ // stdout and stderr, using the `exit` event can cause inconsistent information in stdout and
218+ // stderr due to a race condition around exiting.
219+ childProcess . on ( 'close' , ( exitCode , signal ) => {
220+ const exitDescription =
221+ exitCode !== null ? `exit code "${ exitCode } "` : `signal "${ signal } "` ;
222+ const printFn = outputMode === 'on-error' ? Log . error : Log . debug ;
223+ const status = statusFromExitCodeAndSignal ( exitCode , signal ) ;
224+
225+ printFn ( `Command "${ command } " completed with ${ exitDescription } .` ) ;
226+ printFn ( `Process output: \n${ logOutput } ` ) ;
227+
228+ // On success, resolve the promise. Otherwise reject with the captured stderr
229+ // and stdout log output if the output mode was set to `silent`.
230+ if ( status === 0 || options . suppressErrorOnFailingExitCode ) {
231+ resolve ( { stdout, stderr, status} ) ;
232+ } else {
233+ reject ( outputMode === 'silent' ? logOutput : undefined ) ;
234+ }
235+ } ) ;
236+ } ) ;
237+ }
171238}
172239/**
173240 * Convert the provided exitCode and signal to a single status code.
@@ -188,7 +255,7 @@ function statusFromExitCodeAndSignal(exitCode: number | null, signal: NodeJS.Sig
188255 * Currently we enable `FORCE_COLOR` since non-interactive spawn's with
189256 * non-inherited `stdio` will not have colors enabled due to a missing TTY.
190257 */
191- function getEnvironmentForNonInteractiveSpawn (
258+ function getEnvironmentForNonInteractiveCommand (
192259 userProvidedEnv ?: NodeJS . ProcessEnv ,
193260) : NodeJS . ProcessEnv {
194261 // Pass through the color level from the TTY/process performing the `spawn` call.
0 commit comments