tandrewnichols/file-manifest

View on GitHub
lib/file-manifest.js

Summary

Maintainability
A
45 mins
Test Coverage
A
100%
var pedestrian = require('pedestrian');
var async = require('async');
var path = require('path');
var _ = require('lodash');
var fs = require('fs');
var stack = require('stack-trace');
var loaders = require('./loaders');
var reducers = require('./reducers');
var Promise = require('promise');
var File = require('defiled');
var EventEmitter = require('events').EventEmitter;

/**
 * .generate
 *
 *  Asynchronously generate a manifest of files in from directory
 *
 *  @param {String} dir - The directory to load.
 *  @param {Object} [options] - Options.
 *  @param {String|Array} options.match - Globstar patterns to match when including files in
 *    (or excluding files from) the manifest
 *  @param {*} options.memo - Starting value for the manifest.
 *  @param {Function|String} options.load - Function for loading the file (e.g. require,
 *    fs.readFile) or string that maps to a provided loader
 *  @param {Function|String} options.name - Function for naming keys on the object or
 *    a string that maps to a provided namer
 *  @param {Function} options.reduce - A full replacement for the built-in reduce function
 *    if you need to REALLY custom things
 *  @param {Function|String} [callback] - An optional callback
 *
 *  @returns {Object} The manifest of files
 */
exports.generate = function(dir, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = null;
  }
  options = options || {};
  options.dir = exports.standardizePath(dir);
  options.match = _.isArray(options.match) || _.isEmpty(options.match) ? options.match : [options.match];
  options.memo = options.memo || (['list', 'objectList'].indexOf(options.reduce) > -1  ? [] : {});
  options.load = typeof options.load === 'function' ? options.load : exports.load(options.load);
  options.name = typeof options.name === 'function' ? options.name : exports.name(options.name);
  options.reduce = typeof options.reduce === 'function' ? options.reduce : exports.reduce(options.reduce);
  options.callback = callback;
  return exports.run(options);
};

/**
 * .generateSync
 *
 *  @param {String} dir - The directory to load.
 *  @param {Object} options - Options. Same as in .generate.
 *
 *  @returns {Object} The manifest of files.
 */
exports.generateSync = function(dir, options) {
  return exports.generate(dir, options);
};

/**
 * .generatePromise
 *
 *  Promise-based async implementation of .generate.
 *
 *  @param {String} dir - The directory to load.
 *  @param {Object} options - Options. Same as in .generate.
 *
 *  @returns {Promise} A promise object that will be resolved with the file manifest
 *    or rejected with an error
 */
exports.generatePromise = function(dir, options) {
  return new Promise(function(resolve, reject) {
    exports.generate(dir, options, function(err, manifest) {
      if (err) reject(err);
      else resolve(manifest);
    });
  });
};

/**
 * .generateEvent
 *
 *  Event-based async implementation of .generate.
 *
 *  @param {String} dir - The directory to load.
 *  @param {Object} options - Options. Same as in .generate.
 *
 *  @returns {EventEmitter} An event object that will fire a "manifest" event on success
 *    or an "error" event on an error
 */
exports.generateEvent = function(dir, options) {
  var emitter = new EventEmitter();
  exports.generate(dir, options, function(err, manifest) {
    if (err) emitter.emit('error', err);
    else emitter.emit('manifest', manifest);
  });
  return emitter;
};

/**
 * .standardizePath
 *
 *  Standardize the path passed to be absolute if it is not already
 *
 *  @param {string} dir - The directory to search
 *  @returns {string} The standardized path
 */
exports.standardizePath = function(dir) {
  if (dir.charAt(0) !== path.sep) {
    var trace = stack.get();
    var caller = _.find(trace, function(callsite) {
      var name = callsite.getFileName().split('/');
      return name[name.length - 1] !== 'file-manifest.js' && name.slice(-3).join('/') !== 'promise/lib/core.js';
    });
    dir = path.resolve(path.dirname(caller.getFileName()), dir);
  }
  return path.normalize(dir);
};

/**
 * .run
 *
 *  Get all the files in a directory and process them via reduce
 *
 *  @param {Object} options - The options
 *  @param {String|Array} options.match - Globstar patterns to match when including files in
 *    (or excluding files from) the manifest
 *  @param {*} options.memo - Starting value for the manifest.
 *  @param {Function|String} options.load - Function for loading the file (e.g. require,
 *    fs.readFile) or string that maps to a provided loader
 *  @param {Function|String} options.name - Function for naming keys on the object or
 *    a string that maps to a provided namer
 *  @param {Function} options.reduce - A full replacement for the built-in reduce function
 *    if you need to REALLY custom things
 *  @param {String} options.dir - The directory to search for files
 *  @param {Function} options.callback - The callback for asynchronous manifests
 *
 *  @returns {Object} The manifest of files
 */
exports.run = function(options) {
  var reduce = function(memo, file, next) {
    var fileObj = new File(file, options.dir);
    return options.reduce.call(options, memo, fileObj, next);
  };

  if (options.callback) {
    pedestrian.walk(options.dir, options.match || '', function(err, files) {
      if (err) options.callback(err);
      else async.reduce(files, options.memo, reduce, options.callback);
    });
  } else {
    return _.reduce(pedestrian.walk(options.dir, options.match || ''), reduce, options.memo);
  }
};

/**
 * .reduce
 *
 *  Generate a reduction function for processing files.
 *
 *  @param {String} [reduce] - If a string, this indicates a built-in reducer to use.
 *
 *  @returns {Function} Reduction function
 */
exports.reduce = function(reduce) {
  return function(manifest, file, next) {
    var reducer = reducers[reduce] || reducers[ exports._getDefaultReducer(this.memo) ];
    return reducer.call(this, manifest, file, next);
  };
};

/**
 * .name
 *  
 *  Generate a namer function for processed files.
 *
 *  @param {String} [name] - If a string, this indicates a built-in namer to use.
 *
 *  @returns {Function} Namer function
 */
exports.name = function(name) {
  return function(file) {
    var transformer = file.transformers[name] ? name : 'camel';
    return file.relative({ transform: transformer });
  };
};

/**
 * .load
 *
 *  Generate a loading function for processed files.
 *
 *  @param {String} [load] - If a string, this indicated a built-in loader to use.
 *
 *  @returns {Function} Loading function
 */
exports.load = function(load) {
  return function(file, cb) {
    var loader = loaders[load] || loaders[ exports._getDefaultLoader(file) ];
    return loader(file.abs(), cb);
  };
};

/**
 * ._getDefaultReducer
 *
 *  Choose a reducer based on the the memo.
 *
 *  @param {Object|Array|*} memo - The memo for the reduction
 *
 *  @returns {String} One of "list" and "flat"
 */
exports._getDefaultReducer = function(memo) {
  // If memo is an array, use the "list" reducer,
  // otherwise, use the "flat" reducer.
  if (_.isArray(memo)) {
    return 'list';
  } else {
    return 'flat';
  }
};

/**
 * ._getDefaultLoader
 *
 *  Choose a loader based on whether the file is requirable and whether we're sync or async
 *
 *  @param {File} file - The file to load
 *  @param {Function} [cb] - An optional callback (for determining sync/async)
 *
 *  @returns {String} One of "require" and "readFile"
 */
exports._getDefaultLoader = function(file) {
  var ext = file.ext();
  // If the file can be loaded via require, make that the default,
  // otherwise, use readFile.
  if (ext === '.js' || ext === '.json') {
    return 'require';
  } else {
    return 'readFile';
  }
};