/*1Copyright (c) 2012, Yahoo! Inc. All rights reserved.2Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.3*/45/**6* provides a mechanism to transform code in the scope of `require` or `vm.createScript`.7* This mechanism is general and relies on a user-supplied `matcher` function that determines when transformations should be8* performed and a user-supplied `transformer` function that performs the actual transform.9* Instrumenting code for coverage is one specific example of useful hooking.10*11* Note that both the `matcher` and `transformer` must execute synchronously.12*13* For the common case of matching filesystem paths based on inclusion/ exclusion patterns, use the `matcherFor`14* function in the istanbul API to get a matcher.15*16* It is up to the transformer to perform processing with side-effects, such as caching, storing the original17* source code to disk in case of dynamically generated scripts etc. The `Store` class can help you with this.18*19* Usage20* -----21*22* var hook = require('istanbul').hook,23* myMatcher = function (file) { return file.match(/foo/); },24* myTransformer = function (code, file) { return 'console.log("' + file + '");' + code; };25*26* hook.hookRequire(myMatcher, myTransformer);27*28* var foo = require('foo'); //will now print foo's module path to console29*30* @class Hook31* @module main32*/33var path = require('path'),34fs = require('fs'),35Module = require('module'),36vm = require('vm'),37originalLoaders = {},38originalCreateScript = vm.createScript,39originalRunInThisContext = vm.runInThisContext;4041function transformFn(matcher, transformer, verbose) {4243return function (code, filename) {44var shouldHook = typeof filename === 'string' && matcher(path.resolve(filename)),45transformed,46changed = false;4748if (shouldHook) {49if (verbose) {50console.error('Module load hook: transform [' + filename + ']');51}52try {53transformed = transformer(code, filename);54changed = true;55} catch (ex) {56console.error('Transformation error; return original code');57console.error(ex);58transformed = code;59}60} else {61transformed = code;62}63return { code: transformed, changed: changed };64};65}6667function unloadRequireCache(matcher) {68if (matcher && typeof require !== 'undefined' && require && require.cache) {69Object.keys(require.cache).forEach(function (filename) {70if (matcher(filename)) {71delete require.cache[filename];72}73});74}75}76/**77* hooks `require` to return transformed code to the node module loader.78* Exceptions in the transform result in the original code being used instead.79* @method hookRequire80* @static81* @param matcher {Function(filePath)} a function that is called with the absolute path to the file being82* `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise83* @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file84* from where the code was loaded. Should return the transformed code.85* @param options {Object} options Optional.86* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called87* @param {Function} [options.postLoadHook] a function that is called with the name of the file being88* required. This is called after the require is processed irrespective of whether it was transformed.89*/90function hookRequire(matcher, transformer, options) {91options = options || {};92var extensions,93fn = transformFn(matcher, transformer, options.verbose),94postLoadHook = options.postLoadHook &&95typeof options.postLoadHook === 'function' ? options.postLoadHook : null;9697extensions = options.extensions || ['.js'];9899extensions.forEach(function(ext){100originalLoaders[ext] = Module._extensions[ext];101Module._extensions[ext] = function (module, filename) {102var ret = fn(fs.readFileSync(filename, 'utf8'), filename);103if (ret.changed) {104module._compile(ret.code, filename);105} else {106originalLoaders[ext](module, filename);107}108if (postLoadHook) {109postLoadHook(filename);110}111};112});113}114/**115* unhook `require` to restore it to its original state.116* @method unhookRequire117* @static118*/119function unhookRequire() {120Object.keys(originalLoaders).forEach(function(ext) {121Module._extensions[ext] = originalLoaders[ext];122});123}124/**125* hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.126* Exceptions in the transform result in the original code being used instead.127* @method hookCreateScript128* @static129* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`130* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise131* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to132* `vm.createScript`. Should return the transformed code.133* @param options {Object} options Optional.134* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called135*/136function hookCreateScript(matcher, transformer, opts) {137opts = opts || {};138var fn = transformFn(matcher, transformer, opts.verbose);139vm.createScript = function (code, file) {140var ret = fn(code, file);141return originalCreateScript(ret.code, file);142};143}144145/**146* unhooks vm.createScript, restoring it to its original state.147* @method unhookCreateScript148* @static149*/150function unhookCreateScript() {151vm.createScript = originalCreateScript;152}153154155/**156* hooks `vm.runInThisContext` to return transformed code.157* @method hookRunInThisContext158* @static159* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`160* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise161* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to162* `vm.createScript`. Should return the transformed code.163* @param options {Object} options Optional.164* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called165*/166function hookRunInThisContext(matcher, transformer, opts) {167opts = opts || {};168var fn = transformFn(matcher, transformer, opts.verbose);169vm.runInThisContext = function (code, file) {170var ret = fn(code, file);171return originalRunInThisContext(ret.code, file);172};173}174175/**176* unhooks vm.runInThisContext, restoring it to its original state.177* @method unhookRunInThisContext178* @static179*/180function unhookRunInThisContext() {181vm.runInThisContext = originalRunInThisContext;182}183184185module.exports = {186hookRequire: hookRequire,187unhookRequire: unhookRequire,188hookCreateScript: hookCreateScript,189unhookCreateScript: unhookCreateScript,190hookRunInThisContext : hookRunInThisContext,191unhookRunInThisContext : unhookRunInThisContext,192unloadRequireCache: unloadRequireCache193};194195196197198