UNPKG

7.69 kBJavaScriptView Raw
1/*
2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5
6/**
7 * provides a mechanism to transform code in the scope of `require` or `vm.createScript`.
8 * This mechanism is general and relies on a user-supplied `matcher` function that determines when transformations should be
9 * performed and a user-supplied `transformer` function that performs the actual transform.
10 * Instrumenting code for coverage is one specific example of useful hooking.
11 *
12 * Note that both the `matcher` and `transformer` must execute synchronously.
13 *
14 * For the common case of matching filesystem paths based on inclusion/ exclusion patterns, use the `matcherFor`
15 * function in the istanbul API to get a matcher.
16 *
17 * It is up to the transformer to perform processing with side-effects, such as caching, storing the original
18 * source code to disk in case of dynamically generated scripts etc. The `Store` class can help you with this.
19 *
20 * Usage
21 * -----
22 *
23 * var hook = require('istanbul').hook,
24 * myMatcher = function (file) { return file.match(/foo/); },
25 * myTransformer = function (code, file) { return 'console.log("' + file + '");' + code; };
26 *
27 * hook.hookRequire(myMatcher, myTransformer);
28 *
29 * var foo = require('foo'); //will now print foo's module path to console
30 *
31 * @class Hook
32 * @module main
33 */
34var path = require('path'),
35 fs = require('fs'),
36 Module = require('module'),
37 vm = require('vm'),
38 originalLoaders = {},
39 originalCreateScript = vm.createScript,
40 originalRunInThisContext = vm.runInThisContext;
41
42function transformFn(matcher, transformer, verbose) {
43
44 return function (code, filename) {
45 var shouldHook = typeof filename === 'string' && matcher(path.resolve(filename)),
46 transformed,
47 changed = false;
48
49 if (shouldHook) {
50 if (verbose) {
51 console.error('Module load hook: transform [' + filename + ']');
52 }
53 try {
54 transformed = transformer(code, filename);
55 changed = true;
56 } catch (ex) {
57 console.error('Transformation error; return original code');
58 console.error(ex);
59 transformed = code;
60 }
61 } else {
62 transformed = code;
63 }
64 return { code: transformed, changed: changed };
65 };
66}
67
68function unloadRequireCache(matcher) {
69 if (matcher && typeof require !== 'undefined' && require && require.cache) {
70 Object.keys(require.cache).forEach(function (filename) {
71 if (matcher(filename)) {
72 delete require.cache[filename];
73 }
74 });
75 }
76}
77/**
78 * hooks `require` to return transformed code to the node module loader.
79 * Exceptions in the transform result in the original code being used instead.
80 * @method hookRequire
81 * @static
82 * @param matcher {Function(filePath)} a function that is called with the absolute path to the file being
83 * `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
84 * @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file
85 * from where the code was loaded. Should return the transformed code.
86 * @param options {Object} options Optional.
87 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
88 * @param {Function} [options.postLoadHook] a function that is called with the name of the file being
89 * required. This is called after the require is processed irrespective of whether it was transformed.
90 */
91function hookRequire(matcher, transformer, options) {
92 options = options || {};
93 var extensions,
94 fn = transformFn(matcher, transformer, options.verbose),
95 postLoadHook = options.postLoadHook &&
96 typeof options.postLoadHook === 'function' ? options.postLoadHook : null;
97
98 extensions = options.extensions || ['.js'];
99
100 extensions.forEach(function(ext){
101 if (!(ext in originalLoaders)) {
102 originalLoaders[ext] = Module._extensions[ext] || Module._extensions['.js'];
103 }
104 Module._extensions[ext] = function (module, filename) {
105 var ret = fn(fs.readFileSync(filename, 'utf8'), filename);
106 if (ret.changed) {
107 module._compile(ret.code, filename);
108 } else {
109 originalLoaders[ext](module, filename);
110 }
111 if (postLoadHook) {
112 postLoadHook(filename);
113 }
114 };
115 });
116}
117/**
118 * unhook `require` to restore it to its original state.
119 * @method unhookRequire
120 * @static
121 */
122function unhookRequire() {
123 Object.keys(originalLoaders).forEach(function(ext) {
124 Module._extensions[ext] = originalLoaders[ext];
125 });
126}
127/**
128 * hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.
129 * Exceptions in the transform result in the original code being used instead.
130 * @method hookCreateScript
131 * @static
132 * @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
133 * Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
134 * @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
135 * `vm.createScript`. Should return the transformed code.
136 * @param options {Object} options Optional.
137 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
138 */
139function hookCreateScript(matcher, transformer, opts) {
140 opts = opts || {};
141 var fn = transformFn(matcher, transformer, opts.verbose);
142 vm.createScript = function (code, file) {
143 var ret = fn(code, file);
144 return originalCreateScript(ret.code, file);
145 };
146}
147
148/**
149 * unhooks vm.createScript, restoring it to its original state.
150 * @method unhookCreateScript
151 * @static
152 */
153function unhookCreateScript() {
154 vm.createScript = originalCreateScript;
155}
156
157
158/**
159 * hooks `vm.runInThisContext` to return transformed code.
160 * @method hookRunInThisContext
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 options {Object} options Optional.
167 * @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
168 */
169function hookRunInThisContext(matcher, transformer, opts) {
170 opts = opts || {};
171 var fn = transformFn(matcher, transformer, opts.verbose);
172 vm.runInThisContext = function (code, file) {
173 var ret = fn(code, file);
174 return originalRunInThisContext(ret.code, file);
175 };
176}
177
178/**
179 * unhooks vm.runInThisContext, restoring it to its original state.
180 * @method unhookRunInThisContext
181 * @static
182 */
183function unhookRunInThisContext() {
184 vm.runInThisContext = originalRunInThisContext;
185}
186
187
188module.exports = {
189 hookRequire: hookRequire,
190 unhookRequire: unhookRequire,
191 hookCreateScript: hookCreateScript,
192 unhookCreateScript: unhookCreateScript,
193 hookRunInThisContext : hookRunInThisContext,
194 unhookRunInThisContext : unhookRunInThisContext,
195 unloadRequireCache: unloadRequireCache
196};
197
198