| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";class HookCodeFactory {	constructor(config) {		this.config = config;		this.options = undefined;		this._args = undefined;	}	create(options) {		this.init(options);		let fn;		switch (this.options.type) {			case "sync":				fn = new Function(					this.args(),					'"use strict";\n' +						this.header() +						this.content({							onError: err => `throw ${err};\n`,							onResult: result => `return ${result};\n`,							resultReturns: true,							onDone: () => "",							rethrowIfPossible: true						})				);				break;			case "async":				fn = new Function(					this.args({						after: "_callback"					}),					'"use strict";\n' +						this.header() +						this.content({							onError: err => `_callback(${err});\n`,							onResult: result => `_callback(null, ${result});\n`,							onDone: () => "_callback();\n"						})				);				break;			case "promise":				let errorHelperUsed = false;				const content = this.content({					onError: err => {						errorHelperUsed = true;						return `_error(${err});\n`;					},					onResult: result => `_resolve(${result});\n`,					onDone: () => "_resolve();\n"				});				let code = "";				code += '"use strict";\n';				code += "return new Promise((_resolve, _reject) => {\n";				if (errorHelperUsed) {					code += "var _sync = true;\n";					code += "function _error(_err) {\n";					code += "if(_sync)\n";					code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";					code += "else\n";					code += "_reject(_err);\n";					code += "};\n";				}				code += this.header();				code += content;				if (errorHelperUsed) {					code += "_sync = false;\n";				}				code += "});\n";				fn = new Function(this.args(), code);				break;		}		this.deinit();		return fn;	}	setup(instance, options) {		instance._x = options.taps.map(t => t.fn);	}	/**	 * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options	 */	init(options) {		this.options = options;		this._args = options.args.slice();	}	deinit() {		this.options = undefined;		this._args = undefined;	}	header() {		let code = "";		if (this.needContext()) {			code += "var _context = {};\n";		} else {			code += "var _context;\n";		}		code += "var _x = this._x;\n";		if (this.options.interceptors.length > 0) {			code += "var _taps = this.taps;\n";			code += "var _interceptors = this.interceptors;\n";		}		for (let i = 0; i < this.options.interceptors.length; i++) {			const interceptor = this.options.interceptors[i];			if (interceptor.call) {				code += `${this.getInterceptor(i)}.call(${this.args({					before: interceptor.context ? "_context" : undefined				})});\n`;			}		}		return code;	}	needContext() {		for (const tap of this.options.taps) if (tap.context) return true;		return false;	}	callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {		let code = "";		let hasTapCached = false;		for (let i = 0; i < this.options.interceptors.length; i++) {			const interceptor = this.options.interceptors[i];			if (interceptor.tap) {				if (!hasTapCached) {					code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;					hasTapCached = true;				}				code += `${this.getInterceptor(i)}.tap(${					interceptor.context ? "_context, " : ""				}_tap${tapIndex});\n`;			}		}		code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;		const tap = this.options.taps[tapIndex];		switch (tap.type) {			case "sync":				if (!rethrowIfPossible) {					code += `var _hasError${tapIndex} = false;\n`;					code += "try {\n";				}				if (onResult) {					code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({						before: tap.context ? "_context" : undefined					})});\n`;				} else {					code += `_fn${tapIndex}(${this.args({						before: tap.context ? "_context" : undefined					})});\n`;				}				if (!rethrowIfPossible) {					code += "} catch(_err) {\n";					code += `_hasError${tapIndex} = true;\n`;					code += onError("_err");					code += "}\n";					code += `if(!_hasError${tapIndex}) {\n`;				}				if (onResult) {					code += onResult(`_result${tapIndex}`);				}				if (onDone) {					code += onDone();				}				if (!rethrowIfPossible) {					code += "}\n";				}				break;			case "async":				let cbCode = "";				if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;				else cbCode += `_err${tapIndex} => {\n`;				cbCode += `if(_err${tapIndex}) {\n`;				cbCode += onError(`_err${tapIndex}`);				cbCode += "} else {\n";				if (onResult) {					cbCode += onResult(`_result${tapIndex}`);				}				if (onDone) {					cbCode += onDone();				}				cbCode += "}\n";				cbCode += "}";				code += `_fn${tapIndex}(${this.args({					before: tap.context ? "_context" : undefined,					after: cbCode				})});\n`;				break;			case "promise":				code += `var _hasResult${tapIndex} = false;\n`;				code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({					before: tap.context ? "_context" : undefined				})});\n`;				code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;				code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;				code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;				code += `_hasResult${tapIndex} = true;\n`;				if (onResult) {					code += onResult(`_result${tapIndex}`);				}				if (onDone) {					code += onDone();				}				code += `}, _err${tapIndex} => {\n`;				code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;				code += onError(`_err${tapIndex}`);				code += "});\n";				break;		}		return code;	}	callTapsSeries({		onError,		onResult,		resultReturns,		onDone,		doneReturns,		rethrowIfPossible	}) {		if (this.options.taps.length === 0) return onDone();		const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");		const somethingReturns = resultReturns || doneReturns || false;		let code = "";		let current = onDone;		for (let j = this.options.taps.length - 1; j >= 0; j--) {			const i = j;			const unroll = current !== onDone && this.options.taps[i].type !== "sync";			if (unroll) {				code += `function _next${i}() {\n`;				code += current();				code += `}\n`;				current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;			}			const done = current;			const doneBreak = skipDone => {				if (skipDone) return "";				return onDone();			};			const content = this.callTap(i, {				onError: error => onError(i, error, done, doneBreak),				onResult:					onResult &&					(result => {						return onResult(i, result, done, doneBreak);					}),				onDone: !onResult && done,				rethrowIfPossible:					rethrowIfPossible && (firstAsync < 0 || i < firstAsync)			});			current = () => content;		}		code += current();		return code;	}	callTapsLooping({ onError, onDone, rethrowIfPossible }) {		if (this.options.taps.length === 0) return onDone();		const syncOnly = this.options.taps.every(t => t.type === "sync");		let code = "";		if (!syncOnly) {			code += "var _looper = () => {\n";			code += "var _loopAsync = false;\n";		}		code += "var _loop;\n";		code += "do {\n";		code += "_loop = false;\n";		for (let i = 0; i < this.options.interceptors.length; i++) {			const interceptor = this.options.interceptors[i];			if (interceptor.loop) {				code += `${this.getInterceptor(i)}.loop(${this.args({					before: interceptor.context ? "_context" : undefined				})});\n`;			}		}		code += this.callTapsSeries({			onError,			onResult: (i, result, next, doneBreak) => {				let code = "";				code += `if(${result} !== undefined) {\n`;				code += "_loop = true;\n";				if (!syncOnly) code += "if(_loopAsync) _looper();\n";				code += doneBreak(true);				code += `} else {\n`;				code += next();				code += `}\n`;				return code;			},			onDone:				onDone &&				(() => {					let code = "";					code += "if(!_loop) {\n";					code += onDone();					code += "}\n";					return code;				}),			rethrowIfPossible: rethrowIfPossible && syncOnly		});		code += "} while(_loop);\n";		if (!syncOnly) {			code += "_loopAsync = true;\n";			code += "};\n";			code += "_looper();\n";		}		return code;	}	callTapsParallel({		onError,		onResult,		onDone,		rethrowIfPossible,		onTap = (i, run) => run()	}) {		if (this.options.taps.length <= 1) {			return this.callTapsSeries({				onError,				onResult,				onDone,				rethrowIfPossible			});		}		let code = "";		code += "do {\n";		code += `var _counter = ${this.options.taps.length};\n`;		if (onDone) {			code += "var _done = () => {\n";			code += onDone();			code += "};\n";		}		for (let i = 0; i < this.options.taps.length; i++) {			const done = () => {				if (onDone) return "if(--_counter === 0) _done();\n";				else return "--_counter;";			};			const doneBreak = skipDone => {				if (skipDone || !onDone) return "_counter = 0;\n";				else return "_counter = 0;\n_done();\n";			};			code += "if(_counter <= 0) break;\n";			code += onTap(				i,				() =>					this.callTap(i, {						onError: error => {							let code = "";							code += "if(_counter > 0) {\n";							code += onError(i, error, done, doneBreak);							code += "}\n";							return code;						},						onResult:							onResult &&							(result => {								let code = "";								code += "if(_counter > 0) {\n";								code += onResult(i, result, done, doneBreak);								code += "}\n";								return code;							}),						onDone:							!onResult &&							(() => {								return done();							}),						rethrowIfPossible					}),				done,				doneBreak			);		}		code += "} while(false);\n";		return code;	}	args({ before, after } = {}) {		let allArgs = this._args;		if (before) allArgs = [before].concat(allArgs);		if (after) allArgs = allArgs.concat(after);		if (allArgs.length === 0) {			return "";		} else {			return allArgs.join(", ");		}	}	getTapFn(idx) {		return `_x[${idx}]`;	}	getTap(idx) {		return `_taps[${idx}]`;	}	getInterceptor(idx) {		return `_interceptors[${idx}]`;	}}module.exports = HookCodeFactory;
 |