143 lines · 4.0 KB · TypeScript
RawDownload
import { spawn } from "node:child_process"; export type DenoPermissions = {  /** Allow network access. Pass `true` for all, or an array of allowed hosts. */  allowNet?: boolean | string[];  /** Allow file read access. Pass `true` for all, or an array of allowed paths. */  allowRead?: boolean | string[];  /** Allow file write access. Pass `true` for all, or an array of allowed paths. */  allowWrite?: boolean | string[];  /** Allow environment variable access. Pass `true` for all, or an array of allowed vars. */  allowEnv?: boolean | string[];  /** Allow running subprocesses. Pass `true` for all, or an array of allowed commands. */  allowRun?: boolean | string[];  /** Allow FFI (foreign function interface). */  allowFfi?: boolean | string[];}; export type SpawnDenoWorkerProcessInput = {  executable: string;  scriptPath: string;  permissions?: DenoPermissions;}; export type DenoWorkerProcessCallbacks = {  onStdoutLine: (line: string) => void;  onStderr: (chunk: string) => void;  onError: (error: Error) => void;  onExit: (code: number | null, signal: NodeJS.Signals | null) => void;}; export type DenoWorkerProcess = {  stdin: NodeJS.WritableStream;  dispose: () => void;}; const normalizeError = (cause: unknown): Error =>  cause instanceof Error ? cause : new Error(String(cause)); const buildPermissionArgs = (permissions?: DenoPermissions): string[] => {  if (!permissions) {    return ["--deny-net", "--deny-read", "--deny-write", "--deny-env", "--deny-run", "--deny-ffi"];  }   const args: string[] = [];   const addPermission = (flag: string, value: boolean | string[] | undefined) => {    if (value === true) {      args.push(`--allow-${flag}`);    } else if (Array.isArray(value) && value.length > 0) {      args.push(`--allow-${flag}=${value.join(",")}`);    } else {      args.push(`--deny-${flag}`);    }  };   addPermission("net", permissions.allowNet);  addPermission("read", permissions.allowRead);  addPermission("write", permissions.allowWrite);  addPermission("env", permissions.allowEnv);  addPermission("run", permissions.allowRun);  addPermission("ffi", permissions.allowFfi);   return args;}; export const spawnDenoWorkerProcess = (  input: SpawnDenoWorkerProcessInput,  callbacks: DenoWorkerProcessCallbacks,): DenoWorkerProcess => {  const permissionArgs = buildPermissionArgs(input.permissions);   const child = spawn(    input.executable,    ["run", "--quiet", "--no-prompt", "--no-check", ...permissionArgs, input.scriptPath],    {      stdio: ["pipe", "pipe", "pipe"],    },  );   if (!child.stdin || !child.stdout || !child.stderr) {    throw new Error("Failed to create piped stdio for Deno worker subprocess");  }   child.stdout.setEncoding("utf8");  child.stderr.setEncoding("utf8");   let stdoutBuffer = "";   const onStdoutData = (chunk: string) => {    stdoutBuffer += chunk;     while (true) {      const newlineIndex = stdoutBuffer.indexOf("\n");      if (newlineIndex === -1) {        break;      }       const line = stdoutBuffer.slice(0, newlineIndex);      stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);      callbacks.onStdoutLine(line);    }  };   const onStderrData = (chunk: string) => {    callbacks.onStderr(chunk);  };   const onError = (cause: unknown) => {    callbacks.onError(normalizeError(cause));  };   const onExit = (code: number | null, signal: NodeJS.Signals | null) => {    callbacks.onExit(code, signal);  };   child.stdout.on("data", onStdoutData);  child.stderr.on("data", onStderrData);  child.on("error", onError);  child.on("exit", onExit);   let disposed = false;   const dispose = () => {    if (disposed) {      return;    }     disposed = true;    child.stdout!.removeListener("data", onStdoutData);    child.stderr!.removeListener("data", onStderrData);    child.removeListener("error", onError);    child.removeListener("exit", onExit);     if (!child.killed) {      child.kill("SIGKILL");    }  };   return {    stdin: child.stdin,    dispose,  };};