Here is another example using ffi-napi, which works on Node 20:
https://gist.github.com/oxc/b91f02b55f4973910e5274a26694238d
It was inspired by the existing answers to this question by user1629060 (using ffi) and oleksii-rudenko (removing the "exit on close" flag from the streams).
I only needed execv, but implementing any of the other variants should work equivalently.
Note that args is the full array and needs to include arg0 as first element. It does NOT copy path as first element like in other examples here.
import ref from "ref-napi"; import ffi from "ffi-napi"; import ref_array_di from "ref-array-di"; const ArrayType = ref_array_di(ref); const StringArray = ArrayType("string"); // from fcntl.h const F_GETFD = 1; /* get close_on_exec */ const F_SETFD = 2; /* set/clear close_on_exec */ const FD_CLOEXEC = 1; /* actually anything with low bit set goes */ export function execv(path: string, args: string[]): number | never { const current = ffi.Library(null, { execv: ["int", ["string", StringArray]], fcntl: ["int", ["int", "int", "int"]], }); function dontCloseOnExit(fd: number) { let flags = current.fcntl(fd, F_GETFD, 0); if (flags < 0) return flags; flags &= ~FD_CLOEXEC; //clear FD_CLOEXEC bit return current.fcntl(fd, F_SETFD, flags); } const argsArray = new StringArray(args.length + 1); for (let i = 0; i < args.length; i++) { argsArray[i] = args[i]; } argsArray[args.length] = null; dontCloseOnExit(process.stdin.fd); dontCloseOnExit(process.stdout.fd); dontCloseOnExit(process.stderr.fd); return current.execv(path, argsArray); }