153 lines
5.4 KiB
JavaScript
153 lines
5.4 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
// eslint-disable-next-line node/no-deprecated-api
|
||
|
const resolve = require('url').resolve
|
||
|
const SourceMapConsumer = require('source-map').SourceMapConsumer
|
||
|
const _ = require('lodash')
|
||
|
|
||
|
const PathUtils = require('./utils/path-utils')
|
||
|
const log = require('./logger').create('reporter')
|
||
|
const MultiReporter = require('./reporters/multi')
|
||
|
const baseReporterDecoratorFactory = require('./reporters/base').decoratorFactory
|
||
|
|
||
|
function createErrorFormatter (config, emitter, SourceMapConsumer) {
|
||
|
const basePath = config.basePath
|
||
|
const urlRoot = config.urlRoot === '/' ? '' : (config.urlRoot || '')
|
||
|
let lastServedFiles = []
|
||
|
|
||
|
emitter.on('file_list_modified', (files) => {
|
||
|
lastServedFiles = files.served
|
||
|
})
|
||
|
|
||
|
const URL_REGEXP = new RegExp('(?:https?:\\/\\/' +
|
||
|
config.hostname + '(?:\\:' + config.port + ')?' + ')?\\/?' +
|
||
|
urlRoot + '\\/?' +
|
||
|
'(base/|absolute)' + // prefix, including slash for base/ to create relative paths.
|
||
|
'((?:[A-z]\\:)?[^\\?\\s\\:]*)' + // path
|
||
|
'(\\?\\w*)?' + // sha
|
||
|
'(\\:(\\d+))?' + // line
|
||
|
'(\\:(\\d+))?' + // column
|
||
|
'', 'g')
|
||
|
|
||
|
const cache = new WeakMap()
|
||
|
|
||
|
function getSourceMapConsumer (sourceMap) {
|
||
|
if (!cache.has(sourceMap)) {
|
||
|
cache.set(sourceMap, new SourceMapConsumer(sourceMap))
|
||
|
}
|
||
|
return cache.get(sourceMap)
|
||
|
}
|
||
|
|
||
|
return function (input, indentation) {
|
||
|
indentation = _.isString(indentation) ? indentation : ''
|
||
|
if (_.isError(input)) {
|
||
|
input = input.message
|
||
|
} else if (_.isEmpty(input)) {
|
||
|
input = ''
|
||
|
} else if (!_.isString(input)) {
|
||
|
input = JSON.stringify(input, null, indentation)
|
||
|
}
|
||
|
|
||
|
let msg = input.replace(URL_REGEXP, function (stackTracePath, prefix, path, __, ___, line, ____, column) {
|
||
|
const normalizedPath = prefix === 'base/' ? `${basePath}/${path}` : path
|
||
|
const file = lastServedFiles.find((file) => file.path === normalizedPath)
|
||
|
|
||
|
if (file && file.sourceMap && line) {
|
||
|
line = +line
|
||
|
column = +column
|
||
|
|
||
|
// When no column is given and we default to 0, it doesn't make sense to only search for smaller
|
||
|
// or equal columns in the sourcemap, let's search for equal or greater columns.
|
||
|
const bias = column ? SourceMapConsumer.GREATEST_LOWER_BOUND : SourceMapConsumer.LEAST_UPPER_BOUND
|
||
|
|
||
|
try {
|
||
|
const zeroBasedColumn = Math.max(0, (column || 1) - 1)
|
||
|
const original = getSourceMapConsumer(file.sourceMap).originalPositionFor({ line, column: zeroBasedColumn, bias })
|
||
|
|
||
|
// If there is no original position/source for the current stack trace path, then
|
||
|
// we return early with the formatted generated position. This handles the case of
|
||
|
// generated code which does not map to anything, see Case 1 of the source-map spec.
|
||
|
// https://sourcemaps.info/spec.html.
|
||
|
if (original.source === null) {
|
||
|
return PathUtils.formatPathMapping(path, line, column)
|
||
|
}
|
||
|
|
||
|
// Source maps often only have a local file name, resolve to turn into a full path if
|
||
|
// the path is not absolute yet.
|
||
|
const oneBasedOriginalColumn = original.column == null ? original.column : original.column + 1
|
||
|
return `${PathUtils.formatPathMapping(resolve(path, original.source), original.line, oneBasedOriginalColumn)} <- ${PathUtils.formatPathMapping(path, line, column)}`
|
||
|
} catch (e) {
|
||
|
log.warn(`An unexpected error occurred while resolving the original position for: ${stackTracePath}`)
|
||
|
log.warn(e)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return PathUtils.formatPathMapping(path, line, column) || prefix
|
||
|
})
|
||
|
|
||
|
if (indentation) {
|
||
|
msg = indentation + msg.replace(/\n/g, '\n' + indentation)
|
||
|
}
|
||
|
|
||
|
return config.formatError ? config.formatError(msg) : msg + '\n'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createReporters (names, config, emitter, injector) {
|
||
|
const errorFormatter = createErrorFormatter(config, emitter, SourceMapConsumer)
|
||
|
const reporters = []
|
||
|
|
||
|
names.forEach((name) => {
|
||
|
if (['dots', 'progress'].includes(name)) {
|
||
|
[
|
||
|
require(`./reporters/${name}`),
|
||
|
require(`./reporters/${name}_color`)
|
||
|
].forEach((Reporter) => {
|
||
|
reporters.push(new Reporter(errorFormatter, config.reportSlowerThan, config.colors, config.browserConsoleLogOptions))
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const locals = {
|
||
|
baseReporterDecorator: ['factory', baseReporterDecoratorFactory],
|
||
|
formatError: ['value', errorFormatter]
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
log.debug(`Trying to load reporter: ${name}`)
|
||
|
reporters.push(injector.createChild([locals], ['reporter:' + name]).get('reporter:' + name))
|
||
|
} catch (e) {
|
||
|
if (e.message.includes(`No provider for "reporter:${name}"`)) {
|
||
|
log.error(`Can not load reporter "${name}", it is not registered!\n Perhaps you are missing some plugin?`)
|
||
|
} else {
|
||
|
log.error(`Can not load "${name}"!\n ${e.stack}`)
|
||
|
}
|
||
|
emitter.emit('load_error', 'reporter', name)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const colorName = name + '_color'
|
||
|
if (!names.includes(colorName)) {
|
||
|
try {
|
||
|
log.debug(`Trying to load color-version of reporter: ${name} (${colorName})`)
|
||
|
reporters.push(injector.createChild([locals], ['reporter:' + colorName]).get('reporter:' + name))
|
||
|
} catch (e) {
|
||
|
log.debug('Couldn\'t load color-version.')
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
reporters.forEach((reporter) => emitter.bind(reporter))
|
||
|
|
||
|
return new MultiReporter(reporters)
|
||
|
}
|
||
|
|
||
|
createReporters.$inject = [
|
||
|
'config.reporters',
|
||
|
'config',
|
||
|
'emitter',
|
||
|
'injector'
|
||
|
]
|
||
|
|
||
|
exports.createReporters = createReporters
|