diff --git a/src/bun.js/child_process.exports.js b/src/bun.js/child_process.exports.js index 7ed5159279d6d5..9219e3ab691ce8 100644 --- a/src/bun.js/child_process.exports.js +++ b/src/bun.js/child_process.exports.js @@ -1,3 +1,1712 @@ +const EventEmitter = import.meta.require("node:events"); +const { + Readable: { fromWeb: ReadableFromWeb }, +} = import.meta.require("node:stream"); +const { + constants: { signals }, +} = import.meta.require("node:os"); -export {}; +const MAX_BUFFER = 1024 * 1024; +const debug = process.env.DEBUG ? console.log : () => {}; +// Sections: +// 1. Exported child_process functions +// 2. child_process helpers +// 3. ChildProcess "class" +// 4. ChildProcess helpers +// 5. Validators +// 6. Primordials +// 7. Random utilities +// 8. Node errors / error polyfills + +// TODO: +// Port rest of node tests +// Fix exit codes with Bun.spawn +// ------------------------------ +// Fix errors +// Support file descriptors being passed in for stdio +// ------------------------------ +// TODO: Look at Pipe to see if we can support passing Node Pipe objects to stdio param + +// TODO: Add these params after support added in Bun.spawn +// uid Sets the user identity of the process (see setuid(2)). +// gid Sets the group identity of the process (see setgid(2)). +// detached Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). + +// TODO: After IPC channels can be opened +// serialization Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. + +// TODO: Add support for ipc option, verify only one IPC channel in array +// stdio | Child's stdio configuration (see options.stdio). +// Support wrapped ipc types (e.g. net.Socket, dgram.Socket, TTY, etc.) +// IPC FD passing support + +// From node child_process docs(https://nodejs.org/api/child_process.html#optionsstdio): +// 'ipc': Create an IPC channel for passing messages/file descriptors between parent and child. +// A ChildProcess may have at most one IPC stdio file descriptor. Setting this option enables the subprocess.send() method. +// If the child is a Node.js process, the presence of an IPC channel will enable process.send() and process.disconnect() methods, +// as well as 'disconnect' and 'message' events within the child. + +//------------------------------------------------------------------------------ +// Section 1. Exported child_process functions +//------------------------------------------------------------------------------ + +// TODO: Implement these props when Windows is supported +// * windowsVerbatimArguments?: boolean; +// * windowsHide?: boolean; + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/** + * Spawns a new process using the given `file`. + * @param {string} file + * @param {string[]} [args] + * @param {{ + * cwd?: string; + * env?: Record; + * argv0?: string; + * stdio?: Array | string; + * detached?: boolean; + * uid?: number; + * gid?: number; + * serialization?: string; + * shell?: boolean | string; + * signal?: AbortSignal; + * timeout?: number; + * killSignal?: string | number; + * }} [options] + * @returns {ChildProcess} + */ +export function spawn(file, args, options) { + options = normalizeSpawnArguments(file, args, options); + validateTimeout(options.timeout); + validateAbortSignal(options.signal, "options.signal"); + const killSignal = sanitizeKillSignal(options.killSignal); + const child = new ChildProcess(); + + debug("spawn", options); + child.spawn(options); + + if (options.timeout > 0) { + let timeoutId = setTimeout(() => { + if (timeoutId) { + try { + child.kill(killSignal); + } catch (err) { + child.emit("error", err); + } + timeoutId = null; + } + }, options.timeout); + + child.once("exit", () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }); + } + + if (options.signal) { + const signal = options.signal; + if (signal.aborted) { + process.nextTick(onAbortListener); + } else { + signal.addEventListener("abort", onAbortListener, { once: true }); + child.once("exit", () => + signal.removeEventListener("abort", onAbortListener) + ); + } + + function onAbortListener() { + abortChildProcess(child, killSignal); + } + } + return child; +} + +/** + * Spawns the specified file as a shell. + * @param {string} file + * @param {string[]} [args] + * @param {{ + * cwd?: string; + * env?: Record; + * encoding?: string; + * timeout?: number; + * maxBuffer?: number; + * killSignal?: string | number; + * uid?: number; + * gid?: number; + * windowsHide?: boolean; + * windowsVerbatimArguments?: boolean; + * shell?: boolean | string; + * signal?: AbortSignal; + * }} [options] + * @param {( + * error?: Error, + * stdout?: string | Buffer, + * stderr?: string | Buffer + * ) => any} [callback] + * @returns {ChildProcess} + */ +export function execFile(file, args, options, callback) { + ({ file, args, options, callback } = normalizeExecFileArgs( + file, + args, + options, + callback + )); + + options = { + encoding: "utf8", + timeout: 0, + maxBuffer: MAX_BUFFER, + killSignal: "SIGTERM", + cwd: null, + env: null, + shell: false, + ...options, + }; + + const maxBuffer = options.maxBuffer; + + // Validate the timeout, if present. + validateTimeout(options.timeout); + + // Validate maxBuffer, if present. + validateMaxBuffer(maxBuffer); + + options.killSignal = sanitizeKillSignal(options.killSignal); + + const child = spawn(file, args, { + cwd: options.cwd, + env: options.env, + // gid: options.gid, + shell: options.shell, + signal: options.signal, + // uid: options.uid, + }); + + let encoding; + const _stdout = []; + const _stderr = []; + if (options.encoding !== "buffer" && BufferIsEncoding(options.encoding)) { + encoding = options.encoding; + } else { + encoding = null; + } + let stdoutLen = 0; + let stderrLen = 0; + let killed = false; + let exited = false; + let timeoutId; + let encodedStdoutLen; + let encodedStderrLen; + + let ex = null; + + let cmd = file; + + function exitHandler(code, signal) { + if (exited) return; + exited = true; + + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + if (!callback) return; + + const readableEncoding = child?.stdout?.readableEncoding; + // merge chunks + let stdout; + let stderr; + if (encoding || (child.stdout && readableEncoding)) { + stdout = ArrayPrototypeJoin.call(_stdout, ""); + } else { + stdout = BufferConcat(_stdout); + } + if (encoding || (child.stderr && readableEncoding)) { + stderr = ArrayPrototypeJoin.call(_stderr, ""); + } else { + stderr = BufferConcat(_stderr); + } + + // TODO: Make this check code === 0 when Bun.spawn fixes exit code issue + if (!ex && code >= 0 && signal === null) { + callback(null, stdout, stderr); + return; + } + + if (args?.length) cmd += ` ${ArrayPrototypeJoin.call(args, " ")}`; + + if (!ex) { + ex = genericNodeError(`Command failed: ${cmd}\n${stderr}`, { + // code: code < 0 ? getSystemErrorName(code) : code, // TODO: Add getSystemErrorName + code: code, + killed: child.killed || killed, + signal: signal, + }); + } + + ex.cmd = cmd; + callback(ex, stdout, stderr); + } + + function errorHandler(e) { + ex = e; + + if (child.stdout) child.stdout.destroy(); + if (child.stderr) child.stderr.destroy(); + + exitHandler(); + } + + function kill() { + if (child.stdout) child.stdout.destroy(); + if (child.stderr) child.stderr.destroy(); + + killed = true; + try { + child.kill(options.killSignal); + } catch (e) { + ex = e; + exitHandler(); + } + } + + if (options.timeout > 0) { + timeoutId = setTimeout(function delayedKill() { + kill(); + timeoutId = null; + }, options.timeout); + } + + if (child.stdout) { + if (encoding) child.stdout.setEncoding(encoding); + + child.stdout.on( + "data", + maxBuffer === Infinity + ? function onUnlimitedSizeBufferedData(chunk) { + ArrayPrototypePush.call(_stdout, chunk); + } + : encoding + ? function onChildStdoutEncoded(chunk) { + stdoutLen += chunk.length; + + if (stdoutLen * 4 > maxBuffer) { + const encoding = child.stdout.readableEncoding; + const actualLen = Buffer.byteLength(chunk, encoding); + if (encodedStdoutLen === undefined) { + for (let i = 0; i < _stdout.length; i++) { + encodedStdoutLen += Buffer.byteLength(_stdout[i], encoding); + } + } else { + encodedStdoutLen += actualLen; + } + const truncatedLen = maxBuffer - (encodedStdoutLen - actualLen); + ArrayPrototypePush.call( + _stdout, + StringPrototypeSlice.apply(chunk, 0, truncatedLen) + ); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stdout"); + kill(); + } else { + ArrayPrototypePush.call(_stdout, chunk); + } + } + : function onChildStdoutRaw(chunk) { + stdoutLen += chunk.length; + + if (stdoutLen > maxBuffer) { + const truncatedLen = maxBuffer - (stdoutLen - chunk.length); + ArrayPrototypePush.call(_stdout, chunk.slice(0, truncatedLen)); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stdout"); + kill(); + } else { + ArrayPrototypePush.call(_stdout, chunk); + } + } + ); + } + + if (child.stderr) { + if (encoding) child.stderr.setEncoding(encoding); + + child.stderr.on( + "data", + maxBuffer === Infinity + ? function onUnlimitedSizeBufferedData(chunk) { + ArrayPrototypePush.call(_stderr, chunk); + } + : encoding + ? function onChildStderrEncoded(chunk) { + stderrLen += chunk.length; + + if (stderrLen * 4 > maxBuffer) { + const encoding = child.stderr.readableEncoding; + const actualLen = Buffer.byteLength(chunk, encoding); + if (encodedStderrLen === undefined) { + for (let i = 0; i < _stderr.length; i++) { + encodedStderrLen += Buffer.byteLength(_stderr[i], encoding); + } + } else { + encodedStderrLen += actualLen; + } + const truncatedLen = maxBuffer - (encodedStderrLen - actualLen); + ArrayPrototypePush.call( + _stderr, + StringPrototypeSlice.call(chunk, 0, truncatedLen) + ); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stderr"); + kill(); + } else { + ArrayPrototypePush.call(_stderr, chunk); + } + } + : function onChildStderrRaw(chunk) { + stderrLen += chunk.length; + + if (stderrLen > maxBuffer) { + const truncatedLen = maxBuffer - (stderrLen - chunk.length); + ArrayPrototypePush.call( + _stderr, + StringPrototypeSlice.call(chunk, 0, truncatedLen) + ); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stderr"); + kill(); + } else { + ArrayPrototypePush.call(_stderr, chunk); + } + } + ); + } + + child.addListener("close", exitHandler); + child.addListener("error", errorHandler); + + return child; +} + +/** + * Spawns a shell executing the given command. + * @param {string} command + * @param {{ + * cmd?: string; + * env?: Record; + * encoding?: string; + * shell?: string; + * signal?: AbortSignal; + * timeout?: number; + * maxBuffer?: number; + * killSignal?: string | number; + * uid?: number; + * gid?: number; + * windowsHide?: boolean; + * }} [options] + * @param {( + * error?: Error, + * stdout?: string | Buffer, + * stderr?: string | Buffer + * ) => any} [callback] + * @returns {ChildProcess} + */ +export function exec(command, options, callback) { + const opts = normalizeExecArgs(command, options, callback); + return execFile(opts.file, opts.options, opts.callback); +} + +/** + * Spawns a new process synchronously using the given `file`. + * @param {string} file + * @param {string[]} [args] + * @param {{ + * cwd?: string; + * input?: string | Buffer | TypedArray | DataView; + * argv0?: string; + * stdio?: string | Array; + * env?: Record; + * uid?: number; + * gid?: number; + * timeout?: number; + * killSignal?: string | number; + * maxBuffer?: number; + * encoding?: string; + * shell?: boolean | string; + * }} [options] + * @returns {{ + * pid: number; + * output: Array; + * stdout: Buffer | string; + * stderr: Buffer | string; + * status: number | null; + * signal: string | null; + * error: Error; + * }} + */ +export function spawnSync(file, args, options) { + options = { + maxBuffer: MAX_BUFFER, + ...normalizeSpawnArguments(file, args, options), + }; + + const maxBuffer = options.maxBuffer; + const encoding = options.encoding; + + debug("spawnSync", options); + + // Validate the timeout, if present. + validateTimeout(options.timeout); + + // Validate maxBuffer, if present. + validateMaxBuffer(maxBuffer); + + // Validate and translate the kill signal, if present. + options.killSignal = sanitizeKillSignal(options.killSignal); + + // options.stdio = getValidStdio(options.stdio || "pipe", true).stdio; + // if (options.input) { + // const stdin = (options.stdio[0] = { ...options.stdio[0] }); + // stdin.input = options.input; + // } + // // We may want to pass data in on any given fd, ensure it is a valid buffer + // for (let i = 0; i < options.stdio.length; i++) { + // const input = options.stdio[i] && options.stdio[i].input; + // if (input != null) { + // const pipe = (options.stdio[i] = { ...options.stdio[i] }); + // if (isArrayBufferView(input)) { + // pipe.input = input; + // } else if (typeof input === "string") { + // pipe.input = Buffer.from(input, options.encoding); + // } else { + // throw new ERR_INVALID_ARG_TYPE( + // `options.stdio[${i}]`, + // ["Buffer", "TypedArray", "DataView", "string"], + // input + // ); + // } + // } + // } + + const stdio = options.stdio || "pipe"; + const bunStdio = getBunStdioOptions(stdio); + const { stdout, stderr, success, exitCode } = Bun.spawnSync({ + cmd: options.args, + env: options.env || undefined, + cwd: options.cwd || undefined, + stdin: bunStdio[0], + stdout: bunStdio[1], + stderr: bunStdio[2], + }); + + const result = { + signal: null, + status: exitCode, + output: [null, stdout, stderr], + }; + + if (stdout && encoding && encoding !== "buffer") { + result.output[1] = result.output[1]?.toString(encoding); + } + + if (stderr && encoding && encoding !== "buffer") { + result.output[2] = result.output[2]?.toString(encoding); + } + + result.stdout = result.output[1]; + result.stderr = result.output[2]; + + if (!success) { + result.error = errnoException(result.stderr, "spawnSync " + options.file); + result.error.path = options.file; + result.error.spawnargs = ArrayPrototypeSlice.call(options.args, 1); + } + + return result; +} + +/** + * Spawns a file as a shell synchronously. + * @param {string} file + * @param {string[]} [args] + * @param {{ + * cwd?: string; + * input?: string | Buffer | TypedArray | DataView; + * stdio?: string | Array; + * env?: Record; + * uid?: number; + * gid?: number; + * timeout?: number; + * killSignal?: string | number; + * maxBuffer?: number; + * encoding?: string; + * windowsHide?: boolean; + * shell?: boolean | string; + * }} [options] + * @returns {Buffer | string} + */ +export function execFileSync(file, args, options) { + ({ file, args, options } = normalizeExecFileArgs(file, args, options)); + + const inheritStderr = !options.stdio; + const ret = spawnSync(file, args, options); + + if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); + + const errArgs = [options.argv0 || file]; + ArrayPrototypePush.apply(errArgs, args); + const err = checkExecSyncError(ret, errArgs); + + if (err) throw err; + + return ret.stdout; +} + +/** + * Spawns a shell executing the given `command` synchronously. + * @param {string} command + * @param {{ + * cwd?: string; + * input?: string | Buffer | TypedArray | DataView; + * stdio?: string | Array; + * env?: Record; + * shell?: string; + * uid?: number; + * gid?: number; + * timeout?: number; + * killSignal?: string | number; + * maxBuffer?: number; + * encoding?: string; + * windowsHide?: boolean; + * }} [options] + * @returns {Buffer | string} + */ +export function execSync(command, options) { + const opts = normalizeExecArgs(command, options, null); + const inheritStderr = !opts.options.stdio; + + const ret = spawnSync(opts.file, opts.options); + + if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); + + const err = checkExecSyncError(ret, undefined, command); + + if (err) throw err; + + return ret.stdout; +} + +export function fork() { + throw new Error("Not implemented"); +} + +//------------------------------------------------------------------------------ +// Section 2. child_process helpers +//------------------------------------------------------------------------------ +function convertToValidSignal(signal) { + if (typeof signal === "number" && getSignalsToNamesMapping()[signal]) + return signal; + + if (typeof signal === "string") { + const signalName = signals[StringPrototypeToUpperCase.call(signal)]; + if (signalName) return signalName; + } + + throw new ERR_UNKNOWN_SIGNAL(signal); +} + +function sanitizeKillSignal(killSignal) { + if (typeof killSignal === "string" || typeof killSignal === "number") { + return convertToValidSignal(killSignal); + } else if (killSignal != null) { + throw new ERR_INVALID_ARG_TYPE( + "options.killSignal", + ["string", "number"], + killSignal + ); + } +} + +let signalsToNamesMapping; +function getSignalsToNamesMapping() { + if (signalsToNamesMapping !== undefined) return signalsToNamesMapping; + + signalsToNamesMapping = ObjectCreate(null); + for (const key in signals) { + signalsToNamesMapping[signals[key]] = key; + } + + return signalsToNamesMapping; +} + +function normalizeExecFileArgs(file, args, options, callback) { + if (ArrayIsArray(args)) { + args = ArrayPrototypeSlice.call(args); + } else if (args != null && typeof args === "object") { + callback = options; + options = args; + args = null; + } else if (typeof args === "function") { + callback = args; + options = null; + args = null; + } + + if (args == null) { + args = []; + } + + if (typeof options === "function") { + callback = options; + } else if (options != null) { + validateObject(options, "options"); + } + + if (options == null) { + options = {}; + } + + if (callback != null) { + validateFunction(callback, "callback"); + } + + // Validate argv0, if present. + if (options.argv0 != null) { + validateString(options.argv0, "options.argv0"); + validateArgumentNullCheck(options.argv0, "options.argv0"); + } + + return { file, args, options, callback }; +} + +function normalizeExecArgs(command, options, callback) { + validateString(command, "command"); + validateArgumentNullCheck(command, "command"); + + if (typeof options === "function") { + callback = options; + options = undefined; + } + + // Make a shallow copy so we don't clobber the user's options object. + options = { ...options }; + options.shell = typeof options.shell === "string" ? options.shell : true; + + return { + file: command, + options: options, + callback: callback, + }; +} + +function normalizeSpawnArguments(file, args, options) { + validateString(file, "file"); + validateArgumentNullCheck(file, "file"); + + if (file.length === 0) + throw new ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); + + if (ArrayIsArray(args)) { + args = ArrayPrototypeSlice.call(args); + } else if (args == null) { + args = []; + } else if (typeof args !== "object") { + throw new ERR_INVALID_ARG_TYPE("args", "object", args); + } else { + options = args; + args = []; + } + + validateArgumentsNullCheck(args, "args"); + + if (options === undefined) options = {}; + else validateObject(options, "options"); + + let cwd = options.cwd; + + // Validate the cwd, if present. + if (cwd != null) { + cwd = getValidatedPath(cwd, "options.cwd"); + } + + // TODO: Detached check + // TODO: Gid check + // TODO: Uid check + + // Validate the shell, if present. + if ( + options.shell != null && + typeof options.shell !== "boolean" && + typeof options.shell !== "string" + ) { + throw new ERR_INVALID_ARG_TYPE( + "options.shell", + ["boolean", "string"], + options.shell + ); + } + + // Validate argv0, if present. + if (options.argv0 != null) { + validateString(options.argv0, "options.argv0"); + validateArgumentNullCheck(options.argv0, "options.argv0"); + } + + // TODO: Windows checks for Windows specific options + + // Handle shell + if (options.shell) { + validateArgumentNullCheck(options.shell, "options.shell"); + const command = ArrayPrototypeJoin.call([file, ...args], " "); + // TODO: Windows moment + // Set the shell, switches, and commands. + // if (process.platform === "win32") { + // if (typeof options.shell === "string") file = options.shell; + // else file = process.env.comspec || "cmd.exe"; + // // '/d /s /c' is used only for cmd.exe. + // if (RegExpPrototypeExec(/^(?:.*\\)?cmd(?:\.exe)?$/i, file) !== null) { + // args = ["/d", "/s", "/c", `"${command}"`]; + // windowsVerbatimArguments = true; + // } else { + // args = ["-c", command]; + // } + // } else { + if (typeof options.shell === "string") file = options.shell; + else if (process.platform === "android") file = "sh"; + else file = "sh"; + args = ["-c", command]; + // } + } + + // Handle argv0 + if (typeof options.argv0 === "string") { + ArrayPrototypeUnshift.call(args, options.argv0); + } else { + ArrayPrototypeUnshift.call(args, file); + } + + const env = options.env || process.env; + const envPairs = env; + + // // process.env.NODE_V8_COVERAGE always propagates, making it possible to + // // collect coverage for programs that spawn with white-listed environment. + // copyProcessEnvToEnv(env, "NODE_V8_COVERAGE", options.env); + + // TODO: Windows env support here... + + return { ...options, file, args, cwd, envPairs }; +} + +function checkExecSyncError(ret, args, cmd) { + let err; + if (ret.error) { + err = ret.error; + ObjectAssign(err, ret); + } else if (ret.status !== 0) { + let msg = "Command failed: "; + msg += cmd || ArrayPrototypeJoin.call(args, " "); + if (ret.stderr && ret.stderr.length > 0) + msg += `\n${ret.stderr.toString()}`; + err = genericNodeError(msg, ret); + } + return err; +} + +//------------------------------------------------------------------------------ +// Section 3. ChildProcess class +//------------------------------------------------------------------------------ +export class ChildProcess extends EventEmitter { + #handle; + #exited = false; + #closesNeeded = 1; + #closesGot = 0; + + connected = false; + signalCode = null; + exitCode = null; + killed = false; + spawnfile; + spawnargs; + pid; + stdin; + stdout; + stderr; + stdio; + channel; + + // constructor(options) { + // super(options); + // this.#handle[owner_symbol] = this; + // } + + #handleOnExit(exitCode, signalCode) { + if (this.#exited) return; + if (signalCode) { + this.signalCode = signalCode; + } else { + this.exitCode = exitCode; + } + + if (this.stdin) { + this.stdin.destroy(); + } + + if (this.#handle) { + this.#handle = null; + } + + if (exitCode < 0) { + const syscall = this.spawnfile ? "spawn " + this.spawnfile : "spawn"; + const err = errnoException(exitCode, syscall); + + if (this.spawnfile) err.path = this.spawnfile; + + err.spawnargs = ArrayPrototypeSlice.call(this.spawnargs, 1); + this.emit("error", err); + } else { + this.emit("exit", this.exitCode, this.signalCode); + } + + // If any of the stdio streams have not been touched, + // then pull all the data through so that it can get the + // eof and emit a 'close' event. + // Do it on nextTick so that the user has one last chance + // to consume the output, if for example they only want to + // start reading the data once the process exits. + process.nextTick(flushStdio, this); + + this.#maybeClose(); + this.#exited = true; + } + + #getBunSpawnIo(stdio, options) { + const result = []; + switch (stdio[0]) { + case "pipe": + result[0] = new WrappedFileSink(this.#handle.stdin); + break; + case "inherit": + result[0] = process.stdin; + default: + result[0] = null; + } + let i = 1; + for (; i < stdio.length; i++) { + switch (stdio[i]) { + case "pipe": + result[i] = ReadableFromWeb(this.#handle[fdToStdioName(i)], { + encoding: options.encoding || undefined, + }); + break; + case "inherit": + result[i] = process[fdToStdioName(i)]; + break; + default: + result[i] = null; + } + } + return result; + } + + spawn(options) { + validateObject(options, "options"); + + // validateOneOf(options.serialization, "options.serialization", [ + // undefined, + // "json", + // // "advanced", // TODO + // ]); + // const serialization = options.serialization || "json"; + + // if (ipc !== undefined) { + // // Let child process know about opened IPC channel + // if (options.envPairs === undefined) options.envPairs = []; + // else validateArray(options.envPairs, "options.envPairs"); + + // ArrayPrototypePush.call(options.envPairs, `NODE_CHANNEL_FD=${ipcFd}`); + // ArrayPrototypePush.call( + // options.envPairs, + // `NODE_CHANNEL_SERIALIZATION_MODE=${serialization}` + // ); + // } + + validateString(options.file, "options.file"); + this.spawnfile = options.file; + + if (options.args === undefined) { + this.spawnargs = []; + } else { + validateArray(options.args, "options.args"); + this.spawnargs = options.args; + } + + const stdio = options.stdio || "pipe"; + const bunStdio = getBunStdioOptions(stdio); + + const cmd = options.args; + this.#handle = Bun.spawn({ + cmd, + stdin: bunStdio[0], + stdout: bunStdio[1], + stderr: bunStdio[2], + cwd: options.cwd || undefined, + env: options.envPairs || undefined, + onExit: this.#handleOnExit.bind(this), + }); + + this.stdio = this.#getBunSpawnIo(bunStdio, options); + this.stdin = this.stdio[0]; + this.stdout = this.stdio[1]; + this.stderr = this.stdio[2]; + + process.nextTick(onSpawnNT, this); + + this.pid = this.#handle.pid; + + // If no `stdio` option was given - use default + // let stdio = options.stdio || "pipe"; // TODO: reset default + // let stdio = options.stdio || ["pipe", "pipe", "pipe"]; + + // stdio = getValidStdio(stdio, false); + + // const ipc = stdio.ipc; + // const ipcFd = stdio.ipcFd; + // stdio = options.stdio = stdio.stdio; + + // for (i = 0; i < stdio.length; i++) { + // const stream = stdio[i]; + // if (stream.type === "ignore") continue; + + // if (stream.ipc) { + // this._closesNeeded++; + // continue; + // } + + // // The stream is already cloned and piped, thus stop its readable side, + // // otherwise we might attempt to read from the stream when at the same time + // // the child process does. + // if (stream.type === "wrap") { + // stream.handle.reading = false; + // stream.handle.readStop(); + // stream._stdio.pause(); + // stream._stdio.readableFlowing = false; + // stream._stdio._readableState.reading = false; + // stream._stdio[kIsUsedAsStdio] = true; + // continue; + // } + + // if (stream.handle) { + // stream.socket = createSocket( + // this.pid !== 0 ? stream.handle : null, + // i > 0 + // ); + + // if (i > 0 && this.pid !== 0) { + // this._closesNeeded++; + // stream.socket.on("close", () => { + // maybeClose(this); + // }); + // } + // } + // } + + // this.stdin = + // stdio.length >= 1 && stdio[0].socket !== undefined ? stdio[0].socket : null; + // this.stdout = + // stdio.length >= 2 && stdio[1].socket !== undefined ? stdio[1].socket : null; + // this.stderr = + // stdio.length >= 3 && stdio[2].socket !== undefined ? stdio[2].socket : null; + + // this.stdio = []; + + // for (i = 0; i < stdio.length; i++) + // ArrayPrototypePush.call( + // this.stdio, + // stdio[i].socket === undefined ? null : stdio[i].socket + // ); + + // // Add .send() method and start listening for IPC data + // if (ipc !== undefined) setupChannel(this, ipc, serialization); + } + + kill(sig) { + const signal = + sig === 0 + ? sig + : convertToValidSignal(sig === undefined ? "SIGTERM" : sig); + + if (this.#handle) { + this.#handle.kill(signal); + } + + this.killed = true; + this.emit("exit", null, signal); + this.#maybeClose(); + + // TODO: Make this actually ensure the process has exited before returning + // await this.#handle.exited() + // return this.#handle.killed; + return this.killed; + } + + #maybeClose() { + this.#closesGot++; + + if (this.#closesGot === this.#closesNeeded) { + this.emit("close", this.exitCode, this.signalCode); + } + } + + ref() { + if (this.#handle) this.#handle.ref(); + } + + unref() { + if (this.#handle) this.#handle.unref(); + } +} + +//------------------------------------------------------------------------------ +// Section 4. ChildProcess helpers +//------------------------------------------------------------------------------ +const nodeToBunLookup = { + ignore: null, + pipe: "pipe", + overlapped: "pipe", // TODO: this may need to work differently for Windows + inherit: "inherit", +}; + +function nodeToBun(item) { + // If inherit and we are referencing stdin/stdout/stderr index, + // we can get the fd from the ReadStream for the corresponding stdio + if (typeof item === "number") { + return item; + } else { + const result = nodeToBunLookup[item]; + if (result === undefined) throw new Error("Invalid stdio option"); + return result; + } +} + +function fdToStdioName(fd) { + switch (fd) { + case 0: + return "stdin"; + case 1: + return "stdout"; + case 2: + return "stderr"; + default: + return null; + } +} + +function getBunStdioOptions(stdio) { + const normalizedStdio = normalizeStdio(stdio); + // Node options: + // pipe: just a pipe + // ipc = can only be one in array + // overlapped -- same as pipe on Unix based systems + // inherit -- 'inherit': equivalent to ['inherit', 'inherit', 'inherit'] or [0, 1, 2] + // ignore -- > /dev/null, more or less same as null option for Bun.spawn stdio + // TODO: Stream -- use this stream + // number -- used as FD + // null, undefined: Use default value. Not same as ignore, which is Bun.spawn null. + // null/undefined: For stdio fds 0, 1, and 2 (in other words, stdin, stdout, and stderr) a pipe is created. For fd 3 and up, the default is 'ignore' + + // Important Bun options + // pipe + // fd + // null - no stdin/stdout/stderr + + // Translations: node -> bun + // pipe -> pipe + // overlapped -> pipe + // ignore -> null + // inherit -> inherit (stdin/stdout/stderr) + // Stream -> throw err for now + + return normalizedStdio.map((item) => nodeToBun(item)); +} + +function normalizeStdio(stdio) { + if (typeof stdio === "string") { + switch (stdio) { + case "ignore": + return ["ignore", "ignore", "ignore"]; + case "pipe": + return ["pipe", "pipe", "pipe"]; + case "inherit": + return ["inherit", "inherit", "inherit"]; + default: + throw new ERR_INVALID_OPT_VALUE("stdio", stdio); + } + } else if (ArrayIsArray(stdio)) { + // Validate if each is a valid stdio type + // TODO: Support wrapped types here + + let processedStdio; + if (stdio.length === 0) processedStdio = ["pipe", "pipe", "pipe"]; + else if (stdio.length === 1) processedStdio = [stdio[0], "pipe", "pipe"]; + else if (stdio.length === 2) processedStdio = [stdio[0], stdio[1], "pipe"]; + else if (stdio.length >= 3) processedStdio = [stdio[0], stdio[1], stdio[2]]; + + return processedStdio.map((item) => (!item ? "pipe" : item)); + } else { + throw new ERR_INVALID_OPT_VALUE("stdio", stdio); + } +} + +function flushStdio(subprocess) { + const stdio = subprocess.stdio; + + if (stdio == null) return; + + for (let i = 0; i < stdio.length; i++) { + const stream = stdio[i]; + // TODO(addaleax): This doesn't necessarily account for all the ways in + // which data can be read from a stream, e.g. being consumed on the + // native layer directly as a StreamBase. + if (!stream || !stream.readable) { + continue; + } + stream.resume(); + } +} + +function onSpawnNT(self) { + self.emit("spawn"); +} + +function abortChildProcess(child, killSignal) { + if (!child) return; + try { + if (child.kill(killSignal)) { + child.emit("error", new AbortError()); + } + } catch (err) { + child.emit("error", err); + } +} + +class WrappedFileSink extends EventEmitter { + #fileSink; + + constructor(fileSink) { + super(); + this.#fileSink = fileSink; + } + + write(data) { + this.#fileSink.write(data); + this.#fileSink.flush(true); + } + + destroy() { + this.#fileSink.end(); + } + + end() { + this.#fileSink.end(); + } +} + +//------------------------------------------------------------------------------ +// Section 5. Validators +//------------------------------------------------------------------------------ + +function validateMaxBuffer(maxBuffer) { + if (maxBuffer != null && !(typeof maxBuffer === "number" && maxBuffer >= 0)) { + throw new ERR_OUT_OF_RANGE( + "options.maxBuffer", + "a positive number", + maxBuffer + ); + } +} + +function validateArgumentNullCheck(arg, propName) { + if (typeof arg === "string" && StringPrototypeIncludes.call(arg, "\u0000")) { + throw new ERR_INVALID_ARG_VALUE( + propName, + arg, + "must be a string without null bytes" + ); + } +} + +function validateArgumentsNullCheck(args, propName) { + for (let i = 0; i < args.length; ++i) { + validateArgumentNullCheck(args[i], `${propName}[${i}]`); + } +} + +function validateTimeout(timeout) { + if (timeout != null && !(NumberIsInteger(timeout) && timeout >= 0)) { + throw new ERR_OUT_OF_RANGE("timeout", "an unsigned integer", timeout); + } +} + +function validateBoolean(value, name) { + if (typeof value !== "boolean") + throw new ERR_INVALID_ARG_TYPE(name, "boolean", value); +} + +/** + * @callback validateFunction + * @param {*} value + * @param {string} name + * @returns {asserts value is Function} + */ + +/** @type {validateFunction} */ +function validateFunction(value, name) { + if (typeof value !== "function") + throw new ERR_INVALID_ARG_TYPE(name, "Function", value); +} + +/** + * @callback validateAbortSignal + * @param {*} signal + * @param {string} name + */ + +/** @type {validateAbortSignal} */ +const validateAbortSignal = (signal, name) => { + if ( + signal !== undefined && + (signal === null || typeof signal !== "object" || !("aborted" in signal)) + ) { + throw new ERR_INVALID_ARG_TYPE(name, "AbortSignal", signal); + } +}; + +/** + * @callback validateOneOf + * @template T + * @param {T} value + * @param {string} name + * @param {T[]} oneOf + */ + +/** @type {validateOneOf} */ +const validateOneOf = (value, name, oneOf) => { + // const validateOneOf = hideStackFrames((value, name, oneOf) => { + if (!ArrayPrototypeIncludes.call(oneOf, value)) { + const allowed = ArrayPrototypeJoin.call( + ArrayPrototypeMap.call(oneOf, (v) => + typeof v === "string" ? `'${v}'` : String(v) + ), + ", " + ); + const reason = "must be one of: " + allowed; + throw new ERR_INVALID_ARG_VALUE(name, value, reason); + } +}; + +/** + * @callback validateObject + * @param {*} value + * @param {string} name + * @param {{ + * allowArray?: boolean, + * allowFunction?: boolean, + * nullable?: boolean + * }} [options] + */ + +/** @type {validateObject} */ +const validateObject = (value, name, options = null) => { + // const validateObject = hideStackFrames((value, name, options = null) => { + const allowArray = options?.allowArray ?? false; + const allowFunction = options?.allowFunction ?? false; + const nullable = options?.nullable ?? false; + if ( + (!nullable && value === null) || + (!allowArray && ArrayIsArray.call(value)) || + (typeof value !== "object" && + (!allowFunction || typeof value !== "function")) + ) { + throw new ERR_INVALID_ARG_TYPE(name, "object", value); + } +}; + +/** + * @callback validateArray + * @param {*} value + * @param {string} name + * @param {number} [minLength] + * @returns {asserts value is any[]} + */ + +/** @type {validateArray} */ +const validateArray = (value, name, minLength = 0) => { + // const validateArray = hideStackFrames((value, name, minLength = 0) => { + if (!ArrayIsArray(value)) { + throw new ERR_INVALID_ARG_TYPE(name, "Array", value); + } + if (value.length < minLength) { + const reason = `must be longer than ${minLength}`; + throw new ERR_INVALID_ARG_VALUE(name, value, reason); + } +}; + +/** + * @callback validateString + * @param {*} value + * @param {string} name + * @returns {asserts value is string} + */ + +/** @type {validateString} */ +function validateString(value, name) { + if (typeof value !== "string") + throw new ERR_INVALID_ARG_TYPE(name, "string", value); +} + +function nullCheck(path, propName, throwError = true) { + const pathIsString = typeof path === "string"; + const pathIsUint8Array = isUint8Array(path); + + // We can only perform meaningful checks on strings and Uint8Arrays. + if ( + (!pathIsString && !pathIsUint8Array) || + (pathIsString && !StringPrototypeIncludes.call(path, "\u0000")) || + (pathIsUint8Array && !Uint8ArrayPrototypeIncludes.call(path, 0)) + ) { + return; + } + + const err = new ERR_INVALID_ARG_VALUE( + propName, + path, + "must be a string or Uint8Array without null bytes" + ); + if (throwError) { + throw err; + } + return err; +} + +function validatePath(path, propName = "path") { + if (typeof path !== "string" && !isUint8Array(path)) { + throw new ERR_INVALID_ARG_TYPE(propName, ["string", "Buffer", "URL"], path); + } + + const err = nullCheck(path, propName, false); + + if (err !== undefined) { + throw err; + } +} + +function getValidatedPath(fileURLOrPath, propName = "path") { + const path = toPathIfFileURL(fileURLOrPath); + validatePath(path, propName); + return path; +} + +//------------------------------------------------------------------------------ +// Section 6. Primordials +//------------------------------------------------------------------------------ +var Uint8Array = globalThis.Uint8Array; +var String = globalThis.String; +var Object = globalThis.Object; +var Buffer = globalThis.Buffer; + +var ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; +var ObjectCreate = Object.create; +var ObjectAssign = Object.assign; +var BufferConcat = Buffer.concat; +var BufferIsEncoding = Buffer.isEncoding; + +var ArrayPrototypePush = Array.prototype.push; +var ArrayPrototypeReduce = Array.prototype.reduce; +var ArrayPrototypeFilter = Array.prototype.filter; +var ArrayPrototypeJoin = Array.prototype.join; +var ArrayPrototypeMap = Array.prototype.map; +var ArrayPrototypeIncludes = Array.prototype.includes; +var ArrayPrototypeSlice = Array.prototype.slice; +var ArrayPrototypeUnshift = Array.prototype.unshift; +var ArrayIsArray = Array.isArray; + +var NumberIsInteger = Number.isInteger; +var MathAbs = Math.abs; + +var StringPrototypeToUpperCase = String.prototype.toUpperCase; +var StringPrototypeIncludes = String.prototype.includes; +var Uint8ArrayPrototypeIncludes = Uint8Array.prototype.includes; + +function isUint8Array(value) { + return ( + typeof value === "object" && value !== null && value instanceof Uint8Array + ); +} + +//------------------------------------------------------------------------------ +// Section 7. Random utilities +//------------------------------------------------------------------------------ + +function isURLInstance(fileURLOrPath) { + return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin; +} + +function toPathIfFileURL(fileURLOrPath) { + if (!isURLInstance(fileURLOrPath)) return fileURLOrPath; + return Bun.fileURLToPath(fileURLOrPath); +} + +//------------------------------------------------------------------------------ +// Section 8. Node errors / error polyfills +//------------------------------------------------------------------------------ +var Error = globalThis.Error; +var TypeError = globalThis.TypeError; +var RangeError = globalThis.RangeError; + +// Node uses a slightly different abort error than standard DOM. See: https://github.com/nodejs/node/blob/main/lib/internal/errors.js +class AbortError extends Error { + code = "ABORT_ERR"; + name = "AbortError"; + constructor(message = "The operation was aborted", options = undefined) { + if (options !== undefined && typeof options !== "object") { + throw new ERR_INVALID_ARG_TYPE("options", "Object", options); + } + super(message, options); + } +} + +function genericNodeError(message, options) { + const err = new Error(message); + err.code = options.code; + err.killed = options.killed; + err.signal = options.signal; + return err; +} + +// const messages = new Map(); + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +// function E(sym, val, def) { +// messages.set(sym, val); +// def = makeNodeErrorWithCode(def, sym); +// errorCodes[sym] = def; +// } + +// function makeNodeErrorWithCode(Base, key) { +// return function NodeError(...args) { +// // const limit = Error.stackTraceLimit; +// // if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0; +// const error = new Base(); +// // Reset the limit and setting the name property. +// // if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = limit; +// const message = getMessage(key, args); +// error.message = message; +// // captureLargerStackTrace(error); +// error.code = key; +// return error; +// }; +// } + +// function getMessage(key, args) { +// const msgFn = messages.get(key); +// if (args.length !== msgFn.length) +// throw new Error( +// `Invalid number of args for error message ${key}. Got ${args.length}, expected ${msgFn.length}.` +// ); +// return msgFn(...args); +// } + +// E( +// "ERR_INVALID_ARG_TYPE", +// (name, expected, actual) => { +// assert(typeof name === "string", "'name' must be a string"); +// if (!ArrayIsArray(expected)) { +// expected = [expected]; +// } + +// let msg = "The "; +// if (StringPrototypeEndsWith(name, " argument")) { +// // For cases like 'first argument' +// msg += `${name} `; +// } else { +// const type = StringPrototypeIncludes(name, ".") ? "property" : "argument"; +// msg += `"${name}" ${type} `; +// } +// msg += "must be "; + +// const types = []; +// const instances = []; +// const other = []; + +// for (const value of expected) { +// assert( +// typeof value === "string", +// "All expected entries have to be of type string" +// ); +// if (ArrayPrototypeIncludes.call(kTypes, value)) { +// ArrayPrototypePush(types, StringPrototypeToLowerCase(value)); +// } else if (RegExpPrototypeExec(classRegExp, value) !== null) { +// ArrayPrototypePush(instances, value); +// } else { +// assert( +// value !== "object", +// 'The value "object" should be written as "Object"' +// ); +// ArrayPrototypePush(other, value); +// } +// } + +// // Special handle `object` in case other instances are allowed to outline +// // the differences between each other. +// if (instances.length > 0) { +// const pos = ArrayPrototypeIndexOf(types, "object"); +// if (pos !== -1) { +// ArrayPrototypeSplice.call(types, pos, 1); +// ArrayPrototypePush.call(instances, "Object"); +// } +// } + +// if (types.length > 0) { +// if (types.length > 2) { +// const last = ArrayPrototypePop(types); +// msg += `one of type ${ArrayPrototypeJoin(types, ", ")}, or ${last}`; +// } else if (types.length === 2) { +// msg += `one of type ${types[0]} or ${types[1]}`; +// } else { +// msg += `of type ${types[0]}`; +// } +// if (instances.length > 0 || other.length > 0) msg += " or "; +// } + +// if (instances.length > 0) { +// if (instances.length > 2) { +// const last = ArrayPrototypePop(instances); +// msg += `an instance of ${ArrayPrototypeJoin( +// instances, +// ", " +// )}, or ${last}`; +// } else { +// msg += `an instance of ${instances[0]}`; +// if (instances.length === 2) { +// msg += ` or ${instances[1]}`; +// } +// } +// if (other.length > 0) msg += " or "; +// } + +// if (other.length > 0) { +// if (other.length > 2) { +// const last = ArrayPrototypePop(other); +// msg += `one of ${ArrayPrototypeJoin.call(other, ", ")}, or ${last}`; +// } else if (other.length === 2) { +// msg += `one of ${other[0]} or ${other[1]}`; +// } else { +// if (StringPrototypeToLowerCase(other[0]) !== other[0]) msg += "an "; +// msg += `${other[0]}`; +// } +// } + +// msg += `. Received ${determineSpecificType(actual)}`; + +// return msg; +// }, +// TypeError +// ); + +function ERR_OUT_OF_RANGE(str, range, input, replaceDefaultBoolean = false) { + // Node implementation: + // assert(range, 'Missing "range" argument'); + // let msg = replaceDefaultBoolean + // ? str + // : `The value of "${str}" is out of range.`; + // let received; + // if (NumberIsInteger(input) && MathAbs(input) > 2 ** 32) { + // received = addNumericalSeparator(String(input)); + // } else if (typeof input === "bigint") { + // received = String(input); + // if (input > 2n ** 32n || input < -(2n ** 32n)) { + // received = addNumericalSeparator(received); + // } + // received += "n"; + // } else { + // received = lazyInternalUtilInspect().inspect(input); + // } + // msg += ` It must be ${range}. Received ${received}`; + // return new RangeError(msg); + return new RangeError( + `The value of ${str} is out of range. It must be ${range}. Received ${input}` + ); +} + +function ERR_CHILD_PROCESS_STDIO_MAXBUFFER(stdio) { + return Error(`${stdio} maxBuffer length exceeded`); +} + +function ERR_UNKNOWN_SIGNAL(name) { + const err = new TypeError(`Unknown signal: ${name}`); + err.code = "ERR_UNKNOWN_SIGNAL"; + return err; +} + +function ERR_INVALID_ARG_TYPE(name, type, value) { + const err = new TypeError( + `The "${name}" argument must be of type ${type}. Received ${value}` + ); + err.code = "ERR_INVALID_ARG_TYPE"; + return err; +} + +function ERR_INVALID_OPT_VALUE(name, value) { + return new TypeError(`The value "${value}" is invalid for option "${name}"`); +} + +function ERR_INVALID_ARG_VALUE(name, value, reason) { + return new Error( + `The value "${value}" is invalid for argument '${name}'. Reason: ${reason}` + ); +} + +// TODO: Add actual proper error implementation here +function errnoException(err, name) { + return new Error(`Error: ${name}. Internal error: ${err.message}`); +} + +export default { + ChildProcess, + spawn, + execFile, + exec, + fork, + spawnSync, + execFileSync, + execSync, + + [Symbol.for("CommonJS")]: 0, +}; diff --git a/src/bun.js/streams.exports.js b/src/bun.js/streams.exports.js index 979ef32f64ce91..576f3ea89252e8 100644 --- a/src/bun.js/streams.exports.js +++ b/src/bun.js/streams.exports.js @@ -1,5 +1,6 @@ // "readable-stream" npm package // just transpiled +var { isPromise } = import.meta.primordials; var __create = Object.create; var __defProp = Object.defineProperty; @@ -33,6 +34,76 @@ var __copyProps = (to, from, except, desc) => { var runOnNextTick = process.nextTick; +function isReadableStream(value) { + return ( + typeof value === "object" && + value !== null && + value instanceof ReadableStream + ); +} + +function validateBoolean(value, name) { + if (typeof value !== "boolean") + throw new ERR_INVALID_ARG_TYPE(name, "boolean", value); +} + +/** + * @callback validateObject + * @param {*} value + * @param {string} name + * @param {{ + * allowArray?: boolean, + * allowFunction?: boolean, + * nullable?: boolean + * }} [options] + */ + +/** @type {validateObject} */ +const validateObject = (value, name, options = null) => { + const allowArray = options?.allowArray ?? false; + const allowFunction = options?.allowFunction ?? false; + const nullable = options?.nullable ?? false; + if ( + (!nullable && value === null) || + (!allowArray && ArrayIsArray(value)) || + (typeof value !== "object" && + (!allowFunction || typeof value !== "function")) + ) { + throw new ERR_INVALID_ARG_TYPE(name, "Object", value); + } +}; + +/** + * @callback validateString + * @param {*} value + * @param {string} name + * @returns {asserts value is string} + */ + +/** @type {validateString} */ +function validateString(value, name) { + if (typeof value !== "string") + throw new ERR_INVALID_ARG_TYPE(name, "string", value); +} + +var ArrayIsArray = Array.isArray; + +//------------------------------------------------------------------------------ +// Node error polyfills +//------------------------------------------------------------------------------ + +function ERR_INVALID_ARG_TYPE(name, type, value) { + return new Error( + `The argument '${name}' is invalid. Received '${value}' for type '${type}'` + ); +} + +function ERR_INVALID_ARG_VALUE(name, value, reason) { + return new Error( + `The value '${value}' is invalid for argument '${name}'. Reason: ${reason}` + ); +} + // node_modules/readable-stream/lib/ours/primordials.js var require_primordials = __commonJS({ "node_modules/readable-stream/lib/ours/primordials.js"(exports, module) { @@ -2509,7 +2580,9 @@ var require_readable = __commonJS({ const state = this._readableState; if (ev === "data") { state.readableListening = this.listenerCount("readable") > 0; - if (state.flowing !== false) this.resume(); + if (state.flowing !== false) { + this.resume(); + } } else if (ev === "readable") { if (!state.endEmitted && !state.readableListening) { state.readableListening = state.needReadable = true; @@ -2528,6 +2601,126 @@ var require_readable = __commonJS({ static ReadableState = ReadableState; } + + class ReadableFromWeb extends Readable { + #reader; + #closed; + + constructor(options) { + const { objectMode, highWaterMark, encoding, signal, reader } = options; + super({ + objectMode, + highWaterMark, + encoding, + signal, + }); + + this.#reader = reader; + this.#reader.closed + .then(() => { + this.#closed = true; + }) + .catch((error) => { + this.#closed = true; + destroy(this, error); + }); + } + + async _read() { + var deferredError; + try { + var done, value; + const firstResult = this.#reader.readMany(); + + if (isPromise(firstResult)) { + const result = await firstResult; + done = result.done; + value = result.value; + } else { + done = firstResult.done; + value = firstResult.value; + } + + if (done) { + this.push(null); + return; + } + + if (!value) + throw new Error( + `Invalid value from ReadableStream reader: ${value}` + ); + if (ArrayIsArray(value)) { + this.push(...value); + } else { + this.push(value); + } + } catch (e) { + deferredError = e; + } finally { + if (deferredError) throw deferredError; + } + } + + _destroy(error, callback) { + if (!this.#closed) { + this.#reader.releaseLock(); + this.#reader.cancel(error).then(done).catch(done); + return; + } + try { + callback(error); + } catch (error) { + globalThis.reportError(error); + } + } + + // NOTE(Derrick): For whatever reason this seems to be necessary to make this work + // I couldn't find out why .constructed was getting set to false + // even though construct() was getting called + _construct() { + this._readableState.constructed = true; + } + } + + /** + * @param {ReadableStream} readableStream + * @param {{ + * highWaterMark? : number, + * encoding? : string, + * objectMode? : boolean, + * signal? : AbortSignal, + * }} [options] + * @returns {Readable} + */ + function newStreamReadableFromReadableStream(readableStream, options = {}) { + if (!isReadableStream(readableStream)) { + throw new ERR_INVALID_ARG_TYPE( + "readableStream", + "ReadableStream", + readableStream + ); + } + + validateObject(options, "options"); + const { highWaterMark, encoding, objectMode = false, signal } = options; + + if (encoding !== undefined && !Buffer.isEncoding(encoding)) + throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding"); + validateBoolean(objectMode, "options.objectMode"); + + const reader = readableStream.getReader(); + const readable = new ReadableFromWeb({ + highWaterMark, + encoding, + objectMode, + signal, + reader, + }); + + return readable; + } + module.exports = Readable; var { addAbortSignal } = require_add_abort_signal(); @@ -3327,7 +3520,9 @@ var require_readable = __commonJS({ Readable.from = function (iterable, opts) { return from(Readable, iterable, opts); }; - var webStreamsAdapters; + var webStreamsAdapters = { + newStreamReadableFromReadableStream, + }; function lazyWebStreams() { if (webStreamsAdapters === void 0) webStreamsAdapters = {}; return webStreamsAdapters; diff --git a/test/bun.js/child_process-node.test.js b/test/bun.js/child_process-node.test.js new file mode 100644 index 00000000000000..10135affacf1e9 --- /dev/null +++ b/test/bun.js/child_process-node.test.js @@ -0,0 +1,570 @@ +import { describe, expect, it } from "bun:test"; +import { ChildProcess, spawn, exec } from "node:child_process"; +import { EOL } from "node:os"; +import assertNode from "node:assert"; +import { inspect } from "node:util"; + +const debug = console.log; + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +const common = { + // // TODO: Fix the implementations of these functions, they may be ruining everything... + // mustCallAtLeast: function mustCallAtLeast(callback) { + // return (...args) => { + // callback(...args); + // expect(true).toBe(true); + // }; + // }, + // mustCall: function mustCall(callback) { + // return (...args) => { + // callback(...args); + // expect(true).toBe(true); + // }; + // }, + pwdCommand: ["pwd", []], +}; + +const mustCallChecks = []; + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter(function (context) { + if ("minimum" in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach(function (context) { + console.log( + "Mismatched %s function calls. Expected %s, actual %d.", + context.name, + context.messageSegment, + context.actual + ); + console.log(context.stack.split("\n").slice(2).join("\n")); + }); + + if (failed.length) process.exit(1); +} + +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, "exact"); +} + +function mustSucceed(fn, exact) { + return mustCall(function (err, ...args) { + assert.ifError(err); + if (typeof fn === "function") return fn.apply(this, args); + }, exact); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, "minimum"); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error("Cannot use common.mustCall*() in process exit handler"); + if (typeof fn === "number") { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== "number") + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + const context = { + [field]: criteria, + actual: 0, + stack: inspect(new Error()), + name: fn.name || "", + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on("exit", runCallChecks); + + mustCallChecks.push(context); + + const _return = function () { + // eslint-disable-line func-style + context.actual++; + return fn.apply(this, arguments); + }; + // Function instances have own properties that may be relevant. + // Let's replicate those properties to the returned function. + // Refs: https://tc39.es/ecma262/#sec-function-instances + Object.defineProperties(_return, { + name: { + value: fn.name, + writable: false, + enumerable: false, + configurable: true, + }, + length: { + value: fn.length, + writable: false, + enumerable: false, + configurable: true, + }, + }); + return _return; +} + +const strictEqual = (...args) => { + let error = null; + try { + assertNode.strictEqual(...args); + } catch (err) { + error = err; + } + expect(error).toBe(null); +}; + +const throws = (...args) => { + let error = null; + try { + assertNode.throws(...args); + } catch (err) { + error = err; + } + expect(error).toBe(null); +}; + +const assert = (...args) => { + let error = null; + try { + assertNode(...args); + } catch (err) { + error = err; + } + expect(error).toBe(null); +}; + +const assertOk = (...args) => { + let error = null; + try { + assertNode.ok(...args); + } catch (err) { + error = err; + } + expect(error).toBe(null); +}; + +describe("ChildProcess.constructor", () => { + it("should be a function", () => { + strictEqual(typeof ChildProcess, "function"); + }); +}); + +describe("ChildProcess.spawn()", () => { + it("should throw on invalid options", () => { + // Verify that invalid options to spawn() throw. + const child = new ChildProcess(); + + [undefined, null, "foo", 0, 1, NaN, true, false].forEach((options) => { + throws( + () => { + child.spawn(options); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + // message: + // 'The "options" argument must be of type object.' + + // `${common.invalidArgTypeHelper(options)}`, + } + ); + }); + }); + + it("should throw if file is not a string", () => { + // Verify that spawn throws if file is not a string. + const child = new ChildProcess(); + [undefined, null, 0, 1, NaN, true, false, {}].forEach((file) => { + throws( + () => { + child.spawn({ file }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + // message: + // 'The "options.file" property must be of type string.' + + // `${common.invalidArgTypeHelper(file)}`, + } + ); + }); + }); + + it("should throw if envPairs is not an array or undefined", () => { + // Verify that spawn throws if envPairs is not an array or undefined. + const child = new ChildProcess(); + + [null, 0, 1, NaN, true, false, {}, "foo"].forEach((envPairs) => { + throws( + () => { + child.spawn({ + envPairs, + stdio: ["ignore", "ignore", "ignore", "ipc"], + }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + // message: + // 'The "options.envPairs" property must be an instance of Array.' + + // common.invalidArgTypeHelper(envPairs), + } + ); + }); + }); + + it("should throw if stdio is not an array or undefined", () => { + // Verify that spawn throws if args is not an array or undefined. + const child = new ChildProcess(); + + [null, 0, 1, NaN, true, false, {}, "foo"].forEach((args) => { + throws( + () => { + child.spawn({ file: "foo", args }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + // message: + // 'The "options.args" property must be an instance of Array.' + + // common.invalidArgTypeHelper(args), + } + ); + }); + }); +}); + +describe("ChildProcess.spawn", () => { + const child = new ChildProcess(); + child.spawn({ + file: "bun", + // file: process.execPath, + args: ["--interactive"], + cwd: process.cwd(), + stdio: "pipe", + }); + + it("should spawn a process", () => { + // Test that we can call spawn + + strictEqual(Object.hasOwn(child, "pid"), true); + assert(Number.isInteger(child.pid)); + }); + + it("should throw error on invalid signal", () => { + // Try killing with invalid signal + throws( + () => { + child.kill("foo"); + }, + { code: "ERR_UNKNOWN_SIGNAL", name: "TypeError" } + ); + }); + + it("should die when killed", () => { + strictEqual(child.kill(), true); + }); +}); + +describe("ChildProcess spawn bad stdio", () => { + // Monkey patch spawn() to create a child process normally, but destroy the + // stdout and stderr streams. This replicates the conditions where the streams + // cannot be properly created. + const original = ChildProcess.prototype.spawn; + + ChildProcess.prototype.spawn = function () { + const err = original.apply(this, arguments); + + this.stdout.destroy(); + this.stderr.destroy(); + this.stdout = null; + this.stderr = null; + + return err; + }; + + function createChild(options, callback) { + const cmd = `"${process.execPath}" "${import.meta.path}" child`; + return exec(cmd, options, mustCall(callback)); + } + + it("should handle normal execution of child process", () => { + createChild({}, (err, stdout, stderr) => { + strictEqual(err, null); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }); + }); + + it("should handle error event of child process", () => { + const error = new Error("foo"); + const child = createChild({}, (err, stdout, stderr) => { + strictEqual(err, error); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }); + + child.emit("error", error); + }); + + it("should handle killed process", () => { + createChild({ timeout: 1 }, (err, stdout, stderr) => { + strictEqual(err.killed, true); + strictEqual(stdout, ""); + strictEqual(stderr, ""); + }); + }); + + ChildProcess.prototype.spawn = original; +}); + +describe("child_process cwd", () => { + const tmpdir = { path: Bun.env.TMPDIR }; + + // Spawns 'pwd' with given options, then test + // - whether the child pid is undefined or number, + // - whether the exit code equals expectCode, + // - optionally whether the trimmed stdout result matches expectData + function testCwd(options, expectPidType, expectCode = 0, expectData) { + const child = spawn(...common.pwdCommand, options); + + strictEqual(typeof child.pid, expectPidType); + + child.stdout.setEncoding("utf8"); + + // No need to assert callback since `data` is asserted. + let data = ""; + child.stdout.on("data", function (chunk) { + data += chunk; + }); + + // Can't assert callback, as stayed in to API: + // _The 'exit' event may or may not fire after an error has occurred._ + child.on("exit", function (code, signal) { + strictEqual(code, expectCode).bind(this); + }); + + child.on( + "close", + mustCall(function () { + expectData && strictEqual(data.trim(), expectData); + }) + ); + + return child; + } + + // TODO: Make sure this isn't important + // Currently Bun.spawn will still spawn even though cwd doesn't exist + // // Assume does-not-exist doesn't exist, expect exitCode=-1 and errno=ENOENT + // it("should throw an error when given cwd doesn't exist", () => { + // testCwd({ cwd: "does-not-exist" }, "undefined", -1).on( + // "error", + // mustCall(function (e) { + // console.log(e); + // strictEqual(e.code, "ENOENT"); + // }) + // ); + // }); + + // TODO: Make sure this isn't an important test + // it("should throw when cwd is a non-file url", () => { + // throws(() => { + // testCwd( + // { + // cwd: new URL("http://example.com/"), + // }, + // "number", + // 0, + // tmpdir.path + // ); + // }, /The URL must be of scheme file/); + + // // if (process.platform !== "win32") { + // // throws(() => { + // // testCwd( + // // { + // // cwd: new URL("file://host/dev/null"), + // // }, + // // "number", + // // 0, + // // tmpdir.path + // // ); + // // }, /File URL host must be "localhost" or empty on/); + // // } + // }); + + it("should work for valid given cwd", () => { + // Assume these exist, and 'pwd' gives us the right directory back + testCwd({ cwd: tmpdir.path }, "number", 0, tmpdir.path); + const shouldExistDir = "/dev"; + testCwd({ cwd: shouldExistDir }, "number", 0, shouldExistDir); + testCwd({ cwd: Bun.pathToFileURL(tmpdir.path) }, "number", 0, tmpdir.path); + }); + + it("shouldn't try to chdir to an invalid cwd", () => { + // Spawn() shouldn't try to chdir() to invalid arg, so this should just work + testCwd({ cwd: "" }, "number"); + testCwd({ cwd: undefined }, "number"); + testCwd({ cwd: null }, "number"); + }); +}); + +describe("child_process default options", () => { + process.env.HELLO = "WORLD"; + + let child = spawn("/usr/bin/env", [], {}); + let response = ""; + + child.stdout.setEncoding("utf8"); + + it("should use process.env as default env", () => { + child.stdout.on("data", function (chunk) { + debug(`stdout: ${chunk}`); + response += chunk; + }); + + process.on("exit", function () { + assertOk( + response.includes("HELLO=WORLD"), + "spawn did not use process.env as default " + + `(process.env.HELLO = ${process.env.HELLO})` + ); + }); + }); + + delete process.env.HELLO; +}); + +describe("child_process double pipe", () => { + let grep, sed, echo; + grep = spawn("grep", ["o"]); + sed = spawn("sed", ["s/o/O/"]); + echo = spawn("echo", ["hello\nnode\nand\nworld\n"]); + + it("should allow two pipes to be used at once", () => { + // pipe echo | grep + echo.stdout.on( + "data", + mustCallAtLeast((data) => { + debug(`grep stdin write ${data.length}`); + if (!grep.stdin.write(data)) { + echo.stdout.pause(); + } + }) + ); + + // TODO(Derrick): We don't implement the full API for this yet, + // So stdin has no 'drain' event. + // // TODO(@jasnell): This does not appear to ever be + // // emitted. It's not clear if it is necessary. + // grep.stdin.on("drain", (data) => { + // echo.stdout.resume(); + // }); + + // Propagate end from echo to grep + echo.stdout.on( + "end", + mustCall((code) => { + grep.stdin.end(); + }) + ); + + echo.on( + "exit", + mustCall(() => { + debug("echo exit"); + }) + ); + + grep.on( + "exit", + mustCall(() => { + debug("grep exit"); + }) + ); + + sed.on( + "exit", + mustCall(() => { + debug("sed exit"); + }) + ); + + // pipe grep | sed + grep.stdout.on( + "data", + mustCallAtLeast((data) => { + debug(`grep stdout ${data.length}`); + if (!sed.stdin.write(data)) { + grep.stdout.pause(); + } + }) + ); + + // // TODO(@jasnell): This does not appear to ever be + // // emitted. It's not clear if it is necessary. + // sed.stdin.on("drain", (data) => { + // grep.stdout.resume(); + // }); + + // Propagate end from grep to sed + grep.stdout.on( + "end", + mustCall((code) => { + debug("grep stdout end"); + sed.stdin.end(); + }) + ); + + let result = ""; + + // print sed's output + sed.stdout.on( + "data", + mustCallAtLeast((data) => { + result += data.toString("utf8", 0, data.length); + debug(data); + }) + ); + + sed.stdout.on( + "end", + mustCall((code) => { + strictEqual(result, `hellO${EOL}nOde${EOL}wOrld${EOL}`); + }) + ); + }); +}); diff --git a/test/bun.js/child_process.test.ts b/test/bun.js/child_process.test.ts new file mode 100644 index 00000000000000..fd1c27ae7a8ded --- /dev/null +++ b/test/bun.js/child_process.test.ts @@ -0,0 +1,279 @@ +import { describe, it, expect } from "bun:test"; +import { + ChildProcess, + spawn, + execFile, + exec, + fork, + spawnSync, + execFileSync, + execSync, +} from "node:child_process"; + +// Semver regex: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39?permalink_comment_id=2896416#gistcomment-2896416 +// Not 100% accurate, but good enough for this test +const SEMVER_REGEX = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z\d][-a-zA-Z.\d]*)?(\+[a-zA-Z\d][-a-zA-Z.\d]*)?$/; + +describe("ChildProcess.spawn()", () => { + it("should emit `spawn` on spawn", async () => { + const proc = new ChildProcess(); + const result = await new Promise((resolve) => { + proc.on("spawn", () => { + resolve(true); + }); + proc.spawn({ file: "bun", args: ["bun", "-v"] }); + }); + expect(result).toBe(true); + }); + + it("should emit `exit` when killed", async () => { + const proc = new ChildProcess(); + const result = await new Promise((resolve) => { + proc.on("exit", () => { + resolve(true); + }); + + proc.spawn({ file: "bun", args: ["bun", "-v"] }); + proc.kill(); + }); + expect(result).toBe(true); + }); +}); + +describe("spawn()", () => { + it("should spawn a process", () => { + const child = spawn("echo", ["hello"]); + expect(!!child).toBe(true); + }); + + it("should disallow invalid filename", () => { + let child; + let child2; + try { + child = spawn(123); + child2 = spawn(["echo", "hello"]); + } catch (e) { + console.error(e); + } + expect(!!child).toBe(false); + expect(!!child2).toBe(false); + }); + + it("should allow stdout to be read via Node stream.Readable `data` events", async () => { + const child = spawn("bun", ["-v"]); + const result: string = await new Promise((resolve) => { + child.stdout.on("error", (e) => { + console.error(e); + }); + child.stdout.on("data", (data) => { + console.log(`stdout: ${data}`); + resolve(data.toString()); + }); + child.stderr.on("data", (data) => { + console.log(`stderr: ${data}`); + }); + }); + expect(SEMVER_REGEX.test(result.trim())).toBe(true); + }); + + it("should allow stdout to be read via .read() API", async () => { + const child = spawn("bun", ["-v"]); + const result: string = await new Promise((resolve) => { + let finalData = ""; + child.stdout.on("error", (e) => { + console.error(e); + }); + child.stdout.on("readable", () => { + let data; + + while ((data = child.stdout.read()) !== null) { + finalData += data.toString(); + } + resolve(finalData); + }); + }); + expect(SEMVER_REGEX.test(result.trim())).toBe(true); + }); + + it("should accept stdio option with 'ignore' for no stdio fds", async () => { + const child1 = spawn("bun", ["-v"], { + stdio: "ignore", + }); + const child2 = spawn("bun", ["-v"], { + stdio: ["ignore", "ignore", "ignore"], + }); + + expect(!!child1).toBe(true); + expect(child1.stdin).toBe(null); + expect(child1.stdout).toBe(null); + expect(child1.stderr).toBe(null); + + expect(!!child2).toBe(true); + expect(child2.stdin).toBe(null); + expect(child2.stdout).toBe(null); + expect(child2.stderr).toBe(null); + }); + + it("should allow us to set cwd", async () => { + const child = spawn("pwd", { cwd: process.env.TMPDIR }); + const result: string = await new Promise((resolve) => { + child.stdout.on("data", (data) => { + resolve(data.toString()); + }); + }); + const platformTmpDir = `${process.platform === "darwin" ? "/private" : ""}${ + process.env.TMPDIR + }`; + expect(`${result.trim()}/`).toBe(platformTmpDir); + }); + + it("should allow us to write to stdin", async () => { + const child = spawn("tee"); + const result: string = await new Promise((resolve) => { + child.stdin.write("hello"); + child.stdout.on("data", (data) => { + resolve(data.toString()); + }); + }); + expect(result.trim()).toBe("hello"); + }); + + it("should allow us to timeout hanging processes", async () => { + const child = spawn("sleep", ["2"], { timeout: 400 }); + const start = performance.now(); + let end; + await new Promise((resolve) => { + child.on("exit", () => { + end = performance.now(); + resolve(true); + }); + }); + expect(end - start < 2000).toBe(true); + }); + + it("should allow us to set env", async () => { + const child = spawn("env", { env: { TEST: "test" } }); + const result: string = await new Promise((resolve) => { + child.stdout.on("data", (data) => { + resolve(data.toString()); + }); + }); + expect(/TEST\=test/.test(result)).toBe(true); + }); + + it("should allow explicit setting of argv0", async () => { + const child = spawn("node", ["--help"], { argv0: "bun" }); + const result: string = await new Promise((resolve) => { + let msg; + child.stdout.on("data", (data) => { + msg += data.toString(); + }); + + child.stdout.on("close", () => { + resolve(msg); + }); + }); + expect(/bun:/.test(result)).toBe(true); + }); + + it("should allow us to spawn in a shell", async () => { + const result1: string = await new Promise((resolve) => { + const child1 = spawn("echo", ["$0"], { shell: true }); + child1.stdout.on("data", (data) => { + resolve(data.toString()); + }); + }); + const result2: string = await new Promise((resolve) => { + const child2 = spawn("echo", ["$0"], { shell: "bash" }); + child2.stdout.on("data", (data) => { + resolve(data.toString()); + }); + }); + expect(result1.trim()).toBe("/bin/sh"); + expect(result2.trim()).toBe("/bin/bash"); + }); + it("should spawn a process synchronously", () => { + const { stdout } = spawnSync("echo", ["hello"], { encoding: "utf8" }); + expect(stdout.trim()).toBe("hello"); + }); +}); + +describe("execFile()", () => { + it("should execute a file", async () => { + const result: Buffer = await new Promise((resolve, reject) => { + execFile("bun", ["-v"], (error, stdout, stderr) => { + if (error) { + reject(error); + } + resolve(stdout); + }); + }); + expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true); + }); +}); + +describe("exec()", () => { + it("should execute a command in a shell", async () => { + const result: Buffer = await new Promise((resolve, reject) => { + exec("bun -v", (error, stdout, stderr) => { + if (error) { + reject(error); + } + resolve(stdout); + }); + }); + expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true); + }); +}); + +describe("fork()", () => { + it("should throw an error when used", () => { + let err; + try { + fork("index.js"); + } catch (e) { + err = e; + } + expect(err instanceof Error).toBe(true); + }); +}); + +describe("spawnSync()", () => { + it("should spawn a process synchronously", () => { + const { stdout } = spawnSync("echo", ["hello"], { encoding: "utf8" }); + expect(stdout.trim()).toBe("hello"); + }); +}); + +describe("execFileSync()", () => { + it("should execute a file synchronously", () => { + const result = execFileSync("bun", ["-v"], { encoding: "utf8" }); + expect(SEMVER_REGEX.test(result.trim())).toBe(true); + }); +}); + +describe("execSync()", () => { + it("should execute a command in the shell synchronously", () => { + const result = execSync("bun -v", { encoding: "utf8" }); + expect(SEMVER_REGEX.test(result.trim())).toBe(true); + }); +}); + +// describe("Bun.spawn()", () => { +// it("should return exit code 0 on successful execution", async () => { +// const result = await new Promise((resolve) => { +// Bun.spawn({ +// cmd: ["echo", "hello"], +// encoding: "utf8", +// onExit: (code) => resolve(code), +// stdout: "inherit", +// }); +// }); +// expect(result).toBe(0); +// }); +// it("should fail when given an invalid cwd", () => { +// const child = Bun.spawn({ cmd: ["echo", "hello"], cwd: "/invalid" }); +// expect(child.pid).toBe(undefined); +// }); +// });