Files
cypress/scripts/gulp/utils/childProcessUtils.ts
2023-03-23 12:00:56 -05:00

281 lines
6.0 KiB
TypeScript

import { ChildProcess,
ChildProcessWithoutNullStreams,
exec, ExecOptions,
fork, ForkOptions,
spawn, SpawnOptions,
} from 'child_process'
import through2 from 'through2'
import pDefer from 'p-defer'
import util from 'util'
import { prefixLog, prefixStream } from './prefixStream'
import { addChildProcess } from '../tasks/gulpRegistry'
import stripAnsi from 'strip-ansi'
export type AllSpawnableApps =
| `cmd-${string}`
| `vite-${string}`
| `vite:build-${string}`
| `serve:${string}`
| 'gql-codegen'
| `cy:${string}`
interface TapThroughConfig {
tapOut?: through2.TransformFunction
tapErr?: through2.TransformFunction
}
interface SpawnedOptions extends TapThroughConfig, SpawnOptions {
waitForExit?: boolean
waitForData?: boolean
}
interface SpawnUntilMatchConfig {
command: string
match: string | RegExp
options?: SpawnOptions
}
export async function spawnUntilMatch (
prefix: AllSpawnableApps,
config: SpawnUntilMatchConfig,
) {
const dfd = pDefer()
let ready = false
spawned(prefix, config.command, {
...config.options,
tapOut (chunk, enc, cb) {
if (!ready && stripAnsi(String(chunk)).match(config.match)) {
ready = true
setTimeout(() => dfd.resolve(), 20) // flush the rest of the chunks
}
cb(null, chunk)
},
})
return dfd.promise
}
interface ForkUntilMatchConfig {
modulePath: string
args: string[]
match: string | RegExp
options?: SpawnOptions
}
export async function forkUntilMatch (
prefix: AllSpawnableApps,
config: ForkUntilMatchConfig,
) {
const dfd = pDefer<ChildProcess>()
let ready = false
const cp = await forked(prefix, config.modulePath, config.args, {
...config.options,
tapOut (chunk, enc, cb) {
if (!ready && String(chunk).match(config.match)) {
ready = true
setTimeout(() => dfd.resolve(cp), 20) // flush the rest of the chunks
}
cb(null, chunk)
},
})
return dfd.promise
}
export async function spawned (
prefix: AllSpawnableApps,
command: string,
opts: SpawnedOptions = {},
) {
const { waitForExit, waitForData, tapErr, tapOut, ...spawnOpts } = opts
const [executable, ...rest] = command.split(' ')
const cp = universalSpawn(executable, rest, {
stdio: 'pipe',
...spawnOpts,
env: {
FORCE_COLOR: '1',
NODE_ENV: 'development',
...process.env,
...spawnOpts.env,
},
})
addChildProcess(cp)
return streamHandler(cp, {
tapErr,
tapOut,
command,
prefix,
waitForExit,
waitForData,
})
}
interface ForkedOptions extends TapThroughConfig, ForkOptions {
waitForExit?: boolean
waitForData?: boolean
}
export async function forked (
prefix: AllSpawnableApps,
modulePath: string,
args: string[],
opts: ForkedOptions = {},
) {
const { waitForExit, waitForData, tapErr, tapOut, ...spawnOpts } = opts
const cp = fork(modulePath, args, {
stdio: 'pipe',
...spawnOpts,
env: {
FORCE_COLOR: '1',
NODE_ENV: 'development',
...process.env,
...spawnOpts.env,
},
})
addChildProcess(cp)
return streamHandler(cp, {
tapOut,
tapErr,
command: modulePath,
prefix,
waitForExit,
waitForData,
})
}
function writeError (e: Error) {
return JSON.stringify({ name: e.name, message: e.message, stack: e.stack })
}
const execAsyncLocal = util.promisify(exec)
export interface ExecAsyncOptions extends ExecOptions {
encoding?: string | null
silent?: boolean
}
export const execAsync = async (
command: string,
options: ExecAsyncOptions = {},
) => {
const { silent } = options
if (!silent) {
console.log(command)
}
const result = await execAsyncLocal(command, options)
if (!silent && typeof result.stdout === 'string' && result.stdout.length) {
console.log(result.stdout)
}
if (!silent && typeof result.stderr === 'string' && result.stderr.length) {
console.error(result.stderr)
}
return result
}
export interface StreamHandlerConfig extends TapThroughConfig {
prefix: string
waitForExit?: boolean
waitForData?: boolean
command: string
}
function streamHandler (cp: ChildProcess, config: StreamHandlerConfig) {
const dfd = pDefer<ChildProcess>()
const { command, tapErr = null, tapOut = null, prefix, waitForExit, waitForData = true } = config
const prefixedStdout = cp.stdout?.pipe(
through2(function (chunk, enc, cb) {
if (tapOut) {
tapOut.call(this, chunk, enc, cb)
} else {
cb(null, chunk)
}
}),
)
.pipe(prefixStream(`${prefix}:${cp.pid}`))
const prefixedStderr = cp.stderr?.pipe(
through2(function (chunk, enc, cb) {
if (tapErr) {
tapErr.call(this, chunk, enc, cb)
} else {
cb(null, chunk)
}
}),
)
.pipe(prefixStream(`${prefix}:${cp.pid}`))
prefixedStdout?.pipe(process.stdout)
prefixedStderr?.pipe(process.stderr)
const log = prefixLog(`${prefix}:${cp.pid}`)
if (waitForExit) {
cp.on('exit', (code, signal) => {
log.log(`Exit code: ${code} => ${signal}`)
prefixedStdout?.unpipe(process.stdout)
prefixedStderr?.unpipe(process.stderr)
dfd.resolve(cp)
})
cp.once('error', (e) => {
log.error(`error executing ${command} ${writeError(e)}`)
prefixedStdout?.unpipe(process.stdout)
prefixedStderr?.unpipe(process.stderr)
dfd.reject(e)
})
} else {
cp.once('exit', (code, signal) => {
log.log(`Exit code: ${code} => ${signal}`)
})
cp.once('error', (e) => {
log.error(`error executing ${command} ${writeError(e)}`)
dfd.reject(e)
})
if (waitForData) {
cp.stdout?.once('data', () => {
dfd.resolve(cp)
})
} else {
dfd.resolve(cp)
}
}
return dfd.promise
}
const isWin = process.platform === 'win32'
/**
* Pretreat commands to make them compatible with windows
* @param command
* @returns
*/
export function universalSpawn (
command: string,
args: string[],
opts?: any,
): ChildProcessWithoutNullStreams {
const uCommand = isWin ? `${(command).replace(/\//g, '\\')}.cmd` : command
return spawn(uCommand, args, opts)
}