just-build
Version:
A simple task runner that doesn't bloat your package
437 lines (409 loc) • 18.8 kB
JavaScript
var ref = require ('../bundledExternals/bundle');
var Observable = ref.Observable;
var path = require ('path');
var ref$1 = require ('./tokenize');
var tokenize = ref$1.tokenize;
var surroundWithQuotes = ref$1.surroundWithQuotes;
var ref$2 = require ('./extend');
var clone = ref$2.clone;
var ref$3 = require ('./extract-config');
var extractConfig = ref$3.extractConfig;
var ref$4 = require('./extend');
var extend = ref$4.extend;
var ref$5 = require ('./color-transform');
var ColorTransform = ref$5.ColorTransform;
var clr = require ('./console-colors');
var debug = require('./debug');
var COMMENT_COLOR = clr.GREEN;
var EMIT_COLOR = clr.GREEN + clr.BOLD;
var NOW_WATCHING_COLOR = clr.MAGENTA;
var SPECIAL_PROMPT_COLOR = clr.DIM;
var COMMAND_COLOR = clr.CYAN;
/**
* Execute the build tasks.
*
* @param cfg {{
dir: string,
taskSet: Object.<string, string[]>,
tasksToRun: string[],
watchMode: boolean,
spawn: Function,
env: Object,
log: Function,
packageRoot: string
}} Configuration to execute
@return Promise
*/
function executeAll (cfg) {
console.log(((clr.DIM+clr.LIGHT_MAGENTA) + "Package: " + (cfg.packageRoot) + (clr.RESET)));
return new Promise(function (resolve, reject) {
createObservable(cfg).subscribe({
next: function next (ref) {
var command = ref.command;
var exitCode = ref.exitCode;
if (exitCode == 0)
{ cfg.log(EMIT_COLOR + "just-build " + (cfg.tasksToRun.join(' '))+
" done." + (clr.RESET) + (cfg.watchMode ? NOW_WATCHING_COLOR+' Still watching...'+clr.RESET : '')); }
else {
var errText = "just-build " + (cfg.tasksToRun.join(' ')) + " failed. " + command + " returned " + exitCode;
cfg.log(errText);
if (!cfg.watchMode) {
reject(new Error(errText));
}
}
},
error: function error (err) {
reject(err);
},
complete: function complete () {
resolve();
}
});
});
}
/**
* Create a build-task executer as an Observable
*
* @param cfg {{
dir: string,
taskSet: Object.<string, string[]>,
tasksToRun: string[],
watchMode: boolean,
spawn: Function,
env: Object,
log: Function
}} Configuration to execute
@return Observable
*/
function createObservable (cfg) {
var dir = cfg.dir;
var taskSet = cfg.taskSet;
var tasksToRun = cfg.tasksToRun;
var watchMode = cfg.watchMode;
var spawn = cfg.spawn;
var env = cfg.env;
var log = cfg.log;
var tasks = tasksToRun.map(function (taskName) {
var commandList = taskSet[taskName];
if (!commandList)
{ throw new Error (("No such task name: " + taskName + " was configured")); }
return commandList;
});
return createParallellCommandsExecutor (
tasks,
dir,
env,
watchMode,
{spawn: spawn, log: log});
}
/**
* Create an Observable that will execute all tasks in parallell and emit exitCode
* whenever one of the tasks fails, or whenever all tasks emits successful exit code.
* If then a command is rerun and complete successfully, it will emit again.
* The final subscription will complete when (and if) all tasks completes.
*
* @param tasks {string[][]} Array of tasks (sequences of commands) to execute
* @param workingDir {string} Initial Working Directory
* @param envVars {Object} Initial environment variables
* @param watchMode {boolean} Whether to execute watchers or not
* @param host {{spawn: Function, log: Function}} Mockable host environment (mimicking child_process.spawn() and console.log())
@returns Observable
*/
function createParallellCommandsExecutor (tasks, workingDir, envVars, watchMode, host) {
return new Observable(function (observer) {
var exitCodes = tasks.map(function (){ return undefined; });
var completeCount = 0;
tasks.forEach(function (commands, i) {
var observable = createSequencialCommandExecutor(
commands, workingDir, envVars, watchMode, host);
observable.subscribe({
next: function next (ref) {
var command = ref.command;
var exitCode = ref.exitCode;
exitCodes[i] = exitCode || 0;
if (!!exitCode) {
// Partial failure. Emit the error directly to output to console
// which command that failed. May be repaired by a watcher watching changed
// source.
observer.next({command: command, exitCode: exitCode});
return;
}
if (exitCodes.every(function (code) { return code === 0; })) {
observer.next({exitCode: 0});
}
},
error: function error(err) {
observer.error(err);
},
complete: function complete() {
if (++completeCount === tasks.length) {
observer.complete();
}
}
});
});
});
}
/**
* Create an Observable that would execute a sequence of commands.
*
* @param commands {string[]} A sequence of commands to execute
* @param workingDir {string} Initial Working Directory
* @param envVars {Object} Initial environment variables
* @param watchMode {boolean} Whether to execute watchers or not
* @param host {{spawn: Function, log: Function}} Mockable host environment (mimicking child_process.spawn() and console.log())
@returns Observable
*/
function createSequencialCommandExecutor (commands, workingDir, envVars, watchMode, host) {
var source = Observable.from([{
cwd: workingDir,
env: envVars,
exitCode: 0
}]);
return commands.reduce(function (prev, command) { return createCommandExecutor(command, prev, watchMode, host); }, source);
}
/**
* @param command {string} Command line to execute
* @param prevObservable {Observable} Observable source to get values from
* @param watchMode {boolean} Whether to invoke --watch argument
* @param host {{
spawn: Function,
log: Function
}} Host and Configuration.
*/
function createCommandExecutor (command, prevObservable, watchMode, host) {
return new Observable (function (observer) {
var prevComplete = false;
var childProcess = null;
var childSubscription = null; // Treat childSubscription exactly the same way as childProcess.
var prevSubscription = prevObservable.subscribe({
next: function next (envProps) {
var ref = tokenize (command, envProps.env);
var cmd = ref[0];
var args = ref.slice(1);
if (envProps.exitCode) {
// Previous process exited with non-zero.
// Should not continue flow. Instead, forward the error all the
// way to the end listener. Note: This may happen several times and does not
// mean that observable.error() should be called. Reason: error means the end
// of the whole stream while this is not nescessarily so, as a source may be
// continously watching while a subsequent process failed to do it's job.
observer.next(envProps);
if (prevComplete) { observer.complete(); }
return;
}
if (childProcess) {
// We've created a process as a response to a previous next().
// We are expected to re-execute the command.
try {
//console.log(`Killing ${command}`);
childProcess.kill('SIGTERM'); // Or should we use SIGINT ('CTRL-C')
} catch(err) {
console.error(("Failed to kill '" + command + "'. Error: " + err));
}
childProcess = null;
}
if (childSubscription) {
// We've created a child subscription
// We are expected to re-execute the subscription.
childSubscription.unsubscribe();
childSubscription = null;
}
try { // Don't know if we're required to do try..catch here or if the framework does that for us. Read/test es-observable contract!
if (!cmd) {
// Comment or empty line. ignore.
var text = command.split('#').map(function (s){ return s.trim(); });
if (text.length > 1) {
host.log(
COMMENT_COLOR +
text.slice(1).join(' ') +
clr.RESET);
}
observer.next(clone(envProps, {
command: command,
exitCode: 0
}));
} else if (cmd === 'cd') {
// cd
var newDir = path.resolve(envProps.cwd, args[0]);
host.log((SPECIAL_PROMPT_COLOR + "> " + COMMAND_COLOR + "cd " + (args[0]) + (clr.RESET)));
observer.next(clone(envProps, {
command: command,
exitCode: 0,
cwd: newDir,
}));
} else if (cmd.indexOf('=') !== -1 || args.length > 0 && args[0].indexOf('=') === 0) {
// ENV_VAR = value, ENV_VAR=value, ENV_VAR= value or ENV_VAR =value.
var statement = args.length > 0 ?
args[0] === '=' ?
cmd + args[0] + args[1] :
cmd + args[0] :
cmd;
var ref$1 = statement.split('=');
var variable = ref$1[0];
var value = ref$1[1];
var newEnv = clone(envProps.env);
newEnv[variable] = value;
host.log((SPECIAL_PROMPT_COLOR + "> " + COMMAND_COLOR + variable + "=" + (surroundWithQuotes(value)) + (clr.RESET)));
observer.next(clone(envProps, {
command: command,
exitCode: 0,
env: newEnv
}));
} else if (cmd === 'just-build') {
// Shortcutting "just-build" commands to:
// 1. Not spawn a new process for it.
// 2. Not having to use [--watch] for it.
var ref$2 = refineArguments(args, true, command);
var refinedArgs = ref$2.refinedArgs;
var grepString = ref$2.grepString;
var useWatch = ref$2.useWatch;
if (useWatch) {
host.log((SPECIAL_PROMPT_COLOR + "> " + COMMAND_COLOR + command + (clr.RESET)));
throw new Error("[--watch] is redundant for 'just-build'. It will invoke it automatically. http://tinyurl.com/z6ylnb7");
}
var subCfg = extractConfig (["node", "just-build"].concat(args), {
cwd: envProps.cwd,
env: envProps.env
});
extend (subCfg, {
log: host.log,
spawn: host.spawn,
watchMode: watchMode}); // Override watchmode given to root just-build as we ignore [--watch] argument here.
// Treat childSubscription exactly the same way as childProcess.
// it is conceptually the same thing. Use unsubscribe() istead of kill().
childSubscription = createObservable(subCfg).subscribe ({
next: function next (result) {
observer.next(clone(envProps, {
command: result.command || command, // If succesful exitCode, result.command will be undefined.
exitCode: result.exitCode
}));
},
complete: function complete () {
childSubscription = null;
if (prevComplete) { observer.complete(); }
},
error: function error (err) {
childSubscription = null;
observer.error(err);
}
});
} else {
// Ordinary command
debug.log(("ordinary command: " + (JSON.stringify([args, watchMode, command]))));
var ref$3 = refineArguments(args, watchMode, command);
var refinedArgs$1 = ref$3.refinedArgs;
var grepString$1 = ref$3.grepString;
var useWatch$1 = ref$3.useWatch;
debug.log(("[refinedArgs, grepString, useWatch] = " + (JSON.stringify([refinedArgs$1, grepString$1, useWatch$1]))));
debug.log(("cmd = " + (JSON.stringify(cmd))));
debug.log(("cwd = " + (envProps.cwd) + ", env = " + (envProps.env)));
childProcess = (host.spawn)(
cmd,
refinedArgs$1, {
cwd: envProps.cwd,
env: envProps.env,
shell: true
});
childProcess.stdout.pipe(new ColorTransform()).pipe(process.stdout);
childProcess.stderr.pipe(new ColorTransform(true)).pipe(process.stderr);
childProcess.on('error', function (err) { return observer.error(err); }); // Correct? Or just
if (useWatch$1) {
childProcess.stdout.on('data', function (data) {
if (data.indexOf(grepString$1) !== -1) {
observer.next(clone(envProps, {
command: command,
exitCode: undefined // No real exit code yet. Handled as exitCode 0.
}));
}
});
}
childProcess.on('exit', function (code) {
childProcess = null;
observer.next(clone(envProps, {
command: command,
exitCode: code
}));
if (prevComplete) { observer.complete(); }
});
}
} catch (err) {
observer.error(err);
}
},
error: function error(err) {
observer.error(err);
},
complete: function complete() {
prevComplete = true;
if (!childProcess && !childSubscription) { observer.complete(); }
}
})
return {
unsubscribe: function unsubscribe () {
if (childProcess) {
try {
//console.log(`Killing ${command}`);
childProcess.kill('SIGTERM'); // Or should we use 'SIGINT' (CTRL-C) ?
}
catch (err) {
console.error(("Failed to kill '" + command + "'. Error: " + err));
};
/* Should we remove "exit", "error" and "data" listeners?
if (process.removeListener) {
if (process.stdout.removeListener) {
}
}
*/
childProcess = null;
}
if (childSubscription) {
childSubscription.unsubscribe();
childSubscription = null;
}
prevSubscription.unsubscribe();
},
get closed() {
return !childProcess && !childSubscription && prevSubscription.closed;
}
}
});
}
/**
* Takes an array of arguments and removes "[--watch ...]" if not watchMode. Otherwise,
* includes "--watch".
*
* @returns {{
* refinedArgs: string,
* grepString: string,
* useWatch: boolean
* }} Returns the refined arguments together with the grepString to watch for in case useWatch is true.
*/
function refineArguments(args, watchMode, commandSource) {
var refinedArgs = [];
var hasOptionalWatchArg = false;
var grepString = null;
for (var i=0; i<args.length; ++i) {
var arg = args[i];
if (arg === '[--watch') {
hasOptionalWatchArg = true;
if (watchMode) {
refinedArgs.push('--watch');
if (i + 1 >= args.length)
{ throw new Error (("Missing grepString in the following command: \"" + commandSource + "\"")); }
grepString = args[i + 1];
}
if (i + 2 >= args.length || args[i + 2] !== ']')
{ throw new Error (("Missing ']' in the following command: " + commandSource)); }
i += 2;
} else {
refinedArgs.push(arg);
}
};
return {
refinedArgs: refinedArgs,
grepString: grepString,
useWatch: hasOptionalWatchArg && watchMode
};
}
module.exports = {executeAll: executeAll, createObservable: createObservable, refineArguments: refineArguments};