LevelbossMike/ember-deploy

View on GitHub
lib/models/plugin-registry.js

Summary

Maintainability
C
7 hrs
Test Coverage
var CoreObject  = require('core-object');
var DAG         = require('dag-map').default;
var SilentError = require('silent-error');
var chalk       = require('chalk');
var _           = require('lodash');

/**
 * Glossary
 *
 * - addon: An ember-cli addon
 * - plugin: An addon that contains the `ember-cli-deploy` keyword and implements the `createDeployPlugin` function
 * - pluginInstance: An instance of a plugin created by executing `createDeployPLugin`. A single plugin can have multiple aliased instances created
 **/

module.exports = CoreObject.extend({
  init: function(project, ui, config) {
    this._super();
    this._project        = project;
    this._ui             = ui;
    this._aliasConfig    = (config.pipeline && config.pipeline.alias) || {};
    this._runOrderConfig = (config.pipeline && config.pipeline.runOrder) || {};
    this._disabledConfig = (config.pipeline && config.pipeline.disabled) || {};
  },

  pluginInstances: function() {
    var addons                 = this._project.addons || [];
    var plugins                = this._plugins(addons);
    var aliasMap               = this._buildAliasMap(plugins, this._aliasConfig);
    var disabledMap            = this._buildDisabledMap(aliasMap, this._disabledConfig);
    var pluginInstances        = this._pluginInstances(plugins, aliasMap);
    var runOrderMap            = this._buildRunOrderMap(this._runOrderConfig, aliasMap, pluginInstances);
    var enabledPluginInstances = this._applyDisabledConfig(pluginInstances, disabledMap);
    var orderedPluginInstances = this._applyRunOrderConfig(enabledPluginInstances, runOrderMap);

    if (orderedPluginInstances.length === 0) {
      this._ui.writeError('\nWARNING: No plugins installed/enabled\n');
      this._ui.writeError('ember-cli-deploy works by registering plugins in it\'s pipeline.\n');
      this._ui.writeError('In order to execute a deployment you must install at least one ember-cli-deploy compatible plugin.\n');
      this._ui.writeError('Visit http://ember-cli-deploy.com/plugins/ for a list of supported plugins.\n');
    }

    return orderedPluginInstances;
  },

  _plugins: function(addons) {
    if (!this._cachedValidAddons) {
      this._cachedValidAddons = this._discoverPlugins(addons);
    }

    return this._cachedValidAddons;
  },

  _discoverPlugins: function(addons){
    var self = this;
    return addons.reduce(function(plugins, addon) {
      if (self._isValidPlugin(addon)) {
        var pluginName = self._pluginName(addon);
        plugins[pluginName] = addon;
      }

      if (self._isDeployPluginPack(addon)) {
        var nestedAddons = self._discoverPlugins(addon.addons);
        plugins = Object.keys(nestedAddons).reduce(function(v, key) {
          v[key] = nestedAddons[key];
          return v;
        }, plugins);
      }

      return plugins;
    }, {});
  },

  /**
   * Build up a hash of the all plugin aliases, keyed by plugin name, with the value being an array of aliases.
   * This hash will contain an entry for every installed plugin.
   *
   * If a plugin has not been referenced in `config.pipeline.alias` then it will be added to the map
   * with an alias to it's original name (see `boo` below),
   *
   * Example return value:
   *
   * {
   *   foo: { as: ['bar', 'baz'] },
   *   boo: { as: ['boo'] }
   * }
   * */
  _buildAliasMap: function(plugins, config) {
    var self = this;
    var aliasMap = Object.keys(plugins)
      .reduce(function(aliases, pluginName) {
        if (!aliases[pluginName] || !aliases[pluginName].as) {
          aliases[pluginName] = { as: [pluginName] };
        }

        aliases[pluginName].as = self._castArray(aliases[pluginName].as);
        return aliases;
      }, config);

    var unknownConfigKeys = _.difference(Object.keys(aliasMap), Object.keys(plugins));

    if (unknownConfigKeys.length) {
      this._logUnknownPlugins(unknownConfigKeys, 'config.pipeline.alias');
    }

    return aliasMap;
  },

  /**
   * Build up a hash of disabled plugin instances, keyed by alias, with the value being whether it's disabled or not
   * This hash will contain an entry for every installed plugin.
   *
   * If a plugin has not been referenced in `config.pipeline.disabled` then it will be added to the map as `disabled: false`.
   *
   * Example return value:
   *
   * {
   *   foo: true,
   *   bar: false
   * }
   **/
  _buildDisabledMap: function(aliasMap, config) {
    var aliases = this._flattenAliasMap(aliasMap);
    var allExcept = null;

    if (typeof config.allExcept !== 'undefined') {
      allExcept = this._castArray(config.allExcept);
      delete config.allExcept;
    }

    var keys = Object.keys(config);

    if (allExcept) {
      keys = keys.concat(allExcept);
    }

    var unknownConfigKeys = _.difference(keys, aliases);

    if (unknownConfigKeys.length) {
      this._logUnknownPlugins(unknownConfigKeys, 'config.pipeline.disabled');
    }

    var disabledMap = aliases.reduce(function(map, alias) {
      if (map[alias] === undefined) {
        if (allExcept && allExcept.length) {
          if (allExcept.indexOf(alias) >= 0) {
            map[alias] = false;
          } else {
            map[alias] = true;
          }
        } else {
          map[alias] = false;
        }
      }

      return map;
    }, config);

    return disabledMap;
  },

  /**
   * Build up a hash of plugin instance execution order overrides.
   * This hash will only container an entry for instances that should run in a particular order. All other instances will be ordered by their natural
   * ordering.
   *
   * Ordering of plugin instances can be specified by users of ember-cli-deploy via `config.pipeline.runOrder` and by plugin authors via `run{Before,After}`.
   * This function will attempt to merge the author defined run orders with the user defined ones.
   *
   * Example return value:
   *
   * {
   *   foo: { before: ['bar'] },
   *   baz: { after: ['boo', 'bom'] }
   * }
   *
   *
   **/
  _buildRunOrderMap: function(config, aliasMap, pluginInstances) {
    var self = this;

    pluginInstances.forEach(function(instance) {
      if (instance.runBefore) {
        var befores = self._castArray(instance.runBefore);
        config = self._mergeAuthorProvidedOrderWithConfigOrder('before', instance.name, befores, config, aliasMap);
      }

      if (instance.runAfter) {
        var afters = self._castArray(instance.runAfter);
        config = self._mergeAuthorProvidedOrderWithConfigOrder('after', instance.name, afters, config, aliasMap);
      }
    });

    var aliases = this._flattenAliasMap(aliasMap);

    var configNames = Object.keys(config).reduce(function(arr, key) {
      arr.push(key);

      var befores = self._castArray(config[key].before);
      var afters = self._castArray(config[key].after);

      return arr.concat(befores).concat(afters);
    }, [])
    .reduce(function(arr, key) {
      if (arr.indexOf(key) === -1) {
        arr.push(key);
      }
      return arr;
    }, []);

    var unknownConfigKeys = _.difference(configNames, aliases);

    if (unknownConfigKeys.length) {
      this._logUnknownPlugins(unknownConfigKeys, 'config.pipeline.runOrder');
    }

    return config;
  },

  _pluginInstances: function(plugins, aliasMap) {
    return Object.keys(plugins)
      .map(function(pluginName) {
        var addon   = plugins[pluginName];
        var aliases = aliasMap[pluginName].as;

        return aliases.map(function(alias) {
          var v = addon.createDeployPlugin({ name: alias });
          return v;
        });
      })
      .reduce(function(instances, arr) {
        return instances.concat(arr);
      }, []);
  },

  _applyDisabledConfig: function(pluginInstances, disabledMap) {
    return pluginInstances.filter(function(instance) {
      return !disabledMap[instance.name];
    }) ;
  },

  _applyRunOrderConfig: function(pluginInstances, runOrderMap) {
    var self            = this;
    var graph           = new DAG();
    var sortedInstances = [];

    try {
      pluginInstances.forEach(function(instance) {
        var before = (runOrderMap[instance.name] && self._castArray(runOrderMap[instance.name].before)) || [];
        var after = (runOrderMap[instance.name] && self._castArray(runOrderMap[instance.name].after)) || [];
        graph.add(instance.name, instance, before, after);
      });

      graph.topsort(function (key, value) {
        sortedInstances.push(value);
      });
    } catch(err) {
      if (/cycle detected/.test(err)) {
        throw new SilentError('your ember-cli-deploy plugins have a circular dependency:' + err.message);
      } else {
        throw err;
      }
    }

    return sortedInstances;
  },

  _isDeployPluginPack: function(addon) {
    return this._addonHasKeyword(addon, 'ember-cli-deploy-plugin-pack');
  },

  _isValidPlugin: function(addon) {
    return this._addonHasKeyword(addon, 'ember-cli-deploy-plugin') && this._addonImplementsDeploymentHooks(addon);
  },

  _pluginName: function(addon) {
    if(addon.name.indexOf('ember-cli-deploy') > -1) {
      var pluginNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?(ember-cli-deploy-)(.*)$/;
      return addon.name.match(pluginNameRegex)[3];
    }
    return addon.name;
  },

  _addonHasKeyword: function(addon, keyword) {
    var keywords = addon.pkg.keywords;
    return keywords.indexOf(keyword) > -1;
  },

  _addonImplementsDeploymentHooks: function(addon) {
    return addon.createDeployPlugin && typeof addon.createDeployPlugin === 'function';
  },

  _castArray: function(obj) {
    if (!obj) {
      return [];
    }

    return _.castArray(obj);
  },

  _mergeAuthorProvidedOrderWithConfigOrder: function(type, instanceName, names, config, aliasMap) {
    var self = this;
    names.forEach(function(name) {
      var aliases = aliasMap[name] && aliasMap[name].as;
      if (aliases) {
        if (!config[instanceName]) {
          config[instanceName] = {};
        }

        if (!config[instanceName][type]) {
          config[instanceName][type] = aliases;
        } else {
          var combined = self._castArray(config[instanceName][type])
            .reduce(function(all, current) {
              if (all.indexOf(current) === -1) {
                all.push(current);
              }

              return all;
            }, aliases);
          config[instanceName][type] = combined;
        }
      }
    });

    return config;
  },

  _logUnknownPlugins: function(names, key) {
    var message = chalk.yellow('Your config has referenced the following unknown plugins or aliases in `' + key + '`:\n');
    this._ui.writeLine(message, 'WARNING');
    names.forEach(function(name) {
      message = chalk.yellow('- ' + name + '\n');
      this._ui.writeLine(message, 'WARNING');
    }.bind(this));
  },

  _flattenAliasMap: function(aliasMap) {
    return Object.keys(aliasMap)
      .reduce(function(aliases, pluginName) {
        return aliases.concat(aliasMap[pluginName].as);
      }, []);
  },
});