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