// Coverage Preprocessor // ===================== // // Depends on the the reporter to generate an actual report // Dependencies // ------------ const { createInstrumenter } = require('istanbul-lib-instrument') const minimatch = require('minimatch') const path = require('path') const globalSourceMapStore = require('./source-map-store') const globalCoverageMap = require('./coverage-map') // Regexes // ------- const coverageObjRegex = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g // Preprocessor creator function function createCoveragePreprocessor (logger, basePath, reporters = [], coverageReporter = {}) { const log = logger.create('preprocessor.coverage') // Options // ------- function isConstructor (Func) { try { // eslint-disable-next-line new Func() } catch (err) { // error message should be of the form: "TypeError: func is not a constructor" // test for this type of message to ensure we failed due to the function not being // constructable if (/TypeError.*constructor/.test(err.message)) { return false } } return true } function getCreatorFunction (Obj) { if (Obj.Instrumenter) { return function (opts) { return new Obj.Instrumenter(opts) } } if (typeof Obj !== 'function') { // Object doesn't have old instrumenter variable and isn't a // constructor, so we can't use it to create an instrumenter return null } if (isConstructor(Obj)) { return function (opts) { return new Obj(opts) } } return Obj } const instrumenters = { istanbul: createInstrumenter } const instrumenterOverrides = coverageReporter.instrumenter || {} const { includeAllSources = false, useJSExtensionForCoffeeScript = false } = coverageReporter Object.entries(coverageReporter.instrumenters || {}).forEach(([literal, instrumenter]) => { const creatorFunction = getCreatorFunction(instrumenter) if (creatorFunction) { instrumenters[literal] = creatorFunction } }) const sourceMapStore = globalSourceMapStore.get(basePath) const instrumentersOptions = Object.keys(instrumenters).reduce((memo, key) => { memo[key] = {} if (coverageReporter.instrumenterOptions) { memo[key] = coverageReporter.instrumenterOptions[key] } return memo }, {}) // if coverage reporter is not used, do not preprocess the files if (!reporters.includes('coverage')) { log.info('coverage not included in reporters %s', reporters) return function (content, _, done) { done(content) } } log.debug('coverage included in reporters %s', reporters) // check instrumenter override requests function checkInstrumenters () { const keys = Object.keys(instrumenters) return Object.values(instrumenterOverrides).some(literal => { const notIncluded = !keys.includes(String(literal)) if (notIncluded) { log.error('Unknown instrumenter: %s', literal) } return notIncluded }) } if (checkInstrumenters()) { return function (content, _, done) { return done(1) } } return function (content, file, done) { log.debug('Processing "%s".', file.originalPath) const jsPath = path.resolve(file.originalPath) // 'istanbul' is default instrumenters const instrumenterLiteral = Object.keys(instrumenterOverrides).reduce((res, pattern) => { if (minimatch(file.originalPath, pattern, { dot: true })) { return instrumenterOverrides[pattern] } return res }, 'istanbul') const instrumenterCreator = instrumenters[instrumenterLiteral] const constructOptions = instrumentersOptions[instrumenterLiteral] || {} let options = Object.assign({}, constructOptions) let codeGenerationOptions = null options.autoWrap = options.autoWrap || !options.noAutoWrap if (file.sourceMap) { log.debug('Enabling source map generation for "%s".', file.originalPath) codeGenerationOptions = Object.assign({}, { format: { compact: !constructOptions.noCompact }, sourceMap: file.sourceMap.file, sourceMapWithCode: true, file: file.path }, constructOptions.codeGenerationOptions || {}) options.produceSourceMap = true } options = Object.assign({}, options, { codeGenerationOptions: codeGenerationOptions }) const instrumenter = instrumenterCreator(options) instrumenter.instrument(content, jsPath, function (err, instrumentedCode) { if (err) { log.error('%s\n at %s', err.message, file.originalPath) done(err.message) } else { // Register the incoming sourceMap for transformation during reporting (if it exists) if (file.sourceMap) { sourceMapStore.registerMap(jsPath, file.sourceMap) } // Add merged source map (if it merged correctly) const lastSourceMap = instrumenter.lastSourceMap() if (lastSourceMap) { log.debug('Adding source map to instrumented file for "%s".', file.originalPath) file.sourceMap = lastSourceMap instrumentedCode += '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' instrumentedCode += Buffer.from(JSON.stringify(lastSourceMap)).toString('base64') + '\n' } if (includeAllSources) { let coverageObj // Check if the file coverage object is exposed from the instrumenter directly if (instrumenter.lastFileCoverage) { coverageObj = instrumenter.lastFileCoverage() globalCoverageMap.add(coverageObj) } else { // Attempt to match and parse coverage object from instrumented code // reset stateful regex coverageObjRegex.lastIndex = 0 const coverageObjMatch = coverageObjRegex.exec(instrumentedCode) if (coverageObjMatch !== null) { coverageObj = JSON.parse(coverageObjMatch[0]) globalCoverageMap.add(coverageObj) } } } // RequireJS expects JavaScript files to end with `.js` if (useJSExtensionForCoffeeScript && instrumenterLiteral === 'ibrik') { file.path = file.path.replace(/\.coffee$/, '.js') } done(instrumentedCode) } }, file.sourceMap) } } createCoveragePreprocessor.$inject = [ 'logger', 'config.basePath', 'config.reporters', 'config.coverageReporter' ] module.exports = createCoveragePreprocessor