UNPKG

plug-and-play

Version:

Easily create hooks and let users plug their own logic across your code to make it extensible by everyone with new features.

335 lines (333 loc) 10.9 kB
// src/index.ts import { is_object_literal, is_object } from "mixme"; import * as toposort from "toposort"; // src/error.ts var PlugableError = class _PlugableError extends Error { code; constructor(code, message, ...contexts) { if (Array.isArray(message)) { message = message.filter(function(line) { return !!line; }).join(" "); } message = `${code}: ${message}`; super(message); if (Error.captureStackTrace) { Error.captureStackTrace(this, _PlugableError); } this.code = code; for (let i = 0; i < contexts.length; i++) { const context = contexts[i]; for (const key in context) { if (key === "code") { continue; } const value = context[key]; if (value === void 0) { continue; } this[key] = Buffer.isBuffer(value) ? value.toString() : value === null ? value : JSON.parse(JSON.stringify(value)); } } } }; var error_default = function(...args) { return new PlugableError(...args); }; // src/index.ts var plugandplay = function({ args = [], chain, parent, plugins = [] } = {}) { const store = []; const api = { // Register new plugins register: function(plugin) { if (typeof plugin !== "function" && !is_object_literal(plugin)) { throw error_default("PLUGINS_REGISTER_INVALID_ARGUMENT", [ "a plugin must be an object literal or a function returning an object literal", "with keys such as `name`, `required` and `hooks`,", `got ${JSON.stringify(plugin)} instead.` ]); } plugin = typeof plugin === "function" ? plugin(...args) : plugin; const hooksNormalized = {}; for (const name in plugin.hooks) { if (!plugin.hooks[name]) continue; hooksNormalized[name] = normalize_hook(name, plugin.hooks[name]); } if (plugin.require && !Array.isArray(plugin.require)) { plugin.require = [plugin.require]; } const require2 = !plugin.require ? [] : !Array.isArray(plugin.require) ? [plugin.require] : plugin.require; const requireNormalized = require2.map((require3) => { if (typeof require3 !== "string") throw errors.PLUGINS_REGISTER_INVALID_REQUIRE({ name: plugin.name, require: require3 }); return require3; }); const normalizedPlugin = { hooks: hooksNormalized, require: requireNormalized, name: plugin.name }; store.push(normalizedPlugin); return chain ?? this; }, registered: function(name) { for (const plugin of store) { if (plugin.name === name) { return true; } } if (parent != null && parent.registered(name)) { return true; } return false; }, get: function({ name, hooks = [], sort = true }) { const normalizedHooks = [ // Merge hooks provided by the user ...normalize_hook(name, hooks), // With hooks present in the store ...store.map(function(plugin) { if (!plugin.hooks[name]) return; for (const require2 of plugin.require) { if (!api.registered(require2)) { throw errors.REQUIRED_PLUGIN({ plugin: plugin.name, require: require2 }); } } return plugin.hooks[name]?.map( (hook) => ({ plugin: plugin.name, require: plugin.require, ...hook }) ); }).filter(function(hook) { return hook !== void 0; }).flat(1), ...parent ? parent.get({ name, sort: false }) : [] ]; if (!sort) { return normalizedHooks; } const index = {}; for (const hook of normalizedHooks) { if (hook.plugin) index[hook.plugin] = hook; } const edges_after = normalizedHooks.map(function(hook) { return hook.after.reduce(function(result, after) { if (index[after]) { result.push([index[after], hook]); } else if (api.registered(after)) { throw errors.PLUGINS_HOOK_AFTER_INVALID({ name, plugin: hook.plugin, after }); } return result; }, []); }); const edges_before = normalizedHooks.map(function(hook) { return hook.before.reduce(function(result, before) { if (index[before]) { result.push([hook, index[before]]); } else if (api.registered(before)) { throw errors.PLUGINS_HOOK_BEFORE_INVALID({ name, plugin: hook.plugin, before }); } return result; }, []); }); const edges = [...edges_after, ...edges_before].flat(1); return toposort.array(normalizedHooks, edges); }, // Call a hook against each registered plugin matching the hook name call: async function({ args: args2, handler, hooks = [], name }) { if (arguments.length !== 1) { throw error_default("PLUGINS_INVALID_ARGUMENTS_NUMBER", [ "function `call` expect 1 object argument,", `got ${arguments.length} arguments.` ]); } else if (!is_object_literal(arguments[0])) { throw error_default("PLUGINS_INVALID_ARGUMENT_PROPERTIES", [ "function `call` expect argument to be a literal object", "with the properties `name`, `args`, `hooks` and `handler`,", `got ${JSON.stringify(arguments[0])} arguments.` ]); } else if (typeof name !== "string") { throw error_default("PLUGINS_INVALID_ARGUMENT_NAME", [ "function `call` requires a property `name` in its first argument,", `got ${JSON.stringify(arguments[0])} argument.` ]); } const hooksNormalized = this.get({ hooks, name }); handler = handler || (() => { }); for (const hook of hooksNormalized) { switch (hook.handler.length) { case 0: case 1: await hook.handler(args2, () => { }); break; case 2: const result = await hook.handler(args2, handler); if (result === null) { return null; } else { handler = result; } break; default: throw error_default("PLUGINS_INVALID_HOOK_HANDLER", [ "hook handlers must have 0 to 2 arguments", `got ${hook.handler.length}` ]); } } return handler ? handler(args2, () => { }) : void 0; }, // Call a hook against each registered plugin matching the hook name call_sync: function({ args: args2, handler, hooks = [], name }) { if (arguments.length !== 1) { throw error_default("PLUGINS_INVALID_ARGUMENTS_NUMBER", [ "function `call` expect 1 object argument,", `got ${arguments.length} arguments.` ]); } else if (!is_object_literal(arguments[0])) { throw error_default("PLUGINS_INVALID_ARGUMENT_PROPERTIES", [ "function `call` expect argument to be a literal object", "with the properties `name`, `args`, `hooks` and `handler`,", `got ${JSON.stringify(arguments[0])} arguments.` ]); } else if (typeof name !== "string") { throw error_default("PLUGINS_INVALID_ARGUMENT_NAME", [ "function `call` requires a property `name` in its first argument,", `got ${JSON.stringify(arguments[0])} argument.` ]); } const hooksNormalized = this.get({ hooks, name }); handler = handler || (() => { }); for (const hook of hooksNormalized) { switch (hook.handler.length) { case 0: case 1: hook.handler(args2, () => { }); break; case 2: const result = hook.handler(args2, handler); if (result === null) { return null; } else { handler = result; } break; default: throw error_default("PLUGINS_INVALID_HOOK_HANDLER", [ "hook handlers must have 0 to 2 arguments", `got ${hook.handler.length}` ]); } } return handler ? handler(args2, () => { }) : void 0; } }; for (const plugin of plugins) { api.register(plugin); } return api; }; var normalize_hook = function(name, hook) { const hooks = Array.isArray(hook) ? hook : [hook]; return hooks.map(function(hook2) { if (typeof hook2 !== "function" && !is_object(hook2)) { throw error_default("PLUGINS_HOOK_INVALID_HANDLER", [ "no hook handler function could be found,", "a hook must be defined as a function", "or as an object with an handler property,", `got ${JSON.stringify(hook2)} instead.` ]); } return { after: !(typeof hook2 !== "function" && hook2.after) ? [] : typeof hook2.after === "string" ? [hook2.after] : hook2.after, name, before: !(typeof hook2 !== "function" && hook2.before) ? [] : typeof hook2.before === "string" ? [hook2.before] : hook2.before, handler: typeof hook2 === "function" ? hook2 : hook2.handler }; }); }; var errors = { PLUGINS_HOOK_AFTER_INVALID: function({ name, plugin, after }) { throw error_default("PLUGINS_HOOK_AFTER_INVALID", [ `the hook ${JSON.stringify(name)}`, plugin ? `in plugin ${JSON.stringify(plugin)}` : "", "references an after dependency", `in plugin ${JSON.stringify(after)} which does not exists.` ]); }, PLUGINS_HOOK_BEFORE_INVALID: function({ name, plugin, before }) { throw error_default("PLUGINS_HOOK_BEFORE_INVALID", [ `the hook ${JSON.stringify(name)}`, plugin ? `in plugin ${JSON.stringify(plugin)}` : "", "references a before dependency", `in plugin ${JSON.stringify(before)} which does not exists.` ]); }, REQUIRED_PLUGIN: function({ plugin, require: require2 }) { throw error_default("REQUIRED_PLUGIN", [ `the plugin ${JSON.stringify(plugin)}`, "requires a plugin", `named ${JSON.stringify(require2)} which is not unregistered.` ]); }, PLUGINS_REGISTER_INVALID_REQUIRE: function({ name, require: require2 }) { throw error_default("PLUGINS_REGISTER_INVALID_REQUIRE", [ "the `require` property", name ? `in plugin ${JSON.stringify(name)}` : "", "must be a string or an array,", `got ${JSON.stringify(require2)}.` ]); } }; export { PlugableError, plugandplay }; //# sourceMappingURL=index.js.map