202 lines
6.4 KiB
JavaScript
202 lines
6.4 KiB
JavaScript
|
// 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
|