1 | ;
|
2 | /*
|
3 | Copyright 2012-2015, Yahoo Inc.
|
4 | Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
|
5 | */
|
6 | const path = require('path');
|
7 | const vm = require('vm');
|
8 | const appendTransform = require('append-transform');
|
9 | const originalCreateScript = vm.createScript;
|
10 | const originalRunInThisContext = vm.runInThisContext;
|
11 | const originalRunInContext = vm.runInContext;
|
12 |
|
13 | function 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 | */
|
62 | function 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 | */
|
87 | function 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 | */
|
127 | function 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 | */
|
140 | function 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 | */
|
154 | function 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 | */
|
167 | function 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 | */
|
181 | function 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 | */
|
203 | function 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 | */
|
228 | module.exports = {
|
229 | hookRequire,
|
230 | hookCreateScript,
|
231 | unhookCreateScript,
|
232 | hookRunInThisContext,
|
233 | unhookRunInThisContext,
|
234 | hookRunInContext,
|
235 | unhookRunInContext,
|
236 | unloadRequireCache
|
237 | };
|