CleverStack/cleverstack-cli

View on GitHub
lib/util/grunt.js

Summary

Maintainability
B
4 hrs
Test Coverage
var Promise  = require('bluebird')
  , path     = require('path')
  , async    = require('async')
  , fs       = require('fs')
  , spawn    = require('child_process').spawn
  , readline = require('readline')
  , os       = require('os')
  , isWin    = /^win32/.test(os.platform());

/**
 * Mimmicks a small grunt-cli utility
 *
 * @return {gruntCLI}
 * @private
 */
function gruntCLI() {
  this.tasks = [];

  this.option = function(key) {
    return process.env[ key ];
  };

  this.registerTask = function(cmd) {
    this.tasks.push(cmd);
  };

  this.loadNpmTasks = function() {};
}

/**
 * Finds the correct Gruntfile name
 *
 * @param  {String} pathSrc - Directory path where the Gruntfile resides.
 * @return {Promise}
 * @api public
 */
var findGruntFile = exports.findGruntFile = function(pathSrc) {
  return new Promise(function(resolve, reject) {
    async.detect([
      path.join(pathSrc, 'Gruntfile.js'),
      path.join(pathSrc, 'gruntfile.js'),
      path.join(pathSrc, 'Grunt.js'),
      path.join(pathSrc, 'grunt.js')
    ], fs.exists, function(filePath) {
      if (typeof filePath === 'undefined') {
        return reject('Gruntfile within ' + pathSrc + ' could not be found.');
      }

      resolve(filePath);
    });
  });
};

/**
 * We're essentially creating a small grunt utility to bypass
 * grunt functions... this is better than requiring grunt
 * entirely and trying to regex parse...
 *
 * @param  {String} filePath
 * @return {Object}
 * @api public
 */
var gruntUtility = exports.gruntUtility = function(filePath) {
  var cli  = new gruntCLI()
    , file = require(filePath)(cli);

  if (Array.isArray(file) && typeof file[ file.length-1 ] === 'function') {
    file[ file.length-1 ].call(file,  cli);
  }

  return cli.tasks;
};

/**
 * Reads/parses Grunt tasks within pathSrc
 *
 * @param  {String} pathSrc - Source to the Gruntfile
 * @param  {Boolean} [silence=true] - Silences errors returned from reading the Gruntfile
 * @return {Promise}
 * @api public
 */
var readTasks = exports.readTasks = function(pathSrc, silence) {
  return new Promise(function(resolve, reject) {
    if (typeof silence === 'undefined') {
      silence = true;
    }

    findGruntFile(pathSrc)
      .then(function(filePath) {
        var tasks = gruntUtility(filePath);
        resolve([ tasks, pathSrc ]);
      })
      .catch(function (err) {
        if (err.match(/^Gruntfile within/) !== null && silence === true) {
          return resolve([ [], pathSrc ]);
        }

        reject(err);
      });
  });
};

/**
 * Runs a grunt task within projectFolder
 *
 * @param  {String} projectFolder Path to the project's seed folder
 * @param  {String} cmd           Command to run within grunt
 * @return {Promise}              Returns a promise from bluebird
 * @api private
 */
function runTask(projectFolder, command) {
  var args       = [].slice.call(arguments, 1);

  return new Promise(function(resolve, reject) {
    var cmd      = !isWin ? 'grunt' : 'grunt.cmd'
      , env      = process.env
      , opts     = {env: env, cwd: projectFolder, stdio: 'inherit'}
      , isPrompt = /prompt/i.test(args[0])
      , paths    = process.env.NODE_PATH ? [process.env.NODE_PATH] : [];

    // Pass the --verbose call down to grunt as well
    if (!!program.verbose) {
      args.push('--verbose');
      
      var logMsg = 'Spawning posix child process to run '+lib.colors.lightGreen(cmd+' '+args.join(' ')) + '...';
      lib.utils.info(logMsg).running(logMsg);
    }

    paths.push(path.resolve(path.join(projectFolder, 'lib')) + path.sep);
    paths.push(path.resolve(path.join(projectFolder, 'modules')) + path.sep);

    env.NODE_PATH = paths.join(!!isWin ? ';' : ':');

    var posixProc = spawn(cmd, args, opts)
      , exitHdlr  = process.kill.bind(process, posixProc)
      , error     = '';

    process.on('SIGTERM', exitHdlr);

    posixProc.on('error', function(err) {
      error += err;
    });

    posixProc.on('close', function(code) {
      process.removeListener('SIGTERM', exitHdlr);

      if (code !== 0 || !!error) {
        return reject(error);
      }

      if (command.indexOf('prompt')) {
        lib.utils.progress();
        lib.utils.startBar(function() {
          resolve();
        });
      } else {
        resolve();
      }
    });
  });
}
exports.runTask = runTask;

/**
 * Runs DB migrations from grunt
 *
 * @param  {String} projectFolder Path to the project's folder
 * @return {Promise}              Returns a promise from bluebird
 * @api private
 */
function runDBMigrations(projectFolder) {
  lib.utils.warn('Running database migrations...');

  return new Promise(function(resolve, reject) {
    var env        = process.env
      , paths      = process.env.NODE_PATH ? [process.env.NODE_PATH] : [];

    paths.push(path.resolve(path.join(projectFolder, 'lib')) + path.sep);
    paths.push(path.resolve(path.join(projectFolder, 'modules')) + path.sep);

    env.NODE_PATH  = paths.join(!!isWin ? ';' : ':');

    // check for NODE_ENV json config file if it doesn't exist then revert to local
    if (!fs.existsSync(path.join(projectFolder, 'config', env.NOD_ENV + '.json'))) {
      env.NODE_ENV = 'local';
    }

    var opts = {env: env, cwd: projectFolder}
      , args = ['db']
      , cmd  = !isWin ? 'grunt' : 'grunt.cmd';


    if (!!program.verbose) {
      opts.stdio = 'inherit';
    } else {
      args.push('--silent');
    }

    var proc = spawn(cmd, args, opts)
      , error = '';

    proc.on('error', function(err) {
      error += err;
    });

    proc.on('close', function(code) {
      if (code !== 0 || !!error) {
        return reject(error);
      }

      lib.utils.progress();
      resolve();
    });
  });
}

/**
 * Runs Grunt tasks specifically for CleverStack
 *
 * @param  {String} projectFolder Path to the project's backend folder
 * @param  {String} modulePath    Path to the module (where to look for the grunt file)
 * @return {Promise}              Promise from bluebird
 * @api public
 */
exports.runTasks = function(projectFolder, modulePath) {
  return new Promise(function(resolve, reject) {
    var originalCwd = process.cwd();
    process.chdir(projectFolder);

    readTasks(modulePath)
      .spread(function(tasks) {
        
        process.chdir(originalCwd);

        if (tasks.length) {
          lib.utils.warn('  Running grunt tasks for module ' + modulePath.split(path.sep).pop() + '...');
        }
        
        Promise
          .all(tasks)
          .then(function() {
            if (tasks.indexOf('readme') > -1) {
              lib.utils.expandProgress(1);
              return runTask(projectFolder, 'readme');
            }
            return true;
          })
          .then(function() {
            return new Promise(function(resolve, reject) {
              async.forEachSeries(
                tasks,
                function(task, callback) {
                  if (/^prompt:clever.*/.test(task)) {
                    lib.utils.expandProgress(1);
                    lib.utils.stopBar(function() {
                      runTask(projectFolder, task)
                        .then(function() {
                          callback(null);
                        })
                        .catch(callback);
                    });
                  } else {
                    callback(null);
                  }
                },
                function(err) {
                  !err ? resolve() : reject(err);
                }
               );
            });
          })
          .then(function() {
            if (tasks.indexOf('db') > -1) {
              lib.utils.expandProgress(1);
              return runDBMigrations(projectFolder);
            }
            return true;
          })
          .then(function() {
            resolve();
          }, function(err) {
            reject(err);
          });
      });
  });
};