repo_description: Package your Node.js project into an executable
repo_topics: binaries cli compiler nodejs
code: import { spawn, ChildProcessByStdio } from 'child_process';
  import { Readable, Writable } from 'stream';
  import { log } from './log';
  import { Target } from './types';
  
  const script = `
    var vm = require('vm');
    var module = require('module');
    var stdin = Buffer.alloc(0);
    process.stdin.on('data', function (data) {
      stdin = Buffer.concat([ stdin, data ]);
      if (stdin.length >= 4) {
        var sizeOfSnap = stdin.readInt32LE(0);
        if (stdin.length >= 4 + sizeOfSnap + 4) {
          var sizeOfBody = stdin.readInt32LE(4 + sizeOfSnap);
          if (stdin.length >= 4 + sizeOfSnap + 4 + sizeOfBody) {
            var snap = stdin.toString('utf8', 4, 4 + sizeOfSnap);
            var body = Buffer.alloc(sizeOfBody);
            var startOfBody = 4 + sizeOfSnap + 4;
            stdin.copy(body, 0, startOfBody, startOfBody + sizeOfBody);
            stdin = Buffer.alloc(0);
            var code = module.wrap(body);
            var s = new vm.Script(code, {
              filename: snap,
              produceCachedData: true,
              sourceless: true
            });
            if (!s.cachedDataProduced) {
              console.error('Pkg: Cached data not produced.');
              process.exit(2);
            }
            var h = Buffer.alloc(4);
            var b = s.cachedData;
            h.writeInt32LE(b.length, 0);
            process.stdout.write(h);
            process.stdout.write(b);
          }
        }
      }
    });
    process.stdin.resume();
  `;
  
  const children: Record<
    string,
    ChildProcessByStdio<Writable, Readable, null>
  > = {};
  
  export function fabricate(
    bakes: string[],
    fabricator: Target,
    snap: string,
    body: Buffer,
    cb: (error?: Error, buffer?: Buffer) => void
  ) {
    const activeBakes = bakes.filter((bake) => {
      // list of bakes that don't influence the bytecode
      const bake2 = bake.replace(/_/g, '-');
  
      return !['--prof', '--v8-options', '--trace-opt', '--trace-deopt'].includes(
        bake2
      );
    });
  
    const cmd = fabricator.binaryPath;
    const key = JSON.stringify([cmd, activeBakes]);
    let child = children[key];
  
    if (!child) {
      const stderr = log.debugMode ? process.stdout : 'ignore';
      children[key] = spawn(cmd, activeBakes.concat('-e', script), {
        stdio: ['pipe', 'pipe', stderr],
        env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS' },
      });
      child = children[key];
    }
  
    function kill() {
      delete children[key];
      child.kill();
    }
  
    let stdout = Buffer.alloc(0);
  
    function onError(error: Error) {
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      removeListeners();
      kill();
      cb(
        new Error(
          `Failed to make bytecode ${fabricator.nodeRange}-${fabricator.arch} for file ${snap} error (${error.message})`
        )
      );
    }
  
    function onClose(code: number) {
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      removeListeners();
      kill();
      if (code !== 0) {
        return cb(
          new Error(
            `Failed to make bytecode ${fabricator.nodeRange}-${fabricator.arch} for file ${snap}`
          )
        );
      }
  
      // eslint-disable-next-line no-console
      console.log(stdout.toString());
      return cb(new Error(`${cmd} closed unexpectedly`));
    }
  
    function onData(data: Buffer) {
      stdout = Buffer.concat([stdout, data]);
      if (stdout.length >= 4) {
        const sizeOfBlob = stdout.readInt32LE(0);
        if (stdout.length >= 4 + sizeOfBlob) {
          const blob = Buffer.alloc(sizeOfBlob);
          stdout.copy(blob, 0, 4, 4 + sizeOfBlob);
          // eslint-disable-next-line @typescript-eslint/no-use-before-define
          removeListeners();
          return cb(undefined, blob);
        }
      }
    }
  
    function removeListeners() {
      child.removeListener('error', onError);
      child.removeListener('close', onClose);
      child.stdin.removeListener('error', onError);
      child.stdout.removeListener('error', onError);
      child.stdout.removeListener('data', onData);
    }
  
    child.on('error', onError);
    child.on('close', onClose);
    child.stdin.on('error', onError);
    child.stdout.on('error', onError);
    child.stdout.on('data', onData);
  
    const h = Buffer.alloc(4);
    let b = Buffer.from(snap);
    h.writeInt32LE(b.length, 0);
    child.stdin.write(h);
    child.stdin.write(b);
    b = body;
    h.writeInt32LE(b.length, 0);
    child.stdin.write(h);
    child.stdin.write(b);
  }
  
  export function fabricateTwice(
    bakes: string[],
    fabricator: Target,
    snap: string,
    body: Buffer,
    cb: (error?: Error, buffer?: Buffer) => void
  ) {
    fabricate(bakes, fabricator, snap, body, (error, buffer) => {
      // node0 can not produce second time, even if first time produced fine,
      // probably because of 'filename' cache. also, there are weird cases
      // when node4 can not compile as well, for example file 'lib/js-yaml/dumper.js'
      // of package js-yaml@3.9.0 does not get bytecode second time on node4-win-x64
      if (error) return fabricate(bakes, fabricator, snap, body, cb);
      cb(undefined, buffer);
    });
  }
  
  export function shutdown() {
    for (const key in children) {
      if (children[key]) {
        const child = children[key];
        delete children[key];
        child.kill();
      }
    }
  }
  