Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80668 views
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
*/
34
var 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
42
function 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
68
function 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
*/
91
function 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
originalLoaders[ext] = Module._extensions[ext];
102
Module._extensions[ext] = function (module, filename) {
103
var ret = fn(fs.readFileSync(filename, 'utf8'), filename);
104
if (ret.changed) {
105
module._compile(ret.code, filename);
106
} else {
107
originalLoaders[ext](module, filename);
108
}
109
if (postLoadHook) {
110
postLoadHook(filename);
111
}
112
};
113
});
114
}
115
/**
116
* unhook `require` to restore it to its original state.
117
* @method unhookRequire
118
* @static
119
*/
120
function unhookRequire() {
121
Object.keys(originalLoaders).forEach(function(ext) {
122
Module._extensions[ext] = originalLoaders[ext];
123
});
124
}
125
/**
126
* hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.
127
* Exceptions in the transform result in the original code being used instead.
128
* @method hookCreateScript
129
* @static
130
* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
131
* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
132
* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
133
* `vm.createScript`. Should return the transformed code.
134
* @param options {Object} options Optional.
135
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
136
*/
137
function hookCreateScript(matcher, transformer, opts) {
138
opts = opts || {};
139
var fn = transformFn(matcher, transformer, opts.verbose);
140
vm.createScript = function (code, file) {
141
var ret = fn(code, file);
142
return originalCreateScript(ret.code, file);
143
};
144
}
145
146
/**
147
* unhooks vm.createScript, restoring it to its original state.
148
* @method unhookCreateScript
149
* @static
150
*/
151
function unhookCreateScript() {
152
vm.createScript = originalCreateScript;
153
}
154
155
156
/**
157
* hooks `vm.runInThisContext` to return transformed code.
158
* @method hookRunInThisContext
159
* @static
160
* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
161
* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
162
* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
163
* `vm.createScript`. Should return the transformed code.
164
* @param options {Object} options Optional.
165
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
166
*/
167
function hookRunInThisContext(matcher, transformer, opts) {
168
opts = opts || {};
169
var fn = transformFn(matcher, transformer, opts.verbose);
170
vm.runInThisContext = function (code, file) {
171
var ret = fn(code, file);
172
return originalRunInThisContext(ret.code, file);
173
};
174
}
175
176
/**
177
* unhooks vm.runInThisContext, restoring it to its original state.
178
* @method unhookRunInThisContext
179
* @static
180
*/
181
function unhookRunInThisContext() {
182
vm.runInThisContext = originalRunInThisContext;
183
}
184
185
186
module.exports = {
187
hookRequire: hookRequire,
188
unhookRequire: unhookRequire,
189
hookCreateScript: hookCreateScript,
190
unhookCreateScript: unhookCreateScript,
191
hookRunInThisContext : hookRunInThisContext,
192
unhookRunInThisContext : unhookRunInThisContext,
193
unloadRequireCache: unloadRequireCache
194
};
195
196
197
198