preprocessor.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Coverage Preprocessor
  2. // =====================
  3. //
  4. // Depends on the the reporter to generate an actual report
  5. // Dependencies
  6. // ------------
  7. const { createInstrumenter } = require('istanbul-lib-instrument')
  8. const minimatch = require('minimatch')
  9. const path = require('path')
  10. const globalSourceMapStore = require('./source-map-store')
  11. const globalCoverageMap = require('./coverage-map')
  12. // Regexes
  13. // -------
  14. const coverageObjRegex = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g
  15. // Preprocessor creator function
  16. function createCoveragePreprocessor (logger, basePath, reporters = [], coverageReporter = {}) {
  17. const log = logger.create('preprocessor.coverage')
  18. // Options
  19. // -------
  20. function isConstructor (Func) {
  21. try {
  22. // eslint-disable-next-line
  23. new Func()
  24. } catch (err) {
  25. // error message should be of the form: "TypeError: func is not a constructor"
  26. // test for this type of message to ensure we failed due to the function not being
  27. // constructable
  28. if (/TypeError.*constructor/.test(err.message)) {
  29. return false
  30. }
  31. }
  32. return true
  33. }
  34. function getCreatorFunction (Obj) {
  35. if (Obj.Instrumenter) {
  36. return function (opts) {
  37. return new Obj.Instrumenter(opts)
  38. }
  39. }
  40. if (typeof Obj !== 'function') {
  41. // Object doesn't have old instrumenter variable and isn't a
  42. // constructor, so we can't use it to create an instrumenter
  43. return null
  44. }
  45. if (isConstructor(Obj)) {
  46. return function (opts) {
  47. return new Obj(opts)
  48. }
  49. }
  50. return Obj
  51. }
  52. const instrumenters = { istanbul: createInstrumenter }
  53. const instrumenterOverrides = coverageReporter.instrumenter || {}
  54. const { includeAllSources = false, useJSExtensionForCoffeeScript = false } = coverageReporter
  55. Object.entries(coverageReporter.instrumenters || {}).forEach(([literal, instrumenter]) => {
  56. const creatorFunction = getCreatorFunction(instrumenter)
  57. if (creatorFunction) {
  58. instrumenters[literal] = creatorFunction
  59. }
  60. })
  61. const sourceMapStore = globalSourceMapStore.get(basePath)
  62. const instrumentersOptions = Object.keys(instrumenters).reduce((memo, key) => {
  63. memo[key] = {}
  64. if (coverageReporter.instrumenterOptions) {
  65. memo[key] = coverageReporter.instrumenterOptions[key]
  66. }
  67. return memo
  68. }, {})
  69. // if coverage reporter is not used, do not preprocess the files
  70. if (!reporters.includes('coverage')) {
  71. log.info('coverage not included in reporters %s', reporters)
  72. return function (content, _, done) {
  73. done(content)
  74. }
  75. }
  76. log.debug('coverage included in reporters %s', reporters)
  77. // check instrumenter override requests
  78. function checkInstrumenters () {
  79. const keys = Object.keys(instrumenters)
  80. return Object.values(instrumenterOverrides).some(literal => {
  81. const notIncluded = !keys.includes(String(literal))
  82. if (notIncluded) {
  83. log.error('Unknown instrumenter: %s', literal)
  84. }
  85. return notIncluded
  86. })
  87. }
  88. if (checkInstrumenters()) {
  89. return function (content, _, done) {
  90. return done(1)
  91. }
  92. }
  93. return function (content, file, done) {
  94. log.debug('Processing "%s".', file.originalPath)
  95. const jsPath = path.resolve(file.originalPath)
  96. // 'istanbul' is default instrumenters
  97. const instrumenterLiteral = Object.keys(instrumenterOverrides).reduce((res, pattern) => {
  98. if (minimatch(file.originalPath, pattern, { dot: true })) {
  99. return instrumenterOverrides[pattern]
  100. }
  101. return res
  102. }, 'istanbul')
  103. const instrumenterCreator = instrumenters[instrumenterLiteral]
  104. const constructOptions = instrumentersOptions[instrumenterLiteral] || {}
  105. let options = Object.assign({}, constructOptions)
  106. let codeGenerationOptions = null
  107. options.autoWrap = options.autoWrap || !options.noAutoWrap
  108. if (file.sourceMap) {
  109. log.debug('Enabling source map generation for "%s".', file.originalPath)
  110. codeGenerationOptions = Object.assign({}, {
  111. format: {
  112. compact: !constructOptions.noCompact
  113. },
  114. sourceMap: file.sourceMap.file,
  115. sourceMapWithCode: true,
  116. file: file.path
  117. }, constructOptions.codeGenerationOptions || {})
  118. options.produceSourceMap = true
  119. }
  120. options = Object.assign({}, options, { codeGenerationOptions: codeGenerationOptions })
  121. const instrumenter = instrumenterCreator(options)
  122. instrumenter.instrument(content, jsPath, function (err, instrumentedCode) {
  123. if (err) {
  124. log.error('%s\n at %s', err.message, file.originalPath)
  125. done(err.message)
  126. } else {
  127. // Register the incoming sourceMap for transformation during reporting (if it exists)
  128. if (file.sourceMap) {
  129. sourceMapStore.registerMap(jsPath, file.sourceMap)
  130. }
  131. // Add merged source map (if it merged correctly)
  132. const lastSourceMap = instrumenter.lastSourceMap()
  133. if (lastSourceMap) {
  134. log.debug('Adding source map to instrumented file for "%s".', file.originalPath)
  135. file.sourceMap = lastSourceMap
  136. instrumentedCode += '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,'
  137. instrumentedCode += Buffer.from(JSON.stringify(lastSourceMap)).toString('base64') + '\n'
  138. }
  139. if (includeAllSources) {
  140. let coverageObj
  141. // Check if the file coverage object is exposed from the instrumenter directly
  142. if (instrumenter.lastFileCoverage) {
  143. coverageObj = instrumenter.lastFileCoverage()
  144. globalCoverageMap.add(coverageObj)
  145. } else {
  146. // Attempt to match and parse coverage object from instrumented code
  147. // reset stateful regex
  148. coverageObjRegex.lastIndex = 0
  149. const coverageObjMatch = coverageObjRegex.exec(instrumentedCode)
  150. if (coverageObjMatch !== null) {
  151. coverageObj = JSON.parse(coverageObjMatch[0])
  152. globalCoverageMap.add(coverageObj)
  153. }
  154. }
  155. }
  156. // RequireJS expects JavaScript files to end with `.js`
  157. if (useJSExtensionForCoffeeScript && instrumenterLiteral === 'ibrik') {
  158. file.path = file.path.replace(/\.coffee$/, '.js')
  159. }
  160. done(instrumentedCode)
  161. }
  162. }, file.sourceMap)
  163. }
  164. }
  165. createCoveragePreprocessor.$inject = [
  166. 'logger',
  167. 'config.basePath',
  168. 'config.reporters',
  169. 'config.coverageReporter'
  170. ]
  171. module.exports = createCoveragePreprocessor