@xarc/run
Version:
concurrent or serial run npm scripts, javascript tasks, and more
316 lines (280 loc) • 8.35 kB
JavaScript
"use strict";
const assert = require("assert");
const defaults = require("./defaults");
const chalk = require("chalk");
const XQtor = require("./xqtor");
const XTasks = require("./xtasks");
const XQTree = require("./xqtree");
const logger = require("./logger");
const EventEmitter = require("events");
const printTasks = require("./print-tasks");
const jaroWinkler = require("jaro-winkler");
const XTaskSpec = require("./xtask-spec");
const xsh = require("xsh");
const updateEnv = require("./util/update-env");
const myPkg = require("../package.json");
const { CliContext } = require("./cli-context");
const { exec } = require("child_process");
// full - full stop, interrupting all pending async tasks
// soft - allow pending async tasks to complete, but no more new tasks
// "" - march on
const STOP_ON_ERROR = ["", "soft", "full"];
function _decorateTasks(type, name, ...tasks) {
assert(tasks.length > 0, `${name} no tasks passed`);
tasks = tasks.filter(x => x);
if (tasks.length === 1) {
tasks = tasks[0];
// user passed single argument that's not an array
// assume it's a single task where serial/concurrent is N/A
if (!Array.isArray(tasks)) return tasks;
}
return [type].concat(tasks);
}
function _concurrent(...tasks) {
return _decorateTasks(defaults.CONCURRENT_SYM, "xrun.concurrent", ...tasks);
}
class XRun extends EventEmitter {
constructor(namespace, tasks) {
super();
this._tasks = new XTasks(namespace, tasks);
this.failed = null;
this.xqTree = new XQTree();
this._logger = logger;
this._pending = 0;
this._taskChildren = {};
this._isStop = false;
this._cliContext = new CliContext({});
this.on("spawn-async", () => this._pending++);
this.on("done-async", () => this._pending--);
}
get stopOnError() {
return this._stopOnError;
}
set stopOnError(v) {
if (v === false) {
this._stopOnError = "";
} else if (v === true) {
this._stopOnError = "full";
} else {
assert(
STOP_ON_ERROR.indexOf(v) >= 0,
`stopOnError must be true or false, or one of ${STOP_ON_ERROR.map(JSON.stringify).join(
", "
)}`
);
this._stopOnError = v;
}
}
// priority: lower value => lower priority when searching namespace for a task
load(namespace, tasks, priority = 1) {
this._tasks.load(namespace, tasks, priority);
return this;
}
addTaskChild(child, sym) {
this._taskChildren[sym] = child;
return this._taskChildren;
}
removeTaskChild(child, sym) {
delete this._taskChildren[sym];
return this._taskChildren;
}
/**
* Set the CLI context for this XRun instance
* @param {CliContext} cliContext - Command context object
* @returns {XRun} This instance for chaining
*/
setCliContext(cliContext) {
this._cliContext = cliContext;
return this;
}
/**
* Get the current CLI context
* @returns {CliContext|null} Current CLI context
*/
getCliContext() {
return this._cliContext;
}
asyncRun(tasks) {
return new Promise((resolve, reject) => {
this.run(tasks, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
run(tasks, done) {
if (this.stopOnError === undefined) {
this.stopOnError = true;
}
if (this._tasks.hasFinally() && this.stopOnError === "full") {
this.emit("warn-finally");
}
done = done || this._exitOnError.bind(this);
const xqtor = new XQtor({ tasks: this._tasks, done, xrun: this });
try {
if (typeof tasks === "string") {
this.emit("run", { name: tasks });
xqtor.pushItem(this.xqTree.create({ name: tasks }));
} else {
this.emit("run", { tasks: tasks });
xqtor.pushItem(this.xqTree.create({ name: "run", value: tasks }));
}
xqtor.next();
} catch (err) {
done([err]);
}
return this;
}
printTasks() {
printTasks(this._tasks);
return this;
}
countTasks() {
return this._tasks.count();
}
getNamespaces() {
return this._tasks._namespaces;
}
fail(err) {
if (!this.failed) {
this.failed = err;
} else {
this.failed.more = this.failed.more || [];
this.failed.more.push(err);
}
return this;
}
waitAllPending(done) {
const wait = () => {
if (this._pending === 0) {
return done();
}
setTimeout(wait, 10);
};
process.nextTick(wait);
return this;
}
_showSimilarTasks(res) {
const names = this._tasks.fullNames();
const distances = names
.map(name => {
name = name.split("/")[1];
if (name.startsWith(".")) return false;
const dis = jaroWinkler(res.name, name) * 100000;
return { dis, name };
})
.filter(x => x)
.sort((a, b) => b.dis - a.dis);
const similars = distances.slice(0, 5).map(x => chalk.cyan(x.name));
console.log(` Maybe try: ${similars.join(", ")}`);
}
_exitOnError(err) {
if (!err) {
return;
}
console.log(chalk.bold.red.inverse(" Execution Failed - Errors: "));
const errors = [].concat(err, err.more).filter(x => x);
errors.forEach((e, x) => {
const idx = chalk.bold.red.inverse(` ${x + 1} `);
if (!e.stack) {
console.log(idx, e);
return;
}
const lines = e.stack.split("\n");
console.log(idx, e.message);
if (e.name && e.name.indexOf("AssertionError") >= 0) return;
if (e.code === "TASK_NOT_FOUND") {
this._showSimilarTasks(e.res);
return;
}
if (lines.length < 2) return;
if (lines[1].indexOf("xsh/lib/exec.js") >= 0) return;
const cleaned = lines.filter(l => {
return (
// exclude stack for node.js internal modules and xrun's own files
!l.includes(`(internal/`) &&
!l.includes("/node_modules/xclap/lib/") &&
!l.includes(`/node_modules/${myPkg.name}/lib`)
);
});
// start at 1 to exclude the message that's the first line in stack output
for (let i = 1; i < cleaned.length; i++) {
console.log(
chalk.gray(
/* istanbul ignore next */
process.cwd().length > 2 ? xsh.pathCwd.replace(cleaned[i], ".", "g") : cleaned[i]
)
);
}
});
this.exit(1);
}
exit(code) {
if (this.stopOnError) {
process.exit(code);
}
}
killTaskChildren() {
setTimeout(() => {
const children = this._taskChildren;
this._taskChildren = {};
const symbols = Object.getOwnPropertySymbols(children);
symbols.forEach(sym => {
const child = children[sym];
/* istanbul ignore next */
if (process.platform === "win32") {
try {
/* istanbul ignore next */
exec(`taskkill /T /F /PID ${child.pid}`);
} catch (err) {
/* istanbul ignore next */
child.kill();
}
} else {
child.kill();
}
child[defaults.STOP_SYM] = true;
});
}, 10);
}
actStop() {
this.killTaskChildren();
this._isStop = true;
}
stop() {
return defaults.STOP_SYM;
}
exec(spec, options) {
if (Array.isArray(spec) || typeof spec === "string") {
if (typeof options === "string" || Array.isArray(options)) {
options = { flags: options };
}
return new XTaskSpec(Object.assign({ cmd: spec }, options));
} else if (typeof spec === "object") {
return new XTaskSpec(spec);
} else {
throw new Error(
`xrun.exec - unknown spec type ${typeof spec}: must be a string, array, or an object`
);
}
}
updateEnv(envValues, options = {}) {
return updateEnv(envValues, options.target || process.env, options.override);
}
env(spec, options) {
return new XTaskSpec(Object.assign({ ...options, type: "env", env: spec }));
}
// concurrent([task1, task2, ...]) or concurrent(task1, task2, ...)
concurrent(...tasks) {
return _concurrent(...tasks);
}
// alias for concurrent
parallel(...tasks) {
return _concurrent(...tasks);
}
// serial([task1, task2, ...]) or concurrent(task1, task2, ...)
serial(...tasks) {
return _decorateTasks(defaults.SERIAL_SYM, "xrun.serial", ...tasks);
}
}
module.exports = XRun;