nope-js-browser
Version:
NoPE Runtime for the Browser. For nodejs please use nope-js-node
283 lines (282 loc) • 9.19 kB
JavaScript
import { rgetattr, rsetattr } 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();
});
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, prefix = "", map = null, splitchar = SPLITCHAR, maxDepth = Infinity, level = 0) {
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, options = {}) {
const optionsToUse = Object.assign({
prefix: "",
splitchar: SPLITCHAR,
maxDepth: Infinity,
}, options);
return recursiveForEachModule(lib, optionsToUse.prefix, new Map(), options.splitchar, options.maxDepth, 0);
}
/**
* Helper to list the occourence of a lib.
* @param lib the lib to look for.
* @param options
* @returns
*/
function listOccourence(lib, options = {}) {
const optionsToUse = Object.assign({
splitchar: SPLITCHAR,
maxDepth: Infinity,
}, options);
const flattend = flattenLibrary(lib, optionsToUse);
const occourence = new Map();
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, replacer) {
const { occourence, flattend } = listOccourence(library);
const failed = new Array();
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;
}
/**
* Helper to test if the plugin in is type plugin.
* @param plug the Plugin to test.
* @returns {boolean} the test if it is a plugin.
*/
export function isPlugin(plug) {
if (typeof plug !== "function") {
return false;
}
if (plug.install === undefined) {
return false;
}
if (plug.pluginName === undefined) {
return false;
}
return true;
}
export function plugin(base, extend, name = "") {
if (!Array.isArray(base)) {
base = [base];
}
if (name === "") {
try {
name = `anonymousPlugin${COUNTER++}@${arguments.callee.name}`;
}
catch (e) {
name = `anonymousPlugin${COUNTER++}`;
}
}
extend.base = base;
extend.pluginName = name;
extend.install = (lib) => {
if (typeof lib == "string") {
lib = require(lib);
}
const itemsToUpdate = base.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();
// 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);
return extend;
}
/**
* 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, plugins, log = true) {
let modified = new Set();
if (!Array.isArray(plugins)) {
plugins = [plugins];
}
if (typeof lib == "string") {
lib = require(lib);
}
const pluginsToUse = new Array();
// 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).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();
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, adapted) {
const modified = new Set();
for (const absFileName in require.cache) {
const mod = require.cache[absFileName];
if (mod.loaded && mod.exports) {
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;
}