| /* |
| 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; |