mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-01 12:10:07 -06:00
281 lines
6.0 KiB
TypeScript
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)
|
|
}
|