| /* |
| Copyright (c) 2012, Yahoo! Inc. All rights reserved. |
| Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. |
| */ |
| |
| /** |
| * provides a mechanism to transform code in the scope of `require` or `vm.createScript`. |
| * 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. |
| * |
| * For the common case of matching filesystem paths based on inclusion/ exclusion patterns, use the `matcherFor` |
| * function in the istanbul API to get a matcher. |
| * |
| * It is up to the transformer to perform processing with side-effects, such as caching, storing the original |
| * source code to disk in case of dynamically generated scripts etc. The `Store` class can help you with this. |
| * |
| * Usage |
| * ----- |
| * |
| * var hook = require('istanbul').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 |
| * |
| * @class Hook |
| * @module main |
| */ |
| var path = require('path'), |
| fs = require('fs'), |
| Module = require('module'), |
| vm = require('vm'), |
| originalLoaders = {}, |
| originalCreateScript = vm.createScript, |
| originalRunInThisContext = vm.runInThisContext; |
| |
| function transformFn(matcher, transformer, verbose) { |
| |
| return function (code, filename) { |
| var shouldHook = typeof filename === 'string' && matcher(path.resolve(filename)), |
| transformed, |
| changed = false; |
| |
| if (shouldHook) { |
| if (verbose) { |
| console.error('Module load hook: transform [' + filename + ']'); |
| } |
| try { |
| transformed = transformer(code, filename); |
| changed = true; |
| } catch (ex) { |
| console.error('Transformation error; return original code'); |
| console.error(ex); |
| transformed = code; |
| } |
| } else { |
| transformed = code; |
| } |
| return { code: transformed, changed: changed }; |
| }; |
| } |
| |
| function unloadRequireCache(matcher) { |
| if (matcher && typeof require !== 'undefined' && require && require.cache) { |
| Object.keys(require.cache).forEach(function (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. |
| */ |
| function hookRequire(matcher, transformer, options) { |
| options = options || {}; |
| var extensions, |
| fn = transformFn(matcher, transformer, options.verbose), |
| postLoadHook = options.postLoadHook && |
| typeof options.postLoadHook === 'function' ? options.postLoadHook : null; |
| |
| extensions = options.extensions || ['.js']; |
| |
| extensions.forEach(function(ext){ |
| if (!(ext in originalLoaders)) { |
| originalLoaders[ext] = Module._extensions[ext] || Module._extensions['.js']; |
| } |
| Module._extensions[ext] = function (module, filename) { |
| var ret = fn(fs.readFileSync(filename, 'utf8'), filename); |
| if (ret.changed) { |
| module._compile(ret.code, filename); |
| } else { |
| originalLoaders[ext](module, filename); |
| } |
| if (postLoadHook) { |
| postLoadHook(filename); |
| } |
| }; |
| }); |
| } |
| /** |
| * unhook `require` to restore it to its original state. |
| * @method unhookRequire |
| * @static |
| */ |
| function unhookRequire() { |
| Object.keys(originalLoaders).forEach(function(ext) { |
| Module._extensions[ext] = originalLoaders[ext]; |
| }); |
| } |
| /** |
| * 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 || {}; |
| var fn = transformFn(matcher, transformer, opts.verbose); |
| vm.createScript = function (code, file) { |
| var 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.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 hookRunInThisContext(matcher, transformer, opts) { |
| opts = opts || {}; |
| var fn = transformFn(matcher, transformer, opts.verbose); |
| vm.runInThisContext = function (code, file) { |
| var ret = fn(code, file); |
| return originalRunInThisContext(ret.code, file); |
| }; |
| } |
| |
| /** |
| * unhooks vm.runInThisContext, restoring it to its original state. |
| * @method unhookRunInThisContext |
| * @static |
| */ |
| function unhookRunInThisContext() { |
| vm.runInThisContext = originalRunInThisContext; |
| } |
| |
| |
| module.exports = { |
| hookRequire: hookRequire, |
| unhookRequire: unhookRequire, |
| hookCreateScript: hookCreateScript, |
| unhookCreateScript: unhookCreateScript, |
| hookRunInThisContext : hookRunInThisContext, |
| unhookRunInThisContext : unhookRunInThisContext, |
| unloadRequireCache: unloadRequireCache |
| }; |
| |
| |