"use strict"; /** * Plugins. * * @module */ const fs = require("fs"); const path = require("path"); const util = require("util"); const _ = require("lodash"); const expect = require("chai").expect; const U = require("glace-utils"); const CONF = U.config; // HACK to avoid cross-import between config.js and plugins.js const LOG = U.logger; let systemPlugins = null; let customPlugins = []; /** * Get list of found and registered plugins. Each plugin is an object with keys: * `name` - plugin name, `path` - plugin path, `module` - loaded plugin module. * Order of resolving, if there plugins with the same names: * 1. Registered custom plugins. * 2. Plugins which are far from `glace-core` package. * * @function * @return {array<object>} */ const get = () => { if (!systemPlugins) { systemPlugins = []; systemPlugins = findPlugins(systemPlugins); systemPlugins = getPluginsFromDir(systemPlugins); systemPlugins = loadPlugins(systemPlugins); } const customPluginNames = getNames({ type: "custom" }); return systemPlugins .filter(p => !customPluginNames.includes(p.name)) .concat(customPlugins); }; /** * Traverse folders and find `glace` plugins. * @ignore */ const findPlugins = plugins => { if (CONF.plugins.disableDefault) return plugins; const SKIPPED_PACKAGES = ["glace-js", "glace-core", "glace-utils"]; const foundPlugins = []; const foundPluginNames = []; for (const pluginDir of dirsToSearchPlugins()) { if (!fs.existsSync(pluginDir)) continue; for (const fileName of fs.readdirSync(pluginDir)) { if (!fileName.startsWith("glace-") || SKIPPED_PACKAGES.includes(fileName) || foundPluginNames.includes(fileName)) continue; foundPlugins.push({ name: fileName, path: path.resolve(pluginDir, fileName), }); foundPluginNames.push(fileName); } } return plugins .filter(p => !foundPluginNames.includes(p.name)) .concat(foundPlugins); }; /** * Get list of plugins for folder specified in config. * @ignore */ const getPluginsFromDir = plugins => { if (!CONF.plugins.dir) return plugins; expect(fs.existsSync(CONF.plugins.dir) && fs.statSync(CONF.plugins.dir).isDirectory(), `Plugins folder '${CONF.plugins.dir}' doesn't exist or isn't a folder`).to.be.true; const dirPlugins = []; const dirPluginNames = []; for (const fileName of fs.readdirSync(CONF.plugins.dir)) { dirPlugins.push({ name: fileName, path: path.resolve(CONF.plugins.dir, fileName), }); dirPluginNames.push(fileName); } return plugins .filter(p => !dirPluginNames.includes(p.name)) .concat(dirPlugins); }; /** * Load required plugins and return back list of loaded plugins. * @ignore */ const loadPlugins = plugins => { const loadedPlugins = []; for (const plugin of plugins) { try { plugin.module = require(plugin.path); loadedPlugins.push(Object.freeze(plugin)); } catch (e) { LOG.error(util.format(`Can't load plugin '${plugin.path}':`, e)); } } return loadedPlugins; }; /** * Gets modules from plugins. * * @arg {string} moduleName - Name of module to request from plugins. * @return {object[]} - List of modules requested from plugins. */ const getModules = moduleName => { const modules = []; for (const plugin of get()) { const mod = plugin.module[moduleName]; if (mod) modules.push(mod); } return modules; }; /** * Clear plugins cache. * * @function */ const clearCache = () => systemPlugins = null; /** * Registers custom plugin. * * @function * @arg {string} name - Name of plugin module. */ const register = name => { const customPluginNames = getNames({ type: "custom" }); if (customPluginNames.includes(name)) { LOG.warn(`Plugin '${name}' is registered already`); return; } customPlugins.push({ name: name, path: require.resolve(name), module: require(name), }); }; /** * Removes custom plugin from list of registered. * * @function * @arg {string} name - Name of registered plugin. */ const remove = name => { const customPluginNames = getNames({ type: "custom" }); if (!customPluginNames.includes(name)) { LOG.warn(`Plugin '${name}' isn't registered yet`); return; } customPlugins = customPlugins.filter(p => p.name !== name); }; /** * Gets names of plugins. * * @function * @arg {object} opts - Options. * @arg {string} [opts.type] - Type of plugins. Supported values are `custom` * and `system`, if omitted then names of all plugins will be returned. * @return {array<string>} Plugin names. */ const getNames = opts => { opts = opts || {}; let names; if (opts.type === "custom") { names = customPlugins.map(p => p.name).sort(); } else if (opts.type === "system") { names = (systemPlugins || []).map(p => p.name).sort(); } else { names = _.uniq((systemPlugins || []).map(p => p.name).concat(customPlugins.map(p => p.name))).sort(); }; return names; }; /** * Get folders to search plugins. * @ignore */ const dirsToSearchPlugins = () => { const pluginDirs = _.clone(module.paths); if (require.main) { const mainDir = path.dirname(require.main.filename); if (!pluginDirs.includes(mainDir)) { pluginDirs.unshift(mainDir); } } if (!pluginDirs.includes(U.cwd)) { pluginDirs.unshift(U.cwd); } return pluginDirs; }; module.exports = { get, getModules, getNames, register, remove, clearCache, };