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 | 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 | */
|
122 | function 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 | */
|
139 | function 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 | */
|
153 | function 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 | */
|
169 | function 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 | */
|
183 | function unhookRunInThisContext() {
|
184 | vm.runInThisContext = originalRunInThisContext;
|
185 | }
|
186 |
|
187 |
|
188 | module.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 |
|