const path = require('path');
const fs = require('fs');
const assign = require('lodash/assign');
const noop = require('lodash/noop');
const forOwn = require('lodash/forOwn');
const reduce = require('lodash/reduce');
const assert = require('assert');
const logger = require('../logger').build('addon-loader');
/**
* Addon loader
* @class
*/
class AddonLoader {
/**
*
* @param {string} rootPath location to start looking for addons
* @param {object} pkg package.json object
* @param {string} [modulesDir=node_modules] modules directory
* @param {function} [callbacks] function to later register callbacks on the loader
*/
constructor(rootPath, pkg, modulesDir = 'node_modules', callbacks = require('./callbacks')) {
this.ADDON_KEYWORD = 'flamingo-addon';
this._hooks = {};
this._callbacks = {};
this._loaded = false;
this.callbacks = callbacks;
this.rootPath = rootPath;
this.package = pkg;
this.modulesDir = modulesDir;
this.addons = [];
}
load() {
const addons = this.discover(this.rootPath, this.package, this.modulesDir);
/* istanbul ignore next */
if (addons.length) {
this.addons = addons;
logger.info('using addons: ' +
addons.map(addon => `${addon.pkg.name}@${addon.pkg.version}`).join(', '));
}
this.finalize(this.reduceAddonsToHooks(addons, this._hooks));
return this;
}
unload() {
this._loaded = false;
this._hooks = {};
return this;
}
/**
* Lookup package devDependencies and dependencies and resolve all of them which contain the addon keyword
* @param {string} rootPath
* @param {object} pkg
* @param {string} [modulesDir=node_modules]
* @return {Array.<{pkg, path, hooks}>} package metadata
*/
discover(rootPath, pkg, modulesDir = 'node_modules') {
const deps = assign({}, pkg.dependencies, pkg.devDependencies);
return Object.keys(deps)
.map(dependency => this.fromPackage(path.join(rootPath, modulesDir, dependency, '/')))
.filter(Boolean)
.map(this.resolvePkg)
.filter(Boolean);
}
/**
* Generates metadata for package
* @param {{path: string, pkg: object}} addon addon path and package.json object
* @return {{pkg, path, hooks}} loaded addon. If no entrypoint was found, the package is skipped and a warning logged.
*/
resolvePkg(addon/*: Addon */)/*: ?Addon */ {
const main = addon.pkg.main || 'index.js';
const mainPath = path.join(addon.path, main);
let loadedAddon;
/*eslint no-sync: 0*/
if (fs.existsSync(mainPath)) {
addon.hooks = require(mainPath);
loadedAddon = addon;
} else {
logger.warn('can\'t find entrypoint for addon: ' + addon.pkg.name);
}
return loadedAddon;
}
fromPackage(packagePath) {
// load packagejson if exists
const pkg = path.join(packagePath, 'package.json');
if (fs.existsSync(pkg)) {
const packageJson = require(pkg);
const keywords = packageJson.keywords || [];
if (keywords.indexOf(this.ADDON_KEYWORD) > -1) {
return {
path: packagePath,
pkg: packageJson
};
}
} else {
logger.debug('no package.json found at ' + packagePath);
}
}
/**
* Take an array of resolvePkg results and already loaded hooks.
* It creates a key -> Array.<{hook: string, addon}> map where each key is the hooks identifier.
* @param addons
* @param loaderHooks
* @return {*}
*/
reduceAddonsToHooks(addons/* [hooks: {}] */, loaderHooks/*: {[key: string]: []} */)/*: {} */ {
// map addons to object where key equals the addons hooks name
return reduce(addons, function (hooks, addon) {
forOwn(addon.hooks, function (val, key) {
// provide empty array for hook key
hooks[key] = hooks[key] || [];
hooks[key].push({
hook: addon.hooks[key],
addon: addon
});
});
return hooks;
}, loaderHooks);
}
callback(hookName/*: string */, callback/*: function */) {
this._callbacks[hookName] = callback;
}
finalize(hooks/*: {} */) {
this._hooks = hooks;
this.callbacks(this);
this._loaded = true;
}
/**
* Creates a function that can be called with additional params that calls all addons for a given hook.
* The second param is passed to each addon hook.
* The returned function represents a call to the callback for the hookName.
* @param {string} hookName name of the hook
* @param {object} [hookConfig] config object that is provided to each hook
* @return {function} generated hook function
* @example
* results = hook('IMG_PIPE')(pipe);
*/
hook(hookName/*: string */, hookConfig/*: any */)/*: function */ {
assert(this._loaded, 'addons have to be loaded before calling any hooks');
assert(this._callbacks[hookName], 'no registered callback for ' + hookName);
let hookFunction = noop;
if (this._hooks[hookName]) {
hookFunction = (...args) => {
const callbackFn = this._callbacks[hookName](...args);
return this._hooks[hookName]
.map(hook => callbackFn(hook.hook(hookConfig)));
};
}
return hookFunction;
}
}
module.exports = AddonLoader;