1 | /*
|
---|
2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
---|
3 | Author Tobias Koppers @sokra
|
---|
4 | */
|
---|
5 | "use strict";
|
---|
6 |
|
---|
7 | class HookCodeFactory {
|
---|
8 | constructor(config) {
|
---|
9 | this.config = config;
|
---|
10 | this.options = undefined;
|
---|
11 | this._args = undefined;
|
---|
12 | }
|
---|
13 |
|
---|
14 | create(options) {
|
---|
15 | this.init(options);
|
---|
16 | let fn;
|
---|
17 | switch (this.options.type) {
|
---|
18 | case "sync":
|
---|
19 | fn = new Function(
|
---|
20 | this.args(),
|
---|
21 | '"use strict";\n' +
|
---|
22 | this.header() +
|
---|
23 | this.contentWithInterceptors({
|
---|
24 | onError: err => `throw ${err};\n`,
|
---|
25 | onResult: result => `return ${result};\n`,
|
---|
26 | resultReturns: true,
|
---|
27 | onDone: () => "",
|
---|
28 | rethrowIfPossible: true
|
---|
29 | })
|
---|
30 | );
|
---|
31 | break;
|
---|
32 | case "async":
|
---|
33 | fn = new Function(
|
---|
34 | this.args({
|
---|
35 | after: "_callback"
|
---|
36 | }),
|
---|
37 | '"use strict";\n' +
|
---|
38 | this.header() +
|
---|
39 | this.contentWithInterceptors({
|
---|
40 | onError: err => `_callback(${err});\n`,
|
---|
41 | onResult: result => `_callback(null, ${result});\n`,
|
---|
42 | onDone: () => "_callback();\n"
|
---|
43 | })
|
---|
44 | );
|
---|
45 | break;
|
---|
46 | case "promise":
|
---|
47 | let errorHelperUsed = false;
|
---|
48 | const content = this.contentWithInterceptors({
|
---|
49 | onError: err => {
|
---|
50 | errorHelperUsed = true;
|
---|
51 | return `_error(${err});\n`;
|
---|
52 | },
|
---|
53 | onResult: result => `_resolve(${result});\n`,
|
---|
54 | onDone: () => "_resolve();\n"
|
---|
55 | });
|
---|
56 | let code = "";
|
---|
57 | code += '"use strict";\n';
|
---|
58 | code += this.header();
|
---|
59 | code += "return new Promise((function(_resolve, _reject) {\n";
|
---|
60 | if (errorHelperUsed) {
|
---|
61 | code += "var _sync = true;\n";
|
---|
62 | code += "function _error(_err) {\n";
|
---|
63 | code += "if(_sync)\n";
|
---|
64 | code +=
|
---|
65 | "_resolve(Promise.resolve().then((function() { throw _err; })));\n";
|
---|
66 | code += "else\n";
|
---|
67 | code += "_reject(_err);\n";
|
---|
68 | code += "};\n";
|
---|
69 | }
|
---|
70 | code += content;
|
---|
71 | if (errorHelperUsed) {
|
---|
72 | code += "_sync = false;\n";
|
---|
73 | }
|
---|
74 | code += "}));\n";
|
---|
75 | fn = new Function(this.args(), code);
|
---|
76 | break;
|
---|
77 | }
|
---|
78 | this.deinit();
|
---|
79 | return fn;
|
---|
80 | }
|
---|
81 |
|
---|
82 | setup(instance, options) {
|
---|
83 | instance._x = options.taps.map(t => t.fn);
|
---|
84 | }
|
---|
85 |
|
---|
86 | /**
|
---|
87 | * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
|
---|
88 | */
|
---|
89 | init(options) {
|
---|
90 | this.options = options;
|
---|
91 | this._args = options.args.slice();
|
---|
92 | }
|
---|
93 |
|
---|
94 | deinit() {
|
---|
95 | this.options = undefined;
|
---|
96 | this._args = undefined;
|
---|
97 | }
|
---|
98 |
|
---|
99 | contentWithInterceptors(options) {
|
---|
100 | if (this.options.interceptors.length > 0) {
|
---|
101 | const onError = options.onError;
|
---|
102 | const onResult = options.onResult;
|
---|
103 | const onDone = options.onDone;
|
---|
104 | let code = "";
|
---|
105 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
106 | const interceptor = this.options.interceptors[i];
|
---|
107 | if (interceptor.call) {
|
---|
108 | code += `${this.getInterceptor(i)}.call(${this.args({
|
---|
109 | before: interceptor.context ? "_context" : undefined
|
---|
110 | })});\n`;
|
---|
111 | }
|
---|
112 | }
|
---|
113 | code += this.content(
|
---|
114 | Object.assign(options, {
|
---|
115 | onError:
|
---|
116 | onError &&
|
---|
117 | (err => {
|
---|
118 | let code = "";
|
---|
119 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
120 | const interceptor = this.options.interceptors[i];
|
---|
121 | if (interceptor.error) {
|
---|
122 | code += `${this.getInterceptor(i)}.error(${err});\n`;
|
---|
123 | }
|
---|
124 | }
|
---|
125 | code += onError(err);
|
---|
126 | return code;
|
---|
127 | }),
|
---|
128 | onResult:
|
---|
129 | onResult &&
|
---|
130 | (result => {
|
---|
131 | let code = "";
|
---|
132 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
133 | const interceptor = this.options.interceptors[i];
|
---|
134 | if (interceptor.result) {
|
---|
135 | code += `${this.getInterceptor(i)}.result(${result});\n`;
|
---|
136 | }
|
---|
137 | }
|
---|
138 | code += onResult(result);
|
---|
139 | return code;
|
---|
140 | }),
|
---|
141 | onDone:
|
---|
142 | onDone &&
|
---|
143 | (() => {
|
---|
144 | let code = "";
|
---|
145 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
146 | const interceptor = this.options.interceptors[i];
|
---|
147 | if (interceptor.done) {
|
---|
148 | code += `${this.getInterceptor(i)}.done();\n`;
|
---|
149 | }
|
---|
150 | }
|
---|
151 | code += onDone();
|
---|
152 | return code;
|
---|
153 | })
|
---|
154 | })
|
---|
155 | );
|
---|
156 | return code;
|
---|
157 | } else {
|
---|
158 | return this.content(options);
|
---|
159 | }
|
---|
160 | }
|
---|
161 |
|
---|
162 | header() {
|
---|
163 | let code = "";
|
---|
164 | if (this.needContext()) {
|
---|
165 | code += "var _context = {};\n";
|
---|
166 | } else {
|
---|
167 | code += "var _context;\n";
|
---|
168 | }
|
---|
169 | code += "var _x = this._x;\n";
|
---|
170 | if (this.options.interceptors.length > 0) {
|
---|
171 | code += "var _taps = this.taps;\n";
|
---|
172 | code += "var _interceptors = this.interceptors;\n";
|
---|
173 | }
|
---|
174 | return code;
|
---|
175 | }
|
---|
176 |
|
---|
177 | needContext() {
|
---|
178 | for (const tap of this.options.taps) if (tap.context) return true;
|
---|
179 | return false;
|
---|
180 | }
|
---|
181 |
|
---|
182 | callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
|
---|
183 | let code = "";
|
---|
184 | let hasTapCached = false;
|
---|
185 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
186 | const interceptor = this.options.interceptors[i];
|
---|
187 | if (interceptor.tap) {
|
---|
188 | if (!hasTapCached) {
|
---|
189 | code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
|
---|
190 | hasTapCached = true;
|
---|
191 | }
|
---|
192 | code += `${this.getInterceptor(i)}.tap(${
|
---|
193 | interceptor.context ? "_context, " : ""
|
---|
194 | }_tap${tapIndex});\n`;
|
---|
195 | }
|
---|
196 | }
|
---|
197 | code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
|
---|
198 | const tap = this.options.taps[tapIndex];
|
---|
199 | switch (tap.type) {
|
---|
200 | case "sync":
|
---|
201 | if (!rethrowIfPossible) {
|
---|
202 | code += `var _hasError${tapIndex} = false;\n`;
|
---|
203 | code += "try {\n";
|
---|
204 | }
|
---|
205 | if (onResult) {
|
---|
206 | code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
|
---|
207 | before: tap.context ? "_context" : undefined
|
---|
208 | })});\n`;
|
---|
209 | } else {
|
---|
210 | code += `_fn${tapIndex}(${this.args({
|
---|
211 | before: tap.context ? "_context" : undefined
|
---|
212 | })});\n`;
|
---|
213 | }
|
---|
214 | if (!rethrowIfPossible) {
|
---|
215 | code += "} catch(_err) {\n";
|
---|
216 | code += `_hasError${tapIndex} = true;\n`;
|
---|
217 | code += onError("_err");
|
---|
218 | code += "}\n";
|
---|
219 | code += `if(!_hasError${tapIndex}) {\n`;
|
---|
220 | }
|
---|
221 | if (onResult) {
|
---|
222 | code += onResult(`_result${tapIndex}`);
|
---|
223 | }
|
---|
224 | if (onDone) {
|
---|
225 | code += onDone();
|
---|
226 | }
|
---|
227 | if (!rethrowIfPossible) {
|
---|
228 | code += "}\n";
|
---|
229 | }
|
---|
230 | break;
|
---|
231 | case "async":
|
---|
232 | let cbCode = "";
|
---|
233 | if (onResult)
|
---|
234 | cbCode += `(function(_err${tapIndex}, _result${tapIndex}) {\n`;
|
---|
235 | else cbCode += `(function(_err${tapIndex}) {\n`;
|
---|
236 | cbCode += `if(_err${tapIndex}) {\n`;
|
---|
237 | cbCode += onError(`_err${tapIndex}`);
|
---|
238 | cbCode += "} else {\n";
|
---|
239 | if (onResult) {
|
---|
240 | cbCode += onResult(`_result${tapIndex}`);
|
---|
241 | }
|
---|
242 | if (onDone) {
|
---|
243 | cbCode += onDone();
|
---|
244 | }
|
---|
245 | cbCode += "}\n";
|
---|
246 | cbCode += "})";
|
---|
247 | code += `_fn${tapIndex}(${this.args({
|
---|
248 | before: tap.context ? "_context" : undefined,
|
---|
249 | after: cbCode
|
---|
250 | })});\n`;
|
---|
251 | break;
|
---|
252 | case "promise":
|
---|
253 | code += `var _hasResult${tapIndex} = false;\n`;
|
---|
254 | code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
|
---|
255 | before: tap.context ? "_context" : undefined
|
---|
256 | })});\n`;
|
---|
257 | code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
|
---|
258 | code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
|
---|
259 | code += `_promise${tapIndex}.then((function(_result${tapIndex}) {\n`;
|
---|
260 | code += `_hasResult${tapIndex} = true;\n`;
|
---|
261 | if (onResult) {
|
---|
262 | code += onResult(`_result${tapIndex}`);
|
---|
263 | }
|
---|
264 | if (onDone) {
|
---|
265 | code += onDone();
|
---|
266 | }
|
---|
267 | code += `}), function(_err${tapIndex}) {\n`;
|
---|
268 | code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
|
---|
269 | code += onError(`_err${tapIndex}`);
|
---|
270 | code += "});\n";
|
---|
271 | break;
|
---|
272 | }
|
---|
273 | return code;
|
---|
274 | }
|
---|
275 |
|
---|
276 | callTapsSeries({
|
---|
277 | onError,
|
---|
278 | onResult,
|
---|
279 | resultReturns,
|
---|
280 | onDone,
|
---|
281 | doneReturns,
|
---|
282 | rethrowIfPossible
|
---|
283 | }) {
|
---|
284 | if (this.options.taps.length === 0) return onDone();
|
---|
285 | const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
|
---|
286 | const somethingReturns = resultReturns || doneReturns;
|
---|
287 | let code = "";
|
---|
288 | let current = onDone;
|
---|
289 | let unrollCounter = 0;
|
---|
290 | for (let j = this.options.taps.length - 1; j >= 0; j--) {
|
---|
291 | const i = j;
|
---|
292 | const unroll =
|
---|
293 | current !== onDone &&
|
---|
294 | (this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
|
---|
295 | if (unroll) {
|
---|
296 | unrollCounter = 0;
|
---|
297 | code += `function _next${i}() {\n`;
|
---|
298 | code += current();
|
---|
299 | code += `}\n`;
|
---|
300 | current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
|
---|
301 | }
|
---|
302 | const done = current;
|
---|
303 | const doneBreak = skipDone => {
|
---|
304 | if (skipDone) return "";
|
---|
305 | return onDone();
|
---|
306 | };
|
---|
307 | const content = this.callTap(i, {
|
---|
308 | onError: error => onError(i, error, done, doneBreak),
|
---|
309 | onResult:
|
---|
310 | onResult &&
|
---|
311 | (result => {
|
---|
312 | return onResult(i, result, done, doneBreak);
|
---|
313 | }),
|
---|
314 | onDone: !onResult && done,
|
---|
315 | rethrowIfPossible:
|
---|
316 | rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
|
---|
317 | });
|
---|
318 | current = () => content;
|
---|
319 | }
|
---|
320 | code += current();
|
---|
321 | return code;
|
---|
322 | }
|
---|
323 |
|
---|
324 | callTapsLooping({ onError, onDone, rethrowIfPossible }) {
|
---|
325 | if (this.options.taps.length === 0) return onDone();
|
---|
326 | const syncOnly = this.options.taps.every(t => t.type === "sync");
|
---|
327 | let code = "";
|
---|
328 | if (!syncOnly) {
|
---|
329 | code += "var _looper = (function() {\n";
|
---|
330 | code += "var _loopAsync = false;\n";
|
---|
331 | }
|
---|
332 | code += "var _loop;\n";
|
---|
333 | code += "do {\n";
|
---|
334 | code += "_loop = false;\n";
|
---|
335 | for (let i = 0; i < this.options.interceptors.length; i++) {
|
---|
336 | const interceptor = this.options.interceptors[i];
|
---|
337 | if (interceptor.loop) {
|
---|
338 | code += `${this.getInterceptor(i)}.loop(${this.args({
|
---|
339 | before: interceptor.context ? "_context" : undefined
|
---|
340 | })});\n`;
|
---|
341 | }
|
---|
342 | }
|
---|
343 | code += this.callTapsSeries({
|
---|
344 | onError,
|
---|
345 | onResult: (i, result, next, doneBreak) => {
|
---|
346 | let code = "";
|
---|
347 | code += `if(${result} !== undefined) {\n`;
|
---|
348 | code += "_loop = true;\n";
|
---|
349 | if (!syncOnly) code += "if(_loopAsync) _looper();\n";
|
---|
350 | code += doneBreak(true);
|
---|
351 | code += `} else {\n`;
|
---|
352 | code += next();
|
---|
353 | code += `}\n`;
|
---|
354 | return code;
|
---|
355 | },
|
---|
356 | onDone:
|
---|
357 | onDone &&
|
---|
358 | (() => {
|
---|
359 | let code = "";
|
---|
360 | code += "if(!_loop) {\n";
|
---|
361 | code += onDone();
|
---|
362 | code += "}\n";
|
---|
363 | return code;
|
---|
364 | }),
|
---|
365 | rethrowIfPossible: rethrowIfPossible && syncOnly
|
---|
366 | });
|
---|
367 | code += "} while(_loop);\n";
|
---|
368 | if (!syncOnly) {
|
---|
369 | code += "_loopAsync = true;\n";
|
---|
370 | code += "});\n";
|
---|
371 | code += "_looper();\n";
|
---|
372 | }
|
---|
373 | return code;
|
---|
374 | }
|
---|
375 |
|
---|
376 | callTapsParallel({
|
---|
377 | onError,
|
---|
378 | onResult,
|
---|
379 | onDone,
|
---|
380 | rethrowIfPossible,
|
---|
381 | onTap = (i, run) => run()
|
---|
382 | }) {
|
---|
383 | if (this.options.taps.length <= 1) {
|
---|
384 | return this.callTapsSeries({
|
---|
385 | onError,
|
---|
386 | onResult,
|
---|
387 | onDone,
|
---|
388 | rethrowIfPossible
|
---|
389 | });
|
---|
390 | }
|
---|
391 | let code = "";
|
---|
392 | code += "do {\n";
|
---|
393 | code += `var _counter = ${this.options.taps.length};\n`;
|
---|
394 | if (onDone) {
|
---|
395 | code += "var _done = (function() {\n";
|
---|
396 | code += onDone();
|
---|
397 | code += "});\n";
|
---|
398 | }
|
---|
399 | for (let i = 0; i < this.options.taps.length; i++) {
|
---|
400 | const done = () => {
|
---|
401 | if (onDone) return "if(--_counter === 0) _done();\n";
|
---|
402 | else return "--_counter;";
|
---|
403 | };
|
---|
404 | const doneBreak = skipDone => {
|
---|
405 | if (skipDone || !onDone) return "_counter = 0;\n";
|
---|
406 | else return "_counter = 0;\n_done();\n";
|
---|
407 | };
|
---|
408 | code += "if(_counter <= 0) break;\n";
|
---|
409 | code += onTap(
|
---|
410 | i,
|
---|
411 | () =>
|
---|
412 | this.callTap(i, {
|
---|
413 | onError: error => {
|
---|
414 | let code = "";
|
---|
415 | code += "if(_counter > 0) {\n";
|
---|
416 | code += onError(i, error, done, doneBreak);
|
---|
417 | code += "}\n";
|
---|
418 | return code;
|
---|
419 | },
|
---|
420 | onResult:
|
---|
421 | onResult &&
|
---|
422 | (result => {
|
---|
423 | let code = "";
|
---|
424 | code += "if(_counter > 0) {\n";
|
---|
425 | code += onResult(i, result, done, doneBreak);
|
---|
426 | code += "}\n";
|
---|
427 | return code;
|
---|
428 | }),
|
---|
429 | onDone:
|
---|
430 | !onResult &&
|
---|
431 | (() => {
|
---|
432 | return done();
|
---|
433 | }),
|
---|
434 | rethrowIfPossible
|
---|
435 | }),
|
---|
436 | done,
|
---|
437 | doneBreak
|
---|
438 | );
|
---|
439 | }
|
---|
440 | code += "} while(false);\n";
|
---|
441 | return code;
|
---|
442 | }
|
---|
443 |
|
---|
444 | args({ before, after } = {}) {
|
---|
445 | let allArgs = this._args;
|
---|
446 | if (before) allArgs = [before].concat(allArgs);
|
---|
447 | if (after) allArgs = allArgs.concat(after);
|
---|
448 | if (allArgs.length === 0) {
|
---|
449 | return "";
|
---|
450 | } else {
|
---|
451 | return allArgs.join(", ");
|
---|
452 | }
|
---|
453 | }
|
---|
454 |
|
---|
455 | getTapFn(idx) {
|
---|
456 | return `_x[${idx}]`;
|
---|
457 | }
|
---|
458 |
|
---|
459 | getTap(idx) {
|
---|
460 | return `_taps[${idx}]`;
|
---|
461 | }
|
---|
462 |
|
---|
463 | getInterceptor(idx) {
|
---|
464 | return `_interceptors[${idx}]`;
|
---|
465 | }
|
---|
466 | }
|
---|
467 |
|
---|
468 | module.exports = HookCodeFactory;
|
---|