UNPKG

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