UNPKG

just-build

Version:

A simple task runner that doesn't bloat your package

437 lines (409 loc) 18.8 kB
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};