'use strict'; /* 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 };