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
JavaScript
// 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