| /* |
| Copyright 2012-2015, Yahoo Inc. |
| Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. |
| */ |
| const path = require('path'); |
| const vm = require('vm'); |
| const appendTransform = require('append-transform'); |
| const originalCreateScript = vm.createScript; |
| const originalRunInThisContext = vm.runInThisContext; |
| const originalRunInContext = vm.runInContext; |
| |
| function transformFn(matcher, transformer, verbose) { |
| return function(code, options) { |
| options = options || {}; |
| |
| // prior to 2.x, hookRequire returned filename |
| // rather than object. |
| if (typeof options === 'string') { |
| options = { filename: options }; |
| } |
| |
| const shouldHook = |
| typeof options.filename === 'string' && |
| matcher(path.resolve(options.filename)); |
| let transformed; |
| let changed = false; |
| |
| if (shouldHook) { |
| if (verbose) { |
| console.error( |
| 'Module load hook: transform [' + options.filename + ']' |
| ); |
| } |
| try { |
| transformed = transformer(code, options); |
| changed = true; |
| } catch (ex) { |
| console.error( |
| 'Transformation error for', |
| options.filename, |
| '; return original code' |
| ); |
| console.error(ex.message || String(ex)); |
| if (verbose) { |
| console.error(ex.stack); |
| } |
| transformed = code; |
| } |
| } else { |
| transformed = code; |
| } |
| return { code: transformed, changed }; |
| }; |
| } |
| /** |
| * unloads the required caches, removing all files that would have matched |
| * the supplied matcher. |
| * @param {Function} matcher - the match function that accepts a file name and |
| * returns if that file should be unloaded from the cache. |
| */ |
| function unloadRequireCache(matcher) { |
| /* istanbul ignore else: impossible to test */ |
| if (matcher && typeof require !== 'undefined' && require && require.cache) { |
| Object.keys(require.cache).forEach(filename => { |
| if (matcher(filename)) { |
| delete require.cache[filename]; |
| } |
| }); |
| } |
| } |
| /** |
| * hooks `require` to return transformed code to the node module loader. |
| * Exceptions in the transform result in the original code being used instead. |
| * @method hookRequire |
| * @static |
| * @param matcher {Function(filePath)} a function that is called with the absolute path to the file being |
| * `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise |
| * @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file |
| * from where the code was loaded. Should return the transformed code. |
| * @param options {Object} options Optional. |
| * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called |
| * @param {Function} [options.postLoadHook] a function that is called with the name of the file being |
| * required. This is called after the require is processed irrespective of whether it was transformed. |
| * @returns {Function} a reset function that can be called to remove the hook |
| */ |
| function hookRequire(matcher, transformer, options) { |
| options = options || {}; |
| let disable = false; |
| const fn = transformFn(matcher, transformer, options.verbose); |
| const postLoadHook = |
| options.postLoadHook && typeof options.postLoadHook === 'function' |
| ? options.postLoadHook |
| : null; |
| |
| const extensions = options.extensions || ['.js']; |
| |
| extensions.forEach(ext => { |
| appendTransform((code, filename) => { |
| if (disable) { |
| return code; |
| } |
| const ret = fn(code, filename); |
| if (postLoadHook) { |
| postLoadHook(filename); |
| } |
| return ret.code; |
| }, ext); |
| }); |
| |
| return function() { |
| disable = true; |
| }; |
| } |
| /** |
| * hooks `vm.createScript` to return transformed code out of which a `Script` object will be created. |
| * Exceptions in the transform result in the original code being used instead. |
| * @method hookCreateScript |
| * @static |
| * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript` |
| * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise |
| * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to |
| * `vm.createScript`. Should return the transformed code. |
| * @param options {Object} options Optional. |
| * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called |
| */ |
| function hookCreateScript(matcher, transformer, opts) { |
| opts = opts || {}; |
| const fn = transformFn(matcher, transformer, opts.verbose); |
| vm.createScript = function(code, file) { |
| const ret = fn(code, file); |
| return originalCreateScript(ret.code, file); |
| }; |
| } |
| /** |
| * unhooks vm.createScript, restoring it to its original state. |
| * @method unhookCreateScript |
| * @static |
| */ |
| function unhookCreateScript() { |
| vm.createScript = originalCreateScript; |
| } |
| /** |
| * hooks `vm.runInThisContext` to return transformed code. |
| * @method hookRunInThisContext |
| * @static |
| * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.runInThisContext` |
| * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise |
| * @param transformer {Function(code, options)} a function called with the original code and the filename passed to |
| * `vm.runInThisContext`. Should return the transformed code. |
| * @param opts {Object} [opts={}] options |
| * @param {Boolean} [opts.verbose] write a line to standard error every time the transformer is called |
| */ |
| function hookRunInThisContext(matcher, transformer, opts) { |
| opts = opts || {}; |
| const fn = transformFn(matcher, transformer, opts.verbose); |
| vm.runInThisContext = function(code, options) { |
| const ret = fn(code, options); |
| return originalRunInThisContext(ret.code, options); |
| }; |
| } |
| /** |
| * unhooks vm.runInThisContext, restoring it to its original state. |
| * @method unhookRunInThisContext |
| * @static |
| */ |
| function unhookRunInThisContext() { |
| vm.runInThisContext = originalRunInThisContext; |
| } |
| /** |
| * hooks `vm.runInContext` to return transformed code. |
| * @method hookRunInContext |
| * @static |
| * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript` |
| * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise |
| * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to |
| * `vm.createScript`. Should return the transformed code. |
| * @param opts {Object} [opts={}] options |
| * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called |
| */ |
| function hookRunInContext(matcher, transformer, opts) { |
| opts = opts || {}; |
| const fn = transformFn(matcher, transformer, opts.verbose); |
| vm.runInContext = function(code, context, file) { |
| const ret = fn(code, file); |
| const coverageVariable = opts.coverageVariable || '__coverage__'; |
| // Refer coverage variable in context to global coverage variable. |
| // So that coverage data will be written in global coverage variable for unit tests run in vm.runInContext. |
| // If all unit tests are run in vm.runInContext, no global coverage variable will be generated. |
| // Thus initialize a global coverage variable here. |
| if (!global[coverageVariable]) { |
| global[coverageVariable] = {}; |
| } |
| context[coverageVariable] = global[coverageVariable]; |
| return originalRunInContext(ret.code, context, file); |
| }; |
| } |
| /** |
| * unhooks vm.runInContext, restoring it to its original state. |
| * @method unhookRunInContext |
| * @static |
| */ |
| function unhookRunInContext() { |
| vm.runInContext = originalRunInContext; |
| } |
| /** |
| * istanbul-lib-hook provides mechanisms to transform code in the scope of `require`, |
| * `vm.createScript`, `vm.runInThisContext` etc. |
| * |
| * This mechanism is general and relies on a user-supplied `matcher` function that |
| * determines when transformations should be performed and a user-supplied `transformer` |
| * function that performs the actual transform. Instrumenting code for coverage is |
| * one specific example of useful hooking. |
| * |
| * Note that both the `matcher` and `transformer` must execute synchronously. |
| * |
| * @module Exports |
| * @example |
| * var hook = require('istanbul-lib-hook'), |
| * myMatcher = function (file) { return file.match(/foo/); }, |
| * myTransformer = function (code, file) { |
| * return 'console.log("' + file + '");' + code; |
| * }; |
| * |
| * hook.hookRequire(myMatcher, myTransformer); |
| * var foo = require('foo'); //will now print foo's module path to console |
| */ |
| module.exports = { |
| hookRequire, |
| hookCreateScript, |
| unhookCreateScript, |
| hookRunInThisContext, |
| unhookRunInThisContext, |
| hookRunInContext, |
| unhookRunInContext, |
| unloadRequireCache |
| }; |