jazelle/utils/node-helpers.js (95 lines of code) (raw):
// @flow
const proc = require('child_process');
const {promisify} = require('util');
const {tmpdir} = require('os');
const {
readFile,
writeFile,
access,
readdir,
mkdir: makeDir,
lstat,
realpath,
} = require('fs');
/*::
import {Writable, Readable, Duplex} from 'stream';
export type Exec = (string, ExecOptions, ?StdioOptions) => Promise<string>;
export type ExecOptions = void | {
// https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
cwd?: string,
env?: {[string]: ?string},
encoding?: string,
shell?: string,
timeout?: number,
maxBuffer?: number,
killSignal?: string | number,
uid?: number,
gid?: number,
windowsHide?:boolean,
};
export type StdioOptions = Array<Writable>;
*/
// use exec if you need stdout as a string, or if you need to explicitly setup shell in some way (e.g. export an env var)
const exec /*: Exec */ = (cmd, opts = {}, stdio = []) => {
const errorWithSyncStackTrace = new Error(); // grab stack trace outside of promise so errors are easier to narrow down
return new Promise((resolve, reject) => {
const child = proc.exec(cmd, opts, (err, stdout, stderr) => {
if (err) {
errorWithSyncStackTrace.message = err.message;
reject(errorWithSyncStackTrace);
} else {
resolve(String(stdout));
}
process.off('exit', onExit);
});
if (stdio) {
if (stdio[0]) child.stdout.pipe(stdio[0]);
if (stdio[1]) child.stderr.pipe(stdio[1]);
}
function onExit() {
// $FlowFixMe flow typedef is missing .exitCode
if (child.exitCode === null) child.kill();
}
process.on('exit', onExit);
});
};
/*::
export type Spawn = (string, Array<string>, SpawnOptions) => Promise<void>;
export type SpawnOptions = void | {
// https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
cwd?: string,
env?: {[string]: ?string},
argv0?: string,
stdio?: Stdio,
detached?: boolean,
uid?: number,
gid?: number,
shell?: boolean | string,
windowsVerbatimArguments?: boolean,
windowsHide?: boolean,
};
export type Stdio = string | Array<string | number | null | Writable | Readable | Duplex>;
*/
// use spawn if you just need to run a command for its side effects, or if you want to pipe output straight back to the parent shell
const spawn /*: Spawn */ = (cmd, argv, opts) => {
const errorWithSyncStackTrace = new Error();
return new Promise((resolve, reject) => {
const child = proc.spawn(cmd, argv, opts);
child.on('error', e => {
reject(new Error(e));
});
child.on('close', code => {
if (code > 0) {
const args = argv.join(' ');
const cwd = opts && opts.cwd ? `at ${opts.cwd} ` : '';
errorWithSyncStackTrace.message = `Process failed ${cwd}with exit code ${code}: ${cmd} ${args}`;
reject(errorWithSyncStackTrace);
}
resolve();
});
process.on('exit', () => {
// $FlowFixMe flow typedef is missing .exitCode
if (child.exitCode === null) child.kill();
});
});
};
const accessFile = promisify(access);
/*::
export type Exists = (string) => Promise<boolean>;
*/
const exists /*: Exists */ = filename =>
accessFile(filename)
.then(() => true)
.catch(() => false);
const read = promisify(readFile);
const write = promisify(writeFile);
const ls = promisify(readdir);
const mkdir = promisify(makeDir);
const lstatP = promisify(lstat);
const realpathP = promisify(realpath);
/*::
export type Move = (string, string) => Promise<void>
*/
const move /*: Move */ = async (from, to) => {
await spawn('mv', [from, to]); // fs.rename can't move across devices/partitions so it can die w/ EXDEV error
};
/*::
export type Remove = (string) => Promise<void>;
*/
const remove /*: Remove */ = async dir => {
const tmp = `${tmpdir()}/${Math.random() * 1e17}`;
// $FlowFixMe flow can't handle statics of async function
const fork = remove.fork;
if (await exists(dir)) {
await exec(`mkdir -p ${tmp} && mv ${dir} ${tmp}`);
const child = proc.spawn('rm', ['-rf', tmp], {
detached: fork,
stdio: 'ignore',
});
if (fork) child.unref();
}
};
// $FlowFixMe flow can't handle statics of async function
remove.fork = true;
module.exports = {
exec,
spawn,
exists,
read,
write,
remove,
ls,
mkdir,
move,
lstat: lstatP,
realpath: realpathP,
};