peggy
Version:
Parser generator for JavaScript
160 lines (146 loc) • 3.56 kB
JavaScript
;
/**
* @fileoverview Utility functions for CLI
*/
const { InvalidArgumentError } = require("commander");
const assert = require("assert");
const fs = require("fs");
const path = require("path");
/**
* @typedef {import("node:stream").Readable} Readable
*/
/**
* Realistically, in a catch block, the caught thing is an instance of Error.
* When errors are thrown from inside a node vm however, `instanceof` doesn't
* work like we would prefer. Is "er" sufficiently Error-like? If not,
* throw.
*
* @param {unknown} er
* @returns {asserts er is Error}
*/
function isER(er) {
assert(er);
assert.equal(typeof er, "object");
assert(Object.prototype.hasOwnProperty.call(er, "message"));
}
/**
* Is this error a node ErrnoException?
*
* @param {unknown} er
* @returns {er is NodeJS.ErrnoException}
*/
function isErrno(er) {
return (Boolean(er)
&& (typeof er === "object")
&& (Object.prototype.hasOwnProperty.call(er, "code")));
}
/**
* Select certain properties from an object, deleting those properties from
* the original object as we go.
*
* @template {object} T
* @param {T} obj Object to select from
* @param {string[]} sel
* @returns {Pick<T, sel>}
*/
function select(obj, sel) {
const ret = Object.create(null);
for (const s of sel) {
if (Object.prototype.hasOwnProperty.call(obj, s)) {
// @ts-expect-error Object is not a map
ret[s] = obj[s];
// @ts-expect-error Object is not a map
delete obj[s];
}
}
return ret;
}
/**
* Add comma-separated values to array.
*
* @param {string} val Comma-separated
* @param {string[]?} prev Previous value
* @returns {string[]}
*/
function commaArg(val, prev) {
return (prev || []).concat(val.split(",").map(x => x.trim()));
}
/**
* Take a blob of JSON-formatted text, parse it, and add it to an existing
* object.
*
* @param {string} val
* @param {object} [prev = {}]
* @returns {object}
*/
function moreJSON(val, prev = {}) {
try {
const v = JSON.parse(val);
return Object.assign(prev, v);
} catch (e) {
isER(e);
throw new InvalidArgumentError(
`Error parsing JSON: ${e.message}`
);
}
}
// Files
/**
* Read a UTF8-encoded binary stream to completion.
*
* @param {Readable} inputStream
* @returns {Promise<string>}
*/
function readStream(inputStream) {
return new Promise((resolve, reject) => {
/** @type {Buffer[]} */
const input = [];
inputStream.on("data", data => { input.push(data); });
inputStream.on("end", () => resolve(Buffer.concat(input).toString()));
inputStream.on("error", reject);
});
}
/**
* Ensure that a directory exists that will eventually contain a file of a
* given name.
*
* @param {string} filename
* @returns {Promise<void>}
*/
async function mkFileDir(filename) {
const dir = path.dirname(filename);
try {
const stats = await fs.promises.stat(dir);
if (!stats.isDirectory()) {
throw new Error(`"${dir}" exists and is not a directory`);
}
} catch (er) {
if (!isErrno(er) || (er.code !== "ENOENT")) {
throw er;
}
await fs.promises.mkdir(dir, { recursive: true });
}
}
/**
* Replace the file extension on a file with the given one.
*
* @param {string} filename
* @param {string} ext
* @returns {string}
*/
function replaceExt(filename, ext) {
return filename.slice(
0,
filename.length - path.extname(filename).length
) + ext;
}
module.exports = {
commaArg,
mkFileDir,
isER,
isErrno,
moreJSON,
readStream,
replaceExt,
select,
};