UNPKG

tennu-plugins

Version:
357 lines (305 loc) 13.2 kB
const format = require('util').format; const Result = require("r-result"); const Ok = Result.Ok; const Fail = Result.Fail; const failures = require("./failures"); const Registry = require("./registry"); const PluginSystem = function (context) { const plugins = Registry(); const roles = Registry(); const staticHooks = Registry(); const instanceHooks = Registry(); function addInstanceHook (hook, fn) { if (!(typeof hook === "string" || typeof hook === "symbol")) { throw new TypeError("Hook's name must be string or symbol"); } if (typeof fn !== "function") { throw new TypeError("Hook's function argument is not a function."); } const setInstanceHookResult = instanceHooks.set(hook, fn); return setInstanceHookResult.mapFail(function (failure) { return { failureReason: failures.InstanceHookAlreadyExists, message: format("Tried to set instance hook '%s', but a plugin already has that instance hook.", hook), hook: hook }; }); } function addStaticHook (hook, fn) { if (!(typeof hook === "string" || typeof hook === "symbol")) { throw new TypeError("Hook's name must be string or symbol") } if (typeof fn !== "function") { throw new TypeError("Hook's function argument is not a function."); } const setStaticHookResult = staticHooks.set(hook, fn) return setStaticHookResult.mapFail(function (failure) { return { failureReason: failures.StaticHookAlreadyEixsts, message: format("Tried to set static hook '%s', but a plugin already has that static hook.", hook), hook: hook }; }); } /* enum PluginInitializableValidation { canInitialize: Boolean, pluginFactory: any // but probably a PluginFactory failureReason: Option<Symbol>, reason: Option<String>, help: Option<String>, } If canInitialize is `true`, no other fields will be set. If canInitialize is `false`, `failureReason` and `reason` will be set. `help` may be set. */ // isInitializable(pluginFactory: any*)) -> IsInitializableValidation // * probably a PluginFactory function isInitializable(pluginFactory) { return isInitializableWith(pluginFactory, []); } // isInitializableWith(pluginFactory: any*, assumedPlugins: [PluginFactory]) -> IsInitializableValidation // * probably a PluginFactory // Says if a plugin could be initialized if we also assumed the Plugins in // the second list were also initialized. // WHY(Havvy): This function is used by `use` in loader.js because we want // to know which plugins cannot be initialized due to missing // dependencies that aren't in the list passed to `use`. function isInitializableWith(pluginFactory, assumedPlugins) { if (typeof pluginFactory !== "object" || pluginFactory === null) { const pluginType = typeof pluginFactory === "object" ? "null" : typeof pluginFactory; return { canInitialize: false, pluginFactory, failureReason: failures.validation.NotAnObject, reason: "PluginFactory not an object.", help: `Was given a ${pluginType} instead.` }; } if (typeof pluginFactory.name !== "string") { return { canInitialize: false, pluginFactory, failureReason: failures.validation.NameNotAString, reason: "PluginFactory 'name' property not a string.", help: `PluginFactory 'name' property is a ${typeof pluginFactory.name} instead.` } } if (plugins.has(pluginFactory.name) || assumedPlugins.some(function (assumedPluginFactory) { return assumedPluginFactory.name === pluginFactory.name; })) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.PluginAlreadyExists, reason: `A plugin with the name '${pluginFactory.name}' has already been installed.` } } if (pluginFactory.role) { if (typeof pluginFactory.role !== "string") { return { canInitialize: false, pluginFactory, failureReason: failures.validation.RoleNotAString, reason: "PluginFactory object's 'role' property not be a string (or undefined).", help: `PluginFactory 'role' property is a ${typeof pluginFactory.role} instead.` } } if (roles.has(pluginFactory.role)) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.RoleAlreadyExists, reason: `A plugin with the role '${pluginFactory.role}' has already been installed.` // Can't actually get the name of the plugin at this point // without changing the system to store that map... } } const maybePlugin = assumedPlugins.find(function (assumedPluginFactory) { return pluginFactory.role === assumedPluginFactory.role; }); if (maybePlugin !== undefined) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.RoleAlreadyExists, reason: `A plugin with the role '${pluginFactory.role}' has already been installed.`, help: `The plugin with this role is '${maybePlugin.name}'.`, } } } if (pluginFactory.requires) { if (!Array.isArray(pluginFactory.requires)) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.RequiresNotAnArray, reason: "PluginFactory object's 'requires' property is not an array (or undefined).", help: `PluginFactory 'requires' property is a ${typeof pluginFactory.requires} instead.`, } } const unfulfilledRequirements = pluginFactory.requires.filter(function (requirement) { return !plugins.has(requirement) && !assumedPlugins.some(function (pluginFactory) { return pluginFactory.name === requirement; }); }); if (unfulfilledRequirements.length > 0) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.UnfulfilledPluginDependencies, reason: "Unfulfilled required plugins.", help: `Please provide these plugins first: ${unfulfilledRequirements.join(", ")}`, unfulfilledRequirements } } } if (pluginFactory.requiresRoles) { if (!Array.isArray(pluginFactory.requiresRoles)) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.RequiresRolesNotAnArray, reason: "PluginFactory's 'requiresRoles' property is not an array (or undefined).", help: `PluginFactory 'requires' property is a ${typeof pluginFactory.requiresRoles} instead.` } } const unfulfilledRoleRequirements = pluginFactory.requiresRoles.filter(function (requirement) { return !roles.has(requirement)&& !assumedPlugins.some(function (assumedPluginFactory) { return assumedPluginFactory.role === requirement; }); }); if (unfulfilledRoleRequirements.length > 0) { return { canInitialize: false, pluginFactory, failureReason: failures.validation.UnfulfilledRoleDependencies, reason: "Unfulfilled required roles.", help: `Please provide these roles first: ${unfulfilledRoleRequirements.join(", ")}`, unfulfilledRoleRequirements } } } return { canInitialize: true, pluginFactory }; } function dependencies (pluginFactory) { var res = {}; if (pluginFactory.requires) { pluginFactory.requires.forEach(function (dependency) { res[dependency] = plugins.get(dependency).exports; }); } if (pluginFactory.requiresRoles) { pluginFactory.requiresRoles.forEach(function (role) { res[role] = roles.get(role).exports; }); } return res; } const isLoaded = plugins.has.bind(plugins); function initialize(pluginFactory) { const validation = isInitializable(pluginFactory); if (validation.canInitialize) { return unsafeInitialize(pluginFactory); } else { return Fail({ failureReason: failures.CannotInitialize, message: "The plugin cannot be initialized. For why, check the validationFailure.", validationFailure: validation, validationFailureTypes: failures.validation, pluginFactory }); } } function unsafeInitialize (pluginFactory) { // Apply static hooks. staticHooks.forEach(function (hook, fn) { if (pluginFactory.hasOwnProperty(hook)) { fn(pluginFactory.name, pluginFactory[hook]); } }); // Create instance. const instance = pluginFactory.init(context, dependencies(pluginFactory)); if (typeof instance !== "object") { return Fail({ failureReason: failures.PluginNotAnObject, message: `pluginFactory instance from init function must be an object. Got ${String(instance)}, a ${typeof instance}, instead.` }); } // Add instance hooks. if (instance.hooks) { const addInstanceHooksResult = Object.keys(instance.hooks) .reduce(function (addInstanceHookResult, hook) { return addInstanceHookResult.andThen(function () { return addInstanceHook(hook, instance.hooks[hook]) }); }, Ok()); if (addInstanceHooksResult.isFail()) { return addInstanceHooksResult; } } // Add static hooks. if (instance.staticHooks) { const addStaticHooksResult = Object.keys(instance.staticHooks) .reduce(function (addStaticHookResult, hook) { return addStaticHookResult.andThen(function () { return addStaticHook(hook, instance.staticHooks[hook]) }); }, Ok()); if (addStaticHooksResult.isFail()) { return addStaticHooksResult; } } // Apply instance hooks. instanceHooks.forEach(function (hook, fn) { if (instance.hasOwnProperty(hook)) { fn(pluginFactory.name, instance[hook]); } }); // Register instance. // These cannot return a Fail() because isInitialable would have // already said it wasn't actually initializable. plugins.set(pluginFactory.name, instance); if (pluginFactory.role) { roles.set(pluginFactory.role, instance); } return Ok(); } function pluginExportsOf (name) { if (plugins.has(name)) { return Ok(plugins.get(name).exports); } else { return Fail({ failureReason: failures.PluginNotInstalled, name }); } } function roleExportsOf (name) { if (roles.has(name)) { return Ok(roles.get(name).exports); } else { return Fail({ failureReason: failures.RoleNotInstalled, name }); } return undefined; } return { initialize: initialize, unsafeInitialize: unsafeInitialize, isInitializable: isInitializable, isInitializableWith: isInitializableWith, hasPlugin: plugins.has, hasRole: roles.has, pluginExportsOf: pluginExportsOf, roleExportsOf: roleExportsOf, loadedNames: plugins.keys, addInstanceHook: addInstanceHook, addStaticHook: addStaticHook }; } module.exports = PluginSystem;