0
# Runtime Hooks
1
2
Runtime hooks intercept Node.js module loading and script execution to instrument code transparently at runtime. This enables coverage tracking without pre-instrumenting files on disk.
3
4
## Capabilities
5
6
### Module Loading Hooks
7
8
Hook into Node.js require() calls to instrument modules as they are loaded.
9
10
```javascript { .api }
11
const hook = {
12
/**
13
* Hooks require() to transform modules as they are loaded
14
* @param {Function} matcher - Function that returns true for files to instrument
15
* @param {Function} transformer - Function that instruments the code
16
* @param {Object} options - Hook configuration options
17
*/
18
hookRequire(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, options?: HookOptions): void;
19
20
/**
21
* Restores original require() behavior and unhooks instrumentation
22
*/
23
unhookRequire(): void;
24
25
/**
26
* Hooks vm.createScript() for instrumenting dynamically created scripts
27
* @param {Function} matcher - Function that returns true for scripts to instrument
28
* @param {Function} transformer - Function that instruments the code
29
* @param {Object} opts - Hook configuration options
30
*/
31
hookCreateScript(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, opts?: HookOptions): void;
32
33
/**
34
* Restores original vm.createScript() behavior
35
*/
36
unhookCreateScript(): void;
37
38
/**
39
* Hooks vm.runInThisContext() for instrumenting eval-like code execution
40
* @param {Function} matcher - Function that returns true for code to instrument
41
* @param {Function} transformer - Function that instruments the code
42
* @param {Object} opts - Hook configuration options
43
*/
44
hookRunInThisContext(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, opts?: HookOptions): void;
45
46
/**
47
* Restores original vm.runInThisContext() behavior
48
*/
49
unhookRunInThisContext(): void;
50
51
/**
52
* Removes modules from require cache that match the given matcher
53
* @param {Function} matcher - Function that returns true for modules to unload
54
*/
55
unloadRequireCache(matcher: (filename: string) => boolean): void;
56
};
57
58
interface HookOptions {
59
/** Enable verbose output for hook operations (defaults to false) */
60
verbose?: boolean;
61
62
/** Array of file extensions to process (defaults to ['.js']) */
63
extensions?: string[];
64
65
/** Function called after loading and transforming each module */
66
postLoadHook?: (file: string) => void;
67
68
/** Additional options passed to transformer */
69
[key: string]: any;
70
}
71
```
72
73
**Usage Examples:**
74
75
```javascript
76
const { hook, Instrumenter, matcherFor } = require('istanbul');
77
78
// Create instrumenter
79
const instrumenter = new Instrumenter();
80
81
// Create matcher for JavaScript files (excludes node_modules)
82
matcherFor({
83
root: process.cwd(),
84
includes: ['**/*.js'],
85
excludes: ['**/node_modules/**', '**/test/**']
86
}, (err, matcher) => {
87
if (err) throw err;
88
89
// Hook require with instrumentation
90
hook.hookRequire(matcher, (code, filename) => {
91
return instrumenter.instrumentSync(code, filename);
92
});
93
94
// Now all matching required modules will be instrumented
95
const myModule = require('./my-module'); // This gets instrumented
96
97
// Later, unhook to restore normal behavior
98
hook.unhookRequire();
99
});
100
```
101
102
### File Matching
103
104
Use Istanbul's built-in matcher creation for flexible file selection:
105
106
```javascript
107
const { matcherFor } = require('istanbul');
108
109
// Basic matcher for all JS files except node_modules
110
matcherFor({}, (err, matcher) => {
111
hook.hookRequire(matcher, transformer);
112
});
113
114
// Custom matcher with specific includes/excludes
115
matcherFor({
116
root: '/path/to/project',
117
includes: ['src/**/*.js', 'lib/**/*.js'],
118
excludes: ['**/*.test.js', '**/node_modules/**', 'build/**']
119
}, (err, matcher) => {
120
hook.hookRequire(matcher, transformer);
121
});
122
123
// Custom matcher function
124
function customMatcher(filename) {
125
return filename.includes('/src/') &&
126
filename.endsWith('.js') &&
127
!filename.includes('.test.js');
128
}
129
130
hook.hookRequire(customMatcher, transformer);
131
```
132
133
### VM Script Hooks
134
135
For applications that use vm.createScript() or eval-like constructs:
136
137
```javascript
138
const vm = require('vm');
139
140
// Hook vm.createScript
141
hook.hookCreateScript(matcher, (code, filename) => {
142
console.log('Instrumenting script:', filename);
143
return instrumenter.instrumentSync(code, filename);
144
});
145
146
// Now vm.createScript calls will be instrumented
147
const script = vm.createScript('console.log("Hello World");', 'dynamic-script.js');
148
script.runInThisContext();
149
150
// Hook vm.runInThisContext for direct eval-like execution
151
hook.hookRunInThisContext(matcher, transformer);
152
153
// This will be instrumented if it matches
154
vm.runInThisContext('function test() { return 42; }', 'eval-code.js');
155
```
156
157
### Complete Hook Setup
158
159
Typical setup for comprehensive coverage tracking:
160
161
```javascript
162
const { hook, Instrumenter, matcherFor } = require('istanbul');
163
164
function setupCoverageHooks(callback) {
165
// Initialize coverage tracking
166
global.__coverage__ = {};
167
168
// Create instrumenter
169
const instrumenter = new Instrumenter({
170
coverageVariable: '__coverage__',
171
embedSource: false,
172
preserveComments: false
173
});
174
175
// Create file matcher
176
matcherFor({
177
root: process.cwd(),
178
includes: ['**/*.js'],
179
excludes: [
180
'**/node_modules/**',
181
'**/test/**',
182
'**/tests/**',
183
'**/*.test.js',
184
'**/*.spec.js',
185
'**/coverage/**'
186
]
187
}, (err, matcher) => {
188
if (err) return callback(err);
189
190
// Transformer function
191
const transformer = (code, filename) => {
192
try {
193
return instrumenter.instrumentSync(code, filename);
194
} catch (error) {
195
console.warn('Failed to instrument:', filename, error.message);
196
return code; // Return original code if instrumentation fails
197
}
198
};
199
200
// Hook all the things
201
hook.hookRequire(matcher, transformer);
202
hook.hookCreateScript(matcher, transformer);
203
hook.hookRunInThisContext(matcher, transformer);
204
205
callback(null);
206
});
207
}
208
209
// Setup hooks before loading application code
210
setupCoverageHooks((err) => {
211
if (err) {
212
console.error('Failed to setup coverage hooks:', err);
213
process.exit(1);
214
}
215
216
console.log('Coverage hooks installed');
217
218
// Now load and run your application
219
require('./app');
220
221
// After application runs, unhook and generate reports
222
process.on('exit', () => {
223
hook.unhookRequire();
224
hook.unhookCreateScript();
225
hook.unhookRunInThisContext();
226
227
// Generate coverage reports
228
const { Collector, Reporter } = require('istanbul');
229
const collector = new Collector();
230
collector.add(global.__coverage__);
231
232
const reporter = new Reporter();
233
reporter.addAll(['text-summary', 'html']);
234
reporter.write(collector, true, () => {
235
console.log('Coverage reports generated');
236
});
237
});
238
});
239
```
240
241
### Cache Management
242
243
Manage Node.js require cache for accurate coverage:
244
245
```javascript
246
// Unload modules to ensure fresh instrumentation
247
hook.unloadRequireCache(matcher);
248
249
// Example: Unload specific modules
250
hook.unloadRequireCache((filename) => {
251
return filename.includes('/src/') && !filename.includes('node_modules');
252
});
253
254
// Clear entire cache (use with caution)
255
Object.keys(require.cache).forEach(key => {
256
delete require.cache[key];
257
});
258
```
259
260
### Hook Lifecycle Management
261
262
Proper hook management for test suites:
263
264
```javascript
265
describe('My Test Suite', () => {
266
let originalRequire;
267
268
before((done) => {
269
// Setup hooks
270
matcherFor({}, (err, matcher) => {
271
if (err) return done(err);
272
273
hook.hookRequire(matcher, (code, filename) => {
274
return instrumenter.instrumentSync(code, filename);
275
});
276
277
done();
278
});
279
});
280
281
after(() => {
282
// Clean up hooks
283
hook.unhookRequire();
284
hook.unhookCreateScript();
285
hook.unhookRunInThisContext();
286
});
287
288
beforeEach(() => {
289
// Reset coverage for each test
290
global.__coverage__ = {};
291
});
292
293
it('should track coverage', () => {
294
const myModule = require('./my-module');
295
myModule.someFunction();
296
297
// Coverage should be populated
298
expect(global.__coverage__).to.have.property('./my-module.js');
299
});
300
});
301
```
302
303
### Error Handling and Debugging
304
305
Handle instrumentation errors gracefully:
306
307
```javascript
308
function robustTransformer(code, filename) {
309
try {
310
return instrumenter.instrumentSync(code, filename);
311
} catch (error) {
312
// Log instrumentation failures
313
console.warn(`Instrumentation failed for ${filename}:`, error.message);
314
315
// Return original code to avoid breaking the application
316
return code;
317
}
318
}
319
320
// Enable debug mode for troubleshooting
321
const debugInstrumenter = new Instrumenter({
322
debug: true,
323
walkDebug: true
324
});
325
326
// Custom matcher with debugging
327
function debugMatcher(filename) {
328
const shouldInstrument = filename.endsWith('.js') &&
329
!filename.includes('node_modules');
330
331
if (shouldInstrument) {
332
console.log('Will instrument:', filename);
333
}
334
335
return shouldInstrument;
336
}
337
```
338
339
### Performance Considerations
340
341
Optimize hook performance for large applications:
342
343
```javascript
344
// Cache instrumented results to avoid re-instrumentation
345
const instrumentationCache = new Map();
346
347
function cachedTransformer(code, filename) {
348
const cacheKey = filename + ':' + require('crypto')
349
.createHash('md5')
350
.update(code)
351
.digest('hex');
352
353
if (instrumentationCache.has(cacheKey)) {
354
return instrumentationCache.get(cacheKey);
355
}
356
357
const instrumented = instrumenter.instrumentSync(code, filename);
358
instrumentationCache.set(cacheKey, instrumented);
359
return instrumented;
360
}
361
362
// Optimize matcher for performance
363
function optimizedMatcher(filename) {
364
// Quick checks first
365
if (!filename.endsWith('.js')) return false;
366
if (filename.includes('node_modules')) return false;
367
if (filename.includes('.test.')) return false;
368
369
// More expensive checks last
370
return filename.includes('/src/') || filename.includes('/lib/');
371
}
372
```
373
374
### Integration with Test Frameworks
375
376
Common integration patterns:
377
378
```javascript
379
// Mocha integration
380
function setupMochaCoverage() {
381
before(function(done) {
382
this.timeout(10000); // Increase timeout for instrumentation
383
384
matcherFor({}, (err, matcher) => {
385
if (err) return done(err);
386
hook.hookRequire(matcher, transformer);
387
done();
388
});
389
});
390
391
after(() => {
392
hook.unhookRequire();
393
});
394
}
395
396
// Jest integration (in setup file)
397
const setupJestCoverage = () => {
398
if (process.env.NODE_ENV === 'test') {
399
matcherFor({}, (err, matcher) => {
400
if (!err) {
401
hook.hookRequire(matcher, transformer);
402
}
403
});
404
}
405
};
406
407
// Manual integration for custom test runners
408
function runTestsWithCoverage(testFunction) {
409
return new Promise((resolve, reject) => {
410
matcherFor({}, (err, matcher) => {
411
if (err) return reject(err);
412
413
hook.hookRequire(matcher, transformer);
414
415
Promise.resolve(testFunction())
416
.then(resolve)
417
.catch(reject)
418
.finally(() => {
419
hook.unhookRequire();
420
});
421
});
422
});
423
}
424
```