Source: instantiators/PrototypeWrapper.js

/**
 * @typedef Object PrototypeWrap
 * @property {Object} targetPrototype The proxied and wired up prototype to use
 * @property {Array} originalChain The original prototype chain to use later to restore the prototype
 */

/**
 * The Prototype Wrapper will resolve dependencies up in the prototype tree.
 * If you require a module which inherits other prototypes, this wrapper will insert proxies in
 * between the inheritance steps which then will handle the dependency injections in the parent
 * prototypes. You can still call super() with arguments in the constructor, the supplied arguments
 * will then be appended to the injected properties in the constructor of the parent prototype.
 *
 * @author Wolfgang Felbermeier (@f3lang)
 */
class PrototypeWrapper {

	constructor(objectManager) {
		this.objectManager = objectManager;
	}

	/**
	 * Will wrap the node require and inject prototype proxies for the dependency injection.
	 * @param {String} module The path of the module to require
	 * @param {String} root The identifier of the tree to work in
	 * @param {String} requestId The id of the current injection request
	 * @return {PrototypeWrap} An Object containing the resolved prototype and the original chain
	 */
	require(module, root, requestId) {
		let mod = require(module);
		let prototypeChain = this.buildPrototypeChain(mod);
		if (prototypeChain.length === 1) {
			// no chain detected so we can continue with a regular require()
			return {targetPrototype: mod, originalChain: null};
		} else if (prototypeChain.length === 2) {
			// chain with one level of inheritance detected
			let targetPrototype = prototypeChain[0];
			let wrappedParentPrototype = this.wrapPrototype(prototypeChain[1], root, requestId + prototypeChain[1].name);
			Object.setPrototypeOf(targetPrototype, wrappedParentPrototype);
			return {targetPrototype, originalChain: prototypeChain};
		}
		// more complex chain detected with multiple levels of inheritance
		let originalChain = prototypeChain.slice();
		let i = prototypeChain.length;
		let targetPrototype = prototypeChain[i - 2];
		while (i-- && i > 1) {
			let upperPrototype = prototypeChain[i];
			let proxiedUpperPrototype = this.wrapPrototype(upperPrototype, root, requestId + upperPrototype.name);
			Object.setPrototypeOf(targetPrototype, proxiedUpperPrototype);
			prototypeChain[i - 1] = targetPrototype;
			targetPrototype = prototypeChain[i - 2];
		}
		Object.setPrototypeOf(targetPrototype, prototypeChain[1]);
		return {targetPrototype, originalChain};
	}

	/**
	 * Will traverse the prototype chain recursively up to the top and convert
	 * it to an array.
	 * @param {Object} prototype The prototype to build a chain for
	 * @return {Array} An array with all prototypes of the chain ordered from bottom to top.
	 */
	buildPrototypeChain(prototype) {
		let chain = [];
		chain.push(prototype);
		if (Object.getPrototypeOf(prototype) !== Object.getPrototypeOf(class {
			})) {
			chain.push(...this.buildPrototypeChain(Object.getPrototypeOf(prototype)));
		}
		return chain;
	}

	/**
	 * Will wrap a prototype in a proxy, that will inject the dependencies into the
	 * original prototype constructor.
	 * @param {Object} prototype The prototype to wrap
	 * @param {String} root The identifier of the tree to work in
	 * @param {String} requestId The id of the current injection request
	 * @return {Object} The wrapped prototype
	 */
	wrapPrototype(prototype, root, requestId) {
		let injectConfiguration = this.objectManager.getInjectionConfiguration(prototype.name);
		prototype.tree = root;
		if (!injectConfiguration || injectConfiguration.config.injectMap.length === 0) {
			return prototype;
		}
		let params = this.objectManager.getModuleParams(injectConfiguration.config.injectMap, root, requestId);
		return class extends prototype {
			constructor(...args) {
				module.exports.tree = root;
				super(...params, ...args);
			}
		};
	}

	/**
	 * After instantiation of the module it is important to restore the original
	 * prototype tree, since node will somehow cache it, which will lead to problems
	 * when traversing the tree again.
	 * @param {Array} originalChain The original prototype chain as an Array
	 */
	restoreOriginalPrototypeChain(originalChain) {
		if (originalChain.length === 2) {
			Object.setPrototypeOf(originalChain[0], originalChain[1]);
			return;
		}
		let i = originalChain.length - 1;
		while (i--) {
			Object.setPrototypeOf(originalChain[i], originalChain[i + 1]);
		}
	}

}

module.exports = PrototypeWrapper;
module.exports.inject = ['ObjectManager'];