You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							407 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
	
	
							407 lines
						
					
					
						
							10 KiB
						
					
					
				/* | 
						|
	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;
 | 
						|
 |