HookCodeFactory.js 12 KB

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