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