nope/lib/plugins/plugin.ts

381 lines
8.7 KiB
TypeScript

import { rgetattr, rsetattr, getType } from "../helpers/objectMethods";
import { union } from "../helpers/setMethods";
import { getNopeLogger } from "../logger/index.browser";
import { getSingleton } from "../helpers/singletonMethod";
let COUNTER = 0;
const SPLITCHAR = ".";
const PLUGIN_STORE = getSingleton("nope.plugins", () => {
return new Map<string, Plugin>();
});
const ABORT_INSPECTION_TESTERS = [
(item) => {
const type = typeof item;
const not = [
"string",
"number",
"bigint",
"boolean",
"symbol",
"undefined",
"function",
];
return not.includes(type);
},
(item) => Array.isArray(item),
];
function shouldAbort(item) {
for (const test of ABORT_INSPECTION_TESTERS) {
if (test(item)) {
return true;
}
}
return false;
}
function recursiveForEachModule(
obj: any,
prefix: string = "",
map: Map<string, any> = null,
splitchar: string = SPLITCHAR,
maxDepth = Infinity,
level = 0
): any {
if (map === null) {
map = new Map();
}
map.set(prefix, obj);
if (level > maxDepth) {
return map;
}
if (shouldAbort(obj)) {
return map;
}
// Create an Array with the Keys.
const keys = Object.getOwnPropertyNames(obj);
// If there are Keys => It is a List or a Default Object
if (keys.length > 0) {
for (const _key of keys) {
// Define the variable, containing the path
const path = prefix === "" ? _key : prefix + splitchar + _key;
map = recursiveForEachModule(
obj[_key],
path,
map,
splitchar,
maxDepth,
level + 1
);
}
}
return map;
}
/**
* Flattens an Object to a Map.
*
* For Instance:
*
* data = {a : { b : { c : 1, d: "hallo"}}}
*
* // Normal Call
* res = flatteObject(data)
* => res = {"a.b.c":1,"a.b.d":"hallo"}
*
* // With a Selected prefix 'additional.name'
* res = flatteObject(data,{prefix:'additional.name'})
* => res = {"additional.name.a.b.c":1,"additional.name.a.b.d":"hallo"}
*
* @export
* @param {*} lib The Data that should be converted
* @param {string} [prefix=''] An additional prefix.
* @returns {Map<string, any>} The flatten Object
*/
function flattenLibrary(
lib: any,
options: {
prefix?: string;
splitchar?: string;
maxDepth?: number;
} = {}
): Map<string, any> {
const optionsToUse = Object.assign(
{
prefix: "",
splitchar: SPLITCHAR,
maxDepth: Infinity,
},
options
);
return recursiveForEachModule(
lib,
optionsToUse.prefix,
new Map(),
options.splitchar,
options.maxDepth,
0
);
}
function listOccourence(
lib,
options: {
splitchar?: string;
maxDepth?: number;
} = {}
) {
const optionsToUse = Object.assign(
{
splitchar: SPLITCHAR,
maxDepth: Infinity,
},
options
);
const flattend = flattenLibrary(lib, optionsToUse);
const occourence = new Map<string, Set<string>>();
for (const key of flattend.keys()) {
const split = key.split(optionsToUse.splitchar);
const last = split[split.length - 1];
if (!occourence.has(last)) {
occourence.set(last, new Set());
}
occourence.get(last).add(key);
}
return {
flattend,
occourence,
};
}
/**
* Helper to install an addon.
* @param library
* @param item
* @param replacer
* @returns
*/
function implementChanges(library, item: string | any, replacer) {
const { occourence, flattend } = listOccourence(library);
const failed = new Array<{ error: any; destination: string }>();
if (occourence.has(item)) {
for (const destination of occourence.get(item)) {
try {
rsetattr(library, destination, replacer, SPLITCHAR);
} catch (error) {
failed.push({
error,
destination,
});
}
}
}
return library;
}
export type Plugin = ExtendFunction & {
install: (lib: string | NodeModule) => Set<string>;
base: string[];
pluginName: string;
};
export function isPlugin(plug: Plugin): plug is Plugin {
if (typeof plug !== "function") {
return false;
}
if ((plug as Plugin).install === undefined) {
return false;
}
if ((plug as Plugin).pluginName === undefined) {
return false;
}
return true;
}
export type ExtendFunction = (
...args
) => Array<{ path: string; name: string; adapted: any }>;
export function plugin(
base: string | string[],
extend: ExtendFunction,
name = ""
): Plugin {
if (!Array.isArray(base)) {
base = [base];
}
if (name === "") {
try {
name = `anonymousPlugin${COUNTER++}@${arguments.callee.name}`;
} catch (e) {
name = `anonymousPlugin${COUNTER++}`;
}
}
(extend as Plugin).base = base;
(extend as Plugin).pluginName = name;
(extend as Plugin).install = (lib: string | NodeModule) => {
if (typeof lib == "string") {
lib = require(lib);
}
const itemsToUpdate = (base as string[]).map((item) =>
rgetattr(lib, item, false, ".")
);
if (itemsToUpdate.includes(false)) {
throw Error(
"Faild to grap some of the given base elements. Please check parameter 'base'"
);
}
let modified = new Set<string>();
// Now apply the addon:
const adaptions = extend(...itemsToUpdate);
if (!Array.isArray(adaptions)) {
throw Error("Return-Type of the Plugin doesnt match.");
}
for (const { path, name, adapted } of adaptions) {
lib = implementChanges(lib, path, adapted);
modified = union(modified, checkRequireCache(name, adapted));
}
return modified;
};
// Store our Plugin as store.
PLUGIN_STORE.instance.set(name, extend as Plugin);
return extend as Plugin;
}
/**
* Helper function to install Plugins.
* @param lib The Library to modify.
* @param plugins The Plugins install. This can be the registered names, pathes in the library or the plugin itself.
* @param log Flag to control the log information.
*/
export function installPlugins(
lib: string | NodeModule,
plugins: string | Plugin | Array<Plugin | string>,
log: boolean = true
) {
let modified = new Set<string>();
if (!Array.isArray(plugins)) {
plugins = [plugins];
}
if (typeof lib == "string") {
lib = require(lib);
}
const pluginsToUse = new Array<Plugin>();
// In this loop we ensure that we load the correct plugin.
for (const plug of plugins) {
if (typeof plug === "string") {
// The Plugin is provided as String.
// 1. Check if the name is present:
if (PLUGIN_STORE.instance.has(plug)) {
pluginsToUse.push(PLUGIN_STORE.instance.get(plug));
} else if (isPlugin(rgetattr(lib, plug, false, "."))) {
pluginsToUse.push(rgetattr(lib, plug, false, "."));
} else {
const p = require(plug as string).extend;
if (isPlugin(p)) {
pluginsToUse.push(PLUGIN_STORE.instance.get(plug));
} else {
throw Error(
"Cannot find plugin '" +
plug +
"'. If this is a file, make shure the plugin is exported as 'extend'"
);
}
}
} else if (isPlugin(plug)) {
pluginsToUse.push(plug);
}
}
let used_plugins_str =
"Plugins used!\n\n" +
"-".repeat(50) +
"\nPLUGIN INSTALLTION REPORT:\n" +
"-".repeat(50) +
"\n\nInstalled the following plugins:";
let used_bases_str = "\n\nThe following source have been modified:";
let used_bases = new Set<string>();
for (const plug of pluginsToUse) {
// Store the Plugin
used_plugins_str += "\n\t- " + plug.pluginName;
// Store the modified elements:
plug.base.map((item) => used_bases.add(item));
// Update the modified sources
modified = union(modified, plug.install(lib));
}
Array.from(used_bases).map((item) => (used_bases_str += "\n\t- " + item));
const end_str = "\n\nWatchout this may change the default behavior!\n\n";
const to_print = used_plugins_str + used_bases_str + end_str;
if (log) {
const logger = getNopeLogger("plugin-system", "debug");
logger.warn(to_print);
}
}
/**
* Helper to list all Plugins
* @returns List of recognized Plugins
*/
export function allPlugins() {
return Array.from(PLUGIN_STORE.instance.keys());
}
function checkRequireCache(name: string, adapted: any) {
const modified = new Set<string>();
for (const absFileName in require.cache) {
const mod = require.cache[absFileName];
if (mod.loaded) {
const exportedItems = Object.getOwnPropertyNames(mod.exports);
if (exportedItems.includes(name)) {
try {
mod.exports[name] = adapted;
modified.add(absFileName);
} catch (e) {
// We are not allowed to reassign
// exported members only.
}
}
}
}
return modified;
}