| // 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 |