passy/google-cdn

View on GitHub
googlecdn.js

Summary

Maintainability
C
7 hrs
Test Coverage
'use strict';

var async = require('async');
var semver = require('semver');
var debug = require('debug')('google-cdn');
var requote = require('regexp-quote');
var escape = require('escape-regexp');

var data = {
  google: require('google-cdn-data'),
  cdnjs: require('cdnjs-cdn-data')
};

var bowerUtil = require('./util/bower');

function normalizeBowerName(bowerJson, name) {
  if ((bowerJson.dependencies && bowerJson.dependencies[name]) ||
      (bowerJson.devDependencies && bowerJson.devDependencies[name])) {
    return name;
  } else {
    return name.replace(/\.js$/,'');
  }
}

function getDepVersionStr(deps, name, cleanup) {
  if (deps) {
    var versionStr = deps[name];
    if (versionStr && cleanup) {
      delete deps[name];
    }
    return versionStr;
  }
}

function getVersionStr(bowerJson, name, cleanup) {
  var versionStr;
  var bowerName = normalizeBowerName(bowerJson, name);

  versionStr = getDepVersionStr(bowerJson.dependencies, bowerName, cleanup);

  if (!versionStr) {
    versionStr = getDepVersionStr(bowerJson.devDependencies, bowerName, cleanup);
  }

  return versionStr;
}

function satisfyDependency(bowerJson, name) {
  getVersionStr(bowerJson, name, true);
}


function isFunction(fn) {
  return typeof(fn) === 'function';
}

function getCDNUrl(item, type, version) {
  var url = item[type] || item.url;
  if (url) {
    return (isFunction(url)) ? url(version) : url;
  }
}

function getTypesArray(types) {
  if (Array.isArray(types) || typeof(types) === 'string') {
    return [].concat(types);
  } else if (types) {
    throw new Error('Types must be an array or a string');
  }
}


module.exports = function cdnify(content, bowerJsonOriginal, options, masterCallback) {

  if (!bowerJsonOriginal) {
    throw new Error('Required argument `bowerJson` is missing.');
  }
  //clone bowerJson so that it can be modified
  var bowerJson = JSON.parse(JSON.stringify(bowerJsonOriginal));

  if (isFunction(options)) {
    masterCallback = options;
    options = {};
  }

  options = options || {};
  options.componentsPath = options.componentsPath || 'bower_components';

  var cdn = options.cdn || 'google';
  var cdnData = {};
  if (Array.isArray(cdn)) {
    cdn.forEach(function(host) {
      cdnData[host] = data[host];
    });
  } else if (typeof(cdn) === 'object') {
    cdnData.custom = cdn;
  } else {
    cdnData[cdn] = data[cdn];
  }

  var supportedTypes = {};
  supportedTypes.types = getTypesArray(options.types) || ['js', 'css'];
  supportedTypes.typesRegex = {};
  supportedTypes.types.forEach(function (type) {
    supportedTypes.typesRegex[type] = new RegExp('\\.' + escape(type) + '$', 'i');
  });

  var cdnHostList = Object.keys(cdnData);

  if (!cdnHostList.length) {
    return masterCallback(new Error('CDN ' + cdn + ' is not supported.'));
  }
  for (var i = 0; i < cdnHostList.length; i++) {
    var badCdn = cdnHostList[i];
    if (!cdnData[badCdn]) {
      return masterCallback(new Error('CDN ' + badCdn + ' is not supported.'));
    }
  }

  function generateReplacement(bowerPath, url) {
    // Replace leading slashes if present.
    var from = bowerUtil.joinComponent(options.componentsPath, bowerPath);
    var fromRe = '/?' + requote(from);
    var fromRegex = new RegExp(fromRe);
    return {
      from: from,
      fromRegex: fromRegex,
      to: url
    };
  }

  function buildReplacement(cdnHost) {
    return function(name, callback) {
      var item = cdnData[cdnHost][name];
      var versionStr = getVersionStr(bowerJson, name);

      if (!versionStr) {
        return callback();
      }
      name = normalizeBowerName(bowerJson, name);

      var version = semver.maxSatisfying(item.versions, versionStr);
      if (version) {
        satisfyDependency(bowerJson, name);
        debug('Choosing version %s for dependency %s', version, name);

        if (item.all) {
          var url = (isFunction(item.url)) ? item.url(version) : item.url;
          callback(null, generateReplacement(name, url));
        } else {
          bowerUtil.resolveMainPath(supportedTypes, name, versionStr, function (err, main) {
            if (err) {
              return callback(err);
            } else {
              var replacements = [];
              for (var type in main) {
                var url = getCDNUrl(item, type, version);
                if (url) {
                  replacements.push(generateReplacement(main[type], url));
                }
              }
              callback(null, replacements);

            }
          });
        }
      } else {
        debug('Could not find satisfying version for %s %s', name, versionStr);
        callback();
      }
    };
  }

  var replacementInfo = [];

  function buildReplacementForHost(cdnHost, callback) {
    async.map(Object.keys(cdnData[cdnHost]), buildReplacement(cdnHost), function (err, replacements) {
      if (err) {
        return callback(err);
      }

      function replaceContent(fileReplacement) {
        if (fileReplacement) {
          content = content.replace(fileReplacement.fromRegex, fileReplacement.to);
          debug('Replaced %s with %s', fileReplacement.fromRegex, fileReplacement.to);
          replacementInfo.push(fileReplacement);
        }
      }

      replacements.forEach(function (replacement) {
        if (replacement) {
          if (Array.isArray(replacement)) {
            replacement.forEach(replaceContent);
          } else {
            replaceContent(replacement);
          }
        }
      });
      callback();
    });
  }

  async.eachSeries(cdnHostList, buildReplacementForHost, function(err) {
    if (err) {
      return masterCallback(err);
    }

    masterCallback(null, content, replacementInfo);
  });
};