preprocessor.js 5.09 KB
// Coverage Preprocessor
// =====================
//
// Depends on the the reporter to generate an actual report

// Dependencies
// ------------

var istanbul = require('istanbul')
var minimatch = require('minimatch')
var path = require('path')
var _ = require('lodash')
var SourceMapConsumer = require('source-map').SourceMapConsumer
var SourceMapGenerator = require('source-map').SourceMapGenerator
var globalSourceCache = require('./source-cache')
var extend = require('util')._extend
var coverageMap = require('./coverage-map')

// Regexes
// -------

var coverageObjRegex = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g

// Preprocessor creator function
function createCoveragePreprocessor (logger, helper, basePath, reporters, coverageReporter) {
  var log = logger.create('preprocessor.coverage')

  // Options
  // -------

  var instrumenterOverrides = {}
  var instrumenters = {istanbul: istanbul}
  var includeAllSources = false
  var useJSExtensionForCoffeeScript = false

  if (coverageReporter) {
    instrumenterOverrides = coverageReporter.instrumenter
    instrumenters = extend({istanbul: istanbul}, coverageReporter.instrumenters)
    includeAllSources = coverageReporter.includeAllSources === true
    useJSExtensionForCoffeeScript = coverageReporter.useJSExtensionForCoffeeScript === true
  }

  var sourceCache = globalSourceCache.get(basePath)

  var instrumentersOptions = _.reduce(instrumenters, function getInstumenterOptions (memo, instrument, name) {
    memo[name] = {}

    if (coverageReporter && coverageReporter.instrumenterOptions) {
      memo[name] = coverageReporter.instrumenterOptions[name]
    }

    return memo
  }, {})

  // if coverage reporter is not used, do not preprocess the files
  if (!_.includes(reporters, 'coverage')) {
    return function (content, _, done) {
      done(content)
    }
  }

  // check instrumenter override requests
  function checkInstrumenters () {
    return _.reduce(instrumenterOverrides, function (acc, literal, pattern) {
      if (!_.includes(_.keys(instrumenters), String(literal))) {
        log.error('Unknown instrumenter: %s', literal)
        return false
      }
      return acc
    }, true)
  }

  if (!checkInstrumenters()) {
    return function (content, _, done) {
      return done(1)
    }
  }

  return function (content, file, done) {
    log.debug('Processing "%s".', file.originalPath)

    var jsPath = path.resolve(file.originalPath)
    // default instrumenters
    var instrumenterLiteral = 'istanbul'

    _.forEach(instrumenterOverrides, function (literal, pattern) {
      if (minimatch(file.originalPath, pattern, {dot: true})) {
        instrumenterLiteral = String(literal)
      }
    })

    var InstrumenterConstructor = instrumenters[instrumenterLiteral].Instrumenter
    var constructOptions = instrumentersOptions[instrumenterLiteral] || {}
    var codeGenerationOptions = null

    if (file.sourceMap) {
      log.debug('Enabling source map generation for "%s".', file.originalPath)
      codeGenerationOptions = extend({
        format: {
          compact: !constructOptions.noCompact
        },
        sourceMap: file.sourceMap.file,
        sourceMapWithCode: true,
        file: file.path
      }, constructOptions.codeGenerationOptions || {})
    }

    var options = extend({}, constructOptions)
    options = extend(options, {codeGenerationOptions: codeGenerationOptions})

    var instrumenter = new InstrumenterConstructor(options)
    instrumenter.instrument(content, jsPath, function (err, instrumentedCode) {
      if (err) {
        log.error('%s\n  at %s', err.message, file.originalPath)
        done(err.message)
      } else {
        if (file.sourceMap && instrumenter.lastSourceMap()) {
          log.debug('Adding source map to instrumented file for "%s".', file.originalPath)
          var generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(instrumenter.lastSourceMap().toString()))
          generator.applySourceMap(new SourceMapConsumer(file.sourceMap))
          file.sourceMap = JSON.parse(generator.toString())
          instrumentedCode += '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,'
          instrumentedCode += new Buffer(JSON.stringify(file.sourceMap)).toString('base64') + '\n'
        }

        // remember the actual immediate instrumented JS for given original path
        sourceCache[jsPath] = content

        if (includeAllSources) {
          // reset stateful regex
          coverageObjRegex.lastIndex = 0

          var coverageObjMatch = coverageObjRegex.exec(instrumentedCode)

          if (coverageObjMatch !== null) {
            var coverageObj = JSON.parse(coverageObjMatch[0])

            coverageMap.add(coverageObj)
          }
        }

        // RequireJS expects JavaScript files to end with `.js`
        if (useJSExtensionForCoffeeScript && instrumenterLiteral === 'ibrik') {
          file.path = file.path.replace(/\.coffee$/, '.js')
        }

        done(instrumentedCode)
      }
    })
  }
}

createCoveragePreprocessor.$inject = [
  'logger',
  'helper',
  'config.basePath',
  'config.reporters',
  'config.coverageReporter'
]

module.exports = createCoveragePreprocessor