Comprehensive JavaScript code coverage tool that computes statement, line, function and branch coverage with module loader hooks for transparent instrumentation
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
Hook into Node.js require() calls to instrument modules as they are loaded.
const hook = {
/**
* Hooks require() to transform modules as they are loaded
* @param {Function} matcher - Function that returns true for files to instrument
* @param {Function} transformer - Function that instruments the code
* @param {Object} options - Hook configuration options
*/
hookRequire(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, options?: HookOptions): void;
/**
* Restores original require() behavior and unhooks instrumentation
*/
unhookRequire(): void;
/**
* Hooks vm.createScript() for instrumenting dynamically created scripts
* @param {Function} matcher - Function that returns true for scripts to instrument
* @param {Function} transformer - Function that instruments the code
* @param {Object} opts - Hook configuration options
*/
hookCreateScript(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, opts?: HookOptions): void;
/**
* Restores original vm.createScript() behavior
*/
unhookCreateScript(): void;
/**
* Hooks vm.runInThisContext() for instrumenting eval-like code execution
* @param {Function} matcher - Function that returns true for code to instrument
* @param {Function} transformer - Function that instruments the code
* @param {Object} opts - Hook configuration options
*/
hookRunInThisContext(matcher: (filename: string) => boolean, transformer: (code: string, filename: string) => string, opts?: HookOptions): void;
/**
* Restores original vm.runInThisContext() behavior
*/
unhookRunInThisContext(): void;
/**
* Removes modules from require cache that match the given matcher
* @param {Function} matcher - Function that returns true for modules to unload
*/
unloadRequireCache(matcher: (filename: string) => boolean): void;
};
interface HookOptions {
/** Enable verbose output for hook operations (defaults to false) */
verbose?: boolean;
/** Array of file extensions to process (defaults to ['.js']) */
extensions?: string[];
/** Function called after loading and transforming each module */
postLoadHook?: (file: string) => void;
/** Additional options passed to transformer */
[key: string]: any;
}Usage Examples:
const { hook, Instrumenter, matcherFor } = require('istanbul');
// Create instrumenter
const instrumenter = new Instrumenter();
// Create matcher for JavaScript files (excludes node_modules)
matcherFor({
root: process.cwd(),
includes: ['**/*.js'],
excludes: ['**/node_modules/**', '**/test/**']
}, (err, matcher) => {
if (err) throw err;
// Hook require with instrumentation
hook.hookRequire(matcher, (code, filename) => {
return instrumenter.instrumentSync(code, filename);
});
// Now all matching required modules will be instrumented
const myModule = require('./my-module'); // This gets instrumented
// Later, unhook to restore normal behavior
hook.unhookRequire();
});Use Istanbul's built-in matcher creation for flexible file selection:
const { matcherFor } = require('istanbul');
// Basic matcher for all JS files except node_modules
matcherFor({}, (err, matcher) => {
hook.hookRequire(matcher, transformer);
});
// Custom matcher with specific includes/excludes
matcherFor({
root: '/path/to/project',
includes: ['src/**/*.js', 'lib/**/*.js'],
excludes: ['**/*.test.js', '**/node_modules/**', 'build/**']
}, (err, matcher) => {
hook.hookRequire(matcher, transformer);
});
// Custom matcher function
function customMatcher(filename) {
return filename.includes('/src/') &&
filename.endsWith('.js') &&
!filename.includes('.test.js');
}
hook.hookRequire(customMatcher, transformer);For applications that use vm.createScript() or eval-like constructs:
const vm = require('vm');
// Hook vm.createScript
hook.hookCreateScript(matcher, (code, filename) => {
console.log('Instrumenting script:', filename);
return instrumenter.instrumentSync(code, filename);
});
// Now vm.createScript calls will be instrumented
const script = vm.createScript('console.log("Hello World");', 'dynamic-script.js');
script.runInThisContext();
// Hook vm.runInThisContext for direct eval-like execution
hook.hookRunInThisContext(matcher, transformer);
// This will be instrumented if it matches
vm.runInThisContext('function test() { return 42; }', 'eval-code.js');Typical setup for comprehensive coverage tracking:
const { hook, Instrumenter, matcherFor } = require('istanbul');
function setupCoverageHooks(callback) {
// Initialize coverage tracking
global.__coverage__ = {};
// Create instrumenter
const instrumenter = new Instrumenter({
coverageVariable: '__coverage__',
embedSource: false,
preserveComments: false
});
// Create file matcher
matcherFor({
root: process.cwd(),
includes: ['**/*.js'],
excludes: [
'**/node_modules/**',
'**/test/**',
'**/tests/**',
'**/*.test.js',
'**/*.spec.js',
'**/coverage/**'
]
}, (err, matcher) => {
if (err) return callback(err);
// Transformer function
const transformer = (code, filename) => {
try {
return instrumenter.instrumentSync(code, filename);
} catch (error) {
console.warn('Failed to instrument:', filename, error.message);
return code; // Return original code if instrumentation fails
}
};
// Hook all the things
hook.hookRequire(matcher, transformer);
hook.hookCreateScript(matcher, transformer);
hook.hookRunInThisContext(matcher, transformer);
callback(null);
});
}
// Setup hooks before loading application code
setupCoverageHooks((err) => {
if (err) {
console.error('Failed to setup coverage hooks:', err);
process.exit(1);
}
console.log('Coverage hooks installed');
// Now load and run your application
require('./app');
// After application runs, unhook and generate reports
process.on('exit', () => {
hook.unhookRequire();
hook.unhookCreateScript();
hook.unhookRunInThisContext();
// Generate coverage reports
const { Collector, Reporter } = require('istanbul');
const collector = new Collector();
collector.add(global.__coverage__);
const reporter = new Reporter();
reporter.addAll(['text-summary', 'html']);
reporter.write(collector, true, () => {
console.log('Coverage reports generated');
});
});
});Manage Node.js require cache for accurate coverage:
// Unload modules to ensure fresh instrumentation
hook.unloadRequireCache(matcher);
// Example: Unload specific modules
hook.unloadRequireCache((filename) => {
return filename.includes('/src/') && !filename.includes('node_modules');
});
// Clear entire cache (use with caution)
Object.keys(require.cache).forEach(key => {
delete require.cache[key];
});Proper hook management for test suites:
describe('My Test Suite', () => {
let originalRequire;
before((done) => {
// Setup hooks
matcherFor({}, (err, matcher) => {
if (err) return done(err);
hook.hookRequire(matcher, (code, filename) => {
return instrumenter.instrumentSync(code, filename);
});
done();
});
});
after(() => {
// Clean up hooks
hook.unhookRequire();
hook.unhookCreateScript();
hook.unhookRunInThisContext();
});
beforeEach(() => {
// Reset coverage for each test
global.__coverage__ = {};
});
it('should track coverage', () => {
const myModule = require('./my-module');
myModule.someFunction();
// Coverage should be populated
expect(global.__coverage__).to.have.property('./my-module.js');
});
});Handle instrumentation errors gracefully:
function robustTransformer(code, filename) {
try {
return instrumenter.instrumentSync(code, filename);
} catch (error) {
// Log instrumentation failures
console.warn(`Instrumentation failed for ${filename}:`, error.message);
// Return original code to avoid breaking the application
return code;
}
}
// Enable debug mode for troubleshooting
const debugInstrumenter = new Instrumenter({
debug: true,
walkDebug: true
});
// Custom matcher with debugging
function debugMatcher(filename) {
const shouldInstrument = filename.endsWith('.js') &&
!filename.includes('node_modules');
if (shouldInstrument) {
console.log('Will instrument:', filename);
}
return shouldInstrument;
}Optimize hook performance for large applications:
// Cache instrumented results to avoid re-instrumentation
const instrumentationCache = new Map();
function cachedTransformer(code, filename) {
const cacheKey = filename + ':' + require('crypto')
.createHash('md5')
.update(code)
.digest('hex');
if (instrumentationCache.has(cacheKey)) {
return instrumentationCache.get(cacheKey);
}
const instrumented = instrumenter.instrumentSync(code, filename);
instrumentationCache.set(cacheKey, instrumented);
return instrumented;
}
// Optimize matcher for performance
function optimizedMatcher(filename) {
// Quick checks first
if (!filename.endsWith('.js')) return false;
if (filename.includes('node_modules')) return false;
if (filename.includes('.test.')) return false;
// More expensive checks last
return filename.includes('/src/') || filename.includes('/lib/');
}Common integration patterns:
// Mocha integration
function setupMochaCoverage() {
before(function(done) {
this.timeout(10000); // Increase timeout for instrumentation
matcherFor({}, (err, matcher) => {
if (err) return done(err);
hook.hookRequire(matcher, transformer);
done();
});
});
after(() => {
hook.unhookRequire();
});
}
// Jest integration (in setup file)
const setupJestCoverage = () => {
if (process.env.NODE_ENV === 'test') {
matcherFor({}, (err, matcher) => {
if (!err) {
hook.hookRequire(matcher, transformer);
}
});
}
};
// Manual integration for custom test runners
function runTestsWithCoverage(testFunction) {
return new Promise((resolve, reject) => {
matcherFor({}, (err, matcher) => {
if (err) return reject(err);
hook.hookRequire(matcher, transformer);
Promise.resolve(testFunction())
.then(resolve)
.catch(reject)
.finally(() => {
hook.unhookRequire();
});
});
});
}