reporter.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. 'use strict'
  2. // eslint-disable-next-line node/no-deprecated-api
  3. const resolve = require('url').resolve
  4. const SourceMapConsumer = require('source-map').SourceMapConsumer
  5. const _ = require('lodash')
  6. const PathUtils = require('./utils/path-utils')
  7. const log = require('./logger').create('reporter')
  8. const MultiReporter = require('./reporters/multi')
  9. const baseReporterDecoratorFactory = require('./reporters/base').decoratorFactory
  10. function createErrorFormatter (config, emitter, SourceMapConsumer) {
  11. const basePath = config.basePath
  12. const urlRoot = config.urlRoot === '/' ? '' : (config.urlRoot || '')
  13. let lastServedFiles = []
  14. emitter.on('file_list_modified', (files) => {
  15. lastServedFiles = files.served
  16. })
  17. const URL_REGEXP = new RegExp('(?:https?:\\/\\/' +
  18. config.hostname + '(?:\\:' + config.port + ')?' + ')?\\/?' +
  19. urlRoot + '\\/?' +
  20. '(base/|absolute)' + // prefix, including slash for base/ to create relative paths.
  21. '((?:[A-z]\\:)?[^\\?\\s\\:]*)' + // path
  22. '(\\?\\w*)?' + // sha
  23. '(\\:(\\d+))?' + // line
  24. '(\\:(\\d+))?' + // column
  25. '', 'g')
  26. const cache = new WeakMap()
  27. function getSourceMapConsumer (sourceMap) {
  28. if (!cache.has(sourceMap)) {
  29. cache.set(sourceMap, new SourceMapConsumer(sourceMap))
  30. }
  31. return cache.get(sourceMap)
  32. }
  33. return function (input, indentation) {
  34. indentation = _.isString(indentation) ? indentation : ''
  35. if (_.isError(input)) {
  36. input = input.message
  37. } else if (_.isEmpty(input)) {
  38. input = ''
  39. } else if (!_.isString(input)) {
  40. input = JSON.stringify(input, null, indentation)
  41. }
  42. let msg = input.replace(URL_REGEXP, function (stackTracePath, prefix, path, __, ___, line, ____, column) {
  43. const normalizedPath = prefix === 'base/' ? `${basePath}/${path}` : path
  44. const file = lastServedFiles.find((file) => file.path === normalizedPath)
  45. if (file && file.sourceMap && line) {
  46. line = +line
  47. column = +column
  48. // When no column is given and we default to 0, it doesn't make sense to only search for smaller
  49. // or equal columns in the sourcemap, let's search for equal or greater columns.
  50. const bias = column ? SourceMapConsumer.GREATEST_LOWER_BOUND : SourceMapConsumer.LEAST_UPPER_BOUND
  51. try {
  52. const zeroBasedColumn = Math.max(0, (column || 1) - 1)
  53. const original = getSourceMapConsumer(file.sourceMap).originalPositionFor({ line, column: zeroBasedColumn, bias })
  54. // If there is no original position/source for the current stack trace path, then
  55. // we return early with the formatted generated position. This handles the case of
  56. // generated code which does not map to anything, see Case 1 of the source-map spec.
  57. // https://sourcemaps.info/spec.html.
  58. if (original.source === null) {
  59. return PathUtils.formatPathMapping(path, line, column)
  60. }
  61. // Source maps often only have a local file name, resolve to turn into a full path if
  62. // the path is not absolute yet.
  63. const oneBasedOriginalColumn = original.column == null ? original.column : original.column + 1
  64. return `${PathUtils.formatPathMapping(resolve(path, original.source), original.line, oneBasedOriginalColumn)} <- ${PathUtils.formatPathMapping(path, line, column)}`
  65. } catch (e) {
  66. log.warn(`An unexpected error occurred while resolving the original position for: ${stackTracePath}`)
  67. log.warn(e)
  68. }
  69. }
  70. return PathUtils.formatPathMapping(path, line, column) || prefix
  71. })
  72. if (indentation) {
  73. msg = indentation + msg.replace(/\n/g, '\n' + indentation)
  74. }
  75. return config.formatError ? config.formatError(msg) : msg + '\n'
  76. }
  77. }
  78. function createReporters (names, config, emitter, injector) {
  79. const errorFormatter = createErrorFormatter(config, emitter, SourceMapConsumer)
  80. const reporters = []
  81. names.forEach((name) => {
  82. if (['dots', 'progress'].includes(name)) {
  83. [
  84. require(`./reporters/${name}`),
  85. require(`./reporters/${name}_color`)
  86. ].forEach((Reporter) => {
  87. reporters.push(new Reporter(errorFormatter, config.reportSlowerThan, config.colors, config.browserConsoleLogOptions))
  88. })
  89. return
  90. }
  91. const locals = {
  92. baseReporterDecorator: ['factory', baseReporterDecoratorFactory],
  93. formatError: ['value', errorFormatter]
  94. }
  95. try {
  96. log.debug(`Trying to load reporter: ${name}`)
  97. reporters.push(injector.createChild([locals], ['reporter:' + name]).get('reporter:' + name))
  98. } catch (e) {
  99. if (e.message.includes(`No provider for "reporter:${name}"`)) {
  100. log.error(`Can not load reporter "${name}", it is not registered!\n Perhaps you are missing some plugin?`)
  101. } else {
  102. log.error(`Can not load "${name}"!\n ${e.stack}`)
  103. }
  104. emitter.emit('load_error', 'reporter', name)
  105. return
  106. }
  107. const colorName = name + '_color'
  108. if (!names.includes(colorName)) {
  109. try {
  110. log.debug(`Trying to load color-version of reporter: ${name} (${colorName})`)
  111. reporters.push(injector.createChild([locals], ['reporter:' + colorName]).get('reporter:' + name))
  112. } catch (e) {
  113. log.debug('Couldn\'t load color-version.')
  114. }
  115. }
  116. })
  117. reporters.forEach((reporter) => emitter.bind(reporter))
  118. return new MultiReporter(reporters)
  119. }
  120. createReporters.$inject = [
  121. 'config.reporters',
  122. 'config',
  123. 'emitter',
  124. 'injector'
  125. ]
  126. exports.createReporters = createReporters