238 lines
9.2 KiB
JavaScript
238 lines
9.2 KiB
JavaScript
|
'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
|
||
|
};
|