hook.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
  4. */
  5. var path = require('path'),
  6. vm = require('vm'),
  7. appendTransform = require('append-transform'),
  8. originalCreateScript = vm.createScript,
  9. originalRunInThisContext = vm.runInThisContext,
  10. originalRunInContext = vm.runInContext;
  11. function transformFn(matcher, transformer, verbose) {
  12. return function (code, filename) {
  13. var shouldHook = typeof filename === 'string' && matcher(path.resolve(filename)),
  14. transformed,
  15. changed = false;
  16. if (shouldHook) {
  17. if (verbose) {
  18. console.error('Module load hook: transform [' + filename + ']');
  19. }
  20. try {
  21. transformed = transformer(code, filename);
  22. changed = true;
  23. } catch (ex) {
  24. console.error('Transformation error for', filename, '; return original code');
  25. console.error(ex.message || String(ex));
  26. if (verbose) {
  27. console.error(ex.stack);
  28. }
  29. transformed = code;
  30. }
  31. } else {
  32. transformed = code;
  33. }
  34. return { code: transformed, changed: changed };
  35. };
  36. }
  37. /**
  38. * unloads the required caches, removing all files that would have matched
  39. * the supplied matcher.
  40. * @param {Function} matcher - the match function that accepts a file name and
  41. * returns if that file should be unloaded from the cache.
  42. */
  43. function unloadRequireCache(matcher) {
  44. /* istanbul ignore else: impossible to test */
  45. if (matcher && typeof require !== 'undefined' && require && require.cache) {
  46. Object.keys(require.cache).forEach(function (filename) {
  47. if (matcher(filename)) {
  48. delete require.cache[filename];
  49. }
  50. });
  51. }
  52. }
  53. /**
  54. * hooks `require` to return transformed code to the node module loader.
  55. * Exceptions in the transform result in the original code being used instead.
  56. * @method hookRequire
  57. * @static
  58. * @param matcher {Function(filePath)} a function that is called with the absolute path to the file being
  59. * `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
  60. * @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file
  61. * from where the code was loaded. Should return the transformed code.
  62. * @param options {Object} options Optional.
  63. * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
  64. * @param {Function} [options.postLoadHook] a function that is called with the name of the file being
  65. * required. This is called after the require is processed irrespective of whether it was transformed.
  66. * @returns {Function} a reset function that can be called to remove the hook
  67. */
  68. function hookRequire(matcher, transformer, options) {
  69. options = options || {};
  70. var extensions,
  71. disable = false,
  72. fn = transformFn(matcher, transformer, options.verbose),
  73. postLoadHook = options.postLoadHook &&
  74. typeof options.postLoadHook === 'function' ? options.postLoadHook : null;
  75. extensions = options.extensions || ['.js'];
  76. extensions.forEach(function(ext){
  77. appendTransform(function (code, filename) {
  78. if (disable) {
  79. return code;
  80. }
  81. var ret = fn(code, filename);
  82. if (postLoadHook) {
  83. postLoadHook(filename);
  84. }
  85. return ret.code;
  86. }, ext);
  87. });
  88. return function () {
  89. disable = true;
  90. };
  91. }
  92. /**
  93. * hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.
  94. * Exceptions in the transform result in the original code being used instead.
  95. * @method hookCreateScript
  96. * @static
  97. * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
  98. * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
  99. * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
  100. * `vm.createScript`. Should return the transformed code.
  101. * @param options {Object} options Optional.
  102. * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
  103. */
  104. function hookCreateScript(matcher, transformer, opts) {
  105. opts = opts || {};
  106. var fn = transformFn(matcher, transformer, opts.verbose);
  107. vm.createScript = function (code, file) {
  108. var ret = fn(code, file);
  109. return originalCreateScript(ret.code, file);
  110. };
  111. }
  112. /**
  113. * unhooks vm.createScript, restoring it to its original state.
  114. * @method unhookCreateScript
  115. * @static
  116. */
  117. function unhookCreateScript() {
  118. vm.createScript = originalCreateScript;
  119. }
  120. /**
  121. * hooks `vm.runInThisContext` to return transformed code.
  122. * @method hookRunInThisContext
  123. * @static
  124. * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
  125. * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
  126. * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
  127. * `vm.createScript`. Should return the transformed code.
  128. * @param opts {Object} [opts={}] options
  129. * @param {Boolean} [opts.verbose] write a line to standard error every time the transformer is called
  130. */
  131. function hookRunInThisContext(matcher, transformer, opts) {
  132. opts = opts || {};
  133. var fn = transformFn(matcher, transformer, opts.verbose);
  134. vm.runInThisContext = function (code, file) {
  135. var ret = fn(code, file);
  136. return originalRunInThisContext(ret.code, file);
  137. };
  138. }
  139. /**
  140. * unhooks vm.runInThisContext, restoring it to its original state.
  141. * @method unhookRunInThisContext
  142. * @static
  143. */
  144. function unhookRunInThisContext() {
  145. vm.runInThisContext = originalRunInThisContext;
  146. }
  147. /**
  148. * hooks `vm.runInContext` to return transformed code.
  149. * @method hookRunInContext
  150. * @static
  151. * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
  152. * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
  153. * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
  154. * `vm.createScript`. Should return the transformed code.
  155. * @param opts {Object} [opts={}] options
  156. * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
  157. */
  158. function hookRunInContext(matcher, transformer, opts) {
  159. opts = opts || {};
  160. var fn = transformFn(matcher, transformer, opts.verbose);
  161. vm.runInContext = function (code, context, file) {
  162. var ret = fn(code, file);
  163. var coverageVariable = opts.coverageVariable || '__coverage__';
  164. // Refer coverage variable in context to global coverage variable.
  165. // So that coverage data will be written in global coverage variable for unit tests run in vm.runInContext.
  166. // If all unit tests are run in vm.runInContext, no global coverage variable will be generated.
  167. // Thus initialize a global coverage variable here.
  168. if (!global[coverageVariable]) {
  169. global[coverageVariable] = {};
  170. }
  171. context[coverageVariable] = global[coverageVariable];
  172. return originalRunInContext(ret.code, context, file);
  173. };
  174. }
  175. /**
  176. * unhooks vm.runInContext, restoring it to its original state.
  177. * @method unhookRunInContext
  178. * @static
  179. */
  180. function unhookRunInContext() {
  181. vm.runInContext = originalRunInContext;
  182. }
  183. /**
  184. * istanbul-lib-hook provides mechanisms to transform code in the scope of `require`,
  185. * `vm.createScript`, `vm.runInThisContext` etc.
  186. *
  187. * This mechanism is general and relies on a user-supplied `matcher` function that
  188. * determines when transformations should be performed and a user-supplied `transformer`
  189. * function that performs the actual transform. Instrumenting code for coverage is
  190. * one specific example of useful hooking.
  191. *
  192. * Note that both the `matcher` and `transformer` must execute synchronously.
  193. *
  194. * @module Exports
  195. * @example
  196. * var hook = require('istanbul-lib-hook'),
  197. * myMatcher = function (file) { return file.match(/foo/); },
  198. * myTransformer = function (code, file) {
  199. * return 'console.log("' + file + '");' + code;
  200. * };
  201. *
  202. * hook.hookRequire(myMatcher, myTransformer);
  203. * var foo = require('foo'); //will now print foo's module path to console
  204. */
  205. module.exports = {
  206. hookRequire: hookRequire,
  207. hookCreateScript: hookCreateScript,
  208. unhookCreateScript: unhookCreateScript,
  209. hookRunInThisContext : hookRunInThisContext,
  210. unhookRunInThisContext : unhookRunInThisContext,
  211. hookRunInContext : hookRunInContext,
  212. unhookRunInContext : unhookRunInContext,
  213. unloadRequireCache: unloadRequireCache
  214. };