123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- '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
- };
|