Merge pull request #123 from boneskull/issue/99

enable autodiscovery of plugins; closes #99
This commit is contained in:
Brandon Keepers 2017-04-17 06:14:28 -05:00 committed by GitHub
commit f639dfc30d
5 changed files with 172 additions and 10 deletions

View File

@ -66,7 +66,6 @@ function setupTunnel() {
}
const pkgConf = require('pkg-conf');
const resolve = require('resolve').sync;
const createProbot = require('../');
const probot = createProbot({
@ -77,15 +76,14 @@ const probot = createProbot({
});
pkgConf('probot').then(pkg => {
program.args.concat(pkg.plugins || []).map(plugin => {
try {
const path = resolve(plugin, {basedir: process.cwd()});
probot.load(require(path))
} catch(err) {
console.warn(err.message);
process.exit(1);
}
});
const plugins = require('../lib/plugin')(probot);
const requestedPlugins = program.args.concat(pkg.plugins || []);
// if we have explicitly requested plugins, load them; otherwise use autoloading
if (requestedPlugins.length) {
plugins.load(requestedPlugins);
} else {
plugins.autoload();
}
probot.start();
});

69
lib/plugin.js Normal file
View File

@ -0,0 +1,69 @@
module.exports = pluginLoaderFactory;
function pluginLoaderFactory(probot, opts = {}) {
if (!probot) {
throw new TypeError('expected probot instance');
}
// We could eventually support a different base dir to load plugins from.
const basedir = opts.basedir || process.cwd();
// These are mostly to ease testing
const autoloader = opts.autoloader || require('load-plugins');
const resolver = opts.resolver || require('resolve').sync;
/**
* Resolves a plugin by name from the basedir
* @param {string} pluginName - Module name of plugin
*/
function resolvePlugin(pluginName) {
try {
return resolver(pluginName, {basedir});
} catch (err) {
err.message = `Failed to resolve plugin "${pluginName}". Is it installed?
Original error message:
${err.message}`;
throw err;
}
}
/**
* Load a plugin via filepath or function
* @param {string} pluginName - Plugin name (for error messaging)
* @param {string|Function} plugin - Path to plugin module or function
*/
function loadPlugin(pluginName, plugin) {
try {
probot.load(typeof plugin === 'string' ? require(plugin) : plugin);
} catch (err) {
err.message = `Failed to load plugin "${pluginName}". This is a problem with the plugin itself; not probot.
Original error message:
${err.message}`;
throw err;
}
}
/**
* Loads all accessible plugin modules beginning with "probot-"
*/
function autoload() {
const plugins = autoloader('probot-*');
Object.keys(plugins).forEach(pluginName => {
loadPlugin(pluginName, plugins[pluginName]);
probot.robot.log.info(`Automatically loaded plugin: ${pluginName}`);
});
}
/**
* Loads an explicit list of plugin modules
* @param {string[]} [pluginNames=[]] - List of plugin module names
*/
function load(pluginNames = []) {
pluginNames.forEach(pluginName => {
const pluginPath = resolvePlugin(pluginName);
loadPlugin(pluginName, pluginPath);
probot.robot.log.debug(`Loaded plugin: ${pluginName}`);
});
}
return {load, autoload};
}

View File

@ -24,6 +24,7 @@
"github": "^8.1.0",
"github-integration": "^1.0.0",
"github-webhook-handler": "^0.6.0",
"load-plugins": "^2.1.2",
"pkg-conf": "^2.0.0",
"raven": "^1.2.0",
"resolve": "^1.3.2"

1
test/fixtures/plugin/stub-plugin.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = function () {}

93
test/plugins.js Normal file
View File

@ -0,0 +1,93 @@
/* eslint prefer-arrow-callback: off */
const expect = require('expect');
const pluginLoaderFactory = require('../lib/plugin');
const stubPluginPath = require.resolve('./fixtures/plugin/stub-plugin');
const basedir = process.cwd();
const nullLogger = {};
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
nullLogger[level] = function () { };
});
describe('plugin loader', function () {
let probot;
let pluginLoader;
let autoloader;
let autoplugins;
let resolver;
beforeEach(function () {
probot = {
load: expect.createSpy(),
robot: {
log: nullLogger
}
};
autoplugins = {
probotPlugin: expect.createSpy()
};
autoloader = expect.createSpy().andReturn(autoplugins);
resolver = expect.createSpy().andReturn(stubPluginPath);
});
describe('factory', function () {
describe('when no robot provided', function () {
it('should throw a TypeError', function () {
expect(pluginLoaderFactory).toThrow(TypeError);
});
});
describe('when robot provided', function () {
it('should return an object', function () {
expect(pluginLoaderFactory(probot)).toBeA(Object);
});
});
describe('autoload()', function () {
beforeEach(() => {
pluginLoader = pluginLoaderFactory(probot, {autoloader});
});
it('should ask the autoloader for probot-related plugins', function () {
pluginLoader.autoload();
expect(autoloader).toHaveBeenCalledWith('probot-*');
});
it('should ask the robot to load the plugins', function () {
pluginLoader.autoload();
expect(probot.load).toHaveBeenCalledWith(autoplugins.probotPlugin);
});
});
describe('load()', function () {
beforeEach(() => {
pluginLoader = pluginLoaderFactory(probot, {resolver});
});
describe('when supplied no plugin names', function () {
it('should do nothing', function () {
pluginLoader.load();
expect(resolver).toNotHaveBeenCalled();
expect(probot.load).toNotHaveBeenCalled();
});
});
describe('when supplied plugin name(s)', function () {
it('should attempt to resolve plugins by name and basedir', function () {
pluginLoader.load(['foo', 'bar']);
expect(resolver).toHaveBeenCalledWith('foo', {basedir})
.toHaveBeenCalledWith('bar', {basedir});
});
it('should ask the robot to load a plugin at its resolved path', function () {
pluginLoader.load(['see-stub-for-resolved-path']);
expect(probot.load).toHaveBeenCalledWith(require(stubPluginPath));
});
});
});
});
});