UNPKG

9.35 kBJavaScriptView Raw
1/*
2 Copyright 2012-2015, Yahoo Inc.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5var path = require('path'),
6 vm = require('vm'),
7 appendTransform = require('append-transform'),
8 originalCreateScript = vm.createScript,
9 originalRunInThisContext = vm.runInThisContext,
10 originalRunInContext = vm.runInContext;
11
12function transformFn(matcher, transformer, verbose) {
13
14 return function (code, options) {
15 options = options || {};
16 if (typeof options === 'string') {
17 options = { filename: options };
18 }
19
20 var shouldHook = typeof options.filename === 'string' && matcher(path.resolve(options.filename)),
21 transformed,
22 changed = false;
23
24 if (shouldHook) {
25 if (verbose) {
26 console.error('Module load hook: transform [' + options.filename + ']');
27 }
28 try {
29 transformed = transformer(code, options);
30 changed = true;
31 } catch (ex) {
32 console.error('Transformation error for', options.filename, '; return original code');
33 console.error(ex.message || String(ex));
34 if (verbose) {
35 console.error(ex.stack);
36 }
37 transformed = code;
38 }
39 } else {
40 transformed = code;
41 }
42 return { code: transformed, changed: changed };
43 };
44}
45/**
46 * unloads the required caches, removing all files that would have matched
47 * the supplied matcher.
48 * @param {Function} matcher - the match function that accepts a file name and
49 * returns if that file should be unloaded from the cache.
50 */
51function unloadRequireCache(matcher) {
52 /* istanbul ignore else: impossible to test */
53 if (matcher && typeof require !== 'undefined' && require && require.cache) {
54 Object.keys(require.cache).forEach(function (filename) {
55 if (matcher(filename)) {
56 delete require.cache[filename];
57 }
58 });
59 }
60}
61/**
62 * hooks `require` to return transformed code to the node module loader.
63 * Exceptions in the transform result in the original code being used instead.
64 * @method hookRequire
65 * @static
66 * @param matcher {Function(filePath)} a function that is called with the absolute path to the file being
67 * `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
68 * @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file
69 * from where the code was loaded. Should return the transformed code.
70 * @param options {Object} options Optional.
71 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
72 * @param {Function} [options.postLoadHook] a function that is called with the name of the file being
73 * required. This is called after the require is processed irrespective of whether it was transformed.
74 * @returns {Function} a reset function that can be called to remove the hook
75 */
76function hookRequire(matcher, transformer, options) {
77 options = options || {};
78 var extensions,
79 disable = false,
80 fn = transformFn(matcher, transformer, options.verbose),
81 postLoadHook = options.postLoadHook &&
82 typeof options.postLoadHook === 'function' ? options.postLoadHook : null;
83
84 extensions = options.extensions || ['.js'];
85
86 extensions.forEach(function(ext){
87 appendTransform(function (code, filename) {
88 if (disable) {
89 return code;
90 }
91 var ret = fn(code, filename);
92 if (postLoadHook) {
93 postLoadHook(filename);
94 }
95 return ret.code;
96 }, ext);
97 });
98
99 return function () {
100 disable = true;
101 };
102}
103/**
104 * hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.
105 * Exceptions in the transform result in the original code being used instead.
106 * @method hookCreateScript
107 * @static
108 * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
109 * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
110 * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
111 * `vm.createScript`. Should return the transformed code.
112 * @param options {Object} options Optional.
113 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
114 */
115function hookCreateScript(matcher, transformer, opts) {
116 opts = opts || {};
117 var fn = transformFn(matcher, transformer, opts.verbose);
118 vm.createScript = function (code, file) {
119 var ret = fn(code, file);
120 return originalCreateScript(ret.code, file);
121 };
122}
123/**
124 * unhooks vm.createScript, restoring it to its original state.
125 * @method unhookCreateScript
126 * @static
127 */
128function unhookCreateScript() {
129 vm.createScript = originalCreateScript;
130}
131/**
132 * hooks `vm.runInThisContext` to return transformed code.
133 * @method hookRunInThisContext
134 * @static
135 * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.runInThisContext`
136 * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
137 * @param transformer {Function(code, options)} a function called with the original code and the filename passed to
138 * `vm.runInThisContext`. Should return the transformed code.
139 * @param opts {Object} [opts={}] options
140 * @param {Boolean} [opts.verbose] write a line to standard error every time the transformer is called
141 */
142function hookRunInThisContext(matcher, transformer, opts) {
143 opts = opts || {};
144 var fn = transformFn(matcher, transformer, opts.verbose);
145 vm.runInThisContext = function (code, options) {
146 var ret = fn(code, options);
147 return originalRunInThisContext(ret.code, options);
148 };
149}
150/**
151 * unhooks vm.runInThisContext, restoring it to its original state.
152 * @method unhookRunInThisContext
153 * @static
154 */
155function unhookRunInThisContext() {
156 vm.runInThisContext = originalRunInThisContext;
157}
158/**
159 * hooks `vm.runInContext` to return transformed code.
160 * @method hookRunInContext
161 * @static
162 * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
163 * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
164 * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
165 * `vm.createScript`. Should return the transformed code.
166 * @param opts {Object} [opts={}] options
167 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
168 */
169function hookRunInContext(matcher, transformer, opts) {
170 opts = opts || {};
171 var fn = transformFn(matcher, transformer, opts.verbose);
172 vm.runInContext = function (code, context, file) {
173 var ret = fn(code, file);
174 var coverageVariable = opts.coverageVariable || '__coverage__';
175 // Refer coverage variable in context to global coverage variable.
176 // So that coverage data will be written in global coverage variable for unit tests run in vm.runInContext.
177 // If all unit tests are run in vm.runInContext, no global coverage variable will be generated.
178 // Thus initialize a global coverage variable here.
179 if (!global[coverageVariable]) {
180 global[coverageVariable] = {};
181 }
182 context[coverageVariable] = global[coverageVariable];
183 return originalRunInContext(ret.code, context, file);
184 };
185
186}
187/**
188 * unhooks vm.runInContext, restoring it to its original state.
189 * @method unhookRunInContext
190 * @static
191 */
192function unhookRunInContext() {
193 vm.runInContext = originalRunInContext;
194}
195/**
196 * istanbul-lib-hook provides mechanisms to transform code in the scope of `require`,
197 * `vm.createScript`, `vm.runInThisContext` etc.
198 *
199 * This mechanism is general and relies on a user-supplied `matcher` function that
200 * determines when transformations should be performed and a user-supplied `transformer`
201 * function that performs the actual transform. Instrumenting code for coverage is
202 * one specific example of useful hooking.
203 *
204 * Note that both the `matcher` and `transformer` must execute synchronously.
205 *
206 * @module Exports
207 * @example
208 * var hook = require('istanbul-lib-hook'),
209 * myMatcher = function (file) { return file.match(/foo/); },
210 * myTransformer = function (code, file) {
211 * return 'console.log("' + file + '");' + code;
212 * };
213 *
214 * hook.hookRequire(myMatcher, myTransformer);
215 * var foo = require('foo'); //will now print foo's module path to console
216 */
217module.exports = {
218 hookRequire: hookRequire,
219 hookCreateScript: hookCreateScript,
220 unhookCreateScript: unhookCreateScript,
221 hookRunInThisContext : hookRunInThisContext,
222 unhookRunInThisContext : unhookRunInThisContext,
223 hookRunInContext : hookRunInContext,
224 unhookRunInContext : unhookRunInContext,
225 unloadRequireCache: unloadRequireCache
226};
227
228