HookCodeFactory.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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 (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. const { taps } = options;
  78. const { length } = taps;
  79. const fns = Array.from({ length });
  80. for (let i = 0; i < length; i++) {
  81. fns[i] = taps[i].fn;
  82. }
  83. instance._x = fns;
  84. }
  85. /**
  86. * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
  87. */
  88. init(options) {
  89. this.options = options;
  90. // `_args` is only read (length / join / [0]) - never mutated - so we
  91. // can share the caller's array directly instead of paying for a copy
  92. // on every compile.
  93. this._args = options.args;
  94. this._joinedArgs = undefined;
  95. }
  96. deinit() {
  97. this.options = undefined;
  98. this._args = undefined;
  99. this._joinedArgs = undefined;
  100. }
  101. contentWithInterceptors(options) {
  102. if (this.options.interceptors.length > 0) {
  103. const { onError, onResult, onDone } = options;
  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. }
  158. return this.content(options);
  159. }
  160. header() {
  161. let code = "";
  162. code += this.needContext() ? "var _context = {};\n" : "var _context;\n";
  163. code += "var _x = this._x;\n";
  164. if (this.options.interceptors.length > 0) {
  165. code += "var _taps = this.taps;\n";
  166. code += "var _interceptors = this.interceptors;\n";
  167. }
  168. return code;
  169. }
  170. needContext() {
  171. const { taps } = this.options;
  172. for (let i = 0; i < taps.length; i++) {
  173. if (taps[i].context) return true;
  174. }
  175. return false;
  176. }
  177. callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
  178. let code = "";
  179. let hasTapCached = false;
  180. for (let i = 0; i < this.options.interceptors.length; i++) {
  181. const interceptor = this.options.interceptors[i];
  182. if (interceptor.tap) {
  183. if (!hasTapCached) {
  184. code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
  185. hasTapCached = true;
  186. }
  187. code += `${this.getInterceptor(i)}.tap(${
  188. interceptor.context ? "_context, " : ""
  189. }_tap${tapIndex});\n`;
  190. }
  191. }
  192. code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
  193. const tap = this.options.taps[tapIndex];
  194. switch (tap.type) {
  195. case "sync":
  196. if (!rethrowIfPossible) {
  197. code += `var _hasError${tapIndex} = false;\n`;
  198. code += "try {\n";
  199. }
  200. if (onResult) {
  201. code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
  202. before: tap.context ? "_context" : undefined
  203. })});\n`;
  204. } else {
  205. code += `_fn${tapIndex}(${this.args({
  206. before: tap.context ? "_context" : undefined
  207. })});\n`;
  208. }
  209. if (!rethrowIfPossible) {
  210. code += "} catch(_err) {\n";
  211. code += `_hasError${tapIndex} = true;\n`;
  212. code += onError("_err");
  213. code += "}\n";
  214. code += `if(!_hasError${tapIndex}) {\n`;
  215. }
  216. if (onResult) {
  217. code += onResult(`_result${tapIndex}`);
  218. }
  219. if (onDone) {
  220. code += onDone();
  221. }
  222. if (!rethrowIfPossible) {
  223. code += "}\n";
  224. }
  225. break;
  226. case "async": {
  227. let cbCode = "";
  228. cbCode += onResult
  229. ? `(function(_err${tapIndex}, _result${tapIndex}) {\n`
  230. : `(function(_err${tapIndex}) {\n`;
  231. cbCode += `if(_err${tapIndex}) {\n`;
  232. cbCode += onError(`_err${tapIndex}`);
  233. cbCode += "} else {\n";
  234. if (onResult) {
  235. cbCode += onResult(`_result${tapIndex}`);
  236. }
  237. if (onDone) {
  238. cbCode += onDone();
  239. }
  240. cbCode += "}\n";
  241. cbCode += "})";
  242. code += `_fn${tapIndex}(${this.args({
  243. before: tap.context ? "_context" : undefined,
  244. after: cbCode
  245. })});\n`;
  246. break;
  247. }
  248. case "promise":
  249. code += `var _hasResult${tapIndex} = false;\n`;
  250. code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
  251. before: tap.context ? "_context" : undefined
  252. })});\n`;
  253. code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
  254. code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
  255. code += `_promise${tapIndex}.then((function(_result${tapIndex}) {\n`;
  256. code += `_hasResult${tapIndex} = true;\n`;
  257. if (onResult) {
  258. code += onResult(`_result${tapIndex}`);
  259. }
  260. if (onDone) {
  261. code += onDone();
  262. }
  263. code += `}), function(_err${tapIndex}) {\n`;
  264. code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
  265. code += onError(
  266. `!_err${tapIndex} ? new Error('Tap function (tapPromise) rejects "' + _err${tapIndex} + '" value') : _err${tapIndex}`
  267. );
  268. code += "});\n";
  269. break;
  270. }
  271. return code;
  272. }
  273. callTapsSeries({
  274. onError,
  275. onResult,
  276. resultReturns,
  277. onDone,
  278. doneReturns,
  279. rethrowIfPossible
  280. }) {
  281. const { taps } = this.options;
  282. const tapsLength = taps.length;
  283. if (tapsLength === 0) return onDone();
  284. // Inlined findIndex to avoid the callback allocation.
  285. let firstAsync = -1;
  286. for (let i = 0; i < tapsLength; i++) {
  287. if (taps[i].type !== "sync") {
  288. firstAsync = i;
  289. break;
  290. }
  291. }
  292. const somethingReturns = resultReturns || doneReturns;
  293. // doneBreak doesn't depend on the loop variable - hoist to allocate once.
  294. const doneBreak = (skipDone) => {
  295. if (skipDone) return "";
  296. return onDone();
  297. };
  298. let code = "";
  299. let current = onDone;
  300. let unrollCounter = 0;
  301. for (let j = tapsLength - 1; j >= 0; j--) {
  302. const i = j;
  303. const unroll =
  304. current !== onDone && (taps[i].type !== "sync" || unrollCounter++ > 20);
  305. if (unroll) {
  306. unrollCounter = 0;
  307. code += `function _next${i}() {\n`;
  308. code += current();
  309. code += "}\n";
  310. current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
  311. }
  312. const done = current;
  313. const content = this.callTap(i, {
  314. onError: (error) => onError(i, error, done, doneBreak),
  315. onResult:
  316. onResult && ((result) => onResult(i, result, done, doneBreak)),
  317. onDone: !onResult && done,
  318. rethrowIfPossible:
  319. rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
  320. });
  321. current = () => content;
  322. }
  323. code += current();
  324. return code;
  325. }
  326. callTapsLooping({ onError, onDone, rethrowIfPossible }) {
  327. if (this.options.taps.length === 0) return onDone();
  328. const syncOnly = this.options.taps.every((t) => t.type === "sync");
  329. let code = "";
  330. if (!syncOnly) {
  331. code += "var _looper = (function() {\n";
  332. code += "var _loopAsync = false;\n";
  333. }
  334. code += "var _loop;\n";
  335. code += "do {\n";
  336. code += "_loop = false;\n";
  337. for (let i = 0; i < this.options.interceptors.length; i++) {
  338. const interceptor = this.options.interceptors[i];
  339. if (interceptor.loop) {
  340. code += `${this.getInterceptor(i)}.loop(${this.args({
  341. before: interceptor.context ? "_context" : undefined
  342. })});\n`;
  343. }
  344. }
  345. code += this.callTapsSeries({
  346. onError,
  347. onResult: (i, result, next, doneBreak) => {
  348. let code = "";
  349. code += `if(${result} !== undefined) {\n`;
  350. code += "_loop = true;\n";
  351. if (!syncOnly) code += "if(_loopAsync) _looper();\n";
  352. code += doneBreak(true);
  353. code += "} else {\n";
  354. code += next();
  355. code += "}\n";
  356. return code;
  357. },
  358. onDone:
  359. onDone &&
  360. (() => {
  361. let code = "";
  362. code += "if(!_loop) {\n";
  363. code += onDone();
  364. code += "}\n";
  365. return code;
  366. }),
  367. rethrowIfPossible: rethrowIfPossible && syncOnly
  368. });
  369. code += "} while(_loop);\n";
  370. if (!syncOnly) {
  371. code += "_loopAsync = true;\n";
  372. code += "});\n";
  373. code += "_looper();\n";
  374. }
  375. return code;
  376. }
  377. callTapsParallel({
  378. onError,
  379. onResult,
  380. onDone,
  381. rethrowIfPossible,
  382. onTap = (i, run) => run()
  383. }) {
  384. const { taps } = this.options;
  385. const tapsLength = taps.length;
  386. if (tapsLength <= 1) {
  387. return this.callTapsSeries({
  388. onError,
  389. onResult,
  390. onDone,
  391. rethrowIfPossible
  392. });
  393. }
  394. // done and doneBreak don't depend on the loop variable - hoist them
  395. // so they're allocated once per compile instead of once per tap.
  396. const done = () => {
  397. if (onDone) return "if(--_counter === 0) _done();\n";
  398. return "--_counter;";
  399. };
  400. const doneBreak = (skipDone) => {
  401. if (skipDone || !onDone) return "_counter = 0;\n";
  402. return "_counter = 0;\n_done();\n";
  403. };
  404. let code = "";
  405. code += "do {\n";
  406. code += `var _counter = ${tapsLength};\n`;
  407. if (onDone) {
  408. code += "var _done = (function() {\n";
  409. code += onDone();
  410. code += "});\n";
  411. }
  412. for (let i = 0; i < tapsLength; i++) {
  413. code += "if(_counter <= 0) break;\n";
  414. code += onTap(
  415. i,
  416. () =>
  417. this.callTap(i, {
  418. onError: (error) => {
  419. let code = "";
  420. code += "if(_counter > 0) {\n";
  421. code += onError(i, error, done, doneBreak);
  422. code += "}\n";
  423. return code;
  424. },
  425. onResult:
  426. onResult &&
  427. ((result) => {
  428. let code = "";
  429. code += "if(_counter > 0) {\n";
  430. code += onResult(i, result, done, doneBreak);
  431. code += "}\n";
  432. return code;
  433. }),
  434. onDone: !onResult && (() => done()),
  435. rethrowIfPossible
  436. }),
  437. done,
  438. doneBreak
  439. );
  440. }
  441. code += "} while(false);\n";
  442. return code;
  443. }
  444. args({ before, after } = {}) {
  445. // Hot during code generation. Join `_args` once and cache the result,
  446. // then build the customized variants via string concat instead of
  447. // allocating temporary `[before, ...allArgs]` / `[...allArgs, after]`
  448. // arrays and re-joining.
  449. let joined = this._joinedArgs;
  450. if (joined === undefined) {
  451. joined = this._args.length === 0 ? "" : this._args.join(", ");
  452. this._joinedArgs = joined;
  453. }
  454. if (!before && !after) return joined;
  455. if (joined.length === 0) {
  456. if (before && after) return `${before}, ${after}`;
  457. return before || after;
  458. }
  459. if (before && after) return `${before}, ${joined}, ${after}`;
  460. if (before) return `${before}, ${joined}`;
  461. return `${joined}, ${after}`;
  462. }
  463. getTapFn(idx) {
  464. return `_x[${idx}]`;
  465. }
  466. getTap(idx) {
  467. return `_taps[${idx}]`;
  468. }
  469. getInterceptor(idx) {
  470. return `_interceptors[${idx}]`;
  471. }
  472. }
  473. module.exports = HookCodeFactory;