aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Derrick Farris <mr.dcfarris@gmail.com> 2022-11-06 15:43:42 -0600
committerGravatar GitHub <noreply@github.com> 2022-11-06 13:43:42 -0800
commitbe108c0fea8cef864c14edd048fde4e8c8f92760 (patch)
treed2f06704d9c5732b03705e6aa3e87e595390bf42
parent4e3fb8ed5bd8d2440513013c4e5483eda8437c5a (diff)
downloadbun-be108c0fea8cef864c14edd048fde4e8c8f92760.tar.gz
bun-be108c0fea8cef864c14edd048fde4e8c8f92760.tar.zst
bun-be108c0fea8cef864c14edd048fde4e8c8f92760.zip
feat(child_process): add node:child_process polyfill (#1424)
* feat(child_process): beginning of child_process, add ChildProcess and spawn base case * fix(child_process): remove invalid single arg array syntax (thanks Copilot) * refactor(child_process): unhack Readable.on, move stuff into node:stream * feat(child_process): add more params for spawn, refactor, add fromWeb() to Readable * feat(child_process): finish rest of exports (minus fork), refactor, add tests * cleanup(streams): remove a bunch of unnecessary stuff * cleanup(child_process): remove dead refs * fix(child_process): fix stdio * fix(child_process): change stdio to bunStdio * test(child_process): uncomment timeout test * test(child_process): fix hanging tests * test(child_process): remove stray console.log * test(child_process): fix cwd test for linux * refactor(child_process): divide paths for encoded vs raw execFile stdio * fix(child_process): fix logic for execFile slow path
-rw-r--r--src/bun.js/child_process.exports.js1711
-rw-r--r--src/bun.js/streams.exports.js199
-rw-r--r--test/bun.js/child_process-node.test.js570
-rw-r--r--test/bun.js/child_process.test.ts279
4 files changed, 2756 insertions, 3 deletions
diff --git a/src/bun.js/child_process.exports.js b/src/bun.js/child_process.exports.js
index 7ed515927..9219e3ab6 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 <number> Sets the user identity of the process (see setuid(2)).
+// gid <number> Sets the group identity of the process (see setgid(2)).
+// detached <boolean> 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 <string> 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 <Array> | <string> 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<string, string>;
+ * 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<string, string>;
+ * 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<string, string>;
+ * 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<string, string>;
+ * 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<string, string>;
+ * 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<string, string>;
+ * 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 979ef32f6..576f3ea89 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 000000000..10135affa
--- /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 || "<anonymous>",
+ };
+
+ // 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 000000000..fd1c27ae7
--- /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);
+// });
+// });